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

D46-F01-A05 / Complete engineering topic

Restating EPS After a Stock Split: Preserve the Economics, Change the Unit

Restate Basic EPS after stock splits and share consolidations with exact factors, basis-aware event selection, reporting cutoffs, and reproducible controls.

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

A stock split can make a perfectly flat earnings history look like a collapse. A reverse split can make the same history look like an explosion. Neither impression is economic. Both come from comparing earnings per share measured in different share units.

The remedy is retrospective EPS restatement.

For a split factor FF:

F=post-split sharespre-split sharesF=\frac{\text{post-split shares}}{\text{pre-split shares}} restated shares=source shares×F\text{restated shares}=\text{source shares}\times F restated EPS=source EPSF\text{restated EPS}=\frac{\text{source EPS}}{F}
SymbolMeaningUnit
FFExact post-split shares divided by pre-split sharesdimensionless
EEEarnings available to ordinary shareholderscurrency
WWCompleted weighted-average shares on the source basisshares
WW^*Weighted-average shares on the target basis, W×FW\times Fshares

Shares follow the factor. EPS moves inversely. The earnings numerator does not change.

A share is a unit

Suppose a company earned USD 8 million with 2 million weighted-average ordinary shares. Basic EPS is USD 4:

8,000,000/2,000,000=4.008{,}000{,}000/2{,}000{,}000=4.00

Now it performs a 2-for-1 split. Each old share becomes two new shares. There are 4 million shares on the new basis, but the same shareholders collectively own the same company and the same USD 8 million of historical earnings.

The comparable historical EPS is:

8,000,000/4,000,000=2.008{,}000{,}000/4{,}000{,}000=2.00

Nothing was lost. The ownership unit became smaller, so twice as many units share the same earnings.

Why the adjustment is retrospective

IAS 33 paragraph 64 and FASB Statement No. 128 paragraph 54 require Basic and diluted EPS to be adjusted retrospectively for share splits and reverse splits for all periods presented. The rule also reaches qualifying events after the reporting period but before the statements are authorised or issued.

That timing surprises developers who treat the period-end date as a hard data cutoff.

In the canonical example:

  • FY2025 ends on 2025-12-31;
  • a 2-for-1 split becomes effective on 2026-01-15;
  • the statements are authorized on 2026-02-20.

The FY2025 per-share figures are presented on the new basis even though the split occurred in 2026. The fact that the per-share calculations reflect the change is disclosed.

This is not look-ahead trading data. It is a financial-statement presentation rule applied at the authorization date.

Illustrative post-period split inside the authorization window

The timeline uses synthetic dates and amounts. It explains the rule; it is not presented as an issuer event.

Forward and reverse directions

A forward split has F>1F>1. A 3-for-2 split has:

F=3/2F=3/2

The denominator becomes one and a half times larger; EPS becomes two thirds of its old amount.

A consolidation or reverse split has 0<F<10<F<1. A 1-for-5 consolidation has:

F=1/5F=1/5

The denominator becomes one fifth as large; EPS becomes five times larger in absolute magnitude.

The sign does not change. A loss of USD 0.50 per share on the old basis becomes a loss of USD 2.50 per share on a 1-for-5 basis:

0.50/(1/5)=2.50-0.50/(1/5)=-2.50

Calling that an “EPS improvement” would be a category error. Total loss is unchanged.

Forward and reverse split directions, with earnings held constant

Notice that labels and arrows carry the direction, so the comparison does not depend on color alone.

Basis dates prevent double application

Restatement systems often fail because they know the event but not the basis of the source data.

Suppose a source row already reflects a 2-for-1 split. Multiplying it by two again corrupts every denominator and halves EPS a second time.

The package assigns each row a basis_date. Its meaning is precise:

Every qualifying split or consolidation effective on or before the basis date is already reflected in the row.

An event is selected only when:

event effective date>basis date\text{event effective date}>\text{basis date}

Equality means “already reflected.”

The contract also requires basis_is_uniform=true. This matters for a period that spans an event. A weighted-average denominator must first be expressed entirely on one share basis. A mixed denominator containing unadjusted pre-split segments and already adjusted post-split segments cannot safely be corrected by multiplying the whole number once.

Calculation flow

Notice that the source row's basis is read before any factor is applied:

Rendering system map…

Accessible description: the engine validates a uniform source row, selects only events after its basis date, multiplies exact factors, restates shares, preserves earnings, recalculates EPS, and rounds only the returned display fields.

The reusable source is in visuals/mermaid/restatement-flow.md.

Multiple events compose

Corporate-action history may contain more than one event. Factors multiply.

Consider:

  • 3-for-2 split: 3/23/2;
  • later 1-for-4 consolidation: 1/41/4.

The cumulative factor is:

F=32×14=38F=\frac{3}{2}\times\frac{1}{4}=\frac{3}{8}

If the original weighted-average denominator was 3 million shares:

3,000,000×38=1,125,0003{,}000{,}000\times\frac{3}{8}=1{,}125{,}000

With EUR 6 million earnings:

restated EPS=6,000,0001,125,000=5.3333\text{restated EPS}= \frac{6{,}000{,}000}{1{,}125{,}000} =5.3333\ldots

The exact factor is three eighths, not 0.38, 0.3750 with an arbitrary precision, or a chain of rounded intermediate denominators.

A verified filing comparison: Apple’s 2020 split

A historical case is useful only when its evidence layers stay separate. The following comparison uses issuer filings; it does not claim that a market-data vendor revised its database on a particular date.

What the primary sources report

Apple’s 2019 Form 10-K reported, for 2019:

  • net income of USD 55,256 million;
  • weighted-average basic shares of 4,617,834 thousand; and
  • Basic EPS of USD 11.97.

Apple announced a four-for-one split on July 30, 2020, with an August 24 record date and split-adjusted trading beginning August 31. Apple’s 2020 Form 10-K says the split was effected on August 28 and that share and per-share information was retrospectively adjusted. Its comparative 2019 column shows:

  • the same USD 55,256 million net income;
  • 18,471,336 thousand weighted-average basic shares; and
  • Basic EPS of USD 2.99.
2019 amount2019 filing2020 comparativeInterpretation
Net incomeUSD 55,256mUSD 55,256mThe split did not restate earnings
Weighted-average basic shares4,617,834k18,471,336kExactly four times the old share units
Basic EPSUSD 11.97USD 2.99Same earnings expressed per smaller share unit

What we derive

The share bridge is exact:

4,617,834k×4=18,471,336k4{,}617{,}834\text{k}\times4=18{,}471{,}336\text{k}

Recomputing EPS from the unchanged numerator gives:

55,256m18,471.336m shares=2.99142.99\frac{55{,}256\text{m}} {18{,}471.336\text{m shares}} =2.9914\ldots\approx2.99

This is evidence of a retrospective unit restatement, not evidence that Apple’s 2019 operating earnings fell and not an investment-performance conclusion. A provider-specific statement—such as “vendor X changed its 2019 EPS on date Y”—would need a licensed, timestamped provider snapshot or change log that this package does not have.

Why rational arithmetic is useful

Both reference implementations use arbitrary-precision integers to represent rational numbers:

integer numeratorpositive integer denominator\frac{\text{integer numerator}}{\text{positive integer denominator}}

The 3-for-2 and 1-for-4 factors reduce exactly to 3-for-8. Earnings, unit scales, denominators, and EPS calculations remain rational until the requested display fields are rounded.

This eliminates several avoidable defects:

  • binary floating-point ties;
  • rounded factors compounded across events;
  • denominator rounding before EPS;
  • Python and TypeScript disagreement;
  • failure to reconcile earnings after restatement.

The core invariant remains:

restated shares×exact restated EPS=unchanged earnings\text{restated shares}\times\text{exact restated EPS} =\text{unchanged earnings}

Display-rounded EPS may not multiply back exactly. That is expected and is why the output includes rounding diagnostics.

A production-minded event contract

Each event declares:

  • a stable event ID;
  • its effective date;
  • stock_split or share_consolidation;
  • post-split and pre-split share terms;
  • an assertion that it is effective;
  • whether the combined transaction changes resources; this engine requires changes_resources=false.

Events must be unique and strictly date ordered. They cannot be later than the declared financial-statement authorization date.

The algorithm validates direction:

  • stock_split must reduce to a factor greater than one;
  • share_consolidation must reduce to a factor less than one;
  • 1-for-1 is rejected.

The legal conclusion that an event is effective—and the accounting conclusion that it is a plain no-resource-change unit event—does not belong inside a math function. The event-source policy records where those upstream conclusions came from; the function rejects changes_resources=true rather than silently treating a special-dividend combination as a plain consolidation.

The calculation path

  1. Validate the framework, currency, authorisation date, unit scales, rounding policy, and source policies.
  2. Validate that events are unique, effective, no-resource-change, chronological, and no later than the authorisation cutoff.
  3. Reduce every event ratio to an exact fraction and confirm that its direction matches its type.
  4. Require each period row to have one uniform share basis.
  5. Select only events whose effective date is later than that row's basis_date.
  6. Multiply the selected factors, preserve earnings, restate shares, and recalculate Basic EPS.
  7. Round only the output fields and return event lineage, finality, and rounding diagnostics.

What the engine does not restate

This topic preserves the earnings numerator. It does not claim that every historical numerator is eternally fixed. A different accounting change or prior-period correction could require its own numerator restatement.

It also does not complete diluted EPS. Options, warrants, convertibles, share awards, exercise prices, and antidilution conclusions may need corresponding review on the new basis.

Bonus issues and rights issues are nearby but separate topics because their source facts and factors differ.

EventFactor sourceTreatment here
Plain stock splitLegal post/pre share ratioIncluded
Plain share consolidationLegal post/pre share ratioIncluded
Bonus issueBonus/capitalisation termsD46-F01-A06
Rights issue with a bonus elementTheoretical ex-rights factorD46-F01-A07
Cash share issue or repurchaseConsideration and transaction dateWeighted-average-shares workflow
Consolidation combined with a special dividend or fair-value repurchase effectTransaction substance and resource changeIAS 33.29 analysis; excluded from this engine

Finally, the engine does not decide fractional-share settlement, par value, authorized capital, or journal entries. Those are legal and accounting consequences outside this EPS denominator transformation.

The exclusion in the last row is substantive. IAS 33.29 explains that when a consolidation plus special dividend has the overall effect of a fair-value share repurchase, resources decrease with the share count. In that case, the denominator reduction is recognized from the dividend-recognition date, not applied as a plain no-resource-change consolidation across every prior period.

Testing the restatement

A useful suite should include:

  1. a forward split inside the presentation window;
  2. a split after period-end but before authorization;
  3. a reverse split and negative EPS;
  4. multiple events whose fraction reduces;
  5. two rows with different basis dates;
  6. an event exactly on the basis date;
  7. no events;
  8. zero earnings;
  9. invalid ratios and event directions;
  10. events after authorization;
  11. mixed-basis rejection;
  12. cross-language fixture parity.

The included fixtures cover these structural cases and validate output strings exactly.

They also include the Apple 2019 comparison as a separately labeled documentary regression sourced from the two SEC filings. All other shared cases are synthetic. The documentary case is not evidence of any provider’s ingestion time or adjusted-data convention.

Failure modes to make visible

  • Double application: a source row is already split-adjusted, but its basis_date says otherwise.
  • Mixed units: pre-event and post-event segments were averaged before being normalized to one basis.
  • Rounded factors: a rational ratio such as 3/83/8 is stored as a short decimal and compounded.
  • Rounded EPS scaling: a displayed EPS value is divided by FF instead of recomputing from exact earnings and shares.
  • Wrong cutoff: an event after authorisation is pulled into the current invocation.
  • Label-only classification: a resource-changing combined transaction is treated as a plain consolidation.
  • Diluted-EPS shortcut: instrument terms and antidilution tests are not reconsidered where required.

Reading a restated series

When historical EPS changes after a split, the business has not rewritten history. The unit used to express each share has changed.

For analytical databases, it is helpful to retain:

  • original published value and basis;
  • restated value and target basis;
  • event IDs and factors applied;
  • authorization cutoff;
  • calculation version;
  • disclosure/source links.

That lineage distinguishes a controlled restatement from an unexplained vendor revision.

Closing principle

Stock-split EPS restatement has one economic idea:

Preserve the earnings; re-express the ownership unit.

The implementation becomes trustworthy when it also preserves the dates, basis, exact factor, event lineage, and rounding policy. Then a split no longer creates a fake earnings trend—and a reverse split no longer creates a fake recovery.

See the package README, references, implementations, tests, and visual lab for the complete reproducible system.

Post-period authorization window

An effective split after period-end but before financial-statement authorization changes the share basis used for the presented period.

Rendering system map…

Takeaway: the reporting-period boundary is not the only date that matters.

Restatement calculation flow

The source row’s basis date determines which later effective events are applied.

Rendering system map…

Takeaway: only the display is rounded; the factor and calculation remain exact.

ReferencesPrimary sources and evidence notes

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

Last reviewed: 2026-07-22.

Primary accounting sources

  1. IFRS Foundation, IAS 33 Earnings per Share, paragraphs 26–29 and 64. Official issued standard. Paragraphs 26–28 support retrospective denominator adjustment when share count changes without a corresponding change in resources; paragraph 29 identifies the fair-value-repurchase exception for a consolidation combined with a special dividend; paragraph 64 covers qualifying changes after the reporting period but before authorization and the related disclosure. Accessed 2026-07-22. The linked 2022 issued-standard PDF is used because the freely accessible current-standard page summarizes the topic but does not expose the complete licensed text.
  2. Financial Accounting Standards Board, Statement of Financial Accounting Standards No. 128, Earnings per Share, paragraph 54. Official FASB standard source underlying ASC Topic 260. Supports retroactive adjustment for stock splits and reverse stock splits, post-period/pre-issuance treatment, and disclosure. Accessed 2026-07-19.
  3. Financial Accounting Standards Board, EITF Issue No. 09-E. Official FASB material explicitly identifying ASC 260-10-55-12 as the stock-dividend/split restatement guidance. Used to connect the original standard language to the Codification location. Accessed 2026-07-19.
  4. Ategrity Specialty Insurance Company Holdings, Form 10-Q for the quarter ended September 30, 2025, Notes 2, 15, and 18. Primary SEC-filed example of a 1-for-10.66 reverse split and corporate conversion whose share and per-share presentation was retrospectively adjusted. Used as implementation evidence, not as GAAP itself. Accessed 2026-07-22.

Historical case sources

  1. Apple Inc., 2019 Form 10-K, Note 17. Primary SEC filing. Supports the originally filed 2019 net income of USD 55,256 million, weighted-average basic shares of 4,617,834 thousand, and Basic EPS of USD 11.97. Filed 2019-10-31; accessed 2026-07-22.
  2. Apple Inc., July 30, 2020 results release. Primary issuer announcement. Supports the four-for-one split approval, August 24, 2020 record date, and August 31, 2020 split-adjusted trading date. Accessed 2026-07-22.
  3. Apple Inc., 2020 Form 10-K, common-stock-split disclosure and Note 17. Primary SEC filing. Supports the August 28, 2020 effective split, retrospective adjustment of share and per-share information, and the 2019 comparative amounts: unchanged net income of USD 55,256 million, weighted-average basic shares of 18,471,336 thousand, and Basic EPS of USD 2.99. Filed 2020-10-30; accessed 2026-07-22.

Technical sources

  1. Ecma International, ECMAScript Language Specification — BigInt Objects. Supports the TypeScript implementation’s integer primitive. Accessed 2026-07-22.
  2. Python Software Foundation, Built-in Types — Numeric Types. Supports Python arbitrary-precision integer behavior. Accessed 2026-07-22.

Applicability and limitations

  • The engine covers stock/share splits and share consolidations (reverse splits). Bonus issues are A06; rights-issue bonus factors are A07.
  • The source denominator must be uniform on one declared share basis. Constructing a weighted-average denominator from transaction segments belongs to A03.
  • The algorithm accepts only legally effective, no-resource-change events through the declared authorization date. Legal effectiveness, transaction classification, and reporting authorization remain upstream judgments.
  • Production reporting must be checked against the entity’s complete licensed standards, adopted amendments, jurisdictional rules, facts, and disclosure controls.
  • Shared JSON fixtures are synthetic and carry no market-data license. The Apple documentary regression case is separately identified, uses only facts transcribed from public issuer filings, and is not a provider-data snapshot.
  • No claim is made about when any market-data vendor ingested, revised, or displayed the Apple figures. Proving a provider revision requires a licensed, timestamped provider snapshot or change log.
  • No FMP or other vendor payload is included in this distributable topic. Any future provider QA must remain private and clearance-required unless display and redistribution rights are confirmed.
epsRestatement.ts
/** Exact stock-split and share-consolidation EPS restatement. */

export type AccountingFramework = "IFRS" | "US_GAAP" | "OTHER_DECLARED";
export type RoundingMode = "half_even" | "half_up";
export type CapitalEventType = "stock_split" | "share_consolidation";

export interface CapitalEvent {
  event_id: string;
  effective_date: string;
  event_type: CapitalEventType;
  post_split_shares: string;
  pre_split_shares: string;
  event_is_effective: boolean;
  changes_resources: boolean;
}

export interface EpsPeriod {
  period_id: string;
  period_start: string;
  period_end: string;
  basis_date: string;
  basis_is_uniform: boolean;
  earnings_available_to_ordinary_shareholders: string;
  weighted_average_ordinary_shares: string;
  is_final: boolean;
}

export interface EpsRestatementInput {
  accounting_framework: AccountingFramework;
  currency: string;
  statements_authorized_for_issue_date: string;
  earnings_scale: string;
  share_scale: string;
  share_decimal_places: number;
  eps_decimal_places: number;
  rounding_mode: RoundingMode;
  restatement_policy: string;
  event_source_policy: string;
  events: CapitalEvent[];
  periods: EpsPeriod[];
}

export interface RestatedPeriod {
  period_id: string;
  period_start: string;
  period_end: string;
  source_basis_date: string;
  applied_event_ids: string[];
  factor_numerator: string;
  factor_denominator: string;
  earnings_available_to_ordinary_shareholders: string;
  pre_restatement_shares: string;
  restated_shares: string;
  pre_restatement_basic_eps: string;
  restated_basic_eps: string;
  share_rounding_adjusted: boolean;
  eps_rounding_adjusted: boolean;
  is_final: boolean;
}

export interface EpsRestatementResult {
  metric: "stock_split_consolidation_eps_restatement";
  accounting_framework: AccountingFramework;
  currency: string;
  statements_authorized_for_issue_date: string;
  status: "final" | "provisional";
  rows: RestatedPeriod[];
}

interface Rational {
  numerator: bigint;
  denominator: bigint;
}

const DECIMAL_RE = /^-?(?:0|[1-9]\d*)(?:\.\d+)?$/;
const POSITIVE_INTEGER_RE = /^[1-9]\d*$/;
const DATE_RE = /^\d{4}-\d{2}-\d{2}$/;

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

function abs(value: bigint): bigint {
  return value < 0n ? -value : value;
}

function gcd(left: bigint, right: bigint): bigint {
  let a = abs(left);
  let b = abs(right);
  while (b !== 0n) {
    [a, b] = [b, a % b];
  }
  return a;
}

function rational(numerator: bigint, denominator: bigint): Rational {
  if (denominator === 0n) throw new Error("rational denominator cannot be zero");
  if (denominator < 0n) {
    numerator = -numerator;
    denominator = -denominator;
  }
  const divisor = gcd(numerator, denominator);
  return {numerator: numerator / divisor, denominator: denominator / divisor};
}

function multiply(left: Rational, right: Rational): Rational {
  return rational(
    left.numerator * right.numerator,
    left.denominator * right.denominator,
  );
}

function divide(left: Rational, right: Rational): Rational {
  if (right.numerator === 0n) throw new Error("cannot divide by zero");
  return rational(
    left.numerator * right.denominator,
    left.denominator * right.numerator,
  );
}

function parseDecimal(value: unknown, field: string): Rational {
  if (typeof value !== "string" || !DECIMAL_RE.test(value)) {
    throw new EpsRestatementValidationError(
      `${field} must be a canonical decimal string`,
    );
  }
  const unsigned = value.startsWith("-") ? value.slice(1) : value;
  const [whole, fraction = ""] = unsigned.split(".");
  if (whole.length + fraction.length > 30 || fraction.length > 12) {
    throw new EpsRestatementValidationError(
      `${field} exceeds the decimal size limit`,
    );
  }
  return rational(BigInt(value.replace(".", "")), 10n ** BigInt(fraction.length));
}

function parsePositiveInteger(value: unknown, field: string): bigint {
  if (
    typeof value !== "string" ||
    !POSITIVE_INTEGER_RE.test(value) ||
    value.length > 30
  ) {
    throw new EpsRestatementValidationError(
      `${field} must be a positive canonical integer string`,
    );
  }
  return BigInt(value);
}

function roundFixed(
  value: Rational,
  places: number,
  mode: RoundingMode,
): [string, boolean] {
  const negative = value.numerator < 0n;
  const scaled = abs(value.numerator) * 10n ** BigInt(places);
  let quotient = scaled / value.denominator;
  const remainder = scaled % value.denominator;
  const comparison = remainder * 2n - value.denominator;
  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 terminatingString(value: Rational): string {
  let denominator = value.denominator;
  let twos = 0;
  let fives = 0;
  while (denominator % 2n === 0n) {
    denominator /= 2n;
    twos++;
  }
  while (denominator % 5n === 0n) {
    denominator /= 5n;
    fives++;
  }
  if (denominator !== 1n) throw new Error("expected a terminating decimal");
  const scale = Math.max(twos, fives);
  let scaled = value.numerator *
    2n ** BigInt(scale - twos) *
    5n ** BigInt(scale - fives);
  const negative = scaled < 0n;
  if (negative) scaled = -scaled;
  let digits = scaled.toString();
  if (scale > 0) {
    digits = digits.padStart(scale + 1, "0");
    const split = digits.length - scale;
    digits = `${digits.slice(0, split)}.${digits.slice(split)}`
      .replace(/0+$/, "")
      .replace(/\.$/, "");
  }
  return negative && digits !== "0" ? `-${digits}` : digits;
}

function requireText(value: unknown, field: string): string {
  if (typeof value !== "string" || value.trim() === "") {
    throw new EpsRestatementValidationError(`${field} must be a non-empty string`);
  }
  return value;
}

function parseDate(value: unknown, field: string): string {
  if (typeof value !== "string" || !DATE_RE.test(value)) {
    throw new EpsRestatementValidationError(`${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 EpsRestatementValidationError(`${field} must be a real date`);
  }
  return value;
}

export function restateEpsForCapitalEvents(
  input: EpsRestatementInput,
): EpsRestatementResult {
  if (input === null || typeof input !== "object" || Array.isArray(input)) {
    throw new EpsRestatementValidationError("input must be an object");
  }
  const required: (keyof EpsRestatementInput)[] = [
    "accounting_framework", "currency", "statements_authorized_for_issue_date",
    "earnings_scale", "share_scale", "share_decimal_places",
    "eps_decimal_places", "rounding_mode", "restatement_policy",
    "event_source_policy", "events", "periods",
  ];
  for (const field of required) {
    if (!(field in input)) {
      throw new EpsRestatementValidationError(`missing required field: ${field}`);
    }
  }
  if (!["IFRS", "US_GAAP", "OTHER_DECLARED"].includes(input.accounting_framework)) {
    throw new EpsRestatementValidationError("unsupported accounting_framework");
  }
  if (!/^[A-Z]{3}$/.test(input.currency)) {
    throw new EpsRestatementValidationError(
      "currency must be a three-letter uppercase code",
    );
  }
  const authorization = parseDate(
    input.statements_authorized_for_issue_date,
    "statements_authorized_for_issue_date",
  );
  requireText(input.restatement_policy, "restatement_policy");
  requireText(input.event_source_policy, "event_source_policy");
  const earningsScale = parseDecimal(input.earnings_scale, "earnings_scale");
  const shareScale = parseDecimal(input.share_scale, "share_scale");
  if (earningsScale.numerator <= 0n || shareScale.numerator <= 0n) {
    throw new EpsRestatementValidationError("unit scales must be positive");
  }
  for (const [value, field] of [
    [input.share_decimal_places, "share_decimal_places"],
    [input.eps_decimal_places, "eps_decimal_places"],
  ] as const) {
    if (!Number.isInteger(value) || value < 0 || value > 12) {
      throw new EpsRestatementValidationError(
        `${field} must be an integer from 0 to 12`,
      );
    }
  }
  if (!["half_even", "half_up"].includes(input.rounding_mode)) {
    throw new EpsRestatementValidationError(
      "rounding_mode must be half_even or half_up",
    );
  }
  if (!Array.isArray(input.events)) {
    throw new EpsRestatementValidationError("events must be an array");
  }
  if (!Array.isArray(input.periods) || input.periods.length === 0) {
    throw new EpsRestatementValidationError("periods must be a non-empty array");
  }

  const events: {id: string; effective: string; factor: Rational}[] = [];
  const eventIds = new Set<string>();
  let previousDate: string | null = null;
  input.events.forEach((raw, index) => {
    if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
      throw new EpsRestatementValidationError(`events[${index}] must be an object`);
    }
    const id = requireText(raw.event_id, `events[${index}].event_id`);
    if (eventIds.has(id)) {
      throw new EpsRestatementValidationError("event_id values must be unique");
    }
    eventIds.add(id);
    const effective = parseDate(
      raw.effective_date,
      `events[${index}].effective_date`,
    );
    if (effective > authorization) {
      throw new EpsRestatementValidationError(
        "events after authorization are outside this invocation",
      );
    }
    if (previousDate !== null && effective <= previousDate) {
      throw new EpsRestatementValidationError(
        "events must be strictly ordered by effective_date",
      );
    }
    previousDate = effective;
    if (typeof raw.event_is_effective !== "boolean" || !raw.event_is_effective) {
      throw new EpsRestatementValidationError("every event must be effective");
    }
    if (typeof raw.changes_resources !== "boolean") {
      throw new EpsRestatementValidationError(
        "every event must declare changes_resources",
      );
    }
    if (raw.changes_resources) {
      throw new EpsRestatementValidationError(
        "resource-changing combined transactions are outside this plain " +
          "split/consolidation engine",
      );
    }
    const post = parsePositiveInteger(
      raw.post_split_shares,
      `events[${index}].post_split_shares`,
    );
    const pre = parsePositiveInteger(
      raw.pre_split_shares,
      `events[${index}].pre_split_shares`,
    );
    const factor = rational(post, pre);
    if (raw.event_type === "stock_split" &&
        factor.numerator <= factor.denominator) {
      throw new EpsRestatementValidationError(
        "stock_split factor must be greater than one",
      );
    }
    if (raw.event_type === "share_consolidation" &&
        factor.numerator >= factor.denominator) {
      throw new EpsRestatementValidationError(
        "share_consolidation factor must be less than one",
      );
    }
    if (!["stock_split", "share_consolidation"].includes(raw.event_type)) {
      throw new EpsRestatementValidationError("unsupported event_type");
    }
    events.push({id, effective, factor});
  });

  const periodIds = new Set<string>();
  const rows = input.periods.map((raw, index): RestatedPeriod => {
    if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
      throw new EpsRestatementValidationError(`periods[${index}] must be an object`);
    }
    const periodId = requireText(raw.period_id, `periods[${index}].period_id`);
    if (periodIds.has(periodId)) {
      throw new EpsRestatementValidationError("period_id values must be unique");
    }
    periodIds.add(periodId);
    const start = parseDate(raw.period_start, `periods[${index}].period_start`);
    const end = parseDate(raw.period_end, `periods[${index}].period_end`);
    if (start > end) {
      throw new EpsRestatementValidationError(
        "period_start must not exceed period_end",
      );
    }
    const basis = parseDate(raw.basis_date, `periods[${index}].basis_date`);
    if (basis > authorization) {
      throw new EpsRestatementValidationError(
        "basis_date cannot exceed authorization",
      );
    }
    if (typeof raw.basis_is_uniform !== "boolean" || !raw.basis_is_uniform) {
      throw new EpsRestatementValidationError("every period basis must be uniform");
    }
    if (typeof raw.is_final !== "boolean") {
      throw new EpsRestatementValidationError("period is_final must be boolean");
    }
    const earningsReported = parseDecimal(
      raw.earnings_available_to_ordinary_shareholders,
      `periods[${index}].earnings_available_to_ordinary_shareholders`,
    );
    const sharesReported = parseDecimal(
      raw.weighted_average_ordinary_shares,
      `periods[${index}].weighted_average_ordinary_shares`,
    );
    if (sharesReported.numerator <= 0n) {
      throw new EpsRestatementValidationError(
        "weighted_average_ordinary_shares must be positive",
      );
    }
    const earnings = multiply(earningsReported, earningsScale);
    const originalShares = multiply(sharesReported, shareScale);
    let factor = rational(1n, 1n);
    const applied: string[] = [];
    for (const event of events) {
      if (event.effective > basis) {
        factor = multiply(factor, event.factor);
        applied.push(event.id);
      }
    }
    const restatedShares = multiply(originalShares, factor);
    const preEps = divide(earnings, originalShares);
    const restatedEps = divide(earnings, restatedShares);
    const [shareDisplay, shareAdjusted] = roundFixed(
      restatedShares,
      input.share_decimal_places,
      input.rounding_mode,
    );
    const [preEpsDisplay] = roundFixed(
      preEps,
      input.eps_decimal_places,
      input.rounding_mode,
    );
    const [restatedEpsDisplay, epsAdjusted] = roundFixed(
      restatedEps,
      input.eps_decimal_places,
      input.rounding_mode,
    );
    return {
      period_id: periodId,
      period_start: start,
      period_end: end,
      source_basis_date: basis,
      applied_event_ids: applied,
      factor_numerator: factor.numerator.toString(),
      factor_denominator: factor.denominator.toString(),
      earnings_available_to_ordinary_shareholders: terminatingString(earnings),
      pre_restatement_shares: terminatingString(originalShares),
      restated_shares: shareDisplay,
      pre_restatement_basic_eps: preEpsDisplay,
      restated_basic_eps: restatedEpsDisplay,
      share_rounding_adjusted: shareAdjusted,
      eps_rounding_adjusted: epsAdjusted,
      is_final: raw.is_final,
    };
  });

  return {
    metric: "stock_split_consolidation_eps_restatement",
    accounting_framework: input.accounting_framework,
    currency: input.currency,
    statements_authorized_for_issue_date: authorization,
    status: rows.every((row) => row.is_final) ? "final" : "provisional",
    rows,
  };
}
Full-height labeps restatement labOpen full screen