dsh-metaphysics 0.56.0 → 0.57.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 +1115 -1102
- package/README.md +8 -3
- package/client.almanac.js +62 -58
- package/client.bazi.js +102 -96
- package/client.chenggu.js +65 -61
- package/client.convert.js +64 -60
- package/client.daliuren.js +103 -97
- package/client.fengshui.js +64 -60
- package/client.hepan.js +103 -97
- package/client.js +302 -253
- package/client.liuyao.js +102 -96
- package/client.mbti.js +63 -59
- package/client.name.js +64 -60
- package/client.numerology.js +65 -61
- package/client.qimen.js +103 -97
- package/client.star.js +62 -58
- package/client.tarot.js +99 -95
- package/client.trend.js +100 -94
- package/client.xiaoliuren.js +63 -59
- package/client.zeri.js +63 -59
- package/client.ziwei.js +102 -96
- package/engine/citations.js +549 -320
- package/engine/diff.js +5 -0
- package/engine/fengshui.js +718 -718
- package/engine/geju.js +16 -0
- package/engine/handle.js +192 -0
- package/engine/index.js +57 -8
- package/engine/interpret-plain.js +4 -1
- package/engine/interpret.js +7 -3
- package/engine/qimen-ganying.js +134 -134
- package/engine/qimen-interpret.js +233 -233
- package/engine/revise.js +162 -0
- package/engine/trace.js +493 -456
- package/index.js +41 -4
- package/package.json +115 -113
- package//345/221/275/347/220/206/346/216/222/347/233/230.html +2701 -1873
package/engine/fengshui.js
CHANGED
|
@@ -1,718 +1,718 @@
|
|
|
1
|
-
// 风水:八宅命卦 + 大游年八星 + 年家紫白飞星 + 24 山门主灶布局倾向(纯本地确定性)。
|
|
2
|
-
// 口径:大游年歌与 24 山归属为八宅通行口径,吉凶名词照传统转述;不做现场勘舆结论。
|
|
3
|
-
// 参照公开通行表实现(与常见八宅/玄空入门资料一致),年紫白用纯公式,不依赖任何第三方历法库。
|
|
4
|
-
import {
|
|
5
|
-
partsToJd, termInstantJd, toJDN, dayGanzhiIndex, ganzhiName, termWindow, previousJie, BRANCHES,
|
|
6
|
-
} from './calendar.js';
|
|
7
|
-
import { applyDst } from './dst.js';
|
|
8
|
-
import { fail } from './fail.js';
|
|
9
|
-
|
|
10
|
-
/** 九星名称表:数 → {name, color, element, star(北斗名), lucky}。 */
|
|
11
|
-
export const NINE_STARS = {
|
|
12
|
-
1: { name: '一白', color: '白', element: '水', star: '贪狼', lucky: true },
|
|
13
|
-
2: { name: '二黑', color: '黑', element: '土', star: '巨门', lucky: false },
|
|
14
|
-
3: { name: '三碧', color: '碧', element: '木', star: '禄存', lucky: false },
|
|
15
|
-
4: { name: '四绿', color: '绿', element: '木', star: '文曲', lucky: true },
|
|
16
|
-
5: { name: '五黄', color: '黄', element: '土', star: '廉贞', lucky: false },
|
|
17
|
-
6: { name: '六白', color: '白', element: '金', star: '武曲', lucky: true },
|
|
18
|
-
7: { name: '七赤', color: '赤', element: '金', star: '破军', lucky: false },
|
|
19
|
-
8: { name: '八白', color: '白', element: '土', star: '左辅', lucky: true },
|
|
20
|
-
9: { name: '九紫', color: '紫', element: '火', star: '右弼', lucky: true },
|
|
21
|
-
};
|
|
22
|
-
|
|
23
|
-
/** 后天八卦 → 方位。 */
|
|
24
|
-
export const PALACE_DIRECTION = {
|
|
25
|
-
坎: '正北', 艮: '东北', 震: '正东', 巽: '东南',
|
|
26
|
-
离: '正南', 坤: '西南', 兑: '正西', 乾: '西北',
|
|
27
|
-
};
|
|
28
|
-
|
|
29
|
-
/** 大游年歌:自本卦起,按「坎艮震巽离坤兑乾」循环顺数,数组为自卦后第 1–7 位。 */
|
|
30
|
-
const YOUNIAN_ORDER = ['坎', '艮', '震', '巽', '离', '坤', '兑', '乾'];
|
|
31
|
-
export const DAYOUTIAN = {
|
|
32
|
-
乾: ['六煞', '天医', '五鬼', '祸害', '绝命', '延年', '生气'],
|
|
33
|
-
坎: ['五鬼', '天医', '生气', '延年', '绝命', '祸害', '六煞'],
|
|
34
|
-
艮: ['六煞', '绝命', '祸害', '生气', '延年', '天医', '五鬼'],
|
|
35
|
-
震: ['延年', '生气', '祸害', '绝命', '五鬼', '天医', '六煞'],
|
|
36
|
-
巽: ['天医', '五鬼', '六煞', '祸害', '生气', '绝命', '延年'],
|
|
37
|
-
离: ['六煞', '五鬼', '绝命', '延年', '祸害', '生气', '天医'],
|
|
38
|
-
坤: ['天医', '延年', '绝命', '生气', '祸害', '五鬼', '六煞'],
|
|
39
|
-
兑: ['生气', '祸害', '延年', '绝命', '六煞', '五鬼', '天医'],
|
|
40
|
-
};
|
|
41
|
-
|
|
42
|
-
export const LUCKY_STARS = ['生气', '延年', '天医', '伏位'];
|
|
43
|
-
export const UNLUCKY_STARS = ['绝命', '五鬼', '六煞', '祸害'];
|
|
44
|
-
|
|
45
|
-
/** 24 山(周天 360° 每山 15°,自北顺时针):山 → 后天八卦归属 + 中心度数(子山 = 0°)。 */
|
|
46
|
-
export const MOUNTAINS_24 = [
|
|
47
|
-
{ m: '壬', gua: '坎', deg: 345 }, { m: '子', gua: '坎', deg: 0 }, { m: '癸', gua: '坎', deg: 15 },
|
|
48
|
-
{ m: '丑', gua: '艮', deg: 30 }, { m: '艮', gua: '艮', deg: 45 }, { m: '寅', gua: '艮', deg: 60 },
|
|
49
|
-
{ m: '甲', gua: '震', deg: 75 }, { m: '卯', gua: '震', deg: 90 }, { m: '乙', gua: '震', deg: 105 },
|
|
50
|
-
{ m: '辰', gua: '巽', deg: 120 }, { m: '巽', gua: '巽', deg: 135 }, { m: '巳', gua: '巽', deg: 150 },
|
|
51
|
-
{ m: '丙', gua: '离', deg: 165 }, { m: '午', gua: '离', deg: 180 }, { m: '丁', gua: '离', deg: 195 },
|
|
52
|
-
{ m: '未', gua: '坤', deg: 210 }, { m: '坤', gua: '坤', deg: 225 }, { m: '申', gua: '坤', deg: 240 },
|
|
53
|
-
{ m: '庚', gua: '兑', deg: 255 }, { m: '酉', gua: '兑', deg: 270 }, { m: '辛', gua: '兑', deg: 285 },
|
|
54
|
-
{ m: '戌', gua: '乾', deg: 300 }, { m: '乾', gua: '乾', deg: 315 }, { m: '亥', gua: '乾', deg: 330 },
|
|
55
|
-
];
|
|
56
|
-
|
|
57
|
-
const MOUNTAIN_INDEX = new Map(MOUNTAINS_24.map((x, i) => [x.m, i]));
|
|
58
|
-
|
|
59
|
-
export function mountainInfo(name) {
|
|
60
|
-
const i = MOUNTAIN_INDEX.get(name);
|
|
61
|
-
if (i === undefined) {
|
|
62
|
-
throw fail('E_INPUT', `坐向必须是 24 山之一(${MOUNTAINS_24.map((x) => x.m).join('')}),收到「${name}」——请按罗盘 24 山报坐或报向,不要自造方位词。`);
|
|
63
|
-
}
|
|
64
|
-
return { ...MOUNTAINS_24[i], index: i };
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
export function sittingFromFacing(facing) {
|
|
68
|
-
const f = mountainInfo(facing);
|
|
69
|
-
const i = (f.index + 12) % 24;
|
|
70
|
-
return { ...MOUNTAINS_24[i], index: i };
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
const guaGroup = (gua) => (['坎', '离', '震', '巽'].includes(gua) ? '东四' : '西四');
|
|
74
|
-
|
|
75
|
-
/** 八星 → 布局角色(门主灶厕各宜何星方)。 */
|
|
76
|
-
export const STAR_LAYOUT_ROLE = {
|
|
77
|
-
生气: '大吉:最宜开门、主卧、灶口朝向',
|
|
78
|
-
延年: '大吉:宜开门、主卧(利感情与人缘)',
|
|
79
|
-
天医: '次吉:宜主卧、书房、老人房(利健康)',
|
|
80
|
-
伏位: '次吉:宜卧室、神位(主平稳)',
|
|
81
|
-
祸害: '小凶:宜厕所、储物间(宜静不宜动)',
|
|
82
|
-
六煞: '次凶:宜厕所、储物间',
|
|
83
|
-
五鬼: '大凶:宜厕所,或以灶坐压之(传统「火烧五鬼」说)',
|
|
84
|
-
绝命: '最凶:宜灶坐压之、储物,忌门床',
|
|
85
|
-
};
|
|
86
|
-
|
|
87
|
-
const GUA_BY_NUM = ['坎', '坤', '震', '巽', '中', '乾', '兑', '艮', '离'];
|
|
88
|
-
|
|
89
|
-
/**
|
|
90
|
-
* 八宅命卦:按节气年(立春界)公元年后两位 n:
|
|
91
|
-
* 1900–1999:男 (100−n) ÷ 9 取余,女 (n−4) ÷ 9 取余;
|
|
92
|
-
* 2000–2099:男 (99−n) ÷ 9 取余,女 (n−3) ÷ 9 取余(负数加 9)。
|
|
93
|
-
* 余 0 作 9;余 5 男作坤、女作艮。数→卦:1坎 2坤 3震 4巽 6乾 7兑 8艮 9离。
|
|
94
|
-
*/
|
|
95
|
-
export function mingGua(birthYear, gender) {
|
|
96
|
-
const tail = ((birthYear % 100) + 100) % 100;
|
|
97
|
-
const century = birthYear >= 2000 ? { male: 99, female: 3 } : { male: 100, female: 4 };
|
|
98
|
-
const mod9 = (n) => { const r = ((n % 9) + 9) % 9; return r === 0 ? 9 : r; };
|
|
99
|
-
let num = gender === 'male' ? mod9(century.male - tail) : mod9(tail - century.female);
|
|
100
|
-
if (num === 5) num = gender === 'male' ? 2 : 8;
|
|
101
|
-
const gua = GUA_BY_NUM[num - 1];
|
|
102
|
-
if (gua === '中') throw new Error('命卦计算异常:落中宫');
|
|
103
|
-
return { num, gua, direction: PALACE_DIRECTION[gua] };
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
/** 大游年八星(含伏位=本卦)。 */
|
|
107
|
-
export function eightStars(gua) {
|
|
108
|
-
const result = { 伏位: { palace: gua, direction: PALACE_DIRECTION[gua] } };
|
|
109
|
-
const start = YOUNIAN_ORDER.indexOf(gua);
|
|
110
|
-
for (let step = 1; step <= 7; step++) {
|
|
111
|
-
const palace = YOUNIAN_ORDER[(start + step) % 8];
|
|
112
|
-
result[DAYOUTIAN[gua][step - 1]] = { palace, direction: PALACE_DIRECTION[palace] };
|
|
113
|
-
}
|
|
114
|
-
return result;
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
/** 立春界节气年:生日在当年立春前 → 上一年(按正午判,与黄历一致)。 */
|
|
118
|
-
export function guaYearOf(year, month, day) {
|
|
119
|
-
const birthJd = partsToJd({ year, month, day, hour: 12, minute: 0 });
|
|
120
|
-
return birthJd < termInstantJd(year, 2) ? year - 1 : year;
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
/**
|
|
124
|
-
* 年紫白入中之星(1–9):锚点 2026 丙午 = 一白,逐年退一位,九年一轮。
|
|
125
|
-
* 对照公开表:2024 三碧、2025 二黑、2026 一白、2027 九紫、2034 二黑、2035 一白。
|
|
126
|
-
*/
|
|
127
|
-
export function yearZibaiStar(year) {
|
|
128
|
-
const n = (1 + (2026 - year)) % 9;
|
|
129
|
-
return n <= 0 ? n + 9 : n;
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
const PALACE_ORDER = ['坎', '坤', '震', '巽', '中', '乾', '兑', '艮', '离']; // 洛书数 1–9 对应宫
|
|
133
|
-
|
|
134
|
-
const norm9 = (n) => {
|
|
135
|
-
const r = ((n % 9) + 9) % 9;
|
|
136
|
-
return r === 0 ? 9 : r;
|
|
137
|
-
};
|
|
138
|
-
|
|
139
|
-
/** 紫白九宫:入中星按洛书轨迹飞布。forward=true 顺飞,false 逆飞。 */
|
|
140
|
-
function zibaiChart(center, forward = true) {
|
|
141
|
-
const palaces = [];
|
|
142
|
-
for (const palace of PALACE_ORDER) {
|
|
143
|
-
const k = PALACE_ORDER.indexOf(palace) + 1;
|
|
144
|
-
const num = norm9(k - 5 + center);
|
|
145
|
-
const s = NINE_STARS[num];
|
|
146
|
-
palaces.push({ palace, direction: palace === '中' ? '中宫' : PALACE_DIRECTION[palace], num, name: s.name, star: s.star, element: s.element, lucky: s.lucky });
|
|
147
|
-
}
|
|
148
|
-
return { center, centerName: NINE_STARS[center].name, forward, fly: forward ? '顺飞' : '逆飞', palaces };
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
/** 九宫飞星:年星入中顺飞。 */
|
|
152
|
-
export function yearlyFlying(year) {
|
|
153
|
-
return zibaiChart(yearZibaiStar(year), true);
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
// ── 年、月、日、时紫白(公式派) ─────────────────────────────────
|
|
157
|
-
// 年:年星逐年退一位,入中顺飞(与 yearZibaiStar 同一锚点)。
|
|
158
|
-
// 月:子午卯酉年正月八白、辰戌丑未年正月五黄、寅申巳亥年正月二黑;逐月退一位,入中顺飞。
|
|
159
|
-
// 日:冬至–夏至阳遁、夏至–冬至阴遁;三段(冬至/雨水/谷雨、夏至/处暑/霜降)分别起 1/7/4 与 9/3/6。
|
|
160
|
-
// 时:冬至后四正日/四墓日/四生日子时起 1/7/4;夏至后起 9/3/6;阳遁时星顺行顺飞、阴遁时星逆行逆飞。
|
|
161
|
-
// 口径说明:月按节气月(寅月为正月);日按公历日、23:00 后归次日;各家另有「日星一律顺飞」「月按农历月」等说法,见 note。
|
|
162
|
-
|
|
163
|
-
const JIE_MONTH_NUM = {
|
|
164
|
-
立春: 1, 惊蛰: 2, 清明: 3, 立夏: 4, 芒种: 5, 小暑: 6,
|
|
165
|
-
立秋: 7, 白露: 8, 寒露: 9, 立冬: 10, 大雪: 11, 小寒: 12,
|
|
166
|
-
};
|
|
167
|
-
const MONTH_START_BY_GROUP = { 四正: 8, 四墓: 5, 四生: 2 };
|
|
168
|
-
const HOUR_START_WINTER = { 四正: 1, 四墓: 7, 四生: 4 };
|
|
169
|
-
const HOUR_START_SUMMER = { 四正: 9, 四墓: 3, 四生: 6 };
|
|
170
|
-
|
|
171
|
-
const branchGroupOf = (branch) => (
|
|
172
|
-
['子', '午', '卯', '酉'].includes(branch) ? '四正'
|
|
173
|
-
: ['辰', '戌', '丑', '未'].includes(branch) ? '四墓' : '四生'
|
|
174
|
-
);
|
|
175
|
-
|
|
176
|
-
const yearBranchOf = (year) => ganzhiName((((year - 4) % 60) + 60) % 60).branch;
|
|
177
|
-
|
|
178
|
-
function latestTerm(terms, name, jd) {
|
|
179
|
-
let found = null;
|
|
180
|
-
for (const t of terms) {
|
|
181
|
-
if (t.jd > jd) break;
|
|
182
|
-
if (t.name === name) found = t;
|
|
183
|
-
}
|
|
184
|
-
return found;
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
/** 冬至/夏至分阴阳遁,再按雨水/谷雨或处暑/霜降分上中下元。 */
|
|
188
|
-
function zibaiHalfAt(terms, jd) {
|
|
189
|
-
const dz = latestTerm(terms, '冬至', jd);
|
|
190
|
-
const xz = latestTerm(terms, '夏至', jd);
|
|
191
|
-
const yang = Boolean(dz) && (!xz || dz.jd > xz.jd);
|
|
192
|
-
if (yang) {
|
|
193
|
-
const gy = latestTerm(terms, '谷雨', jd);
|
|
194
|
-
const ys = latestTerm(terms, '雨水', jd);
|
|
195
|
-
if (gy && dz && gy.jd > dz.jd) return { half: '阳遁', segment: '谷雨–夏至(下元)', offset: 3, boundary: '冬至后阳遁,谷雨至上元转下元' };
|
|
196
|
-
if (ys && dz && ys.jd > dz.jd) return { half: '阳遁', segment: '雨水–谷雨(中元)', offset: 6, boundary: '冬至后阳遁,雨水至上元转中元' };
|
|
197
|
-
return { half: '阳遁', segment: '冬至–雨水(上元)', offset: 0, boundary: '冬至后阳遁上元' };
|
|
198
|
-
}
|
|
199
|
-
const sj = latestTerm(terms, '霜降', jd);
|
|
200
|
-
const cs = latestTerm(terms, '处暑', jd);
|
|
201
|
-
if (sj && xz && sj.jd > xz.jd) return { half: '阴遁', segment: '霜降–冬至(下元)', offset: 3, boundary: '夏至后阴遁,霜降至上元转下元' };
|
|
202
|
-
if (cs && xz && cs.jd > xz.jd) return { half: '阴遁', segment: '处暑–霜降(中元)', offset: 6, boundary: '夏至后阴遁,处暑至上元转中元' };
|
|
203
|
-
return { half: '阴遁', segment: '夏至–处暑(上元)', offset: 0, boundary: '夏至后阴遁上元' };
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
/**
|
|
207
|
-
* 年月日时紫白飞星。
|
|
208
|
-
* @param {{year:number,month:number,day:number,hour?:number,minute?:number}} at 公历日期与时刻
|
|
209
|
-
*/
|
|
210
|
-
export function timeFlyingOf(at) {
|
|
211
|
-
const year = Number(at.year); const month = Number(at.month); const day = Number(at.day);
|
|
212
|
-
const hour = Number.isInteger(at.hour) ? at.hour : 12;
|
|
213
|
-
const minute = Number.isInteger(at.minute) ? at.minute : 0;
|
|
214
|
-
const jd = partsToJd({ year, month, day, hour, minute });
|
|
215
|
-
const terms = termWindow(year);
|
|
216
|
-
const guaYear = guaYearOf(year, month, day);
|
|
217
|
-
const yearBranch = yearBranchOf(guaYear);
|
|
218
|
-
const yearGroup = branchGroupOf(yearBranch);
|
|
219
|
-
const prevJie = previousJie(terms, jd);
|
|
220
|
-
const monthNum = JIE_MONTH_NUM[prevJie ? prevJie.name : '立春'] || 1;
|
|
221
|
-
const half = zibaiHalfAt(terms, jd);
|
|
222
|
-
const jdn = toJDN(year, month, day) + (hour >= 23 ? 1 : 0);
|
|
223
|
-
const dayIdx = dayGanzhiIndex(jdn);
|
|
224
|
-
const dayName = ganzhiName(dayIdx).name;
|
|
225
|
-
const dayBranch = ganzhiName(dayIdx).branch;
|
|
226
|
-
const dayGroup = branchGroupOf(dayBranch);
|
|
227
|
-
const hourIdx = Math.floor(((hour + 1) % 24) / 2);
|
|
228
|
-
|
|
229
|
-
const yearCenter = yearZibaiStar(guaYear);
|
|
230
|
-
const monthCenter = norm9(MONTH_START_BY_GROUP[yearGroup] - (monthNum - 1));
|
|
231
|
-
const dayBase = norm9(dayIdx + 1 + half.offset);
|
|
232
|
-
const dayCenter = half.half === '阳遁' ? dayBase : norm9(10 - dayBase);
|
|
233
|
-
const hourStart = half.half === '阳遁' ? HOUR_START_WINTER[dayGroup] : HOUR_START_SUMMER[dayGroup];
|
|
234
|
-
const hourCenter = norm9(hourStart + (half.half === '阳遁' ? hourIdx : -hourIdx));
|
|
235
|
-
|
|
236
|
-
return {
|
|
237
|
-
year: { ...zibaiChart(yearCenter, true), basis: `${guaYear} 年(立春界)年星 ${NINE_STARS[yearCenter].name} 入中顺飞` },
|
|
238
|
-
month: { ...zibaiChart(monthCenter, true), basis: `${yearBranch}年(${yearGroup})${prevJie ? prevJie.name : '立春'}月,${monthNum} 月 ${NINE_STARS[monthCenter].name} 入中顺飞` },
|
|
239
|
-
day: { ...zibaiChart(dayCenter, half.half === '阳遁'), basis: `${half.half}·${half.segment},${dayName}日第 ${dayIdx + 1} 位,${NINE_STARS[dayCenter].name} 入中${half.half === '阳遁' ? '顺' : '逆'}飞` },
|
|
240
|
-
hour: { ...zibaiChart(hourCenter, half.half === '阳遁'), basis: `${half.half}·${half.segment},${dayGroup}日 ${BRANCHES[hourIdx]}时,${NINE_STARS[hourCenter].name} 入中${half.half === '阳遁' ? '顺' : '逆'}飞` },
|
|
241
|
-
meta: {
|
|
242
|
-
guaYear, yearBranch, yearGroup, monthNum, monthJie: prevJie ? prevJie.name : '立春',
|
|
243
|
-
dayGanzhi: dayName, daySeq: dayIdx + 1, dayGroup,
|
|
244
|
-
hourBranch: BRANCHES[hourIdx], half: half.half, segment: half.segment, boundary: half.boundary,
|
|
245
|
-
note: '月按节气月(寅月为正月);日按公历日、23:00 后归次日;日/时星入中后的顺逆取「冬至顺、夏至逆」。另有「一律顺飞」与「月按农历月」派,本插件不合并,差异见本注。',
|
|
246
|
-
},
|
|
247
|
-
};
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
const pad2 = (n) => String(n).padStart(2, '0');
|
|
251
|
-
|
|
252
|
-
// ── 玄空飞星(下卦) ─────────────────────────────────────────────
|
|
253
|
-
// 口径:三元九运每运 20 年,1864 年起一运;运盘当运星入中顺飞。
|
|
254
|
-
// 山盘/向盘:取坐宫/向宫的运星入中,按「入中运星本宫同元龙」的阴阳定顺逆(五黄入中按本山阴阳)。
|
|
255
|
-
// 这一步是玄空挨星的关键:不是按坐山本身的阴阳,而是按入中运星原宫对应元龙的阴阳;
|
|
256
|
-
// 按本山阴阳会把旺山旺向算成每运固定的六局,与通行二十四山旺山旺向表对不上。
|
|
257
|
-
|
|
258
|
-
/** 三元九运:1864 起每 20 年一运,九运一轮(180 年)。 */
|
|
259
|
-
export function periodOfYear(year) {
|
|
260
|
-
const k = Math.floor((year - 1864) / 20);
|
|
261
|
-
const period = ((k % 9) + 9) % 9 + 1;
|
|
262
|
-
const start = 1864 + 20 * k;
|
|
263
|
-
return { period, start, end: start + 19, label: `${period}运(${start}–${start + 19})` };
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
/** 三元龙阴阳:天元/人元四隅为阳、四正为阴;地元四正为阳、四隅为阴。 */
|
|
267
|
-
const YANG_MOUNTAINS = new Set(['乾', '巽', '艮', '坤', '甲', '庚', '壬', '丙', '寅', '申', '巳', '亥']);
|
|
268
|
-
const YUAN_NAME = ['地元', '天元', '人元'];
|
|
269
|
-
|
|
270
|
-
/** 24 山 → 三元龙 + 阴阳(MOUNTAINS_24 每卦三山依次为 地元/天元/人元)。 */
|
|
271
|
-
export const MOUNTAIN_META = {};
|
|
272
|
-
MOUNTAINS_24.forEach((x, i) => {
|
|
273
|
-
MOUNTAIN_META[x.m] = { ...x, yuan: YUAN_NAME[i % 3], yinYang: YANG_MOUNTAINS.has(x.m) ? '阳' : '阴' };
|
|
274
|
-
});
|
|
275
|
-
|
|
276
|
-
/** 飞星轨迹:中 → 乾 → 兑 → 艮 → 离 → 坎 → 坤 → 震 → 巽(即洛书 5→6→7→8→9→1→2→3→4)。 */
|
|
277
|
-
const FLY_SEQ = ['中', '乾', '兑', '艮', '离', '坎', '坤', '震', '巽'];
|
|
278
|
-
const NUM_TO_GUA = ['坎', '坤', '震', '巽', '中', '乾', '兑', '艮', '离']; // 洛书数 1–9 本位卦
|
|
279
|
-
const YUAN_INDEX = { 地元: 0, 天元: 1, 人元: 2 };
|
|
280
|
-
|
|
281
|
-
const mountainsOfGua = (gua) => MOUNTAINS_24.filter((x) => x.gua === gua).map((x) => MOUNTAIN_META[x.m]);
|
|
282
|
-
|
|
283
|
-
/** 入中星按轨迹飞布:forward=true 顺飞,false 逆飞。返回 宫 → 星数。 */
|
|
284
|
-
function fly(center, forward) {
|
|
285
|
-
const out = {};
|
|
286
|
-
for (let i = 0; i < 9; i++) {
|
|
287
|
-
const v = ((center - 1 + (forward ? i : -i)) % 9 + 9) % 9 + 1;
|
|
288
|
-
out[FLY_SEQ[i]] = v;
|
|
289
|
-
}
|
|
290
|
-
return out;
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
/** 入中运星定顺逆:取本宫(运星本位卦)同元龙之山的阴阳;五黄入中按本山阴阳。 */
|
|
294
|
-
function flightOf(mount, center) {
|
|
295
|
-
if (center === 5) {
|
|
296
|
-
return { forward: mount.yinYang === '阳', basis: `五黄入中,按${mount.m}本山${mount.yuan}${mount.yinYang}论` };
|
|
297
|
-
}
|
|
298
|
-
const home = NUM_TO_GUA[center - 1];
|
|
299
|
-
const hm = mountainsOfGua(home)[YUAN_INDEX[mount.yuan]];
|
|
300
|
-
return { forward: hm.yinYang === '阳', basis: `${center}${home}宫取${mount.yuan}${hm.m}(${hm.yinYang})` };
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
// ── 兼向替卦 ─────────────────────────────────────────────────────
|
|
304
|
-
// 兼向超过 4.5°(离山界 3° 以内)用替卦:山星/向星改以该山的替星入中,顺逆仍按
|
|
305
|
-
// 「替星本宫同元龙」的阴阳定。替星取《沈氏玄空学》坤壬乙诀(1/2/6/7/9 五星);
|
|
306
|
-
// 三碧、四绿、八白在此诀中不作替星,属各家分歧,见 note。
|
|
307
|
-
export const TIGUA_SOURCE = '《沈氏玄空学》坤壬乙诀(子癸甲申一白、壬卯乙未坤二黑、乾亥辰巽巳戌六白、酉辛丑艮丙七赤、寅午庚丁九紫)';
|
|
308
|
-
|
|
309
|
-
export const TIGUA_TABLE = Object.freeze({
|
|
310
|
-
子: 1, 癸: 1, 甲: 1, 申: 1,
|
|
311
|
-
壬: 2, 卯: 2, 乙: 2, 未: 2, 坤: 2,
|
|
312
|
-
乾: 6, 亥: 6, 辰: 6, 巽: 6, 巳: 6, 戌: 6,
|
|
313
|
-
酉: 7, 辛: 7, 丑: 7, 艮: 7, 丙: 7,
|
|
314
|
-
寅: 9, 午: 9, 庚: 9, 丁: 9,
|
|
315
|
-
});
|
|
316
|
-
|
|
317
|
-
export const JIAN_XIANG_THRESHOLD = 4.5;
|
|
318
|
-
|
|
319
|
-
/** 兼向判定:offset 为坐山相对本山中心的度数,正数向下一山(顺时针)。 */
|
|
320
|
-
export function jianXiangOf(sitting, offset = 0) {
|
|
321
|
-
const off = Number(offset) || 0;
|
|
322
|
-
if (Math.abs(off) <= JIAN_XIANG_THRESHOLD) {
|
|
323
|
-
return {
|
|
324
|
-
active: false, offset: off, threshold: JIAN_XIANG_THRESHOLD,
|
|
325
|
-
sitting: sitting.m, jian: null, kind: null, sameYinYang: null,
|
|
326
|
-
note: `坐山偏离中心 ${off}°,未超 ${JIAN_XIANG_THRESHOLD}°,按正向下卦排盘。`,
|
|
327
|
-
};
|
|
328
|
-
}
|
|
329
|
-
const idx = MOUNTAINS_24.findIndex((x) => x.m === sitting.m);
|
|
330
|
-
const neighbor = MOUNTAINS_24[(idx + (off > 0 ? 1 : -1) + 24) % 24];
|
|
331
|
-
const sMeta = MOUNTAIN_META[sitting.m];
|
|
332
|
-
const nMeta = MOUNTAIN_META[neighbor.m];
|
|
333
|
-
const sameGua = neighbor.gua === sitting.gua;
|
|
334
|
-
return {
|
|
335
|
-
active: true, offset: off, threshold: JIAN_XIANG_THRESHOLD,
|
|
336
|
-
sitting: sitting.m, jian: neighbor.m, side: off > 0 ? '右' : '左',
|
|
337
|
-
kind: sameGua ? '同卦兼' : '出卦兼',
|
|
338
|
-
sameYinYang: nMeta.yinYang === sMeta.yinYang,
|
|
339
|
-
note: `坐山偏离中心 ${off}°,兼${neighbor.m}(${sameGua ? '同卦兼' : '出卦兼'}${sameGua && nMeta.yinYang === sMeta.yinYang ? ',同阴同阳' : ''});按沈氏替卦,山星以 ${TIGUA_TABLE[sitting.m]} 入中。`,
|
|
340
|
-
};
|
|
341
|
-
}
|
|
342
|
-
|
|
343
|
-
/** 通行二十四山旺山旺向表(每项含正反两向;一运、九运无局)。 */
|
|
344
|
-
export const WANG_SHAN_WANG_XIANG = {
|
|
345
|
-
1: [],
|
|
346
|
-
2: [['乾', '巽'], ['亥', '巳'], ['丑', '未']],
|
|
347
|
-
3: [['卯', '酉'], ['乙', '辛'], ['辰', '戌']],
|
|
348
|
-
4: [['艮', '坤'], ['寅', '申'], ['甲', '庚']],
|
|
349
|
-
5: [['子', '午'], ['癸', '丁'], ['卯', '酉'], ['乙', '辛'], ['丑', '未'], ['辰', '戌']],
|
|
350
|
-
6: [['艮', '坤'], ['寅', '申'], ['甲', '庚']],
|
|
351
|
-
7: [['卯', '酉'], ['乙', '辛'], ['辰', '戌']],
|
|
352
|
-
8: [['乾', '巽'], ['亥', '巳'], ['丑', '未']],
|
|
353
|
-
9: [],
|
|
354
|
-
};
|
|
355
|
-
|
|
356
|
-
export const XUANKONG_SOURCES = [
|
|
357
|
-
'三元九运(1864 年起一运、每运 20 年,九运 180 年一轮)',
|
|
358
|
-
'玄空下卦挨星(运盘顺飞;山盘/向盘以入中运星本宫同元龙阴阳定顺逆,五黄入中按本山阴阳)',
|
|
359
|
-
'兼向替卦:坐山偏离中心超 4.5° 用《沈氏玄空学》坤壬乙诀替星入中,顺逆按替星本宫同元龙',
|
|
360
|
-
'年月日时紫白(公式派:年逐年退一位;月按年支三组起 8/5/2;日按冬至夏至三段起 1/7/4 与 9/3/6;时按日支三组子时起 1/7/4 与 9/3/6)',
|
|
361
|
-
'二十四山旺山旺向通行表(每项含正反两向;一运、九运无局;只对正向下卦)',
|
|
362
|
-
];
|
|
363
|
-
|
|
364
|
-
export const XUANKONG_DISCLAIMER = '玄空排盘为传统通行口径的本地计算,属民俗参考,不构成现场勘舆结论,仅供娱乐;兼向超 4.5° 按沈氏替卦,年月日时紫白按公式派;各家另有异说,形峦与现场勘舆不在本工具范围内。';
|
|
365
|
-
|
|
366
|
-
/** 玄空排盘:运盘 + 山盘 + 向盘 + 当运星 + 合十 + 表法旺山旺向;兼向超阈值改用替卦。 */
|
|
367
|
-
export function xuankongChart(period, sitting, facing, opts = {}) {
|
|
368
|
-
const sit = MOUNTAIN_META[sitting];
|
|
369
|
-
const face = MOUNTAIN_META[facing];
|
|
370
|
-
const offset = Number(opts.offset) || 0;
|
|
371
|
-
const jianXiang = jianXiangOf(sit, offset);
|
|
372
|
-
const yun = fly(period, true);
|
|
373
|
-
const sCenter = jianXiang.active ? TIGUA_TABLE[sit.m] : yun[sit.gua];
|
|
374
|
-
const fCenter = jianXiang.active ? TIGUA_TABLE[face.m] : yun[face.gua];
|
|
375
|
-
const sFly = flightOf(sit, sCenter);
|
|
376
|
-
const fFly = flightOf(face, fCenter);
|
|
377
|
-
const shan = fly(sCenter, sFly.forward);
|
|
378
|
-
const xiang = fly(fCenter, fFly.forward);
|
|
379
|
-
const palaces = PALACE_ORDER.map((palace, i) => ({
|
|
380
|
-
palace,
|
|
381
|
-
direction: palace === '中' ? '中宫' : PALACE_DIRECTION[palace],
|
|
382
|
-
luoshu: i + 1,
|
|
383
|
-
yun: yun[palace],
|
|
384
|
-
shan: shan[palace],
|
|
385
|
-
xiang: xiang[palace],
|
|
386
|
-
}));
|
|
387
|
-
const wheres = (map) => PALACE_ORDER.filter((p) => map[p] === period);
|
|
388
|
-
const pairs = WANG_SHAN_WANG_XIANG[period];
|
|
389
|
-
const hit = pairs.find(([a, b]) => (sit.m === a && face.m === b) || (sit.m === b && face.m === a));
|
|
390
|
-
const byChart = shan[sit.gua] === period && xiang[face.gua] === period;
|
|
391
|
-
const replaced = {
|
|
392
|
-
shan: jianXiang.active && sCenter !== yun[sit.gua],
|
|
393
|
-
xiang: jianXiang.active && fCenter !== yun[face.gua],
|
|
394
|
-
};
|
|
395
|
-
const note = jianXiang.active
|
|
396
|
-
? `替卦盘不套正向旺山旺向表;${byChart ? '本盘当运山星/向星仍到坐向。' : '本盘当运山星/向星未同时到坐向。'}${!replaced.shan && !replaced.xiang ? '山向两替星均「用替而未替」,实际仍按原卦气。' : ''}`
|
|
397
|
-
: byChart && hit
|
|
398
|
-
? '盘面与表法一致:本局到山到向。'
|
|
399
|
-
: byChart
|
|
400
|
-
? '盘面到山到向,但不在通行表法所列之局(表法与盘面口径不同,并列不合并)。'
|
|
401
|
-
: hit
|
|
402
|
-
? '通行表法列为旺山旺向,但本盘当运山星/向星未同时落坐向(表法与盘面口径不同,并列不合并)。'
|
|
403
|
-
: '本局既未到山到向,也不在表法旺山旺向之列。';
|
|
404
|
-
return {
|
|
405
|
-
palaces,
|
|
406
|
-
sitting: sit,
|
|
407
|
-
facing: face,
|
|
408
|
-
yun,
|
|
409
|
-
shan,
|
|
410
|
-
xiang,
|
|
411
|
-
yunCenter: period,
|
|
412
|
-
jianXiang,
|
|
413
|
-
shanCenter: {
|
|
414
|
-
star: sCenter, ...sFly,
|
|
415
|
-
mode: jianXiang.active ? '替卦' : '下卦',
|
|
416
|
-
original: yun[sit.gua],
|
|
417
|
-
replaced: replaced.shan,
|
|
418
|
-
},
|
|
419
|
-
xiangCenter: {
|
|
420
|
-
star: fCenter, ...fFly,
|
|
421
|
-
mode: jianXiang.active ? '替卦' : '下卦',
|
|
422
|
-
original: yun[face.gua],
|
|
423
|
-
replaced: replaced.xiang,
|
|
424
|
-
},
|
|
425
|
-
dangYun: { shan: wheres(shan), xiang: wheres(xiang) },
|
|
426
|
-
heshi: {
|
|
427
|
-
shan: PALACE_ORDER.filter((p) => shan[p] + yun[p] === 10),
|
|
428
|
-
xiang: PALACE_ORDER.filter((p) => xiang[p] + yun[p] === 10),
|
|
429
|
-
},
|
|
430
|
-
wangShanWangXiang: {
|
|
431
|
-
byChart,
|
|
432
|
-
byTable: jianXiang.active ? null : Boolean(hit),
|
|
433
|
-
table: pairs.map(([a, b]) => `${a}${b}`),
|
|
434
|
-
note,
|
|
435
|
-
},
|
|
436
|
-
};
|
|
437
|
-
}
|
|
438
|
-
|
|
439
|
-
/** DST 校正出生日期(1986–1991),返回校正后的年月日。 */
|
|
440
|
-
function civilDate(input) {
|
|
441
|
-
const dst = input.dst === false
|
|
442
|
-
? { year: input.year, month: input.month, day: input.day, hour: 0, minute: 0, applied: false, note: null }
|
|
443
|
-
: applyDst({ year: input.year, month: input.month, day: input.day, hour: 0, minute: 0 });
|
|
444
|
-
return { year: dst.year, month: dst.month, day: dst.day, dst };
|
|
445
|
-
}
|
|
446
|
-
|
|
447
|
-
const SOURCES = [
|
|
448
|
-
'大游年歌(八宅通行口径)',
|
|
449
|
-
'年家紫白飞星(纯公式:以 2026 丙午一白入中、逐年退一位、九年一轮;按立春换年)',
|
|
450
|
-
'24 山归属与度数(八宅通行口径,每山 15°)',
|
|
451
|
-
];
|
|
452
|
-
|
|
453
|
-
const DISCLAIMER = '八宅与飞星为传统通行口径的本地计算,属民俗参考,不构成现场勘舆结论,仅供娱乐;实际户型以现场为准。';
|
|
454
|
-
|
|
455
|
-
/**
|
|
456
|
-
* 风水主函数。
|
|
457
|
-
* @param {{action?:'natal'|'advise'|'xuankong', year?:number, month?:number, day?:number, gender?:'male'|'female',
|
|
458
|
-
* fortuneYear?:number, sitting?:string, facing?:string, period?:number, dst?:boolean}} input
|
|
459
|
-
* natal(默认):要生日 + 性别 + 流年(缺流年则只算命卦与八星)。
|
|
460
|
-
* advise:要坐山或向山(24 山,给一即可);出生信息可选,给了才做命宅配。
|
|
461
|
-
* xuankong:要坐山或向山;运由 period(1–9)或 fortuneYear 定,缺省按九运并注明。
|
|
462
|
-
*/
|
|
463
|
-
export function buildFengshui(input) {
|
|
464
|
-
if (input.action === 'advise') return buildAdvise(input);
|
|
465
|
-
if (input.action === 'xuankong') return buildXuankong(input);
|
|
466
|
-
const gender = input.gender;
|
|
467
|
-
if (gender !== 'male' && gender !== 'female') {
|
|
468
|
-
throw fail('E_INPUT', '风水命卦需要性别:男女命卦算法不同,不可缺省');
|
|
469
|
-
}
|
|
470
|
-
if (!Number.isInteger(input.year) || !Number.isInteger(input.month) || !Number.isInteger(input.day)) {
|
|
471
|
-
throw fail('E_INPUT', '风水命卦需要完整出生年月日(公历)——只知道年份不够:命卦按立春界划分出生年');
|
|
472
|
-
}
|
|
473
|
-
const civil = civilDate(input);
|
|
474
|
-
const guaYear = guaYearOf(civil.year, civil.month, civil.day);
|
|
475
|
-
const { num, gua, direction } = mingGua(guaYear, gender);
|
|
476
|
-
const stars = eightStars(gua);
|
|
477
|
-
const eastFour = ['坎', '离', '震', '巽'].includes(gua);
|
|
478
|
-
const fortuneYear = Number.isInteger(input.fortuneYear) ? input.fortuneYear : null;
|
|
479
|
-
|
|
480
|
-
return {
|
|
481
|
-
system: 'fengshui',
|
|
482
|
-
meta: {
|
|
483
|
-
birth: `${civil.year}-${pad2(civil.month)}-${pad2(civil.day)}`,
|
|
484
|
-
guaYear,
|
|
485
|
-
gender: gender === 'male' ? '男' : '女',
|
|
486
|
-
fortuneYear,
|
|
487
|
-
...(civil.dst.applied ? { dst: civil.dst.note } : {}),
|
|
488
|
-
...(fortuneYear === null ? { note: '未给流年:未算年紫白飞星(需要时补 fortuneYear)。' } : {}),
|
|
489
|
-
scope: '八宅命卦 + 大游年八星 + 年紫白;玄空飞星见 action=xuankong;不含形峦与现场勘舆。',
|
|
490
|
-
},
|
|
491
|
-
mingGua: {
|
|
492
|
-
num,
|
|
493
|
-
gua,
|
|
494
|
-
direction,
|
|
495
|
-
group: eastFour ? '东四命' : '西四命',
|
|
496
|
-
compatible: eastFour ? '宜东四宅(坐坎离震巽)' : '宜西四宅(坐乾坤艮兑)',
|
|
497
|
-
},
|
|
498
|
-
luckyDirections: LUCKY_STARS.map((s) => ({ star: s, ...stars[s] })),
|
|
499
|
-
unluckyDirections: UNLUCKY_STARS.map((s) => ({ star: s, ...stars[s] })),
|
|
500
|
-
...(fortuneYear === null ? {} : { yearlyFlying: yearlyFlying(fortuneYear) }),
|
|
501
|
-
sources: SOURCES,
|
|
502
|
-
disclaimer: DISCLAIMER,
|
|
503
|
-
};
|
|
504
|
-
}
|
|
505
|
-
|
|
506
|
-
/** 坐向解析:至少给坐或向;两者都给则必须对宫。 */
|
|
507
|
-
function resolveSittingFacing(input) {
|
|
508
|
-
const hasSitting = typeof input.sitting === 'string' && input.sitting.trim() !== '';
|
|
509
|
-
const hasFacing = typeof input.facing === 'string' && input.facing.trim() !== '';
|
|
510
|
-
if (!hasSitting && !hasFacing) {
|
|
511
|
-
throw fail('E_INPUT', '缺少坐向:sitting(坐山)或 facing(向山)至少给一个——按罗盘 24 山报坐或报向,例如「坐北朝南」对应坐子向午');
|
|
512
|
-
}
|
|
513
|
-
if (hasSitting && hasFacing) {
|
|
514
|
-
const s = mountainInfo(input.sitting.trim());
|
|
515
|
-
const f = mountainInfo(input.facing.trim());
|
|
516
|
-
if ((s.index + 12) % 24 !== f.index) {
|
|
517
|
-
throw fail('E_INPUT', `坐向不自洽:坐${s.m}(${s.deg}°)与向${f.m}(${f.deg}°)不是对宫(相差应 180°,实测 ${((f.deg - s.deg) + 360) % 360}°)——请核对罗盘读数。`);
|
|
518
|
-
}
|
|
519
|
-
}
|
|
520
|
-
const sitting = hasSitting ? mountainInfo(input.sitting.trim()) : sittingFromFacing(input.facing.trim());
|
|
521
|
-
const facing = MOUNTAINS_24[(sitting.index + 12) % 24];
|
|
522
|
-
return { sitting, facing };
|
|
523
|
-
}
|
|
524
|
-
|
|
525
|
-
/** 宅卦坐向 + 门主灶厕布局倾向 + 命宅配。 */
|
|
526
|
-
function buildAdvise(input) {
|
|
527
|
-
const { sitting, facing } = resolveSittingFacing(input);
|
|
528
|
-
const houseGua = sitting.gua;
|
|
529
|
-
const stars = eightStars(houseGua);
|
|
530
|
-
const lucky = LUCKY_STARS.map((s) => stars[s]);
|
|
531
|
-
const unlucky = UNLUCKY_STARS.map((s) => stars[s]);
|
|
532
|
-
const dir = (x) => `${x.direction}(${x.palace}宫)`;
|
|
533
|
-
|
|
534
|
-
const starChart = [...LUCKY_STARS, ...UNLUCKY_STARS].map((s) => ({
|
|
535
|
-
star: s,
|
|
536
|
-
lucky: LUCKY_STARS.includes(s),
|
|
537
|
-
...stars[s],
|
|
538
|
-
role: STAR_LAYOUT_ROLE[s],
|
|
539
|
-
}));
|
|
540
|
-
|
|
541
|
-
const layout = {
|
|
542
|
-
door: `门宜开于吉方:首选生气方 ${dir(lucky[0])},次选延年方 ${dir(lucky[1])}、天医方 ${dir(lucky[2])}。`,
|
|
543
|
-
stove: `灶宜「坐凶向吉」:灶身压凶方(首选绝命方 ${dir(unlucky[0])}、五鬼方 ${dir(unlucky[1])}),灶口朝吉方。`,
|
|
544
|
-
bed: `主卧与床头宜安吉方:生气方 ${dir(lucky[0])} 或延年方 ${dir(lucky[1])};床头宜向吉方。`,
|
|
545
|
-
toilet: `厕所宜压凶方:祸害方 ${dir(unlucky[3])} 或六煞方 ${dir(unlucky[2])}(污去凶气,宜静常闭)。`,
|
|
546
|
-
};
|
|
547
|
-
|
|
548
|
-
let mingMatch = null;
|
|
549
|
-
const hasBirth = Number.isInteger(input.year) && Number.isInteger(input.month) && Number.isInteger(input.day)
|
|
550
|
-
&& (input.gender === 'male' || input.gender === 'female');
|
|
551
|
-
if (hasBirth) {
|
|
552
|
-
const civil = civilDate(input);
|
|
553
|
-
const guaYear = guaYearOf(civil.year, civil.month, civil.day);
|
|
554
|
-
const ming = mingGua(guaYear, input.gender);
|
|
555
|
-
const mGroup = guaGroup(ming.gua);
|
|
556
|
-
const hGroup = guaGroup(houseGua);
|
|
557
|
-
const same = mGroup === hGroup;
|
|
558
|
-
const mingLucky = LUCKY_STARS.map((s) => `${s}位${eightStars(ming.gua)[s].direction}`).join('、');
|
|
559
|
-
mingMatch = {
|
|
560
|
-
birth: `${civil.year}-${pad2(civil.month)}-${pad2(civil.day)}`,
|
|
561
|
-
guaYear,
|
|
562
|
-
gender: input.gender === 'male' ? '男' : '女',
|
|
563
|
-
mingGua: { gua: ming.gua, direction: ming.direction, group: `${mGroup}命` },
|
|
564
|
-
houseGua: { gua: houseGua, group: `${hGroup}宅` },
|
|
565
|
-
sameGroup: same,
|
|
566
|
-
text: same
|
|
567
|
-
? `命宅相配(${mGroup}命住${hGroup}宅):以宅之八星盘布局即可,个人吉方与宅吉方大体同向。`
|
|
568
|
-
: `命宅不配(${mGroup}命住${hGroup}宅):传统调停法——门与床以命卦吉方为先(此命四吉方:${mingLucky}),灶仍按宅卦坐凶向吉,两盘各自取用。`,
|
|
569
|
-
};
|
|
570
|
-
}
|
|
571
|
-
|
|
572
|
-
return {
|
|
573
|
-
system: 'fengshui',
|
|
574
|
-
meta: {
|
|
575
|
-
action: 'advise',
|
|
576
|
-
...(hasBirth ? {} : { birthNote: '未提供生辰:未做命宅配;如需按个人命卦调停门床,请补充出生年月日与性别。' }),
|
|
577
|
-
sitting: `${sitting.m}山(罗盘 ${sitting.deg}°)`,
|
|
578
|
-
facing: `${facing.m}山(罗盘 ${facing.deg}°)`,
|
|
579
|
-
houseGua,
|
|
580
|
-
houseGroup: `${guaGroup(houseGua)}宅`,
|
|
581
|
-
scope: '门主灶厕走向按八宅通行口径给倾向,玄空飞星见 action=xuankong;不含形峦、不是现场勘舆结论。',
|
|
582
|
-
},
|
|
583
|
-
houseStars: starChart,
|
|
584
|
-
layout,
|
|
585
|
-
...(mingMatch ? { mingMatch } : {}),
|
|
586
|
-
sources: SOURCES,
|
|
587
|
-
disclaimer: DISCLAIMER,
|
|
588
|
-
};
|
|
589
|
-
}
|
|
590
|
-
|
|
591
|
-
/** 兼向读数:接受坐/向的偏移度数或罗盘度数,两边都给时必须一致。 */
|
|
592
|
-
function resolveOffset(input, sitting, facing) {
|
|
593
|
-
const present = (v) => v !== undefined && v !== null && v !== '';
|
|
594
|
-
const number = (v, label) => {
|
|
595
|
-
if (typeof v !== 'number' || !Number.isFinite(v)) throw fail('E_INPUT', `${label} 必须是数字(度),收到「${v}」`);
|
|
596
|
-
return v;
|
|
597
|
-
};
|
|
598
|
-
const degreeOffset = (v, mountain, label) => {
|
|
599
|
-
const deg = number(v, label);
|
|
600
|
-
if (deg < 0 || deg > 360) throw fail('E_INPUT', `${label} 必须在 0–360° 之间(罗盘度数),收到「${deg}」`);
|
|
601
|
-
const diff = ((deg - mountain.deg + 540) % 360) - 180;
|
|
602
|
-
if (Math.abs(diff) > 7.5 + 1e-9) throw fail('E_INPUT', `${label} ${deg}° 不在${mountain.m}山范围内(${mountain.m}中心 ${mountain.deg}°,每山 15°),请核对坐向。`);
|
|
603
|
-
return diff;
|
|
604
|
-
};
|
|
605
|
-
const values = [];
|
|
606
|
-
if (present(input.sittingOffset)) values.push(number(input.sittingOffset, 'sittingOffset'));
|
|
607
|
-
if (present(input.sittingDegree)) values.push(degreeOffset(input.sittingDegree, sitting, 'sittingDegree'));
|
|
608
|
-
if (present(input.facingOffset)) values.push(number(input.facingOffset, 'facingOffset'));
|
|
609
|
-
if (present(input.facingDegree)) values.push(degreeOffset(input.facingDegree, facing, 'facingDegree'));
|
|
610
|
-
if (!values.length) return { offset: 0, source: '未报兼向度数,按正向(0°)' };
|
|
611
|
-
const offset = values[0];
|
|
612
|
-
if (values.some((v) => Math.abs(v - offset) > 1e-9)) throw fail('E_INPUT', `坐向兼度不一致:${values.join('° / ')}°——坐与向应同向偏离,请只报一个偏移或核对度数。`);
|
|
613
|
-
if (Math.abs(offset) > 7.5 + 1e-9) throw fail('E_INPUT', `兼向偏移必须在 −7.5°–7.5°(相对本山中心),收到「${offset}」——超过 7.5° 已进入下一山。`);
|
|
614
|
-
return { offset, source: '按所报兼向度数' };
|
|
615
|
-
}
|
|
616
|
-
|
|
617
|
-
/** 飞星日期:fortuneDate=YYYY-MM-DD、fortuneTime=HH:MM;缺省 12:00。 */
|
|
618
|
-
function parseFortuneAt(input) {
|
|
619
|
-
const present = (v) => v !== undefined && v !== null && v !== '';
|
|
620
|
-
if (!present(input.fortuneDate)) return null;
|
|
621
|
-
if (typeof input.fortuneDate !== 'string' || !/^\d{4}-\d{2}-\d{2}$/.test(input.fortuneDate)) {
|
|
622
|
-
throw fail('E_INPUT', `fortuneDate 需为 YYYY-MM-DD,收到「${input.fortuneDate}」`);
|
|
623
|
-
}
|
|
624
|
-
const [year, month, day] = input.fortuneDate.split('-').map(Number);
|
|
625
|
-
const probe = new Date(Date.UTC(year, month - 1, day));
|
|
626
|
-
if (probe.getUTCFullYear() !== year || probe.getUTCMonth() !== month - 1 || probe.getUTCDate() !== day) {
|
|
627
|
-
throw fail('E_INPUT', `fortuneDate 不是有效日期:「${input.fortuneDate}」`);
|
|
628
|
-
}
|
|
629
|
-
const rawTime = present(input.fortuneTime) ? input.fortuneTime : '12:00';
|
|
630
|
-
if (typeof rawTime !== 'string' || !/^\d{2}:\d{2}$/.test(rawTime)) {
|
|
631
|
-
throw fail('E_INPUT', `fortuneTime 需为 HH:MM,收到「${rawTime}」`);
|
|
632
|
-
}
|
|
633
|
-
const [hour, minute] = rawTime.split(':').map(Number);
|
|
634
|
-
if (hour > 23 || minute > 59) throw fail('E_INPUT', `fortuneTime 超出范围(HH:MM):「${rawTime}」`);
|
|
635
|
-
const dst = input.dst === false ? null : applyDst({ year, month, day, hour, minute });
|
|
636
|
-
const at = dst ? { year: dst.year, month: dst.month, day: dst.day, hour: dst.hour, minute: dst.minute } : { year, month, day, hour, minute };
|
|
637
|
-
return { at, dst };
|
|
638
|
-
}
|
|
639
|
-
|
|
640
|
-
/** 玄空排盘:坐向 + 运 → 九宫盘、当运星、合十、表法旺山旺向;兼向超阈值用替卦。 */
|
|
641
|
-
function buildXuankong(input) {
|
|
642
|
-
const { sitting, facing } = resolveSittingFacing(input);
|
|
643
|
-
const offsetInfo = resolveOffset(input, sitting, facing);
|
|
644
|
-
const fortune = parseFortuneAt(input);
|
|
645
|
-
let period;
|
|
646
|
-
let yearInfo = null;
|
|
647
|
-
let note;
|
|
648
|
-
if (input.period !== undefined && input.period !== null && !Number.isInteger(input.period)) {
|
|
649
|
-
throw fail('E_INPUT', `运必须是 1–9 的整数(三元九运),收到「${input.period}」——一运 1864 起、九运 2024–2043。`);
|
|
650
|
-
}
|
|
651
|
-
if (Number.isInteger(input.period)) {
|
|
652
|
-
if (input.period < 1 || input.period > 9) {
|
|
653
|
-
throw fail('E_INPUT', `运必须是 1–9 的整数(三元九运),收到「${input.period}」——一运 1864 起、九运 2024–2043。`);
|
|
654
|
-
}
|
|
655
|
-
period = input.period;
|
|
656
|
-
} else if (Number.isInteger(input.fortuneYear)) {
|
|
657
|
-
yearInfo = periodOfYear(input.fortuneYear);
|
|
658
|
-
period = yearInfo.period;
|
|
659
|
-
} else if (fortune) {
|
|
660
|
-
yearInfo = periodOfYear(guaYearOf(fortune.at.year, fortune.at.month, fortune.at.day));
|
|
661
|
-
period = yearInfo.period;
|
|
662
|
-
note = `未显式给运:按飞星日期归入 ${yearInfo.label}。`;
|
|
663
|
-
} else {
|
|
664
|
-
period = 9;
|
|
665
|
-
note = '未给运:默认按九运(2024–2043)排盘;可传 period(1–9)或 fortuneYear 指定。';
|
|
666
|
-
}
|
|
667
|
-
const chart = xuankongChart(period, sitting.m, facing.m, { offset: offsetInfo.offset });
|
|
668
|
-
const sitMeta = MOUNTAIN_META[sitting.m];
|
|
669
|
-
const faceMeta = MOUNTAIN_META[facing.m];
|
|
670
|
-
const center = (c) => ({
|
|
671
|
-
star: c.star, name: NINE_STARS[c.star].name, fly: c.forward ? '顺飞' : '逆飞', basis: c.basis,
|
|
672
|
-
mode: c.mode, original: c.original, replaced: c.replaced,
|
|
673
|
-
});
|
|
674
|
-
const where = (list) => list.map((p) => (p === '中' ? '中宫' : `${p}宫${PALACE_DIRECTION[p]}`)).join('、') || '无';
|
|
675
|
-
let timeFlying = null;
|
|
676
|
-
if (fortune) {
|
|
677
|
-
timeFlying = timeFlyingOf(fortune.at);
|
|
678
|
-
if (fortune.dst && fortune.dst.applied) timeFlying.meta.dst = fortune.dst.note;
|
|
679
|
-
} else if (Number.isInteger(input.fortuneYear)) {
|
|
680
|
-
const centerStar = yearZibaiStar(input.fortuneYear);
|
|
681
|
-
timeFlying = {
|
|
682
|
-
year: { ...zibaiChart(centerStar, true), basis: `${input.fortuneYear} 年紫白 ${NINE_STARS[centerStar].name} 入中顺飞` },
|
|
683
|
-
month: null, day: null, hour: null,
|
|
684
|
-
meta: { guaYear: input.fortuneYear, note: '只给年份:已排年紫白;补 fortuneDate(YYYY-MM-DD)与 fortuneTime(HH:MM)可排月、日、时。' },
|
|
685
|
-
};
|
|
686
|
-
}
|
|
687
|
-
return {
|
|
688
|
-
system: 'fengshui',
|
|
689
|
-
meta: {
|
|
690
|
-
action: 'xuankong',
|
|
691
|
-
period,
|
|
692
|
-
periodLabel: yearInfo ? yearInfo.label : `${period}运`,
|
|
693
|
-
...(yearInfo ? { year: input.fortuneYear || (fortune && fortune.at.year), periodStart: yearInfo.start, periodEnd: yearInfo.end } : {}),
|
|
694
|
-
sitting: `${sitting.m}山(罗盘 ${sitting.deg}°)`,
|
|
695
|
-
facing: `${facing.m}山(罗盘 ${facing.deg}°)`,
|
|
696
|
-
sittingGua: sitting.gua,
|
|
697
|
-
facingGua: facing.gua,
|
|
698
|
-
sittingYuan: `${sitMeta.yuan}${sitMeta.yinYang}`,
|
|
699
|
-
facingYuan: `${faceMeta.yuan}${faceMeta.yinYang}`,
|
|
700
|
-
sittingOffset: offsetInfo.offset,
|
|
701
|
-
jianXiang: chart.jianXiang,
|
|
702
|
-
shanCenter: center(chart.shanCenter),
|
|
703
|
-
xiangCenter: center(chart.xiangCenter),
|
|
704
|
-
...(note ? { note } : {}),
|
|
705
|
-
scope: '玄空下卦/替卦:运盘 + 山盘 + 向盘 + 当运星 + 合十 + 表法旺山旺向;兼向超 4.5° 按沈氏替卦;年月日时紫白按公式派;不含形峦与现场勘舆。',
|
|
706
|
-
},
|
|
707
|
-
palaces: chart.palaces,
|
|
708
|
-
dangYun: {
|
|
709
|
-
shan: { palaces: chart.dangYun.shan, text: where(chart.dangYun.shan) },
|
|
710
|
-
xiang: { palaces: chart.dangYun.xiang, text: where(chart.dangYun.xiang) },
|
|
711
|
-
},
|
|
712
|
-
heshi: { shan: chart.heshi.shan, xiang: chart.heshi.xiang },
|
|
713
|
-
wangShanWangXiang: chart.wangShanWangXiang,
|
|
714
|
-
...(timeFlying ? { timeFlying } : {}),
|
|
715
|
-
sources: XUANKONG_SOURCES,
|
|
716
|
-
disclaimer: XUANKONG_DISCLAIMER,
|
|
717
|
-
};
|
|
718
|
-
}
|
|
1
|
+
// 风水:八宅命卦 + 大游年八星 + 年家紫白飞星 + 24 山门主灶布局倾向(纯本地确定性)。
|
|
2
|
+
// 口径:大游年歌与 24 山归属为八宅通行口径,吉凶名词照传统转述;不做现场勘舆结论。
|
|
3
|
+
// 参照公开通行表实现(与常见八宅/玄空入门资料一致),年紫白用纯公式,不依赖任何第三方历法库。
|
|
4
|
+
import {
|
|
5
|
+
partsToJd, termInstantJd, toJDN, dayGanzhiIndex, ganzhiName, termWindow, previousJie, BRANCHES,
|
|
6
|
+
} from './calendar.js';
|
|
7
|
+
import { applyDst } from './dst.js';
|
|
8
|
+
import { fail } from './fail.js';
|
|
9
|
+
|
|
10
|
+
/** 九星名称表:数 → {name, color, element, star(北斗名), lucky}。 */
|
|
11
|
+
export const NINE_STARS = {
|
|
12
|
+
1: { name: '一白', color: '白', element: '水', star: '贪狼', lucky: true },
|
|
13
|
+
2: { name: '二黑', color: '黑', element: '土', star: '巨门', lucky: false },
|
|
14
|
+
3: { name: '三碧', color: '碧', element: '木', star: '禄存', lucky: false },
|
|
15
|
+
4: { name: '四绿', color: '绿', element: '木', star: '文曲', lucky: true },
|
|
16
|
+
5: { name: '五黄', color: '黄', element: '土', star: '廉贞', lucky: false },
|
|
17
|
+
6: { name: '六白', color: '白', element: '金', star: '武曲', lucky: true },
|
|
18
|
+
7: { name: '七赤', color: '赤', element: '金', star: '破军', lucky: false },
|
|
19
|
+
8: { name: '八白', color: '白', element: '土', star: '左辅', lucky: true },
|
|
20
|
+
9: { name: '九紫', color: '紫', element: '火', star: '右弼', lucky: true },
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
/** 后天八卦 → 方位。 */
|
|
24
|
+
export const PALACE_DIRECTION = {
|
|
25
|
+
坎: '正北', 艮: '东北', 震: '正东', 巽: '东南',
|
|
26
|
+
离: '正南', 坤: '西南', 兑: '正西', 乾: '西北',
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
/** 大游年歌:自本卦起,按「坎艮震巽离坤兑乾」循环顺数,数组为自卦后第 1–7 位。 */
|
|
30
|
+
const YOUNIAN_ORDER = ['坎', '艮', '震', '巽', '离', '坤', '兑', '乾'];
|
|
31
|
+
export const DAYOUTIAN = {
|
|
32
|
+
乾: ['六煞', '天医', '五鬼', '祸害', '绝命', '延年', '生气'],
|
|
33
|
+
坎: ['五鬼', '天医', '生气', '延年', '绝命', '祸害', '六煞'],
|
|
34
|
+
艮: ['六煞', '绝命', '祸害', '生气', '延年', '天医', '五鬼'],
|
|
35
|
+
震: ['延年', '生气', '祸害', '绝命', '五鬼', '天医', '六煞'],
|
|
36
|
+
巽: ['天医', '五鬼', '六煞', '祸害', '生气', '绝命', '延年'],
|
|
37
|
+
离: ['六煞', '五鬼', '绝命', '延年', '祸害', '生气', '天医'],
|
|
38
|
+
坤: ['天医', '延年', '绝命', '生气', '祸害', '五鬼', '六煞'],
|
|
39
|
+
兑: ['生气', '祸害', '延年', '绝命', '六煞', '五鬼', '天医'],
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
export const LUCKY_STARS = ['生气', '延年', '天医', '伏位'];
|
|
43
|
+
export const UNLUCKY_STARS = ['绝命', '五鬼', '六煞', '祸害'];
|
|
44
|
+
|
|
45
|
+
/** 24 山(周天 360° 每山 15°,自北顺时针):山 → 后天八卦归属 + 中心度数(子山 = 0°)。 */
|
|
46
|
+
export const MOUNTAINS_24 = [
|
|
47
|
+
{ m: '壬', gua: '坎', deg: 345 }, { m: '子', gua: '坎', deg: 0 }, { m: '癸', gua: '坎', deg: 15 },
|
|
48
|
+
{ m: '丑', gua: '艮', deg: 30 }, { m: '艮', gua: '艮', deg: 45 }, { m: '寅', gua: '艮', deg: 60 },
|
|
49
|
+
{ m: '甲', gua: '震', deg: 75 }, { m: '卯', gua: '震', deg: 90 }, { m: '乙', gua: '震', deg: 105 },
|
|
50
|
+
{ m: '辰', gua: '巽', deg: 120 }, { m: '巽', gua: '巽', deg: 135 }, { m: '巳', gua: '巽', deg: 150 },
|
|
51
|
+
{ m: '丙', gua: '离', deg: 165 }, { m: '午', gua: '离', deg: 180 }, { m: '丁', gua: '离', deg: 195 },
|
|
52
|
+
{ m: '未', gua: '坤', deg: 210 }, { m: '坤', gua: '坤', deg: 225 }, { m: '申', gua: '坤', deg: 240 },
|
|
53
|
+
{ m: '庚', gua: '兑', deg: 255 }, { m: '酉', gua: '兑', deg: 270 }, { m: '辛', gua: '兑', deg: 285 },
|
|
54
|
+
{ m: '戌', gua: '乾', deg: 300 }, { m: '乾', gua: '乾', deg: 315 }, { m: '亥', gua: '乾', deg: 330 },
|
|
55
|
+
];
|
|
56
|
+
|
|
57
|
+
const MOUNTAIN_INDEX = new Map(MOUNTAINS_24.map((x, i) => [x.m, i]));
|
|
58
|
+
|
|
59
|
+
export function mountainInfo(name) {
|
|
60
|
+
const i = MOUNTAIN_INDEX.get(name);
|
|
61
|
+
if (i === undefined) {
|
|
62
|
+
throw fail('E_INPUT', `坐向必须是 24 山之一(${MOUNTAINS_24.map((x) => x.m).join('')}),收到「${name}」——请按罗盘 24 山报坐或报向,不要自造方位词。`);
|
|
63
|
+
}
|
|
64
|
+
return { ...MOUNTAINS_24[i], index: i };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function sittingFromFacing(facing) {
|
|
68
|
+
const f = mountainInfo(facing);
|
|
69
|
+
const i = (f.index + 12) % 24;
|
|
70
|
+
return { ...MOUNTAINS_24[i], index: i };
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const guaGroup = (gua) => (['坎', '离', '震', '巽'].includes(gua) ? '东四' : '西四');
|
|
74
|
+
|
|
75
|
+
/** 八星 → 布局角色(门主灶厕各宜何星方)。 */
|
|
76
|
+
export const STAR_LAYOUT_ROLE = {
|
|
77
|
+
生气: '大吉:最宜开门、主卧、灶口朝向',
|
|
78
|
+
延年: '大吉:宜开门、主卧(利感情与人缘)',
|
|
79
|
+
天医: '次吉:宜主卧、书房、老人房(利健康)',
|
|
80
|
+
伏位: '次吉:宜卧室、神位(主平稳)',
|
|
81
|
+
祸害: '小凶:宜厕所、储物间(宜静不宜动)',
|
|
82
|
+
六煞: '次凶:宜厕所、储物间',
|
|
83
|
+
五鬼: '大凶:宜厕所,或以灶坐压之(传统「火烧五鬼」说)',
|
|
84
|
+
绝命: '最凶:宜灶坐压之、储物,忌门床',
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
const GUA_BY_NUM = ['坎', '坤', '震', '巽', '中', '乾', '兑', '艮', '离'];
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* 八宅命卦:按节气年(立春界)公元年后两位 n:
|
|
91
|
+
* 1900–1999:男 (100−n) ÷ 9 取余,女 (n−4) ÷ 9 取余;
|
|
92
|
+
* 2000–2099:男 (99−n) ÷ 9 取余,女 (n−3) ÷ 9 取余(负数加 9)。
|
|
93
|
+
* 余 0 作 9;余 5 男作坤、女作艮。数→卦:1坎 2坤 3震 4巽 6乾 7兑 8艮 9离。
|
|
94
|
+
*/
|
|
95
|
+
export function mingGua(birthYear, gender) {
|
|
96
|
+
const tail = ((birthYear % 100) + 100) % 100;
|
|
97
|
+
const century = birthYear >= 2000 ? { male: 99, female: 3 } : { male: 100, female: 4 };
|
|
98
|
+
const mod9 = (n) => { const r = ((n % 9) + 9) % 9; return r === 0 ? 9 : r; };
|
|
99
|
+
let num = gender === 'male' ? mod9(century.male - tail) : mod9(tail - century.female);
|
|
100
|
+
if (num === 5) num = gender === 'male' ? 2 : 8;
|
|
101
|
+
const gua = GUA_BY_NUM[num - 1];
|
|
102
|
+
if (gua === '中') throw new Error('命卦计算异常:落中宫');
|
|
103
|
+
return { num, gua, direction: PALACE_DIRECTION[gua] };
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** 大游年八星(含伏位=本卦)。 */
|
|
107
|
+
export function eightStars(gua) {
|
|
108
|
+
const result = { 伏位: { palace: gua, direction: PALACE_DIRECTION[gua] } };
|
|
109
|
+
const start = YOUNIAN_ORDER.indexOf(gua);
|
|
110
|
+
for (let step = 1; step <= 7; step++) {
|
|
111
|
+
const palace = YOUNIAN_ORDER[(start + step) % 8];
|
|
112
|
+
result[DAYOUTIAN[gua][step - 1]] = { palace, direction: PALACE_DIRECTION[palace] };
|
|
113
|
+
}
|
|
114
|
+
return result;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** 立春界节气年:生日在当年立春前 → 上一年(按正午判,与黄历一致)。 */
|
|
118
|
+
export function guaYearOf(year, month, day) {
|
|
119
|
+
const birthJd = partsToJd({ year, month, day, hour: 12, minute: 0 });
|
|
120
|
+
return birthJd < termInstantJd(year, 2) ? year - 1 : year;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* 年紫白入中之星(1–9):锚点 2026 丙午 = 一白,逐年退一位,九年一轮。
|
|
125
|
+
* 对照公开表:2024 三碧、2025 二黑、2026 一白、2027 九紫、2034 二黑、2035 一白。
|
|
126
|
+
*/
|
|
127
|
+
export function yearZibaiStar(year) {
|
|
128
|
+
const n = (1 + (2026 - year)) % 9;
|
|
129
|
+
return n <= 0 ? n + 9 : n;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const PALACE_ORDER = ['坎', '坤', '震', '巽', '中', '乾', '兑', '艮', '离']; // 洛书数 1–9 对应宫
|
|
133
|
+
|
|
134
|
+
const norm9 = (n) => {
|
|
135
|
+
const r = ((n % 9) + 9) % 9;
|
|
136
|
+
return r === 0 ? 9 : r;
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
/** 紫白九宫:入中星按洛书轨迹飞布。forward=true 顺飞,false 逆飞。 */
|
|
140
|
+
function zibaiChart(center, forward = true) {
|
|
141
|
+
const palaces = [];
|
|
142
|
+
for (const palace of PALACE_ORDER) {
|
|
143
|
+
const k = PALACE_ORDER.indexOf(palace) + 1;
|
|
144
|
+
const num = norm9(k - 5 + center);
|
|
145
|
+
const s = NINE_STARS[num];
|
|
146
|
+
palaces.push({ palace, direction: palace === '中' ? '中宫' : PALACE_DIRECTION[palace], num, name: s.name, star: s.star, element: s.element, lucky: s.lucky });
|
|
147
|
+
}
|
|
148
|
+
return { center, centerName: NINE_STARS[center].name, forward, fly: forward ? '顺飞' : '逆飞', palaces };
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/** 九宫飞星:年星入中顺飞。 */
|
|
152
|
+
export function yearlyFlying(year) {
|
|
153
|
+
return zibaiChart(yearZibaiStar(year), true);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// ── 年、月、日、时紫白(公式派) ─────────────────────────────────
|
|
157
|
+
// 年:年星逐年退一位,入中顺飞(与 yearZibaiStar 同一锚点)。
|
|
158
|
+
// 月:子午卯酉年正月八白、辰戌丑未年正月五黄、寅申巳亥年正月二黑;逐月退一位,入中顺飞。
|
|
159
|
+
// 日:冬至–夏至阳遁、夏至–冬至阴遁;三段(冬至/雨水/谷雨、夏至/处暑/霜降)分别起 1/7/4 与 9/3/6。
|
|
160
|
+
// 时:冬至后四正日/四墓日/四生日子时起 1/7/4;夏至后起 9/3/6;阳遁时星顺行顺飞、阴遁时星逆行逆飞。
|
|
161
|
+
// 口径说明:月按节气月(寅月为正月);日按公历日、23:00 后归次日;各家另有「日星一律顺飞」「月按农历月」等说法,见 note。
|
|
162
|
+
|
|
163
|
+
const JIE_MONTH_NUM = {
|
|
164
|
+
立春: 1, 惊蛰: 2, 清明: 3, 立夏: 4, 芒种: 5, 小暑: 6,
|
|
165
|
+
立秋: 7, 白露: 8, 寒露: 9, 立冬: 10, 大雪: 11, 小寒: 12,
|
|
166
|
+
};
|
|
167
|
+
const MONTH_START_BY_GROUP = { 四正: 8, 四墓: 5, 四生: 2 };
|
|
168
|
+
const HOUR_START_WINTER = { 四正: 1, 四墓: 7, 四生: 4 };
|
|
169
|
+
const HOUR_START_SUMMER = { 四正: 9, 四墓: 3, 四生: 6 };
|
|
170
|
+
|
|
171
|
+
const branchGroupOf = (branch) => (
|
|
172
|
+
['子', '午', '卯', '酉'].includes(branch) ? '四正'
|
|
173
|
+
: ['辰', '戌', '丑', '未'].includes(branch) ? '四墓' : '四生'
|
|
174
|
+
);
|
|
175
|
+
|
|
176
|
+
const yearBranchOf = (year) => ganzhiName((((year - 4) % 60) + 60) % 60).branch;
|
|
177
|
+
|
|
178
|
+
function latestTerm(terms, name, jd) {
|
|
179
|
+
let found = null;
|
|
180
|
+
for (const t of terms) {
|
|
181
|
+
if (t.jd > jd) break;
|
|
182
|
+
if (t.name === name) found = t;
|
|
183
|
+
}
|
|
184
|
+
return found;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/** 冬至/夏至分阴阳遁,再按雨水/谷雨或处暑/霜降分上中下元。 */
|
|
188
|
+
function zibaiHalfAt(terms, jd) {
|
|
189
|
+
const dz = latestTerm(terms, '冬至', jd);
|
|
190
|
+
const xz = latestTerm(terms, '夏至', jd);
|
|
191
|
+
const yang = Boolean(dz) && (!xz || dz.jd > xz.jd);
|
|
192
|
+
if (yang) {
|
|
193
|
+
const gy = latestTerm(terms, '谷雨', jd);
|
|
194
|
+
const ys = latestTerm(terms, '雨水', jd);
|
|
195
|
+
if (gy && dz && gy.jd > dz.jd) return { half: '阳遁', segment: '谷雨–夏至(下元)', offset: 3, boundary: '冬至后阳遁,谷雨至上元转下元' };
|
|
196
|
+
if (ys && dz && ys.jd > dz.jd) return { half: '阳遁', segment: '雨水–谷雨(中元)', offset: 6, boundary: '冬至后阳遁,雨水至上元转中元' };
|
|
197
|
+
return { half: '阳遁', segment: '冬至–雨水(上元)', offset: 0, boundary: '冬至后阳遁上元' };
|
|
198
|
+
}
|
|
199
|
+
const sj = latestTerm(terms, '霜降', jd);
|
|
200
|
+
const cs = latestTerm(terms, '处暑', jd);
|
|
201
|
+
if (sj && xz && sj.jd > xz.jd) return { half: '阴遁', segment: '霜降–冬至(下元)', offset: 3, boundary: '夏至后阴遁,霜降至上元转下元' };
|
|
202
|
+
if (cs && xz && cs.jd > xz.jd) return { half: '阴遁', segment: '处暑–霜降(中元)', offset: 6, boundary: '夏至后阴遁,处暑至上元转中元' };
|
|
203
|
+
return { half: '阴遁', segment: '夏至–处暑(上元)', offset: 0, boundary: '夏至后阴遁上元' };
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* 年月日时紫白飞星。
|
|
208
|
+
* @param {{year:number,month:number,day:number,hour?:number,minute?:number}} at 公历日期与时刻
|
|
209
|
+
*/
|
|
210
|
+
export function timeFlyingOf(at) {
|
|
211
|
+
const year = Number(at.year); const month = Number(at.month); const day = Number(at.day);
|
|
212
|
+
const hour = Number.isInteger(at.hour) ? at.hour : 12;
|
|
213
|
+
const minute = Number.isInteger(at.minute) ? at.minute : 0;
|
|
214
|
+
const jd = partsToJd({ year, month, day, hour, minute });
|
|
215
|
+
const terms = termWindow(year);
|
|
216
|
+
const guaYear = guaYearOf(year, month, day);
|
|
217
|
+
const yearBranch = yearBranchOf(guaYear);
|
|
218
|
+
const yearGroup = branchGroupOf(yearBranch);
|
|
219
|
+
const prevJie = previousJie(terms, jd);
|
|
220
|
+
const monthNum = JIE_MONTH_NUM[prevJie ? prevJie.name : '立春'] || 1;
|
|
221
|
+
const half = zibaiHalfAt(terms, jd);
|
|
222
|
+
const jdn = toJDN(year, month, day) + (hour >= 23 ? 1 : 0);
|
|
223
|
+
const dayIdx = dayGanzhiIndex(jdn);
|
|
224
|
+
const dayName = ganzhiName(dayIdx).name;
|
|
225
|
+
const dayBranch = ganzhiName(dayIdx).branch;
|
|
226
|
+
const dayGroup = branchGroupOf(dayBranch);
|
|
227
|
+
const hourIdx = Math.floor(((hour + 1) % 24) / 2);
|
|
228
|
+
|
|
229
|
+
const yearCenter = yearZibaiStar(guaYear);
|
|
230
|
+
const monthCenter = norm9(MONTH_START_BY_GROUP[yearGroup] - (monthNum - 1));
|
|
231
|
+
const dayBase = norm9(dayIdx + 1 + half.offset);
|
|
232
|
+
const dayCenter = half.half === '阳遁' ? dayBase : norm9(10 - dayBase);
|
|
233
|
+
const hourStart = half.half === '阳遁' ? HOUR_START_WINTER[dayGroup] : HOUR_START_SUMMER[dayGroup];
|
|
234
|
+
const hourCenter = norm9(hourStart + (half.half === '阳遁' ? hourIdx : -hourIdx));
|
|
235
|
+
|
|
236
|
+
return {
|
|
237
|
+
year: { ...zibaiChart(yearCenter, true), basis: `${guaYear} 年(立春界)年星 ${NINE_STARS[yearCenter].name} 入中顺飞` },
|
|
238
|
+
month: { ...zibaiChart(monthCenter, true), basis: `${yearBranch}年(${yearGroup})${prevJie ? prevJie.name : '立春'}月,${monthNum} 月 ${NINE_STARS[monthCenter].name} 入中顺飞` },
|
|
239
|
+
day: { ...zibaiChart(dayCenter, half.half === '阳遁'), basis: `${half.half}·${half.segment},${dayName}日第 ${dayIdx + 1} 位,${NINE_STARS[dayCenter].name} 入中${half.half === '阳遁' ? '顺' : '逆'}飞` },
|
|
240
|
+
hour: { ...zibaiChart(hourCenter, half.half === '阳遁'), basis: `${half.half}·${half.segment},${dayGroup}日 ${BRANCHES[hourIdx]}时,${NINE_STARS[hourCenter].name} 入中${half.half === '阳遁' ? '顺' : '逆'}飞` },
|
|
241
|
+
meta: {
|
|
242
|
+
guaYear, yearBranch, yearGroup, monthNum, monthJie: prevJie ? prevJie.name : '立春',
|
|
243
|
+
dayGanzhi: dayName, daySeq: dayIdx + 1, dayGroup,
|
|
244
|
+
hourBranch: BRANCHES[hourIdx], half: half.half, segment: half.segment, boundary: half.boundary,
|
|
245
|
+
note: '月按节气月(寅月为正月);日按公历日、23:00 后归次日;日/时星入中后的顺逆取「冬至顺、夏至逆」。另有「一律顺飞」与「月按农历月」派,本插件不合并,差异见本注。',
|
|
246
|
+
},
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
const pad2 = (n) => String(n).padStart(2, '0');
|
|
251
|
+
|
|
252
|
+
// ── 玄空飞星(下卦) ─────────────────────────────────────────────
|
|
253
|
+
// 口径:三元九运每运 20 年,1864 年起一运;运盘当运星入中顺飞。
|
|
254
|
+
// 山盘/向盘:取坐宫/向宫的运星入中,按「入中运星本宫同元龙」的阴阳定顺逆(五黄入中按本山阴阳)。
|
|
255
|
+
// 这一步是玄空挨星的关键:不是按坐山本身的阴阳,而是按入中运星原宫对应元龙的阴阳;
|
|
256
|
+
// 按本山阴阳会把旺山旺向算成每运固定的六局,与通行二十四山旺山旺向表对不上。
|
|
257
|
+
|
|
258
|
+
/** 三元九运:1864 起每 20 年一运,九运一轮(180 年)。 */
|
|
259
|
+
export function periodOfYear(year) {
|
|
260
|
+
const k = Math.floor((year - 1864) / 20);
|
|
261
|
+
const period = ((k % 9) + 9) % 9 + 1;
|
|
262
|
+
const start = 1864 + 20 * k;
|
|
263
|
+
return { period, start, end: start + 19, label: `${period}运(${start}–${start + 19})` };
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/** 三元龙阴阳:天元/人元四隅为阳、四正为阴;地元四正为阳、四隅为阴。 */
|
|
267
|
+
const YANG_MOUNTAINS = new Set(['乾', '巽', '艮', '坤', '甲', '庚', '壬', '丙', '寅', '申', '巳', '亥']);
|
|
268
|
+
const YUAN_NAME = ['地元', '天元', '人元'];
|
|
269
|
+
|
|
270
|
+
/** 24 山 → 三元龙 + 阴阳(MOUNTAINS_24 每卦三山依次为 地元/天元/人元)。 */
|
|
271
|
+
export const MOUNTAIN_META = {};
|
|
272
|
+
MOUNTAINS_24.forEach((x, i) => {
|
|
273
|
+
MOUNTAIN_META[x.m] = { ...x, yuan: YUAN_NAME[i % 3], yinYang: YANG_MOUNTAINS.has(x.m) ? '阳' : '阴' };
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
/** 飞星轨迹:中 → 乾 → 兑 → 艮 → 离 → 坎 → 坤 → 震 → 巽(即洛书 5→6→7→8→9→1→2→3→4)。 */
|
|
277
|
+
const FLY_SEQ = ['中', '乾', '兑', '艮', '离', '坎', '坤', '震', '巽'];
|
|
278
|
+
const NUM_TO_GUA = ['坎', '坤', '震', '巽', '中', '乾', '兑', '艮', '离']; // 洛书数 1–9 本位卦
|
|
279
|
+
const YUAN_INDEX = { 地元: 0, 天元: 1, 人元: 2 };
|
|
280
|
+
|
|
281
|
+
const mountainsOfGua = (gua) => MOUNTAINS_24.filter((x) => x.gua === gua).map((x) => MOUNTAIN_META[x.m]);
|
|
282
|
+
|
|
283
|
+
/** 入中星按轨迹飞布:forward=true 顺飞,false 逆飞。返回 宫 → 星数。 */
|
|
284
|
+
function fly(center, forward) {
|
|
285
|
+
const out = {};
|
|
286
|
+
for (let i = 0; i < 9; i++) {
|
|
287
|
+
const v = ((center - 1 + (forward ? i : -i)) % 9 + 9) % 9 + 1;
|
|
288
|
+
out[FLY_SEQ[i]] = v;
|
|
289
|
+
}
|
|
290
|
+
return out;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/** 入中运星定顺逆:取本宫(运星本位卦)同元龙之山的阴阳;五黄入中按本山阴阳。 */
|
|
294
|
+
function flightOf(mount, center) {
|
|
295
|
+
if (center === 5) {
|
|
296
|
+
return { forward: mount.yinYang === '阳', basis: `五黄入中,按${mount.m}本山${mount.yuan}${mount.yinYang}论` };
|
|
297
|
+
}
|
|
298
|
+
const home = NUM_TO_GUA[center - 1];
|
|
299
|
+
const hm = mountainsOfGua(home)[YUAN_INDEX[mount.yuan]];
|
|
300
|
+
return { forward: hm.yinYang === '阳', basis: `${center}${home}宫取${mount.yuan}${hm.m}(${hm.yinYang})` };
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// ── 兼向替卦 ─────────────────────────────────────────────────────
|
|
304
|
+
// 兼向超过 4.5°(离山界 3° 以内)用替卦:山星/向星改以该山的替星入中,顺逆仍按
|
|
305
|
+
// 「替星本宫同元龙」的阴阳定。替星取《沈氏玄空学》坤壬乙诀(1/2/6/7/9 五星);
|
|
306
|
+
// 三碧、四绿、八白在此诀中不作替星,属各家分歧,见 note。
|
|
307
|
+
export const TIGUA_SOURCE = '《沈氏玄空学》坤壬乙诀(子癸甲申一白、壬卯乙未坤二黑、乾亥辰巽巳戌六白、酉辛丑艮丙七赤、寅午庚丁九紫)';
|
|
308
|
+
|
|
309
|
+
export const TIGUA_TABLE = Object.freeze({
|
|
310
|
+
子: 1, 癸: 1, 甲: 1, 申: 1,
|
|
311
|
+
壬: 2, 卯: 2, 乙: 2, 未: 2, 坤: 2,
|
|
312
|
+
乾: 6, 亥: 6, 辰: 6, 巽: 6, 巳: 6, 戌: 6,
|
|
313
|
+
酉: 7, 辛: 7, 丑: 7, 艮: 7, 丙: 7,
|
|
314
|
+
寅: 9, 午: 9, 庚: 9, 丁: 9,
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
export const JIAN_XIANG_THRESHOLD = 4.5;
|
|
318
|
+
|
|
319
|
+
/** 兼向判定:offset 为坐山相对本山中心的度数,正数向下一山(顺时针)。 */
|
|
320
|
+
export function jianXiangOf(sitting, offset = 0) {
|
|
321
|
+
const off = Number(offset) || 0;
|
|
322
|
+
if (Math.abs(off) <= JIAN_XIANG_THRESHOLD) {
|
|
323
|
+
return {
|
|
324
|
+
active: false, offset: off, threshold: JIAN_XIANG_THRESHOLD,
|
|
325
|
+
sitting: sitting.m, jian: null, kind: null, sameYinYang: null,
|
|
326
|
+
note: `坐山偏离中心 ${off}°,未超 ${JIAN_XIANG_THRESHOLD}°,按正向下卦排盘。`,
|
|
327
|
+
};
|
|
328
|
+
}
|
|
329
|
+
const idx = MOUNTAINS_24.findIndex((x) => x.m === sitting.m);
|
|
330
|
+
const neighbor = MOUNTAINS_24[(idx + (off > 0 ? 1 : -1) + 24) % 24];
|
|
331
|
+
const sMeta = MOUNTAIN_META[sitting.m];
|
|
332
|
+
const nMeta = MOUNTAIN_META[neighbor.m];
|
|
333
|
+
const sameGua = neighbor.gua === sitting.gua;
|
|
334
|
+
return {
|
|
335
|
+
active: true, offset: off, threshold: JIAN_XIANG_THRESHOLD,
|
|
336
|
+
sitting: sitting.m, jian: neighbor.m, side: off > 0 ? '右' : '左',
|
|
337
|
+
kind: sameGua ? '同卦兼' : '出卦兼',
|
|
338
|
+
sameYinYang: nMeta.yinYang === sMeta.yinYang,
|
|
339
|
+
note: `坐山偏离中心 ${off}°,兼${neighbor.m}(${sameGua ? '同卦兼' : '出卦兼'}${sameGua && nMeta.yinYang === sMeta.yinYang ? ',同阴同阳' : ''});按沈氏替卦,山星以 ${TIGUA_TABLE[sitting.m]} 入中。`,
|
|
340
|
+
};
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/** 通行二十四山旺山旺向表(每项含正反两向;一运、九运无局)。 */
|
|
344
|
+
export const WANG_SHAN_WANG_XIANG = {
|
|
345
|
+
1: [],
|
|
346
|
+
2: [['乾', '巽'], ['亥', '巳'], ['丑', '未']],
|
|
347
|
+
3: [['卯', '酉'], ['乙', '辛'], ['辰', '戌']],
|
|
348
|
+
4: [['艮', '坤'], ['寅', '申'], ['甲', '庚']],
|
|
349
|
+
5: [['子', '午'], ['癸', '丁'], ['卯', '酉'], ['乙', '辛'], ['丑', '未'], ['辰', '戌']],
|
|
350
|
+
6: [['艮', '坤'], ['寅', '申'], ['甲', '庚']],
|
|
351
|
+
7: [['卯', '酉'], ['乙', '辛'], ['辰', '戌']],
|
|
352
|
+
8: [['乾', '巽'], ['亥', '巳'], ['丑', '未']],
|
|
353
|
+
9: [],
|
|
354
|
+
};
|
|
355
|
+
|
|
356
|
+
export const XUANKONG_SOURCES = [
|
|
357
|
+
'三元九运(1864 年起一运、每运 20 年,九运 180 年一轮)',
|
|
358
|
+
'玄空下卦挨星(运盘顺飞;山盘/向盘以入中运星本宫同元龙阴阳定顺逆,五黄入中按本山阴阳)',
|
|
359
|
+
'兼向替卦:坐山偏离中心超 4.5° 用《沈氏玄空学》坤壬乙诀替星入中,顺逆按替星本宫同元龙',
|
|
360
|
+
'年月日时紫白(公式派:年逐年退一位;月按年支三组起 8/5/2;日按冬至夏至三段起 1/7/4 与 9/3/6;时按日支三组子时起 1/7/4 与 9/3/6)',
|
|
361
|
+
'二十四山旺山旺向通行表(每项含正反两向;一运、九运无局;只对正向下卦)',
|
|
362
|
+
];
|
|
363
|
+
|
|
364
|
+
export const XUANKONG_DISCLAIMER = '玄空排盘为传统通行口径的本地计算,属民俗参考,不构成现场勘舆结论,仅供娱乐;兼向超 4.5° 按沈氏替卦,年月日时紫白按公式派;各家另有异说,形峦与现场勘舆不在本工具范围内。';
|
|
365
|
+
|
|
366
|
+
/** 玄空排盘:运盘 + 山盘 + 向盘 + 当运星 + 合十 + 表法旺山旺向;兼向超阈值改用替卦。 */
|
|
367
|
+
export function xuankongChart(period, sitting, facing, opts = {}) {
|
|
368
|
+
const sit = MOUNTAIN_META[sitting];
|
|
369
|
+
const face = MOUNTAIN_META[facing];
|
|
370
|
+
const offset = Number(opts.offset) || 0;
|
|
371
|
+
const jianXiang = jianXiangOf(sit, offset);
|
|
372
|
+
const yun = fly(period, true);
|
|
373
|
+
const sCenter = jianXiang.active ? TIGUA_TABLE[sit.m] : yun[sit.gua];
|
|
374
|
+
const fCenter = jianXiang.active ? TIGUA_TABLE[face.m] : yun[face.gua];
|
|
375
|
+
const sFly = flightOf(sit, sCenter);
|
|
376
|
+
const fFly = flightOf(face, fCenter);
|
|
377
|
+
const shan = fly(sCenter, sFly.forward);
|
|
378
|
+
const xiang = fly(fCenter, fFly.forward);
|
|
379
|
+
const palaces = PALACE_ORDER.map((palace, i) => ({
|
|
380
|
+
palace,
|
|
381
|
+
direction: palace === '中' ? '中宫' : PALACE_DIRECTION[palace],
|
|
382
|
+
luoshu: i + 1,
|
|
383
|
+
yun: yun[palace],
|
|
384
|
+
shan: shan[palace],
|
|
385
|
+
xiang: xiang[palace],
|
|
386
|
+
}));
|
|
387
|
+
const wheres = (map) => PALACE_ORDER.filter((p) => map[p] === period);
|
|
388
|
+
const pairs = WANG_SHAN_WANG_XIANG[period];
|
|
389
|
+
const hit = pairs.find(([a, b]) => (sit.m === a && face.m === b) || (sit.m === b && face.m === a));
|
|
390
|
+
const byChart = shan[sit.gua] === period && xiang[face.gua] === period;
|
|
391
|
+
const replaced = {
|
|
392
|
+
shan: jianXiang.active && sCenter !== yun[sit.gua],
|
|
393
|
+
xiang: jianXiang.active && fCenter !== yun[face.gua],
|
|
394
|
+
};
|
|
395
|
+
const note = jianXiang.active
|
|
396
|
+
? `替卦盘不套正向旺山旺向表;${byChart ? '本盘当运山星/向星仍到坐向。' : '本盘当运山星/向星未同时到坐向。'}${!replaced.shan && !replaced.xiang ? '山向两替星均「用替而未替」,实际仍按原卦气。' : ''}`
|
|
397
|
+
: byChart && hit
|
|
398
|
+
? '盘面与表法一致:本局到山到向。'
|
|
399
|
+
: byChart
|
|
400
|
+
? '盘面到山到向,但不在通行表法所列之局(表法与盘面口径不同,并列不合并)。'
|
|
401
|
+
: hit
|
|
402
|
+
? '通行表法列为旺山旺向,但本盘当运山星/向星未同时落坐向(表法与盘面口径不同,并列不合并)。'
|
|
403
|
+
: '本局既未到山到向,也不在表法旺山旺向之列。';
|
|
404
|
+
return {
|
|
405
|
+
palaces,
|
|
406
|
+
sitting: sit,
|
|
407
|
+
facing: face,
|
|
408
|
+
yun,
|
|
409
|
+
shan,
|
|
410
|
+
xiang,
|
|
411
|
+
yunCenter: period,
|
|
412
|
+
jianXiang,
|
|
413
|
+
shanCenter: {
|
|
414
|
+
star: sCenter, ...sFly,
|
|
415
|
+
mode: jianXiang.active ? '替卦' : '下卦',
|
|
416
|
+
original: yun[sit.gua],
|
|
417
|
+
replaced: replaced.shan,
|
|
418
|
+
},
|
|
419
|
+
xiangCenter: {
|
|
420
|
+
star: fCenter, ...fFly,
|
|
421
|
+
mode: jianXiang.active ? '替卦' : '下卦',
|
|
422
|
+
original: yun[face.gua],
|
|
423
|
+
replaced: replaced.xiang,
|
|
424
|
+
},
|
|
425
|
+
dangYun: { shan: wheres(shan), xiang: wheres(xiang) },
|
|
426
|
+
heshi: {
|
|
427
|
+
shan: PALACE_ORDER.filter((p) => shan[p] + yun[p] === 10),
|
|
428
|
+
xiang: PALACE_ORDER.filter((p) => xiang[p] + yun[p] === 10),
|
|
429
|
+
},
|
|
430
|
+
wangShanWangXiang: {
|
|
431
|
+
byChart,
|
|
432
|
+
byTable: jianXiang.active ? null : Boolean(hit),
|
|
433
|
+
table: pairs.map(([a, b]) => `${a}${b}`),
|
|
434
|
+
note,
|
|
435
|
+
},
|
|
436
|
+
};
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
/** DST 校正出生日期(1986–1991),返回校正后的年月日。 */
|
|
440
|
+
function civilDate(input) {
|
|
441
|
+
const dst = input.dst === false
|
|
442
|
+
? { year: input.year, month: input.month, day: input.day, hour: 0, minute: 0, applied: false, note: null }
|
|
443
|
+
: applyDst({ year: input.year, month: input.month, day: input.day, hour: 0, minute: 0 });
|
|
444
|
+
return { year: dst.year, month: dst.month, day: dst.day, dst };
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
const SOURCES = [
|
|
448
|
+
'大游年歌(八宅通行口径)',
|
|
449
|
+
'年家紫白飞星(纯公式:以 2026 丙午一白入中、逐年退一位、九年一轮;按立春换年)',
|
|
450
|
+
'24 山归属与度数(八宅通行口径,每山 15°)',
|
|
451
|
+
];
|
|
452
|
+
|
|
453
|
+
const DISCLAIMER = '八宅与飞星为传统通行口径的本地计算,属民俗参考,不构成现场勘舆结论,仅供娱乐;实际户型以现场为准。';
|
|
454
|
+
|
|
455
|
+
/**
|
|
456
|
+
* 风水主函数。
|
|
457
|
+
* @param {{action?:'natal'|'advise'|'xuankong', year?:number, month?:number, day?:number, gender?:'male'|'female',
|
|
458
|
+
* fortuneYear?:number, sitting?:string, facing?:string, period?:number, dst?:boolean}} input
|
|
459
|
+
* natal(默认):要生日 + 性别 + 流年(缺流年则只算命卦与八星)。
|
|
460
|
+
* advise:要坐山或向山(24 山,给一即可);出生信息可选,给了才做命宅配。
|
|
461
|
+
* xuankong:要坐山或向山;运由 period(1–9)或 fortuneYear 定,缺省按九运并注明。
|
|
462
|
+
*/
|
|
463
|
+
export function buildFengshui(input) {
|
|
464
|
+
if (input.action === 'advise') return buildAdvise(input);
|
|
465
|
+
if (input.action === 'xuankong') return buildXuankong(input);
|
|
466
|
+
const gender = input.gender;
|
|
467
|
+
if (gender !== 'male' && gender !== 'female') {
|
|
468
|
+
throw fail('E_INPUT', '风水命卦需要性别:男女命卦算法不同,不可缺省');
|
|
469
|
+
}
|
|
470
|
+
if (!Number.isInteger(input.year) || !Number.isInteger(input.month) || !Number.isInteger(input.day)) {
|
|
471
|
+
throw fail('E_INPUT', '风水命卦需要完整出生年月日(公历)——只知道年份不够:命卦按立春界划分出生年');
|
|
472
|
+
}
|
|
473
|
+
const civil = civilDate(input);
|
|
474
|
+
const guaYear = guaYearOf(civil.year, civil.month, civil.day);
|
|
475
|
+
const { num, gua, direction } = mingGua(guaYear, gender);
|
|
476
|
+
const stars = eightStars(gua);
|
|
477
|
+
const eastFour = ['坎', '离', '震', '巽'].includes(gua);
|
|
478
|
+
const fortuneYear = Number.isInteger(input.fortuneYear) ? input.fortuneYear : null;
|
|
479
|
+
|
|
480
|
+
return {
|
|
481
|
+
system: 'fengshui',
|
|
482
|
+
meta: {
|
|
483
|
+
birth: `${civil.year}-${pad2(civil.month)}-${pad2(civil.day)}`,
|
|
484
|
+
guaYear,
|
|
485
|
+
gender: gender === 'male' ? '男' : '女',
|
|
486
|
+
fortuneYear,
|
|
487
|
+
...(civil.dst.applied ? { dst: civil.dst.note } : {}),
|
|
488
|
+
...(fortuneYear === null ? { note: '未给流年:未算年紫白飞星(需要时补 fortuneYear)。' } : {}),
|
|
489
|
+
scope: '八宅命卦 + 大游年八星 + 年紫白;玄空飞星见 action=xuankong;不含形峦与现场勘舆。',
|
|
490
|
+
},
|
|
491
|
+
mingGua: {
|
|
492
|
+
num,
|
|
493
|
+
gua,
|
|
494
|
+
direction,
|
|
495
|
+
group: eastFour ? '东四命' : '西四命',
|
|
496
|
+
compatible: eastFour ? '宜东四宅(坐坎离震巽)' : '宜西四宅(坐乾坤艮兑)',
|
|
497
|
+
},
|
|
498
|
+
luckyDirections: LUCKY_STARS.map((s) => ({ star: s, ...stars[s] })),
|
|
499
|
+
unluckyDirections: UNLUCKY_STARS.map((s) => ({ star: s, ...stars[s] })),
|
|
500
|
+
...(fortuneYear === null ? {} : { yearlyFlying: yearlyFlying(fortuneYear) }),
|
|
501
|
+
sources: SOURCES,
|
|
502
|
+
disclaimer: DISCLAIMER,
|
|
503
|
+
};
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
/** 坐向解析:至少给坐或向;两者都给则必须对宫。 */
|
|
507
|
+
function resolveSittingFacing(input) {
|
|
508
|
+
const hasSitting = typeof input.sitting === 'string' && input.sitting.trim() !== '';
|
|
509
|
+
const hasFacing = typeof input.facing === 'string' && input.facing.trim() !== '';
|
|
510
|
+
if (!hasSitting && !hasFacing) {
|
|
511
|
+
throw fail('E_INPUT', '缺少坐向:sitting(坐山)或 facing(向山)至少给一个——按罗盘 24 山报坐或报向,例如「坐北朝南」对应坐子向午');
|
|
512
|
+
}
|
|
513
|
+
if (hasSitting && hasFacing) {
|
|
514
|
+
const s = mountainInfo(input.sitting.trim());
|
|
515
|
+
const f = mountainInfo(input.facing.trim());
|
|
516
|
+
if ((s.index + 12) % 24 !== f.index) {
|
|
517
|
+
throw fail('E_INPUT', `坐向不自洽:坐${s.m}(${s.deg}°)与向${f.m}(${f.deg}°)不是对宫(相差应 180°,实测 ${((f.deg - s.deg) + 360) % 360}°)——请核对罗盘读数。`);
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
const sitting = hasSitting ? mountainInfo(input.sitting.trim()) : sittingFromFacing(input.facing.trim());
|
|
521
|
+
const facing = MOUNTAINS_24[(sitting.index + 12) % 24];
|
|
522
|
+
return { sitting, facing };
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
/** 宅卦坐向 + 门主灶厕布局倾向 + 命宅配。 */
|
|
526
|
+
function buildAdvise(input) {
|
|
527
|
+
const { sitting, facing } = resolveSittingFacing(input);
|
|
528
|
+
const houseGua = sitting.gua;
|
|
529
|
+
const stars = eightStars(houseGua);
|
|
530
|
+
const lucky = LUCKY_STARS.map((s) => stars[s]);
|
|
531
|
+
const unlucky = UNLUCKY_STARS.map((s) => stars[s]);
|
|
532
|
+
const dir = (x) => `${x.direction}(${x.palace}宫)`;
|
|
533
|
+
|
|
534
|
+
const starChart = [...LUCKY_STARS, ...UNLUCKY_STARS].map((s) => ({
|
|
535
|
+
star: s,
|
|
536
|
+
lucky: LUCKY_STARS.includes(s),
|
|
537
|
+
...stars[s],
|
|
538
|
+
role: STAR_LAYOUT_ROLE[s],
|
|
539
|
+
}));
|
|
540
|
+
|
|
541
|
+
const layout = {
|
|
542
|
+
door: `门宜开于吉方:首选生气方 ${dir(lucky[0])},次选延年方 ${dir(lucky[1])}、天医方 ${dir(lucky[2])}。`,
|
|
543
|
+
stove: `灶宜「坐凶向吉」:灶身压凶方(首选绝命方 ${dir(unlucky[0])}、五鬼方 ${dir(unlucky[1])}),灶口朝吉方。`,
|
|
544
|
+
bed: `主卧与床头宜安吉方:生气方 ${dir(lucky[0])} 或延年方 ${dir(lucky[1])};床头宜向吉方。`,
|
|
545
|
+
toilet: `厕所宜压凶方:祸害方 ${dir(unlucky[3])} 或六煞方 ${dir(unlucky[2])}(污去凶气,宜静常闭)。`,
|
|
546
|
+
};
|
|
547
|
+
|
|
548
|
+
let mingMatch = null;
|
|
549
|
+
const hasBirth = Number.isInteger(input.year) && Number.isInteger(input.month) && Number.isInteger(input.day)
|
|
550
|
+
&& (input.gender === 'male' || input.gender === 'female');
|
|
551
|
+
if (hasBirth) {
|
|
552
|
+
const civil = civilDate(input);
|
|
553
|
+
const guaYear = guaYearOf(civil.year, civil.month, civil.day);
|
|
554
|
+
const ming = mingGua(guaYear, input.gender);
|
|
555
|
+
const mGroup = guaGroup(ming.gua);
|
|
556
|
+
const hGroup = guaGroup(houseGua);
|
|
557
|
+
const same = mGroup === hGroup;
|
|
558
|
+
const mingLucky = LUCKY_STARS.map((s) => `${s}位${eightStars(ming.gua)[s].direction}`).join('、');
|
|
559
|
+
mingMatch = {
|
|
560
|
+
birth: `${civil.year}-${pad2(civil.month)}-${pad2(civil.day)}`,
|
|
561
|
+
guaYear,
|
|
562
|
+
gender: input.gender === 'male' ? '男' : '女',
|
|
563
|
+
mingGua: { gua: ming.gua, direction: ming.direction, group: `${mGroup}命` },
|
|
564
|
+
houseGua: { gua: houseGua, group: `${hGroup}宅` },
|
|
565
|
+
sameGroup: same,
|
|
566
|
+
text: same
|
|
567
|
+
? `命宅相配(${mGroup}命住${hGroup}宅):以宅之八星盘布局即可,个人吉方与宅吉方大体同向。`
|
|
568
|
+
: `命宅不配(${mGroup}命住${hGroup}宅):传统调停法——门与床以命卦吉方为先(此命四吉方:${mingLucky}),灶仍按宅卦坐凶向吉,两盘各自取用。`,
|
|
569
|
+
};
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
return {
|
|
573
|
+
system: 'fengshui',
|
|
574
|
+
meta: {
|
|
575
|
+
action: 'advise',
|
|
576
|
+
...(hasBirth ? {} : { birthNote: '未提供生辰:未做命宅配;如需按个人命卦调停门床,请补充出生年月日与性别。' }),
|
|
577
|
+
sitting: `${sitting.m}山(罗盘 ${sitting.deg}°)`,
|
|
578
|
+
facing: `${facing.m}山(罗盘 ${facing.deg}°)`,
|
|
579
|
+
houseGua,
|
|
580
|
+
houseGroup: `${guaGroup(houseGua)}宅`,
|
|
581
|
+
scope: '门主灶厕走向按八宅通行口径给倾向,玄空飞星见 action=xuankong;不含形峦、不是现场勘舆结论。',
|
|
582
|
+
},
|
|
583
|
+
houseStars: starChart,
|
|
584
|
+
layout,
|
|
585
|
+
...(mingMatch ? { mingMatch } : {}),
|
|
586
|
+
sources: SOURCES,
|
|
587
|
+
disclaimer: DISCLAIMER,
|
|
588
|
+
};
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
/** 兼向读数:接受坐/向的偏移度数或罗盘度数,两边都给时必须一致。 */
|
|
592
|
+
function resolveOffset(input, sitting, facing) {
|
|
593
|
+
const present = (v) => v !== undefined && v !== null && v !== '';
|
|
594
|
+
const number = (v, label) => {
|
|
595
|
+
if (typeof v !== 'number' || !Number.isFinite(v)) throw fail('E_INPUT', `${label} 必须是数字(度),收到「${v}」`);
|
|
596
|
+
return v;
|
|
597
|
+
};
|
|
598
|
+
const degreeOffset = (v, mountain, label) => {
|
|
599
|
+
const deg = number(v, label);
|
|
600
|
+
if (deg < 0 || deg > 360) throw fail('E_INPUT', `${label} 必须在 0–360° 之间(罗盘度数),收到「${deg}」`);
|
|
601
|
+
const diff = ((deg - mountain.deg + 540) % 360) - 180;
|
|
602
|
+
if (Math.abs(diff) > 7.5 + 1e-9) throw fail('E_INPUT', `${label} ${deg}° 不在${mountain.m}山范围内(${mountain.m}中心 ${mountain.deg}°,每山 15°),请核对坐向。`);
|
|
603
|
+
return diff;
|
|
604
|
+
};
|
|
605
|
+
const values = [];
|
|
606
|
+
if (present(input.sittingOffset)) values.push(number(input.sittingOffset, 'sittingOffset'));
|
|
607
|
+
if (present(input.sittingDegree)) values.push(degreeOffset(input.sittingDegree, sitting, 'sittingDegree'));
|
|
608
|
+
if (present(input.facingOffset)) values.push(number(input.facingOffset, 'facingOffset'));
|
|
609
|
+
if (present(input.facingDegree)) values.push(degreeOffset(input.facingDegree, facing, 'facingDegree'));
|
|
610
|
+
if (!values.length) return { offset: 0, source: '未报兼向度数,按正向(0°)' };
|
|
611
|
+
const offset = values[0];
|
|
612
|
+
if (values.some((v) => Math.abs(v - offset) > 1e-9)) throw fail('E_INPUT', `坐向兼度不一致:${values.join('° / ')}°——坐与向应同向偏离,请只报一个偏移或核对度数。`);
|
|
613
|
+
if (Math.abs(offset) > 7.5 + 1e-9) throw fail('E_INPUT', `兼向偏移必须在 −7.5°–7.5°(相对本山中心),收到「${offset}」——超过 7.5° 已进入下一山。`);
|
|
614
|
+
return { offset, source: '按所报兼向度数' };
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
/** 飞星日期:fortuneDate=YYYY-MM-DD、fortuneTime=HH:MM;缺省 12:00。 */
|
|
618
|
+
function parseFortuneAt(input) {
|
|
619
|
+
const present = (v) => v !== undefined && v !== null && v !== '';
|
|
620
|
+
if (!present(input.fortuneDate)) return null;
|
|
621
|
+
if (typeof input.fortuneDate !== 'string' || !/^\d{4}-\d{2}-\d{2}$/.test(input.fortuneDate)) {
|
|
622
|
+
throw fail('E_INPUT', `fortuneDate 需为 YYYY-MM-DD,收到「${input.fortuneDate}」`);
|
|
623
|
+
}
|
|
624
|
+
const [year, month, day] = input.fortuneDate.split('-').map(Number);
|
|
625
|
+
const probe = new Date(Date.UTC(year, month - 1, day));
|
|
626
|
+
if (probe.getUTCFullYear() !== year || probe.getUTCMonth() !== month - 1 || probe.getUTCDate() !== day) {
|
|
627
|
+
throw fail('E_INPUT', `fortuneDate 不是有效日期:「${input.fortuneDate}」`);
|
|
628
|
+
}
|
|
629
|
+
const rawTime = present(input.fortuneTime) ? input.fortuneTime : '12:00';
|
|
630
|
+
if (typeof rawTime !== 'string' || !/^\d{2}:\d{2}$/.test(rawTime)) {
|
|
631
|
+
throw fail('E_INPUT', `fortuneTime 需为 HH:MM,收到「${rawTime}」`);
|
|
632
|
+
}
|
|
633
|
+
const [hour, minute] = rawTime.split(':').map(Number);
|
|
634
|
+
if (hour > 23 || minute > 59) throw fail('E_INPUT', `fortuneTime 超出范围(HH:MM):「${rawTime}」`);
|
|
635
|
+
const dst = input.dst === false ? null : applyDst({ year, month, day, hour, minute });
|
|
636
|
+
const at = dst ? { year: dst.year, month: dst.month, day: dst.day, hour: dst.hour, minute: dst.minute } : { year, month, day, hour, minute };
|
|
637
|
+
return { at, dst };
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
/** 玄空排盘:坐向 + 运 → 九宫盘、当运星、合十、表法旺山旺向;兼向超阈值用替卦。 */
|
|
641
|
+
function buildXuankong(input) {
|
|
642
|
+
const { sitting, facing } = resolveSittingFacing(input);
|
|
643
|
+
const offsetInfo = resolveOffset(input, sitting, facing);
|
|
644
|
+
const fortune = parseFortuneAt(input);
|
|
645
|
+
let period;
|
|
646
|
+
let yearInfo = null;
|
|
647
|
+
let note;
|
|
648
|
+
if (input.period !== undefined && input.period !== null && !Number.isInteger(input.period)) {
|
|
649
|
+
throw fail('E_INPUT', `运必须是 1–9 的整数(三元九运),收到「${input.period}」——一运 1864 起、九运 2024–2043。`);
|
|
650
|
+
}
|
|
651
|
+
if (Number.isInteger(input.period)) {
|
|
652
|
+
if (input.period < 1 || input.period > 9) {
|
|
653
|
+
throw fail('E_INPUT', `运必须是 1–9 的整数(三元九运),收到「${input.period}」——一运 1864 起、九运 2024–2043。`);
|
|
654
|
+
}
|
|
655
|
+
period = input.period;
|
|
656
|
+
} else if (Number.isInteger(input.fortuneYear)) {
|
|
657
|
+
yearInfo = periodOfYear(input.fortuneYear);
|
|
658
|
+
period = yearInfo.period;
|
|
659
|
+
} else if (fortune) {
|
|
660
|
+
yearInfo = periodOfYear(guaYearOf(fortune.at.year, fortune.at.month, fortune.at.day));
|
|
661
|
+
period = yearInfo.period;
|
|
662
|
+
note = `未显式给运:按飞星日期归入 ${yearInfo.label}。`;
|
|
663
|
+
} else {
|
|
664
|
+
period = 9;
|
|
665
|
+
note = '未给运:默认按九运(2024–2043)排盘;可传 period(1–9)或 fortuneYear 指定。';
|
|
666
|
+
}
|
|
667
|
+
const chart = xuankongChart(period, sitting.m, facing.m, { offset: offsetInfo.offset });
|
|
668
|
+
const sitMeta = MOUNTAIN_META[sitting.m];
|
|
669
|
+
const faceMeta = MOUNTAIN_META[facing.m];
|
|
670
|
+
const center = (c) => ({
|
|
671
|
+
star: c.star, name: NINE_STARS[c.star].name, fly: c.forward ? '顺飞' : '逆飞', basis: c.basis,
|
|
672
|
+
mode: c.mode, original: c.original, replaced: c.replaced,
|
|
673
|
+
});
|
|
674
|
+
const where = (list) => list.map((p) => (p === '中' ? '中宫' : `${p}宫${PALACE_DIRECTION[p]}`)).join('、') || '无';
|
|
675
|
+
let timeFlying = null;
|
|
676
|
+
if (fortune) {
|
|
677
|
+
timeFlying = timeFlyingOf(fortune.at);
|
|
678
|
+
if (fortune.dst && fortune.dst.applied) timeFlying.meta.dst = fortune.dst.note;
|
|
679
|
+
} else if (Number.isInteger(input.fortuneYear)) {
|
|
680
|
+
const centerStar = yearZibaiStar(input.fortuneYear);
|
|
681
|
+
timeFlying = {
|
|
682
|
+
year: { ...zibaiChart(centerStar, true), basis: `${input.fortuneYear} 年紫白 ${NINE_STARS[centerStar].name} 入中顺飞` },
|
|
683
|
+
month: null, day: null, hour: null,
|
|
684
|
+
meta: { guaYear: input.fortuneYear, note: '只给年份:已排年紫白;补 fortuneDate(YYYY-MM-DD)与 fortuneTime(HH:MM)可排月、日、时。' },
|
|
685
|
+
};
|
|
686
|
+
}
|
|
687
|
+
return {
|
|
688
|
+
system: 'fengshui',
|
|
689
|
+
meta: {
|
|
690
|
+
action: 'xuankong',
|
|
691
|
+
period,
|
|
692
|
+
periodLabel: yearInfo ? yearInfo.label : `${period}运`,
|
|
693
|
+
...(yearInfo ? { year: input.fortuneYear || (fortune && fortune.at.year), periodStart: yearInfo.start, periodEnd: yearInfo.end } : {}),
|
|
694
|
+
sitting: `${sitting.m}山(罗盘 ${sitting.deg}°)`,
|
|
695
|
+
facing: `${facing.m}山(罗盘 ${facing.deg}°)`,
|
|
696
|
+
sittingGua: sitting.gua,
|
|
697
|
+
facingGua: facing.gua,
|
|
698
|
+
sittingYuan: `${sitMeta.yuan}${sitMeta.yinYang}`,
|
|
699
|
+
facingYuan: `${faceMeta.yuan}${faceMeta.yinYang}`,
|
|
700
|
+
sittingOffset: offsetInfo.offset,
|
|
701
|
+
jianXiang: chart.jianXiang,
|
|
702
|
+
shanCenter: center(chart.shanCenter),
|
|
703
|
+
xiangCenter: center(chart.xiangCenter),
|
|
704
|
+
...(note ? { note } : {}),
|
|
705
|
+
scope: '玄空下卦/替卦:运盘 + 山盘 + 向盘 + 当运星 + 合十 + 表法旺山旺向;兼向超 4.5° 按沈氏替卦;年月日时紫白按公式派;不含形峦与现场勘舆。',
|
|
706
|
+
},
|
|
707
|
+
palaces: chart.palaces,
|
|
708
|
+
dangYun: {
|
|
709
|
+
shan: { palaces: chart.dangYun.shan, text: where(chart.dangYun.shan) },
|
|
710
|
+
xiang: { palaces: chart.dangYun.xiang, text: where(chart.dangYun.xiang) },
|
|
711
|
+
},
|
|
712
|
+
heshi: { shan: chart.heshi.shan, xiang: chart.heshi.xiang },
|
|
713
|
+
wangShanWangXiang: chart.wangShanWangXiang,
|
|
714
|
+
...(timeFlying ? { timeFlying } : {}),
|
|
715
|
+
sources: XUANKONG_SOURCES,
|
|
716
|
+
disclaimer: XUANKONG_DISCLAIMER,
|
|
717
|
+
};
|
|
718
|
+
}
|