Skip to content

Koyomi (暦注)

A koyomi day-quality engine. Given a civil date, it reports the day's 六曜 (rokuyō), its sexagenary 干支, the sectional solar month, and the supported 選日 day flags: 一粒万倍日, 三隣亡, and 天赦日. It reuses the lunisolar converter, solar-term astronomy, and the sexagenary day count.

The annotations that lacked a single authoritative public definition (不成就日, whose day pattern differs between sources, and the calendrical 二十八宿 cycle, which had no verifiable epoch anchor) are intentionally omitted rather than guessed.

from fortune_telling_core import Querent, ReadingRequest
from fortune_telling_core.traditions.koyomi import KOYOMI_DECK, KOYOMI_SPREAD, build_engine

request = ReadingRequest(
    deck_id=KOYOMI_DECK.id,
    spread_id=KOYOMI_SPREAD.id,
    querent=Querent(
        id="sample",
        display_name="Sample",
        attributes={"target_datetime": "2024-01-01T12:00:00+09:00"},
    ),
)
reading = build_engine().cast(request)

The day to evaluate may also be supplied as the request-level as_of moment instead of the target_datetime attribute; an explicit target_datetime takes precedence when both are given.

koyomi

KOYOMI_DECK module-attribute

KOYOMI_DECK = Deck(
    id="koyomi.deck.rokuyo.v1",
    symbols=tuple(
        (
            Symbol(
                id=f"koyomi.rokuyo.{slug}",
                name=f"{cjk} {romaji}",
                attributes={"cjk": cjk},
            )
        )
        for slug, cjk, romaji in ROKUYO
    ),
)

KOYOMI_SPREAD module-attribute

KOYOMI_SPREAD = Spread(
    id="koyomi.spread.day.v1",
    name="Koyomi Day Quality",
    positions=(
        Position(
            "rokuyo",
            "Rokuyō",
            "The day's 六曜, with 干支 and 選日 annotations.",
        ),
    ),
)

KoyomiEngine

KoyomiEngine(ephemeris: Ephemeris | None = None)

Bases: AbstractEngine

Koyomi day-quality engine.

Parameters:

Name Type Description Default
ephemeris Ephemeris | None

Optional backend implementing the shared ephemeris protocol. Defaults to BuiltinEphemeris.

None
Source code in src/fortune_telling_core/traditions/koyomi/engine.py
def __init__(self, ephemeris: Ephemeris | None = None) -> None:
    self.ephemeris = ephemeris or BuiltinEphemeris()

id class-attribute instance-attribute

id = 'koyomi.engine'

version class-attribute instance-attribute

version = '0.1.0'

ephemeris instance-attribute

ephemeris = ephemeris or BuiltinEphemeris()

deck

deck(request: ReadingRequest) -> Deck

Return the koyomi rokuyō deck.

Source code in src/fortune_telling_core/traditions/koyomi/engine.py
def deck(self, request: ReadingRequest) -> Deck:
    """Return the koyomi rokuyō deck."""

    if request.deck_id != KOYOMI_DECK.id:
        raise ValidationError(f"unsupported koyomi deck: {request.deck_id}")
    return KOYOMI_DECK

spread

spread(request: ReadingRequest) -> Spread

Return the koyomi day-quality spread.

Source code in src/fortune_telling_core/traditions/koyomi/engine.py
def spread(self, request: ReadingRequest) -> Spread:
    """Return the koyomi day-quality spread."""

    if request.spread_id != KOYOMI_SPREAD.id:
        raise ValidationError(f"unsupported koyomi spread: {request.spread_id}")
    return KOYOMI_SPREAD

draw

draw(request: ReadingRequest, rng: Rng) -> Draw

Compute the day's koyomi annotations as a deterministic draw.

Source code in src/fortune_telling_core/traditions/koyomi/engine.py
def draw(self, request: ReadingRequest, rng: Rng) -> Draw:
    """Compute the day's koyomi annotations as a deterministic draw."""

    del rng
    target = self._target(request)
    dt = target.target_datetime
    jd_tt = jd_tt_from_utc(julian_day_utc(dt))
    sekki_index = solar_month_index(sun_longitude(jd_tt, self.ephemeris))

    offset = dt.utcoffset()
    tz_hours = offset.total_seconds() / 3600.0 if offset is not None else 0.0
    lunisolar = to_lunisolar(jd_tt, tz_hours=tz_hours, ephemeris=self.ephemeris)

    day_index = (julian_day_from_date(dt.year, dt.month, dt.day) - DAY_JIAZI_JDN) % 60
    day = ganzhi(day_index)

    notes = compute_day_notes(sekki_index, lunisolar.month, lunisolar.day, day)
    slug, cjk, _ = ROKUYO[notes.rokuyo_index]
    selection = Selection(
        "rokuyo",
        f"koyomi.rokuyo.{slug}",
        {
            "rokuyo_cjk": cjk,
            "day_ganzhi": day.cjk,
            "sekki_month": str(notes.sekki_month),
            "lunisolar": (
                f"{lunisolar.year}-"
                f"{'leap-' if lunisolar.is_leap_month else ''}"
                f"{lunisolar.month}-{lunisolar.day}"
            ),
            "ichiryu_manbai": str(notes.is_ichiryu_manbai).lower(),
            "sanrinbo": str(notes.is_sanrinbo).lower(),
            "tensha": str(notes.is_tensha).lower(),
        },
    )
    return Draw(KOYOMI_DECK.id, KOYOMI_SPREAD.id, (selection,))

cast

cast(request: ReadingRequest) -> Reading

Compute a koyomi reading without a caller RNG.

Source code in src/fortune_telling_core/traditions/koyomi/engine.py
def cast(self, request: ReadingRequest) -> Reading:
    """Compute a koyomi reading without a caller RNG."""

    draw = self.draw(request, _NULL_RNG)
    return self._interpret(request, draw, rng=None)

build_engine

build_engine(
    ephemeris: Ephemeris | None = None,
) -> KoyomiEngine

Create a koyomi day-quality engine.

Source code in src/fortune_telling_core/traditions/koyomi/engine.py
def build_engine(ephemeris: Ephemeris | None = None) -> KoyomiEngine:
    """Create a koyomi day-quality engine."""

    return KoyomiEngine(ephemeris=ephemeris)