Skip to content

Commit b70db8a

Browse files
authored
fix(updater): n-port name key from the shared template 3380d91 (#90)
1 parent 0f1bd33 commit b70db8a

2 files changed

Lines changed: 26 additions & 10 deletions

File tree

‎scripts/update-data.test.ts‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -328,6 +328,14 @@ describe('parsing 3', () => {
328328
expect(looksLikeProductPage('Title: Just a moment...\n\n## Performing security verification')).toBe(false);
329329
expect(looksLikeProductPage(usfrPage)).toBe(true);
330330
});
331+
332+
test('N-PORT issuer names match SEC titles across initials, a leading US and backslash EDGAR suffixes', () => {
333+
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>`;
334+
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>`);
335+
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.' } });
336+
// U.S. Bancorp is not The Bancorp, and C.H. Robinson is one issuer however its initials are spaced
337+
expect(nportTickers(filing.positions, names)).toEqual(['USB', 'TBBK', 'CHRW']);
338+
});
331339
});
332340

333341
// ===========================================================================

‎scripts/update-data.ts‎

Lines changed: 18 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2745,20 +2745,28 @@ export function nportIdentifier(position: NportPosition): string {
27452745
}
27462746

27472747
/**
2748-
* A company name as a lookup key: upper case, punctuation and legal suffixes dropped, `CLASS A` -> `CL A`. Dotted
2749-
* 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`)
2750-
* and `PUBLIC LIMITED COMPANY` is `plc`, so both spellings of one issuer give one key.
2748+
* A company name as a lookup key: upper case, punctuation and legal suffixes dropped, `CLASS A` -> `CL A`. Initials are one
2749+
* word whether written with dots, spaces or a slash (`N.V.` = `N V` = `NV`, `C.H. ROBINSON` = `C H ROBINSON`, `A/S` = `A S`,
2750+
* `S.A.B. de C.V.` = `SAB DE CV`), `PUBLIC LIMITED COMPANY` is `plc`, and a leading `US` stays (`U.S. BANCORP` is not
2751+
* `BANCORP, INC.`), so both spellings of one issuer give one key.
27512752
*/
27522753
export function nportNameKey(value: unknown): string {
2753-
// EDGAR adds a state or qualifier suffix to company titles ("COSTCO WHOLESALE CORP /NEW", "BANK OF AMERICA CORP /DE/"): drop a trailing one
2754-
const raw = String(value === null || value === undefined ? '' : value).replace(/(?:\s*\/\s*[A-Za-z]{2,3}\s*\/?)+\s*$/, '')
2755-
.replace(/\b[A-Za-z](?:\.[A-Za-z])+\.?(?![A-Za-z])/g, (dotted) => dotted.replace(/\./g, ''))
2756-
.replace(/\bA\/S\b/gi, 'AS');
2757-
let text = nportText(raw).toUpperCase().replace(/['’]/g, '').replace(/&/g, ' AND ').replace(/[^A-Z0-9]+/g, ' ');
2758-
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, ' ');
2754+
// 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
2755+
const raw = String(value === null || value === undefined ? '' : value).replace(/(?:\s*[\/\\]\s*[A-Za-z]{2,3}\s*[\/\\]?)+\s*$/, '');
2756+
let text = nportText(raw).toUpperCase().replace(/['’]/g, '').replace(/&/g, ' AND ').replace(/[^A-Z0-9]+/g, ' ').trim();
27592757
text = text.replace(/\bCLASS\s+([A-Z])\b/g, 'CL $1').replace(/\bCL\s+([A-Z])\b/g, 'CL $1');
2758+
// a run of single letters is one word, never the class letter after CL
2759+
const words: string[] = [];
2760+
let joining = false;
2761+
text.split(' ').forEach((word) => {
2762+
const letter = /^[A-Z]$/.test(word);
2763+
if (letter && joining) words[words.length - 1] += word;
2764+
else words.push(word);
2765+
joining = letter && (joining || words[words.length - 2] !== 'CL');
2766+
});
2767+
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, ' ');
27602768
const share = /\bCL [A-Z]\b/.exec(text);
2761-
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();
2769+
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();
27622770
if (share && !/\bCL [A-Z]\b/.test(text)) text = `${text} ${share[0]}`;
27632771
return text;
27642772
}

0 commit comments

Comments
 (0)