96 lines
3.5 KiB
JavaScript
96 lines
3.5 KiB
JavaScript
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.`);
|