Chinese calendar
The Chinese lunisolar calendar: a date's lunar year, month and day in Chinese, leap months included, the year's stem and branch, zodiac animal and element, Chinese New Year, lunar dates back to Gregorian, and the 24 solar terms.
Version 1.0.0 · 4 functions · 3 crates
Functions
What use chinese_calendar.qwer gives a program, with the types Quillo sees. To take only some, or under other names: use chinese_date, chinese_new_year from chinese_calendar (Quillo 0.6).
chinese_date(date: str) → anyor an errorA Gregorian date ("2026-10-29", or "today" in China) in the Chinese calendar: { year, month, day, leap_month, days_in_month, month_zh, day_zh, text, stem_branch, stem_branch_pinyin, cycle_year, animal, animal_zh, element, yin_yang }. year is the Gregorian year the lunar year began in, so January before New Year still has the old year's animal; text is as in Chinese, "丙午年九月十九".
chinese_new_year(year: int) → anyor an errorThe Gregorian date of Chinese New Year (Spring Festival) in year, with the year beginning: { date, weekday, animal, animal_zh, element, stem_branch }.
chinese_to_date(year: int, month: int, day: int, leap: bool) → stror an errorThe Gregorian date of a lunar date: day day of month month (with leap for a leap month) of the lunar year that began in Gregorian year. For birthdays and festivals kept by the lunar calendar: the Mid-Autumn Festival is chinese_to_date(2026, 8, 15, false).
chinese_solar_terms(year: int) → listor an errorThe 24 solar terms (节气) of year: a list of { name, pinyin, english, longitude, date, time }, date the day in China and time the moment in UTC, to within about a quarter of an hour.
Get it
$ quillo install chinese_calendar # chinese_calendar.qwer into this folder$ quillo install chinese_calendar --example # and its example, chinese_calendar.qweOr with curl:
$ curl -O https://quillolang.org/plugins/files/chinese_calendar.qwer$ curl -O https://quillolang.org/plugins/files/chinese_calendar.qweThen, in your program, all of it or only what you call:
use chinese_calendar.qweruse chinese_date, chinese_new_year from chinese_calendar // Quillo 0.6: only theseIt uses these crates, which cargo fetches the first time the plugin is built:
icu_calendar = "=1.5.2"chrono = { version = "0.4", default-features = false, features = ["std", "clock"] }serde_json = { version = "1", features = ["preserve_order"] }Example
// A Chinese restaurant's site: today's lunar date on the menu, this year's festivals, and the solar terms for its seasonal dishes.//// quillo run chinese_calendar.qwe then open http://localhost:8080/calendaruse "chinese_calendar.qwer"route GET /calendar { { today: chinese_date("2026-10-29")?, new_year_2026: chinese_new_year(2026)?, new_year_2027: chinese_new_year(2027)?.date, before_new_year: chinese_date("2026-02-16")?.animal, dragon_boat: chinese_to_date(2026, 5, 5, false)?, mid_autumn: chinese_to_date(2026, 8, 15, false)?, leap: chinese_date("2025-07-30")?.text, no_leap: chinese_to_date(2026, 6, 1, true).error, }}route GET /terms/:year(int) = chinese_solar_terms(year)? | map((t) => `${t.date} ${t.name} ${t.english}`)$ quillo run chinese_calendar.qwe$ curl 'localhost:8080/calendar'$ curl 'localhost:8080/terms/2026'Source
//! The Chinese lunisolar calendar: a date's lunar year, month and day (leap months too) written in Chinese, the year's stem and branch, zodiac animal and element, Chinese New Year's date, a lunar date back to the Gregorian one, and the 24 solar terms of a year.//! version: 1.0.0//!//! [dependencies]//! icu_calendar = "=1.5.2"//! chrono = { version = "0.4", default-features = false, features = ["std", "clock"] }//! serde_json = { version = "1", features = ["preserve_order"] }use chrono::{Datelike, Duration, NaiveDate};use icu_calendar::chinese::Chinese;use icu_calendar::Date;use serde_json::{json, Value};const STEMS: [&str; 10] = ["甲", "乙", "丙", "丁", "戊", "己", "庚", "辛", "壬", "癸"];const STEMS_PY: [&str; 10] = ["jia", "yi", "bing", "ding", "wu", "ji", "geng", "xin", "ren", "gui"];const ELEMENTS: [&str; 5] = ["Wood", "Fire", "Earth", "Metal", "Water"];const BRANCHES: [&str; 12] = ["子", "丑", "寅", "卯", "辰", "巳", "午", "未", "申", "酉", "戌", "亥"];const BRANCHES_PY: [&str; 12] = ["zi", "chou", "yin", "mao", "chen", "si", "wu", "wei", "shen", "you", "xu", "hai"];const ANIMALS: [&str; 12] = ["Rat", "Ox", "Tiger", "Rabbit", "Dragon", "Snake", "Horse", "Goat", "Monkey", "Rooster", "Dog", "Pig"];const ANIMALS_ZH: [&str; 12] = ["鼠", "牛", "虎", "兔", "龙", "蛇", "马", "羊", "猴", "鸡", "狗", "猪"];const MONTHS_ZH: [&str; 12] = ["正月", "二月", "三月", "四月", "五月", "六月", "七月", "八月", "九月", "十月", "冬月", "腊月"];const DIGITS: [&str; 11] = ["", "一", "二", "三", "四", "五", "六", "七", "八", "九", "十"];// The 24 solar terms from 小寒, with the sun's longitude at each.const TERMS: [(&str, &str, &str, f64); 24] = [ ("小寒", "Xiaohan", "Minor Cold", 285.0), ("大寒", "Dahan", "Major Cold", 300.0), ("立春", "Lichun", "Start of Spring", 315.0), ("雨水", "Yushui", "Rain Water", 330.0), ("惊蛰", "Jingzhe", "Awakening of Insects", 345.0), ("春分", "Chunfen", "Spring Equinox", 0.0), ("清明", "Qingming", "Pure Brightness", 15.0), ("谷雨", "Guyu", "Grain Rain", 30.0), ("立夏", "Lixia", "Start of Summer", 45.0), ("小满", "Xiaoman", "Grain Buds", 60.0), ("芒种", "Mangzhong", "Grain in Ear", 75.0), ("夏至", "Xiazhi", "Summer Solstice", 90.0), ("小暑", "Xiaoshu", "Minor Heat", 105.0), ("大暑", "Dashu", "Major Heat", 120.0), ("立秋", "Liqiu", "Start of Autumn", 135.0), ("处暑", "Chushu", "End of Heat", 150.0), ("白露", "Bailu", "White Dew", 165.0), ("秋分", "Qiufen", "Autumn Equinox", 180.0), ("寒露", "Hanlu", "Cold Dew", 195.0), ("霜降", "Shuangjiang", "Frost's Descent", 210.0), ("立冬", "Lidong", "Start of Winter", 225.0), ("小雪", "Xiaoxue", "Minor Snow", 240.0), ("大雪", "Daxue", "Major Snow", 255.0), ("冬至", "Dongzhi", "Winter Solstice", 270.0),];fn parse_date(date: &str) -> Result<NaiveDate, String> { let t = date.trim(); if t.is_empty() || t == "today" { // The day in China (UTC+8). return Ok((chrono::Utc::now() + Duration::hours(8)).date_naive()); } NaiveDate::parse_from_str(t.get(..10).unwrap_or(t), "%Y-%m-%d").map_err(|_| format!("`{}` is not a date (2026-10-29)", date))}struct Lunar { year: i32, month: u8, leap: bool, day: u8, days_in_month: u8 }fn lunar(d: NaiveDate) -> Result<Lunar, String> { let iso = Date::try_new_iso_date(d.year(), d.month() as u8, d.day() as u8).map_err(|e| format!("{}: {:?}", d, e))?; let c = iso.to_calendar(Chinese::new()); let code = c.month().code.0; let code = code.as_str(); let month: u8 = code.get(1..3).and_then(|m| m.parse().ok()).ok_or("an unknown month")?; Ok(Lunar { year: c.year().related_iso.unwrap_or(d.year()), month, leap: code.ends_with('L'), day: c.day_of_month().0 as u8, days_in_month: c.days_in_month(), })}fn day_zh(d: u8) -> String { match d { 1..=10 => format!("初{}", DIGITS[d as usize]), 11..=19 => format!("十{}", DIGITS[d as usize - 10]), 20 => "二十".into(), 21..=29 => format!("廿{}", DIGITS[d as usize - 20]), _ => "三十".into(), }}fn year_info(y: i32) -> Value { let n = (y - 4).rem_euclid(60) as usize; let (s, b) = (n % 10, n % 12); json!({ "stem_branch": format!("{}{}", STEMS[s], BRANCHES[b]), "stem_branch_pinyin": format!("{}{}", STEMS_PY[s], BRANCHES_PY[b]), "cycle_year": n + 1, "animal": ANIMALS[b], "animal_zh": ANIMALS_ZH[b], "element": ELEMENTS[s / 2], "yin_yang": if s % 2 == 0 { "yang" } else { "yin" }, })}fn new_year(y: i32) -> Result<NaiveDate, String> { let mut d = NaiveDate::from_ymd_opt(y, 1, 20).ok_or("the year is out of range")?; for _ in 0..33 { let l = lunar(d)?; if l.month == 1 && !l.leap && l.day == 1 { return Ok(d) } d += Duration::days(1); } Err(format!("no Chinese New Year found in {}", y))}/// A Gregorian date (`"2026-10-29"`, or `"today"` in China) in the Chinese calendar:/// `{ year, month, day, leap_month, days_in_month, month_zh, day_zh, text, stem_branch, stem_branch_pinyin, cycle_year, animal, animal_zh, element, yin_yang }`./// `year` is the Gregorian year the lunar year began in, so January before New Year still has the old year's animal; `text` is as in Chinese, "丙午年九月十九".pub fn chinese_date(date: &str) -> Result<Value, String> { let d = parse_date(date)?; let l = lunar(d)?; let month_zh = format!("{}{}", if l.leap { "闰" } else { "" }, MONTHS_ZH[l.month as usize - 1]); let mut v = json!({ "year": l.year, "month": l.month, "day": l.day, "leap_month": l.leap, "days_in_month": l.days_in_month, "month_zh": month_zh, "day_zh": day_zh(l.day), }); let yi = year_info(l.year); v["text"] = json!(format!("{}年{}{}", yi["stem_branch"].as_str().unwrap_or(""), month_zh, day_zh(l.day))); for (k, x) in yi.as_object().unwrap() { v[k] = x.clone() } Ok(v)}/// The Gregorian date of Chinese New Year (Spring Festival) in `year`, with the year beginning: `{ date, weekday, animal, animal_zh, element, stem_branch }`.pub fn chinese_new_year(year: i64) -> Result<Value, String> { if !(1901..=2099).contains(&year) { return Err("years 1901 to 2099".into()) } let d = new_year(year as i32)?; let mut v = json!({ "date": d.to_string(), "weekday": d.format("%A").to_string() }); let yi = year_info(year as i32); for k in ["animal", "animal_zh", "element", "stem_branch"] { v[k] = yi[k].clone() } Ok(v)}/// The Gregorian date of a lunar date: day `day` of month `month` (with `leap` for a leap month) of the lunar year that began in Gregorian `year`. For birthdays and festivals kept by the lunar calendar: the Mid-Autumn Festival is `chinese_to_date(2026, 8, 15, false)`.pub fn chinese_to_date(year: i64, month: i64, day: i64, leap: bool) -> Result<String, String> { if !(1901..=2098).contains(&year) || !(1..=12).contains(&month) || !(1..=30).contains(&day) { return Err(format!("{} {}/{} is not a lunar date (years 1901 to 2098, months 1 to 12, days 1 to 30)", year, month, day)); } let start = new_year(year as i32)?; let end = new_year(year as i32 + 1)?; let mut d = start; while d < end { let l = lunar(d)?; if l.month as i64 == month && l.leap == leap && l.day as i64 == day { return Ok(d.to_string()) } d += Duration::days(1); } Err(format!("lunar year {} has no {}month {} day {}", year, if leap { "leap " } else { "" }, month, day))}// The sun's apparent longitude (Meeus ch. 25, to about 0.01°).fn sun_longitude(jd: f64) -> f64 { let t = (jd - 2451545.0) / 36525.0; let l0 = 280.46646 + 36000.76983 * t + 0.0003032 * t * t; let m = (357.52911 + 35999.05029 * t - 0.0001537 * t * t).to_radians(); let c = (1.914602 - 0.004817 * t - 0.000014 * t * t) * m.sin() + (0.019993 - 0.000101 * t) * (2.0 * m).sin() + 0.000289 * (3.0 * m).sin(); let om = (125.04 - 1934.136 * t).to_radians(); (l0 + c - 0.00569 - 0.00478 * om.sin()).rem_euclid(360.0)}/// The 24 solar terms (节气) of `year`: a list of `{ name, pinyin, english, longitude, date, time }`, `date` the day in China and `time` the moment in UTC, to within about a quarter of an hour.pub fn chinese_solar_terms(year: i64) -> Result<Vec<Value>, String> { if !(1901..=2099).contains(&year) { return Err("years 1901 to 2099".into()) } let jan1 = NaiveDate::from_ymd_opt(year as i32, 1, 1).unwrap(); let jd0 = (jan1 - NaiveDate::from_ymd_opt(2000, 1, 1).unwrap()).num_days() as f64 + 2451544.5; TERMS.iter().enumerate().map(|(i, (zh, py, en, lon))| { // A first guess from the term's place in the year, then Newton steps on the sun's longitude. let mut jd = jd0 + 5.0 + i as f64 * 15.22; for _ in 0..8 { let diff = ((lon - sun_longitude(jd) + 540.0) % 360.0) - 180.0; jd += diff * 365.2422 / 360.0; } // Terrestrial time to UTC, about 69 s these years. let ms = ((jd - 2440587.5) * 86_400_000.0 - 69_000.0).round() as i64; let utc = chrono::DateTime::from_timestamp_millis(ms).ok_or("the date is out of range")?; let china = utc + Duration::hours(8); Ok(json!({ "name": zh, "pinyin": py, "english": en, "longitude": lon, "date": china.date_naive().to_string(), "time": utc.format("%Y-%m-%dT%H:%MZ").to_string() })) }).collect()}Related
hijri.qwer · 1.0.0Islamic (Hijri) dates: a Gregorian date in Umm al-Qura, civil or observational reckoning, written in English and Arabic; a Hijri date back to the Gregorian; a month's first and last days; and the year's holy days.7 views · 7 downloadsPublic holidays holidays.qwer · 1.0.0Public holidays of 27 countries for any year from 1970 to 2099, computed rather than listed: Easter and its feasts, "the last Monday of May", Japan's equinoxes, with the day each is taken when it falls on a weekend.8 views · 11 downloadsWorld calendars world_calendars.qwer · 1.0.0Dates in the world's other calendars, each way and with month names: Hebrew, Persian, Ethiopian, Coptic, Indian national, Thai Buddhist, Japanese eras (令和8年) and Taiwan's Minguo; and the Jewish holidays of a year.8 views · 6 downloadsBusiness days business_days.qwer · 1.0.0Working days for due dates, delivery estimates and SLAs: add them, count them between dates, skip weekends and holidays, with US, UK, French and German public holidays built in.6 views · 5 downloadsCalendar files ical.qwer · 1.0.0Calendar files (.ics) for bookings and events, added to Google Calendar, Outlook and Apple Calendar in a click.6 views · 6 downloadsEnglish dates dates_english.qwer · 1.0.0Dates typed in English read as times, in the user's time zone and US or UK style: next friday at 3pm, tomorrow 9am, in 2 weeks, 3 days ago.8 views · 10 downloadsThread 0 posts
On Chinese calendar: questions, tips, corrections, and code that uses it.
No posts yet. The first one opens the thread.