DEV Community

Shirabe Dev
Shirabe Dev

Posted on • Originally published at qiita.com

Stop hand-coding the Japanese Rokuyo calendar: LLM-generated lunar logic silently breaks

Originally published in Japanese on Qiita. This is the English edition.

TL;DR

  • Business requirements that touch the Japanese Rokuyo calendar are surging in the LLM era — wedding-date pickers, moving-day scorers, fortune-telling chatbots.
  • But if you compute Rokuyo yourself, you get a class of bug that silently miscalculates on specific dates.
  • The root cause is not the Rokuyo formula. It's the step before it: converting the Gregorian date to the Japanese lunar calendar requires astronomical precision.
  • The fix is to put an AI-agent-oriented Japanese calendar API in front of it (e.g. Shirabe Calendar API). It's 3 lines of curl / fetch / requests.

What is Rokuyo, briefly

Rokuyo (六曜) is a six-day cycle (Taian / Tomobiki / Sensho / Senbu / Butsumetsu / Shakko) still used across Japan to decide auspicious dates — weddings, funerals, ground-breakings, store openings. If you build anything for the Japanese market that recommends or scores dates, you will eventually need it. It looks trivial. It is not.

Who this is for

  • Full-stack engineers building LLM agents / AI SaaS.
  • Anyone who once thought "Rokuyo? I can just write that myself."
  • Anyone who has shipped LLM-generated date logic to production.

The problem: what happens when you ask an AI to "compute Rokuyo"

Ask a coding assistant for "a function that tells me whether a given date is Taian" and you'll usually get something shaped like this:

// The "naive Rokuyo" an LLM tends to propose
function getRokuyo(gregorianDate: Date): string {
  // (1) Convert Gregorian -> lunar (year / month / day)
  const lunar = convertToLunar(gregorianDate);   // <- this is where accidents live

  // (2) Rokuyo = (lunar month + lunar day) mod 6
  const index = (lunar.month + lunar.day) % 6;
  return ["Taian", "Shakko", "Sensho", "Tomobiki", "Senbu", "Butsumetsu"][index];
}
Enter fullscreen mode Exit fullscreen mode

Step (2) is genuinely correct — that mapping is right. The bug is always in (1), convertToLunar.

The usual ways convertToLunar is written (all wrong)

  • Counting days from the last new moon using a 29.5-day period. The synodic month actually varies between 29.27 and 29.84 days. Even using the mean 29.5306, run it for a year and you drift by days.
  • Embedding some lunar-conversion table found online. Past the table's terminal year it silently drifts, and leap-month handling differs table to table.
  • Reusing a Chinese-lunar (农历) library like lunar-javascript. Japan's Tenpō calendar and the Chinese calendar differ in the reference timezone for the new moon (UTC+8 vs UTC+9) and in leap-month placement — so a few dates per year diverge.
  • A 30–50 line formula the LLM wrote. The solar/lunar ecliptic-longitude approximation is sloppy by one digit somewhere, and dates landing on the new-moon day boundary come out one day off.

Why the drift is scary

  • It never throws. Types pass, no exception, always returns a Rokuyo-looking string.
  • Tests miss it. It's wrong on only 2–3 pinpoint days, so sampling tests pass.
  • You find out on the wedding day. The day you advertised as "Taian" turns out to be "Butsumetsu."

This is the silent-failure class — the most common kind of bug in the era of letting an LLM write the code.


Root cause: lunar conversion needs astronomy

The Rokuyo rule (lunar month + lunar day) mod 6 is deterministic and always correct once the lunar date is right. The hard part is the lunar date. Japan's old calendar (Tenpō) requires:

  1. Compute the instant of the new moon (saku) at 135°E, Japan Standard Time.
  2. The day of the new moon is lunar day 1.
  3. A month that contains no major solar term (chūki) is a leap month.
  4. Leap-month placement changes by year (7 times per ~19 years — the Metonic cycle).

Doing this properly means computing solar and lunar ecliptic longitude at astronomical precision (VSOP87 and friends). Use an approximation and, on days where the new moon falls near midnight, you shift by one day — and every subsequent lunar date that year dominoes by one day.

A widely cited reference implementation is QREKI (by Hideaki Takano): hundreds of lines of AWK computing apparent ecliptic longitudes via series expansion. Hand-porting that is, both in volume and in principle, not the kind of task you should hand to an LLM.


The fix: call a Japanese-calendar API

Drop the self-implementation and hand it to an API designed for AI agents — you win on accuracy, maintenance cost, and ease of AI integration. The examples below use Shirabe Calendar API. The free tier is 10,000 calls/month and you can try it without an API key. The fetch / requests examples below still pass X-API-Key — it's optional, used to track usage and raise limits.

Three lines that work

# One date -> Rokuyo, rekichu (auspicious days), kanshi, solar term, all in one response
curl https://shirabe.dev/api/v1/calendar/2026-04-15
Enter fullscreen mode Exit fullscreen mode

The JSON looks like:

{
  "date": "2026-04-15",
  "rokuyo": {
    "name": "大安",
    "reading": "Taian",
    "timeSlots": { "morning": "good", "noon": "good", "afternoon": "good", "evening": "good" }
  },
  "rekichu": [
    { "name": "一粒万倍日", "type": "auspicious" }
  ],
  "context": {
    "wedding": { "judgment": "excellent", "score": 9,
      "note": "Taian x Ichiryu-manbai-bi. An excellent day for a wedding." }
  },
  "summary": "Reiwa 8 (2026)-04-15 (Wed) Taian / Ichiryu-manbai-bi. Excellent for weddings and openings."
}
Enter fullscreen mode Exit fullscreen mode

The key fields are context and summary. Instead of a bare "Taian" label, you get a per-purpose judgment (8 categories, each scored 1–10) and a one-line summary. An AI agent can drop that summary straight into its answer to the user.

TypeScript

const res = await fetch(
  "https://shirabe.dev/api/v1/calendar/2026-04-15",
  { headers: { "X-API-Key": process.env.SHIRABE_API_KEY! } }
);
const data = await res.json();
console.log(data.rokuyo.name);   // "大安" (Taian)
console.log(data.summary);       // "Reiwa 8 (2026)-04-15 (Wed) Taian ..."
Enter fullscreen mode Exit fullscreen mode

Python

import os, requests

r = requests.get(
    "https://shirabe.dev/api/v1/calendar/2026-04-15",
    headers={"X-API-Key": os.environ["SHIRABE_API_KEY"]},
    timeout=10,
)
print(r.json()["rokuyo"]["name"])
Enter fullscreen mode Exit fullscreen mode

Range queries and best-day search

You're not limited to a single date.

# All Taian/Tomobiki days in April (up to 93 days)
curl "https://shirabe.dev/api/v1/calendar/range?start=2026-04-01&end=2026-04-30&filter_rokuyo=大安,友引"

# Top 5 best wedding days from April to December, by score
curl "https://shirabe.dev/api/v1/calendar/best-days?purpose=wedding&start=2026-04-01&end=2026-12-31&limit=5"
Enter fullscreen mode Exit fullscreen mode

best-days is built so an AI agent can externalize the judgment itself. Pass a date range and a purpose (wedding / moving / business and 8 categories total) and you get a ranked list of top-scoring days with the reasoning (e.g. Taian × Tensha-bi).


Integrating with AI agents

Publish an OpenAPI 3.1 spec and this kind of API can be called in one shot from ChatGPT GPTs Actions, Claude tool use, Gemini function calling, LangChain, or LlamaIndex. Shirabe serves it directly at https://shirabe.dev/openapi.yaml.

ChatGPT GPTs Actions

In the GPT Builder, "Create new action" → paste the OpenAPI URL into Import URL:

https://shirabe.dev/openapi.yaml
Enter fullscreen mode Exit fullscreen mode

Choose API Key auth (header name X-API-Key). That's all it takes for a custom GPT to call Shirabe automatically.

LangChain / LlamaIndex / Dify

Feed the same URL to an OpenAPI loader and it's turned into tools. operationId becomes the function name by design, so no glue code is needed.


Self-implementation vs API

Dimension Self-implementation / LLM-generated code Japanese calendar API (Shirabe etc.)
Lunar accuracy Approximations slip by a day at new-moon boundaries Astronomical precision
Rekichu coverage Implementing the ~13 auspicious-day types is a separate big job 13+ types out of the box
Per-purpose judgment Hard to spec and to build Provided via context.wedding.score etc.
Maintenance Versioning the saku-calculation library, etc. Zero on the caller side
AI integration Write your own tool/function definitions One OpenAPI URL
Initial cost Looks like zero — but a single wrong-date liability sits behind it Free tier, 10,000/month

"Free because I built it myself" is only superficially free. Priced with the business risk of a failure, it usually doesn't pay off in this domain.


Wrap-up

  • The Rokuyo formula itself is easy. The hard part is the lunar conversion before it, which needs astronomical-precision new-moon computation.
  • Let an LLM write the self-implementation and you get a silent failure that's off by one day exactly on new-moon-boundary dates.
  • In domains where you find out you were wrong on the day itself — weddings, funerals, moving, contract dates — avoiding self-implementation is the cheaper choice.
  • Use a Japanese calendar API built for AI agents (OpenAPI published, free tier) and you're done in 3 lines of fetch.

Sample code is also collected under the examples in https://shirabe.dev/openapi.yaml. The same URL works whether you're calling from ChatGPT GPTs Actions, Claude tool use, or Gemini function calling.


Links

Top comments (0)