Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions scripts/update-data.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -328,6 +328,14 @@ describe('parsing 3', () => {
expect(looksLikeProductPage('Title: Just a moment...\n\n## Performing security verification')).toBe(false);
expect(looksLikeProductPage(usfrPage)).toBe(true);
});

test('N-PORT issuer names match SEC titles across initials, a leading US and backslash EDGAR suffixes', () => {
const stock = (name: string, cusip: string) => `<invstOrSec><name>${name}</name><cusip>${cusip}</cusip><balance>1</balance><curCd>USD</curCd><valUSD>1</valUSD><pctVal>1</pctVal><assetCat>EC</assetCat></invstOrSec>`;
const filing = parseNportFiling(`<edgarSubmission><genInfo><seriesId>S000099999</seriesId></genInfo><invstOrSecs>${stock('US Bancorp', '902973304')}${stock('Bancorp Inc/The', '05969A105')}${stock('CH Robinson Worldwide Inc', '12541W209')}</invstOrSecs></edgarSubmission>`);
const names = parseSecCompanyNames({ 0: { ticker: 'USB', title: 'US BANCORP \\DE\\' }, 1: { ticker: 'TBBK', title: 'Bancorp, Inc.' }, 2: { ticker: 'CHRW', title: 'C H ROBINSON WORLDWIDE, INC.' } });
// U.S. Bancorp is not The Bancorp, and C.H. Robinson is one issuer however its initials are spaced
expect(nportTickers(filing.positions, names)).toEqual(['USB', 'TBBK', 'CHRW']);
});
});

// ===========================================================================
Expand Down
28 changes: 18 additions & 10 deletions scripts/update-data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2745,20 +2745,28 @@ export function nportIdentifier(position: NportPosition): string {
}

/**
* A company name as a lookup key: upper case, punctuation and legal suffixes dropped, `CLASS A` -> `CL A`. Dotted
* abbreviations collapse like the undotted forms (`N.V.` = `NV`, `S.p.A.` = `SPA`, `A/S` = `A S` = `AS`, `S.A.B. de C.V.` = `SAB DE CV`)
* and `PUBLIC LIMITED COMPANY` is `plc`, so both spellings of one issuer give one key.
* A company name as a lookup key: upper case, punctuation and legal suffixes dropped, `CLASS A` -> `CL A`. Initials are one
* word whether written with dots, spaces or a slash (`N.V.` = `N V` = `NV`, `C.H. ROBINSON` = `C H ROBINSON`, `A/S` = `A S`,
* `S.A.B. de C.V.` = `SAB DE CV`), `PUBLIC LIMITED COMPANY` is `plc`, and a leading `US` stays (`U.S. BANCORP` is not
* `BANCORP, INC.`), so both spellings of one issuer give one key.
*/
export function nportNameKey(value: unknown): string {
// EDGAR adds a state or qualifier suffix to company titles ("COSTCO WHOLESALE CORP /NEW", "BANK OF AMERICA CORP /DE/"): drop a trailing one
const raw = String(value === null || value === undefined ? '' : value).replace(/(?:\s*\/\s*[A-Za-z]{2,3}\s*\/?)+\s*$/, '')
.replace(/\b[A-Za-z](?:\.[A-Za-z])+\.?(?![A-Za-z])/g, (dotted) => dotted.replace(/\./g, ''))
.replace(/\bA\/S\b/gi, 'AS');
let text = nportText(raw).toUpperCase().replace(/['’]/g, '').replace(/&/g, ' AND ').replace(/[^A-Z0-9]+/g, ' ');
text = text.replace(/ A S ?$/, ' AS').replace(/\bPUBLIC (?:(?:LIMITED|LTD)(?: (?:COMP[A-Z]*|CO))?|(?:COMPANY|CO) (?:LIMITED|LTD))\b/g, 'PLC').replace(/\b(?:S DE RL )?DE CV\b/g, ' ');
// EDGAR adds a state or qualifier suffix to company titles ("COSTCO WHOLESALE CORP /NEW", "BANK OF AMERICA CORP /DE/", "US BANCORP \DE\"): drop a trailing one
const raw = String(value === null || value === undefined ? '' : value).replace(/(?:\s*[\/\\]\s*[A-Za-z]{2,3}\s*[\/\\]?)+\s*$/, '');
let text = nportText(raw).toUpperCase().replace(/['’]/g, '').replace(/&/g, ' AND ').replace(/[^A-Z0-9]+/g, ' ').trim();
text = text.replace(/\bCLASS\s+([A-Z])\b/g, 'CL $1').replace(/\bCL\s+([A-Z])\b/g, 'CL $1');
// a run of single letters is one word, never the class letter after CL
const words: string[] = [];
let joining = false;
text.split(' ').forEach((word) => {
const letter = /^[A-Z]$/.test(word);
if (letter && joining) words[words.length - 1] += word;
else words.push(word);
joining = letter && (joining || words[words.length - 2] !== 'CL');
});
text = words.join(' ').replace(/\bPUBLIC (?:(?:LIMITED|LTD)(?: (?:COMP[A-Z]*|CO))?|(?:COMPANY|CO) (?:LIMITED|LTD))\b/g, 'PLC').replace(/\b(?:S DE RL )?DE CV\b/g, ' ');
const share = /\bCL [A-Z]\b/.exec(text);
text = text.replace(/\b(THE|INC|INCORPORATED|CORP|CORPORATION|CO|COMPANY|LTD|LIMITED|PLC|SA|SAB|NV|AG|SE|SPA|AS|ASA|ORDINARY|COMMON|STOCK|SHS|SHARES|ADR|DEPOSITARY|RECEIPT|USD|US|REG|REGISTERED)\b/g, ' ').replace(/\s+/g, ' ').trim();
text = text.replace(/\b(THE|INC|INCORPORATED|CORP|CORPORATION|CO|COMPANY|LTD|LIMITED|PLC|SA|SAB|NV|AG|SE|SPA|AS|ASA|ORDINARY|COMMON|STOCK|SHS|SHARES|ADR|DEPOSITARY|RECEIPT|USD|US|REG|REGISTERED)\b/g, (word: string, _name: string, at: number) => (word === 'US' && at === 0 ? word : ' ')).replace(/\s+/g, ' ').trim();
if (share && !/\bCL [A-Z]\b/.test(text)) text = `${text} ${share[0]}`;
return text;
}
Expand Down
Loading