@finbheara/domain 0.50.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
@@ -50,6 +50,7 @@ ESM, Node ≥ 22 or Bun ≥ 1.4.2. MIT. Not affiliated with or endorsed by any d
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
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
@@ -454,6 +455,50 @@ secondIn("Preliminary Champion, Supplementary Prelim (U15)"); // true
454
455
  secondIn("Preliminary Champion, Hard Shoe (U10)"); // false
455
456
  ```
456
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
+
457
502
  ## Ask a schedule
458
503
 
459
504
  A split band's competitions share their card numbers, so a card heard on a stage fits each of
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
+ };
@@ -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>;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@finbheara/domain",
3
- "version": "0.50.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.23.0"
96
+ "@finbheara/names": "0.25.0"
93
97
  },
94
98
  "devDependencies": {
95
99
  "typescript": "7.0.2"