Files
smart-fin-chart/scripts/normalize_brk_holdings.mjs
T

96 lines
3.5 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import fs from 'node:fs/promises';
import path from 'node:path';
const sourcePath = path.resolve('data/brk/rawData/brk_holdings_1988_1997_export.csv');
const outputPath = path.resolve('data/brk/brk_holdings.csv');
const COMPANY_NAMES = [
['american express|美国运通', '美国运通(American Express)'],
['coca-cola|可口可乐', '可口可乐(Coca-Cola)'],
['disney|迪士尼', '华特迪士尼(Walt Disney)'],
['freddie mac|房地美|联邦住', '房地美(Freddie Mac)'],
['gillette|吉列', '吉列(Gillette)'],
['travelers|旅行家', '旅行家集团(Travelers Group)'],
['washington post|华盛顿邮报', '华盛顿邮报(Washington Post)'],
['wells fargo|富国银行', '富国银行(Wells Fargo)'],
['capital cities|大都会', '大都会/ABC(Capital Cities/ABC)'],
['geico|盖可', '盖可保险(GEICO)'],
['gannett|甘尼特', '甘尼特(Gannett)'],
['pnc', 'PNC 银行(PNC Bank)'],
['general dynamics|通用动力', '通用动力(General Dynamics)'],
['guinness|健力士', '健力士(Guinness)'],
["mcdonald|麦当劳", '麦当劳(McDonald\'s)'],
].map(([pattern, name]) => ({ pattern: new RegExp(pattern, 'i'), name }));
function parseCSV(text) {
const rows = [];
let row = [];
let value = '';
let quoted = false;
for (let i = 0; i < text.length; i += 1) {
const char = text[i];
if (char === '"') {
if (quoted && text[i + 1] === '"') {
value += '"';
i += 1;
} else {
quoted = !quoted;
}
} else if (char === ',' && !quoted) {
row.push(value.trim());
value = '';
} else if ((char === '\n' || char === '\r') && !quoted) {
if (char === '\r' && text[i + 1] === '\n') i += 1;
row.push(value.trim());
rows.push(row);
row = [];
value = '';
} else {
value += char;
}
}
if (value || row.length) rows.push([...row, value.trim()]);
return rows;
}
function canonicalName(value) {
return COMPANY_NAMES.find(({ pattern }) => pattern.test(value))?.name || value.trim();
}
function parseShares(value) {
const digits = value.replace(/[^\d]/g, '');
return digits ? Number(digits) : NaN;
}
const rows = parseCSV(await fs.readFile(sourcePath, 'utf8'));
const holdings = [];
for (let index = 0; index < rows.length; index += 1) {
const dateMatch = rows[index][0]?.match(/^(\d{4})\/12\/31$/);
if (!dateMatch) continue;
const year = Number(dateMatch[1]);
const header = rows[index + 1] || [];
const sharesFirst = /股(份)?数量|股数|持股数量/.test(header[0] || '');
const companyIndex = sharesFirst ? 1 : 0;
const sharesIndex = sharesFirst ? 0 : 1;
for (let rowIndex = index + 2; rowIndex < rows.length; rowIndex += 1) {
const record = rows[rowIndex];
if (!record.some(Boolean)) break;
if (/^\d{4}\/12\/31$/.test(record[0] || '')) break;
const company = record[companyIndex];
const shares = parseShares(record[sharesIndex] || '');
if (!company || !Number.isFinite(shares)) continue;
holdings.push({ year, company: canonicalName(company), shares });
}
}
holdings.sort((a, b) => a.year - b.year || a.company.localeCompare(b.company, 'zh-CN'));
const lines = ['year,company,shares', ...holdings.map((row) => `${row.year},${row.company},${row.shares}`)];
await fs.mkdir(path.dirname(outputPath), { recursive: true });
await fs.writeFile(outputPath, `${lines.join('\n')}\n`, 'utf8');
console.log(`Normalized ${holdings.length} holdings across ${new Set(holdings.map((row) => row.year)).size} years.`);