@finbheara/domain 0.4.0 → 0.6.2
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 +102 -7
- package/dist/age.js +150 -31
- package/dist/ceili.js +1 -1
- package/dist/chunks/{place-qbp3xfb4.js → place-h7tvdfyp.js} +553 -30
- package/dist/chunks/{place-0ehc2q69.js → place-kk48pfja.js} +4 -0
- package/dist/chunks/place-q9kwsmzd.js +11 -0
- package/dist/chunks/{place-2ja9yx08.js → place-yndzaae5.js} +51 -43
- package/dist/dance.js +9 -2
- package/dist/place.js +1 -1
- package/dist/promotion.js +22 -2
- package/dist/school.js +16 -129
- package/dist/tune.js +1 -1
- package/dist/types/age-title.d.ts +12 -0
- package/dist/types/age-types.d.ts +5 -3
- package/dist/types/age.d.ts +13 -5
- package/dist/types/ceili.d.ts +3 -1
- package/dist/types/dance.d.ts +4 -1
- package/dist/types/place-types.d.ts +5 -2
- package/dist/types/place.d.ts +3 -3
- package/dist/types/promotion.d.ts +2 -0
- package/dist/types/school-location.d.ts +1 -0
- package/dist/types/school-name.d.ts +2 -2
- package/dist/types/school.d.ts +1 -0
- package/dist/types/tune.d.ts +8 -1
- package/dist/types/validated.d.ts +6 -2
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -36,10 +36,10 @@ ESM, Node ≥ 22 or Bun ≥ 1.4.2. MIT. Not affiliated with or endorsed by any d
|
|
|
36
36
|
| `@finbheara/domain/place` | `place()`, `createPlaceReader`, `placeRelation`, the `Place` types |
|
|
37
37
|
| `@finbheara/domain/clrg-regions` | `REGIONS`, `regionOf`, `resolvePhrase` |
|
|
38
38
|
| `@finbheara/domain/school` | `createSchoolNameReader`, `DEFAULT_SCHOOL_NAME_READER`, `compareSchoolNames`, `foldSchoolName`, `schoolEncoder`, `schoolColumns` |
|
|
39
|
-
| `@finbheara/domain/tune` | `tune()`, `tuneRelation`, `tempoLegal`, `settingAt`, `TUNE_NODES`: the CLRG set dance list |
|
|
39
|
+
| `@finbheara/domain/tune` | `tune()`, `tuneRelation`, `tempoLegal`, `settingAt`, `TUNE_NODES`: the CLRG set dance list, each tune with its time, speeds and bars |
|
|
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`, `intersectBirthYears`,
|
|
42
|
+
| `@finbheara/domain/age` | `Age.from`, `Age.fromYOB`, `Age.fromTitle`, `ageTitleToken`, `ageRelation`, `canOverlap`, `averageAge`, `isAveraged`, `intersectBirthYears`, `validYOB`, 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
45
|
| `@finbheara/domain/promotion` | `samePromotedRow`, `samePromotedSchool`, `samePromotedHeir`, `samePromotedName`, `questionMarksRead`, `foldCharSynonyms`: is a row the same row after a promotion (below) |
|
|
@@ -47,6 +47,37 @@ ESM, Node ≥ 22 or Bun ≥ 1.4.2. MIT. Not affiliated with or endorsed by any d
|
|
|
47
47
|
The library does no I/O and installs nothing process-wide. Every reader has a default and a
|
|
48
48
|
factory: build your own with options when the default is not what you want.
|
|
49
49
|
|
|
50
|
+
## Empty, refused, valid
|
|
51
|
+
|
|
52
|
+
A reader never throws on what was printed. Every value it returns (`Place`, `SchoolName`,
|
|
53
|
+
`Age`, `TuneReading`, `CeiliReading`, `DanceReading`) is `Validated`, and answers three
|
|
54
|
+
questions:
|
|
55
|
+
|
|
56
|
+
| method | true when |
|
|
57
|
+
|---|---|
|
|
58
|
+
| `isEmpty()` | nothing was printed: an empty or blank cell, no dance named |
|
|
59
|
+
| `isRefused()` | something was printed and the type declines it: a placeholder, a school in a place field, a URL or an online event in a place field, `U2`, a `1900` year of birth |
|
|
60
|
+
| `isValid()` | neither: `!isEmpty() && !isRefused()` |
|
|
61
|
+
|
|
62
|
+
`invalidReason()` is `null` when the value is valid, `"empty"` when it is empty, and the
|
|
63
|
+
refusal's reason otherwise, in the type's own words. Every entry point exports the `Validated`
|
|
64
|
+
type.
|
|
65
|
+
|
|
66
|
+
Valid does not mean usable. A print the reader does not know but cannot rule out is valid:
|
|
67
|
+
`place("Ardmore")` is valid and `isPlace()` is false. Ask the type's own question (`isPlace()`,
|
|
68
|
+
`isSchool()`, `isAge()`, `isTune()`, `isCeili()`) for a value you can use.
|
|
69
|
+
|
|
70
|
+
```ts
|
|
71
|
+
place("").invalidReason(); // "empty"
|
|
72
|
+
place("n/a").invalidReason(); // "placeholder"
|
|
73
|
+
place("Ardmore").isValid(); // true: valid, unknown
|
|
74
|
+
place("Video Feis").invalidReason(); // "online"
|
|
75
|
+
DEFAULT_SCHOOL_NAME_READER.schoolName("N/A").isRefused(); // true
|
|
76
|
+
Age.fromYOB(1900).invalidReason(); // "year-out-of-range"
|
|
77
|
+
tune("").isEmpty(); // true
|
|
78
|
+
tune("Reel").invalidReason(); // "dance-type"
|
|
79
|
+
```
|
|
80
|
+
|
|
50
81
|
## Read a place
|
|
51
82
|
|
|
52
83
|
```ts
|
|
@@ -72,6 +103,13 @@ A `Place` is one of four arms: `resolved`, `plural` (a print naming more than on
|
|
|
72
103
|
`ambiguous` (one print, several readings) and `unread`. Only `conflict` between two readings
|
|
73
104
|
says they cannot be one place; `same`, `contains` and `overlaps` do not.
|
|
74
105
|
|
|
106
|
+
A print that is clearly not a place is refused, with its reason: a placeholder
|
|
107
|
+
(`[address withheld]`, `Other`), a URL anywhere in the print (`web`), an online event in the
|
|
108
|
+
venue field (`online`), a change-of-venue notice (`notice`), or a replacement character where
|
|
109
|
+
the source lost bytes (`damaged`). These are asked only where the store reads nothing, so a
|
|
110
|
+
refusal never takes a reading away. Mojibake that can be decoded back is not refused: repair it
|
|
111
|
+
before reading.
|
|
112
|
+
|
|
75
113
|
## Find a CLRG region
|
|
76
114
|
|
|
77
115
|
```ts
|
|
@@ -156,6 +194,31 @@ invisible characters dropped and its whitespace collapsed; a `regex` is compiled
|
|
|
156
194
|
flag and anchors itself. Your rows are asked before the shipped ones and the first match
|
|
157
195
|
answers.
|
|
158
196
|
|
|
197
|
+
## Read a set dance
|
|
198
|
+
|
|
199
|
+
`tune()` reads a spoken or printed set dance as the tunes of the official list it may name. A
|
|
200
|
+
stated tempo is taken out of the print and narrows the reading. Each tune answers its time
|
|
201
|
+
signature, its speed (a fixed speed for the seven traditional sets, a minimum for any
|
|
202
|
+
contemporary set) and the bars of music in its two parts, the step and the set.
|
|
203
|
+
|
|
204
|
+
```ts
|
|
205
|
+
import { tempoLegal, tune } from "@finbheara/domain/tune";
|
|
206
|
+
|
|
207
|
+
const r = tune("Kilkenny Races at 80");
|
|
208
|
+
r.tune?.name; // "Kilkenny Races"
|
|
209
|
+
r.bpm; // 80
|
|
210
|
+
r.tune?.time(); // "4/4"
|
|
211
|
+
tempoLegal(r, 80); // true: hornpipe time, at or above 76
|
|
212
|
+
r.tune?.stepBars(); // 8
|
|
213
|
+
r.tune?.setBars(); // 24
|
|
214
|
+
|
|
215
|
+
tune("Blackbird").tune?.speed(); // 144: a traditional set's fixed speed
|
|
216
|
+
tune("Planxty").candidates.length; // 3: Davis, Drury and Hugh O'Donnell
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
The bars are data on the tune and never read from a print: they say how long a step and a
|
|
220
|
+
set run at a given speed, and are the same in a traditional and a contemporary round.
|
|
221
|
+
|
|
159
222
|
## Read an age
|
|
160
223
|
|
|
161
224
|
Ages are as of 1 January of the competition year. An `Age` is a closed range of birth years,
|
|
@@ -179,19 +242,51 @@ Age.fromYOB(1999).fitsIn(Age.from("U18", { year: 2025 })); // fals
|
|
|
179
242
|
Age.fromYOB(1999).fitsIn(Age.from("Over 21", { year: 2026, top: true })); // true
|
|
180
243
|
|
|
181
244
|
canOverlap(u12, Age.from("U14", { year: 2026 }), dancer); // true
|
|
245
|
+
|
|
246
|
+
Age.from("13 & Over", { year: 2026 }).interval(); // { lo: 13, hi: Infinity }, as "Over 13"
|
|
182
247
|
```
|
|
183
248
|
|
|
184
|
-
A
|
|
185
|
-
`
|
|
186
|
-
|
|
187
|
-
|
|
249
|
+
A competition title usually carries its age group in its words. `Age.fromTitle` finds the
|
|
250
|
+
phrase, reads it as `Age.from` would, and says where it was. A title that names no age group
|
|
251
|
+
is empty, not refused. Competition numbers are never read as ages.
|
|
252
|
+
|
|
253
|
+
```ts
|
|
254
|
+
const t = Age.fromTitle("Novice Hornpipe Under 10", { year: 2026 });
|
|
255
|
+
t.token; // "u10"
|
|
256
|
+
t.span; // { start: 16, end: 24, text: "Under 10" }
|
|
257
|
+
t.age.birthYears(); // { from: 2016, to: 2023 }
|
|
258
|
+
|
|
259
|
+
Age.fromTitle("Open Championship 15-16", { year: 2026 }).token; // "15-16"
|
|
260
|
+
Age.fromTitle("Competition 719OC - Open Championship (O18)").token; // "o18", not "o9"
|
|
261
|
+
Age.fromTitle("Treble Reel Special", { year: 2026 }).age.isEmpty(); // true
|
|
262
|
+
Age.fromTitle("Mixed Reel 7 & Under").token; // "u8": a 7-year-old dances in it
|
|
263
|
+
Age.fromTitle("Girls 8 and under 9 years").token; // "8-9"
|
|
264
|
+
Age.fromTitle("Junior Boys 12 Years").token; // "u13"
|
|
265
|
+
Age.fromTitle("10-11 Single Jig").token; // "10-11", a band, not a number
|
|
266
|
+
Age.fromTitle("Competition 001-013 - Reel").token; // "", a competition number
|
|
267
|
+
Age.fromTitle("Senior Boys Reel 15&16").token; // "15-16": a range beats the word
|
|
268
|
+
Age.fromTitle("Beginner Adults Reel U40").token; // "adult": a split of the adult band
|
|
269
|
+
Age.fromTitle("Adult / Child Reel Under 10").token; // "u10": no adult dances in it
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
An adult word beside an `Under N` at 18 or above reads `adult`: Adult is a band with an 18
|
|
273
|
+
floor, and the `Under 40` splits it. Below 18 the youth band stands.
|
|
274
|
+
|
|
275
|
+
An age group is the age on the first of January, so a title that admits the dancer who is
|
|
276
|
+
`N` (`7 & Under`, `7/U`, `7 Years`) is the `Under N+1` group.
|
|
277
|
+
|
|
278
|
+
A value the reader cannot stand behind is returned, not thrown. An `Age` is `Validated` (above):
|
|
279
|
+
an age group under `U3` admits nobody old enough to dance, and a year of birth outside 1901 to
|
|
280
|
+
2099 is not one, so the placeholders `1000` and `1900` are refused. An invalid `Age` has no
|
|
281
|
+
birth years and rules nothing out.
|
|
188
282
|
|
|
189
283
|
```ts
|
|
190
284
|
Age.from("U2", { year: 2026 }).isValid(); // false
|
|
191
285
|
Age.from("U2", { year: 2026 }).invalidReason(); // "below-youngest-band"
|
|
192
286
|
Age.fromYOB(2222).invalidReason(); // "year-out-of-range"
|
|
193
287
|
Age.from("All", { year: 2026 }).invalidReason(); // "not-a-band"
|
|
194
|
-
Age.from("", { year: 2026 }).
|
|
288
|
+
Age.from("", { year: 2026 }).isEmpty(); // true: empty, not refused
|
|
289
|
+
Age.from("", { year: 2026 }).invalidReason(); // "empty"
|
|
195
290
|
```
|
|
196
291
|
|
|
197
292
|
A team's age is the average of its dancers' ages, rounded to two decimals. `averageAge`
|
package/dist/age.js
CHANGED
|
@@ -1,3 +1,112 @@
|
|
|
1
|
+
import {
|
|
2
|
+
validity
|
|
3
|
+
} from "./chunks/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 = /(?<![a-z0-9])(\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 = /(?:\bunder\s*|\bfaoi\s*|(?<![a-z0-9])u\.?\s*)(\d{1,2})(?![a-z0-9])/i;
|
|
12
|
+
var AND_UNDER = /(?<![a-z0-9])(\d{1,2})\s*(?:years?\s*)?(?:&|\/|\band\b)\s*u(?:nder)?(?![a-z])(?!\s*\d)/i;
|
|
13
|
+
var AND_UNDER_RANGE = /(?<![a-z0-9])(\d{1,2})\s*(?:&|\band\b)\s*under\s*(\d{1,2})(?![a-z0-9])/i;
|
|
14
|
+
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;
|
|
15
|
+
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;
|
|
16
|
+
var RANGE_DASH = String.raw`(?:\u00c2?[-\u0096–—]|\bto\b)`;
|
|
17
|
+
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");
|
|
18
|
+
var LEAD_RANGE = new RegExp(String.raw`^\s*([1-9]\d?)\s*${RANGE_DASH}\s*([1-9]\d?)(?![a-z0-9])(?!\s*[/&])`, "i");
|
|
19
|
+
var ADULT_CODE = /^\s*a(?:\d{1,2}(?:u|o|&o))?\s*$/i;
|
|
20
|
+
var ADULT = /(?<!non[\s-])(?<!-)\b(?:adults?|advanced\s+adults?|masters?)\b/i;
|
|
21
|
+
var SENIOR = /(?<!-)\bseniors?\b/i;
|
|
22
|
+
var TITLE_RANGE_MIN = 3;
|
|
23
|
+
var TITLE_RANGE_MAX = 40;
|
|
24
|
+
var NONE = Object.freeze({ token: "", span: null });
|
|
25
|
+
function at(title, offset, start, end, token) {
|
|
26
|
+
const a = offset + start;
|
|
27
|
+
const b = offset + end;
|
|
28
|
+
return { token, span: { start: a, end: b, text: title.slice(a, b) } };
|
|
29
|
+
}
|
|
30
|
+
function inFence(a, b) {
|
|
31
|
+
const lo = Number(a);
|
|
32
|
+
const hi = Number(b);
|
|
33
|
+
return lo >= TITLE_RANGE_MIN && hi <= TITLE_RANGE_MAX && lo < hi;
|
|
34
|
+
}
|
|
35
|
+
function readAnchored(title, s, offset, aside = false) {
|
|
36
|
+
for (const re of [AND_OVER, OVER_WORD, OVER_GLUED]) {
|
|
37
|
+
const m = re.exec(s);
|
|
38
|
+
if (m?.[1] !== undefined)
|
|
39
|
+
return at(title, offset, m.index, m.index + m[0].length, `o${Number(m[1])}`);
|
|
40
|
+
}
|
|
41
|
+
const u = AND_UNDER_RANGE.exec(s);
|
|
42
|
+
if (u?.[1] !== undefined && u[2] !== undefined && inFence(u[1], u[2]))
|
|
43
|
+
return at(title, offset, u.index, u.index + u[0].length, `${Number(u[1])}-${Number(u[2])}`);
|
|
44
|
+
const j = aside ? null : AND_RANGE.exec(s);
|
|
45
|
+
if (j?.[1] !== undefined && j[2] !== undefined && inFence(j[1], j[2]))
|
|
46
|
+
return at(title, offset, j.index, j.index + j[0].length, `${Number(j[1])}-${Number(j[2])}`);
|
|
47
|
+
const l = LEAD_RANGE.exec(s);
|
|
48
|
+
if (l?.[1] !== undefined && l[2] !== undefined && inFence(l[1], l[2])) {
|
|
49
|
+
const lead = l[0].length - l[0].trimStart().length;
|
|
50
|
+
return at(title, offset, lead, l[0].length, `${Number(l[1])}-${Number(l[2])}`);
|
|
51
|
+
}
|
|
52
|
+
const stripped = s.replace(LEAD_ID, "");
|
|
53
|
+
const shift = s.length - stripped.length;
|
|
54
|
+
const r = RANGE.exec(stripped);
|
|
55
|
+
if (r?.[1] !== undefined && r[2] !== undefined && inFence(r[1], r[2]))
|
|
56
|
+
return at(title, offset + shift, r.index, r.index + r[0].length, `${Number(r[1])}-${Number(r[2])}`);
|
|
57
|
+
const au = AND_UNDER.exec(s);
|
|
58
|
+
if (au?.[1] !== undefined)
|
|
59
|
+
return at(title, offset, au.index, au.index + au[0].length, `u${Number(au[1]) + 1}`);
|
|
60
|
+
const m = UNDER.exec(s);
|
|
61
|
+
if (m?.[1] !== undefined)
|
|
62
|
+
return at(title, offset, m.index, m.index + m[0].length, `u${Number(m[1])}`);
|
|
63
|
+
const y = YEARS.exec(s);
|
|
64
|
+
if (y?.[1] !== undefined)
|
|
65
|
+
return at(title, offset, y.index, y.index + y[0].length, `u${Number(y[1]) + 1}`);
|
|
66
|
+
const p = OVER_PLUS.exec(s);
|
|
67
|
+
if (p?.[1] !== undefined)
|
|
68
|
+
return at(title, offset, p.index, p.index + p[0].length, `o${Number(p[1])}`);
|
|
69
|
+
const a = ADULT.exec(s);
|
|
70
|
+
if (a !== null)
|
|
71
|
+
return at(title, offset, a.index, a.index + a[0].length, "adult");
|
|
72
|
+
const n = SENIOR.exec(s);
|
|
73
|
+
if (n !== null)
|
|
74
|
+
return at(title, offset, n.index, n.index + n[0].length, "senior");
|
|
75
|
+
return NONE;
|
|
76
|
+
}
|
|
77
|
+
function ageTitleToken(title) {
|
|
78
|
+
const s = title.trim();
|
|
79
|
+
if (s === "")
|
|
80
|
+
return NONE;
|
|
81
|
+
const lead = title.length - title.trimStart().length;
|
|
82
|
+
PAREN.lastIndex = 0;
|
|
83
|
+
let adultAside = null;
|
|
84
|
+
for (const m of s.matchAll(PAREN)) {
|
|
85
|
+
const inner = m[1] ?? "";
|
|
86
|
+
const from = lead + (m.index ?? 0) + 1;
|
|
87
|
+
const i = m.index ?? 0;
|
|
88
|
+
const tail = i + m[0].length === s.length && !/[a-z0-9]/i.test(s[i - 1] ?? "");
|
|
89
|
+
if (tail && ADULT_CODE.test(inner)) {
|
|
90
|
+
const a = inner.length - inner.trimStart().length;
|
|
91
|
+
adultAside = at(title, from, a, inner.trimEnd().length, "adult");
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
const t = readAnchored(title, inner, from, true);
|
|
95
|
+
if (t.token !== "")
|
|
96
|
+
return adultOverUnder(title, s, lead, t);
|
|
97
|
+
}
|
|
98
|
+
const words = readAnchored(title, s, lead);
|
|
99
|
+
return words.token === "" && adultAside !== null ? adultAside : adultOverUnder(title, s, lead, words);
|
|
100
|
+
}
|
|
101
|
+
var ADULT_UNDER_MIN = 18;
|
|
102
|
+
function adultOverUnder(title, s, lead, t) {
|
|
103
|
+
const n = /^u(\d+)$/.exec(t.token)?.[1];
|
|
104
|
+
if (n === undefined || Number(n) < ADULT_UNDER_MIN)
|
|
105
|
+
return t;
|
|
106
|
+
const a = ADULT.exec(s);
|
|
107
|
+
return a === null ? t : at(title, lead, a.index, a.index + a[0].length, "adult");
|
|
108
|
+
}
|
|
109
|
+
|
|
1
110
|
var YOUNGEST_FLOOR = 0;
|
|
2
111
|
var ADULT_FLOOR = 18;
|
|
3
112
|
var MAJORS_FLOOR_YEARS = 3;
|
|
@@ -7,13 +116,14 @@ var YOUNG_FLOOR = Object.freeze({ hard: 2, soft: 4 });
|
|
|
7
116
|
var MIN_MAJOR_AGE = 4;
|
|
8
117
|
var MIN_FEIS_AGE = 3;
|
|
9
118
|
var YOUNGEST_BAND_TOP = MAJORS_FLOOR_YEARS;
|
|
10
|
-
var YOB_RANGE = Object.freeze({ first:
|
|
119
|
+
var YOB_RANGE = Object.freeze({ first: 1901, last: 2099 });
|
|
11
120
|
var DANCE_UP_YEARS = 1;
|
|
12
121
|
var ageOnJanuary1 = (year, born) => year - born - 1;
|
|
13
122
|
var yearsSinceBirth = (year, born) => year - born;
|
|
14
|
-
var
|
|
123
|
+
var UNDER2 = /^(?:u|under\s*)(\d+)$/i;
|
|
15
124
|
var OVER = /^(?:o|over\s*)(\d+)$/i;
|
|
16
|
-
var
|
|
125
|
+
var AND_OVER2 = /^(\d+)\s*(?:&|and\b)\s*(?:over|o)$/i;
|
|
126
|
+
var RANGE2 = /^u?(\d+)\s*[-–]\s*(\d+)$/i;
|
|
17
127
|
var ADULT_WORD = /^(?:adults?|ad|advanced|masters?)$/i;
|
|
18
128
|
var SENIOR_WORD = /^seniors?$/i;
|
|
19
129
|
var NO_LADDER = Object.freeze({ caps: [], overs: [], ranges: [] });
|
|
@@ -40,29 +150,36 @@ function readRange(a, b, ladder) {
|
|
|
40
150
|
clauses: [],
|
|
41
151
|
parts: [a, b]
|
|
42
152
|
};
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
153
|
+
if (ladder.ranges.some(([c, d]) => (c !== a || d !== b) && c < b && a < d))
|
|
154
|
+
return {
|
|
155
|
+
kind: "range",
|
|
156
|
+
interval: null,
|
|
157
|
+
refusal: "overlapping-ranges",
|
|
158
|
+
clauses: ["range-overlapping"],
|
|
159
|
+
parts: [a, b]
|
|
160
|
+
};
|
|
161
|
+
const below = maxWhere(ladder.caps, (c) => c < b);
|
|
162
|
+
const adultFloor = below === undefined && ladder.adult === true && b > ADULT_FLOOR;
|
|
163
|
+
const meetsOver = ladder.overs.includes(b + 1) && !ladder.caps.some((c) => c > b);
|
|
164
|
+
const clauses = [
|
|
165
|
+
"range-as-under",
|
|
166
|
+
below !== undefined ? "under-floored-on-rung" : adultFloor ? "range-adult-floor" : "youngest-absorbs"
|
|
167
|
+
];
|
|
168
|
+
if (meetsOver)
|
|
169
|
+
clauses.push("range-top-meets-over");
|
|
56
170
|
return {
|
|
57
171
|
kind: "range",
|
|
58
|
-
interval: {
|
|
172
|
+
interval: {
|
|
173
|
+
lo: below ?? (adultFloor ? ADULT_FLOOR : YOUNGEST_FLOOR),
|
|
174
|
+
hi: meetsOver ? b + 1 : b
|
|
175
|
+
},
|
|
59
176
|
refusal: null,
|
|
60
177
|
clauses,
|
|
61
178
|
parts: [a, b]
|
|
62
179
|
};
|
|
63
180
|
}
|
|
64
181
|
function read(token, ladder, top) {
|
|
65
|
-
const r =
|
|
182
|
+
const r = RANGE2.exec(token);
|
|
66
183
|
if (r !== null)
|
|
67
184
|
return readRange(Number(r[1]), Number(r[2]), ladder);
|
|
68
185
|
if (ADULT_WORD.test(token))
|
|
@@ -83,7 +200,7 @@ function read(token, ladder, top) {
|
|
|
83
200
|
parts: []
|
|
84
201
|
};
|
|
85
202
|
}
|
|
86
|
-
const u =
|
|
203
|
+
const u = UNDER2.exec(token);
|
|
87
204
|
if (u !== null) {
|
|
88
205
|
const n = Number(u[1]);
|
|
89
206
|
const below = maxWhere(ladder.caps, (c) => c < n);
|
|
@@ -95,7 +212,7 @@ function read(token, ladder, top) {
|
|
|
95
212
|
parts: [n]
|
|
96
213
|
};
|
|
97
214
|
}
|
|
98
|
-
const o = OVER.exec(token);
|
|
215
|
+
const o = OVER.exec(token) ?? AND_OVER2.exec(token);
|
|
99
216
|
if (o !== null) {
|
|
100
217
|
const n = Number(o[1]);
|
|
101
218
|
const above = top ? undefined : minWhere(ladder.overs, (c) => c > n);
|
|
@@ -169,13 +286,11 @@ function fromPrint(print, context = {}) {
|
|
|
169
286
|
parts: reading.parts,
|
|
170
287
|
length: ag2.length,
|
|
171
288
|
refusal: reading.refusal,
|
|
172
|
-
|
|
173
|
-
invalidReason: () => reading.refusal,
|
|
289
|
+
...validity(arm === "empty", reading.refusal),
|
|
174
290
|
source,
|
|
175
291
|
clauses: reading.clauses,
|
|
176
292
|
ladder,
|
|
177
293
|
year,
|
|
178
|
-
isEmpty: () => arm === "empty",
|
|
179
294
|
isAge: () => iv !== null,
|
|
180
295
|
isExact: () => {
|
|
181
296
|
if (iv === null || year === null)
|
|
@@ -192,7 +307,7 @@ function fromPrint(print, context = {}) {
|
|
|
192
307
|
throw new Error(`Age.from(${JSON.stringify(print)}): birthYears needs a competition year`);
|
|
193
308
|
return birthYearsOfInterval(iv, at);
|
|
194
309
|
},
|
|
195
|
-
floorObserved: () => reading.kind !== "under" || iv === null ? null : reading.clauses.includes("under-floored-on-rung")
|
|
310
|
+
floorObserved: () => reading.kind !== "under" && reading.kind !== "range" || iv === null ? null : reading.clauses.includes("under-floored-on-rung") || reading.clauses.includes("range-adult-floor")
|
|
196
311
|
});
|
|
197
312
|
}
|
|
198
313
|
function fromYOB(from, to = from, context = {}) {
|
|
@@ -221,13 +336,11 @@ function fromYOB(from, to = from, context = {}) {
|
|
|
221
336
|
parts: from !== null && from === to ? [from] : [from, to].filter((y) => y !== null),
|
|
222
337
|
length: ag2.length,
|
|
223
338
|
refusal,
|
|
224
|
-
|
|
225
|
-
invalidReason: () => refusal,
|
|
339
|
+
...validity(false, refusal),
|
|
226
340
|
source: valid ? "printed-yob" : "open",
|
|
227
341
|
clauses: [],
|
|
228
342
|
ladder: NO_LADDER,
|
|
229
343
|
year: null,
|
|
230
|
-
isEmpty: () => false,
|
|
231
344
|
isAge: () => valid,
|
|
232
345
|
isExact: () => exact,
|
|
233
346
|
isAndOver: () => valid && from === null,
|
|
@@ -237,21 +350,24 @@ function fromYOB(from, to = from, context = {}) {
|
|
|
237
350
|
floorObserved: () => null
|
|
238
351
|
});
|
|
239
352
|
}
|
|
240
|
-
|
|
353
|
+
function fromTitle(title, context = {}) {
|
|
354
|
+
const t = ageTitleToken(title);
|
|
355
|
+
return Object.freeze({ age: fromPrint(t.token, context), token: t.token, span: t.span });
|
|
356
|
+
}
|
|
357
|
+
var Age = Object.freeze({ from: fromPrint, fromYOB, fromTitle });
|
|
241
358
|
function sourceOf(r) {
|
|
242
359
|
const iv = r.interval;
|
|
243
360
|
if (iv === null)
|
|
244
361
|
return "open";
|
|
245
362
|
switch (r.kind) {
|
|
246
363
|
case "range":
|
|
247
|
-
|
|
364
|
+
case "under":
|
|
365
|
+
return iv.lo > YOUNGEST_FLOOR ? "sibling-narrowed" : "printed-band";
|
|
248
366
|
case "over":
|
|
249
367
|
return iv.hi === Number.POSITIVE_INFINITY ? "printed-band" : "sibling-narrowed";
|
|
250
368
|
case "adult":
|
|
251
369
|
case "senior":
|
|
252
370
|
return "printed-band";
|
|
253
|
-
case "under":
|
|
254
|
-
return iv.lo > YOUNGEST_FLOOR ? "sibling-narrowed" : "printed-band";
|
|
255
371
|
default:
|
|
256
372
|
return "open";
|
|
257
373
|
}
|
|
@@ -435,6 +551,8 @@ export {
|
|
|
435
551
|
MAJORS_FLOOR_YEARS,
|
|
436
552
|
MIN_FEIS_AGE,
|
|
437
553
|
MIN_MAJOR_AGE,
|
|
554
|
+
TITLE_RANGE_MAX,
|
|
555
|
+
TITLE_RANGE_MIN,
|
|
438
556
|
UNDER_CAP,
|
|
439
557
|
UNDER_CAP_MAX_TOP,
|
|
440
558
|
YOB_RANGE,
|
|
@@ -443,6 +561,7 @@ export {
|
|
|
443
561
|
YOUNG_FLOOR,
|
|
444
562
|
ageOnJanuary1,
|
|
445
563
|
ageRelation,
|
|
564
|
+
ageTitleToken,
|
|
446
565
|
averageAge,
|
|
447
566
|
birthYearsDisjoint,
|
|
448
567
|
birthYearsMeet,
|