@finbheara/domain 0.51.0 → 0.53.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,9 +44,9 @@ 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
- | `@finbheara/domain/dance-types` | `danceTypes()`, `DANCE_TYPES`, `rhythmOfTime`, and the shared `DanceMatch`/`DanceRelation`/`SetRhythm`/`TimeSignature` types |
49
+ | `@finbheara/domain/dance-types` | `danceTypes()`, `DANCE_TYPES`, `GRADE_DANCES` and `isGradeDance()` (the six solo dances a grade is held in, per dance), `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) |
@@ -354,6 +354,31 @@ band alone reads on the same ladder. So
354
354
  `Senior Adult` beside an and-over is its oldest youth championship, an open championship that
355
355
  qualifies for the Worlds, so `Senior Adult Ladies 19 & Over` is `o19`. A closing split
356
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
+ ```
357
382
 
358
383
  `adultBeside(band)` is that reading as one function, the band beside the adult word as one token,
359
384
  or `undefined` where the band admits youth. A caller holding a band from another field asks it: a
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,
package/dist/ceili.js CHANGED
@@ -4,8 +4,8 @@ import {
4
4
  ceiliById2,
5
5
  ceili2,
6
6
  ceiliRelation2
7
- } from "./chunks/place-gd57gavk.js";
8
- import"./chunks/place-ng98cts4.js";
7
+ } from "./chunks/place-3xxgddqw.js";
8
+ import"./chunks/place-3pgn1jgx.js";
9
9
  export {
10
10
  CEILI_NODES2 as CEILI_NODES,
11
11
  allCeilis2 as allCeilis,
@@ -99,6 +99,18 @@ var DANCE_TYPES2 = Object.freeze([
99
99
  "traditional-set",
100
100
  "set-dance"
101
101
  ]);
102
+ var GRADE_DANCES2 = Object.freeze([
103
+ "reel",
104
+ "light-jig",
105
+ "single-jig",
106
+ "slip-jig",
107
+ "treble-jig",
108
+ "hornpipe"
109
+ ]);
110
+ var GRADED = new Set(GRADE_DANCES2);
111
+ function isGradeDance2(d) {
112
+ return d != null && GRADED.has(d);
113
+ }
102
114
  function rhythmOfTime2(time) {
103
115
  return time === "2/4" || time === "4/4" ? "hornpipe" : "jig";
104
116
  }
@@ -138,4 +150,4 @@ function isDanceTypePrint2(print) {
138
150
  return danceTypes2(print).length > 0;
139
151
  }
140
152
 
141
- export { foldDancePrint, splitTempo, matchNodes, candidateRelation, DANCE_TYPES2, rhythmOfTime2, danceTypes2, danceTypePrints2, isDanceTypePrint2 };
153
+ export { foldDancePrint, splitTempo, matchNodes, candidateRelation, DANCE_TYPES2, GRADE_DANCES2, isGradeDance2, rhythmOfTime2, danceTypes2, danceTypePrints2, isDanceTypePrint2 };
@@ -3,7 +3,7 @@ import {
3
3
  matchNodes,
4
4
  candidateRelation,
5
5
  isDanceTypePrint2
6
- } from "./place-ng98cts4.js";
6
+ } from "./place-3pgn1jgx.js";
7
7
  import {
8
8
  validity
9
9
  } from "./place-q9kwsmzd.js";
@@ -5,7 +5,7 @@ import {
5
5
  candidateRelation,
6
6
  rhythmOfTime2,
7
7
  isDanceTypePrint2
8
- } from "./place-ng98cts4.js";
8
+ } from "./place-3pgn1jgx.js";
9
9
  import {
10
10
  validity
11
11
  } from "./place-q9kwsmzd.js";
@@ -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 CHANGED
@@ -1,10 +1,10 @@
1
1
  import {
2
2
  Age2
3
- } from "./chunks/place-kmh59zkw.js";
3
+ } from "./chunks/place-w6d4xfzc.js";
4
4
  import {
5
5
  populationIn2,
6
6
  compTitle2
7
- } from "./chunks/place-7jfd4m3j.js";
7
+ } from "./chunks/place-dn6qk21g.js";
8
8
  import {
9
9
  validity
10
10
  } from "./chunks/place-q9kwsmzd.js";
@@ -1,14 +1,18 @@
1
1
  import {
2
2
  DANCE_TYPES2,
3
+ GRADE_DANCES2,
4
+ isGradeDance2,
3
5
  rhythmOfTime2,
4
6
  danceTypes2,
5
7
  danceTypePrints2,
6
8
  isDanceTypePrint2
7
- } from "./chunks/place-ng98cts4.js";
9
+ } from "./chunks/place-3pgn1jgx.js";
8
10
  export {
9
11
  DANCE_TYPES2 as DANCE_TYPES,
12
+ GRADE_DANCES2 as GRADE_DANCES,
10
13
  danceTypePrints2 as danceTypePrints,
11
14
  danceTypes2 as danceTypes,
12
15
  isDanceTypePrint2 as isDanceTypePrint,
16
+ isGradeDance2 as isGradeDance,
13
17
  rhythmOfTime2 as rhythmOfTime
14
18
  };
package/dist/dance.js CHANGED
@@ -1,10 +1,10 @@
1
- import"./chunks/place-ng98cts4.js";
1
+ import"./chunks/place-3pgn1jgx.js";
2
2
  import {
3
3
  ceili2
4
- } from "./chunks/place-gd57gavk.js";
4
+ } from "./chunks/place-3xxgddqw.js";
5
5
  import {
6
6
  tune2
7
- } from "./chunks/place-tpj00cfp.js";
7
+ } from "./chunks/place-4gycgaw0.js";
8
8
  import {
9
9
  validity
10
10
  } from "./chunks/place-q9kwsmzd.js";
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,
package/dist/tune.js CHANGED
@@ -8,8 +8,8 @@ import {
8
8
  settingAt2,
9
9
  tune2,
10
10
  tuneRelation2
11
- } from "./chunks/place-tpj00cfp.js";
12
- import"./chunks/place-ng98cts4.js";
11
+ } from "./chunks/place-4gycgaw0.js";
12
+ import"./chunks/place-3pgn1jgx.js";
13
13
  export {
14
14
  PLAUSIBLE_SET_TEMPO_MAX2 as PLAUSIBLE_SET_TEMPO_MAX,
15
15
  SET_MIN_SPEED2 as SET_MIN_SPEED,
@@ -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;
@@ -1,6 +1,9 @@
1
1
  export type { DanceMatch, DanceRelation } from "./dance-match.ts";
2
2
  export type DanceType = "reel" | "light-jig" | "single-jig" | "slip-jig" | "treble-jig" | "hornpipe" | "traditional-set" | "set-dance";
3
3
  export declare const DANCE_TYPES: readonly DanceType[];
4
+ export type GradeDance = Exclude<DanceType, "traditional-set" | "set-dance">;
5
+ export declare const GRADE_DANCES: readonly GradeDance[];
6
+ export declare function isGradeDance(d: string | null | undefined): d is GradeDance;
4
7
  export type TimeSignature = "2/4" | "4/4" | "6/8" | "9/8" | "6/8+9/8";
5
8
  export type SetRhythm = "jig" | "hornpipe";
6
9
  export declare function rhythmOfTime(time: TimeSignature): SetRhythm;
@@ -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,6 +1,6 @@
1
1
  {
2
2
  "name": "@finbheara/domain",
3
- "version": "0.51.0",
3
+ "version": "0.53.0",
4
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",