lunar-rs

A Rust calendar library for the Gregorian/Solar calendar, Chinese Lunar calendar, JieQi, GanZhi, almanac data, Buddhist calendar, Taoist calendar, BaZi, and fortune-cycle calculations.

The implementation is a Rust port inspired by lunar-go and lunar-javascript, with ShouXing astronomical algorithms and embedded data tables. The core crate is designed to avoid third-party runtime dependencies.

Status: 1.0.0-rc1 release candidate. cargo test and crate doctests pass against the local golden integration suite. A verified feature-by-feature comparison against 6tail/tyme4rs v1.5 confirms lunar-rs is now a functional superset (30 calendar systems vs 4, plus Buddhist/Taoist calendars, runtime holiday override, i18n, and serde), with a tyme4rs compatibility layer of type aliases and get_* entry points.

Documentation

Features

Installation

From crates.io after release:

[dependencies]
lunar-rs = "1.0.0-rc1"

From Git while the crate is under active development:

[dependencies]
lunar-rs = { git = "https://github.com/houseme/lunar-rs" }

The library crate name is lunar_rs:

use lunar_rs::{Lunar, Solar};

Quick Start

Solar to Lunar:

use lunar_rs::Solar;

let solar = Solar::from_ymd(2020, 5, 24).unwrap();
let lunar = solar.lunar();

assert_eq!(lunar.to_string(), "二〇二〇年闰四月初二");
assert_eq!(lunar.year_in_gan_zhi(), "庚子");
assert_eq!(lunar.year_sheng_xiao(), "鼠");

Lunar to Solar:

use lunar_rs::Lunar;

let lunar = Lunar::from_ymd(2019, 3, 27).unwrap();
let solar = lunar.solar();

assert_eq!(solar.to_ymd(), "2019-05-01");

JieQi lookup:

use lunar_rs::Lunar;

let lunar = Lunar::from_ymd(2012, 9, 1).unwrap();
let bai_lu = lunar.jie_qi_table().get("白露").unwrap();

assert_eq!(bai_lu.to_ymd_hms(), "2012-09-07 13:29:01");

Date traversal:

use lunar_rs::Solar;

assert_eq!(
    Solar::from_ymd(1582, 10, 4).unwrap().next_day(1).to_ymd(),
    "1582-10-15"
);

assert_eq!(
    Solar::from_ymd(2023, 8, 31).unwrap().next_month(6).to_ymd(),
    "2024-02-29"
);

BaZi and fortune cycles:

use lunar_rs::{Gender, Solar};

let lunar = Solar::from_ymd(2021, 12, 21).unwrap().lunar();
let eight_char = lunar.eight_char();
let yun = eight_char.yun(1 as Gender);

println!("BaZi: {} {} {} {}", eight_char.year(), eight_char.month(), eight_char.day(), eight_char.time());
println!("Yun starts at {} years {} months", yun.start_year(), yun.start_month());

Buddhist and Taoist calendars:

use lunar_rs::Solar;

let lunar = Solar::from_ymd(2024, 5, 15).unwrap().lunar();
let foto = lunar.foto();
let tao = lunar.tao();

println!("Buddhist calendar: {}", foto.to_string_cn());
println!("Taoist calendar: {}", tao.to_string_cn());

Multi-calendar conversion (30 systems reachable from Solar):

use lunar_rs::Solar;

let solar = Solar::from_ymd(2024, 5, 15).unwrap();
println!("Hijri:    {}", solar.hijri());
println!("Tibetan:  {}", solar.rab_byung_day().unwrap());
println!("Minguo:   {}", solar.minguo());
println!("Japanese: {}", solar.japanese().unwrap());
println!("Julian:   {}", solar.julian_calendar());

Event aggregation (festivals, JieQi, legal holidays in one call):

use lunar_rs::Solar;

let solar = Solar::from_ymd(2024, 10, 1).unwrap();
for event in solar.events() {
    println!("[{:?}] {}", event.kind(), event.name());
}

// Range scan over a period
let end = Solar::from_ymd(2024, 10, 7).unwrap();
let count = solar.events_until(end).len();

tyme4rs Migration Notes

lunar-rs now exposes a compatibility layer for many common tyme4rs-style entry points. A few practical patterns:

use lunar_rs::{LunarHour, SolarDay, SolarFestival, SolarTime, SolarTerm};

let solar_time: SolarTime = SolarTime::from_ymd_hms(2024, 2, 10, 8, 30, 0).unwrap();
let lunar_hour: LunarHour<'static> = solar_time.get_lunar_hour();

assert_eq!(lunar_hour.get_solar_time().to_ymd_hms(), "2024-02-10 08:30:00");
assert_eq!(lunar_hour.get_minor_ren().name(), solar_time.lunar().time_minor_ren().name());

let solar_day: SolarDay = SolarDay::from_ymd(2024, 10, 1).unwrap();
let festival: SolarFestival = solar_day.get_festival().unwrap();
assert_eq!(festival.get_name(), "国庆节");

let term: SolarTerm = SolarTerm::from_name(2023, "大雪").unwrap();
assert_eq!(term.get_solar_day().to_ymd(), "2023-12-07");

For migrated code, keep two semantic differences in mind:

Validation

cargo check
cargo test
cargo bench --bench convert
cargo run --bin lunar_ref_driver -- solar 2024 4 22 23 30 0
cargo test --features i18n

Current local validation:

Differential test workflow:

cargo run --bin lunar_ref_driver -- solar 2024 4 22 23 30 0
sh scripts/run_differential_self_check.sh
bash scripts/run_tyme4rs_diff_check.sh

Project Layout

License

Licensed under either of: