- 新增时标、ΔT 模型、质心时间与 UT1 支持 - 改进日月食、月掩、行星事件及路径边界计算 - 完善恒星三维自行与动态距离传播 - 扩展 SVG、GeoJSON、KML 输出与底层距离换算工具 - 整理中英文手册、示例资源及回归测试
37 KiB
Calendar and Solar Terms
The calendar package converts between Gregorian dates and the traditional Chinese lunisolar calendar, and exposes solar terms. The supported range is from 721 BCE through 3000 CE; 104 BCE is the calendar-table switch point, and beyond 3000 CE the result is pure theoretical extrapolation computed on the fly.
The calendar is lunisolar in the strict sense, but public function names use Lunar rather than the more academic Lunisolar, for readability and convention.
For historical input, Chinese era names stay in Chinese. This is part of the API surface, because historical Chinese dates are normally written that way.
Contents
- Converting a date and calculating a solar term
- API Reference
- Usage examples
- Calendar notes
- Usage notes
- Calendar conversion
- Solar terms
- Parameter and result conventions
Converting a date and calculating a solar term
package main
import (
"fmt"
"log"
"time"
"b612.me/astro/calendar"
)
func main() {
cst := time.FixedZone("CST", 8*3600)
date := time.Date(2026, 2, 17, 0, 0, 0, 0, cst)
solar, err := calendar.SolarToLunar(date) // Gregorian to lunisolar
if err != nil {
log.Fatal(err)
}
fmt.Println(solar.Lunar().LunarYear(), solar.Lunar().MonthDay())
fmt.Println(calendar.JieQi(2026, calendar.JQ_立春)) // solar-term instant in UTC+08:00
}
Handle conversion errors before reading the result. Time.Lunar() returns the first lunar-calendar candidate; Time.Lunars() returns all candidates for periods with concurrent calendars. Solar terms use Beijing time.
API Reference
Gregorian and Lunisolar Conversion
| Name | Purpose | Units and convention |
|---|---|---|
SolarToLunar |
Gregorian time.Time to lunisolar |
the input is moved to UTC+8 before the civil day is taken; returns a Time that may hold several parallel-regime candidates |
SolarToLunarByYMD |
integer Gregorian year/month/day to lunisolar | never goes through time.Time, so a Julian-only leap day survives; negative years are BCE |
LunarToSolar |
lunisolar description string to Gregorian candidates | accepts year+month+day, era name+month+day, and the ... + ganzhi day forms; returns []Time |
LunarToSolarByYMD |
lunisolar year/month/day plus leap flag to Gregorian | month counts the distance from the first month, which is 1; leap is the leap-month flag |
LunarToSolarSingle |
the same, returning one Time |
deprecated, identical to LunarToSolarByYMD |
Solar |
lunisolar year/month/day to Gregorian under a custom zone | timezone is in hours; the returned zone is always named CST with the offset of the supplied timezone |
Lunar |
Gregorian year/month/day to lunisolar under a custom zone | returns (lunisolar year, month, day, leap flag, text description); timezone is in hours |
Time / LunarTime |
Gregorian-plus-lunisolar carriers | fields and JSON keys are listed under "Structure and JSON" |
// Prelude: date is a time.Time; the string entry point may return several parallel-regime results.
solar, _ := calendar.SolarToLunar(date)
fmt.Println(solar.LunarDesc(), solar.Lunar().MonthDay())
lunar, _ := calendar.LunarToSolar("元丰六年十月十二日")
for _, v := range lunar {
fmt.Println(v.Time().Format("2006-01-02"), v.Eras())
}
old, _ := calendar.LunarToSolarByYMD(-202, 1, 1, false) // negative years are BCE
fmt.Println(old.Solar().Format("2006-01-02"))
Julian-only Leap Days and Multiple Candidates
| Name | Purpose | Units and convention |
|---|---|---|
Time.JulianOnly |
whether the primary lunisolar day exists only in the Julian calendar | boolean; before 1582 this covers 29 February of years divisible by 100 but not 400 |
Time.JD |
exact Julian day of the primary lunisolar day | for a Julian-only leap day it is one day earlier than Solar(), otherwise equal to Date2JD(Solar()) |
Time.SolarCandidates |
every legal Gregorian candidate of that lunisolar day | the first element always equals Solar(); with a single candidate the slice holds only Solar() |
LunarTime.JulianOnly / LunarTime.JD |
the same information on one candidate | same convention as the methods on Time |
Date2JD |
time.Time to Julian day |
reads the year/month/day/hour/minute/second fields directly, with no time-zone conversion |
JD2Date |
Julian day to time.Time |
the result lands in the local zone of the host (the underlying helper uses Local) |
NowJD |
current Julian day | same convention as Date2JD |
Time.Add |
time offset | crossing the 1582 reform gap moves along the Julian-day axis and skips the ten missing days |
// The integer entry point keeps the leap day; 700-02-29 does not exist in Go's time.Time.
julian, _ := calendar.SolarToLunarByYMD(700, 2, 29)
fmt.Println(julian.JulianOnly(), julian.JD(), julian.Solar().Format("2006-01-02"))
// A dual-numbering reform yields several legal Gregorian candidates, Solar() still first.
res, _ := calendar.LunarToSolarByYMD(700, 11, 1, false)
fmt.Println(res.Solar().Format("2006-01-02"), len(res.SolarCandidates()))
fmt.Println(calendar.Date2JD(time.Date(2026, 2, 17, 0, 0, 0, 0, time.UTC)), calendar.JD2Date(2461088.5))
Solar Terms and Pentads
| Name | Purpose | Units and convention |
|---|---|---|
JieQi |
solar-term instant | Beijing time; term is the apparent solar longitude in degrees |
CalendricalJieQi |
calendar-compatible solar-term date | the date the term falls on under the default calendar, fixed at 00:00 Beijing time |
CalendricalJieQiWithCalendar |
calendar-compatible date with an explicit ancient calendar | the Chunqiu calendar and years without calendrical term data return an error |
WuHou |
pentad instant | Beijing time; the 72 pentads step by 5°, and the argument matches JieQi |
JQ_春分 … JQ_惊蛰 |
solar-term constants | apparent solar longitude on a 15° grid, usable directly as the term of JieQi/WuHou |
There are 24 constants, ordered by increasing longitude:
| Constant | Longitude | Constant | Longitude | Constant | Longitude |
|---|---|---|---|---|---|
JQ_春分 |
0° | JQ_清明 |
15° | JQ_谷雨 |
30° |
JQ_立夏 |
45° | JQ_小满 |
60° | JQ_芒种 |
75° |
JQ_夏至 |
90° | JQ_小暑 |
105° | JQ_大暑 |
120° |
JQ_立秋 |
135° | JQ_处暑 |
150° | JQ_白露 |
165° |
JQ_秋分 |
180° | JQ_寒露 |
195° | JQ_霜降 |
210° |
JQ_立冬 |
225° | JQ_小雪 |
240° | JQ_大雪 |
255° |
JQ_冬至 |
270° | JQ_小寒 |
285° | JQ_大寒 |
300° |
JQ_立春 |
315° | JQ_雨水 |
330° | JQ_惊蛰 |
345° |
// term is the apparent solar longitude in degrees; a raw number works too, 0 being the March Equinox.
fmt.Println(calendar.JieQi(2020, calendar.JQ_立春))
fmt.Println(calendar.JieQi(2020, calendar.JQ_冬至))
fmt.Println(calendar.WuHou(2020, calendar.JQ_立春+5))
termDate, err := calendar.CalendricalJieQi(1582, calendar.JQ_冬至)
fmt.Println(termDate, err)
Ancient Calendar Systems
| Name | Purpose | Units and convention |
|---|---|---|
AncientCalendarSystem |
ancient-calendar enumerator | a string type |
AncientCalendarDefault |
default routing (zero value) | selects by year; the explicit-calendar entry points fall back to the default implementation |
AncientCalendarChunqiu / AncientCalendarZhou / AncientCalendarLu |
pre-Qin Chunqiu, Zhou, and Lu calendars | values chunqiu/zhou/lu |
AncientCalendarHuangdi / AncientCalendarYin |
pre-Qin Huangdi and Yin calendars | values huangdi/yin |
AncientCalendarXia1 / AncientCalendarXia2 |
Xia calendar, winter-solstice and rain-water editions | values xia1/xia2 |
AncientCalendarZhuanxu |
pre-Qin Zhuanxu calendar | value zhuanxu |
AncientCalendarQinHan |
Qin/Han Zhuanxu calendar | value qin_han |
SolarToLunarWithCalendar |
Gregorian time.Time to lunisolar with an explicit calendar |
the input is moved to UTC+8 before the civil day is taken |
SolarToLunarByYMDWithCalendar |
integer Gregorian year/month/day to lunisolar with an explicit calendar | never goes through time.Time |
LunarToSolarWithCalendar |
lunisolar description to Gregorian with an explicit calendar | an era-name description works only when the ancient era table matches, otherwise it errors |
LunarToSolarByYMDWithCalendar |
lunisolar year/month/day plus leap flag with an explicit calendar | years without ancient-calendar data return an error |
// The zero value AncientCalendarDefault routes by year; an explicit calendar needs years that have data.
zhuanxu, err := calendar.SolarToLunarByYMDWithCalendar(-300, 1, 5, calendar.AncientCalendarZhuanxu)
fmt.Println(zhuanxu.Lunar().CalendarSystem(), zhuanxu.Lunar().CalendarName(), err)
back, err := calendar.LunarToSolarByYMDWithCalendar(
zhuanxu.Lunar().LunarYear(), zhuanxu.Lunar().LunarMonth(), zhuanxu.Lunar().LunarDay(),
zhuanxu.Lunar().IsLeap(), calendar.AncientCalendarZhuanxu)
fmt.Println(back.Solar().Format("2006-01-02"), err)
Ganzhi and Era Names
| Name | Purpose | Units and convention |
|---|---|---|
GanZhiOfYear |
sexagenary year name | the argument is a Gregorian year; returns a two-character name such as 甲子 |
GanZhiOfDay |
sexagenary day name | reads the year/month/day fields of t and evaluates the day at 00:00 Beijing time |
LunarTime.GanZhiYear / GanZhiMonth / GanZhiDay |
sexagenary year, month, and day of one candidate | a leap month inherits the month name of the preceding month |
Era |
era-table record | fields Year/Emperor/Nianhao/OtherNianHaoStart/Dynasty/Offset |
EraDesc |
era description | fields YearOfNianHao/Emperor/Nianhao/Dynasty |
EraDesc.String |
era-name string | the first year reads 元年 and later years use Chinese numerals plus 年 |
Time.Eras |
era information of every candidate | returns []EraDesc |
LunarTime.Eras |
era information of one candidate | returns []EraDesc |
ERR_NIANHAO_NOT_FOUND |
sentinel for an unknown era name | returned when the text entry point cannot resolve the era name |
fmt.Println(calendar.GanZhiOfYear(2026))
fmt.Println(calendar.GanZhiOfDay(time.Date(2026, 2, 17, 0, 0, 0, 0, time.UTC)))
res, _ := calendar.LunarToSolarByYMD(1083, 10, 12, false)
fmt.Println(res.Lunar().GanZhiYear(), res.Lunar().GanZhiMonth(), res.Lunar().GanZhiDay())
for _, era := range res.Eras() {
fmt.Println(era.Dynasty, era.Emperor, era.String())
}
Structure and JSON
| Name | Purpose | Units and convention |
|---|---|---|
Time |
Gregorian-plus-lunisolar carrier | Solar()/Time() give the Gregorian side, Lunar()/Lunars() the lunisolar candidates, LunarInfo() the structured records |
LunarTime |
one lunisolar candidate | LunarYear/LunarMonth/LunarDay/IsLeap plus MonthDay, ganzhi, zodiac, and calendar name |
LunarInfo |
structured lunisolar record | fields and JSON keys are listed below |
Time.Solar / Time.Time |
the stored Gregorian time.Time |
no further zone or calendar conversion; the two are synonyms |
Time.Lunar / Time.Lunars |
first / all lunisolar candidates | with no result Lunar returns a zero-value LunarTime |
Time.LunarInfo / LunarTime.LunarInfo |
structured record slice | several records when parallel era names exist |
Time.LunarDesc / Time.LunarDescWithEmperor / Time.LunarDescWithDynasty / Time.LunarDescWithDynastyAndEmperor |
description family over all candidates | all return []string |
LunarTime.LunarDesc / LunarDescWithEmperor / LunarDescWithDynasty / LunarDescWithDynastyAndEmperor |
description family over one candidate | all return []string |
LunarTime.LunarYear / LunarMonth / LunarDay / IsLeap |
lunisolar year, month, day, and leap flag | LunarMonth counts the distance from the first month |
LunarTime.MonthDay |
month-day description | month 11 is written 冬月 and month 12 腊月 |
LunarTime.ShengXiao / LunarTime.Zodiac |
Chinese zodiac | synonyms, computed from the lunisolar year modulo 12 |
LunarTime.CalendarSystem / LunarTime.CalendarName |
the ancient calendar and its Chinese name | empty strings for default routing and the modern range |
LunarInfo JSON keys and fields:
| JSON key | Field | Convention |
|---|---|---|
solarDate |
SolarDate |
Gregorian instant |
lunarYear / lunarYearChn |
LunarYear / LunarYearChn |
the civil-year mapping of the lunisolar year and its Chinese numeral form |
lunarMonth / lunarDay |
LunarMonth / LunarDay |
the month counts the distance from the first month; the day is within [1,30] |
isLeap |
IsLeap |
leap-month flag |
lunarMonthDayDesc |
LunarMonthDayDesc |
month-day description, month 11 as 冬月 and month 12 as 腊月 |
ganzhiYear / ganzhiMonth / ganzhiDay |
GanzhiYear / GanzhiMonth / GanzhiDay |
sexagenary year, month, and day |
calendarSystem / calendarName |
CalendarSystem / CalendarName |
ancient calendar and its name; empty strings for default routing |
jd |
JD |
exact Julian day of the lunisolar day |
julianOnly |
JulianOnly |
carries omitempty; only a Julian-only leap day emits true |
dynasty / emperor / nianhao / yearOfNianhao / eraDesc |
Dynasty / Emperor / Nianhao / YearOfNianhao / EraDesc |
dynasty, emperor, era name, and era regnal year |
lunarWithNianhaoDesc |
LunarWithEraDesc |
note that the field name differs from the JSON key |
chineseZodiac |
ChineseZodiac |
Chinese zodiac |
// Prelude: import "encoding/json".
res, _ := calendar.SolarToLunarByYMD(2026, 2, 17)
for _, info := range res.LunarInfo() {
fmt.Println(info.LunarYear, info.LunarMonthDayDesc, info.GanzhiDay, info.ChineseZodiac, info.JD)
}
data, _ := json.Marshal(res.LunarInfo()[0])
fmt.Println(string(data))
Usage examples
Gregorian to lunisolar and the sexagenary day
solar, _ := calendar.SolarToLunar(date) // date = 2026-02-17 00:00:00 CST
lunar := solar.Lunar()
fmt.Println(lunar.MonthDay(), lunar.LunarYear(), lunar.GanZhiDay())
fmt.Println(calendar.GanZhiOfDay(date))
正月初一 2026 壬戌
壬戌
SolarToLunar and GanZhiOfDay share one day-counting convention, so the last line equals GanZhiDay().
Parallel historical regimes can give one Gregorian day several lunisolar candidates (Lunar() returns the first and Lunars() all of them), see Usage notes; a leap month is marked separately by IsLeap(), see Parameter and result conventions.
Solar-term instants and calendrical dates
fmt.Println(calendar.JieQi(2020, calendar.JQ_立春)) // modern astronomical instant
termDate, err := calendar.CalendricalJieQi(1582, calendar.JQ_冬至) // calendrical date
fmt.Println(termDate, err)
2020-02-04 17:03:20.471614301 +0800 CST
1582-12-22 00:00:00 +0800 CST <nil>
JieQi returns the exact instant from modern astronomy while CalendricalJieQi returns the date the calendar assigns (always 00:00 Beijing time); the two are not interchangeable, and calendrical terms exist only for years that have table data (2026 returns an error). See Solar terms and Calendar notes.
Custom time zones and Julian-only leap days
fmt.Println(calendar.Solar(1985, 1, 1, false, 8.0), calendar.Solar(1985, 1, 1, false, 7.0))
julian, _ := calendar.SolarToLunarByYMD(700, 2, 29) // a Julian-only leap day
fmt.Println(julian.Solar().Format("2006-01-02"), julian.JulianOnly(), julian.JD())
1985-02-20 00:00:00 +0800 CST 1985-01-21 00:00:00 +0700 CST
0700-03-01 true 1.9767915e+06
New moons and solar terms are computed in Beijing time by default, so under UTC+7 one lunisolar day maps to a different Gregorian date (here the first day of month 1 moves from 20 February to 21 January, because a shifted new moon or solstice reorders the months); the timezone of Solar/Lunar is in hours. 700-02-29 exists only in the Julian calendar: Solar() always reports the following day and JD() holds the exact date, see Usage notes.
Several candidates for a dual-numbering reform
res, _ := calendar.LunarToSolarByYMD(700, 11, 1, false)
for _, c := range res.SolarCandidates() {
fmt.Println(c.Format("2006-01-02"))
}
0700-12-15
0700-10-17
The dual-numbering reforms (Wang Mang, Emperor Ming of Wei, Wu Zetian, Emperor Suzong of Tang) and the Taichu handoff give one lunisolar day two legal Gregorian days; the first line is the default candidate Solar() and SolarCandidates() returns them all, while a single-element slice is returned outside those windows, see Usage notes.
Ancient calendars and era-name conventions
zhuanxu, err := calendar.SolarToLunarByYMDWithCalendar(-300, 1, 5, calendar.AncientCalendarZhuanxu)
fmt.Println(zhuanxu.Lunar().CalendarSystem(), zhuanxu.Lunar().CalendarName(), err)
era, _ := calendar.LunarToSolarByYMD(1083, 10, 12, false)
fmt.Println(era.LunarDescWithEmperor())
zhuanxu 颛顼历 <nil>
[宋神宗 元丰六年十月十二 辽道宗 大康九年十月十二]
The explicit-calendar entry points work only for years that have data, and AncientCalendarDefault is the zero value that falls back to routing by year; CalendarSystem()/CalendarName() are empty under default routing and in the modern range, and era names come from Eras()/LunarDescWithEmperor(), see Calendar notes and Ancient Calendar Systems.
Calendar notes
-
Default routing: the package selects by year automatically.
The pre-Qin range uses reconstructed Chunqiu and ancient-six-calendar systems,
-220..-104uses the Qin/Han Zhuanxu calendar,-103..1912uses calendar tables, and1913onward uses the modern algorithm. -
Explicit ancient calendars: use APIs such as
SolarToLunarWithCalendar/LunarToSolarWithCalendarwhen a specific ancient calendar system is required. -
Data sources: ancient-calendar support mainly references 《寿星天文历》; Professor ytliu0's ChineseCalendar data is used for validation.
The modern range follows GB/T 33661-2017 and uses VSOP87/ELP computations for solar terms and new moons.
-
Solar terms:
JieQireturns modern astronomical solar-term instants;CalendricalJieQireturns calendar-compatible solar-term dates.
Usage notes
1. One Gregorian date may map to several lunisolar dates
In periods when several regimes coexisted with different calendars (the Three Kingdoms, for example), one Gregorian date can map to several lunisolar dates. The package returns every conversion it can determine.
2. One lunisolar date may map to several Gregorian dates
Rival calendars are not the only cause: one regime can produce the same effect during a calendar reform. After Wu Zetian's reform, for example, the third year of Shengli had two twelfth months.
3. Gregorian calendar rules
All computation goes through Julian Days. The Gregorian side follows these rules:
- after
1582-10-15: Gregorian calendar - before
1582-10-04: Julian calendar - before year 8 CE: proleptic Julian calendar
- the day after
1582-10-04is1582-10-15 1582-10-05through1582-10-14do not exist and the corresponding entry points reject them- year numbering: year
0is 1 BCE, year-1is 2 BCE, and so on
4. Time zone
The package targets the Chinese calendar, so solar terms and new moons are computed in Beijing time (UTC+8) by default. Applying Chinese lunisolar rules in another time zone can shift dates.
For exploration and research, the lower-level Solar and Lunar methods convert between the Gregorian and lunisolar calendars under a custom time zone, following the current Chinese calendar algorithm (GB/T 33661-2017).
For standard conversions in Beijing time, use the wrapped SolarToLunar and LunarToSolar methods.
Example: the calendar rules require the winter solstice to fall in the eleventh lunisolar month. For the 1984 winter solstice:
ws := calendar.JieQi(1984, 270)
fmt.Println(ws)
fmt.Println(moon.ClosestShuoYue(ws))
| Event | UTC+8 | UTC+7 |
|---|---|---|
| Winter solstice | 1984-12-22 | 1984-12-21 |
| New moon | 1984-12-22 | 1984-12-22 |
In UTC+8 (China), 1984-12-22 is both the winter solstice and the new moon, so it is the first day of the eleventh lunisolar month; in UTC+7 the solstice moves up to 12-21, which makes 12-22 the first day of the twelfth month.
Chinese New Year can differ by a month as well. Under this calendar convention, the first day of the first lunisolar month of 1985 maps to February 20 in UTC+8 and January 21 in UTC+7:
fmt.Println(calendar.Solar(1985, 1, 1, false, 8.0))
fmt.Println(calendar.Solar(1985, 1, 1, false, 7.0))
5. Go-specific note
⚠️ Go's standard-library time.Time differs from this package in calendar handling:
- Before 1582-10-15 Go uses the proleptic Gregorian calendar rather than the Julian calendar. Without
Add, it generally works as expected. - So before 1582-10-15,
time.Time.Weekday()does not agree with this package: for 1582-10-04 this package reports Thursday, Go reports Monday.
Weekdays and date arithmetic
To obtain the weekday this package uses:
// date should be the local midnight of the target day.
weekday := int(calendar.Date2JD(date)+1.5) % 7
// 0 means Sunday, 1 means Monday, ..., 6 means Saturday.
Using Add or AddDate on a time.Time before 1582 runs through the proleptic Gregorian calendar and lands one day away from the Julian calendar whenever it crosses a leap day that only the Julian calendar has.
For example, 700 is a leap year in the Julian calendar but not in Go's proleptic Gregorian calendar.
6. Julian-only leap days (for example 700-02-29)
Before 1582, years divisible by 100 but not 400 (such as 100, 700 or 1500) have a 29 February in the Julian calendar,
while Go's time.Time uses the proleptic Gregorian calendar and has no such day (time.Date(700, 2, 29, ...) normalises to 700-03-01). The library accepts 700-02-29 as an existing day, with these constraints:
Time.Solar()/LunarTime.SolarDate: the library's canonical output, always the following day (700-03-01 for 700-02-29), matchingbasic.JD2DateByZone;Time.JulianOnly(): whether the lunar date exists only in the Julian calendar (JSON fieldjulianOnly);Time.JD(): the exact Julian day; for a Julian-only leap day it is one day earlier thanSolar(), otherwise the two agree (JSON fieldjd).
julian, _ := calendar.SolarToLunarByYMD(700, 2, 29)
fmt.Println(julian.Solar().Format("2006-01-02"), julian.JulianOnly(), julian.JD(), julian.Lunar().MonthDay())
// 0700-03-01 true 1.9767915e+06 二月初五
Two kinds of entry point keep this leap day: the integer year/month/day entry points
SolarToLunarByYMD/LunarToSolarByYMD, and a directbasic.JDCalc(700, 2, 29)call returning the exact Julian day1976791.5; building atime.Timefirst is the step that loses it.
7. Several Gregorian candidates for one lunar date
The dual-numbering reforms (Wang Mang 9-23 CE, Emperor Ming of Wei 237-240 CE, Wu Zetian 689-700 CE, Emperor Suzong of Tang 761-762 CE)
and the Taichu calendar handoff (104 BCE) give one lunar date two legal Gregorian days. Solar() remains the library default, and
SolarCandidates() returns every candidate with the first element always equal to Solar():
res, _ := calendar.LunarToSolarByYMD(700, 11, 1, false)
fmt.Println(res.Solar().Format("2006-01-02"))
fmt.Println(len(res.SolarCandidates()))
// 0700-12-15
// 2
Outside the reform windows there is a single candidate and SolarCandidates() returns a one-element
slice; a Julian-only leap day also reports only its canonical output, because its single legal day
cannot be expressed as a time.Time - use JD() for the exact date.
Calendar conversion
Gregorian to lunar
- Input: a Gregorian date, as
time.Time - Output: a
calendar.Timeobject, holding one or more matching lunisolar dates - The result carries:
- a full lunisolar date description
- sexagenary (ganzhi) year, month, and day
- dynasty, emperor, and era name
- the complete structured lunisolar record
Lunar to Gregorian
Two calling styles:
Style 1: pass a lunisolar string
Formats:
era name + year + month + day, for example"元丰六年十月十二"(prefix a leap month with闰; day names read初一,二十, and so on)era name + year + month + ganzhi day, for example"元嘉二十七年七月庚午"year + month + day, for example"二零二五年正月初一"(prefix a leap month with闰; suits modern dates)year + month + ganzhi day, for example"二零二五年正月戊戌日"Arabic digits + month + day: Chinese numerals may be written as Arabic digits, for example"2025年1月1日", which stands for二零二五年正月初一- Historical cases: month names could differ from modern usage (under Wu Zetian,
正月and一月denoted different months); such month names are read as Chinese numerals
⚠️ A lunisolar year and a Gregorian year do not line up exactly. 2025-01-28, Chinese New Year's Eve, is the 29th day of the 12th lunisolar month of 2024, so its string is
"二零二四年腊月廿九".
Style 2: pass numeric fields
- Parameters: year (
int), month (int), day (int), leap-month flag (bool) - Semantics: locate the date by lunisolar year, month, day, and the leap-month flag; for modern conversions
One lunisolar day can have several legal Gregorian candidates: Solar() takes the first, SolarCandidates() returns all of them (see note 7 above).
Code example
package main
import (
"b612.me/astro/calendar"
"encoding/json"
"fmt"
"time"
)
func main() {
cst := time.FixedZone("CST", 8*3600)
// Example 1: Gregorian to lunisolar. This date is in the Three Kingdoms period, so multiple parallel-calendar results are returned.
date := time.Date(240, 1, 1, 8, 8, 8, 8, cst)
lunar, _ := calendar.SolarToLunar(date)
fmt.Println(lunar.LunarDescWithEmperor())
// Structured lunisolar information for each matching historical result.
info := lunar.LunarInfo()
data, _ := json.MarshalIndent(info, "", " ")
fmt.Println(string(data))
// Example 2: lunisolar to Gregorian by string. This is the date from Su Shi's 《记承天寺夜游》.
solar, _ := calendar.LunarToSolar("元丰六年十月十二日")
for _, v := range solar {
fmt.Println(v.Time())
fmt.Println(v.LunarDescWithEmperor())
}
// Example 3: lunisolar to Gregorian by numeric fields. 2026 month 1 day 1 is Chinese New Year.
modernDate, _ := calendar.LunarToSolarByYMD(2026, 1, 1, false)
fmt.Println(modernDate.Time())
}
Output:
[魏明帝 景初三年腊月二十 蜀后主 延熙二年冬月十九 吴大帝 赤乌二年冬月二十] // one Gregorian instant maps to parallel Three Kingdoms lunisolar results
[
{
"solarDate": "0240-01-01T08:08:08.000000008+08:00",
"lunarYear": 239,
"lunarYearChn": "二三九",
"lunarMonth": 12,
"lunarDay": 20,
"isLeap": false,
"lunarMonthDayDesc": "腊月二十",
"ganzhiYear": "己未",
"ganzhiMonth": "丙子",
"ganzhiDay": "辛未",
"calendarSystem": "",
"calendarName": "",
"jd": 1808717.8389814815,
"dynasty": "魏",
"emperor": "魏明帝",
"nianhao": "景初",
"yearOfNianhao": 3,
"eraDesc": "景初三年",
"lunarWithNianhaoDesc": "景初三年腊月二十",
"chineseZodiac": "羊"
},
{
"solarDate": "0240-01-01T08:08:08.000000008+08:00",
"lunarYear": 239,
"lunarYearChn": "二三九",
"lunarMonth": 11,
"lunarDay": 19,
"isLeap": false,
"lunarMonthDayDesc": "冬月十九",
"ganzhiYear": "己未",
"ganzhiMonth": "丙子",
"ganzhiDay": "辛未",
"calendarSystem": "",
"calendarName": "",
"jd": 1808717.8389814815,
"dynasty": "蜀",
"emperor": "蜀后主",
"nianhao": "延熙",
"yearOfNianhao": 2,
"eraDesc": "延熙二年",
"lunarWithNianhaoDesc": "延熙二年冬月十九",
"chineseZodiac": "羊"
},
{
"solarDate": "0240-01-01T08:08:08.000000008+08:00",
"lunarYear": 239,
"lunarYearChn": "二三九",
"lunarMonth": 11,
"lunarDay": 20,
"isLeap": false,
"lunarMonthDayDesc": "冬月二十",
"ganzhiYear": "己未",
"ganzhiMonth": "丙子",
"ganzhiDay": "辛未",
"calendarSystem": "",
"calendarName": "",
"jd": 1808717.8389814815,
"dynasty": "吴",
"emperor": "吴大帝",
"nianhao": "赤乌",
"yearOfNianhao": 2,
"eraDesc": "赤乌二年",
"lunarWithNianhaoDesc": "赤乌二年冬月二十",
"chineseZodiac": "羊"
}
] // structured lunisolar records, one object per matching historical result
1083-11-24 00:00:00 +0800 CST // Gregorian date corresponding to 元丰六年十月十二日
[宋神宗 元丰六年十月十二 辽道宗 大康九年十月十二] // the same day also matches a Liao calendar result
2026-02-17 00:00:00 +0800 CST // Chinese New Year in 2026
Solar terms
JieQi(year, term) returns the exact solar-term instant computed by the modern astronomical algorithm.
CalendricalJieQi(year, term) returns the date on which the solar term falls under the default calendar, fixed at 00:00 Beijing time for that day.
Use CalendricalJieQiWithCalendar(year, term, system) when a specific ancient calendar system is required.
package main
import (
"fmt"
"b612.me/astro/calendar"
)
func main() {
// Beginning of Spring in 2020. Solar-term constants correspond to apparent solar longitude.
fmt.Println(calendar.JieQi(2020, calendar.JQ_立春))
// Winter Solstice in 2020.
fmt.Println(calendar.JieQi(2020, calendar.JQ_冬至))
// March Equinox in 2020.
fmt.Println(calendar.JieQi(2020, calendar.JQ_春分))
// Direct longitude input is also supported; 0 degrees is the March Equinox.
fmt.Println(calendar.JieQi(2020, 0))
}
Output:
2020-02-04 17:03:20.471614301 +0800 CST // Beginning of Spring
2020-12-21 18:02:20.648710727 +0800 CST // Winter Solstice
2020-03-20 11:49:37.149532735 +0800 CST // March Equinox
2020-03-20 11:49:37.149532735 +0800 CST // same result from direct longitude input
Calendrical solar-term example:
date, err := calendar.CalendricalJieQi(1582, calendar.JQ_冬至)
fmt.Println(date, err)
date, err = calendar.CalendricalJieQiWithCalendar(-202, calendar.JQ_冬至, calendar.AncientCalendarQinHan)
fmt.Printf("%d-%02d-%02d %v\n", date.Year(), int(date.Month()), date.Day(), err)
Output:
1582-12-22 00:00:00 +0800 CST <nil>
-202-12-25 <nil>
Parameter and result conventions
Units and Parameter Conventions
- Year numbering is astronomical:
0is 1 BCE,-1is 2 BCE; Gregorian results are supported from 721 BCE through 3000 CE. - The
timezoneargument ofSolarandLunaris in hours:8.0is UTC+8 and7.0is UTC+7.JieQi,WuHou, andCalendricalJieQi*take no time zone and always output Beijing time (UTC+8). - The lunisolar month counts the distance from the first month: month 1 is
正月, month 2 is二月, and so on. A leap month is marked by the separateleap/IsLeapflag and is never inferred from the month number. - The lunisolar year argument uses the civil year as a proxy but starts at the lunisolar new year: the 30th day of the 12th month of the Ji-Hai year must be passed as
Solar(2019, 12, 30, false, 8), notSolar(2020, 12, 30, false, 8). Date2JDreads only the year/month/day/hour/minute/second fields of atime.Timeand performs no time-zone conversion, so Beijing midnight and UTC midnight of the same civil date give the same Julian day;JD2Datereturns a time in the local zone of the host.
Time Scale
-
Every public instant is treated as a civil value, meaning the fields of the
time.Timeare read directly as a UTC label.When the library converts civil time to TT, values before 1972-01-01 are treated as UT1 and the exact window uses the built-in leap-second table; for explicit conversion use the root package's
astro.UT1FromUTC/astro.TTFromUTC/astro.DUT1. -
JieQiandWuHoureturn civil instants in Beijing time with full fractional-second precision;CalendricalJieQi*always returns 00:00 Beijing time on the matching day. -
JD,Date2JD, andNowJDare all Julian-day floats on that same civil convention: the Beijing civil day2026-02-17corresponds to2461088.5.
Zero Values and Out-of-Range Input
- The ten Gregorian days from 1582-10-05 through 1582-10-14 do not exist; the affected entry points report an error instead of silently normalising to a neighbouring date.
SolarToLunar/SolarToLunarByYMDreturn an error outside[-721,3000].LunarToSolar*is limited to the same Gregorian range, and the boundary lunisolar year-722is accepted while its result still falls inside that range.CalendricalJieQiWithCalendarreturns an error for the Chunqiu calendar and for years without calendrical solar-term data;LunarToSolarWithCalendaraccepts an era-name description only when the ancient era table matches and errors otherwise.SolarCandidates()returns a one-element slice holdingSolar()when there is no second candidate, never an empty slice;Lunar()returns a zero-valueLunarTimewhen there is no candidate, and itsJulianOnly()then reportsfalse.LunarToSolarSingleis deprecated, useLunarToSolarByYMD.AncientCalendarDefaultis the zero value, and the explicit-calendar entry points fall back to the default implementation when they receive it.LunarTime.CalendarSystem()andCalendarName()return empty strings under default routing and in the modern range, which means "no explicit ancient calendar" rather than an error.
Accuracy and Applicability
-
The modern range follows the current Chinese calendar standard GB/T 33661-2017 and is recommended for
[1929,3000].Solar/Lunaruse the same new-moon and solar-term rules and merely allow a custom time zone, so outside the recommended years the dates can differ from historical records. -
[-103,1912]uses the built-in calendar tables,[-220,-104]uses the reconstructed Qin/Han Zhuanxu calendar, and[-721,-221]uses the reconstructed default pre-Qin calendars.All three ranges are reconstructions and are not guaranteed to match every day actually promulgated at the time.
-
New moons and solar terms are computed with modern astronomy, so for ancient dates their instants differ from contemporary observation and the resulting lunisolar dates can differ from the historical record.
-
The ancient-calendar reconstruction stops at the level of whole days:
CalendricalJieQi*returns the calendar-compatible date whileJieQireturns the modern astronomical instant, and the two are not interchangeable.
Related Manuals
The astronomical definition of a solar term (apparent solar longitude) and the determination of new moons are covered by Sun and Moon and Sun and Moon; those pages give modern astronomical quantities, while this manual gives the calendrical arrangement.