amlich

package module
v0.1.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 25, 2026 License: MIT Imports: 3 Imported by: 0

README

amlich 🌖

Astronomical Vietnamese & Korean lunisolar calendar engine in pure Go — with an MCP server so AI agents stop guessing when Tết is.

amlich computes the lunisolar calendar from first principles — new-moon instants and solar longitude, not lookup tables — so it can convert any date, resolve every lunisolar holiday (Tết, Giỗ tổ, Trung thu · 설날, 석가탄신일, 추석), name the sexagenary year in Vietnamese and Korean readings, and answer a question almost nobody can: in which years do Vietnam and Korea celebrate the "same" lunar holiday on different days?

$ amlich divergence 2026 2031
năm âm 2027, ngày 1/1: VN 2027-02-06 ≠ KR 2027-02-07 (Tết Nguyên Đán / 설날)
năm âm 2028, ngày 1/1: VN 2028-01-26 ≠ KR 2028-01-27 (Tết Nguyên Đán / 설날)
năm âm 2030, ngày 1/1: VN 2030-02-02 ≠ KR 2030-02-03 (Tết Nguyên Đán / 설날)

Same new moon — but Vietnam numbers its calendar at UTC+7 and Korea at UTC+9, so a new moon between 23:00 ICT and 01:00 KST starts the month on different civil days. Ask an LLM without a tool and it will happily hallucinate these dates; give it this MCP server and it computes them from planetary motion.

  • Zero dependencies in the engine (import "github.com/doxuta/amlich" pulls only the standard library)
  • Cross-validated: 59,810 golden conversions + 1,732 astronomy samples against the implementation running in production inside TEdu, 1900–2199, both zones
  • Fuzzed: native round-trip fuzz target, 38.9M executions clean; the inputs that broke earlier revisions live in testdata/fuzz as a regression corpus
  • Fast: ~360 ns and 0 allocs per conversion (Apple M3 Pro)
  • Stricter than the reference algorithm: rejects day 30 of a 29-day month and correctly handles a leap 12th month — two real defects of the classical implementation, documented in DESIGN.md

Library

go get github.com/doxuta/amlich
l, _ := amlich.SolarToLunar(2026, 2, 17, amlich.Vietnam)
fmt.Println(l)                            // 1/1/2026 — Tết Bính Ngọ
fmt.Println(amlich.CanChi(l.Year).Korean) // 병오년

y, m, d, _ := amlich.LunarToSolar(amlich.LunarDate{Year: 2026, Month: 8, Day: 15}, amlich.Korea)
// 2026-09-25 — Chuseok

for _, h := range amlich.Holidays(2027, amlich.VN) { fmt.Println(h.ISO(), h.Name) }
for _, div := range amlich.Divergence(2026, 2060) { fmt.Println(div.NameVN, div.VN, "≠", div.KR) }

API: SolarToLunar · LunarToSolar · MonthDays · Holidays · CanChi · Divergence — see pkg.go.dev/github.com/doxuta/amlich.

MCP server (for AI agents)

go install github.com/doxuta/amlich/cmd/amlich-mcp@latest
claude mcp add amlich -- amlich-mcp

Tools: convert_date, lunar_today, holidays_in_year, vn_kr_divergence. Deterministic, offline, no API keys — an agent asked "when is Seollal 2035?" answers from astronomy instead of vibes.

CLI

go install github.com/doxuta/amlich/cmd/amlich@latest
amlich today                # hôm nay âm lịch bao nhiêu?
amlich convert 2026-02-17
amlich holidays 2027 -kr    # Korean holiday set at UTC+9
amlich canchi 2027          # Đinh Mùi · 정미년 · Goat

Benchmarks

BenchmarkSolarToLunar-12    3405927    360.0 ns/op    0 B/op    0 allocs/op
BenchmarkLunarToSolar-12    3507360    339.8 ns/op    0 B/op    0 allocs/op

Vietnamese — Tóm tắt

Thư viện Go thuần (không dependency) tính âm lịch Việt Nam và Hàn Quốc bằng thiên văn — điểm sóc và kinh độ mặt trời — thay vì bảng tra, kèm CLI và MCP server cho AI agent. Trích xuất từ thuật toán đang chạy thật trong TEdu, đối chứng 59.810 phép đổi (1900–2199, cả UTC+7 lẫn UTC+9), fuzz 38,9 triệu lượt. Điểm thú vị nhất: liệt kê những năm Tết Việt Nam và Seollal Hàn Quốc lệch nhau một ngày vì cùng một trăng non nhưng khác múi giờ pháp định (ví dụ 2027, 2028, 2030, 2053).

한국어 — 요약

베트남(UTC+7)과 한국(UTC+9)의 음력을 천문 계산으로 구하는 순수 Go 라이브러리입니다. 신월 시각과 태양 황경을 직접 계산하므로 조견표 없이 어떤 날짜든 변환할 수 있고, 설날·석가탄신일·추석 등 음력 명절 날짜와 간지(예: 2027년 정미년)를 제공합니다. 같은 신월이라도 두 나라의 표준시가 달라 설날과 Tết이 하루 어긋나는 해(2027, 2028, 2030, 2053...)를 계산으로 찾아내는 Divergence API가 특징입니다. MCP 서버를 통해 AI 에이전트가 음력 날짜를 추측하지 않고 계산하도록 할 수 있습니다.

Provenance & AI disclosure

The astronomy follows the classical Meeus-series method popularized for the Vietnamese calendar by Hồ Ngọc Đức; the porting, hardening, and documentation were done with an AI coding agent under human review. Full lineage, the three reference-implementation defects found along the way, and the validation methodology are in DESIGN.md.

MIT © Xuan Tai Doan

Documentation

Overview

Package amlich computes the Vietnamese and Korean lunisolar calendars from first-principles astronomy — new-moon instants and apparent solar longitude — instead of lookup tables, so any date (past or future) can be converted.

The same astronomical month runs on different civil clocks: Vietnam numbers its calendar at UTC+7, Korea at UTC+9 (China at UTC+8). When a new moon falls close to local midnight, the two national calendars can start a month on different days — which is why Tết and Seollal, or Trung thu and Chuseok, occasionally land on different dates. Divergence enumerates those years.

Accuracy: the truncated series used here (Meeus, via the reference implementation by Hồ Ngọc Đức) is reliable for roughly 1200–3000 CE; results are cross-validated against the production implementation inside TEdu for 1900–2199 (see testdata/).

Index

Examples

Constants

This section is empty.

Variables

View Source
var ErrInvalidLunarDate = errors.New("amlich: lunar date does not exist")

ErrInvalidLunarDate is returned when a lunar date does not exist, e.g. day 30 of a 29-day month, or a leap-month flag on a year/month without one.

View Source
var ErrInvalidSolarDate = errors.New("amlich: invalid solar date")

ErrInvalidSolarDate is returned for an impossible civil date.

Functions

func LunarToSolar

func LunarToSolar(l LunarDate, z Zone) (year, month, day int, err error)

LunarToSolar converts a lunisolar date (as numbered in zone z) to the civil (Gregorian) date it falls on. Unlike the reference implementation, it validates month length: day 30 of a 29-day month is ErrInvalidLunarDate instead of silently overflowing into the next month.

Example
package main

import (
	"fmt"

	"github.com/doxuta/amlich"
)

func main() {
	// Trung thu (full moon of the 8th month), lunar year 2026.
	y, m, d, _ := amlich.LunarToSolar(amlich.LunarDate{Year: 2026, Month: 8, Day: 15}, amlich.Vietnam)
	fmt.Printf("%04d-%02d-%02d\n", y, m, d)
}
Output:
2026-09-25

func MonthDays

func MonthDays(l LunarDate, z Zone) (int, error)

MonthDays reports the length (29 or 30 days) of the given lunar month as numbered in zone z. The Day field of l is ignored.

Types

type Country

type Country int

Country selects a national lunisolar tradition: which holidays are observed and which civil clock (Zone) numbers the calendar.

const (
	// VN — Vietnam: calendar at UTC+7.
	VN Country = iota
	// KR — Korea: calendar at UTC+9.
	KR
)

func (Country) Zone

func (c Country) Zone() Zone

Zone returns the civil time zone the country numbers its calendar in.

type Divergent

type Divergent struct {
	LunarDay   int
	LunarMonth int
	LunarYear  int
	NameVN     string
	NameKR     string
	VN         string // YYYY-MM-DD as observed in Vietnam
	KR         string // YYYY-MM-DD as observed in Korea
}

Divergent is a lunar anchor date whose civil date differs between the Vietnamese (UTC+7) and Korean (UTC+9) calendars in a given year.

func Divergence

func Divergence(fromYear, toYear int) []Divergent

Divergence enumerates, over lunar years [fromYear, toYear], the shared VN/KR observances whose civil dates differ because the same new moon falls on different sides of midnight at UTC+7 versus UTC+9.

type Holiday

type Holiday struct {
	Name    string // native name
	NameEN  string
	Day     int // lunar day
	Month   int // lunar month
	Country Country
}

Holiday is a lunisolar holiday definition.

type HolidayDate

type HolidayDate struct {
	Holiday
	Year   int // Gregorian year the holiday falls in
	SolarY int
	SolarM int
	SolarD int
}

HolidayDate is a holiday resolved to a civil date for a specific year.

func Holidays

func Holidays(year int, c Country) []HolidayDate

Holidays resolves the country's lunisolar holidays that fall inside Gregorian year year, in calendar order.

Lunar months 1..8 of lunar year Y fall in Gregorian year Y; month 12 dates (Ông Táo) fall early in Gregorian year Y+1, so they are resolved from lunar year year-1.

Example
package main

import (
	"fmt"

	"github.com/doxuta/amlich"
)

func main() {
	for _, h := range amlich.Holidays(2026, amlich.KR)[:2] {
		fmt.Println(h.ISO(), h.NameEN)
	}
}
Output:
2026-02-17 Seollal (Lunar New Year)
2026-03-03 Daeboreum (First Full Moon)

func (HolidayDate) ISO

func (h HolidayDate) ISO() string

ISO renders the resolved civil date as YYYY-MM-DD.

type LunarDate

type LunarDate struct {
	Year  int
	Month int
	Day   int
	Leap  bool
}

LunarDate is a date in a lunisolar calendar. Leap reports whether the date belongs to the intercalary (leap) month with the same Month number.

func SolarToLunar

func SolarToLunar(year, month, day int, z Zone) (LunarDate, error)

SolarToLunar converts a civil (Gregorian) date to the lunisolar date as numbered in zone z.

Example
package main

import (
	"fmt"

	"github.com/doxuta/amlich"
)

func main() {
	l, _ := amlich.SolarToLunar(2026, 2, 17, amlich.Vietnam)
	fmt.Println(l, amlich.CanChi(l.Year).Vietnamese)
}
Output:
1/1/2026 Bính Ngọ

func (LunarDate) String

func (l LunarDate) String() string

String renders the date in the conventional Vietnamese order, e.g. "15/8/2026" or "2/6+/2025" for a leap-month date.

type YearName

type YearName struct {
	Vietnamese string // e.g. "Bính Ngọ"
	Korean     string // e.g. "병오년"
	AnimalEN   string // e.g. "Horse" — note: branch 4 (Mão) is Cat in Vietnam, Rabbit elsewhere
}

YearName is the sexagenary name of a lunar year in both readings.

func CanChi

func CanChi(lunarYear int) YearName

CanChi returns the sexagenary name of the given lunar year.

Example
package main

import (
	"fmt"

	"github.com/doxuta/amlich"
)

func main() {
	n := amlich.CanChi(2027)
	fmt.Println(n.Vietnamese, "·", n.Korean, "·", n.AnimalEN)
}
Output:
Đinh Mùi · 정미년 · Goat

type Zone

type Zone float64

Zone is a civil time zone expressed in hours east of UTC, used as the reference clock for numbering the lunisolar calendar.

const (
	// Vietnam numbers its lunisolar calendar at UTC+7 (since 1967-08-08 in
	// the North, unified 1975).
	Vietnam Zone = 7
	// Korea numbers its lunisolar calendar at UTC+9 (KST).
	Korea Zone = 9
	// China numbers its lunisolar calendar at UTC+8 (CST).
	China Zone = 8
)

Directories

Path Synopsis
cmd
amlich command
Command amlich is a small CLI for the amlich lunisolar engine.
Command amlich is a small CLI for the amlich lunisolar engine.
amlich-mcp command
Command amlich-mcp exposes the amlich lunisolar engine as an MCP server (stdio transport), so AI agents can compute — not guess — Vietnamese and Korean lunar dates and holidays.
Command amlich-mcp exposes the amlich lunisolar engine as an MCP server (stdio transport), so AI agents can compute — not guess — Vietnamese and Korean lunar dates and holidays.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL