dsh-metaphysics 0.17.1 → 0.18.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/CHANGELOG.md +99 -23
- package/README.md +35 -10
- package/client.js +2098 -35
- package/engine/almanac.js +215 -0
- package/engine/bazi.js +10 -2
- package/engine/chenggu.js +126 -0
- package/engine/dst.js +68 -0
- package/engine/fengshui.js +288 -0
- package/engine/geo.js +1 -1
- package/engine/hepan.js +82 -1
- package/engine/index.js +277 -2
- package/engine/name.js +303 -0
- package/engine/numerology.js +60 -0
- package/engine/rectify.js +1 -0
- package/engine/star.js +82 -0
- package/engine/trend.js +193 -0
- package/engine/xiaoliuren.js +64 -0
- package/engine/zeri.js +127 -0
- package/engine/ziwei.js +7 -2
- package/index.js +93 -25
- package/locale/en.json +1 -1
- package/locale/zh.json +1 -1
- package/package.json +15 -3
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
// 风水:八宅命卦 + 大游年八星 + 年家紫白飞星 + 24 山门主灶布局倾向(纯本地确定性)。
|
|
2
|
+
// 口径:大游年歌与 24 山归属为八宅通行口径,吉凶名词照传统转述;不做现场勘舆结论。
|
|
3
|
+
// 参照公开通行表实现(与常见八宅/玄空入门资料一致),年紫白用纯公式,不依赖任何第三方历法库。
|
|
4
|
+
import { partsToJd, termInstantJd } from './calendar.js';
|
|
5
|
+
import { applyDst } from './dst.js';
|
|
6
|
+
|
|
7
|
+
/** 九星名称表:数 → {name, color, element, star(北斗名), lucky}。 */
|
|
8
|
+
export const NINE_STARS = {
|
|
9
|
+
1: { name: '一白', color: '白', element: '水', star: '贪狼', lucky: true },
|
|
10
|
+
2: { name: '二黑', color: '黑', element: '土', star: '巨门', lucky: false },
|
|
11
|
+
3: { name: '三碧', color: '碧', element: '木', star: '禄存', lucky: false },
|
|
12
|
+
4: { name: '四绿', color: '绿', element: '木', star: '文曲', lucky: true },
|
|
13
|
+
5: { name: '五黄', color: '黄', element: '土', star: '廉贞', lucky: false },
|
|
14
|
+
6: { name: '六白', color: '白', element: '金', star: '武曲', lucky: true },
|
|
15
|
+
7: { name: '七赤', color: '赤', element: '金', star: '破军', lucky: false },
|
|
16
|
+
8: { name: '八白', color: '白', element: '土', star: '左辅', lucky: true },
|
|
17
|
+
9: { name: '九紫', color: '紫', element: '火', star: '右弼', lucky: true },
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
/** 后天八卦 → 方位。 */
|
|
21
|
+
export const PALACE_DIRECTION = {
|
|
22
|
+
坎: '正北', 艮: '东北', 震: '正东', 巽: '东南',
|
|
23
|
+
离: '正南', 坤: '西南', 兑: '正西', 乾: '西北',
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
/** 大游年歌:自本卦起,按「坎艮震巽离坤兑乾」循环顺数,数组为自卦后第 1–7 位。 */
|
|
27
|
+
const YOUNIAN_ORDER = ['坎', '艮', '震', '巽', '离', '坤', '兑', '乾'];
|
|
28
|
+
export const DAYOUTIAN = {
|
|
29
|
+
乾: ['六煞', '天医', '五鬼', '祸害', '绝命', '延年', '生气'],
|
|
30
|
+
坎: ['五鬼', '天医', '生气', '延年', '绝命', '祸害', '六煞'],
|
|
31
|
+
艮: ['六煞', '绝命', '祸害', '生气', '延年', '天医', '五鬼'],
|
|
32
|
+
震: ['延年', '生气', '祸害', '绝命', '五鬼', '天医', '六煞'],
|
|
33
|
+
巽: ['天医', '五鬼', '六煞', '祸害', '生气', '绝命', '延年'],
|
|
34
|
+
离: ['六煞', '五鬼', '绝命', '延年', '祸害', '生气', '天医'],
|
|
35
|
+
坤: ['天医', '延年', '绝命', '生气', '祸害', '五鬼', '六煞'],
|
|
36
|
+
兑: ['生气', '祸害', '延年', '绝命', '六煞', '五鬼', '天医'],
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
export const LUCKY_STARS = ['生气', '延年', '天医', '伏位'];
|
|
40
|
+
export const UNLUCKY_STARS = ['绝命', '五鬼', '六煞', '祸害'];
|
|
41
|
+
|
|
42
|
+
/** 24 山(周天 360° 每山 15°,自北顺时针):山 → 后天八卦归属 + 中心度数(子山 = 0°)。 */
|
|
43
|
+
export const MOUNTAINS_24 = [
|
|
44
|
+
{ m: '壬', gua: '坎', deg: 345 }, { m: '子', gua: '坎', deg: 0 }, { m: '癸', gua: '坎', deg: 15 },
|
|
45
|
+
{ m: '丑', gua: '艮', deg: 30 }, { m: '艮', gua: '艮', deg: 45 }, { m: '寅', gua: '艮', deg: 60 },
|
|
46
|
+
{ m: '甲', gua: '震', deg: 75 }, { m: '卯', gua: '震', deg: 90 }, { m: '乙', gua: '震', deg: 105 },
|
|
47
|
+
{ m: '辰', gua: '巽', deg: 120 }, { m: '巽', gua: '巽', deg: 135 }, { m: '巳', gua: '巽', deg: 150 },
|
|
48
|
+
{ m: '丙', gua: '离', deg: 165 }, { m: '午', gua: '离', deg: 180 }, { m: '丁', gua: '离', deg: 195 },
|
|
49
|
+
{ m: '未', gua: '坤', deg: 210 }, { m: '坤', gua: '坤', deg: 225 }, { m: '申', gua: '坤', deg: 240 },
|
|
50
|
+
{ m: '庚', gua: '兑', deg: 255 }, { m: '酉', gua: '兑', deg: 270 }, { m: '辛', gua: '兑', deg: 285 },
|
|
51
|
+
{ m: '戌', gua: '乾', deg: 300 }, { m: '乾', gua: '乾', deg: 315 }, { m: '亥', gua: '乾', deg: 330 },
|
|
52
|
+
];
|
|
53
|
+
|
|
54
|
+
const MOUNTAIN_INDEX = new Map(MOUNTAINS_24.map((x, i) => [x.m, i]));
|
|
55
|
+
|
|
56
|
+
export function mountainInfo(name) {
|
|
57
|
+
const i = MOUNTAIN_INDEX.get(name);
|
|
58
|
+
if (i === undefined) {
|
|
59
|
+
throw new Error(`坐向必须是 24 山之一(${MOUNTAINS_24.map((x) => x.m).join('')}),收到「${name}」——请按罗盘 24 山报坐或报向,不要自造方位词。`);
|
|
60
|
+
}
|
|
61
|
+
return { ...MOUNTAINS_24[i], index: i };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function sittingFromFacing(facing) {
|
|
65
|
+
const f = mountainInfo(facing);
|
|
66
|
+
const i = (f.index + 12) % 24;
|
|
67
|
+
return { ...MOUNTAINS_24[i], index: i };
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const guaGroup = (gua) => (['坎', '离', '震', '巽'].includes(gua) ? '东四' : '西四');
|
|
71
|
+
|
|
72
|
+
/** 八星 → 布局角色(门主灶厕各宜何星方)。 */
|
|
73
|
+
export const STAR_LAYOUT_ROLE = {
|
|
74
|
+
生气: '大吉:最宜开门、主卧、灶口朝向',
|
|
75
|
+
延年: '大吉:宜开门、主卧(利感情与人缘)',
|
|
76
|
+
天医: '次吉:宜主卧、书房、老人房(利健康)',
|
|
77
|
+
伏位: '次吉:宜卧室、神位(主平稳)',
|
|
78
|
+
祸害: '小凶:宜厕所、储物间(宜静不宜动)',
|
|
79
|
+
六煞: '次凶:宜厕所、储物间',
|
|
80
|
+
五鬼: '大凶:宜厕所,或以灶坐压之(传统「火烧五鬼」说)',
|
|
81
|
+
绝命: '最凶:宜灶坐压之、储物,忌门床',
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
const GUA_BY_NUM = ['坎', '坤', '震', '巽', '中', '乾', '兑', '艮', '离'];
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* 八宅命卦:按节气年(立春界)公元年后两位 n:
|
|
88
|
+
* 1900–1999:男 (100−n) ÷ 9 取余,女 (n−4) ÷ 9 取余;
|
|
89
|
+
* 2000–2099:男 (99−n) ÷ 9 取余,女 (n−3) ÷ 9 取余(负数加 9)。
|
|
90
|
+
* 余 0 作 9;余 5 男作坤、女作艮。数→卦:1坎 2坤 3震 4巽 6乾 7兑 8艮 9离。
|
|
91
|
+
*/
|
|
92
|
+
export function mingGua(birthYear, gender) {
|
|
93
|
+
const tail = ((birthYear % 100) + 100) % 100;
|
|
94
|
+
const century = birthYear >= 2000 ? { male: 99, female: 3 } : { male: 100, female: 4 };
|
|
95
|
+
const mod9 = (n) => { const r = ((n % 9) + 9) % 9; return r === 0 ? 9 : r; };
|
|
96
|
+
let num = gender === 'male' ? mod9(century.male - tail) : mod9(tail - century.female);
|
|
97
|
+
if (num === 5) num = gender === 'male' ? 2 : 8;
|
|
98
|
+
const gua = GUA_BY_NUM[num - 1];
|
|
99
|
+
if (gua === '中') throw new Error('命卦计算异常:落中宫');
|
|
100
|
+
return { num, gua, direction: PALACE_DIRECTION[gua] };
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** 大游年八星(含伏位=本卦)。 */
|
|
104
|
+
export function eightStars(gua) {
|
|
105
|
+
const result = { 伏位: { palace: gua, direction: PALACE_DIRECTION[gua] } };
|
|
106
|
+
const start = YOUNIAN_ORDER.indexOf(gua);
|
|
107
|
+
for (let step = 1; step <= 7; step++) {
|
|
108
|
+
const palace = YOUNIAN_ORDER[(start + step) % 8];
|
|
109
|
+
result[DAYOUTIAN[gua][step - 1]] = { palace, direction: PALACE_DIRECTION[palace] };
|
|
110
|
+
}
|
|
111
|
+
return result;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** 立春界节气年:生日在当年立春前 → 上一年(按正午判,与黄历一致)。 */
|
|
115
|
+
export function guaYearOf(year, month, day) {
|
|
116
|
+
const birthJd = partsToJd({ year, month, day, hour: 12, minute: 0 });
|
|
117
|
+
return birthJd < termInstantJd(year, 2) ? year - 1 : year;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* 年紫白入中之星(1–9):锚点 2026 丙午 = 一白,逐年退一位,九年一轮。
|
|
122
|
+
* 对照公开表:2024 三碧、2025 二黑、2026 一白、2027 九紫、2034 二黑、2035 一白。
|
|
123
|
+
*/
|
|
124
|
+
export function yearZibaiStar(year) {
|
|
125
|
+
const n = (1 + (2026 - year)) % 9;
|
|
126
|
+
return n <= 0 ? n + 9 : n;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const PALACE_ORDER = ['坎', '坤', '震', '巽', '中', '乾', '兑', '艮', '离']; // 洛书数 1–9 对应宫
|
|
130
|
+
|
|
131
|
+
/** 九宫飞星:年星入中顺飞。 */
|
|
132
|
+
export function yearlyFlying(year) {
|
|
133
|
+
const center = yearZibaiStar(year);
|
|
134
|
+
const palaces = [];
|
|
135
|
+
for (const palace of PALACE_ORDER) {
|
|
136
|
+
const k = PALACE_ORDER.indexOf(palace) + 1;
|
|
137
|
+
const num = ((((k - 5 + center - 1) % 9) + 9) % 9) + 1;
|
|
138
|
+
const s = NINE_STARS[num];
|
|
139
|
+
palaces.push({ palace, direction: palace === '中' ? '中宫' : PALACE_DIRECTION[palace], num, name: s.name, star: s.star, element: s.element, lucky: s.lucky });
|
|
140
|
+
}
|
|
141
|
+
return { center, centerName: NINE_STARS[center].name, palaces };
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const pad2 = (n) => String(n).padStart(2, '0');
|
|
145
|
+
|
|
146
|
+
/** DST 校正出生日期(1986–1991),返回校正后的年月日。 */
|
|
147
|
+
function civilDate(input) {
|
|
148
|
+
const dst = input.dst === false
|
|
149
|
+
? { year: input.year, month: input.month, day: input.day, hour: 0, minute: 0, applied: false, note: null }
|
|
150
|
+
: applyDst({ year: input.year, month: input.month, day: input.day, hour: 0, minute: 0 });
|
|
151
|
+
return { year: dst.year, month: dst.month, day: dst.day, dst };
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const SOURCES = [
|
|
155
|
+
'大游年歌(八宅通行口径)',
|
|
156
|
+
'年家紫白飞星(纯公式:以 2026 丙午一白入中、逐年退一位、九年一轮;按立春换年)',
|
|
157
|
+
'24 山归属与度数(八宅通行口径,每山 15°)',
|
|
158
|
+
];
|
|
159
|
+
|
|
160
|
+
const DISCLAIMER = '八宅与飞星为传统通行口径的本地计算,属民俗参考,不构成现场勘舆结论,仅供娱乐;实际户型以现场为准。';
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* 风水主函数。
|
|
164
|
+
* @param {{action?:'natal'|'advise', year?:number, month?:number, day?:number, gender?:'male'|'female',
|
|
165
|
+
* fortuneYear?:number, sitting?:string, facing?:string, dst?:boolean}} input
|
|
166
|
+
* natal(默认):要生日 + 性别 + 流年(缺流年则只算命卦与八星)。
|
|
167
|
+
* advise:要坐山或向山(24 山,给一即可);出生信息可选,给了才做命宅配。
|
|
168
|
+
*/
|
|
169
|
+
export function buildFengshui(input) {
|
|
170
|
+
if (input.action === 'advise') return buildAdvise(input);
|
|
171
|
+
const gender = input.gender;
|
|
172
|
+
if (gender !== 'male' && gender !== 'female') {
|
|
173
|
+
throw new Error('风水命卦需要性别:男女命卦算法不同,不可缺省');
|
|
174
|
+
}
|
|
175
|
+
if (!Number.isInteger(input.year) || !Number.isInteger(input.month) || !Number.isInteger(input.day)) {
|
|
176
|
+
throw new Error('风水命卦需要完整出生年月日(公历)——只知道年份不够:命卦按立春界划分出生年');
|
|
177
|
+
}
|
|
178
|
+
const civil = civilDate(input);
|
|
179
|
+
const guaYear = guaYearOf(civil.year, civil.month, civil.day);
|
|
180
|
+
const { num, gua, direction } = mingGua(guaYear, gender);
|
|
181
|
+
const stars = eightStars(gua);
|
|
182
|
+
const eastFour = ['坎', '离', '震', '巽'].includes(gua);
|
|
183
|
+
const fortuneYear = Number.isInteger(input.fortuneYear) ? input.fortuneYear : null;
|
|
184
|
+
|
|
185
|
+
return {
|
|
186
|
+
system: 'fengshui',
|
|
187
|
+
meta: {
|
|
188
|
+
birth: `${civil.year}-${pad2(civil.month)}-${pad2(civil.day)}`,
|
|
189
|
+
guaYear,
|
|
190
|
+
gender: gender === 'male' ? '男' : '女',
|
|
191
|
+
fortuneYear,
|
|
192
|
+
...(civil.dst.applied ? { dst: civil.dst.note } : {}),
|
|
193
|
+
...(fortuneYear === null ? { note: '未给流年:未算年紫白飞星(需要时补 fortuneYear)。' } : {}),
|
|
194
|
+
scope: '八宅命卦 + 大游年八星 + 年紫白;不含玄空飞星各运盘、不含形峦与现场勘舆。',
|
|
195
|
+
},
|
|
196
|
+
mingGua: {
|
|
197
|
+
num,
|
|
198
|
+
gua,
|
|
199
|
+
direction,
|
|
200
|
+
group: eastFour ? '东四命' : '西四命',
|
|
201
|
+
compatible: eastFour ? '宜东四宅(坐坎离震巽)' : '宜西四宅(坐乾坤艮兑)',
|
|
202
|
+
},
|
|
203
|
+
luckyDirections: LUCKY_STARS.map((s) => ({ star: s, ...stars[s] })),
|
|
204
|
+
unluckyDirections: UNLUCKY_STARS.map((s) => ({ star: s, ...stars[s] })),
|
|
205
|
+
...(fortuneYear === null ? {} : { yearlyFlying: yearlyFlying(fortuneYear) }),
|
|
206
|
+
sources: SOURCES,
|
|
207
|
+
disclaimer: DISCLAIMER,
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/** 宅卦坐向 + 门主灶厕布局倾向 + 命宅配。 */
|
|
212
|
+
function buildAdvise(input) {
|
|
213
|
+
const hasSitting = typeof input.sitting === 'string' && input.sitting.trim() !== '';
|
|
214
|
+
const hasFacing = typeof input.facing === 'string' && input.facing.trim() !== '';
|
|
215
|
+
if (!hasSitting && !hasFacing) {
|
|
216
|
+
throw new Error('缺少坐向:sitting(坐山)或 facing(向山)至少给一个——按罗盘 24 山报坐或报向,例如「坐北朝南」对应坐子向午');
|
|
217
|
+
}
|
|
218
|
+
if (hasSitting && hasFacing) {
|
|
219
|
+
const s = mountainInfo(input.sitting.trim());
|
|
220
|
+
const f = mountainInfo(input.facing.trim());
|
|
221
|
+
if ((s.index + 12) % 24 !== f.index) {
|
|
222
|
+
throw new Error(`坐向不自洽:坐${s.m}(${s.deg}°)与向${f.m}(${f.deg}°)不是对宫(相差应 180°,实测 ${((f.deg - s.deg) + 360) % 360}°)——请核对罗盘读数。`);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
const sitting = hasSitting ? mountainInfo(input.sitting.trim()) : sittingFromFacing(input.facing.trim());
|
|
226
|
+
const facing = MOUNTAINS_24[(sitting.index + 12) % 24];
|
|
227
|
+
const houseGua = sitting.gua;
|
|
228
|
+
const stars = eightStars(houseGua);
|
|
229
|
+
const lucky = LUCKY_STARS.map((s) => stars[s]);
|
|
230
|
+
const unlucky = UNLUCKY_STARS.map((s) => stars[s]);
|
|
231
|
+
const dir = (x) => `${x.direction}(${x.palace}宫)`;
|
|
232
|
+
|
|
233
|
+
const starChart = [...LUCKY_STARS, ...UNLUCKY_STARS].map((s) => ({
|
|
234
|
+
star: s,
|
|
235
|
+
lucky: LUCKY_STARS.includes(s),
|
|
236
|
+
...stars[s],
|
|
237
|
+
role: STAR_LAYOUT_ROLE[s],
|
|
238
|
+
}));
|
|
239
|
+
|
|
240
|
+
const layout = {
|
|
241
|
+
door: `门宜开于吉方:首选生气方 ${dir(lucky[0])},次选延年方 ${dir(lucky[1])}、天医方 ${dir(lucky[2])}。`,
|
|
242
|
+
stove: `灶宜「坐凶向吉」:灶身压凶方(首选绝命方 ${dir(unlucky[0])}、五鬼方 ${dir(unlucky[1])}),灶口朝吉方。`,
|
|
243
|
+
bed: `主卧与床头宜安吉方:生气方 ${dir(lucky[0])} 或延年方 ${dir(lucky[1])};床头宜向吉方。`,
|
|
244
|
+
toilet: `厕所宜压凶方:祸害方 ${dir(unlucky[3])} 或六煞方 ${dir(unlucky[2])}(污去凶气,宜静常闭)。`,
|
|
245
|
+
};
|
|
246
|
+
|
|
247
|
+
let mingMatch = null;
|
|
248
|
+
const hasBirth = Number.isInteger(input.year) && Number.isInteger(input.month) && Number.isInteger(input.day)
|
|
249
|
+
&& (input.gender === 'male' || input.gender === 'female');
|
|
250
|
+
if (hasBirth) {
|
|
251
|
+
const civil = civilDate(input);
|
|
252
|
+
const guaYear = guaYearOf(civil.year, civil.month, civil.day);
|
|
253
|
+
const ming = mingGua(guaYear, input.gender);
|
|
254
|
+
const mGroup = guaGroup(ming.gua);
|
|
255
|
+
const hGroup = guaGroup(houseGua);
|
|
256
|
+
const same = mGroup === hGroup;
|
|
257
|
+
const mingLucky = LUCKY_STARS.map((s) => `${s}位${eightStars(ming.gua)[s].direction}`).join('、');
|
|
258
|
+
mingMatch = {
|
|
259
|
+
birth: `${civil.year}-${pad2(civil.month)}-${pad2(civil.day)}`,
|
|
260
|
+
guaYear,
|
|
261
|
+
gender: input.gender === 'male' ? '男' : '女',
|
|
262
|
+
mingGua: { gua: ming.gua, direction: ming.direction, group: `${mGroup}命` },
|
|
263
|
+
houseGua: { gua: houseGua, group: `${hGroup}宅` },
|
|
264
|
+
sameGroup: same,
|
|
265
|
+
text: same
|
|
266
|
+
? `命宅相配(${mGroup}命住${hGroup}宅):以宅之八星盘布局即可,个人吉方与宅吉方大体同向。`
|
|
267
|
+
: `命宅不配(${mGroup}命住${hGroup}宅):传统调停法——门与床以命卦吉方为先(此命四吉方:${mingLucky}),灶仍按宅卦坐凶向吉,两盘各自取用。`,
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
return {
|
|
272
|
+
system: 'fengshui',
|
|
273
|
+
meta: {
|
|
274
|
+
action: 'advise',
|
|
275
|
+
...(hasBirth ? {} : { birthNote: '未提供生辰:未做命宅配;如需按个人命卦调停门床,请补充出生年月日与性别。' }),
|
|
276
|
+
sitting: `${sitting.m}山(罗盘 ${sitting.deg}°)`,
|
|
277
|
+
facing: `${facing.m}山(罗盘 ${facing.deg}°)`,
|
|
278
|
+
houseGua,
|
|
279
|
+
houseGroup: `${guaGroup(houseGua)}宅`,
|
|
280
|
+
scope: '门主灶厕走向按八宅通行口径给倾向,不含形峦、不含玄空飞星各运盘、不是现场勘舆结论。',
|
|
281
|
+
},
|
|
282
|
+
houseStars: starChart,
|
|
283
|
+
layout,
|
|
284
|
+
...(mingMatch ? { mingMatch } : {}),
|
|
285
|
+
sources: SOURCES,
|
|
286
|
+
disclaimer: DISCLAIMER,
|
|
287
|
+
};
|
|
288
|
+
}
|
package/engine/geo.js
CHANGED
|
@@ -143,7 +143,7 @@ const DETAIL = /(区|县|镇|乡|村|小区|街道|路|号|巷|弄|组|队)/;
|
|
|
143
143
|
|
|
144
144
|
/** OpenStreetMap Nominatim:免密钥、支持中文。失败返回 null。
|
|
145
145
|
* UA 按 OSM 使用政策要求写明应用与联系方式(改版本时同步这里)。 */
|
|
146
|
-
const USER_AGENT = 'dsh-metaphysics/0.
|
|
146
|
+
const USER_AGENT = 'dsh-metaphysics/0.18.0 (+https://github.com/xuanfu-web/dsh-metaphysics)';
|
|
147
147
|
|
|
148
148
|
async function onlineLookup(text, { timeoutMs = 2500 } = {}) {
|
|
149
149
|
if (typeof fetch !== 'function') return null;
|
package/engine/hepan.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// 合盘(合婚):日主关系 + 夫妻宫关系 + 五行互补。
|
|
2
2
|
// 口径:日主取五行生克(双向平均),夫妻宫取日支的六合/三合/冲/害/刑/破,
|
|
3
3
|
// 五行互补看「一方喜用是否正是另一方所旺」。各派取法不同,这里只给通用版结论。
|
|
4
|
-
import { buildBazi } from './bazi.js';
|
|
4
|
+
import { buildBazi, tenGod } from './bazi.js';
|
|
5
5
|
import { STEMS, BRANCHES } from './calendar.js';
|
|
6
6
|
import { assertCivil } from './validate.js';
|
|
7
7
|
import { SANHE_GROUPS } from './sanhe.js';
|
|
@@ -46,6 +46,21 @@ function wxToMe(other, me) {
|
|
|
46
46
|
return { name: '克我', score: -12, text: '对方克我,压力感偏重,宜把话说开' };
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
+
/** 年支(属相)关系:六合、三合、六冲、六害、相刑、自刑,其余为无直接关系。 */
|
|
50
|
+
function yearBranchRelation(a, b) {
|
|
51
|
+
if (a === b) return { name: '同属相(自刑之一)', score: -3, text: '属相相同,传统称「自刑」,容易互相较劲、谁也不让谁' };
|
|
52
|
+
if (pairIn(LIU_HE, a, b)) return { name: '六合', score: 3, text: '属相六合,初印象与长辈缘通常不错' };
|
|
53
|
+
if (SANHE_GROUPS.some((g) => g.branches.includes(a) && g.branches.includes(b))) {
|
|
54
|
+
return { name: '三合', score: 3, text: '属相三合,容易有共同话题与共同朋友' };
|
|
55
|
+
}
|
|
56
|
+
if (pairIn(CHONG, a, b)) return { name: '六冲', score: -3, text: '属相六冲,节奏与立场容易相反,重大决定前宜多对一遍' };
|
|
57
|
+
if (pairIn(HAI, a, b)) return { name: '相害', score: -2, text: '属相相害,容易因第三方或误会生嫌隙' };
|
|
58
|
+
for (const g of XING) {
|
|
59
|
+
if (g.includes(a) && g.includes(b)) return { name: '相刑', score: -2, text: '属相相刑,摩擦多起于相处细节' };
|
|
60
|
+
}
|
|
61
|
+
return { name: '无直接关系', score: 0, text: '属相之间没有合冲刑害,表层影响小,以日柱为主' };
|
|
62
|
+
}
|
|
63
|
+
|
|
49
64
|
/** 合盘主函数 */
|
|
50
65
|
export function buildHepan(input) {
|
|
51
66
|
assertCivil(input && input.a, 'a');
|
|
@@ -102,6 +117,71 @@ export function buildHepan(input) {
|
|
|
102
117
|
const pct = Math.max(0, Math.min(100, Math.round(((raw + 27) / 83) * 100)));
|
|
103
118
|
const level = pct >= 75 ? '契合度较高' : pct >= 55 ? '中等偏顺' : pct >= 40 ? '需要磨合' : '差异较大';
|
|
104
119
|
|
|
120
|
+
// ⑤ 细化层(对齐传统合婚口径):属相(年支)关系、天干五合、双向十神、
|
|
121
|
+
// 跨盘地支关系、年命纳音生克、吉凶象计数。只增加明细,不改动上面的总分与三维度。
|
|
122
|
+
const aYear = A.pillars.year;
|
|
123
|
+
const bYear = B.pillars.year;
|
|
124
|
+
const yearRel = yearBranchRelation(aYear.branch, bYear.branch);
|
|
125
|
+
const HE5 = [['甲', '己'], ['乙', '庚'], ['丙', '辛'], ['丁', '壬'], ['戊', '癸']];
|
|
126
|
+
const he5 = HE5.some((p) => p.includes(aStem) && p.includes(bStem) && aStem !== bStem);
|
|
127
|
+
const godA = tenGod(aStem, bStem); // 对方日干在我盘中为
|
|
128
|
+
const godB = tenGod(bStem, aStem);
|
|
129
|
+
const A_BR = ['year', 'month', 'day', 'hour'].map((k) => A.pillars[k].branch);
|
|
130
|
+
const B_BR = ['year', 'month', 'day', 'hour'].map((k) => B.pillars[k].branch);
|
|
131
|
+
const cross = [];
|
|
132
|
+
for (const x of A_BR) {
|
|
133
|
+
for (const y of B_BR) {
|
|
134
|
+
if (pairIn(LIU_HE, x, y)) cross.push({ a: x, b: y, name: '六合', score: 1, text: '跨盘六合,易有自然的亲近与默契' });
|
|
135
|
+
else if (SANHE_GROUPS.some((g) => g.branches.includes(x) && g.branches.includes(y))) {
|
|
136
|
+
cross.push({ a: x, b: y, name: '半合', score: 1, text: '跨盘半合,某些事上容易一拍即合' });
|
|
137
|
+
} else if (pairIn(CHONG, x, y)) cross.push({ a: x, b: y, name: '相冲', score: -1, text: '跨盘相冲,某些领域节奏相反,需要分工' });
|
|
138
|
+
else if (pairIn(HAI, x, y)) cross.push({ a: x, b: y, name: '相害', score: -1, text: '跨盘相害,容易在第三方或琐事上起误会' });
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
const uniqCross = [];
|
|
142
|
+
const seen = new Set();
|
|
143
|
+
for (const c of cross) {
|
|
144
|
+
const k = `${c.name}${[c.a, c.b].sort().join('')}`;
|
|
145
|
+
if (seen.has(k)) continue;
|
|
146
|
+
seen.add(k);
|
|
147
|
+
uniqCross.push(c);
|
|
148
|
+
}
|
|
149
|
+
// 年命纳音:纳音名末字即五行(如「大林木」→ 木)
|
|
150
|
+
const nyA = aYear.nayin;
|
|
151
|
+
const nyB = bYear.nayin;
|
|
152
|
+
const naEl = nyA[nyA.length - 1];
|
|
153
|
+
const nbEl = nyB[nyB.length - 1];
|
|
154
|
+
const nayinRel = naEl === nbEl
|
|
155
|
+
? { name: '纳音比和', score: 0, text: '两人年命纳音同五行,底色相近' }
|
|
156
|
+
: SHENG[naEl] === nbEl
|
|
157
|
+
? { name: '纳音相生', score: 1, text: `你的年命纳音属${naEl}生对方${nbEl},你多是先付出的一方` }
|
|
158
|
+
: SHENG[nbEl] === naEl
|
|
159
|
+
? { name: '纳音相生', score: 1, text: `对方年命纳音属${nbEl}生你${naEl},你容易得到照应` }
|
|
160
|
+
: KE[naEl] === nbEl
|
|
161
|
+
? { name: '纳音相克', score: -1, text: `你的年命纳音属${naEl}克对方${nbEl},主导感偏强` }
|
|
162
|
+
: { name: '纳音相克', score: -1, text: `对方年命纳音属${nbEl}克你${naEl},压力感偏重` };
|
|
163
|
+
const good = (yearRel.score > 0 ? 1 : 0) + (he5 ? 1 : 0) + (gong.score > 5 ? 1 : 0)
|
|
164
|
+
+ uniqCross.filter((c) => c.score > 0).length + (nayinRel.score > 0 ? 1 : 0);
|
|
165
|
+
const bad = (yearRel.score < 0 ? 1 : 0) + (gong.score < 0 ? 1 : 0)
|
|
166
|
+
+ uniqCross.filter((c) => c.score < 0).length + (nayinRel.score < 0 ? 1 : 0);
|
|
167
|
+
const lean = good - bad >= 3 ? '吉象偏多' : good - bad >= 1 ? '吉略多于凶' : good - bad === 0 ? '吉凶相当' : '凶象偏多';
|
|
168
|
+
|
|
169
|
+
const detail = {
|
|
170
|
+
yearRelation: { label: yearRel.name, text: `属相:你${aYear.branch}(${aYear.ganzhi}),对方${bYear.branch}(${bYear.ganzhi})。${yearRel.text}。` },
|
|
171
|
+
stems: {
|
|
172
|
+
he5,
|
|
173
|
+
tenGodToB: godA,
|
|
174
|
+
tenGodToA: godB,
|
|
175
|
+
text: he5
|
|
176
|
+
? `日干五合(${aStem}${bStem}),传统称「天干五合」,主彼此吸引强,也容易黏在一起分不开。`
|
|
177
|
+
: `日干无五合:你在对方盘里是「${godA}」,对方在你盘里是「${godB}」。`,
|
|
178
|
+
},
|
|
179
|
+
crossBranches: uniqCross.slice(0, 8),
|
|
180
|
+
nayin: { a: nyA, b: nyB, label: nayinRel.name, text: nayinRel.text },
|
|
181
|
+
tally: { good, bad, lean },
|
|
182
|
+
framing: '属相(年支)只影响表层印象,传统合婚以日柱为主;分档只说明吉凶象多少,不作「必配/必离」断言。',
|
|
183
|
+
};
|
|
184
|
+
|
|
105
185
|
const dim = (key, label, score, text, extra) => ({ key, label, score: Math.round(score), text, ...(extra || {}) });
|
|
106
186
|
|
|
107
187
|
return {
|
|
@@ -134,6 +214,7 @@ export function buildHepan(input) {
|
|
|
134
214
|
summary: B.summary,
|
|
135
215
|
},
|
|
136
216
|
score: { total: pct, level },
|
|
217
|
+
detail,
|
|
137
218
|
dimensions: [
|
|
138
219
|
dim('stem', '日主关系', stemScore, `你的日主 ${aStem}${aWx},对方 ${bStem}${bWx}。${aToB.text};${bToA.text}。`),
|
|
139
220
|
dim('gong', '夫妻宫(日支)', gong.score, `${gong.name}:${gong.text}。你日支 ${aZhi},对方日支 ${bZhi}。`),
|