@finbheara/domain 0.6.2 → 0.8.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
@@ -265,12 +265,25 @@ Age.fromTitle("Junior Boys 12 Years").token; // "u13"
265
265
  Age.fromTitle("10-11 Single Jig").token; // "10-11", a band, not a number
266
266
  Age.fromTitle("Competition 001-013 - Reel").token; // "", a competition number
267
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
268
+ Age.fromTitle("Beginner Adults Reel U40").token; // "adult u40": the adult band under 40
269
+ Age.fromTitle("Solo Championship - Adult O40").token; // "adult o40": adult, and 40 or over
270
+ Age.fromTitle("Adult Championship 18 - 29").token; // "adult 18-29": adult, and the range
269
271
  Age.fromTitle("Adult / Child Reel Under 10").token; // "u10": no adult dances in it
272
+ Age.fromTitle("Senior Adult Ladies 19 & Over").token; // "o19": senior or adult is not settled
270
273
  ```
271
274
 
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.
275
+ An adult word beside an `Under N` above 18 reads both: `adult u40` is adult (`isAdult()`), with
276
+ the adult floor of 18 and a ceiling of 40, so `Age.from("adult u40", { year: 2026 })` is born
277
+ 1986 to 2007. At `Under 18` the ceiling meets the floor and the word alone is read. Below 18 the
278
+ youth band stands.
279
+
280
+ An adult word beside an and-over from 18 or a range from 18 reads both as well: `adult o40` and
281
+ `adult 18-29` are adult (`isAdult()`), and their `kind`, interval and birth years are the ones the
282
+ band alone reads on the same ladder, a range floored at 18 where no rung sits below it. So
283
+ `Age.from("adult o40", { year: 2026 })` is born 1985 or earlier, as `o40` is. An and-over under
284
+ 18 or a range opening under 18 (`Teen & Adult 13 & Over`) admits youth, and the band stands. A
285
+ title naming the senior rung beside the word (`Senior Adult Ladies 19 & Over`) reads the band
286
+ alone.
274
287
 
275
288
  An age group is the age on the first of January, so a title that admits the dancer who is
276
289
  `N` (`7 & Under`, `7/U`, `7 Years`) is the `Under N+1` group.
package/dist/age.js CHANGED
@@ -99,12 +99,22 @@ function ageTitleToken(title) {
99
99
  return words.token === "" && adultAside !== null ? adultAside : adultOverUnder(title, s, lead, words);
100
100
  }
101
101
  var ADULT_UNDER_MIN = 18;
102
+ var SENIOR_ADULT = /\bseniors?\s+adults?\b/i;
102
103
  function adultOverUnder(title, s, lead, t) {
104
+ const a = ADULT.exec(s);
105
+ if (a === null)
106
+ return t;
103
107
  const n = /^u(\d+)$/.exec(t.token)?.[1];
104
- if (n === undefined || Number(n) < ADULT_UNDER_MIN)
108
+ if (n !== undefined) {
109
+ if (Number(n) < ADULT_UNDER_MIN)
110
+ return t;
111
+ const token = Number(n) > ADULT_UNDER_MIN ? `adult u${n}` : "adult";
112
+ return at(title, lead, a.index, a.index + a[0].length, token);
113
+ }
114
+ const floor = /^o(\d+)$/.exec(t.token)?.[1] ?? /^(\d+)-\d+$/.exec(t.token)?.[1];
115
+ if (floor === undefined || Number(floor) < ADULT_UNDER_MIN || SENIOR_ADULT.test(s))
105
116
  return t;
106
- const a = ADULT.exec(s);
107
- return a === null ? t : at(title, lead, a.index, a.index + a[0].length, "adult");
117
+ return { token: `adult ${t.token}`, span: t.span };
108
118
  }
109
119
 
110
120
  var YOUNGEST_FLOOR = 0;
@@ -125,6 +135,8 @@ var OVER = /^(?:o|over\s*)(\d+)$/i;
125
135
  var AND_OVER2 = /^(\d+)\s*(?:&|and\b)\s*(?:over|o)$/i;
126
136
  var RANGE2 = /^u?(\d+)\s*[-–]\s*(\d+)$/i;
127
137
  var ADULT_WORD = /^(?:adults?|ad|advanced|masters?)$/i;
138
+ var ADULT_UNDER = /^(?:adults?|ad)[\s-]*(?:u|under\s*)(\d+)$/i;
139
+ var ADULT_BAND = /^(?:adults?|ad)[\s-]+(\S.*)$/i;
128
140
  var SENIOR_WORD = /^seniors?$/i;
129
141
  var NO_LADDER = Object.freeze({ caps: [], overs: [], ranges: [] });
130
142
  function maxWhere(xs, pred) {
@@ -182,7 +194,22 @@ function read(token, ladder, top) {
182
194
  const r = RANGE2.exec(token);
183
195
  if (r !== null)
184
196
  return readRange(Number(r[1]), Number(r[2]), ladder);
185
- if (ADULT_WORD.test(token))
197
+ const au = ADULT_UNDER.exec(token);
198
+ if (au !== null && Number(au[1]) > ADULT_FLOOR)
199
+ return {
200
+ kind: "adult",
201
+ interval: { lo: ADULT_FLOOR, hi: Number(au[1]) },
202
+ refusal: null,
203
+ clauses: ["adult-under-ceiling"],
204
+ parts: [Number(au[1])]
205
+ };
206
+ const ab = au === null ? ADULT_BAND.exec(token) : null;
207
+ if (ab !== null) {
208
+ const inner = read(ab[1].trim(), { ...ladder, adult: true }, top);
209
+ if (inner.kind === "over" || inner.kind === "range")
210
+ return { ...inner, clauses: [...inner.clauses, "adult-band"], adult: true };
211
+ }
212
+ if (au !== null || ADULT_WORD.test(token))
186
213
  return {
187
214
  kind: "adult",
188
215
  interval: { lo: ADULT_FLOOR, hi: Number.POSITIVE_INFINITY },
@@ -299,7 +326,7 @@ function fromPrint(print, context = {}) {
299
326
  return b.from !== null && b.from === b.to;
300
327
  },
301
328
  isAndOver: () => iv !== null && iv.hi === Number.POSITIVE_INFINITY,
302
- isAdult: () => reading.kind === "adult",
329
+ isAdult: () => reading.kind === "adult" || reading.adult === true,
303
330
  interval: () => iv,
304
331
  birthYears: (y) => {
305
332
  const at = y ?? year;
package/dist/school.js CHANGED
@@ -1019,6 +1019,13 @@ function createSchoolNameReader(options = {}) {
1019
1019
  const CP1252_HIGH = "€‚ƒ„…†‡ˆ‰Š‹Œ" + "Ž‘’“”•–—˜™š›" + "œžŸ";
1020
1020
  const BYTE_OF_CP1252 = new Map([...CP1252_HIGH].map((ch, i) => [ch.codePointAt(0), 128 + i]));
1021
1021
  const MOJIBAKE = /[Â-ßà-ï][€-¿‚-›ŒŠŽ]/u;
1022
+ const MOJIBAKE_ALL = new RegExp(MOJIBAKE.source, "gu");
1023
+ const NON_LATIN_LETTER = /(?!\p{Script=Latin})\p{L}/u;
1024
+ function isRepair(before, after) {
1025
+ if (!NON_LATIN_LETTER.test(after))
1026
+ return true;
1027
+ return (before.match(MOJIBAKE_ALL)?.length ?? 0) >= 2;
1028
+ }
1022
1029
  function undoMojibake(s) {
1023
1030
  const bytes = new Uint8Array(s.length);
1024
1031
  for (let i = 0;i < s.length; i++) {
@@ -1038,7 +1045,7 @@ function createSchoolNameReader(options = {}) {
1038
1045
  let s = print;
1039
1046
  for (let round = 0;round < 3 && MOJIBAKE.test(s); round++) {
1040
1047
  const undone = undoMojibake(s);
1041
- if (undone === undefined || undone === s)
1048
+ if (undone === undefined || undone === s || !isRepair(s, undone))
1042
1049
  return s;
1043
1050
  s = undone;
1044
1051
  }
@@ -25,7 +25,7 @@ export type AgeArm = "band" | "yob" | "open" | "empty";
25
25
  export type AgeKind = "under" | "over" | "range" | "adult" | "senior" | "yob";
26
26
  export type AgeRefusal = "not-a-band" | "backwards-range" | "overlapping-ranges" | "senior-without-ladder" | "below-youngest-band" | "year-out-of-range";
27
27
  export type AgeSource = "sibling-narrowed" | "printed-band" | "printed-range" | "printed-yob" | "open";
28
- export type AgeClause = "under-floored-on-rung" | "youngest-absorbs" | "over-ceiling-from-next-over" | "over-ceiling-refused-gap" | "and-over-no-ceiling" | "range-overlapping" | "range-as-under" | "range-top-meets-over" | "range-adult-floor";
28
+ export type AgeClause = "under-floored-on-rung" | "youngest-absorbs" | "over-ceiling-from-next-over" | "over-ceiling-refused-gap" | "and-over-no-ceiling" | "adult-under-ceiling" | "adult-band" | "range-overlapping" | "range-as-under" | "range-top-meets-over" | "range-adult-floor";
29
29
  export type AgeRelation = "same" | "contains" | "overlaps" | "disjoint" | "silent";
30
30
  export type AgeRelationReason = "equal" | "a-contains-b" | "b-contains-a" | "partial" | "a-older" | "a-younger" | "a-not-a-band" | "b-not-a-band";
31
31
  export interface AgeComparison {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@finbheara/domain",
3
- "version": "0.6.2",
3
+ "version": "0.8.0",
4
4
  "description": "Places, CLRG regions, school names, ages and dances as they are printed: read, compared and told apart",
5
5
  "keywords": [
6
6
  "places",
@@ -73,6 +73,6 @@
73
73
  "prepublishOnly": "bun scripts/build.ts && bun scripts/guard-tarball.ts"
74
74
  },
75
75
  "dependencies": {
76
- "@finbheara/names": "0.18.0"
76
+ "@finbheara/names": "0.20.0"
77
77
  }
78
78
  }