Library/Earnings and Per-Share Analytics/Earnings and Share Foundations

D46-F01-A08 / Complete engineering topic

Basic EPS

A production-minded guide to Basic EPS.

D46 · EARNINGS AND PER-SHARE ANALYT…
D46-F01-A08Canonical / Tested / Open
D46 / D46-F01

Basic earnings per share is often introduced with a fraction:

Basic EPS=earnings available to ordinary shareholdersweighted-average ordinary shares\text{Basic EPS}= \frac{\text{earnings available to ordinary shareholders}} {\text{weighted-average ordinary shares}}

That is the correct destination. It is not a complete implementation plan.

A reliable EPS system must explain where both sides of the fraction came from, which accounting framework applies, how units were normalized, whether historical shares were restated, and how the final quotient was rounded. The division takes one line of code; earning trust in the result takes the rest of the pipeline.

What Basic EPS is measuring

Suppose a company reports USD 12 million available to ordinary shareholders and had 6 million weighted-average ordinary shares. The claim made by Basic EPS is narrow:

12,000,000/6,000,000=2.0012{,}000{,}000 / 6{,}000{,}000 = 2.00

Each weighted-average ordinary share is allocated USD 2.00 of the period’s available earnings.

This does not mean each shareholder received USD 2.00 in cash. EPS is an earnings allocation, not a dividend, cash-flow measure, or valuation multiple. It also says nothing by itself about the quality or persistence of those earnings.

The numerator is attributed earnings

Starting from consolidated net income is usually too early. The calculation needs the amount available to the relevant ordinary shareholders.

This package reconciles:

Er=PApAs+AoE_r=P-A_p-A_s+A_o
  • PP: profit or loss attributable to owners;
  • ApA_p: preference dividends and equivalent numerator deductions;
  • AsA_s: earnings allocated to participating securities;
  • AoA_o: other signed, policy-documented adjustments.

Here, ErE_r is earnings available to ordinary shareholders in reported units; explicit scaling comes next.

The stable input field for PP is named profit_loss_attributable_to_owners. That name matches the IFRS-shaped starting line. Under US GAAP, the field carries the declared framework-appropriate net-income starting amount, and numerator_policy must document the mapping.

For consolidated IFRS statements, IAS 33 centers the numerator on profit or loss attributable to ordinary equity holders of the parent. Under US GAAP Topic 260, participating securities can trigger a two-class allocation. The allocation depends on contractual rights, so an algorithm cannot safely infer it from a security label alone.

That is why the implementation requires numerator_is_attribution_complete=true. It is a control assertion: upstream work has finished before division begins.

The denominator is a period exposure, not a closing balance

Using year-end shares is tempting and frequently wrong. If shares were issued, repurchased, or cancelled during the year, they did not all participate for the full period. The denominator therefore uses weighted-average ordinary shares.

Capital events can add another layer. Splits, consolidations, bonus issues, and the bonus element of rights issues can require retrospective denominator adjustment. A correct current-period ledger with an unadjusted comparative denominator still creates misleading EPS history.

This package does not redo the A03–A07 algorithms. Instead, it requires denominator_is_restatement_complete=true and a written denominator_policy.

A weighted-share timeline before the final division

The Basic EPS function receives a completed denominator, but a visible upstream timeline prevents the common mistake of using closing shares. Consider a synthetic calendar year:

IntervalShares outstandingPeriod weightWeighted contribution
January–March12,000,0003/123,000,000
April–September, after issuing 3,000,000 shares15,000,0006/127,500,000
October–December, after repurchasing 1,000,000 shares14,000,0003/123,500,000
Weighted-average shares14,000,000

With USD 28 million of earnings available, the completed ledgers produce:

28,000,000/14,000,000=2.0028{,}000{,}000 / 14{,}000{,}000 = 2.00

In this deliberately balanced example, the weighted average happens to equal the year-end count. The timeline proves why; it does not create a shortcut that closing shares are acceptable.

Basic is not diluted

Basic EPS describes the ordinary shares actually in the basic denominator under the applicable policy. Potential ordinary shares—options, warrants, convertibles, and similar instruments—belong to diluted EPS.

That distinction prevents a common modeling mistake: quietly adding every possible future share to the Basic EPS denominator. Such a number is neither Basic EPS nor necessarily valid diluted EPS, because diluted EPS also requires method-specific numerator adjustments and antidilution tests.

Unit scales are part of the data

Financial statements often say “in millions, except per-share amounts.” A data feed may store earnings in thousands while a share table stores shares in millions. If the units are implicit, the output can be wrong by three or six orders of magnitude and still look like a valid number.

The contract therefore carries two multipliers:

Ebase=Ereported×SeE_{\text{base}}=E_{\text{reported}}\times S_e Wbase=Wreported×SwW_{\text{base}}=W_{\text{reported}}\times S_w

For the canonical example, both reported amounts are in thousands:

12,000×1,0006,000×1,000=2.00\frac{12{,}000\times1{,}000}{6{,}000\times1{,}000}=2.00

The multipliers cancel numerically, but recording them is still valuable. It proves why they cancel and catches cases where they do not.

The diagram makes the integration boundary explicit: reconcile, scale, and only then divide.

Basic EPS fraction anatomy showing USD 12 million of reconciled earnings divided by six million weighted-average shares to produce USD 2.0000 per share.

A historical filing that reproduces

Farmers & Merchants Bancorp, Inc. filed its Form 10-Q for the period ended 2025-06-30 on 2025-08-04. In its SEC-filed Note 6, the company reported these lines for the three months ended 2025-06-30:

Issuer-filed factValue
Net incomeUSD 7,710 thousand
Distributed earnings allocated to participating securitiesUSD 38 thousand deduction
Undistributed earnings allocated to participating securitiesUSD 59 thousand deduction
Net earnings available to common shareholdersUSD 7,613 thousand
Weighted-average common shares including participating securities13,720,339
Average unvested restricted shares172,894 deduction
Weighted-average common shares outstanding13,547,445
Basic and diluted EPSUSD 0.56

For this US GAAP example, the filed Net income line is the declared starting amount mapped into the stable input field. Those filed facts produce this independent calculation:

(7,7103859)×1,000=7,613,000(7{,}710-38-59)\times1{,}000=7{,}613{,}000 13,720,339172,894=13,547,44513{,}720{,}339-172{,}894=13{,}547{,}445 7,613,000/13,547,445=0.56195098040.567{,}613{,}000 / 13{,}547{,}445 =0.5619509804\ldots \longrightarrow 0.56

This is a real two-class allocation example. It is not evidence that EPS growth occurred, that the issuer's operating performance improved, or that the security was attractive.

Provider comparison remains a separate evidence and licensing layer. A provider can map the same filing into fields with different semantics or revision states, so its EPS must not be reconstructed from a share field without a tested contract. This public article does not expose third-party provider values because display and redistribution rights have not been confirmed. A future approved comparison must state the provider, field definitions, observation time, and any unresolved mismatch without displacing the issuer filing as the accounting source. The publication boundary is recorded in the evidence note.

Exact decimals and declared rounding

Binary floating-point is excellent for many scientific computations but awkward for exact decimal ties. An accounting pipeline should not let language-specific floating-point behavior decide whether 1.005 becomes 1.00 or 1.01.

The Python and TypeScript implementations parse decimal strings into:

  • an integer coefficient;
  • a decimal scale.

For example, 1.005 becomes coefficient 1005, scale 3. Division and tie detection then use integers.

Two rounding modes are supported:

  • half_even: at an exact half, increment only if the retained final digit is odd;
  • half_up: at an exact half, increase the absolute magnitude.

Therefore 1.005 rounded to two places is 1.00 under half-even, while -1.005 becomes -1.01 under half-up.

The output also reports rounding_adjusted. This is true whenever the exact division had a discarded non-zero remainder—even when half-even leaves the retained digit unchanged.

Profit, loss, and break-even

A negative numerator produces a negative EPS. It does not make the calculation invalid:

3,000,000/2,000,000=1.50-3{,}000{,}000 / 2{,}000{,}000=-1.50

Likewise, a zero numerator is a valid break-even result. Only the denominator must be strictly positive.

The result uses a direction field—profit, loss, or break_even—so downstream systems do not need to reparse the formatted EPS string to determine the economic sign.

A production-minded contract

The function refuses to guess. It requires:

  • reporting identity and real period dates;
  • an RFC 3339 observation timestamp;
  • accounting framework and statement scope;
  • currency and explicit unit multipliers;
  • a complete numerator reconciliation;
  • a positive, fully restated denominator;
  • documented numerator and denominator policies;
  • rounding precision and mode;
  • finality state.

This strictness is deliberate. Silent defaults are convenient during a demo and expensive during a restatement.

What to test

A serious test suite should go beyond one profitable company:

  1. a canonical profit case with scaled units;
  2. a loss-per-share case;
  3. exact zero earnings;
  4. a participating-security allocation;
  5. different earnings and share scales;
  6. positive and negative rounding ties;
  7. invalid dates and timestamps;
  8. partial attribution and incomplete restatement flags;
  9. zero and negative denominators;
  10. cross-language equality from shared JSON fixtures.

The included suite covers each of these and verifies that the function does not mutate the input.

Reading EPS responsibly

Basic EPS is useful for reporting, trend analysis, valuation inputs, and data-quality controls. It is not a standalone investment signal.

When comparing companies or periods, ask:

  • Are the accounting framework and statement scope comparable?
  • Are continuing and discontinued operations treated consistently?
  • Were historical shares restated for capital events?
  • Did participating securities change the allocation?
  • Is the result final or provisional?
  • Did a buyback raise EPS while total earnings stayed flat?
  • Are one-off items driving the numerator?

The last question is especially important. EPS can rise because earnings improved, because the weighted-average share count fell, or both. The formula tells you the allocation; the reconciliation tells you the story.

The visual below holds earnings constant so that only share exposure changes. It demonstrates allocation mechanics, not company quality or expected returns.

Two synthetic companies with the same USD 12 million earnings: six million weighted-average shares produce USD 2.00 EPS and eight million produce USD 1.50.

Explore the guided lab

The Basic EPS reconciliation lab lets you compare profit, loss, participating-allocation, rounding, and the cited filing reconciliation. The provider-publication boundary is shown separately because no licensed provider comparison is embedded. Each scenario keeps the active rule, source context, numerator, denominator, and output visible.

Closing principle

Basic EPS is best understood as the meeting point of two controlled ledgers:

  • an earnings-attribution ledger;
  • a weighted-average share ledger.

Once both are complete, restated, scaled, and documented, the final division is easy. When either ledger is ambiguous, a precise quotient merely hides imprecision upstream.

For sources, implementation details, fixtures, and visual assets, see the package README and references.

Basic EPS boundary

Basic EPS reconciles ordinary-holder earnings and actual weighted-average ordinary shares. Potential ordinary shares enter a separate diluted-EPS process.

Rendering system map…

Takeaway: adding options or convertibles directly to the Basic EPS denominator changes the metric.

Calculation gates

The division is allowed only after both ledgers are complete and the units are explicit.

Rendering system map…

Takeaway: a calculation result is not accepted merely because the denominator is non-zero.

ReferencesPrimary sources and evidence notes

Expand the source trail, evidence role, and limitations behind the engineering choices.

Last reviewed: 2026-07-22.

IAS33 — IAS 33 Earnings per Share

  • Organization or authors: IFRS Foundation and International Accounting Standards Board
  • Source type: Official standard overview
  • Publication or effective date: 2026 issued-standard page; the page records the April 2024 IFRS 18 consequential amendment
  • Version: Web edition accessed 2026-07-22
  • URL or DOI: IAS 33 Earnings per Share
  • Accessed: 2026-07-22
  • Jurisdiction: IFRS-reporting jurisdictions, subject to local adoption
  • Supports: IAS 33 scope; consolidated numerator attribution; weighted-average denominator; basic-versus-diluted boundary; numerator and denominator reconciliations; capital-event effects
  • Limitations: The public overview is condensed. Production reporting requires the complete licensed standard, adopted amendments, local requirements, and entity-specific judgment.

ASC260 — FASB Accounting Standards Codification Topic 260

  • Organization or authors: Financial Accounting Standards Board
  • Source type: Official accounting codification
  • Publication or effective date: Current codification; continuously maintained
  • Version: Topic 260 table of contents accessed 2026-07-22
  • URL or DOI: ASC Topic 260 — Earnings Per Share
  • Accessed: 2026-07-22
  • Jurisdiction: United States GAAP
  • Supports: US GAAP EPS presentation and computation framework, including participating-security considerations
  • Limitations: Detailed codification text may require access credentials. The implementation deliberately requires two-class allocations to be prepared upstream and does not replace the codification.

FMAO-10Q — Farmers & Merchants Bancorp, Inc. 2025 second-quarter Form 10-Q

  • Organization or authors: Farmers & Merchants Bancorp, Inc.; filed with the U.S. Securities and Exchange Commission
  • Source type: Primary issuer filing and inline XBRL disclosure
  • Publication or effective date: Period ended 2025-06-30; filed 2025-08-04; accepted 2025-08-04T16:27:36Z
  • Version: Accession 0000950170-25-101771, Note 6
  • URL or DOI: SEC filing — Earnings Per Share note
  • Accessed: 2026-07-22
  • Jurisdiction: United States SEC registrant; US GAAP filing
  • Supports: The real historical case: two-class numerator allocation, common-share denominator reconciliation, reported Basic EPS, units, reporting period, and issuer explanation
  • Limitations: This is evidence about one registrant and one filing, not a universal policy example. The source is public filing data; no claim is made about later amendments beyond the version accessed.

SEC-SUBMISSIONS — SEC EDGAR submissions metadata for CIK 0000792966

  • Organization or authors: U.S. Securities and Exchange Commission
  • Source type: Official filing metadata API
  • Publication or effective date: Continuously maintained
  • Version: Retrieved 2026-07-22
  • URL or DOI: SEC submissions JSON
  • Accessed: 2026-07-22
  • Jurisdiction: United States
  • Supports: Registrant identity, ticker, filing date, report date, form, accession, and acceptance timestamp used in the historical case
  • Limitations: Filing metadata does not establish accounting measurement; the filed note supplies the accounting facts.

RFC3339 — Date and Time on the Internet: Timestamps

  • Organization or authors: Internet Engineering Task Force
  • Source type: Internet standard
  • Publication or effective date: July 2002
  • Version: RFC 3339
  • URL or DOI: RFC 3339
  • Accessed: 2026-07-22
  • Jurisdiction: Global technical standard
  • Supports: The explicit-offset as_of timestamp contract
  • Limitations: The package accepts the RFC 3339 profile implemented by its validators, not every ISO 8601 variant.

ECMASCRIPT-BIGINT — ECMAScript BigInt Objects

  • Organization or authors: Ecma International
  • Source type: Official language specification
  • Publication or effective date: Living ECMAScript specification
  • Version: Accessed 2026-07-22
  • URL or DOI: ECMAScript BigInt Objects
  • Accessed: 2026-07-22
  • Jurisdiction: Global technical specification
  • Supports: The exact-integer primitive used by the TypeScript implementation
  • Limitations: The specification defines the primitive; package tests establish this implementation's behavior.

PYTHON-INT — Python integer numeric type

  • Organization or authors: Python Software Foundation
  • Source type: Official language documentation
  • Publication or effective date: Current Python documentation
  • Version: Accessed 2026-07-22
  • URL or DOI: Python numeric types
  • Accessed: 2026-07-22
  • Jurisdiction: Global technical documentation
  • Supports: The arbitrary-precision integer primitive used by the Python implementation
  • Limitations: The documentation defines the primitive; package tests establish this implementation's behavior.

Research notes and limits

  • The package implements a calculation engine, not a complete accounting-policy engine. Upstream work must decide which earnings belong in the numerator and which instruments participate.
  • participating_security_allocation is explicit because US GAAP two-class allocations can affect Basic EPS. A zero is not an assertion that no participating securities exist.
  • Potential ordinary shares are excluded from this Basic EPS algorithm. Their effect belongs to diluted EPS.
  • The canonical and weighted-share-timeline examples are synthetic. The FMAO case is a cited public filing example and remains separated from the synthetic fixtures in prose and metadata.
  • Provider observations are timestamped observations, not accounting facts. Exact third-party values must remain private until display and redistribution rights are confirmed. A mismatch is a prompt to inspect semantics, mapping, versions, and source lineage—not permission to call either source “wrong” without further evidence.
basicEps.ts
/** Exact, dependency-free Basic EPS reference implementation. */

export type AccountingFramework = "IFRS" | "US_GAAP" | "OTHER_DECLARED";
export type StatementScope = "consolidated" | "separate";
export type RoundingMode = "half_even" | "half_up";

export interface BasicEpsInput {
  entity_id: string;
  period_start: string;
  period_end: string;
  as_of: string;
  accounting_framework: AccountingFramework;
  statement_scope: StatementScope;
  currency: string;
  profit_loss_attributable_to_owners: string;
  preference_dividend_adjustment: string;
  participating_security_allocation: string;
  other_numerator_adjustment: string;
  earnings_scale: string;
  weighted_average_ordinary_shares: string;
  share_scale: string;
  numerator_policy: string;
  denominator_policy: string;
  numerator_is_attribution_complete: boolean;
  denominator_is_restatement_complete: boolean;
  is_final: boolean;
  decimal_places: number;
  rounding_mode: RoundingMode;
}

export interface BasicEpsResult {
  metric: "basic_earnings_per_ordinary_share";
  entity_id: string;
  period_start: string;
  period_end: string;
  as_of: string;
  accounting_framework: AccountingFramework;
  statement_scope: StatementScope;
  currency: string;
  numerator_policy: string;
  denominator_policy: string;
  earnings_available_to_ordinary_shareholders: string;
  weighted_average_ordinary_shares_base: string;
  basic_eps: string;
  decimal_places: number;
  rounding_mode: RoundingMode;
  rounding_adjusted: boolean;
  direction: "profit" | "loss" | "break_even";
  status: "final" | "provisional";
  is_final: boolean;
}

interface ExactDecimal {
  coefficient: bigint;
  scale: number;
}

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 BasicEpsInput)[] = [
  "entity_id", "period_start", "period_end", "as_of", "accounting_framework",
  "statement_scope", "currency", "profit_loss_attributable_to_owners",
  "preference_dividend_adjustment", "participating_security_allocation",
  "other_numerator_adjustment", "earnings_scale",
  "weighted_average_ordinary_shares", "share_scale", "numerator_policy",
  "denominator_policy", "numerator_is_attribution_complete",
  "denominator_is_restatement_complete", "is_final", "decimal_places",
  "rounding_mode",
];

export class BasicEpsValidationError extends Error {
  constructor(message: string) {
    super(message);
    this.name = "BasicEpsValidationError";
  }
}

function parseDecimal(value: unknown, field: string): ExactDecimal {
  if (typeof value !== "string" || !DECIMAL_RE.test(value)) {
    throw new BasicEpsValidationError(
      `${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 BasicEpsValidationError(
      `${field} exceeds 30 digits or 12 fraction digits`,
    );
  }
  return {
    coefficient: BigInt(value.replace(".", "")),
    scale: fraction.length,
  };
}

function power10(exponent: number): bigint {
  return 10n ** BigInt(exponent);
}

function align(a: ExactDecimal, b: ExactDecimal): [bigint, bigint, number] {
  const scale = Math.max(a.scale, b.scale);
  return [
    a.coefficient * power10(scale - a.scale),
    b.coefficient * power10(scale - b.scale),
    scale,
  ];
}

function add(a: ExactDecimal, b: ExactDecimal): ExactDecimal {
  const [left, right, scale] = align(a, b);
  return {coefficient: left + right, scale};
}

function subtract(a: ExactDecimal, b: ExactDecimal): ExactDecimal {
  const [left, right, scale] = align(a, b);
  return {coefficient: left - right, scale};
}

function multiply(a: ExactDecimal, b: ExactDecimal): ExactDecimal {
  return {
    coefficient: a.coefficient * b.coefficient,
    scale: a.scale + b.scale,
  };
}

function normalString(value: ExactDecimal): string {
  const negative = value.coefficient < 0n;
  let digits = (negative ? -value.coefficient : value.coefficient).toString();
  if (value.scale > 0) {
    digits = digits.padStart(value.scale + 1, "0");
    const split = digits.length - value.scale;
    digits = `${digits.slice(0, split)}.${digits.slice(split)}`
      .replace(/0+$/, "")
      .replace(/\.$/, "");
  }
  return negative && digits !== "0" ? `-${digits}` : digits;
}

function roundRatio(
  numerator: ExactDecimal,
  denominator: ExactDecimal,
  places: number,
  mode: RoundingMode,
): [string, boolean] {
  const negative = numerator.coefficient < 0n;
  const magnitude = negative ? -numerator.coefficient : numerator.coefficient;
  const scaledNumerator = magnitude * power10(denominator.scale + places);
  const scaledDenominator =
    (denominator.coefficient < 0n
      ? -denominator.coefficient
      : denominator.coefficient) * power10(numerator.scale);
  let quotient = scaledNumerator / scaledDenominator;
  const remainder = scaledNumerator % scaledDenominator;
  const comparison = remainder * 2n - scaledDenominator;
  const increment = comparison > 0n ||
    (comparison === 0n &&
      (mode === "half_up" ||
        (mode === "half_even" && quotient % 2n === 1n)));
  if (increment) quotient += 1n;
  let digits = quotient.toString();
  if (places > 0) {
    digits = digits.padStart(places + 1, "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 requireText(value: unknown, field: string): string {
  if (typeof value !== "string" || value.trim() === "") {
    throw new BasicEpsValidationError(`${field} must be a non-empty string`);
  }
  return value;
}

function validateDate(value: unknown, field: string): string {
  if (typeof value !== "string" || !DATE_RE.test(value)) {
    throw new BasicEpsValidationError(`${field} must use YYYY-MM-DD`);
  }
  const parsed = new Date(`${value}T00:00:00Z`);
  if (Number.isNaN(parsed.valueOf()) || parsed.toISOString().slice(0, 10) !== value) {
    throw new BasicEpsValidationError(`${field} must be a real date`);
  }
  return value;
}

export function calculateBasicEps(input: BasicEpsInput): BasicEpsResult {
  if (input === null || typeof input !== "object" || Array.isArray(input)) {
    throw new BasicEpsValidationError("input must be an object");
  }
  for (const field of REQUIRED_FIELDS) {
    if (!(field in input)) {
      throw new BasicEpsValidationError(`missing required field: ${field}`);
    }
  }
  const entityId = requireText(input.entity_id, "entity_id");
  const start = validateDate(input.period_start, "period_start");
  const end = validateDate(input.period_end, "period_end");
  if (start > end) {
    throw new BasicEpsValidationError(
      "period_start must not be after period_end",
    );
  }
  if (!RFC3339_RE.test(input.as_of) || Number.isNaN(Date.parse(input.as_of))) {
    throw new BasicEpsValidationError(
      "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 BasicEpsValidationError("accounting_framework is not supported");
  }
  if (!["consolidated", "separate"].includes(input.statement_scope)) {
    throw new BasicEpsValidationError(
      "statement_scope must be consolidated or separate",
    );
  }
  if (!/^[A-Z]{3}$/.test(input.currency)) {
    throw new BasicEpsValidationError(
      "currency must be a three-letter uppercase code",
    );
  }
  requireText(input.numerator_policy, "numerator_policy");
  requireText(input.denominator_policy, "denominator_policy");
  for (const field of [
    "numerator_is_attribution_complete",
    "denominator_is_restatement_complete",
    "is_final",
  ] as const) {
    if (typeof input[field] !== "boolean") {
      throw new BasicEpsValidationError(`${field} must be boolean`);
    }
  }
  if (!input.numerator_is_attribution_complete) {
    throw new BasicEpsValidationError("numerator attribution must be complete");
  }
  if (!input.denominator_is_restatement_complete) {
    throw new BasicEpsValidationError("denominator restatement must be complete");
  }
  if (!Number.isInteger(input.decimal_places) ||
      input.decimal_places < 0 || input.decimal_places > 12) {
    throw new BasicEpsValidationError(
      "decimal_places must be an integer from 0 to 12",
    );
  }
  if (!["half_even", "half_up"].includes(input.rounding_mode)) {
    throw new BasicEpsValidationError(
      "rounding_mode must be half_even or half_up",
    );
  }

  const profit = parseDecimal(
    input.profit_loss_attributable_to_owners,
    "profit_loss_attributable_to_owners",
  );
  const preference = parseDecimal(
    input.preference_dividend_adjustment,
    "preference_dividend_adjustment",
  );
  const participating = parseDecimal(
    input.participating_security_allocation,
    "participating_security_allocation",
  );
  const other = parseDecimal(
    input.other_numerator_adjustment,
    "other_numerator_adjustment",
  );
  const earningsScale = parseDecimal(input.earnings_scale, "earnings_scale");
  const shares = parseDecimal(
    input.weighted_average_ordinary_shares,
    "weighted_average_ordinary_shares",
  );
  const shareScale = parseDecimal(input.share_scale, "share_scale");

  if (preference.coefficient < 0n) {
    throw new BasicEpsValidationError(
      "preference_dividend_adjustment must be non-negative",
    );
  }
  if (participating.coefficient < 0n) {
    throw new BasicEpsValidationError(
      "participating_security_allocation must be non-negative",
    );
  }
  if (earningsScale.coefficient <= 0n) {
    throw new BasicEpsValidationError("earnings_scale must be positive");
  }
  if (shares.coefficient <= 0n) {
    throw new BasicEpsValidationError(
      "weighted_average_ordinary_shares must be positive",
    );
  }
  if (shareScale.coefficient <= 0n) {
    throw new BasicEpsValidationError("share_scale must be positive");
  }

  const reportedNumerator = add(
    subtract(subtract(profit, preference), participating),
    other,
  );
  const numerator = multiply(reportedNumerator, earningsScale);
  const denominator = multiply(shares, shareScale);
  const [basicEps, roundingAdjusted] = roundRatio(
    numerator,
    denominator,
    input.decimal_places,
    input.rounding_mode,
  );
  const direction = numerator.coefficient > 0n
    ? "profit"
    : numerator.coefficient < 0n
    ? "loss"
    : "break_even";

  return {
    metric: "basic_earnings_per_ordinary_share",
    entity_id: entityId,
    period_start: start,
    period_end: end,
    as_of: input.as_of,
    accounting_framework: input.accounting_framework,
    statement_scope: input.statement_scope,
    currency: input.currency,
    numerator_policy: input.numerator_policy,
    denominator_policy: input.denominator_policy,
    earnings_available_to_ordinary_shareholders: normalString(numerator),
    weighted_average_ordinary_shares_base: normalString(denominator),
    basic_eps: basicEps,
    decimal_places: input.decimal_places,
    rounding_mode: input.rounding_mode,
    rounding_adjusted: roundingAdjusted,
    direction,
    status: input.is_final ? "final" : "provisional",
    is_final: input.is_final,
  };
}
Full-height labbasic eps labOpen full screen