frindlymap/lib/modules/kinship/src/relation.dart

97 lines
3.5 KiB
Dart

import 'graph.dart';
import 'titles.dart';
/// 두 사람 사이의 관계. [chon] 은 촌수(배우자는 0), 모르는 사이면 null.
class Relation {
const Relation(this.kind, this.title, {this.chon, this.up = 0, this.down = 0});
/// self | blood | spouse | inlaw | none
final String kind;
/// "b 는 a 의 [title]" 에 들어가는 말.
final String title;
final int? chon;
final int up; // 혈족일 때: a 에서 공통 조상까지 올라간 단계
final int down; // 공통 조상에서 b 까지 내려간 단계
bool get related => kind != 'none';
/// "삼촌 (3촌)" 처럼 읽기 좋게.
String get label => chon == null || kind == 'self' ? title : (chon == 0 ? title : '$title ($chon촌)');
}
class _Anc {
const _Anc(this.dist, this.via);
final int dist;
final String? via; // 처음 올라간 부모
}
Map<String, _Anc> _ancestors(KinGraph g, String id) {
final out = <String, _Anc>{id: const _Anc(0, null)};
var frontier = <String>[id];
var d = 0;
while (frontier.isNotEmpty) {
d++;
final next = <String>[];
for (final x in frontier) {
for (final p in g.parentsOf(x)) {
if (out.containsKey(p)) continue;
out[p] = _Anc(d, x == id ? p : out[x]!.via);
next.add(p);
}
}
frontier = next;
}
return out;
}
/// 혈족 관계의 (올라간 단계, 내려간 단계, a 의 첫 부모, b 의 첫 부모). 이어지지 않으면 null.
({int up, int down, String? aVia, String? bVia})? bloodPath(KinGraph g, String a, String b) {
final aa = _ancestors(g, a), bb = _ancestors(g, b);
({int up, int down, String? aVia, String? bVia})? best;
for (final id in (aa.keys.toList()..sort())) {
final x = bb[id];
if (x == null) continue;
final up = aa[id]!.dist, down = x.dist;
if (best == null || up + down < best.up + best.down) {
best = (up: up, down: down, aVia: aa[id]!.via, bVia: x.via);
}
}
return best;
}
/// "b 는 a 의 무엇인가". 혈족, 배우자, 배우자 쪽 인척(혈족의 배우자·배우자의 혈족)을 찾는다.
Relation relate(KinGraph g, String a, String b) {
if (!g.has(a) || !g.has(b)) return const Relation('none', '');
if (a == b) return const Relation('self', '본인', chon: 0);
final blood = bloodPath(g, a, b);
if (blood != null) {
final chon = blood.up + blood.down;
return Relation('blood', bloodTitle(g, a, b, blood.up, blood.down, blood.aVia, blood.bVia), chon: chon, up: blood.up, down: blood.down);
}
if (g.spousesOf(a).contains(b)) return Relation('spouse', spouseTitle(g.person(b)!), chon: 0);
// b 가 a 의 혈족(또는 a 본인)의 배우자
Relation? best;
for (final s in g.spousesOf(b)) {
final r = s == a ? const Relation('self', '본인', chon: 0) : relate0(g, a, s);
if (r == null) continue;
final t = Relation('inlaw', spouseOfRelativeTitle(g, a, b, s, r), chon: r.chon);
if (best == null || (t.chon ?? 99) < (best.chon ?? 99)) best = t;
}
// b 가 a 의 배우자의 혈족
for (final s in g.spousesOf(a)) {
final r = relate0(g, s, b);
if (r == null) continue;
final t = Relation('inlaw', relativeOfSpouseTitle(g, a, b, s, r), chon: r.chon);
if (best == null || (t.chon ?? 99) < (best.chon ?? 99)) best = t;
}
return best ?? const Relation('none', '');
}
/// 혈족만 따진 관계 (배우자를 건너지 않는다).
Relation? relate0(KinGraph g, String a, String b) {
final p = bloodPath(g, a, b);
if (p == null) return null;
return Relation('blood', bloodTitle(g, a, b, p.up, p.down, p.aVia, p.bVia), chon: p.up + p.down, up: p.up, down: p.down);
}