@finbheara/domain 0.47.0 → 0.49.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
@@ -49,7 +49,7 @@ ESM, Node ≥ 22 or Bun ≥ 1.4.2. MIT. Not affiliated with or endorsed by any d
49
49
  | `@finbheara/domain/dance-types` | `danceTypes()`, `DANCE_TYPES`, `rhythmOfTime`, and the shared `DanceMatch`/`DanceRelation`/`SetRhythm`/`TimeSignature` types |
50
50
  | `@finbheara/domain/promotion` | `samePromotedRow`, `samePromotedSchool`, `samePromotedHeir`, `samePromotedName`, `questionMarksRead`, `foldCharSynonyms`: is a row the same row after a promotion (below) |
51
51
  | `@finbheara/domain/person` | `seatName`, `dancerName`, `teacherName`, `roleName`, `roleSentinelOf`, `rolePrintsNoName`, `dancerPrintsNoName`, the role lists and `SHARED_ROW_SENTINELS`: a person's print read in its role (below) |
52
- | `@finbheara/domain/title` | `compTitle`, `titleRelation`, `matchTitle`, `ladderOf`, `populationIn`, `populationRelation`, `roundNumbersIn`: a competition title read and matched onto syllabus entries (below) |
52
+ | `@finbheara/domain/title` | `compTitle`, `titleRelation`, `matchTitle`, `ladderOf`, `populationIn`, `populationRelation`, `roundNumbersIn`, `codePopulation`, `secondIn`: a competition title read and matched onto syllabus entries, a code's sex suffix, a second chance (below) |
53
53
  | `@finbheara/domain/schedule` | `scheduleAt`, `sameHall`, `readRunningOrder`, the `ScheduleSlot` type: which entries a hall runs (below) |
54
54
 
55
55
  The library does no I/O and installs nothing process-wide. Every reader has a default and a
@@ -302,6 +302,12 @@ canOverlap(u12, Age.from("U14", { year: 2026 }), dancer); // tru
302
302
  Age.from("13 & Over", { year: 2026 }).interval(); // { lo: 13, hi: Infinity }, as "Over 13"
303
303
  ```
304
304
 
305
+ A range is another way of writing the `Under` band at its top: `11-13` reads as `U13`, and its
306
+ low number bounds nothing, since a youth ladder's range names the rung and not every age in it.
307
+ A range printed from 18 up is an age bracket, and its low number is its floor:
308
+ `Age.from("24-30", { year: 2026 })` is ages 24 to 29, born 1996 to 2001. A range that prints no
309
+ floor of 18 or more, on a ladder that offered the adult rung, floors at 18.
310
+
305
311
  A competition title usually carries its age group in its words. `Age.fromTitle` finds the
306
312
  phrase, reads it as `Age.from` would, and says where it was. A title that names no age group
307
313
  is empty, not refused. Competition numbers are never read as ages.
@@ -341,7 +347,7 @@ youth band stands.
341
347
 
342
348
  An adult word beside an and-over from 18 or a range from 18 reads both as well: `adult o40` and
343
349
  `adult 18-29` are adult (`isAdult()`), and their `kind`, interval and birth years are the ones the
344
- band alone reads on the same ladder, a range floored at 18 where no rung sits below it. So
350
+ band alone reads on the same ladder. So
345
351
  `Age.from("adult o40", { year: 2026 })` is born 1985 or earlier, as `o40` is. An and-over under
346
352
  18 or a range opening under 18 (`Teen & Adult 13 & Over`) admits youth, and the band stands. TIDA's
347
353
  `Senior Adult` beside an and-over is its oldest youth championship, an open championship that
@@ -424,6 +430,23 @@ A band split into parallel competitions (`Girls Under 13 A`, `Girls Under 13 B`)
424
430
  split, unless the title prints the letter. A title naming no band names nothing. Hand in the
425
431
  entries of the kind you mean: a solo championship's ages are often a team event's too.
426
432
 
433
+ A code may carry the sex where the title does not. `codePopulation` reads a suffix against every
434
+ code the same event printed, as a letter alone also marks sections and merges: `B` beside `G` of
435
+ the same code (and no `A` or `C` of it) is the boys' and the girls' half, and a trailing `M` is
436
+ boys only beside the code without it. `secondIn` says whether a print names a second chance or a
437
+ supplementary competition beside the main one, from the words alone (`2nd Chance`, `2nd Chc`,
438
+ `Second Chance`, `Supplementary`, `2nd` before `Open` or `Prelim`), never from a code's digit.
439
+
440
+ ```ts
441
+ import { codePopulation, secondIn } from "@finbheara/domain/title";
442
+
443
+ codePopulation("720OCB", ["720OCB", "720OCG"]); // "boys"
444
+ codePopulation("512M", ["512", "512M"]); // "boys"
445
+ codePopulation("713OCM", ["713OCM", "714OC"]); // "": alone, it may be a merge
446
+ secondIn("Preliminary Champion, Supplementary Prelim (U15)"); // true
447
+ secondIn("Preliminary Champion, Hard Shoe (U10)"); // false
448
+ ```
449
+
427
450
  ## Ask a schedule
428
451
 
429
452
  A split band's competitions share their card numbers, so a card heard on a stage fits each of
package/dist/age.js CHANGED
@@ -39,7 +39,7 @@ import {
39
39
  youngFloor2,
40
40
  tooYoungForMajor2,
41
41
  tooYoungForFeis2
42
- } from "./chunks/place-mpeps8tn.js";
42
+ } from "./chunks/place-kmh59zkw.js";
43
43
  export {
44
44
  ADULT_FLOOR2 as ADULT_FLOOR,
45
45
  AVERAGE_AGE_DECIMALS2 as AVERAGE_AGE_DECIMALS,
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  Age2,
3
3
  ageRelation2
4
- } from "./place-mpeps8tn.js";
4
+ } from "./place-kmh59zkw.js";
5
5
  import {
6
6
  validity
7
7
  } from "./place-q9kwsmzd.js";
@@ -24,6 +24,30 @@ function populationIn2(text) {
24
24
  return "boys";
25
25
  return "";
26
26
  }
27
+ function codePopulation2(code, codes) {
28
+ const c = code.trim().toUpperCase();
29
+ const all = new Set;
30
+ for (const x of codes)
31
+ all.add(x.trim().toUpperCase());
32
+ all.add(c);
33
+ if (!/\d/.test(c))
34
+ return "";
35
+ const pair = /^(.*\d[A-Z]*?)([BG])$/.exec(c);
36
+ if (pair !== null) {
37
+ const [, stem, letter] = pair;
38
+ const other = letter === "B" ? "G" : "B";
39
+ if (all.has(stem + other) && !all.has(`${stem}A`) && !all.has(`${stem}C`))
40
+ return letter === "B" ? "boys" : "girls";
41
+ }
42
+ const male = /^(.*\d[A-Z]*?)-?M$/.exec(c);
43
+ if (male !== null && all.has(male[1]))
44
+ return "boys";
45
+ return "";
46
+ }
47
+ var SECOND_WORDS = /\b(?:(?:2nd|second)[\s-]*ch(?:ance|c)?\b|supplementary\b|(?:2nd|second)\s+(?:open|prelim|preliminary)\b)/;
48
+ function secondIn2(text) {
49
+ return SECOND_WORDS.test(toAscii(text).toLowerCase());
50
+ }
27
51
  function roundNumbersIn2(folded, printed) {
28
52
  const out = new Set;
29
53
  for (const m of folded.matchAll(/ rounds? ((?:\d+ (?:and )?)+)/g))
@@ -136,4 +160,4 @@ function matchTitle2(print, entries, ctx = {}) {
136
160
  });
137
161
  }
138
162
 
139
- export { populationIn2, roundNumbersIn2, compTitle2, populationRelation2, titleRelation2, ladderOf2, matchTitle2 };
163
+ export { populationIn2, codePopulation2, secondIn2, roundNumbersIn2, compTitle2, populationRelation2, titleRelation2, ladderOf2, matchTitle2 };
@@ -242,12 +242,14 @@ function readRange(a, b, ladder) {
242
242
  clauses: ["range-overlapping"],
243
243
  parts: [a, b]
244
244
  };
245
- const below = maxWhere(ladder.caps, (c) => c < b);
245
+ const capBelow = maxWhere(ladder.caps, (c) => c < b);
246
+ const printedFloor = a >= ADULT_FLOOR2 && (capBelow === undefined || capBelow <= a);
247
+ const below = printedFloor ? a : capBelow;
246
248
  const adultFloor = below === undefined && ladder.adult === true && b > ADULT_FLOOR2;
247
249
  const meetsOver = ladder.overs.includes(b + 1) && !ladder.caps.some((c) => c > b);
248
250
  const clauses = [
249
251
  "range-as-under",
250
- below !== undefined ? "under-floored-on-rung" : adultFloor ? "range-adult-floor" : "youngest-absorbs"
252
+ printedFloor ? "range-printed-floor" : below !== undefined ? "under-floored-on-rung" : adultFloor ? "range-adult-floor" : "youngest-absorbs"
251
253
  ];
252
254
  if (meetsOver)
253
255
  clauses.push("range-top-meets-over");
@@ -429,7 +431,7 @@ function fromPrint(print, context = {}) {
429
431
  throw new Error(`Age.from(${JSON.stringify(print)}): birthYears needs a competition year`);
430
432
  return birthYearsOfInterval2(iv, at);
431
433
  },
432
- 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")
434
+ 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("range-printed-floor") || reading.clauses.includes("under-stride")
433
435
  });
434
436
  }
435
437
  function fromYOB(from, to = from, context = {}) {
@@ -484,7 +486,7 @@ function sourceOf(r) {
484
486
  switch (r.kind) {
485
487
  case "range":
486
488
  case "under":
487
- if (r.clauses.includes("under-stride"))
489
+ if (r.clauses.includes("under-stride") || r.clauses.includes("range-printed-floor"))
488
490
  return "printed-band";
489
491
  return iv.lo > YOUNGEST_FLOOR2 ? "sibling-narrowed" : "printed-band";
490
492
  case "over":
package/dist/schedule.js CHANGED
@@ -1,8 +1,8 @@
1
- import"./chunks/place-mpeps8tn.js";
1
+ import"./chunks/place-kmh59zkw.js";
2
2
  import {
3
3
  compTitle2,
4
4
  matchTitle2
5
- } from "./chunks/place-y66f4138.js";
5
+ } from "./chunks/place-k0p9af4f.js";
6
6
 
7
7
  function hallKey(hall) {
8
8
  return hall.normalize("NFKD").replace(/\p{M}/gu, "").toLowerCase().replace(/[^a-z0-9]+/g, " ").trim();
package/dist/title.js CHANGED
@@ -1,19 +1,23 @@
1
1
  import {
2
2
  populationIn2,
3
+ codePopulation2,
4
+ secondIn2,
3
5
  roundNumbersIn2,
4
6
  compTitle2,
5
7
  populationRelation2,
6
8
  titleRelation2,
7
9
  ladderOf2,
8
10
  matchTitle2
9
- } from "./chunks/place-y66f4138.js";
10
- import"./chunks/place-mpeps8tn.js";
11
+ } from "./chunks/place-k0p9af4f.js";
12
+ import"./chunks/place-kmh59zkw.js";
11
13
  export {
14
+ codePopulation2 as codePopulation,
12
15
  compTitle2 as compTitle,
13
16
  ladderOf2 as ladderOf,
14
17
  matchTitle2 as matchTitle,
15
18
  populationIn2 as populationIn,
16
19
  populationRelation2 as populationRelation,
17
20
  roundNumbersIn2 as roundNumbersIn,
21
+ secondIn2 as secondIn,
18
22
  titleRelation2 as titleRelation
19
23
  };
@@ -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" | "above-oldest-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" | "under-stride" | "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";
28
+ export type AgeClause = "under-floored-on-rung" | "under-stride" | "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" | "range-printed-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 {
@@ -5,6 +5,8 @@ export type { EmptyReason, Validated } from "./validated.ts";
5
5
  export type { AgeComparison, AgeLadder } from "./age.ts";
6
6
  export type Population = "girls" | "boys" | "mixed";
7
7
  export declare function populationIn(text: string): Population | "";
8
+ export declare function codePopulation(code: string, codes: Iterable<string>): Population | "";
9
+ export declare function secondIn(text: string): boolean;
8
10
  export declare function roundNumbersIn(folded: string, printed: boolean): Set<number>;
9
11
  export interface TitleContext {
10
12
  readonly year?: number | null;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@finbheara/domain",
3
- "version": "0.47.0",
3
+ "version": "0.49.0",
4
4
  "description": "Places, CLRG regions, school names, ages, dances, competition titles and schedules, and a person's print in its role, as they are printed: read, compared and told apart",
5
5
  "keywords": [
6
6
  "places",