@finbheara/domain 0.2.0 → 0.3.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 +14 -1
- package/dist/age.js +55 -13
- package/dist/ceili.js +2 -2
- package/dist/chunks/{place-7gqecaxd.js → place-0ehc2q69.js} +1 -1
- package/dist/chunks/{place-kr42294h.js → place-2ja9yx08.js} +1 -1
- package/dist/chunks/{place-g6yw4cq6.js → place-htxgptyv.js} +1 -0
- package/dist/dance-types.js +1 -1
- package/dist/dance.js +3 -3
- package/dist/tune.js +2 -2
- package/dist/types/age-types.d.ts +4 -1
- package/dist/types/age.d.ts +16 -4
- package/dist/types/validated.d.ts +4 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -39,7 +39,7 @@ ESM, Node ≥ 22 or Bun ≥ 1.4.2. MIT. Not affiliated with or endorsed by any d
|
|
|
39
39
|
| `@finbheara/domain/tune` | `tune()`, `tuneRelation`, `tempoLegal`, `settingAt`, `TUNE_NODES`: the CLRG set dance list |
|
|
40
40
|
| `@finbheara/domain/ceili` | `ceili()`, `ceiliRelation`, `CEILI_NODES`: the céilí handbook dances |
|
|
41
41
|
| `@finbheara/domain/dance` | `dance()`: a print read as a tune and a céilí dance, narrowed by its round |
|
|
42
|
-
| `@finbheara/domain/age` | `Age.from`, `Age.fromYOB`, `ageRelation`, `canOverlap`, `averageAge`, `isAveraged`, the age constants |
|
|
42
|
+
| `@finbheara/domain/age` | `Age.from`, `Age.fromYOB`, `ageRelation`, `canOverlap`, `averageAge`, `isAveraged`, `intersectBirthYears`, the `Validated` type, the age constants |
|
|
43
43
|
| `@finbheara/domain/noun-index` | `createIndex`, `indexSchema`, `personEncoder`, and the `SqlHandle`/`NounEncoder` types: a database-backed name map (below) |
|
|
44
44
|
| `@finbheara/domain/dance-types` | `danceTypes()`, `DANCE_TYPES`, `rhythmOfTime`, and the shared `DanceMatch`/`DanceRelation`/`SetRhythm`/`TimeSignature` types |
|
|
45
45
|
|
|
@@ -180,6 +180,19 @@ Age.fromYOB(1999).fitsIn(Age.from("Over 21", { year: 2026, top: true })); // tru
|
|
|
180
180
|
canOverlap(u12, Age.from("U14", { year: 2026 }), dancer); // true
|
|
181
181
|
```
|
|
182
182
|
|
|
183
|
+
A value the reader cannot stand behind is returned, not thrown. Every `Age` answers
|
|
184
|
+
`isValid()` and `invalidReason()`: an age group under `U3` admits nobody old enough to dance,
|
|
185
|
+
and a year of birth outside 1900 to 2099 is not one. An invalid `Age` has no birth years and
|
|
186
|
+
rules nothing out.
|
|
187
|
+
|
|
188
|
+
```ts
|
|
189
|
+
Age.from("U2", { year: 2026 }).isValid(); // false
|
|
190
|
+
Age.from("U2", { year: 2026 }).invalidReason(); // "below-youngest-band"
|
|
191
|
+
Age.fromYOB(2222).invalidReason(); // "year-out-of-range"
|
|
192
|
+
Age.from("All", { year: 2026 }).invalidReason(); // "not-a-band"
|
|
193
|
+
Age.from("", { year: 2026 }).isValid(); // true: empty, not invalid
|
|
194
|
+
```
|
|
195
|
+
|
|
183
196
|
A team's age is the average of its dancers' ages, rounded to two decimals. `averageAge`
|
|
184
197
|
takes exact ages only and returns an `AverageAge`, a separate type that refuses, by name,
|
|
185
198
|
any use that would treat it as one dancer's age.
|
package/dist/age.js
CHANGED
|
@@ -6,6 +6,8 @@ var UNDER_CAP_MAX_TOP = 21;
|
|
|
6
6
|
var YOUNG_FLOOR = Object.freeze({ hard: 2, soft: 4 });
|
|
7
7
|
var MIN_MAJOR_AGE = 4;
|
|
8
8
|
var MIN_FEIS_AGE = 3;
|
|
9
|
+
var YOUNGEST_BAND_TOP = MAJORS_FLOOR_YEARS;
|
|
10
|
+
var YOB_RANGE = Object.freeze({ first: 1900, last: 2099 });
|
|
9
11
|
var DANCE_UP_YEARS = 1;
|
|
10
12
|
var ageOnJanuary1 = (year, born) => year - born - 1;
|
|
11
13
|
var yearsSinceBirth = (year, born) => year - born;
|
|
@@ -116,6 +118,11 @@ function read(token, ladder, top) {
|
|
|
116
118
|
}
|
|
117
119
|
return { kind: null, interval: null, refusal: "not-a-band", clauses: [], parts: [] };
|
|
118
120
|
}
|
|
121
|
+
function refuseBelowYoungest(r) {
|
|
122
|
+
if (r.interval === null || r.interval.hi >= YOUNGEST_BAND_TOP)
|
|
123
|
+
return r;
|
|
124
|
+
return { ...r, interval: null, refusal: "below-youngest-band", clauses: [] };
|
|
125
|
+
}
|
|
119
126
|
|
|
120
127
|
class AgeMisuseError extends Error {
|
|
121
128
|
misuse;
|
|
@@ -146,7 +153,7 @@ function fromPrint(print, context = {}) {
|
|
|
146
153
|
const ladder = context.ladder ?? NO_LADDER;
|
|
147
154
|
const year = context.year === undefined ? currentYear() : context.year;
|
|
148
155
|
const ag1 = print.trim().toLowerCase();
|
|
149
|
-
const reading = ag1 === "" ? { kind: null, interval: null, refusal: null, clauses: [], parts: [] } : read(print.trim(), ladder, context.top === true);
|
|
156
|
+
const reading = ag1 === "" ? { kind: null, interval: null, refusal: null, clauses: [], parts: [] } : refuseBelowYoungest(read(print.trim(), ladder, context.top === true));
|
|
150
157
|
const iv = reading.interval;
|
|
151
158
|
const arm = ag1 === "" ? "empty" : iv === null ? "open" : "band";
|
|
152
159
|
const ag2 = iv !== null ? display(iv) : ag1 === "" ? "" : `${ag1}?`;
|
|
@@ -162,6 +169,8 @@ function fromPrint(print, context = {}) {
|
|
|
162
169
|
parts: reading.parts,
|
|
163
170
|
length: ag2.length,
|
|
164
171
|
refusal: reading.refusal,
|
|
172
|
+
isValid: () => reading.refusal === null,
|
|
173
|
+
invalidReason: () => reading.refusal,
|
|
165
174
|
source,
|
|
166
175
|
clauses: reading.clauses,
|
|
167
176
|
ladder,
|
|
@@ -186,7 +195,7 @@ function fromPrint(print, context = {}) {
|
|
|
186
195
|
floorObserved: () => reading.kind !== "under" || iv === null ? null : reading.clauses.includes("under-floored-on-rung")
|
|
187
196
|
});
|
|
188
197
|
}
|
|
189
|
-
function fromYOB(from, to = from) {
|
|
198
|
+
function fromYOB(from, to = from, context = {}) {
|
|
190
199
|
for (const y of [from, to])
|
|
191
200
|
if (y !== null && !Number.isInteger(y))
|
|
192
201
|
throw new RangeError(`Age.fromYOB(${from}, ${to}): a year of birth is a whole number`);
|
|
@@ -194,29 +203,34 @@ function fromYOB(from, to = from) {
|
|
|
194
203
|
throw new RangeError("Age.fromYOB(null, null): at least one end is a year");
|
|
195
204
|
if (from !== null && to !== null && from > to)
|
|
196
205
|
throw new RangeError(`Age.fromYOB(${from}, ${to}): the earliest year comes first`);
|
|
197
|
-
const
|
|
198
|
-
const
|
|
199
|
-
const
|
|
200
|
-
const
|
|
206
|
+
const at = context.year ?? null;
|
|
207
|
+
const valid = [from, to].every((y) => y === null || y >= YOB_RANGE.first && y <= YOB_RANGE.last) && (at === null || to === null || yearsSinceBirth(at, to) >= YOUNG_FLOOR.hard);
|
|
208
|
+
const refusal = valid ? null : "year-out-of-range";
|
|
209
|
+
const exact = valid && from !== null && from === to;
|
|
210
|
+
const ag0 = from !== null && from === to ? String(from) : `${from ?? ""}..${to ?? ""}`;
|
|
211
|
+
const ag2 = valid ? `b${ag0}` : `b${ag0}?`;
|
|
212
|
+
const years = Object.freeze(valid ? { from, to } : { from: null, to: null });
|
|
201
213
|
return withRelations({
|
|
202
214
|
[Symbol.toStringTag]: "Age",
|
|
203
215
|
toString: () => ag2,
|
|
204
|
-
arm: "yob",
|
|
216
|
+
arm: valid ? "yob" : "open",
|
|
205
217
|
kind: "yob",
|
|
206
218
|
ag0,
|
|
207
219
|
ag1: ag0,
|
|
208
220
|
ag2,
|
|
209
|
-
parts:
|
|
221
|
+
parts: from !== null && from === to ? [from] : [from, to].filter((y) => y !== null),
|
|
210
222
|
length: ag2.length,
|
|
211
|
-
refusal
|
|
212
|
-
|
|
223
|
+
refusal,
|
|
224
|
+
isValid: () => valid,
|
|
225
|
+
invalidReason: () => refusal,
|
|
226
|
+
source: valid ? "printed-yob" : "open",
|
|
213
227
|
clauses: [],
|
|
214
228
|
ladder: NO_LADDER,
|
|
215
229
|
year: null,
|
|
216
230
|
isEmpty: () => false,
|
|
217
|
-
isAge: () =>
|
|
231
|
+
isAge: () => valid,
|
|
218
232
|
isExact: () => exact,
|
|
219
|
-
isAndOver: () => from === null,
|
|
233
|
+
isAndOver: () => valid && from === null,
|
|
220
234
|
isAdult: () => false,
|
|
221
235
|
interval: () => null,
|
|
222
236
|
birthYears: () => years,
|
|
@@ -250,7 +264,7 @@ function birthYearsOfInterval(iv, year) {
|
|
|
250
264
|
to: Math.min(year - iv.lo - 1, year - MAJORS_FLOOR_YEARS)
|
|
251
265
|
};
|
|
252
266
|
}
|
|
253
|
-
var inBirthYears = (a) => a.
|
|
267
|
+
var inBirthYears = (a) => a.kind === "yob" || a.year !== null;
|
|
254
268
|
function spanOf(a) {
|
|
255
269
|
if (!a.isAge())
|
|
256
270
|
return null;
|
|
@@ -355,6 +369,25 @@ function birthYearsDisjoint(a, b) {
|
|
|
355
369
|
return true;
|
|
356
370
|
return false;
|
|
357
371
|
}
|
|
372
|
+
function intersectBirthYears(windows) {
|
|
373
|
+
let from = null;
|
|
374
|
+
let to = null;
|
|
375
|
+
let n = 0;
|
|
376
|
+
for (const w of windows) {
|
|
377
|
+
if (w.from === null && w.to === null)
|
|
378
|
+
continue;
|
|
379
|
+
n++;
|
|
380
|
+
if (w.from !== null)
|
|
381
|
+
from = from === null ? w.from : Math.max(from, w.from);
|
|
382
|
+
if (w.to !== null)
|
|
383
|
+
to = to === null ? w.to : Math.min(to, w.to);
|
|
384
|
+
}
|
|
385
|
+
if (n === 0)
|
|
386
|
+
return { from: null, to: null, n: 0 };
|
|
387
|
+
if (from !== null && to !== null && from > to)
|
|
388
|
+
return null;
|
|
389
|
+
return { from, to, n };
|
|
390
|
+
}
|
|
358
391
|
function birthYearsMeet(card, other, slack = 0) {
|
|
359
392
|
if (card.from !== null && other.to !== null && card.from > other.to)
|
|
360
393
|
return false;
|
|
@@ -378,6 +411,11 @@ function underCappedYears(b, year, n = UNDER_CAP.plain) {
|
|
|
378
411
|
function underCapRefuses(bandFrom, identityFrom, n) {
|
|
379
412
|
return identityFrom > bandFrom + n;
|
|
380
413
|
}
|
|
414
|
+
function validYOB(y, year) {
|
|
415
|
+
if (y === null || y === undefined || !Number.isInteger(y))
|
|
416
|
+
return null;
|
|
417
|
+
return Age.fromYOB(y, y, { year: year ?? null }).isValid() ? y : null;
|
|
418
|
+
}
|
|
381
419
|
function youngFloor(year, oldest) {
|
|
382
420
|
const s = yearsSinceBirth(year, oldest);
|
|
383
421
|
if (s >= YOUNG_FLOOR.soft)
|
|
@@ -399,6 +437,8 @@ export {
|
|
|
399
437
|
MIN_MAJOR_AGE,
|
|
400
438
|
UNDER_CAP,
|
|
401
439
|
UNDER_CAP_MAX_TOP,
|
|
440
|
+
YOB_RANGE,
|
|
441
|
+
YOUNGEST_BAND_TOP,
|
|
402
442
|
YOUNGEST_FLOOR,
|
|
403
443
|
YOUNG_FLOOR,
|
|
404
444
|
ageOnJanuary1,
|
|
@@ -408,6 +448,7 @@ export {
|
|
|
408
448
|
birthYearsMeet,
|
|
409
449
|
birthYearsOfInterval,
|
|
410
450
|
canOverlap,
|
|
451
|
+
intersectBirthYears,
|
|
411
452
|
isAveraged,
|
|
412
453
|
roundedMeanHundredths,
|
|
413
454
|
tooYoungForFeis,
|
|
@@ -415,6 +456,7 @@ export {
|
|
|
415
456
|
underCapApplies,
|
|
416
457
|
underCapRefuses,
|
|
417
458
|
underCappedYears,
|
|
459
|
+
validYOB,
|
|
418
460
|
yearsSinceBirth,
|
|
419
461
|
youngFloor
|
|
420
462
|
};
|
package/dist/ceili.js
CHANGED
|
@@ -4,8 +4,8 @@ import {
|
|
|
4
4
|
ceiliById2,
|
|
5
5
|
ceili2,
|
|
6
6
|
ceiliRelation2
|
|
7
|
-
} from "./chunks/place-
|
|
8
|
-
import"./chunks/place-
|
|
7
|
+
} from "./chunks/place-0ehc2q69.js";
|
|
8
|
+
import"./chunks/place-htxgptyv.js";
|
|
9
9
|
export {
|
|
10
10
|
CEILI_NODES2 as CEILI_NODES,
|
|
11
11
|
allCeilis2 as allCeilis,
|
|
@@ -3,7 +3,7 @@ import {
|
|
|
3
3
|
matchNodes,
|
|
4
4
|
candidateRelation,
|
|
5
5
|
isDanceTypePrint2
|
|
6
|
-
} from "./place-
|
|
6
|
+
} from "./place-htxgptyv.js";
|
|
7
7
|
|
|
8
8
|
function node(id, name, irish, family, formation, opts = {}) {
|
|
9
9
|
const kind = "round" in formation ? "round" : ("long" in formation) ? "long" : "set";
|
package/dist/dance-types.js
CHANGED
package/dist/dance.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import"./chunks/place-
|
|
1
|
+
import"./chunks/place-htxgptyv.js";
|
|
2
2
|
import {
|
|
3
3
|
ceili2
|
|
4
|
-
} from "./chunks/place-
|
|
4
|
+
} from "./chunks/place-0ehc2q69.js";
|
|
5
5
|
import {
|
|
6
6
|
tune2
|
|
7
|
-
} from "./chunks/place-
|
|
7
|
+
} from "./chunks/place-2ja9yx08.js";
|
|
8
8
|
|
|
9
9
|
function dance(print, round) {
|
|
10
10
|
const setting = round === "traditional-set" ? "traditional" : undefined;
|
package/dist/tune.js
CHANGED
|
@@ -8,8 +8,8 @@ import {
|
|
|
8
8
|
settingAt2,
|
|
9
9
|
tune2,
|
|
10
10
|
tuneRelation2
|
|
11
|
-
} from "./chunks/place-
|
|
12
|
-
import"./chunks/place-
|
|
11
|
+
} from "./chunks/place-2ja9yx08.js";
|
|
12
|
+
import"./chunks/place-htxgptyv.js";
|
|
13
13
|
export {
|
|
14
14
|
PLAUSIBLE_SET_TEMPO_MAX2 as PLAUSIBLE_SET_TEMPO_MAX,
|
|
15
15
|
SET_MIN_SPEED2 as SET_MIN_SPEED,
|
|
@@ -11,6 +11,9 @@ export interface AgeLadder {
|
|
|
11
11
|
readonly overs: readonly number[];
|
|
12
12
|
readonly ranges: readonly number[];
|
|
13
13
|
}
|
|
14
|
+
export interface YOBContext {
|
|
15
|
+
readonly year?: number | null;
|
|
16
|
+
}
|
|
14
17
|
export interface AgeContext {
|
|
15
18
|
readonly ladder?: AgeLadder;
|
|
16
19
|
readonly year?: number | null;
|
|
@@ -18,7 +21,7 @@ export interface AgeContext {
|
|
|
18
21
|
}
|
|
19
22
|
export type AgeArm = "band" | "yob" | "open" | "empty";
|
|
20
23
|
export type AgeKind = "under" | "over" | "range" | "adult" | "senior" | "yob";
|
|
21
|
-
export type AgeRefusal = "not-a-band" | "backwards-range" | "senior-without-ladder";
|
|
24
|
+
export type AgeRefusal = "not-a-band" | "backwards-range" | "senior-without-ladder" | "below-youngest-band" | "year-out-of-range";
|
|
22
25
|
export type AgeSource = "sibling-narrowed" | "printed-band" | "printed-range" | "printed-yob" | "open";
|
|
23
26
|
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-inclusive" | "range-floor-on-rung" | "range-floor-widened" | "range-adult-floor";
|
|
24
27
|
export type AgeRelation = "same" | "contains" | "overlaps" | "disjoint" | "silent";
|
package/dist/types/age.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import type { AgeArm, AgeClause, AgeComparison, AgeContext, AgeInterval, AgeKind, AgeLadder, AgeMisuse, AgeRefusal, AgeRelation, AgeRelationReason, AgeSource, BirthYears, YoungFloorVerdict } from "./age-types.ts";
|
|
2
|
-
|
|
1
|
+
import type { AgeArm, AgeClause, AgeComparison, AgeContext, AgeInterval, AgeKind, AgeLadder, AgeMisuse, AgeRefusal, AgeRelation, AgeRelationReason, AgeSource, BirthYears, YOBContext, YoungFloorVerdict } from "./age-types.ts";
|
|
2
|
+
import type { Validated } from "./validated.ts";
|
|
3
|
+
export type { AgeArm, AgeClause, AgeComparison, AgeContext, AgeInterval, AgeKind, AgeLadder, AgeMisuse, AgeRefusal, AgeRelation, AgeRelationReason, AgeSource, BirthYears, Validated, YOBContext, YoungFloorVerdict, };
|
|
3
4
|
export declare const YOUNGEST_FLOOR = 0;
|
|
4
5
|
export declare const ADULT_FLOOR = 18;
|
|
5
6
|
export declare const MAJORS_FLOOR_YEARS = 3;
|
|
@@ -14,10 +15,15 @@ export declare const YOUNG_FLOOR: Readonly<{
|
|
|
14
15
|
}>;
|
|
15
16
|
export declare const MIN_MAJOR_AGE = 4;
|
|
16
17
|
export declare const MIN_FEIS_AGE = 3;
|
|
18
|
+
export declare const YOUNGEST_BAND_TOP = 3;
|
|
19
|
+
export declare const YOB_RANGE: Readonly<{
|
|
20
|
+
readonly first: 1900;
|
|
21
|
+
readonly last: 2099;
|
|
22
|
+
}>;
|
|
17
23
|
export declare const DANCE_UP_YEARS = 1;
|
|
18
24
|
export declare const ageOnJanuary1: (year: number, born: number) => number;
|
|
19
25
|
export declare const yearsSinceBirth: (year: number, born: number) => number;
|
|
20
|
-
export interface Age {
|
|
26
|
+
export interface Age extends Validated<AgeRefusal> {
|
|
21
27
|
readonly [Symbol.toStringTag]: "Age";
|
|
22
28
|
toString(): string;
|
|
23
29
|
readonly arm: AgeArm;
|
|
@@ -48,7 +54,7 @@ export declare class AgeMisuseError extends Error {
|
|
|
48
54
|
constructor(misuse: AgeMisuse, detail: string);
|
|
49
55
|
}
|
|
50
56
|
declare function fromPrint(print: string, context?: AgeContext): Age;
|
|
51
|
-
declare function fromYOB(from: number | null, to?: number | null): Age;
|
|
57
|
+
declare function fromYOB(from: number | null, to?: number | null, context?: YOBContext): Age;
|
|
52
58
|
export declare const Age: Readonly<{
|
|
53
59
|
from: typeof fromPrint;
|
|
54
60
|
fromYOB: typeof fromYOB;
|
|
@@ -75,10 +81,16 @@ export interface AverageAge {
|
|
|
75
81
|
export declare function isAveraged(a: Age | AverageAge): a is AverageAge;
|
|
76
82
|
export declare function averageAge(ages: readonly Age[]): AverageAge;
|
|
77
83
|
export declare function birthYearsDisjoint(a: BirthYears, b: BirthYears): boolean;
|
|
84
|
+
export declare function intersectBirthYears(windows: readonly BirthYears[]): {
|
|
85
|
+
from: number | null;
|
|
86
|
+
to: number | null;
|
|
87
|
+
n: number;
|
|
88
|
+
} | null;
|
|
78
89
|
export declare function birthYearsMeet(card: BirthYears, other: BirthYears, slack?: number): boolean;
|
|
79
90
|
export declare function underCapApplies(bandFrom: number | null, year: number): boolean;
|
|
80
91
|
export declare function underCappedYears(b: BirthYears, year: number, n?: number): BirthYears;
|
|
81
92
|
export declare function underCapRefuses(bandFrom: number, identityFrom: number, n: number): boolean;
|
|
93
|
+
export declare function validYOB(y: number | null | undefined, year?: number | null): number | null;
|
|
82
94
|
export declare function youngFloor(year: number, oldest: number): YoungFloorVerdict;
|
|
83
95
|
export declare const tooYoungForMajor: (year: number, oldest: number) => boolean;
|
|
84
96
|
export declare const tooYoungForFeis: (year: number, oldest: number) => boolean;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@finbheara/domain",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.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",
|
|
@@ -69,6 +69,6 @@
|
|
|
69
69
|
"prepublishOnly": "bun scripts/build.ts && bun scripts/guard-tarball.ts"
|
|
70
70
|
},
|
|
71
71
|
"dependencies": {
|
|
72
|
-
"@finbheara/names": "0.
|
|
72
|
+
"@finbheara/names": "0.17.0"
|
|
73
73
|
}
|
|
74
74
|
}
|