153 lines
4.9 KiB
TypeScript
153 lines
4.9 KiB
TypeScript
import type {
|
|
DimensionBreakdownRow,
|
|
FinancialStatementKind,
|
|
FinancialStatementPeriod,
|
|
StandardizedFinancialRow,
|
|
TaxonomyFactRow,
|
|
TaxonomyStatementRow
|
|
} from '@/lib/types';
|
|
|
|
function valueOrNull(values: Record<string, number | null>, periodId: string) {
|
|
return periodId in values ? values[periodId] : null;
|
|
}
|
|
|
|
function sumValues(values: Array<number | null>, treatNullAsZero = false) {
|
|
if (!treatNullAsZero && values.some((value) => value === null)) {
|
|
return null;
|
|
}
|
|
|
|
return values.reduce<number>((sum, value) => sum + (value ?? 0), 0);
|
|
}
|
|
|
|
export function factMatchesPeriod(fact: TaxonomyFactRow, period: FinancialStatementPeriod) {
|
|
if (period.periodStart) {
|
|
return fact.periodStart === period.periodStart && fact.periodEnd === period.periodEnd;
|
|
}
|
|
|
|
return (fact.periodInstant ?? fact.periodEnd) === period.periodEnd;
|
|
}
|
|
|
|
export function buildDimensionBreakdown(
|
|
facts: TaxonomyFactRow[],
|
|
periods: FinancialStatementPeriod[],
|
|
faithfulRows: TaxonomyStatementRow[],
|
|
standardizedRows: StandardizedFinancialRow[]
|
|
) {
|
|
const periodByFilingId = new Map<number, FinancialStatementPeriod>();
|
|
for (const period of periods) {
|
|
periodByFilingId.set(period.filingId, period);
|
|
}
|
|
|
|
const faithfulRowByKey = new Map(faithfulRows.map((row) => [row.key, row]));
|
|
const standardizedRowsBySource = new Map<string, StandardizedFinancialRow[]>();
|
|
for (const row of standardizedRows) {
|
|
for (const sourceRowKey of row.sourceRowKeys) {
|
|
const existing = standardizedRowsBySource.get(sourceRowKey);
|
|
if (existing) {
|
|
existing.push(row);
|
|
} else {
|
|
standardizedRowsBySource.set(sourceRowKey, [row]);
|
|
}
|
|
}
|
|
}
|
|
|
|
const map = new Map<string, DimensionBreakdownRow[]>();
|
|
const pushRow = (key: string, row: DimensionBreakdownRow) => {
|
|
const existing = map.get(key);
|
|
if (existing) {
|
|
existing.push(row);
|
|
} else {
|
|
map.set(key, [row]);
|
|
}
|
|
};
|
|
|
|
for (const fact of facts) {
|
|
if (fact.dimensions.length === 0) {
|
|
continue;
|
|
}
|
|
|
|
const period = periodByFilingId.get(fact.filingId) ?? null;
|
|
if (!period || !factMatchesPeriod(fact, period)) {
|
|
continue;
|
|
}
|
|
|
|
const faithfulRow = faithfulRowByKey.get(fact.conceptKey) ?? null;
|
|
const standardizedMatches = standardizedRowsBySource.get(fact.conceptKey) ?? [];
|
|
|
|
for (const dimension of fact.dimensions) {
|
|
const faithfulDimensionRow: DimensionBreakdownRow = {
|
|
rowKey: fact.conceptKey,
|
|
concept: fact.qname,
|
|
sourceRowKey: fact.conceptKey,
|
|
sourceLabel: faithfulRow?.label ?? null,
|
|
periodId: period.id,
|
|
axis: dimension.axis,
|
|
member: dimension.member,
|
|
value: fact.value,
|
|
unit: fact.unit,
|
|
provenanceType: 'taxonomy'
|
|
};
|
|
|
|
pushRow(fact.conceptKey, faithfulDimensionRow);
|
|
for (const standardizedRow of standardizedMatches) {
|
|
pushRow(standardizedRow.key, {
|
|
...faithfulDimensionRow,
|
|
rowKey: standardizedRow.key
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
return map.size > 0 ? Object.fromEntries(map.entries()) : null;
|
|
}
|
|
|
|
function cloneStandardizedRows(rows: StandardizedFinancialRow[]) {
|
|
return rows.map((row) => ({
|
|
...row,
|
|
values: { ...row.values },
|
|
sourceConcepts: [...row.sourceConcepts],
|
|
sourceRowKeys: [...row.sourceRowKeys],
|
|
sourceFactIds: [...row.sourceFactIds],
|
|
resolvedSourceRowKeys: { ...row.resolvedSourceRowKeys }
|
|
}));
|
|
}
|
|
|
|
export function buildLtmStandardizedRows(
|
|
quarterlyRows: StandardizedFinancialRow[],
|
|
quarterlyPeriods: FinancialStatementPeriod[],
|
|
ltmPeriods: FinancialStatementPeriod[],
|
|
statement: Extract<FinancialStatementKind, 'income' | 'balance' | 'cash_flow' | 'equity' | 'disclosure'>
|
|
) {
|
|
const sortedQuarterlyPeriods = [...quarterlyPeriods].sort((left, right) => {
|
|
return Date.parse(left.periodEnd ?? left.filingDate) - Date.parse(right.periodEnd ?? right.filingDate);
|
|
});
|
|
const result = cloneStandardizedRows(quarterlyRows).map((row) => ({
|
|
...row,
|
|
values: {} as Record<string, number | null>,
|
|
resolvedSourceRowKeys: {} as Record<string, string | null>
|
|
}));
|
|
|
|
for (const row of result) {
|
|
const source = quarterlyRows.find((entry) => entry.key === row.key);
|
|
if (!source) {
|
|
continue;
|
|
}
|
|
|
|
for (const ltmPeriod of ltmPeriods) {
|
|
const anchorIndex = sortedQuarterlyPeriods.findIndex((period) => `ltm:${period.id}` === ltmPeriod.id);
|
|
if (anchorIndex < 3) {
|
|
continue;
|
|
}
|
|
|
|
const slice = sortedQuarterlyPeriods.slice(anchorIndex - 3, anchorIndex + 1);
|
|
const sourceValues = slice.map((period) => source.values[period.id] ?? null);
|
|
row.values[ltmPeriod.id] = statement === 'balance' || statement === 'equity'
|
|
? sourceValues[sourceValues.length - 1] ?? null
|
|
: sumValues(sourceValues);
|
|
row.resolvedSourceRowKeys[ltmPeriod.id] = source.formulaKey ? null : source.resolvedSourceRowKeys[slice[slice.length - 1]?.id ?? ''] ?? null;
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}
|