dsh-metaphysics 0.59.0 → 0.59.1
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 +97 -5
- package/README.md +15 -0
- package/client.almanac.js +56 -53
- package/client.bazi.js +112 -109
- package/client.chenggu.js +56 -53
- package/client.convert.js +55 -52
- package/client.daliuren.js +119 -116
- package/client.fengshui.js +61 -58
- package/client.hepan.js +110 -107
- package/client.js +322 -290
- package/client.liuyao.js +119 -116
- package/client.mbti.js +57 -54
- package/client.name.js +69 -66
- package/client.numerology.js +64 -61
- package/client.qimen.js +119 -116
- package/client.star.js +57 -54
- package/client.tarot.js +118 -115
- package/client.trend.js +107 -104
- package/client.xiaoliuren.js +65 -62
- package/client.zeri.js +57 -54
- package/client.ziwei.js +112 -109
- package/engine/bazi.js +800 -740
- package/engine/diff.js +3 -0
- package/engine/geju.js +548 -547
- package/engine/index.js +69 -6
- package/engine/interpret.js +4 -1
- package/engine/limits.js +5 -0
- package/engine/plain-summary.js +604 -0
- package/engine/rectify.js +2 -1
- package/engine/trend.js +3 -0
- package/index.js +5 -0
- package/package.json +11 -3
- package//345/221/275/347/220/206/346/216/222/347/233/230.html +2168 -1316
package/engine/bazi.js
CHANGED
|
@@ -1,740 +1,800 @@
|
|
|
1
|
-
// 四柱八字引擎
|
|
2
|
-
import {
|
|
3
|
-
STEMS, BRANCHES, TERM_NAMES,
|
|
4
|
-
ganzhiName, dayGanzhiIndex, jdToParts, partsToJd, sexagenaryIndex,
|
|
5
|
-
termWindow, previousJie, nextJie, equationOfTime, formatJd, xunKongOf,
|
|
6
|
-
} from './calendar.js';
|
|
7
|
-
import { resolveAddress } from './geo.js';
|
|
8
|
-
import { computeBaziShensha } from './shensha.js';
|
|
9
|
-
import { assertCivil, daysInMonth } from './validate.js';
|
|
10
|
-
import { applyDst } from './dst.js';
|
|
11
|
-
import { nayinOf } from './nayin.js';
|
|
12
|
-
import { SANHE } from './sanhe.js';
|
|
13
|
-
import { analyzeBaziFrame, scoreLuck } from './geju.js';
|
|
14
|
-
|
|
15
|
-
// 地支藏干(本气 / 中气 / 余气)
|
|
16
|
-
export const HIDDEN_STEMS = {
|
|
17
|
-
子: ['癸'],
|
|
18
|
-
丑: ['己', '癸', '辛'],
|
|
19
|
-
寅: ['甲', '丙', '戊'],
|
|
20
|
-
卯: ['乙'],
|
|
21
|
-
辰: ['戊', '乙', '癸'],
|
|
22
|
-
巳: ['丙', '戊', '庚'],
|
|
23
|
-
午: ['丁', '己'],
|
|
24
|
-
未: ['己', '丁', '乙'],
|
|
25
|
-
申: ['庚', '壬', '戊'],
|
|
26
|
-
酉: ['辛'],
|
|
27
|
-
戌: ['戊', '辛', '丁'],
|
|
28
|
-
亥: ['壬', '甲'],
|
|
29
|
-
};
|
|
30
|
-
|
|
31
|
-
// 人元司权的 30 日表:余气 → 中气 → 本气,按「节」当日起算。
|
|
32
|
-
// 各家对日数分法不一(另有 7/7/16、9/3/18 等),这里固定通行 30 日表;
|
|
33
|
-
// 只作解释层的一层细节,不参与取格。亥月的余气戊不在本柱藏干表中,两表各照原样。
|
|
34
|
-
export const RENYUAN_TABLE = {
|
|
35
|
-
寅: [['戊', 7], ['丙', 7], ['甲', 16]],
|
|
36
|
-
卯: [['甲', 10], ['乙', 20]],
|
|
37
|
-
辰: [['乙', 9], ['癸', 3], ['戊', 18]],
|
|
38
|
-
巳: [['戊', 5], ['庚', 9], ['丙', 16]],
|
|
39
|
-
午: [['丙', 10], ['己', 9], ['丁', 11]],
|
|
40
|
-
未: [['丁', 9], ['乙', 3], ['己', 18]],
|
|
41
|
-
申: [['戊', 7], ['壬', 3], ['庚', 20]],
|
|
42
|
-
酉: [['庚', 10], ['辛', 20]],
|
|
43
|
-
戌: [['辛', 9], ['丁', 3], ['戊', 18]],
|
|
44
|
-
亥: [['戊', 7], ['甲', 5], ['壬', 18]],
|
|
45
|
-
子: [['壬', 10], ['癸', 20]],
|
|
46
|
-
丑: [['癸', 9], ['辛', 3], ['己', 18]],
|
|
47
|
-
};
|
|
48
|
-
|
|
49
|
-
export const STEM_ELEMENT = {
|
|
50
|
-
甲: '木', 乙: '木', 丙: '火', 丁: '火', 戊: '土',
|
|
51
|
-
己: '土', 庚: '金', 辛: '金', 壬: '水', 癸: '水',
|
|
52
|
-
};
|
|
53
|
-
|
|
54
|
-
export const BRANCH_ELEMENT = {
|
|
55
|
-
子: '水', 丑: '土', 寅: '木', 卯: '木', 辰: '土', 巳: '火',
|
|
56
|
-
午: '火', 未: '土', 申: '金', 酉: '金', 戌: '土', 亥: '水',
|
|
57
|
-
};
|
|
58
|
-
|
|
59
|
-
const GENERATES = { 木: '火', 火: '土', 土: '金', 金: '水', 水: '木' };
|
|
60
|
-
const OVERCOMES = { 木: '土', 土: '水', 水: '火', 火: '金', 金: '木' };
|
|
61
|
-
|
|
62
|
-
const isYangStem = (stem) => STEMS.indexOf(stem) % 2 === 0;
|
|
63
|
-
|
|
64
|
-
// 两套藏干权重是不同用途,不是"同一件事写了两次":
|
|
65
|
-
// · 五行占比——看这一行在盘里有多少,本气 1 / 中气 0.5 / 余气 0.3;
|
|
66
|
-
// · 日主通根——看这一支能不能让日主站住,本气权重更高(1.2 / 0.6 / 0.3)。
|
|
67
|
-
const WUXING_HIDDEN_WEIGHT = [1, 0.5, 0.3];
|
|
68
|
-
const ROOT_HIDDEN_WEIGHT = [1.2, 0.6, 0.3];
|
|
69
|
-
|
|
70
|
-
const STEM_HE = { 甲己: '土', 乙庚: '金', 丙辛: '水', 丁壬: '木', 戊癸: '火' };
|
|
71
|
-
const STEM_CHONG_PAIRS = ['甲庚', '乙辛', '丙壬', '丁癸'];
|
|
72
|
-
const BRANCH_HE = { 子丑: '土', 寅亥: '木', 卯戌: '火', 辰酉: '金', 巳申: '水', 午未: '土' };
|
|
73
|
-
const SANHUI = { 寅卯辰: '木', 巳午未: '火', 申酉戌: '金', 亥子丑: '水' };
|
|
74
|
-
const BRANCH_CHONG = ['子午', '丑未', '寅申', '卯酉', '辰戌', '巳亥'];
|
|
75
|
-
const BRANCH_HAI = ['子未', '丑午', '寅巳', '卯辰', '申亥', '酉戌'];
|
|
76
|
-
const BRANCH_PO = ['子酉', '丑辰', '寅亥', '卯午', '巳申', '未戌'];
|
|
77
|
-
// 三刑:寅巳申(无恩之刑)、丑戌未(恃势之刑)、子卯(无礼之刑)。
|
|
78
|
-
// 另有「自刑」:辰午酉亥四支见同支即自刑(辰辰、午午、酉酉、亥亥),此前漏判。
|
|
79
|
-
const XING_GROUPS = [['寅', '巳', '申'], ['丑', '戌', '未'], ['子', '卯']];
|
|
80
|
-
const XING_SELF = ['辰', '午', '酉', '亥'];
|
|
81
|
-
|
|
82
|
-
const hasPair = (list, a, b) => list.includes(a + b) || list.includes(b + a);
|
|
83
|
-
|
|
84
|
-
// 旺衰用的合会折根:三会成方、三合成局时,参与支按化神折去本气;
|
|
85
|
-
// 六合折半;已并入会/合局的支归该局,六合只折局外那一支(否则「巳午未会火 + 午未合土」会把火局自己折掉,
|
|
86
|
-
// 「亥子丑会水 + 寅亥合木」也会把壬日的亥水比劫折半)。
|
|
87
|
-
// 只参与旺衰量化,不改盘面关系列表;半合/半会不折,避免把「有合意」当成「已成局」。
|
|
88
|
-
function branchRootFactors(pillars, me) {
|
|
89
|
-
const inMe = Object.keys(GENERATES).find((k) => GENERATES[k] === me);
|
|
90
|
-
const branches = ['year', 'month', 'day', 'hour'].map((k) => pillars[k].branch);
|
|
91
|
-
const set = new Set(branches);
|
|
92
|
-
const factor = {};
|
|
93
|
-
for (const b of branches) factor[b] = 1;
|
|
94
|
-
const notes = [];
|
|
95
|
-
const inJu = new Set();
|
|
96
|
-
const apply = (key, el, f, kind) => {
|
|
97
|
-
const hit = [];
|
|
98
|
-
for (const b of [...key]) {
|
|
99
|
-
if ((factor[b] ?? 1) > f) { factor[b] = f; hit.push(b); }
|
|
100
|
-
inJu.add(b);
|
|
101
|
-
}
|
|
102
|
-
if (hit.length && f < 1) notes.push(`${key}${kind}${el},${hit.join('')}根气按${Math.round(f * 10)}成计`);
|
|
103
|
-
};
|
|
104
|
-
for (const [k, el] of Object.entries(SANHUI)) {
|
|
105
|
-
if (![...k].every((c) => set.has(c))) continue;
|
|
106
|
-
apply(k, el, el === me ? 1 : el === inMe ? 0.6 : 0.1, '三会');
|
|
107
|
-
}
|
|
108
|
-
for (const [k, el] of Object.entries(SANHE)) {
|
|
109
|
-
if (![...k].every((c) => set.has(c))) continue;
|
|
110
|
-
apply(k, el, el === me ? 1 : el === inMe ? 0.6 : 0.2, '三合');
|
|
111
|
-
}
|
|
112
|
-
for (let i = 0; i < branches.length; i++) {
|
|
113
|
-
for (let j = i + 1; j < branches.length; j++) {
|
|
114
|
-
const a = branches[i];
|
|
115
|
-
const b = branches[j];
|
|
116
|
-
const el = BRANCH_HE[a + b] || BRANCH_HE[b + a];
|
|
117
|
-
if (!el) continue;
|
|
118
|
-
if (inJu.has(a) && inJu.has(b)) continue;
|
|
119
|
-
const f = el === me ? 1 : el === inMe ? 0.8 : 0.5;
|
|
120
|
-
const hit = [];
|
|
121
|
-
for (const x of [a, b]) {
|
|
122
|
-
if (inJu.has(x)) continue;
|
|
123
|
-
if ((factor[x] ?? 1) > f) { factor[x] = f; hit.push(x); }
|
|
124
|
-
}
|
|
125
|
-
if (hit.length && f < 1) notes.push(`${a}${b}合${el},${hit.join('')}根气按${Math.round(f * 10)}成计`);
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
return { factor, notes, inJu };
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
// 天干生扶是否真有根:折根后同类藏干累计 ≥0.3 才算有根(余气 0.3 折半后不足,作虚透)。
|
|
132
|
-
const ROOT_EFFECTIVE_MIN = 0.3;
|
|
133
|
-
function rootedWeightOf(pillars, el, factor) {
|
|
134
|
-
let w = 0;
|
|
135
|
-
for (const k of ['year', 'month', 'day', 'hour']) {
|
|
136
|
-
const pl = pillars[k];
|
|
137
|
-
const f = factor[pl.branch] ?? 1;
|
|
138
|
-
if (f <= 0.1) continue;
|
|
139
|
-
pl.hidden.forEach((h, i) => {
|
|
140
|
-
if (STEM_ELEMENT[h.stem] === el) w += (ROOT_HIDDEN_WEIGHT[i] ?? 0.1) * f;
|
|
141
|
-
});
|
|
142
|
-
}
|
|
143
|
-
return w;
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
/** 干支关系:五合、相冲、相克;六合、三合半合、三会半会拱会、冲、害、破、刑 */
|
|
147
|
-
export function buildRelations(pillars) {
|
|
148
|
-
const stems =
|
|
149
|
-
const branches =
|
|
150
|
-
|
|
151
|
-
const stemOut = [];
|
|
152
|
-
for (let i = 0; i <
|
|
153
|
-
for (let j = i + 1; j <
|
|
154
|
-
const a = stems[i];
|
|
155
|
-
const b = stems[j];
|
|
156
|
-
if (a === b) continue;
|
|
157
|
-
const he = STEM_HE[a + b] || STEM_HE[b + a];
|
|
158
|
-
// 五合只是「有合」;是否真化要看月令、化神与争合妒合,不在这一层断言
|
|
159
|
-
if (he) { stemOut.push({ text: `${a}${b}合(化${he}待定)`, rank: 0 }); continue; }
|
|
160
|
-
if (hasPair(STEM_CHONG_PAIRS, a, b)) { stemOut.push({ text: `${a}${b}相冲`, rank: 1 }); continue; }
|
|
161
|
-
const ea = STEM_ELEMENT[a];
|
|
162
|
-
const eb = STEM_ELEMENT[b];
|
|
163
|
-
if (OVERCOMES[ea] === eb) stemOut.push({ text: `${a}克${b}`, rank: 3 });
|
|
164
|
-
else if (OVERCOMES[eb] === ea) stemOut.push({ text: `${b}克${a}`, rank: 3 });
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
const branchOut = [];
|
|
169
|
-
const set = new Set(branches);
|
|
170
|
-
const fullSans = [];
|
|
171
|
-
for (const [k, v] of Object.entries(SANHE)) {
|
|
172
|
-
if ([...k].every((c) => set.has(c))) {
|
|
173
|
-
branchOut.push({ text: `${k}三合${v}`, rank: 0 });
|
|
174
|
-
fullSans.push(k);
|
|
175
|
-
}
|
|
176
|
-
}
|
|
177
|
-
const fullHuis = [];
|
|
178
|
-
for (const [k, v] of Object.entries(SANHUI)) {
|
|
179
|
-
if ([...k].every((c) => set.has(c))) {
|
|
180
|
-
branchOut.push({ text: `${k}三会${v}`, rank: 0 });
|
|
181
|
-
fullHuis.push(k);
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
|
-
for (let i = 0; i <
|
|
185
|
-
for (let j = i + 1; j <
|
|
186
|
-
const a = branches[i];
|
|
187
|
-
const b = branches[j];
|
|
188
|
-
if (a === b) {
|
|
189
|
-
// 同支不是"没关系":辰午酉亥两柱同见为自刑
|
|
190
|
-
if (XING_SELF.includes(a)) branchOut.push({ text: `${a}${b}自刑`, rank: 2 });
|
|
191
|
-
continue;
|
|
192
|
-
}
|
|
193
|
-
const he = BRANCH_HE[a + b] || BRANCH_HE[b + a];
|
|
194
|
-
const sh = Object.keys(SANHE).find((k) => k.includes(a) && k.includes(b));
|
|
195
|
-
const hui = Object.keys(SANHUI).find((k) => k.includes(a) && k.includes(b));
|
|
196
|
-
if (he) {
|
|
197
|
-
// 六合优先,不再重复标半合/半会
|
|
198
|
-
branchOut.push({ text: `${a}${b}合${he}`, rank: 0 });
|
|
199
|
-
} else {
|
|
200
|
-
if (sh && !fullSans.includes(sh)) {
|
|
201
|
-
const mid = sh[1];
|
|
202
|
-
const kind = a === mid || b === mid ? '半合' : '拱合';
|
|
203
|
-
branchOut.push({ text: `${a}${b}${kind}${SANHE[sh]}`, rank: 0 });
|
|
204
|
-
}
|
|
205
|
-
if (hui && !fullHuis.includes(hui)) {
|
|
206
|
-
const adj = Math.abs(hui.indexOf(a) - hui.indexOf(b)) === 1;
|
|
207
|
-
branchOut.push({ text: `${a}${b}${adj ? '半会' : '拱会'}${SANHUI[hui]}`, rank: 0 });
|
|
208
|
-
}
|
|
209
|
-
}
|
|
210
|
-
if (hasPair(BRANCH_CHONG, a, b)) branchOut.push({ text: `${a}${b}相冲`, rank: 1 });
|
|
211
|
-
if (hasPair(BRANCH_HAI, a, b)) branchOut.push({ text: `${a}${b}相害`, rank: 2 });
|
|
212
|
-
if (hasPair(BRANCH_PO, a, b)) branchOut.push({ text: `${a}${b}相破`, rank: 2 });
|
|
213
|
-
for (const g of XING_GROUPS) {
|
|
214
|
-
if (g.includes(a) && g.includes(b)) branchOut.push({ text: `${a}${b}相刑`, rank: 2 });
|
|
215
|
-
}
|
|
216
|
-
}
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
const uniq = (arr) => {
|
|
220
|
-
const seen = new Set();
|
|
221
|
-
const out = [];
|
|
222
|
-
for (const x of [...arr].sort((p, q) => p.rank - q.rank)) {
|
|
223
|
-
if (!seen.has(x.text)) { seen.add(x.text); out.push(x.text); }
|
|
224
|
-
}
|
|
225
|
-
return out;
|
|
226
|
-
};
|
|
227
|
-
return { stems: uniq(stemOut), branches: uniq(branchOut) };
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
/** 十神:以日干为我 */
|
|
231
|
-
export function tenGod(dayStem, otherStem) {
|
|
232
|
-
const me = STEM_ELEMENT[dayStem];
|
|
233
|
-
const it = STEM_ELEMENT[otherStem];
|
|
234
|
-
const samePolarity = isYangStem(dayStem) === isYangStem(otherStem);
|
|
235
|
-
if (me === it) return samePolarity ? '比肩' : '劫财';
|
|
236
|
-
if (GENERATES[me] === it) return samePolarity ? '食神' : '伤官';
|
|
237
|
-
if (OVERCOMES[me] === it) return samePolarity ? '偏财' : '正财';
|
|
238
|
-
if (OVERCOMES[it] === me) return samePolarity ? '七杀' : '正官';
|
|
239
|
-
if (GENERATES[it] === me) return samePolarity ? '偏印' : '正印';
|
|
240
|
-
return '?';
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
export function nayin(ganzhiIdx) {
|
|
244
|
-
return nayinOf(ganzhiIdx).name;
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
const JIE_TO_MONTH_BRANCH = { 0: 1, 2: 2, 4: 3, 6: 4, 8: 5, 10: 6, 12: 7, 14: 8, 16: 9, 18: 10, 20: 11, 22: 0 };
|
|
248
|
-
|
|
249
|
-
function pillar(ganzhiIdx, dayStem) {
|
|
250
|
-
const g = ganzhiName(ganzhiIdx);
|
|
251
|
-
const hidden = HIDDEN_STEMS[g.branch];
|
|
252
|
-
return {
|
|
253
|
-
ganzhi: g.name,
|
|
254
|
-
stem: g.stem,
|
|
255
|
-
branch: g.branch,
|
|
256
|
-
stemElement: STEM_ELEMENT[g.stem],
|
|
257
|
-
branchElement: BRANCH_ELEMENT[g.branch],
|
|
258
|
-
stemTenGod: dayStem ? tenGod(dayStem, g.stem) : null,
|
|
259
|
-
hidden: hidden.map((s, i) => ({
|
|
260
|
-
stem: s,
|
|
261
|
-
element: STEM_ELEMENT[s],
|
|
262
|
-
tenGod: dayStem ? tenGod(dayStem, s) : null,
|
|
263
|
-
role: ['本气', '中气', '余气'][i] ?? '余气',
|
|
264
|
-
})),
|
|
265
|
-
nayin: nayin(ganzhiIdx),
|
|
266
|
-
};
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
/**
|
|
270
|
-
* 真太阳时修正
|
|
271
|
-
* @returns {{offsetMinutes:number, jd:number, note:string}}
|
|
272
|
-
*/
|
|
273
|
-
export function applyTrueSolar(jd, longitude) {
|
|
274
|
-
const p = jdToParts(jd);
|
|
275
|
-
if (longitude == null || Number.isNaN(longitude)) {
|
|
276
|
-
return { offsetMinutes: 0, jd, note: '未提供经度,按东八区平太阳时' };
|
|
277
|
-
}
|
|
278
|
-
const longitudeMinutes = (longitude - 120) * 4;
|
|
279
|
-
const eot = equationOfTime(p.year, p.month, p.day);
|
|
280
|
-
const offset = longitudeMinutes + eot;
|
|
281
|
-
return {
|
|
282
|
-
offsetMinutes: Math.round(offset * 10) / 10,
|
|
283
|
-
jd: jd + offset / 1440,
|
|
284
|
-
note: `经度差 ${longitudeMinutes.toFixed(1)} 分 + 均时差 ${eot.toFixed(1)} 分`,
|
|
285
|
-
};
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
/**
|
|
289
|
-
* 人元司权:生在本月第几日、哪一藏干当权。
|
|
290
|
-
* 日数按「节令日」与出生日的日差计,节当日为第 1 日;固定 30 日表,
|
|
291
|
-
* 第 31 日以后不再硬分摊,按本气算并标 beyond。
|
|
292
|
-
* @param {string} monthBranch 月支(寅…丑)
|
|
293
|
-
* @param {number} birthJdn 出生日的儒略日数(含真太阳时修正后的日)
|
|
294
|
-
* @param {number} termJd 本月「节」的儒略日
|
|
295
|
-
*/
|
|
296
|
-
export function renyuanOf(monthBranch, birthJdn, termJd) {
|
|
297
|
-
const table = RENYUAN_TABLE[monthBranch];
|
|
298
|
-
if (!table) return null;
|
|
299
|
-
const segments = [];
|
|
300
|
-
let from = 1;
|
|
301
|
-
for (const [stem, days] of table) {
|
|
302
|
-
segments.push({ stem, element: STEM_ELEMENT[stem], from, to: from + days - 1 });
|
|
303
|
-
from += days;
|
|
304
|
-
}
|
|
305
|
-
const tableDays = from - 1;
|
|
306
|
-
const day = Math.max(1, Math.floor(birthJdn) - Math.floor(termJd + 0.5) + 1);
|
|
307
|
-
const beyond = day > tableDays;
|
|
308
|
-
const idx = beyond ? segments.length - 1 : segments.findIndex((s) => day >= s.from && day <= s.to);
|
|
309
|
-
const seg = segments[idx] || segments[segments.length - 1];
|
|
310
|
-
const next = segments[idx + 1] || null;
|
|
311
|
-
return {
|
|
312
|
-
month: monthBranch,
|
|
313
|
-
day,
|
|
314
|
-
tableDays,
|
|
315
|
-
segments,
|
|
316
|
-
active: seg.stem,
|
|
317
|
-
activeElement: seg.element,
|
|
318
|
-
segment: { from: seg.from, to: seg.to },
|
|
319
|
-
next: next ? { stem: next.stem, element: next.element, from: next.from, inDays: next.from - day } : null,
|
|
320
|
-
// 司权表与本柱藏干表口径不同:亥月的余气戊只出现在司权表里
|
|
321
|
-
inHidden: (HIDDEN_STEMS[monthBranch] || []).includes(seg.stem),
|
|
322
|
-
beyond,
|
|
323
|
-
note: '人元司权按「节」当日起算,节当日为第 1 日,日差用本盘所用时刻(含真太阳时)所在日计;本插件固定 30 日表(余气 → 中气 → 本气),第 31 日以后按本气算。只作解释层的一层细节,不改取格。',
|
|
324
|
-
};
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
/**
|
|
328
|
-
* 排四柱八字
|
|
329
|
-
* @param {object} input { year,month,day,hour,minute, gender:'male'|'female', longitude?, lateZi?:boolean, trueSolar?:boolean }
|
|
330
|
-
*/
|
|
331
|
-
export function buildBazi(input) {
|
|
332
|
-
assertCivil(input);
|
|
333
|
-
let { year, month, day, hour = 0, minute = 0, gender = 'male' } = input;
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
const
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
const
|
|
350
|
-
const
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
const
|
|
354
|
-
const
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
const
|
|
359
|
-
const
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
const
|
|
365
|
-
const
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
const
|
|
369
|
-
|
|
370
|
-
//
|
|
371
|
-
const
|
|
372
|
-
const
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
const
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
const
|
|
379
|
-
const
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
const
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
//
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
//
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
const
|
|
410
|
-
|
|
411
|
-
const
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
const
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
//
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
pl.
|
|
494
|
-
pl.
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
const
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
if (
|
|
572
|
-
|
|
573
|
-
if (
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
}
|
|
594
|
-
for (const
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
}
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
return
|
|
612
|
-
};
|
|
613
|
-
|
|
614
|
-
const
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
const
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
//
|
|
659
|
-
const
|
|
660
|
-
const
|
|
661
|
-
const
|
|
662
|
-
//
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
const
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
const
|
|
670
|
-
const
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
const
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
1
|
+
// 四柱八字引擎
|
|
2
|
+
import {
|
|
3
|
+
STEMS, BRANCHES, TERM_NAMES,
|
|
4
|
+
ganzhiName, dayGanzhiIndex, jdToParts, partsToJd, sexagenaryIndex,
|
|
5
|
+
termWindow, previousJie, nextJie, equationOfTime, formatJd, xunKongOf,
|
|
6
|
+
} from './calendar.js';
|
|
7
|
+
import { resolveAddress } from './geo.js';
|
|
8
|
+
import { computeBaziShensha } from './shensha.js';
|
|
9
|
+
import { assertCivil, daysInMonth } from './validate.js';
|
|
10
|
+
import { applyDst } from './dst.js';
|
|
11
|
+
import { nayinOf } from './nayin.js';
|
|
12
|
+
import { SANHE } from './sanhe.js';
|
|
13
|
+
import { analyzeBaziFrame, scoreLuck } from './geju.js';
|
|
14
|
+
|
|
15
|
+
// 地支藏干(本气 / 中气 / 余气)
|
|
16
|
+
export const HIDDEN_STEMS = {
|
|
17
|
+
子: ['癸'],
|
|
18
|
+
丑: ['己', '癸', '辛'],
|
|
19
|
+
寅: ['甲', '丙', '戊'],
|
|
20
|
+
卯: ['乙'],
|
|
21
|
+
辰: ['戊', '乙', '癸'],
|
|
22
|
+
巳: ['丙', '戊', '庚'],
|
|
23
|
+
午: ['丁', '己'],
|
|
24
|
+
未: ['己', '丁', '乙'],
|
|
25
|
+
申: ['庚', '壬', '戊'],
|
|
26
|
+
酉: ['辛'],
|
|
27
|
+
戌: ['戊', '辛', '丁'],
|
|
28
|
+
亥: ['壬', '甲'],
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
// 人元司权的 30 日表:余气 → 中气 → 本气,按「节」当日起算。
|
|
32
|
+
// 各家对日数分法不一(另有 7/7/16、9/3/18 等),这里固定通行 30 日表;
|
|
33
|
+
// 只作解释层的一层细节,不参与取格。亥月的余气戊不在本柱藏干表中,两表各照原样。
|
|
34
|
+
export const RENYUAN_TABLE = {
|
|
35
|
+
寅: [['戊', 7], ['丙', 7], ['甲', 16]],
|
|
36
|
+
卯: [['甲', 10], ['乙', 20]],
|
|
37
|
+
辰: [['乙', 9], ['癸', 3], ['戊', 18]],
|
|
38
|
+
巳: [['戊', 5], ['庚', 9], ['丙', 16]],
|
|
39
|
+
午: [['丙', 10], ['己', 9], ['丁', 11]],
|
|
40
|
+
未: [['丁', 9], ['乙', 3], ['己', 18]],
|
|
41
|
+
申: [['戊', 7], ['壬', 3], ['庚', 20]],
|
|
42
|
+
酉: [['庚', 10], ['辛', 20]],
|
|
43
|
+
戌: [['辛', 9], ['丁', 3], ['戊', 18]],
|
|
44
|
+
亥: [['戊', 7], ['甲', 5], ['壬', 18]],
|
|
45
|
+
子: [['壬', 10], ['癸', 20]],
|
|
46
|
+
丑: [['癸', 9], ['辛', 3], ['己', 18]],
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
export const STEM_ELEMENT = {
|
|
50
|
+
甲: '木', 乙: '木', 丙: '火', 丁: '火', 戊: '土',
|
|
51
|
+
己: '土', 庚: '金', 辛: '金', 壬: '水', 癸: '水',
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
export const BRANCH_ELEMENT = {
|
|
55
|
+
子: '水', 丑: '土', 寅: '木', 卯: '木', 辰: '土', 巳: '火',
|
|
56
|
+
午: '火', 未: '土', 申: '金', 酉: '金', 戌: '土', 亥: '水',
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
const GENERATES = { 木: '火', 火: '土', 土: '金', 金: '水', 水: '木' };
|
|
60
|
+
const OVERCOMES = { 木: '土', 土: '水', 水: '火', 火: '金', 金: '木' };
|
|
61
|
+
|
|
62
|
+
const isYangStem = (stem) => STEMS.indexOf(stem) % 2 === 0;
|
|
63
|
+
|
|
64
|
+
// 两套藏干权重是不同用途,不是"同一件事写了两次":
|
|
65
|
+
// · 五行占比——看这一行在盘里有多少,本气 1 / 中气 0.5 / 余气 0.3;
|
|
66
|
+
// · 日主通根——看这一支能不能让日主站住,本气权重更高(1.2 / 0.6 / 0.3)。
|
|
67
|
+
const WUXING_HIDDEN_WEIGHT = [1, 0.5, 0.3];
|
|
68
|
+
const ROOT_HIDDEN_WEIGHT = [1.2, 0.6, 0.3];
|
|
69
|
+
|
|
70
|
+
const STEM_HE = { 甲己: '土', 乙庚: '金', 丙辛: '水', 丁壬: '木', 戊癸: '火' };
|
|
71
|
+
const STEM_CHONG_PAIRS = ['甲庚', '乙辛', '丙壬', '丁癸'];
|
|
72
|
+
const BRANCH_HE = { 子丑: '土', 寅亥: '木', 卯戌: '火', 辰酉: '金', 巳申: '水', 午未: '土' };
|
|
73
|
+
const SANHUI = { 寅卯辰: '木', 巳午未: '火', 申酉戌: '金', 亥子丑: '水' };
|
|
74
|
+
const BRANCH_CHONG = ['子午', '丑未', '寅申', '卯酉', '辰戌', '巳亥'];
|
|
75
|
+
const BRANCH_HAI = ['子未', '丑午', '寅巳', '卯辰', '申亥', '酉戌'];
|
|
76
|
+
const BRANCH_PO = ['子酉', '丑辰', '寅亥', '卯午', '巳申', '未戌'];
|
|
77
|
+
// 三刑:寅巳申(无恩之刑)、丑戌未(恃势之刑)、子卯(无礼之刑)。
|
|
78
|
+
// 另有「自刑」:辰午酉亥四支见同支即自刑(辰辰、午午、酉酉、亥亥),此前漏判。
|
|
79
|
+
const XING_GROUPS = [['寅', '巳', '申'], ['丑', '戌', '未'], ['子', '卯']];
|
|
80
|
+
const XING_SELF = ['辰', '午', '酉', '亥'];
|
|
81
|
+
|
|
82
|
+
const hasPair = (list, a, b) => list.includes(a + b) || list.includes(b + a);
|
|
83
|
+
|
|
84
|
+
// 旺衰用的合会折根:三会成方、三合成局时,参与支按化神折去本气;
|
|
85
|
+
// 六合折半;已并入会/合局的支归该局,六合只折局外那一支(否则「巳午未会火 + 午未合土」会把火局自己折掉,
|
|
86
|
+
// 「亥子丑会水 + 寅亥合木」也会把壬日的亥水比劫折半)。
|
|
87
|
+
// 只参与旺衰量化,不改盘面关系列表;半合/半会不折,避免把「有合意」当成「已成局」。
|
|
88
|
+
function branchRootFactors(pillars, me) {
|
|
89
|
+
const inMe = Object.keys(GENERATES).find((k) => GENERATES[k] === me);
|
|
90
|
+
const branches = ['year', 'month', 'day', 'hour'].map((k) => pillars[k].branch);
|
|
91
|
+
const set = new Set(branches);
|
|
92
|
+
const factor = {};
|
|
93
|
+
for (const b of branches) factor[b] = 1;
|
|
94
|
+
const notes = [];
|
|
95
|
+
const inJu = new Set();
|
|
96
|
+
const apply = (key, el, f, kind) => {
|
|
97
|
+
const hit = [];
|
|
98
|
+
for (const b of [...key]) {
|
|
99
|
+
if ((factor[b] ?? 1) > f) { factor[b] = f; hit.push(b); }
|
|
100
|
+
inJu.add(b);
|
|
101
|
+
}
|
|
102
|
+
if (hit.length && f < 1) notes.push(`${key}${kind}${el},${hit.join('')}根气按${Math.round(f * 10)}成计`);
|
|
103
|
+
};
|
|
104
|
+
for (const [k, el] of Object.entries(SANHUI)) {
|
|
105
|
+
if (![...k].every((c) => set.has(c))) continue;
|
|
106
|
+
apply(k, el, el === me ? 1 : el === inMe ? 0.6 : 0.1, '三会');
|
|
107
|
+
}
|
|
108
|
+
for (const [k, el] of Object.entries(SANHE)) {
|
|
109
|
+
if (![...k].every((c) => set.has(c))) continue;
|
|
110
|
+
apply(k, el, el === me ? 1 : el === inMe ? 0.6 : 0.2, '三合');
|
|
111
|
+
}
|
|
112
|
+
for (let i = 0; i < branches.length; i++) {
|
|
113
|
+
for (let j = i + 1; j < branches.length; j++) {
|
|
114
|
+
const a = branches[i];
|
|
115
|
+
const b = branches[j];
|
|
116
|
+
const el = BRANCH_HE[a + b] || BRANCH_HE[b + a];
|
|
117
|
+
if (!el) continue;
|
|
118
|
+
if (inJu.has(a) && inJu.has(b)) continue;
|
|
119
|
+
const f = el === me ? 1 : el === inMe ? 0.8 : 0.5;
|
|
120
|
+
const hit = [];
|
|
121
|
+
for (const x of [a, b]) {
|
|
122
|
+
if (inJu.has(x)) continue;
|
|
123
|
+
if ((factor[x] ?? 1) > f) { factor[x] = f; hit.push(x); }
|
|
124
|
+
}
|
|
125
|
+
if (hit.length && f < 1) notes.push(`${a}${b}合${el},${hit.join('')}根气按${Math.round(f * 10)}成计`);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
return { factor, notes, inJu };
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// 天干生扶是否真有根:折根后同类藏干累计 ≥0.3 才算有根(余气 0.3 折半后不足,作虚透)。
|
|
132
|
+
const ROOT_EFFECTIVE_MIN = 0.3;
|
|
133
|
+
function rootedWeightOf(pillars, el, factor) {
|
|
134
|
+
let w = 0;
|
|
135
|
+
for (const k of ['year', 'month', 'day', 'hour']) {
|
|
136
|
+
const pl = pillars[k];
|
|
137
|
+
const f = factor[pl.branch] ?? 1;
|
|
138
|
+
if (f <= 0.1) continue;
|
|
139
|
+
pl.hidden.forEach((h, i) => {
|
|
140
|
+
if (STEM_ELEMENT[h.stem] === el) w += (ROOT_HIDDEN_WEIGHT[i] ?? 0.1) * f;
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
return w;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/** 干支关系:五合、相冲、相克;六合、三合半合、三会半会拱会、冲、害、破、刑 */
|
|
147
|
+
export function buildRelations(pillars, keys = ['year', 'month', 'day', 'hour']) {
|
|
148
|
+
const stems = keys.map((k) => pillars[k].stem);
|
|
149
|
+
const branches = keys.map((k) => pillars[k].branch);
|
|
150
|
+
|
|
151
|
+
const stemOut = [];
|
|
152
|
+
for (let i = 0; i < stems.length; i++) {
|
|
153
|
+
for (let j = i + 1; j < stems.length; j++) {
|
|
154
|
+
const a = stems[i];
|
|
155
|
+
const b = stems[j];
|
|
156
|
+
if (a === b) continue;
|
|
157
|
+
const he = STEM_HE[a + b] || STEM_HE[b + a];
|
|
158
|
+
// 五合只是「有合」;是否真化要看月令、化神与争合妒合,不在这一层断言
|
|
159
|
+
if (he) { stemOut.push({ text: `${a}${b}合(化${he}待定)`, rank: 0 }); continue; }
|
|
160
|
+
if (hasPair(STEM_CHONG_PAIRS, a, b)) { stemOut.push({ text: `${a}${b}相冲`, rank: 1 }); continue; }
|
|
161
|
+
const ea = STEM_ELEMENT[a];
|
|
162
|
+
const eb = STEM_ELEMENT[b];
|
|
163
|
+
if (OVERCOMES[ea] === eb) stemOut.push({ text: `${a}克${b}`, rank: 3 });
|
|
164
|
+
else if (OVERCOMES[eb] === ea) stemOut.push({ text: `${b}克${a}`, rank: 3 });
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const branchOut = [];
|
|
169
|
+
const set = new Set(branches);
|
|
170
|
+
const fullSans = [];
|
|
171
|
+
for (const [k, v] of Object.entries(SANHE)) {
|
|
172
|
+
if ([...k].every((c) => set.has(c))) {
|
|
173
|
+
branchOut.push({ text: `${k}三合${v}`, rank: 0 });
|
|
174
|
+
fullSans.push(k);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
const fullHuis = [];
|
|
178
|
+
for (const [k, v] of Object.entries(SANHUI)) {
|
|
179
|
+
if ([...k].every((c) => set.has(c))) {
|
|
180
|
+
branchOut.push({ text: `${k}三会${v}`, rank: 0 });
|
|
181
|
+
fullHuis.push(k);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
for (let i = 0; i < branches.length; i++) {
|
|
185
|
+
for (let j = i + 1; j < branches.length; j++) {
|
|
186
|
+
const a = branches[i];
|
|
187
|
+
const b = branches[j];
|
|
188
|
+
if (a === b) {
|
|
189
|
+
// 同支不是"没关系":辰午酉亥两柱同见为自刑
|
|
190
|
+
if (XING_SELF.includes(a)) branchOut.push({ text: `${a}${b}自刑`, rank: 2 });
|
|
191
|
+
continue;
|
|
192
|
+
}
|
|
193
|
+
const he = BRANCH_HE[a + b] || BRANCH_HE[b + a];
|
|
194
|
+
const sh = Object.keys(SANHE).find((k) => k.includes(a) && k.includes(b));
|
|
195
|
+
const hui = Object.keys(SANHUI).find((k) => k.includes(a) && k.includes(b));
|
|
196
|
+
if (he) {
|
|
197
|
+
// 六合优先,不再重复标半合/半会
|
|
198
|
+
branchOut.push({ text: `${a}${b}合${he}`, rank: 0 });
|
|
199
|
+
} else {
|
|
200
|
+
if (sh && !fullSans.includes(sh)) {
|
|
201
|
+
const mid = sh[1];
|
|
202
|
+
const kind = a === mid || b === mid ? '半合' : '拱合';
|
|
203
|
+
branchOut.push({ text: `${a}${b}${kind}${SANHE[sh]}`, rank: 0 });
|
|
204
|
+
}
|
|
205
|
+
if (hui && !fullHuis.includes(hui)) {
|
|
206
|
+
const adj = Math.abs(hui.indexOf(a) - hui.indexOf(b)) === 1;
|
|
207
|
+
branchOut.push({ text: `${a}${b}${adj ? '半会' : '拱会'}${SANHUI[hui]}`, rank: 0 });
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
if (hasPair(BRANCH_CHONG, a, b)) branchOut.push({ text: `${a}${b}相冲`, rank: 1 });
|
|
211
|
+
if (hasPair(BRANCH_HAI, a, b)) branchOut.push({ text: `${a}${b}相害`, rank: 2 });
|
|
212
|
+
if (hasPair(BRANCH_PO, a, b)) branchOut.push({ text: `${a}${b}相破`, rank: 2 });
|
|
213
|
+
for (const g of XING_GROUPS) {
|
|
214
|
+
if (g.includes(a) && g.includes(b)) branchOut.push({ text: `${a}${b}相刑`, rank: 2 });
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const uniq = (arr) => {
|
|
220
|
+
const seen = new Set();
|
|
221
|
+
const out = [];
|
|
222
|
+
for (const x of [...arr].sort((p, q) => p.rank - q.rank)) {
|
|
223
|
+
if (!seen.has(x.text)) { seen.add(x.text); out.push(x.text); }
|
|
224
|
+
}
|
|
225
|
+
return out;
|
|
226
|
+
};
|
|
227
|
+
return { stems: uniq(stemOut), branches: uniq(branchOut) };
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/** 十神:以日干为我 */
|
|
231
|
+
export function tenGod(dayStem, otherStem) {
|
|
232
|
+
const me = STEM_ELEMENT[dayStem];
|
|
233
|
+
const it = STEM_ELEMENT[otherStem];
|
|
234
|
+
const samePolarity = isYangStem(dayStem) === isYangStem(otherStem);
|
|
235
|
+
if (me === it) return samePolarity ? '比肩' : '劫财';
|
|
236
|
+
if (GENERATES[me] === it) return samePolarity ? '食神' : '伤官';
|
|
237
|
+
if (OVERCOMES[me] === it) return samePolarity ? '偏财' : '正财';
|
|
238
|
+
if (OVERCOMES[it] === me) return samePolarity ? '七杀' : '正官';
|
|
239
|
+
if (GENERATES[it] === me) return samePolarity ? '偏印' : '正印';
|
|
240
|
+
return '?';
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
export function nayin(ganzhiIdx) {
|
|
244
|
+
return nayinOf(ganzhiIdx).name;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
const JIE_TO_MONTH_BRANCH = { 0: 1, 2: 2, 4: 3, 6: 4, 8: 5, 10: 6, 12: 7, 14: 8, 16: 9, 18: 10, 20: 11, 22: 0 };
|
|
248
|
+
|
|
249
|
+
function pillar(ganzhiIdx, dayStem) {
|
|
250
|
+
const g = ganzhiName(ganzhiIdx);
|
|
251
|
+
const hidden = HIDDEN_STEMS[g.branch];
|
|
252
|
+
return {
|
|
253
|
+
ganzhi: g.name,
|
|
254
|
+
stem: g.stem,
|
|
255
|
+
branch: g.branch,
|
|
256
|
+
stemElement: STEM_ELEMENT[g.stem],
|
|
257
|
+
branchElement: BRANCH_ELEMENT[g.branch],
|
|
258
|
+
stemTenGod: dayStem ? tenGod(dayStem, g.stem) : null,
|
|
259
|
+
hidden: hidden.map((s, i) => ({
|
|
260
|
+
stem: s,
|
|
261
|
+
element: STEM_ELEMENT[s],
|
|
262
|
+
tenGod: dayStem ? tenGod(dayStem, s) : null,
|
|
263
|
+
role: ['本气', '中气', '余气'][i] ?? '余气',
|
|
264
|
+
})),
|
|
265
|
+
nayin: nayin(ganzhiIdx),
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* 真太阳时修正
|
|
271
|
+
* @returns {{offsetMinutes:number, jd:number, note:string}}
|
|
272
|
+
*/
|
|
273
|
+
export function applyTrueSolar(jd, longitude) {
|
|
274
|
+
const p = jdToParts(jd);
|
|
275
|
+
if (longitude == null || Number.isNaN(longitude)) {
|
|
276
|
+
return { offsetMinutes: 0, jd, note: '未提供经度,按东八区平太阳时' };
|
|
277
|
+
}
|
|
278
|
+
const longitudeMinutes = (longitude - 120) * 4;
|
|
279
|
+
const eot = equationOfTime(p.year, p.month, p.day);
|
|
280
|
+
const offset = longitudeMinutes + eot;
|
|
281
|
+
return {
|
|
282
|
+
offsetMinutes: Math.round(offset * 10) / 10,
|
|
283
|
+
jd: jd + offset / 1440,
|
|
284
|
+
note: `经度差 ${longitudeMinutes.toFixed(1)} 分 + 均时差 ${eot.toFixed(1)} 分`,
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* 人元司权:生在本月第几日、哪一藏干当权。
|
|
290
|
+
* 日数按「节令日」与出生日的日差计,节当日为第 1 日;固定 30 日表,
|
|
291
|
+
* 第 31 日以后不再硬分摊,按本气算并标 beyond。
|
|
292
|
+
* @param {string} monthBranch 月支(寅…丑)
|
|
293
|
+
* @param {number} birthJdn 出生日的儒略日数(含真太阳时修正后的日)
|
|
294
|
+
* @param {number} termJd 本月「节」的儒略日
|
|
295
|
+
*/
|
|
296
|
+
export function renyuanOf(monthBranch, birthJdn, termJd) {
|
|
297
|
+
const table = RENYUAN_TABLE[monthBranch];
|
|
298
|
+
if (!table) return null;
|
|
299
|
+
const segments = [];
|
|
300
|
+
let from = 1;
|
|
301
|
+
for (const [stem, days] of table) {
|
|
302
|
+
segments.push({ stem, element: STEM_ELEMENT[stem], from, to: from + days - 1 });
|
|
303
|
+
from += days;
|
|
304
|
+
}
|
|
305
|
+
const tableDays = from - 1;
|
|
306
|
+
const day = Math.max(1, Math.floor(birthJdn) - Math.floor(termJd + 0.5) + 1);
|
|
307
|
+
const beyond = day > tableDays;
|
|
308
|
+
const idx = beyond ? segments.length - 1 : segments.findIndex((s) => day >= s.from && day <= s.to);
|
|
309
|
+
const seg = segments[idx] || segments[segments.length - 1];
|
|
310
|
+
const next = segments[idx + 1] || null;
|
|
311
|
+
return {
|
|
312
|
+
month: monthBranch,
|
|
313
|
+
day,
|
|
314
|
+
tableDays,
|
|
315
|
+
segments,
|
|
316
|
+
active: seg.stem,
|
|
317
|
+
activeElement: seg.element,
|
|
318
|
+
segment: { from: seg.from, to: seg.to },
|
|
319
|
+
next: next ? { stem: next.stem, element: next.element, from: next.from, inDays: next.from - day } : null,
|
|
320
|
+
// 司权表与本柱藏干表口径不同:亥月的余气戊只出现在司权表里
|
|
321
|
+
inHidden: (HIDDEN_STEMS[monthBranch] || []).includes(seg.stem),
|
|
322
|
+
beyond,
|
|
323
|
+
note: '人元司权按「节」当日起算,节当日为第 1 日,日差用本盘所用时刻(含真太阳时)所在日计;本插件固定 30 日表(余气 → 中气 → 本气),第 31 日以后按本气算。只作解释层的一层细节,不改取格。',
|
|
324
|
+
};
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* 排四柱八字
|
|
329
|
+
* @param {object} input { year,month,day,hour,minute, gender:'male'|'female', longitude?, lateZi?:boolean, trueSolar?:boolean }
|
|
330
|
+
*/
|
|
331
|
+
export function buildBazi(input) {
|
|
332
|
+
assertCivil(input);
|
|
333
|
+
let { year, month, day, hour = 0, minute = 0, gender = 'male' } = input;
|
|
334
|
+
// 时辰未知:内部仍按「正午」跑完整条管线(大运、旺衰都要有个基准才能算),
|
|
335
|
+
// 但对外一律不承认这个钟点——时柱置空、文本不出现具体时刻、精度降为按日。
|
|
336
|
+
const hourUnknown = input.hourUnknown === true;
|
|
337
|
+
if (hourUnknown) { hour = 12; minute = 0; }
|
|
338
|
+
const lateZi = input.lateZi !== false;
|
|
339
|
+
const useTrueSolar = input.trueSolar !== false;
|
|
340
|
+
// 1986–1991 中国夏令时:出生证钟面时间普遍是「拨快 1 小时」的钟面时间,
|
|
341
|
+
// 不校正会让时柱(乃至跨 23:00 的日柱)整档偏;显式传 dst:false 可关闭。
|
|
342
|
+
const dst = input.dst === false ? { applied: false, note: null } : applyDst({ year, month, day, hour, minute });
|
|
343
|
+
if (dst.applied) {
|
|
344
|
+
year = dst.year; month = dst.month; day = dst.day; hour = dst.hour; minute = dst.minute;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
const civilJd = partsToJd({ year, month, day, hour, minute });
|
|
348
|
+
// 地址 -> 经度:手填的 longitude 优先,其次按地址查表
|
|
349
|
+
const geo = input.geo || (input.address ? resolveAddress(input.address) : null);
|
|
350
|
+
const longitude = typeof input.longitude === 'number'
|
|
351
|
+
? input.longitude
|
|
352
|
+
: (geo && typeof geo.lon === 'number' ? geo.lon : undefined);
|
|
353
|
+
const solar = useTrueSolar ? applyTrueSolar(civilJd, longitude) : { offsetMinutes: 0, jd: civilJd, note: '未启用真太阳时' };
|
|
354
|
+
const jd = solar.jd;
|
|
355
|
+
|
|
356
|
+
// 八字只用「节」定月柱与起运(气不参与),传 jieOnly 省掉一半节气求值
|
|
357
|
+
const terms = termWindow(year, { jieOnly: true });
|
|
358
|
+
const prevJieTerm = previousJie(terms, jd);
|
|
359
|
+
const nextJieTerm = nextJie(terms, jd);
|
|
360
|
+
|
|
361
|
+
// 年柱:以立春为界
|
|
362
|
+
const lichun = terms.find((t) => t.index === 2 && t.year === year);
|
|
363
|
+
const solarYear = lichun && jd < lichun.jd ? year - 1 : year;
|
|
364
|
+
const yearIdx = (((solarYear - 4) % 60) + 60) % 60;
|
|
365
|
+
const yearStem = STEMS[yearIdx % 10];
|
|
366
|
+
|
|
367
|
+
// 月柱:以「节」为界
|
|
368
|
+
const monthBranchIdx = JIE_TO_MONTH_BRANCH[prevJieTerm.index];
|
|
369
|
+
const yinMonthStem = ((STEMS.indexOf(yearStem) % 5) * 2 + 2) % 10;
|
|
370
|
+
const monthOffset = ((monthBranchIdx - 2) % 12 + 12) % 12; // 0 = 寅月
|
|
371
|
+
const monthStemIdx = (yinMonthStem + monthOffset) % 10;
|
|
372
|
+
const monthIdx = sexagenaryIndex(monthStemIdx, monthBranchIdx);
|
|
373
|
+
|
|
374
|
+
// 日柱:23:00 后是否换日
|
|
375
|
+
const jdnBase = Math.floor(jd + 0.5);
|
|
376
|
+
const hourOfDay = jdToParts(jd).hour;
|
|
377
|
+
const jdn = lateZi && hourOfDay >= 23 ? jdnBase + 1 : jdnBase;
|
|
378
|
+
const dayIdx = dayGanzhiIndex(jdn);
|
|
379
|
+
const dayStem = STEMS[dayIdx % 10];
|
|
380
|
+
|
|
381
|
+
// 时柱:五鼠遁
|
|
382
|
+
const hourBranchIdx = Math.floor(((hourOfDay + 1) % 24) / 2);
|
|
383
|
+
const ziHourStem = ((STEMS.indexOf(dayStem) % 5) * 2) % 10;
|
|
384
|
+
const hourStemIdx = (ziHourStem + hourBranchIdx) % 10;
|
|
385
|
+
const hourIdx = sexagenaryIndex(hourStemIdx, hourBranchIdx);
|
|
386
|
+
|
|
387
|
+
const pillars = {
|
|
388
|
+
year: pillar(yearIdx, dayStem),
|
|
389
|
+
month: pillar(monthIdx, dayStem),
|
|
390
|
+
day: pillar(dayIdx, dayStem),
|
|
391
|
+
hour: pillar(hourIdx, dayStem),
|
|
392
|
+
};
|
|
393
|
+
|
|
394
|
+
// 大运
|
|
395
|
+
const isYangYear = STEMS.indexOf(yearStem) % 2 === 0;
|
|
396
|
+
const forward = (isYangYear && gender === 'male') || (!isYangYear && gender === 'female');
|
|
397
|
+
const spanDays = forward ? nextJieTerm.jd - jd : jd - prevJieTerm.jd;
|
|
398
|
+
const totalMonths = spanDays * 4; // 3 天 = 1 年 = 12 个月
|
|
399
|
+
let startAgeYears = Math.floor(totalMonths / 12);
|
|
400
|
+
let startAgeMonths = Math.round(totalMonths % 12);
|
|
401
|
+
// 四舍五入到 12 个月时必须进位,否则显示「X 岁 12 个月起运」
|
|
402
|
+
if (startAgeMonths >= 12) { startAgeMonths -= 12; startAgeYears += 1; }
|
|
403
|
+
|
|
404
|
+
// 交运:三日折一年。
|
|
405
|
+
// 古法三条换算是同一比例的三种说法——3 日折 1 年、1 日折 4 个月、1 时辰折 10 日:
|
|
406
|
+
// 把「出生 → 起运那一侧的节令」的真实间隔按 365.2422/3(≈121.7474)倍放大,
|
|
407
|
+
// 就是起运那一刻;放大后的 1/12 恰好 121.7474/12 ≈ 10.15 天,即「一时辰十日」。
|
|
408
|
+
// 一年取回归年 365.2422 日(交运日细算各家不一,这里固定这一个口径并写明)。
|
|
409
|
+
const JIAO_YEAR_DAYS = 365.2422;
|
|
410
|
+
const JIAO_SCALE = JIAO_YEAR_DAYS / 3;
|
|
411
|
+
const jiaoFromTerm = forward ? nextJieTerm : prevJieTerm;
|
|
412
|
+
const jiaoJd = jd + spanDays * JIAO_SCALE;
|
|
413
|
+
const jiaoParts = jdToParts(jiaoJd);
|
|
414
|
+
// 每步大运在「同月同日同时刻」换;公历闰日(2 月 29 日生)退到 2 月 28 日
|
|
415
|
+
const addYearsClamped = (p, n) => {
|
|
416
|
+
const y = p.year + n;
|
|
417
|
+
return { year: y, month: p.month, day: Math.min(p.day, daysInMonth(y, p.month)), hour: p.hour, minute: p.minute };
|
|
418
|
+
};
|
|
419
|
+
|
|
420
|
+
const dayun = [];
|
|
421
|
+
const dayunWindows = [];
|
|
422
|
+
for (let i = 1; i <= 8; i++) {
|
|
423
|
+
const idx = (((monthIdx + (forward ? i : -i)) % 60) + 60) % 60;
|
|
424
|
+
const g = ganzhiName(idx);
|
|
425
|
+
const fromJd = partsToJd(addYearsClamped(jiaoParts, (i - 1) * 10));
|
|
426
|
+
const toJd = partsToJd(addYearsClamped(jiaoParts, i * 10));
|
|
427
|
+
dayun.push({
|
|
428
|
+
step: i,
|
|
429
|
+
startAge: startAgeYears + (i - 1) * 10,
|
|
430
|
+
ganzhi: g.name,
|
|
431
|
+
stem: g.stem,
|
|
432
|
+
branch: g.branch,
|
|
433
|
+
stemTenGod: tenGod(dayStem, g.stem),
|
|
434
|
+
nayin: nayin(idx),
|
|
435
|
+
// 这一步大运的起止交运时刻(与摘要/面板同源,可直接拿来对年表)
|
|
436
|
+
fromDate: formatJd(fromJd),
|
|
437
|
+
toDate: formatJd(toJd),
|
|
438
|
+
});
|
|
439
|
+
dayunWindows.push({ step: i, ganzhi: g.name, fromJd, toJd });
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
// 流年(默认从「今年」起排 10 年;传 input.nowYear 可钉死基准年,用于复现历史结论)
|
|
443
|
+
const thisYear = Number(input.nowYear) || new Date().getFullYear();
|
|
444
|
+
const liunian = [];
|
|
445
|
+
for (let y = thisYear; y < thisYear + 10; y++) {
|
|
446
|
+
const idx = (((y - 4) % 60) + 60) % 60;
|
|
447
|
+
liunian.push({
|
|
448
|
+
year: y,
|
|
449
|
+
age: y - solarYear + 1,
|
|
450
|
+
ganzhi: ganzhiName(idx).name,
|
|
451
|
+
stemTenGod: tenGod(dayStem, ganzhiName(idx).stem),
|
|
452
|
+
});
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
// 五行统计(天干 1.0;地支藏干按 WUXING_HIDDEN_WEIGHT)
|
|
456
|
+
const wuxing = { 木: 0, 火: 0, 土: 0, 金: 0, 水: 0 };
|
|
457
|
+
for (const p of Object.values(pillars)) {
|
|
458
|
+
wuxing[p.stemElement] += 1;
|
|
459
|
+
p.hidden.forEach((h, i) => {
|
|
460
|
+
wuxing[h.element] += WUXING_HIDDEN_WEIGHT[i] ?? 0.1;
|
|
461
|
+
});
|
|
462
|
+
}
|
|
463
|
+
for (const k of Object.keys(wuxing)) wuxing[k] = Math.round(wuxing[k] * 10) / 10;
|
|
464
|
+
|
|
465
|
+
// 旬空(以日柱所在旬)
|
|
466
|
+
const xunStart = Math.floor(dayIdx / 10) * 10;
|
|
467
|
+
const kongwang = [
|
|
468
|
+
BRANCHES[(xunStart + 10) % 12],
|
|
469
|
+
BRANCHES[(xunStart + 11) % 12],
|
|
470
|
+
];
|
|
471
|
+
|
|
472
|
+
// ── 十二长生、各柱空亡、神煞 ──────────────────────────────
|
|
473
|
+
const CHANGSHENG = ['长生', '沐浴', '冠带', '临官', '帝旺', '衰', '病', '死', '墓', '绝', '胎', '养'];
|
|
474
|
+
const CS_START = { 甲: 11, 乙: 6, 丙: 2, 丁: 9, 戊: 2, 己: 9, 庚: 5, 辛: 0, 壬: 8, 癸: 3 };
|
|
475
|
+
const isYang = (s) => STEMS.indexOf(s) % 2 === 0;
|
|
476
|
+
|
|
477
|
+
function changsheng(stem, branch) {
|
|
478
|
+
const start = CS_START[stem];
|
|
479
|
+
const bi = BRANCHES.indexOf(branch);
|
|
480
|
+
const step = isYang(stem) ? bi - start : start - bi;
|
|
481
|
+
return CHANGSHENG[((step % 12) + 12) % 12];
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
// 旬空统一走 calendar.xunKongOf:此前 bazi / liuyao / daliuren 各写一份公式,
|
|
485
|
+
// 三份形式不同(一处用 (start+10)%12、一处用 (10-2k)%12、一处查表),改一处极易漏改另两处。
|
|
486
|
+
const monthBranch = pillars.month.branch;
|
|
487
|
+
|
|
488
|
+
// 神煞:表驱动、双查法(年干/日干、年支/日支、月支、日柱、时柱),详见 engine/shensha.js
|
|
489
|
+
const shensha = computeBaziShensha(pillars, kongwang);
|
|
490
|
+
|
|
491
|
+
for (const key of ['year', 'month', 'day', 'hour']) {
|
|
492
|
+
const pl = pillars[key];
|
|
493
|
+
pl.dishi = changsheng(dayStem, pl.branch);
|
|
494
|
+
pl.zizuo = changsheng(pl.stem, pl.branch);
|
|
495
|
+
pl.kongwang = xunKongOf(pl.ganzhi);
|
|
496
|
+
// shensha 保持字符串数组以兼容既有 UI / 测试;明细见 shenshaDetail
|
|
497
|
+
pl.shensha = shensha.detail[key].map((x) => x.name);
|
|
498
|
+
pl.shenshaDetail = shensha.detail[key];
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
// ── 日主旺衰(月令 + 通根 + 天干帮扶,量化但规则透明)──
|
|
502
|
+
const SEASON_EL = { 寅: '木', 卯: '木', 巳: '火', 午: '火', 申: '金', 酉: '金', 亥: '水', 子: '水', 辰: '土', 戌: '土', 丑: '土', 未: '土' };
|
|
503
|
+
const me = STEM_ELEMENT[dayStem];
|
|
504
|
+
const seasonEl = SEASON_EL[monthBranch];
|
|
505
|
+
let monthW;
|
|
506
|
+
let monthWord;
|
|
507
|
+
if (me === seasonEl) { monthW = 3; monthWord = '旺'; }
|
|
508
|
+
else if (GENERATES[seasonEl] === me) { monthW = 2; monthWord = '相'; }
|
|
509
|
+
else if (GENERATES[me] === seasonEl) { monthW = 1; monthWord = '休'; }
|
|
510
|
+
else if (OVERCOMES[me] === seasonEl) { monthW = 0.5; monthWord = '囚'; }
|
|
511
|
+
else { monthW = 0; monthWord = '死'; }
|
|
512
|
+
|
|
513
|
+
const factorInfo = branchRootFactors(pillars, me);
|
|
514
|
+
let rootScore = 0;
|
|
515
|
+
const rootBranches = [];
|
|
516
|
+
for (const key of ['year', 'month', 'day', 'hour']) {
|
|
517
|
+
const pl = pillars[key];
|
|
518
|
+
const f = factorInfo.factor[pl.branch] ?? 1;
|
|
519
|
+
let hit = false;
|
|
520
|
+
pl.hidden.forEach((h, i) => {
|
|
521
|
+
if (STEM_ELEMENT[h.stem] === me) { rootScore += (ROOT_HIDDEN_WEIGHT[i] ?? 0.1) * f; hit = true; }
|
|
522
|
+
else if (GENERATES[STEM_ELEMENT[h.stem]] === me) { rootScore += (ROOT_HIDDEN_WEIGHT[i] ?? 0.1) * 0.4 * f; hit = true; }
|
|
523
|
+
});
|
|
524
|
+
if (hit) rootBranches.push(pl.branch);
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
let helpScore = 0;
|
|
528
|
+
const helpers = [];
|
|
529
|
+
const drainers = [];
|
|
530
|
+
const virtualHelpers = [];
|
|
531
|
+
for (const key of ['year', 'month', 'hour']) {
|
|
532
|
+
const st = pillars[key].stem;
|
|
533
|
+
const el2 = STEM_ELEMENT[st];
|
|
534
|
+
if (el2 === me || GENERATES[el2] === me) {
|
|
535
|
+
// 天干有根才出得上力;虚透(折根后不足 0.3)只按半力计
|
|
536
|
+
const base = el2 === me ? 1 : 0.8;
|
|
537
|
+
if (rootedWeightOf(pillars, el2, factorInfo.factor) >= ROOT_EFFECTIVE_MIN) helpScore += base;
|
|
538
|
+
else { helpScore += base * 0.5; virtualHelpers.push(st); }
|
|
539
|
+
helpers.push(st);
|
|
540
|
+
} else if (OVERCOMES[el2] === me) { helpScore -= 1; drainers.push(st); }
|
|
541
|
+
else if (OVERCOMES[me] === el2) { helpScore -= 0.8; drainers.push(st); }
|
|
542
|
+
else { helpScore -= 0.5; drainers.push(st); }
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
const total = Math.round((monthW + rootScore + helpScore) * 10) / 10;
|
|
546
|
+
const level = total >= 4 ? '身强' : total >= 2.5 ? '偏强' : total >= 1.5 ? '中和' : total >= 0.5 ? '偏弱' : '身弱';
|
|
547
|
+
const structureText = factorInfo.notes.length ? `;合会折根:${factorInfo.notes.join('、')}` : '';
|
|
548
|
+
const virtualText = virtualHelpers.length ? `;虚透${virtualHelpers.join('')}帮扶减半` : '';
|
|
549
|
+
const strength = {
|
|
550
|
+
level,
|
|
551
|
+
score: total,
|
|
552
|
+
detail: `月令${monthBranch}${seasonEl},日主${me}${monthWord}(${monthW});通根${rootBranches.join('') || '无'}(${rootScore.toFixed(1)});天干帮扶${helpers.join('') || '无'}/耗泄${drainers.join('') || '无'}(${helpScore.toFixed(1)})${structureText}${virtualText}`,
|
|
553
|
+
method: '月令权重 3/2/1/0.5/0 + 地支藏干通根(三会/三合/六合折根)+ 天干帮扶(虚透减半),量化后分档;属简化算法,非唯一流派',
|
|
554
|
+
parts: {
|
|
555
|
+
season: { branch: monthBranch, element: seasonEl, word: monthWord, weight: monthW },
|
|
556
|
+
roots: rootBranches,
|
|
557
|
+
rootScore: Math.round(rootScore * 10) / 10,
|
|
558
|
+
helpers,
|
|
559
|
+
drainers,
|
|
560
|
+
helpScore: Math.round(helpScore * 10) / 10,
|
|
561
|
+
structure: factorInfo.notes,
|
|
562
|
+
virtualHelpers,
|
|
563
|
+
},
|
|
564
|
+
};
|
|
565
|
+
|
|
566
|
+
// ── 格局 / 用神 / 行运 ─────────────────────────────────────
|
|
567
|
+
const frame = analyzeBaziFrame({ pillars, dayStem, monthBranch, strength, wuxing });
|
|
568
|
+
|
|
569
|
+
// ── 大运/流年与原局的地支关系 ──
|
|
570
|
+
const pairRel = (a, b) => {
|
|
571
|
+
if (!a || !b) return [];
|
|
572
|
+
// 大运/流年支与原局同支:辰午酉亥为自刑,其余同支不算关系
|
|
573
|
+
if (a === b) return XING_SELF.includes(a) ? ['自刑'] : [];
|
|
574
|
+
const out = [];
|
|
575
|
+
if (BRANCH_HE[a + b] || BRANCH_HE[b + a]) out.push('合');
|
|
576
|
+
if (hasPair(BRANCH_CHONG, a, b)) out.push('冲');
|
|
577
|
+
if (hasPair(BRANCH_HAI, a, b)) out.push('害');
|
|
578
|
+
if (hasPair(BRANCH_PO, a, b)) out.push('破');
|
|
579
|
+
for (const g of XING_GROUPS) if (g.includes(a) && g.includes(b)) out.push('刑');
|
|
580
|
+
return out;
|
|
581
|
+
};
|
|
582
|
+
const natalBranches = ['year', 'month', 'day', 'hour'].map((k) => pillars[k].branch);
|
|
583
|
+
const relOf = (branch) => {
|
|
584
|
+
const seen = new Set();
|
|
585
|
+
const out = [];
|
|
586
|
+
for (const b of natalBranches) {
|
|
587
|
+
for (const r of pairRel(b, branch)) {
|
|
588
|
+
const t = r + b;
|
|
589
|
+
if (!seen.has(t)) { seen.add(t); out.push(t); }
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
return out;
|
|
593
|
+
};
|
|
594
|
+
for (const d of dayun) {
|
|
595
|
+
d.relations = relOf(d.branch);
|
|
596
|
+
d.luck = scoreLuck(d.ganzhi, frame);
|
|
597
|
+
}
|
|
598
|
+
for (const l of liunian) {
|
|
599
|
+
l.relations = relOf(l.ganzhi[1]);
|
|
600
|
+
l.luck = scoreLuck(l.ganzhi, frame);
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
// ── 岁运并临 / 伏吟反吟:只标干支层面的「变动信号」,不判吉凶 ──
|
|
604
|
+
// 流年按立春起算(该年立春到次年立春),当值大运取与这一区间相交的步;
|
|
605
|
+
// 交运落在这一年里时,前后两步都在本年当过值,两条都列出。
|
|
606
|
+
const NATAL_LABEL = { year: '年柱', month: '月柱', day: '日柱', hour: '时柱' };
|
|
607
|
+
const stemOvercomes = (a, b) => OVERCOMES[STEM_ELEMENT[a]] === STEM_ELEMENT[b];
|
|
608
|
+
const tianKeDiChong = (a, b) => {
|
|
609
|
+
if (typeof a !== 'string' || typeof b !== 'string' || a.length < 2 || b.length < 2) return false;
|
|
610
|
+
if (!(a[0] in STEM_ELEMENT) || !(b[0] in STEM_ELEMENT)) return false;
|
|
611
|
+
return (stemOvercomes(a[0], b[0]) || stemOvercomes(b[0], a[0])) && hasPair(BRANCH_CHONG, a[1], b[1]);
|
|
612
|
+
};
|
|
613
|
+
const lichunJd = (y) => {
|
|
614
|
+
const t = termWindow(y, { jieOnly: true }).find((x) => x.index === 2 && jdToParts(x.jd).year === y);
|
|
615
|
+
return t ? t.jd : partsToJd({ year: y, month: 2, day: 4 });
|
|
616
|
+
};
|
|
617
|
+
for (const l of liunian) {
|
|
618
|
+
const yStart = lichunJd(l.year);
|
|
619
|
+
const yEnd = lichunJd(l.year + 1);
|
|
620
|
+
const active = dayunWindows.filter((w) => w.fromJd < yEnd && w.toJd > yStart);
|
|
621
|
+
const signals = [];
|
|
622
|
+
for (const w of active) {
|
|
623
|
+
if (w.ganzhi === l.ganzhi) {
|
|
624
|
+
signals.push({ type: '岁运并临', target: `第${w.step}步大运`, detail: `大运与流年同为${l.ganzhi}` });
|
|
625
|
+
} else if (tianKeDiChong(w.ganzhi, l.ganzhi)) {
|
|
626
|
+
signals.push({ type: '运年反吟', target: `第${w.step}步大运`, detail: `大运${w.ganzhi}与流年${l.ganzhi}天克地冲` });
|
|
627
|
+
}
|
|
628
|
+
// 大运与本命四柱的伏吟/反吟:整步大运都在,逐年列出,不按流年另算一次
|
|
629
|
+
for (const key of ['year', 'month', 'day', 'hour']) {
|
|
630
|
+
const g = pillars[key].ganzhi;
|
|
631
|
+
if (!g) continue;
|
|
632
|
+
if (w.ganzhi === g) {
|
|
633
|
+
signals.push({ type: '大运伏吟', target: `第${w.step}步大运·${NATAL_LABEL[key]}`, detail: `大运${w.ganzhi}与${NATAL_LABEL[key]}同干支` });
|
|
634
|
+
} else if (tianKeDiChong(w.ganzhi, g)) {
|
|
635
|
+
signals.push({ type: '大运反吟', target: `第${w.step}步大运·${NATAL_LABEL[key]}`, detail: `大运${w.ganzhi}与${NATAL_LABEL[key]}${g}天克地冲` });
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
for (const key of ['year', 'month', 'day', 'hour']) {
|
|
640
|
+
const g = pillars[key].ganzhi;
|
|
641
|
+
if (!g) continue;
|
|
642
|
+
if (l.ganzhi === g) {
|
|
643
|
+
signals.push({ type: '流年伏吟', target: NATAL_LABEL[key], detail: `流年${l.ganzhi}与${NATAL_LABEL[key]}同干支` });
|
|
644
|
+
} else if (tianKeDiChong(l.ganzhi, g)) {
|
|
645
|
+
signals.push({ type: '流年反吟', target: NATAL_LABEL[key], detail: `流年${l.ganzhi}与${NATAL_LABEL[key]}${g}天克地冲` });
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
l.suiyun = {
|
|
649
|
+
dayun: active.map((w) => ({ step: w.step, ganzhi: w.ganzhi })),
|
|
650
|
+
signals,
|
|
651
|
+
note: '只标干支重复与天克地冲这类变动信号,不判吉凶;大运按该年立春到次年立春的区间取,交运落在年内时前后两步并列;大运与本命四柱的伏吟/反吟整步大运都在,逐年重复列出',
|
|
652
|
+
};
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
const p = jdToParts(jd);
|
|
656
|
+
|
|
657
|
+
// ── 胎元 / 胎息 / 命宫 / 身宫 ──
|
|
658
|
+
// 胎元:月柱天干进一位、地支进三位(受胎之月的干支)
|
|
659
|
+
const mStemIdx = STEMS.indexOf(pillars.month.stem);
|
|
660
|
+
const mBranchIdx = BRANCHES.indexOf(pillars.month.branch);
|
|
661
|
+
const taiyuan = ganzhiName(sexagenaryIndex((mStemIdx + 1) % 10, (mBranchIdx + 3) % 12));
|
|
662
|
+
// 胎息:日柱天干五合、地支六合(六合两支序数之和恒为 1 mod 12)
|
|
663
|
+
const dayStemIdx = STEMS.indexOf(pillars.day.stem);
|
|
664
|
+
const dayBranchIdx = BRANCHES.indexOf(pillars.day.branch);
|
|
665
|
+
const taixi = ganzhiName(sexagenaryIndex((dayStemIdx + 5) % 10, (1 - dayBranchIdx + 12) % 12));
|
|
666
|
+
// 命宫 / 身宫:以生月、生时两支的合数定宫(Tyme 口径)。
|
|
667
|
+
// 命宫:27 − 月支 − 时支,取模后 +2;身宫:11 + 月支 + 时支,取模后 +2。
|
|
668
|
+
// 两支合数落 0–11 后并入六十甲子,天干随年干自然对齐,无需另推五虎遁。
|
|
669
|
+
const hourBrIdxForGong = BRANCHES.indexOf(pillars.hour.branch);
|
|
670
|
+
const yearStemIdxForGong = STEMS.indexOf(pillars.year.stem);
|
|
671
|
+
const mingIdx = (((yearStemIdxForGong * 12 + ((27 - mBranchIdx - hourBrIdxForGong) % 12) + 2) % 60) + 60) % 60;
|
|
672
|
+
const bodyIdx = (((yearStemIdxForGong * 12 + ((11 + mBranchIdx + hourBrIdxForGong) % 12) + 2) % 60) + 60) % 60;
|
|
673
|
+
const minggong = ganzhiName(mingIdx);
|
|
674
|
+
const shengong = ganzhiName(bodyIdx);
|
|
675
|
+
|
|
676
|
+
// 人元司权:按节令到出生日的日差定当权藏干(节当日为第 1 日)
|
|
677
|
+
const renyuan = renyuanOf(BRANCHES[monthBranchIdx], jdnBase, prevJieTerm.jd);
|
|
678
|
+
if (renyuan) renyuan.term = { name: prevJieTerm.name, time: formatJd(prevJieTerm.jd) };
|
|
679
|
+
|
|
680
|
+
const out = {
|
|
681
|
+
system: 'bazi',
|
|
682
|
+
// 时柱未知时不参与干支关系(否则「时干合日干」这类结论就建立在编造的钟点上)
|
|
683
|
+
relations: buildRelations(pillars, hourUnknown ? ['year', 'month', 'day'] : undefined),
|
|
684
|
+
palaces: {
|
|
685
|
+
taiyuan: { ganzhi: taiyuan.name, note: '月干进一、月支进三' },
|
|
686
|
+
taixi: { ganzhi: taixi.name, note: '日干五合、日支六合' },
|
|
687
|
+
minggong: { ganzhi: minggong.name, branch: minggong.branch, note: '生月与生时支合数定宫(27 减,取模后 +2);干随六十甲子' },
|
|
688
|
+
shengong: { ganzhi: shengong.name, branch: shengong.branch, note: '生月与生时支合数定宫(11 加,取模后 +2);干随六十甲子' },
|
|
689
|
+
},
|
|
690
|
+
input: {
|
|
691
|
+
civil: { year, month, day, hour, minute },
|
|
692
|
+
gender,
|
|
693
|
+
longitude: longitude ?? null,
|
|
694
|
+
address: input.address || null,
|
|
695
|
+
geo,
|
|
696
|
+
lateZi,
|
|
697
|
+
trueSolar: useTrueSolar,
|
|
698
|
+
dst: { applied: !!dst.applied, note: dst.note || null },
|
|
699
|
+
},
|
|
700
|
+
solarTime: {
|
|
701
|
+
civilText: `${year}-${String(month).padStart(2, '0')}-${String(day).padStart(2, '0')} ${String(hour).padStart(2, '0')}:${String(minute).padStart(2, '0')}`,
|
|
702
|
+
trueSolarText: formatJd(jd),
|
|
703
|
+
offsetMinutes: solar.offsetMinutes,
|
|
704
|
+
note: dst.applied ? `${dst.note};${solar.note}` : solar.note,
|
|
705
|
+
lunarYearForPillar: solarYear,
|
|
706
|
+
},
|
|
707
|
+
jieqi: {
|
|
708
|
+
previous: { name: prevJieTerm.name, time: formatJd(prevJieTerm.jd) },
|
|
709
|
+
next: { name: nextJieTerm.name, time: formatJd(nextJieTerm.jd) },
|
|
710
|
+
// 距「节」的分钟数(取两侧较小值)。是否提示复核由摘要层决定,阈值 20 分钟:
|
|
711
|
+
// 与 NAOJ 历要项对拍实测节气最多偏 13.5 分钟,20 分钟留出余量。
|
|
712
|
+
nearBoundaryMinutes: Math.round(Math.min(jd - prevJieTerm.jd, nextJieTerm.jd - jd) * 1440),
|
|
713
|
+
},
|
|
714
|
+
renyuan,
|
|
715
|
+
pillars,
|
|
716
|
+
dayMaster: { stem: dayStem, element: STEM_ELEMENT[dayStem], yinYang: isYangStem(dayStem) ? '阳' : '阴' },
|
|
717
|
+
wuxing,
|
|
718
|
+
kongwang,
|
|
719
|
+
shensha,
|
|
720
|
+
dayun: {
|
|
721
|
+
direction: forward ? '顺行' : '逆行',
|
|
722
|
+
rule: `${isYangYear ? '阳' : '阴'}年${gender === 'male' ? '男' : '女'}${forward ? '顺排' : '逆排'}`,
|
|
723
|
+
startAge: { years: startAgeYears, months: startAgeMonths },
|
|
724
|
+
toNextJieDays: Math.round(spanDays * 100) / 100,
|
|
725
|
+
// 交运时刻(日/时辰级):上方 startAge 是四舍五入到月的旧字段,两者同源、不互相覆盖
|
|
726
|
+
jiao: {
|
|
727
|
+
at: formatJd(jiaoJd),
|
|
728
|
+
jd: Math.round(jiaoJd * 1e6) / 1e6,
|
|
729
|
+
scaleDaysPerDay: JIAO_SCALE,
|
|
730
|
+
yearDays: JIAO_YEAR_DAYS,
|
|
731
|
+
spanDays: Math.round(spanDays * 1e4) / 1e4,
|
|
732
|
+
from: { name: jiaoFromTerm.name, time: formatJd(jiaoFromTerm.jd) },
|
|
733
|
+
frame: solar.offsetMinutes ? '真太阳时' : '平太阳时',
|
|
734
|
+
offsetMinutes: solar.offsetMinutes,
|
|
735
|
+
note: '三日折一年:出生到节令的真实间隔按 365.2422/3 放大后加到出生时刻;同一比例即「一日折四月、一时辰折十日」,每步大运在同月同日同时刻换',
|
|
736
|
+
},
|
|
737
|
+
list: dayun,
|
|
738
|
+
},
|
|
739
|
+
strength,
|
|
740
|
+
frame,
|
|
741
|
+
climate: frame.yongshen.climate || frame.yongshen.tiaohou || null,
|
|
742
|
+
liunian,
|
|
743
|
+
liunianFrom: thisYear,
|
|
744
|
+
};
|
|
745
|
+
|
|
746
|
+
// ── 时辰未知:把「不该被当成已知」的东西全部收回 ──────────
|
|
747
|
+
// 内部按正午跑完整条管线,是为了让大运/旺衰有个基准;但对外必须守住三件事:
|
|
748
|
+
// 时柱不给、具体钟点不给、精度不冒充。任何一处漏了,用户就会把它当成算准的盘。
|
|
749
|
+
if (hourUnknown) {
|
|
750
|
+
const hourPillar = out.pillars.hour;
|
|
751
|
+
out.hourUnknown = true;
|
|
752
|
+
// 内部语义标记,不是钟点;正文里只说「正午」
|
|
753
|
+
out.provisionalHour = 'noon';
|
|
754
|
+
// 时柱整体不排:键保留为 null(不删键),下游 for...in 不会崩,但读不到值
|
|
755
|
+
out.pillars.hour = null;
|
|
756
|
+
// 命宫 / 身宫由「生月 + 生时支」定,时支没了就不能给
|
|
757
|
+
out.palaces.minggong = null;
|
|
758
|
+
out.palaces.shengong = null;
|
|
759
|
+
// 输入回显也不留钟点:否则等于把占位值还给了用户
|
|
760
|
+
out.input.civil.hour = null;
|
|
761
|
+
out.input.civil.minute = null;
|
|
762
|
+
const dateOnly = `${out.input.civil.year}-${String(out.input.civil.month).padStart(2, '0')}-${String(out.input.civil.day).padStart(2, '0')}`;
|
|
763
|
+
out.solarTime.civilText = `${dateOnly}(时辰未知)`;
|
|
764
|
+
out.solarTime.trueSolarText = `${dateOnly}(时辰未知)`;
|
|
765
|
+
// 大运:起运与交运都是「出生时刻 + 放大间隔」推出来的,没有出生时刻就只能到日
|
|
766
|
+
out.dayun.precision = 'day';
|
|
767
|
+
out.dayun.jiao = null;
|
|
768
|
+
out.dayun.note = '时柱暂缺:起运与交运按日估算,仅供参考';
|
|
769
|
+
out.dayun.list = out.dayun.list.map((d) => ({
|
|
770
|
+
...d,
|
|
771
|
+
fromDate: d.fromDate ? String(d.fromDate).slice(0, 10) : d.fromDate,
|
|
772
|
+
toDate: d.toDate ? String(d.toDate).slice(0, 10) : d.toDate,
|
|
773
|
+
}));
|
|
774
|
+
// 五行不含时柱重算:否则等于把一个编出来的钟点算进了旺衰
|
|
775
|
+
if (hourPillar) {
|
|
776
|
+
const w = { ...out.wuxing };
|
|
777
|
+
w[hourPillar.stemElement] -= 1;
|
|
778
|
+
hourPillar.hidden.forEach((h, i) => { w[h.element] -= (WUXING_HIDDEN_WEIGHT[i] ?? 0.1); });
|
|
779
|
+
for (const k of Object.keys(w)) w[k] = Math.round(Math.max(0, w[k]) * 10) / 10;
|
|
780
|
+
out.wuxing = w;
|
|
781
|
+
}
|
|
782
|
+
// 神煞:时柱那一列清空;只落在时柱上的条目整条去掉(不能留个空壳说「时柱有X」)
|
|
783
|
+
if (out.shensha && out.shensha.detail) {
|
|
784
|
+
out.shensha.detail.hour = [];
|
|
785
|
+
if (Array.isArray(out.shensha.groups)) {
|
|
786
|
+
out.shensha.groups = out.shensha.groups
|
|
787
|
+
.map((g) => ({ ...g, items: (g.items || []).filter((i) => (i.pillars || []).some((k) => k !== 'hour')) }))
|
|
788
|
+
.filter((g) => g.items.length);
|
|
789
|
+
out.shensha.count = out.shensha.groups.reduce((n, g) => n + g.items.length, 0);
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
// 旺衰与格局:算法没变,但基准是正午——标出来,别让人以为是定时辰算的
|
|
793
|
+
out.strength.precision = 'three-pillars';
|
|
794
|
+
out.notes = [
|
|
795
|
+
'时柱暂缺:未提供出生时刻,时柱不参与任何判定',
|
|
796
|
+
'旺衰、格局、用神按年/月/日三柱估算,补上时柱后可能改变',
|
|
797
|
+
];
|
|
798
|
+
}
|
|
799
|
+
return out;
|
|
800
|
+
}
|