@finbheara/domain 0.5.0 → 0.7.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 +43 -2
- package/dist/age.js +155 -25
- package/dist/chunks/{place-0j6k50wf.js → place-h7tvdfyp.js} +548 -29
- package/dist/place.js +1 -1
- package/dist/promotion.js +22 -2
- package/dist/school.js +20 -129
- package/dist/types/age-title.d.ts +12 -0
- package/dist/types/age-types.d.ts +5 -3
- package/dist/types/age.d.ts +11 -2
- package/dist/types/place-types.d.ts +5 -2
- package/dist/types/promotion.d.ts +2 -0
- package/dist/types/school-location.d.ts +1 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -39,7 +39,7 @@ ESM, Node ≥ 22 or Bun ≥ 1.4.2. MIT. Not affiliated with or endorsed by any d
|
|
|
39
39
|
| `@finbheara/domain/tune` | `tune()`, `tuneRelation`, `tempoLegal`, `settingAt`, `TUNE_NODES`: the CLRG set dance list, 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`, `validYOB`, the age constants |
|
|
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) |
|
|
@@ -56,7 +56,7 @@ questions:
|
|
|
56
56
|
| method | true when |
|
|
57
57
|
|---|---|
|
|
58
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, `U2`, a `1900` year of birth |
|
|
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
60
|
| `isValid()` | neither: `!isEmpty() && !isRefused()` |
|
|
61
61
|
|
|
62
62
|
`invalidReason()` is `null` when the value is valid, `"empty"` when it is empty, and the
|
|
@@ -71,6 +71,7 @@ Valid does not mean usable. A print the reader does not know but cannot rule out
|
|
|
71
71
|
place("").invalidReason(); // "empty"
|
|
72
72
|
place("n/a").invalidReason(); // "placeholder"
|
|
73
73
|
place("Ardmore").isValid(); // true: valid, unknown
|
|
74
|
+
place("Video Feis").invalidReason(); // "online"
|
|
74
75
|
DEFAULT_SCHOOL_NAME_READER.schoolName("N/A").isRefused(); // true
|
|
75
76
|
Age.fromYOB(1900).invalidReason(); // "year-out-of-range"
|
|
76
77
|
tune("").isEmpty(); // true
|
|
@@ -102,6 +103,13 @@ A `Place` is one of four arms: `resolved`, `plural` (a print naming more than on
|
|
|
102
103
|
`ambiguous` (one print, several readings) and `unread`. Only `conflict` between two readings
|
|
103
104
|
says they cannot be one place; `same`, `contains` and `overlaps` do not.
|
|
104
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
|
+
|
|
105
113
|
## Find a CLRG region
|
|
106
114
|
|
|
107
115
|
```ts
|
|
@@ -234,8 +242,41 @@ Age.fromYOB(1999).fitsIn(Age.from("U18", { year: 2025 })); // fals
|
|
|
234
242
|
Age.fromYOB(1999).fitsIn(Age.from("Over 21", { year: 2026, top: true })); // true
|
|
235
243
|
|
|
236
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"
|
|
247
|
+
```
|
|
248
|
+
|
|
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 u40": the adult band under 40
|
|
269
|
+
Age.fromTitle("Adult / Child Reel Under 10").token; // "u10": no adult dances in it
|
|
237
270
|
```
|
|
238
271
|
|
|
272
|
+
An adult word beside an `Under N` above 18 reads both: `adult u40` is adult (`isAdult()`), with
|
|
273
|
+
the adult floor of 18 and a ceiling of 40, so `Age.from("adult u40", { year: 2026 })` is born
|
|
274
|
+
1986 to 2007. At `Under 18` the ceiling meets the floor and the word alone is read. Below 18 the
|
|
275
|
+
youth band stands.
|
|
276
|
+
|
|
277
|
+
An age group is the age on the first of January, so a title that admits the dancer who is
|
|
278
|
+
`N` (`7 & Under`, `7/U`, `7 Years`) is the `Under N+1` group.
|
|
279
|
+
|
|
239
280
|
A value the reader cannot stand behind is returned, not thrown. An `Age` is `Validated` (above):
|
|
240
281
|
an age group under `U3` admits nobody old enough to dance, and a year of birth outside 1901 to
|
|
241
282
|
2099 is not one, so the placeholders `1000` and `1900` are refused. An invalid `Age` has no
|
package/dist/age.js
CHANGED
|
@@ -2,6 +2,112 @@ import {
|
|
|
2
2
|
validity
|
|
3
3
|
} from "./chunks/place-q9kwsmzd.js";
|
|
4
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
|
+
const token = Number(n) > ADULT_UNDER_MIN ? `adult u${n}` : "adult";
|
|
108
|
+
return a === null ? t : at(title, lead, a.index, a.index + a[0].length, token);
|
|
109
|
+
}
|
|
110
|
+
|
|
5
111
|
var YOUNGEST_FLOOR = 0;
|
|
6
112
|
var ADULT_FLOOR = 18;
|
|
7
113
|
var MAJORS_FLOOR_YEARS = 3;
|
|
@@ -15,10 +121,12 @@ var YOB_RANGE = Object.freeze({ first: 1901, last: 2099 });
|
|
|
15
121
|
var DANCE_UP_YEARS = 1;
|
|
16
122
|
var ageOnJanuary1 = (year, born) => year - born - 1;
|
|
17
123
|
var yearsSinceBirth = (year, born) => year - born;
|
|
18
|
-
var
|
|
124
|
+
var UNDER2 = /^(?:u|under\s*)(\d+)$/i;
|
|
19
125
|
var OVER = /^(?:o|over\s*)(\d+)$/i;
|
|
20
|
-
var
|
|
126
|
+
var AND_OVER2 = /^(\d+)\s*(?:&|and\b)\s*(?:over|o)$/i;
|
|
127
|
+
var RANGE2 = /^u?(\d+)\s*[-–]\s*(\d+)$/i;
|
|
21
128
|
var ADULT_WORD = /^(?:adults?|ad|advanced|masters?)$/i;
|
|
129
|
+
var ADULT_UNDER = /^(?:adults?|ad)[\s-]*(?:u|under\s*)(\d+)$/i;
|
|
22
130
|
var SENIOR_WORD = /^seniors?$/i;
|
|
23
131
|
var NO_LADDER = Object.freeze({ caps: [], overs: [], ranges: [] });
|
|
24
132
|
function maxWhere(xs, pred) {
|
|
@@ -44,32 +152,48 @@ function readRange(a, b, ladder) {
|
|
|
44
152
|
clauses: [],
|
|
45
153
|
parts: [a, b]
|
|
46
154
|
};
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
155
|
+
if (ladder.ranges.some(([c, d]) => (c !== a || d !== b) && c < b && a < d))
|
|
156
|
+
return {
|
|
157
|
+
kind: "range",
|
|
158
|
+
interval: null,
|
|
159
|
+
refusal: "overlapping-ranges",
|
|
160
|
+
clauses: ["range-overlapping"],
|
|
161
|
+
parts: [a, b]
|
|
162
|
+
};
|
|
163
|
+
const below = maxWhere(ladder.caps, (c) => c < b);
|
|
164
|
+
const adultFloor = below === undefined && ladder.adult === true && b > ADULT_FLOOR;
|
|
165
|
+
const meetsOver = ladder.overs.includes(b + 1) && !ladder.caps.some((c) => c > b);
|
|
166
|
+
const clauses = [
|
|
167
|
+
"range-as-under",
|
|
168
|
+
below !== undefined ? "under-floored-on-rung" : adultFloor ? "range-adult-floor" : "youngest-absorbs"
|
|
169
|
+
];
|
|
170
|
+
if (meetsOver)
|
|
171
|
+
clauses.push("range-top-meets-over");
|
|
60
172
|
return {
|
|
61
173
|
kind: "range",
|
|
62
|
-
interval: {
|
|
174
|
+
interval: {
|
|
175
|
+
lo: below ?? (adultFloor ? ADULT_FLOOR : YOUNGEST_FLOOR),
|
|
176
|
+
hi: meetsOver ? b + 1 : b
|
|
177
|
+
},
|
|
63
178
|
refusal: null,
|
|
64
179
|
clauses,
|
|
65
180
|
parts: [a, b]
|
|
66
181
|
};
|
|
67
182
|
}
|
|
68
183
|
function read(token, ladder, top) {
|
|
69
|
-
const r =
|
|
184
|
+
const r = RANGE2.exec(token);
|
|
70
185
|
if (r !== null)
|
|
71
186
|
return readRange(Number(r[1]), Number(r[2]), ladder);
|
|
72
|
-
|
|
187
|
+
const au = ADULT_UNDER.exec(token);
|
|
188
|
+
if (au !== null && Number(au[1]) > ADULT_FLOOR)
|
|
189
|
+
return {
|
|
190
|
+
kind: "adult",
|
|
191
|
+
interval: { lo: ADULT_FLOOR, hi: Number(au[1]) },
|
|
192
|
+
refusal: null,
|
|
193
|
+
clauses: ["adult-under-ceiling"],
|
|
194
|
+
parts: [Number(au[1])]
|
|
195
|
+
};
|
|
196
|
+
if (au !== null || ADULT_WORD.test(token))
|
|
73
197
|
return {
|
|
74
198
|
kind: "adult",
|
|
75
199
|
interval: { lo: ADULT_FLOOR, hi: Number.POSITIVE_INFINITY },
|
|
@@ -87,7 +211,7 @@ function read(token, ladder, top) {
|
|
|
87
211
|
parts: []
|
|
88
212
|
};
|
|
89
213
|
}
|
|
90
|
-
const u =
|
|
214
|
+
const u = UNDER2.exec(token);
|
|
91
215
|
if (u !== null) {
|
|
92
216
|
const n = Number(u[1]);
|
|
93
217
|
const below = maxWhere(ladder.caps, (c) => c < n);
|
|
@@ -99,7 +223,7 @@ function read(token, ladder, top) {
|
|
|
99
223
|
parts: [n]
|
|
100
224
|
};
|
|
101
225
|
}
|
|
102
|
-
const o = OVER.exec(token);
|
|
226
|
+
const o = OVER.exec(token) ?? AND_OVER2.exec(token);
|
|
103
227
|
if (o !== null) {
|
|
104
228
|
const n = Number(o[1]);
|
|
105
229
|
const above = top ? undefined : minWhere(ladder.overs, (c) => c > n);
|
|
@@ -194,7 +318,7 @@ function fromPrint(print, context = {}) {
|
|
|
194
318
|
throw new Error(`Age.from(${JSON.stringify(print)}): birthYears needs a competition year`);
|
|
195
319
|
return birthYearsOfInterval(iv, at);
|
|
196
320
|
},
|
|
197
|
-
floorObserved: () => reading.kind !== "under" || iv === null ? null : reading.clauses.includes("under-floored-on-rung")
|
|
321
|
+
floorObserved: () => reading.kind !== "under" && reading.kind !== "range" || iv === null ? null : reading.clauses.includes("under-floored-on-rung") || reading.clauses.includes("range-adult-floor")
|
|
198
322
|
});
|
|
199
323
|
}
|
|
200
324
|
function fromYOB(from, to = from, context = {}) {
|
|
@@ -237,21 +361,24 @@ function fromYOB(from, to = from, context = {}) {
|
|
|
237
361
|
floorObserved: () => null
|
|
238
362
|
});
|
|
239
363
|
}
|
|
240
|
-
|
|
364
|
+
function fromTitle(title, context = {}) {
|
|
365
|
+
const t = ageTitleToken(title);
|
|
366
|
+
return Object.freeze({ age: fromPrint(t.token, context), token: t.token, span: t.span });
|
|
367
|
+
}
|
|
368
|
+
var Age = Object.freeze({ from: fromPrint, fromYOB, fromTitle });
|
|
241
369
|
function sourceOf(r) {
|
|
242
370
|
const iv = r.interval;
|
|
243
371
|
if (iv === null)
|
|
244
372
|
return "open";
|
|
245
373
|
switch (r.kind) {
|
|
246
374
|
case "range":
|
|
247
|
-
|
|
375
|
+
case "under":
|
|
376
|
+
return iv.lo > YOUNGEST_FLOOR ? "sibling-narrowed" : "printed-band";
|
|
248
377
|
case "over":
|
|
249
378
|
return iv.hi === Number.POSITIVE_INFINITY ? "printed-band" : "sibling-narrowed";
|
|
250
379
|
case "adult":
|
|
251
380
|
case "senior":
|
|
252
381
|
return "printed-band";
|
|
253
|
-
case "under":
|
|
254
|
-
return iv.lo > YOUNGEST_FLOOR ? "sibling-narrowed" : "printed-band";
|
|
255
382
|
default:
|
|
256
383
|
return "open";
|
|
257
384
|
}
|
|
@@ -435,6 +562,8 @@ export {
|
|
|
435
562
|
MAJORS_FLOOR_YEARS,
|
|
436
563
|
MIN_FEIS_AGE,
|
|
437
564
|
MIN_MAJOR_AGE,
|
|
565
|
+
TITLE_RANGE_MAX,
|
|
566
|
+
TITLE_RANGE_MIN,
|
|
438
567
|
UNDER_CAP,
|
|
439
568
|
UNDER_CAP_MAX_TOP,
|
|
440
569
|
YOB_RANGE,
|
|
@@ -443,6 +572,7 @@ export {
|
|
|
443
572
|
YOUNG_FLOOR,
|
|
444
573
|
ageOnJanuary1,
|
|
445
574
|
ageRelation,
|
|
575
|
+
ageTitleToken,
|
|
446
576
|
averageAge,
|
|
447
577
|
birthYearsDisjoint,
|
|
448
578
|
birthYearsMeet,
|