Fix annual financial selector and QCOM standardization
This commit is contained in:
@@ -13,6 +13,11 @@ type PrimaryPeriodSelection = {
|
||||
snapshots: FilingTaxonomySnapshotRecord[];
|
||||
};
|
||||
|
||||
type CandidateSelectionInput = {
|
||||
rows: TaxonomyStatementRow[];
|
||||
periods: FinancialStatementPeriod[];
|
||||
};
|
||||
|
||||
function parseEpoch(value: string | null) {
|
||||
if (!value) {
|
||||
return Number.NaN;
|
||||
@@ -54,6 +59,147 @@ function preferredDurationDays(filingType: FinancialStatementPeriod['filingType'
|
||||
return filingType === '10-K' ? 365 : 90;
|
||||
}
|
||||
|
||||
function isPresentedStatementRow(row: TaxonomyStatementRow) {
|
||||
return row.roleUri !== null && row.order !== Number.MAX_SAFE_INTEGER;
|
||||
}
|
||||
|
||||
function isPlausiblePrimaryPeriod(period: FinancialStatementPeriod, filingDate: string) {
|
||||
const filingEpoch = parseEpoch(filingDate);
|
||||
const periodEndEpoch = parseEpoch(period.periodEnd ?? period.filingDate);
|
||||
if (Number.isFinite(filingEpoch) && Number.isFinite(periodEndEpoch) && periodEndEpoch > filingEpoch) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const periodStartEpoch = parseEpoch(period.periodStart);
|
||||
if (Number.isFinite(filingEpoch) && Number.isFinite(periodStartEpoch) && periodStartEpoch > filingEpoch) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
function candidatePeriodsForRows(input: CandidateSelectionInput) {
|
||||
const usedPeriodIds = new Set<string>();
|
||||
for (const row of input.rows) {
|
||||
for (const periodId of Object.keys(row.values)) {
|
||||
usedPeriodIds.add(periodId);
|
||||
}
|
||||
}
|
||||
|
||||
return input.periods.filter((period) => usedPeriodIds.has(period.id));
|
||||
}
|
||||
|
||||
function coverageScore(rows: TaxonomyStatementRow[], periodId: string) {
|
||||
let count = 0;
|
||||
|
||||
for (const row of rows) {
|
||||
if (periodId in row.values) {
|
||||
count += 1;
|
||||
}
|
||||
}
|
||||
|
||||
return count;
|
||||
}
|
||||
|
||||
function compareBalancePeriods(
|
||||
left: FinancialStatementPeriod,
|
||||
right: FinancialStatementPeriod,
|
||||
rowCoverage: Map<string, number>
|
||||
) {
|
||||
const leftInstant = isInstantPeriod(left) ? 1 : 0;
|
||||
const rightInstant = isInstantPeriod(right) ? 1 : 0;
|
||||
if (leftInstant !== rightInstant) {
|
||||
return rightInstant - leftInstant;
|
||||
}
|
||||
|
||||
const leftCoverage = rowCoverage.get(left.id) ?? 0;
|
||||
const rightCoverage = rowCoverage.get(right.id) ?? 0;
|
||||
if (leftCoverage !== rightCoverage) {
|
||||
return rightCoverage - leftCoverage;
|
||||
}
|
||||
|
||||
const leftDate = parseEpoch(left.periodEnd ?? left.filingDate);
|
||||
const rightDate = parseEpoch(right.periodEnd ?? right.filingDate);
|
||||
if (Number.isFinite(leftDate) && Number.isFinite(rightDate) && leftDate !== rightDate) {
|
||||
return rightDate - leftDate;
|
||||
}
|
||||
|
||||
return left.id.localeCompare(right.id);
|
||||
}
|
||||
|
||||
function compareFlowPeriods(
|
||||
left: FinancialStatementPeriod,
|
||||
right: FinancialStatementPeriod,
|
||||
rowCoverage: Map<string, number>,
|
||||
targetDays: number,
|
||||
preferLaterBeforeDistance: boolean
|
||||
) {
|
||||
const leftDuration = isInstantPeriod(left) ? 0 : 1;
|
||||
const rightDuration = isInstantPeriod(right) ? 0 : 1;
|
||||
if (leftDuration !== rightDuration) {
|
||||
return rightDuration - leftDuration;
|
||||
}
|
||||
|
||||
const leftDate = parseEpoch(left.periodEnd ?? left.filingDate);
|
||||
const rightDate = parseEpoch(right.periodEnd ?? right.filingDate);
|
||||
if (preferLaterBeforeDistance && Number.isFinite(leftDate) && Number.isFinite(rightDate) && leftDate !== rightDate) {
|
||||
return rightDate - leftDate;
|
||||
}
|
||||
|
||||
const leftCoverage = rowCoverage.get(left.id) ?? 0;
|
||||
const rightCoverage = rowCoverage.get(right.id) ?? 0;
|
||||
if (leftCoverage !== rightCoverage) {
|
||||
return rightCoverage - leftCoverage;
|
||||
}
|
||||
|
||||
const leftDistance = Math.abs((periodDurationDays(left) ?? targetDays) - targetDays);
|
||||
const rightDistance = Math.abs((periodDurationDays(right) ?? targetDays) - targetDays);
|
||||
if (leftDistance !== rightDistance) {
|
||||
return leftDistance - rightDistance;
|
||||
}
|
||||
|
||||
if (Number.isFinite(leftDate) && Number.isFinite(rightDate) && leftDate !== rightDate) {
|
||||
return rightDate - leftDate;
|
||||
}
|
||||
|
||||
return left.id.localeCompare(right.id);
|
||||
}
|
||||
|
||||
function chooseCandidates(snapshot: FilingTaxonomySnapshotRecord, rows: TaxonomyStatementRow[]) {
|
||||
const periods = snapshot.periods ?? [];
|
||||
const presentedRows = rows.filter(isPresentedStatementRow);
|
||||
const plausiblePresented = candidatePeriodsForRows({
|
||||
rows: presentedRows,
|
||||
periods
|
||||
}).filter((period) => isPlausiblePrimaryPeriod(period, snapshot.filing_date));
|
||||
if (plausiblePresented.length > 0) {
|
||||
return {
|
||||
candidates: plausiblePresented,
|
||||
coverageRows: presentedRows,
|
||||
fallbackMode: false
|
||||
};
|
||||
}
|
||||
|
||||
const allCandidates = candidatePeriodsForRows({
|
||||
rows,
|
||||
periods
|
||||
});
|
||||
const plausibleAll = allCandidates.filter((period) => isPlausiblePrimaryPeriod(period, snapshot.filing_date));
|
||||
if (plausibleAll.length > 0) {
|
||||
return {
|
||||
candidates: plausibleAll,
|
||||
coverageRows: rows,
|
||||
fallbackMode: true
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
candidates: allCandidates,
|
||||
coverageRows: rows,
|
||||
fallbackMode: true
|
||||
};
|
||||
}
|
||||
|
||||
function selectPrimaryPeriodFromSnapshot(
|
||||
snapshot: FilingTaxonomySnapshotRecord,
|
||||
statement: FinancialStatementKind
|
||||
@@ -63,45 +209,21 @@ function selectPrimaryPeriodFromSnapshot(
|
||||
return null;
|
||||
}
|
||||
|
||||
const usedPeriodIds = new Set<string>();
|
||||
for (const row of rows) {
|
||||
for (const periodId of Object.keys(row.values)) {
|
||||
usedPeriodIds.add(periodId);
|
||||
}
|
||||
}
|
||||
|
||||
const candidates = (snapshot.periods ?? []).filter((period) => usedPeriodIds.has(period.id));
|
||||
const { candidates, coverageRows, fallbackMode } = chooseCandidates(snapshot, rows);
|
||||
if (candidates.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (statement === 'balance') {
|
||||
const instantCandidates = candidates.filter(isInstantPeriod);
|
||||
return (instantCandidates.length > 0 ? instantCandidates : candidates)
|
||||
.sort((left, right) => periodSorter(right, left))[0] ?? null;
|
||||
}
|
||||
const rowCoverage = new Map<string, number>(
|
||||
candidates.map((period) => [period.id, coverageScore(coverageRows, period.id)])
|
||||
);
|
||||
|
||||
const durationCandidates = candidates.filter((period) => !isInstantPeriod(period));
|
||||
if (durationCandidates.length === 0) {
|
||||
return candidates.sort((left, right) => periodSorter(right, left))[0] ?? null;
|
||||
if (statement === 'balance') {
|
||||
return [...candidates].sort((left, right) => compareBalancePeriods(left, right, rowCoverage))[0] ?? null;
|
||||
}
|
||||
|
||||
const targetDays = preferredDurationDays(snapshot.filing_type);
|
||||
return durationCandidates.sort((left, right) => {
|
||||
const leftDate = parseEpoch(left.periodEnd ?? left.filingDate);
|
||||
const rightDate = parseEpoch(right.periodEnd ?? right.filingDate);
|
||||
if (Number.isFinite(leftDate) && Number.isFinite(rightDate) && leftDate !== rightDate) {
|
||||
return rightDate - leftDate;
|
||||
}
|
||||
|
||||
const leftDistance = Math.abs((periodDurationDays(left) ?? targetDays) - targetDays);
|
||||
const rightDistance = Math.abs((periodDurationDays(right) ?? targetDays) - targetDays);
|
||||
if (leftDistance !== rightDistance) {
|
||||
return leftDistance - rightDistance;
|
||||
}
|
||||
|
||||
return left.id.localeCompare(right.id);
|
||||
})[0] ?? null;
|
||||
return [...candidates].sort((left, right) => compareFlowPeriods(left, right, rowCoverage, targetDays, fallbackMode))[0] ?? null;
|
||||
}
|
||||
|
||||
function filingTypeForCadence(cadence: FinancialCadence) {
|
||||
|
||||
Reference in New Issue
Block a user