@finbheara/domain 0.5.0 → 0.7.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/dist/place.js CHANGED
@@ -15,7 +15,7 @@ import {
15
15
  placesRelation2,
16
16
  probablySamePlace2,
17
17
  sameReading2
18
- } from "./chunks/place-0j6k50wf.js";
18
+ } from "./chunks/place-h7tvdfyp.js";
19
19
  export {
20
20
  DEFAULT_PLACE_CAPACITY2 as DEFAULT_PLACE_CAPACITY,
21
21
  DEFAULT_PLACE_READER2 as DEFAULT_PLACE_READER,
package/dist/promotion.js CHANGED
@@ -46,7 +46,7 @@ function samePromotedRow(was, now) {
46
46
  }
47
47
  var STOOD_FOR = /^(?:'|(?![A-Za-z])\p{L})$/u;
48
48
  function questionMarksRead(was, now) {
49
- const a = [...collapse(was)];
49
+ const a = [...collapse(mojibakeRead(was))];
50
50
  if (!a.includes("?"))
51
51
  return a.join("");
52
52
  const b = [...collapse(now)];
@@ -67,6 +67,19 @@ function questionMarksRead(was, now) {
67
67
  }
68
68
  return out.join("");
69
69
  }
70
+ function mojibakeRead(print) {
71
+ let out = print;
72
+ const codes = [...out].map((c) => c.codePointAt(0) ?? 0);
73
+ if (codes.some((c) => c >= 128) && codes.every((c) => c <= 255)) {
74
+ const bytes = Uint8Array.from(codes);
75
+ try {
76
+ const decoded = new TextDecoder("utf-8", { fatal: true }).decode(bytes);
77
+ if (decoded !== out)
78
+ out = decoded;
79
+ } catch {}
80
+ }
81
+ return out.replace(/\uFFFD/gu, "?");
82
+ }
70
83
  function samePromotedName(was, now) {
71
84
  const read = questionMarksRead(was, now);
72
85
  if (read === null)
@@ -86,20 +99,27 @@ function samePromotedName(was, now) {
86
99
  return glued !== "" && (glued === nameFold(`${given}${surname}`) || glued === nameFold(`${surname}${given}`));
87
100
  }
88
101
  function samePromotedHeir(was, now) {
89
- if (was.name !== "" && was.name === now.name)
102
+ if (collapse(was.name) !== "" && collapse(was.name) === collapse(now.name))
90
103
  return true;
91
104
  const school = questionMarksRead(was.school, now.school);
92
105
  if (school === null || !samePromotedSchool(school, now.school))
93
106
  return false;
107
+ if (collapse(was.name) === "" && collapse(now.name) !== "")
108
+ return true;
94
109
  return samePromotedName(was.name, now.name);
95
110
  }
111
+ function samePromotedPair(was, now) {
112
+ return samePromotedRow(was, now) || samePromotedHeir(was, now);
113
+ }
96
114
  export {
97
115
  CHAR_SYNONYM_CLASSES,
98
116
  collapse,
99
117
  foldCharSynonyms,
118
+ mojibakeRead,
100
119
  questionMarksRead,
101
120
  samePromotedHeir,
102
121
  samePromotedName,
122
+ samePromotedPair,
103
123
  samePromotedRow,
104
124
  samePromotedSchool
105
125
  };
package/dist/school.js CHANGED
@@ -1,11 +1,14 @@
1
1
  import {
2
+ NEVER_NAME,
3
+ printZones,
4
+ opensLocation,
2
5
  place2,
3
6
  isPlaceToken2,
4
7
  countryCodeOf2,
5
8
  areAllPlaceTokens2,
6
9
  placeRelation2,
7
10
  sameReading2
8
- } from "./chunks/place-0j6k50wf.js";
11
+ } from "./chunks/place-h7tvdfyp.js";
9
12
  import {
10
13
  validity
11
14
  } from "./chunks/place-q9kwsmzd.js";
@@ -870,130 +873,6 @@ function truncatedInstitution(tail) {
870
873
  return found;
871
874
  }
872
875
 
873
- var QUALIFIER_DELIMITER = /[,;:()[\]/–—-]/;
874
- var NEVER_PLACE = new Map([
875
- ["de", "name particle: `De <Surname>`, and Spanish `de <Place>`"],
876
- ["ni", "Irish patronymic particle: `Ní <Surname>`"],
877
- ["mo", "Irish possessive: `Mo <Word>`"],
878
- ["na", "Irish article: `Rince na <Word>`"],
879
- ["in", "Irish preposition: `<Word> In <Word>`; and `Inc` truncated to `In`"],
880
- ["co", "Company: `<Surname> & Co.`. Bracketed `(US-CO)` is still Colorado"],
881
- ["id", "Irish Dance: `<Surname> School of ID`"],
882
- ["sd", "School of Dance: `<Surname> SD`"]
883
- ]);
884
- var NEVER_NAME = new Map([
885
- ["de", "name particle: `De <Surname>`; and Spanish `de <Place>`"],
886
- ["ni", "Irish patronymic particle: `Ní <Surname>`"],
887
- ["ui", "Irish patronymic particle: `Uí <Surname>`"],
888
- ["mo", "Irish possessive: `Mo <Word>`"],
889
- ["na", "Irish article: `Rince na <Word>`"],
890
- ["an", "Irish article: `<Word> an Rince`"],
891
- ["in", "Irish preposition: `<Word> In <Word>`; and `Inc` truncated to `In`"],
892
- ["co", "Company: `<Surname> & Co`"],
893
- ["id", "Irish Dance: `ID de <Place>` keys the place, as `Irish Dancing de <Place>` does"],
894
- ["sd", "School of Dance: `<Surname> SD`"],
895
- ["da", "`Dance` truncated: `<Name> Academy of Irish Da` keys the name"]
896
- ]);
897
- var GENERIC_TLD = "com|net|org|edu|gov|mil|int|info|biz";
898
- var WEB_SPAN = new RegExp([
899
- "[A-Za-z][A-Za-z0-9+.-]*://\\S+",
900
- "(?:^|[\\s([])www\\.\\S+",
901
- "[^\\s@]+@[^\\s@]+\\.[A-Za-z]{2,}",
902
- `(?<![A-Za-z0-9.@-])(?:[A-Za-z0-9][A-Za-z0-9-]*\\.)*[A-Za-z0-9][A-Za-z0-9-]+\\.(?:${GENERIC_TLD})(?![A-Za-z0-9-])`
903
- ].join("|"), "giu");
904
- function printZones(raw) {
905
- const qualifier = new Set;
906
- const bracketed = new Set;
907
- const qualifierAny = new Set;
908
- const runs = [...raw.matchAll(/[A-Za-z0-9]+/g)];
909
- const web = [];
910
- for (const m of raw.matchAll(WEB_SPAN))
911
- web.push([m.index ?? 0, (m.index ?? 0) + m[0].length]);
912
- const inside = new Set;
913
- const outside = new Set;
914
- const trailing = new Set;
915
- for (let i = runs.length - 1;i > 0; i--) {
916
- const here = runs[i];
917
- const prev = runs[i - 1];
918
- if (here === undefined || prev === undefined)
919
- break;
920
- const gap = raw.slice((prev.index ?? 0) + prev[0].length, here.index ?? 0);
921
- if (!QUALIFIER_DELIMITER.test(gap))
922
- break;
923
- if (i - 1 === 0)
924
- break;
925
- trailing.add(i - 1);
926
- }
927
- let depth = 0;
928
- let cursor = 0;
929
- for (let i = 0;i < runs.length; i++) {
930
- const run = runs[i];
931
- const text = run?.[0];
932
- if (run === undefined || text === undefined)
933
- continue;
934
- const at = run.index ?? cursor;
935
- for (const ch of raw.slice(cursor, at)) {
936
- if (ch === "(" || ch === "[")
937
- depth++;
938
- else if ((ch === ")" || ch === "]") && depth > 0)
939
- depth--;
940
- }
941
- cursor = at + text.length;
942
- const folded = text.toLowerCase().normalize("NFD").replace(/[̀-ͯ]/g, "");
943
- (web.some(([s, e]) => at >= s && at < e) ? inside : outside).add(folded);
944
- const before = raw.slice(0, at).trimEnd().slice(-1);
945
- const after = raw.slice(cursor).trimStart().slice(0, 1);
946
- const atQualifier = i === runs.length - 1 || before !== "" && QUALIFIER_DELIMITER.test(before) || after !== "" && QUALIFIER_DELIMITER.test(after) && trailing.has(i);
947
- if (atQualifier)
948
- qualifierAny.add(folded);
949
- if (folded.length > 2)
950
- continue;
951
- if (atQualifier)
952
- qualifier.add(folded);
953
- if (depth > 0)
954
- bracketed.add(folded);
955
- }
956
- const webOnly = new Set([...inside].filter((f) => !outside.has(f)));
957
- return { qualifier, bracketed, qualifierAny, web, webOnly, tailCode: tailCode(raw, runs) };
958
- }
959
- var GOVERNS_THE_NEXT_WORD = new Set(["de", "mo"]);
960
- function tailCode(raw, runs) {
961
- const last = runs[runs.length - 1];
962
- const prev = runs[runs.length - 2];
963
- if (last === undefined || prev === undefined)
964
- return null;
965
- const text = last[0];
966
- if (text.length > 2 || text !== text.toUpperCase() || !/^[A-Z]+$/.test(text))
967
- return null;
968
- const at = last.index ?? 0;
969
- const folded = text.toLowerCase();
970
- const gap = raw.slice((prev.index ?? 0) + prev[0].length, at);
971
- const marked = /[,;:/\\\u2013\u2014]/u.test(gap) || /\s-|-\s/u.test(gap);
972
- const bare = GOVERNS_THE_NEXT_WORD.has(folded) && /^\s+$/u.test(gap) && /\p{Ll}/u.test(raw);
973
- if (!marked && !bare)
974
- return null;
975
- const only = runs.filter((r) => r[0].toLowerCase() === folded).length === 1;
976
- return { folded, at, only };
977
- }
978
- function opensLocation(folded, at, index) {
979
- if (index === undefined ? at.webOnly.has(folded) : inWebSpan(at, index))
980
- return false;
981
- if (folded.length > 2)
982
- return true;
983
- if (NEVER_PLACE.has(folded))
984
- return at.bracketed.has(folded) || isTailCode(folded, at, index);
985
- return at.qualifier.has(folded);
986
- }
987
- function isTailCode(folded, at, index) {
988
- const tail = at.tailCode;
989
- if (tail === null || tail.folded !== folded)
990
- return false;
991
- return index === undefined ? tail.only : index === tail.at;
992
- }
993
- function inWebSpan(at, index) {
994
- return at.web.some(([start, end]) => index >= start && index < end);
995
- }
996
-
997
876
  var SCHOOL_FLUFF_WORDS = new Set([
998
877
  "academy",
999
878
  "irish",
@@ -1140,6 +1019,13 @@ function createSchoolNameReader(options = {}) {
1140
1019
  const CP1252_HIGH = "€‚ƒ„…†‡ˆ‰Š‹Œ" + "Ž‘’“”•–—˜™š›" + "œžŸ";
1141
1020
  const BYTE_OF_CP1252 = new Map([...CP1252_HIGH].map((ch, i) => [ch.codePointAt(0), 128 + i]));
1142
1021
  const MOJIBAKE = /[Â-ßà-ï][€-¿‚-›ŒŠŽ]/u;
1022
+ const MOJIBAKE_ALL = new RegExp(MOJIBAKE.source, "gu");
1023
+ const NON_LATIN_LETTER = /(?!\p{Script=Latin})\p{L}/u;
1024
+ function isRepair(before, after) {
1025
+ if (!NON_LATIN_LETTER.test(after))
1026
+ return true;
1027
+ return (before.match(MOJIBAKE_ALL)?.length ?? 0) >= 2;
1028
+ }
1143
1029
  function undoMojibake(s) {
1144
1030
  const bytes = new Uint8Array(s.length);
1145
1031
  for (let i = 0;i < s.length; i++) {
@@ -1159,14 +1045,19 @@ function createSchoolNameReader(options = {}) {
1159
1045
  let s = print;
1160
1046
  for (let round = 0;round < 3 && MOJIBAKE.test(s); round++) {
1161
1047
  const undone = undoMojibake(s);
1162
- if (undone === undefined || undone === s)
1048
+ if (undone === undefined || undone === s || !isRepair(s, undone))
1163
1049
  return s;
1164
1050
  s = undone;
1165
1051
  }
1166
1052
  return s;
1167
1053
  }
1054
+ const SPACING_ACUTE_AFTER_VOWEL = /((?<=\p{L})[AEIOUaeiou]|[AEIUaeiu])[´ˊ](?![sS]\b)/gu;
1055
+ const SPACING_ACUTE = /[´ˊ]/gu;
1056
+ function readSpacingAcute(print) {
1057
+ return print.replace(SPACING_ACUTE_AFTER_VOWEL, "$1́").replace(SPACING_ACUTE, "'");
1058
+ }
1168
1059
  function renderSn1(print) {
1169
- return normalizeApostrophes(repairMojibake(print).normalize("NFKC")).replace(/\p{Cf}+/gu, "").replace(/\p{Cc}+/gu, " ").replace(/\s+/gu, " ").trim();
1060
+ return normalizeApostrophes(readSpacingAcute(repairMojibake(print)).normalize("NFKC")).replace(/\p{Cf}+/gu, "").replace(/\p{Cc}+/gu, " ").replace(/\s+/gu, " ").trim();
1170
1061
  }
1171
1062
  const COMPOUND_OPENERS = new Set([
1172
1063
  "hyphen",
@@ -2035,8 +1926,8 @@ function isColumnCutOf(a, b) {
2035
1926
  if (!comparable(a) || !comparable(b))
2036
1927
  return false;
2037
1928
  const cutFrom = (cut, whole) => {
2038
- const c = foldPrint(cut.sn0);
2039
- const w = foldPrint(whole.sn0);
1929
+ const c = foldPrint(cut.sn1);
1930
+ const w = foldPrint(whole.sn1);
2040
1931
  if (c.length === 0 || c.length >= w.length || !w.startsWith(c))
2041
1932
  return false;
2042
1933
  if (w[c.length] === " ")
@@ -0,0 +1,12 @@
1
+ export declare const TITLE_RANGE_MIN = 3;
2
+ export declare const TITLE_RANGE_MAX = 40;
3
+ export interface AgeTitleSpan {
4
+ readonly start: number;
5
+ readonly end: number;
6
+ readonly text: string;
7
+ }
8
+ export interface AgeTitleToken {
9
+ readonly token: string;
10
+ readonly span: AgeTitleSpan | null;
11
+ }
12
+ export declare function ageTitleToken(title: string): AgeTitleToken;
@@ -9,8 +9,10 @@ export interface BirthYears {
9
9
  export interface AgeLadder {
10
10
  readonly caps: readonly number[];
11
11
  readonly overs: readonly number[];
12
- readonly ranges: readonly number[];
12
+ readonly ranges: readonly AgeSpan[];
13
+ readonly adult?: boolean;
13
14
  }
15
+ export type AgeSpan = readonly [number, number];
14
16
  export interface YOBContext {
15
17
  readonly year?: number | null;
16
18
  }
@@ -21,9 +23,9 @@ export interface AgeContext {
21
23
  }
22
24
  export type AgeArm = "band" | "yob" | "open" | "empty";
23
25
  export type AgeKind = "under" | "over" | "range" | "adult" | "senior" | "yob";
24
- export type AgeRefusal = "not-a-band" | "backwards-range" | "senior-without-ladder" | "below-youngest-band" | "year-out-of-range";
26
+ export type AgeRefusal = "not-a-band" | "backwards-range" | "overlapping-ranges" | "senior-without-ladder" | "below-youngest-band" | "year-out-of-range";
25
27
  export type AgeSource = "sibling-narrowed" | "printed-band" | "printed-range" | "printed-yob" | "open";
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";
28
+ export type AgeClause = "under-floored-on-rung" | "youngest-absorbs" | "over-ceiling-from-next-over" | "over-ceiling-refused-gap" | "and-over-no-ceiling" | "adult-under-ceiling" | "range-overlapping" | "range-as-under" | "range-top-meets-over" | "range-adult-floor";
27
29
  export type AgeRelation = "same" | "contains" | "overlaps" | "disjoint" | "silent";
28
30
  export type AgeRelationReason = "equal" | "a-contains-b" | "b-contains-a" | "partial" | "a-older" | "a-younger" | "a-not-a-band" | "b-not-a-band";
29
31
  export interface AgeComparison {
@@ -1,6 +1,8 @@
1
- import type { AgeArm, AgeClause, AgeComparison, AgeContext, AgeInterval, AgeKind, AgeLadder, AgeMisuse, AgeRefusal, AgeRelation, AgeRelationReason, AgeSource, BirthYears, YOBContext, YoungFloorVerdict } from "./age-types.ts";
1
+ import { type AgeTitleSpan } from "./age-title.ts";
2
+ import type { AgeArm, AgeClause, AgeComparison, AgeContext, AgeInterval, AgeKind, AgeLadder, AgeMisuse, AgeRefusal, AgeRelation, AgeRelationReason, AgeSource, AgeSpan, BirthYears, YOBContext, YoungFloorVerdict } from "./age-types.ts";
2
3
  import { type EmptyReason, type Validated } from "./validated.ts";
3
- export type { AgeArm, AgeClause, AgeComparison, AgeContext, AgeInterval, AgeKind, AgeLadder, AgeMisuse, AgeRefusal, AgeRelation, AgeRelationReason, AgeSource, BirthYears, EmptyReason, Validated, YOBContext, YoungFloorVerdict, };
4
+ export { type AgeTitleSpan, type AgeTitleToken, TITLE_RANGE_MAX, TITLE_RANGE_MIN, ageTitleToken, } from "./age-title.ts";
5
+ export type { AgeArm, AgeClause, AgeComparison, AgeContext, AgeInterval, AgeKind, AgeLadder, AgeMisuse, AgeRefusal, AgeRelation, AgeRelationReason, AgeSource, AgeSpan, BirthYears, EmptyReason, Validated, YOBContext, YoungFloorVerdict, };
4
6
  export declare const YOUNGEST_FLOOR = 0;
5
7
  export declare const ADULT_FLOOR = 18;
6
8
  export declare const MAJORS_FLOOR_YEARS = 3;
@@ -54,9 +56,16 @@ export declare class AgeMisuseError extends Error {
54
56
  }
55
57
  declare function fromPrint(print: string, context?: AgeContext): Age;
56
58
  declare function fromYOB(from: number | null, to?: number | null, context?: YOBContext): Age;
59
+ export interface AgeInTitle {
60
+ readonly age: Age;
61
+ readonly token: string;
62
+ readonly span: AgeTitleSpan | null;
63
+ }
64
+ declare function fromTitle(title: string, context?: AgeContext): AgeInTitle;
57
65
  export declare const Age: Readonly<{
58
66
  from: typeof fromPrint;
59
67
  fromYOB: typeof fromYOB;
68
+ fromTitle: typeof fromTitle;
60
69
  }>;
61
70
  export declare function birthYearsOfInterval(iv: AgeInterval | null, year: number): BirthYears;
62
71
  export declare function ageRelation(a: Age, b: Age): AgeComparison;
@@ -8,13 +8,16 @@ export interface PlaceNode {
8
8
  readonly prints: readonly string[];
9
9
  readonly codes: readonly string[];
10
10
  readonly truncations?: readonly string[];
11
+ readonly headings?: readonly string[];
12
+ readonly runPrints?: readonly string[];
13
+ readonly listedCutsOnly?: true;
11
14
  readonly iso2?: string;
12
15
  readonly iso3?: string;
13
16
  readonly isoNumeric?: string;
14
17
  readonly oireachtas?: string;
15
18
  }
16
- export type PlaceMatch = "id" | "code" | "print" | "phrase" | "iso" | "county" | "truncation";
17
- export type PlaceRefusal = "school" | "organization" | "placeholder" | "damaged";
19
+ export type PlaceMatch = "id" | "code" | "print" | "phrase" | "iso" | "county" | "truncation" | "desmushed";
20
+ export type PlaceRefusal = "school" | "organization" | "placeholder" | "damaged" | "web" | "online" | "notice";
18
21
  export type PlaceRelation = "same" | "contains" | "overlaps" | "conflict" | "silent";
19
22
  export interface PlaceContext {
20
23
  readonly region?: string;
@@ -12,5 +12,7 @@ export declare const collapse: (s: string) => string;
12
12
  export declare function samePromotedSchool(was: string, now: string): boolean;
13
13
  export declare function samePromotedRow(was: PromotedPrint, now: PromotedPrint): boolean;
14
14
  export declare function questionMarksRead(was: string, now: string): string | null;
15
+ export declare function mojibakeRead(print: string): string;
15
16
  export declare function samePromotedName(was: string, now: string): boolean;
16
17
  export declare function samePromotedHeir(was: PromotedPrint, now: PromotedPrint): boolean;
18
+ export declare function samePromotedPair(was: PromotedPrint, now: PromotedPrint): boolean;
@@ -12,5 +12,6 @@ export interface PrintZones {
12
12
  readonly only: boolean;
13
13
  } | null;
14
14
  }
15
+ export declare function printsWeb(raw: string): boolean;
15
16
  export declare function printZones(raw: string): PrintZones;
16
17
  export declare function opensLocation(folded: string, at: PrintZones, index?: number): boolean;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@finbheara/domain",
3
- "version": "0.5.0",
3
+ "version": "0.7.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",
@@ -73,6 +73,6 @@
73
73
  "prepublishOnly": "bun scripts/build.ts && bun scripts/guard-tarball.ts"
74
74
  },
75
75
  "dependencies": {
76
- "@finbheara/names": "0.17.0"
76
+ "@finbheara/names": "0.18.0"
77
77
  }
78
78
  }