@finbheara/domain 0.2.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
@@ -39,9 +39,10 @@ ESM, Node ≥ 22 or Bun ≥ 1.4.2. MIT. Not affiliated with or endorsed by any d
39
39
  | `@finbheara/domain/tune` | `tune()`, `tuneRelation`, `tempoLegal`, `settingAt`, `TUNE_NODES`: the CLRG set dance list |
40
40
  | `@finbheara/domain/ceili` | `ceili()`, `ceiliRelation`, `CEILI_NODES`: the céilí handbook dances |
41
41
  | `@finbheara/domain/dance` | `dance()`: a print read as a tune and a céilí dance, narrowed by its round |
42
- | `@finbheara/domain/age` | `Age.from`, `Age.fromYOB`, `ageRelation`, `canOverlap`, `averageAge`, `isAveraged`, the age constants |
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.
@@ -180,6 +181,19 @@ Age.fromYOB(1999).fitsIn(Age.from("Over 21", { year: 2026, top: true })); // tru
180
181
  canOverlap(u12, Age.from("U14", { year: 2026 }), dancer); // true
181
182
  ```
182
183
 
184
+ A value the reader cannot stand behind is returned, not thrown. Every `Age` answers
185
+ `isValid()` and `invalidReason()`: an age group under `U3` admits nobody old enough to dance,
186
+ and a year of birth outside 1900 to 2099 is not one. An invalid `Age` has no birth years and
187
+ rules nothing out.
188
+
189
+ ```ts
190
+ Age.from("U2", { year: 2026 }).isValid(); // false
191
+ Age.from("U2", { year: 2026 }).invalidReason(); // "below-youngest-band"
192
+ Age.fromYOB(2222).invalidReason(); // "year-out-of-range"
193
+ Age.from("All", { year: 2026 }).invalidReason(); // "not-a-band"
194
+ Age.from("", { year: 2026 }).isValid(); // true: empty, not invalid
195
+ ```
196
+
183
197
  A team's age is the average of its dancers' ages, rounded to two decimals. `averageAge`
184
198
  takes exact ages only and returns an `AverageAge`, a separate type that refuses, by name,
185
199
  any use that would treat it as one dancer's age.
@@ -264,6 +278,67 @@ column of your own by spreading one, `{ ...enc, columns: [...enc.columns, "k"],
264
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
265
279
  holds is refused by the key).
266
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
+
267
342
  ## Known gaps
268
343
 
269
344
  - `place()` reads `"Derry in Ireland"` and `"Calgary in Western Canada"` as `unread`: it does
package/dist/age.js CHANGED
@@ -6,6 +6,8 @@ var UNDER_CAP_MAX_TOP = 21;
6
6
  var YOUNG_FLOOR = Object.freeze({ hard: 2, soft: 4 });
7
7
  var MIN_MAJOR_AGE = 4;
8
8
  var MIN_FEIS_AGE = 3;
9
+ var YOUNGEST_BAND_TOP = MAJORS_FLOOR_YEARS;
10
+ var YOB_RANGE = Object.freeze({ first: 1900, last: 2099 });
9
11
  var DANCE_UP_YEARS = 1;
10
12
  var ageOnJanuary1 = (year, born) => year - born - 1;
11
13
  var yearsSinceBirth = (year, born) => year - born;
@@ -116,6 +118,11 @@ function read(token, ladder, top) {
116
118
  }
117
119
  return { kind: null, interval: null, refusal: "not-a-band", clauses: [], parts: [] };
118
120
  }
121
+ function refuseBelowYoungest(r) {
122
+ if (r.interval === null || r.interval.hi >= YOUNGEST_BAND_TOP)
123
+ return r;
124
+ return { ...r, interval: null, refusal: "below-youngest-band", clauses: [] };
125
+ }
119
126
 
120
127
  class AgeMisuseError extends Error {
121
128
  misuse;
@@ -146,7 +153,7 @@ function fromPrint(print, context = {}) {
146
153
  const ladder = context.ladder ?? NO_LADDER;
147
154
  const year = context.year === undefined ? currentYear() : context.year;
148
155
  const ag1 = print.trim().toLowerCase();
149
- const reading = ag1 === "" ? { kind: null, interval: null, refusal: null, clauses: [], parts: [] } : read(print.trim(), ladder, context.top === true);
156
+ const reading = ag1 === "" ? { kind: null, interval: null, refusal: null, clauses: [], parts: [] } : refuseBelowYoungest(read(print.trim(), ladder, context.top === true));
150
157
  const iv = reading.interval;
151
158
  const arm = ag1 === "" ? "empty" : iv === null ? "open" : "band";
152
159
  const ag2 = iv !== null ? display(iv) : ag1 === "" ? "" : `${ag1}?`;
@@ -162,6 +169,8 @@ function fromPrint(print, context = {}) {
162
169
  parts: reading.parts,
163
170
  length: ag2.length,
164
171
  refusal: reading.refusal,
172
+ isValid: () => reading.refusal === null,
173
+ invalidReason: () => reading.refusal,
165
174
  source,
166
175
  clauses: reading.clauses,
167
176
  ladder,
@@ -186,7 +195,7 @@ function fromPrint(print, context = {}) {
186
195
  floorObserved: () => reading.kind !== "under" || iv === null ? null : reading.clauses.includes("under-floored-on-rung")
187
196
  });
188
197
  }
189
- function fromYOB(from, to = from) {
198
+ function fromYOB(from, to = from, context = {}) {
190
199
  for (const y of [from, to])
191
200
  if (y !== null && !Number.isInteger(y))
192
201
  throw new RangeError(`Age.fromYOB(${from}, ${to}): a year of birth is a whole number`);
@@ -194,29 +203,34 @@ function fromYOB(from, to = from) {
194
203
  throw new RangeError("Age.fromYOB(null, null): at least one end is a year");
195
204
  if (from !== null && to !== null && from > to)
196
205
  throw new RangeError(`Age.fromYOB(${from}, ${to}): the earliest year comes first`);
197
- const exact = from !== null && from === to;
198
- const ag0 = exact ? String(from) : `${from ?? ""}..${to ?? ""}`;
199
- const ag2 = `b${ag0}`;
200
- const years = Object.freeze({ from, to });
206
+ const at = context.year ?? null;
207
+ const valid = [from, to].every((y) => y === null || y >= YOB_RANGE.first && y <= YOB_RANGE.last) && (at === null || to === null || yearsSinceBirth(at, to) >= YOUNG_FLOOR.hard);
208
+ const refusal = valid ? null : "year-out-of-range";
209
+ const exact = valid && from !== null && from === to;
210
+ const ag0 = from !== null && from === to ? String(from) : `${from ?? ""}..${to ?? ""}`;
211
+ const ag2 = valid ? `b${ag0}` : `b${ag0}?`;
212
+ const years = Object.freeze(valid ? { from, to } : { from: null, to: null });
201
213
  return withRelations({
202
214
  [Symbol.toStringTag]: "Age",
203
215
  toString: () => ag2,
204
- arm: "yob",
216
+ arm: valid ? "yob" : "open",
205
217
  kind: "yob",
206
218
  ag0,
207
219
  ag1: ag0,
208
220
  ag2,
209
- parts: exact ? [from] : [from, to].filter((y) => y !== null),
221
+ parts: from !== null && from === to ? [from] : [from, to].filter((y) => y !== null),
210
222
  length: ag2.length,
211
- refusal: null,
212
- source: "printed-yob",
223
+ refusal,
224
+ isValid: () => valid,
225
+ invalidReason: () => refusal,
226
+ source: valid ? "printed-yob" : "open",
213
227
  clauses: [],
214
228
  ladder: NO_LADDER,
215
229
  year: null,
216
230
  isEmpty: () => false,
217
- isAge: () => true,
231
+ isAge: () => valid,
218
232
  isExact: () => exact,
219
- isAndOver: () => from === null,
233
+ isAndOver: () => valid && from === null,
220
234
  isAdult: () => false,
221
235
  interval: () => null,
222
236
  birthYears: () => years,
@@ -250,7 +264,7 @@ function birthYearsOfInterval(iv, year) {
250
264
  to: Math.min(year - iv.lo - 1, year - MAJORS_FLOOR_YEARS)
251
265
  };
252
266
  }
253
- var inBirthYears = (a) => a.arm === "yob" || a.year !== null;
267
+ var inBirthYears = (a) => a.kind === "yob" || a.year !== null;
254
268
  function spanOf(a) {
255
269
  if (!a.isAge())
256
270
  return null;
@@ -355,6 +369,25 @@ function birthYearsDisjoint(a, b) {
355
369
  return true;
356
370
  return false;
357
371
  }
372
+ function intersectBirthYears(windows) {
373
+ let from = null;
374
+ let to = null;
375
+ let n = 0;
376
+ for (const w of windows) {
377
+ if (w.from === null && w.to === null)
378
+ continue;
379
+ n++;
380
+ if (w.from !== null)
381
+ from = from === null ? w.from : Math.max(from, w.from);
382
+ if (w.to !== null)
383
+ to = to === null ? w.to : Math.min(to, w.to);
384
+ }
385
+ if (n === 0)
386
+ return { from: null, to: null, n: 0 };
387
+ if (from !== null && to !== null && from > to)
388
+ return null;
389
+ return { from, to, n };
390
+ }
358
391
  function birthYearsMeet(card, other, slack = 0) {
359
392
  if (card.from !== null && other.to !== null && card.from > other.to)
360
393
  return false;
@@ -378,6 +411,11 @@ function underCappedYears(b, year, n = UNDER_CAP.plain) {
378
411
  function underCapRefuses(bandFrom, identityFrom, n) {
379
412
  return identityFrom > bandFrom + n;
380
413
  }
414
+ function validYOB(y, year) {
415
+ if (y === null || y === undefined || !Number.isInteger(y))
416
+ return null;
417
+ return Age.fromYOB(y, y, { year: year ?? null }).isValid() ? y : null;
418
+ }
381
419
  function youngFloor(year, oldest) {
382
420
  const s = yearsSinceBirth(year, oldest);
383
421
  if (s >= YOUNG_FLOOR.soft)
@@ -399,6 +437,8 @@ export {
399
437
  MIN_MAJOR_AGE,
400
438
  UNDER_CAP,
401
439
  UNDER_CAP_MAX_TOP,
440
+ YOB_RANGE,
441
+ YOUNGEST_BAND_TOP,
402
442
  YOUNGEST_FLOOR,
403
443
  YOUNG_FLOOR,
404
444
  ageOnJanuary1,
@@ -408,6 +448,7 @@ export {
408
448
  birthYearsMeet,
409
449
  birthYearsOfInterval,
410
450
  canOverlap,
451
+ intersectBirthYears,
411
452
  isAveraged,
412
453
  roundedMeanHundredths,
413
454
  tooYoungForFeis,
@@ -415,6 +456,7 @@ export {
415
456
  underCapApplies,
416
457
  underCapRefuses,
417
458
  underCappedYears,
459
+ validYOB,
418
460
  yearsSinceBirth,
419
461
  youngFloor
420
462
  };
package/dist/ceili.js CHANGED
@@ -4,8 +4,8 @@ import {
4
4
  ceiliById2,
5
5
  ceili2,
6
6
  ceiliRelation2
7
- } from "./chunks/place-7gqecaxd.js";
8
- import"./chunks/place-g6yw4cq6.js";
7
+ } from "./chunks/place-0ehc2q69.js";
8
+ import"./chunks/place-htxgptyv.js";
9
9
  export {
10
10
  CEILI_NODES2 as CEILI_NODES,
11
11
  allCeilis2 as allCeilis,
@@ -3,7 +3,7 @@ import {
3
3
  matchNodes,
4
4
  candidateRelation,
5
5
  isDanceTypePrint2
6
- } from "./place-g6yw4cq6.js";
6
+ } from "./place-htxgptyv.js";
7
7
 
8
8
  function node(id, name, irish, family, formation, opts = {}) {
9
9
  const kind = "round" in formation ? "round" : ("long" in formation) ? "long" : "set";
@@ -5,7 +5,7 @@ import {
5
5
  candidateRelation,
6
6
  rhythmOfTime2,
7
7
  isDanceTypePrint2
8
- } from "./place-g6yw4cq6.js";
8
+ } from "./place-htxgptyv.js";
9
9
 
10
10
  var SET_MIN_SPEED2 = Object.freeze({
11
11
  jig: 66,
@@ -104,6 +104,7 @@ function rhythmOfTime2(time) {
104
104
  }
105
105
  var TYPE_PRINTS = new Map([
106
106
  ["reel", ["reel"]],
107
+ ["jig", ["light-jig", "single-jig", "treble-jig"]],
107
108
  ["light jig", ["light-jig"]],
108
109
  ["single jig", ["single-jig"]],
109
110
  ["slip jig", ["slip-jig"]],
@@ -4,7 +4,7 @@ import {
4
4
  danceTypes2,
5
5
  danceTypePrints2,
6
6
  isDanceTypePrint2
7
- } from "./chunks/place-g6yw4cq6.js";
7
+ } from "./chunks/place-htxgptyv.js";
8
8
  export {
9
9
  DANCE_TYPES2 as DANCE_TYPES,
10
10
  danceTypePrints2 as danceTypePrints,
package/dist/dance.js CHANGED
@@ -1,10 +1,10 @@
1
- import"./chunks/place-g6yw4cq6.js";
1
+ import"./chunks/place-htxgptyv.js";
2
2
  import {
3
3
  ceili2
4
- } from "./chunks/place-7gqecaxd.js";
4
+ } from "./chunks/place-0ehc2q69.js";
5
5
  import {
6
6
  tune2
7
- } from "./chunks/place-kr42294h.js";
7
+ } from "./chunks/place-2ja9yx08.js";
8
8
 
9
9
  function dance(print, round) {
10
10
  const setting = round === "traditional-set" ? "traditional" : undefined;
@@ -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
+ };
package/dist/tune.js CHANGED
@@ -8,8 +8,8 @@ import {
8
8
  settingAt2,
9
9
  tune2,
10
10
  tuneRelation2
11
- } from "./chunks/place-kr42294h.js";
12
- import"./chunks/place-g6yw4cq6.js";
11
+ } from "./chunks/place-2ja9yx08.js";
12
+ import"./chunks/place-htxgptyv.js";
13
13
  export {
14
14
  PLAUSIBLE_SET_TEMPO_MAX2 as PLAUSIBLE_SET_TEMPO_MAX,
15
15
  SET_MIN_SPEED2 as SET_MIN_SPEED,
@@ -11,6 +11,9 @@ export interface AgeLadder {
11
11
  readonly overs: readonly number[];
12
12
  readonly ranges: readonly number[];
13
13
  }
14
+ export interface YOBContext {
15
+ readonly year?: number | null;
16
+ }
14
17
  export interface AgeContext {
15
18
  readonly ladder?: AgeLadder;
16
19
  readonly year?: number | null;
@@ -18,7 +21,7 @@ export interface AgeContext {
18
21
  }
19
22
  export type AgeArm = "band" | "yob" | "open" | "empty";
20
23
  export type AgeKind = "under" | "over" | "range" | "adult" | "senior" | "yob";
21
- export type AgeRefusal = "not-a-band" | "backwards-range" | "senior-without-ladder";
24
+ export type AgeRefusal = "not-a-band" | "backwards-range" | "senior-without-ladder" | "below-youngest-band" | "year-out-of-range";
22
25
  export type AgeSource = "sibling-narrowed" | "printed-band" | "printed-range" | "printed-yob" | "open";
23
26
  export type AgeClause = "under-floored-on-rung" | "youngest-absorbs" | "over-ceiling-from-next-over" | "over-ceiling-refused-gap" | "and-over-no-ceiling" | "range-overlapping" | "range-inclusive" | "range-floor-on-rung" | "range-floor-widened" | "range-adult-floor";
24
27
  export type AgeRelation = "same" | "contains" | "overlaps" | "disjoint" | "silent";
@@ -1,5 +1,6 @@
1
- import type { AgeArm, AgeClause, AgeComparison, AgeContext, AgeInterval, AgeKind, AgeLadder, AgeMisuse, AgeRefusal, AgeRelation, AgeRelationReason, AgeSource, BirthYears, YoungFloorVerdict } from "./age-types.ts";
2
- export type { AgeArm, AgeClause, AgeComparison, AgeContext, AgeInterval, AgeKind, AgeLadder, AgeMisuse, AgeRefusal, AgeRelation, AgeRelationReason, AgeSource, BirthYears, YoungFloorVerdict, };
1
+ import type { AgeArm, AgeClause, AgeComparison, AgeContext, AgeInterval, AgeKind, AgeLadder, AgeMisuse, AgeRefusal, AgeRelation, AgeRelationReason, AgeSource, BirthYears, YOBContext, YoungFloorVerdict } from "./age-types.ts";
2
+ import type { Validated } from "./validated.ts";
3
+ export type { AgeArm, AgeClause, AgeComparison, AgeContext, AgeInterval, AgeKind, AgeLadder, AgeMisuse, AgeRefusal, AgeRelation, AgeRelationReason, AgeSource, BirthYears, Validated, YOBContext, YoungFloorVerdict, };
3
4
  export declare const YOUNGEST_FLOOR = 0;
4
5
  export declare const ADULT_FLOOR = 18;
5
6
  export declare const MAJORS_FLOOR_YEARS = 3;
@@ -14,10 +15,15 @@ export declare const YOUNG_FLOOR: Readonly<{
14
15
  }>;
15
16
  export declare const MIN_MAJOR_AGE = 4;
16
17
  export declare const MIN_FEIS_AGE = 3;
18
+ export declare const YOUNGEST_BAND_TOP = 3;
19
+ export declare const YOB_RANGE: Readonly<{
20
+ readonly first: 1900;
21
+ readonly last: 2099;
22
+ }>;
17
23
  export declare const DANCE_UP_YEARS = 1;
18
24
  export declare const ageOnJanuary1: (year: number, born: number) => number;
19
25
  export declare const yearsSinceBirth: (year: number, born: number) => number;
20
- export interface Age {
26
+ export interface Age extends Validated<AgeRefusal> {
21
27
  readonly [Symbol.toStringTag]: "Age";
22
28
  toString(): string;
23
29
  readonly arm: AgeArm;
@@ -48,7 +54,7 @@ export declare class AgeMisuseError extends Error {
48
54
  constructor(misuse: AgeMisuse, detail: string);
49
55
  }
50
56
  declare function fromPrint(print: string, context?: AgeContext): Age;
51
- declare function fromYOB(from: number | null, to?: number | null): Age;
57
+ declare function fromYOB(from: number | null, to?: number | null, context?: YOBContext): Age;
52
58
  export declare const Age: Readonly<{
53
59
  from: typeof fromPrint;
54
60
  fromYOB: typeof fromYOB;
@@ -75,10 +81,16 @@ export interface AverageAge {
75
81
  export declare function isAveraged(a: Age | AverageAge): a is AverageAge;
76
82
  export declare function averageAge(ages: readonly Age[]): AverageAge;
77
83
  export declare function birthYearsDisjoint(a: BirthYears, b: BirthYears): boolean;
84
+ export declare function intersectBirthYears(windows: readonly BirthYears[]): {
85
+ from: number | null;
86
+ to: number | null;
87
+ n: number;
88
+ } | null;
78
89
  export declare function birthYearsMeet(card: BirthYears, other: BirthYears, slack?: number): boolean;
79
90
  export declare function underCapApplies(bandFrom: number | null, year: number): boolean;
80
91
  export declare function underCappedYears(b: BirthYears, year: number, n?: number): BirthYears;
81
92
  export declare function underCapRefuses(bandFrom: number, identityFrom: number, n: number): boolean;
93
+ export declare function validYOB(y: number | null | undefined, year?: number | null): number | null;
82
94
  export declare function youngFloor(year: number, oldest: number): YoungFloorVerdict;
83
95
  export declare const tooYoungForMajor: (year: number, oldest: number) => boolean;
84
96
  export declare const tooYoungForFeis: (year: number, oldest: number) => boolean;
@@ -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;
@@ -0,0 +1,4 @@
1
+ export interface Validated<Reason extends string> {
2
+ isValid(): boolean;
3
+ invalidReason(): Reason | null;
4
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@finbheara/domain",
3
- "version": "0.2.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"],
@@ -69,6 +73,6 @@
69
73
  "prepublishOnly": "bun scripts/build.ts && bun scripts/guard-tarball.ts"
70
74
  },
71
75
  "dependencies": {
72
- "@finbheara/names": "0.12.0"
76
+ "@finbheara/names": "0.17.0"
73
77
  }
74
78
  }