@finbheara/domain 0.49.0 → 0.51.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,8 @@ 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`, `codePopulation`, `secondIn`: a competition title read and matched onto syllabus entries, a code's sex suffix, a second chance (below) |
52
+ | `@finbheara/domain/title` | `compTitle`, `titleRelation`, `matchTitle`, `ladderOf`, `populationIn`, `populationRelation`, `roundNumbersIn`, `roundFold`, `codePopulation`, `secondIn`: a competition title read and matched onto syllabus entries, a code's sex suffix, a second chance (below) |
53
+ | `@finbheara/domain/code` | `createCodeReader`, `compCode`, `DEFAULT_CODE_READER`, the `CompCode` type: a competition code read at the feis it belongs to, from that feis's syllabus or, on a platform you name as standardized, from the code alone (below) |
53
54
  | `@finbheara/domain/schedule` | `scheduleAt`, `sameHall`, `readRunningOrder`, the `ScheduleSlot` type: which entries a hall runs (below) |
54
55
 
55
56
  The library does no I/O and installs nothing process-wide. Every reader has a default and a
@@ -426,6 +427,13 @@ compTitle("Treble Reel Special").invalidReason(); // "no-age"
426
427
  A spoken dash is read as a space: `boys u-17`, `Boys U-17` and `boys u- 17` read as `boys u17`,
427
428
  `girls o-16` as `girls o16`, and `under-15` as `under 15`.
428
429
 
430
+ A count printed before `Rounds` is how many rounds are danced, and the number after it is the
431
+ band: `Ladies Champion 3 Rounds 18 & Over` reads `[18,∞)` and no round, and `2 Rounds 1 & 2`
432
+ lists the two rounds it counts. A number an and-over closes (`18 & Over`, `18&O`, `18+`) is the
433
+ band, so `Round 2 - 18&O` is round 2, and so is the number after an and-over word (`Rounds 1-3
434
+ Over 18` is rounds 1 and 3). An and-over word with no connector pops nothing: `Round 2 Plus` is
435
+ round 2.
436
+
429
437
  A band split into parallel competitions (`Girls Under 13 A`, `Girls Under 13 B`) answers every
430
438
  split, unless the title prints the letter. A title naming no band names nothing. Hand in the
431
439
  entries of the kind you mean: a solo championship's ages are often a team event's too.
@@ -447,6 +455,50 @@ secondIn("Preliminary Champion, Supplementary Prelim (U15)"); // true
447
455
  secondIn("Preliminary Champion, Hard Shoe (U10)"); // false
448
456
  ```
449
457
 
458
+ ## Read a competition code
459
+
460
+ A code is a feis's own number for a competition. Some platforms number every feis by one
461
+ grammar: a level digit (`1` first feis to `7` open championship), two digits for the age column
462
+ (fifty more for a boys' column, `99` for adults) and letters for the dance, so `512SJ` is a
463
+ prizewinner slip jig under 12 wherever it is printed. Most platforms let each feis number as it
464
+ likes, and the same `344` is a novice under 11 at one feis and a beginner reel at the next. Read
465
+ with the grammar, such a code gives an age and a level that look right and are wrong.
466
+
467
+ So a code is read only at a feis. `read` takes the platform and the feis's address or slug, and
468
+ answers from the feis's syllabus where you hand it in, on any platform. With no syllabus, only a
469
+ platform you name as standardized reads its code alone. Everything else is refused, with its
470
+ reason: `no-feis`, `feis-specific`, `not-in-syllabus`, `two-entries` or `not-a-code`. An empty
471
+ `syllabus: []` is no syllabus, never a syllabus that prints no code. The default
472
+ reader names no platform, so with no configuration a code reads only from a syllabus. A title is
473
+ not a code: `compTitle` still reads it at any feis.
474
+
475
+ ```ts
476
+ import { createCodeReader } from "@finbheara/domain/code";
477
+
478
+ const codes = createCodeReader({ standardized: ["examplefeis"] });
479
+
480
+ const c = codes.read("512SJ", { platform: "examplefeis", feis: "ex:1001" });
481
+ c.age.ag1; // "u12"
482
+ c.level; // 500
483
+ c.source; // "code"
484
+
485
+ codes.read("344", { platform: "other", feis: "x:ABC26" }).invalidReason(); // "feis-specific"
486
+ codes.read("512SJ", { platform: "examplefeis", feis: null }).invalidReason(); // "no-feis"
487
+
488
+ const syllabus = [{ code: "344", title: "Novice Girls Under 11 Light Jig" }];
489
+ const s = codes.read("344", { platform: "other", feis: "x:ABC26", syllabus });
490
+ s.age.ag1; // "u11"
491
+ s.entry; // the entry: read its level yourself
492
+ ```
493
+
494
+ The level comes from the grammar's level digit only. A set, a treble reel, a figure and a future
495
+ champions competition run off the grade ladder and read no level, and a syllabus answer reads none,
496
+ on a standardized platform too (the syllabus wins, and the package reads no level word): the
497
+ matching `entry` comes back for you to read. The oldest
498
+ column a feis offers in one series is its and-over (`719OC` is 18 and over where it is the top
499
+ open column). Hand in every code the feis printed (`codes`) to read the top column and a level
500
+ split by sex. Without them a column reads as its `Under` band and who dances is not said.
501
+
450
502
  ## Ask a schedule
451
503
 
452
504
  A split band's competitions share their card numbers, so a card heard on a stage fits each of
@@ -48,11 +48,24 @@ var SECOND_WORDS = /\b(?:(?:2nd|second)[\s-]*ch(?:ance|c)?\b|supplementary\b|(?:
48
48
  function secondIn2(text) {
49
49
  return SECOND_WORDS.test(toAscii(text).toLowerCase());
50
50
  }
51
+ var ROUND_LIST = / (?:(\d+) )?(rounds?) ((?:\d+ (?:and )?)+)/g;
51
52
  function roundNumbersIn2(folded, printed) {
52
53
  const out = new Set;
53
- for (const m of folded.matchAll(/ rounds? ((?:\d+ (?:and )?)+)/g))
54
- for (const n of m[1].match(/\d+/g) ?? [])
55
- out.add(Number(n));
54
+ for (const m of folded.matchAll(ROUND_LIST)) {
55
+ const list = m[3];
56
+ const after = folded.slice((m.index ?? 0) + m[0].length);
57
+ const ns = (list.match(/\d+/g) ?? []).map(Number);
58
+ const andOver = list.endsWith(" and ") && /^(?:over|o) /.test(after);
59
+ if (andOver || after.startsWith("+ "))
60
+ ns.pop();
61
+ if (m[1] !== undefined && m[2] === "rounds") {
62
+ const count = Number(m[1]);
63
+ if (ns.length < 2 || ns.some((n) => n < 1 || n > count))
64
+ continue;
65
+ }
66
+ for (const n of ns)
67
+ out.add(n);
68
+ }
56
69
  for (const m of folded.matchAll(/ r([1-9])(?= )/g))
57
70
  out.add(Number(m[1]));
58
71
  if (printed)
@@ -60,8 +73,8 @@ function roundNumbersIn2(folded, printed) {
60
73
  out.add(Number(m[1]));
61
74
  return out;
62
75
  }
63
- function roundFold(print) {
64
- return ` ${toAscii(print).toLowerCase().replace(/(\d)(\p{L})/gu, "$1 $2").replace(/[-–—,/()&+:;|.']/g, " ").replace(/\s+/g, " ").trim()} `;
76
+ function roundFold2(print) {
77
+ return ` ${toAscii(print).toLowerCase().replace(/(\d)(\p{L})/gu, "$1 $2").replace(/&/g, " and ").replace(/\+/g, " + ").replace(/[-–—,/():;|.']/g, " ").replace(/\s+/g, " ").trim()} `;
65
78
  }
66
79
  var SPLIT = / ([a-d]) $|(?:\d|girls?|boys?|lad(?:y|ies)|m[ae]n|mixed) ([a-d])(?= )/;
67
80
  var spoken = (print) => print.replace(/\b(under|over|u|o)-(?=[ \t]*\d)/giu, "$1 ");
@@ -76,7 +89,7 @@ function read(print, ctx, agePrint) {
76
89
  age = t.age;
77
90
  ageSpan = t.span;
78
91
  }
79
- const folded = roundFold(print);
92
+ const folded = roundFold2(print);
80
93
  const rounds = [...roundNumbersIn2(folded, / rounds? /.test(folded))].sort((a, b) => a - b);
81
94
  const split = SPLIT.exec(folded);
82
95
  const empty = print.trim() === "" && (agePrint ?? "").trim() === "";
@@ -160,4 +173,4 @@ function matchTitle2(print, entries, ctx = {}) {
160
173
  });
161
174
  }
162
175
 
163
- export { populationIn2, codePopulation2, secondIn2, roundNumbersIn2, compTitle2, populationRelation2, titleRelation2, ladderOf2, matchTitle2 };
176
+ export { populationIn2, codePopulation2, secondIn2, roundNumbersIn2, roundFold2, compTitle2, populationRelation2, titleRelation2, ladderOf2, matchTitle2 };
package/dist/code.js ADDED
@@ -0,0 +1,136 @@
1
+ import {
2
+ Age2
3
+ } from "./chunks/place-kmh59zkw.js";
4
+ import {
5
+ populationIn2,
6
+ compTitle2
7
+ } from "./chunks/place-7jfd4m3j.js";
8
+ import {
9
+ validity
10
+ } from "./chunks/place-q9kwsmzd.js";
11
+
12
+ var TRAILING = /(?:-[A-Za-z0-9]+|\[[A-Za-z0-9]+\])$/;
13
+ var GROUP = /^([1-7])(\d{2})([A-Za-z]+?)2?$/;
14
+ var OFF_LADDER = new Set(["TR", "TS", "SD", "FD", "FC"]);
15
+ function group(print) {
16
+ let s = print.trim();
17
+ let male = false;
18
+ let female = false;
19
+ for (let m = TRAILING.exec(s);m !== null; m = TRAILING.exec(s)) {
20
+ const t = m[0].toUpperCase();
21
+ if (t === "-M" || t === "[M]")
22
+ male = true;
23
+ if (t === "-F" || t === "[F]")
24
+ female = true;
25
+ s = s.slice(0, m.index);
26
+ }
27
+ const g = GROUP.exec(s);
28
+ if (g === null)
29
+ return null;
30
+ const mm = Number(g[2]);
31
+ const plus50 = mm >= 50 && mm <= 89;
32
+ return {
33
+ digit: g[1],
34
+ letters: g[3].toUpperCase(),
35
+ column: mm === 99 ? null : plus50 ? mm - 50 : mm,
36
+ boys: plus50 || male,
37
+ girls: female
38
+ };
39
+ }
40
+ function groups(print) {
41
+ const out = [];
42
+ for (const part of print.split("/")) {
43
+ const g = group(part);
44
+ if (g === null)
45
+ return null;
46
+ out.push(g);
47
+ }
48
+ return out.length === 0 ? null : out;
49
+ }
50
+ function decode(print, year, codes) {
51
+ const gs = groups(print);
52
+ if (gs === null)
53
+ return null;
54
+ const first = gs[0];
55
+ if (gs.some((g) => g.digit !== first.digit))
56
+ return null;
57
+ const adult = gs.filter((g) => g.column === null).length;
58
+ if (adult > 0 && adult < gs.length)
59
+ return null;
60
+ const level = OFF_LADDER.has(first.letters) ? null : Number(first.digit) * 100;
61
+ const others = codes === null ? [] : codes.flatMap((c) => groups(c) ?? []);
62
+ const boys = gs.every((g) => g.boys);
63
+ const split = others.some((g) => g.digit === first.digit && g.boys);
64
+ const population = gs.some((g) => g.boys) && !boys ? "mixed" : boys ? "boys" : codes === null ? "" : split || first.girls && others.some((g) => g.boys) ? "girls" : "mixed";
65
+ if (adult > 0)
66
+ return { age: Age2.from("Adult", { year }), level, population };
67
+ const cols = gs.map((g) => g.column);
68
+ const lo = Math.min(...cols);
69
+ const hi = Math.max(...cols);
70
+ const series = others.filter((g) => g.digit === first.digit && g.letters === first.letters && g.boys === boys);
71
+ const offered = series.map((g) => g.column).filter((c) => c !== null);
72
+ const top = offered.length > 1 && hi >= Math.max(...offered);
73
+ const band = top ? `O${lo - 1}` : lo === hi ? `U${hi}` : `U${hi}/${hi - lo + 1}yr`;
74
+ return { age: Age2.from(band, { year }), level, population };
75
+ }
76
+ var foldCode = (code) => code.trim().toUpperCase();
77
+ var NO_AGE = Age2.from("", { year: null });
78
+ function createCodeReader(options = {}) {
79
+ const standardized = new Set([...options.standardized ?? []].map((p) => p.trim().toLowerCase()));
80
+ const scheme = (platform) => standardized.has(platform.trim().toLowerCase()) ? "standardized" : "feis-specific";
81
+ function read(code, ctx) {
82
+ const s = scheme(ctx.platform);
83
+ const feis = (ctx.feis ?? "").trim() === "" ? null : ctx.feis.trim();
84
+ const year = ctx.year ?? null;
85
+ const make = (refusal, source, age, level, population, entry) => {
86
+ const v = validity(code.trim() === "", refusal);
87
+ return Object.freeze({
88
+ [Symbol.toStringTag]: "CompCode",
89
+ ...v,
90
+ print: code,
91
+ platform: ctx.platform,
92
+ feis,
93
+ scheme: s,
94
+ source: v.isValid() ? source : null,
95
+ age: v.isValid() ? age : NO_AGE,
96
+ level: v.isValid() ? level : null,
97
+ population: v.isValid() ? population : "",
98
+ entry: v.isValid() ? entry : null,
99
+ isComp: () => v.isValid() && age.isAge()
100
+ });
101
+ };
102
+ const refuse = (r) => make(r, null, NO_AGE, null, "", null);
103
+ if (code.trim() === "")
104
+ return refuse("not-a-code");
105
+ if (feis === null)
106
+ return refuse("no-feis");
107
+ const codes = ctx.codes === undefined ? null : [...ctx.codes];
108
+ if (ctx.syllabus !== undefined && ctx.syllabus.length > 0) {
109
+ const hits = ctx.syllabus.filter((e) => foldCode(e.code) === foldCode(code));
110
+ if (hits.length === 0)
111
+ return refuse("not-in-syllabus");
112
+ if (hits.length > 1)
113
+ return refuse("two-entries");
114
+ const e = hits[0];
115
+ const printed = e.age !== undefined && e.age.trim() !== "" ? Age2.from(e.age, { year }) : null;
116
+ const age = printed?.isAge() === true ? printed : compTitle2(e.title, { year }).age;
117
+ return make(null, "syllabus", age, null, populationIn2(e.title), e);
118
+ }
119
+ if (s === "feis-specific")
120
+ return refuse("feis-specific");
121
+ const decoded = decode(code, year, codes);
122
+ if (decoded === null || !decoded.age.isAge())
123
+ return refuse("not-a-code");
124
+ return make(null, "code", decoded.age, decoded.level, decoded.population, null);
125
+ }
126
+ return Object.freeze({ scheme, read });
127
+ }
128
+ var DEFAULT_CODE_READER = createCodeReader();
129
+ function compCode(code, ctx) {
130
+ return DEFAULT_CODE_READER.read(code, ctx);
131
+ }
132
+ export {
133
+ DEFAULT_CODE_READER,
134
+ compCode,
135
+ createCodeReader
136
+ };
package/dist/schedule.js CHANGED
@@ -2,7 +2,7 @@ import"./chunks/place-kmh59zkw.js";
2
2
  import {
3
3
  compTitle2,
4
4
  matchTitle2
5
- } from "./chunks/place-k0p9af4f.js";
5
+ } from "./chunks/place-7jfd4m3j.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
@@ -3,12 +3,13 @@ import {
3
3
  codePopulation2,
4
4
  secondIn2,
5
5
  roundNumbersIn2,
6
+ roundFold2,
6
7
  compTitle2,
7
8
  populationRelation2,
8
9
  titleRelation2,
9
10
  ladderOf2,
10
11
  matchTitle2
11
- } from "./chunks/place-k0p9af4f.js";
12
+ } from "./chunks/place-7jfd4m3j.js";
12
13
  import"./chunks/place-kmh59zkw.js";
13
14
  export {
14
15
  codePopulation2 as codePopulation,
@@ -17,6 +18,7 @@ export {
17
18
  matchTitle2 as matchTitle,
18
19
  populationIn2 as populationIn,
19
20
  populationRelation2 as populationRelation,
21
+ roundFold2 as roundFold,
20
22
  roundNumbersIn2 as roundNumbersIn,
21
23
  secondIn2 as secondIn,
22
24
  titleRelation2 as titleRelation
@@ -0,0 +1,41 @@
1
+ import { Age } from "./age.ts";
2
+ import { type Population } from "./title.ts";
3
+ import { type Validated } from "./validated.ts";
4
+ export type { EmptyReason, Validated } from "./validated.ts";
5
+ export type CodeScheme = "standardized" | "feis-specific";
6
+ export type CodeRefusal = "no-feis" | "feis-specific" | "not-in-syllabus" | "two-entries" | "not-a-code";
7
+ export interface CodeEntry {
8
+ readonly code: string;
9
+ readonly title: string;
10
+ readonly age?: string;
11
+ }
12
+ export interface CodeContext<E extends CodeEntry = CodeEntry> {
13
+ readonly platform: string;
14
+ readonly feis: string | null | undefined;
15
+ readonly year?: number | null;
16
+ readonly syllabus?: readonly E[];
17
+ readonly codes?: Iterable<string>;
18
+ }
19
+ export interface CompCode<E extends CodeEntry = CodeEntry> extends Validated<CodeRefusal> {
20
+ readonly [Symbol.toStringTag]: "CompCode";
21
+ readonly print: string;
22
+ readonly platform: string;
23
+ readonly feis: string | null;
24
+ readonly scheme: CodeScheme;
25
+ readonly source: "syllabus" | "code" | null;
26
+ readonly age: Age;
27
+ readonly level: number | null;
28
+ readonly population: Population | "";
29
+ readonly entry: E | null;
30
+ isComp(): boolean;
31
+ }
32
+ export interface CodeReaderOptions {
33
+ readonly standardized?: Iterable<string>;
34
+ }
35
+ export interface CodeReader {
36
+ scheme(platform: string): CodeScheme;
37
+ read<E extends CodeEntry>(code: string, ctx: CodeContext<E>): CompCode<E>;
38
+ }
39
+ export declare function createCodeReader(options?: CodeReaderOptions): CodeReader;
40
+ export declare const DEFAULT_CODE_READER: CodeReader;
41
+ export declare function compCode<E extends CodeEntry>(code: string, ctx: CodeContext<E>): CompCode<E>;
@@ -8,6 +8,7 @@ export declare function populationIn(text: string): Population | "";
8
8
  export declare function codePopulation(code: string, codes: Iterable<string>): Population | "";
9
9
  export declare function secondIn(text: string): boolean;
10
10
  export declare function roundNumbersIn(folded: string, printed: boolean): Set<number>;
11
+ export declare function roundFold(print: string): string;
11
12
  export interface TitleContext {
12
13
  readonly year?: number | null;
13
14
  readonly ladder?: AgeLadder;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@finbheara/domain",
3
- "version": "0.49.0",
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",
3
+ "version": "0.51.0",
4
+ "description": "Places, CLRG regions, school names, ages, dances, competition titles, codes 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",
7
7
  "regions",
@@ -73,6 +73,10 @@
73
73
  "types": "./dist/types/schedule.d.ts",
74
74
  "default": "./dist/schedule.js"
75
75
  },
76
+ "./code": {
77
+ "types": "./dist/types/code.d.ts",
78
+ "default": "./dist/code.js"
79
+ },
76
80
  "./package.json": "./package.json"
77
81
  },
78
82
  "files": ["dist/*.js", "dist/chunks/*.js", "dist/types/**/*.d.ts", "README.md", "LICENSE"],
@@ -89,7 +93,7 @@
89
93
  "prepublishOnly": "bun scripts/build.ts && bun scripts/guard-tarball.ts"
90
94
  },
91
95
  "dependencies": {
92
- "@finbheara/names": "0.22.0"
96
+ "@finbheara/names": "0.25.0"
93
97
  },
94
98
  "devDependencies": {
95
99
  "typescript": "7.0.2"