@octo-cyber/kinship-calc 0.5.2
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/.turbo/turbo-build.log +22 -0
- package/dist/controllers/family-tree.controller.d.ts +27 -0
- package/dist/controllers/family-tree.controller.d.ts.map +1 -0
- package/dist/controllers/family-tree.controller.js +88 -0
- package/dist/controllers/family-tree.controller.js.map +1 -0
- package/dist/controllers/kinship-calc.controller.d.ts +15 -0
- package/dist/controllers/kinship-calc.controller.d.ts.map +1 -0
- package/dist/controllers/kinship-calc.controller.js +45 -0
- package/dist/controllers/kinship-calc.controller.js.map +1 -0
- package/dist/engine/index.d.ts +7 -0
- package/dist/engine/index.d.ts.map +1 -0
- package/dist/engine/index.js +15 -0
- package/dist/engine/index.js.map +1 -0
- package/dist/engine/kinship-data.d.ts +31 -0
- package/dist/engine/kinship-data.d.ts.map +1 -0
- package/dist/engine/kinship-data.js +150 -0
- package/dist/engine/kinship-data.js.map +1 -0
- package/dist/engine/kinship-engine.d.ts +44 -0
- package/dist/engine/kinship-engine.d.ts.map +1 -0
- package/dist/engine/kinship-engine.js +184 -0
- package/dist/engine/kinship-engine.js.map +1 -0
- package/dist/engine/relation-path.d.ts +37 -0
- package/dist/engine/relation-path.d.ts.map +1 -0
- package/dist/engine/relation-path.js +60 -0
- package/dist/engine/relation-path.js.map +1 -0
- package/dist/entities/family-tree-person.entity.d.ts +12 -0
- package/dist/entities/family-tree-person.entity.d.ts.map +1 -0
- package/dist/entities/family-tree-person.entity.js +61 -0
- package/dist/entities/family-tree-person.entity.js.map +1 -0
- package/dist/entities/family-tree-relation.entity.d.ts +15 -0
- package/dist/entities/family-tree-relation.entity.d.ts.map +1 -0
- package/dist/entities/family-tree-relation.entity.js +58 -0
- package/dist/entities/family-tree-relation.entity.js.map +1 -0
- package/dist/entities/family-tree.entity.d.ts +9 -0
- package/dist/entities/family-tree.entity.d.ts.map +1 -0
- package/dist/entities/family-tree.entity.js +50 -0
- package/dist/entities/family-tree.entity.js.map +1 -0
- package/dist/entities/index.d.ts +12 -0
- package/dist/entities/index.d.ts.map +1 -0
- package/dist/entities/index.js +22 -0
- package/dist/entities/index.js.map +1 -0
- package/dist/entities/kinship-title.entity.d.ts +18 -0
- package/dist/entities/kinship-title.entity.d.ts.map +1 -0
- package/dist/entities/kinship-title.entity.js +59 -0
- package/dist/entities/kinship-title.entity.js.map +1 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +33 -0
- package/dist/index.js.map +1 -0
- package/dist/kinship-calc.module.d.ts +9 -0
- package/dist/kinship-calc.module.d.ts.map +1 -0
- package/dist/kinship-calc.module.js +45 -0
- package/dist/kinship-calc.module.js.map +1 -0
- package/dist/schemas/kinship.schema.d.ts +129 -0
- package/dist/schemas/kinship.schema.d.ts.map +1 -0
- package/dist/schemas/kinship.schema.js +50 -0
- package/dist/schemas/kinship.schema.js.map +1 -0
- package/dist/services/family-tree.service.d.ts +38 -0
- package/dist/services/family-tree.service.d.ts.map +1 -0
- package/dist/services/family-tree.service.js +179 -0
- package/dist/services/family-tree.service.js.map +1 -0
- package/dist/services/kinship-calc.service.d.ts +26 -0
- package/dist/services/kinship-calc.service.d.ts.map +1 -0
- package/dist/services/kinship-calc.service.js +66 -0
- package/dist/services/kinship-calc.service.js.map +1 -0
- package/package.json +60 -0
- package/src/controllers/family-tree.controller.ts +102 -0
- package/src/controllers/kinship-calc.controller.ts +50 -0
- package/src/engine/index.ts +6 -0
- package/src/engine/kinship-data.ts +188 -0
- package/src/engine/kinship-engine.ts +230 -0
- package/src/engine/relation-path.ts +63 -0
- package/src/entities/family-tree-person.entity.ts +37 -0
- package/src/entities/family-tree-relation.entity.ts +36 -0
- package/src/entities/family-tree.entity.ts +28 -0
- package/src/entities/index.ts +18 -0
- package/src/entities/kinship-title.entity.ts +38 -0
- package/src/index.ts +33 -0
- package/src/kinship-calc.module.ts +51 -0
- package/src/schemas/kinship.schema.ts +70 -0
- package/src/services/family-tree.service.ts +211 -0
- package/src/services/kinship-calc.service.ts +68 -0
- package/tsconfig.build.json +4 -0
- package/tsconfig.json +10 -0
- package/web/components/FamilyTreeCanvas.tsx +177 -0
- package/web/components/FamilyTreeDialogs.tsx +275 -0
- package/web/components/KinshipResultCard.tsx +98 -0
- package/web/components/RegionSelector.tsx +32 -0
- package/web/components/RelationChainBuilder.tsx +104 -0
- package/web/index.ts +29 -0
- package/web/manifest.ts +24 -0
- package/web/messages/en-US.json +108 -0
- package/web/messages/zh-CN.json +108 -0
- package/web/pages/FamilyTreePage.tsx +240 -0
- package/web/pages/KinshipCalcPage.tsx +140 -0
- package/web/services/kinship-service.ts +140 -0
- package/web/stores/kinship-store.ts +80 -0
- package/web/types/kinship.ts +85 -0
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { type KinshipResult, type Region } from '../engine/index.js';
|
|
2
|
+
import type { CalculateKinshipDto, PathFromChainDto } from '../schemas/kinship.schema.js';
|
|
3
|
+
export declare class KinshipCalcService {
|
|
4
|
+
private readonly logger;
|
|
5
|
+
initialize(): void;
|
|
6
|
+
calculate(dto: CalculateKinshipDto): KinshipResult;
|
|
7
|
+
calculateFromChain(dto: PathFromChainDto): KinshipResult;
|
|
8
|
+
/** Check if two paths to the same person create a generational conflict */
|
|
9
|
+
checkConflict(paths: string[]): {
|
|
10
|
+
conflict: boolean;
|
|
11
|
+
message: string | null;
|
|
12
|
+
};
|
|
13
|
+
/** Return all known relation paths with their standard titles */
|
|
14
|
+
listAllTitles(region?: Region): Array<{
|
|
15
|
+
path: string;
|
|
16
|
+
title: string;
|
|
17
|
+
generationDelta: number;
|
|
18
|
+
}>;
|
|
19
|
+
/** Return available relation atoms for building chains */
|
|
20
|
+
getRelationAtoms(): Array<{
|
|
21
|
+
code: string;
|
|
22
|
+
label: string;
|
|
23
|
+
generationDelta: number;
|
|
24
|
+
}>;
|
|
25
|
+
}
|
|
26
|
+
//# sourceMappingURL=kinship-calc.service.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"kinship-calc.service.d.ts","sourceRoot":"","sources":["../../src/services/kinship-calc.service.ts"],"names":[],"mappings":"AACA,OAAO,EAKL,KAAK,aAAa,EAClB,KAAK,MAAM,EACZ,MAAM,oBAAoB,CAAA;AAC3B,OAAO,KAAK,EAAE,mBAAmB,EAAE,gBAAgB,EAAE,MAAM,8BAA8B,CAAA;AAEzF,qBACa,kBAAkB;IAC7B,OAAO,CAAC,QAAQ,CAAC,MAAM,CAA+B;IAEtD,UAAU,IAAI,IAAI;IAIlB,SAAS,CAAC,GAAG,EAAE,mBAAmB,GAAG,aAAa;IAOlD,kBAAkB,CAAC,GAAG,EAAE,gBAAgB,GAAG,aAAa;IAQxD,2EAA2E;IAC3E,aAAa,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG;QAAE,QAAQ,EAAE,OAAO,CAAC;QAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAAA;KAAE;IAK7E,iEAAiE;IACjE,aAAa,CAAC,MAAM,GAAE,MAAmB,GAAG,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,eAAe,EAAE,MAAM,CAAA;KAAE,CAAC;IAW3G,0DAA0D;IAC1D,gBAAgB,IAAI,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,eAAe,EAAE,MAAM,CAAA;KAAE,CAAC;CAcpF"}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
3
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
4
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
5
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
6
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
7
|
+
};
|
|
8
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
|
+
exports.KinshipCalcService = void 0;
|
|
10
|
+
const core_1 = require("@octo-cyber/core");
|
|
11
|
+
const index_js_1 = require("../engine/index.js");
|
|
12
|
+
let KinshipCalcService = class KinshipCalcService {
|
|
13
|
+
logger = core_1.Container.get(core_1.LoggerService);
|
|
14
|
+
initialize() {
|
|
15
|
+
this.logger.info('KinshipCalcService initialized');
|
|
16
|
+
}
|
|
17
|
+
calculate(dto) {
|
|
18
|
+
return (0, index_js_1.calculateKinship)(dto.path, {
|
|
19
|
+
region: dto.region,
|
|
20
|
+
egoGender: dto.egoGender,
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
calculateFromChain(dto) {
|
|
24
|
+
const path = (0, index_js_1.encodePath)(dto.chain);
|
|
25
|
+
return (0, index_js_1.calculateKinship)(path, {
|
|
26
|
+
region: dto.region,
|
|
27
|
+
egoGender: dto.egoGender,
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
/** Check if two paths to the same person create a generational conflict */
|
|
31
|
+
checkConflict(paths) {
|
|
32
|
+
const message = (0, index_js_1.detectGenerationConflict)(paths);
|
|
33
|
+
return { conflict: message !== null, message };
|
|
34
|
+
}
|
|
35
|
+
/** Return all known relation paths with their standard titles */
|
|
36
|
+
listAllTitles(region = 'standard') {
|
|
37
|
+
return index_js_1.KINSHIP_TABLE.map((entry) => {
|
|
38
|
+
const result = (0, index_js_1.calculateKinship)(entry.path, { region });
|
|
39
|
+
return {
|
|
40
|
+
path: entry.path,
|
|
41
|
+
title: result.title,
|
|
42
|
+
generationDelta: result.generationDelta,
|
|
43
|
+
};
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
/** Return available relation atoms for building chains */
|
|
47
|
+
getRelationAtoms() {
|
|
48
|
+
return [
|
|
49
|
+
{ code: 'F', label: '父亲', generationDelta: 1 },
|
|
50
|
+
{ code: 'M', label: '母亲', generationDelta: 1 },
|
|
51
|
+
{ code: 'S', label: '儿子', generationDelta: -1 },
|
|
52
|
+
{ code: 'D', label: '女儿', generationDelta: -1 },
|
|
53
|
+
{ code: 'B+', label: '哥哥', generationDelta: 0 },
|
|
54
|
+
{ code: 'B-', label: '弟弟', generationDelta: 0 },
|
|
55
|
+
{ code: 'Z+', label: '姐姐', generationDelta: 0 },
|
|
56
|
+
{ code: 'Z-', label: '妹妹', generationDelta: 0 },
|
|
57
|
+
{ code: 'H', label: '丈夫', generationDelta: 0 },
|
|
58
|
+
{ code: 'W', label: '妻子', generationDelta: 0 },
|
|
59
|
+
];
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
exports.KinshipCalcService = KinshipCalcService;
|
|
63
|
+
exports.KinshipCalcService = KinshipCalcService = __decorate([
|
|
64
|
+
(0, core_1.Service)()
|
|
65
|
+
], KinshipCalcService);
|
|
66
|
+
//# sourceMappingURL=kinship-calc.service.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"kinship-calc.service.js","sourceRoot":"","sources":["../../src/services/kinship-calc.service.ts"],"names":[],"mappings":";;;;;;;;;AAAA,2CAAoE;AACpE,iDAO2B;AAIpB,IAAM,kBAAkB,GAAxB,MAAM,kBAAkB;IACZ,MAAM,GAAG,gBAAS,CAAC,GAAG,CAAC,oBAAa,CAAC,CAAA;IAEtD,UAAU;QACR,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,gCAAgC,CAAC,CAAA;IACpD,CAAC;IAED,SAAS,CAAC,GAAwB;QAChC,OAAO,IAAA,2BAAgB,EAAC,GAAG,CAAC,IAAI,EAAE;YAChC,MAAM,EAAE,GAAG,CAAC,MAAgB;YAC5B,SAAS,EAAE,GAAG,CAAC,SAAS;SACzB,CAAC,CAAA;IACJ,CAAC;IAED,kBAAkB,CAAC,GAAqB;QACtC,MAAM,IAAI,GAAG,IAAA,qBAAU,EAAC,GAAG,CAAC,KAAK,CAAC,CAAA;QAClC,OAAO,IAAA,2BAAgB,EAAC,IAAI,EAAE;YAC5B,MAAM,EAAE,GAAG,CAAC,MAAgB;YAC5B,SAAS,EAAE,GAAG,CAAC,SAAS;SACzB,CAAC,CAAA;IACJ,CAAC;IAED,2EAA2E;IAC3E,aAAa,CAAC,KAAe;QAC3B,MAAM,OAAO,GAAG,IAAA,mCAAwB,EAAC,KAAK,CAAC,CAAA;QAC/C,OAAO,EAAE,QAAQ,EAAE,OAAO,KAAK,IAAI,EAAE,OAAO,EAAE,CAAA;IAChD,CAAC;IAED,iEAAiE;IACjE,aAAa,CAAC,SAAiB,UAAU;QACvC,OAAO,wBAAa,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE;YACjC,MAAM,MAAM,GAAG,IAAA,2BAAgB,EAAC,KAAK,CAAC,IAAI,EAAE,EAAE,MAAM,EAAE,CAAC,CAAA;YACvD,OAAO;gBACL,IAAI,EAAE,KAAK,CAAC,IAAI;gBAChB,KAAK,EAAE,MAAM,CAAC,KAAK;gBACnB,eAAe,EAAE,MAAM,CAAC,eAAe;aACxC,CAAA;QACH,CAAC,CAAC,CAAA;IACJ,CAAC;IAED,0DAA0D;IAC1D,gBAAgB;QACd,OAAO;YACL,EAAE,IAAI,EAAE,GAAG,EAAG,KAAK,EAAE,IAAI,EAAG,eAAe,EAAE,CAAC,EAAG;YACjD,EAAE,IAAI,EAAE,GAAG,EAAG,KAAK,EAAE,IAAI,EAAG,eAAe,EAAE,CAAC,EAAG;YACjD,EAAE,IAAI,EAAE,GAAG,EAAG,KAAK,EAAE,IAAI,EAAG,eAAe,EAAE,CAAC,CAAC,EAAE;YACjD,EAAE,IAAI,EAAE,GAAG,EAAG,KAAK,EAAE,IAAI,EAAG,eAAe,EAAE,CAAC,CAAC,EAAE;YACjD,EAAE,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAG,eAAe,EAAE,CAAC,EAAG;YACjD,EAAE,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAG,eAAe,EAAE,CAAC,EAAG;YACjD,EAAE,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAG,eAAe,EAAE,CAAC,EAAG;YACjD,EAAE,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAG,eAAe,EAAE,CAAC,EAAG;YACjD,EAAE,IAAI,EAAE,GAAG,EAAG,KAAK,EAAE,IAAI,EAAG,eAAe,EAAE,CAAC,EAAG;YACjD,EAAE,IAAI,EAAE,GAAG,EAAG,KAAK,EAAE,IAAI,EAAG,eAAe,EAAE,CAAC,EAAG;SAClD,CAAA;IACH,CAAC;CACF,CAAA;AAvDY,gDAAkB;6BAAlB,kBAAkB;IAD9B,IAAA,cAAO,GAAE;GACG,kBAAkB,CAuD9B"}
|
package/package.json
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@octo-cyber/kinship-calc",
|
|
3
|
+
"version": "0.5.2",
|
|
4
|
+
"description": "辈分计算器 — 输入关系链,自动计算亲属称呼,支持南北方差异与方言",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"types": "dist/index.d.ts",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": {
|
|
9
|
+
"types": "./dist/index.d.ts",
|
|
10
|
+
"import": "./dist/index.js",
|
|
11
|
+
"default": "./dist/index.js"
|
|
12
|
+
},
|
|
13
|
+
"./web": {
|
|
14
|
+
"types": "./web/index.ts",
|
|
15
|
+
"import": "./web/index.ts",
|
|
16
|
+
"default": "./web/index.ts"
|
|
17
|
+
},
|
|
18
|
+
"./web/pages/*": {
|
|
19
|
+
"types": "./web/pages/*.tsx",
|
|
20
|
+
"import": "./web/pages/*.tsx",
|
|
21
|
+
"default": "./web/pages/*.tsx"
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
"dependencies": {
|
|
25
|
+
"@octo-cyber/core": "^0.5.4",
|
|
26
|
+
"@octo-cyber/auth": "^0.5.6",
|
|
27
|
+
"@octo-cyber/ui": "^0.5.3"
|
|
28
|
+
},
|
|
29
|
+
"peerDependencies": {
|
|
30
|
+
"next": ">=15",
|
|
31
|
+
"next-intl": ">=3",
|
|
32
|
+
"react": ">=19",
|
|
33
|
+
"react-dom": ">=19",
|
|
34
|
+
"zod": ">=3"
|
|
35
|
+
},
|
|
36
|
+
"devDependencies": {
|
|
37
|
+
"@types/node": "^22.0.0",
|
|
38
|
+
"@types/react": "^19",
|
|
39
|
+
"typescript": "^5.8.0"
|
|
40
|
+
},
|
|
41
|
+
"repository": {
|
|
42
|
+
"type": "git",
|
|
43
|
+
"url": "https://github.com/jefflower/octo-kinship-calc.git"
|
|
44
|
+
},
|
|
45
|
+
"publishMeta": {
|
|
46
|
+
"repository": "jefflower/octo-kinship-calc",
|
|
47
|
+
"branch": "main",
|
|
48
|
+
"commitHash": "7490bd3995fad90547d8d8d7febf4143142e5b35",
|
|
49
|
+
"publishedAt": "2026-03-27T15:19:48.392Z",
|
|
50
|
+
"publishedFrom": "monorepo:packages/kinship-calc"
|
|
51
|
+
},
|
|
52
|
+
"publishConfig": {
|
|
53
|
+
"registry": "https://registry.npmjs.org/"
|
|
54
|
+
},
|
|
55
|
+
"scripts": {
|
|
56
|
+
"build": "tsc -p tsconfig.build.json",
|
|
57
|
+
"dev": "tsc -p tsconfig.build.json --watch",
|
|
58
|
+
"typecheck": "tsc --noEmit"
|
|
59
|
+
}
|
|
60
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import type { Request, Response, RequestHandler } from '@octo-cyber/core'
|
|
2
|
+
import { Container, ApiResponse, asyncHandler, AppError } from '@octo-cyber/core'
|
|
3
|
+
import { FamilyTreeService } from '../services/family-tree.service.js'
|
|
4
|
+
import {
|
|
5
|
+
CreateFamilyTreeSchema,
|
|
6
|
+
UpdateFamilyTreeSchema,
|
|
7
|
+
CreatePersonSchema,
|
|
8
|
+
UpdatePersonSchema,
|
|
9
|
+
CreateRelationSchema,
|
|
10
|
+
ResolveTreePathSchema,
|
|
11
|
+
} from '../schemas/kinship.schema.js'
|
|
12
|
+
|
|
13
|
+
type AuthRequest = Request & { user?: { userId: number } }
|
|
14
|
+
|
|
15
|
+
function ownerId(req: Request): number {
|
|
16
|
+
const id = (req as AuthRequest).user?.userId
|
|
17
|
+
if (!id) throw new AppError('未认证', 401)
|
|
18
|
+
return id
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export class FamilyTreeController {
|
|
22
|
+
private readonly service = Container.get(FamilyTreeService)
|
|
23
|
+
|
|
24
|
+
/** GET /api/v1/kinship/trees */
|
|
25
|
+
listTrees: RequestHandler = asyncHandler(async (req: Request, res: Response) => {
|
|
26
|
+
const trees = await this.service.listTrees(ownerId(req))
|
|
27
|
+
res.json(ApiResponse.ok(trees))
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
/** GET /api/v1/kinship/trees/:id */
|
|
31
|
+
getTree: RequestHandler = asyncHandler(async (req: Request, res: Response) => {
|
|
32
|
+
const graph = await this.service.getTree(Number(req.params.id), ownerId(req))
|
|
33
|
+
res.json(ApiResponse.ok(graph))
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
/** POST /api/v1/kinship/trees */
|
|
37
|
+
createTree: RequestHandler = asyncHandler(async (req: Request, res: Response) => {
|
|
38
|
+
const dto = CreateFamilyTreeSchema.parse(req.body)
|
|
39
|
+
const tree = await this.service.createTree(dto, ownerId(req))
|
|
40
|
+
res.status(201).json(ApiResponse.ok(tree))
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
/** PUT /api/v1/kinship/trees/:id */
|
|
44
|
+
updateTree: RequestHandler = asyncHandler(async (req: Request, res: Response) => {
|
|
45
|
+
const dto = UpdateFamilyTreeSchema.parse(req.body)
|
|
46
|
+
const tree = await this.service.updateTree(Number(req.params.id), dto, ownerId(req))
|
|
47
|
+
res.json(ApiResponse.ok(tree))
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
/** DELETE /api/v1/kinship/trees/:id */
|
|
51
|
+
deleteTree: RequestHandler = asyncHandler(async (req: Request, res: Response) => {
|
|
52
|
+
await this.service.deleteTree(Number(req.params.id), ownerId(req))
|
|
53
|
+
res.json(ApiResponse.ok(null))
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
/** POST /api/v1/kinship/persons */
|
|
57
|
+
addPerson: RequestHandler = asyncHandler(async (req: Request, res: Response) => {
|
|
58
|
+
const dto = CreatePersonSchema.parse(req.body)
|
|
59
|
+
const person = await this.service.addPerson(dto, ownerId(req))
|
|
60
|
+
res.status(201).json(ApiResponse.ok(person))
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
/** PUT /api/v1/kinship/persons/:id?treeId= */
|
|
64
|
+
updatePerson: RequestHandler = asyncHandler(async (req: Request, res: Response) => {
|
|
65
|
+
const personId = Number(req.params.id)
|
|
66
|
+
const treeId = Number(req.query.treeId)
|
|
67
|
+
if (!treeId) throw new AppError('缺少 treeId 参数', 400)
|
|
68
|
+
const dto = UpdatePersonSchema.parse(req.body)
|
|
69
|
+
const person = await this.service.updatePerson(personId, treeId, dto, ownerId(req))
|
|
70
|
+
res.json(ApiResponse.ok(person))
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
/** DELETE /api/v1/kinship/persons/:id?treeId= */
|
|
74
|
+
deletePerson: RequestHandler = asyncHandler(async (req: Request, res: Response) => {
|
|
75
|
+
const treeId = Number(req.query.treeId)
|
|
76
|
+
if (!treeId) throw new AppError('缺少 treeId 参数', 400)
|
|
77
|
+
await this.service.deletePerson(Number(req.params.id), treeId, ownerId(req))
|
|
78
|
+
res.json(ApiResponse.ok(null))
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
/** POST /api/v1/kinship/relations */
|
|
82
|
+
addRelation: RequestHandler = asyncHandler(async (req: Request, res: Response) => {
|
|
83
|
+
const dto = CreateRelationSchema.parse(req.body)
|
|
84
|
+
const relation = await this.service.addRelation(dto, ownerId(req))
|
|
85
|
+
res.status(201).json(ApiResponse.ok(relation))
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
/** DELETE /api/v1/kinship/relations/:id?treeId= */
|
|
89
|
+
deleteRelation: RequestHandler = asyncHandler(async (req: Request, res: Response) => {
|
|
90
|
+
const treeId = Number(req.query.treeId)
|
|
91
|
+
if (!treeId) throw new AppError('缺少 treeId 参数', 400)
|
|
92
|
+
await this.service.deleteRelation(Number(req.params.id), treeId, ownerId(req))
|
|
93
|
+
res.json(ApiResponse.ok(null))
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
/** POST /api/v1/kinship/resolve */
|
|
97
|
+
resolveKinship: RequestHandler = asyncHandler(async (req: Request, res: Response) => {
|
|
98
|
+
const dto = ResolveTreePathSchema.parse(req.body)
|
|
99
|
+
const result = await this.service.resolveKinship(dto, ownerId(req))
|
|
100
|
+
res.json(ApiResponse.ok(result))
|
|
101
|
+
})
|
|
102
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import type { Request, Response, RequestHandler } from '@octo-cyber/core'
|
|
2
|
+
import { Container, ApiResponse, asyncHandler, AppError } from '@octo-cyber/core'
|
|
3
|
+
import { KinshipCalcService } from '../services/kinship-calc.service.js'
|
|
4
|
+
import {
|
|
5
|
+
CalculateKinshipSchema,
|
|
6
|
+
PathFromChainSchema,
|
|
7
|
+
} from '../schemas/kinship.schema.js'
|
|
8
|
+
import type { Region } from '../engine/index.js'
|
|
9
|
+
|
|
10
|
+
const VALID_REGIONS: Region[] = ['standard', 'northern', 'southern', 'cantonese', 'minnan', 'wu']
|
|
11
|
+
|
|
12
|
+
export class KinshipCalcController {
|
|
13
|
+
private readonly service = Container.get(KinshipCalcService)
|
|
14
|
+
|
|
15
|
+
/** POST /api/kinship/calculate */
|
|
16
|
+
calculate: RequestHandler = asyncHandler(async (req: Request, res: Response) => {
|
|
17
|
+
const dto = CalculateKinshipSchema.parse(req.body)
|
|
18
|
+
const result = this.service.calculate(dto)
|
|
19
|
+
res.json(ApiResponse.ok(result))
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
/** POST /api/kinship/chain */
|
|
23
|
+
calculateFromChain: RequestHandler = asyncHandler(async (req: Request, res: Response) => {
|
|
24
|
+
const dto = PathFromChainSchema.parse(req.body)
|
|
25
|
+
const result = this.service.calculateFromChain(dto)
|
|
26
|
+
res.json(ApiResponse.ok(result))
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
/** POST /api/kinship/conflict-check */
|
|
30
|
+
checkConflict: RequestHandler = asyncHandler(async (req: Request, res: Response) => {
|
|
31
|
+
const { paths } = req.body as { paths: string[] }
|
|
32
|
+
if (!Array.isArray(paths)) throw AppError.badRequest('paths 必须是数组')
|
|
33
|
+
const result = this.service.checkConflict(paths)
|
|
34
|
+
res.json(ApiResponse.ok(result))
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
/** GET /api/kinship/titles?region=standard */
|
|
38
|
+
listTitles: RequestHandler = asyncHandler(async (req: Request, res: Response) => {
|
|
39
|
+
const region = ((req.query.region as string) || 'standard') as Region
|
|
40
|
+
if (!VALID_REGIONS.includes(region)) throw AppError.badRequest(`不支持的地区:${region}`)
|
|
41
|
+
const list = this.service.listAllTitles(region)
|
|
42
|
+
res.json(ApiResponse.ok(list))
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
/** GET /api/kinship/atoms */
|
|
46
|
+
getAtoms: RequestHandler = asyncHandler(async (_req: Request, res: Response) => {
|
|
47
|
+
const atoms = this.service.getRelationAtoms()
|
|
48
|
+
res.json(ApiResponse.ok(atoms))
|
|
49
|
+
})
|
|
50
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export { calculateKinship, detectGenerationConflict } from './kinship-engine.js'
|
|
2
|
+
export type { KinshipResult, CalcOptions, Region } from './kinship-engine.js'
|
|
3
|
+
export { KINSHIP_TABLE, KINSHIP_LOOKUP } from './kinship-data.js'
|
|
4
|
+
export type { KinshipEntry } from './kinship-data.js'
|
|
5
|
+
export { encodePath, decodePath, pathGenerationDelta, atomGender } from './relation-path.js'
|
|
6
|
+
export type { RelationAtom, EgoGender } from './relation-path.js'
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Comprehensive Chinese kinship term lookup table.
|
|
3
|
+
*
|
|
4
|
+
* Each entry: { path, standard, northern?, southern?, cantonese?, minnan?, wu?, reverse?, note? }
|
|
5
|
+
*
|
|
6
|
+
* path: encoded relation path (dot-separated atoms)
|
|
7
|
+
* standard: standard Mandarin title (普通话)
|
|
8
|
+
* northern: northern dialect variant (北方)
|
|
9
|
+
* southern: southern Mandarin variant (南方官话)
|
|
10
|
+
* cantonese: Cantonese variant (粤语)
|
|
11
|
+
* minnan: Min-nan / Hokkien variant (闽南语)
|
|
12
|
+
* wu: Wu dialect variant (吴语/上海话)
|
|
13
|
+
* reverse: how the relative addresses ego (男/女 separated by /)
|
|
14
|
+
* note: explanatory note
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
export interface KinshipEntry {
|
|
18
|
+
path: string
|
|
19
|
+
standard: string
|
|
20
|
+
northern?: string
|
|
21
|
+
southern?: string
|
|
22
|
+
cantonese?: string
|
|
23
|
+
minnan?: string
|
|
24
|
+
wu?: string
|
|
25
|
+
reverse?: string // e.g. "孙子/孙女" (male ego / female ego)
|
|
26
|
+
note?: string
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export const KINSHIP_TABLE: KinshipEntry[] = [
|
|
30
|
+
// ── 直系 ─────────────────────────────────────────────────────────
|
|
31
|
+
|
|
32
|
+
// Parents
|
|
33
|
+
{ path: 'F', standard: '父亲', northern: '爸', cantonese: '爸爸', reverse: '孩子', note: '父' },
|
|
34
|
+
{ path: 'M', standard: '母亲', northern: '妈', cantonese: '妈妈', reverse: '孩子', note: '母' },
|
|
35
|
+
|
|
36
|
+
// Grandparents (paternal)
|
|
37
|
+
{ path: 'F.F', standard: '祖父', northern: '爷爷', southern: '公公', cantonese: '爺爺', minnan: '阿公', wu: '阿爷', reverse: '孙子/孙女', note: '父父' },
|
|
38
|
+
{ path: 'F.M', standard: '祖母', northern: '奶奶', southern: '婆婆', cantonese: '嫲嫲', minnan: '阿嬷', wu: '阿嬶', reverse: '孙子/孙女', note: '父母' },
|
|
39
|
+
|
|
40
|
+
// Grandparents (maternal)
|
|
41
|
+
{ path: 'M.F', standard: '外祖父', northern: '姥爷', southern: '外公', cantonese: '外公', minnan: '阿公', wu: '外公', reverse: '外孙/外孙女', note: '母父' },
|
|
42
|
+
{ path: 'M.M', standard: '外祖母', northern: '姥姥', southern: '外婆', cantonese: '外婆', minnan: '阿妈', wu: '外婆', reverse: '外孙/外孙女', note: '母母' },
|
|
43
|
+
|
|
44
|
+
// Great-grandparents (paternal)
|
|
45
|
+
{ path: 'F.F.F', standard: '曾祖父', northern: '太爷爷', reverse: '曾孙/曾孙女' },
|
|
46
|
+
{ path: 'F.F.M', standard: '曾祖母', northern: '太奶奶', reverse: '曾孙/曾孙女' },
|
|
47
|
+
{ path: 'M.M.F', standard: '外曾祖父', northern: '太姥爷', reverse: '曾外孙/曾外孙女' },
|
|
48
|
+
{ path: 'M.M.M', standard: '外曾祖母', northern: '太姥姥', reverse: '曾外孙/曾外孙女' },
|
|
49
|
+
|
|
50
|
+
// Great-great-grandparents
|
|
51
|
+
{ path: 'F.F.F.F', standard: '高祖父', reverse: '玄孙/玄孙女' },
|
|
52
|
+
{ path: 'F.F.F.M', standard: '高祖母', reverse: '玄孙/玄孙女' },
|
|
53
|
+
|
|
54
|
+
// Children
|
|
55
|
+
{ path: 'S', standard: '儿子', northern: '儿子', cantonese: '仔', minnan: '囝', reverse: '父亲/母亲' },
|
|
56
|
+
{ path: 'D', standard: '女儿', northern: '闺女', cantonese: '囡', reverse: '父亲/母亲' },
|
|
57
|
+
|
|
58
|
+
// Grandchildren (paternal line)
|
|
59
|
+
{ path: 'S.S', standard: '孙子', cantonese: '孙仔', reverse: '祖父/祖母' },
|
|
60
|
+
{ path: 'S.D', standard: '孙女', cantonese: '孙囡', reverse: '祖父/祖母' },
|
|
61
|
+
{ path: 'D.S', standard: '外孙', reverse: '外祖父/外祖母' },
|
|
62
|
+
{ path: 'D.D', standard: '外孙女', reverse: '外祖父/外祖母' },
|
|
63
|
+
|
|
64
|
+
// Great-grandchildren
|
|
65
|
+
{ path: 'S.S.S', standard: '曾孙', reverse: '曾祖父/曾祖母' },
|
|
66
|
+
{ path: 'S.S.D', standard: '曾孙女', reverse: '曾祖父/曾祖母' },
|
|
67
|
+
|
|
68
|
+
// Spouse
|
|
69
|
+
{ path: 'H', standard: '丈夫', northern: '老公', cantonese: '老公', reverse: '妻子/老婆' },
|
|
70
|
+
{ path: 'W', standard: '妻子', northern: '老婆', cantonese: '老婆', reverse: '丈夫/老公' },
|
|
71
|
+
|
|
72
|
+
// ── 旁系 (collateral) ────────────────────────────────────────────
|
|
73
|
+
|
|
74
|
+
// Father's brothers
|
|
75
|
+
{ path: 'F.B+', standard: '伯父', northern: '大爷', cantonese: '伯父', minnan: '阿伯', wu: '伯伯', reverse: '侄子/侄女', note: '父之兄' },
|
|
76
|
+
{ path: 'F.B-', standard: '叔父', northern: '叔叔', cantonese: '叔父', minnan: '阿叔', wu: '叔叔', reverse: '侄子/侄女', note: '父之弟' },
|
|
77
|
+
|
|
78
|
+
// Father's sisters
|
|
79
|
+
{ path: 'F.Z+', standard: '姑母', northern: '大姑', cantonese: '姑母', minnan: '阿姑', wu: '姑妈', reverse: '侄子/侄女', note: '父之姐' },
|
|
80
|
+
{ path: 'F.Z-', standard: '姑母', northern: '姑姑', cantonese: '姑母', minnan: '阿姑', wu: '姑姑', reverse: '侄子/侄女', note: '父之妹' },
|
|
81
|
+
|
|
82
|
+
// Mother's brothers
|
|
83
|
+
{ path: 'M.B+', standard: '舅父', northern: '大舅', cantonese: '舅父', minnan: '阿舅', wu: '舅舅', reverse: '外甥/外甥女', note: '母之兄' },
|
|
84
|
+
{ path: 'M.B-', standard: '舅父', northern: '舅舅', cantonese: '舅父', minnan: '阿舅', wu: '舅舅', reverse: '外甥/外甥女', note: '母之弟' },
|
|
85
|
+
|
|
86
|
+
// Mother's sisters
|
|
87
|
+
{ path: 'M.Z+', standard: '姨母', northern: '大姨', cantonese: '姨母', minnan: '阿姨', wu: '阿姨', reverse: '外甥/外甥女', note: '母之姐' },
|
|
88
|
+
{ path: 'M.Z-', standard: '姨母', northern: '阿姨', cantonese: '姨母', minnan: '阿姨', wu: '阿姨', reverse: '外甥/外甥女', note: '母之妹' },
|
|
89
|
+
|
|
90
|
+
// Siblings
|
|
91
|
+
{ path: 'B+', standard: '兄长', northern: '哥哥', cantonese: '大佬', minnan: '阿兄', wu: '阿哥', reverse: '弟弟/妹妹' },
|
|
92
|
+
{ path: 'B-', standard: '弟弟', northern: '弟弟', cantonese: '细佬', minnan: '小弟', wu: '阿弟', reverse: '哥哥/姐姐' },
|
|
93
|
+
{ path: 'Z+', standard: '姐姐', northern: '姐姐', cantonese: '家姐', minnan: '阿姐', wu: '阿姐', reverse: '弟弟/妹妹' },
|
|
94
|
+
{ path: 'Z-', standard: '妹妹', northern: '妹妹', cantonese: '妹妹', minnan: '小妹', wu: '阿妹', reverse: '哥哥/姐姐' },
|
|
95
|
+
|
|
96
|
+
// Paternal cousins (堂)
|
|
97
|
+
{ path: 'F.B+.S', standard: '堂兄', northern: '堂哥', note: '伯父之子(年长)' },
|
|
98
|
+
{ path: 'F.B+.D', standard: '堂姐', northern: '堂姐', note: '伯父之女(年长)' },
|
|
99
|
+
{ path: 'F.B-.S', standard: '堂弟', northern: '堂弟', note: '叔叔之子(年幼)' },
|
|
100
|
+
{ path: 'F.B-.D', standard: '堂妹', northern: '堂妹', note: '叔叔之女(年幼)' },
|
|
101
|
+
{ path: 'F.Z+.S', standard: '表兄', northern: '表哥', note: '姑母之子(年长)' },
|
|
102
|
+
{ path: 'F.Z+.D', standard: '表姐', northern: '表姐', note: '姑母之女(年长)' },
|
|
103
|
+
{ path: 'F.Z-.S', standard: '表弟', northern: '表弟', note: '姑母之子(年幼)' },
|
|
104
|
+
{ path: 'F.Z-.D', standard: '表妹', northern: '表妹', note: '姑母之女(年幼)' },
|
|
105
|
+
|
|
106
|
+
// Maternal cousins (表)
|
|
107
|
+
{ path: 'M.B+.S', standard: '表兄', northern: '表哥', note: '舅父之子(年长)' },
|
|
108
|
+
{ path: 'M.B+.D', standard: '表姐', northern: '表姐', note: '舅父之女(年长)' },
|
|
109
|
+
{ path: 'M.B-.S', standard: '表弟', northern: '表弟', note: '舅父之子(年幼)' },
|
|
110
|
+
{ path: 'M.B-.D', standard: '表妹', northern: '表妹', note: '舅父之女(年幼)' },
|
|
111
|
+
{ path: 'M.Z+.S', standard: '表兄', northern: '表哥', note: '姨母之子(年长)' },
|
|
112
|
+
{ path: 'M.Z+.D', standard: '表姐', northern: '表姐', note: '姨母之女(年长)' },
|
|
113
|
+
{ path: 'M.Z-.S', standard: '表弟', northern: '表弟', note: '姨母之子(年幼)' },
|
|
114
|
+
{ path: 'M.Z-.D', standard: '表妹', northern: '表妹', note: '姨母之女(年幼)' },
|
|
115
|
+
|
|
116
|
+
// Nephews/nieces (siblings' children)
|
|
117
|
+
{ path: 'B+.S', standard: '侄子', reverse: '伯父/伯母' },
|
|
118
|
+
{ path: 'B+.D', standard: '侄女', reverse: '伯父/伯母' },
|
|
119
|
+
{ path: 'B-.S', standard: '侄子', reverse: '叔叔/婶婶' },
|
|
120
|
+
{ path: 'B-.D', standard: '侄女', reverse: '叔叔/婶婶' },
|
|
121
|
+
{ path: 'Z+.S', standard: '外甥', northern: '外甥', note: '姐之子', reverse: '舅父/舅母' },
|
|
122
|
+
{ path: 'Z+.D', standard: '外甥女', northern: '外甥女', note: '姐之女', reverse: '舅父/舅母' },
|
|
123
|
+
{ path: 'Z-.S', standard: '外甥', northern: '外甥', note: '妹之子', reverse: '舅父/舅母' },
|
|
124
|
+
{ path: 'Z-.D', standard: '外甥女', northern: '外甥女', note: '妹之女', reverse: '舅父/舅母' },
|
|
125
|
+
|
|
126
|
+
// ── 姻亲 (in-laws) ───────────────────────────────────────────────
|
|
127
|
+
|
|
128
|
+
// Spouse's parents
|
|
129
|
+
{ path: 'W.F', standard: '岳父', northern: '老丈人', cantonese: '外父', wu: '丈人', reverse: '女婿' },
|
|
130
|
+
{ path: 'W.M', standard: '岳母', northern: '丈母娘', cantonese: '外母', wu: '丈母', reverse: '女婿' },
|
|
131
|
+
{ path: 'H.F', standard: '公公', cantonese: '家翁', wu: '阿公', reverse: '儿媳' },
|
|
132
|
+
{ path: 'H.M', standard: '婆婆', cantonese: '家姑', wu: '阿婆', reverse: '儿媳' },
|
|
133
|
+
|
|
134
|
+
// Children's spouses
|
|
135
|
+
{ path: 'S.W', standard: '儿媳妇', northern: '儿媳', reverse: '公公/婆婆' },
|
|
136
|
+
{ path: 'D.H', standard: '女婿', reverse: '岳父/岳母' },
|
|
137
|
+
|
|
138
|
+
// Siblings' spouses
|
|
139
|
+
{ path: 'B+.W', standard: '嫂子', northern: '嫂嫂', cantonese: '大嫂', reverse: '小叔/小姑' },
|
|
140
|
+
{ path: 'B-.W', standard: '弟媳', northern: '弟妹', cantonese: '弟妇', reverse: '大伯/大姑' },
|
|
141
|
+
{ path: 'Z+.H', standard: '姐夫', cantonese: '姐夫', reverse: '小舅/小姨' },
|
|
142
|
+
{ path: 'Z-.H', standard: '妹夫', cantonese: '妹夫', reverse: '大舅/大姨' },
|
|
143
|
+
|
|
144
|
+
// Spouse's siblings
|
|
145
|
+
{ path: 'H.B+', standard: '大伯子', northern: '大伯', note: '夫之兄' },
|
|
146
|
+
{ path: 'H.B-', standard: '小叔子', northern: '小叔', note: '夫之弟' },
|
|
147
|
+
{ path: 'H.Z+', standard: '大姑子', northern: '大姑', note: '夫之姐' },
|
|
148
|
+
{ path: 'H.Z-', standard: '小姑子', northern: '小姑', note: '夫之妹' },
|
|
149
|
+
{ path: 'W.B+', standard: '大舅子', northern: '大舅哥', note: '妻之兄' },
|
|
150
|
+
{ path: 'W.B-', standard: '小舅子', northern: '小舅子', note: '妻之弟' },
|
|
151
|
+
{ path: 'W.Z+', standard: '大姨子', northern: '大姨姐', note: '妻之姐' },
|
|
152
|
+
{ path: 'W.Z-', standard: '小姨子', northern: '小姨妹', note: '妻之妹' },
|
|
153
|
+
|
|
154
|
+
// Father's brothers' wives
|
|
155
|
+
{ path: 'F.B+.W', standard: '伯母', northern: '大娘', cantonese: '伯娘', minnan: '阿婶', wu: '伯娘', reverse: '侄子/侄女' },
|
|
156
|
+
{ path: 'F.B-.W', standard: '婶母', northern: '婶婶', cantonese: '婶母', minnan: '阿婶', wu: '阿婶', reverse: '侄子/侄女' },
|
|
157
|
+
|
|
158
|
+
// Father's sisters' husbands
|
|
159
|
+
{ path: 'F.Z+.H', standard: '姑父', northern: '姑父', reverse: '外甥/外甥女' },
|
|
160
|
+
{ path: 'F.Z-.H', standard: '姑父', northern: '姑父', reverse: '外甥/外甥女' },
|
|
161
|
+
|
|
162
|
+
// Mother's brothers' wives
|
|
163
|
+
{ path: 'M.B+.W', standard: '舅母', northern: '大妗', note: '舅父之妻' },
|
|
164
|
+
{ path: 'M.B-.W', standard: '舅母', northern: '舅妈', note: '舅父之妻' },
|
|
165
|
+
|
|
166
|
+
// Mother's sisters' husbands
|
|
167
|
+
{ path: 'M.Z+.H', standard: '姨父', northern: '姨夫', reverse: '外甥/外甥女' },
|
|
168
|
+
{ path: 'M.Z-.H', standard: '姨父', northern: '姨夫', reverse: '外甥/外甥女' },
|
|
169
|
+
|
|
170
|
+
// Grandparents' siblings (祖父/祖母的兄弟姐妹)
|
|
171
|
+
{ path: 'F.F.B+', standard: '伯祖父', northern: '老爷爷' },
|
|
172
|
+
{ path: 'F.F.B-', standard: '叔祖父', northern: '老爷爷' },
|
|
173
|
+
{ path: 'F.F.Z+', standard: '姑祖母', northern: '姑奶奶' },
|
|
174
|
+
{ path: 'F.F.Z-', standard: '姑祖母', northern: '姑奶奶' },
|
|
175
|
+
{ path: 'F.M.B+', standard: '舅祖父', northern: '舅太爷' },
|
|
176
|
+
{ path: 'F.M.Z+', standard: '姨祖母', northern: '姨奶奶' },
|
|
177
|
+
]
|
|
178
|
+
|
|
179
|
+
/** Build a lookup map for O(1) access */
|
|
180
|
+
export function buildLookupMap(table: KinshipEntry[]): Map<string, KinshipEntry> {
|
|
181
|
+
const map = new Map<string, KinshipEntry>()
|
|
182
|
+
for (const entry of table) {
|
|
183
|
+
map.set(entry.path, entry)
|
|
184
|
+
}
|
|
185
|
+
return map
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
export const KINSHIP_LOOKUP = buildLookupMap(KINSHIP_TABLE)
|