@finbheara/domain 0.3.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -42,6 +42,7 @@ ESM, Node ≥ 22 or Bun ≥ 1.4.2. MIT. Not affiliated with or endorsed by any d
42
42
  | `@finbheara/domain/age` | `Age.from`, `Age.fromYOB`, `ageRelation`, `canOverlap`, `averageAge`, `isAveraged`, `intersectBirthYears`, the `Validated` type, the age constants |
43
43
  | `@finbheara/domain/noun-index` | `createIndex`, `indexSchema`, `personEncoder`, and the `SqlHandle`/`NounEncoder` types: a database-backed name map (below) |
44
44
  | `@finbheara/domain/dance-types` | `danceTypes()`, `DANCE_TYPES`, `rhythmOfTime`, and the shared `DanceMatch`/`DanceRelation`/`SetRhythm`/`TimeSignature` types |
45
+ | `@finbheara/domain/promotion` | `samePromotedRow`, `samePromotedSchool`, `samePromotedHeir`, `samePromotedName`, `questionMarksRead`, `foldCharSynonyms`: is a row the same row after a promotion (below) |
45
46
 
46
47
  The library does no I/O and installs nothing process-wide. Every reader has a default and a
47
48
  factory: build your own with options when the default is not what you want.
@@ -277,6 +278,67 @@ column of your own by spreading one, `{ ...enc, columns: [...enc.columns, "k"],
277
278
  type (`{ value: "id", valueType: "INTEGER PRIMARY KEY" }` makes the value the row id); on such a table `map(level)` iterates in value order, and a `set` to a value another row
278
279
  holds is refused by the key).
279
280
 
281
+ ## Decide whether a promoted row is the same row
282
+
283
+ A results sheet can be collected twice: first as a secondary view (an aggregator's copy), and
284
+ later as the primary transcript read from the publisher's own document, which supersedes it.
285
+ Whoever filed something against a row of the old view must then ask whether the row at the
286
+ same card of the new transcript is the same row. The producer that retires old rows and every
287
+ consumer that moves filed records onto successors must give the same answer, so the rule lives
288
+ here once. **A consumer re-deciding a superseded view's address must use these functions, not a
289
+ copy of them.**
290
+
291
+ ```ts
292
+ import {
293
+ samePromotedHeir,
294
+ samePromotedRow,
295
+ samePromotedSchool,
296
+ } from "@finbheara/domain/promotion";
297
+
298
+ samePromotedSchool("Brambleton : Mid Region", "Brambleton"); // true: a location tail
299
+ samePromotedSchool("Brambleton :", "Brambleton"); // true: a colon alone
300
+ samePromotedSchool("Brambleton Academy", "Brambleton"); // false
301
+
302
+ samePromotedRow(
303
+ { name: "Foireann A", school: "Brambleton" },
304
+ { name: "Team A", school: "Brambleton" },
305
+ ); // true: one designation
306
+
307
+ samePromotedHeir(
308
+ { name: "?ilis O?zumock", school: "Rinceoir? Na Zumock" },
309
+ { name: "Éilis O'zumock", school: "Rinceoirí Na Zumock" },
310
+ ); // true: `?` for one character
311
+ samePromotedHeir(
312
+ { name: "LenaZumock", school: "Brambleton" },
313
+ { name: "Lena (Xia) Zumock", school: "Brambleton" },
314
+ ); // true: glued, an aside added
315
+ samePromotedHeir(
316
+ { name: "?ilis Zumock", school: "Brambleton" },
317
+ { name: "Éilis Rervton", school: "Brambleton" },
318
+ ); // false: another surname
319
+ ```
320
+
321
+ What counts as the same row:
322
+
323
+ - **Both prints go through the character fold first** (`foldCharSynonyms`): the apostrophe,
324
+ dash and space glyphs each to their lowest code point, then runs of whitespace as one space.
325
+ - **The school** (`samePromotedSchool`) is equal after the fold, or the old print is the new one
326
+ with an apostrophe restored, or followed by `:` and a location, or by `:` alone.
327
+ - **An ordinary promotion** (`samePromotedRow`): the school the same, and the name equal after
328
+ the fold, an apostrophe restored, or the same team designation.
329
+ - **The heir of a retired row** (`samePromotedHeir`), where the producer retired the old row and
330
+ named no successor: the name byte-identical, or the school the same and the name the same
331
+ person's (`samePromotedName`). A `?` in the old print stands for exactly one character, an
332
+ apostrophe or a letter outside ASCII, and is read back from the new print at the same
333
+ position (`questionMarksRead`). The names are then compared through `@finbheara/names` at
334
+ NN3, which drops case, punctuation and accents and strips a parenthetical aside; a given name
335
+ and surname the old view glued into one token are compared by the name fold against the new
336
+ name's fields, in either order.
337
+
338
+ Anything else is another row: a different surname or given name, one letter printed for
339
+ another, a `?` over an ASCII letter, a school that differs anywhere but a trailing `: location`.
340
+ The caller pairs the card; nothing here reads an address.
341
+
280
342
  ## Known gaps
281
343
 
282
344
  - `place()` reads `"Derry in Ireland"` and `"Calgary in Western Canada"` as `unread`: it does
@@ -0,0 +1,105 @@
1
+ import { canonicalTeamName, nameFold, personName, probablySame } from "@finbheara/names/normalize";
2
+ var CHAR_SYNONYM_CLASSES = [
3
+ {
4
+ name: "apostrophe",
5
+ canonical: "'",
6
+ members: ["'", "’", "ʼ", "‘", "’"]
7
+ },
8
+ {
9
+ name: "dash",
10
+ canonical: "-",
11
+ members: ["-", "‐", "‑", "–", "—"]
12
+ },
13
+ {
14
+ name: "space",
15
+ canonical: " ",
16
+ members: [" ", " ", " ", " ", " ", " "]
17
+ }
18
+ ];
19
+ var FOLD_MAP = new Map(CHAR_SYNONYM_CLASSES.flatMap((c) => c.members.map((m) => [m, c.canonical])));
20
+ var FOLD_PATTERN = new RegExp(`[${[...FOLD_MAP.keys()].map((m) => `\\u${(m.codePointAt(0) ?? 0).toString(16).padStart(4, "0")}`).join("")}]`, "gu");
21
+ function foldCharSynonyms(text) {
22
+ return text.replace(FOLD_PATTERN, (ch) => FOLD_MAP.get(ch) ?? ch);
23
+ }
24
+ var collapse = (s) => foldCharSynonyms(s).replace(/\s+/gu, " ").trim();
25
+ function samePromotedSchool(was, now) {
26
+ const a = collapse(was);
27
+ const b = collapse(now);
28
+ if (a === b || apostropheRestored(was, now))
29
+ return true;
30
+ return b !== "" && a.startsWith(b) && /^ ?:( ?\S|$)/u.test(a.slice(b.length));
31
+ }
32
+ function apostropheRestored(was, now) {
33
+ const count = (s) => (collapse(s).match(/'/gu) ?? []).length;
34
+ const bare = (s) => collapse(collapse(s).replace(/'/gu, ""));
35
+ return count(now) >= count(was) && bare(was) === bare(now);
36
+ }
37
+ function samePromotedRow(was, now) {
38
+ if (!samePromotedSchool(was.school, now.school))
39
+ return false;
40
+ if (collapse(was.name) === collapse(now.name))
41
+ return true;
42
+ if (apostropheRestored(was.name, now.name))
43
+ return true;
44
+ const team = canonicalTeamName(collapse(was.name));
45
+ return team !== null && team === canonicalTeamName(collapse(now.name));
46
+ }
47
+ var STOOD_FOR = /^(?:'|(?![A-Za-z])\p{L})$/u;
48
+ function questionMarksRead(was, now) {
49
+ const a = [...collapse(was)];
50
+ if (!a.includes("?"))
51
+ return a.join("");
52
+ const b = [...collapse(now)];
53
+ if (a.length !== b.length)
54
+ return null;
55
+ const out = [];
56
+ for (let i = 0;i < a.length; i++) {
57
+ const x = a[i];
58
+ const y = b[i];
59
+ if (x === "?") {
60
+ if (!STOOD_FOR.test(y))
61
+ return null;
62
+ out.push(y);
63
+ } else if (x !== y)
64
+ return null;
65
+ else
66
+ out.push(x);
67
+ }
68
+ return out.join("");
69
+ }
70
+ function samePromotedName(was, now) {
71
+ const read = questionMarksRead(was, now);
72
+ if (read === null)
73
+ return false;
74
+ const n = collapse(now);
75
+ if (read === n)
76
+ return true;
77
+ const b = personName(n);
78
+ if (!b.isPerson())
79
+ return false;
80
+ const a = personName(read);
81
+ if (a.isPerson())
82
+ return probablySame(a, b, "nn3");
83
+ const glued = nameFold(read);
84
+ const given = b.givenFields().join("");
85
+ const surname = b.surnameKey();
86
+ return glued !== "" && (glued === nameFold(`${given}${surname}`) || glued === nameFold(`${surname}${given}`));
87
+ }
88
+ function samePromotedHeir(was, now) {
89
+ if (was.name !== "" && was.name === now.name)
90
+ return true;
91
+ const school = questionMarksRead(was.school, now.school);
92
+ if (school === null || !samePromotedSchool(school, now.school))
93
+ return false;
94
+ return samePromotedName(was.name, now.name);
95
+ }
96
+ export {
97
+ CHAR_SYNONYM_CLASSES,
98
+ collapse,
99
+ foldCharSynonyms,
100
+ questionMarksRead,
101
+ samePromotedHeir,
102
+ samePromotedName,
103
+ samePromotedRow,
104
+ samePromotedSchool
105
+ };
@@ -0,0 +1,16 @@
1
+ export interface PromotedPrint {
2
+ name: string;
3
+ school: string;
4
+ }
5
+ export declare const CHAR_SYNONYM_CLASSES: readonly {
6
+ readonly name: string;
7
+ readonly canonical: string;
8
+ readonly members: readonly string[];
9
+ }[];
10
+ export declare function foldCharSynonyms(text: string): string;
11
+ export declare const collapse: (s: string) => string;
12
+ export declare function samePromotedSchool(was: string, now: string): boolean;
13
+ export declare function samePromotedRow(was: PromotedPrint, now: PromotedPrint): boolean;
14
+ export declare function questionMarksRead(was: string, now: string): string | null;
15
+ export declare function samePromotedName(was: string, now: string): boolean;
16
+ export declare function samePromotedHeir(was: PromotedPrint, now: PromotedPrint): boolean;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@finbheara/domain",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "description": "Places, CLRG regions, school names, ages and dances as they are printed: read, compared and told apart",
5
5
  "keywords": [
6
6
  "places",
@@ -53,6 +53,10 @@
53
53
  "types": "./dist/types/noun-index.d.ts",
54
54
  "default": "./dist/noun-index.js"
55
55
  },
56
+ "./promotion": {
57
+ "types": "./dist/types/promotion.d.ts",
58
+ "default": "./dist/promotion.js"
59
+ },
56
60
  "./package.json": "./package.json"
57
61
  },
58
62
  "files": ["dist/*.js", "dist/chunks/*.js", "dist/types/**/*.d.ts", "README.md", "LICENSE"],