diff --git a/scripts/update-data.test.ts b/scripts/update-data.test.ts
index ecebdf4..696c904 100644
--- a/scripts/update-data.test.ts
+++ b/scripts/update-data.test.ts
@@ -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) => `${name}${cusip}1USD11EC`;
+ const filing = parseNportFiling(`S000099999${stock('US Bancorp', '902973304')}${stock('Bancorp Inc/The', '05969A105')}${stock('CH Robinson Worldwide Inc', '12541W209')}`);
+ 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']);
+ });
});
// ===========================================================================
diff --git a/scripts/update-data.ts b/scripts/update-data.ts
index 0a62f4f..02341c5 100755
--- a/scripts/update-data.ts
+++ b/scripts/update-data.ts
@@ -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;
}