feat: 祈福小助手万年历小程序第一期

This commit is contained in:
gouki
2026-08-06 08:47:09 +00:00
commit a3e6aa9c01
136 changed files with 16597 additions and 0 deletions
+34
View File
@@ -0,0 +1,34 @@
{
"name": "@lunar/core",
"version": "0.1.0",
"private": true,
"description": "Core calculation engine for lunar calendar app",
"type": "module",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.js",
"require": "./dist/index.cjs"
}
},
"files": [
"dist"
],
"scripts": {
"dev": "tsup --watch",
"build": "tsup",
"test": "vitest run",
"lint": "eslint .",
"clean": "rm -rf dist"
},
"dependencies": {
"tyme4ts": "^1.5.1"
},
"devDependencies": {
"tsup": "^8.4.0",
"typescript": "^5.7.0",
"vitest": "^4.1.10"
}
}
@@ -0,0 +1,51 @@
import { describe, it, expect } from 'vitest';
import { birthInfoToBazi } from '../index';
const GANZHI = /^[甲乙丙丁戊己庚辛壬癸][子丑寅卯辰巳午未申酉戌亥]$/;
describe('bazi transformer', () => {
const bazi = birthInfoToBazi({ year: 1990, month: 5, day: 15, hour: 12, minute: 0, gender: 'male' });
it('produces four pillars with valid ganzhi', () => {
expect(bazi.eightChar.yearPillar.ganzhi).toMatch(GANZHI);
expect(bazi.eightChar.monthPillar.ganzhi).toMatch(GANZHI);
expect(bazi.eightChar.dayPillar.ganzhi).toMatch(GANZHI);
expect(bazi.eightChar.hourPillar.ganzhi).toMatch(GANZHI);
});
it('exposes day master info', () => {
expect(bazi.eightChar.dayMasterStem).toMatch(/^[甲乙丙丁戊己庚辛壬癸]$/);
expect(['木', '火', '土', '金', '水']).toContain(bazi.eightChar.dayMasterElement);
});
it('returns 10 decade fortunes and 10 annual fortunes', () => {
expect(bazi.decadeFortunes).toHaveLength(10);
expect(bazi.annualFortunes).toHaveLength(10);
expect(bazi.decadeFortunes[0].ganzhi).toMatch(GANZHI);
});
it('returns child limit with non-negative start age', () => {
expect(bazi.childLimit.startAge).toBeGreaterThanOrEqual(0);
expect(bazi.childLimit.forward).toBeTypeOf('boolean');
});
it('includes hidden stems with ten stars', () => {
const yearHs = bazi.eightChar.yearPillar.hideStems;
expect(yearHs.length).toBeGreaterThan(0);
expect(yearHs[0].stem).toMatch(/^[甲乙丙丁戊己庚辛壬癸]$/);
});
it('default sect: late zi (23:30) rolls to next day', () => {
const late = birthInfoToBazi({ year: 2026, month: 8, day: 3, hour: 23, minute: 30, gender: 'male' });
expect(late.eightChar.dayPillar.ganzhi).toBe('庚戌');
expect(late.eightChar.hourPillar.ganzhi).toBe('丙子');
});
it('earlyZiSameDay sect: late zi (23:30) keeps the current day', () => {
const early = birthInfoToBazi({
year: 2026, month: 8, day: 3, hour: 23, minute: 30, gender: 'male', ziSect: 'earlyZiSameDay',
});
expect(early.eightChar.dayPillar.ganzhi).toBe('己酉');
expect(early.eightChar.hourPillar.ganzhi).toBe('甲子');
});
});
@@ -0,0 +1,68 @@
import { describe, it, expect } from 'vitest';
import { calculateBoneWeight } from '../index';
describe('bone weight', () => {
it('sums weights correctly for 甲子年正月初一子时', () => {
const r = calculateBoneWeight(0, 1, 1, 0);
expect(r.yearWeight).toBe(12);
expect(r.monthWeight).toBe(6);
expect(r.dayWeight).toBe(5);
expect(r.hourWeight).toBe(16);
expect(r.totalWeight).toBe(39);
expect(r.totalLiang).toBe(3);
expect(r.totalQian).toBe(9);
});
it('returns a non-empty interpretation with a valid fortune grade', () => {
const r = calculateBoneWeight(12, 6, 15, 6);
expect(r.interpretation.length).toBeGreaterThan(0);
expect(['上上', '上', '中上', '中', '中下', '下']).toContain(r.fortune);
});
it('falls back to the nearest interpretation for unknown totals', () => {
// 4 + 9 + 9 + 9 = 31 → exact match exists; use an out-of-range input
const r = calculateBoneWeight(0, 1, 1, 11); // 12 + 6 + 5 + 6 = 29
expect(r.totalWeight).toBe(29);
expect(r.interpretation.length).toBeGreaterThan(0);
});
it('year table matches the mainstream 称骨年表', () => {
const cases: [number, number][] = [
[0, 12], // 甲子
[7, 8], // 辛未
[16, 12], // 庚辰
[17, 6], // 辛巳
[18, 8], // 壬午
[21, 15], // 乙酉
[51, 8], // 乙卯
[54, 19], // 戊午
];
for (const [idx, wt] of cases) {
const r = calculateBoneWeight(idx, 1, 1, 0);
expect(r.yearWeight, `year index ${idx}`).toBe(wt);
}
});
it('day table: 初五 is 1两6钱', () => {
const r = calculateBoneWeight(0, 1, 5, 0);
expect(r.dayWeight).toBe(16);
});
it('previously missing poems (5两8钱~7两1钱) resolve exactly, not by nearest match', () => {
const cases: [number, number, number, number, number, string][] = [
[18, 6, 26, 5, 58, '雁塔题名'],
[15, 6, 8, 7, 59, '甲第之中'],
[24, 3, 18, 6, 61, '金榜客'],
[42, 6, 26, 5, 63, '定中高科'],
[54, 9, 18, 6, 65, '安邦'],
[54, 6, 8, 5, 67, '田园家业'],
[54, 6, 18, 5, 69, '前禄星'],
[54, 3, 26, 5, 71, '公侯卿相'],
];
for (const [y, m, d, h, total, phrase] of cases) {
const r = calculateBoneWeight(y, m, d, h);
expect(r.totalWeight, `expected total ${total}`).toBe(total);
expect(r.interpretation, `poem for ${total}`).toContain(phrase);
}
});
});
@@ -0,0 +1,62 @@
import { describe, it, expect } from 'vitest';
import { getDayInfo, getMonthCalendar, getTodayInfo } from '../index';
describe('day transformers', () => {
it('returns correct lunar info for 2024-02-10 (春节正月初一)', () => {
const di = getDayInfo(2024, 2, 10);
expect(di.solarDate).toBe('2024-02-10');
expect(di.lunarMonth).toBe(1);
expect(di.lunarDay).toBe(1);
expect(di.lunarDayName).toBe('初一');
expect(di.lunarYearGanzhi).toMatch(/^[甲乙丙丁戊己庚辛壬癸][子丑寅卯辰巳午未申酉戌亥]$/);
expect(di.weekDayIndex).toBe(6);
expect(di.isWeekend).toBe(true);
});
it('getTodayInfo returns today', () => {
const now = new Date();
const di = getTodayInfo();
expect(di.solarYear).toBe(now.getFullYear());
expect(di.solarMonth).toBe(now.getMonth() + 1);
expect(di.solarDay).toBe(now.getDate());
expect(di.isToday).toBe(true);
});
it('getMonthCalendar respects weekStart', () => {
// 2024-02-01 is a Thursday
const sunFirst = getMonthCalendar(2024, 2, 0);
const monFirst = getMonthCalendar(2024, 2, 1);
for (const row of sunFirst) expect(row).toHaveLength(7);
for (const row of monFirst) expect(row).toHaveLength(7);
expect(sunFirst[0][0].weekDayIndex).toBe(0); // Sunday first
expect(monFirst[0][0].weekDayIndex).toBe(1); // Monday first
// Both grids must contain the 1st of the month
const flatSun = sunFirst.flat();
const flatMon = monFirst.flat();
expect(flatSun.some(d => d.solarDate === '2024-02-01')).toBe(true);
expect(flatMon.some(d => d.solarDate === '2024-02-01')).toBe(true);
});
it('provides season, term progress, julian, buddhist era and hijri date', () => {
const di = getDayInfo(2026, 8, 3); // 大暑期间,农历六月廿一
expect(di.season).toBe('夏季');
expect(di.termDayIndex).toBeGreaterThan(0);
expect(di.nextSolarTerm).toBe('立秋');
expect(di.daysToNextTerm).toBe(4);
expect(di.julianDay).toBeGreaterThan(2450000);
expect(di.buddhistYear).toBe(2026 + 543);
expect(di.hijriDate).toMatch(/^\d{4}年\d{2}月\d{2}日$/);
});
it('marks Buddhist festivals by lunar date', () => {
// 2024-05-15 is 佛诞(浴佛节)农历四月初八
const di = getDayInfo(2024, 5, 15);
expect(di.lunarMonth).toBe(4);
expect(di.lunarDay).toBe(8);
expect(di.buddhistFestival).toBe('释迦牟尼佛圣诞(浴佛节)');
});
});
@@ -0,0 +1,22 @@
import { describe, it, expect } from 'vitest';
import { analyzeElementBalance, birthInfoToBazi } from '../index';
describe('element balance', () => {
const eightChar = birthInfoToBazi({
year: 1990, month: 5, day: 15, hour: 12, minute: 0, gender: 'male',
}).eightChar;
const profile = analyzeElementBalance(eightChar);
it('has positive total that matches sum of parts', () => {
expect(profile.total).toBeGreaterThan(0);
expect(profile.wood + profile.fire + profile.earth + profile.metal + profile.water)
.toBeCloseTo(profile.total);
});
it('identifies dominant and weakest elements', () => {
expect(['木', '火', '土', '金', '水']).toContain(profile.dominant);
expect(['木', '火', '土', '金', '水']).toContain(profile.weakest);
expect(profile.isBalanced).toBeTypeOf('boolean');
});
});
@@ -0,0 +1,34 @@
import { describe, it, expect } from 'vitest';
import { calculateDailyFortune, birthInfoToBazi } from '../index';
const SCORE_LEVELS = ['great', 'good', 'fair', 'poor', 'bad'] as const;
describe('daily fortune', () => {
const eightChar = birthInfoToBazi({
year: 1990, month: 5, day: 15, hour: 12, minute: 0, gender: 'female',
}).eightChar;
const fortune = calculateDailyFortune(eightChar, new Date(2026, 7, 2));
it('returns bounded score and valid level', () => {
expect(fortune.overallScore).toBeGreaterThanOrEqual(-100);
expect(fortune.overallScore).toBeLessThanOrEqual(100);
expect(SCORE_LEVELS).toContain(fortune.scoreLevel);
});
it('evaluates all four pillars', () => {
expect(fortune.pillarRelationships).toHaveLength(4);
expect(fortune.pillarRelationships.map(r => r.pillar)).toEqual(['year', 'month', 'day', 'hour']);
});
it('returns the requested date', () => {
expect(fortune.date).toBe('2026-08-02');
});
it('produces suggestions and category scores', () => {
expect(fortune.suggestions.length).toBeGreaterThan(0);
expect(fortune.luckyAspects.length).toBeGreaterThan(0);
const { love, career, wealth, health } = fortune.categoryScores;
expect([love, career, wealth, health].every(v => v >= -100 && v <= 100)).toBe(true);
});
});
@@ -0,0 +1,40 @@
import { describe, it, expect } from 'vitest';
import { analyzeFortuneGanzhi, birthInfoToBazi } from '../index';
describe('fortune luck (大运/流年 vs 日主)', () => {
// 2026-08-03 12:00: 日干己、日支酉
const bazi = birthInfoToBazi({ year: 2026, month: 8, day: 3, hour: 12, minute: 0, gender: 'male' });
const dayStem = bazi.eightChar.dayPillar.heavenStem;
const dayBranch = bazi.eightChar.dayPillar.earthBranch;
it('甲子 vs 己酉: 正官 + 天干合 + 克我 → 吉', () => {
const r = analyzeFortuneGanzhi('甲子', dayStem, dayBranch);
expect(r.tenStar).toBe('正官');
expect(r.stemCombine).toBe(true);
expect(r.elementRelation).toBe('克我');
expect(r.level).toBe('吉');
});
it('乙酉 vs 己酉: 七杀 + 自刑 → 凶', () => {
const r = analyzeFortuneGanzhi('乙酉', dayStem, dayBranch);
expect(r.tenStar).toBe('七杀');
expect(r.branchPunish).toBe(true); // 酉酉自刑
expect(r.level).toBe('凶');
});
it('丙子 vs 己未: 正印 + 子未六害', () => {
const r = analyzeFortuneGanzhi('丙子', '己', '未');
expect(r.tenStar).toBe('正印');
expect(r.branchHarm).toBe(true);
});
it('all decade fortunes produce valid luck info', () => {
for (const df of bazi.decadeFortunes) {
const r = analyzeFortuneGanzhi(df.ganzhi, dayStem, dayBranch);
expect(['比和', '生我', '我生', '克我', '我克']).toContain(r.elementRelation);
expect(['吉', '凶', '平']).toContain(r.level);
expect(r.score).toBeGreaterThanOrEqual(-10);
expect(r.score).toBeLessThanOrEqual(10);
}
});
});
@@ -0,0 +1,47 @@
import { describe, it, expect } from 'vitest';
import fs from 'node:fs';
import { fileURLToPath } from 'node:url';
import { calculatePlumBlossom } from '../index';
describe('plum blossom', () => {
it('produces expected hexagram for known input (2026-08-02 12:00)', () => {
const r = calculatePlumBlossom(2026, 8, 2, 12);
expect(r.upperTrigram.name).toBe('巽');
expect(r.lowerTrigram.name).toBe('兑');
expect(r.originalHexagram.name).toBe('风泽中孚');
expect(r.originalHexagram.changingLine).toBe(4);
expect(r.relationship).toBeTruthy();
});
it('is deterministic', () => {
const a = calculatePlumBlossom(2024, 3, 5, 8);
const b = calculatePlumBlossom(2024, 3, 5, 8);
expect(a).toEqual(b);
});
it('dataset covers all 64 hexagram combinations', () => {
const srcFile = fileURLToPath(new URL('../calculators/plumBlossom.ts', import.meta.url));
const src = fs.readFileSync(srcFile, 'utf8');
const keys = [...src.matchAll(/'(\d,\d)':/g)].map(m => m[1]);
expect(keys).toHaveLength(64);
expect(new Set(keys).size).toBe(64);
for (let upper = 1; upper <= 8; upper++) {
for (let lower = 1; lower <= 8; lower++) {
expect(keys).toContain(`${upper},${lower}`);
}
}
});
it('never falls back to a placeholder across a broad date grid', () => {
for (let y = 2000; y <= 2030; y++) {
for (let m = 1; m <= 12; m++) {
for (let d = 1; d <= 28; d += 7) {
const r = calculatePlumBlossom(y, m, d, 12);
// Real hexagram names are 3+ chars; fallback names are 2-char concatenations
expect(r.originalHexagram.name.length).toBeGreaterThan(2);
}
}
}
});
});
@@ -0,0 +1,45 @@
import { describe, it, expect } from 'vitest';
import {
getBranchRelationship,
getTenStarRelationship,
checkStemCombine,
checkStemOpposite,
} from '../index';
describe('relationship calculator', () => {
it('detects 六合 (子丑合)', () => {
expect(getBranchRelationship('子', '丑').combine).toBe(true);
expect(getBranchRelationship('子', '午').combine).toBe(false);
});
it('detects 六冲 (子午冲)', () => {
expect(getBranchRelationship('子', '午').opposite).toBe(true);
});
it('detects 三合 (申子辰水局)', () => {
const rel = getBranchRelationship('申', '辰');
expect(rel.threeCombine).toBe(true);
expect(rel.formation).toBe('水局');
});
it('detects 六害 (子未害)', () => {
expect(getBranchRelationship('子', '未').harm).toBe(true);
});
it('detects 相刑 (寅巳无恩之刑)', () => {
expect(getBranchRelationship('寅', '巳').punish).toBe(true);
});
it('detects 天干合 (甲己合)', () => {
expect(checkStemCombine('甲', '己')).toBe(true);
expect(checkStemCombine('甲', '乙')).toBe(false);
});
it('detects 天干冲 (甲庚冲)', () => {
expect(checkStemOpposite('甲', '庚')).toBe(true);
});
it('computes ten star for a day master (甲日主见戊土 → 偏财)', () => {
expect(getTenStarRelationship('甲', '戊')).toContain('财');
});
});
@@ -0,0 +1,55 @@
import { describe, it, expect } from 'vitest';
import { analyzeShensha, birthInfoToBazi } from '../index';
describe('shensha', () => {
// 2026-08-03 12:00: 年柱丙午、月柱乙未、日柱己酉、时柱庚午
const bazi = birthInfoToBazi({ year: 2026, month: 8, day: 3, hour: 12, minute: 0, gender: 'male' });
it('finds 天乙贵人 by year stem 丙 → 亥酉, hit 日支酉', () => {
const s = analyzeShensha(bazi.eightChar);
const t = s.find(x => x.name === '天乙贵人');
expect(t).toBeDefined();
expect(t!.foundIn).toContain('日支');
expect(t!.anchors.some(a => a.includes('年干'))).toBe(true);
});
it('finds 文昌贵人 by day stem 己 → 酉, hit 日支酉', () => {
const s = analyzeShensha(bazi.eightChar);
const t = s.find(x => x.name === '文昌贵人');
expect(t).toBeDefined();
expect(t!.anchors.some(a => a.includes('日干'))).toBe(true);
});
it('finds 禄神 and 羊刃 by day/year stem 己/丙 → 午', () => {
const s = analyzeShensha(bazi.eightChar);
const lu = s.find(x => x.name === '禄神');
const yang = s.find(x => x.name === '羊刃');
expect(lu).toBeDefined();
expect(lu!.foundIn).toEqual(expect.arrayContaining(['年支', '时支']));
expect(yang).toBeDefined();
});
it('finds 桃花 by day branch 酉 (巳酉丑) → 午', () => {
const s = analyzeShensha(bazi.eightChar);
const t = s.find(x => x.name.startsWith('桃花'));
expect(t).toBeDefined();
expect(t!.foundIn).toContain('年支');
});
it('finds 将星 and 红鸾', () => {
const s = analyzeShensha(bazi.eightChar);
expect(s.find(x => x.name === '将星')).toBeDefined(); // 年支午(寅午戌) → 午
expect(s.find(x => x.name === '红鸾')).toBeDefined(); // 年支午 → 酉,日支酉
});
it('does not flag 魁罡/阴差阳错 for day 己酉', () => {
const s = analyzeShensha(bazi.eightChar);
expect(s.find(x => x.name === '魁罡')).toBeUndefined();
expect(s.find(x => x.name === '阴差阳错')).toBeUndefined();
});
it('all stem rules cover all 10 stems and branch rules cover 12 branches', () => {
const s = analyzeShensha(bazi.eightChar);
expect(s.length).toBeGreaterThan(0);
});
});
@@ -0,0 +1,26 @@
import { describe, it, expect } from 'vitest';
import { getYearMonths } from '../index';
describe('getYearMonths (流月)', () => {
it('returns 12 months starting from 立春', () => {
const months = getYearMonths(2026);
expect(months).toHaveLength(12);
expect(months[0].name).toBe('正月');
expect(months[0].startDate).toBe('2026-02-04'); // 立春 2026
expect(months[0].ganzhi).toBe('庚寅'); // 丙午年 五虎遁 → 寅月庚寅
expect(months[11].name).toBe('腊月');
expect(months[11].startDate).toMatch(/^2027-01-/); // 小寒 2027
});
it('endDate is the day before the next 节', () => {
const months = getYearMonths(2026);
expect(months[0].endDate).toBe('2026-03-04'); // 惊蛰 2026-03-05 前一天
expect(months[1].ganzhi).toBe('辛卯'); // 惊蛰起卯月
});
it('all month ganzhi match the 五虎遁 sequence', () => {
const months = getYearMonths(2026);
const seq = ['庚寅', '辛卯', '壬辰', '癸巳', '甲午', '乙未', '丙申', '丁酉', '戊戌', '己亥', '庚子', '辛丑'];
expect(months.map(m => m.ganzhi)).toEqual(seq);
});
});
@@ -0,0 +1,161 @@
/**
* 袁天罡称骨算命法 (Bone Weight Fortune Telling)
* Based on year/month/day/hour pillars to calculate total "bone weight"
* Each unit = 钱 (1两 = 10钱)
*/
export interface BoneWeightResult {
yearWeight: number; // in 钱
monthWeight: number;
dayWeight: number;
hourWeight: number;
totalWeight: number; // in 钱
totalLiang: number; // 两
totalQian: number; // 钱
interpretation: string; // The fate poem/interpretation
fortune: '上上' | '上' | '中上' | '中' | '中下' | '下';
}
// Year bone weight table (by 干支 year stem-branch)
// 主流称骨年表(百度百科/网易/算准网等通行版本):year % 60 → weight in 钱
const YEAR_WEIGHTS: Record<number, number> = {
0: 12, 1: 9, 2: 6, 3: 7, 4: 12, 5: 5, 6: 9, 7: 8, 8: 7, 9: 8,
10: 15, 11: 9, 12: 16, 13: 8, 14: 8, 15: 19, 16: 12, 17: 6, 18: 8, 19: 7,
20: 5, 21: 15, 22: 6, 23: 16, 24: 15, 25: 7, 26: 9, 27: 12, 28: 10, 29: 7,
30: 15, 31: 6, 32: 5, 33: 14, 34: 14, 35: 9, 36: 7, 37: 7, 38: 9, 39: 12,
40: 8, 41: 7, 42: 13, 43: 5, 44: 14, 45: 5, 46: 9, 47: 17, 48: 5, 49: 7,
50: 12, 51: 8, 52: 8, 53: 6, 54: 19, 55: 6, 56: 8, 57: 16, 58: 10, 59: 6,
};
// Month bone weight (lunar month 1-12)
const MONTH_WEIGHTS: Record<number, number> = {
1: 6, 2: 7, 3: 18, 4: 9, 5: 5, 6: 16,
7: 9, 8: 15, 9: 18, 10: 8, 11: 9, 12: 5,
};
// Day bone weight (lunar day 1-30)
const DAY_WEIGHTS: Record<number, number> = {
1: 5, 2: 10, 3: 8, 4: 15, 5: 16, 6: 15, 7: 8, 8: 16, 9: 8, 10: 16,
11: 9, 12: 17, 13: 8, 14: 17, 15: 10, 16: 8, 17: 9, 18: 18, 19: 5, 20: 15,
21: 10, 22: 9, 23: 8, 24: 9, 25: 15, 26: 18, 27: 7, 28: 8, 29: 16, 30: 6,
};
// Hour bone weight (时辰, 地支 index 0-11)
const HOUR_WEIGHTS: Record<number, number> = {
0: 16, // 子时 23-01
1: 6, // 丑时 01-03
2: 7, // 寅时 03-05
3: 10, // 卯时 05-07
4: 9, // 辰时 07-09
5: 16, // 巳时 09-11
6: 10, // 午时 11-13
7: 8, // 未时 13-15
8: 8, // 申时 15-17
9: 9, // 酉时 17-19
10: 6, // 戌时 19-21
11: 6, // 亥时 21-23
};
// Interpretation for each total weight (in 钱)
const INTERPRETATIONS: Record<number, { text: string; fortune: string }> = {
21: { text: '短命非业谓大凶,平生灾难事重重,凶祸频临陷逆境,终世困苦事不成。', fortune: '下' },
22: { text: '身寒骨冷苦伶仃,此命推来行乞人,劳劳碌碌无度日,终年打拱过平生。', fortune: '下' },
23: { text: '此命推来骨格轻,求谋作事事难成,妻儿兄弟应难许,别处他乡作散人。', fortune: '下' },
24: { text: '此命推来福禄无,门庭困苦总难荣,六亲骨肉皆无靠,流浪他乡作老翁。', fortune: '下' },
25: { text: '此命推来祖业微,门庭营度似稀奇,六亲骨肉如冰炭,一世勤劳自把持。', fortune: '中下' },
26: { text: '平生衣禄苦中求,独自营谋事不休,离祖出门宜早计,晚来衣禄自无休。', fortune: '中下' },
27: { text: '一生作事少商量,难靠祖宗怎主张,独马单枪空做去,早年晚岁总无长。', fortune: '中下' },
28: { text: '一生行事似飘蓬,祖宗产业在梦中,若不过房改名姓,也当移徒二三通。', fortune: '中下' },
29: { text: '初年运限未曾亨,纵有功名在后成,须过四旬才可立,移居改姓始为良。', fortune: '中' },
30: { text: '劳劳碌碌苦中求,东奔西走何日休,若使终身勤与俭,老来稍可免忧愁。', fortune: '中' },
31: { text: '忙忙碌碌苦中求,何日云开见日头,难得祖基家可立,中年衣食渐无忧。', fortune: '中' },
32: { text: '初年运蹇事难谋,渐有财源如水流,到得中年衣食旺,那时名利一齐收。', fortune: '中上' },
33: { text: '早年做事事难成,百年勤劳枉费心,半世自如流水去,后来运到始得金。', fortune: '中上' },
34: { text: '此命福气果如何,僧道门中衣禄多,离祖出家方为妙,朝晚拜佛念弥陀。', fortune: '中上' },
35: { text: '生平福量不周全,祖业根基觉少传,营事生涯宜守旧,时来衣食胜从前。', fortune: '中' },
36: { text: '不须劳碌过平生,独自成家福不轻,早有福星常照命,任君行去百般成。', fortune: '上' },
37: { text: '此命般般事不成,弟兄少力自孤行,虽然祖业须微有,来得明时去不明。', fortune: '中下' },
38: { text: '一身骨肉最清高,早入簧门姓氏标,待到年将三十六,蓝衫脱去换红袍。', fortune: '上' },
39: { text: '此命终身运不通,劳劳作事尽皆空,苦心竭力成家计,到得那时在梦中。', fortune: '中下' },
40: { text: '平生衣禄是绵长,件件心中自主张,前面风霜多受过,后来必定享安康。', fortune: '上' },
41: { text: '此命推来自不同,为人能干异凡庸,中年还有逍遥福,不比前时运未通。', fortune: '上' },
42: { text: '得宽怀处且宽怀,何用双眉皱不开,若使中年命运济,那时名利一齐来。', fortune: '上' },
43: { text: '为人心性最聪明,作事轩昂近贵人,衣禄一生天注定,不须劳碌是丰亨。', fortune: '上上' },
44: { text: '万事由天莫苦求,须知福碌赖人修,当年财帛难如意,晚景欣然便不忧。', fortune: '中上' },
45: { text: '名利推求竟若何,前番辛苦后奔波,命中难养男和女,骨肉扶持也不多。', fortune: '中' },
46: { text: '东西南北尽皆通,出姓移居更觉隆,衣禄无穷无数定,中年晚景一般同。', fortune: '上' },
47: { text: '此命推求旺末年,妻荣子贵自怡然,平生原有滔滔福,可卜财源若水泉。', fortune: '上' },
48: { text: '初年运道未曾通,几许蹉跎命亦穷,兄弟六亲无依靠,一生事业晚来整。', fortune: '中' },
49: { text: '此命推来福不轻,自成自立显门庭,从来富贵人钦敬,使婢差奴过一生。', fortune: '上' },
50: { text: '为利为名终日劳,中年福禄也多遭,老来自有财星照,不比前番目下高。', fortune: '上' },
51: { text: '一世荣华事事通,不须劳碌自亨通,兄弟叔侄皆如意,家业成时福禄宏。', fortune: '上' },
52: { text: '一世亨通事事能,不须劳苦自然宁,宗族有光欣喜甚,家产丰盈自称心。', fortune: '上' },
53: { text: '此格推来福泽宏,兴家立业在其中,一生衣食安排定,却是人间一福翁。', fortune: '上' },
54: { text: '此命推来厚且清,诗书满腹看功成,丰衣足食自然稳,正是人间有福人。', fortune: '上上' },
55: { text: '走马扬鞭争利名,少年作事费筹论,一朝福禄源源至,富贵荣华显六亲。', fortune: '上' },
56: { text: '此格推来礼义通,一身福禄用无穷,甜酸苦辣皆尝过,滚滚财源稳且丰。', fortune: '上' },
57: { text: '福禄丰盈万事全,一身荣耀乐天年,名扬威震人争羡,此世逍遥宛似仙。', fortune: '上上' },
58: { text: '平生福禄自然来,名利双全福禄偕,雁塔题名为贵客,紫袍玉带走金阶。', fortune: '上' },
59: { text: '细推此格妙且清,必定财高礼义通,甲第之中应有分,扬鞭走马显威荣。', fortune: '上' },
60: { text: '一朝金榜快题名,显祖荣宗大器成,衣禄定然无欠缺,田园财帛更丰盈。', fortune: '上上' },
61: { text: '不作朝中金榜客,定为世上一财翁,聪明天赋经书熟,名显高科自是荣。', fortune: '上上' },
62: { text: '此命生来福不穷,读书必定显亲宗,紫衣玉带为卿相,富贵荣华孰与同。', fortune: '上上' },
63: { text: '命主为官福禄长,得来富贵定非常,名题雁塔传金榜,定中高科天下扬。', fortune: '上上' },
64: { text: '此命生成福不轻,读书必定有功名,果然富贵前生定,一世荣华事事成。', fortune: '上上' },
65: { text: '细推此命福不轻,安国安邦极品人,文纷雕梁徽富贵,威声照耀四方闻。', fortune: '上上' },
66: { text: '此命推来福且宏,荣华富贵自然通,命中注定衣禄足,一世亨通稳且丰。', fortune: '上上' },
67: { text: '此命生来福自宏,田园家业最高隆,平生衣禄丰盈足,一世荣华万事通。', fortune: '上上' },
68: { text: '富贵荣华莫强求,强求不出反成羞,有福之人还自至,无福之人反成忧。', fortune: '中' },
69: { text: '君是人间前禄星,一生富贵众人钦,纵然福禄由天定,安享荣华过一生。', fortune: '上上' },
70: { text: '此命推来福禄宏,不须劳碌过平生,妻儿和顺皆如意,家道兴隆福自成。', fortune: '上' },
71: { text: '此命生来大不同,公侯卿相在其中,一生自有逍遥福,富贵荣华极品隆。', fortune: '上上' },
72: { text: '此命生来福泽长,兴家立业有祯祥,一生自有逍遥福,富贵荣华极品良。', fortune: '上上' },
};
/** Calculate Bone Weight Fortune from lunar calendar data */
export function calculateBoneWeight(
lunarYearGanzhiIndex: number, // 0-59 六十甲子 index
lunarMonth: number, // 1-12
lunarDay: number, // 1-30
earthBranchHourIndex: number, // 0-11 地支时辰 index
): BoneWeightResult {
const yearWt = YEAR_WEIGHTS[lunarYearGanzhiIndex] || 9;
const monthWt = MONTH_WEIGHTS[lunarMonth] || 9;
const dayWt = DAY_WEIGHTS[lunarDay] || 9;
const hourWt = HOUR_WEIGHTS[earthBranchHourIndex] || 9;
const totalWeight = yearWt + monthWt + dayWt + hourWt;
const totalLiang = Math.floor(totalWeight / 10);
const totalQian = totalWeight % 10;
// Find closest interpretation
const interpretation = findInterpretation(totalWeight);
return {
yearWeight: yearWt,
monthWeight: monthWt,
dayWeight: dayWt,
hourWeight: hourWt,
totalWeight,
totalLiang,
totalQian,
interpretation: interpretation.text,
fortune: interpretation.fortune as BoneWeightResult['fortune'],
};
}
function findInterpretation(totalQian: number): { text: string; fortune: string } {
// Direct match
if (INTERPRETATIONS[totalQian]) return INTERPRETATIONS[totalQian];
// Find closest
const keys = Object.keys(INTERPRETATIONS).map(Number).sort((a, b) => a - b);
let closest = keys[0];
let minDiff = Math.abs(totalQian - closest);
for (const k of keys) {
const diff = Math.abs(totalQian - k);
if (diff < minDiff) { minDiff = diff; closest = k; }
}
return INTERPRETATIONS[closest] || { text: '命格推来,自有天定', fortune: '中' };
}
@@ -0,0 +1,32 @@
/**
* 佛教节日(农历)— tyme4ts 不提供,这里维护常用重大佛教日期表
*/
const BUDDHIST_FESTIVALS: Record<string, string> = {
'1-1': '弥勒菩萨圣诞',
'2-8': '释迦牟尼佛出家日',
'2-15': '释迦牟尼佛涅槃日',
'2-19': '观音菩萨圣诞',
'2-21': '普贤菩萨圣诞',
'3-16': '准提菩萨圣诞',
'4-4': '文殊菩萨圣诞',
'4-8': '释迦牟尼佛圣诞(浴佛节)',
'5-3': '伽蓝菩萨圣诞',
'6-3': '韦驮菩萨圣诞',
'6-19': '观音菩萨成道日',
'7-13': '大势至菩萨圣诞',
'7-15': '盂兰盆节(佛欢喜日)',
'7-30': '地藏菩萨圣诞',
'8-22': '燃灯佛圣诞',
'9-19': '观音菩萨出家日',
'9-30': '药师佛圣诞',
'10-5': '达摩祖师诞辰',
'11-17': '阿弥陀佛圣诞',
'12-8': '释迦牟尼佛成道日(腊八)',
'12-29': '华严菩萨圣诞',
};
/** Get Buddhist festival name by lunar month/day (闰月不重复过节) */
export function getBuddhistFestival(lunarMonth: number, lunarDay: number): string | null {
return BUDDHIST_FESTIVALS[`${lunarMonth}-${lunarDay}`] || null;
}
@@ -0,0 +1,275 @@
import { SolarDay } from 'tyme4ts';
import type { EightCharInfo, PillarInfo } from '../types/bazi';
import type { DailyFortuneResult, PillarRelationship } from '../types/fortune';
import {
getBranchRelationship,
getTenStarRelationship,
checkStemCombine,
checkStemOpposite,
} from './relationship';
const PILLAR_LABELS: Record<string, string> = {
year: '年柱', month: '月柱', day: '日柱', hour: '时柱',
};
const PILLAR_KEYS = ['year', 'month', 'day', 'hour'] as const;
export function calculateDailyFortune(userBazi: EightCharInfo, date: Date): DailyFortuneResult {
const solarDay = SolarDay.fromYmd(date.getFullYear(), date.getMonth() + 1, date.getDate());
const lunarDay = solarDay.getLunarDay();
const lunarHour = lunarDay.getHours()[6];
const dayEightChar = lunarHour.getEightChar();
const dayPillars: Record<string, { ganzhi: string; stem: string; branch: string }> = {
year: { ganzhi: dayEightChar.getYear().getName(), stem: dayEightChar.getYear().getHeavenStem().getName(), branch: dayEightChar.getYear().getEarthBranch().getName() },
month: { ganzhi: dayEightChar.getMonth().getName(), stem: dayEightChar.getMonth().getHeavenStem().getName(), branch: dayEightChar.getMonth().getEarthBranch().getName() },
day: { ganzhi: dayEightChar.getDay().getName(), stem: dayEightChar.getDay().getHeavenStem().getName(), branch: dayEightChar.getDay().getEarthBranch().getName() },
hour: { ganzhi: dayEightChar.getHour().getName(), stem: dayEightChar.getHour().getHeavenStem().getName(), branch: dayEightChar.getHour().getEarthBranch().getName() },
};
const userPillars: Record<string, PillarInfo> = {
year: userBazi.yearPillar, month: userBazi.monthPillar, day: userBazi.dayPillar, hour: userBazi.hourPillar,
};
const relationships: PillarRelationship[] = [];
for (const key of PILLAR_KEYS) {
const userPillar = userPillars[key]; const dayP = dayPillars[key];
const stemTenStar = getTenStarRelationship(userPillar.heavenStem, dayP.stem);
const stemCombine = checkStemCombine(userPillar.heavenStem, dayP.stem);
const stemOpposite = checkStemOpposite(userPillar.heavenStem, dayP.stem);
const branchRel = getBranchRelationship(userPillar.earthBranch, dayP.branch);
let score = 0;
if (stemTenStar) {
const good = ['正印','偏印','食神','正财','偏财','正官'];
const bad = ['七杀','劫财','伤官'];
if (good.includes(stemTenStar)) score += 4;
else if (bad.includes(stemTenStar)) score -= 3;
else score += 1;
}
if (stemCombine) score += 5;
if (stemOpposite) score -= 6;
if (branchRel.combine) score += 4;
if (branchRel.threeCombine) score += 3;
if (branchRel.opposite) score -= 5;
if (branchRel.harm) score -= 4;
if (branchRel.punish) score -= 3;
score = Math.max(-10, Math.min(10, score));
relationships.push({
pillar: key, pillarLabel: PILLAR_LABELS[key],
userGanzhi: userPillar.ganzhi, dayGanzhi: dayP.ganzhi,
stemTenStar, stemCombine, stemOpposite,
branchCombine: branchRel.combine, branchThreeCombine: branchRel.threeCombine,
branchOpposite: branchRel.opposite, branchHarm: branchRel.harm,
branchPunish: branchRel.punish, branchFormation: branchRel.formation,
score,
});
}
const weightedScore =
relationships[0].score * 0.20 + relationships[1].score * 0.25 +
relationships[2].score * 0.40 + relationships[3].score * 0.15;
const overallScore = Math.max(-100, Math.min(100, Math.round(weightedScore * 10)));
const scoreLevel: DailyFortuneResult['scoreLevel'] =
overallScore >= 50 ? 'great' : overallScore >= 20 ? 'good' :
overallScore >= -20 ? 'fair' : overallScore >= -50 ? 'poor' : 'bad';
const luckyAspects = generateLuckyAspects(relationships);
const unluckyAspects = generateUnluckyAspects(relationships);
const suggestions = generateSuggestions(relationships, userBazi, overallScore);
const affectedAreas = determineAffectedAreas(relationships);
const luckyMeta = computeLuckyMeta(userBazi, dayPillars.day.ganzhi, overallScore);
const categoryScores = computeCategoryScores(relationships);
const lunarDateStr = `${lunarDay.getLunarMonth().getLunarYear().getYear()}${lunarDay.getLunarMonth().getName()}${lunarDay.getName()}`;
return {
date: `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`,
lunarDate: lunarDateStr, dayGanzhi: dayPillars.day.ganzhi,
overallScore, scoreLevel, pillarRelationships: relationships,
luckyAspects, unluckyAspects, suggestions, affectedAreas, luckyMeta, categoryScores,
};
}
function generateLuckyAspects(relationships: PillarRelationship[]): string[] {
const seen = new Set<string>(); const aspects: string[] = [];
const tenStarPlain: Record<string, string> = {
'正官':'事业运不错,工作上容易得到认可和赏识',
'七杀':'进取心强,适合挑战难题和竞争性事务',
'正印':'学习运佳,适合读书充电或向长辈请教',
'偏印':'灵感丰富,适合研究和创意工作',
'食神':'心情舒畅,适合休闲放松和享受生活',
'伤官':'表达欲强,适合沟通交流和展示自我',
'正财':'财运平稳,适合处理日常财务和长期投资',
'偏财':'偏财运佳,可能有意外之财或投资收益',
'比肩':'人际关系和睦,容易得到朋友和同事的帮助',
'劫财':'社交活跃,适合参加聚会和团队活动',
};
const hasStemCombine = relationships.some(r => r.stemCombine);
const hasBranchCombine = relationships.some(r => r.branchCombine || r.branchThreeCombine);
for (const rel of relationships) {
if (rel.stemTenStar && tenStarPlain[rel.stemTenStar] && !seen.has(rel.stemTenStar)) {
aspects.push(tenStarPlain[rel.stemTenStar]); seen.add(rel.stemTenStar);
}
}
if (hasStemCombine && !seen.has('combine')) { aspects.push('天时地利人和,容易遇到贵人相助和好机会'); seen.add('combine'); }
if (hasBranchCombine && !seen.has('branch')) { aspects.push('身边关系和谐,适合与人合作或开展团队项目'); seen.add('branch'); }
if (aspects.length === 0) aspects.push('今天整体运势平稳,适合按部就班处理日常事务');
return aspects.slice(0, 4);
}
function generateUnluckyAspects(relationships: PillarRelationship[]): string[] {
const aspects: string[] = [];
const hasOpposite = relationships.some(r => r.branchOpposite);
const hasHarm = relationships.some(r => r.branchHarm);
const hasPunish = relationships.some(r => r.branchPunish);
const hasStemOpposite = relationships.some(r => r.stemOpposite);
const hasQiSha = relationships.some(r => r.stemTenStar === '七杀');
const hasJieCai = relationships.some(r => r.stemTenStar === '劫财');
const hasShangGuan = relationships.some(r => r.stemTenStar === '伤官');
if (hasStemOpposite) aspects.push('今天可能遇到突发变化或计划被打乱,保持冷静和弹性很重要');
if (hasOpposite) aspects.push('与他人意见不合的可能性较高,尽量避免争执和正面冲突');
if (hasHarm) aspects.push('留意身边的小人和是非,不要轻易透露自己的计划和秘密');
if (hasPunish) aspects.push('容易说错话或做出不恰当的举动,多听少说更安全');
if (hasQiSha) aspects.push('压力较大的一天,注意调节情绪,重要决策可暂缓');
if (hasJieCai) aspects.push('财务方面要谨慎,不宜大额消费或借钱给别人');
if (hasShangGuan) aspects.push('表达时注意方式方法,容易词不达意引起误会');
if (aspects.length === 0) aspects.push('今天没有什么特别需要注意的,保持平常心即可');
return aspects.slice(0, 3);
}
function generateSuggestions(
relationships: PillarRelationship[],
userBazi: EightCharInfo,
overallScore: number,
): string[] {
const s: string[] = [];
const de = userBazi.dayMasterElement;
const colors: Record<string,string> = { '木':'绿色/青色','火':'红色/紫色','土':'黄色/棕色','金':'白色/浅色','水':'蓝色/黑色' };
const dirs: Record<string,string> = { '木':'东方','火':'南方','土':'中央','金':'西方','水':'北方' };
const hasConflict = relationships.some(r => r.branchOpposite || r.stemOpposite);
const hasCombine = relationships.some(r => r.branchCombine || r.stemCombine);
const hasHarm = relationships.some(r => r.branchHarm);
const hasPunish = relationships.some(r => r.branchPunish);
const goodPool = [
de ? `👔 幸运色${colors[de]||'浅色'},穿对颜色运气更好` : `👔 今天穿浅色衣服心情更好`,
`✅ 适合推进重要事项,果断决策会有好结果`,
`📝 把计划写下来按部就班执行,效率翻倍`,
`🎯 专注一件事比多线作战更有效果`,
`🤝 适合约重要的人见面,贵人运在线`,
`🌅 早起一点早晨的效率最高`,
`📊 适合总结和复盘能发现新机会`,
`🏃 运动或户外活动对运势有加成`,
];
const fairPool = [
`💡 运势平稳该做什么就做什么`,
`☕ 适合处理日常事务不宜做重大改变`,
`📖 适合学习充电多读多听少说`,
`🧹 整理收纳会让心情变好`,
`🍵 节奏放慢一点稳扎稳打`,
`📞 适合和老朋友聊聊天`,
];
const poorPool = [
`🧘 适合保守行事多观察多思考`,
`☕ 适合独处和复盘少社交`,
`🛡️ 以守成为主避免冲动`,
`📿 冥想或听音乐能化解负面情绪`,
`🙏 遇到不顺心的事深呼吸三秒`,
`📝 把想法写下来明天再行动`,
];
const badPool = [
`🛡️ 宜静不宜动重要决策能缓则缓`,
`🚫 避免签合同或做出重大承诺`,
`🧘 适合冥想休息养精蓄锐`,
`📿 去清净处走走转换气场`,
`💤 早点休息好睡眠是最好的转运`,
`🙏 好事多磨心态会好很多`,
];
let pool = fairPool;
if (overallScore >= 30) pool = goodPool;
else if (overallScore >= 0) pool = fairPool;
else if (overallScore >= -30) pool = poorPool;
else pool = badPool;
const seed = new Date().getDate() + new Date().getMonth() * 31;
s.push(pool[seed % pool.length]);
s.push(pool[(seed + 5) % pool.length]);
if (overallScore >= 10 && de && dirs[de]) s.push(`🧭 向${dirs[de]}行事有意外好运`);
if (hasCombine && !hasConflict) s.push('🤝 人际关系活跃适合合作洽谈');
else if (hasConflict && hasHarm) s.push('⚠️ 人际关系有暗礁保持微笑少说话');
else if (hasConflict) s.push('🙏 意见不合时先认同再引导');
else if (hasPunish) s.push('💬 说话前过遍脑子今天容易说错话');
const seen = new Set<string>();
const unique = s.filter(t => { if (seen.has(t)) return false; seen.add(t); return true; });
return unique.slice(0, 4);
}
function computeLuckyMeta(userBazi: EightCharInfo, dayGanzhi: string, score: number) {
const de = userBazi.dayMasterElement;
const colorMap: Record<string, string[]> = { '木': ['绿色','青色','翠绿'], '火': ['红色','紫色','粉色'], '土': ['黄色','棕色','米色'], '金': ['白色','银色','浅灰'], '水': ['蓝色','黑色','深灰'] };
const dirMap: Record<string, string> = { '木': '东方', '火': '南方', '土': '中央', '金': '西方', '水': '北方' };
const actMap: Record<string, string[]> = {
'木': ['户外散步','园艺','阅读','写作'], '火': ['社交','演讲','创意','运动'], '土': ['整理收纳','理财','烹饪','冥想'],
'金': ['商务洽谈','签约','购物','美容'], '水': ['学习进修','旅行','游泳','听音乐'],
};
const colors = colorMap[de] || ['红色','白色'];
const direction = dirMap[de] || '东方';
const seed = parseInt(dayGanzhi.charCodeAt(0).toString()) + new Date().getDate();
const n1 = (seed % 9) + 1;
const n2 = ((seed * 3 + 7) % 9) + 1;
const activities = actMap[de] || ['运动','阅读'];
const activity = activities[seed % activities.length];
return {
colors: [colors[0], colors[1]],
numbers: [n1, n2],
direction,
element: de,
activity: score >= 0 ? activity : activities[(seed + 3) % activities.length],
};
}
function computeCategoryScores(relationships: PillarRelationship[]) {
const tenStarLove: Record<string,number> = { '正官':8,'七杀':-2,'正印':3,'偏印':2,'食神':6,'伤官':5,'正财':4,'偏财':3 };
const tenStarCareer: Record<string,number> = { '正官':10,'七杀':8,'正印':5,'偏印':4,'食神':3,'伤官':2,'正财':2 };
const tenStarWealth: Record<string,number> = { '正财':10,'偏财':8,'食神':6,'伤官':4,'正官':2,'七杀':-2 };
const tenStarHealth: Record<string,number> = { '正印':8,'偏印':6,'比肩':5,'食神':4,'七杀':-4,'伤官':-2 };
let love=0, career=0, wealth=0, health=0;
for (const rel of relationships) {
const ts = rel.stemTenStar;
if (!ts) continue;
const w = rel.pillar === 'day' ? 3 : rel.pillar === 'month' ? 2 : 1;
love += (tenStarLove[ts]||0) * w + (rel.branchHarm?-5:0) * w + (rel.branchCombine?4:0) * w;
career += (tenStarCareer[ts]||0) * w + (rel.branchOpposite?-4:0) * w + (rel.branchThreeCombine?5:0) * w;
wealth += (tenStarWealth[ts]||0) * w + (rel.stemCombine?5:0) * w;
health += (tenStarHealth[ts]||0) * w + (rel.branchOpposite?-3:0) * w;
}
// Normalize to -100..100
const clamp = (v:number) => Math.max(-100, Math.min(100, Math.round(v * 2.5)));
return { love: clamp(love), career: clamp(career), wealth: clamp(wealth), health: clamp(health) };
}
function determineAffectedAreas(relationships: PillarRelationship[]): string[] {
const areas = new Set<string>();
const areaMapping: Record<string, string> = {
'正财':'财运','偏财':'偏财/投资','正官':'事业','七杀':'事业/压力',
'正印':'学业','偏印':'学业/智慧','食神':'创作/享乐','伤官':'口才/表达',
'比肩':'人际关系','劫财':'竞争/人际',
};
for (const rel of relationships) {
if (rel.stemTenStar && areaMapping[rel.stemTenStar]) areas.add(areaMapping[rel.stemTenStar]);
if (rel.branchOpposite && rel.pillar === 'year') areas.add('长辈/根基');
if (rel.branchOpposite && rel.pillar === 'month') areas.add('事业/家庭');
if (rel.branchOpposite && rel.pillar === 'day') areas.add('婚姻/健康');
if (rel.branchOpposite && rel.pillar === 'hour') areas.add('子女/晚年');
}
return areas.size > 0 ? [...areas] : ['综合运势'];
}
@@ -0,0 +1,99 @@
import type { EightCharInfo } from '../types/bazi';
/** Five Element profile */
export interface ElementProfile {
wood: number;
fire: number;
earth: number;
metal: number;
water: number;
total: number;
dominant: string;
weakest: string;
isBalanced: boolean;
}
/** Analyze the five element balance in a Bazi chart */
export function analyzeElementBalance(bazi: EightCharInfo): ElementProfile {
const pillars = [bazi.yearPillar, bazi.monthPillar, bazi.dayPillar, bazi.hourPillar];
const counts: Record<string, number> = {
'木': 0,
'火': 0,
'土': 0,
'金': 0,
'水': 0,
};
for (const pillar of pillars) {
// Count stem element
if (pillar.elementStem && counts[pillar.elementStem] !== undefined) {
counts[pillar.elementStem] += 1;
}
// Count branch element
if (pillar.elementBranch && counts[pillar.elementBranch] !== undefined) {
counts[pillar.elementBranch] += 1;
}
// Count hidden stem elements (half weight)
for (const hs of pillar.hideStems) {
// Infer element from stem name
const element = inferElementFromStem(hs.stem);
if (element && counts[element] !== undefined) {
counts[element] += 0.5;
}
}
}
const total = counts['木'] + counts['火'] + counts['土'] + counts['金'] + counts['水'];
// Find dominant and weakest
let dominant = '木';
let weakest = '木';
let maxCount = 0;
let minCount = Infinity;
for (const [element, count] of Object.entries(counts)) {
if (count > maxCount) {
maxCount = count;
dominant = element;
}
if (count < minCount) {
minCount = count;
weakest = element;
}
}
// Balance check: each element should be within 30% of ideal (20% each)
const ideal = total / 5;
let isBalanced = true;
for (const count of Object.values(counts)) {
if (Math.abs(count - ideal) > ideal * 0.4) {
isBalanced = false;
break;
}
}
return {
wood: counts['木'] || 0,
fire: counts['火'] || 0,
earth: counts['土'] || 0,
metal: counts['金'] || 0,
water: counts['水'] || 0,
total,
dominant,
weakest,
isBalanced,
};
}
/** Infer five element from heavenly stem name */
function inferElementFromStem(stem: string): string | null {
const stemElements: Record<string, string> = {
'甲': '木', '乙': '木',
'丙': '火', '丁': '火',
'戊': '土', '己': '土',
'庚': '金', '辛': '金',
'壬': '水', '癸': '水',
};
return stemElements[stem] || null;
}
@@ -0,0 +1,88 @@
/**
* 大运/流年/流月/流日 与日主的生克冲合分析
* 将任意干支与日主(日干/日支)比较,输出十神、五行生克、天干合冲、地支关系与简化吉凶。
*/
import { getTenStarRelationship, checkStemCombine, checkStemOpposite, getBranchRelationship } from './relationship';
export interface FortuneLuck {
ganzhi: string;
tenStar: string | null;
/** 该柱五行 vs 日主五行:比和/生我/我生/克我/我克 */
elementRelation: string;
stemCombine: boolean;
stemOpposite: boolean;
branchCombine: boolean;
branchThreeCombine: boolean;
branchOpposite: boolean;
branchHarm: boolean;
branchPunish: boolean;
score: number;
level: '吉' | '凶' | '平';
}
const STEM_ELEMENT: Record<string, string> = {
'甲': '木', '乙': '木', '丙': '火', '丁': '火', '戊': '土',
'己': '土', '庚': '金', '辛': '金', '壬': '水', '癸': '水',
};
const GENERATES: Record<string, string> = { '木': '火', '火': '土', '土': '金', '金': '水', '水': '木' };
const KILLS: Record<string, string> = { '木': '土', '土': '水', '水': '火', '火': '金', '金': '木' };
const GOOD_TEN_STARS = ['正印', '偏印', '食神', '正财', '偏财', '正官'];
const BAD_TEN_STARS = ['七杀', '劫财', '伤官'];
/** Analyze an external ganzhi (大运/流年/流月/流日) against the day master pillar */
export function analyzeFortuneGanzhi(
ganzhi: string,
dayStem: string,
dayBranch: string,
): FortuneLuck {
const stem = ganzhi[0];
const branch = ganzhi[1];
const tenStar = getTenStarRelationship(dayStem, stem);
const el = STEM_ELEMENT[stem];
const de = STEM_ELEMENT[dayStem];
let elementRelation: string;
if (!el || !de || el === de) elementRelation = '比和';
else if (GENERATES[el] === de) elementRelation = '生我';
else if (GENERATES[de] === el) elementRelation = '我生';
else if (KILLS[el] === de) elementRelation = '克我';
else elementRelation = '我克';
const stemCombine = checkStemCombine(stem, dayStem);
const stemOpposite = checkStemOpposite(stem, dayStem);
const br = getBranchRelationship(branch, dayBranch);
let score = 0;
if (tenStar) {
if (GOOD_TEN_STARS.includes(tenStar)) score += 4;
else if (BAD_TEN_STARS.includes(tenStar)) score -= 3;
else score += 1;
}
if (stemCombine) score += 5;
if (stemOpposite) score -= 6;
if (br.combine) score += 4;
if (br.threeCombine) score += 3;
if (br.opposite) score -= 5;
if (br.harm) score -= 4;
if (br.punish) score -= 3;
score = Math.max(-10, Math.min(10, score));
const level: FortuneLuck['level'] = score >= 3 ? '吉' : score <= -3 ? '凶' : '平';
return {
ganzhi,
tenStar,
elementRelation,
stemCombine,
stemOpposite,
branchCombine: br.combine,
branchThreeCombine: br.threeCombine,
branchOpposite: br.opposite,
branchHarm: br.harm,
branchPunish: br.punish,
score,
level,
};
}
@@ -0,0 +1,258 @@
/**
* 梅花易数 (Plum Blossom I-Ching Divination)
* Based on time (year, month, day, hour) to derive hexagrams
*/
export interface TrigramInfo {
index: number; // 1-8 (乾兑离震巽坎艮坤)
name: string; // Chinese name
symbol: string; // Unicode trigram symbol
element: string; // Five element
direction: string; // Direction
nature: string; // Natural phenomenon
trait: string; // Personality trait
body: string; // Body part
}
export interface HexagramInfo {
number: number; // 1-64
name: string; // Chinese name e.g. "乾为天"
upperTrigram: TrigramInfo;
lowerTrigram: TrigramInfo;
changingLine: number; // 1-6, which line changes
interpretation: string; // Overall interpretation
judgment: string; // 彖辞
image: string; // 象辞
lines: string[]; // 6 line interpretations
}
export interface PlumBlossomResult {
originalHexagram: HexagramInfo;
transformedHexagram: HexagramInfo | null;
mutualHexagram: HexagramInfo | null;
upperTrigram: TrigramInfo;
lowerTrigram: TrigramInfo;
changingLine: number;
constitution: string; // 体卦
function: string; // 用卦
relationship: string; // 体用关系
}
// 8 Trigrams (八卦)
const TRIGRAMS: Record<number, TrigramInfo> = {
1: { index: 1, name: '乾', symbol: '☰', element: '金', direction: '西北', nature: '天', trait: '健', body: '首' },
2: { index: 2, name: '兑', symbol: '☱', element: '金', direction: '西', nature: '泽', trait: '悦', body: '口' },
3: { index: 3, name: '离', symbol: '☲', element: '火', direction: '南', nature: '火', trait: '丽', body: '目' },
4: { index: 4, name: '震', symbol: '☳', element: '木', direction: '东', nature: '雷', trait: '动', body: '足' },
5: { index: 5, name: '巽', symbol: '☴', element: '木', direction: '东南', nature: '风', trait: '入', body: '股' },
6: { index: 6, name: '坎', symbol: '☵', element: '水', direction: '北', nature: '水', trait: '陷', body: '耳' },
7: { index: 7, name: '艮', symbol: '☶', element: '土', direction: '东北', nature: '山', trait: '止', body: '手' },
8: { index: 8, name: '坤', symbol: '☷', element: '土', direction: '西南', nature: '地', trait: '顺', body: '腹' },
};
// All 64 Hexagrams (complete I-Ching)
const H: Record<string, { name: string; judgment: string; image: string; lines: string[] }> = {
// 乾宫八卦 (1-8)
'1,1':{name:'乾为天',judgment:'大哉乾元,万物资始,乃统天。云行雨施,品物流形',image:'天行健,君子以自强不息',lines:['潜龙勿用','见龙在田,利见大人','君子终日乾乾,夕惕若厉','或跃在渊,无咎','飞龙在天,利见大人','亢龙有悔']},
'1,5':{name:'天风姤',judgment:'姤,遇也,柔遇刚也。天地相遇,品物咸章',image:'天下有风,姤。后以施命诰四方',lines:['系于金柅,贞吉','包有鱼,无咎','臀无肤,其行次且','包无鱼,起凶','以杞包瓜,含章','姤其角,吝']},
'1,7':{name:'天山遁',judgment:'遁亨,遁而亨也。刚当位而应,与时行也',image:'天下有山,遁。君子以远小人,不恶而严',lines:['遁尾厉,勿用有攸往','执之用黄牛之革','系遁,有疾厉','好遁,君子吉','嘉遁,贞吉','肥遁,无不利']},
'1,8':{name:'天地否',judgment:'否之匪人,不利君子贞。大往小来',image:'天地不交,否。君子以俭德辟难',lines:['拔茅茹,以其汇,贞吉','包承,小人吉,大人否','包羞','有命无咎,畴离祉','休否,大人吉','倾否,先否后喜']},
'5,8':{name:'风地观',judgment:'观,盥而不荐,有孚颙若。观天之神道而四时不忒',image:'风行地上,观。先王以省方观民设教',lines:['童观,小人无咎','窥观,利女贞','观我生进退','观国之光,利用宾于王','观我生,君子无咎','观其生,君子无咎']},
'7,8':{name:'山地剥',judgment:'剥,剥也,柔变刚也。不利有攸往',image:'山附于地,剥。上以厚下安宅',lines:['剥床以足,蔑贞凶','剥床以辨,蔑贞凶','剥之无咎','剥床以肤,凶','贯鱼以宫人宠,无不利','硕果不食,君子得舆']},
'3,8':{name:'火地晋',judgment:'晋,进也。明出地上,顺而丽乎大明',image:'明出地上,晋。君子以自昭明德',lines:['晋如摧如,贞吉','晋如愁如,贞吉','众允,悔亡','晋如鼫鼠,贞厉','悔亡,失得勿恤','晋其角,维用伐邑']},
'3,1':{name:'火天大有',judgment:'大有,柔得尊位,大中而上下应之',image:'火在天上,大有。君子以遏恶扬善,顺天休命',lines:['无交害,匪咎','大车以载,有攸往','公用亨于天子','匪其彭,无咎','厥孚交如,威如','自天佑之,吉无不利']},
// 坎宫八卦 (9-16)
'6,6':{name:'坎为水',judgment:'习坎,重险也。水流而不盈,行险而不失其信',image:'水洊至,习坎。君子以常德行习教事',lines:['习坎,入于坎窞','坎有险,求小得','来之坎坎,险且枕','樽酒簋贰,用缶','坎不盈,祗既平','系用徽纆,寘于丛棘']},
'6,2':{name:'水泽节',judgment:'节亨,苦节不可贞',image:'泽上有水,节。君子以制数度议德行',lines:['不出户庭,无咎','不出门庭,凶','不节若,则嗟若','安节,亨','甘节,吉','苦节,贞凶']},
'6,4':{name:'水雷屯',judgment:'屯,刚柔始交而难生。动乎险中,大亨贞',image:'云雷屯,君子以经纶',lines:['磐桓,利居贞','屯如邅如,乘马班如','即鹿无虞,惟入于林中','乘马班如,求婚媾','屯其膏,小贞吉','乘马班如,泣血涟如']},
'6,3':{name:'水火既济',judgment:'既济亨,小者亨也。利贞,初吉终乱',image:'水在火上,既济。君子以思患而豫防之',lines:['曳其轮,濡其尾','妇丧其茀,勿逐','高宗伐鬼方,三年克之','繻有衣袽,终日戒','东邻杀牛,不如西邻','濡其首,厉']},
'2,3':{name:'泽火革',judgment:'革,水火相息。天地革而四时成',image:'泽中有火,革。君子以治历明时',lines:['巩用黄牛之革','巳日乃革之,征吉','征凶,贞厉','悔亡,有孚改命','大人虎变,未占有孚','君子豹变,小人革面']},
'4,3':{name:'雷火丰',judgment:'丰,大也。明以动,故丰',image:'雷电皆至,丰。君子以折狱致刑',lines:['遇其配主,虽旬无咎','丰其蔀,日中见斗','丰其沛,日中见沬','丰其蔀,日中见斗','来章,有庆誉','丰其屋,蔀其家']},
'8,3':{name:'地火明夷',judgment:'明夷,利艰贞。明入地中,明夷',image:'明入地中,明夷。君子以莅众用晦而明',lines:['明夷于飞,垂其翼','明夷,夷于左股','明夷于南狩,得其大首','入于左腹,获明夷之心','箕子之明夷,利贞','不明晦,初登于天']},
'8,6':{name:'地水师',judgment:'师,众也。贞,丈人吉,无咎',image:'地中有水,师。君子以容民畜众',lines:['师出以律,否臧凶','在师中,吉无咎','师或舆尸,凶','师左次,无咎','田有禽,利执言','大君有命,开国承家']},
// 艮宫八卦 (17-24)
'7,7':{name:'艮为山',judgment:'艮,止也。时止则止,时行则行',image:'兼山,艮。君子以思不出其位',lines:['艮其趾,无咎','艮其腓,不拯其随','艮其限,列其夤','艮其身,无咎','艮其辅,言有序','敦艮,吉']},
'7,3':{name:'山火贲',judgment:'贲亨,柔来而文刚,故亨',image:'山下有火,贲。君子以明庶政无敢折狱',lines:['贲其趾,舍车而徒','贲其须','贲如濡如,永贞吉','贲如皤如,白马翰如','贲于丘园,束帛戋戋','白贲,无咎']},
'7,1':{name:'山天大畜',judgment:'大畜,刚健笃实辉光,日新其德',image:'天在山中,大畜。君子以多识前言往行',lines:['有厉,利已','舆说輹','良马逐,利艰贞','童牛之牿,元吉','豮豕之牙,吉','何天之衢,亨']},
'7,2':{name:'山泽损',judgment:'损,损下益上,其道上行',image:'山下有泽,损。君子以惩忿窒欲',lines:['已事遄往,无咎','利贞,征凶,弗损益之','三人行则损一人','损其疾,使遄有喜','或益之十朋之龟','弗损益之,无咎']},
'3,2':{name:'火泽睽',judgment:'睽,火动而上,泽动而下',image:'上火下泽,睽。君子以同而异',lines:['悔亡,丧马勿逐','遇主于巷,无咎','见舆曳,其牛掣','睽孤,遇元夫','悔亡,厥宗噬肤','睽孤,见豕负涂']},
'1,2':{name:'天泽履',judgment:'履,柔履刚也。说而应乎乾',image:'上天下泽,履。君子以辩上下定民志',lines:['素履往,无咎','履道坦坦,幽人贞吉','眇能视,跛能履','履虎尾,愬愬终吉','夬履,贞厉','视履考祥,其旋元吉']},
'5,2':{name:'风泽中孚',judgment:'中孚,柔在内而刚得中',image:'泽上有风,中孚。君子以议狱缓死',lines:['虞吉,有它不燕','鸣鹤在阴,其子和之','得敌,或鼓或罢','月几望,马匹亡','有孚挛如,无咎','翰音登于天,贞凶']},
'5,7':{name:'风山渐',judgment:'渐,女归吉也。进得位,往有功也',image:'山上有木,渐。君子以居贤德善俗',lines:['鸿渐于干,小子厉','鸿渐于磐,饮食衎衎','鸿渐于陆,夫征不复','鸿渐于木,或得其桷','鸿渐于陵,妇三岁不孕','鸿渐于逵,其羽可用为仪']},
// 震宫八卦 (25-32)
'4,4':{name:'震为雷',judgment:'震亨。震来虩虩,笑言哑哑,震惊百里',image:'洊雷,震。君子以恐惧修省',lines:['震来虩虩,后笑言哑哑','震来厉,亿丧贝','震苏苏,震行无眚','震遂泥','震往来厉,亿无丧','震索索,视矍矍']},
'4,8':{name:'雷地豫',judgment:'豫,刚应而志行,顺以动',image:'雷出地奋,豫。先王以作乐崇德',lines:['鸣豫,凶','介于石,不终日','盱豫悔,迟有悔','由豫,大有得','贞疾,恒不死','冥豫,成有渝']},
'4,6':{name:'雷水解',judgment:'解,险以动,动而免乎险',image:'雷雨作,解。君子以赦过宥罪',lines:['无咎','田获三狐,得黄矢','负且乘,致寇至','解而拇,朋至斯孚','君子维有解,吉','公用射隼于高墉之上']},
'4,5':{name:'雷风恒',judgment:'恒,久也。刚上而柔下',image:'雷风,恒。君子以立不易方',lines:['浚恒,贞凶','悔亡','不恒其德,或承之羞','田无禽','恒其德,贞妇人吉','振恒,凶']},
'8,5':{name:'地风升',judgment:'柔以时升,巽而顺,刚中而应',image:'地中生木,升。君子以顺德积小以高大',lines:['允升,大吉','孚乃利用禴,无咎','升虚邑','王用亨于岐山','贞吉,升阶','冥升,利于不息之贞']},
'6,5':{name:'水风井',judgment:'井,改邑不改井,无丧无得',image:'木上有水,井。君子以劳民劝相',lines:['井泥不食,旧井无禽','井谷射鲋,瓮敝漏','井渫不食,为我心恻','井甃,无咎','井洌,寒泉食','井收勿幕,有孚元吉']},
'2,5':{name:'泽风大过',judgment:'大过,大者过也。栋桡,本末弱也',image:'泽灭木,大过。君子以独立不惧遁世无闷',lines:['藉用白茅,无咎','枯杨生稊,老夫得其女妻','栋桡,凶','栋隆,吉','枯杨生华,老妇得士夫','过涉灭顶,凶']},
'2,4':{name:'泽雷随',judgment:'随,刚来而下柔,动而说',image:'泽中有雷,随。君子以向晦入宴息',lines:['官有渝,贞吉','系小子,失丈夫','系丈夫,失小子','随有获,贞凶','孚于嘉,吉','拘系之,乃从维之']},
// 巽宫八卦 (33-40)
'5,5':{name:'巽为风',judgment:'重巽以申命,刚巽乎中正而志行',image:'随风,巽。君子以申命行事',lines:['进退,利武人之贞','巽在床下,用史巫纷若','频巽,吝','悔亡,田获三品','贞吉,悔亡无不利','巽在床下,丧其资斧']},
'5,1':{name:'风天小畜',judgment:'小畜,柔得位而上下应之',image:'风行天上,小畜。君子以懿文德',lines:['复自道,何其咎','牵复,吉','舆说辐,夫妻反目','有孚,血去惕出','有孚挛如,富以其邻','既雨既处,尚德载']},
'5,3':{name:'风火家人',judgment:'家人,女正位乎内,男正位乎外',image:'风自火出,家人。君子以言有物而行有恒',lines:['闲有家,悔亡','无攸遂,在中馈','家人嗃嗃,悔厉吉','富家,大吉','王假有家,勿恤','有孚威如,终吉']},
'5,4':{name:'风雷益',judgment:'益,损上益下,民说无疆',image:'风雷,益。君子以见善则迁有过则改',lines:['利用为大作,元吉','或益之十朋之龟','益之用凶事,无咎','中行告公从,利用为依迁国','有孚惠心,勿问元吉','莫益之,或击之']},
'1,4':{name:'天雷无妄',judgment:'无妄,刚自外来而为主于内',image:'天下雷行,物与无妄。先王以茂对时育万物',lines:['无妄,往吉','不耕获,不菑畬','无妄之灾,或系之牛','可贞,无咎','无妄之疾,勿药有喜','无妄,行有眚']},
'3,4':{name:'火雷噬嗑',judgment:'噬嗑亨,利用狱。刚柔分动而明',image:'雷电噬嗑,先王以明罚敕法',lines:['屦校灭趾,无咎','噬肤灭鼻,无咎','噬腊肉,遇毒','噬干胏,得金矢','噬干肉,得黄金','何校灭耳,凶']},
'7,4':{name:'山雷颐',judgment:'颐,贞吉。观颐,自求口实',image:'山下有雷,颐。君子以慎言语节饮食',lines:['舍尔灵龟,观我朵颐','颠颐,拂经于丘颐','拂颐,贞凶','颠颐,吉','拂经,居贞吉','由颐,厉吉,利涉大川']},
'7,5':{name:'山风蛊',judgment:'蛊,元亨。利涉大川,先甲三日后甲三日',image:'山下有风,蛊。君子以振民育德',lines:['干父之蛊,有子考无咎','干母之蛊,不可贞','干父之蛊,小有悔','裕父之蛊,往见吝','干父之蛊,用誉','不事王侯,高尚其事']},
// 离宫八卦 (41-48)
'3,3':{name:'离为火',judgment:'离,丽也。日月丽乎天,百谷草木丽乎土',image:'明两作,离。大人以继明照于四方',lines:['履错然,敬之无咎','黄离,元吉','日昃之离,不鼓缶而歌','突如其来如,焚如死如弃如','出涕沱若,戚嗟若','王用出征,有嘉折首']},
'3,7':{name:'火山旅',judgment:'旅,小亨。旅贞吉',image:'山上有火,旅。君子以明慎用刑而不留狱',lines:['旅琐琐,斯其所取灾','旅即次,怀其资','旅焚其次,丧其童仆','旅于处,得其资斧','射雉,一矢亡','鸟焚其巢,旅人先笑后号咷']},
'3,5':{name:'火风鼎',judgment:'鼎,象也。以木巽火,亨饪也',image:'木上有火,鼎。君子以正位凝命',lines:['鼎颠趾,利出否','鼎有实,我仇有疾','鼎耳革,其行塞','鼎折足,覆公餗','鼎黄耳金铉,利贞','鼎玉铉,大吉']},
'3,6':{name:'火水未济',judgment:'未济亨,小狐汔济,濡其尾',image:'火在水上,未济。君子以慎辨物居方',lines:['濡其尾,吝','曳其轮,贞吉','未济,征凶','贞吉,悔亡','贞吉,无悔','有孚于饮酒,无咎']},
'7,6':{name:'山水蒙',judgment:'蒙亨。匪我求童蒙,童蒙求我',image:'山下出泉,蒙。君子以果行育德',lines:['发蒙,利用刑人','包蒙,吉','勿用取女,见金夫','困蒙,吝','童蒙,吉','击蒙,不利为寇']},
'5,6':{name:'风水涣',judgment:'涣亨。王假有庙,利涉大川',image:'风行水上,涣。先王以享于帝立庙',lines:['用拯马壮,吉','涣奔其机,悔亡','涣其躬,无悔','涣其群,元吉','涣汗其大号','涣其血,去逖出']},
'1,6':{name:'天水讼',judgment:'讼,上刚下险,险而健,讼',image:'天与水违行,讼。君子以作事谋始',lines:['不永所事,小有言','不克讼,归而逋','食旧德,贞厉终吉','不克讼,复即命渝','讼,元吉','或锡之鞶带,终朝三褫']},
'1,3':{name:'天火同人',judgment:'同人,柔得位得中而应乎乾',image:'天与火,同人。君子以类族辨物',lines:['同人于门,无咎','同人于宗,吝','伏戎于莽,升其高陵','乘其墉,弗克攻','同人先号咷而后笑','同人于郊,无悔']},
// 坤宫八卦 (49-56)
'8,8':{name:'坤为地',judgment:'至哉坤元,万物资生,乃顺承天',image:'地势坤,君子以厚德载物',lines:['履霜,坚冰至','直方大,不习无不利','含章可贞,或从王事','括囊,无咎无誉','黄裳,元吉','龙战于野,其血玄黄']},
'8,4':{name:'地雷复',judgment:'复亨。出入无疾,朋来无咎',image:'雷在地中,复。先王以至日闭关',lines:['不远复,无祗悔','休复,吉','频复,厉','中行独复','敦复,无悔','迷复,凶有灾眚']},
'8,2':{name:'地泽临',judgment:'临,刚浸而长,说而顺',image:'泽上有地,临。君子以教思无穷容保民无疆',lines:['咸临,贞吉','咸临,吉无不利','甘临,无攸利','至临,无咎','知临,大君之宜','敦临,吉无咎']},
'8,1':{name:'地天泰',judgment:'泰,小往大来,吉亨。天地交而万物通',image:'天地交,泰。后以财成天地之道',lines:['拔茅茹,以其汇','包荒,用冯河','无平不陂,无往不复','翩翩,不富以其邻','帝乙归妹,以祉元吉','城复于隍,勿用师']},
'4,1':{name:'雷天大壮',judgment:'大壮,大者壮也。刚以动,故壮',image:'雷在天上,大壮。君子以非礼勿履',lines:['壮于趾,征凶','贞吉','小人用壮,君子用罔','贞吉,悔亡','丧羊于易,无悔','羝羊触藩,不能退']},
'2,1':{name:'泽天夬',judgment:'夬,决也,刚决柔也。健而说,决而和',image:'泽上于天,夬。君子以施禄及下',lines:['壮于前趾,往不胜为咎','惕号,莫夜有戎','壮于頄,有凶','臀无肤,其行次且','苋陆夬夬,中行无咎','无号,终有凶']},
'6,1':{name:'水天需',judgment:'需,须也。险在前也,刚健而不陷',image:'云上于天,需。君子以饮食宴乐',lines:['需于郊,利用恒','需于沙,小有言','需于泥,致寇至','需于血,出自穴','需于酒食,贞吉','入于穴,有不速之客三人来']},
'6,8':{name:'水地比',judgment:'比,吉也。比,辅也,下顺从也',image:'地上有水,比。先王以建万国亲诸侯',lines:['有孚比之,无咎','比之自内,贞吉','比之匪人','外比之,贞吉','显比,王用三驱','比之无首,凶']},
// 兑宫八卦 (57-64)
'2,2':{name:'兑为泽',judgment:'兑,说也。刚中而柔外,说以利贞',image:'丽泽,兑。君子以朋友讲习',lines:['和兑,吉','孚兑,吉','来兑,凶','商兑未宁,介疾有喜','孚于剥,有厉','引兑']},
'2,6':{name:'泽水困',judgment:'困,刚掩也。险以说,困而不失其所',image:'泽无水,困。君子以致命遂志',lines:['臀困于株木,入于幽谷','困于酒食,朱绂方来','困于石,据于蒺藜','来徐徐,困于金车','劓刖,困于赤绂','困于葛藟,于臲兀']},
'2,8':{name:'泽地萃',judgment:'萃,聚也。顺以说,刚中而应',image:'泽上于地,萃。君子以除戎器戒不虞',lines:['有孚不终,乃乱乃萃','引吉,无咎','萃如嗟如,无攸利','大吉,无咎','萃有位,无咎','赍咨涕洟,无咎']},
'2,7':{name:'泽山咸',judgment:'咸,感也。柔上而刚下,二气感应以相与',image:'山上有泽,咸。君子以虚受人',lines:['咸其拇','咸其腓,凶','咸其股,执其随','贞吉,悔亡','咸其脢,无悔','咸其辅颊舌']},
'6,7':{name:'水山蹇',judgment:'蹇,难也,险在前也。见险而能止',image:'山上有水,蹇。君子以反身修德',lines:['往蹇,来誉','王臣蹇蹇,匪躬之故','往蹇,来反','往蹇,来连','大蹇,朋来','往蹇,来硕']},
'8,7':{name:'地山谦',judgment:'谦亨。天道下济而光明,地道卑而上行',image:'地中有山,谦。君子以裒多益寡称物平施',lines:['谦谦君子,用涉大川','鸣谦,贞吉','劳谦君子,有终吉','无不利,撝谦','不富以其邻,利用侵伐','鸣谦,利用行师']},
'4,7':{name:'雷山小过',judgment:'小过,小者过而亨也。过以利贞',image:'山上有雷,小过。君子以行过乎恭',lines:['飞鸟以凶','过其祖,遇其妣','弗过防之,从或戕之','无咎,弗过遇之','密云不雨,自我西郊','弗遇过之,飞鸟离之']},
'4,2':{name:'雷泽归妹',judgment:'归妹,天地之大义也。天地不交而万物不兴',image:'泽上有雷,归妹。君子以永终知敝',lines:['归妹以娣,跛能履','眇能视,利幽人之贞','归妹以须,反归以娣','归妹愆期,迟归有时','帝乙归妹,其君之袂','女承筐无实,士刲羊无血']},
};
// Generate all 64 hexagrams
function buildHexagramMap() {
const map: Record<string, HexagramInfo> = {};
for (let upper = 1; upper <= 8; upper++) {
for (let lower = 1; lower <= 8; lower++) {
const key = `${upper},${lower}`;
const hName = H[key];
if (hName) {
map[key] = {
number: (upper - 1) * 8 + lower,
name: hName.name,
upperTrigram: TRIGRAMS[upper],
lowerTrigram: TRIGRAMS[lower],
changingLine: 0,
interpretation: hName.judgment,
judgment: hName.judgment,
image: hName.image,
lines: hName.lines,
};
}
}
}
return map;
}
const HEXAGRAM_MAP = buildHexagramMap();
/** Calculate Plum Blossom I-Ching from date and time */
export function calculatePlumBlossom(
year: number,
month: number,
day: number,
hour: number = 12,
): PlumBlossomResult {
// Use full numbers for more entropy
const yearNum = year;
const monthNum = month;
const dayNum = day;
const hourIndex = Math.floor(((hour + 1) % 24) / 2); // 0-11 地支时辰
// Standard Plum Blossom formula:
// Upper trigram: (year + month + day) % 8 → 0-7 → +1 → 1-8
const upperIdx = ((yearNum + monthNum + dayNum) % 8) + 1;
// Lower trigram: (month + day + hourIndex + 1) % 8 + 1
const lowerIdx = ((monthNum + dayNum + hourIndex + 1) % 8) + 1;
// Changing line: (year + month + day + hourIndex + 1) % 6 + 1
const changingLine = ((yearNum + monthNum + dayNum + hourIndex + 1) % 6) + 1;
const upperTrigram = TRIGRAMS[upperIdx];
const lowerTrigram = TRIGRAMS[lowerIdx];
// Original hexagram
const originalKey = `${upperIdx},${lowerIdx}`;
const originalHexagram = HEXAGRAM_MAP[originalKey] || createFallbackHexagram(upperIdx, lowerIdx, changingLine);
// Transformed hexagram (after changing line)
let transUpperIdx = upperIdx;
let transLowerIdx = lowerIdx;
// Changing line 1-3 affects lower trigram, 4-6 affects upper
if (changingLine >= 4) {
transUpperIdx = flipTrigramLine(upperIdx, changingLine - 3);
} else {
transLowerIdx = flipTrigramLine(lowerIdx, changingLine);
}
const transKey = `${transUpperIdx},${transLowerIdx}`;
const transformedHexagram = HEXAGRAM_MAP[transKey] || createFallbackHexagram(transUpperIdx, transLowerIdx, 0);
// Mutual hexagram (互卦): 2-4 lines → lower, 3-5 lines → upper
// Simplified: use middle two trigrams
const mutualUpperIdx = lowerIdx; // simplified
const mutualLowerIdx = upperIdx; // simplified
const mutualKey = `${mutualUpperIdx},${mutualLowerIdx}`;
const mutualHexagram = HEXAGRAM_MAP[mutualKey] || null;
// Constitution (体卦) and Function (用卦)
// 体卦 = lower trigram, 用卦 = upper trigram
const constitution = lowerTrigram.element;
const function_ = upperTrigram.element;
const relationship = getElementRelationship(constitution, function_);
return {
originalHexagram: { ...originalHexagram, changingLine },
transformedHexagram,
mutualHexagram,
upperTrigram,
lowerTrigram,
changingLine,
constitution,
function: function_,
relationship,
};
}
function flipTrigramLine(trigramIdx: number, line: number): number {
// Flip a single line of the trigram
// Trigrams encoded as bits: 乾111=7, 兑110=6, 离101=5, 震100=4, 巽011=3, 坎010=2, 艮001=1, 坤000=0
const encoding = [0, 7, 6, 5, 4, 3, 2, 1, 0]; // index → bit pattern
let bits = encoding[trigramIdx] || 0;
bits ^= (1 << (line - 1)); // flip the line
// Map back
const decoding = [8, 7, 6, 2, 5, 3, 4, 1]; // bit pattern → index
return decoding[bits] || trigramIdx;
}
function getElementRelationship(body: string, func: string): string {
const cycle: Record<string, string> = { '木': '火', '火': '土', '土': '金', '金': '水', '水': '木' };
const reverse: Record<string, string> = { '木': '水', '水': '金', '金': '土', '土': '火', '火': '木' };
if (body === func) return '比和(体用相同,诸事顺利)';
if (cycle[body] === func) return '体生用(泄气,宜守不宜攻)';
if (reverse[body] === func) return '用生体(得力,有贵人相助)';
if (cycle[func] === body) return '用克体(受制,诸事不顺)';
if (reverse[func] === body) return '体克用(主动,需付出努力)';
return '体用相生';
}
function createFallbackHexagram(upper: number, lower: number, changingLine: number): HexagramInfo {
const ut = TRIGRAMS[upper];
const lt = TRIGRAMS[lower];
return {
number: (upper - 1) * 8 + lower,
name: `${ut.name}${lt.name}`,
upperTrigram: ut,
lowerTrigram: lt,
changingLine,
interpretation: '此卦象需结合具体事理参详',
judgment: '',
image: '',
lines: ['','','','','',''],
};
}
@@ -0,0 +1,116 @@
import {
HeavenStem,
EarthBranch,
} from 'tyme4ts';
/** Relationship classification for branch interactions */
export interface BranchRelationship {
combine: boolean; // 六合
threeCombine: boolean; // 三合
opposite: boolean; // 六冲
harm: boolean; // 六害
punish: boolean; // 相刑
formation: string | null; // 三合局名称
}
/** Three-combine formations (三合局) */
const THREE_COMBINES: Record<string, { branches: string[]; name: string; element: string }> = {
'水局': { branches: ['申', '子', '辰'], name: '水局', element: '水' },
'木局': { branches: ['亥', '卯', '未'], name: '木局', element: '木' },
'火局': { branches: ['寅', '午', '戌'], name: '火局', element: '火' },
'金局': { branches: ['巳', '酉', '丑'], name: '金局', element: '金' },
};
/** Check branch relationships between two earth branches */
export function getBranchRelationship(branchA: string, branchB: string): BranchRelationship {
const a = EarthBranch.fromName(branchA);
const b = EarthBranch.fromName(branchB);
// Six combine (六合): each branch's combine partner
const combinePartner = a.getCombine();
const hasCombine = combinePartner ? combinePartner.getName() === b.getName() : false;
// Opposite (六冲): each branch's opposite
const oppositePartner = a.getOpposite();
const hasOpposite = oppositePartner ? oppositePartner.getName() === b.getName() : false;
// Harm (六害): each branch's harm partner
let hasHarm = false;
try {
const harmPartner = a.getHarm();
hasHarm = harmPartner ? harmPartner.getName() === b.getName() : false;
} catch { /* harm may not be available */ }
// Three combine (三合): check if both branches are in the same formation
let hasThreeCombine = false;
let formation: string | null = null;
for (const [name, info] of Object.entries(THREE_COMBINES)) {
if (info.branches.includes(branchA) && info.branches.includes(branchB)) {
hasThreeCombine = true;
formation = name;
break;
}
}
// Punish (相刑)
const hasPunish = checkPunish(branchA, branchB);
return {
combine: hasCombine,
threeCombine: hasThreeCombine,
opposite: hasOpposite,
harm: hasHarm,
punish: hasPunish,
formation,
};
}
/** Check for punishment relationship between two branches */
function checkPunish(a: string, b: string): boolean {
// Self punishment (自刑)
const selfPunish = ['辰', '午', '酉', '亥'];
if (a === b && selfPunish.includes(a)) return true;
// Classic punishment pairs
const punishPairs: [string, string][] = [
['寅', '巳'], ['巳', '申'], ['申', '寅'], // 无恩之刑
['丑', '戌'], ['戌', '未'], ['未', '丑'], // 恃势之刑
['子', '卯'], ['卯', '子'], // 无礼之刑
];
return punishPairs.some(([x, y]) => x === a && y === b);
}
/** Get Ten Star relationship between two heavenly stems */
export function getTenStarRelationship(subjectStem: string, objectStem: string): string {
try {
const s = HeavenStem.fromName(subjectStem);
const o = HeavenStem.fromName(objectStem);
return s.getTenStar(o).getName();
} catch {
return '';
}
}
/** Check if two heavenly stems combine (天干合) */
export function checkStemCombine(stemA: string, stemB: string): boolean {
const combinePairs: Record<string, string> = {
'甲': '己', '己': '甲',
'乙': '庚', '庚': '乙',
'丙': '辛', '辛': '丙',
'丁': '壬', '壬': '丁',
'戊': '癸', '癸': '戊',
};
return combinePairs[stemA] === stemB;
}
/** Check if two heavenly stems oppose (天干冲) */
export function checkStemOpposite(stemA: string, stemB: string): boolean {
const oppositePairs: Record<string, string> = {
'甲': '庚', '庚': '甲',
'乙': '辛', '辛': '乙',
'丙': '壬', '壬': '丙',
'丁': '癸', '癸': '丁',
};
return oppositePairs[stemA] === stemB;
}
@@ -0,0 +1,359 @@
/**
* 八字神煞 (Shen Sha)
* 神煞是固定规则查表 + 四柱匹配的标记,规则来源《三命通会》《渊海子平》。
* 注意:部分神煞网上存在版本差异,本模块采用主流排盘工具通行版本。
*/
import type { EightCharInfo, PillarInfo } from '../types/bazi';
export interface ShenshaInfo {
name: string;
/** 吉/凶/中性 */
type: '吉' | '凶' | '中性';
/** 分类:贵人/文星/财禄/感情/变动/威权/灾煞/孤独/格局 */
category: string;
/** 命中查法说明,如 "日干己查四支" */
anchors: string[];
/** 命中位置,如 ["年支","时支"] */
foundIn: string[];
/** 吉凶含义简述 */
description: string;
}
interface ShenshaRule {
name: string;
type: ShenshaInfo['type'];
category: string;
description: string;
/** 按天干(年干/日干)查四支 */
byStem?: Record<string, string[]>;
/** 按地支(年支/日支)查四支 */
byBranch?: Record<string, string[]>;
/** 按月支查四干 */
byMonthStem?: Record<string, string[]>;
/** 日柱特殊格局 */
byDayPillar?: string[];
}
const PILLAR_KEYS = ['year', 'month', 'day', 'hour'] as const;
const RULES: ShenshaRule[] = [
{
name: '天乙贵人',
type: '吉',
category: '贵人',
description: '最吉之神煞,主逢凶化吉、贵人相助',
byStem: {
'甲': ['丑', '未'], '戊': ['丑', '未'],
'乙': ['子', '申'], '己': ['子', '申'],
'丙': ['亥', '酉'], '丁': ['亥', '酉'],
'壬': ['卯', '巳'], '癸': ['卯', '巳'],
'辛': ['寅', '午'],
},
},
{
name: '天厨贵人',
type: '吉',
category: '贵人',
description: '主口福与衣食之禄,衣食无忧',
byStem: {
'甲': ['巳'], '乙': ['午'], '丙': ['巳'], '丁': ['午'],
'戊': ['申'], '己': ['酉'], '庚': ['亥'], '辛': ['子'],
'壬': ['寅'], '癸': ['卯'],
},
},
{
name: '文昌贵人',
type: '吉',
category: '文星',
description: '主聪明好学、利科名学业',
byStem: {
'甲': ['巳'], '乙': ['午'], '丙': ['申'], '丁': ['酉'],
'戊': ['申'], '己': ['酉'], '庚': ['亥'], '辛': ['子'],
'壬': ['寅'], '癸': ['卯'],
},
},
{
name: '禄神',
type: '吉',
category: '财禄',
description: '日干之禄,主衣禄与财运根基',
byStem: {
'甲': ['寅'], '乙': ['卯'], '丙': ['巳'], '丁': ['午'],
'戊': ['巳'], '己': ['午'], '庚': ['申'], '辛': ['酉'],
'壬': ['亥'], '癸': ['子'],
},
},
{
name: '羊刃',
type: '凶',
category: '灾煞',
description: '刚烈冲动之星,主脾气刚猛,喜用则有权柄',
byStem: {
'甲': ['卯'], '乙': ['寅'], '丙': ['午'], '丁': ['巳'],
'戊': ['午'], '己': ['巳'], '庚': ['酉'], '辛': ['申'],
'壬': ['子'], '癸': ['亥'],
},
},
{
name: '金舆',
type: '吉',
category: '财禄',
description: '禄前二位,主婚恋顺遂、富贵安逸',
byStem: {
'甲': ['辰'], '乙': ['巳'], '丙': ['未'], '丁': ['申'],
'戊': ['未'], '己': ['申'], '庚': ['戌'], '辛': ['亥'],
'壬': ['丑'], '癸': ['寅'],
},
},
{
name: '天德贵人',
type: '吉',
category: '贵人',
description: '主心地仁慈、逢凶化吉,利化解灾厄',
byMonthStem: {
'寅': ['丁'], '卯': ['申'], '辰': ['壬'], '巳': ['辛'],
'午': ['亥'], '未': ['甲'], '申': ['癸'], '酉': ['寅'],
'戌': ['丙'], '亥': ['乙'], '子': ['巳'], '丑': ['庚'],
},
},
{
name: '月德贵人',
type: '吉',
category: '贵人',
description: '主福荫深厚、遇难呈祥',
byMonthStem: {
'寅': ['丙'], '午': ['丙'], '戌': ['丙'],
'申': ['壬'], '子': ['壬'], '辰': ['壬'],
'亥': ['甲'], '卯': ['甲'], '未': ['甲'],
'巳': ['庚'], '酉': ['庚'], '丑': ['庚'],
},
},
{
name: '桃花(咸池)',
type: '中性',
category: '感情',
description: '主异性缘、魅力与风流,利艺术才华',
byBranch: {
'申': ['酉'], '子': ['酉'], '辰': ['酉'],
'寅': ['卯'], '午': ['卯'], '戌': ['卯'],
'巳': ['午'], '酉': ['午'], '丑': ['午'],
'亥': ['子'], '卯': ['子'], '未': ['子'],
},
},
{
name: '驿马',
type: '中性',
category: '变动',
description: '主奔波变动、外出发展,动中得财',
byBranch: {
'申': ['寅'], '子': ['寅'], '辰': ['寅'],
'寅': ['申'], '午': ['申'], '戌': ['申'],
'巳': ['亥'], '酉': ['亥'], '丑': ['亥'],
'亥': ['巳'], '卯': ['巳'], '未': ['巳'],
},
},
{
name: '华盖',
type: '中性',
category: '孤独',
description: '主艺术天赋、悟性高,也主清高孤傲',
byBranch: {
'申': ['辰'], '子': ['辰'], '辰': ['辰'],
'寅': ['戌'], '午': ['戌'], '戌': ['戌'],
'巳': ['丑'], '酉': ['丑'], '丑': ['丑'],
'亥': ['未'], '卯': ['未'], '未': ['未'],
},
},
{
name: '劫煞',
type: '凶',
category: '灾煞',
description: '主突发变故、是非破财,宜谨慎',
byBranch: {
'申': ['巳'], '子': ['巳'], '辰': ['巳'],
'寅': ['亥'], '午': ['亥'], '戌': ['亥'],
'巳': ['寅'], '酉': ['寅'], '丑': ['寅'],
'亥': ['申'], '卯': ['申'], '未': ['申'],
},
},
{
name: '亡神',
type: '凶',
category: '灾煞',
description: '主心机深、谋略强,亦主口舌是非',
byBranch: {
'申': ['亥'], '子': ['亥'], '辰': ['亥'],
'寅': ['巳'], '午': ['巳'], '戌': ['巳'],
'巳': ['申'], '酉': ['申'], '丑': ['申'],
'亥': ['寅'], '卯': ['寅'], '未': ['寅'],
},
},
{
name: '将星',
type: '吉',
category: '威权',
description: '主领导才能与威严,掌权柄之星',
byBranch: {
'申': ['子'], '子': ['子'], '辰': ['子'],
'寅': ['午'], '午': ['午'], '戌': ['午'],
'巳': ['酉'], '酉': ['酉'], '丑': ['酉'],
'亥': ['卯'], '卯': ['卯'], '未': ['卯'],
},
},
{
name: '红鸾',
type: '吉',
category: '感情',
description: '主婚恋喜事、姻缘早成',
byBranch: {
'子': ['卯'], '丑': ['寅'], '寅': ['丑'], '卯': ['子'],
'辰': ['亥'], '巳': ['戌'], '午': ['酉'], '未': ['申'],
'申': ['未'], '酉': ['午'], '戌': ['巳'], '亥': ['辰'],
},
},
{
name: '天喜',
type: '吉',
category: '感情',
description: '红鸾对宫,主喜事临门、人缘佳',
byBranch: {
'子': ['酉'], '丑': ['申'], '寅': ['未'], '卯': ['午'],
'辰': ['巳'], '巳': ['辰'], '午': ['卯'], '未': ['寅'],
'申': ['丑'], '酉': ['子'], '戌': ['亥'], '亥': ['戌'],
},
},
{
name: '孤辰',
type: '凶',
category: '孤独',
description: '主孤独离群、六亲缘薄',
byBranch: {
'亥': ['寅'], '子': ['寅'], '丑': ['寅'],
'寅': ['巳'], '卯': ['巳'], '辰': ['巳'],
'巳': ['申'], '午': ['申'], '未': ['申'],
'申': ['亥'], '酉': ['亥'], '戌': ['亥'],
},
},
{
name: '寡宿',
type: '凶',
category: '孤独',
description: '主孤寡之象,婚恋宜晚',
byBranch: {
'亥': ['戌'], '子': ['戌'], '丑': ['戌'],
'寅': ['丑'], '卯': ['丑'], '辰': ['丑'],
'巳': ['辰'], '午': ['辰'], '未': ['辰'],
'申': ['未'], '酉': ['未'], '戌': ['未'],
},
},
{
name: '天罗',
type: '凶',
category: '灾煞',
description: '戌亥为天罗,主困顿束缚,男命尤忌',
byBranch: { '子': ['戌', '亥'], '丑': ['戌', '亥'], '寅': ['戌', '亥'], '卯': ['戌', '亥'], '辰': ['戌', '亥'], '巳': ['戌', '亥'], '午': ['戌', '亥'], '未': ['戌', '亥'], '申': ['戌', '亥'], '酉': ['戌', '亥'], '戌': ['戌', '亥'], '亥': ['戌', '亥'] },
},
{
name: '地网',
type: '凶',
category: '灾煞',
description: '辰巳为地网,主束缚波折,女命尤忌',
byBranch: { '子': ['辰', '巳'], '丑': ['辰', '巳'], '寅': ['辰', '巳'], '卯': ['辰', '巳'], '辰': ['辰', '巳'], '巳': ['辰', '巳'], '午': ['辰', '巳'], '未': ['辰', '巳'], '申': ['辰', '巳'], '酉': ['辰', '巳'], '戌': ['辰', '巳'], '亥': ['辰', '巳'] },
},
{
name: '魁罡',
type: '中性',
category: '格局',
description: '日柱魁罡,主刚毅果断、聪明果敢,忌刑冲',
byDayPillar: ['庚辰', '庚戌', '壬辰', '戊戌'],
},
{
name: '阴差阳错',
type: '凶',
category: '格局',
description: '日柱阴差阳错,主婚姻不顺、易生波折',
byDayPillar: ['丙子', '丁丑', '戊寅', '辛卯', '壬辰', '癸巳', '丙午', '丁未', '戊申', '辛酉', '壬戌', '癸亥'],
},
];
const PILLAR_LABELS: Record<string, string> = {
yearStem: '年干', monthStem: '月干', dayStem: '日干', hourStem: '时干',
yearBranch: '年支', monthBranch: '月支', dayBranch: '日支', hourBranch: '时支',
};
/** Analyze Shen Sha presence in a Bazi chart */
export function analyzeShensha(bazi: EightCharInfo): ShenshaInfo[] {
const pillars: Record<string, PillarInfo> = {
year: bazi.yearPillar, month: bazi.monthPillar, day: bazi.dayPillar, hour: bazi.hourPillar,
};
const stems: Record<string, string> = {};
const branches: Record<string, string> = {};
for (const k of PILLAR_KEYS) {
stems[`${k}Stem`] = pillars[k].heavenStem;
branches[`${k}Branch`] = pillars[k].earthBranch;
}
const dayGanzhi = pillars.day.ganzhi;
const result: ShenshaInfo[] = [];
for (const rule of RULES) {
const anchors: string[] = [];
const foundIn = new Set<string>();
if (rule.byStem) {
// 年干、日干查四支
for (const anchorKey of ['yearStem', 'dayStem'] as const) {
const targets = rule.byStem[stems[anchorKey]];
if (!targets) continue;
const hits = PILLAR_KEYS.filter(k => targets.includes(branches[`${k}Branch`]));
if (hits.length > 0) {
anchors.push(PILLAR_LABELS[anchorKey]);
hits.forEach(k => foundIn.add(PILLAR_LABELS[`${k}Branch`]));
}
}
}
if (rule.byBranch) {
// 年支、日支查四支
for (const anchorKey of ['yearBranch', 'dayBranch'] as const) {
const targets = rule.byBranch[branches[anchorKey]];
if (!targets) continue;
const hits = PILLAR_KEYS.filter(k => targets.includes(branches[`${k}Branch`]));
if (hits.length > 0) {
anchors.push(PILLAR_LABELS[anchorKey]);
hits.forEach(k => foundIn.add(PILLAR_LABELS[`${k}Branch`]));
}
}
}
if (rule.byMonthStem) {
// 月支查四干
const targets = rule.byMonthStem[branches.monthBranch];
if (targets) {
const hits = PILLAR_KEYS.filter(k => targets.includes(stems[`${k}Stem`]));
if (hits.length > 0) {
anchors.push('月支');
hits.forEach(k => foundIn.add(PILLAR_LABELS[`${k}Stem`]));
}
}
}
if (rule.byDayPillar && rule.byDayPillar.includes(dayGanzhi)) {
anchors.push('日柱');
foundIn.add('日柱');
}
if (foundIn.size > 0) {
result.push({
name: rule.name,
type: rule.type,
category: rule.category,
anchors,
foundIn: [...foundIn],
description: rule.description,
});
}
}
return result;
}
+84
View File
@@ -0,0 +1,84 @@
// Types
export type {
DayInfo,
AlmanacInfo,
HourAlmanac,
PillarInfo,
HideStemInfo,
EightCharInfo,
DecadeFortuneInfo,
FortuneInfo,
ChildLimitInfo,
BaziFullResult,
PillarRelationship,
DailyFortuneResult,
} from './types';
// Transformers
export {
solarDayToDayInfo,
getMonthCalendar,
getDayInfo,
getTodayInfo,
} from './transformers/day';
export {
solarDayToAlmanacInfo,
getAlmanacInfo,
} from './transformers/almanac';
export {
birthInfoToBazi,
} from './transformers/bazi';
export type { BirthParams } from './transformers/bazi';
export {
getYearMonths,
} from './transformers/yearMonths';
export type { YearMonthInfo } from './transformers/yearMonths';
// Calculators
export {
getBranchRelationship,
getTenStarRelationship,
checkStemCombine,
checkStemOpposite,
} from './calculators/relationship';
export type { BranchRelationship } from './calculators/relationship';
export {
calculateDailyFortune,
} from './calculators/dailyMatch';
export {
analyzeElementBalance,
} from './calculators/elementStrength';
export type { ElementProfile } from './calculators/elementStrength';
export {
calculatePlumBlossom,
} from './calculators/plumBlossom';
export type {
TrigramInfo,
HexagramInfo,
PlumBlossomResult,
} from './calculators/plumBlossom';
export {
calculateBoneWeight,
} from './calculators/boneWeight';
export type { BoneWeightResult } from './calculators/boneWeight';
export {
getBuddhistFestival,
} from './calculators/buddhistDates';
export {
analyzeShensha,
} from './calculators/shensha';
export type { ShenshaInfo } from './calculators/shensha';
export {
analyzeFortuneGanzhi,
} from './calculators/fortuneLuck';
export type { FortuneLuck } from './calculators/fortuneLuck';
@@ -0,0 +1,186 @@
import {
SolarDay,
type LunarHour,
} from 'tyme4ts';
import type { AlmanacInfo, HourAlmanac } from '../types/almanac';
/** Transform a SolarDay into a full AlmanacInfo object */
export function solarDayToAlmanacInfo(solarDay: SolarDay): AlmanacInfo {
const lunarDay = solarDay.getLunarDay();
const sixtyCycle = lunarDay.getSixtyCycle();
const stem = sixtyCycle.getHeavenStem();
const branch = sixtyCycle.getEarthBranch();
// Duty officer
const duty = lunarDay.getDuty();
// Twelve star
const twelveStar = lunarDay.getTwelveStar();
const ecliptic = twelveStar.getEcliptic();
// Twenty-eight star
const twentyEightStar = lunarDay.getTwentyEightStar();
// Nine star
const nineStar = lunarDay.getNineStar();
// Six star
const sixStar = lunarDay.getSixStar ? lunarDay.getSixStar().getName() : '';
// Minor Ren
const minorRen = lunarDay.getMinorRen ? lunarDay.getMinorRen() : null;
// Fetus
const fetusDay = lunarDay.getFetusDay();
// Recommendations and avoidances
let recommends: string[] = [];
let avoids: string[] = [];
try {
recommends = lunarDay.getRecommends().map(r => r.getName());
avoids = lunarDay.getAvoids().map(a => a.getName());
} catch { /* may throw for some dates */ }
// Gods
const goodGods: string[] = [];
const badGods: string[] = [];
try {
const gods = lunarDay.getGods();
for (const god of gods) {
const luck = god.getLuck();
if (luck) {
if (luck.getName() === '吉') {
goodGods.push(god.getName());
} else {
badGods.push(god.getName());
}
}
}
} catch { /* may throw */ }
// Peng Zu taboos
const pengZu = sixtyCycle.getPengZu();
// Branch relationships
const opposite = branch.getOpposite();
const harm = branch.getHarm();
const combine = branch.getCombine();
// Evil direction
const ominous = branch.getOminous ? branch.getOminous() : null;
// Na Yin
const nayin = sixtyCycle.getSound().getName();
// Moon phase
let phase = '';
try {
const p = lunarDay.getPhase();
if (p) phase = p.getName();
} catch { /* no phase info */ }
// Hourly almanac
const hourDetails = buildHourlyAlmanac(lunarDay.getHours(), recommends, avoids);
// Determine duty luck
const dutyName = duty.getName();
const luckyDuties = ['除', '执', '危', '成', '开'];
const unluckyDuties = ['建', '满', '平', '破', '收', '闭'];
let dutyLuck: 'good' | 'bad' | 'neutral' = 'neutral';
if (luckyDuties.includes(dutyName)) dutyLuck = 'good';
else if (unluckyDuties.includes(dutyName)) dutyLuck = 'bad';
// Note: duty luck also depends on day branch; simplified here
return {
duty: dutyName,
dutyLuck,
twelveStar: {
name: twelveStar.getName(),
ecliptic: ecliptic ? ecliptic.getName() : '',
luck: ecliptic && ecliptic.getName() === '黄道' ? 'good' : 'bad',
},
twentyEightStar: {
name: twentyEightStar.getName(),
luck: twentyEightStar.getLuck()?.getName() === '吉' ? 'good' : 'bad',
animal: twentyEightStar.getAnimal()?.getName() || '',
},
nineStar: {
name: nineStar.getName(),
color: nineStar.getColor ? nineStar.getColor() : '',
element: nineStar.getElement ? nineStar.getElement().getName() : '',
},
sixStar,
minorRen: minorRen ? {
name: minorRen.getName(),
luck: minorRen.getLuck()?.getName() === '吉' ? 'good' : 'bad',
element: minorRen.getElement()?.getName() || '',
} : { name: '', luck: 'bad' as const, element: '' },
phase,
fetus: {
direction: fetusDay.getDirection()?.getName() || '',
side: fetusDay.getSide() !== undefined ? (fetusDay.getSide() as unknown as number === 0 ? '房内' : '房外') : '',
position: fetusDay.getName(),
},
recommends,
avoids,
goodGods,
badGods,
dayStem: stem.getName(),
dayBranch: branch.getName(),
dayGanzhi: sixtyCycle.getName(),
pengZu: pengZu.getName(),
pengZuStem: pengZu.getPengZuHeavenStem()?.getName() || '',
pengZuBranch: pengZu.getPengZuEarthBranch()?.getName() || '',
clash: `${opposite.getZodiac().getName()}(${opposite.getName()})`,
harm: harm ? `${harm.getZodiac().getName()}(${harm.getName()})` : '',
combine: combine ? `${combine.getZodiac().getName()}(${combine.getName()})` : '',
evilDirection: ominous ? ominous.getName() : '',
nayin,
hourDetails,
};
}
function buildHourlyAlmanac(hours: LunarHour[], _dayRecommends: string[], _dayAvoids: string[]): HourAlmanac[] {
return hours.map(hour => {
const hourSixtyCycle = hour.getSixtyCycle();
const twelveStar = hour.getTwelveStar ? hour.getTwelveStar() : null;
const nineStar = hour.getNineStar ? hour.getNineStar() : null;
let hourRecommends: string[] = [];
let hourAvoids: string[] = [];
try {
hourRecommends = hour.getRecommends().map(r => r.getName());
hourAvoids = hour.getAvoids().map(a => a.getName());
} catch { /* may throw */ }
const branchName = hourSixtyCycle.getEarthBranch().getName();
const hourNames: Record<string, string> = {
'子': '子时', '丑': '丑时', '寅': '寅时', '卯': '卯时',
'辰': '辰时', '巳': '巳时', '午': '午时', '未': '未时',
'申': '申时', '酉': '酉时', '戌': '戌时', '亥': '亥时',
};
const hourRanges: Record<string, string> = {
'子': '23:00-01:00', '丑': '01:00-03:00', '寅': '03:00-05:00',
'卯': '05:00-07:00', '辰': '07:00-09:00', '巳': '09:00-11:00',
'午': '11:00-13:00', '未': '13:00-15:00', '申': '15:00-17:00',
'酉': '17:00-19:00', '戌': '19:00-21:00', '亥': '21:00-23:00',
};
return {
branch: branchName,
name: hourNames[branchName] || branchName,
range: hourRanges[branchName] || '',
ganzhi: hourSixtyCycle.getName(),
recommends: hourRecommends,
avoids: hourAvoids,
twelveStar: twelveStar?.getName() || '',
nineStar: nineStar?.getName() || '',
};
});
}
/** Get AlmanacInfo for a specific date */
export function getAlmanacInfo(year: number, month: number, day: number): AlmanacInfo {
const solarDay = SolarDay.fromYmd(year, month, day);
return solarDayToAlmanacInfo(solarDay);
}
+212
View File
@@ -0,0 +1,212 @@
import {
SolarTime,
Gender,
ChildLimit,
HideHeavenStemType,
YinYang,
type SixtyCycle,
type HeavenStem,
} from 'tyme4ts';
import type {
PillarInfo,
HideStemInfo,
DecadeFortuneInfo,
FortuneInfo,
BaziFullResult,
} from '../types/bazi';
export interface BirthParams {
year: number;
month: number;
day: number;
hour: number;
minute: number;
gender: 'male' | 'female';
/** 八字流派:lateZiNextDay=晚子时算次日(23点换日,默认);earlyZiSameDay=晚子时算当日(0点换日) */
ziSect?: 'lateZiNextDay' | 'earlyZiSameDay';
}
/** Transform birth parameters into full Bazi result */
export function birthInfoToBazi(params: BirthParams): BaziFullResult {
const { year, month, day, hour, minute, gender, ziSect = 'lateZiNextDay' } = params;
const solarTime = SolarTime.fromYmdHms(year, month, day, hour, minute, 0);
// 早子时流派:23:00 后出生按当日早子时排四柱(日柱不换日),起运仍按真实出生时间
const ziHour =
ziSect === 'earlyZiSameDay' && hour >= 23
? SolarTime.fromYmdHms(year, month, day, 0, minute, 0)
: solarTime;
const lunarHour = ziHour.getLunarHour();
const eightChar = lunarHour.getEightChar();
const yearPillar = eightChar.getYear();
const monthPillar = eightChar.getMonth();
const dayPillar = eightChar.getDay();
const hourPillar = eightChar.getHour();
// Day master
const dayStem = dayPillar.getHeavenStem();
const dayMasterStem = dayStem.getName();
const dayMasterElement = dayStem.getElement().getName();
// Extract pillars with Ten Star relative to day master
const yearPillarInfo = extractPillarInfo(yearPillar, dayStem);
const monthPillarInfo = extractPillarInfo(monthPillar, dayStem);
const dayPillarInfo = extractPillarInfo(dayPillar, dayStem);
const hourPillarInfo = extractPillarInfo(hourPillar, dayStem);
// Fetal origin, fetal breath, own sign, body sign
const fetalOrigin = eightChar.getFetalOrigin();
const fetalBreath = eightChar.getFetalBreath();
const ownSign = eightChar.getOwnSign();
const bodySign = eightChar.getBodySign();
// Empty branches
const emptyTen = dayPillar.getTen();
const emptyBranches = emptyTen ? dayPillar.getExtraEarthBranches().map(b => b.getName()) : [];
// Child limit
const genderEnum = gender === 'male' ? Gender.MAN : Gender.WOMAN;
const childLimit = ChildLimit.fromSolarTime(solarTime, genderEnum);
const startDecade = childLimit.getStartDecadeFortune();
const startFortune = childLimit.getStartFortune();
// Decade fortunes (大运) — 10 decades
const decadeFortunes: DecadeFortuneInfo[] = [];
let currentDecade = startDecade;
for (let i = 0; i < 10 && currentDecade; i++) {
const sc = currentDecade.getSixtyCycle();
decadeFortunes.push({
index: i,
ganzhi: sc.getName(),
startAge: currentDecade.getStartAge(),
endAge: currentDecade.getEndAge(),
startYear: currentDecade.getStartLunarYear().getYear(),
endYear: currentDecade.getEndLunarYear().getYear(),
heavenStem: sc.getHeavenStem().getName(),
earthBranch: sc.getEarthBranch().getName(),
nayin: sc.getSound().getName(),
});
currentDecade = currentDecade.next(1) as typeof currentDecade;
}
// Annual fortunes (流年) — 10 years
const annualFortunes: FortuneInfo[] = [];
let currentFortune = startFortune;
for (let i = 0; i < 10 && currentFortune; i++) {
const sc = currentFortune.getSixtyCycle();
annualFortunes.push({
age: currentFortune.getAge(),
year: currentFortune.getLunarYear().getYear(),
ganzhi: sc.getName(),
nayin: sc.getSound().getName(),
});
currentFortune = currentFortune.next(1) as typeof currentFortune;
}
return {
eightChar: {
birthDate: `${year}-${String(month).padStart(2, '0')}-${String(day).padStart(2, '0')}`,
birthTime: `${String(hour).padStart(2, '0')}:${String(minute).padStart(2, '0')}`,
gender,
yearPillar: yearPillarInfo,
monthPillar: monthPillarInfo,
dayPillar: dayPillarInfo,
hourPillar: hourPillarInfo,
dayMaster: `${dayMasterStem}${dayMasterElement}`,
dayMasterStem,
dayMasterElement,
fetalOrigin: fetalOrigin.getName(),
fetalBreath: fetalBreath.getName(),
ownSign: ownSign.getName(),
bodySign: bodySign.getName(),
emptyBranches,
emptyTen: emptyTen ? emptyTen.getName() : '',
},
childLimit: {
startTime: childLimit.getStartTime().toString(),
endTime: childLimit.getEndTime().toString(),
yearCount: childLimit.getYearCount(),
monthCount: childLimit.getMonthCount(),
dayCount: childLimit.getDayCount(),
hourCount: childLimit.getHourCount(),
minuteCount: childLimit.getMinuteCount(),
forward: childLimit.isForward(),
startAge: childLimit.getStartAge(),
endAge: childLimit.getEndAge(),
},
decadeFortunes,
annualFortunes,
};
}
/** Extract pillar info from a SixtyCycle, with Ten Star relative to day master */
function extractPillarInfo(sixtyCycle: SixtyCycle, dayMasterStem: HeavenStem): PillarInfo {
const stem = sixtyCycle.getHeavenStem();
const branch = sixtyCycle.getEarthBranch();
const sound = sixtyCycle.getSound();
// Hidden stems
const hideStems: HideStemInfo[] = [];
try {
const allHideStems = branch.getHideHeavenStems();
if (allHideStems) {
for (const hs of allHideStems) {
let typeName = '';
const type = hs.getType();
if (type === HideHeavenStemType.MAIN) typeName = '本气';
else if (type === HideHeavenStemType.MIDDLE) typeName = '中气';
else if (type === HideHeavenStemType.RESIDUAL) typeName = '余气';
let tenStarName: string | null = null;
try {
tenStarName = hs.getHeavenStem().getTenStar(dayMasterStem).getName();
} catch { /* ten star may not be available */ }
hideStems.push({
stem: hs.getHeavenStem().getName(),
type: typeName,
tenStar: tenStarName,
});
}
}
} catch { /* hide stems may not be available */ }
// Terrain (十二长生)
let terrainName = '';
let terrainFortune: 'good' | 'bad' | 'neutral' = 'neutral';
try {
const terrain = stem.getTerrain(branch);
terrainName = terrain.getName();
const goodTerrain = ['长生', '冠带', '临官', '帝旺', '胎', '养'];
const badTerrain = ['死', '墓', '绝'];
if (goodTerrain.includes(terrainName)) terrainFortune = 'good';
else if (badTerrain.includes(terrainName)) terrainFortune = 'bad';
} catch { /* terrain might fail */ }
// Ten Star
let tenStarName: string | null = null;
try {
tenStarName = stem.getTenStar(dayMasterStem).getName();
} catch { /* ten star may not be available */ }
const yinYang = stem.getYinYang();
const branchYinYang = branch.getYinYang();
return {
ganzhi: sixtyCycle.getName(),
heavenStem: stem.getName(),
earthBranch: branch.getName(),
elementStem: stem.getElement().getName(),
elementBranch: branch.getElement().getName(),
yinYangStem: yinYang === YinYang.YANG ? 'yang' : 'yin',
yinYangBranch: branchYinYang === YinYang.YANG ? 'yang' : 'yin',
hideStems,
nayin: sound.getName(),
terrain: {
name: terrainName,
fortune: terrainFortune,
},
tenStar: tenStarName,
};
}
+201
View File
@@ -0,0 +1,201 @@
import {
SolarDay,
SolarMonth,
} from 'tyme4ts';
import type { DayInfo } from '../types/calendar';
import { getBuddhistFestival } from '../calculators/buddhistDates';
const SEASON_BY_MONTH: Record<number, string> = {
3: '春季', 4: '春季', 5: '春季',
6: '夏季', 7: '夏季', 8: '夏季',
9: '秋季', 10: '秋季', 11: '秋季',
12: '冬季', 1: '冬季', 2: '冬季',
};
/** Transform a SolarDay into a plain DayInfo object */
export function solarDayToDayInfo(solarDay: SolarDay): DayInfo {
const lunarDay = solarDay.getLunarDay();
const lunarMonth = lunarDay.getLunarMonth();
const week = solarDay.getWeek();
const constellation = solarDay.getConstellation();
const term = solarDay.getTerm();
const termDay = solarDay.getTermDay();
const month = solarDay.getSolarMonth().getMonth();
const solarYear = solarDay.getSolarMonth().getSolarYear().getYear();
// Solar term
const isTermDay = termDay !== null;
const solarTerm = isTermDay ? term.getName() : null;
let solarTermTime: string | null = null;
if (isTermDay) {
try {
const jd = term.getJulianDay();
if (jd) {
const st = jd.getSolarTime();
solarTermTime = `${String(st.getHour()).padStart(2,'0')}:${String(st.getMinute()).padStart(2,'0')}`;
}
} catch { /* time not available */ }
}
// Season + term progress (every day belongs to a solar term)
const season = SEASON_BY_MONTH[month] || '';
let currentSolarTerm: string | null = null;
let termDayIndex: number | null = null;
let nextSolarTerm: string | null = null;
let daysToNextTerm: number | null = null;
try {
const currentTerm = solarDay.getTerm();
const nextTerm = currentTerm.next(1);
currentSolarTerm = currentTerm.getName();
termDayIndex = solarDay.subtract(currentTerm.getSolarDay()) + 1;
nextSolarTerm = nextTerm.getName();
daysToNextTerm = nextTerm.getSolarDay().subtract(solarDay);
} catch { /* term may fail */ }
// Julian day, Buddhist era, Islamic/Hijri date
let julianDay: number | null = null;
try { julianDay = solarDay.getJulianDay().getDay(); } catch { /* */ }
const buddhistYear = solarYear + 543;
let hijriDate: string | null = null;
try {
const hd = solarDay.getHijriDay();
const hm = hd.getHijriMonth();
hijriDate = `${hm.getHijriYear().getYear()}${String(hm.getIndexInYear()).padStart(2, '0')}${String(hd.getDay()).padStart(2, '0')}`;
} catch { /* */ }
const buddhistFestival = getBuddhistFestival(lunarMonth.getMonth(), lunarDay.getDay());
// Phenology
let phenology: string | null = null;
try {
const pd = solarDay.getPhenologyDay();
if (pd) phenology = pd.getPhenology().getName();
} catch { /* not available for all dates */ }
// Dog days
let dogDay: string | null = null;
try {
const dd = solarDay.getDogDay();
if (dd) dogDay = dd.getDog().getName();
} catch { /* not in dog days */ }
// Nine-day cold
let nineDay: string | null = null;
try {
const nd = solarDay.getNineDay();
if (nd) nineDay = nd.getNine().getName();
} catch { /* not in nine days */ }
// Moon phase
let moonPhase: string | null = null;
try {
const phase = solarDay.getPhase();
if (phase) moonPhase = phase.getName();
} catch { /* not available */ }
// Festivals
let lunarFestival: string | null = null;
try {
const lf = lunarDay.getFestival();
if (lf) lunarFestival = lf.getName();
} catch { /* no festival */ }
let solarFestival: string | null = null;
try {
const sf = solarDay.getFestival();
if (sf) solarFestival = sf.getName();
} catch { /* no festival */ }
let legalHoliday: { name: string; isWork: boolean } | null = null;
try {
const lh = solarDay.getLegalHoliday();
if (lh) legalHoliday = { name: lh.getName(), isWork: lh.isWork() };
} catch { /* not a legal holiday */ }
// Gan-Zhi
const daySixtyCycle = lunarDay.getSixtyCycle();
const yearSixtyCycle = lunarDay.getYearSixtyCycle();
const monthSixtyCycle = lunarDay.getMonthSixtyCycle();
// Today check
const now = new Date();
const isToday =
solarDay.getSolarMonth().getSolarYear().getYear() === now.getFullYear() &&
solarDay.getSolarMonth().getMonth() === now.getMonth() + 1 &&
solarDay.getDay() === now.getDate();
const weekDayIndex = week.getIndex();
const isWeekend = weekDayIndex === 0 || weekDayIndex === 6;
return {
solarDate: `${solarDay.getSolarMonth().getSolarYear().getYear()}-${String(solarDay.getSolarMonth().getMonth()).padStart(2, '0')}-${String(solarDay.getDay()).padStart(2, '0')}`,
solarDay: solarDay.getDay(),
solarMonth: solarDay.getSolarMonth().getMonth(),
solarYear: solarDay.getSolarMonth().getSolarYear().getYear(),
weekDay: week.getName(),
weekDayIndex,
constellation: constellation.getName(),
solarTerm,
solarTermTime,
isTermDay,
currentSolarTerm,
season,
termDayIndex,
nextSolarTerm,
daysToNextTerm,
julianDay,
buddhistYear,
hijriDate,
buddhistFestival,
phenology,
dogDay,
nineDay,
lunarYear: lunarMonth.getLunarYear().getYear(),
lunarMonth: lunarMonth.getMonth(),
lunarMonthName: lunarMonth.getName(),
lunarDay: lunarDay.getDay(),
lunarDayName: lunarDay.getName(),
isLeapMonth: lunarMonth.isLeap(),
lunarYearGanzhi: yearSixtyCycle.getName(),
lunarMonthGanzhi: monthSixtyCycle.getName(),
lunarDayGanzhi: daySixtyCycle.getName(),
zodiac: daySixtyCycle.getEarthBranch().getZodiac().getName(),
lunarFestival,
solarFestival,
legalHoliday,
moonPhase,
isToday,
isWeekend,
dayOfWeek: weekDayIndex,
};
}
/** Get calendar days for a month as a 2D array (weeks × days) */
export function getMonthCalendar(year: number, month: number, weekStart: 0 | 1 = 0): DayInfo[][] {
const solarMonth = SolarMonth.fromYm(year, month);
const weekCount = solarMonth.getWeekCount(weekStart);
const weeks = solarMonth.getWeeks(weekStart);
const result: DayInfo[][] = [];
for (let w = 0; w < weekCount; w++) {
const week = weeks[w];
const days = week.getDays();
const row: DayInfo[] = [];
for (const day of days) {
row.push(solarDayToDayInfo(day));
}
result.push(row);
}
return result;
}
/** Get DayInfo for a specific date */
export function getDayInfo(year: number, month: number, day: number): DayInfo {
const solarDay = SolarDay.fromYmd(year, month, day);
return solarDayToDayInfo(solarDay);
}
/** Get today's DayInfo */
export function getTodayInfo(): DayInfo {
const now = new Date();
return getDayInfo(now.getFullYear(), now.getMonth() + 1, now.getDate());
}
@@ -0,0 +1,59 @@
/**
* 流月计算:按节气月(立春起 12 节)划分某公历年的 12 个流月
*/
import { SolarTerm } from 'tyme4ts';
import { getDayInfo } from './day';
export interface YearMonthInfo {
/** 0-11,从寅月(正月/立春)起 */
index: number;
/** 正月..腊月 */
name: string;
/** 起始公历月 */
solarMonth: number;
/** 节日期 YYYY-MM-DD */
startDate: string;
/** 月末 YYYY-MM-DD(下一节前一天) */
endDate: string;
/** 月柱干支 */
ganzhi: string;
}
// SolarTerm 索引:冬至0 小寒1 大寒2 立春3 雨水4 惊蛰5 春分6 清明7 谷雨8 立夏9 小满10 芒种11 夏至12 小暑13 大暑14 立秋15 处暑16 白露17 秋分18 寒露19 霜降20 立冬21 小雪22 大雪23
const JIE_INDICES = [3, 5, 7, 9, 11, 13, 15, 17, 19, 21, 23, 1]; // 立春..小寒(次年)
const MONTH_NAME_BY_BRANCH: Record<string, string> = {
'寅': '正月', '卯': '二月', '辰': '三月', '巳': '四月', '午': '五月', '未': '六月',
'申': '七月', '酉': '八月', '戌': '九月', '亥': '十月', '子': '冬月', '丑': '腊月',
};
function pad(n: number): string {
return String(n).padStart(2, '0');
}
function toDateStr(day: { getYear(): number; getMonth(): number; getDay(): number }): string {
return `${day.getYear()}-${pad(day.getMonth())}-${pad(day.getDay())}`;
}
/** Get the 12 节气月(流月)of a solar year, starting from 立春 */
export function getYearMonths(year: number): YearMonthInfo[] {
const months: YearMonthInfo[] = [];
for (let k = 0; k < 12; k++) {
const termYear = k === 11 ? year + 1 : year;
const start = SolarTerm.fromIndex(termYear, JIE_INDICES[k]).getSolarDay();
const end = k === 11
? start
: SolarTerm.fromIndex(year, JIE_INDICES[k + 1]).getSolarDay().next(-1);
const di = getDayInfo(start.getYear(), start.getMonth(), start.getDay());
const branch = di.lunarMonthGanzhi[1];
months.push({
index: k,
name: MONTH_NAME_BY_BRANCH[branch] || di.lunarMonthName || `${k + 1}`,
solarMonth: start.getMonth(),
startDate: toDateStr(start),
endDate: toDateStr(end),
ganzhi: di.lunarMonthGanzhi,
});
}
return months;
}
+89
View File
@@ -0,0 +1,89 @@
/** Full almanac (黄历) information for a single day */
export interface AlmanacInfo {
// Duty officer (建除十二值神)
duty: string;
dutyLuck: 'good' | 'bad' | 'neutral';
// Twelve star (黄道黑道十二神)
twelveStar: {
name: string;
ecliptic: string; // 黄道 or 黑道
luck: 'good' | 'bad';
};
// Twenty-eight lunar mansion (二十八星宿)
twentyEightStar: {
name: string;
luck: 'good' | 'bad';
animal: string;
};
// Nine star (九星)
nineStar: {
name: string;
color: string;
element: string;
};
// Six star (六曜)
sixStar: string;
// Minor Ren (小六壬)
minorRen: {
name: string;
luck: 'good' | 'bad';
element: string;
};
// Moon phase
phase: string;
// Fetus god (胎神)
fetus: {
direction: string;
side: string; // 房内/房外
position: string; // Full position description
};
// Recommendations and avoidances (宜忌)
recommends: string[];
avoids: string[];
// Gods (神煞)
goodGods: string[];
badGods: string[];
// Day stem-branch info
dayStem: string; // 天干 e.g. "甲"
dayBranch: string; // 地支 e.g. "子"
dayGanzhi: string; // 干支 e.g. "甲子"
// Peng Zu taboo (彭祖百忌)
pengZu: string;
pengZuStem: string; // 天干禁忌
pengZuBranch: string; // 地支禁忌
// Chong/Sha/Harm/Combine (冲煞害合)
clash: string; // 冲 e.g. "马(午)"
harm: string; // 害 e.g. "羊(未)"
combine: string; // 合 e.g. "牛(丑)"
evilDirection: string;// 煞 e.g. "北"
// Na Yin sound (纳音)
nayin: string;
// Hourly almanac
hourDetails: HourAlmanac[];
}
/** Hourly almanac for each of the 12 two-hour periods (时辰) */
export interface HourAlmanac {
branch: string; // 地支 e.g. "子"
name: string; // 时辰名 e.g. "子时"
range: string; // Time range e.g. "23:00-01:00"
ganzhi: string; // Hour pillar e.g. "甲子"
recommends: string[];
avoids: string[];
twelveStar: string;
nineStar: string;
}
+93
View File
@@ -0,0 +1,93 @@
/** A single pillar (柱) in the Bazi — year, month, day, or hour */
export interface PillarInfo {
ganzhi: string; // "甲子"
heavenStem: string; // "甲"
earthBranch: string; // "子"
elementStem: string; // Stem's five element "木"
elementBranch: string; // Branch's five element "水"
yinYangStem: 'yin' | 'yang';
yinYangBranch: 'yin' | 'yang';
hideStems: HideStemInfo[];
nayin: string; // Na Yin sound "海中金"
terrain: {
name: string; // 十二长生 stage
fortune: 'good' | 'bad' | 'neutral';
};
tenStar: string | null; // Ten Star relative to day master
}
/** Hidden stem within an earth branch (藏干) */
export interface HideStemInfo {
stem: string; // "甲"
type: string; // "本气" | "中气" | "余气"
tenStar: string | null; // Ten Star relative to day master
}
/** Complete Bazi (八字) result for a birth date/time */
export interface EightCharInfo {
birthDate: string; // ISO date
birthTime: string; // HH:mm
gender: 'male' | 'female';
yearPillar: PillarInfo;
monthPillar: PillarInfo;
dayPillar: PillarInfo; // Day master pillar
hourPillar: PillarInfo;
// Derived
dayMaster: string; // "甲木" — day stem + element
dayMasterStem: string; // "甲"
dayMasterElement: string; // "木"
fetalOrigin: string; // 胎元
fetalBreath: string; // 胎息
ownSign: string; // 命宫
bodySign: string; // 身宫
emptyBranches: string[]; // 空亡 branches
emptyTen: string; // 旬
}
/** Decade fortune (大运) */
export interface DecadeFortuneInfo {
index: number;
ganzhi: string;
startAge: number;
endAge: number;
startYear: number;
endYear: number;
heavenStem: string;
earthBranch: string;
nayin: string;
}
/** Annual fortune (流年/小运) */
export interface FortuneInfo {
age: number;
year: number;
ganzhi: string;
nayin: string;
}
/** Child limit (起运) information */
export interface ChildLimitInfo {
startTime: string; // ISO datetime
endTime: string;
yearCount: number;
monthCount: number;
dayCount: number;
hourCount: number;
minuteCount: number;
forward: boolean; // 顺排/逆排
startAge: number;
endAge: number;
}
/** Full Bazi analysis result */
export interface BaziFullResult {
eightChar: EightCharInfo;
childLimit: ChildLimitInfo;
decadeFortunes: DecadeFortuneInfo[];
annualFortunes: FortuneInfo[];
/** True when the caller applied solar-time longitude correction to the birth time */
solarAdjusted?: boolean;
}
+76
View File
@@ -0,0 +1,76 @@
/** Core calendar day information — framework-agnostic plain object */
export interface DayInfo {
/** ISO date string YYYY-MM-DD */
solarDate: string;
solarDay: number;
solarMonth: number;
solarYear: number;
/** Chinese weekday name: 日,一,二,三,四,五,六 */
weekDay: string;
/** 0=Sunday ... 6=Saturday */
weekDayIndex: number;
/** Western zodiac constellation name */
constellation: string;
/** Solar term name if this day is a term day */
solarTerm: string | null;
/** Exact solar term time (HH:mm) */
solarTermTime: string | null;
/** Whether this day is the exact solar term transition day */
isTermDay: boolean;
/** The solar term this day belongs to (e.g. 大暑) */
currentSolarTerm: string | null;
/** Season name: 春季/夏季/秋季/冬季 */
season: string;
/** 1-based day index within the current solar term (节气第几天) */
termDayIndex: number | null;
/** Next solar term name */
nextSolarTerm: string | null;
/** Days until the next solar term */
daysToNextTerm: number | null;
/** Julian Day number (儒略日) */
julianDay: number | null;
/** Buddhist Era year (佛历年 = 公历年 + 543) */
buddhistYear: number | null;
/** Islamic/Hijri date as "1448年02月18日" */
hijriDate: string | null;
/** Buddhist festival name (农历) if applicable */
buddhistFestival: string | null;
/** 72 phenology name if applicable */
phenology: string | null;
/** Three periods (三伏) if applicable */
dogDay: string | null;
/** Nine-day cold period (数九) if applicable */
nineDay: string | null;
// Lunar calendar
lunarYear: number;
lunarMonth: number;
/** Chinese lunar month name e.g. "五月" */
lunarMonthName: string;
lunarDay: number;
/** Chinese lunar day name e.g. "初十", "廿一" */
lunarDayName: string;
isLeapMonth: boolean;
/** Gan-Zhi of the lunar year e.g. "丙午" */
lunarYearGanzhi: string;
/** Gan-Zhi of the lunar month */
lunarMonthGanzhi: string;
/** Gan-Zhi of the lunar day */
lunarDayGanzhi: string;
/** Chinese zodiac animal */
zodiac: string;
// Festivals & holidays
lunarFestival: string | null;
solarFestival: string | null;
legalHoliday: { name: string; isWork: boolean } | null;
// Moon phase
moonPhase: string | null;
// Metadata
isToday: boolean;
isWeekend: boolean;
/** Day-of-week position within the month grid (0-based column) */
dayOfWeek: number;
}
+66
View File
@@ -0,0 +1,66 @@
/** Relationship between a single pillar in user's Bazi and day's Bazi */
export interface PillarRelationship {
pillar: 'year' | 'month' | 'day' | 'hour';
pillarLabel: string; // "年柱", "月柱", "日柱", "时柱"
userGanzhi: string;
dayGanzhi: string;
// Heaven stem relationships
stemTenStar: string; // Ten Star: day stem vs user stem
stemCombine: boolean; // 天干合
stemOpposite: boolean; // 天干冲
// Earth branch relationships
branchCombine: boolean; // 六合
branchThreeCombine: boolean; // 三合
branchOpposite: boolean; // 六冲
branchHarm: boolean; // 六害
branchPunish: boolean; // 相刑
branchFormation: string | null;// 三合局名
// Score contribution
score: number; // -10 to +10
}
/** Complete daily fortune result for a user on a specific day */
export interface DailyFortuneResult {
date: string; // ISO date
lunarDate: string; // Lunar date description
dayGanzhi: string; // Day's Gan-Zhi
/** Overall score from -100 to +100 */
overallScore: number;
/** Score level classification */
scoreLevel: 'great' | 'good' | 'fair' | 'poor' | 'bad';
/** Pillar-by-pillar relationship analysis */
pillarRelationships: PillarRelationship[];
/** Positive aspects of the day */
luckyAspects: string[];
/** Negative aspects / warnings */
unluckyAspects: string[];
/** Actionable suggestions */
suggestions: string[];
/** Primary affected life area */
affectedAreas: string[];
/** Lucky meta info for the day (personalized) */
luckyMeta: {
colors: string[];
numbers: number[];
direction: string;
element: string;
activity: string;
};
/** Category scores (-100 to +100) */
categoryScores: {
love: number;
career: number;
wealth: number;
health: number;
};
}
+15
View File
@@ -0,0 +1,15 @@
export type { DayInfo } from './calendar';
export type { AlmanacInfo, HourAlmanac } from './almanac';
export type {
PillarInfo,
HideStemInfo,
EightCharInfo,
DecadeFortuneInfo,
FortuneInfo,
ChildLimitInfo,
BaziFullResult,
} from './bazi';
export type {
PillarRelationship,
DailyFortuneResult,
} from './fortune';
+9
View File
@@ -0,0 +1,9 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"outDir": "./dist",
"rootDir": "./src",
"lib": ["ES2022"]
},
"include": ["src"]
}
+11
View File
@@ -0,0 +1,11 @@
import { defineConfig } from 'tsup';
export default defineConfig({
entry: ['src/index.ts'],
format: ['esm', 'cjs'],
dts: true,
clean: true,
sourcemap: true,
splitting: false,
treeshake: true,
});