fix(algorithm): 修复紫微斗数核心算法错误并补全紫微星安星法

全网交叉验证发现三处算法错误(信源:uranaischool.com、pvlib/NOAA、feiyuntiegui.com等):

1. 四化飞星映射修复:替换错误的化禄/化权映射(原来10项仅4项正确),
   新增化科/化忌映射,采用主星派标准四化表

2. 紫微星安星法补全:实现完整公式(五行局+农历日),
   新增五虎遁命宫天干、纳音五行速算、紫微系/天府系14主星正确排列

3. 真太阳时公式勘误:修正Spencer(1971)两个已知系数错误
   (0.000075→0.0000075, 0.04089→0.040849)

新增 docs/ALGORITHM_KNOWHOW.md 永久性算法知识库
This commit is contained in:
2026-08-12 21:56:12 +08:00
parent 34e9c3721d
commit 64a1ce7bcd
4 changed files with 737 additions and 59 deletions
@@ -97,8 +97,13 @@ function getDayOfYear(date: Date): number {
* 计算时差方程(分钟)
*
* 基于 Spencer (1971) 傅里叶级数近似:
* EoT = 229.2 × (0.000075 + 0.001868×cos(γ) - 0.032077×sin(γ)
* - 0.014615×cos(2γ) - 0.04089×sin(2γ))
* EoT = 229.2 × (0.0000075 + 0.001868×cos(γ) - 0.032077×sin(γ)
* - 0.014615×cos(2γ) - 0.040849×sin(2γ))
*
* 注意:原始 Spencer (1971) 论文有两个已知勘误:
* 1. 常数项 0.000075 → 应为 0.0000075Spencer 本人确认)
* 2. sin(2γ) 系数 0.04089 → 应为 0.040849Bird Clear Sky 模型值)
* 参考:pvlib-python 官方文档 equation_of_time_spencer71
*
* 其中 γ = 2π/365 × (N - 1 + (t - 12) / 24)N 为一年中的第几天
*
@@ -106,12 +111,16 @@ function getDayOfYear(date: Date): number {
*/
function calculateEquationOfTime(fractionalYear: number): number {
const coeff = 229.2
// 修正值说明:Spencer (1971) 原始论文有两个已知勘误
// 1. 常数项: 0.000075 → 0.0000075Spencer 本人确认的排版错误)
// 2. sin(2γ) 系数: 0.04089 → 0.040849Bird Clear Sky 模型值)
// 参考: pvlib-python equation_of_time_spencer71
const eot = coeff * (
0.000075 +
0.0000075 +
0.001868 * Math.cos(fractionalYear) -
0.032077 * Math.sin(fractionalYear) -
0.014615 * Math.cos(2 * fractionalYear) -
0.04089 * Math.sin(2 * fractionalYear)
0.040849 * Math.sin(2 * fractionalYear)
)
return eot
}
@@ -3,6 +3,7 @@ import { HeavenlyStem, EarthlyBranch, PalaceType, MajorStar, StarNature } from '
import { calculatePalaceScore } from '../algorithms/ziweiAlgorithm'
import { calculateSanFangSiZheng } from '../algorithms/sanFangSiZheng'
import { calculateTrueSolarTime } from '../algorithms/solarTime'
import { solarToLunar } from '../utils/lunar'
import { LRUCache } from '../utils/lruCache'
import { storage, STORAGE_KEYS } from '../utils/storage'
@@ -57,26 +58,6 @@ const MONTH_BRANCHES: EarthlyBranch[] = [
EarthlyBranch.CHOU,
]
const MAJOR_STAR_SEQUENCE: MajorStar[] = [
MajorStar.ZIWEI,
MajorStar.TIANJI,
MajorStar.TAIYANG,
MajorStar.WUQU,
MajorStar.TIANTONG,
MajorStar.LIANCHEN,
]
const MAJOR_STAR_SEQUENCE2: MajorStar[] = [
MajorStar.TIANFU,
MajorStar.TAIYIN,
MajorStar.TANLANG,
MajorStar.JUMEN,
MajorStar.TIANXIANG,
MajorStar.TIANLIANG,
MajorStar.QISHA,
MajorStar.POJUN,
]
export class ZiweiService {
private cache = new LRUCache<ZiweiChart>({ maxSize: 50 })
@@ -110,6 +91,9 @@ export class ZiweiService {
trueSolarTime = `${datePart}T${solarResult.trueSolarTime}`
}
// 计算农历日期(用于紫微星安星法)
const lunarDate = solarToLunar(birthDate)
return {
birthTime: params.birthTime,
yearStem,
@@ -126,6 +110,7 @@ export class ZiweiService {
latitude: params.latitude,
birthPlace: params.birthPlace,
trueSolarTime,
lunarDate,
}
}
@@ -152,13 +137,64 @@ export class ZiweiService {
const mingGongIndex = EarthlyBranch.index(mingGongBranch)
const mingBranchArrayIndex = branches.indexOf(mingGongBranch)
// 计算紫微星位置(完整安星法)
const ziweiBranchIndex = this.calculateZiweiBranchIndex(birthInfo, mingGongBranch)
const tianfuBranchIndex = 13 - ziweiBranchIndex // 天府在紫微对宫
// 计算紫微系星曜位置(逆时针排列)
const ziwiXiPositions: Record<number, MajorStar> = {}
ziwiXiPositions[ziweiBranchIndex] = MajorStar.ZIWEI
// 天机隔一宫(逆时针2位)
const tianjiPos = ((ziweiBranchIndex - 2 + 12) % 12) || 12
ziwiXiPositions[tianjiPos] = MajorStar.TIANJI
// 太阳再隔一宫(逆时针4位)
const taiyangPos = ((ziweiBranchIndex - 4 + 12) % 12) || 12
ziwiXiPositions[taiyangPos] = MajorStar.TAIYANG
// 武曲(逆时针5位)
const wuquPos = ((ziweiBranchIndex - 5 + 12) % 12) || 12
ziwiXiPositions[wuquPos] = MajorStar.WUQU
// 天同(逆时针6位)
const tiantongPos = ((ziweiBranchIndex - 6 + 12) % 12) || 12
ziwiXiPositions[tiantongPos] = MajorStar.TIANTONG
// 廉贞空二宫(逆时针8位)
const lianchenPos = ((ziweiBranchIndex - 8 + 12) % 12) || 12
ziwiXiPositions[lianchenPos] = MajorStar.LIANCHEN
// 计算天府系星曜位置(顺时针排列)
const tianfuXiPositions: Record<number, MajorStar> = {}
tianfuXiPositions[tianfuBranchIndex] = MajorStar.TIANFU
// 天府系顺时针:太阴(+1)、贪狼(+2)、巨门(+3)、天相(+4)、天梁(+5)、七杀(+6)、破军空三宫(+8)
const tianfuSequence = [
MajorStar.TAIYIN, MajorStar.TANLANG, MajorStar.JUMEN,
MajorStar.TIANXIANG, MajorStar.TIANLIANG, MajorStar.QISHA,
]
for (let i = 0; i < tianfuSequence.length; i++) {
const pos = ((tianfuBranchIndex + (i + 1) - 1) % 12) + 1
tianfuXiPositions[pos] = tianfuSequence[i]
}
// 破军空三宫(顺时针+8
const pojunPos = ((tianfuBranchIndex + 8 - 1) % 12) + 1
tianfuXiPositions[pojunPos] = MajorStar.POJUN
const palaces: Palace[] = []
for (let i = 0; i < 12; i++) {
const branchArrayIndex = (mingBranchArrayIndex + i) % 12
const earthlyBranch = branches[branchArrayIndex]
const palaceType = palaceTypes[i] ?? PalaceType.MING
const branchIndex = EarthlyBranch.index(earthlyBranch)
const majorStars = this.assignStarsToPalace(i, mingGongIndex, birthInfo)
// 收集当前宫位的星曜
const majorStars: StarInfo[] = []
const yearStemIndex = HeavenlyStem.index(birthInfo.yearStem)
// 紫微系星曜
if (ziwiXiPositions[branchIndex]) {
majorStars.push(this.createStarInfo(ziwiXiPositions[branchIndex], yearStemIndex))
}
// 天府系星曜
if (tianfuXiPositions[branchIndex]) {
majorStars.push(this.createStarInfo(tianfuXiPositions[branchIndex], yearStemIndex))
}
const palace: Palace = {
palaceType,
@@ -176,29 +212,98 @@ export class ZiweiService {
return palaces
}
private assignStarsToPalace(palaceOffset: number, mingGongIndex: number, birthInfo: BirthInfo): StarInfo[] {
const stars: StarInfo[] = []
const yearStemIndex = HeavenlyStem.index(birthInfo.yearStem)
if (palaceOffset === 0) {
stars.push(this.createStarInfo(MajorStar.ZIWEI, yearStemIndex))
} else if (palaceOffset <= 5) {
stars.push(this.createStarInfo(MAJOR_STAR_SEQUENCE[palaceOffset], yearStemIndex))
/**
* 计算紫微星所在的地支索引(1-12)
*
* 紫微星安星法(完整公式):
* 公式:(农历日 + x) / 五行局 = y (整除)
* - x=0(整除) → 紫微宫 = 寅宫(3) + y - 1
* - x为偶数 → 紫微宫 = 寅宫(3) + y
* - x为奇数 → 紫微宫 = 寅宫(3) - y
*
* 信源:feiyuntiegui.com 安星诀、zhanbugua.com 手工排盘教程
*/
private calculateZiweiBranchIndex(birthInfo: BirthInfo, mingGongBranch: EarthlyBranch): number {
// 1. 获取农历日
let lunarDay = 15 // 默认值
if (birthInfo.lunarDate?.lunarDay) {
lunarDay = parseInt(birthInfo.lunarDate.lunarDay, 10) || 15
}
const fuOffset = (12 - (mingGongIndex - 1)) % 12
if (palaceOffset === fuOffset) {
stars.push(this.createStarInfo(MajorStar.TIANFU, yearStemIndex))
// 2. 计算五行局
const mingGongBranchIndex = EarthlyBranch.index(mingGongBranch)
const mingGongStemIndex = this.calculateMingGongStem(
HeavenlyStem.index(birthInfo.yearStem),
mingGongBranchIndex,
)
const fiveBureau = this.calculateFiveBureau(mingGongStemIndex, mingGongBranchIndex)
// 3. 紫微星安星公式
let x = 0
while ((lunarDay + x) % fiveBureau !== 0) {
x++
}
const y = (lunarDay + x) / fiveBureau
let ziweiIndex: number
if (x === 0) {
// 整除:寅宫(3) + y - 1
ziweiIndex = 3 + y - 1
} else if (x % 2 === 0) {
// x为偶数:寅宫(3) + y
ziweiIndex = 3 + y
} else {
// x为奇数:寅宫(3) - y
ziweiIndex = 3 - y
}
for (let i = 0; i < MAJOR_STAR_SEQUENCE2.length; i++) {
const targetOffset = (fuOffset + i) % 12
if (palaceOffset === targetOffset && palaceOffset !== fuOffset) {
stars.push(this.createStarInfo(MAJOR_STAR_SEQUENCE2[i], yearStemIndex))
}
}
// 调整到1-12范围
while (ziweiIndex <= 0) ziweiIndex += 12
while (ziweiIndex > 12) ziweiIndex -= 12
return stars
return ziweiIndex
}
/**
* 计算命宫天干索引(五虎遁)
*
* 五虎遁口诀:
* 甲己之年丙作首,乙庚之岁戊为头,
* 丙辛之年寻庚上,丁壬壬寅顺水流,
* 若问戊癸何处起,甲寅之上好追求。
*/
private calculateMingGongStem(yearStemIndex: number, mingGongBranchIndex: number): number {
// 寅宫天干 = ((yearStemIndex - 1) * 2 + 3) % 10
let yinStem = ((yearStemIndex - 1) * 2 + 3) % 10
if (yinStem === 0) yinStem = 10
// 命宫天干 = (寅宫天干 + (mingGongBranchIndex - 3)) % 10
let mingStem = (yinStem + (mingGongBranchIndex - 3)) % 10
if (mingStem <= 0) mingStem += 10
return mingStem
}
/**
* 计算五行局数
*
* 纳音五行 → 五行局映射:
* 木(1)→木三局(3), 金(2)→金四局(4), 水(3)→水二局(2),
* 火(4)→火六局(6), 土(5)→土五局(5)
*/
private calculateFiveBureau(stemIndex: number, branchIndex: number): number {
// 纳音五行速算:天干取数甲乙=1,丙丁=2,戊己=3,庚辛=4,壬癸=5
const stemNum = Math.ceil(stemIndex / 2)
// 地支取数:子丑午未=1, 寅卯申酉=2, 辰巳戌亥=3
const branchNumMap: Record<number, number> = {
1: 1, 2: 1, 3: 2, 4: 2, 5: 3, 6: 3,
7: 1, 8: 1, 9: 2, 10: 2, 11: 3, 12: 3,
}
const branchNum = branchNumMap[branchIndex] ?? 1
// 纳音五行 = (天干数 + 地支数) % 5 → 1木2金3水4火5土
let nayinElement = (stemNum + branchNum) % 5
if (nayinElement === 0) nayinElement = 5
// 五行局映射:1木→3, 2金→4, 3水→2, 4火→6, 5土→5
const bureauMap: Record<number, number> = { 1: 3, 2: 4, 3: 2, 4: 6, 5: 5 }
return bureauMap[nayinElement] ?? 5
}
private createStarInfo(star: MajorStar, yearStemIndex: number): StarInfo {
@@ -211,22 +316,42 @@ export class ZiweiService {
}
}
private getTransformation(star: MajorStar, yearStemIndex: number): StarInfo['transformation'] {
const luMap: Record<number, MajorStar> = {
1: MajorStar.POJUN, 2: MajorStar.TIANJI, 3: MajorStar.TAIYANG,
4: MajorStar.WUQU, 5: MajorStar.LIANCHEN, 6: MajorStar.TIANFU,
7: MajorStar.TAIYANG, 8: MajorStar.JUMEN, 9: MajorStar.TIANLIANG,
10: MajorStar.ZIWEI,
}
const quanMap: Record<number, MajorStar> = {
1: MajorStar.TIANLIANG, 2: MajorStar.TIANLIANG, 3: MajorStar.WUQU,
4: MajorStar.ZIWEI, 5: MajorStar.POJUN, 6: MajorStar.TIANXIANG,
7: MajorStar.TIANJI, 8: MajorStar.TANLANG, 9: MajorStar.QISHA,
10: MajorStar.LIANCHEN,
}
/**
* 获取四化飞星(主星派标准表)
*
* 信源:uranaischool.com 自力開運クラブ教材、starnum.com.tw、shen88.cn
* 口诀:甲廉破武阳,乙机梁紫阴,丙同机昌廉,丁阴同机巨,
* 戊贪阴阳机,己武贪梁曲,庚阳武阴同,辛巨阳曲昌,
* 壬梁紫府武,癸破巨阴贪。
*
* 年干索引:1=甲, 2=乙, 3=丙, 4=丁, 5=戊, 6=己, 7=庚, 8=辛, 9=壬, 10=癸
*/
private static readonly TRANSFORMATION_TABLE: Record<number, {
lu: MajorStar
quan: MajorStar
ke: MajorStar
ji: MajorStar
}> = {
1: { lu: MajorStar.LIANCHEN, quan: MajorStar.POJUN, ke: MajorStar.WUQU, ji: MajorStar.TAIYANG },
2: { lu: MajorStar.TIANJI, quan: MajorStar.TIANLIANG, ke: MajorStar.ZIWEI, ji: MajorStar.TAIYIN },
3: { lu: MajorStar.TIANTONG, quan: MajorStar.TIANJI, ke: MajorStar.WENCHANG, ji: MajorStar.LIANCHEN },
4: { lu: MajorStar.TAIYIN, quan: MajorStar.TIANTONG, ke: MajorStar.TIANJI, ji: MajorStar.JUMEN },
5: { lu: MajorStar.TANLANG, quan: MajorStar.TAIYIN, ke: MajorStar.TAIYANG, ji: MajorStar.TIANJI },
6: { lu: MajorStar.WUQU, quan: MajorStar.TANLANG, ke: MajorStar.TIANLIANG, ji: MajorStar.WENQU },
7: { lu: MajorStar.TAIYANG, quan: MajorStar.WUQU, ke: MajorStar.TAIYIN, ji: MajorStar.TIANTONG },
8: { lu: MajorStar.JUMEN, quan: MajorStar.TAIYANG, ke: MajorStar.WENQU, ji: MajorStar.WENCHANG },
9: { lu: MajorStar.TIANLIANG, quan: MajorStar.ZIWEI, ke: MajorStar.TIANFU, ji: MajorStar.WUQU },
10: { lu: MajorStar.POJUN, quan: MajorStar.JUMEN, ke: MajorStar.TAIYIN, ji: MajorStar.TANLANG },
}
if (luMap[yearStemIndex] === star) return 'LU' as any
if (quanMap[yearStemIndex] === star) return 'QUAN' as any
private getTransformation(star: MajorStar, yearStemIndex: number): StarInfo['transformation'] {
const entry = ZiweiService.TRANSFORMATION_TABLE[yearStemIndex]
if (!entry) return null
if (entry.lu === star) return 'LU' as any
if (entry.quan === star) return 'QUAN' as any
if (entry.ke === star) return 'KE' as any
if (entry.ji === star) return 'JI' as any
return null
}