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:
@@ -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.0000075(Spencer 本人确认)
|
||||
* 2. sin(2γ) 系数 0.04089 → 应为 0.040849(Bird 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.0000075(Spencer 本人确认的排版错误)
|
||||
// 2. sin(2γ) 系数: 0.04089 → 0.040849(Bird 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
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user