chinese-calendar converts between the Chinese lunar and Gregorian calendars in PHP
:date: 中国农历(阴历)与阳历(公历)转换与查询工具
At a glance
- What is it?
- chinese-calendar is a PHP library for converting and querying the Chinese lunar calendar, returning the full set of derived values in one call, including the sexagenary cycle pillars, the five elements, the zodiac animal, the solar term and the constellation. It is MIT licensed, has no dependencies beyond ext-mbstring, and its 2.x line targets PHP 8.5 and above.
- Who is it for?
- chinese-calendar suits a PHP application that has to display or process lunar dates correctly, since one call returns every derived value rather than making you recompute the pillars or the solar term, and the data has been checked day by day against the Hong Kong Observatory table.
- Can I use it commercially?
- Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 14 days ago.
- What is it written in?
- Mainly PHP, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 22, 2026, and from our analysis. They are not legal advice.
Editorial analysis
What the library returns in one call
The library's Chinese name in the repository description describes it as a conversion and query tool for the Chinese lunar calendar, and the feature list is specific about what one call gives you.
It supports bidirectional conversion between the Gregorian calendar and the lunar calendar for the range 1900-01-31 to 2100-12-31, and a single call returns the complete information: the lunar date, the sexagenary cycle pillars, the five elements, the zodiac animal, the solar term, the constellation, the Chinese character representation and more.
That last point is the design decision that matters. A library that returned only the lunar year, month and day would leave you to derive the ganzhi pillars, the wuxing, the animal and the jieqi yourself, and each of those is a place to get the boundary conditions wrong. Here every one of them comes out of the same call.
The result array shows the shape of it:
'lunar_year' => '2017', // 农历年
'lunar_month' => '04', // 农历月
'lunar_day' => '10', // 农历日
'ganzhi_year' => '丁酉', // (干支)年柱
'ganzhi_month' => '乙巳', // (干支)月柱
'ganzhi_day' => '壬辰', // (干支)日柱
'wuxing_year' => '火金', // (五行)年
'color_year' => '红', // (颜色)年
'animal' => '鸡', // 生肖
'term' => '立夏', // 节气The Chinese character forms are there alongside the numeric ones, so lunar_year_chinese gives 二零一七, lunar_month_chinese gives 四月 and lunar_day_chinese gives 初十. The wuxing values are pairs such as 火金 for the year pillar and 水土 for the day pillar, and the colour field gives the single character that the element maps to.
The remaining fields cover the Gregorian side, the week, a week name in Chinese, a flag for whether the date is today, a constellation, a flag for whether the lunar and Gregorian years coincide, and a leap month flag. The hour fields are null unless you pass an hour, which is why the example passes one.
The primary language is PHP, the licence is MIT, and the last push to the repository was on 2026-10-09, with version 2.0.0 released on 2026-08-26.
How you call it
The usage is three lines of construction and a method call.
use Overtrue\ChineseCalendar\Calendar;
$calendar = new Calendar();
$result = $calendar->solar(2017, 5, 5); // 公历 -> 农历
$result = $calendar->lunar(2017, 4, 10); // 农历 -> 公历
$result = $calendar->solar(2017, 5, 5, 23); // 带小时参数(23 点按晚子时归入次日)The two directions are solar() for Gregorian to lunar and lunar() for lunar to Gregorian, and the optional fourth argument to solar() is the hour, with the note that 23 o'clock is treated as the late zi hour and belongs to the following day.
Installation is a single Composer command:
composer require overtrue/chinese-calendarThe wider API is worth knowing about, because the library is not limited to the two conversion calls. There are solar2lunar() and lunar2solar() which return only the lunar or only the Gregorian parts, so you can skip the rest of the array when you only need a date. There is leapMonth() and leapDays() for which month is the leap month in a given lunar year and how long it is, and lunarDays(), daysOfYear() and monthsOfYear() for the shape of a lunar year, plus solarDays() for the length of a Gregorian month.
The solar term accessor is getTerm($year, $no), where the number one is 小寒, and the README notes that the numbering follows the order of the twenty-four terms. The diffInDays() method takes two lunar dates and returns the number of days between them, with an $absolute parameter that defaults to true, and the same family includes diffInMonths() and diffInYears().
Arithmetic is also covered: addDays, subDays, addMonths, subMonths, addYears and subYears all operate on lunar dates.
Everything else is in the source, as the README says, and the API surface is a reason to read it rather than to rely on the summary table alone.
The conventions, and the cases where other libraries disagree
The section of the README on conventions and caveats is the most valuable part of it, because it is where a calendar library either earns trust or loses it.
The first convention concerns the hour. Passing $hour = 23 treats the time as the late zi hour and assigns it to the following day, so 四月初十 at 23 o'clock gives 四月十一, and the day pillar and hour pillar are computed accordingly. That behaviour is traced to issue 13, so it is a decision that was argued about rather than an accident.
The second concerns the year boundary. The ganzhi_year and animal values use the first day of the first lunar month as the dividing line, which is consistent with lunar_year. The README then points out that the convention in fortune-telling practice, which uses 立春 as the boundary, is a different one and that you should convert it yourself.
The third is the error behaviour. Out-of-range or invalid arguments throw InvalidArgumentException or TypeError, and the README stresses that they will not silently return a wrong result. That is worth more than it sounds for a calendar library, because a silent wrong result in a date calculation is very hard to find later.
The fourth is the interesting one. In 2057, the first day of the ninth lunar month differs between implementations, because the new moon occurs at around 23:59 Beijing time on 2057-09-28, only a few seconds short of midnight. This library adopts the Hong Kong Observatory's result, which is 2057-09-28.
That is a genuinely hard edge case and the README describes it rather than hiding it. A library that quietly picked a different answer would be indistinguishable from one that was correct until someone checked that specific date against an authority, and the choice of authority is exactly the kind of thing you want stated in advance.
The other conventions worth noting are structural rather than calendrical. All calculations are fixed to Beijing time in the Asia/Shanghai zone, and are independent of the process default timezone set by date_default_timezone_set(). That matters on a server whose default is UTC, where a naive implementation would be off by a day for part of the day, and it is handled here by design.
How the data was checked
The third feature in the README is about verification, and it is the strongest signal of how the library should be trusted.
The algorithm is pure integer Julian day arithmetic with zero dependencies apart from ext-mbstring. There is no calendar library underneath, so there is no inherited bug and no version drift to worry about.
The lunar big and small month data and the twenty-four solar terms data have been checked day by day against the Hong Kong Observatory's Gregorian and Lunar calendar conversion table for the period 1901 to 2100, and they are included in the test suite as fixtures. The README says that any data regression will be caught directly by CI.
That is the part worth pausing on. A fixture covering every day of two centuries is not a sample, it is the whole domain. It means a wrong leap month, a wrong solar term boundary or an off-by-one in the new moon calculation fails the build rather than shipping.
The test commands are three:
composer test # PHPUnit(含香港天文台全量对照 fixture)
composer check-style # laravel/pint --test
composer phpstan # 静态分析So the suite is PHPUnit with the Observatory fixtures, the style check is laravel/pint in test mode, and static analysis runs through phpstan. The repository layout backs that up, with phpunit.xml.dist, pint.json, phpstan.neon.dist and infection.json5, the last being the configuration for mutation testing.
The mutation testing configuration is the interesting extra. Infection mutates the source to check that the tests actually fail when the code is wrong, and having it in the repository means the fixture coverage is treated as a property worth defending rather than an incidental.
The reference list credits where the data came from. The algorithm and data originate partly in a JavaScript implementation of Gregorian and lunar conversion for 1900 to 2100, and the Hong Kong Observatory's conversion table is named as the validation source for the lunar and solar term data, with Wikipedia pages on the lunar calendar, the sexagenary cycle, the zodiac and the constellations cited as references.
CI runs on the ci.yml workflow, and the CI badge sits at the top of the README.
The 1.x to 2.x upgrade, and where the project stands
The 2.x release is recent and the upgrade notes are precise about what changed.
The calculation results in 2.0 are completely identical to 1.1.0, and there are several incompatible changes.
PHP 8.5 or above is now required, the source enables strict_types, and all methods have complete parameter and return types. The deprecated second parameter $termIndex has been removed from ganZhiYear() and getAnimal(). The diffInDays() method returns an int where it previously returned a numeric string, and getTerm() does the same. And invalid arguments now throw TypeError or InvalidArgumentException instead of silently producing wrong results.
That list is a good illustration of why a major version was warranted. The results did not change, but the contract did, in ways that break code without looking like a breaking change at runtime. A diffInDays() that returned 5 as a string and now returns 5 as an int will pass a loose comparison and fail a strict one, which is the class of bug that only shows up in production.
The version support table sets out the branches:
The 2.x branch requires PHP 8.5 or above, sits on master, and is the current development version. The 1.x branch requires PHP 5.5.9 or above, sits on the 1.x branch, and accepts only defect fixes.
So the 1.x line is a maintenance branch for the many projects still on old PHP, and the fact that it is explicitly bug-fix-only tells you what the maintainer's intent is. The repository carries a CHANGELOG.md, a CONTRIBUTING.md, a SECURITY.md and an AGENTS.md, and the default branch is master, which is where the 2.x work happens.
The release history shows the 2.x work concentrating in one week: 1.0.3 on 2026-08-22, then 1.1.0 on 2026-08-26, then 2.0.0 the same day. The last push was on 2026-10-09, so work has continued after the release rather than stopping at it.
Where chinese-calendar fits, and where it stops
The comparison worth making is with a general purpose date library with a lunar calendar plugin, and with a table lookup approach.
Against a general purpose date library, the difference is that this library is built around the lunisolar calendar rather than adapting it. The Julian day arithmetic, the leap month rules, the twenty-four solar terms and the sexagenary cycle are all first class here, and the derivations come out of one call instead of being assembled from several helper objects. The cost is scope: this is a calendar conversion library and nothing else, so you still need another library for time zones, durations and parsing.
Against a lookup table, the difference is coverage and reasoning. A table of precomputed conversions covering the range you need is fast and simple, and it goes wrong the moment a date falls outside it. The pure integer arithmetic here covers 1900-01-31 to 2100-12-31, and the whole domain is verified against an authority in CI.
The limits are worth stating plainly. The supported range has hard edges, and outside it the library throws rather than approximating. The 2057 ninth month case shows that for dates at the very edge of the algorithms' agreement, there is a defensible answer and a different defensible answer, and this library picks the Hong Kong Observatory's. The ganzhi year boundary convention differs from the one used in fortune-telling practice, and the README is direct that you have to convert it yourself if that is the convention you need. And the hour handling has a rule of its own, with 23 o'clock belonging to the next day.
There is also a licensing point that matters for a calendar library specifically. The MIT licence covers the code, and the data was validated against the Hong Kong Observatory's published conversion table, which the README credits as the validation source. Anyone reusing the underlying lunar and solar term data commercially should read both that credit and the licence file rather than assume the MIT grant covers the data provenance.
On maintenance, the project is actively worked, with the last push on 2026-10-09 and a 2.0 release in August 2026, a CI workflow, mutation testing configuration and static analysis in the repository. The 1.x branch remains available for projects that cannot move to PHP 8.5.
Editorial conclusion
chinese-calendar suits a PHP application that has to display or process lunar dates correctly, since one call returns every derived value rather than making you recompute the pillars or the solar term, and the data has been checked day by day against the Hong Kong Observatory table. It is the wrong tool if you need dates outside 1900 to 2100, and it is the wrong tool for the 2057 new-moon boundary case, where the library deliberately follows the Hong Kong Observatory rather than the majority of other implementations. Before you adopt the 2.x line, read the upgrade section, because it requires PHP 8.5, adds strict types and changes diffInDays() and getTerm() to return integers rather than numeric strings.
Frequently asked questions
What does the chinese-calendar PHP library do?
It converts and queries the Chinese lunar calendar against the Gregorian calendar for the range 1900-01-31 to 2100-12-31, and a single call returns the full information including the lunar date, the sexagenary cycle pillars, the five elements, the zodiac animal, the solar term, the constellation and the Chinese character representations.
How do I install and use chinese-calendar?
Run composer require overtrue/chinese-calendar, then create a Calendar instance and call solar() for Gregorian to lunar or lunar() for lunar to Gregorian. An optional hour argument is accepted, with 23 o'clock treated as the late zi hour and assigned to the following day.
How was the lunar and solar term data verified?
The lunar big and small month data and the twenty-four solar terms were checked day by day against the Hong Kong Observatory's Gregorian and lunar calendar conversion table for 1901 to 2100, and the results are included in the test suite as fixtures, so any data regression is caught by CI.
What changed between chinese-calendar 1.x and 2.0?
The calculation results are identical, but 2.0 requires PHP 8.5 or above, enables strict_types, adds parameter and return types, removes the deprecated $termIndex parameter from ganZhiYear() and getAnimal(), changes diffInDays() and getTerm() to return ints rather than numeric strings, and throws TypeError or InvalidArgumentException on invalid input instead of silently returning a wrong result.
Which timezone does chinese-calendar calculate in?
All calculations are fixed to Beijing time in the Asia/Shanghai zone, and are independent of the process default timezone set by date_default_timezone_set(), so a server running in UTC still gets the correct lunar date.
How does the library handle the disputed 2057 new moon?
The first day of the ninth lunar month in 2057 differs between implementations, because the new moon falls at around 23:59 Beijing time on 2057-09-28. The library adopts the Hong Kong Observatory's result, which is 2057-09-28.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/overtrue-chinese-calendar)Was this useful?