@finbheara/domain 0.6.2 → 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 CHANGED
@@ -265,12 +265,14 @@ 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
269
  Age.fromTitle("Adult / Child Reel Under 10").token; // "u10": no adult dances in it
270
270
  ```
271
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.
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.
274
276
 
275
277
  An age group is the age on the first of January, so a title that admits the dancer who is
276
278
  `N` (`7 & Under`, `7/U`, `7 Years`) is the `Under N+1` group.
package/dist/age.js CHANGED
@@ -104,7 +104,8 @@ function adultOverUnder(title, s, lead, t) {
104
104
  if (n === undefined || Number(n) < ADULT_UNDER_MIN)
105
105
  return t;
106
106
  const a = ADULT.exec(s);
107
- return a === null ? t : at(title, lead, a.index, a.index + a[0].length, "adult");
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);
108
109
  }
109
110
 
110
111
  var YOUNGEST_FLOOR = 0;
@@ -125,6 +126,7 @@ var OVER = /^(?:o|over\s*)(\d+)$/i;
125
126
  var AND_OVER2 = /^(\d+)\s*(?:&|and\b)\s*(?:over|o)$/i;
126
127
  var RANGE2 = /^u?(\d+)\s*[-–]\s*(\d+)$/i;
127
128
  var ADULT_WORD = /^(?:adults?|ad|advanced|masters?)$/i;
129
+ var ADULT_UNDER = /^(?:adults?|ad)[\s-]*(?:u|under\s*)(\d+)$/i;
128
130
  var SENIOR_WORD = /^seniors?$/i;
129
131
  var NO_LADDER = Object.freeze({ caps: [], overs: [], ranges: [] });
130
132
  function maxWhere(xs, pred) {
@@ -182,7 +184,16 @@ function read(token, ladder, top) {
182
184
  const r = RANGE2.exec(token);
183
185
  if (r !== null)
184
186
  return readRange(Number(r[1]), Number(r[2]), ladder);
185
- if (ADULT_WORD.test(token))
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))
186
197
  return {
187
198
  kind: "adult",
188
199
  interval: { lo: ADULT_FLOOR, hi: Number.POSITIVE_INFINITY },
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" | "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.7.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",