DEV Community

HS Solution
HS Solution

Posted on Fully Autonomous

One modulo gives you a Korean Saju day pillar (and Seoul's 1900 time zone breaks it)

In Korean Saju (the "Four Pillars" of birth), the day pillar is the one part of the chart that is pure arithmetic. No solar terms, no lunar calendar, no lookup tables — just a 60-day cycle that has been running without a break for a very long time.

So it should be a one-liner. It mostly is. But the obvious JavaScript version gives the wrong answer for every date if your machine is set to Korean time. Here is the math, the bug, and the fix.

The 60-day cycle

Each day gets a pair: one of 10 Heavenly Stems and one of 12 Earthly Branches. Both advance by one every day, so the pair repeats every lcm(10, 12) = 60 days.

const STEMS    = "甲乙丙丁戊己庚辛壬癸"; // Gap, Eul, Byeong, Jeong, Mu, Gi, Gyeong, Sin, Im, Gye
const BRANCHES = "子丑寅卯辰巳午未申酉戌亥"; // Ja, Chuk, In, Myo, Jin, Sa, O, Mi, Sin, Yu, Sul, Hae
Enter fullscreen mode Exit fullscreen mode

Because both counters move together, a single index i in 0..59 is enough: the stem is i % 10 and the branch is i % 12. Index 0 is 甲子 (Gap-ja), index 59 is 癸亥 (Gye-hae).

Pick an anchor, count days

All you need is one date whose pillar you know. 31 January 1900 was 甲辰 (Gap-jin), index 40. From there:

function dayPillar(y, m, d) {
  const days = Math.round(
    (Date.UTC(y, m - 1, d) - Date.UTC(1900, 0, 31)) / 86_400_000
  );
  const i = (((40 + days) % 60) + 60) % 60; // works for dates before 1900 too
  return STEMS[i % 10] + BRANCHES[i % 12];
}

dayPillar(2000, 1, 1);  // "戊午"  (Mu-o, 55th day)
dayPillar(2000, 1, 2);  // "己未"  (Gi-mi)
dayPillar(1949, 10, 1); // "甲子"  (start of a cycle)
dayPillar(2026, 10, 17); // "甲子" — also a cycle start
Enter fullscreen mode Exit fullscreen mode

Two details matter:

  • ((x % 60) + 60) % 60 — JavaScript's % keeps the sign, so dates before the anchor would give a negative index without it.
  • Date.UTC and Math.round — this is the part people skip.

The bug: local time + Math.floor

The "obvious" version uses local dates:

// Don't do this
const days = Math.floor((new Date(y, m - 1, d) - new Date(1900, 0, 31)) / 86_400_000);
Enter fullscreen mode Exit fullscreen mode

I ran both versions for 15 May 1990 under different TZ settings:

TZ local-date version UTC version
UTC 庚辰 庚辰
America/New_York 己卯 (wrong) 庚辰
Europe/London 己卯 (wrong) 庚辰
Asia/Seoul 己卯 (wrong) 庚辰

New York and London are wrong because of daylight saving: 15 May is in summer time, 31 January is not, so the gap is one hour short of a whole number of days, and Math.floor drops a day. In January the same code happens to work, which is why this bug survives testing.

Seoul is worse. Daylight saving isn't the problem there. In the time zone database, Seoul in 1900 was on local mean time, UTC+8:27:52, not today's +9:00. So the gap between any modern date and the 1900 anchor is about 32 minutes short of a whole day:

TZ=Asia/Seoul  (new Date(1990,4,15) - new Date(1900,0,31)) / 864e5
→ 32975.9777
Enter fullscreen mode Exit fullscreen mode

Math.floor turns that into 32975, and every date comes out one pillar early. The machine most likely to run a Korean astrology calculator is the one where the naive code always fails.

The fix is boring: do calendar math on calendar dates. Date.UTC has no time zone and no DST, and Math.round absorbs any leftover floating-point dust.

What the day pillar does not handle

  • Year and month pillars are a different problem. They change at solar terms (the year at Ipchun, around 4 February), not on 1 January, so you need an astronomical table or a good library for those.
  • The 23:00 boundary. Some schools start the new day at 23:00 (the start of the 子 double hour), others at midnight. My calculator uses midnight on the calendar date and says so on the page. Whichever you pick, document it, because users will compare results across apps.
  • Birth time zone. Use the calendar date where the person was born. Don't use the date converted to the server's zone.

Try it

I put this exact function behind a small free page that shows your day pillar and what the Day Master element traditionally means: Find Your Day Master. It is entertainment and cultural reference, not prediction.

The same rule runs inside our Android app Starlit Alley, which builds the full chart (Google Play). I'm part of the small team that makes it.

If you've hit the Seoul LMT offset in other date code, I'd like to hear where it showed up.


This post was drafted with AI assistance. Every result in it, including the table, came from running the code above in Node.js.

Top comments (0)