/** * ASCII Chart Parser * Programmatically decodes South Indian ASCII grids into structured planetary data. */ // Grid Coordinate to Sign (4x4 matrix) const GRID_TO_SIGN: (string | null)[][] = [ ["PISCES", "ARIES", "TAURUS", "GEMINI"], ["AQUARIUS", null, null, "CANCER"], ["CAPRICORN", null, null, "LEO"], ["SAGITTARIUS", "SCORPIO", "LIBRA", "VIRGO"] ]; const SIGN_NAME_TO_ID: Record = { "ARIES": "1", "TAURUS": "2", "GEMINI": "3", "CANCER": "4", "LEO": "5", "VIRGO": "6", "LIBRA": "7", "SCORPIO": "8", "SAGITTARIUS": "9", "CAPRICORN": "10", "AQUARIUS": "11", "PISCES": "12" }; import { getDignityLabel } from './dignityRules'; function getDignity(planet: string, sign: string): string { const dignity = getDignityLabel(planet, sign); return dignity; } export function getPlacements(text: string): { placements: Record, chartTitle: string } | string { if (!text) return "Error: No text provided"; const trimmed = text.trim(); if (trimmed.startsWith('|') || trimmed.includes('| House | Sign |')) { return getPlacementsFromMarkdownTable(trimmed); } return getPlacementsFromASCII(text); } /** * Normalizes chart types like "Navamsa (D9)", "Navamsha", or "D-9" to a plain "D9" format. */ export function normalizeVargaType(type: string): string { const t = type.toUpperCase().trim(); // 1. Core mapping for Sanskrit/Common Names const vargaMap: Record = { 'RASI': 'D1', 'RASHI': 'D1', 'HORA': 'D2', 'DREKKANA': 'D3', 'CHATURTHAMSA': 'D4', 'CATURTHAMSA': 'D4', 'SAPTAMSA': 'D7', 'NAVAMSA': 'D9', 'NAVAMSHA': 'D9', 'DASAMSA': 'D10', 'DASHAMSHA': 'D10', 'DWADASAMSA': 'D12', 'DVADASAMSHA': 'D12', 'SHODASAMSA': 'D16', 'VIMSAMSA': 'D20', 'SIDDHAMSA': 'D24', 'BHAMSA': 'D27', 'TRIMSAMSA': 'D30', 'KHAVEDAMSA': 'D40', 'AKSHAVEDAMSA': 'D45', 'SHASHTIAMSA': 'D60', 'SHASHTYAMSHA': 'D60' }; // Check if it's a known Sanskrit name (handles partial matches like "NAVAMSA (D9)") for (const [k, v] of Object.entries(vargaMap)) { if (t.includes(k)) return v; } // 2. Standard D-Number mapping (e.g. D9, D-9, D 9) const match = t.match(/D[- ]?(\d+)/); if (match) return `D${match[1]}`; return t; // Default to original if no match } export function getPlacementsFromMarkdownTable(md: string): { placements: Record, chartTitle: string } | string { const lines = md.trim().split('\n'); const placements: Record = {}; let chartTitle = "Stored Chart"; lines.forEach(line => { const trimmedLine = line.trim(); if (!trimmedLine.startsWith('|')) return; if (trimmedLine.toLowerCase().includes('house') && trimmedLine.toLowerCase().includes('sign')) return; if (trimmedLine.includes(':---')) return; const parts = trimmedLine.split('|').map(p => p.trim()).filter(Boolean); if (parts.length >= 3) { // parts[1] is definitely the Sign (ARIES, TAURUS...) const sign = parts[1].toUpperCase(); const planetRaw = parts[2]; // e.g. "Su (Ex), Ma (MT), **As (Lagna)**" if (!placements[sign]) placements[sign] = []; // Extract tokens logic const tokens = planetRaw.split(',').map(t => { let cleaned = t.trim().replace(/\*\*/g, ''); // If it specifically contains "As" or "Lagna", normalize to "As" if (cleaned.toLowerCase().includes('as') || cleaned.toLowerCase().includes('lagna')) { // Keep specific degree info if present e.g. "As(Cp)" const degreeMatch = cleaned.match(/As\([A-Za-z]+\)/i); if (degreeMatch) return degreeMatch[0]; return "As"; // Standard code for Lagna } // Standard code extraction (Su, Mo, Ma...) const match = cleaned.match(/^([A-Za-z0-9()]+)/); return match ? match[1] : cleaned; }).filter(Boolean); placements[sign].push(...tokens); } }); return { placements, chartTitle }; } export function getPlacementsFromASCII(ascii: string): { placements: Record, chartTitle: string } | string { const cleanedAscii = ascii.split('\n').map(line => line.trimEnd()).join('\n'); const rawLines = cleanedAscii.split('\n'); const dashBoundaries: number[] = []; rawLines.forEach((line, idx) => { if (line.includes('---')) dashBoundaries.push(idx); }); if (dashBoundaries.length < 5) return `Error: Could not identify rows in ASCII.`; const rowTexts = [ rawLines.slice(dashBoundaries[0] + 1, dashBoundaries[1]), rawLines.slice(dashBoundaries[1] + 1, dashBoundaries[2]), rawLines.slice(dashBoundaries[2] + 1, dashBoundaries[3]), rawLines.slice(dashBoundaries[3] + 1, dashBoundaries[4]) ]; const placements: Record = {}; let middleTextLines: string[] = []; rowTexts.forEach((rowLines, rowIdx) => { rowLines.forEach(line => { if (!line.includes('|')) return; const pipePos: number[] = []; for (let i = 0; i < line.length; i++) if (line[i] === '|') pipePos.push(i); const firstPipe = pipePos[0]; const lastPipe = pipePos[pipePos.length - 1]; const totalWidth = lastPipe - firstPipe; for (let pIdx = 0; pIdx < pipePos.length - 1; pIdx++) { const start = pipePos[pIdx], end = pipePos[pIdx + 1]; const cellCenter = (start + end) / 2; const relativePos = (cellCenter - firstPipe) / totalWidth; const colIdx = Math.floor(relativePos * 4); const sign = GRID_TO_SIGN[rowIdx][colIdx]; if (!sign) { const content = line.substring(start + 1, end).trim(); if (content) middleTextLines.push(content); continue; } const cellContent = line.substring(start + 1, end).trim(); if (!cellContent) continue; const tokens = cellContent.split(/\s+/).filter(Boolean); const codes = ["Su", "Mo", "Ma", "Me", "Ju", "Ve", "Sa", "Ra", "Ke", "As", "HL", "GL", "AL", "Md", "Gk"]; tokens.forEach(token => { if (token.toLowerCase().includes('as') || token.toLowerCase().includes('lagna')) { if (!placements[sign]) placements[sign] = []; if (!placements[sign].includes("As")) placements[sign].push("As"); return; } const foundCode = codes.find(code => token.toUpperCase().startsWith(code.toUpperCase())); if (foundCode) { if (!placements[sign]) placements[sign] = []; if (!placements[sign].includes(foundCode)) placements[sign].push(foundCode); } }); } }); }); const chartTitle = [...new Set(middleTextLines)].join(' ').replace(/\s+/g, ' ').trim(); return { placements, chartTitle }; } export function parseJHoraGrid(ascii: string): string { // If it's already a markdown table, return it as is if (ascii.trim().startsWith('|')) { return ascii.trim(); } const result = getPlacementsFromASCII(ascii); if (typeof result === 'string') { return result; } const { placements } = result; // 4. Generate MANDATORY TECHNICAL TABLE const zodOrder = ["ARIES", "TAURUS", "GEMINI", "CANCER", "LEO", "VIRGO", "LIBRA", "SCORPIO", "SAGITTARIUS", "CAPRICORN", "AQUARIUS", "PISCES"]; // Find Lagna Sign let ascSign = "Unknown"; zodOrder.forEach(sign => { if (placements[sign] && placements[sign].includes("As")) { ascSign = sign; } }); const getHouseNumber = (sign: string) => { if (ascSign === "Unknown") return 0; const lagnaIdx = zodOrder.indexOf(ascSign); const signIdx = zodOrder.indexOf(sign); return ((signIdx - lagnaIdx + 12) % 12) + 1; }; let summary = "| House | Sign | Planets (Dignity) |\n| :--- | :--- | :--- |\n"; zodOrder.forEach(sign => { const houseNum = getHouseNumber(sign); const planets = placements[sign] || []; const planetDetails = planets.map(p => { const dignity = getDignityLabel(p, sign); const shortDignity = dignity .replace("EXALTED", "Ex") .replace("DEBILITATED", "Deb") .replace("MOOLATRIKONA", "MT") .replace("OWN SIGN", "Own") .replace("-", "-"); return p === "As" ? "**As (Lagna)**" : `${p} (${shortDignity})`; }); summary += `| H${houseNum || '?'} | ${sign} | ${planetDetails.join(", ")} |\n`; }); summary += `\n**Lagna**: **${ascSign}**\n`; return summary; }/** * Parses SAV (Sarvastakavarga) points from a table-like text structure. */ export function parseSAVTable(text: string): Record { const scores: Record = {}; const lines = text.split('\n'); lines.forEach(line => { // Look for SIGN NAME followed by a number // e.g. | ARIES | 27 | // e.g. ARIES: 27 const tokens = line.trim().toUpperCase().split(/[\s|:]+/).filter(Boolean); tokens.forEach((token, idx) => { if (SIGN_NAME_TO_ID[token]) { const nextToken = tokens[idx + 1]; if (nextToken && !isNaN(parseInt(nextToken))) { scores[token] = parseInt(nextToken); } } }); }); return scores; }