@finbheara/domain 0.50.0 → 0.52.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
@@ -44,12 +44,13 @@ ESM, Node ≥ 22 or Bun ≥ 1.4.2. MIT. Not affiliated with or endorsed by any d
44
44
  | `@finbheara/domain/tune` | `tune()`, `tuneRelation`, `tempoLegal`, `settingAt`, `TUNE_NODES`: the CLRG set dance list, each tune with its time, speeds and bars |
45
45
  | `@finbheara/domain/ceili` | `ceili()`, `ceiliRelation`, `CEILI_NODES`: the céilí handbook dances |
46
46
  | `@finbheara/domain/dance` | `dance()`: a print read as a tune and a céilí dance, narrowed by its round |
47
- | `@finbheara/domain/age` | `Age.from`, `Age.fromYOB`, `Age.fromTitle`, `ageTitleToken`, `ageRelation`, `canOverlap`, `averageAge`, `isAveraged`, `intersectBirthYears`, `validYOB`, the age constants |
47
+ | `@finbheara/domain/age` | `Age.from`, `Age.fromYOB`, `Age.fromTitle`, `ageTitleToken`, `ageRelation`, `canOverlap`, `averageAge`, `isAveraged`, `intersectBirthYears`, `validYOB`, `DEFAULT_ADULT_KEYWORDS`, the age constants |
48
48
  | `@finbheara/domain/noun-index` | `createIndex`, `indexSchema`, `personEncoder`, `roleEncoder`, `roleSchoolGlob`, and the `SqlHandle`/`NounEncoder` types: a database-backed name map (below) |
49
49
  | `@finbheara/domain/dance-types` | `danceTypes()`, `DANCE_TYPES`, `rhythmOfTime`, and the shared `DanceMatch`/`DanceRelation`/`SetRhythm`/`TimeSignature` types |
50
50
  | `@finbheara/domain/promotion` | `samePromotedRow`, `samePromotedSchool`, `samePromotedHeir`, `samePromotedName`, `questionMarksRead`, `foldCharSynonyms`: is a row the same row after a promotion (below) |
51
51
  | `@finbheara/domain/person` | `seatName`, `dancerName`, `teacherName`, `roleName`, `roleSentinelOf`, `rolePrintsNoName`, `dancerPrintsNoName`, the role lists and `SHARED_ROW_SENTINELS`: a person's print read in its role (below) |
52
52
  | `@finbheara/domain/title` | `compTitle`, `titleRelation`, `matchTitle`, `ladderOf`, `populationIn`, `populationRelation`, `roundNumbersIn`, `roundFold`, `codePopulation`, `secondIn`: a competition title read and matched onto syllabus entries, a code's sex suffix, a second chance (below) |
53
+ | `@finbheara/domain/code` | `createCodeReader`, `compCode`, `DEFAULT_CODE_READER`, the `CompCode` type: a competition code read at the feis it belongs to, from that feis's syllabus or, on a platform you name as standardized, from the code alone (below) |
53
54
  | `@finbheara/domain/schedule` | `scheduleAt`, `sameHall`, `readRunningOrder`, the `ScheduleSlot` type: which entries a hall runs (below) |
54
55
 
55
56
  The library does no I/O and installs nothing process-wide. Every reader has a default and a
@@ -353,6 +354,31 @@ band alone reads on the same ladder. So
353
354
  `Senior Adult` beside an and-over is its oldest youth championship, an open championship that
354
355
  qualifies for the Worlds, so `Senior Adult Ladies 19 & Over` is `o19`. A closing split
355
356
  adult code keeps its split: `(A40U)` is `adult u40`, `(A40O)` and `(A40&O)` are `adult o40`.
357
+ An age cell that names a level beside the adult word, either side (`Advanced Adult`, `Open
358
+ Adults`, `Novice Adult`, `Adult Beginner`, `Adult Prelim`), is the adult rung, 18 and over: the
359
+ level is the competition's grade. A level word alone (`Novice`, `Open`) reads no age.
360
+
361
+ **The adult category is not an age of 18 or over.** `O18`, `18 & Over`, `Over 21` and `Senior` are
362
+ open-age competitions, danced mostly by dancers who came up through the youth bands: they read
363
+ their band, and `isAdult()` is false. `Adult`, `Novice Adult`, `Adult Beginner`, `Adult Masters` and
364
+ `Masters` name the adult category, a division with entry rules of its own (no youth competition
365
+ for some years, or never danced as a youth): they read `[18, ∞)` unless a band beside them splits
366
+ it, and `isAdult()` is true. Ask `isAdult()` for the category and `interval()` for the age.
367
+
368
+ Which words name the category is set by whoever runs the competition, and it differs by place and
369
+ year. Pass the list you hold for the competition as `adultKeywords` (on `Age.from`, `Age.fromTitle`
370
+ and `compTitle`). `DEFAULT_ADULT_KEYWORDS` is `["Adult", "Masters"]`. A keyword matches in either
371
+ case and its plural, several in a row read as one (`Adult Masters`), and the keyword `Adult` also
372
+ brings `Ad`, a bare `Advanced` and the `(A)` code. `[]` says no word names the category there.
373
+
374
+ ```ts
375
+ import { Age, DEFAULT_ADULT_KEYWORDS } from "@finbheara/domain/age";
376
+
377
+ Age.from("O18", { year: 2026 }).isAdult(); // false: an age
378
+ Age.from("Adult Masters", { year: 2026 }).isAdult(); // true: the category
379
+ Age.from("Veterans", { year: 2026, adultKeywords: ["Veteran"] }).isAdult(); // true, by your list
380
+ Age.fromTitle("Adult Ladies Over 21", { year: 2011, adultKeywords: [] }).token; // "o21"
381
+ ```
356
382
 
357
383
  `adultBeside(band)` is that reading as one function, the band beside the adult word as one token,
358
384
  or `undefined` where the band admits youth. A caller holding a band from another field asks it: a
@@ -454,6 +480,50 @@ secondIn("Preliminary Champion, Supplementary Prelim (U15)"); // true
454
480
  secondIn("Preliminary Champion, Hard Shoe (U10)"); // false
455
481
  ```
456
482
 
483
+ ## Read a competition code
484
+
485
+ A code is a feis's own number for a competition. Some platforms number every feis by one
486
+ grammar: a level digit (`1` first feis to `7` open championship), two digits for the age column
487
+ (fifty more for a boys' column, `99` for adults) and letters for the dance, so `512SJ` is a
488
+ prizewinner slip jig under 12 wherever it is printed. Most platforms let each feis number as it
489
+ likes, and the same `344` is a novice under 11 at one feis and a beginner reel at the next. Read
490
+ with the grammar, such a code gives an age and a level that look right and are wrong.
491
+
492
+ So a code is read only at a feis. `read` takes the platform and the feis's address or slug, and
493
+ answers from the feis's syllabus where you hand it in, on any platform. With no syllabus, only a
494
+ platform you name as standardized reads its code alone. Everything else is refused, with its
495
+ reason: `no-feis`, `feis-specific`, `not-in-syllabus`, `two-entries` or `not-a-code`. An empty
496
+ `syllabus: []` is no syllabus, never a syllabus that prints no code. The default
497
+ reader names no platform, so with no configuration a code reads only from a syllabus. A title is
498
+ not a code: `compTitle` still reads it at any feis.
499
+
500
+ ```ts
501
+ import { createCodeReader } from "@finbheara/domain/code";
502
+
503
+ const codes = createCodeReader({ standardized: ["examplefeis"] });
504
+
505
+ const c = codes.read("512SJ", { platform: "examplefeis", feis: "ex:1001" });
506
+ c.age.ag1; // "u12"
507
+ c.level; // 500
508
+ c.source; // "code"
509
+
510
+ codes.read("344", { platform: "other", feis: "x:ABC26" }).invalidReason(); // "feis-specific"
511
+ codes.read("512SJ", { platform: "examplefeis", feis: null }).invalidReason(); // "no-feis"
512
+
513
+ const syllabus = [{ code: "344", title: "Novice Girls Under 11 Light Jig" }];
514
+ const s = codes.read("344", { platform: "other", feis: "x:ABC26", syllabus });
515
+ s.age.ag1; // "u11"
516
+ s.entry; // the entry: read its level yourself
517
+ ```
518
+
519
+ The level comes from the grammar's level digit only. A set, a treble reel, a figure and a future
520
+ champions competition run off the grade ladder and read no level, and a syllabus answer reads none,
521
+ on a standardized platform too (the syllabus wins, and the package reads no level word): the
522
+ matching `entry` comes back for you to read. The oldest
523
+ column a feis offers in one series is its and-over (`719OC` is 18 and over where it is the top
524
+ open column). Hand in every code the feis printed (`codes`) to read the top column and a level
525
+ split by sex. Without them a column reads as its `Under` band and who dances is not said.
526
+
457
527
  ## Ask a schedule
458
528
 
459
529
  A split band's competitions share their card numbers, so a card heard on a stage fits each of
package/dist/age.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import {
2
+ DEFAULT_ADULT_KEYWORDS2,
2
3
  printsUnderOpen2,
3
4
  printsAdultCode2,
4
5
  TITLE_RANGE_MIN2,
@@ -39,13 +40,14 @@ import {
39
40
  youngFloor2,
40
41
  tooYoungForMajor2,
41
42
  tooYoungForFeis2
42
- } from "./chunks/place-kmh59zkw.js";
43
+ } from "./chunks/place-w6d4xfzc.js";
43
44
  export {
44
45
  ADULT_FLOOR2 as ADULT_FLOOR,
45
46
  AVERAGE_AGE_DECIMALS2 as AVERAGE_AGE_DECIMALS,
46
47
  Age2 as Age,
47
48
  AgeMisuseError2 as AgeMisuseError,
48
49
  DANCE_UP_YEARS2 as DANCE_UP_YEARS,
50
+ DEFAULT_ADULT_KEYWORDS2 as DEFAULT_ADULT_KEYWORDS,
49
51
  MAJORS_FLOOR_YEARS2 as MAJORS_FLOOR_YEARS,
50
52
  MIN_FEIS_AGE2 as MIN_FEIS_AGE,
51
53
  MIN_MAJOR_AGE2 as MIN_MAJOR_AGE,
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  Age2,
3
3
  ageRelation2
4
- } from "./place-kmh59zkw.js";
4
+ } from "./place-w6d4xfzc.js";
5
5
  import {
6
6
  validity
7
7
  } from "./place-q9kwsmzd.js";
@@ -79,7 +79,11 @@ function roundFold2(print) {
79
79
  var SPLIT = / ([a-d]) $|(?:\d|girls?|boys?|lad(?:y|ies)|m[ae]n|mixed) ([a-d])(?= )/;
80
80
  var spoken = (print) => print.replace(/\b(under|over|u|o)-(?=[ \t]*\d)/giu, "$1 ");
81
81
  function read(print, ctx, agePrint) {
82
- const actx = { year: ctx.year ?? null, ...ctx.ladder ? { ladder: ctx.ladder } : {} };
82
+ const actx = {
83
+ year: ctx.year ?? null,
84
+ ...ctx.ladder ? { ladder: ctx.ladder } : {},
85
+ ...ctx.adultKeywords ? { adultKeywords: ctx.adultKeywords } : {}
86
+ };
83
87
  let age;
84
88
  let ageSpan = null;
85
89
  if (agePrint !== undefined && agePrint.trim() !== "") {
@@ -130,7 +134,7 @@ function ladderOf2(bands) {
130
134
  const bare = Age2.from(b.ag0, { year: null });
131
135
  if (!bare.isValid())
132
136
  continue;
133
- if (bare.isAdult())
137
+ if (b.isAdult())
134
138
  adult = true;
135
139
  const [p, q] = bare.parts;
136
140
  if (bare.kind === "under" && p !== undefined)
@@ -2,6 +2,29 @@ import {
2
2
  validity
3
3
  } from "./place-q9kwsmzd.js";
4
4
 
5
+ var DEFAULT_ADULT_KEYWORDS2 = Object.freeze(["Adult", "Masters"]);
6
+ var ADULT_STEM = "adult";
7
+ var escape = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
8
+ function stem(keyword) {
9
+ const k = keyword.trim().toLowerCase().replace(/\s+/g, " ");
10
+ return k.length > 2 && k.endsWith("s") ? k.slice(0, -1) : k;
11
+ }
12
+ var cache = new Map;
13
+ function adultPatterns(keywords) {
14
+ const stems = [...new Set((keywords ?? DEFAULT_ADULT_KEYWORDS2).map(stem))].filter((s) => s !== "").sort((a, b) => b.length - a.length || a.localeCompare(b));
15
+ const key = stems.join("\x00");
16
+ const hit = cache.get(key);
17
+ if (hit !== undefined)
18
+ return hit;
19
+ const adult = stems.includes(ADULT_STEM);
20
+ const words = stems.map((s) => s === ADULT_STEM ? "adults?|ad" : `${escape(s).replace(/ /g, "[\\s-]+")}s?`);
21
+ const one = words.length === 0 ? null : words.join("|");
22
+ const run = one === null ? null : `(?:${one})(?:[\\s-]+(?:${one}))*`;
23
+ const out = Object.freeze({ adult, one, run });
24
+ cache.set(key, out);
25
+ return out;
26
+ }
27
+
5
28
  var LEAD_ID = /^\s*(?:comp(?:etition)?\.?\s*)?\d+[a-z]*(?:[/-]\d*[a-z]*)*\s*[-:.]?\s*/i;
6
29
  var PAREN = /\(([^)]*)\)/g;
7
30
  var AND_OVER = /(?<![0-9])(?<![0-9][a-z]*)(?<!(?:^|[^a-z0-9])[a-z])(\d{1,2})\s*(?:years?\s*)?(?:&|\+|\band\b)\s*(?:over|older|up|o)(?![a-z])/i;
@@ -27,7 +50,21 @@ var RANGE = new RegExp(String.raw`(?<![0-9])(?<![0-9][a-z]*)(?<!(?:^|[^a-z0-9])[
27
50
  var LEAD_RANGE = new RegExp(String.raw`^\s*([1-9]\d?)\s*${RANGE_DASH}\s*([1-9]\d?)(?![a-z0-9])(?!\s*[/&])`, "i");
28
51
  var ADULT_CODE = /^\s*a(?:(\d{1,2})(u|o|&o))?\s*$/i;
29
52
  var printsAdultCode2 = (phrase) => ADULT_CODE.test(phrase);
30
- var ADULT = /(?<!non[\s-])(?<!-)\b(?:adults?|advanced\s+adults?|masters?)\b/i;
53
+ var adultTitleCache = new Map;
54
+ function adultTitleWords(keywords) {
55
+ const p = adultPatterns(keywords);
56
+ const key = `${p.adult}\x00${p.one ?? ""}`;
57
+ const hit = adultTitleCache.get(key);
58
+ if (hit !== undefined)
59
+ return hit;
60
+ const one = p.one === null ? null : p.adult ? `advanced\\s+adults?|${p.one}` : p.one;
61
+ const out = Object.freeze({
62
+ word: one === null ? null : new RegExp(`(?<!non[\\s-])(?<!-)\\b(?:${one})\\b`, "i"),
63
+ code: p.adult
64
+ });
65
+ adultTitleCache.set(key, out);
66
+ return out;
67
+ }
31
68
  var SENIOR = /(?<!-)\bseniors?\b/i;
32
69
  var TITLE_RANGE_MIN2 = 3;
33
70
  var TITLE_RANGE_MAX2 = 40;
@@ -59,7 +96,7 @@ function readJoined(title, s, offset) {
59
96
  return NONE;
60
97
  return at(title, offset, j.index, end, `u${Math.max(...caps)}`);
61
98
  }
62
- function readAnchored(title, s, offset, aside = false) {
99
+ function readAnchored(title, s, offset, adultWord, aside = false) {
63
100
  const joined = readJoined(title, s, offset);
64
101
  if (joined !== null)
65
102
  return joined;
@@ -106,7 +143,7 @@ function readAnchored(title, s, offset, aside = false) {
106
143
  const p = OVER_PLUS.exec(s);
107
144
  if (p?.[1] !== undefined)
108
145
  return at(title, offset, p.index, p.index + p[0].length, `o${Number(p[1])}`);
109
- const a = ADULT.exec(s);
146
+ const a = adultWord?.exec(s) ?? null;
110
147
  if (a !== null)
111
148
  return at(title, offset, a.index, a.index + a[0].length, "adult");
112
149
  const n = SENIOR.exec(s);
@@ -114,7 +151,8 @@ function readAnchored(title, s, offset, aside = false) {
114
151
  return at(title, offset, n.index, n.index + n[0].length, "senior");
115
152
  return NONE;
116
153
  }
117
- function ageTitleToken2(title) {
154
+ function ageTitleToken2(title, options = {}) {
155
+ const aw = adultTitleWords(options.adultKeywords);
118
156
  const s = title.trim();
119
157
  if (s === "")
120
158
  return NONE;
@@ -127,7 +165,7 @@ function ageTitleToken2(title) {
127
165
  const from = lead + (m.index ?? 0) + 1;
128
166
  const i = m.index ?? 0;
129
167
  const tail = i + m[0].length === s.length && !/[a-z0-9]/i.test(s[i - 1] ?? "");
130
- const code = tail ? ADULT_CODE.exec(inner) : null;
168
+ const code = tail && aw.code ? ADULT_CODE.exec(inner) : null;
131
169
  if (code !== null) {
132
170
  const split = code[1] === undefined ? "" : `${/u/i.test(code[2] ?? "") ? "u" : "o"}${code[1]}`;
133
171
  const a = inner.length - inner.trimStart().length;
@@ -136,19 +174,19 @@ function ageTitleToken2(title) {
136
174
  }
137
175
  if (adultWord !== null)
138
176
  continue;
139
- const t = readAnchored(title, inner, from, true);
177
+ const t = readAnchored(title, inner, from, aw.word, true);
140
178
  if (t.token === "")
141
179
  continue;
142
180
  if (t.token === "adult") {
143
181
  adultWord = t;
144
182
  continue;
145
183
  }
146
- return adultOverUnder(title, s, lead, t);
184
+ return adultOverUnder(title, s, lead, t, aw.word);
147
185
  }
148
186
  if (adultWord !== null)
149
187
  return adultAside ?? adultWord;
150
- const words = readAnchored(title, s, lead);
151
- return adultAside !== null && (words.token === "" || words.token === "adult") ? adultAside : adultOverUnder(title, s, lead, words);
188
+ const words = readAnchored(title, s, lead, aw.word);
189
+ return adultAside !== null && (words.token === "" || words.token === "adult") ? adultAside : adultOverUnder(title, s, lead, words, aw.word);
152
190
  }
153
191
  var ADULT_UNDER_MIN = 18;
154
192
  var SENIOR_ADULT_OVER = /\bsenior\s+adult\b/i;
@@ -172,10 +210,10 @@ function adultBeside2(band) {
172
210
  return band;
173
211
  return Number(floor) < ADULT_UNDER_MIN ? undefined : `adult ${b}`;
174
212
  }
175
- function adultOverUnder(title, s, lead, t) {
213
+ function adultOverUnder(title, s, lead, t, adultWord) {
176
214
  if (t.token === "")
177
215
  return t;
178
- const a = ADULT.exec(s);
216
+ const a = adultWord?.exec(s) ?? null;
179
217
  if (a === null)
180
218
  return t;
181
219
  if (/^o\d+$/.test(t.token) && SENIOR_ADULT_OVER.test(s))
@@ -206,9 +244,31 @@ var UNDER2 = /^(?:u|under\s*)(\d+)(?:\/([1-9]\d*)yr)?$/i;
206
244
  var OVER = /^(?:o|over\s*)(\d+)$/i;
207
245
  var AND_OVER2 = /^(\d+)\s*(?:&|and\b)\s*(?:over|o)$/i;
208
246
  var RANGE2 = /^u?(\d+)\s*[-–]\s*(\d+)$/i;
209
- var ADULT_WORD = /^(?:adults?|ad|advanced|masters?)$/i;
210
- var ADULT_UNDER = /^(?:adults?|ad)[\s-]*(?:u|under\s*)(\d+)(?:\/([1-9]\d*)yr)?$/i;
211
- var ADULT_BAND = /^(?:adults?|ad)[\s-]+(\S.*)$/i;
247
+ var LEVEL_WORDS = "first[\\s-]*feis|(?:advanced[\\s-]+)?beginners?(?:[\\s-]*(?:1|2|i|ii))?|novice|prizewinners?|intermediate|advanced|open|prelim(?:inary)?|champions?(?:hip)?s?|champs?";
248
+ var NO_ADULT_WORDS = Object.freeze({
249
+ word: null,
250
+ level: null,
251
+ under: null,
252
+ band: null
253
+ });
254
+ var adultWordsCache = new Map;
255
+ function adultWordsOf(keywords) {
256
+ const p = adultPatterns(keywords);
257
+ if (p.run === null)
258
+ return NO_ADULT_WORDS;
259
+ const hit = adultWordsCache.get(p.run);
260
+ if (hit !== undefined)
261
+ return hit;
262
+ const run = p.run;
263
+ const out = Object.freeze({
264
+ word: new RegExp(`^(?:${run}${p.adult ? "|advanced" : ""})$`, "i"),
265
+ level: new RegExp(`^(?:(?:${LEVEL_WORDS})[\\s-]+(?:${run})|(?:${run})[\\s-]+(?:${LEVEL_WORDS}))$`, "i"),
266
+ under: new RegExp(`^(?:${run})[\\s-]*(?:u|under\\s*)(\\d+)(?:\\/([1-9]\\d*)yr)?$`, "i"),
267
+ band: new RegExp(`^(?:${run})[\\s-]+(\\S.*)$`, "i")
268
+ });
269
+ adultWordsCache.set(p.run, out);
270
+ return out;
271
+ }
212
272
  var SENIOR_WORD = /^seniors?$/i;
213
273
  var NO_LADDER = Object.freeze({ caps: [], overs: [], ranges: [] });
214
274
  function maxWhere(xs, pred) {
@@ -270,11 +330,11 @@ function strideOf(k) {
270
330
  const n = Number(k);
271
331
  return Number.isInteger(n) && n >= 1 ? n : null;
272
332
  }
273
- function read(token, ladder, top) {
333
+ function read(token, ladder, top, aw) {
274
334
  const r = RANGE2.exec(token);
275
335
  if (r !== null)
276
336
  return readRange(Number(r[1]), Number(r[2]), ladder);
277
- const au = ADULT_UNDER.exec(token);
337
+ const au = aw.under?.exec(token) ?? null;
278
338
  if (au !== null && Number(au[1]) > ADULT_FLOOR2) {
279
339
  const k = strideOf(au[2]);
280
340
  const hi = Number(au[1]);
@@ -286,13 +346,13 @@ function read(token, ladder, top) {
286
346
  parts: k === null ? [hi] : [hi, k]
287
347
  };
288
348
  }
289
- const ab = au === null ? ADULT_BAND.exec(token) : null;
349
+ const ab = au === null ? aw.band?.exec(token) ?? null : null;
290
350
  if (ab !== null) {
291
- const inner = read(ab[1].trim(), { ...ladder, adult: true }, top);
351
+ const inner = read(ab[1].trim(), { ...ladder, adult: true }, top, aw);
292
352
  if (inner.kind === "over" || inner.kind === "range")
293
353
  return { ...inner, clauses: [...inner.clauses, "adult-band"], adult: true };
294
354
  }
295
- if (au !== null || ADULT_WORD.test(token))
355
+ if (au !== null || aw.word?.test(token) === true || aw.level?.test(token) === true)
296
356
  return {
297
357
  kind: "adult",
298
358
  interval: { lo: ADULT_FLOOR2, hi: Number.POSITIVE_INFINITY },
@@ -394,7 +454,7 @@ function fromPrint(print, context = {}) {
394
454
  const ladder = context.ladder ?? NO_LADDER;
395
455
  const year = context.year === undefined ? currentYear() : context.year;
396
456
  const ag1 = print.trim().toLowerCase();
397
- const reading = ag1 === "" ? { kind: null, interval: null, refusal: null, clauses: [], parts: [] } : refuseAboveOldest(refuseBelowYoungest(read(print.trim(), ladder, context.top === true)));
457
+ const reading = ag1 === "" ? { kind: null, interval: null, refusal: null, clauses: [], parts: [] } : refuseAboveOldest(refuseBelowYoungest(read(print.trim(), ladder, context.top === true, adultWordsOf(context.adultKeywords))));
398
458
  const iv = reading.interval;
399
459
  const arm = ag1 === "" ? "empty" : iv === null ? "open" : "band";
400
460
  const ag2 = iv !== null ? display(iv) : ag1 === "" ? "" : `${ag1}?`;
@@ -475,9 +535,11 @@ function fromYOB(from, to = from, context = {}) {
475
535
  });
476
536
  }
477
537
  function fromTitle(title, context = {}) {
478
- const t = ageTitleToken2(title);
479
- return Object.freeze({ age: fromPrint(t.token, context), token: t.token, span: t.span });
538
+ const t = ageTitleToken2(title, { adultKeywords: context.adultKeywords });
539
+ const age = fromPrint(t.token, { ...context, adultKeywords: TOKEN_ADULT_KEYWORDS });
540
+ return Object.freeze({ age, token: t.token, span: t.span });
480
541
  }
542
+ var TOKEN_ADULT_KEYWORDS = Object.freeze(["adult"]);
481
543
  var Age2 = Object.freeze({ from: fromPrint, fromYOB, fromTitle });
482
544
  function sourceOf(r) {
483
545
  const iv = r.interval;
@@ -669,4 +731,4 @@ function youngFloor2(year, oldest) {
669
731
  var tooYoungForMajor2 = (year, oldest) => yearsSinceBirth2(year, oldest) < MIN_MAJOR_AGE2;
670
732
  var tooYoungForFeis2 = (year, oldest) => yearsSinceBirth2(year, oldest) < MIN_FEIS_AGE2;
671
733
 
672
- export { printsUnderOpen2, printsAdultCode2, TITLE_RANGE_MIN2, TITLE_RANGE_MAX2, ageTitleToken2, readsSeniorAdult2, adultBeside2, YOUNGEST_FLOOR2, ADULT_FLOOR2, MAJORS_FLOOR_YEARS2, UNDER_CAP2, UNDER_CAP_MAX_TOP2, YOUNG_FLOOR2, MIN_MAJOR_AGE2, MIN_FEIS_AGE2, YOUNGEST_BAND_TOP2, OLDEST_BAND_FLOOR2, YOB_RANGE2, DANCE_UP_YEARS2, ageOnJanuary12, yearsSinceBirth2, AgeMisuseError2, Age2, birthYearsOfInterval2, ageRelation2, canOverlap2, AVERAGE_AGE_DECIMALS2, roundedMeanHundredths2, isAveraged2, averageAge2, birthYearsDisjoint2, intersectBirthYears2, birthYearsMeet2, underCapApplies2, underCappedYears2, underCapRefuses2, validYOB2, youngFloor2, tooYoungForMajor2, tooYoungForFeis2 };
734
+ export { DEFAULT_ADULT_KEYWORDS2, printsUnderOpen2, printsAdultCode2, TITLE_RANGE_MIN2, TITLE_RANGE_MAX2, ageTitleToken2, readsSeniorAdult2, adultBeside2, YOUNGEST_FLOOR2, ADULT_FLOOR2, MAJORS_FLOOR_YEARS2, UNDER_CAP2, UNDER_CAP_MAX_TOP2, YOUNG_FLOOR2, MIN_MAJOR_AGE2, MIN_FEIS_AGE2, YOUNGEST_BAND_TOP2, OLDEST_BAND_FLOOR2, YOB_RANGE2, DANCE_UP_YEARS2, ageOnJanuary12, yearsSinceBirth2, AgeMisuseError2, Age2, birthYearsOfInterval2, ageRelation2, canOverlap2, AVERAGE_AGE_DECIMALS2, roundedMeanHundredths2, isAveraged2, averageAge2, birthYearsDisjoint2, intersectBirthYears2, birthYearsMeet2, underCapApplies2, underCappedYears2, underCapRefuses2, validYOB2, youngFloor2, tooYoungForMajor2, tooYoungForFeis2 };
package/dist/code.js ADDED
@@ -0,0 +1,136 @@
1
+ import {
2
+ Age2
3
+ } from "./chunks/place-w6d4xfzc.js";
4
+ import {
5
+ populationIn2,
6
+ compTitle2
7
+ } from "./chunks/place-dn6qk21g.js";
8
+ import {
9
+ validity
10
+ } from "./chunks/place-q9kwsmzd.js";
11
+
12
+ var TRAILING = /(?:-[A-Za-z0-9]+|\[[A-Za-z0-9]+\])$/;
13
+ var GROUP = /^([1-7])(\d{2})([A-Za-z]+?)2?$/;
14
+ var OFF_LADDER = new Set(["TR", "TS", "SD", "FD", "FC"]);
15
+ function group(print) {
16
+ let s = print.trim();
17
+ let male = false;
18
+ let female = false;
19
+ for (let m = TRAILING.exec(s);m !== null; m = TRAILING.exec(s)) {
20
+ const t = m[0].toUpperCase();
21
+ if (t === "-M" || t === "[M]")
22
+ male = true;
23
+ if (t === "-F" || t === "[F]")
24
+ female = true;
25
+ s = s.slice(0, m.index);
26
+ }
27
+ const g = GROUP.exec(s);
28
+ if (g === null)
29
+ return null;
30
+ const mm = Number(g[2]);
31
+ const plus50 = mm >= 50 && mm <= 89;
32
+ return {
33
+ digit: g[1],
34
+ letters: g[3].toUpperCase(),
35
+ column: mm === 99 ? null : plus50 ? mm - 50 : mm,
36
+ boys: plus50 || male,
37
+ girls: female
38
+ };
39
+ }
40
+ function groups(print) {
41
+ const out = [];
42
+ for (const part of print.split("/")) {
43
+ const g = group(part);
44
+ if (g === null)
45
+ return null;
46
+ out.push(g);
47
+ }
48
+ return out.length === 0 ? null : out;
49
+ }
50
+ function decode(print, year, codes) {
51
+ const gs = groups(print);
52
+ if (gs === null)
53
+ return null;
54
+ const first = gs[0];
55
+ if (gs.some((g) => g.digit !== first.digit))
56
+ return null;
57
+ const adult = gs.filter((g) => g.column === null).length;
58
+ if (adult > 0 && adult < gs.length)
59
+ return null;
60
+ const level = OFF_LADDER.has(first.letters) ? null : Number(first.digit) * 100;
61
+ const others = codes === null ? [] : codes.flatMap((c) => groups(c) ?? []);
62
+ const boys = gs.every((g) => g.boys);
63
+ const split = others.some((g) => g.digit === first.digit && g.boys);
64
+ const population = gs.some((g) => g.boys) && !boys ? "mixed" : boys ? "boys" : codes === null ? "" : split || first.girls && others.some((g) => g.boys) ? "girls" : "mixed";
65
+ if (adult > 0)
66
+ return { age: Age2.from("Adult", { year }), level, population };
67
+ const cols = gs.map((g) => g.column);
68
+ const lo = Math.min(...cols);
69
+ const hi = Math.max(...cols);
70
+ const series = others.filter((g) => g.digit === first.digit && g.letters === first.letters && g.boys === boys);
71
+ const offered = series.map((g) => g.column).filter((c) => c !== null);
72
+ const top = offered.length > 1 && hi >= Math.max(...offered);
73
+ const band = top ? `O${lo - 1}` : lo === hi ? `U${hi}` : `U${hi}/${hi - lo + 1}yr`;
74
+ return { age: Age2.from(band, { year }), level, population };
75
+ }
76
+ var foldCode = (code) => code.trim().toUpperCase();
77
+ var NO_AGE = Age2.from("", { year: null });
78
+ function createCodeReader(options = {}) {
79
+ const standardized = new Set([...options.standardized ?? []].map((p) => p.trim().toLowerCase()));
80
+ const scheme = (platform) => standardized.has(platform.trim().toLowerCase()) ? "standardized" : "feis-specific";
81
+ function read(code, ctx) {
82
+ const s = scheme(ctx.platform);
83
+ const feis = (ctx.feis ?? "").trim() === "" ? null : ctx.feis.trim();
84
+ const year = ctx.year ?? null;
85
+ const make = (refusal, source, age, level, population, entry) => {
86
+ const v = validity(code.trim() === "", refusal);
87
+ return Object.freeze({
88
+ [Symbol.toStringTag]: "CompCode",
89
+ ...v,
90
+ print: code,
91
+ platform: ctx.platform,
92
+ feis,
93
+ scheme: s,
94
+ source: v.isValid() ? source : null,
95
+ age: v.isValid() ? age : NO_AGE,
96
+ level: v.isValid() ? level : null,
97
+ population: v.isValid() ? population : "",
98
+ entry: v.isValid() ? entry : null,
99
+ isComp: () => v.isValid() && age.isAge()
100
+ });
101
+ };
102
+ const refuse = (r) => make(r, null, NO_AGE, null, "", null);
103
+ if (code.trim() === "")
104
+ return refuse("not-a-code");
105
+ if (feis === null)
106
+ return refuse("no-feis");
107
+ const codes = ctx.codes === undefined ? null : [...ctx.codes];
108
+ if (ctx.syllabus !== undefined && ctx.syllabus.length > 0) {
109
+ const hits = ctx.syllabus.filter((e) => foldCode(e.code) === foldCode(code));
110
+ if (hits.length === 0)
111
+ return refuse("not-in-syllabus");
112
+ if (hits.length > 1)
113
+ return refuse("two-entries");
114
+ const e = hits[0];
115
+ const printed = e.age !== undefined && e.age.trim() !== "" ? Age2.from(e.age, { year }) : null;
116
+ const age = printed?.isAge() === true ? printed : compTitle2(e.title, { year }).age;
117
+ return make(null, "syllabus", age, null, populationIn2(e.title), e);
118
+ }
119
+ if (s === "feis-specific")
120
+ return refuse("feis-specific");
121
+ const decoded = decode(code, year, codes);
122
+ if (decoded === null || !decoded.age.isAge())
123
+ return refuse("not-a-code");
124
+ return make(null, "code", decoded.age, decoded.level, decoded.population, null);
125
+ }
126
+ return Object.freeze({ scheme, read });
127
+ }
128
+ var DEFAULT_CODE_READER = createCodeReader();
129
+ function compCode(code, ctx) {
130
+ return DEFAULT_CODE_READER.read(code, ctx);
131
+ }
132
+ export {
133
+ DEFAULT_CODE_READER,
134
+ compCode,
135
+ createCodeReader
136
+ };
package/dist/schedule.js CHANGED
@@ -1,8 +1,8 @@
1
- import"./chunks/place-kmh59zkw.js";
1
+ import"./chunks/place-w6d4xfzc.js";
2
2
  import {
3
3
  compTitle2,
4
4
  matchTitle2
5
- } from "./chunks/place-7jfd4m3j.js";
5
+ } from "./chunks/place-dn6qk21g.js";
6
6
 
7
7
  function hallKey(hall) {
8
8
  return hall.normalize("NFKD").replace(/\p{M}/gu, "").toLowerCase().replace(/[^a-z0-9]+/g, " ").trim();
package/dist/title.js CHANGED
@@ -9,8 +9,8 @@ import {
9
9
  titleRelation2,
10
10
  ladderOf2,
11
11
  matchTitle2
12
- } from "./chunks/place-7jfd4m3j.js";
13
- import"./chunks/place-kmh59zkw.js";
12
+ } from "./chunks/place-dn6qk21g.js";
13
+ import"./chunks/place-w6d4xfzc.js";
14
14
  export {
15
15
  codePopulation2 as codePopulation,
16
16
  compTitle2 as compTitle,
@@ -0,0 +1,7 @@
1
+ export declare const DEFAULT_ADULT_KEYWORDS: readonly string[];
2
+ export interface AdultPatterns {
3
+ readonly adult: boolean;
4
+ readonly one: string | null;
5
+ readonly run: string | null;
6
+ }
7
+ export declare function adultPatterns(keywords: readonly string[] | undefined): AdultPatterns;
@@ -1,5 +1,8 @@
1
1
  export declare const printsUnderOpen: (print: string) => boolean;
2
2
  export declare const printsAdultCode: (phrase: string) => boolean;
3
+ export interface AgeTitleOptions {
4
+ readonly adultKeywords?: readonly string[];
5
+ }
3
6
  export declare const TITLE_RANGE_MIN = 3;
4
7
  export declare const TITLE_RANGE_MAX = 40;
5
8
  export interface AgeTitleSpan {
@@ -11,6 +14,6 @@ export interface AgeTitleToken {
11
14
  readonly token: string;
12
15
  readonly span: AgeTitleSpan | null;
13
16
  }
14
- export declare function ageTitleToken(title: string): AgeTitleToken;
17
+ export declare function ageTitleToken(title: string, options?: AgeTitleOptions): AgeTitleToken;
15
18
  export declare function readsSeniorAdult(title: string, span: AgeTitleSpan): boolean;
16
19
  export declare function adultBeside(band: string): string | undefined;
@@ -20,6 +20,7 @@ export interface AgeContext {
20
20
  readonly ladder?: AgeLadder;
21
21
  readonly year?: number | null;
22
22
  readonly top?: boolean;
23
+ readonly adultKeywords?: readonly string[];
23
24
  }
24
25
  export type AgeArm = "band" | "yob" | "open" | "empty";
25
26
  export type AgeKind = "under" | "over" | "range" | "adult" | "senior" | "yob";
@@ -1,7 +1,8 @@
1
1
  import { type AgeTitleSpan } from "./age-title.ts";
2
2
  import type { AgeArm, AgeClause, AgeComparison, AgeContext, AgeInterval, AgeKind, AgeLadder, AgeMisuse, AgeRefusal, AgeRelation, AgeRelationReason, AgeSource, AgeSpan, BirthYears, YOBContext, YoungFloorVerdict } from "./age-types.ts";
3
3
  import { type EmptyReason, type Validated } from "./validated.ts";
4
- export { type AgeTitleSpan, type AgeTitleToken, TITLE_RANGE_MAX, TITLE_RANGE_MIN, adultBeside, ageTitleToken, printsAdultCode, printsUnderOpen, readsSeniorAdult, } from "./age-title.ts";
4
+ export { DEFAULT_ADULT_KEYWORDS } from "./adult-keywords.ts";
5
+ export { type AgeTitleOptions, type AgeTitleSpan, type AgeTitleToken, TITLE_RANGE_MAX, TITLE_RANGE_MIN, adultBeside, ageTitleToken, printsAdultCode, printsUnderOpen, readsSeniorAdult, } from "./age-title.ts";
5
6
  export type { AgeArm, AgeClause, AgeComparison, AgeContext, AgeInterval, AgeKind, AgeLadder, AgeMisuse, AgeRefusal, AgeRelation, AgeRelationReason, AgeSource, AgeSpan, BirthYears, EmptyReason, Validated, YOBContext, YoungFloorVerdict, };
6
7
  export declare const YOUNGEST_FLOOR = 0;
7
8
  export declare const ADULT_FLOOR = 18;
@@ -0,0 +1,41 @@
1
+ import { Age } from "./age.ts";
2
+ import { type Population } from "./title.ts";
3
+ import { type Validated } from "./validated.ts";
4
+ export type { EmptyReason, Validated } from "./validated.ts";
5
+ export type CodeScheme = "standardized" | "feis-specific";
6
+ export type CodeRefusal = "no-feis" | "feis-specific" | "not-in-syllabus" | "two-entries" | "not-a-code";
7
+ export interface CodeEntry {
8
+ readonly code: string;
9
+ readonly title: string;
10
+ readonly age?: string;
11
+ }
12
+ export interface CodeContext<E extends CodeEntry = CodeEntry> {
13
+ readonly platform: string;
14
+ readonly feis: string | null | undefined;
15
+ readonly year?: number | null;
16
+ readonly syllabus?: readonly E[];
17
+ readonly codes?: Iterable<string>;
18
+ }
19
+ export interface CompCode<E extends CodeEntry = CodeEntry> extends Validated<CodeRefusal> {
20
+ readonly [Symbol.toStringTag]: "CompCode";
21
+ readonly print: string;
22
+ readonly platform: string;
23
+ readonly feis: string | null;
24
+ readonly scheme: CodeScheme;
25
+ readonly source: "syllabus" | "code" | null;
26
+ readonly age: Age;
27
+ readonly level: number | null;
28
+ readonly population: Population | "";
29
+ readonly entry: E | null;
30
+ isComp(): boolean;
31
+ }
32
+ export interface CodeReaderOptions {
33
+ readonly standardized?: Iterable<string>;
34
+ }
35
+ export interface CodeReader {
36
+ scheme(platform: string): CodeScheme;
37
+ read<E extends CodeEntry>(code: string, ctx: CodeContext<E>): CompCode<E>;
38
+ }
39
+ export declare function createCodeReader(options?: CodeReaderOptions): CodeReader;
40
+ export declare const DEFAULT_CODE_READER: CodeReader;
41
+ export declare function compCode<E extends CodeEntry>(code: string, ctx: CodeContext<E>): CompCode<E>;
@@ -12,6 +12,7 @@ export declare function roundFold(print: string): string;
12
12
  export interface TitleContext {
13
13
  readonly year?: number | null;
14
14
  readonly ladder?: AgeLadder;
15
+ readonly adultKeywords?: readonly string[];
15
16
  }
16
17
  export type TitleRefusal = "no-age";
17
18
  export interface CompTitle extends Validated<TitleRefusal> {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@finbheara/domain",
3
- "version": "0.50.0",
4
- "description": "Places, CLRG regions, school names, ages, dances, competition titles and schedules, and a person's print in its role, as they are printed: read, compared and told apart",
3
+ "version": "0.52.0",
4
+ "description": "Places, CLRG regions, school names, ages, dances, competition titles, codes and schedules, and a person's print in its role, as they are printed: read, compared and told apart",
5
5
  "keywords": [
6
6
  "places",
7
7
  "regions",
@@ -73,6 +73,10 @@
73
73
  "types": "./dist/types/schedule.d.ts",
74
74
  "default": "./dist/schedule.js"
75
75
  },
76
+ "./code": {
77
+ "types": "./dist/types/code.d.ts",
78
+ "default": "./dist/code.js"
79
+ },
76
80
  "./package.json": "./package.json"
77
81
  },
78
82
  "files": ["dist/*.js", "dist/chunks/*.js", "dist/types/**/*.d.ts", "README.md", "LICENSE"],
@@ -89,7 +93,7 @@
89
93
  "prepublishOnly": "bun scripts/build.ts && bun scripts/guard-tarball.ts"
90
94
  },
91
95
  "dependencies": {
92
- "@finbheara/names": "0.23.0"
96
+ "@finbheara/names": "0.25.0"
93
97
  },
94
98
  "devDependencies": {
95
99
  "typescript": "7.0.2"