dsh-metaphysics 0.17.0 → 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 -14
- package/README.md +54 -14
- 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 +17 -4
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
// 生命数字(西方数字学):生命路径数 / 生日数 / 态度数,11、22、33 主数保留。
|
|
2
|
+
// 纯本地确定性;与八字/紫微是两套不可互证的体系,输出里明确提示。
|
|
3
|
+
import { daysInMonth } from './validate.js';
|
|
4
|
+
|
|
5
|
+
/** 1–9 与主数 11/22/33 的数义(白话提炼)。 */
|
|
6
|
+
export const NUMBER_MEANING = {
|
|
7
|
+
1: { title: '开创者', keywords: '独立、主见、起步快', tendency: '习惯自己拍板,适合从零开始的事。', lesson: '课题是学会合作与示弱,别把「我能行」当唯一答案。' },
|
|
8
|
+
2: { title: '协调者', keywords: '细腻、共情、善配合', tendency: '擅长读空气、做黏合剂,团队里离不开的角色。', lesson: '课题是别过度讨好、别把委屈攒成怨气。' },
|
|
9
|
+
3: { title: '表达者', keywords: '活跃、创意、感染力', tendency: '靠表达与点子吃饭,越有舞台越发光。', lesson: '课题是持续力与收尾,别让兴趣过载成半成品。' },
|
|
10
|
+
4: { title: '建构者', keywords: '务实、秩序、靠得住', tendency: '擅长把混乱理成流程,慢工出细活。', lesson: '课题是别过度求稳、别用规则捆住自己和别人。' },
|
|
11
|
+
5: { title: '探索者', keywords: '自由、变化、新鲜感', tendency: '适应力强,喜欢移动与尝鲜,厌烦一成不变。', lesson: '课题是承诺与专注,别把「换一个」当解药。' },
|
|
12
|
+
6: { title: '照顾者', keywords: '责任、顾家、审美', tendency: '天然把身边人放进自己的责任清单。', lesson: '课题是别替别人扛过头,爱不等于包办。' },
|
|
13
|
+
7: { title: '探究者', keywords: '内省、钻研、独处', tendency: '需要个人空间,喜欢把问题想透再动。', lesson: '课题是信任与敞开,别用距离保护自己。' },
|
|
14
|
+
8: { title: '经营者', keywords: '资源、格局、执行力', tendency: '对钱与权有直觉,擅长把资源盘活。', lesson: '课题是平衡控制欲,别用成果定义自我价值。' },
|
|
15
|
+
9: { title: '理想者', keywords: '情怀、包容、完成感', tendency: '看得远,愿意为更大的意义投入。', lesson: '课题是落地与界限,别在付出里丢了自己。' },
|
|
16
|
+
11: { title: '直觉者(主数)', keywords: '敏感、灵感、感召力', tendency: '直觉极准,容易成为别人的灯,但情绪消耗也大。', lesson: '课题是把天赋落地成具体作品,而不是只停在感受。' },
|
|
17
|
+
22: { title: '实践家(主数)', keywords: '愿景、系统、扛大梁', tendency: '能把大想法拆成可行工程,压力与能力同步放大。', lesson: '课题是别被宏大目标压住,先完成小闭环。' },
|
|
18
|
+
33: { title: '疗愈者(主数)', keywords: '大爱、教导、承担', tendency: '为他人承担是本能,适合教导与疗愈型角色。', lesson: '课题是先照顾好自己,别把牺牲当价值。' },
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
/** 逐位求和;11/22/33 保留为主数(不再归约)。 */
|
|
22
|
+
export function reduceNumber(n, keepMasters = true) {
|
|
23
|
+
let v = Math.abs(Math.trunc(n));
|
|
24
|
+
while (v > 9) {
|
|
25
|
+
if (keepMasters && (v === 11 || v === 22 || v === 33)) return v;
|
|
26
|
+
v = String(v).split('').reduce((s, c) => s + Number(c), 0);
|
|
27
|
+
}
|
|
28
|
+
return v;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const digitSum = (n) => String(Math.abs(Math.trunc(n))).split('').reduce((s, c) => s + Number(c), 0);
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* 生命数字主函数(公历出生日期)。
|
|
35
|
+
* @param {{year:number, month:number, day:number}} input
|
|
36
|
+
*/
|
|
37
|
+
export function buildNumerology(input) {
|
|
38
|
+
const { year, month, day } = input || {};
|
|
39
|
+
if (!Number.isInteger(year) || !Number.isInteger(month) || !Number.isInteger(day)
|
|
40
|
+
|| year < 1900 || year > 2100 || month < 1 || month > 12 || day < 1 || day > daysInMonth(year, month)) {
|
|
41
|
+
throw new Error('生命数字需要真实存在的公历出生日期(year 1900–2100、month、day)——请向用户询问,勿猜');
|
|
42
|
+
}
|
|
43
|
+
const lifePath = reduceNumber(digitSum(year) + digitSum(month) + digitSum(day));
|
|
44
|
+
const birthday = reduceNumber(day);
|
|
45
|
+
const attitude = reduceNumber(month + day);
|
|
46
|
+
const parts = [
|
|
47
|
+
{ key: 'lifePath', name: '生命路径数', number: lifePath, meaning: NUMBER_MEANING[lifePath] },
|
|
48
|
+
{ key: 'birthday', name: '生日数', number: birthday, meaning: NUMBER_MEANING[birthday] },
|
|
49
|
+
{ key: 'attitude', name: '态度数', number: attitude, meaning: NUMBER_MEANING[attitude] },
|
|
50
|
+
];
|
|
51
|
+
return {
|
|
52
|
+
system: 'numerology',
|
|
53
|
+
meta: { date: `${year}-${String(month).padStart(2, '0')}-${String(day).padStart(2, '0')}`, system: '西方数字学' },
|
|
54
|
+
numbers: { lifePath, birthday, attitude },
|
|
55
|
+
parts,
|
|
56
|
+
primary: parts[0],
|
|
57
|
+
sources: ['生命路径=出生年月日逐位求和归约;生日数=日归约;态度数=月+日归约(11/22/33 保留)'],
|
|
58
|
+
disclaimer: '生命数字属西方数字学体系,与八字/紫微等东方术数口径不同、不可互相印证;仅供自我认识参考。',
|
|
59
|
+
};
|
|
60
|
+
}
|
package/engine/rectify.js
CHANGED
|
@@ -80,6 +80,7 @@ export function rectifyBirth(opts) {
|
|
|
80
80
|
gender,
|
|
81
81
|
lateZi: opts.lateZi !== false,
|
|
82
82
|
trueSolar: opts.trueSolar !== false,
|
|
83
|
+
dst: opts.dst !== false,
|
|
83
84
|
address: typeof opts.address === 'string' ? opts.address : undefined,
|
|
84
85
|
longitude: typeof opts.longitude === 'number' ? opts.longitude : undefined,
|
|
85
86
|
geo: opts.geo && typeof opts.geo === 'object' ? opts.geo : undefined,
|
package/engine/star.js
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
// 星座档案 + 当日娱乐向基调:纯本地确定性(同一天同一星座结果永远一致)。
|
|
2
|
+
// 星座按太阳过宫的通行日期区间(公历);当日基调由日期哈希生成,不是随机数。
|
|
3
|
+
import { daysInMonth } from './validate.js';
|
|
4
|
+
|
|
5
|
+
/** 12 星座(区间含起止日;摩羯跨年由两端分别匹配)。 */
|
|
6
|
+
export const SIGNS = [
|
|
7
|
+
{ name: '白羊座', element: '火象', planet: '火星', range: [3, 21, 4, 19], keywords: '热血、开拓、直来直往', compatible: '狮子、射手、天秤' },
|
|
8
|
+
{ name: '金牛座', element: '土象', planet: '金星', range: [4, 20, 5, 20], keywords: '沉稳、务实、惜物恋旧', compatible: '处女、摩羯、巨蟹' },
|
|
9
|
+
{ name: '双子座', element: '风象', planet: '水星', range: [5, 21, 6, 21], keywords: '灵动、好奇、能言善变', compatible: '天秤、水瓶、白羊' },
|
|
10
|
+
{ name: '巨蟹座', element: '水象', planet: '月亮', range: [6, 22, 7, 22], keywords: '顾家、念旧、外柔内韧', compatible: '天蝎、双鱼、金牛' },
|
|
11
|
+
{ name: '狮子座', element: '火象', planet: '太阳', range: [7, 23, 8, 22], keywords: '大方、自信、天生舞台感', compatible: '白羊、射手、天秤' },
|
|
12
|
+
{ name: '处女座', element: '土象', planet: '水星', range: [8, 23, 9, 22], keywords: '细致、挑剔、完美主义', compatible: '金牛、摩羯、巨蟹' },
|
|
13
|
+
{ name: '天秤座', element: '风象', planet: '金星', range: [9, 23, 10, 23], keywords: '优雅、权衡、社交高手', compatible: '双子、水瓶、狮子' },
|
|
14
|
+
{ name: '天蝎座', element: '水象', planet: '冥王星', range: [10, 24, 11, 22], keywords: '深挚、洞察、爱憎分明', compatible: '巨蟹、双鱼、处女' },
|
|
15
|
+
{ name: '射手座', element: '火象', planet: '木星', range: [11, 23, 12, 21], keywords: '乐观、自由、远方体质', compatible: '白羊、狮子、水瓶' },
|
|
16
|
+
{ name: '摩羯座', element: '土象', planet: '土星', range: [12, 22, 1, 19], keywords: '自律、坚忍、大器晚成', compatible: '金牛、处女、天蝎' },
|
|
17
|
+
{ name: '水瓶座', element: '风象', planet: '天王星', range: [1, 20, 2, 18], keywords: '独立、前卫、脑洞担当', compatible: '双子、天秤、射手' },
|
|
18
|
+
{ name: '双鱼座', element: '水象', planet: '海王星', range: [2, 19, 3, 20], keywords: '温柔、共情、浪漫滤镜', compatible: '巨蟹、天蝎、金牛' },
|
|
19
|
+
];
|
|
20
|
+
|
|
21
|
+
/** 日期 → 星座条目;非法日期返回 null。 */
|
|
22
|
+
export function signOfDate(month, day) {
|
|
23
|
+
for (const s of SIGNS) {
|
|
24
|
+
const [m1, d1, m2, d2] = s.range;
|
|
25
|
+
if ((month === m1 && day >= d1) || (month === m2 && day <= d2)) return s;
|
|
26
|
+
}
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** 日期哈希(同日同结果;Math.imul 保证 32 位乘法不丢精度)。 */
|
|
31
|
+
function daySeed(y, m, d, salt) {
|
|
32
|
+
let x = ((y * 73856093) ^ (m * 19349663) ^ (d * 83492791) ^ Math.imul(salt, 2654435761)) >>> 0;
|
|
33
|
+
x = Math.imul(x ^ (x >>> 13), 0x5bd1e995);
|
|
34
|
+
return (x ^ (x >>> 15)) >>> 0;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const COLORS = ['晨雾白', '海盐蓝', '苔原绿', '落日橘', '琥珀黄', '黛蓝', '藕荷粉', '石墨灰', '橄榄绿', '雾紫'];
|
|
38
|
+
const TONE_TEXT = {
|
|
39
|
+
5: '势头极好,主动出击。',
|
|
40
|
+
4: '顺风缓行,乘势而为。',
|
|
41
|
+
3: '平顺持中,稳字当头。',
|
|
42
|
+
2: '略有阻滞,慢即是快。',
|
|
43
|
+
1: '宜静宜守,养精蓄锐。',
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* 星座 + 当日基调。
|
|
48
|
+
* @param {{year:number, month:number, day:number}} input 公历日期
|
|
49
|
+
*/
|
|
50
|
+
export function buildStar(input) {
|
|
51
|
+
const { year, month, day } = input || {};
|
|
52
|
+
if (!Number.isInteger(year) || !Number.isInteger(month) || !Number.isInteger(day)
|
|
53
|
+
|| year < 1900 || year > 2100 || month < 1 || month > 12 || day < 1 || day > daysInMonth(year, month)) {
|
|
54
|
+
throw new Error('星座需要真实存在的公历日期(year 1900–2100、month、day)——请向用户询问');
|
|
55
|
+
}
|
|
56
|
+
const sign = signOfDate(month, day);
|
|
57
|
+
if (!sign) throw new Error(`无法识别星座:${year}-${month}-${day}`);
|
|
58
|
+
const dims = ['事业', '感情', '财运', '健康'].map((k, i) => {
|
|
59
|
+
const stars = (daySeed(year, month, day, i + 1) % 5) + 1;
|
|
60
|
+
return { name: k, stars, text: TONE_TEXT[stars] };
|
|
61
|
+
});
|
|
62
|
+
return {
|
|
63
|
+
system: 'star',
|
|
64
|
+
meta: { date: `${year}-${String(month).padStart(2, '0')}-${String(day).padStart(2, '0')}` },
|
|
65
|
+
sign: {
|
|
66
|
+
name: sign.name,
|
|
67
|
+
element: sign.element,
|
|
68
|
+
planet: sign.planet,
|
|
69
|
+
keywords: sign.keywords,
|
|
70
|
+
compatible: sign.compatible,
|
|
71
|
+
range: `${sign.range[0]}月${sign.range[1]}日–${sign.range[2]}月${sign.range[3]}日`,
|
|
72
|
+
},
|
|
73
|
+
daily: {
|
|
74
|
+
tone: TONE_TEXT[(daySeed(year, month, day, 9) % 5) + 1],
|
|
75
|
+
dims,
|
|
76
|
+
luckyColor: COLORS[daySeed(year, month, day, 10) % COLORS.length],
|
|
77
|
+
luckyNumber: (daySeed(year, month, day, 11) % 9) + 1,
|
|
78
|
+
},
|
|
79
|
+
sources: ['星座区间:太阳过宫通行日期', '当日基调:日期哈希确定性生成(同日同结果)'],
|
|
80
|
+
disclaimer: '星座档案为通行天文口径;当日基调纯娱乐向,同日同结果,不构成任何现实预测。',
|
|
81
|
+
};
|
|
82
|
+
}
|
package/engine/trend.js
ADDED
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
// 人生 K 线:把「大运 + 流年」的十神对日主的影响折算成逐年 0–100 分,
|
|
2
|
+
// 再画成开高低收的蜡烛形态与 ASCII 迷你图。
|
|
3
|
+
//
|
|
4
|
+
// 口径:这是一套**简化启发式**——十神计分表固定、正负号随日主旺衰方向翻转,
|
|
5
|
+
// 只用于看形态与相对起伏;分高不等于真实吉凶,旺衰落在换档边界时连排序都不给。
|
|
6
|
+
import { buildBazi, HIDDEN_STEMS, tenGod } from './bazi.js';
|
|
7
|
+
import { ganzhiName, sexagenaryIndex, STEMS, BRANCHES } from './calendar.js';
|
|
8
|
+
|
|
9
|
+
// ── 十神计分表 ────────────────────────────────────────────────
|
|
10
|
+
// 三张表各自求和接近 0,保证分数有正负双向起伏;符号随日主旺衰方向翻转:
|
|
11
|
+
// 偏弱 → 喜生扶(印 / 比劫)为正,忌克泄耗(官杀 / 食伤 / 财)为负;
|
|
12
|
+
// 偏强 → 喜克泄耗为正,忌生扶为负;
|
|
13
|
+
// 中和 → 财官印温和加分,杀伤伤官小扣。
|
|
14
|
+
const SHISHEN_WEAK = {
|
|
15
|
+
正印: 2, 偏印: 2, 比肩: 2, 劫财: 1,
|
|
16
|
+
食神: -1, 伤官: -2, 偏财: -2, 正财: -2,
|
|
17
|
+
正官: -1, 七杀: -3,
|
|
18
|
+
};
|
|
19
|
+
const SHISHEN_STRONG = {
|
|
20
|
+
正印: -2, 偏印: -2, 比肩: -2, 劫财: -1,
|
|
21
|
+
食神: 1, 伤官: 1, 偏财: 2, 正财: 2,
|
|
22
|
+
正官: 2, 七杀: 1,
|
|
23
|
+
};
|
|
24
|
+
const SHISHEN_NEUTRAL = {
|
|
25
|
+
正印: 1, 偏印: 0, 比肩: 0, 劫财: -1,
|
|
26
|
+
食神: 1, 伤官: -2, 偏财: 1, 正财: 2,
|
|
27
|
+
正官: 2, 七杀: -2,
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const FAVOR = { 身强: 'strong', 偏强: 'strong', 中和: 'neutral', 偏弱: 'weak', 身弱: 'weak' };
|
|
31
|
+
const TABLE_OF = { strong: SHISHEN_STRONG, neutral: SHISHEN_NEUTRAL, weak: SHISHEN_WEAK };
|
|
32
|
+
|
|
33
|
+
/** 旺衰分档的换档阈值:分数贴着阈值时,多一个干支就换表,整套符号翻转。 */
|
|
34
|
+
const LEVEL_CUTS = [0.5, 1.5, 2.5, 4];
|
|
35
|
+
const BOUNDARY_EPS = 0.3;
|
|
36
|
+
|
|
37
|
+
const clamp = (v, lo, hi) => Math.max(lo, Math.min(hi, v));
|
|
38
|
+
|
|
39
|
+
/** 干支(天干 + 地支本气)对日主的十神;地支取本气藏干,与四柱十神口径一致。 */
|
|
40
|
+
function tenGodOfZhi(dayStem, branch) {
|
|
41
|
+
const main = HIDDEN_STEMS[branch] && HIDDEN_STEMS[branch][0];
|
|
42
|
+
return main ? tenGod(dayStem, main) : null;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const yearGanzhi = (year) => ganzhiName(((year - 4) % 60 + 60) % 60);
|
|
46
|
+
|
|
47
|
+
/** 把大运表补到覆盖 toAge(引擎默认只排 8 步 / 80 年)。 */
|
|
48
|
+
function extendDayun(list, direction, toAge) {
|
|
49
|
+
const out = list.slice();
|
|
50
|
+
const step = direction === '逆行' ? -1 : 1;
|
|
51
|
+
let guard = 0;
|
|
52
|
+
while (out.length && out[out.length - 1].startAge <= toAge && guard < 24) {
|
|
53
|
+
const last = out[out.length - 1];
|
|
54
|
+
// last.ganzhi 是「甲辰」这样的名字,不是六十甲子序号:先换回序号再顺/逆推一步
|
|
55
|
+
const lastIdx = sexagenaryIndex(STEMS.indexOf(last.ganzhi[0]), BRANCHES.indexOf(last.ganzhi[1]));
|
|
56
|
+
const nextIdx = (lastIdx + step + 60) % 60;
|
|
57
|
+
const g = ganzhiName(nextIdx);
|
|
58
|
+
out.push({ step: last.step + 1, startAge: last.startAge + 10, ganzhi: g.name, stem: g.stem, branch: g.branch, stemTenGod: last.stemTenGod });
|
|
59
|
+
guard++;
|
|
60
|
+
}
|
|
61
|
+
return out;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* 人生 K 线主函数。
|
|
66
|
+
* @param {{year:number,month:number,day:number,hour?:number,minute?:number,gender:'male'|'female',
|
|
67
|
+
* toAge?:number,lateZi?:boolean,trueSolar?:boolean,dst?:boolean,
|
|
68
|
+
* address?:string,longitude?:number,geo?:object}} input
|
|
69
|
+
*/
|
|
70
|
+
export function buildTrend(input) {
|
|
71
|
+
const gender = input.gender;
|
|
72
|
+
if (gender !== 'male' && gender !== 'female') {
|
|
73
|
+
throw new Error('人生 K 线需要性别:大运顺逆由性别决定,不可缺省');
|
|
74
|
+
}
|
|
75
|
+
const toAge = clamp(Number.isInteger(input.toAge) ? input.toAge : 100, 10, 100);
|
|
76
|
+
const r = buildBazi({
|
|
77
|
+
year: input.year,
|
|
78
|
+
month: input.month,
|
|
79
|
+
day: input.day,
|
|
80
|
+
hour: Number.isInteger(input.hour) ? input.hour : 12,
|
|
81
|
+
minute: Number.isInteger(input.minute) ? input.minute : 0,
|
|
82
|
+
gender,
|
|
83
|
+
lateZi: input.lateZi !== false,
|
|
84
|
+
trueSolar: input.trueSolar !== false,
|
|
85
|
+
dst: input.dst !== false,
|
|
86
|
+
address: typeof input.address === 'string' ? input.address : undefined,
|
|
87
|
+
longitude: typeof input.longitude === 'number' ? input.longitude : undefined,
|
|
88
|
+
geo: input.geo && typeof input.geo === 'object' ? input.geo : undefined,
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
const dayStem = r.pillars.day.stem;
|
|
92
|
+
const table = TABLE_OF[FAVOR[r.strength.level] || 'neutral'];
|
|
93
|
+
const rawBound = 5 * Math.max(...Object.values(table).map(Math.abs));
|
|
94
|
+
const scoreOf = (god) => table[god] ?? 0;
|
|
95
|
+
const boundary = LEVEL_CUTS.some((c) => Math.abs(r.strength.score - c) <= BOUNDARY_EPS);
|
|
96
|
+
|
|
97
|
+
const dayun = extendDayun(r.dayun.list, r.dayun.direction, toAge);
|
|
98
|
+
const segments = dayun
|
|
99
|
+
.map((d, i) => ({
|
|
100
|
+
ganZhi: d.ganzhi,
|
|
101
|
+
startAge: d.startAge,
|
|
102
|
+
endAge: (dayun[i + 1] ? dayun[i + 1].startAge : d.startAge + 10) - 1,
|
|
103
|
+
}))
|
|
104
|
+
.filter((s) => s.startAge <= toAge);
|
|
105
|
+
|
|
106
|
+
const birthYear = Number(String(r.solarTime.civilText).slice(0, 4));
|
|
107
|
+
const points = [];
|
|
108
|
+
let prev = 50;
|
|
109
|
+
for (let age = 1; age <= toAge; age++) {
|
|
110
|
+
const year = birthYear + age - 1;
|
|
111
|
+
const gz = yearGanzhi(year);
|
|
112
|
+
const seg = segments.find((s) => age >= s.startAge && age <= s.endAge) || null;
|
|
113
|
+
const godGan = tenGod(dayStem, gz.stem);
|
|
114
|
+
const godZhi = tenGodOfZhi(dayStem, gz.branch);
|
|
115
|
+
const dyGodGan = seg ? tenGod(dayStem, seg.ganZhi[0]) : null;
|
|
116
|
+
const dyGodZhi = seg ? tenGodOfZhi(dayStem, seg.ganZhi[1]) : null;
|
|
117
|
+
|
|
118
|
+
const raw = scoreOf(godGan) + scoreOf(godZhi)
|
|
119
|
+
+ (seg ? (scoreOf(dyGodGan) + scoreOf(dyGodZhi)) * 1.5 : 0);
|
|
120
|
+
const score = clamp(Math.round(((raw + rawBound) / (rawBound * 2)) * 100), 2, 99);
|
|
121
|
+
points.push({
|
|
122
|
+
age,
|
|
123
|
+
year,
|
|
124
|
+
ganZhi: gz.name,
|
|
125
|
+
dayunGanZhi: seg ? seg.ganZhi : null,
|
|
126
|
+
shiShenGan: godGan,
|
|
127
|
+
shiShenZhi: godZhi,
|
|
128
|
+
score,
|
|
129
|
+
kline: {
|
|
130
|
+
open: prev,
|
|
131
|
+
close: score,
|
|
132
|
+
high: clamp(score + 4, 0, 100),
|
|
133
|
+
low: clamp(score - 4, 0, 100),
|
|
134
|
+
},
|
|
135
|
+
});
|
|
136
|
+
prev = score;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const sorted = points.slice().sort((a, b) => b.score - a.score);
|
|
140
|
+
return {
|
|
141
|
+
system: 'trend',
|
|
142
|
+
meta: {
|
|
143
|
+
birth: String(r.solarTime.civilText).slice(0, 10),
|
|
144
|
+
gender,
|
|
145
|
+
dayMaster: `${dayStem}(${r.dayMaster.yinYang}${r.dayMaster.element})`,
|
|
146
|
+
strength: { level: r.strength.level, score: r.strength.score, boundary },
|
|
147
|
+
toAge,
|
|
148
|
+
...(boundary
|
|
149
|
+
? {
|
|
150
|
+
boundaryNote:
|
|
151
|
+
`旺衰分 ${r.strength.score} 正落在换档阈值附近(阈值 ${LEVEL_CUTS.join(' / ')},容差 ${BOUNDARY_EPS}):`
|
|
152
|
+
+ '多一个同类干支或少一个就会换计分表,整套正负号随之翻转——因此这里不出「最好 / 最差年份」的排序结论。',
|
|
153
|
+
}
|
|
154
|
+
: {}),
|
|
155
|
+
},
|
|
156
|
+
segments,
|
|
157
|
+
points,
|
|
158
|
+
highlights: boundary
|
|
159
|
+
? { suppressed: '旺衰落在换档边界,年份排序不可靠,不给顺位 / 谨慎年份', best: [], worst: [] }
|
|
160
|
+
: {
|
|
161
|
+
best: sorted.slice(0, 3).map((p) => `${p.age}岁(${p.year},${p.ganZhi},${p.score}分)`),
|
|
162
|
+
worst: sorted.slice(-3).reverse().map((p) => `${p.age}岁(${p.year},${p.ganZhi},${p.score}分)`),
|
|
163
|
+
},
|
|
164
|
+
sparkline: asciiSparkline(points),
|
|
165
|
+
sources: [
|
|
166
|
+
'运势分:流年 / 大运干支十神对日主的简化启发式计分(旺衰方向决定正负号)',
|
|
167
|
+
'K 线形态:收盘 = 当年分,开盘 = 上年分,高低各 ±4(确定性生成)',
|
|
168
|
+
],
|
|
169
|
+
disclaimer: boundary
|
|
170
|
+
? '人生 K 线为简化启发式的可视化;本盘旺衰判定处于换档边界,趋势与年份排序均不可靠,仅供展示,不构成任何现实决策依据。'
|
|
171
|
+
: '人生 K 线为简化启发式的可视化,趋势形态仅供娱乐参考,不构成任何现实决策依据;十年大运内每年分值不等于真实吉凶。',
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/** ASCII 迷你 K 线:每年一个方块(▁▂▃▄▅▆▇█),按大运分段成行。 */
|
|
176
|
+
function asciiSparkline(points) {
|
|
177
|
+
const BLOCKS = ['▁', '▂', '▃', '▄', '▅', '▆', '▇', '█'];
|
|
178
|
+
const lines = [];
|
|
179
|
+
const groups = [];
|
|
180
|
+
for (const p of points) {
|
|
181
|
+
const key = p.dayunGanZhi || '起运前';
|
|
182
|
+
const last = groups[groups.length - 1];
|
|
183
|
+
if (!last || last.key !== key) groups.push({ key, points: [] });
|
|
184
|
+
groups[groups.length - 1].points.push(p);
|
|
185
|
+
}
|
|
186
|
+
for (const g of groups) {
|
|
187
|
+
const bars = g.points
|
|
188
|
+
.map((p) => BLOCKS[clamp(Math.floor((p.score / 100) * BLOCKS.length), 0, BLOCKS.length - 1)])
|
|
189
|
+
.join('');
|
|
190
|
+
lines.push(`${g.key}(${g.points[0].age}–${g.points[g.points.length - 1].age}岁):${bars}`);
|
|
191
|
+
}
|
|
192
|
+
return lines;
|
|
193
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
// 小六壬(诸葛马前课):月/日/时(或报三个数)掐指落宫,六宫循环。
|
|
2
|
+
// 口径:通行掌诀;断语为白话提炼(非原文照录)。轻量快占,属民俗参考。
|
|
3
|
+
import { solarToLunar } from './lunar.js';
|
|
4
|
+
import { hourToBranch } from './almanac.js';
|
|
5
|
+
import { BRANCHES } from './calendar.js';
|
|
6
|
+
|
|
7
|
+
/** 六宫:吉凶 + 白话断语。 */
|
|
8
|
+
export const PALACES = {
|
|
9
|
+
大安: { luck: '吉', text: '事事昌顺,宜静守不宜妄动;所问之事根基稳,缓一缓更妥。' },
|
|
10
|
+
留连: { luck: '凶', text: '事多牵扯反复,暗中有耗;宜守不宜进,防拖延与口舌。' },
|
|
11
|
+
速喜: { luck: '吉', text: '喜事将至,行动宜快;所问之事三日内外见分晓。' },
|
|
12
|
+
赤口: { luck: '凶', text: '口舌是非、官讼之象;宜谨言慎行,凡事留凭证。' },
|
|
13
|
+
小吉: { luck: '吉', text: '小有助力,慢慢来能成;合作与人际有贵人。' },
|
|
14
|
+
空亡: { luck: '凶', text: '落空之象,谋事多不成;宜停一停,改期再议。' },
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export const PALACE_ORDER = ['大安', '留连', '速喜', '赤口', '小吉', '空亡'];
|
|
18
|
+
|
|
19
|
+
const pick = (n) => PALACE_ORDER[(((n - 1) % 6) + 6) % 6];
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* 小六壬主函数。
|
|
23
|
+
* @param {{mode:'time'|'number', date?:string, hour?:number, num1?:number, num2?:number, num3?:number, question?:string}} input
|
|
24
|
+
* time 模式下 date 与 hour 必须成对(缺时辰则同一天任何时刻同宫,等于静默代填)。
|
|
25
|
+
*/
|
|
26
|
+
export function buildXiaoliuren(input) {
|
|
27
|
+
const mode = input.mode === 'number' ? 'number' : 'time';
|
|
28
|
+
let m;
|
|
29
|
+
let d;
|
|
30
|
+
let h;
|
|
31
|
+
let originText;
|
|
32
|
+
if (mode === 'time') {
|
|
33
|
+
if (typeof input.hour !== 'number' || !Number.isInteger(input.hour) || input.hour < 0 || input.hour > 23) {
|
|
34
|
+
throw new Error('时间起课必须同时给 date 与 hour(0–23):掐指按时辰推进,缺时辰不可代填');
|
|
35
|
+
}
|
|
36
|
+
const mt = /^(\d{4})-(\d{1,2})-(\d{1,2})$/.exec(String(input.date || '').trim());
|
|
37
|
+
if (!mt) throw new Error('时间起课需要 date(公历 YYYY-MM-DD)——请向用户询问日期');
|
|
38
|
+
const lunar = solarToLunar(Number(mt[1]), Number(mt[2]), Number(mt[3]));
|
|
39
|
+
const branch = hourToBranch(input.hour);
|
|
40
|
+
m = lunar.month;
|
|
41
|
+
d = lunar.day;
|
|
42
|
+
h = BRANCHES.indexOf(branch) + 1;
|
|
43
|
+
originText = `农历${lunar.month}月${lunar.day}日 ${branch}时:月${m} → 日${d} → 时${h} 掐指落宫`;
|
|
44
|
+
} else {
|
|
45
|
+
const nums = [input.num1, input.num2, input.num3].map(Number);
|
|
46
|
+
if (nums.some((x) => !Number.isInteger(x) || x < 1 || x > 99)) {
|
|
47
|
+
throw new Error('报数起课需要三个 1–99 的正整数(随口报即可)——缺数请向用户索要,不要代填');
|
|
48
|
+
}
|
|
49
|
+
[m, d, h] = nums;
|
|
50
|
+
originText = `报数 ${m}、${d}、${h}:依次按月/日/时位掐指落宫`;
|
|
51
|
+
}
|
|
52
|
+
const p1 = pick(m);
|
|
53
|
+
const p2 = pick(PALACE_ORDER.indexOf(p1) + 1 + d - 1);
|
|
54
|
+
const p3 = pick(PALACE_ORDER.indexOf(p2) + 1 + h - 1);
|
|
55
|
+
const info = PALACES[p3];
|
|
56
|
+
return {
|
|
57
|
+
system: 'xiaoliuren',
|
|
58
|
+
meta: { mode: mode === 'time' ? '时间起课' : '报数起课', question: input.question || null, originText },
|
|
59
|
+
steps: [p1, p2, p3],
|
|
60
|
+
result: { palace: p3, luck: info.luck, text: info.text },
|
|
61
|
+
sources: ['掌诀口径:通行马前课(白话提炼,非原文照录)'],
|
|
62
|
+
disclaimer: '小六壬为轻量快占,属民俗娱乐参考;重要事项请综合现实信息判断。',
|
|
63
|
+
};
|
|
64
|
+
}
|
package/engine/zeri.js
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
// 择日:逐日按黄历「建除简表宜忌」+ 建除神适配 + 冲属相排除打分排序。
|
|
2
|
+
// 口径透明:这是排序引导,不是吉凶断语;未收录逐日神煞,冲煞按日支对生肖。
|
|
3
|
+
import { fromJDN, toJDN } from './calendar.js';
|
|
4
|
+
import { buildAlmanac, SHENGXIAO } from './almanac.js';
|
|
5
|
+
|
|
6
|
+
/** 支持的事项:直接宜词 + 关联宜词(黄历措辞)。 */
|
|
7
|
+
export const TASKS = {
|
|
8
|
+
嫁娶: { yi: ['嫁娶', '结亲', '订盟', '纳采'], related: ['会亲友', '裁衣'], category: 'marriage' },
|
|
9
|
+
订婚: { yi: ['订盟', '纳采'], related: ['嫁娶', '会亲友'], category: 'marriage' },
|
|
10
|
+
开业: { yi: ['开市', '立券', '交易'], related: ['纳财', '挂匾'], category: 'business' },
|
|
11
|
+
入宅: { yi: ['入宅', '移徙'], related: ['安床', '修造', '扫舍'], category: 'move' },
|
|
12
|
+
搬家: { yi: ['移徙', '入宅'], related: ['安床', '修造'], category: 'move' },
|
|
13
|
+
动土: { yi: ['动土', '修造'], related: ['竖柱', '上梁'], category: 'construction' },
|
|
14
|
+
装修: { yi: ['修造', '修饰垣墙'], related: ['动土'], category: 'construction' },
|
|
15
|
+
安葬: { yi: ['安葬', '破土'], related: ['立碑', '埋穴'], category: 'burial' },
|
|
16
|
+
出行: { yi: ['出行'], related: ['上任', '赴任'], category: 'travel' },
|
|
17
|
+
祭祀: { yi: ['祭祀'], related: ['祈福', '求嗣'], category: 'worship' },
|
|
18
|
+
签约: { yi: ['立券', '交易'], related: ['纳财', '开市'], category: 'contract' },
|
|
19
|
+
求医: { yi: ['求医', '疗病'], related: ['针灸'], category: 'medical' },
|
|
20
|
+
纳财: { yi: ['纳财', '收账'], related: ['开市', '立券'], category: 'contract' },
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
/** 建除十二神按事项大类的通行适配(简化口径)。 */
|
|
24
|
+
const ZHIXING_FAVOR = {
|
|
25
|
+
marriage: ['满', '定', '成', '开'],
|
|
26
|
+
business: ['开', '成', '满', '收'],
|
|
27
|
+
move: ['定', '成', '开', '除'],
|
|
28
|
+
construction: ['建', '除', '成', '开'],
|
|
29
|
+
burial: ['除', '定', '闭', '破'],
|
|
30
|
+
travel: ['建', '成', '开', '除'],
|
|
31
|
+
worship: ['除', '满', '平', '定', '执', '成', '开'],
|
|
32
|
+
contract: ['定', '成', '收', '开'],
|
|
33
|
+
medical: ['除', '开'],
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const VERDICT = (score, ctx) => {
|
|
37
|
+
if (ctx.chongSelf) {
|
|
38
|
+
return { verdict: '回避', text: `当日冲${ctx.chongSelf}(当事人属相):传统择日径直排除,不进推荐与可用名单。`, excluded: true };
|
|
39
|
+
}
|
|
40
|
+
if (score >= 3) return { verdict: '推荐', text: '宜项与建除相配,此日对所问事项顺遂。', excluded: false };
|
|
41
|
+
if (score >= 1) {
|
|
42
|
+
return {
|
|
43
|
+
verdict: '可用',
|
|
44
|
+
text: ctx.jiHit.length ? `可考虑,但当日忌${ctx.jiHit.join('、')}:宜项加分、忌项减分,未抵消干净。` : '未见冲克,可用;若有更好的日子,优先取推荐日。',
|
|
45
|
+
excluded: false,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
if (score >= 0) return { verdict: '平平', text: ctx.jiHit.length ? `当日忌${ctx.jiHit.join('、')},宜项不相干,不作首选。` : '宜忌不相干,无助力也无冲克。', excluded: false };
|
|
49
|
+
return { verdict: '回避', text: ctx.jiHit.length ? `当日忌${ctx.jiHit.join('、')},对所问事项不宜。` : '减分项多于加分项,此日不宜。', excluded: false };
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* 择日主函数。
|
|
54
|
+
* @param {{task:string, startDate:string, days?:number, shengXiao?:string}} input
|
|
55
|
+
* startDate 必填(公历 YYYY-MM-DD),避免引擎在无时钟环境下静默取「今天」。
|
|
56
|
+
*/
|
|
57
|
+
export function buildZeri(input) {
|
|
58
|
+
const task = input.task;
|
|
59
|
+
if (!TASKS[task]) throw new Error(`不支持的事项「${task}」。支持:${Object.keys(TASKS).join('、')}`);
|
|
60
|
+
const m = /^(\d{4})-(\d{1,2})-(\d{1,2})$/.exec(String(input.startDate || '').trim());
|
|
61
|
+
if (!m) throw new Error('择日需要 startDate(公历 YYYY-MM-DD)——请向用户询问起算日,不要代填');
|
|
62
|
+
const start = { year: Number(m[1]), month: Number(m[2]), day: Number(m[3]) };
|
|
63
|
+
const days = Math.max(1, Math.min(60, Number.isInteger(input.days) ? input.days : 30));
|
|
64
|
+
const shengXiaoRaw = typeof input.shengXiao === 'string' ? input.shengXiao.trim() : '';
|
|
65
|
+
const shengXiao = shengXiaoRaw || null;
|
|
66
|
+
if (shengXiao && !SHENGXIAO.includes(shengXiao)) throw new Error(`shengXiao 须为生肖名:${SHENGXIAO.join('、')}`);
|
|
67
|
+
|
|
68
|
+
const spec = TASKS[task];
|
|
69
|
+
const zhixingSet = ZHIXING_FAVOR[spec.category];
|
|
70
|
+
const startJdn = toJDN(start.year, start.month, start.day);
|
|
71
|
+
const list = [];
|
|
72
|
+
for (let i = 0; i < days; i++) {
|
|
73
|
+
const p = fromJDN(startJdn + i);
|
|
74
|
+
const a = buildAlmanac(p);
|
|
75
|
+
let score = 0;
|
|
76
|
+
const reasons = [];
|
|
77
|
+
const yiHit = spec.yi.filter((w) => a.yi.includes(w));
|
|
78
|
+
if (yiHit.length) { score += 2; reasons.push(`宜${yiHit.join('、')}(+2)`); }
|
|
79
|
+
const relHit = spec.related.filter((w) => a.yi.includes(w));
|
|
80
|
+
if (relHit.length) { score += 1; reasons.push(`关联宜项${relHit.join('、')}(+1)`); }
|
|
81
|
+
const jiHit = [...spec.yi, ...spec.related].filter((w) => a.ji.includes(w));
|
|
82
|
+
if (jiHit.length) { score -= 3; reasons.push(`忌${jiHit.join('、')}(-3)`); }
|
|
83
|
+
const chongSelf = shengXiao && a.chong.shengXiao === shengXiao ? shengXiao : null;
|
|
84
|
+
if (chongSelf) { score -= 2; reasons.push(`冲${shengXiao}(当事人属相,径直排除)`); }
|
|
85
|
+
if (zhixingSet.includes(a.zhiXing)) { score += 1; reasons.push(`建除${a.zhiXing}神利${task}(+1)`); }
|
|
86
|
+
if (a.zhiXing === '破' && spec.category !== 'burial') { score -= 1; reasons.push('破日(-1)'); }
|
|
87
|
+
if (a.zhiXing === '危' && spec.category !== 'travel' && spec.category !== 'worship') { score -= 1; reasons.push('危日(-1)'); }
|
|
88
|
+
|
|
89
|
+
const v = VERDICT(score, { chongSelf, jiHit });
|
|
90
|
+
list.push({
|
|
91
|
+
date: a.meta.date,
|
|
92
|
+
weekday: a.meta.weekday,
|
|
93
|
+
lunar: a.lunar.text,
|
|
94
|
+
dayGanZhi: a.ganzhi.day,
|
|
95
|
+
zhiXing: a.zhiXing,
|
|
96
|
+
yi: a.yi,
|
|
97
|
+
ji: a.ji,
|
|
98
|
+
chong: a.chong.shengXiao,
|
|
99
|
+
score,
|
|
100
|
+
reasons,
|
|
101
|
+
verdict: v.verdict,
|
|
102
|
+
verdictText: v.text,
|
|
103
|
+
...(v.excluded ? { excluded: true } : {}),
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const ranked = list.slice().sort((x, y) => y.score - x.score || (x.date < y.date ? -1 : 1));
|
|
108
|
+
return {
|
|
109
|
+
system: 'zeri',
|
|
110
|
+
meta: {
|
|
111
|
+
task,
|
|
112
|
+
startDate: `${start.year}-${String(start.month).padStart(2, '0')}-${String(start.day).padStart(2, '0')}`,
|
|
113
|
+
days,
|
|
114
|
+
shengXiao,
|
|
115
|
+
scope: shengXiao
|
|
116
|
+
? `宜忌按建除简表;已给当事人属相(${shengXiao}),凡当日冲该属相径直排除。评分仅用于排序引导。`
|
|
117
|
+
: '宜忌按建除简表;未给当事人属相,未做冲属相排除。评分仅用于排序引导。',
|
|
118
|
+
},
|
|
119
|
+
best: ranked.filter((d) => d.verdict === '推荐' && !d.excluded).slice(0, 5).map((d) => d.date),
|
|
120
|
+
usable: ranked.filter((d) => d.verdict === '可用' && !d.excluded).slice(0, 5).map((d) => d.date),
|
|
121
|
+
avoid: list.filter((d) => d.verdict === '回避').map((d) => `${d.date}(${d.reasons.join(';') || '综合减分'})`),
|
|
122
|
+
days: list,
|
|
123
|
+
excludedCount: list.filter((d) => d.excluded).length,
|
|
124
|
+
sources: ['宜忌:建除十二神通行简表', '建除适配与冲煞:传统通行口径', '排序:宜忌加减分 + 属相排除(确定性)'],
|
|
125
|
+
disclaimer: '择日为传统黄历口径的本地计算,属民俗参考;评分只用于排序,不构成择日结论。',
|
|
126
|
+
};
|
|
127
|
+
}
|
package/engine/ziwei.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import { STEMS, BRANCHES, ganzhiName, sexagenaryIndex, jdToParts, partsToJd, formatJd } from './calendar.js';
|
|
3
3
|
import { solarToLunar } from './lunar.js';
|
|
4
4
|
import { applyTrueSolar } from './bazi.js';
|
|
5
|
+
import { applyDst } from './dst.js';
|
|
5
6
|
import { assertCivil } from './validate.js';
|
|
6
7
|
import { nayinOf } from './nayin.js';
|
|
7
8
|
import { resolveAddress } from './geo.js';
|
|
@@ -203,7 +204,10 @@ export function buildZiwei(input) {
|
|
|
203
204
|
const longitude = typeof input.longitude === 'number'
|
|
204
205
|
? input.longitude
|
|
205
206
|
: (geo && typeof geo.lon === 'number' ? geo.lon : undefined);
|
|
206
|
-
const
|
|
207
|
+
const civilJdRaw = partsToJd({ year: cy, month: cm, day: cd, hour: ch, minute: cmin });
|
|
208
|
+
// 1986–1991 中国夏令时:先按钟面时间校正,再叠加真太阳时
|
|
209
|
+
const dst = input.dst === false ? { applied: false, note: null } : applyDst({ year: cy, month: cm, day: cd, hour: ch, minute: cmin });
|
|
210
|
+
const civilJd = dst.applied ? partsToJd({ year: dst.year, month: dst.month, day: dst.day, hour: dst.hour, minute: dst.minute }) : civilJdRaw;
|
|
207
211
|
const solar = useTrueSolar
|
|
208
212
|
? applyTrueSolar(civilJd, longitude)
|
|
209
213
|
: { offsetMinutes: 0, jd: civilJd, note: '未启用真太阳时' };
|
|
@@ -458,12 +462,13 @@ export function buildZiwei(input) {
|
|
|
458
462
|
address: input.address || null,
|
|
459
463
|
geo,
|
|
460
464
|
trueSolar: useTrueSolar,
|
|
465
|
+
dst: { applied: !!dst.applied, note: dst.note || null },
|
|
461
466
|
},
|
|
462
467
|
solarTime: {
|
|
463
468
|
civilText: `${cy}-${String(cm).padStart(2, '0')}-${String(cd).padStart(2, '0')} ${String(ch).padStart(2, '0')}:${String(cmin).padStart(2, '0')}`,
|
|
464
469
|
trueSolarText: formatJd(solar.jd),
|
|
465
470
|
offsetMinutes: solar.offsetMinutes,
|
|
466
|
-
note: solar.note,
|
|
471
|
+
note: dst.applied ? `${dst.note};${solar.note}` : solar.note,
|
|
467
472
|
},
|
|
468
473
|
lunar: {
|
|
469
474
|
year: lunarRaw.lunarYear,
|