@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/LICENSE +21 -0
- package/README.md +141 -0
- package/package.json +40 -0
- package/src/data/fallback-terms.js +66 -0
- package/src/data/golden-terms.js +1146 -0
- package/src/index.js +5 -0
- package/src/primitives.js +15 -0
- package/src/resolve.js +138 -0
package/src/index.js
ADDED
|
@@ -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
|
+
}
|