@finbheara/domain 0.1.0 → 0.2.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,8 @@ 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
+ - **A database-backed index** of any of these nouns: the slots an in-memory name map hashes
20
+ on, stored as indexed columns of a SQLite table you own.
19
21
 
20
22
  It was built on the stage announcements and results sheets of Irish dance competitions,
21
23
  where one school arrives as `Ardvarna Academy of Irish Dance (Calgary, AB)` and as
@@ -33,11 +35,12 @@ ESM, Node ≥ 22 or Bun ≥ 1.4.2. MIT. Not affiliated with or endorsed by any d
33
35
  |---|---|
34
36
  | `@finbheara/domain/place` | `place()`, `createPlaceReader`, `placeRelation`, the `Place` types |
35
37
  | `@finbheara/domain/clrg-regions` | `REGIONS`, `regionOf`, `resolvePhrase` |
36
- | `@finbheara/domain/school` | `createSchoolNameReader`, `DEFAULT_SCHOOL_NAME_READER`, `compareSchoolNames`, `foldSchoolName` |
38
+ | `@finbheara/domain/school` | `createSchoolNameReader`, `DEFAULT_SCHOOL_NAME_READER`, `compareSchoolNames`, `foldSchoolName`, `schoolEncoder`, `schoolColumns` |
37
39
  | `@finbheara/domain/tune` | `tune()`, `tuneRelation`, `tempoLegal`, `settingAt`, `TUNE_NODES`: the CLRG set dance list |
38
40
  | `@finbheara/domain/ceili` | `ceili()`, `ceiliRelation`, `CEILI_NODES`: the céilí handbook dances |
39
41
  | `@finbheara/domain/dance` | `dance()`: a print read as a tune and a céilí dance, narrowed by its round |
40
42
  | `@finbheara/domain/age` | `Age.from`, `Age.fromYOB`, `ageRelation`, `canOverlap`, `averageAge`, `isAveraged`, the age constants |
43
+ | `@finbheara/domain/noun-index` | `createIndex`, `indexSchema`, `personEncoder`, and the `SqlHandle`/`NounEncoder` types: a database-backed name map (below) |
41
44
  | `@finbheara/domain/dance-types` | `danceTypes()`, `DANCE_TYPES`, `rhythmOfTime`, and the shared `DanceMatch`/`DanceRelation`/`SetRhythm`/`TimeSignature` types |
42
45
 
43
46
  The library does no I/O and installs nothing process-wide. Every reader has a default and a
@@ -189,6 +192,78 @@ team.fitsIn(Age.from("U40", { year: 2026 })); // true
189
192
  averageAge([u12]); // throws AgeMisuseError "not-exact"
190
193
  ```
191
194
 
195
+ ## Index names in a database
196
+
197
+ `createNameMap` (from `@finbheara/names`) and `mapOfSchools` hold their names in memory. An
198
+ index holds them in a SQLite table, as columns: every slot those maps hash a name on, indexed,
199
+ so a lookup at a level is one equality and nothing is loaded. It is a multi-map: a row is one
200
+ name and one value, and a lookup answers every value filed at the name's slot.
201
+
202
+ The package depends on no SQLite module. Hand it a handle with `prepare(sql)` returning a
203
+ statement with `run(...)` and `all(...)`: `bun:sqlite`'s `Database` and `node:sqlite`'s
204
+ `DatabaseSync` (Node ≥ 22.5) are both one as they are.
205
+
206
+ ```ts
207
+ import { Database } from "bun:sqlite"; // or: import { DatabaseSync } from "node:sqlite"
208
+ import { createNameMap, personName } from "@finbheara/names/normalize";
209
+ import { createIndex, indexSchema, personEncoder } from "@finbheara/domain/noun-index";
210
+
211
+ const db = new Database(":memory:");
212
+ const people = personEncoder();
213
+ const { table, indexes } = indexSchema(people, { table: "person" });
214
+ db.prepare(table).run();
215
+ for (const s of indexes) db.prepare(s).run(); // after a bulk load, if you have one
216
+
217
+ const index = createIndex(db, people, { table: "person" });
218
+ index.add(personName("Aoife Ní Mhaolvarna"), 1);
219
+ index.add(personName("Mhaolvarna, Aoife Ni"), 2);
220
+ index.add(personName("Kestrel Evbatyrov"), 3);
221
+
222
+ index.get(personName("AOIFE NI MHAOLVARNA"), "nn3"); // [1, 2]
223
+ index.get(personName("Aoife Ní Mhaolvarna"), "nn0"); // [1]
224
+ index.find("surname_key", personName("Aoife Ní Mhaolvarna").surnameKey()); // [1, 2]
225
+ index.nouns(2)[0]?.nn2; // "NiMhaolvarna<<Aoife": the parse, read back
226
+ ```
227
+
228
+ `map(level)` is the single-valued view, with `createNameMap`'s interface and its answers:
229
+ the first name filed at a slot keeps it, a later `set` replaces the value, and a print that
230
+ names no person is held and never found.
231
+
232
+ ```ts
233
+ const byName = index.map("nn3");
234
+ byName.get(personName("Aoife Ni Mhaolvarna")); // 1
235
+ byName.size; // 2
236
+ createNameMap<number>("nn3", [...byName]).size; // 2
237
+ ```
238
+
239
+ The person columns are the print, the reading it was parsed under, and `nameColumns()` from
240
+ `@finbheara/names`; the levels are `nn0`-`nn4`. A school's (`schoolEncoder` from
241
+ `@finbheara/domain/school`) are the print, `sn0`-`sn4`, `realWords` and `matchKey`, and its
242
+ levels are `mapOfSchools`'s. One print may be filed under several schools, and a school under
243
+ many prints:
244
+
245
+ ```ts
246
+ import { DEFAULT_SCHOOL_NAME_READER, schoolEncoder } from "@finbheara/domain/school";
247
+
248
+ const read = (p: string) => DEFAULT_SCHOOL_NAME_READER.schoolName(p);
249
+ const schools = createIndex(db, schoolEncoder(), { table: "school" }); // after its indexSchema
250
+ schools.add(read("Ardvarna Academy"), 10);
251
+ schools.add(read("Ardvarna Academy"), 11);
252
+ schools.add(read("Ardvarna Academy (Ontario)"), 11);
253
+ schools.add(read("ARDVARNA ACADEMY - Calgary, Alberta"), 10);
254
+
255
+ schools.get(read("Ardvarna Academy"), "sn0"); // [10, 11]
256
+ schools.nouns(11).map((s) => s.sn0); // ["Ardvarna Academy", "Ardvarna Academy (Ontario)"]
257
+ schools.get(read("Ardvarna Academy of Irish Dance (Calgary, AB)"), "sn3"); // [10]
258
+ ```
259
+
260
+ `rows({ column: keys, ... })` answers every row where any column holds one of its keys, and
261
+ `glob(column, pattern)` a SQLite `GLOB` on one column. An encoder is a plain object: add a
262
+ column of your own by spreading one, `{ ...enc, columns: [...enc.columns, "k"], encode: (x) =>
263
+ ({ ...enc.encode(x), k: f(x) }) }`. `indexSchema` takes the table, the value column and its
264
+ type (`{ value: "id", valueType: "INTEGER PRIMARY KEY" }` makes the value the row id); on such a table `map(level)` iterates in value order, and a `set` to a value another row
265
+ holds is refused by the key).
266
+
192
267
  ## Known gaps
193
268
 
194
269
  - `place()` reads `"Derry in Ireland"` and `"Calgary in Western Canada"` as `unread`: it does
@@ -0,0 +1,233 @@
1
+ import {
2
+ NAME_COLUMNS,
3
+ NAME_LEVEL_COLUMNS,
4
+ nameColumns,
5
+ personName
6
+ } from "@finbheara/names/normalize";
7
+ var IDENT = /^[A-Za-z_][A-Za-z0-9_]*$/u;
8
+ var RESERVED = new Set(`rowid oid _rowid_ __r __v all and as by check create default delete distinct drop
9
+ from group in index insert into is key limit not null on or order primary references
10
+ select set table to union unique update values where`.split(/\s+/u));
11
+ function ident(s, what) {
12
+ if (!IDENT.test(s) || RESERVED.has(s.toLowerCase()))
13
+ throw new Error(`noun-index: ${what} ${JSON.stringify(s)} is not a usable identifier`);
14
+ return s;
15
+ }
16
+ function resolve(encoder, options) {
17
+ const table = ident(options.table ?? "noun_index", "table");
18
+ const value = ident(options.value ?? "value", "value column");
19
+ const valueType = (options.valueType ?? "").trim();
20
+ if (!/^[A-Za-z ()0-9]*$/u.test(valueType))
21
+ throw new Error(`noun-index: value type ${JSON.stringify(valueType)} is not a type`);
22
+ const seen = new Set([value.toLowerCase()]);
23
+ for (const c of encoder.columns) {
24
+ ident(c, "column");
25
+ if (seen.has(c.toLowerCase()))
26
+ throw new Error(`noun-index: column ${c} is declared twice`);
27
+ seen.add(c.toLowerCase());
28
+ }
29
+ for (const c of [...encoder.levels, ...encoder.indexed])
30
+ if (!encoder.columns.includes(c))
31
+ throw new Error(`noun-index: ${c} is not a column`);
32
+ return { table, value, valueType, isKey: /primary\s+key/iu.test(valueType) };
33
+ }
34
+ function indexSchema(encoder, options = {}) {
35
+ const r = resolve(encoder, options);
36
+ const cols = [`${r.value}${r.valueType === "" ? "" : ` ${r.valueType}`}`];
37
+ for (const c of encoder.columns)
38
+ cols.push(`${c} TEXT`);
39
+ const indexes = [];
40
+ if (!r.isKey)
41
+ indexes.push(`CREATE INDEX IF NOT EXISTS ${r.table}_${r.value} ON ${r.table}(${r.value})`);
42
+ for (const c of encoder.indexed)
43
+ indexes.push(`CREATE INDEX IF NOT EXISTS ${r.table}_${c} ON ${r.table}(${c})`);
44
+ return { table: `CREATE TABLE IF NOT EXISTS ${r.table}(${cols.join(", ")})`, indexes };
45
+ }
46
+ var CHUNK = 500;
47
+ function createIndex(handle, encoder, options = {}) {
48
+ const r = resolve(encoder, options);
49
+ const { table: T, value: VC } = r;
50
+ const cols = encoder.columns;
51
+ const prepared = new Map;
52
+ const stmt = (sql) => {
53
+ let s = prepared.get(sql);
54
+ if (s === undefined) {
55
+ s = handle.prepare(sql);
56
+ prepared.set(sql, s);
57
+ }
58
+ return s;
59
+ };
60
+ const column = (c) => {
61
+ if (!cols.includes(c))
62
+ throw new Error(`noun-index: ${c} is not a column of ${T}`);
63
+ return c;
64
+ };
65
+ const level = (l) => {
66
+ if (!encoder.levels.includes(l))
67
+ throw new Error(`noun-index: ${l} is not a level of ${T}`);
68
+ return l;
69
+ };
70
+ const insertSql = `INSERT INTO ${T}(${VC}, ${cols.join(", ")}) VALUES(${cols.map(() => "?").join(", ")}, ?)`;
71
+ const params = (x) => {
72
+ const e = encoder.encode(x);
73
+ return cols.map((c) => e[c] ?? null);
74
+ };
75
+ const insert = (x, value) => {
76
+ stmt(insertSql).run(value, ...params(x));
77
+ };
78
+ const keyAt = (x, l) => {
79
+ level(l);
80
+ return encoder.key === undefined ? encoder.encode(x)[l] ?? null : encoder.key(x, l);
81
+ };
82
+ const valuesWhere = (col, key) => stmt(`SELECT ${VC} AS v FROM ${T} WHERE ${col} = ? ORDER BY rowid`).all(key).map((o) => o.v);
83
+ const pick = (columns) => columns === undefined ? cols : columns.map(column);
84
+ const toRows = (raw, picked) => raw.map((o) => {
85
+ const row = {};
86
+ for (const c of picked)
87
+ row[c] = o[c] ?? null;
88
+ return { value: o.__v, row };
89
+ });
90
+ const selectList = (picked) => ["rowid AS __r", `${VC} AS __v`, ...picked].join(", ");
91
+ const index = {
92
+ table: T,
93
+ encoder,
94
+ add: insert,
95
+ addAll(entries) {
96
+ for (const [x, v] of entries)
97
+ insert(x, v);
98
+ },
99
+ get(x, l) {
100
+ const k = keyAt(x, l);
101
+ return k === null ? [] : valuesWhere(l, k);
102
+ },
103
+ has(x, l) {
104
+ const k = keyAt(x, l);
105
+ return k !== null && stmt(`SELECT 1 FROM ${T} WHERE ${l} = ? LIMIT 1`).all(k).length > 0;
106
+ },
107
+ find: (c, key) => valuesWhere(column(c), key),
108
+ rows(where, columns) {
109
+ const picked = pick(columns);
110
+ const seen = new Map;
111
+ for (const [c, keys] of Object.entries(where)) {
112
+ column(c);
113
+ const uniq = [...new Set(keys)];
114
+ for (let i = 0;i < uniq.length; i += CHUNK) {
115
+ const part = uniq.slice(i, i + CHUNK);
116
+ const sql = `SELECT ${selectList(picked)} FROM ${T} WHERE ${c} IN (${part.map(() => "?").join(",")})`;
117
+ for (const o of stmt(sql).all(...part))
118
+ seen.set(Number(o.__r), o);
119
+ }
120
+ }
121
+ return toRows([...seen].sort((a, b) => a[0] - b[0]).map(([, o]) => o), picked);
122
+ },
123
+ glob(c, pattern, columns) {
124
+ const picked = pick(columns);
125
+ const raw = stmt(`SELECT ${selectList(picked)} FROM ${T} WHERE ${column(c)} GLOB ? ORDER BY rowid`).all(pattern);
126
+ return toRows(raw, picked);
127
+ },
128
+ nouns(value) {
129
+ const raw = stmt(`SELECT ${cols.join(", ")} FROM ${T} WHERE ${VC} = ? ORDER BY rowid`).all(value);
130
+ return raw.map((o) => encoder.decode(o));
131
+ },
132
+ remove(value) {
133
+ const n = stmt(`SELECT count(*) AS n FROM ${T} WHERE ${VC} = ?`).all(value)[0].n;
134
+ if (n > 0)
135
+ stmt(`DELETE FROM ${T} WHERE ${VC} = ?`).run(value);
136
+ return Number(n);
137
+ },
138
+ size: () => Number(stmt(`SELECT count(*) AS n FROM ${T}`).all()[0].n),
139
+ map: (l) => viewAt(l)
140
+ };
141
+ function viewAt(l) {
142
+ const lc = level(l);
143
+ const heads = `SELECT rowid AS __r, ${VC} AS __v, ${cols.join(", ")} FROM ${T}
144
+ WHERE ${lc} IS NULL OR rowid IN (SELECT min(rowid) FROM ${T} WHERE ${lc} IS NOT NULL GROUP BY ${lc})
145
+ ORDER BY rowid`;
146
+ const head = (k) => stmt(`SELECT rowid AS __r, ${VC} AS __v FROM ${T} WHERE ${lc} = ? ORDER BY rowid LIMIT 1`).all(k)[0];
147
+ const all = () => stmt(heads).all();
148
+ const view = {
149
+ dop: l,
150
+ get size() {
151
+ const o = stmt(`SELECT (SELECT count(DISTINCT ${lc}) FROM ${T}) + (SELECT count(*) FROM ${T} WHERE ${lc} IS NULL) AS n`).all()[0];
152
+ return Number(o.n);
153
+ },
154
+ get(x) {
155
+ const k = keyAt(x, l);
156
+ return k === null ? undefined : head(k)?.__v;
157
+ },
158
+ has(x) {
159
+ const k = keyAt(x, l);
160
+ return k !== null && head(k) !== undefined;
161
+ },
162
+ set(x, value) {
163
+ const k = keyAt(x, l);
164
+ const h = k === null ? undefined : head(k);
165
+ if (h === undefined)
166
+ insert(x, value);
167
+ else
168
+ stmt(`UPDATE ${T} SET ${VC} = ? WHERE rowid = ?`).run(value, h.__r);
169
+ return this;
170
+ },
171
+ delete(x) {
172
+ const k = keyAt(x, l);
173
+ if (k === null || head(k) === undefined)
174
+ return false;
175
+ stmt(`DELETE FROM ${T} WHERE ${lc} = ?`).run(k);
176
+ return true;
177
+ },
178
+ clear() {
179
+ stmt(`DELETE FROM ${T}`).run();
180
+ },
181
+ *keys() {
182
+ for (const o of all())
183
+ yield encoder.decode(o);
184
+ },
185
+ *values() {
186
+ for (const o of all())
187
+ yield o.__v;
188
+ },
189
+ *entries() {
190
+ for (const o of all())
191
+ yield [encoder.decode(o), o.__v];
192
+ },
193
+ [Symbol.iterator]() {
194
+ return this.entries();
195
+ }
196
+ };
197
+ return view;
198
+ }
199
+ return index;
200
+ }
201
+ var sameColumns = (a, b) => NAME_COLUMNS.every((c) => a[c] === b[c]);
202
+ function personEncoder(options = {}) {
203
+ const read = options.read ?? ((p, o) => personName(p, o));
204
+ const printCol = options.print ?? "print";
205
+ const readingCol = options.reading ?? "reading";
206
+ const readingOf = (x, cols) => {
207
+ const g = read(x.nn0, "given-first");
208
+ if (g === x || sameColumns(cols, nameColumns(g)))
209
+ return "given-first";
210
+ if (sameColumns(cols, nameColumns(read(x.nn0, "surname-first"))))
211
+ return "surname-first";
212
+ throw new Error(`personEncoder: ${JSON.stringify(x.nn0)} was not read by this encoder's reader in either order`);
213
+ };
214
+ return {
215
+ columns: [printCol, readingCol, ...NAME_COLUMNS],
216
+ levels: NAME_LEVEL_COLUMNS,
217
+ indexed: NAME_COLUMNS.filter((c) => c !== "given_key"),
218
+ encode(x) {
219
+ const cols = nameColumns(x);
220
+ return { [printCol]: x.nn0, [readingCol]: readingOf(x, cols), ...cols };
221
+ },
222
+ key: (x, level) => nameColumns(x)[level],
223
+ decode(row) {
224
+ const order = row[readingCol] === "surname-first" ? "surname-first" : "given-first";
225
+ return read(row[printCol] ?? "", order);
226
+ }
227
+ };
228
+ }
229
+ export {
230
+ createIndex,
231
+ indexSchema,
232
+ personEncoder
233
+ };
package/dist/school.js CHANGED
@@ -2172,9 +2172,35 @@ function mapOfSchools(dop, entries) {
2172
2172
  map.set(name, value);
2173
2173
  return map;
2174
2174
  }
2175
+ var SCHOOL_LEVEL_COLUMNS = ["sn0", "sn1", "sn2", "sn3", "sn4", "realWords"];
2176
+ function schoolColumns(name) {
2177
+ return {
2178
+ sn0: slotOf(name, "sn0"),
2179
+ sn1: slotOf(name, "sn1"),
2180
+ sn2: slotOf(name, "sn2"),
2181
+ sn3: slotOf(name, "sn3"),
2182
+ sn4: slotOf(name, "sn4"),
2183
+ realWords: slotOf(name, "realWords")
2184
+ };
2185
+ }
2175
2186
  function isLadderLevel(dop) {
2176
2187
  return LADDER.has(dop);
2177
2188
  }
2189
+ function schoolEncoder(options = {}) {
2190
+ const reader = options.reader ?? DEFAULT_SCHOOL_NAME_READER;
2191
+ const printCol = options.print ?? "print";
2192
+ return {
2193
+ columns: [printCol, ...SCHOOL_LEVEL_COLUMNS, "matchKey"],
2194
+ levels: SCHOOL_LEVEL_COLUMNS,
2195
+ indexed: [...SCHOOL_LEVEL_COLUMNS, "matchKey"],
2196
+ encode(x) {
2197
+ const key = foldSchoolName(x.sn0);
2198
+ return { [printCol]: x.sn0, ...schoolColumns(x), matchKey: key === "" ? null : key };
2199
+ },
2200
+ key: (x, level) => schoolColumns(x)[level],
2201
+ decode: (row) => reader.schoolName(row[printCol] ?? "")
2202
+ };
2203
+ }
2178
2204
  function compareSchoolNames(a, b, reader) {
2179
2205
  return compareSchools(reader.schoolName(a), reader.schoolName(b));
2180
2206
  }
@@ -2217,6 +2243,7 @@ export {
2217
2243
  DEFAULT_SCHOOL_NAME_CAPACITY,
2218
2244
  DEFAULT_SCHOOL_NAME_READER,
2219
2245
  NOT_A_SCHOOL,
2246
+ SCHOOL_LEVEL_COLUMNS,
2220
2247
  SCHOOL_NAME_EXCEPTIONS,
2221
2248
  compareSchoolNames,
2222
2249
  compareSchools,
@@ -2231,7 +2258,9 @@ export {
2231
2258
  realWords,
2232
2259
  realWordsInPrintOrder,
2233
2260
  sameInEveryPart,
2261
+ schoolColumns,
2234
2262
  schoolCompare,
2263
+ schoolEncoder,
2235
2264
  schoolInitials,
2236
2265
  schoolKeyTokens,
2237
2266
  schoolPrintSplit,
@@ -0,0 +1,67 @@
1
+ import { type PersonName, type ResolvedNameOrder, type TransitiveLevel } from "@finbheara/names/normalize";
2
+ export type SqlValue = string | number | bigint | null;
3
+ export interface SqlStatement {
4
+ run(...params: SqlValue[]): unknown;
5
+ all(...params: SqlValue[]): unknown[];
6
+ }
7
+ export interface SqlHandle {
8
+ prepare(sql: string): SqlStatement;
9
+ }
10
+ export type EncodedRow = Readonly<Record<string, string | null>>;
11
+ export interface NounEncoder<X, L extends string = string> {
12
+ readonly columns: readonly string[];
13
+ readonly levels: readonly L[];
14
+ readonly indexed: readonly string[];
15
+ encode(x: X): EncodedRow;
16
+ key?(x: X, level: L): string | null;
17
+ decode(row: EncodedRow): X;
18
+ }
19
+ export interface IndexOptions {
20
+ readonly table?: string;
21
+ readonly value?: string;
22
+ readonly valueType?: string;
23
+ }
24
+ export interface IndexRow<V> {
25
+ readonly value: V;
26
+ readonly row: EncodedRow;
27
+ }
28
+ export interface IndexMap<X, V, L extends string> {
29
+ readonly dop: L;
30
+ readonly size: number;
31
+ get(x: X): V | undefined;
32
+ has(x: X): boolean;
33
+ set(x: X, value: V): this;
34
+ delete(x: X): boolean;
35
+ clear(): void;
36
+ keys(): IterableIterator<X>;
37
+ values(): IterableIterator<V>;
38
+ entries(): IterableIterator<[X, V]>;
39
+ [Symbol.iterator](): IterableIterator<[X, V]>;
40
+ }
41
+ export interface NounIndex<X, L extends string, V extends SqlValue> {
42
+ readonly table: string;
43
+ readonly encoder: NounEncoder<X, L>;
44
+ add(x: X, value: V): void;
45
+ addAll(entries: Iterable<readonly [X, V]>): void;
46
+ get(x: X, level: L): V[];
47
+ has(x: X, level: L): boolean;
48
+ find(column: string, key: string): V[];
49
+ rows(where: Readonly<Record<string, readonly string[]>>, columns?: readonly string[]): IndexRow<V>[];
50
+ glob(column: string, pattern: string, columns?: readonly string[]): IndexRow<V>[];
51
+ nouns(value: V): X[];
52
+ remove(value: V): number;
53
+ size(): number;
54
+ map(level: L): IndexMap<X, V, L>;
55
+ }
56
+ export declare function indexSchema(encoder: NounEncoder<unknown>, options?: IndexOptions): {
57
+ table: string;
58
+ indexes: string[];
59
+ };
60
+ export declare function createIndex<X, L extends string, V extends SqlValue = SqlValue>(handle: SqlHandle, encoder: NounEncoder<X, L>, options?: IndexOptions): NounIndex<X, L, V>;
61
+ export type PersonReader = (print: string, order: ResolvedNameOrder) => PersonName;
62
+ export interface PersonEncoderOptions {
63
+ readonly read?: PersonReader;
64
+ readonly print?: string;
65
+ readonly reading?: string;
66
+ }
67
+ export declare function personEncoder(options?: PersonEncoderOptions): NounEncoder<PersonName, TransitiveLevel>;
@@ -33,4 +33,7 @@ export interface SchoolMap<V> {
33
33
  }
34
34
  export declare function setOfSchools(dop: SchoolDop): SchoolSet;
35
35
  export declare function mapOfSchools<V>(dop: SchoolDop, entries?: Iterable<readonly [SchoolName, V]>): SchoolMap<V>;
36
+ export declare const SCHOOL_LEVEL_COLUMNS: readonly ["sn0", "sn1", "sn2", "sn3", "sn4", "realWords"];
37
+ export type SchoolLevelColumn = (typeof SCHOOL_LEVEL_COLUMNS)[number];
38
+ export declare function schoolColumns(name: SchoolName): Readonly<Record<SchoolLevelColumn, string | null>>;
36
39
  export declare function isLadderLevel(dop: SchoolDop): boolean;
@@ -0,0 +1,8 @@
1
+ import type { NounEncoder } from "./noun-index.ts";
2
+ import { type SchoolLevelColumn } from "./school-compare.ts";
3
+ import { type SchoolName, type SchoolNameParser } from "./school-name.ts";
4
+ export interface SchoolEncoderOptions {
5
+ readonly reader?: SchoolNameParser;
6
+ readonly print?: string;
7
+ }
8
+ export declare function schoolEncoder(options?: SchoolEncoderOptions): NounEncoder<SchoolName, SchoolLevelColumn>;
@@ -1,7 +1,9 @@
1
1
  export { createSchoolNameReader, DEFAULT_SCHOOL_NAME_CAPACITY, DEFAULT_SCHOOL_NAME_READER, realWords, realWordsInPrintOrder, schoolInitials, schoolPrintSplit, } from "./school-name.ts";
2
2
  export type { LexiconFlags, SchoolConcern, SchoolInitials, SchoolLexicon, SchoolName, SchoolNameParser, SchoolNameReader, SchoolNameReaderOptions, SchoolPart, SchoolPrintSplit, SchoolRefusal, } from "./school-name.ts";
3
- export { isColumnCutOf, isInitialismOf, isLadderLevel, mapOfSchools, probablySameSchool, sameInEveryPart, schoolCompare, schoolTokensAt, setOfSchools, } from "./school-compare.ts";
4
- export type { SchoolDop, SchoolMap, SchoolSet } from "./school-compare.ts";
3
+ export { isColumnCutOf, isInitialismOf, isLadderLevel, mapOfSchools, probablySameSchool, SCHOOL_LEVEL_COLUMNS, schoolColumns, sameInEveryPart, schoolCompare, schoolTokensAt, setOfSchools, } from "./school-compare.ts";
4
+ export type { SchoolDop, SchoolLevelColumn, SchoolMap, SchoolSet } from "./school-compare.ts";
5
+ export { schoolEncoder } from "./school-encoder.ts";
6
+ export type { SchoolEncoderOptions } from "./school-encoder.ts";
5
7
  export { compareSchoolNames, compareSchools } from "./school-identity.ts";
6
8
  export type { IdentityResult, IdentityVerdict } from "./school-identity.ts";
7
9
  export { foldSchoolName, hasSpecificToken, schoolKeyTokens } from "./school-match-key.ts";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@finbheara/domain",
3
- "version": "0.1.0",
3
+ "version": "0.2.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",
@@ -49,6 +49,10 @@
49
49
  "types": "./dist/types/dance-types.d.ts",
50
50
  "default": "./dist/dance-types.js"
51
51
  },
52
+ "./noun-index": {
53
+ "types": "./dist/types/noun-index.d.ts",
54
+ "default": "./dist/noun-index.js"
55
+ },
52
56
  "./package.json": "./package.json"
53
57
  },
54
58
  "files": ["dist/*.js", "dist/chunks/*.js", "dist/types/**/*.d.ts", "README.md", "LICENSE"],