107 lines
3.0 KiB
TypeScript
107 lines
3.0 KiB
TypeScript
import type { Filing } from '@/lib/types';
|
|
import type { TaxonomyFact } from '@/lib/server/taxonomy/types';
|
|
|
|
const METRIC_LOCAL_NAME_PRIORITY = {
|
|
revenue: [
|
|
'Revenues',
|
|
'SalesRevenueNet',
|
|
'RevenueFromContractWithCustomerExcludingAssessedTax',
|
|
'TotalRevenuesAndOtherIncome'
|
|
],
|
|
netIncome: ['NetIncomeLoss', 'ProfitLoss'],
|
|
totalAssets: ['Assets'],
|
|
cash: [
|
|
'CashAndCashEquivalentsAtCarryingValue',
|
|
'CashCashEquivalentsRestrictedCashAndRestrictedCashEquivalents'
|
|
],
|
|
debtDirect: [
|
|
'DebtAndFinanceLeaseLiabilities',
|
|
'Debt',
|
|
'LongTermDebtAndCapitalLeaseObligations'
|
|
],
|
|
debtCurrent: [
|
|
'DebtCurrent',
|
|
'ShortTermBorrowings',
|
|
'LongTermDebtCurrent'
|
|
],
|
|
debtNonCurrent: [
|
|
'LongTermDebtNoncurrent',
|
|
'LongTermDebt',
|
|
'DebtNoncurrent'
|
|
]
|
|
} as const;
|
|
|
|
function normalizeDateToEpoch(value: string | null) {
|
|
if (!value) {
|
|
return Number.NaN;
|
|
}
|
|
|
|
return Date.parse(value);
|
|
}
|
|
|
|
function sameLocalName(left: string, right: string) {
|
|
return left.toLowerCase() === right.toLowerCase();
|
|
}
|
|
|
|
function pickPreferredFact(facts: TaxonomyFact[]) {
|
|
const ordered = [...facts].sort((left, right) => {
|
|
const leftDimensionScore = left.isDimensionless ? 1 : 0;
|
|
const rightDimensionScore = right.isDimensionless ? 1 : 0;
|
|
if (leftDimensionScore !== rightDimensionScore) {
|
|
return rightDimensionScore - leftDimensionScore;
|
|
}
|
|
|
|
const leftDate = normalizeDateToEpoch(left.periodEnd ?? left.periodInstant);
|
|
const rightDate = normalizeDateToEpoch(right.periodEnd ?? right.periodInstant);
|
|
if (Number.isFinite(leftDate) && Number.isFinite(rightDate) && leftDate !== rightDate) {
|
|
return rightDate - leftDate;
|
|
}
|
|
|
|
return Math.abs(right.value) - Math.abs(left.value);
|
|
});
|
|
|
|
return ordered[0] ?? null;
|
|
}
|
|
|
|
function pickBestFact(facts: TaxonomyFact[], localNames: readonly string[]) {
|
|
for (const localName of localNames) {
|
|
const matches = facts.filter((fact) => sameLocalName(fact.localName, localName));
|
|
if (matches.length === 0) {
|
|
continue;
|
|
}
|
|
|
|
return pickPreferredFact(matches);
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
function sumIfBoth(left: number | null, right: number | null) {
|
|
if (left === null || right === null) {
|
|
return null;
|
|
}
|
|
|
|
return left + right;
|
|
}
|
|
|
|
export function deriveTaxonomyMetrics(facts: TaxonomyFact[]): NonNullable<Filing['metrics']> {
|
|
const revenue = pickBestFact(facts, METRIC_LOCAL_NAME_PRIORITY.revenue)?.value ?? null;
|
|
const netIncome = pickBestFact(facts, METRIC_LOCAL_NAME_PRIORITY.netIncome)?.value ?? null;
|
|
const totalAssets = pickBestFact(facts, METRIC_LOCAL_NAME_PRIORITY.totalAssets)?.value ?? null;
|
|
const cash = pickBestFact(facts, METRIC_LOCAL_NAME_PRIORITY.cash)?.value ?? null;
|
|
|
|
const directDebt = pickBestFact(facts, METRIC_LOCAL_NAME_PRIORITY.debtDirect)?.value ?? null;
|
|
const debt = directDebt ?? sumIfBoth(
|
|
pickBestFact(facts, METRIC_LOCAL_NAME_PRIORITY.debtCurrent)?.value ?? null,
|
|
pickBestFact(facts, METRIC_LOCAL_NAME_PRIORITY.debtNonCurrent)?.value ?? null
|
|
);
|
|
|
|
return {
|
|
revenue,
|
|
netIncome,
|
|
totalAssets,
|
|
cash,
|
|
debt
|
|
};
|
|
}
|