A birth-chart form looks simple: ask for a date, time, and place, then calculate. The interface may be simple. The input is not. 1992-11-01 01:30 does not identify one universal instant. It is a wall-clock reading that only becomes meaningful after you resolve the place, the historical time-zone rule, and any daylight-saving transition. If the time is unknown, inventing a convenient default can create chart features that were never supported by the user’s data. I ran into these problems while building AstroZen, a Next.js application that calculates a BaZi Four Pillars chart and a Western natal chart before generating an optional interpretation. The most important architectural decision was this: Calculation is an evidence pipeline. Interpretation is a separate layer. This post explains the calculation pipeline, the failure modes I had to remove, and why “unknown” must remain unknown. 1. A city name is not a coordinate Early prototypes often use a short city list or a default coordinate. That works for a layout demo, but it is not acceptable once location affects the result. Names are ambiguous: Paris can mean France or Texas. Springfield needs a state or region. Country abbreviations come in several forms. A valid city must resolve to both coordinates and a time-zone identifier. AstroZen sends the submitted city to Open-Meteo’s geocoding service, then scores the returned candidates against the requested country and optional region hint. It keeps the following structured result: type ResolvedPlace = { name: string; region: string | null; country: string; countryCode: string; latitude: number; longitude: number; timeZone: string; // IANA, for example "Europe/Madrid" }; Enter fullscreen mode Exit fullscreen mode Candidate selection considers exact city-name matches, country matches, an optional region hint, and population as a small tie-breaker. Population never replaces the country check. The more important rule is what happens when resolution fails: if (!selected || !countryMatches(selected, requestedCountry)) { throw new Error( "We could not match that city and country. Add a state or region." ); } Enter fullscreen mode Exit fullscreen mode There is no silent fallback to a famous city. A visible error is better than a polished chart calculated for the wrong place. 2. A time zone is not a fixed UTC offset Storing UTC+1 is not enough. The same location may use different offsets across seasons and historical periods. Governments also change their rules. The geocoder therefore returns an IANA identifier such as America/New_York, not just an offset. The server converts the submitted civil time to UTC using that zone and the runtime’s time-zone database. A simplified version of the conversion looks like this: function zonedTimeToUtc(input: CivilTime, timeZone: string): Date { const guess = new Date(Date.UTC( input.year, input.month - 1, input.day, input.hour, input.minute )); const offsetMinutes = getOffsetFromIntl(timeZone, guess); return new Date(guess.getTime() - offsetMinutes * 60_000); } Enter fullscreen mode Exit fullscreen mode This is already safer than accepting a browser-supplied offset. It also reveals an edge case that every global date-time application should test explicitly: daylight-saving gaps and folds. During a spring transition, some local times never occur. During an autumn transition, one wall-clock time may occur twice. An IANA zone gives you the rules, but the product still needs a declared policy for ambiguous or nonexistent local times. A date picker cannot make that decision for you. 3. UTC conversion and true solar time solve different problems The Western chart needs a UTC instant and geographic coordinates. The BaZi calculation in AstroZen also applies a declared true-solar-time strategy before establishing the Four Pillars. These are separate transformations. The longitude correction is based on the difference between the birthplace longitude and the standard meridian for the local offset: longitude correction (minutes) = (birthplace longitude - standard meridian) × 4 Enter fullscreen mode Exit fullscreen mode The pipeline then adds an equation-of-time correction and applies the total minute shift with full date rollover. That rollover matters: a correction near midnight can move the effective local solar time into the previous or next day. const standardMeridian = offsetMinutes / 4; const solarTime = resolveTrueSolarDateTime( civilBirthTime, place.longitude, standardMeridian ); Enter fullscreen mode Exit fullscreen mode This is a methodology choice, not an invisible truth. Different BaZi lineages can use different boundary conventions, especially around late-night births. The application therefore records the clock time, corrected solar time, longitude, correction in minutes, and any day rollover so the result can be reviewed. 4. Planet positions should not be browser approximations For the Western layer, I replaced lightweight orbital approximations with Swiss Ephemeris compiled to WebAssembly. The server calculates: geocentric tropical planetary longitudes; retrograde state from longitude speed; Placidus house cusps; Ascendant and Midheaven; major aspects with declared orb limits. The WASM package is loaded only on the server and cached after initialization: let ephemeris: SwissEph | null = null; let initialization: Promise | null = null; async function getEphemeris() { if (ephemeris) return ephemeris; if (initialization) return initialization; initialization = (async () => { const { default: SwissEph } = await import("swisseph-wasm"); const instance = new SwissEph(); await instance.initSwissEph(); ephemeris = instance; return instance; })(); return initialization; } Enter fullscreen mode Exit fullscreen mode Lazy initialization helps with serverless cold starts, but WASM deployment adds its own failure mode: the binary must actually be present in the production bundle. A build that passes TypeScript can still fail at runtime if the .wasm asset is missing. Production-like deployment tests are essential. 5. “Unknown birth time” is not 12:00 PM Many chart applications ask for a birth time but quietly substitute noon when it is missing. Noon is useful as an internal date reference because it is far from a day boundary. It is not evidence that the person was born at noon. AstroZen uses local noon only as a neutral calculation reference, then removes every output that depends materially on the unknown time. For BaZi, unknown-time mode omits: the Hour Pillar; Hour-dependent stem and branch relationships; Zi Wei Dou Shu; Dayun start timing where the boundary cannot be supported. For the Western chart, it omits: the Moon; Ascendant and Midheaven; all houses; time-sensitive aspects; the sidereal/Vedic validation snapshot. The remaining result is explicitly labeled as date-based. The internal noon reference is recorded as a limitation, not shown as the user’s birth time. This led to a general rule I now use beyond astrology software: A fallback may keep a pipeline running, but it must not manufacture confidence in the output. 6. The browser should not own the paid calculation Another tempting shortcut is to calculate in the browser and send the finished chart to checkout. That makes the interface responsive, but it also lets a modified client become the source of truth. AstroZen instead sends raw, validated birth input to a server endpoint. The endpoint: limits request size and rate; resolves the place and time zone; performs the Eastern and Western calculations; returns a display model plus a detailed calculation context; signs that context on the server. raw birth input → place + IANA zone → UTC and declared solar-time strategy → deterministic chart engines → signed calculation context → optional interpretation Enter fullscreen mode Exit fullscreen mode Checkout verifies the signature before accepting the context. A user can inspect the free result, but changing the browser payload does not silently change the evidence used for the paid report. 7. AI interprets evidence; it does not calculate the chart The report-generation model receives the structured calculation context after the deterministic engines finish. It does not decide the coordinates, time zone, Four Pillars, planet positions, houses, or aspects. That separation gives the system two useful properties: Repeatability The same supported input produces the same base chart. Generated prose may vary, but the underlying evidence does not. Auditability The report can show which calculated facts support an interpretation. When a factor is unavailable because the birth time is unknown, the model receives that limitation instead of a fabricated value. This does not eliminate model errors. It constrains where they can occur and makes them easier to detect. 8. Tests should target boundaries, not just happy paths The most valuable fixtures are not ordinary noon births in major cities. They are cases close to a boundary: two cities with the same name in different countries; dates around daylight-saving transitions; births close to a solar-term boundary; longitude corrections that cross midnight; unknown-time inputs; repeated identical inputs; production builds that must load a WASM asset. I also test invariants rather than only snapshots: expect(unknownTime.pillars).toHaveLength(3); expect(unknownTime.moon).toBeNull(); expect(unknownTime.ascendant).toBeNull(); expect(unknownTime.houses).toEqual([]); Enter fullscreen mode Exit fullscreen mode An attractive result is not proof that the input pipeline is correct. Boundary tests are where hidden assumptions become visible. What I would keep if I rebuilt it The reusable lessons are not specific to birth charts: Resolve human place names into structured, reviewable data. Store an IANA zone, not a fixed offset. Keep distinct time transformations separate and named. Never turn “unknown” into a confident feature. Make the server the source of truth for paid calculations. Separate deterministic evidence from generative explanation. Expose methodology choices and limitations in the product. You can inspect the public explanation of the calculation pipeline on AstroZen’s methodology page, or try the free calculated chart. No email is required for the free calculation. I would be especially interested in how other developers handle DST folds, historical time-zone data, and WASM assets in serverless deployments. Scope note: BaZi and astrology are traditional symbolic systems used for reflection and entertainment. They are not scientifically validated diagnostic or predictive methods. Better engineering can make a calculation pipeline more consistent and transparent; it does not establish the scientific validity of the interpretation.
The Hard Part of a Global Birth-Chart Calculator Was Time
Full Article
Original Source
Read the full article at Dev →KhanList aggregates and links to publicly available news content. We do not host full articles from third-party sources. Always verify important information with original sources.