Files
Jyotisha/jyotish-app/jyotish-advanced.js
T

674 lines
26 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* Jyotish Advanced Calculations v1.0
* 高级计算模块: Karaka, Arudha, Varga, Ashtakavarga, Shadbala, Tithi, Combustion, Dasha-3级
*/
import {
SIGNS, SIGNS_CN, SIGN_LORDS, PLANET_CN, NAKSHATRA_LIST, DASHA_ORDER, DASHA_YEARS,
EXALTATION, DEBILITATION, PLANET_RELATIONS
} from './jyotish-engine.js';
// ========== 常量 ==========
export const COMBUSTION_RANGE = { Moon:12, Mars:17, Mercury:14, Jupiter:11, Venus:10, Saturn:15 };
export const SHADBALA_REQUIRED = { Sun:5, Moon:6, Mars:5, Mercury:7, Jupiter:6.5, Venus:5.5, Saturn:5 };
export const VARGA_DEFS = [
{id:'D1',name:'Rasi',cn:'本命盘',d:1},{id:'D2',name:'Hora',cn:'财富盘',d:2},
{id:'D3',name:'Drekkana',cn:'兄弟盘',d:3},{id:'D4',name:'Chaturthamsa',cn:'房产盘',d:4},
{id:'D7',name:'Saptamsa',cn:'子女盘',d:7},{id:'D9',name:'Navamsa',cn:'九分盘',d:9},
{id:'D10',name:'Dasamsa',cn:'事业盘',d:10},{id:'D12',name:'Dwadasamsa',cn:'父母盘',d:12},
{id:'D16',name:'Shodasamsa',cn:'车辆盘',d:16},{id:'D20',name:'Vimsamsa',cn:'灵性盘',d:20},
{id:'D24',name:'Chaturvimsamsa',cn:'教育盘',d:24},{id:'D30',name:'Trimsamsa',cn:'厄运盘',d:30},
{id:'D40',name:'Khavedamsa',cn:'吉凶盘',d:40},{id:'D45',name:'Akshavedamsa',cn:'总体盘',d:45},
{id:'D60',name:'Shastyamsa',cn:'业力盘',d:60},
];
export const TITHI_CN = [
'初一(Pratipat)','初二(Dvitiya)','初三(Tritiya)','初四(Chaturthi)','初五(Panchami)',
'初六(Shashthi)','初七(Saptami)','初八(Ashtami)','初九(Navami)','初十(Dashami)',
'十一(Ekadashi)','十二(Dvadashi)','十三(Trayodashi)','十四(Chaturdashi)','满月/新月'
];
export const SUN_MOON_YOGAS = [
'Vishkambha','Priti','Ayushman','Saubhagya','Shobhana','Atiganda','Sukarma','Dhriti',
'Shula','Ganda','Vriddhi','Dhruva','Vyaghata','Harshana','Vajra','Siddhi','Vyatipata',
'Variyan','Parigha','Shiva','Siddha','Sadhya','Shubha','Shukla','Brahma','Indra','Vaidhriti'
];
// ========== Combustion ==========
export function computeCombustion(planets) {
const sunDeg = planets.Sun?.degree;
if (!sunDeg) return;
for (const pn of ['Moon','Mars','Mercury','Jupiter','Venus','Saturn']) {
const p = planets[pn]; if (!p || p.error) continue;
const dist = Math.min(Math.abs(p.degree-sunDeg), 360-Math.abs(p.degree-sunDeg));
if (dist < (COMBUSTION_RANGE[pn]||15)) { p.combust=true; p.combust_degree=Math.round(dist*100)/100; }
}
}
// ========== Karaka ==========
export function computeKaraka(planets) {
const seven = ['Sun','Moon','Mars','Mercury','Jupiter','Venus','Saturn'];
// Fix: sort by degree_in_sign (0-30°), NOT absolute longitude (0-360°)
const sorted = seven.filter(p=>planets[p]&&!planets[p].error).map(p=>({name:p,degree:planets[p].degree_in_sign})).sort((a,b)=>b.degree-a.degree);
const L7=['AK','AmK','BK','MK','PK','GK','DK'];
const CN7={AK:'Atmakaraka 自我灵魂',AmK:'Amatyakaraka 事业顾问',BK:'Bhratrikaraka 兄弟姐妹',MK:'Matrikaraka 母亲',PK:'Putrakaraka 子女',GK:'Gnatikaraka 障碍冲突',DK:'Darakaraka 配偶'};
const k7={};
for(let i=0;i<Math.min(sorted.length,7);i++){
k7[L7[i]]={planet:sorted[i].name,planet_cn:PLANET_CN[sorted[i].name],degree:sorted[i].degree,label_cn:CN7[L7[i]],sign:planets[sorted[i].name]?.sign,sign_cn:planets[sorted[i].name]?.sign_cn,house:planets[sorted[i].name]?.house};
}
// 8-Karaka — Rahu uses reverse degree_in_sign: 30 - degree_in_sign
const rahuK=planets.Rahu&&!planets.Rahu.error?[{name:'Rahu',degree:(30-planets.Rahu.degree_in_sign)%30}]:[];
const s8=seven.filter(p=>planets[p]&&!planets[p].error).map(p=>({name:p,degree:planets[p].degree_in_sign})).concat(rahuK).sort((a,b)=>b.degree-a.degree);
const L8=['AK','AmK','BK','MK','PK','GK','DK','PiK'];
const CN8={...CN7,PiK:'Pitrukaraka 父亲'};
const k8={};
for(let i=0;i<Math.min(s8.length,8);i++){
k8[L8[i]]={planet:s8[i].name,planet_cn:PLANET_CN[s8[i].name],degree:s8[i].degree,label_cn:CN8[L8[i]],sign:planets[s8[i].name]?.sign,sign_cn:planets[s8[i].name]?.sign_cn,house:planets[s8[i].name]?.house};
}
return {karaka7:k7,karaka8:k8};
}
// ========== Arudha ==========
export function computeArudha(planets, ascSign) {
const ai=SIGNS.indexOf(ascSign); const padas={};
function calc(h){
const si=(ai+h-1)%12; const s=SIGNS[si]; const lord=SIGN_LORDS[s];
const lp=planets[lord]; if(!lp||lp.error)return null;
const li=SIGNS.indexOf(lp.sign); let cnt=((li-si+12)%12)+1;
if(cnt===h)cnt=((cnt+9)%12)+1; else if(cnt===((h+6-1)%12+1))cnt=((cnt+9)%12)+1;
const pi=(si+cnt-1)%12;
return{house:h,sign:SIGNS[pi],sign_cn:SIGNS_CN[SIGNS[pi]],pada_house:((pi-ai+12)%12)+1,lord,lord_cn:PLANET_CN[lord]};
}
const al=calc(1); if(al){al.label='Arudha Lagna 社会形象'; padas.AL=al;}
for(let h=2;h<=12;h++){const p=calc(h);if(p)padas[`A${h}`]=p;}
if(padas.A12)padas.UL={...padas.A12,label:'Upapada Lagna 配偶征象'};
return padas;
}
// ========== Varga ==========
// BPHS 分盘映射:星座索引 si 的第 pi 份 → 目标星座索引
// 奇数星座(si%2==0): Aries,Gemini,Leo,Libra,Sagittarius,Aquarius
// 偶数星座(si%2==1): Taurus,Cancer,Virgo,Scorpio,Capricorn,Pisces
function _vargaMap(si, pi, d) {
const o = si % 2 === 0; // 奇数星座: Aries(0),Gemini(2)...
switch(d) {
case 2: return pi === 0 ? (o ? 4 : 3) : (o ? 3 : 4);
case 3: return o ? (si + pi * 4) % 12 : (si + 8 + pi * 4) % 12;
case 4: return o ? (si + pi) % 12 : (si + 8 + pi) % 12;
case 7: return o ? (si + pi) % 12 : (si + 6 + pi) % 12;
case 9: { const el = [0,9,6,3]; return (el[si % 4] + pi) % 12; }
case 10: return o ? (si + pi) % 12 : (si + 8 + pi) % 12;
case 12: return (si + pi) % 12;
case 16: return ((o ? 0 : 1) + pi) % 12;
case 20: return ((o ? 0 : 8) + pi) % 12;
case 24: return ((o ? 4 : 3) + pi) % 12;
case 27: return ((o ? 0 : 6) + pi) % 12;
case 30: {
if (o) { return pi<5?0:pi<10?10:pi<18?8:pi<25?2:6; }
else { return pi<5?1:pi<12?5:pi<20?9:pi<25?7:11; }
}
case 40: return ((o ? 0 : 6) + pi) % 12;
case 45: return ((o ? 0 : 6) + pi) % 12;
case 60: return o ? (si + pi) % 12 : (si + 1 + pi) % 12;
default: return (si + pi) % 12;
}
}
export function computeVarga(planets, vargaId) {
const def=VARGA_DEFS.find(v=>v.id===vargaId); if(!def)return null;
const{id,name,cn,d}=def;
if(d===1){const r={};for(const[pn,pi]of Object.entries(planets)){if(pi.error)continue;r[pn]={sign:pi.sign,sign_cn:pi.sign_cn,house:pi.house};}return{id,name,name_cn:cn,planets:r};}
const varga={};
for(const[pn,pi]of Object.entries(planets)){
if(pi.error)continue;
const deg=pi.degree_in_sign; const si=SIGNS.indexOf(pi.sign);
const sw=30/d; const seg=Math.floor(deg/sw);
const vi=_vargaMap(si, seg, d);
varga[pn]={sign:SIGNS[vi],sign_cn:SIGNS_CN[SIGNS[vi]],house:pi.house};
}
return{id,name,name_cn:cn,planets:varga};
}
export function computeAllVargas(planets){const r={};for(const v of VARGA_DEFS)r[v.id]=computeVarga(planets,v.id);return r;}
// ========== Ashtakavarga (BPHS Standard v2.0) ==========
// BAV_TABLE[planet][source] = favorable house numbers counted from source's sign
// SAV total = 337 (cosmic constant): 48+49+39+54+56+52+39
const BAV_TABLE = {
Sun: {
Sun:[1,2,4,7,8,9,10,11], Moon:[3,6,10,11], Mars:[1,2,4,7,8,9,10,11],
Mercury:[3,5,6,9,10,11,12], Jupiter:[5,6,9,11], Venus:[6,7,12],
Saturn:[1,2,4,7,8,9,10,11], Lagna:[3,4,6,10,11,12]
},
Moon: {
Sun:[3,6,7,8,10,11], Moon:[1,3,6,7,10,11], Mars:[2,3,5,6,9,10,11],
Mercury:[1,3,4,5,7,8,10,11], Jupiter:[1,4,7,8,10,11],
Venus:[3,4,5,7,9,10,11], Saturn:[3,5,6,11], Lagna:[3,6,10,11,12]
},
Mars: {
Sun:[3,5,6,10,11], Moon:[3,6,11], Mars:[1,2,4,7,8,10,11],
Mercury:[3,5,6,11], Jupiter:[6,10,11,12], Venus:[6,8,11,12],
Saturn:[1,4,7,8,9,10,11], Lagna:[1,3,6,10,11]
},
Mercury: {
Sun:[5,6,9,11,12], Moon:[2,4,6,8,10,11], Mars:[1,2,4,7,8,9,10,11],
Mercury:[1,3,5,6,9,10,11,12], Jupiter:[6,8,11,12],
Venus:[1,2,3,4,5,8,9,11], Saturn:[1,2,4,7,8,9,10,11], Lagna:[1,2,4,6,8,10,11]
},
Jupiter: {
Sun:[1,2,3,4,7,8,9,10,11], Moon:[2,5,7,9,11], Mars:[1,2,4,7,8,10,11],
Mercury:[1,2,4,5,6,9,10,11], Jupiter:[1,2,3,4,7,8,10,11],
Venus:[2,5,6,9,10,11], Saturn:[3,5,6,12], Lagna:[1,2,4,5,6,7,9,10,11]
},
Venus: {
Sun:[8,11,12], Moon:[1,2,3,4,5,8,9,11,12], Mars:[3,5,6,9,11,12],
Mercury:[3,5,6,9,11,12], Jupiter:[5,8,9,10,11],
Venus:[1,2,3,4,5,8,9,10,11], Saturn:[3,4,5,8,9,10,11], Lagna:[1,2,3,4,5,8,9]
},
Saturn: {
Sun:[1,2,4,7,8,10,11], Moon:[3,6,11], Mars:[3,5,6,10,11,12],
Mercury:[6,8,9,10,11,12], Jupiter:[5,6,11,12], Venus:[6,11,12],
Saturn:[3,5,6,11], Lagna:[1,3,4,6,10,11]
},
Lagna: {
Sun:[3,4,6,10,11,12], Moon:[3,6,10,11,12], Mars:[1,3,6,10,11],
Mercury:[1,2,4,6,8,10,11], Jupiter:[1,2,4,5,6,7,9,10,11],
Venus:[1,2,3,4,5,8,9], Saturn:[1,3,4,6,10,11], Lagna:[3,6,10,11]
}
};
export function computeAshtakavarga(planets, ascSign) {
const ai = SIGNS.indexOf(ascSign);
const pOrder = ['Sun','Moon','Mars','Mercury','Jupiter','Venus','Saturn'];
const allSrc = [...pOrder, 'Lagna'];
// Sign index for each source (7 planets + Lagna)
const srcSI = {};
for (const pn of pOrder) {
if (planets[pn] && !planets[pn].error) srcSI[pn] = SIGNS.indexOf(planets[pn].sign);
}
srcSI.Lagna = ai;
// Compute BAV for each of 8 bodies
const bav = {};
const sav = new Array(12).fill(0);
for (const planet of allSrc) {
const rules = BAV_TABLE[planet];
if (!rules) continue;
const scores = new Array(12).fill(0);
for (const source of allSrc) {
if (!(source in srcSI)) continue;
const favHouses = rules[source] || [];
const si = srcSI[source];
for (const h of favHouses) {
scores[(si + h - 1) % 12] += 1;
}
}
bav[planet] = scores;
// Only 7 planets contribute to SAV (Lagna BAV shown separately)
if (planet !== 'Lagna') {
for (let i = 0; i < 12; i++) sav[i] += scores[i];
}
}
// Map to houses
const savH = {}, bavH = {};
for (let h = 1; h <= 12; h++) {
const si = (ai + h - 1) % 12;
savH[h] = { sign: SIGNS[si], score: sav[si] };
bavH[h] = {};
for (const pn of pOrder) bavH[h][pn] = bav[pn]?.[si] || 0;
}
return {
sav_total: sav.reduce((a, b) => a + b, 0),
sav, sav_by_house: savH, bav_by_house: bavH,
bav_raw: bav,
lagna_bav: bav.Lagna || new Array(12).fill(0)
};
}
// ========== Shadbala (Parashara 6-dimension) ==========
// 60 Virupas = 1 Rupa; All internal calculations in Virupas
const EXALT_DEG = { Sun:10, Moon:33, Mars:298, Mercury:165, Jupiter:95, Venus:357, Saturn:200 };
const DEBIL_DEG = {};
for (const [p,d] of Object.entries(EXALT_DEG)) DEBIL_DEG[p] = (d+180)%360;
const DIG_BEST = { Sun:10, Mars:10, Moon:4, Venus:4, Jupiter:1, Mercury:1, Saturn:7 };
const NAIS_V = { Sun:60, Moon:60, Venus:52.5, Jupiter:45, Mercury:37.5, Mars:30, Saturn:22.5 };
const MIN_RUPA = { Sun:5, Moon:6, Mars:5, Mercury:7, Jupiter:6.5, Venus:5.5, Saturn:5 };
const DIURNAL_S = ['Sun','Jupiter','Venus'];
const NOCTURNAL_S = ['Moon','Mars','Saturn'];
const BENEFS = ['Jupiter','Venus','Mercury'];
const MALEFS = ['Saturn','Mars','Sun'];
const SPEC_ASP = { Mars:[4,8], Jupiter:[5,9], Saturn:[3,10] };
// Saptavargaja dignity score (compound relationship, BPHS)
// Own=30, Great Friend=22.5, Friend=15, Equal=7.5, Enemy=3.75, Bitter Enemy=1.875
function _saptaScore(pn, sign) {
const lord = SIGN_LORDS[sign];
if (lord === pn) return 30; // Own sign
const rel = PLANET_RELATIONS[pn];
if (!rel) return 7.5; // Neutral
const isFriend = rel.friends?.includes(lord);
const isEnemy = rel.enemies?.includes(lord);
const lordRel = PLANET_RELATIONS[lord];
const lordIsFriend = lordRel?.friends?.includes(pn);
const lordIsEnemy = lordRel?.enemies?.includes(pn);
if (isFriend && lordIsFriend) return 22.5; // Great Friend
if (isFriend) return 15; // Friend
if (!isFriend && !isEnemy) return 7.5; // Equal/Neutral
if (isEnemy && lordIsEnemy) return 1.875; // Bitter Enemy
return 3.75; // Enemy
}
function _sthanaBala(pn, lon, sign, house, allV) {
// A. Uchcha Bala (0-60 Virupas)
const debDeg = DEBIL_DEG[pn] || 0;
let offset = (lon - debDeg + 360) % 360;
if (offset > 180) offset = 360 - offset;
const ucha = offset / 180 * 60;
// B. Saptavargaja Bala (7 vargas: D1/D2/D3/D7/D9/D12/D30)
// Sum dignity scores from 7 vargas, max = 7*30 = 210 Shashtiamsas
const SAPTA_IDS = ['D1','D2','D3','D7','D9','D12','D30'];
let sapta = 0;
for (const vid of SAPTA_IDS) {
const vSign = vid === 'D1' ? sign : (allV?.[vid]?.planets?.[pn]?.sign || sign);
sapta += _saptaScore(pn, vSign);
}
// C. Ojayugma Bala (15 Virupas for sign + 15 for Navamsa = max 30)
let oja = 0;
// Sign-based: benefics in even signs, malefics in odd signs
const signIdx = SIGNS.indexOf(sign);
const signOdd = signIdx % 2 === 0; // Aries=0(odd), Taurus=1(even), ...
if (pn === 'Mercury' || pn === 'Venus') { if (!signOdd) oja += 15; }
else { if (signOdd) oja += 15; }
// Navamsa-based: same rule for D9 sign
const d9Sign = allV?.D9?.planets?.[pn]?.sign;
if (d9Sign) {
const d9Odd = SIGNS.indexOf(d9Sign) % 2 === 0;
if (pn === 'Mercury' || pn === 'Venus') { if (!d9Odd) oja += 15; }
else { if (d9Odd) oja += 15; }
}
// D. Kendra Bala (BPHS: Kendra=60, Panaphara=30, Apoklima=15)
let kendra;
if ([1,4,7,10].includes(house)) kendra = 60;
else if ([2,5,8,11].includes(house)) kendra = 30;
else kendra = 15;
// E. Drekkana Bala (15 Virupas)
const degInSign = lon % 30;
let drekkana = 0;
if (['Sun','Mars','Jupiter'].includes(pn)) drekkana = degInSign < 10 ? 15 : 0;
else if (['Moon','Venus'].includes(pn)) drekkana = (degInSign >= 10 && degInSign < 20) ? 15 : 0;
else drekkana = degInSign >= 20 ? 15 : 0;
return { ucha: Math.round(ucha*100)/100, sapta: Math.round(sapta*100)/100, oja, kendra, drekkana,
total: Math.round((ucha+sapta+oja+kendra+drekkana)*100)/100 };
}
function _digBala(pn, house) {
const best = DIG_BEST[pn] || 1;
let diff = Math.abs(house - best);
if (diff > 6) diff = 12 - diff;
return Math.max(0, (6 - diff) * 10);
}
function _kalaBala(pn, isNight, sunNorth, sunLon, moonLon) {
// A. Nathonnata Bala (60 or 0)
let nathon = 0;
if (pn === 'Mercury') nathon = 60;
else if (isNight && NOCTURNAL_S.includes(pn)) nathon = 60;
else if (!isNight && DIURNAL_S.includes(pn)) nathon = 60;
// B. Paksha Bala (0-30)
const moonSunDiff = (moonLon - sunLon + 360) % 360;
let paksha = 0;
if (['Jupiter','Venus','Moon'].includes(pn)) {
paksha = moonSunDiff / 180 * 30;
} else {
paksha = moonSunDiff <= 180
? (180 - moonSunDiff) / 180 * 30
: (moonSunDiff - 180) / 180 * 30;
}
// C. Tribhaga Bala (45 or 0)
const tribhaga = ['Jupiter','Venus','Saturn'].includes(pn) ? 45 : 0;
// D. Ayana Bala (30 or 15)
let ayana = 15;
if (pn === 'Mercury') ayana = 30;
else if (sunNorth && ['Sun','Mars','Moon'].includes(pn)) ayana = 30;
else if (!sunNorth && ['Jupiter','Venus','Saturn'].includes(pn)) ayana = 30;
const total = nathon + paksha + tribhaga + ayana;
return { nathon, paksha: Math.round(paksha*100)/100, tribhaga, ayana, total: Math.round(total*100)/100 };
}
function _chestaBala(pn, retro, speed, sunLon, moonLon) {
if (pn === 'Sun') return 60;
if (pn === 'Moon') {
const diff = (moonLon - sunLon + 360) % 360;
return diff / 180 * 60;
}
if (retro) return 60;
const absSpd = Math.abs(speed);
if (absSpd > 1.0) return 50;
if (absSpd > 0.5) return 35;
if (absSpd > 0.1) return 20;
return 10;
}
function _drikBala(pn, planets) {
let drik = 0;
const mySI = SIGNS.indexOf(planets[pn]?.sign || 'Aries');
if (mySI < 0) return 0;
for (const [on, od] of Object.entries(planets)) {
if (on === pn || on === 'Rahu' || on === 'Ketu' || od.error) continue;
const oSI = SIGNS.indexOf(od.sign);
if (oSI < 0) continue;
// House difference from other to me (1=conjunction, 7=opposition)
const hDiff = (mySI - oSI + 12) % 12 + 1;
let hasAspect = hDiff === 7 || hDiff === 1;
if (SPEC_ASP[on]) {
if (SPEC_ASP[on].includes(hDiff)) hasAspect = true;
}
if (hasAspect) {
let val = hDiff === 1 ? 30 : 15;
if (BENEFS.includes(on)) drik += val;
else if (MALEFS.includes(on)) drik -= val;
else drik += val * 0.5;
}
}
return Math.max(-60, Math.min(60, drik));
}
export function computeShadbala(planets, ascSign, birth, allVargas) {
// Parse birth hour from birth.time string (e.g. "14:45")
const hour = birth.time ? parseInt(birth.time.split(':')[0]) : (birth.hour || 12);
const isNight = hour < 6 || hour >= 18;
const sunLon = planets.Sun?.degree || 0;
const moonLon = planets.Moon?.degree || 0;
const sunNorth = sunLon >= 270 || sunLon < 90;
const r = {};
for (const pn of ['Sun','Moon','Mars','Mercury','Jupiter','Venus','Saturn']) {
const p = planets[pn]; if (!p || p.error) continue;
const sthana = _sthanaBala(pn, p.degree, p.sign, p.house, allVargas);
const dig = _digBala(pn, p.house);
const kala = _kalaBala(pn, isNight, sunNorth, sunLon, moonLon);
const chesta = _chestaBala(pn, p.retrograde, p.speed || 1, sunLon, moonLon);
const nais = NAIS_V[pn] || 30;
const drik = _drikBala(pn, planets);
const totalV = sthana.total + dig + kala.total + chesta + nais + drik;
const totalR = totalV / 60;
const req = MIN_RUPA[pn] || 5;
const pct = Math.round(totalR / req * 100);
r[pn] = {
planet_cn: PLANET_CN[pn],
total: Math.round(totalR * 100) / 100,
required: req,
percentage: pct,
status: pct >= 150 ? '极强' : pct >= 125 ? '强' : pct >= 100 ? '充足' : pct >= 75 ? '略弱' : pct >= 50 ? '弱' : '极弱',
details: {
sthana: Math.round(sthana.total / 60 * 100) / 100,
dig: Math.round(dig / 60 * 100) / 100,
kala: Math.round(kala.total / 60 * 100) / 100,
chesta: Math.round(chesta / 60 * 100) / 100,
naisargika: Math.round(nais / 60 * 100) / 100,
drik: Math.round(drik / 60 * 100) / 100
},
raw_virupas: { sthana, dig, kala, chesta, nais, drik, totalV }
};
}
return r;
}
// ========== Tithi / Yoga / Karana ==========
export function computeTithiYoga(planets, birthInfo) {
const s=planets.Sun?.degree,m=planets.Moon?.degree;
if(s==null||m==null)return null;
let diff=((m-s)%360+360)%360;
const tithi=Math.floor(diff/12)+1;
const tInP=((tithi-1)%15)+1;
const isShukla=tithi<=15;
let sum=(m+s)%360;
const yoga=Math.floor(sum/(800/60))+1;
// Karana(半月相)
const KARANAS = [
'Bava','Balava','Kaulava','Taitila','Gara','Vanija','Vishti',
'Shakuni','Chatushpada','Naga','Kimstughna'
];
const karanaIdx = ((tithi - 1) * 2) % 11;
const karana = KARANAS[karanaIdx];
// Vara(星期几)
let varDay = null;
const DAYS = ['周日(Ravi)','周一(Soma)','周二(Mangala)','周三(Budha)','周四(Guru)','周五(Shukra)','周六(Shani)'];
if (birthInfo && birthInfo.date) {
const d = new Date(birthInfo.date);
varDay = DAYS[d.getDay()];
}
return{
tithi:{number:tithi,name:TITHI_CN[tInP-1]||`Tithi ${tithi}`,paksha:isShukla?'Shukla Paksha 亮半月':'Krishna Paksha 暗半月',isShukla},
yoga:{number:yoga,name:SUN_MOON_YOGAS[(yoga-1)%27]||`Yoga ${yoga}`},
karana: { name: karana },
vara: varDay,
};
}
// ========== Dasha 三级 ==========
// Vimshottari Dasha 使用热带年(tropical year= 365.24219 天
const TROPICAL_YEAR = 365.24219;
// Julian Day 日期转换(标准天文公式,避免 JS Date 浮点精度损失)
function _dateToJD(y, m, d) {
if (m <= 2) { y--; m += 12; }
const A = Math.floor(y / 100);
const B = 2 - A + Math.floor(A / 4);
return Math.floor(365.25 * (y + 4716)) + Math.floor(30.6001 * (m + 1)) + d + B - 1524.5;
}
function _jdToDateStr(jd) {
let Z = Math.floor(jd + 0.5), F = jd + 0.5 - Z, A;
if (Z < 2299161) A = Z;
else { const a = Math.floor((Z - 1867216.25) / 36524.25); A = Z + 1 + a - Math.floor(a / 4); }
const B = A + 1524, C = Math.floor((B - 122.1) / 365.25), D = Math.floor(365.25 * C), E = Math.floor((B - D) / 30.6001);
const day = Math.floor(B - D - Math.floor(30.6001 * E) + F);
const month = E < 14 ? E - 1 : E - 13;
const year = month > 2 ? C - 4716 : C - 4715;
return `${year}-${String(month).padStart(2,'0')}-${String(day).padStart(2,'0')}`;
}
export function computeDashaWithPratyantar(moonLon, birthDate, referenceDate) {
const nakSpan=360/27, ni=Math.floor(moonLon/nakSpan), prog=(moonLon%nakSpan)/nakSpan;
const nak=NAKSHATRA_LIST[ni%27];
const [by,bm,bd]=birthDate.split('-').map(Number);
const birthJD=_dateToJD(by,bm,bd);
const elapsed=prog*nak.years;
const startJD=birthJD-elapsed*TROPICAL_YEAR;
const sIdx=DASHA_ORDER.indexOf(nak.lord);
const timeline=[];let curJD=startJD;
for(let i=0;i<9;i++){
const lord=DASHA_ORDER[(sIdx+i)%9],yrs=DASHA_YEARS[lord];
const endJD=curJD+yrs*TROPICAL_YEAR;
timeline.push({lord,lord_cn:PLANET_CN[lord],start:_jdToDateStr(curJD),end:_jdToDateStr(endJD),years:yrs,antardasha:null});
curJD=endJD;
}
// reference date → JD
let refJD;
if(referenceDate){const[ry,rm,rd]=referenceDate.split('-').map(Number);refJD=_dateToJD(ry,rm,rd);}
else{const t=new Date();refJD=_dateToJD(t.getFullYear(),t.getMonth()+1,t.getDate());}
let current=null;
for(const d of timeline){
const dsJ=_dateToJD(...d.start.split('-').map(Number)),deJ=_dateToJD(...d.end.split('-').map(Number));
if(dsJ<=refJD&&refJD<deJ){
const tDays=deJ-dsJ;const li=DASHA_ORDER.indexOf(d.lord);
const sub=[];let sJD=dsJ;
for(let j=0;j<9;j++){
const sl=DASHA_ORDER[(li+j)%9],sd=tDays*DASHA_YEARS[sl]/120;
const seJD=sJD+sd;
const isCur=sJD<=refJD&&refJD<seJD;
// Pratyantardasha
const praty=[];let pJD=sJD;
const pDays=seJD-sJD;
for(let k=0;k<9;k++){
const pl=DASHA_ORDER[(DASHA_ORDER.indexOf(sl)+k)%9];
const pDur=pDays*DASHA_YEARS[pl]/120;
const peJD=pJD+pDur;
praty.push({lord:pl,lord_cn:PLANET_CN[pl],start:_jdToDateStr(pJD),end:_jdToDateStr(peJD),is_current:pJD<=refJD&&refJD<peJD});
pJD=peJD;
}
sub.push({lord:sl,lord_cn:PLANET_CN[sl],start:_jdToDateStr(sJD),end:_jdToDateStr(seJD),is_current:isCur,pratyantardasha:praty});
sJD=seJD;
}
d.antardasha=sub;d.pratyantardasha_available=true;current=d;break;
}
}
return{moon_nakshatra:nak.name,birth_date:birthDate,reference_date:_jdToDateStr(refJD),timeline,current_dasha:current};
}
// ========== Nakshatra Advanced ==========
// Gana & Element per Nakshatra
const NAK_GANA = [
'Dev','Rakshasa','Rakshasa','Manushya','Dev','Manushya','Dev','Dev','Rakshasa',
'Rakshasa','Manushya','Manushya','Dev','Rakshasa','Rakshasa','Manushya','Dev',
'Rakshasa','Rakshasa','Manushya','Dev','Dev','Rakshasa','Rakshasa','Manushya',
'Manushya','Dev'
];
const NAK_ELEMENT = [
'火','土','火','土','土','风','风','水','水','火','火','土','土','风','风',
'水','水','火','火','土','土','风','风','水','水','火','火'
];
export function computeNakshatraAdvanced(planets, birthNakIdx) {
const nakSpan = 360 / 27;
const results = {};
const planetNames = ['Sun','Moon','Mars','Mercury','Jupiter','Venus','Saturn','Rahu','Ketu'];
for (const pn of planetNames) {
const p = planets[pn];
if (!p || p.error || p.degree == null) continue;
const lon = p.degree;
const nakIdx = Math.floor(lon / nakSpan) % 27;
const degInNak = lon - nakIdx * nakSpan;
const pada = Math.floor(degInNak / (nakSpan / 4)) + 1;
const nak = NAKSHATRA_LIST[nakIdx];
// Tara Bala: distance from birth nakshatra
const taraDist = ((nakIdx - birthNakIdx) % 27 + 27) % 27 % 9;
const taraNames = ['Janma','Sampat','Vipat','Kshema','Pratyak','Sadhana','Vadha','Mitra','Atma Mitra'];
// Sub-lord: each nakshatra divided into 9 sub-sections (Vimshottari order)
const subSpan = nakSpan / 9;
const subIdx = Math.floor(degInNak / subSpan) % 9;
const subLord = DASHA_ORDER[subIdx];
results[pn] = {
nakshatra: nak.name,
nakshatra_idx: nakIdx,
nakshatra_lord: nak.lord,
dasha_years: nak.years,
pada,
degree_in_nakshatra: Math.round(degInNak * 1e4) / 1e4,
degree_in_pada: Math.round((degInNak - (pada - 1) * nakSpan / 4) * 1e4) / 1e4,
gana: NAK_GANA[nakIdx],
element: NAK_ELEMENT[nakIdx],
tara_bala: { tara: taraNames[taraDist], index: taraDist },
sub_lord: subLord,
sub_index: subIdx,
};
}
return { planets: results };
}
// ========== Chara Dasha ==========
export function computeCharaDasha(planets, ascSign) {
const SEVEN = ['Sun','Moon','Mars','Mercury','Jupiter','Venus','Saturn'];
const ascIdx = SIGNS.indexOf(ascSign);
// Determine direction: signs 0-5 (Aries-Virgo) = forward, 6-11 (Libra-Pisces) = backward
const forward = ascIdx < 6;
// Build sequence starting from ascendant
const sequence = [];
for (let i = 0; i < 12; i++) {
const signIdx = forward ? (ascIdx + i) % 12 : (ascIdx - i + 12) % 12;
const sign = SIGNS[signIdx];
const lord = SIGN_LORDS[sign];
// Find karaka planet with lowest degree in that lord's sign
// Duration = (360 - planet's navamsa progression) * 120 / 360 simplified
// Standard: 10 years per sign, adjusted by planet position
let years = 10; // default
if (lord && planets[lord] && !planets[lord].error) {
// Duration based on degree in sign: (30 - deg_in_sign) / 30 * 10 + some base
years = Math.round(((30 - (planets[lord].degree_in_sign || 0)) / 30 * 9 + 1) * 10) / 10;
}
sequence.push({ sign, sign_idx: signIdx, lord, years, order: i + 1 });
}
const totalYears = sequence.reduce((s, d) => s + d.years, 0);
return {
ascendant: ascSign,
direction: forward ? 'forward' : 'backward',
dasha_sequence: sequence,
total_years: Math.round(totalYears * 10) / 10,
};
}
// ========== Karakamsha ==========
export function computeKarakamsha(planets) {
// Karakamsha = Navamsa sign of Atmakaraka
const karaka = computeKaraka(planets);
const ak = karaka.karaka7?.AK;
if (!ak) return null;
// Get Navamsa sign of AK planet
const akPlanet = planets[ak.planet];
if (!akPlanet || akPlanet.error) return null;
const lon = akPlanet.degree;
const navamsaSpan = 30 / 9;
const navamsaIdx = Math.floor(akPlanet.degree_in_sign / navamsaSpan);
const signIdx = (SIGNS.indexOf(akPlanet.sign) * 9 + navamsaIdx) % 12;
const karakamshaSign = SIGNS[signIdx];
return {
atmakaraka: ak.planet,
atmakaraka_degree: ak.degree,
karakamsha_sign: karakamshaSign,
karakamsha_lord: SIGN_LORDS[karakamshaSign],
};
}