@finbheara/domain 0.30.0 → 0.32.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
@@ -16,6 +16,10 @@ person names.
16
16
  team's `averageAge`.
17
17
  - **Dances**: a spoken or printed set dance or céilí dance in, the dances it may name out,
18
18
  with tempo rules and hand counts.
19
+ - **Competition titles**: a spoken or printed title (`girls 16 to 17`, `U17 BOYS R3`) in, the
20
+ syllabus entries it names and its round out, read through `Age` on the entries' own ladder.
21
+ - **Schedules**: which entries, and which round, a hall runs at a time, and a printed running
22
+ order read into that shape.
19
23
  - **A database-backed index** of any of these nouns: the slots an in-memory name map hashes
20
24
  on, stored as indexed columns of a SQLite table you own.
21
25
 
@@ -45,6 +49,8 @@ ESM, Node ≥ 22 or Bun ≥ 1.4.2. MIT. Not affiliated with or endorsed by any d
45
49
  | `@finbheara/domain/dance-types` | `danceTypes()`, `DANCE_TYPES`, `rhythmOfTime`, and the shared `DanceMatch`/`DanceRelation`/`SetRhythm`/`TimeSignature` types |
46
50
  | `@finbheara/domain/promotion` | `samePromotedRow`, `samePromotedSchool`, `samePromotedHeir`, `samePromotedName`, `questionMarksRead`, `foldCharSynonyms`: is a row the same row after a promotion (below) |
47
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) |
53
+ | `@finbheara/domain/schedule` | `scheduleAt`, `sameHall`, `readRunningOrder`, the `ScheduleSlot` type: which entries a hall runs (below) |
48
54
 
49
55
  The library does no I/O and installs nothing process-wide. Every reader has a default and a
50
56
  factory: build your own with options when the default is not what you want.
@@ -122,13 +128,19 @@ geocode("Brooklyn").isPlace(); // false: a town of six US states and more, so
122
128
  // `Brooklyn, NY` reads
123
129
  place("Drogheda").arm; // "unread": the store holds no Drogheda
124
130
  geocode("Prague", { near: "RCCEA", declared: true }).isPlace();
125
- // false: the context lies outside the layer's
126
- // countries, so no namesake town is read
131
+ // false: the town lies outside the context, so
132
+ // no namesake town is read
133
+ geocode("Fife").isPlace(); // false: a town of Washington, and a council area
134
+ // of Scotland
127
135
  ```
128
136
 
129
- A context (a `country` or `near`) that reads to a place outside those six countries stops the
130
- layer for a bare town, so a city elsewhere is never read as its namesake in them. With no
131
- context `geocode("Prague")` is Prague, Oklahoma, the one Prague the layer holds. A print with a
137
+ The layer reads a bare town by its name alone, so it reads one only where nothing contradicts
138
+ it. A context (a `country` or `near`, read by the store, never as a town of the layer) that
139
+ reads to ONE place which neither holds the town nor shares its country stops the layer: `Prague`
140
+ near `RCCEA` and `Whitecross` under `Ireland` and `Ulster` read nothing. A context the store
141
+ reads nothing for, or reads as several places, stops nothing, so with no context
142
+ `geocode("Prague")` is still Prague, Oklahoma. A town named like a GeoNames admin2 area of those
143
+ six countries that does not hold it (`Fife`) reads only where a context places it. A print with a
132
144
  qualifier (`Yonkers, NY`) is never stopped: the context never overrules the print.
133
145
 
134
146
  A `Place` is one of four arms: `resolved`, `plural` (a print naming more than one place),
@@ -364,6 +376,63 @@ team.fitsIn(Age.from("U40", { year: 2026 })); // true
364
376
  averageAge([u12]); // throws AgeMisuseError "not-exact"
365
377
  ```
366
378
 
379
+ ## Match a competition title
380
+
381
+ An announcer names a competition by its title more often than by its number, and never quite
382
+ as the syllabus prints it. `matchTitle` reads a title's age band with `Age.fromTitle`, reads every
383
+ entry's band on the same ladder (the rungs the entries of the title's population offer), and
384
+ answers the entries whose band is the same and whose population (girls and ladies one line, boys
385
+ and men another) the title does not contradict. The entries are yours, as you hold them: a
386
+ `title`, an optional `age` where the syllabus prints the band apart, and anything else you keep.
387
+
388
+ ```ts
389
+ import { compTitle, matchTitle } from "@finbheara/domain/title";
390
+
391
+ const entries = [
392
+ { code: "27", title: "Girls 15 - 16", age: "15-16" },
393
+ { code: "28", title: "Girls 14 - 15", age: "14-15" },
394
+ { code: "29", title: "Girls 13 - 14", age: "13-14" },
395
+ { code: "8", title: "Boys 14 - 15", age: "14-15" },
396
+ ];
397
+ matchTitle("under 15 girls", entries).entries; // [{ code: "28", ... }]: a rung at 14
398
+ matchTitle("girls 14 to 15", entries).entries; // the same entry
399
+ matchTitle("boys 14-15 R2", entries).round; // 2
400
+
401
+ const t = compTitle("U17 BOYS R3");
402
+ t.age.ag1; // "u17"
403
+ t.population; // "boys"
404
+ t.round; // 3
405
+ compTitle("Girls Under 13 B").split; // "b"
406
+ compTitle("Treble Reel Special").invalidReason(); // "no-age"
407
+ ```
408
+
409
+ A band split into parallel competitions (`Girls Under 13 A`, `Girls Under 13 B`) answers every
410
+ split, unless the title prints the letter. A title naming no band names nothing. Hand in the
411
+ entries of the kind you mean: a solo championship's ages are often a team event's too.
412
+
413
+ ## Ask a schedule
414
+
415
+ A split band's competitions share their card numbers, so a card heard on a stage fits each of
416
+ them. Only the schedule says which split runs in which hall. A schedule is slots: an entry, a
417
+ round where one is printed, a hall, a day, and a start and an end in whatever clock you keep.
418
+
419
+ ```ts
420
+ import { readRunningOrder, scheduleAt } from "@finbheara/domain/schedule";
421
+
422
+ const slots = [
423
+ { hall: "Hall A", day: 1, start: 540, end: 660, round: 1, entry: girlsU13A },
424
+ { hall: "Hall B", day: 1, start: 540, end: 660, round: 1, entry: girlsU13B },
425
+ ];
426
+ scheduleAt(slots, { hall: "HALL B", day: 1, at: 600 }); // the B split
427
+
428
+ // a running order printed for one hall: titles cut after each round mark and at each comma,
429
+ // an age list sharing the population before it
430
+ readRunningOrder("U17 BOYS R3 U13 GIRLS B R1 & R2", entries, { hall: "Hall B", day: 2 });
431
+ ```
432
+
433
+ A slot with no times is its hall's whole day. `sameHall` reads `Hall A`, `HALL A` and `hall-a` as
434
+ one hall. The module reads no file: a full schedule, with its times, is yours to transcribe.
435
+
367
436
  ## Index names in a database
368
437
 
369
438
  `createNameMap` (from `@finbheara/names`) and `mapOfSchools` hold their names in memory. An