@finbheara/domain 0.31.0 → 0.32.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.
@@ -0,0 +1,655 @@
1
+ import {
2
+ validity
3
+ } from "./place-q9kwsmzd.js";
4
+
5
+ var LEAD_ID = /^\s*(?:comp(?:etition)?\.?\s*)?\d+[a-z]*(?:[/-]\d*[a-z]*)*\s*[-:.]?\s*/i;
6
+ var PAREN = /\(([^)]*)\)/g;
7
+ 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;
8
+ var OVER_WORD = /(?:\bover\s*|\bos\s+cionn\s*|(?<![a-z0-9])o[./]?\s*)(\d{1,2})(?![a-z0-9])/i;
9
+ var OVER_GLUED = /(?<![a-z0-9])(\d{1,2})\s*o(?![a-z'’])/i;
10
+ var OVER_PLUS = /(?<![a-z0-9])(\d{1,2})\s*\+/;
11
+ var UNDER_WORD = String.raw`(?:\bund(?:er)?\.?\s*(?:age\s*)?|\bfaoi\s*|(?<![a-z0-9])u[./]?\s*)`;
12
+ var UNDER = new RegExp(String.raw`${UNDER_WORD}(\d{1,2})(?![0-9])(?![a-z][a-z0-9])`, "i");
13
+ var JOINED_CAPS = new RegExp(String.raw`${UNDER_WORD}\d{1,2}(?![0-9])(?:(?:\s*(?:&|,|\/|\band\b)\s*(?:${UNDER_WORD})?|\s+${UNDER_WORD})\d{1,2}(?![0-9]))+`, "i");
14
+ var JOINED_NOT = /^(?:\s*(?:yrs?|years?|-?\s*hand)\b|[a-z])/i;
15
+ var JOINED_OPEN = /^(?:\s*\+|\s*(?:&|and)\s*(?:o|over|older|up)\b)/i;
16
+ var UNDER_STRIDE = /^\s*\/\s*0*([1-9]\d?)\s*yrs?(?![a-z0-9])/i;
17
+ var AND_UNDER = /(?<![a-z0-9])(\d{1,2})\s*(?:years?\s*)?(?:&|\/|\band\b)\s*u(?:nder)?(?![a-z])(?!\s*\d)/i;
18
+ var AND_UNDER_RANGE = /(?<![a-z0-9])(\d{1,2})\s*(?:&|\band\b)\s*under\s*(\d{1,2})(?![a-z0-9])/i;
19
+ var AND_RANGE = /(?<![a-z0-9])(?<!\b(?:under|und|over|faoi|cionn|u|o)\.?\s*)(?<!\d\s*,\s*)(?<!\(\s*)(\d{1,2})\s*(?:&|\band\b)\s*(\d{1,2})(?![a-z0-9])(?!\s*-?\s*hand)(?!\s*(?:&|\+|\/|,|\)|\band\b|\bor\b))/i;
20
+ var SLASH_RANGE = /(?<![a-z0-9/])(\d{1,2})\s*\/\s*(\d{1,2})(?=\s*(?:years?|yrs?|boys?|girls?|ladies|men)\b)/i;
21
+ var AGE_N = /(?<!\b(?:under|over)\s*)\bage\s*(\d{1,2})(?![a-z0-9])(?!\s*-?\s*hand)/i;
22
+ var YEARS = /(?<![a-z0-9])(?<!\d\s*(?:&|\/|\band\b|[-\u0096–—]|\bto\b)\s*)(\d{1,2})\s*(?:years?|yrs?)(?![a-z0-9])(?!\s*(?:&|\+|\/|\band\b|\bor\b|old))/i;
23
+ var RANGE_DASH = String.raw`(?:\u00c2?[-\u0096–—]|\bto\b)`;
24
+ var RANGE = new RegExp(String.raw`(?<![0-9])(?<![0-9][a-z]*)(?<!(?:^|[^a-z0-9])[a-z])(?:u\s*)?(\d{1,2})\s*${RANGE_DASH}\s*(\d{1,2})(?![a-z0-9])`, "i");
25
+ var LEAD_RANGE = new RegExp(String.raw`^\s*([1-9]\d?)\s*${RANGE_DASH}\s*([1-9]\d?)(?![a-z0-9])(?!\s*[/&])`, "i");
26
+ var ADULT_CODE = /^\s*a(?:(\d{1,2})(u|o|&o))?\s*$/i;
27
+ var ADULT = /(?<!non[\s-])(?<!-)\b(?:adults?|advanced\s+adults?|masters?)\b/i;
28
+ var SENIOR = /(?<!-)\bseniors?\b/i;
29
+ var TITLE_RANGE_MIN2 = 3;
30
+ var TITLE_RANGE_MAX2 = 40;
31
+ var NONE = Object.freeze({ token: "", span: null });
32
+ function at(title, offset, start, end, token) {
33
+ const a = offset + start;
34
+ const b = offset + end;
35
+ return { token, span: { start: a, end: b, text: title.slice(a, b) } };
36
+ }
37
+ function inFence(a, b) {
38
+ if (a.startsWith("0") || b.startsWith("0"))
39
+ return false;
40
+ const lo = Number(a);
41
+ const hi = Number(b);
42
+ return lo >= TITLE_RANGE_MIN2 && hi <= TITLE_RANGE_MAX2 && lo < hi;
43
+ }
44
+ function readJoined(title, s, offset) {
45
+ const j = JOINED_CAPS.exec(s);
46
+ if (j === null)
47
+ return null;
48
+ const end = j.index + j[0].length;
49
+ const tail = s.slice(end);
50
+ if (JOINED_NOT.test(tail))
51
+ return null;
52
+ const caps = [...j[0].matchAll(/\d{1,2}/g)].map((d) => Number(d[0]));
53
+ if (caps.some((c, i) => i > 0 && c < (caps[i - 1] ?? 0)))
54
+ return null;
55
+ if (JOINED_OPEN.test(tail))
56
+ return NONE;
57
+ return at(title, offset, j.index, end, `u${Math.max(...caps)}`);
58
+ }
59
+ function readAnchored(title, s, offset, aside = false) {
60
+ const joined = readJoined(title, s, offset);
61
+ if (joined !== null)
62
+ return joined;
63
+ for (const re of [AND_OVER, OVER_WORD, OVER_GLUED]) {
64
+ const m = re.exec(s);
65
+ if (m?.[1] !== undefined)
66
+ return at(title, offset, m.index, m.index + m[0].length, `o${Number(m[1])}`);
67
+ }
68
+ const u = AND_UNDER_RANGE.exec(s);
69
+ if (u?.[1] !== undefined && u[2] !== undefined && inFence(u[1], u[2]))
70
+ return at(title, offset, u.index, u.index + u[0].length, `${Number(u[1])}-${Number(u[2])}`);
71
+ const j = aside ? null : AND_RANGE.exec(s);
72
+ if (j?.[1] !== undefined && j[2] !== undefined && inFence(j[1], j[2]))
73
+ return at(title, offset, j.index, j.index + j[0].length, `${Number(j[1])}-${Number(j[2])}`);
74
+ const sl = aside ? null : SLASH_RANGE.exec(s);
75
+ if (sl?.[1] !== undefined && sl[2] !== undefined && inFence(sl[1], sl[2]) && Number(sl[2]) === Number(sl[1]) + 1)
76
+ return at(title, offset, sl.index, sl.index + sl[0].length, `${Number(sl[1])}-${Number(sl[2])}`);
77
+ const l = LEAD_RANGE.exec(s);
78
+ if (l?.[1] !== undefined && l[2] !== undefined && inFence(l[1], l[2])) {
79
+ const lead = l[0].length - l[0].trimStart().length;
80
+ return at(title, offset, lead, l[0].length, `${Number(l[1])}-${Number(l[2])}`);
81
+ }
82
+ const stripped = s.replace(LEAD_ID, "");
83
+ const shift = s.length - stripped.length;
84
+ const r = RANGE.exec(stripped);
85
+ if (r?.[1] !== undefined && r[2] !== undefined && inFence(r[1], r[2]))
86
+ return at(title, offset + shift, r.index, r.index + r[0].length, `${Number(r[1])}-${Number(r[2])}`);
87
+ const au = AND_UNDER.exec(s);
88
+ if (au?.[1] !== undefined)
89
+ return at(title, offset, au.index, au.index + au[0].length, `u${Number(au[1]) + 1}`);
90
+ const m = UNDER.exec(s);
91
+ if (m?.[1] !== undefined) {
92
+ const end = m.index + m[0].length;
93
+ const k = UNDER_STRIDE.exec(s.slice(end));
94
+ if (k?.[1] !== undefined)
95
+ return at(title, offset, m.index, end + k[0].length, `u${Number(m[1])}/${Number(k[1])}yr`);
96
+ return at(title, offset, m.index, end, `u${Number(m[1])}`);
97
+ }
98
+ const y = YEARS.exec(s) ?? AGE_N.exec(s);
99
+ if (y?.[1] !== undefined)
100
+ return at(title, offset, y.index, y.index + y[0].length, `u${Number(y[1]) + 1}`);
101
+ const p = OVER_PLUS.exec(s);
102
+ if (p?.[1] !== undefined)
103
+ return at(title, offset, p.index, p.index + p[0].length, `o${Number(p[1])}`);
104
+ const a = ADULT.exec(s);
105
+ if (a !== null)
106
+ return at(title, offset, a.index, a.index + a[0].length, "adult");
107
+ const n = SENIOR.exec(s);
108
+ if (n !== null)
109
+ return at(title, offset, n.index, n.index + n[0].length, "senior");
110
+ return NONE;
111
+ }
112
+ function ageTitleToken2(title) {
113
+ const s = title.trim();
114
+ if (s === "")
115
+ return NONE;
116
+ const lead = title.length - title.trimStart().length;
117
+ PAREN.lastIndex = 0;
118
+ let adultAside = null;
119
+ let adultWord = null;
120
+ for (const m of s.matchAll(PAREN)) {
121
+ const inner = m[1] ?? "";
122
+ const from = lead + (m.index ?? 0) + 1;
123
+ const i = m.index ?? 0;
124
+ const tail = i + m[0].length === s.length && !/[a-z0-9]/i.test(s[i - 1] ?? "");
125
+ const code = tail ? ADULT_CODE.exec(inner) : null;
126
+ if (code !== null) {
127
+ const split = code[1] === undefined ? "" : `${/u/i.test(code[2] ?? "") ? "u" : "o"}${code[1]}`;
128
+ const a = inner.length - inner.trimStart().length;
129
+ adultAside = at(title, from, a, inner.trimEnd().length, adultBeside2(split) ?? "adult");
130
+ continue;
131
+ }
132
+ if (adultWord !== null)
133
+ continue;
134
+ const t = readAnchored(title, inner, from, true);
135
+ if (t.token === "")
136
+ continue;
137
+ if (t.token === "adult") {
138
+ adultWord = t;
139
+ continue;
140
+ }
141
+ return adultOverUnder(title, s, lead, t);
142
+ }
143
+ if (adultWord !== null)
144
+ return adultAside ?? adultWord;
145
+ const words = readAnchored(title, s, lead);
146
+ return adultAside !== null && (words.token === "" || words.token === "adult") ? adultAside : adultOverUnder(title, s, lead, words);
147
+ }
148
+ var ADULT_UNDER_MIN = 18;
149
+ var SENIOR_ADULT_OVER = /\bsenior\s+adult\b/i;
150
+ function adultBeside2(band) {
151
+ const b = band.trim().toLowerCase();
152
+ if (b === "")
153
+ return "adult";
154
+ const u = /^u(\d+)(\/\d+yr)?$/.exec(b);
155
+ const n = u?.[1];
156
+ if (n !== undefined) {
157
+ if (Number(n) < ADULT_UNDER_MIN)
158
+ return;
159
+ return Number(n) > ADULT_UNDER_MIN ? `adult u${Number(n)}${u?.[2] ?? ""}` : "adult";
160
+ }
161
+ const floor = /^o(\d+)$/.exec(b)?.[1] ?? /^(\d+)-\d+$/.exec(b)?.[1];
162
+ if (floor === undefined)
163
+ return band;
164
+ return Number(floor) < ADULT_UNDER_MIN ? undefined : `adult ${b}`;
165
+ }
166
+ function adultOverUnder(title, s, lead, t) {
167
+ if (t.token === "")
168
+ return t;
169
+ const a = ADULT.exec(s);
170
+ if (a === null)
171
+ return t;
172
+ if (/^o\d+$/.test(t.token) && SENIOR_ADULT_OVER.test(s))
173
+ return t;
174
+ const token = adultBeside2(t.token);
175
+ if (token === undefined || token === t.token)
176
+ return t;
177
+ if (/^u\d+(?:\/\d+yr)?$/.test(t.token))
178
+ return at(title, lead, a.index, a.index + a[0].length, token);
179
+ return { token, span: t.span };
180
+ }
181
+
182
+ var YOUNGEST_FLOOR2 = 0;
183
+ var ADULT_FLOOR2 = 18;
184
+ var MAJORS_FLOOR_YEARS2 = 3;
185
+ var UNDER_CAP2 = Object.freeze({ plain: 2, exceptional: 4 });
186
+ var UNDER_CAP_MAX_TOP2 = 21;
187
+ var YOUNG_FLOOR2 = Object.freeze({ hard: 2, soft: 4 });
188
+ var MIN_MAJOR_AGE2 = 4;
189
+ var MIN_FEIS_AGE2 = 3;
190
+ var YOUNGEST_BAND_TOP2 = MAJORS_FLOOR_YEARS2;
191
+ var YOB_RANGE2 = Object.freeze({ first: 1901, last: 2099 });
192
+ var DANCE_UP_YEARS2 = 1;
193
+ var ageOnJanuary12 = (year, born) => year - born - 1;
194
+ var yearsSinceBirth2 = (year, born) => year - born;
195
+ var UNDER2 = /^(?:u|under\s*)(\d+)(?:\/([1-9]\d*)yr)?$/i;
196
+ var OVER = /^(?:o|over\s*)(\d+)$/i;
197
+ var AND_OVER2 = /^(\d+)\s*(?:&|and\b)\s*(?:over|o)$/i;
198
+ var RANGE2 = /^u?(\d+)\s*[-–]\s*(\d+)$/i;
199
+ var ADULT_WORD = /^(?:adults?|ad|advanced|masters?)$/i;
200
+ var ADULT_UNDER = /^(?:adults?|ad)[\s-]*(?:u|under\s*)(\d+)(?:\/([1-9]\d*)yr)?$/i;
201
+ var ADULT_BAND = /^(?:adults?|ad)[\s-]+(\S.*)$/i;
202
+ var SENIOR_WORD = /^seniors?$/i;
203
+ var NO_LADDER = Object.freeze({ caps: [], overs: [], ranges: [] });
204
+ function maxWhere(xs, pred) {
205
+ let best;
206
+ for (const x of xs)
207
+ if (pred(x) && (best === undefined || x > best))
208
+ best = x;
209
+ return best;
210
+ }
211
+ function minWhere(xs, pred) {
212
+ let best;
213
+ for (const x of xs)
214
+ if (pred(x) && (best === undefined || x < best))
215
+ best = x;
216
+ return best;
217
+ }
218
+ function readRange(a, b, ladder) {
219
+ if (!Number.isFinite(a) || !Number.isFinite(b) || b <= a)
220
+ return {
221
+ kind: "range",
222
+ interval: null,
223
+ refusal: "backwards-range",
224
+ clauses: [],
225
+ parts: [a, b]
226
+ };
227
+ if (ladder.ranges.some(([c, d]) => (c !== a || d !== b) && c < b && a < d))
228
+ return {
229
+ kind: "range",
230
+ interval: null,
231
+ refusal: "overlapping-ranges",
232
+ clauses: ["range-overlapping"],
233
+ parts: [a, b]
234
+ };
235
+ const below = maxWhere(ladder.caps, (c) => c < b);
236
+ const adultFloor = below === undefined && ladder.adult === true && b > ADULT_FLOOR2;
237
+ const meetsOver = ladder.overs.includes(b + 1) && !ladder.caps.some((c) => c > b);
238
+ const clauses = [
239
+ "range-as-under",
240
+ below !== undefined ? "under-floored-on-rung" : adultFloor ? "range-adult-floor" : "youngest-absorbs"
241
+ ];
242
+ if (meetsOver)
243
+ clauses.push("range-top-meets-over");
244
+ return {
245
+ kind: "range",
246
+ interval: {
247
+ lo: below ?? (adultFloor ? ADULT_FLOOR2 : YOUNGEST_FLOOR2),
248
+ hi: meetsOver ? b + 1 : b
249
+ },
250
+ refusal: null,
251
+ clauses,
252
+ parts: [a, b]
253
+ };
254
+ }
255
+ function strideOf(k) {
256
+ if (k === undefined)
257
+ return null;
258
+ const n = Number(k);
259
+ return Number.isInteger(n) && n >= 1 ? n : null;
260
+ }
261
+ function read(token, ladder, top) {
262
+ const r = RANGE2.exec(token);
263
+ if (r !== null)
264
+ return readRange(Number(r[1]), Number(r[2]), ladder);
265
+ const au = ADULT_UNDER.exec(token);
266
+ if (au !== null && Number(au[1]) > ADULT_FLOOR2) {
267
+ const k = strideOf(au[2]);
268
+ const hi = Number(au[1]);
269
+ return {
270
+ kind: "adult",
271
+ interval: { lo: k === null ? ADULT_FLOOR2 : Math.max(ADULT_FLOOR2, hi - k), hi },
272
+ refusal: null,
273
+ clauses: k === null ? ["adult-under-ceiling"] : ["adult-under-ceiling", "under-stride"],
274
+ parts: k === null ? [hi] : [hi, k]
275
+ };
276
+ }
277
+ const ab = au === null ? ADULT_BAND.exec(token) : null;
278
+ if (ab !== null) {
279
+ const inner = read(ab[1].trim(), { ...ladder, adult: true }, top);
280
+ if (inner.kind === "over" || inner.kind === "range")
281
+ return { ...inner, clauses: [...inner.clauses, "adult-band"], adult: true };
282
+ }
283
+ if (au !== null || ADULT_WORD.test(token))
284
+ return {
285
+ kind: "adult",
286
+ interval: { lo: ADULT_FLOOR2, hi: Number.POSITIVE_INFINITY },
287
+ refusal: null,
288
+ clauses: ["and-over-no-ceiling"],
289
+ parts: []
290
+ };
291
+ if (SENIOR_WORD.test(token)) {
292
+ const top = maxWhere(ladder.caps, () => true);
293
+ return top === undefined ? { kind: "senior", interval: null, refusal: "senior-without-ladder", clauses: [], parts: [] } : {
294
+ kind: "senior",
295
+ interval: { lo: top, hi: Number.POSITIVE_INFINITY },
296
+ refusal: null,
297
+ clauses: ["and-over-no-ceiling"],
298
+ parts: []
299
+ };
300
+ }
301
+ const u = UNDER2.exec(token);
302
+ if (u !== null) {
303
+ const n = Number(u[1]);
304
+ const k = strideOf(u[2]);
305
+ if (k !== null)
306
+ return {
307
+ kind: "under",
308
+ interval: { lo: Math.max(YOUNGEST_FLOOR2, n - k), hi: n },
309
+ refusal: null,
310
+ clauses: ["under-stride"],
311
+ parts: [n, k]
312
+ };
313
+ const below = maxWhere(ladder.caps, (c) => c < n);
314
+ return {
315
+ kind: "under",
316
+ interval: { lo: below ?? YOUNGEST_FLOOR2, hi: n },
317
+ refusal: null,
318
+ clauses: [below === undefined ? "youngest-absorbs" : "under-floored-on-rung"],
319
+ parts: [n]
320
+ };
321
+ }
322
+ const o = OVER.exec(token) ?? AND_OVER2.exec(token);
323
+ if (o !== null) {
324
+ const n = Number(o[1]);
325
+ const above = top ? undefined : minWhere(ladder.overs, (c) => c > n);
326
+ if (above === undefined)
327
+ return {
328
+ kind: "over",
329
+ interval: { lo: n, hi: Number.POSITIVE_INFINITY },
330
+ refusal: null,
331
+ clauses: ["and-over-no-ceiling"],
332
+ parts: [n]
333
+ };
334
+ const gapped = ladder.caps.length > 0 && !ladder.caps.some((c) => c > n && c < above);
335
+ return {
336
+ kind: "over",
337
+ interval: { lo: n, hi: gapped ? Number.POSITIVE_INFINITY : above },
338
+ refusal: null,
339
+ clauses: [gapped ? "over-ceiling-refused-gap" : "over-ceiling-from-next-over"],
340
+ parts: [n]
341
+ };
342
+ }
343
+ return { kind: null, interval: null, refusal: "not-a-band", clauses: [], parts: [] };
344
+ }
345
+ function refuseBelowYoungest(r) {
346
+ if (r.interval === null || r.interval.hi >= YOUNGEST_BAND_TOP2)
347
+ return r;
348
+ return { ...r, interval: null, refusal: "below-youngest-band", clauses: [] };
349
+ }
350
+
351
+ class AgeMisuseError2 extends Error {
352
+ misuse;
353
+ constructor(misuse, detail) {
354
+ super(`age: ${misuse}: ${detail}`);
355
+ this.name = "AgeMisuseError";
356
+ this.misuse = misuse;
357
+ }
358
+ }
359
+ var display = (iv) => `[${iv.lo},${iv.hi === Number.POSITIVE_INFINITY ? "∞" : iv.hi})`;
360
+ var currentYear = () => new Date().getUTCFullYear();
361
+ function withRelations(a) {
362
+ const self = a;
363
+ return Object.assign(self, {
364
+ fitsIn: (container) => {
365
+ if (isAveraged2(container))
366
+ throw new AgeMisuseError2("average-as-container", `${self.ag2} cannot fit inside an average age (${String(container)}); an average is not a dancer`);
367
+ const r = ageRelation2(self, container);
368
+ return r.relation === "same" || r.reason === "b-contains-a";
369
+ },
370
+ overlaps: (other) => {
371
+ const r = ageRelation2(self, other).relation;
372
+ return r === "same" || r === "contains" || r === "overlaps";
373
+ }
374
+ });
375
+ }
376
+ function fromPrint(print, context = {}) {
377
+ const ladder = context.ladder ?? NO_LADDER;
378
+ const year = context.year === undefined ? currentYear() : context.year;
379
+ const ag1 = print.trim().toLowerCase();
380
+ const reading = ag1 === "" ? { kind: null, interval: null, refusal: null, clauses: [], parts: [] } : refuseBelowYoungest(read(print.trim(), ladder, context.top === true));
381
+ const iv = reading.interval;
382
+ const arm = ag1 === "" ? "empty" : iv === null ? "open" : "band";
383
+ const ag2 = iv !== null ? display(iv) : ag1 === "" ? "" : `${ag1}?`;
384
+ const source = sourceOf(reading);
385
+ return withRelations({
386
+ [Symbol.toStringTag]: "Age",
387
+ toString: () => ag2,
388
+ arm,
389
+ kind: reading.kind,
390
+ ag0: print,
391
+ ag1,
392
+ ag2,
393
+ parts: reading.parts,
394
+ length: ag2.length,
395
+ refusal: reading.refusal,
396
+ ...validity(arm === "empty", reading.refusal),
397
+ source,
398
+ clauses: reading.clauses,
399
+ ladder,
400
+ year,
401
+ isAge: () => iv !== null,
402
+ isExact: () => {
403
+ if (iv === null || year === null)
404
+ return false;
405
+ const b = birthYearsOfInterval2(iv, year);
406
+ return b.from !== null && b.from === b.to;
407
+ },
408
+ isAndOver: () => iv !== null && iv.hi === Number.POSITIVE_INFINITY,
409
+ isAdult: () => reading.kind === "adult" || reading.adult === true,
410
+ interval: () => iv,
411
+ birthYears: (y) => {
412
+ const at = y ?? year;
413
+ if (at === null)
414
+ throw new Error(`Age.from(${JSON.stringify(print)}): birthYears needs a competition year`);
415
+ return birthYearsOfInterval2(iv, at);
416
+ },
417
+ floorObserved: () => reading.kind !== "under" && reading.kind !== "range" || iv === null ? null : reading.clauses.includes("under-floored-on-rung") || reading.clauses.includes("range-adult-floor") || reading.clauses.includes("under-stride")
418
+ });
419
+ }
420
+ function fromYOB(from, to = from, context = {}) {
421
+ for (const y of [from, to])
422
+ if (y !== null && !Number.isInteger(y))
423
+ throw new RangeError(`Age.fromYOB(${from}, ${to}): a year of birth is a whole number`);
424
+ if (from === null && to === null)
425
+ throw new RangeError("Age.fromYOB(null, null): at least one end is a year");
426
+ if (from !== null && to !== null && from > to)
427
+ throw new RangeError(`Age.fromYOB(${from}, ${to}): the earliest year comes first`);
428
+ const at = context.year ?? null;
429
+ const valid = [from, to].every((y) => y === null || y >= YOB_RANGE2.first && y <= YOB_RANGE2.last) && (at === null || to === null || yearsSinceBirth2(at, to) >= YOUNG_FLOOR2.hard);
430
+ const refusal = valid ? null : "year-out-of-range";
431
+ const exact = valid && from !== null && from === to;
432
+ const ag0 = from !== null && from === to ? String(from) : `${from ?? ""}..${to ?? ""}`;
433
+ const ag2 = valid ? `b${ag0}` : `b${ag0}?`;
434
+ const years = Object.freeze(valid ? { from, to } : { from: null, to: null });
435
+ return withRelations({
436
+ [Symbol.toStringTag]: "Age",
437
+ toString: () => ag2,
438
+ arm: valid ? "yob" : "open",
439
+ kind: "yob",
440
+ ag0,
441
+ ag1: ag0,
442
+ ag2,
443
+ parts: from !== null && from === to ? [from] : [from, to].filter((y) => y !== null),
444
+ length: ag2.length,
445
+ refusal,
446
+ ...validity(false, refusal),
447
+ source: valid ? "printed-yob" : "open",
448
+ clauses: [],
449
+ ladder: NO_LADDER,
450
+ year: null,
451
+ isAge: () => valid,
452
+ isExact: () => exact,
453
+ isAndOver: () => valid && from === null,
454
+ isAdult: () => false,
455
+ interval: () => null,
456
+ birthYears: () => years,
457
+ floorObserved: () => null
458
+ });
459
+ }
460
+ function fromTitle(title, context = {}) {
461
+ const t = ageTitleToken2(title);
462
+ return Object.freeze({ age: fromPrint(t.token, context), token: t.token, span: t.span });
463
+ }
464
+ var Age2 = Object.freeze({ from: fromPrint, fromYOB, fromTitle });
465
+ function sourceOf(r) {
466
+ const iv = r.interval;
467
+ if (iv === null)
468
+ return "open";
469
+ switch (r.kind) {
470
+ case "range":
471
+ case "under":
472
+ if (r.clauses.includes("under-stride"))
473
+ return "printed-band";
474
+ return iv.lo > YOUNGEST_FLOOR2 ? "sibling-narrowed" : "printed-band";
475
+ case "over":
476
+ return iv.hi === Number.POSITIVE_INFINITY ? "printed-band" : "sibling-narrowed";
477
+ case "adult":
478
+ case "senior":
479
+ return "printed-band";
480
+ default:
481
+ return "open";
482
+ }
483
+ }
484
+ function birthYearsOfInterval2(iv, year) {
485
+ if (iv === null)
486
+ return { from: null, to: null };
487
+ return {
488
+ from: iv.hi === Number.POSITIVE_INFINITY ? null : year - iv.hi,
489
+ to: Math.min(year - iv.lo - 1, year - MAJORS_FLOOR_YEARS2)
490
+ };
491
+ }
492
+ var inBirthYears = (a) => a.kind === "yob" || a.year !== null;
493
+ function spanOf(a) {
494
+ if (!a.isAge())
495
+ return null;
496
+ if (inBirthYears(a)) {
497
+ const b = a.birthYears();
498
+ return {
499
+ lo: b.from ?? Number.NEGATIVE_INFINITY,
500
+ hi: b.to ?? Number.POSITIVE_INFINITY
501
+ };
502
+ }
503
+ const iv = a.interval();
504
+ return { lo: iv.lo, hi: iv.hi - 1 };
505
+ }
506
+ function ageRelation2(a, b) {
507
+ for (const x of [a, b])
508
+ if (isAveraged2(x))
509
+ throw new AgeMisuseError2("average-in-relation", `${String(x)} is an average, not a dancer's birth years; ask its fitsIn`);
510
+ if (inBirthYears(a) !== inBirthYears(b))
511
+ throw new Error("ageRelation: one reading is in birth years and the other has no competition year; read both with a year, or neither");
512
+ if (!a.isAge())
513
+ return { relation: "silent", reason: "a-not-a-band" };
514
+ if (!b.isAge())
515
+ return { relation: "silent", reason: "b-not-a-band" };
516
+ const sa = spanOf(a);
517
+ const sb = spanOf(b);
518
+ const years = inBirthYears(a);
519
+ if (sa.hi < sb.lo)
520
+ return { relation: "disjoint", reason: years ? "a-older" : "a-younger" };
521
+ if (sb.hi < sa.lo)
522
+ return { relation: "disjoint", reason: years ? "a-younger" : "a-older" };
523
+ if (sa.lo === sb.lo && sa.hi === sb.hi)
524
+ return { relation: "same", reason: "equal" };
525
+ if (sa.lo <= sb.lo && sb.hi <= sa.hi)
526
+ return { relation: "contains", reason: "a-contains-b" };
527
+ if (sb.lo <= sa.lo && sa.hi <= sb.hi)
528
+ return { relation: "contains", reason: "b-contains-a" };
529
+ return { relation: "overlaps", reason: "partial" };
530
+ }
531
+ function canOverlap2(...ages) {
532
+ for (let i = 0;i < ages.length; i++)
533
+ for (let j = i + 1;j < ages.length; j++)
534
+ if (ageRelation2(ages[i], ages[j]).relation === "disjoint")
535
+ return false;
536
+ return true;
537
+ }
538
+ var AVERAGE_AGE_DECIMALS2 = 2;
539
+ var HUNDREDTHS = 10 ** AVERAGE_AGE_DECIMALS2;
540
+ function roundedMeanHundredths2(k, n) {
541
+ if (!Number.isInteger(k) || !Number.isInteger(n) || n <= 0)
542
+ throw new RangeError(`roundedMeanHundredths(${k}, ${n}): whole numbers, n > 0`);
543
+ return Math.floor((2 * HUNDREDTHS * k + n) / (2 * n));
544
+ }
545
+ function isAveraged2(a) {
546
+ return a[Symbol.toStringTag] === "AverageAge";
547
+ }
548
+ function averageAge2(ages) {
549
+ if (ages.length === 0)
550
+ throw new AgeMisuseError2("empty-average", "averageAge needs at least one exact age");
551
+ let sum = 0;
552
+ for (const [i, a] of ages.entries()) {
553
+ if (isAveraged2(a))
554
+ throw new AgeMisuseError2("average-of-averages", `input ${i} (${String(a)}) is already an average; average the members instead`);
555
+ if (!a.isExact())
556
+ throw new AgeMisuseError2("not-exact", `input ${i} (${String(a)}) is not one birth year; an average takes exact ages only`);
557
+ sum += a.birthYears().from;
558
+ }
559
+ const n = ages.length;
560
+ const members = Object.freeze([...ages]);
561
+ const hundredths = (year) => roundedMeanHundredths2(n * (year - 1) - sum, n);
562
+ const self = {
563
+ [Symbol.toStringTag]: "AverageAge",
564
+ provenance: `average of ${n}`,
565
+ members,
566
+ n,
567
+ birthYearSum: sum,
568
+ meanBirthYear: sum / n,
569
+ toString: () => `avg(${n}):b${sum / n}`,
570
+ isExact: () => false,
571
+ meanAgeHundredths: hundredths,
572
+ meanAge: (year) => hundredths(year) / HUNDREDTHS,
573
+ fitsIn: (container) => {
574
+ if (isAveraged2(container))
575
+ throw new AgeMisuseError2("average-as-container", `nothing fits inside an average age (${String(container)})`);
576
+ if (container.year === null)
577
+ throw new AgeMisuseError2("average-container-no-year", `${String(container)} has no competition year to take the average age in`);
578
+ const iv = container.interval();
579
+ if (iv === null)
580
+ return false;
581
+ const h = hundredths(container.year);
582
+ return iv.lo * HUNDREDTHS <= h && h < iv.hi * HUNDREDTHS;
583
+ },
584
+ birthYears: () => {
585
+ throw new AgeMisuseError2("average-birth-years", `an average of ${n} has no year of birth; read meanBirthYear, which is not a dancer's`);
586
+ }
587
+ };
588
+ return self;
589
+ }
590
+ function birthYearsDisjoint2(a, b) {
591
+ if (a.to !== null && b.from !== null && a.to < b.from)
592
+ return true;
593
+ if (b.to !== null && a.from !== null && b.to < a.from)
594
+ return true;
595
+ return false;
596
+ }
597
+ function intersectBirthYears2(windows) {
598
+ let from = null;
599
+ let to = null;
600
+ let n = 0;
601
+ for (const w of windows) {
602
+ if (w.from === null && w.to === null)
603
+ continue;
604
+ n++;
605
+ if (w.from !== null)
606
+ from = from === null ? w.from : Math.max(from, w.from);
607
+ if (w.to !== null)
608
+ to = to === null ? w.to : Math.min(to, w.to);
609
+ }
610
+ if (n === 0)
611
+ return { from: null, to: null, n: 0 };
612
+ if (from !== null && to !== null && from > to)
613
+ return null;
614
+ return { from, to, n };
615
+ }
616
+ function birthYearsMeet2(card, other, slack = 0) {
617
+ if (card.from !== null && other.to !== null && card.from > other.to)
618
+ return false;
619
+ if (card.to !== null && other.from !== null && card.to + slack < other.from)
620
+ return false;
621
+ return true;
622
+ }
623
+ function underCapApplies2(bandFrom, year) {
624
+ return bandFrom !== null && year - bandFrom <= UNDER_CAP_MAX_TOP2;
625
+ }
626
+ function underCappedYears2(b, year, n = UNDER_CAP2.plain) {
627
+ if (n > UNDER_CAP2.exceptional)
628
+ throw new Error(`under-cap: ${n} is over the exceptional cap of ${UNDER_CAP2.exceptional}`);
629
+ if (!underCapApplies2(b.from, year))
630
+ return b;
631
+ const cap = b.from + n;
632
+ if (b.to !== null && b.to <= cap)
633
+ return b;
634
+ return { from: b.from, to: cap };
635
+ }
636
+ function underCapRefuses2(bandFrom, identityFrom, n) {
637
+ return identityFrom > bandFrom + n;
638
+ }
639
+ function validYOB2(y, year) {
640
+ if (y === null || y === undefined || !Number.isInteger(y))
641
+ return null;
642
+ return Age2.fromYOB(y, y, { year: year ?? null }).isValid() ? y : null;
643
+ }
644
+ function youngFloor2(year, oldest) {
645
+ const s = yearsSinceBirth2(year, oldest);
646
+ if (s >= YOUNG_FLOOR2.soft)
647
+ return "clear";
648
+ if (s < YOUNG_FLOOR2.hard)
649
+ return "too-young";
650
+ return "same-school-only";
651
+ }
652
+ var tooYoungForMajor2 = (year, oldest) => yearsSinceBirth2(year, oldest) < MIN_MAJOR_AGE2;
653
+ var tooYoungForFeis2 = (year, oldest) => yearsSinceBirth2(year, oldest) < MIN_FEIS_AGE2;
654
+
655
+ export { TITLE_RANGE_MIN2, TITLE_RANGE_MAX2, ageTitleToken2, 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, 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 };