dsh-metaphysics 0.14.2 → 0.15.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 +187 -134
- package/README.md +221 -216
- package/client.js +1152 -112
- package/engine/bazi.js +505 -515
- package/engine/calendar.js +325 -256
- package/engine/daliuren.js +337 -340
- package/engine/diff.js +197 -0
- package/engine/fail.js +13 -0
- package/engine/geo.js +1 -1
- package/engine/hepan.js +147 -144
- package/engine/index.js +522 -512
- package/engine/interpret-knowledge.js +277 -0
- package/engine/interpret-plain.js +197 -0
- package/engine/interpret.js +1240 -952
- package/engine/liuyao.js +237 -227
- package/engine/lru.js +69 -0
- package/engine/meihua.js +137 -136
- package/engine/nayin.js +24 -0
- package/engine/qimen.js +442 -438
- package/engine/sanhe.js +26 -0
- package/engine/validate.js +56 -0
- package/engine/ziwei-hepan.js +123 -120
- package/engine/ziwei.js +821 -828
- package/index.js +222 -212
- package/package.json +1 -1
package/engine/calendar.js
CHANGED
|
@@ -1,256 +1,325 @@
|
|
|
1
|
-
import { earthL } from './vsop-earth.js';
|
|
2
|
-
|
|
3
|
-
//
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
export const
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
'
|
|
11
|
-
'
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
const
|
|
23
|
-
const
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
Math.floor(y /
|
|
30
|
-
Math.floor(y /
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
const
|
|
39
|
-
const
|
|
40
|
-
const
|
|
41
|
-
const
|
|
42
|
-
const
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
*
|
|
76
|
-
*
|
|
77
|
-
*
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
const
|
|
82
|
-
const
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
theta = ((
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
//
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
const
|
|
99
|
-
const
|
|
100
|
-
const
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
const
|
|
106
|
-
const
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
[
|
|
117
|
-
[
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
const [
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
//
|
|
132
|
-
//
|
|
133
|
-
|
|
134
|
-
export const
|
|
135
|
-
|
|
136
|
-
//
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
* index
|
|
147
|
-
*
|
|
148
|
-
*
|
|
149
|
-
*
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
const
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
const
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
export function
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
const
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
/**
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
if (
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
const
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
1
|
+
import { earthL } from './vsop-earth.js';
|
|
2
|
+
import { createLru } from './lru.js';
|
|
3
|
+
// 历法底座:儒略日、干支、二十四节气、真太阳时
|
|
4
|
+
// 全部为纯函数,不依赖 DOM / Node API,Host 与 Client 可共用。
|
|
5
|
+
|
|
6
|
+
export const STEMS = ['甲', '乙', '丙', '丁', '戊', '己', '庚', '辛', '壬', '癸'];
|
|
7
|
+
export const BRANCHES = ['子', '丑', '寅', '卯', '辰', '巳', '午', '未', '申', '酉', '戌', '亥'];
|
|
8
|
+
|
|
9
|
+
export const TERM_NAMES = [
|
|
10
|
+
'小寒', '大寒', '立春', '雨水', '惊蛰', '春分', '清明', '谷雨',
|
|
11
|
+
'立夏', '小满', '芒种', '夏至', '小暑', '大暑', '立秋', '处暑',
|
|
12
|
+
'白露', '秋分', '寒露', '霜降', '立冬', '小雪', '大雪', '冬至',
|
|
13
|
+
];
|
|
14
|
+
|
|
15
|
+
// 二十四节气对应的太阳视黄经(0 号为小寒 285°)
|
|
16
|
+
export function termLongitude(index) {
|
|
17
|
+
return (285 + 15 * index) % 360;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** 公历 -> 儒略日数(当日正午的整数 JDN) */
|
|
21
|
+
export function toJDN(year, month, day) {
|
|
22
|
+
const a = Math.floor((14 - month) / 12);
|
|
23
|
+
const y = year + 4800 - a;
|
|
24
|
+
const m = month + 12 * a - 3;
|
|
25
|
+
return (
|
|
26
|
+
day +
|
|
27
|
+
Math.floor((153 * m + 2) / 5) +
|
|
28
|
+
365 * y +
|
|
29
|
+
Math.floor(y / 4) -
|
|
30
|
+
Math.floor(y / 100) +
|
|
31
|
+
Math.floor(y / 400) -
|
|
32
|
+
32045
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** 儒略日数 -> 公历 */
|
|
37
|
+
export function fromJDN(jdn) {
|
|
38
|
+
const a = jdn + 32044;
|
|
39
|
+
const b = Math.floor((4 * a + 3) / 146097);
|
|
40
|
+
const c = a - Math.floor((146097 * b) / 4);
|
|
41
|
+
const d = Math.floor((4 * c + 3) / 1461);
|
|
42
|
+
const e = c - Math.floor((1461 * d) / 4);
|
|
43
|
+
const m = Math.floor((5 * e + 2) / 153);
|
|
44
|
+
return {
|
|
45
|
+
year: 100 * b + d - 4800 + Math.floor(m / 10),
|
|
46
|
+
month: m + 3 - 12 * Math.floor(m / 10),
|
|
47
|
+
day: e - Math.floor((153 * m + 2) / 5) + 1,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** 由天干/地支序号求六十甲子序号(0 = 甲子),阴阳不配返回 -1 */
|
|
52
|
+
export function sexagenaryIndex(stemIdx, branchIdx) {
|
|
53
|
+
for (let i = 0; i < 60; i++) {
|
|
54
|
+
if (i % 10 === stemIdx && i % 12 === branchIdx) return i;
|
|
55
|
+
}
|
|
56
|
+
return -1;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function ganzhiName(index) {
|
|
60
|
+
const i = ((index % 60) + 60) % 60;
|
|
61
|
+
return {
|
|
62
|
+
index: i,
|
|
63
|
+
stem: STEMS[i % 10],
|
|
64
|
+
branch: BRANCHES[i % 12],
|
|
65
|
+
name: STEMS[i % 10] + BRANCHES[i % 12],
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** 日柱干支序号。基准:1900-01-31 甲辰旬外推,(JDN + 49) % 60 = 0 即甲子日 */
|
|
70
|
+
export function dayGanzhiIndex(jdn) {
|
|
71
|
+
return (((jdn + 49) % 60) + 60) % 60;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* 太阳视黄经(度)。
|
|
76
|
+
* 黄经本体改用 VSOP87B 截断级数(105 项),再把地心黄经转成视黄经:
|
|
77
|
+
* λ = Θ + FK5修正 + 章动Δψ + 光行差
|
|
78
|
+
* 与 NAOJ 历要项对拍:2024/2025 共 48 个节气,偏差从原来的最多 13.5 分钟降到 1 分钟内。
|
|
79
|
+
*/
|
|
80
|
+
export function sunLongitude(jd) {
|
|
81
|
+
const d = jd - 2451545.0;
|
|
82
|
+
const T = d / 36525;
|
|
83
|
+
const Tv = d / 365250; // VSOP87 的时间参量单位是「儒略千年」,不是世纪
|
|
84
|
+
|
|
85
|
+
// 1) 地球日心黄经 → 太阳地心黄经
|
|
86
|
+
let theta = (earthL(Tv) * 180) / Math.PI + 180;
|
|
87
|
+
theta = ((theta % 360) + 360) % 360;
|
|
88
|
+
|
|
89
|
+
// 2) 岁差:VSOP87B 的黄经是 J2000 参考系,必须先推到当日平春分点
|
|
90
|
+
// (2024 年约 0.335°,相当于 8 小时,漏掉会让整个节气表错开)
|
|
91
|
+
const prec = (5029.0966 * T + 1.11113 * T * T - 0.000006 * T * T * T) / 3600;
|
|
92
|
+
theta += prec;
|
|
93
|
+
|
|
94
|
+
// 3) FK5 系统修正(Meeus 25.9)
|
|
95
|
+
theta -= 0.09033 / 3600;
|
|
96
|
+
|
|
97
|
+
// 3) 章动(Meeus 22,取四项主项,精度约 0.5″)
|
|
98
|
+
const om = ((125.04452 - 1934.136261 * T) * Math.PI) / 180;
|
|
99
|
+
const ls = ((280.4665 + 36000.7698 * T) * Math.PI) / 180;
|
|
100
|
+
const lm = ((218.3165 + 481267.8813 * T) * Math.PI) / 180;
|
|
101
|
+
const dPsi = (-17.2 * Math.sin(om) - 1.32 * Math.sin(2 * ls) - 0.23 * Math.sin(2 * lm) + 0.21 * Math.sin(2 * om)) / 3600;
|
|
102
|
+
theta += dPsi;
|
|
103
|
+
|
|
104
|
+
// 4) 光行差:-20.4898″ / R;R 只需三位有效数字,沿用低精度日地距离即可
|
|
105
|
+
const m = ((357.52911 + 35999.05029 * T - 0.0001537 * T * T) * Math.PI) / 180;
|
|
106
|
+
const e = 0.016708634 - 0.000042037 * T - 0.0000001267 * T * T;
|
|
107
|
+
const R = (1.000001018 * (1 - e * e)) / (1 + e * Math.cos(m + (1.914602 - 0.004817 * T) * (Math.PI / 180) * Math.sin(m)));
|
|
108
|
+
theta -= 20.4898 / 3600 / (R || 1);
|
|
109
|
+
|
|
110
|
+
return ((theta % 360) + 360) % 360;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** ΔT(TT - UT,秒)。分段线性近似,误差 < 2s,对分钟级排盘足够 */
|
|
114
|
+
export function deltaTSeconds(year) {
|
|
115
|
+
const knots = [
|
|
116
|
+
[1900, -2.8], [1920, 21.2], [1940, 24.3], [1950, 29.1], [1960, 33.2],
|
|
117
|
+
[1970, 40.2], [1980, 50.5], [1990, 56.9], [2000, 63.8], [2010, 66.1],
|
|
118
|
+
[2020, 69.4], [2030, 72.0], [2050, 80.0], [2100, 110.0],
|
|
119
|
+
];
|
|
120
|
+
if (year <= knots[0][0]) return knots[0][1];
|
|
121
|
+
for (let i = 1; i < knots.length; i++) {
|
|
122
|
+
if (year <= knots[i][0]) {
|
|
123
|
+
const [y0, v0] = knots[i - 1];
|
|
124
|
+
const [y1, v1] = knots[i];
|
|
125
|
+
return v0 + ((v1 - v0) * (year - y0)) / (y1 - y0);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
return knots[knots.length - 1][1];
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// 1929 年元旦之前,中国历书按「北京地方平时」(116.4°E,UTC+7.7611)推算朔与中气;
|
|
132
|
+
// 之后改用东经 120° 标准时(UTC+8)。这 14 分钟会让交节/合朔跨零点,进而整月错位,
|
|
133
|
+
// 所以节气与农历必须共用同一套换算,不能各写一份。
|
|
134
|
+
export const OFFSET_MODERN = 8 / 24;
|
|
135
|
+
export const OFFSET_BEIJING_LOCAL = 7.7611 / 24;
|
|
136
|
+
// 切点 = 1929-01-01 00:00(东八区)对应的 UT 儒略日 = 1928-12-31 16:00 UT。
|
|
137
|
+
// 这里用的是真儒略日轴:partsToJd(1929,1,1) = 2425612.5,减 8/24 即 2425612.1667。
|
|
138
|
+
export const CUTOVER_JD = 2425612 + 4 / 24; // 2425612.1667
|
|
139
|
+
|
|
140
|
+
/** 某时刻(UT 轴)应当使用的本地时区偏移量 */
|
|
141
|
+
export function localOffsetFor(jdUt) {
|
|
142
|
+
return jdUt < CUTOVER_JD ? OFFSET_BEIJING_LOCAL : OFFSET_MODERN;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* 第 index 个节气(0 = 小寒)在指定年份的时刻,返回「当时本地时间轴」的儒略日。
|
|
147
|
+
* index 为偶数的是「节」(定月柱),奇数的是「气」。
|
|
148
|
+
*
|
|
149
|
+
* 结果按 (year, index) 缓存:农历换算一次要问几百次中气(每个候选月都要判有无中气),
|
|
150
|
+
* 而 VSOP87 的 earthL 是整个引擎最贵的一段——不缓存时紫微盘 10ms 里有 8ms 花在这里。
|
|
151
|
+
*/
|
|
152
|
+
const TERM_INSTANT_CACHE = createLru({ max: 4096, name: 'termInstant' });
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* 牛顿迭代收敛容差(度)。
|
|
156
|
+
* 1e-7° ≈ 0.002 秒、1e-5° ≈ 0.02 秒——输出只精确到分钟,取 1e-5 即可:
|
|
157
|
+
* 实测 24 节气总迭代数 131 → 105(-20%),节气时刻最大差 0.02 秒。
|
|
158
|
+
*/
|
|
159
|
+
const TERM_CONVERGE_DEG = 1e-5;
|
|
160
|
+
|
|
161
|
+
// 同一节气在相邻年份的时刻相差约一个回归年(365.2422 日)。
|
|
162
|
+
// 把它当作下一年同一节气的牛顿初值,可从 5.5 次迭代降到 2~3 次——
|
|
163
|
+
// 实测「第二次以后取同一节气」的成本约为首次的 45%。
|
|
164
|
+
const TERM_GUESS_CACHE = new Map(); // index -> 上次解出的本地时间轴 jd
|
|
165
|
+
const TROPICAL_YEAR = 365.2422;
|
|
166
|
+
|
|
167
|
+
function computeTermInstantJd(year, index) {
|
|
168
|
+
const target = termLongitude(index);
|
|
169
|
+
const anchor = toJDN(year, 1, 5) - 0.5 + index * 15.2184;
|
|
170
|
+
const prev = TERM_GUESS_CACHE.get(index);
|
|
171
|
+
// 初值取「上一年同一节气 + 回归年」与几何锚点中更接近的一个
|
|
172
|
+
let jd = prev !== undefined ? prev + TROPICAL_YEAR : anchor;
|
|
173
|
+
if (Math.abs(jd - anchor) > 20) jd = anchor; // 偏离过大说明缓存跨了多年,退回锚点
|
|
174
|
+
for (let n = 0; n < 12; n++) {
|
|
175
|
+
let diff = target - sunLongitude(jd);
|
|
176
|
+
diff = ((((diff + 180) % 360) + 360) % 360) - 180;
|
|
177
|
+
jd += diff / 0.98564736;
|
|
178
|
+
if (Math.abs(diff) < TERM_CONVERGE_DEG) break;
|
|
179
|
+
}
|
|
180
|
+
TERM_GUESS_CACHE.set(index, jd);
|
|
181
|
+
// TT -> UT -> 当时本地时间轴(1929 前是北京地方平时,之后是东八区)
|
|
182
|
+
const jdUt = jd - deltaTSeconds(year) / 86400;
|
|
183
|
+
return jdUt + localOffsetFor(jdUt);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
export function termInstantJd(year, index) {
|
|
187
|
+
const key = year * 24 + index;
|
|
188
|
+
const hit = TERM_INSTANT_CACHE.get(key);
|
|
189
|
+
if (hit !== undefined) return hit;
|
|
190
|
+
const jd = computeTermInstantJd(year, index);
|
|
191
|
+
TERM_INSTANT_CACHE.set(key, jd);
|
|
192
|
+
return jd;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/** 儒略日(本地时间轴)拆成日期与时刻 */
|
|
196
|
+
export function jdToParts(jdLocal) {
|
|
197
|
+
const jdn = Math.floor(jdLocal + 0.5);
|
|
198
|
+
const frac = jdLocal + 0.5 - jdn;
|
|
199
|
+
const totalMinutes = Math.round(frac * 1440);
|
|
200
|
+
const { year, month, day } = fromJDN(jdn);
|
|
201
|
+
return {
|
|
202
|
+
year,
|
|
203
|
+
month,
|
|
204
|
+
day,
|
|
205
|
+
hour: Math.floor(totalMinutes / 60) % 24,
|
|
206
|
+
minute: totalMinutes % 60,
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
export function partsToJd({ year, month, day, hour = 0, minute = 0 }) {
|
|
211
|
+
return toJDN(year, month, day) - 0.5 + (hour * 60 + minute) / 1440;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/** 均时差(分钟):真太阳时 = 平太阳时 + 经度差 + 均时差 */
|
|
215
|
+
export function equationOfTime(year, month, day) {
|
|
216
|
+
const n = toJDN(year, month, day) - toJDN(year, 1, 1) + 1;
|
|
217
|
+
const B = (2 * Math.PI * (n - 81)) / 364;
|
|
218
|
+
return 9.87 * Math.sin(2 * B) - 7.53 * Math.cos(B) - 1.5 * Math.sin(B);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// 一次排盘要算 72 个节气(三年窗口),每次十多轮牛顿迭代 × VSOP87 105 项。
|
|
222
|
+
// 同一年的盘经常连着排(面板切 tab、合盘两侧),按年缓存能把这块成本降到常数级。
|
|
223
|
+
const TERM_WINDOW_CACHE = createLru({ max: 8, name: 'termWindow' });
|
|
224
|
+
// 单年 24 节气也单独缓存:相邻年份的窗口高度重叠(year±1),
|
|
225
|
+
// 只按窗口缓存会让 1996 与 1997 各算一遍 1996 的 24 个节气。
|
|
226
|
+
const TERM_YEAR_CACHE = createLru({ max: 32, name: 'termYear' });
|
|
227
|
+
const ALL_TERM_INDEXES = Array.from({ length: 24 }, (_, i) => i);
|
|
228
|
+
const JIE_TERM_INDEXES = ALL_TERM_INDEXES.filter((i) => i % 2 === 0); // 节:定月柱用,气不参与
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* 单年节气(按 jd 升序),结果只读,调用方不得修改
|
|
232
|
+
* @param {number} year
|
|
233
|
+
* @param {boolean} jieOnly 只要十二「节」——八字定月柱、定大运起运只用到节,
|
|
234
|
+
* 算全套 24 个等于替用不上的「气」白付一半 VSOP 求值成本
|
|
235
|
+
*/
|
|
236
|
+
function termYearList(year, jieOnly) {
|
|
237
|
+
const key = jieOnly ? `${year}|jie` : `${year}|all`;
|
|
238
|
+
const hit = TERM_YEAR_CACHE.get(key);
|
|
239
|
+
if (hit) return hit;
|
|
240
|
+
// 若该年全年 24 节气已在缓存里(紫微/奇门会算全年),只要节时直接过滤复用,
|
|
241
|
+
// 避免「八字先算节、紫微再算气」时两边各建一份、缓存互不命中。
|
|
242
|
+
if (jieOnly) {
|
|
243
|
+
const full = TERM_YEAR_CACHE.get(`${year}|all`);
|
|
244
|
+
if (full) {
|
|
245
|
+
const jie = full.filter((t) => t.index % 2 === 0);
|
|
246
|
+
TERM_YEAR_CACHE.set(key, jie);
|
|
247
|
+
return jie;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
const list = [];
|
|
251
|
+
for (const i of (jieOnly ? JIE_TERM_INDEXES : ALL_TERM_INDEXES)) {
|
|
252
|
+
list.push({ year, index: i, name: TERM_NAMES[i], jd: termInstantJd(year, i) });
|
|
253
|
+
}
|
|
254
|
+
list.sort((a, b) => a.jd - b.jd);
|
|
255
|
+
TERM_YEAR_CACHE.set(key, list);
|
|
256
|
+
return list;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* 列出 [year-1, year+1] 三年的节气,按时间升序(结果按年缓存,调用方不要修改返回值)
|
|
261
|
+
* @param {number} year
|
|
262
|
+
* @param {{jieOnly?: boolean}} [opts] jieOnly=true 时只返回十二节(default:false,含气)
|
|
263
|
+
*/
|
|
264
|
+
export function termWindow(year, opts = {}) {
|
|
265
|
+
const jieOnly = opts.jieOnly === true;
|
|
266
|
+
const cacheKey = jieOnly ? `${year}|jie` : year;
|
|
267
|
+
const cached = TERM_WINDOW_CACHE.get(cacheKey);
|
|
268
|
+
if (cached) return cached;
|
|
269
|
+
const list = [
|
|
270
|
+
...termYearList(year - 1, jieOnly),
|
|
271
|
+
...termYearList(year, jieOnly),
|
|
272
|
+
...termYearList(year + 1, jieOnly),
|
|
273
|
+
];
|
|
274
|
+
list.sort((a, b) => a.jd - b.jd);
|
|
275
|
+
TERM_WINDOW_CACHE.set(cacheKey, list);
|
|
276
|
+
return list;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* 由任一干支求旬空两支(全库唯一实现)。
|
|
281
|
+
* 六十甲子按旬分六组,空亡即旬首所辖十支之外的两支。
|
|
282
|
+
* @param {string} ganzhi 两字干支,如 '丙申'
|
|
283
|
+
* @returns {string[]} 两个空亡地支,如 ['辰','巳'];非干支返回 []
|
|
284
|
+
*/
|
|
285
|
+
export function xunKongOf(ganzhi) {
|
|
286
|
+
if (typeof ganzhi !== 'string' || ganzhi.length < 2) return [];
|
|
287
|
+
const s = STEMS.indexOf(ganzhi[0]);
|
|
288
|
+
const b = BRANCHES.indexOf(ganzhi[1]);
|
|
289
|
+
if (s < 0 || b < 0) return [];
|
|
290
|
+
const idx = sexagenaryIndex(s, b);
|
|
291
|
+
if (idx < 0) return [];
|
|
292
|
+
const head = Math.floor(idx / 10) * 10; // 旬首序号:甲子=0、甲戌=10…
|
|
293
|
+
return [BRANCHES[(head + 10) % 12], BRANCHES[(head + 11) % 12]];
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/** 找到 jd 之前(含)最近的一个「节」(偶数索引) */
|
|
297
|
+
export function previousJie(terms, jd) {
|
|
298
|
+
let found = null;
|
|
299
|
+
for (const t of terms) {
|
|
300
|
+
if (t.index % 2 !== 0) continue;
|
|
301
|
+
if (t.jd <= jd) found = t;
|
|
302
|
+
else break;
|
|
303
|
+
}
|
|
304
|
+
return found;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/** 找到 jd 之后最近的一个「节」 */
|
|
308
|
+
export function nextJie(terms, jd) {
|
|
309
|
+
for (const t of terms) {
|
|
310
|
+
if (t.index % 2 !== 0) continue;
|
|
311
|
+
if (t.jd > jd) return t;
|
|
312
|
+
}
|
|
313
|
+
return null;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
export function formatJd(jd) {
|
|
317
|
+
const p = jdToParts(jd);
|
|
318
|
+
const pad = (n) => String(n).padStart(2, '0');
|
|
319
|
+
return `${p.year}-${pad(p.month)}-${pad(p.day)} ${pad(p.hour)}:${pad(p.minute)}`;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/** 缓存命中率快照:DSH_METRICS=1 时由 renderText 打印,便于判断缓存是否真的有用 */
|
|
323
|
+
export function cacheMetrics() {
|
|
324
|
+
return [TERM_INSTANT_CACHE.stats(), TERM_YEAR_CACHE.stats(), TERM_WINDOW_CACHE.stats()];
|
|
325
|
+
}
|