dsh-metaphysics 0.13.5 → 0.14.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/CHANGELOG.md +57 -0
- package/README.md +24 -5
- package/client.js +3209 -2101
- package/engine/bazi.js +52 -13
- package/engine/calendar.js +88 -16
- package/engine/daliuren.js +36 -1
- package/engine/geo.js +28 -11
- package/engine/hepan.js +144 -0
- package/engine/index.js +262 -39
- package/engine/interpret.js +108 -6
- package/engine/lunar.js +46 -12
- package/engine/meihua.js +2 -2
- package/engine/qimen-interpret.js +176 -0
- package/engine/qimen.js +440 -0
- package/engine/random.js +33 -0
- package/engine/tarot.js +5 -4
- package/engine/vsop-earth.js +40 -0
- package/engine/ziwei-hepan.js +120 -0
- package/engine/ziwei.js +2 -2
- package/index.js +109 -25
- package/locale/en.json +1 -1
- package/locale/zh.json +2 -2
- package/package.json +13 -9
package/engine/bazi.js
CHANGED
|
@@ -39,6 +39,12 @@ const OVERCOMES = { 木: '土', 土: '水', 水: '火', 火: '金', 金: '木' }
|
|
|
39
39
|
|
|
40
40
|
const isYangStem = (stem) => STEMS.indexOf(stem) % 2 === 0;
|
|
41
41
|
|
|
42
|
+
// 两套藏干权重是不同用途,不是"同一件事写了两次":
|
|
43
|
+
// · 五行占比——看这一行在盘里有多少,本气 1 / 中气 0.5 / 余气 0.3;
|
|
44
|
+
// · 日主通根——看这一支能不能让日主站住,本气权重更高(1.2 / 0.6 / 0.3)。
|
|
45
|
+
const WUXING_HIDDEN_WEIGHT = [1, 0.5, 0.3];
|
|
46
|
+
const ROOT_HIDDEN_WEIGHT = [1.2, 0.6, 0.3];
|
|
47
|
+
|
|
42
48
|
const STEM_HE = { 甲己: '土', 乙庚: '金', 丙辛: '水', 丁壬: '木', 戊癸: '火' };
|
|
43
49
|
const STEM_CHONG_PAIRS = ['甲庚', '乙辛', '丙壬', '丁癸'];
|
|
44
50
|
const BRANCH_HE = { 子丑: '土', 寅亥: '木', 卯戌: '火', 辰酉: '金', 巳申: '水', 午未: '土' };
|
|
@@ -47,7 +53,10 @@ const SANHUI = { 寅卯辰: '木', 巳午未: '火', 申酉戌: '金', 亥子丑
|
|
|
47
53
|
const BRANCH_CHONG = ['子午', '丑未', '寅申', '卯酉', '辰戌', '巳亥'];
|
|
48
54
|
const BRANCH_HAI = ['子未', '丑午', '寅巳', '卯辰', '申亥', '酉戌'];
|
|
49
55
|
const BRANCH_PO = ['子酉', '丑辰', '寅亥', '卯午', '巳申', '未戌'];
|
|
56
|
+
// 三刑:寅巳申(无恩之刑)、丑戌未(恃势之刑)、子卯(无礼之刑)。
|
|
57
|
+
// 另有「自刑」:辰午酉亥四支见同支即自刑(辰辰、午午、酉酉、亥亥),此前漏判。
|
|
50
58
|
const XING_GROUPS = [['寅', '巳', '申'], ['丑', '戌', '未'], ['子', '卯']];
|
|
59
|
+
const XING_SELF = ['辰', '午', '酉', '亥'];
|
|
51
60
|
|
|
52
61
|
const hasPair = (list, a, b) => list.includes(a + b) || list.includes(b + a);
|
|
53
62
|
|
|
@@ -92,7 +101,11 @@ export function buildRelations(pillars) {
|
|
|
92
101
|
for (let j = i + 1; j < 4; j++) {
|
|
93
102
|
const a = branches[i];
|
|
94
103
|
const b = branches[j];
|
|
95
|
-
if (a === b)
|
|
104
|
+
if (a === b) {
|
|
105
|
+
// 同支不是"没关系":辰午酉亥两柱同见为自刑
|
|
106
|
+
if (XING_SELF.includes(a)) branchOut.push({ text: `${a}${b}自刑`, rank: 2 });
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
96
109
|
const he = BRANCH_HE[a + b] || BRANCH_HE[b + a];
|
|
97
110
|
const sh = Object.keys(SANHE).find((k) => k.includes(a) && k.includes(b));
|
|
98
111
|
const hui = Object.keys(SANHUI).find((k) => k.includes(a) && k.includes(b));
|
|
@@ -258,8 +271,10 @@ export function buildBazi(input) {
|
|
|
258
271
|
const forward = (isYangYear && gender === 'male') || (!isYangYear && gender === 'female');
|
|
259
272
|
const spanDays = forward ? nextJieTerm.jd - jd : jd - prevJieTerm.jd;
|
|
260
273
|
const totalMonths = spanDays * 4; // 3 天 = 1 年 = 12 个月
|
|
261
|
-
|
|
262
|
-
|
|
274
|
+
let startAgeYears = Math.floor(totalMonths / 12);
|
|
275
|
+
let startAgeMonths = Math.round(totalMonths % 12);
|
|
276
|
+
// 四舍五入到 12 个月时必须进位,否则显示「X 岁 12 个月起运」
|
|
277
|
+
if (startAgeMonths >= 12) { startAgeMonths -= 12; startAgeYears += 1; }
|
|
263
278
|
|
|
264
279
|
const dayun = [];
|
|
265
280
|
for (let i = 1; i <= 8; i++) {
|
|
@@ -276,8 +291,8 @@ export function buildBazi(input) {
|
|
|
276
291
|
});
|
|
277
292
|
}
|
|
278
293
|
|
|
279
|
-
//
|
|
280
|
-
const thisYear = new Date().getFullYear();
|
|
294
|
+
// 流年(默认从「今年」起排 10 年;传 input.nowYear 可钉死基准年,用于复现历史结论)
|
|
295
|
+
const thisYear = Number(input.nowYear) || new Date().getFullYear();
|
|
281
296
|
const liunian = [];
|
|
282
297
|
for (let y = thisYear; y < thisYear + 10; y++) {
|
|
283
298
|
const idx = (((y - 4) % 60) + 60) % 60;
|
|
@@ -289,13 +304,12 @@ export function buildBazi(input) {
|
|
|
289
304
|
});
|
|
290
305
|
}
|
|
291
306
|
|
|
292
|
-
// 五行统计(天干 1.0;地支藏干按
|
|
293
|
-
const weights = [1, 0.5, 0.3];
|
|
307
|
+
// 五行统计(天干 1.0;地支藏干按 WUXING_HIDDEN_WEIGHT)
|
|
294
308
|
const wuxing = { 木: 0, 火: 0, 土: 0, 金: 0, 水: 0 };
|
|
295
309
|
for (const p of Object.values(pillars)) {
|
|
296
310
|
wuxing[p.stemElement] += 1;
|
|
297
311
|
p.hidden.forEach((h, i) => {
|
|
298
|
-
wuxing[h.element] +=
|
|
312
|
+
wuxing[h.element] += WUXING_HIDDEN_WEIGHT[i] ?? 0.1;
|
|
299
313
|
});
|
|
300
314
|
}
|
|
301
315
|
for (const k of Object.keys(wuxing)) wuxing[k] = Math.round(wuxing[k] * 10) / 10;
|
|
@@ -352,15 +366,14 @@ export function buildBazi(input) {
|
|
|
352
366
|
else if (OVERCOMES[me] === seasonEl) { monthW = 0.5; monthWord = '囚'; }
|
|
353
367
|
else { monthW = 0; monthWord = '死'; }
|
|
354
368
|
|
|
355
|
-
const HID_W = [1.2, 0.6, 0.3];
|
|
356
369
|
let rootScore = 0;
|
|
357
370
|
const rootBranches = [];
|
|
358
371
|
for (const key of ['year', 'month', 'day', 'hour']) {
|
|
359
372
|
const pl = pillars[key];
|
|
360
373
|
let hit = false;
|
|
361
374
|
pl.hidden.forEach((h, i) => {
|
|
362
|
-
if (STEM_ELEMENT[h.stem] === me) { rootScore +=
|
|
363
|
-
else if (GENERATES[STEM_ELEMENT[h.stem]] === me) { rootScore += (
|
|
375
|
+
if (STEM_ELEMENT[h.stem] === me) { rootScore += ROOT_HIDDEN_WEIGHT[i] ?? 0.1; hit = true; }
|
|
376
|
+
else if (GENERATES[STEM_ELEMENT[h.stem]] === me) { rootScore += (ROOT_HIDDEN_WEIGHT[i] ?? 0.1) * 0.4; hit = true; }
|
|
364
377
|
});
|
|
365
378
|
if (hit) rootBranches.push(pl.branch);
|
|
366
379
|
}
|
|
@@ -400,7 +413,9 @@ export function buildBazi(input) {
|
|
|
400
413
|
|
|
401
414
|
// ── 大运/流年与原局的地支关系 ──
|
|
402
415
|
const pairRel = (a, b) => {
|
|
403
|
-
if (!a || !b
|
|
416
|
+
if (!a || !b) return [];
|
|
417
|
+
// 大运/流年支与原局同支:辰午酉亥为自刑,其余同支不算关系
|
|
418
|
+
if (a === b) return XING_SELF.includes(a) ? ['自刑'] : [];
|
|
404
419
|
const out = [];
|
|
405
420
|
if (BRANCH_HE[a + b] || BRANCH_HE[b + a]) out.push('合');
|
|
406
421
|
if (hasPair(BRANCH_CHONG, a, b)) out.push('冲');
|
|
@@ -432,9 +447,31 @@ export function buildBazi(input) {
|
|
|
432
447
|
|
|
433
448
|
const p = jdToParts(jd);
|
|
434
449
|
|
|
450
|
+
// ── 胎元 / 命宫 / 身宫 ──
|
|
451
|
+
// 胎元:月柱天干进一位、地支进三位(受胎之月的干支)
|
|
452
|
+
const mStemIdx = STEMS.indexOf(pillars.month.stem);
|
|
453
|
+
const mBranchIdx = BRANCHES.indexOf(pillars.month.branch);
|
|
454
|
+
const taiyuan = ganzhiName(sexagenaryIndex((mStemIdx + 1) % 10, (mBranchIdx + 3) % 12));
|
|
455
|
+
// 命宫:以子月起正月顺数至生月,再自该宫起子时逆数至生时
|
|
456
|
+
const hourBrIdxForGong = BRANCHES.indexOf(pillars.hour.branch);
|
|
457
|
+
const soulIdx = (((mBranchIdx - hourBrIdxForGong) % 12) + 12) % 12;
|
|
458
|
+
// 命宫天干按五虎遁从年干起(寅为正月)
|
|
459
|
+
const yinStem = ((STEMS.indexOf(pillars.year.stem) % 5) * 2 + 2) % 10;
|
|
460
|
+
const soulStemIdx = (yinStem + ((soulIdx - 2 + 12) % 12)) % 10;
|
|
461
|
+
const minggong = ganzhiName(sexagenaryIndex(soulStemIdx, soulIdx));
|
|
462
|
+
// 身宫:命宫对宫(相隔六位)
|
|
463
|
+
const bodyIdx = (soulIdx + 6) % 12;
|
|
464
|
+
const bodyStemIdx = (yinStem + ((bodyIdx - 2 + 12) % 12)) % 10;
|
|
465
|
+
const shengong = ganzhiName(sexagenaryIndex(bodyStemIdx, bodyIdx));
|
|
466
|
+
|
|
435
467
|
return {
|
|
436
468
|
system: 'bazi',
|
|
437
469
|
relations: buildRelations(pillars),
|
|
470
|
+
palaces: {
|
|
471
|
+
taiyuan: { ganzhi: taiyuan.name, note: '月干进一、月支进三' },
|
|
472
|
+
minggong: { ganzhi: minggong.name, branch: minggong.branch, note: '子月起正月顺数至生月,再逆数至生时;干按五虎遁' },
|
|
473
|
+
shengong: { ganzhi: shengong.name, branch: shengong.branch, note: '命宫对宫' },
|
|
474
|
+
},
|
|
438
475
|
input: {
|
|
439
476
|
civil: { year, month, day, hour, minute },
|
|
440
477
|
gender,
|
|
@@ -454,7 +491,8 @@ export function buildBazi(input) {
|
|
|
454
491
|
jieqi: {
|
|
455
492
|
previous: { name: prevJieTerm.name, time: formatJd(prevJieTerm.jd) },
|
|
456
493
|
next: { name: nextJieTerm.name, time: formatJd(nextJieTerm.jd) },
|
|
457
|
-
//
|
|
494
|
+
// 距「节」的分钟数(取两侧较小值)。是否提示复核由摘要层决定,阈值 20 分钟:
|
|
495
|
+
// 与 NAOJ 历要项对拍实测节气最多偏 13.5 分钟,20 分钟留出余量。
|
|
458
496
|
nearBoundaryMinutes: Math.round(Math.min(jd - prevJieTerm.jd, nextJieTerm.jd - jd) * 1440),
|
|
459
497
|
},
|
|
460
498
|
pillars,
|
|
@@ -472,5 +510,6 @@ export function buildBazi(input) {
|
|
|
472
510
|
strength,
|
|
473
511
|
frame,
|
|
474
512
|
liunian,
|
|
513
|
+
liunianFrom: thisYear,
|
|
475
514
|
};
|
|
476
515
|
}
|
package/engine/calendar.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { earthL } from './vsop-earth.js';
|
|
1
2
|
// 历法底座:儒略日、干支、二十四节气、真太阳时
|
|
2
3
|
// 全部为纯函数,不依赖 DOM / Node API,Host 与 Client 可共用。
|
|
3
4
|
|
|
@@ -69,18 +70,43 @@ export function dayGanzhiIndex(jdn) {
|
|
|
69
70
|
return (((jdn + 49) % 60) + 60) % 60;
|
|
70
71
|
}
|
|
71
72
|
|
|
72
|
-
/**
|
|
73
|
+
/**
|
|
74
|
+
* 太阳视黄经(度)。
|
|
75
|
+
* 黄经本体改用 VSOP87B 截断级数(105 项),再把地心黄经转成视黄经:
|
|
76
|
+
* λ = Θ + FK5修正 + 章动Δψ + 光行差
|
|
77
|
+
* 与 NAOJ 历要项对拍:2024/2025 共 48 个节气,偏差从原来的最多 13.5 分钟降到 1 分钟内。
|
|
78
|
+
*/
|
|
73
79
|
export function sunLongitude(jd) {
|
|
74
|
-
const
|
|
75
|
-
const
|
|
76
|
-
const
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
80
|
+
const d = jd - 2451545.0;
|
|
81
|
+
const T = d / 36525;
|
|
82
|
+
const Tv = d / 365250; // VSOP87 的时间参量单位是「儒略千年」,不是世纪
|
|
83
|
+
|
|
84
|
+
// 1) 地球日心黄经 → 太阳地心黄经
|
|
85
|
+
let theta = (earthL(Tv) * 180) / Math.PI + 180;
|
|
86
|
+
theta = ((theta % 360) + 360) % 360;
|
|
87
|
+
|
|
88
|
+
// 2) 岁差:VSOP87B 的黄经是 J2000 参考系,必须先推到当日平春分点
|
|
89
|
+
// (2024 年约 0.335°,相当于 8 小时,漏掉会让整个节气表错开)
|
|
90
|
+
const prec = (5029.0966 * T + 1.11113 * T * T - 0.000006 * T * T * T) / 3600;
|
|
91
|
+
theta += prec;
|
|
92
|
+
|
|
93
|
+
// 3) FK5 系统修正(Meeus 25.9)
|
|
94
|
+
theta -= 0.09033 / 3600;
|
|
95
|
+
|
|
96
|
+
// 3) 章动(Meeus 22,取四项主项,精度约 0.5″)
|
|
97
|
+
const om = ((125.04452 - 1934.136261 * T) * Math.PI) / 180;
|
|
98
|
+
const ls = ((280.4665 + 36000.7698 * T) * Math.PI) / 180;
|
|
99
|
+
const lm = ((218.3165 + 481267.8813 * T) * Math.PI) / 180;
|
|
100
|
+
const dPsi = (-17.2 * Math.sin(om) - 1.32 * Math.sin(2 * ls) - 0.23 * Math.sin(2 * lm) + 0.21 * Math.sin(2 * om)) / 3600;
|
|
101
|
+
theta += dPsi;
|
|
102
|
+
|
|
103
|
+
// 4) 光行差:-20.4898″ / R;R 只需三位有效数字,沿用低精度日地距离即可
|
|
104
|
+
const m = ((357.52911 + 35999.05029 * T - 0.0001537 * T * T) * Math.PI) / 180;
|
|
105
|
+
const e = 0.016708634 - 0.000042037 * T - 0.0000001267 * T * T;
|
|
106
|
+
const R = (1.000001018 * (1 - e * e)) / (1 + e * Math.cos(m + (1.914602 - 0.004817 * T) * (Math.PI / 180) * Math.sin(m)));
|
|
107
|
+
theta -= 20.4898 / 3600 / (R || 1);
|
|
108
|
+
|
|
109
|
+
return ((theta % 360) + 360) % 360;
|
|
84
110
|
}
|
|
85
111
|
|
|
86
112
|
/** ΔT(TT - UT,秒)。分段线性近似,误差 < 2s,对分钟级排盘足够 */
|
|
@@ -101,11 +127,31 @@ export function deltaTSeconds(year) {
|
|
|
101
127
|
return knots[knots.length - 1][1];
|
|
102
128
|
}
|
|
103
129
|
|
|
130
|
+
// 1929 年元旦之前,中国历书按「北京地方平时」(116.4°E,UTC+7.7611)推算朔与中气;
|
|
131
|
+
// 之后改用东经 120° 标准时(UTC+8)。这 14 分钟会让交节/合朔跨零点,进而整月错位,
|
|
132
|
+
// 所以节气与农历必须共用同一套换算,不能各写一份。
|
|
133
|
+
export const OFFSET_MODERN = 8 / 24;
|
|
134
|
+
export const OFFSET_BEIJING_LOCAL = 7.7611 / 24;
|
|
135
|
+
// 切点 = 1929-01-01 00:00(东八区)对应的 UT 儒略日 = 1928-12-31 16:00 UT。
|
|
136
|
+
// 这里用的是真儒略日轴:partsToJd(1929,1,1) = 2425612.5,减 8/24 即 2425612.1667。
|
|
137
|
+
export const CUTOVER_JD = 2425612 + 4 / 24; // 2425612.1667
|
|
138
|
+
|
|
139
|
+
/** 某时刻(UT 轴)应当使用的本地时区偏移量 */
|
|
140
|
+
export function localOffsetFor(jdUt) {
|
|
141
|
+
return jdUt < CUTOVER_JD ? OFFSET_BEIJING_LOCAL : OFFSET_MODERN;
|
|
142
|
+
}
|
|
143
|
+
|
|
104
144
|
/**
|
|
105
|
-
* 第 index 个节气(0 =
|
|
145
|
+
* 第 index 个节气(0 = 小寒)在指定年份的时刻,返回「当时本地时间轴」的儒略日。
|
|
106
146
|
* index 为偶数的是「节」(定月柱),奇数的是「气」。
|
|
147
|
+
*
|
|
148
|
+
* 结果按 (year, index) 缓存:农历换算一次要问几百次中气(每个候选月都要判有无中气),
|
|
149
|
+
* 而 VSOP87 的 earthL 是整个引擎最贵的一段——不缓存时紫微盘 10ms 里有 8ms 花在这里。
|
|
107
150
|
*/
|
|
108
|
-
|
|
151
|
+
const TERM_INSTANT_CACHE = new Map();
|
|
152
|
+
const TERM_INSTANT_CACHE_MAX = 4096;
|
|
153
|
+
|
|
154
|
+
function computeTermInstantJd(year, index) {
|
|
109
155
|
const target = termLongitude(index);
|
|
110
156
|
const anchor = toJDN(year, 1, 5) - 0.5 + index * 15.2184;
|
|
111
157
|
let jd = anchor;
|
|
@@ -115,8 +161,21 @@ export function termInstantJd(year, index) {
|
|
|
115
161
|
jd += diff / 0.98564736;
|
|
116
162
|
if (Math.abs(diff) < 1e-7) break;
|
|
117
163
|
}
|
|
118
|
-
// TT -> UT ->
|
|
119
|
-
|
|
164
|
+
// TT -> UT -> 当时本地时间轴(1929 前是北京地方平时,之后是东八区)
|
|
165
|
+
const jdUt = jd - deltaTSeconds(year) / 86400;
|
|
166
|
+
return jdUt + localOffsetFor(jdUt);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export function termInstantJd(year, index) {
|
|
170
|
+
const key = year * 24 + index;
|
|
171
|
+
const hit = TERM_INSTANT_CACHE.get(key);
|
|
172
|
+
if (hit !== undefined) return hit;
|
|
173
|
+
const jd = computeTermInstantJd(year, index);
|
|
174
|
+
if (TERM_INSTANT_CACHE.size >= TERM_INSTANT_CACHE_MAX) {
|
|
175
|
+
TERM_INSTANT_CACHE.delete(TERM_INSTANT_CACHE.keys().next().value);
|
|
176
|
+
}
|
|
177
|
+
TERM_INSTANT_CACHE.set(key, jd);
|
|
178
|
+
return jd;
|
|
120
179
|
}
|
|
121
180
|
|
|
122
181
|
/** 儒略日(本地时间轴)拆成日期与时刻 */
|
|
@@ -145,8 +204,15 @@ export function equationOfTime(year, month, day) {
|
|
|
145
204
|
return 9.87 * Math.sin(2 * B) - 7.53 * Math.cos(B) - 1.5 * Math.sin(B);
|
|
146
205
|
}
|
|
147
206
|
|
|
148
|
-
|
|
207
|
+
// 一次排盘要算 72 个节气(三年窗口),每次 12 轮牛顿迭代 × VSOP87 105 项。
|
|
208
|
+
// 同一年的盘经常连着排(面板切 tab、合盘两侧),按年缓存能把这块成本降到常数级。
|
|
209
|
+
const TERM_WINDOW_CACHE = new Map();
|
|
210
|
+
const TERM_WINDOW_CACHE_MAX = 8;
|
|
211
|
+
|
|
212
|
+
/** 列出 [year-1, year+1] 三年的节气,按时间升序(结果按年缓存,调用方不要修改返回值) */
|
|
149
213
|
export function termWindow(year) {
|
|
214
|
+
const cached = TERM_WINDOW_CACHE.get(year);
|
|
215
|
+
if (cached) return cached;
|
|
150
216
|
const list = [];
|
|
151
217
|
for (let y = year - 1; y <= year + 1; y++) {
|
|
152
218
|
for (let i = 0; i < 24; i++) {
|
|
@@ -154,6 +220,10 @@ export function termWindow(year) {
|
|
|
154
220
|
}
|
|
155
221
|
}
|
|
156
222
|
list.sort((a, b) => a.jd - b.jd);
|
|
223
|
+
if (TERM_WINDOW_CACHE.size >= TERM_WINDOW_CACHE_MAX) {
|
|
224
|
+
TERM_WINDOW_CACHE.delete(TERM_WINDOW_CACHE.keys().next().value);
|
|
225
|
+
}
|
|
226
|
+
TERM_WINDOW_CACHE.set(year, list);
|
|
157
227
|
return list;
|
|
158
228
|
}
|
|
159
229
|
|
|
@@ -182,3 +252,5 @@ export function formatJd(jd) {
|
|
|
182
252
|
const pad = (n) => String(n).padStart(2, '0');
|
|
183
253
|
return `${p.year}-${pad(p.month)}-${pad(p.day)} ${pad(p.hour)}:${pad(p.minute)}`;
|
|
184
254
|
}
|
|
255
|
+
|
|
256
|
+
|
package/engine/daliuren.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// 大六壬:天地盘、四课、九宗门三传。
|
|
1
|
+
// 大六壬:天地盘、四课、九宗门三传。
|
|
2
2
|
// 算法移植自本地 MIT 授权插件 @dhicoc/dsh-chinese-traditional-wisdom-skill 的
|
|
3
3
|
// daliurenEngine.ts(仅取 classic 流派:贵人顺逆以天盘临方判定)。
|
|
4
4
|
import { toJDN, dayGanzhiIndex, ganzhiName, termWindow, formatJd } from './calendar.js';
|
|
@@ -57,6 +57,38 @@ const DAY_NIGHT_MAP = {
|
|
|
57
57
|
酉: '夜', 戌: '夜', 亥: '夜', 子: '夜', 丑: '夜', 寅: '夜',
|
|
58
58
|
};
|
|
59
59
|
|
|
60
|
+
// ── 神煞(只收各派无争议、六壬断课常用的几项) ──
|
|
61
|
+
const RI_LU = { 甲: '寅', 乙: '卯', 丙: '巳', 丁: '午', 戊: '巳', 己: '午', 庚: '申', 辛: '酉', 壬: '亥', 癸: '子' };
|
|
62
|
+
const YANG_REN = { 甲: '卯', 乙: '辰', 丙: '午', 丁: '未', 戊: '午', 己: '未', 庚: '酉', 辛: '戌', 壬: '子', 癸: '丑' };
|
|
63
|
+
const SAN_HE = { 申: '水', 子: '水', 辰: '水', 寅: '火', 午: '火', 戌: '火', 巳: '金', 酉: '金', 丑: '金', 亥: '木', 卯: '木', 未: '木' };
|
|
64
|
+
const SAN_HE_SHEN = {
|
|
65
|
+
水: { 驿马: '寅', 桃花: '酉', 华盖: '辰', 劫煞: '巳', 亡神: '亥' },
|
|
66
|
+
火: { 驿马: '申', 桃花: '卯', 华盖: '戌', 劫煞: '亥', 亡神: '巳' },
|
|
67
|
+
金: { 驿马: '亥', 桃花: '午', 华盖: '丑', 劫煞: '寅', 亡神: '申' },
|
|
68
|
+
木: { 驿马: '巳', 桃花: '子', 华盖: '未', 劫煞: '申', 亡神: '寅' },
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
/** 按日干支算神煞 */
|
|
72
|
+
export function calcShensha(dayGz) {
|
|
73
|
+
const gan = dayGz[0];
|
|
74
|
+
const zhi = dayGz[1];
|
|
75
|
+
const out = [];
|
|
76
|
+
const push = (name, branch, note) => {
|
|
77
|
+
if (!out.some((x) => x.name === name)) out.push({ name, branch, note });
|
|
78
|
+
};
|
|
79
|
+
if (RI_LU[gan]) push('日禄', RI_LU[gan], '日干临官之位');
|
|
80
|
+
if (YANG_REN[gan]) push('羊刃', YANG_REN[gan], '日干帝旺之位');
|
|
81
|
+
const trio = SAN_HE_SHEN[SAN_HE[zhi]];
|
|
82
|
+
if (trio) {
|
|
83
|
+
push('驿马', trio.驿马, '日支三合冲位,主迁动');
|
|
84
|
+
push('桃花', trio.桃花, '咸池,主情缘');
|
|
85
|
+
push('华盖', trio.华盖, '主孤高、技艺');
|
|
86
|
+
push('劫煞', trio.劫煞, '主意外耗损');
|
|
87
|
+
push('亡神', trio.亡神, '主失脱、心神不定');
|
|
88
|
+
}
|
|
89
|
+
return out;
|
|
90
|
+
}
|
|
91
|
+
|
|
60
92
|
const XUN_KONG = {
|
|
61
93
|
甲子: ['戌', '亥'], 甲戌: ['申', '酉'], 甲申: ['午', '未'],
|
|
62
94
|
甲午: ['辰', '巳'], 甲辰: ['寅', '卯'], 甲寅: ['子', '丑'],
|
|
@@ -300,8 +332,11 @@ export function buildDaliuren(input) {
|
|
|
300
332
|
zhong: chuan('中传', zhong, tdp, dayGan, dayGz),
|
|
301
333
|
mo: chuan('末传', mo, tdp, dayGan, dayGz),
|
|
302
334
|
},
|
|
335
|
+
shensha: calcShensha(dayGz),
|
|
303
336
|
geJu: { name: geJu, detail },
|
|
304
337
|
};
|
|
305
338
|
}
|
|
306
339
|
|
|
307
340
|
export { ZHI, GAN, TIAN_JIANG, JIANG_FULL, JIEQI_YUE_JIANG };
|
|
341
|
+
|
|
342
|
+
|
package/engine/geo.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
|
-
// 地址 -> 经度(真太阳时用)。内置省级 +
|
|
2
|
-
// 坐标为近似值(±0.2°≈0.8
|
|
1
|
+
// 地址 -> 经度(真太阳时用)。内置省级 + 主要地级市坐标,离线即可用。
|
|
2
|
+
// 坐标为近似值(±0.2°≈0.8 分钟时差),足够真太阳时使用。
|
|
3
|
+
// 联网(Nominatim)是显式可选项:只有调用方传 online: true 才会把地址原文发出去,
|
|
4
|
+
// 默认走内置表,不上传任何数据。
|
|
3
5
|
const RAW = `
|
|
4
6
|
北京 116.41 39.90;上海 121.47 31.23;天津 117.20 39.13;重庆 106.55 29.56
|
|
5
7
|
广州 113.26 23.13;深圳 114.06 22.55;珠海 113.58 22.27;东莞 113.75 23.02;佛山 113.12 23.02
|
|
@@ -12,6 +14,8 @@ const RAW = `
|
|
|
12
14
|
徐州 117.28 34.26;扬州 119.41 32.39;盐城 120.16 33.35;泰州 119.92 32.46;镇江 119.45 32.20
|
|
13
15
|
淮安 119.02 33.61;连云港 119.22 34.60;宿迁 118.28 33.96
|
|
14
16
|
合肥 117.23 31.82;芜湖 118.43 31.35;蚌埠 117.39 32.92;安庆 117.05 30.54;阜阳 115.81 32.90
|
|
17
|
+
大同 113.30 40.08;阳泉 113.58 37.86;长治 113.12 36.19;晋城 112.85 35.49;朔州 112.43 39.33
|
|
18
|
+
晋中 112.75 37.69;运城 111.00 35.02;忻州 112.73 38.42;临汾 111.52 36.09;吕梁 111.14 37.52
|
|
15
19
|
六安 116.50 31.75;马鞍山 118.50 31.67;淮南 117.00 32.63;滁州 118.32 32.30;黄山 118.34 29.71
|
|
16
20
|
福州 119.30 26.08;厦门 118.09 24.48;泉州 118.68 24.87;漳州 117.65 24.51;莆田 119.01 25.45
|
|
17
21
|
宁德 119.55 26.67;三明 117.64 26.26;南平 118.18 26.64;龙岩 117.02 25.08
|
|
@@ -121,11 +125,15 @@ export function resolveAddress(input) {
|
|
|
121
125
|
return { lon: c.lon, lat: c.lat, matched: n, level: '地级市', note: `按 ${n} 估算(约 ${c.lon}°E)` };
|
|
122
126
|
}
|
|
123
127
|
}
|
|
128
|
+
// 省名判定用未去后缀的原文,否则「河北区」会被去掉「区」后误判成河北省
|
|
129
|
+
const rawFlat = raw.replace(/\s/g, '');
|
|
124
130
|
for (const [prov, capital] of Object.entries(PROVINCES)) {
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
131
|
+
const at = rawFlat.indexOf(prov);
|
|
132
|
+
if (at < 0) continue;
|
|
133
|
+
const after = rawFlat.charAt(at + prov.length);
|
|
134
|
+
if (after === '区' || after === '县') continue;
|
|
135
|
+
const c = CITIES[capital] || { lon: EXTRA[capital][0], lat: EXTRA[capital][1] };
|
|
136
|
+
return { lon: c.lon, lat: c.lat, matched: capital, level: '省会', note: `未匹配到具体城市,按 ${prov} 省会 ${capital} 估算(约 ${c.lon}°E)` };
|
|
129
137
|
}
|
|
130
138
|
return { lon: null, lat: null, matched: null, level: '未匹配', note: `没查到「${raw}」,可改填经度(如 116.4)` };
|
|
131
139
|
}
|
|
@@ -133,7 +141,10 @@ export function resolveAddress(input) {
|
|
|
133
141
|
/** 地址里出现了比「城市」更细的层级时,优先联网 */
|
|
134
142
|
const DETAIL = /(区|县|镇|乡|村|小区|街道|路|号|巷|弄|组|队)/;
|
|
135
143
|
|
|
136
|
-
/** OpenStreetMap Nominatim:免密钥、支持中文。失败返回 null。
|
|
144
|
+
/** OpenStreetMap Nominatim:免密钥、支持中文。失败返回 null。
|
|
145
|
+
* UA 按 OSM 使用政策要求写明应用与联系方式(改版本时同步这里)。 */
|
|
146
|
+
const USER_AGENT = 'dsh-metaphysics/0.14.2 (+https://github.com/xuanfu-web/dsh-metaphysics)';
|
|
147
|
+
|
|
137
148
|
async function onlineLookup(text, { timeoutMs = 2500 } = {}) {
|
|
138
149
|
if (typeof fetch !== 'function') return null;
|
|
139
150
|
let timer = null;
|
|
@@ -144,7 +155,7 @@ async function onlineLookup(text, { timeoutMs = 2500 } = {}) {
|
|
|
144
155
|
+ encodeURIComponent(text);
|
|
145
156
|
const res = await fetch(url, {
|
|
146
157
|
signal: controller ? controller.signal : undefined,
|
|
147
|
-
headers: { 'User-Agent':
|
|
158
|
+
headers: { 'User-Agent': USER_AGENT },
|
|
148
159
|
});
|
|
149
160
|
if (!res || !res.ok) return null;
|
|
150
161
|
const list = await res.json();
|
|
@@ -162,7 +173,8 @@ async function onlineLookup(text, { timeoutMs = 2500 } = {}) {
|
|
|
162
173
|
}
|
|
163
174
|
|
|
164
175
|
/**
|
|
165
|
-
* 地址 ->
|
|
176
|
+
* 地址 -> 经度。默认只查内置表;显式 online: true 时,地址更细(含区/县/村/小区等)
|
|
177
|
+
* 或离线未命中才会联网查。
|
|
166
178
|
* @param {string} input 地址
|
|
167
179
|
* @param {{online?: boolean, timeoutMs?: number}} opts
|
|
168
180
|
*/
|
|
@@ -170,12 +182,17 @@ export async function geocodeAddress(input, opts = {}) {
|
|
|
170
182
|
const local = resolveAddress(input);
|
|
171
183
|
const text = String(input || '').trim();
|
|
172
184
|
if (!text) return local;
|
|
173
|
-
|
|
185
|
+
// 默认离线:联网必须显式开启(会把出生地原文发给第三方)
|
|
186
|
+
const wantOnline = opts.online === true;
|
|
174
187
|
if (!wantOnline) return local;
|
|
175
188
|
if (local && typeof local.lon === 'number' && !DETAIL.test(text)) return local;
|
|
176
189
|
const remote = await onlineLookup(text, opts);
|
|
177
190
|
if (remote) return { ...remote, local: local && local.lon != null ? local : null };
|
|
178
|
-
|
|
191
|
+
// 联网失败不能静默:明确告诉用户已退回内置表,以及精度落在哪一级
|
|
192
|
+
if (local && typeof local.lon === 'number') {
|
|
193
|
+
return { ...local, onlineFailed: true, note: `${local.note}(联网解析失败或超时,已退回内置坐标表)` };
|
|
194
|
+
}
|
|
195
|
+
return { ...local, onlineFailed: true };
|
|
179
196
|
}
|
|
180
197
|
|
|
181
198
|
export { CITIES, PROVINCES };
|
package/engine/hepan.js
ADDED
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
// 合盘(合婚):日主关系 + 夫妻宫关系 + 五行互补。
|
|
2
|
+
// 口径:日主取五行生克(双向平均),夫妻宫取日支的六合/三合/冲/害/刑/破,
|
|
3
|
+
// 五行互补看「一方喜用是否正是另一方所旺」。各派取法不同,这里只给通用版结论。
|
|
4
|
+
import { buildBazi } from './bazi.js';
|
|
5
|
+
import { STEMS, BRANCHES } from './calendar.js';
|
|
6
|
+
|
|
7
|
+
const WX_GAN = { 甲: '木', 乙: '木', 丙: '火', 丁: '火', 戊: '土', 己: '土', 庚: '金', 辛: '金', 壬: '水', 癸: '水' };
|
|
8
|
+
const WX_ZHI = { 子: '水', 丑: '土', 寅: '木', 卯: '木', 辰: '土', 巳: '火', 午: '火', 未: '土', 申: '金', 酉: '金', 戌: '土', 亥: '水' };
|
|
9
|
+
const SHENG = { 木: '火', 火: '土', 土: '金', 金: '水', 水: '木' };
|
|
10
|
+
const KE = { 木: '土', 土: '水', 水: '火', 火: '金', 金: '木' };
|
|
11
|
+
|
|
12
|
+
// 地支两两关系(只取合、冲、害、刑、破;三合按半合算)
|
|
13
|
+
const LIU_HE = [['子', '丑'], ['寅', '亥'], ['卯', '戌'], ['辰', '酉'], ['巳', '申'], ['午', '未']];
|
|
14
|
+
const CHONG = [['子', '午'], ['丑', '未'], ['寅', '申'], ['卯', '酉'], ['辰', '戌'], ['巳', '亥']];
|
|
15
|
+
const HAI = [['子', '未'], ['丑', '午'], ['寅', '巳'], ['卯', '辰'], ['申', '亥'], ['酉', '戌']];
|
|
16
|
+
const PO = [['子', '酉'], ['卯', '午'], ['辰', '丑'], ['未', '戌'], ['寅', '亥'], ['巳', '申']];
|
|
17
|
+
const SAN_HE = [['申', '子', '辰'], ['寅', '午', '戌'], ['巳', '酉', '丑'], ['亥', '卯', '未']];
|
|
18
|
+
const XING = [['寅', '巳', '申'], ['丑', '戌', '未'], ['子', '卯']];
|
|
19
|
+
|
|
20
|
+
const pairIn = (list, a, b) => list.some((g) => g.includes(a) && g.includes(b) && a !== b);
|
|
21
|
+
|
|
22
|
+
function zhiRelation(a, b) {
|
|
23
|
+
if (a === b) return { name: '同支', score: 0, text: '日支相同,性情相近,容易互相理解,也容易同时卡在同一处' };
|
|
24
|
+
if (pairIn(LIU_HE, a, b)) return { name: '六合', score: 20, text: '日支六合,相处自然、彼此补位' };
|
|
25
|
+
for (const g of SAN_HE) {
|
|
26
|
+
if (g.includes(a) && g.includes(b)) return { name: '半合', score: 14, text: '日支同属三合局,易有共同语言' };
|
|
27
|
+
}
|
|
28
|
+
if (pairIn(CHONG, a, b)) return { name: '相冲', score: -15, text: '日支相冲,节奏与需求容易错位,需要刻意协调' };
|
|
29
|
+
if (pairIn(XING, a, b)) {
|
|
30
|
+
for (const g of XING) {
|
|
31
|
+
if (g.includes(a) && g.includes(b)) return { name: '相刑', score: -10, text: '日支相刑,摩擦多起于细节,宜少用指责' };
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
if (pairIn(HAI, a, b)) return { name: '相害', score: -9, text: '日支相害,容易因第三方或误解生嫌隙' };
|
|
35
|
+
if (pairIn(PO, a, b)) return { name: '相破', score: -5, text: '日支相破,小事易起波澜' };
|
|
36
|
+
return { name: '无直接关系', score: 4, text: '日支之间没有直接刑冲合害,关系好坏主要看经营' };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** 两个五行之间:对方对我 */
|
|
40
|
+
function wxToMe(other, me) {
|
|
41
|
+
if (other === me) return { name: '比和', score: 13, text: '两人日主同五行,气味相投,也容易一起犯同样的毛病' };
|
|
42
|
+
if (SHENG[other] === me) return { name: '生我', score: 20, text: '对方五行生我,容易得到照顾与助力' };
|
|
43
|
+
if (SHENG[me] === other) return { name: '我生', score: 10, text: '我生对方,多为付出的一方,久了要留意消耗' };
|
|
44
|
+
if (KE[me] === other) return { name: '我克', score: 5, text: '我克对方,主导感较强,需留意对方的空间' };
|
|
45
|
+
return { name: '克我', score: -12, text: '对方克我,压力感偏重,宜把话说开' };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** 合盘主函数 */
|
|
49
|
+
export function buildHepan(input) {
|
|
50
|
+
const A = buildBazi({ ...input.a, gender: input.a.gender });
|
|
51
|
+
const B = buildBazi({ ...input.b, gender: input.b.gender });
|
|
52
|
+
|
|
53
|
+
const aStem = A.pillars.day.stem;
|
|
54
|
+
const bStem = B.pillars.day.stem;
|
|
55
|
+
const aWx = WX_GAN[aStem];
|
|
56
|
+
const bWx = WX_GAN[bStem];
|
|
57
|
+
const aZhi = A.pillars.day.branch;
|
|
58
|
+
const bZhi = B.pillars.day.branch;
|
|
59
|
+
|
|
60
|
+
// ① 日主
|
|
61
|
+
const aToB = wxToMe(bWx, aWx); // 以 A 为主体看 B
|
|
62
|
+
const bToA = wxToMe(aWx, bWx);
|
|
63
|
+
const stemScore = (aToB.score + bToA.score) / 2;
|
|
64
|
+
|
|
65
|
+
// ② 夫妻宫(日支)
|
|
66
|
+
const gong = zhiRelation(aZhi, bZhi);
|
|
67
|
+
|
|
68
|
+
// ③ 五行互补:一方喜用是否正是另一方所旺
|
|
69
|
+
const favorA = (A.frame && A.frame.yongshen ? A.frame.yongshen.favor : []).slice();
|
|
70
|
+
const favorB = (B.frame && B.frame.yongshen ? B.frame.yongshen.favor : []).slice();
|
|
71
|
+
const wxCount = (r) => {
|
|
72
|
+
const m = {};
|
|
73
|
+
for (const k of ['year', 'month', 'day', 'hour']) {
|
|
74
|
+
m[WX_GAN[r.pillars[k].stem]] = (m[WX_GAN[r.pillars[k].stem]] || 0) + 1;
|
|
75
|
+
m[WX_ZHI[r.pillars[k].branch]] = (m[WX_ZHI[r.pillars[k].branch]] || 0) + 1;
|
|
76
|
+
}
|
|
77
|
+
return m;
|
|
78
|
+
};
|
|
79
|
+
const wxA = wxCount(A);
|
|
80
|
+
const wxB = wxCount(B);
|
|
81
|
+
const strongOf = (m) => Object.entries(m).sort((x, y) => y[1] - x[1])[0] || ['', 0];
|
|
82
|
+
const [strongA] = strongOf(wxA);
|
|
83
|
+
const [strongB] = strongOf(wxB);
|
|
84
|
+
const bFeedsA = favorA.includes(strongB);
|
|
85
|
+
const aFeedsB = favorB.includes(strongA);
|
|
86
|
+
let compScore = 0;
|
|
87
|
+
const compNotes = [];
|
|
88
|
+
if (bFeedsA) { compScore += 12; compNotes.push(`对方的旺行 ${strongB} 正是你的喜用`); }
|
|
89
|
+
if (aFeedsB) { compScore += 12; compNotes.push(`你的旺行 ${strongA} 正是对方的喜用`); }
|
|
90
|
+
if (!bFeedsA && !aFeedsB) { compScore += 3; compNotes.push('双方的旺行都不是对方的喜用,互补性一般,靠沟通补'); }
|
|
91
|
+
// 一方缺的五行正好是对方所旺,也算补
|
|
92
|
+
const missingA = ['木', '火', '土', '金', '水'].filter((w) => !wxA[w]);
|
|
93
|
+
const missingB = ['木', '火', '土', '金', '水'].filter((w) => !wxB[w]);
|
|
94
|
+
if (missingA.includes(strongB)) { compScore += 4; compNotes.push(`你命局缺 ${strongB},对方正好偏旺`); }
|
|
95
|
+
if (missingB.includes(strongA)) { compScore += 4; compNotes.push(`对方命局缺 ${strongA},你正好偏旺`); }
|
|
96
|
+
|
|
97
|
+
// ④ 综合
|
|
98
|
+
const raw = stemScore + gong.score + compScore; // 理论区间约 -27 ~ 56
|
|
99
|
+
const pct = Math.max(0, Math.min(100, Math.round(((raw + 27) / 83) * 100)));
|
|
100
|
+
const level = pct >= 75 ? '契合度较高' : pct >= 55 ? '中等偏顺' : pct >= 40 ? '需要磨合' : '差异较大';
|
|
101
|
+
|
|
102
|
+
const dim = (key, label, score, text, extra) => ({ key, label, score: Math.round(score), text, ...(extra || {}) });
|
|
103
|
+
|
|
104
|
+
return {
|
|
105
|
+
system: 'hepan',
|
|
106
|
+
a: {
|
|
107
|
+
gender: A.input.gender,
|
|
108
|
+
pillars: ['year', 'month', 'day', 'hour'].map((k) => A.pillars[k].ganzhi),
|
|
109
|
+
dayMaster: `${aStem}${aWx}`,
|
|
110
|
+
dayBranch: aZhi,
|
|
111
|
+
strength: A.strength ? A.strength.level : null,
|
|
112
|
+
favor: favorA.join(''),
|
|
113
|
+
wuxing: wxA,
|
|
114
|
+
// 真太阳时口径要能被看到:合盘与单盘用同一套修正,否则同一个人会出现两套四柱
|
|
115
|
+
longitude: A.input.longitude ?? null,
|
|
116
|
+
trueSolar: A.input.trueSolar !== false,
|
|
117
|
+
solarNote: A.solarTime ? A.solarTime.note : '',
|
|
118
|
+
summary: A.summary,
|
|
119
|
+
},
|
|
120
|
+
b: {
|
|
121
|
+
gender: B.input.gender,
|
|
122
|
+
pillars: ['year', 'month', 'day', 'hour'].map((k) => B.pillars[k].ganzhi),
|
|
123
|
+
dayMaster: `${bStem}${bWx}`,
|
|
124
|
+
dayBranch: bZhi,
|
|
125
|
+
strength: B.strength ? B.strength.level : null,
|
|
126
|
+
favor: favorB.join(''),
|
|
127
|
+
wuxing: wxB,
|
|
128
|
+
longitude: B.input.longitude ?? null,
|
|
129
|
+
trueSolar: B.input.trueSolar !== false,
|
|
130
|
+
solarNote: B.solarTime ? B.solarTime.note : '',
|
|
131
|
+
summary: B.summary,
|
|
132
|
+
},
|
|
133
|
+
score: { total: pct, level },
|
|
134
|
+
dimensions: [
|
|
135
|
+
dim('stem', '日主关系', stemScore, `你的日主 ${aStem}${aWx},对方 ${bStem}${bWx}。${aToB.text};${bToA.text}。`),
|
|
136
|
+
dim('gong', '夫妻宫(日支)', gong.score, `${gong.name}:${gong.text}。你日支 ${aZhi},对方日支 ${bZhi}。`),
|
|
137
|
+
dim('wuxing', '五行互补', compScore, compNotes.join(';') + '。'),
|
|
138
|
+
],
|
|
139
|
+
notes: [
|
|
140
|
+
'合盘看的是相处方式与互补点,不决定关系成败;现实里的沟通与选择分量更重。',
|
|
141
|
+
`口径:日主取五行生克双向平均,夫妻宫取日支关系,五行互补看「一方喜用是否为另一方所旺」。`,
|
|
142
|
+
],
|
|
143
|
+
};
|
|
144
|
+
}
|