@jeongpd/korean-kinship 0.1.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/src/index.js ADDED
@@ -0,0 +1,5 @@
1
+ export { resolve } from './resolve.js';
2
+ export { GOLDEN_TERMS as termTable } from './data/golden-terms.js';
3
+ import { PRIMITIVES } from './primitives.js';
4
+
5
+ export const relationIds = Object.keys(PRIMITIVES);
@@ -0,0 +1,15 @@
1
+ export const PRIMITIVES = {
2
+ father: { generationDelta: 1, edgeCost: 1, gender: 'M', category: 'parent' },
3
+ mother: { generationDelta: 1, edgeCost: 1, gender: 'F', category: 'parent' },
4
+ olderBro: { generationDelta: 0, edgeCost: 2, gender: 'M', category: 'sibling', order: 'older' },
5
+ olderSis: { generationDelta: 0, edgeCost: 2, gender: 'F', category: 'sibling', order: 'older' },
6
+ youngerBro: { generationDelta: 0, edgeCost: 2, gender: 'M', category: 'sibling', order: 'younger' },
7
+ youngerSis: { generationDelta: 0, edgeCost: 2, gender: 'F', category: 'sibling', order: 'younger' },
8
+ son: { generationDelta: -1, edgeCost: 1, gender: 'M', category: 'child' },
9
+ daughter: { generationDelta: -1, edgeCost: 1, gender: 'F', category: 'child' },
10
+ spouse: { generationDelta: 0, edgeCost: 0, gender: null, category: 'spouse' },
11
+ };
12
+
13
+ export function isValidRelationId(id) {
14
+ return Object.prototype.hasOwnProperty.call(PRIMITIVES, id);
15
+ }
package/src/resolve.js ADDED
@@ -0,0 +1,138 @@
1
+ import { PRIMITIVES, isValidRelationId } from './primitives.js';
2
+ import { GOLDEN_TERMS } from './data/golden-terms.js';
3
+ import { FALLBACK_TERMS, genericFallback } from './data/fallback-terms.js';
4
+
5
+ function assertValidPath(path) {
6
+ if (!Array.isArray(path) || path.length === 0) {
7
+ throw new Error('resolve() requires a non-empty array of relation ids');
8
+ }
9
+ for (const id of path) {
10
+ if (!isValidRelationId(id)) {
11
+ throw new Error(`Unknown relation id: "${id}"`);
12
+ }
13
+ }
14
+ }
15
+
16
+ function pickBySelfGender(record, selfGender) {
17
+ if (selfGender === 'F' && record.F !== undefined) return record.F;
18
+ if (selfGender === 'M' && record.M !== undefined) return record.M;
19
+ return record.M ?? record.F;
20
+ }
21
+
22
+ // Exported for the compositional-rule tests only — not re-exported from src/index.js,
23
+ // and package `exports` blocks deep imports, so this is not public API.
24
+ export function walkPath(path) {
25
+ let generationDelta = 0;
26
+ let chonEdges = 0;
27
+ let bloodChonSoFar = 0;
28
+ let bloodChonAtFirstSpouse = null;
29
+ let firstSpouseIndex = null;
30
+ let relationClass = 'blood';
31
+ let olderYounger = null;
32
+ // Gender of the person the path lands on. Every primitive but `spouse` fixes it
33
+ // outright; a spouse hop flips it (null stays null — a path of nothing but spouse
34
+ // hops has no gendered person to anchor on).
35
+ let terminalGender = null;
36
+
37
+ for (const [index, id] of path.entries()) {
38
+ const rel = PRIMITIVES[id];
39
+ generationDelta += rel.generationDelta;
40
+ chonEdges += rel.edgeCost;
41
+
42
+ if (rel.category === 'sibling') {
43
+ olderYounger = rel.order;
44
+ }
45
+
46
+ if (rel.category === 'spouse') {
47
+ if (firstSpouseIndex === null) {
48
+ firstSpouseIndex = index;
49
+ bloodChonAtFirstSpouse = bloodChonSoFar;
50
+ }
51
+ relationClass = 'inlaw';
52
+ if (terminalGender !== null) {
53
+ terminalGender = terminalGender === 'M' ? 'F' : 'M';
54
+ }
55
+ } else {
56
+ bloodChonSoFar += rel.edgeCost;
57
+ terminalGender = rel.gender;
58
+ }
59
+ }
60
+
61
+ // In-law 촌수 is not the blood chon carried through. Korean puts no number on a
62
+ // relation once the chain crosses into another family: any hop *after* the first
63
+ // spouse hop (a second marriage link, or a blood hop taken from the spouse's own
64
+ // family — 사돈 territory) reports chon 0, as does a spouse hop onto a close blood
65
+ // relative (chon ≤ 2 at that point: self, child, sibling). Only a spouse hop onto a
66
+ // distant blood relative with nothing after it keeps that relative's own chon
67
+ // (큰아버지 3촌 → 큰어머니 3촌).
68
+ const hopsAfterFirstSpouse = firstSpouseIndex === null
69
+ ? 0
70
+ : path.length - 1 - firstSpouseIndex;
71
+
72
+ const chon = relationClass === 'inlaw'
73
+ ? ((hopsAfterFirstSpouse > 0 || bloodChonAtFirstSpouse <= 2) ? 0 : bloodChonAtFirstSpouse)
74
+ : chonEdges;
75
+
76
+ return { generationDelta, relationClass, olderYounger, chon, terminalGender };
77
+ }
78
+
79
+ // The descriptor key deliberately omits 친가/외가/처가/시가 `side`. Detecting it
80
+ // correctly means finding the first hop that branches off the direct blood line (and
81
+ // 처가 vs 시가 depends on opts.selfGender) — the previous implementation instead took
82
+ // any father/mother hop at any position, which mis-tagged 사돈 paths like
83
+ // son.spouse.father as 'paternal'. FALLBACK_TERMS is empty, so no entry needs side
84
+ // yet; add it here, correctly, alongside the first Tier-2 entry that actually keys on it.
85
+ function descriptorKey({ generationDelta, relationClass, olderYounger }) {
86
+ return [generationDelta, relationClass, olderYounger ?? '-'].join('|');
87
+ }
88
+
89
+ export function resolve(path, opts = {}) {
90
+ assertValidPath(path);
91
+ const selfGender = opts.selfGender;
92
+
93
+ const exact = GOLDEN_TERMS[path.join('.')];
94
+ if (exact) {
95
+ return {
96
+ path: [...path],
97
+ label: pickBySelfGender(exact, selfGender),
98
+ reverseLabel: exact.rev_M !== undefined
99
+ ? pickBySelfGender({ M: exact.rev_M, F: exact.rev_F }, selfGender)
100
+ : undefined,
101
+ chon: exact.chon,
102
+ formal: exact.formal !== undefined
103
+ ? exact.formal
104
+ : (exact.formal_M !== undefined || exact.formal_F !== undefined
105
+ ? pickBySelfGender({ M: exact.formal_M, F: exact.formal_F }, selfGender)
106
+ : undefined),
107
+ note: exact.note,
108
+ // Golden entries don't record it, but the same rule the compositional path uses
109
+ // applies: any spouse hop anywhere in the chain makes it an in-law relation.
110
+ relationClass: path.includes('spouse') ? 'inlaw' : 'blood',
111
+ source: 'golden',
112
+ };
113
+ }
114
+
115
+ const descriptor = walkPath(path);
116
+ const key = descriptorKey(descriptor);
117
+ const fallback = FALLBACK_TERMS[key];
118
+ if (fallback) {
119
+ return {
120
+ path: [...path],
121
+ label: pickBySelfGender(fallback, selfGender),
122
+ reverseLabel: fallback.rev_M !== undefined
123
+ ? pickBySelfGender({ M: fallback.rev_M, F: fallback.rev_F }, selfGender)
124
+ : undefined,
125
+ chon: descriptor.chon,
126
+ relationClass: descriptor.relationClass,
127
+ source: 'fallback-table',
128
+ };
129
+ }
130
+
131
+ return {
132
+ path: [...path],
133
+ label: genericFallback(descriptor),
134
+ chon: descriptor.chon,
135
+ relationClass: descriptor.relationClass,
136
+ source: 'generic',
137
+ };
138
+ }