Files
chinese-family-tree-2/lib/relationship-calculator.ts
T
freedakgmail d797f1abb9 0.0.4.0
2025-11-23 14:46:53 +08:00

471 lines
12 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import relationship from 'relationship.js'
import type { FamilyMember } from '@/types/family'
/**
* 家族关系计算器
* 使用 relationship.js 库计算中文亲属关系
*/
export class RelationshipCalculator {
private members: Map<string, FamilyMember>
constructor(members: FamilyMember[]) {
this.members = new Map(members.map(m => [m.id, m]))
}
/**
* 计算两个成员之间的关系
* @param fromId 起始成员ID(成员A
* @param toId 目标成员ID(成员B
* @returns 关系描述(toId 对 fromId 的称谓,即B是A的XXX
*/
getRelationship(fromId: string, toId: string): RelationshipResult {
if (fromId === toId) {
return {
term: '本人',
path: '',
success: true
}
}
const fromMember = this.members.get(fromId)
const toMember = this.members.get(toId)
if (!fromMember || !toMember) {
return {
term: '未知',
path: '',
success: false,
error: '成员不存在'
}
}
// 构建关系路径(从 fromId 到 toId
const path = this.buildRelationPath(fromId, toId)
if (!path) {
return {
term: '无血缘关系',
path: '',
success: false
}
}
// 将路径转换为中文
const chinesePath = this.pathToChineseText(path)
// 使用 relationship.js 计算关系
try {
// 获取fromId成员的性别(从谁的角度看关系)
const fromMemberGender = fromMember.gender === 'FEMALE' ? 0 : 1
// 计算关系(toId 对 fromId 的称谓)
const result = relationship({
text: chinesePath,
target: '',
sex: fromMemberGender, // 传入fromId的性别(从谁的角度看)
reverse: true // 使用反向,因为路径是从A到B,但我们要的是B对A的称谓
})
const term = Array.isArray(result) && result.length > 0 ? result[0] : '亲戚'
return {
term,
path: chinesePath,
success: true
}
} catch (error) {
return {
term: '亲戚',
path: chinesePath,
success: true
}
}
}
/**
* 构建关系路径字符串
* 例如:'f' = 父亲, 'm' = 母亲, 'h' = 丈夫, 'w' = 妻子
* 'ob' = 哥哥, 'lb' = 弟弟, 'os' = 姐姐, 'ls' = 妹妹
*/
private buildRelationPath(fromId: string, toId: string): string | null {
// 使用BFS查找最短路径
const queue: Array<{ id: string; path: string }> = [{ id: fromId, path: '' }]
const visited = new Set<string>()
while (queue.length > 0) {
const { id, path } = queue.shift()!
if (id === toId) {
return path
}
if (visited.has(id)) continue
visited.add(id)
const member = this.members.get(id)
if (!member) continue
// 向上:父亲
if (member.fatherId && !visited.has(member.fatherId)) {
queue.push({ id: member.fatherId, path: path + 'f' })
}
// 向上:母亲
if (member.motherId && !visited.has(member.motherId)) {
queue.push({ id: member.motherId, path: path + 'm' })
}
// 配偶关系
if (member.spouseIds) {
for (const spouseId of member.spouseIds) {
if (!visited.has(spouseId)) {
const spouse = this.members.get(spouseId)
if (spouse) {
const spousePath = member.gender === 'MALE' ? 'w' : 'h'
queue.push({ id: spouseId, path: path + spousePath })
}
}
}
}
// 向下:子女
if (member.childrenIds) {
for (const childId of member.childrenIds) {
if (!visited.has(childId)) {
const child = this.members.get(childId)
if (child) {
const childPath = child.gender === 'MALE' ? 's' : 'd'
queue.push({ id: childId, path: path + childPath })
}
}
}
}
// 兄弟姐妹(通过父母查找)
if (member.fatherId || member.motherId) {
const siblings = this.getSiblings(id)
for (const siblingId of siblings) {
if (!visited.has(siblingId)) {
const sibling = this.members.get(siblingId)
if (sibling) {
// 需要比较年龄确定是哥哥/弟弟/姐姐/妹妹
const siblingPath = this.getSiblingPath(member, sibling)
queue.push({ id: siblingId, path: path + siblingPath })
}
}
}
}
}
return null
}
/**
* 获取兄弟姐妹
*/
private getSiblings(memberId: string): string[] {
const member = this.members.get(memberId)
if (!member) return []
const siblings: string[] = []
// 通过父亲查找
if (member.fatherId) {
const father = this.members.get(member.fatherId)
if (father?.childrenIds) {
siblings.push(...father.childrenIds.filter(id => id !== memberId))
}
}
// 通过母亲查找
if (member.motherId) {
const mother = this.members.get(member.motherId)
if (mother?.childrenIds) {
for (const childId of mother.childrenIds) {
if (childId !== memberId && !siblings.includes(childId)) {
siblings.push(childId)
}
}
}
}
return siblings
}
/**
* 获取兄弟姐妹关系路径
*/
private getSiblingPath(from: FamilyMember, to: FamilyMember): string {
const fromAge = this.getAge(from)
const toAge = this.getAge(to)
if (to.gender === 'MALE') {
// 男性:哥哥(ob) 或 弟弟(lb)
return toAge > fromAge ? 'ob' : 'lb'
} else {
// 女性:姐姐(os) 或 妹妹(ls)
return toAge > fromAge ? 'os' : 'ls'
}
}
/**
* 获取年龄(用于比较)
*/
private getAge(member: FamilyMember): number {
if (!member.birthDate) return 0
const birthYear = new Date(member.birthDate).getFullYear()
return new Date().getFullYear() - birthYear
}
/**
* 获取关系的详细说明
*/
getRelationshipDetail(fromId: string, toId: string): RelationshipDetail {
const result = this.getRelationship(fromId, toId)
if (!result.success) {
return {
...result,
description: result.error || '无法计算关系'
}
}
// 构建带人名的路径(从 fromId 到 toId
const pathWithNames = this.buildPathWithNames(fromId, toId)
// 只返回关系称谓,不包含完整句子
return {
...result,
path: pathWithNames || result.path,
description: result.term
}
}
/**
* 构建带人名的路径描述
* @param fromId 起始ID(查询的起点)
* @param toId 目标ID(查询的终点)
*/
private buildPathWithNames(fromId: string, toId: string): string | null {
const queue: Array<{ id: string; path: Array<{name: string, relation: string}> }> = [
{ id: fromId, path: [] }
]
const visited = new Set<string>()
// 获取起始成员名字
const startMember = this.members.get(fromId)
const startName = startMember?.fullName || '未知'
while (queue.length > 0) {
const { id, path } = queue.shift()!
if (id === toId) {
// 构建路径字符串:从起点到终点
if (path.length === 0) return '本人'
// 格式:起点 → 关系1(人名1) → 关系2(人名2) → ...
const pathStr = path.map(p => `${p.relation}(${p.name})`).join(' → ')
return `${startName}${pathStr}`
}
if (visited.has(id)) continue
visited.add(id)
const member = this.members.get(id)
if (!member) continue
// 向上:父亲
if (member.fatherId && !visited.has(member.fatherId)) {
const father = this.members.get(member.fatherId)
if (father) {
queue.push({
id: member.fatherId,
path: [...path, { name: father.fullName, relation: '父亲' }]
})
}
}
// 向上:母亲
if (member.motherId && !visited.has(member.motherId)) {
const mother = this.members.get(member.motherId)
if (mother) {
queue.push({
id: member.motherId,
path: [...path, { name: mother.fullName, relation: '母亲' }]
})
}
}
// 配偶关系
if (member.spouseIds) {
for (const spouseId of member.spouseIds) {
if (!visited.has(spouseId)) {
const spouse = this.members.get(spouseId)
if (spouse) {
const relation = member.gender === 'MALE' ? '妻子' : '丈夫'
queue.push({
id: spouseId,
path: [...path, { name: spouse.fullName, relation }]
})
}
}
}
}
// 向下:子女
if (member.childrenIds) {
for (const childId of member.childrenIds) {
if (!visited.has(childId)) {
const child = this.members.get(childId)
if (child) {
const relation = child.gender === 'MALE' ? '儿子' : '女儿'
queue.push({
id: childId,
path: [...path, { name: child.fullName, relation }]
})
}
}
}
}
// 兄弟姐妹
if (member.fatherId || member.motherId) {
const siblings = this.getSiblings(id)
for (const siblingId of siblings) {
if (!visited.has(siblingId)) {
const sibling = this.members.get(siblingId)
if (sibling) {
const siblingPath = this.getSiblingRelationName(member, sibling)
queue.push({
id: siblingId,
path: [...path, { name: sibling.fullName, relation: siblingPath }]
})
}
}
}
}
}
return null
}
/**
* 获取兄弟姐妹关系名称
*/
private getSiblingRelationName(from: FamilyMember, to: FamilyMember): string {
const fromAge = this.getAge(from)
const toAge = this.getAge(to)
if (to.gender === 'MALE') {
return toAge > fromAge ? '哥哥' : '弟弟'
} else {
return toAge > fromAge ? '姐姐' : '妹妹'
}
}
/**
* 计算路径长度(关系代数)
*/
private calculatePathLength(path: string): number {
let length = 0
let i = 0
while (i < path.length) {
// 两字符的关系
if (i + 1 < path.length) {
const twoChar = path.substring(i, i + 2)
if (['ob', 'lb', 'os', 'ls'].includes(twoChar)) {
length++
i += 2
continue
}
}
// 单字符的关系
length++
i++
}
return length
}
/**
* 将符号路径转换为中文文本
* 例如:'fm' -> '父亲的母亲'
*/
private pathToChineseText(path: string): string {
const pathMap: Record<string, string> = {
'f': '父亲',
'm': '母亲',
'h': '丈夫',
'w': '妻子',
's': '儿子',
'd': '女儿',
'ob': '哥哥',
'lb': '弟弟',
'os': '姐姐',
'ls': '妹妹'
}
const parts: string[] = []
let i = 0
while (i < path.length) {
// 处理两字符的关系(如 ob, lb, os, ls
if (i + 1 < path.length) {
const twoChar = path.substring(i, i + 2)
if (pathMap[twoChar]) {
parts.push(pathMap[twoChar])
i += 2
continue
}
}
// 处理单字符的关系
const oneChar = path[i]
if (pathMap[oneChar]) {
parts.push(pathMap[oneChar])
}
i++
}
return parts.join('的')
}
/**
* 将路径转换为中文描述(带"的"字)
*/
private getPathDescription(path: string): string {
const pathMap: Record<string, string> = {
'f': '的父亲',
'm': '的母亲',
'h': '的丈夫',
'w': '的妻子',
's': '的儿子',
'd': '的女儿',
'ob': '的哥哥',
'lb': '的弟弟',
'os': '的姐姐',
'ls': '的妹妹'
}
let description = ''
for (const char of path) {
description += pathMap[char] || ''
}
return description || '本人'
}
}
export interface RelationshipResult {
term: string // 关系称谓(如:父亲、堂兄弟)
path: string // 关系路径(如:f, fbs
success: boolean // 是否成功计算
error?: string // 错误信息
}
export interface RelationshipDetail extends RelationshipResult {
description: string // 关系描述(如:父亲的哥哥的儿子)
}