dsh-metaphysics 0.17.1 → 0.18.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +99 -23
- package/README.md +35 -10
- package/client.js +2098 -35
- package/engine/almanac.js +215 -0
- package/engine/bazi.js +10 -2
- package/engine/chenggu.js +126 -0
- package/engine/dst.js +68 -0
- package/engine/fengshui.js +288 -0
- package/engine/geo.js +1 -1
- package/engine/hepan.js +82 -1
- package/engine/index.js +277 -2
- package/engine/name.js +303 -0
- package/engine/numerology.js +60 -0
- package/engine/rectify.js +1 -0
- package/engine/star.js +82 -0
- package/engine/trend.js +193 -0
- package/engine/xiaoliuren.js +64 -0
- package/engine/zeri.js +127 -0
- package/engine/ziwei.js +7 -2
- package/index.js +93 -25
- package/locale/en.json +1 -1
- package/locale/zh.json +1 -1
- package/package.json +15 -3
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
// 每日黄历(本地确定性 · 通行简表口径)
|
|
2
|
+
//
|
|
3
|
+
// 提供:农历日期、年/月/日干支(年按立春界,另列春节界生肖)、节气、建除十二神、
|
|
4
|
+
// 冲煞(冲什么生肖 / 煞在何方)、财神与喜神方位、建除简表宜忌、每日一卦。
|
|
5
|
+
//
|
|
6
|
+
// 诚实边界:本插件不依赖 lunar-javascript,逐日神煞(吉神凶煞)、二十八宿值日、
|
|
7
|
+
// 彭祖百忌、胎神等全量黄历数据未收录——输出里如实标注「未收录」,不臆造。
|
|
8
|
+
// 宜忌只按「建除十二神」通行简表给出(传统择日的第一层判据),不是逐日全量黄历。
|
|
9
|
+
import { solarToLunar } from './lunar.js';
|
|
10
|
+
import {
|
|
11
|
+
toJDN, fromJDN, dayGanzhiIndex, ganzhiName, termWindow, termInstantJd, partsToJd, STEMS, BRANCHES, TERM_NAMES,
|
|
12
|
+
} from './calendar.js';
|
|
13
|
+
import { HEX64 } from './interpret-data.js';
|
|
14
|
+
import { assertCivil, daysInMonth } from './validate.js';
|
|
15
|
+
|
|
16
|
+
/** 建除十二神(按日支相对月支顺数)。 */
|
|
17
|
+
export const ZHIXING = ['建', '除', '满', '平', '定', '执', '破', '危', '成', '收', '开', '闭'];
|
|
18
|
+
|
|
19
|
+
/** 建除十二神宜忌(通行简表;传统黄历在此基础上再叠神煞与宿值)。 */
|
|
20
|
+
export const ZHIXING_YI = {
|
|
21
|
+
建: ['出行', '上任', '会友', '上书', '见工'],
|
|
22
|
+
除: ['除服', '疗病', '扫舍', '出行'],
|
|
23
|
+
满: ['祈福', '祭祀', '结亲', '开市', '交易'],
|
|
24
|
+
平: ['修饰垣墙', '平治道涂'],
|
|
25
|
+
定: ['嫁娶', '订盟', '纳采', '祭祀', '祈福'],
|
|
26
|
+
执: ['祭祀', '祈福', '捕捉', '纳畜'],
|
|
27
|
+
破: ['破屋', '坏垣', '求医'],
|
|
28
|
+
危: ['祭祀', '祈福', '安床'],
|
|
29
|
+
成: ['嫁娶', '开市', '入学', '上任', '修造'],
|
|
30
|
+
收: ['纳财', '收账', '进人口'],
|
|
31
|
+
开: ['开市', '交易', '嫁娶', '入学', '动土'],
|
|
32
|
+
闭: ['筑堤', '埋穴', '补垣'],
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
export const ZHIXING_JI = {
|
|
36
|
+
建: ['动土', '开仓', '出货财', '安葬'],
|
|
37
|
+
除: ['求财', '赴任', '嫁娶'],
|
|
38
|
+
满: ['服药', '栽种', '安葬'],
|
|
39
|
+
平: ['祈福', '求嗣'],
|
|
40
|
+
定: ['诉讼', '出行'],
|
|
41
|
+
执: ['开市', '求财', '出行'],
|
|
42
|
+
破: ['嫁娶', '开市', '动土'],
|
|
43
|
+
危: ['登高', '行船', '出行'],
|
|
44
|
+
成: ['诉讼'],
|
|
45
|
+
收: ['开市', '出行', '安葬'],
|
|
46
|
+
开: ['安葬'],
|
|
47
|
+
闭: ['开市', '出行', '求医'],
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
/** 财神方位歌(按日干)。 */
|
|
51
|
+
const CAI_SHEN = { 甲: '东北', 乙: '东北', 丙: '西南', 丁: '西南', 戊: '正北', 己: '正北', 庚: '正东', 辛: '正东', 壬: '正南', 癸: '正南' };
|
|
52
|
+
|
|
53
|
+
/** 喜神方位(通行口诀,按日干)。 */
|
|
54
|
+
const XI_SHEN = { 甲: '东北', 乙: '西北', 丙: '西南', 丁: '正南', 戊: '东南', 己: '东北', 庚: '西北', 辛: '西南', 壬: '正南', 癸: '东南' };
|
|
55
|
+
|
|
56
|
+
/** 日支 → 冲的生肖 + 煞方(传统:申子辰煞南、寅午戌煞北、巳酉丑煞东、亥卯未煞西)。 */
|
|
57
|
+
export const SHENGXIAO = ['鼠', '牛', '虎', '兔', '龙', '蛇', '马', '羊', '猴', '鸡', '狗', '猪'];
|
|
58
|
+
const SHA_BY_GROUP = { 申子辰: '南', 寅午戌: '北', 巳酉丑: '东', 亥卯未: '西' };
|
|
59
|
+
function shaOf(branch) {
|
|
60
|
+
for (const [g, v] of Object.entries(SHA_BY_GROUP)) if (g.includes(branch)) return v;
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** 24 小时制 → 时辰地支。 */
|
|
65
|
+
export function hourToBranch(hour, minute = 0) {
|
|
66
|
+
const h = Number.isInteger(hour) ? ((hour % 24) + 24) % 24 : 12;
|
|
67
|
+
// 子时 23:00–00:59;分钟不改变时辰(真太阳时已在调用方处理)
|
|
68
|
+
return BRANCHES[Math.floor(((h + 1) % 24) / 2)];
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** 由日支与月支推建除十二神。 */
|
|
72
|
+
export function zhixingOf(dayBranch, monthBranch) {
|
|
73
|
+
const d = BRANCHES.indexOf(dayBranch);
|
|
74
|
+
const m = BRANCHES.indexOf(monthBranch);
|
|
75
|
+
return ZHIXING[((d - m) % 12 + 12) % 12];
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** 农历月/日中文。 */
|
|
79
|
+
const MONTH_TEXT = ['正', '二', '三', '四', '五', '六', '七', '八', '九', '十', '冬', '腊'];
|
|
80
|
+
const DAY_TENS = ['初', '十', '廿'];
|
|
81
|
+
const DAY_UNITS = ['十', '一', '二', '三', '四', '五', '六', '七', '八', '九'];
|
|
82
|
+
function dayText(day) {
|
|
83
|
+
if (day === 10) return '初十';
|
|
84
|
+
if (day === 20) return '二十';
|
|
85
|
+
if (day === 30) return '三十';
|
|
86
|
+
return `${DAY_TENS[Math.floor(day / 10)]}${DAY_UNITS[day % 10]}`;
|
|
87
|
+
}
|
|
88
|
+
export function lunarMonthText(month, isLeap = false) {
|
|
89
|
+
return `${isLeap ? '闰' : ''}${MONTH_TEXT[month - 1] || month}月`;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** 每日一卦:日期哈希确定性映射到通行本 64 卦(同日同卦)。 */
|
|
93
|
+
export function dailyHexagram(year, month, day) {
|
|
94
|
+
const names = Object.keys(HEX64);
|
|
95
|
+
if (!names.length) return null;
|
|
96
|
+
const seed = (((year * 10007) ^ (month * 911) ^ (day * 97)) >>> 0) % names.length;
|
|
97
|
+
const name = names[seed];
|
|
98
|
+
return { name, ...HEX64[name] };
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** 节气名(TERM_NAMES 与 termWindow 的 index 一一对应)。 */
|
|
102
|
+
const termName = (t) => (t && (t.name || TERM_NAMES[t.index])) || '';
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* 黄历主函数。
|
|
106
|
+
* @param {{year:number, month:number, day:number}} input 公历日期(必须真实存在)
|
|
107
|
+
*/
|
|
108
|
+
export function buildAlmanac(input) {
|
|
109
|
+
assertCivil(input, '黄历');
|
|
110
|
+
const { year, month, day } = input;
|
|
111
|
+
if (day > daysInMonth(year, month)) {
|
|
112
|
+
throw new Error(`非法日期:${year} 年 ${month} 月没有 ${day} 日`);
|
|
113
|
+
}
|
|
114
|
+
const jdNoon = partsToJd({ year, month, day, hour: 12, minute: 0 });
|
|
115
|
+
const lunar = solarToLunar(year, month, day);
|
|
116
|
+
const jdn = toJDN(year, month, day);
|
|
117
|
+
|
|
118
|
+
// 年柱:立春界;同时给出春节界口径
|
|
119
|
+
const lichunJd = termInstantJd(year, 2);
|
|
120
|
+
const ganzhiYear = jdNoon < lichunJd ? year - 1 : year;
|
|
121
|
+
const yearIdx = (((ganzhiYear - 4) % 60) + 60) % 60;
|
|
122
|
+
const yearGz = ganzhiName(yearIdx);
|
|
123
|
+
const springYear = lunar.lunarYear;
|
|
124
|
+
const springGz = ganzhiName((((springYear - 4) % 60) + 60) % 60);
|
|
125
|
+
|
|
126
|
+
// 月柱:以「节」为界
|
|
127
|
+
const jieTerms = termWindow(year, { jieOnly: true });
|
|
128
|
+
let prevJie = null;
|
|
129
|
+
let nextJie = null;
|
|
130
|
+
for (const t of jieTerms) {
|
|
131
|
+
if (t.jd <= jdNoon) prevJie = t; else { if (!nextJie) nextJie = t; }
|
|
132
|
+
}
|
|
133
|
+
const monthBranchIdx = ((2 + (prevJie.index - 2) / 2) % 12 + 12) % 12;
|
|
134
|
+
const yinMonthStem = ((STEMS.indexOf(yearGz.stem) % 5) * 2 + 2) % 10;
|
|
135
|
+
const monthStemIdx = (yinMonthStem + ((monthBranchIdx - 2 + 12) % 12)) % 10;
|
|
136
|
+
// 月柱干支按「天干地支同旬」组合:用 sexagenary 序推
|
|
137
|
+
const monthGz = ganzhiName(monthComposite(monthStemIdx, monthBranchIdx));
|
|
138
|
+
|
|
139
|
+
const dayGz = ganzhiName(dayGanzhiIndex(jdn));
|
|
140
|
+
const dayBranch = dayGz.branch;
|
|
141
|
+
const zhiXing = zhixingOf(dayBranch, BRANCHES[monthBranchIdx]);
|
|
142
|
+
const chongIdx = (BRANCHES.indexOf(dayBranch) + 6) % 12;
|
|
143
|
+
const chongShengXiao = SHENGXIAO[chongIdx];
|
|
144
|
+
|
|
145
|
+
const allTerms = termWindow(year);
|
|
146
|
+
let prevTerm = null;
|
|
147
|
+
let nextTerm = null;
|
|
148
|
+
for (const t of allTerms) {
|
|
149
|
+
if (t.jd <= jdNoon) prevTerm = t; else { if (!nextTerm) nextTerm = t; }
|
|
150
|
+
}
|
|
151
|
+
const fmtJdDate = (jd) => {
|
|
152
|
+
const p = fromJDN(Math.floor(jd + 0.5));
|
|
153
|
+
return `${p.year}-${String(p.month).padStart(2, '0')}-${String(p.day).padStart(2, '0')}`;
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
const weekday = ['日', '一', '二', '三', '四', '五', '六'][(jdn + 1) % 7];
|
|
157
|
+
const lunarText = `农历${springGz.name}年${lunarMonthText(lunar.month, lunar.isLeap)}${dayText(lunar.day)}`;
|
|
158
|
+
const boundaryNote = yearGz.name !== springGz.name
|
|
159
|
+
? `年界:本条按立春界作${yearGz.name}年·属${SHENGXIAO[BRANCHES.indexOf(yearGz.branch)]}(命理排盘用);春节界为${springGz.name}年·属${SHENGXIAO[BRANCHES.indexOf(springGz.branch)]},两种都对,取用场合不同`
|
|
160
|
+
: null;
|
|
161
|
+
|
|
162
|
+
return {
|
|
163
|
+
system: 'almanac',
|
|
164
|
+
meta: {
|
|
165
|
+
date: `${year}-${String(month).padStart(2, '0')}-${String(day).padStart(2, '0')}`,
|
|
166
|
+
weekday: `星期${weekday}`,
|
|
167
|
+
...(boundaryNote ? { boundaryNote } : {}),
|
|
168
|
+
scope: '通行简表口径:宜忌只按建除十二神;逐日神煞/二十八宿/彭祖百忌/胎神等全量黄历数据未收录',
|
|
169
|
+
},
|
|
170
|
+
lunar: {
|
|
171
|
+
year: springYear,
|
|
172
|
+
month: lunar.month,
|
|
173
|
+
day: lunar.day,
|
|
174
|
+
isLeap: lunar.isLeap,
|
|
175
|
+
text: lunarText,
|
|
176
|
+
},
|
|
177
|
+
ganzhi: {
|
|
178
|
+
year: yearGz.name,
|
|
179
|
+
month: monthGz.name,
|
|
180
|
+
day: dayGz.name,
|
|
181
|
+
shengXiao: SHENGXIAO[BRANCHES.indexOf(yearGz.branch)],
|
|
182
|
+
springYear: springGz.name,
|
|
183
|
+
springShengXiao: SHENGXIAO[BRANCHES.indexOf(springGz.branch)],
|
|
184
|
+
},
|
|
185
|
+
jieQi: {
|
|
186
|
+
prev: prevTerm ? { name: termName(prevTerm), date: fmtJdDate(prevTerm.jd) } : null,
|
|
187
|
+
next: nextTerm ? { name: termName(nextTerm), date: fmtJdDate(nextTerm.jd) } : null,
|
|
188
|
+
},
|
|
189
|
+
positions: { caiShen: CAI_SHEN[dayGz.stem] || null, xiShen: XI_SHEN[dayGz.stem] || null },
|
|
190
|
+
zhiXing,
|
|
191
|
+
yi: ZHIXING_YI[zhiXing].slice(),
|
|
192
|
+
ji: ZHIXING_JI[zhiXing].slice(),
|
|
193
|
+
chong: {
|
|
194
|
+
shengXiao: chongShengXiao,
|
|
195
|
+
text: `冲${chongShengXiao}`,
|
|
196
|
+
sha: shaOf(dayBranch) ? `煞${shaOf(dayBranch)}` : null,
|
|
197
|
+
},
|
|
198
|
+
dailyHexagram: dailyHexagram(year, month, day),
|
|
199
|
+
sources: [
|
|
200
|
+
'农历与节气:本地天文历法(定朔 + 节气黄经)',
|
|
201
|
+
'建除十二神与冲煞:传统通行口径',
|
|
202
|
+
'宜忌:建除十二神通行简表(非逐日全量黄历)',
|
|
203
|
+
'每日一卦:按公历日期确定性取卦(同日同卦)',
|
|
204
|
+
],
|
|
205
|
+
disclaimer: '黄历为传统民俗口径的本地确定性计算;宜忌仅按建除简表,属娱乐参考,不构成择日结论。',
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/** 由月干序与月支序求六十甲子序(同旬组合必然存在)。 */
|
|
210
|
+
function monthComposite(stemIdx, branchIdx) {
|
|
211
|
+
for (let i = 0; i < 60; i++) {
|
|
212
|
+
if (i % 10 === stemIdx && i % 12 === branchIdx) return i;
|
|
213
|
+
}
|
|
214
|
+
return 0;
|
|
215
|
+
}
|
package/engine/bazi.js
CHANGED
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
import { resolveAddress } from './geo.js';
|
|
8
8
|
import { computeBaziShensha } from './shensha.js';
|
|
9
9
|
import { assertCivil } from './validate.js';
|
|
10
|
+
import { applyDst } from './dst.js';
|
|
10
11
|
import { nayinOf } from './nayin.js';
|
|
11
12
|
import { SANHE } from './sanhe.js';
|
|
12
13
|
import { analyzeBaziFrame, scoreLuck } from './geju.js';
|
|
@@ -209,9 +210,15 @@ export function applyTrueSolar(jd, longitude) {
|
|
|
209
210
|
*/
|
|
210
211
|
export function buildBazi(input) {
|
|
211
212
|
assertCivil(input);
|
|
212
|
-
|
|
213
|
+
let { year, month, day, hour = 0, minute = 0, gender = 'male' } = input;
|
|
213
214
|
const lateZi = input.lateZi !== false;
|
|
214
215
|
const useTrueSolar = input.trueSolar !== false;
|
|
216
|
+
// 1986–1991 中国夏令时:出生证钟面时间普遍是「拨快 1 小时」的钟面时间,
|
|
217
|
+
// 不校正会让时柱(乃至跨 23:00 的日柱)整档偏;显式传 dst:false 可关闭。
|
|
218
|
+
const dst = input.dst === false ? { applied: false, note: null } : applyDst({ year, month, day, hour, minute });
|
|
219
|
+
if (dst.applied) {
|
|
220
|
+
year = dst.year; month = dst.month; day = dst.day; hour = dst.hour; minute = dst.minute;
|
|
221
|
+
}
|
|
215
222
|
|
|
216
223
|
const civilJd = partsToJd({ year, month, day, hour, minute });
|
|
217
224
|
// 地址 -> 经度:手填的 longitude 优先,其次按地址查表
|
|
@@ -470,12 +477,13 @@ export function buildBazi(input) {
|
|
|
470
477
|
geo,
|
|
471
478
|
lateZi,
|
|
472
479
|
trueSolar: useTrueSolar,
|
|
480
|
+
dst: { applied: !!dst.applied, note: dst.note || null },
|
|
473
481
|
},
|
|
474
482
|
solarTime: {
|
|
475
483
|
civilText: `${year}-${String(month).padStart(2, '0')}-${String(day).padStart(2, '0')} ${String(hour).padStart(2, '0')}:${String(minute).padStart(2, '0')}`,
|
|
476
484
|
trueSolarText: formatJd(jd),
|
|
477
485
|
offsetMinutes: solar.offsetMinutes,
|
|
478
|
-
note: solar.note,
|
|
486
|
+
note: dst.applied ? `${dst.note};${solar.note}` : solar.note,
|
|
479
487
|
lunarYearForPillar: solarYear,
|
|
480
488
|
},
|
|
481
489
|
jieqi: {
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
// 袁天罡称骨:按农历年/月/日/时辰四柱骨重求和,给分档白话批语。
|
|
2
|
+
// 骨重表为通行本口径;批语是白话自撰(不冒充原版称骨歌诀原文)。
|
|
3
|
+
import { solarToLunar } from './lunar.js';
|
|
4
|
+
import { ganzhiName, toJDN, dayGanzhiIndex } from './calendar.js';
|
|
5
|
+
import { assertCivil } from './validate.js';
|
|
6
|
+
import { hourToBranch } from './almanac.js';
|
|
7
|
+
|
|
8
|
+
/** 六十甲子年骨重(单位:钱;索引 = (农历年 - 4) mod 60,0 = 甲子)。 */
|
|
9
|
+
export const YEAR_WEIGHT = [
|
|
10
|
+
12, 9, 6, 7, 12, 5, 9, 8, 7, 8,
|
|
11
|
+
15, 9, 16, 8, 8, 19, 12, 6, 8, 7,
|
|
12
|
+
5, 15, 6, 16, 15, 7, 9, 12, 10, 7,
|
|
13
|
+
15, 6, 5, 14, 14, 9, 7, 7, 9, 12,
|
|
14
|
+
8, 7, 13, 5, 14, 5, 9, 17, 5, 7,
|
|
15
|
+
12, 8, 8, 6, 19, 6, 8, 16, 10, 7,
|
|
16
|
+
];
|
|
17
|
+
|
|
18
|
+
/** 农历月骨重(正月…腊月)。 */
|
|
19
|
+
export const MONTH_WEIGHT = [6, 7, 18, 9, 5, 16, 9, 15, 18, 8, 9, 5];
|
|
20
|
+
|
|
21
|
+
/** 农历日骨重(初一…三十)。 */
|
|
22
|
+
export const DAY_WEIGHT = [
|
|
23
|
+
5, 10, 8, 15, 16, 15, 8, 16, 8, 16,
|
|
24
|
+
9, 17, 8, 17, 10, 8, 9, 18, 5, 15,
|
|
25
|
+
10, 9, 8, 9, 15, 18, 7, 8, 16, 6,
|
|
26
|
+
];
|
|
27
|
+
|
|
28
|
+
/** 时辰骨重(按地支)。 */
|
|
29
|
+
export const HOUR_WEIGHT = { 子: 16, 丑: 6, 寅: 7, 卯: 10, 辰: 9, 巳: 16, 午: 10, 未: 8, 申: 8, 酉: 9, 戌: 6, 亥: 6 };
|
|
30
|
+
|
|
31
|
+
const LIANG = ['一', '二', '三', '四', '五', '六', '七'];
|
|
32
|
+
const QIAN = ['一', '二', '三', '四', '五', '六', '七', '八', '九'];
|
|
33
|
+
const MONTH_TEXT = ['正', '二', '三', '四', '五', '六', '七', '八', '九', '十', '冬', '腊'];
|
|
34
|
+
const DAY_TENS = ['初', '十', '廿'];
|
|
35
|
+
const DAY_UNITS = ['十', '一', '二', '三', '四', '五', '六', '七', '八', '九'];
|
|
36
|
+
|
|
37
|
+
/** 钱 → 「X两Y钱」(不足一两只写钱数)。 */
|
|
38
|
+
export function formatWeight(qian) {
|
|
39
|
+
const liang = Math.floor(qian / 10);
|
|
40
|
+
const rem = qian % 10;
|
|
41
|
+
const liangPart = liang > 0 ? `${LIANG[liang - 1]}两` : '';
|
|
42
|
+
const remPart = rem > 0 ? `${QIAN[rem - 1]}钱` : liang > 0 ? '整' : '零';
|
|
43
|
+
return `${liangPart}${remPart}`;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** 农历日序中文:初一…三十。 */
|
|
47
|
+
export function lunarDayText(day) {
|
|
48
|
+
if (day === 10) return '初十';
|
|
49
|
+
if (day === 20) return '二十';
|
|
50
|
+
if (day === 30) return '三十';
|
|
51
|
+
const ten = Math.floor(day / 10);
|
|
52
|
+
const unit = day % 10;
|
|
53
|
+
return `${DAY_TENS[ten]}${DAY_UNITS[unit]}`;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** 农历月序中文。 */
|
|
57
|
+
export function lunarMonthText(month, isLeap = false) {
|
|
58
|
+
return `${isLeap ? '闰' : ''}${MONTH_TEXT[month - 1] || month}月`;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** 骨重分档(白话自撰)。 */
|
|
62
|
+
function bandVerdict(qian) {
|
|
63
|
+
if (qian >= 55) return { band: '上上', text: '骨重沉实,根基深厚,一生多逢贵人,谋事易成;守正持重,福泽可久。' };
|
|
64
|
+
if (qian >= 45) return { band: '上', text: '骨重充实,中年前后渐入佳境,事业家宅两旺;忌骄忌躁,善守即善成。' };
|
|
65
|
+
if (qian >= 35) return { band: '中上', text: '骨重适中,先劳后逸,凭本事立身;中年转运之象,稳扎稳打自见分晓。' };
|
|
66
|
+
if (qian >= 25) return { band: '中', text: '骨重平平,成败多靠自身经营;宜有一技傍身、广结善缘,勤则补拙。' };
|
|
67
|
+
return { band: '中下', text: '骨重较轻,早年多磨,凡事宜稳不宜急;积跬步以至千里,晚景反可安宁。' };
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* 称骨主函数。
|
|
72
|
+
* @param {{ year:number, month:number, day:number, hour:number, minute?:number }} input
|
|
73
|
+
* 一律按公历给生日(农历换算由引擎完成);时辰必填——称骨四柱缺时辰不可算。
|
|
74
|
+
*/
|
|
75
|
+
export function buildChenggu(input) {
|
|
76
|
+
assertCivil(input, '称骨');
|
|
77
|
+
const { year, month, day, hour } = input;
|
|
78
|
+
const minute = input.minute || 0;
|
|
79
|
+
if (!Number.isInteger(hour) || hour < 0 || hour > 23) {
|
|
80
|
+
throw new Error('称骨必须给时辰:hour 为 0–23 的整数(缺时辰四柱不齐,骨重无法求和)');
|
|
81
|
+
}
|
|
82
|
+
const lunar = solarToLunar(year, month, day);
|
|
83
|
+
const lunarYear = lunar.lunarYear;
|
|
84
|
+
const lunarMonth = lunar.month;
|
|
85
|
+
const lunarDay = lunar.day;
|
|
86
|
+
const yearGz = ganzhiName((((lunarYear - 4) % 60) + 60) % 60);
|
|
87
|
+
const hourBranch = hourToBranch(hour, minute);
|
|
88
|
+
|
|
89
|
+
const wYear = YEAR_WEIGHT[((lunarYear - 4) % 60 + 60) % 60];
|
|
90
|
+
const wMonth = MONTH_WEIGHT[lunarMonth - 1];
|
|
91
|
+
const wDay = DAY_WEIGHT[lunarDay - 1];
|
|
92
|
+
const wHour = HOUR_WEIGHT[hourBranch];
|
|
93
|
+
if ([wYear, wMonth, wDay, wHour].some((x) => typeof x !== 'number')) {
|
|
94
|
+
throw new Error(`称骨表缺项:年${wYear} 月${wMonth} 日${wDay} 时${wHour}`);
|
|
95
|
+
}
|
|
96
|
+
const total = wYear + wMonth + wDay + wHour;
|
|
97
|
+
const verdict = bandVerdict(total);
|
|
98
|
+
const jdn = toJDN(year, month, day);
|
|
99
|
+
|
|
100
|
+
return {
|
|
101
|
+
system: 'chenggu',
|
|
102
|
+
input: { solar: `${year}-${String(month).padStart(2, '0')}-${String(day).padStart(2, '0')}`, hour, minute },
|
|
103
|
+
lunar: {
|
|
104
|
+
text: `农历${yearGz.name}年${lunarMonthText(lunarMonth, lunar.isLeap)}${lunarDayText(lunarDay)} ${hourBranch}时`,
|
|
105
|
+
year: lunarYear,
|
|
106
|
+
month: lunarMonth,
|
|
107
|
+
day: lunarDay,
|
|
108
|
+
isLeap: lunar.isLeap,
|
|
109
|
+
hourBranch,
|
|
110
|
+
},
|
|
111
|
+
ganzhi: { year: yearGz.name, day: ganzhiName(dayGanzhiIndex(jdn)).name },
|
|
112
|
+
weights: {
|
|
113
|
+
year: { qian: wYear, label: `${yearGz.name}年 ${formatWeight(wYear)}` },
|
|
114
|
+
month: { qian: wMonth, label: `农历${lunarMonthText(lunarMonth, lunar.isLeap)} ${formatWeight(wMonth)}` },
|
|
115
|
+
day: { qian: wDay, label: `农历${lunarDayText(lunarDay)} ${formatWeight(wDay)}` },
|
|
116
|
+
hour: { qian: wHour, label: `${hourBranch}时 ${formatWeight(wHour)}` },
|
|
117
|
+
},
|
|
118
|
+
total: { qian: total, label: formatWeight(total) },
|
|
119
|
+
verdict,
|
|
120
|
+
sources: [
|
|
121
|
+
'骨重表:通行本袁天罡称骨(六十甲子年 / 十二月 / 三十日 / 十二时辰)',
|
|
122
|
+
'批语:白话自撰,非原版称骨歌诀原文,可自行对照',
|
|
123
|
+
],
|
|
124
|
+
disclaimer: '骨重为通行表口径;批语为白话解读,属民俗娱乐参考,不构成命运断言。',
|
|
125
|
+
};
|
|
126
|
+
}
|
package/engine/dst.js
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
// 中国夏令时(1986–1991)校正:纯本地确定性。
|
|
2
|
+
//
|
|
3
|
+
// 这六年春夏钟表拨快 1 小时:出生证上的时间多数是「夏令时钟面时间」,
|
|
4
|
+
// 不校正则时辰(乃至日柱)整体错一档。区间为通行口径(春季 02:00 起,秋季 02:00 止,含头不含尾)。
|
|
5
|
+
// 依据:国务院 1986–1991 年夏令时通告;与公开万年历对拍一致。
|
|
6
|
+
import { daysInMonth } from './validate.js';
|
|
7
|
+
|
|
8
|
+
/** 历年夏令时区间:[起始日, 结束日](本地钟面时间)。 */
|
|
9
|
+
export const DST_SPANS = {
|
|
10
|
+
1986: ['1986-05-04', '1986-09-14'],
|
|
11
|
+
1987: ['1987-04-12', '1987-09-13'],
|
|
12
|
+
1988: ['1988-04-10', '1988-09-11'],
|
|
13
|
+
1989: ['1989-04-16', '1989-09-17'],
|
|
14
|
+
1990: ['1990-04-15', '1990-09-16'],
|
|
15
|
+
1991: ['1991-04-14', '1991-09-15'],
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
const ORD = (y, m, d) => y * 10000 + m * 100 + d;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* 给定钟面时刻是否落在夏令时区间内。
|
|
22
|
+
* @param {{year:number,month:number,day:number,hour:number,minute?:number}} t
|
|
23
|
+
*/
|
|
24
|
+
export function inDstSpan(t) {
|
|
25
|
+
const span = DST_SPANS[t.year];
|
|
26
|
+
if (!span) return false;
|
|
27
|
+
const [sy, sm, sd] = span[0].split('-').map(Number);
|
|
28
|
+
const [ey, em, ed] = span[1].split('-').map(Number);
|
|
29
|
+
const n = ORD(t.year, t.month, t.day);
|
|
30
|
+
if (n < ORD(sy, sm, sd) || n > ORD(ey, em, ed)) return false;
|
|
31
|
+
const minutes = t.hour * 60 + (t.minute || 0);
|
|
32
|
+
if (n === ORD(sy, sm, sd) && minutes < 120) return false; // 起始日 02:00 起
|
|
33
|
+
if (n === ORD(ey, em, ed) && minutes >= 120) return false; // 结束日 02:00 止
|
|
34
|
+
return true;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* 夏令时校正:钟面时间减 1 小时(跨日正确回退)。
|
|
39
|
+
* @param {{year:number,month:number,day:number,hour:number,minute?:number}} t
|
|
40
|
+
* @returns {{year,month,day,hour,minute,applied:boolean,note:string|null}}
|
|
41
|
+
*/
|
|
42
|
+
export function applyDst(t) {
|
|
43
|
+
const minute = t.minute || 0;
|
|
44
|
+
if (!inDstSpan(t)) return { year: t.year, month: t.month, day: t.day, hour: t.hour, minute, applied: false, note: null };
|
|
45
|
+
let { year, month, day, hour } = t;
|
|
46
|
+
hour -= 1;
|
|
47
|
+
if (hour < 0) {
|
|
48
|
+
hour += 24;
|
|
49
|
+
day -= 1;
|
|
50
|
+
if (day < 1) {
|
|
51
|
+
month -= 1;
|
|
52
|
+
if (month < 1) { month = 12; year -= 1; }
|
|
53
|
+
day = daysInMonth(year, month);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return {
|
|
57
|
+
year, month, day, hour, minute,
|
|
58
|
+
applied: true,
|
|
59
|
+
note: `检出自 ${DST_SPANS[t.year][0]} 至 ${DST_SPANS[t.year][1]} 的中国夏令时:出生钟面时间已减 1 小时校正`,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** 把夏令时说明折进 notes 数组(不改动其它口径)。 */
|
|
64
|
+
export function withDstNote(notes, dst) {
|
|
65
|
+
const out = Array.isArray(notes) ? notes.slice() : [];
|
|
66
|
+
if (dst && dst.applied && dst.note) out.push(dst.note);
|
|
67
|
+
return out;
|
|
68
|
+
}
|