A convertible preferred share is not “dilutive” merely because it can create ordinary shares. Conversion changes both sides of earnings per share: dividends previously withheld from ordinary shareholders return to the numerator, while new ordinary shares enter the denominator. The denominator can grow and EPS can still improve.
By the end of this article, you will be able to build the exact if-converted candidate for one fixed-share preferred series, classify it without rounding error, and handle mid-period issue or actual conversion without double counting.
Start with the counterfactual
Basic EPS describes the capital structure that actually existed. The if-converted method asks a controlled counterfactual:
What would continuing-operations EPS have been if this preferred series had converted at the beginning of the period, or on its issue date if later?
That counterfactual has two bridges:
- Numerator: reverse the preferred dividend adjustment that Basic EPS deducted, then add separately evidenced pre-tax income/expense consequences and their tax effect.
- Denominator: add the ordinary shares deliverable on conversion, weighted only for the period in which the instrument was a potential ordinary share.
This is the core treatment in IAS 33 and U.S. EPS guidance. It is also why the test must be performed as a ratio, not as a share-count checklist.
The exact model
Let and be the continuing-operations Basic numerator and weighted-average denominator. Let be the period-aligned preferred dividend deducted from Basic EPS, another signed pre-tax conversion adjustment, its signed tax effect, the fixed ordinary shares delivered on conversion, and the exact fraction of the reporting period for which the preferred instrument was a potential ordinary share.
Under IAS 33, include the series only if:
Notice the numerical comparison. In a loss period, moving from to is an increase, even though the absolute loss is smaller. The assumed conversion is therefore antidilutive.
U.S. GAAP adds a hard control: when continuing operations available to common stockholders is a loss, all potential common shares are excluded. IAS 33 retains the signed comparison. The two frameworks can therefore differ in the unusual case where other conversion consequences make the candidate loss per share more negative.
For a single series with positive incremental shares, an equivalent diagnostic is the incremental EPS:
If , the candidate is dilutive under the signed comparison. The reference implementation calculates this diagnostic, applies the framework loss control, and otherwise classifies from the complete candidate ratio.
A worked example you can audit in your head
Suppose a company reports:
- USD 90 million of continuing-operations earnings available to ordinary holders;
- 30 million weighted-average ordinary shares;
- USD 6 million of preferred dividends deducted in arriving at that numerator;
- 4 million ordinary shares on conversion;
- full-year potential-share exposure.
Basic EPS is:
The preferred dividend comes back, and the conversion shares enter:
The candidate is below USD 3.00, so the series is dilutive. The incremental return is per new share—below the existing USD 3.00 per share—so the new shares pull the average down.
Now change only the addback to USD 10 million and the conversion shares to 2 million:
The instrument creates shares, but it improves EPS. It must be excluded.
Try all four scenarios in the interactive preference-dilution lab.
Timing: weight the shares, not the dividend twice
The reporting period is inclusive. Internally, the implementation turns its final date into an exclusive boundary one day later. Potential-share exposure starts at the later of period start and issue date. It stops at the earliest of:
- period end plus one day;
- actual conversion date;
- lapse, redemption, or extinguishment date.
An instrument issued on 1 July in a 365-day year contributes of its conversion shares. If it converts on 1 July, potential exposure runs from 1 January through 30 June: . Actual shares from 1 July onward belong in the Basic weighted-average denominator upstream.
The preferred-dividend input is different. It is the amount actually deducted from the Basic numerator for the relevant pre-conversion or post-issue interval. It is already period-aligned. Multiplying it by again would double-weight it. It is also already on the Basic EPS tax basis. Do not apply a generic tax rate to it. If assumed conversion changes another income or expense item, provide that pre-tax effect and its evidenced tax consequence as separate inputs.
What the implementation refuses to guess
Real instruments contain traps: participating rights, variable conversion rates, make-whole provisions, induced conversion, contingencies, and retrospective share adjustments. A trustworthy calculator needs hard boundaries.
The implementation rejects:
- participating preferred stock, because the two-class method may have to be compared with if-converted;
- partial or induced conversion that has not been decomposed into affected and remaining tranches;
- incomplete contingency analysis under the declared framework;
- unresolved variable or multiple conversion rates;
- missing legal-term sources or incomplete “holder-advantageous terms” selection;
- incomplete Basic numerator, Basic denominator, dividend, or restatement inputs;
- instruments with no potential-share exposure in the period.
This is not a partial implementation. It is a complete implementation of a declared canonical case, with explicit routes for materially different accounting algorithms. A silent approximation would be less complete because the output could not be defended.
Exact arithmetic is part of accounting correctness
The midyear fraction generally produces nonterminating decimal shares. Binary floating point adds another approximation. If classification is based on rounded EPS, an equality or near-equality case can flip.
Both reference implementations parse decimal strings into integers and powers of ten, reduce all calculations to rational numbers, compare cross-products exactly, and round only display fields. The output preserves values such as:
time_weight_exact: "184/365"candidate_eps_exact: "48/17"candidate_eps: "2.82"
The Python source is convertible_preference.py; the TypeScript source is convertiblePreference.ts. Both consume the same fixture file.
Tests that matter
The fixture suite does more than prove the happy path:
| Case | Expected result | Why it matters |
|---|---|---|
| Full-year canonical | Dilutive, | Numerator/denominator bridge |
| Higher addback, fewer shares | Antidilutive, | Shares alone do not prove dilution |
| Ordinary IFRS loss case | Antidilutive, | Correct signed comparison |
| IFRS/U.S. GAAP loss edge | Include under IAS 33; exclude under U.S. GAAP | Framework control is explicit |
| Explicit tax-effect case | Dilutive, | No hidden generic tax rate |
| Exact equality | Excluded, | “No dilution” means antidilutive |
| 1 July issue | Weight | Inclusive calendar convention |
| 1 July actual conversion | Weight | Prevent denominator double count |
Negative tests then force rejection of malformed decimals, impossible dates, missing fields, nonpositive share amounts, incomplete gates, unresolved terms, and participating securities. Python and TypeScript assert the same expected exact strings, making cross-language parity observable rather than assumed.
A filed example—and the boundary of what it proves
Celsius Holdings' Form 10-Q for the quarter ended 31 March 2026 says its preferred
stock is participating and that diluted EPS uses the more dilutive of the two-class
and if-converted methods. The filed EPS table reports tabular dollars and shares in thousands:
85,080 as net income attributable to common stockholders, 85,184 after assumed
conversions, 257,012 basic weighted shares, and 259,764 diluted weighted shares.
It also reports 33.4 million excluded potential shares, primarily preferred conversion
shares with a remainder from performance share units.
Those are filed facts. They are not the synthetic 90 / 30 example above, and the
filing does not isolate the preferred series' incremental shares from other dilutive
securities. A one-series if-converted candidate cannot be reconstructed defensibly
from this note alone. Its value here is historical method evidence and a practical
lesson: stop when the filing does not expose the inputs required by the algorithm.
Where this result goes next
This topic produces a standalone candidate, not the final diluted EPS for an entity with many instruments. Each series is first tested separately. Dilutive series are then ordered from most to least dilutive and accumulated without allowing a later instrument to improve the running EPS. Participating securities take a separate two-class path before method selection.
That separation keeps responsibilities clean:
- this algorithm answers “what is this preferred series’ exact candidate?”;
- the two-class algorithm resolves participating allocation;
- the ordering algorithm combines multiple eligible instruments;
- the final reconciliation explains Basic-to-diluted movement.
Summary
Convertible-preference dilution is a two-sided, framework-controlled counterfactual. Reverse the exact Basic-basis preferred dividend adjustment, preserve separate evidence for other pre-tax and tax effects, add only the correctly weighted conversion shares, and apply the declared continuing-operations control. That discipline survives profit, loss, timing, precision, and provenance edge cases.
Rendered from the canonical Mermaid sources linked by this article.
If-converted preference decision flow
The branches deliberately place policy and data completeness before computation. Neither rounding nor an increase in share count is a classification rule.
References4 primary sources and evidence notesExpand the source trail, evidence role, and limitations behind the engineering choices.
Expand the source trail, evidence role, and limitations behind the engineering choices.
Primary accounting guidance governs the formulas and timing rules. Filed-company material is included only as evidence of current application, not as authority.
R01 — IAS 33 Earnings per Share
- Organization: IFRS Foundation / International Accounting Standards Board
- Source type: issued accounting standard
- Version: issued standards HTML, 2023 compilation
- URL: https://www.ifrs.org/content/dam/ifrs/publications/html-standards/english/2023/issued/ias33.html
- Accessed: 2026-07-23
- Jurisdiction: IFRS
- Supports: paragraphs 31–36 (numerator adjustment and assumed conversion); 38–44 (time weighting, holder-advantageous terms, antidilution and sequencing); 49–51 (convertible instruments, convertible preference tests, partial conversion).
- Limitation: local endorsement and later amendments must be checked for a live filing.
R02 — FASB Statement No. 128, Earnings per Share
- Organization: Financial Accounting Standards Board
- Source type: accounting standard PDF
- Publication date: February 1997
- URL: https://storage.fasb.org/aop_fas128.pdf
- Accessed: 2026-07-23
- Jurisdiction: United States
- Supports: paragraphs 13–16 (antidilution and continuing-operations control); paragraphs 26–28 (preferred-dividend addback and assumed-conversion timing).
- Limitation: later Codification amendments supersede presentation in the original statement.
R03 — ASU 2020-06, Debt—Debt with Conversion and Other Options and Derivatives and Hedging
- Organization: Financial Accounting Standards Board
- Source type: Accounting Standards Update
- Publication date: August 2020
- URL: https://storage.fasb.org/ASU_2020-06.pdf
- Accessed: 2026-07-23
- Jurisdiction: United States
- Supports: current amendments to ASC 260-10-45-40 and the if-converted treatment of convertible instruments, including convertible preferred-stock dividend addbacks.
- Limitation: the FASB Accounting Standards Codification remains the authoritative US GAAP source.
R04 — Celsius Holdings, Inc. 2026 first-quarter EPS note
- Organization: Celsius Holdings, Inc.; filed with the U.S. Securities and Exchange Commission
- Source type: Inline XBRL filing exhibit
- Reporting period: three months ended 2026-03-31
- Filed: 2026-05-07
- URL: https://www.sec.gov/Archives/edgar/data/1341766/000134176626000039/R12.htm
- Accessed: 2026-07-23
- Jurisdiction: US SEC filer
- Supports: an issuer example in which participating convertible preferred stock is
evaluated using the more dilutive of the two-class and if-converted methods;
filed table values, in thousands, of
85,080/85,184for the common-stockholder and assumed-conversion numerators,257,012/259,764basic/diluted weighted shares, and 33.4 million excluded potential shares described as primarily preferred conversion shares with a remainder from performance share units. - Limitation: entity-specific disclosure, not normative accounting guidance. It does not isolate the one preferred series' incremental shares or every numerator bridge, so it cannot support a reconstructed one-series candidate.
Research reconciliation
IFRS and U.S. GAAP agree on the central mechanics: add back the Basic-basis convertible-preference dividend adjustment, reflect evidenced income/expense and tax consequences, add conversion shares from the beginning of the period or issue date if later, and test antidilution using continuing operations. They are not identical in every edge case. This package makes the U.S. GAAP continuing-operations loss exclusion explicit, while IAS 33 applies its signed dilution definition. Participating-security method selection, contingencies, partial conversions, variable terms, and sequencing remain separate policy or aggregation decisions rather than hidden assumptions.
Full dependency-light reference implementations in both supported languages.
/** Exact if-converted dilution test for one convertible-preference series. */
export type AccountingFramework = "IFRS" | "US_GAAP" | "OTHER_DECLARED";
export type RoundingMode = "half_even" | "half_up";
export interface ConvertiblePreferenceInput {
entity_id: string;
instrument_id: string;
period_start: string;
period_end: string;
as_of: string;
accounting_framework: AccountingFramework;
loss_control_rule: "candidate_comparison" | "exclude_all_when_control_loss";
currency: string;
basic_control_numerator: string;
basic_weighted_average_shares: string;
earnings_scale: string;
share_scale: string;
preferred_dividend_adjustment_deducted_from_basic: string;
other_pre_tax_income_adjustment_if_converted: string;
income_tax_effect_of_other_adjustment_if_converted: string;
fixed_ordinary_shares_on_conversion: string;
issue_date: string;
actual_conversion_date: string | null;
lapse_redemption_or_extinguishment_date: string | null;
numerator_policy: string;
tax_effect_policy: string;
denominator_policy: string;
framework_policy_source: string;
day_count_policy: "ACTUAL_DAYS_IN_PERIOD";
basic_components_complete: boolean;
continuing_operations_control_complete: boolean;
basic_preference_adjustment_complete: boolean;
share_basis_restatement_complete: boolean;
dividend_adjustment_complete: boolean;
dividend_period_aligned: boolean;
conversion_terms_are_fixed: boolean;
conversion_scope: "entire_series";
holder_advantageous_terms_selected: boolean;
conversion_terms_source: string;
terms_restatement_complete: boolean;
conversion_is_contingent: boolean;
contingency_assessment_complete: boolean;
contingency_includable_at_period_end: boolean;
contingency_policy: string;
is_participating: boolean;
instrument_is_final: boolean;
is_final: boolean;
eps_decimal_places: number;
share_decimal_places: number;
rounding_mode: RoundingMode;
}
export interface ConvertiblePreferenceResult {
metric: "if_converted_convertible_preference_dilution";
entity_id: string;
instrument_id: string;
period_start: string;
period_end: string;
accounting_framework: AccountingFramework;
loss_control_rule: "candidate_comparison" | "exclude_all_when_control_loss";
currency: string;
basic_control_numerator_base: string;
basic_weighted_average_shares_base: string;
preferred_dividend_addback_base: string;
other_pre_tax_income_adjustment_base: string;
income_tax_effect_of_other_adjustment_base: string;
net_numerator_adjustment_base: string;
fixed_conversion_shares_base: string;
exposure_start: string;
exposure_end_exclusive: string;
exposure_days: number;
period_days: number;
time_weight_exact: string;
weighted_incremental_shares_exact: string;
weighted_incremental_shares: string;
basic_eps_exact: string;
basic_eps: string;
incremental_eps_exact: string;
incremental_eps: string;
candidate_eps_exact: string;
candidate_eps: string;
classification: "dilutive" | "antidilutive";
include_in_standalone_candidate: boolean;
reason_code: "candidate_below_control" | "candidate_not_below_control" |
"framework_loss_control_exclusion";
eps_decimal_places: number;
share_decimal_places: number;
rounding_mode: RoundingMode;
rounding_adjusted: boolean;
status: "final" | "provisional";
is_final: boolean;
}
interface Rational { n: bigint; d: bigint }
const DECIMAL_RE = /^-?(?:0|[1-9]\d*)(?:\.\d+)?$/;
const DATE_RE = /^\d{4}-\d{2}-\d{2}$/;
const RFC3339_RE =
/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})$/;
const REQUIRED_FIELDS: (keyof ConvertiblePreferenceInput)[] = [
"entity_id", "instrument_id", "period_start", "period_end", "as_of",
"accounting_framework", "currency", "basic_control_numerator",
"basic_weighted_average_shares", "earnings_scale", "share_scale",
"preferred_dividend_adjustment_deducted_from_basic",
"other_pre_tax_income_adjustment_if_converted",
"income_tax_effect_of_other_adjustment_if_converted",
"fixed_ordinary_shares_on_conversion", "issue_date",
"actual_conversion_date", "lapse_redemption_or_extinguishment_date",
"numerator_policy", "tax_effect_policy", "denominator_policy",
"framework_policy_source", "loss_control_rule", "day_count_policy",
"basic_components_complete", "continuing_operations_control_complete",
"basic_preference_adjustment_complete",
"share_basis_restatement_complete", "dividend_adjustment_complete",
"dividend_period_aligned", "conversion_terms_are_fixed", "conversion_scope",
"holder_advantageous_terms_selected", "conversion_terms_source",
"terms_restatement_complete", "conversion_is_contingent",
"contingency_assessment_complete", "contingency_includable_at_period_end",
"contingency_policy", "is_participating", "instrument_is_final",
"is_final", "eps_decimal_places", "share_decimal_places", "rounding_mode",
];
export class ConvertiblePreferenceValidationError extends Error {
constructor(message: string) {
super(message);
this.name = "ConvertiblePreferenceValidationError";
}
}
function abs(value: bigint): bigint { return value < 0n ? -value : value; }
function gcd(a: bigint, b: bigint): bigint {
a = abs(a); b = abs(b);
while (b !== 0n) [a, b] = [b, a % b];
return a;
}
function rational(n: bigint, d = 1n): Rational {
if (d === 0n) throw new ConvertiblePreferenceValidationError("division by zero");
if (d < 0n) { n = -n; d = -d; }
const g = gcd(n, d);
return {n: n / g, d: d / g};
}
function add(a: Rational, b: Rational): Rational {
return rational(a.n * b.d + b.n * a.d, a.d * b.d);
}
function mul(a: Rational, b: Rational): Rational {
return rational(a.n * b.n, a.d * b.d);
}
function div(a: Rational, b: Rational): Rational {
return rational(a.n * b.d, a.d * b.n);
}
function less(a: Rational, b: Rational): boolean {
return a.n * b.d < b.n * a.d;
}
function fractionString(value: Rational): string {
return `${value.n}/${value.d}`;
}
function decimal(value: unknown, field: string): Rational {
if (typeof value !== "string" || !DECIMAL_RE.test(value)) {
throw new ConvertiblePreferenceValidationError(
`${field} must be a canonical decimal string without exponent notation`,
);
}
const unsigned = value.startsWith("-") ? value.slice(1) : value;
const [whole, fraction = ""] = unsigned.split(".");
if (whole.length + fraction.length > 30 || fraction.length > 12) {
throw new ConvertiblePreferenceValidationError(
`${field} exceeds 30 digits or 12 fraction digits`,
);
}
return rational(BigInt(value.replace(".", "")), 10n ** BigInt(fraction.length));
}
function normalDecimal(value: Rational): string {
let denominator = value.d;
let twos = 0, fives = 0;
while (denominator % 2n === 0n) { denominator /= 2n; twos++; }
while (denominator % 5n === 0n) { denominator /= 5n; fives++; }
if (denominator !== 1n) return fractionString(value);
const places = Math.max(twos, fives);
let scaled = value.n *
2n ** BigInt(places - twos) *
5n ** BigInt(places - fives);
const negative = scaled < 0n;
if (negative) scaled = -scaled;
let digits = scaled.toString().padStart(places + 1, "0");
if (places > 0) {
const split = digits.length - places;
digits = `${digits.slice(0, split)}.${digits.slice(split)}`
.replace(/0+$/, "").replace(/\.$/, "");
}
return negative && digits !== "0" ? `-${digits}` : digits;
}
function round(value: Rational, places: number, mode: RoundingMode): [string, boolean] {
const negative = value.n < 0n;
const scaled = abs(value.n) * 10n ** BigInt(places);
let quotient = scaled / value.d;
const remainder = scaled % value.d;
const comparison = remainder * 2n - value.d;
if (comparison > 0n ||
(comparison === 0n &&
(mode === "half_up" || (mode === "half_even" && quotient % 2n === 1n)))) {
quotient++;
}
let digits = quotient.toString().padStart(places + 1, "0");
if (places > 0) {
const split = digits.length - places;
digits = `${digits.slice(0, split)}.${digits.slice(split)}`;
}
if (negative && quotient !== 0n) digits = `-${digits}`;
return [digits, remainder !== 0n];
}
function text(value: unknown, field: string): string {
if (typeof value !== "string" || value.trim() === "") {
throw new ConvertiblePreferenceValidationError(`${field} must be non-empty text`);
}
return value;
}
function validDate(value: unknown, field: string, nullable = false): string | null {
if (nullable && value === null) return null;
if (typeof value !== "string" || !DATE_RE.test(value)) {
throw new ConvertiblePreferenceValidationError(
`${field} must use YYYY-MM-DD${nullable ? " or null" : ""}`,
);
}
if (Number(value.slice(0, 4)) < 1) {
throw new ConvertiblePreferenceValidationError(`${field} must use a year from 0001 to 9999`);
}
const parsed = new Date(`${value}T00:00:00Z`);
if (Number.isNaN(parsed.valueOf()) || parsed.toISOString().slice(0, 10) !== value) {
throw new ConvertiblePreferenceValidationError(`${field} must be a real date`);
}
return value;
}
function dayNumber(value: string): number {
return Math.floor(Date.parse(`${value}T00:00:00Z`) / 86400000);
}
function dateFromDay(value: number): string {
return new Date(value * 86400000).toISOString().slice(0, 10);
}
export function calculateConvertiblePreferenceDilution(
input: ConvertiblePreferenceInput,
): ConvertiblePreferenceResult {
if (input === null || typeof input !== "object" || Array.isArray(input)) {
throw new ConvertiblePreferenceValidationError("input must be an object");
}
for (const field of REQUIRED_FIELDS) {
if (!(field in input)) {
throw new ConvertiblePreferenceValidationError(`missing required field: ${field}`);
}
}
const entityId = text(input.entity_id, "entity_id");
const instrumentId = text(input.instrument_id, "instrument_id");
const start = validDate(input.period_start, "period_start")!;
const end = validDate(input.period_end, "period_end")!;
const issue = validDate(input.issue_date, "issue_date")!;
const conversion = validDate(input.actual_conversion_date, "actual_conversion_date", true);
const lapse = validDate(
input.lapse_redemption_or_extinguishment_date,
"lapse_redemption_or_extinguishment_date",
true,
);
if (start > end) {
throw new ConvertiblePreferenceValidationError("period_start must not be after period_end");
}
if (end === "9999-12-31") {
throw new ConvertiblePreferenceValidationError(
"period_end must allow an exclusive next-day boundary",
);
}
if (issue > end) {
throw new ConvertiblePreferenceValidationError("issue_date must not be after period_end");
}
for (const [event, field] of [
[conversion, "actual_conversion_date"],
[lapse, "lapse_redemption_or_extinguishment_date"],
] as const) {
if (event !== null && event < issue) {
throw new ConvertiblePreferenceValidationError(`${field} must not be before issue_date`);
}
}
if (!RFC3339_RE.test(input.as_of) || Number.isNaN(Date.parse(input.as_of))) {
throw new ConvertiblePreferenceValidationError(
"as_of must be a real RFC 3339 timestamp with an explicit offset",
);
}
if (!["IFRS", "US_GAAP", "OTHER_DECLARED"].includes(input.accounting_framework)) {
throw new ConvertiblePreferenceValidationError("accounting_framework is not supported");
}
if (!/^[A-Z]{3}$/.test(input.currency)) {
throw new ConvertiblePreferenceValidationError(
"currency must be a three-letter uppercase code",
);
}
text(input.numerator_policy, "numerator_policy");
text(input.tax_effect_policy, "tax_effect_policy");
text(input.denominator_policy, "denominator_policy");
text(input.framework_policy_source, "framework_policy_source");
text(input.contingency_policy, "contingency_policy");
text(input.conversion_terms_source, "conversion_terms_source");
if (input.day_count_policy !== "ACTUAL_DAYS_IN_PERIOD") {
throw new ConvertiblePreferenceValidationError(
"day_count_policy must be ACTUAL_DAYS_IN_PERIOD",
);
}
const trueFlags = [
"basic_components_complete", "continuing_operations_control_complete",
"basic_preference_adjustment_complete",
"share_basis_restatement_complete", "dividend_adjustment_complete",
"dividend_period_aligned", "conversion_terms_are_fixed",
"holder_advantageous_terms_selected", "terms_restatement_complete",
"contingency_assessment_complete", "contingency_includable_at_period_end",
] as const;
for (const field of [
...trueFlags, "conversion_is_contingent", "is_participating",
"instrument_is_final", "is_final",
] as const) {
if (typeof input[field] !== "boolean") {
throw new ConvertiblePreferenceValidationError(`${field} must be boolean`);
}
}
for (const field of trueFlags) {
if (!input[field]) {
throw new ConvertiblePreferenceValidationError(`${field} must be true`);
}
}
if (input.is_participating) {
throw new ConvertiblePreferenceValidationError(
"participating preferred stock requires D46-F02-A05 two-class method resolution",
);
}
if (input.conversion_scope !== "entire_series") {
throw new ConvertiblePreferenceValidationError(
"conversion_scope must be entire_series; split partial conversions into tranches",
);
}
if (!["candidate_comparison", "exclude_all_when_control_loss"].includes(
input.loss_control_rule,
)) {
throw new ConvertiblePreferenceValidationError("loss_control_rule is not supported");
}
const expectedLossRule = input.accounting_framework === "IFRS"
? "candidate_comparison"
: input.accounting_framework === "US_GAAP"
? "exclude_all_when_control_loss"
: null;
if (expectedLossRule !== null && input.loss_control_rule !== expectedLossRule) {
throw new ConvertiblePreferenceValidationError(
"loss_control_rule conflicts with accounting_framework",
);
}
if (!Number.isInteger(input.eps_decimal_places) ||
input.eps_decimal_places < 0 || input.eps_decimal_places > 12) {
throw new ConvertiblePreferenceValidationError(
"eps_decimal_places must be an integer from 0 to 12",
);
}
if (!Number.isInteger(input.share_decimal_places) ||
input.share_decimal_places < 0 || input.share_decimal_places > 12) {
throw new ConvertiblePreferenceValidationError(
"share_decimal_places must be an integer from 0 to 12",
);
}
if (!["half_even", "half_up"].includes(input.rounding_mode)) {
throw new ConvertiblePreferenceValidationError(
"rounding_mode must be half_even or half_up",
);
}
const basicReported = decimal(input.basic_control_numerator, "basic_control_numerator");
const basicSharesReported = decimal(
input.basic_weighted_average_shares, "basic_weighted_average_shares",
);
const earningsScale = decimal(input.earnings_scale, "earnings_scale");
const shareScale = decimal(input.share_scale, "share_scale");
const dividendReported = decimal(
input.preferred_dividend_adjustment_deducted_from_basic,
"preferred_dividend_adjustment_deducted_from_basic",
);
const otherPreTaxReported = decimal(
input.other_pre_tax_income_adjustment_if_converted,
"other_pre_tax_income_adjustment_if_converted",
);
const otherTaxEffectReported = decimal(
input.income_tax_effect_of_other_adjustment_if_converted,
"income_tax_effect_of_other_adjustment_if_converted",
);
const conversionSharesReported = decimal(
input.fixed_ordinary_shares_on_conversion,
"fixed_ordinary_shares_on_conversion",
);
if (basicSharesReported.n <= 0n || earningsScale.n <= 0n || shareScale.n <= 0n) {
throw new ConvertiblePreferenceValidationError(
"basic shares and both unit scales must be positive",
);
}
if (dividendReported.n < 0n) {
throw new ConvertiblePreferenceValidationError(
"preferred_dividend_adjustment_deducted_from_basic must be non-negative",
);
}
if (conversionSharesReported.n <= 0n) {
throw new ConvertiblePreferenceValidationError(
"fixed_ordinary_shares_on_conversion must be positive",
);
}
const startDay = dayNumber(start);
const endExclusiveDay = dayNumber(end) + 1;
const exposureStartDay = Math.max(startDay, dayNumber(issue));
const endCandidates = [endExclusiveDay];
if (conversion !== null) endCandidates.push(dayNumber(conversion));
if (lapse !== null) endCandidates.push(dayNumber(lapse));
const exposureEndDay = Math.min(...endCandidates);
const exposureDays = exposureEndDay - exposureStartDay;
const periodDays = endExclusiveDay - startDay;
if (exposureDays <= 0) {
throw new ConvertiblePreferenceValidationError(
"instrument has no potential-share exposure in the period",
);
}
const basicNumerator = mul(basicReported, earningsScale);
const basicShares = mul(basicSharesReported, shareScale);
const dividend = mul(dividendReported, earningsScale);
const otherPreTax = mul(otherPreTaxReported, earningsScale);
const otherTaxEffect = mul(otherTaxEffectReported, earningsScale);
const netAdjustment = add(add(dividend, otherPreTax), otherTaxEffect);
const fixedShares = mul(conversionSharesReported, shareScale);
const weight = rational(BigInt(exposureDays), BigInt(periodDays));
const incrementalShares = mul(fixedShares, weight);
const basicEps = div(basicNumerator, basicShares);
const incrementalEps = div(netAdjustment, incrementalShares);
const candidateEps = div(
add(basicNumerator, netAdjustment),
add(basicShares, incrementalShares),
);
const lossControlExclusion =
input.loss_control_rule === "exclude_all_when_control_loss" &&
basicNumerator.n < 0n;
const dilutive = !lossControlExclusion && less(candidateEps, basicEps);
const [basicDisplay, basicAdjusted] = round(
basicEps, input.eps_decimal_places, input.rounding_mode,
);
const [incrementalDisplay, incrementalAdjusted] = round(
incrementalEps, input.eps_decimal_places, input.rounding_mode,
);
const [candidateDisplay, candidateAdjusted] = round(
candidateEps, input.eps_decimal_places, input.rounding_mode,
);
const [weightedDisplay, weightedAdjusted] = round(
incrementalShares, input.share_decimal_places, input.rounding_mode,
);
const isFinal = input.is_final && input.instrument_is_final;
return {
metric: "if_converted_convertible_preference_dilution",
entity_id: entityId,
instrument_id: instrumentId,
period_start: start,
period_end: end,
accounting_framework: input.accounting_framework,
loss_control_rule: input.loss_control_rule,
currency: input.currency,
basic_control_numerator_base: normalDecimal(basicNumerator),
basic_weighted_average_shares_base: normalDecimal(basicShares),
preferred_dividend_addback_base: normalDecimal(dividend),
other_pre_tax_income_adjustment_base: normalDecimal(otherPreTax),
income_tax_effect_of_other_adjustment_base: normalDecimal(otherTaxEffect),
net_numerator_adjustment_base: normalDecimal(netAdjustment),
fixed_conversion_shares_base: normalDecimal(fixedShares),
exposure_start: dateFromDay(exposureStartDay),
exposure_end_exclusive: dateFromDay(exposureEndDay),
exposure_days: exposureDays,
period_days: periodDays,
time_weight_exact: fractionString(weight),
weighted_incremental_shares_exact: fractionString(incrementalShares),
weighted_incremental_shares: weightedDisplay,
basic_eps_exact: fractionString(basicEps),
basic_eps: basicDisplay,
incremental_eps_exact: fractionString(incrementalEps),
incremental_eps: incrementalDisplay,
candidate_eps_exact: fractionString(candidateEps),
candidate_eps: candidateDisplay,
classification: dilutive ? "dilutive" : "antidilutive",
include_in_standalone_candidate: dilutive,
reason_code: lossControlExclusion
? "framework_loss_control_exclusion"
: dilutive
? "candidate_below_control"
: "candidate_not_below_control",
eps_decimal_places: input.eps_decimal_places,
share_decimal_places: input.share_decimal_places,
rounding_mode: input.rounding_mode,
rounding_adjusted:
basicAdjusted || incrementalAdjusted || candidateAdjusted || weightedAdjusted,
status: isFinal ? "final" : "provisional",
is_final: isFinal,
};
}
The embedded lab now expands to its full document height, keeping the article as the only scroll surface.