Calendrical.Lunisolar (Calendrical v2.2.0)

Copy Markdown

Shared base implementation for Calendrical's lunisolar calendars.

A lunisolar calendar approximates the tropical year using lunar months, inserting an intercalary (leap) month roughly every three years to keep the calendar aligned with the seasons. Calendrical implements three lunisolar calendars on top of this module:

Each implementation is a thin wrapper that supplies its epoch and an observation-location function (latitude, longitude, altitude, time-zone offset). All of the astronomical heavy lifting — winter solstice, mean and true new moon, leap-month detection, sexagesimal cycle calculations — is delegated to Astro and lives in this module.

This module is not intended to be called directly from application code. Use one of the lunisolar calendar modules above and the standard Date and Calendar APIs.

Naming conventions

Several "year" and "month" concepts coexist in a lunisolar calendar. To keep the code unambiguous, this module uses the following names consistently:

  • year is the calendar year, counted as the number of years since the calendar's epoch.

  • cyclical_year is the position within the 60-year sexagesimal cycle (1..60).

  • cycle is the number of completed sexagesimal cycles since the epoch.

  • month is the ordinal month of the calendar year — 1..12 in an ordinary year and 1..13 in a leap year. This is the value the Calendar behaviour expects.

  • lunar_month is the traditional month number, 1..12 in all years, with leap months represented as {month, :leap}. This is the value users see in cultural contexts and the form returned by Calendrical.localize/3.

References

  • Reingold and Dershowitz, Calendrical Calculations: The Ultimate Edition, 4th ed., chapters on the Chinese, Korean and Japanese calendars.

  • The accompanying Common Lisp source distributed with the same book.

Summary

Types

A sexigesimal cycle number

A lunar month

Functions

Returns the day of the Lunar New Year in a Gregorian year, at the calendar's place.

Returns the last Chinese major solar term (zhongqi) before iso_days.

Returns the last minor solar term (jieqi) before iso_days.

Returns the ISO day number of a lunisolar date whose year is given as a cycle and the year within the cycle, and whose months are ordinal.

Returns the lunisolar date of an ISO day number, with ordinal months that run from 1 to 12, or 1 to 13 in a leap year.

Returns the ISO day number of a lunisolar date whose months are ordinal: numbered 1 to 12, or 1 to 13 in a leap year, with the leap month unmarked.

Returns the sexagesimal name of a day.

Returns the latest day on or before date that has the given sexagesimal name.

Returns the day of the winter solstice on or before iso_days, at the calendar's place.

Returns the Gregorian date for the lunar month and day in a given Gregorian year.

Returns the ordinal number of a year's leap month.

Returns whether the given month of a year is its leap month.

Returns whether month is the leap month of the year given as a cycle and the year within the cycle.

Returns whether the given year is a leap year.

Returns the place a lunisolar calendar is reckoned at on a day, as the point Astro takes.

Returns the moment of the first major solar term (zhongqi) on or after iso_days. The major terms begin when the sun's longitude is a multiple of 30 degrees.

Returns the universal time of the (clock) midnight that begins iso_days at the calendar's place.

Returns the moment of the first minor solar term (jieqi) on or after iso_days. The minor terms begin when the sun's longitude is an odd multiple of 15 degrees.

Returns the sexagesimal name of a month of a Chinese year.

Returns the number of names from one Lunar name to the next occurrence of another.

Creates a date in the lunisolar calendar as an ISO day number.

Returns the day of the first new moon before iso_days, at the calendar's place.

Returns the day of the first new moon on or after iso_days, at the calendar's place.

Returns the day of the Lunar New Year in the sui (the period from one winter solstice to the next) that contains iso_days.

Returns the day of the Lunar New Year on or before iso_days, at the calendar's place.

Returns whether the lunar month starting on iso_days has no major solar term, as only a leap month has none.

Returns whether there is a leap month from the lunar month starting on m_prime up to and including the lunar month starting on m.

Return moment at location of the first date on or after iso_days when the solar longitude will be lambda degrees.

The proleptic-Gregorian date on which the sun reaches the index-th solar term (jié-qì) during Gregorian year, observed at location_fun.

The name of the solar term (jié-qì) at a 1-based index.

Returns the name of a year in the Lunar sexagesimal cycle, as its celestial stem and terrestrial branch.

Types

cycle()

@type cycle() :: pos_integer()

A sexigesimal cycle number

lunar_month()

@type lunar_month() :: Calendar.month() | {Calendar.month(), :leap}

A lunar month

Functions

chinese_new_year_for_gregorian_year(gregorian_year, location_fun)

Returns the day of the Lunar New Year in a Gregorian year, at the calendar's place.

Arguments

  • gregorian_year is a Gregorian year.

  • location_fun is a 1-arity function of the ISO day number that returns {latitude, longitude, altitude, offset}, the place the calendar is reckoned at, as a calendar's location/1 does.

Returns

  • The ISO day number of the new year.

Examples

iex> location = &Calendrical.Chinese.location/1
iex> Calendrical.Lunisolar.chinese_new_year_for_gregorian_year(2023, location)
738907

current_major_solar_term(iso_days, location_fun)

Returns the last Chinese major solar term (zhongqi) before iso_days.

Arguments

  • iso_days is an ISO day number.

  • location_fun is a 1-arity function of the ISO day number that returns {latitude, longitude, altitude, offset}, the place the calendar is reckoned at, as a calendar's location/1 does.

Returns

  • The major solar term, 1 to 12.

Examples

iex> location = &Calendrical.Chinese.location/1
iex> Calendrical.Lunisolar.current_major_solar_term(739_038, location)
4

current_minor_solar_term(iso_days, location_fun)

Returns the last minor solar term (jieqi) before iso_days.

Arguments

  • iso_days is an ISO day number.

  • location_fun is a 1-arity function of the ISO day number that returns {latitude, longitude, altitude, offset}, the place the calendar is reckoned at, as a calendar's location/1 does.

Returns

  • The minor solar term, 1 to 12.

Examples

iex> location = &Calendrical.Chinese.location/1
iex> Calendrical.Lunisolar.current_minor_solar_term(739_038, location)
4

cyclical_date_to_iso_days(cycle, cyclical_year, month, day, epoch, location_fun)

@spec cyclical_date_to_iso_days(
  integer(),
  integer(),
  integer(),
  integer(),
  integer(),
  (Astro.Time.time() ->
     {Astro.angle(), Astro.angle(), Astro.meters(), Astro.Time.hours()})
) :: integer()

Returns the ISO day number of a lunisolar date whose year is given as a cycle and the year within the cycle, and whose months are ordinal.

Arguments

  • cycle is the 60-year cycle.

  • cyclical_year is the year within the cycle, from 1 to 60.

  • month is the ordinal month of the year.

  • day is the day of the month.

  • epoch is the calendar's epoch in ISO days.

  • location_fun is a function that takes a moment and returns the {latitude, longitude, altitude, offset} of the calendar's reference location.

Returns

  • The number of days since the ISO epoch, as an integer.

Examples

iex> epoch = Calendrical.Chinese.epoch()
iex> location = &Calendrical.Chinese.location/1
iex> Calendrical.Lunisolar.cyclical_date_to_iso_days(78, 40, 1, 1, epoch, location)
738907

date_from_iso_days(iso_days, epoch, location_fun)

Returns the lunisolar date of an ISO day number, with ordinal months that run from 1 to 12, or 1 to 13 in a leap year.

Arguments

  • iso_days is the number of days since the ISO epoch.

  • epoch is the calendar's epoch in ISO days.

  • location_fun is a function that takes a moment and returns the {latitude, longitude, altitude, offset} of the calendar's reference location.

Returns

  • A {year, month, day} tuple, the year counted from the calendar's epoch.

Examples

iex> epoch = Calendrical.Chinese.epoch()
iex> location = &Calendrical.Chinese.location/1
iex> Calendrical.Lunisolar.date_from_iso_days(738907, epoch, location)
{4660, 1, 1}

date_to_iso_days(year, month, day, epoch, location_fun)

@spec date_to_iso_days(
  integer(),
  integer(),
  integer(),
  integer(),
  (Astro.Time.time() ->
     {Astro.angle(), Astro.angle(), Astro.meters(), Astro.Time.hours()})
) ::
  integer()

Returns the ISO day number of a lunisolar date whose months are ordinal: numbered 1 to 12, or 1 to 13 in a leap year, with the leap month unmarked.

Arguments

  • year is the year counted from the calendar's epoch.

  • month is the ordinal month of the year.

  • day is the day of the month.

  • epoch is the calendar's epoch in ISO days.

  • location_fun is a function that takes a moment and returns the {latitude, longitude, altitude, offset} of the calendar's reference location.

Returns

  • The number of days since the ISO epoch, as an integer.

Examples

iex> epoch = Calendrical.Chinese.epoch()
iex> location = &Calendrical.Chinese.location/1
iex> Calendrical.Lunisolar.date_to_iso_days(4660, 1, 1, epoch, location)
738907

day_name(date)

Returns the sexagesimal name of a day.

Arguments

  • date is an ISO day number.

Returns

  • {stem, branch}.

Examples

# 22 January 2023, Chinese New Year, was a 庚辰 day
iex> Calendrical.Lunisolar.day_name(738_907)
{7, 5}

day_name_on_or_before(name, date)

Returns the latest day on or before date that has the given sexagesimal name.

Arguments

  • name is a {stem, branch} name.

  • date is an ISO day number.

Returns

  • The ISO day number of that day.

Examples

iex> Calendrical.Lunisolar.day_name_on_or_before({1, 1}, 738_907)
738891

december_solstice_on_or_before(iso_days, location_fun)

Returns the day of the winter solstice on or before iso_days, at the calendar's place.

Arguments

  • iso_days is an ISO day number.

  • location_fun is a 1-arity function of the ISO day number that returns {latitude, longitude, altitude, offset}, the place the calendar is reckoned at, as a calendar's location/1 does.

Returns

  • The ISO day number of the solstice.

Examples

iex> location = &Calendrical.Chinese.location/1
iex> Calendrical.Lunisolar.december_solstice_on_or_before(739_038, location)
738876

gregorian_date_for_lunar(gregorian_year, lunar_month, lunar_day, epoch, location_fun)

Returns the Gregorian date for the lunar month and day in a given Gregorian year.

Arguments

  • gregorian_year is a Gregorian year.

  • lunar_month is a month of the lunar year that begins in that Gregorian year.

  • lunar_day is a day of that month.

  • epoch is the ISO day number of the first day of the calendar's year 1, as a calendar's epoch/0 returns it.

  • location_fun is a 1-arity function of the ISO day number that returns {latitude, longitude, altitude, offset}, the place the calendar is reckoned at, as a calendar's location/1 does.

Returns

  • {year, month, day} in the Gregorian calendar.

Raises Calendrical.InvalidDateError when the lunar year has no such month, or the month no such day.

Examples

iex> epoch = Calendrical.Chinese.epoch()
iex> location = &Calendrical.Chinese.location/1
iex> Calendrical.Lunisolar.gregorian_date_for_lunar(2023, 1, 1, epoch, location)
{2023, 1, 22}

iex> epoch = Calendrical.Chinese.epoch()
iex> location = &Calendrical.Chinese.location/1
iex> Calendrical.Lunisolar.gregorian_date_for_lunar(2024, {2, :leap}, 1, epoch, location)
** (Calendrical.InvalidDateError) [gregorian_year: 2024, lunar_month: {2, :leap}, lunar_day: 1] is not a date

leap_month(year, epoch, location_fun)

Returns the ordinal number of a year's leap month.

Arguments

  • year is a year of the lunisolar calendar.

  • epoch is the ISO day number of the first day of the calendar's year 1, as a calendar's epoch/0 returns it.

  • location_fun is a 1-arity function of the ISO day number that returns {latitude, longitude, altitude, offset}, the place the calendar is reckoned at, as a calendar's location/1 does.

Returns

  • The ordinal month that is the leap month, counting it among the year's months, or nil if the year has none.

Examples

iex> epoch = Calendrical.Chinese.epoch()
iex> location = &Calendrical.Chinese.location/1
iex> Calendrical.Lunisolar.leap_month(4660, epoch, location)
3
iex> Calendrical.Lunisolar.leap_month(4661, epoch, location)
nil

leap_month?(year, month, epoch, location_fun)

Returns whether the given month of a year is its leap month.

Approximately every three years (7 times in 19 years), a leap month is added to the Chinese calendar.

To determine when, find the number of new moons between the 11th month in one year and the 11th month in the following year.

A leap month is inserted if there are 13 New Moons from the start of the 11th month in the first year to the start of the 11th month in the next year.

The Chinese calendar uses a solar term system that has 12 principal terms to indicate when the Sun's longitude is a multiple of 30 degrees.

Unlike all other months, the leap month does not contain a principal term (Zhongqi).

Arguments

  • year is a year of the lunisolar calendar.

  • month is an ordinal month of the year, counting the leap month.

  • epoch is the ISO day number of the first day of the calendar's year 1, as a calendar's epoch/0 returns it.

  • location_fun is a 1-arity function of the ISO day number that returns {latitude, longitude, altitude, offset}, the place the calendar is reckoned at, as a calendar's location/1 does.

Returns

  • true if month is the year's leap month, otherwise false.

Examples

iex> epoch = Calendrical.Chinese.epoch()
iex> location = &Calendrical.Chinese.location/1
iex> Calendrical.Lunisolar.leap_month?(4660, 3, epoch, location)
true
iex> Calendrical.Lunisolar.leap_month?(4660, 4, epoch, location)
false

leap_month?(cycle, cyclical_year, month, epoch, location_fun)

Returns whether month is the leap month of the year given as a cycle and the year within the cycle.

Arguments

  • cycle is the 60-year cycle.

  • cyclical_year is the year within the cycle, from 1 to 60.

  • month is the ordinal month of the year.

  • epoch is the calendar's epoch in ISO days.

  • location_fun is a function that takes a moment and returns the {latitude, longitude, altitude, offset} of the calendar's reference location.

Returns

  • true if month is the year's leap month, otherwise false.

Examples

iex> epoch = Calendrical.Chinese.epoch()
iex> location = &Calendrical.Chinese.location/1
iex> Calendrical.Lunisolar.leap_month?(78, 40, 3, epoch, location)
true

leap_year?(year, epoch, location_fun)

Returns whether the given year is a leap year.

Leap years have 13 months. To determine if a year is a leap year, calculate the number of new moons between the 11th month in one year (i.e., the month containing the Winter Solstice) and the 11th month in the following year.

If there are 13 new moons from the start of the 11th month in the first year to the start of the 11th month in the second year, a leap month must be inserted.

In leap years, at least one month does not contain a Principal Term. The first such month is the leap month.

The additional complexity is that a leap year is calculated for the solar year, but the calendar is managed in lunar years and months. Therefore when a leap year is detected, the leap month could be in the current lunar year or the next lunar year.

Arguments

  • year is a year of the lunisolar calendar.

  • epoch is the ISO day number of the first day of the calendar's year 1, as a calendar's epoch/0 returns it.

  • location_fun is a 1-arity function of the ISO day number that returns {latitude, longitude, altitude, offset}, the place the calendar is reckoned at, as a calendar's location/1 does.

Returns

  • true if the year has 13 months, otherwise false.

Examples

iex> epoch = Calendrical.Chinese.epoch()
iex> location = &Calendrical.Chinese.location/1
iex> Calendrical.Lunisolar.leap_year?(4660, epoch, location)
true
iex> Calendrical.Lunisolar.leap_year?(4661, epoch, location)
false

location(iso_days, location_fun)

Returns the place a lunisolar calendar is reckoned at on a day, as the point Astro takes.

Arguments

  • iso_days is an ISO day number.

  • location_fun is a 1-arity function of the ISO day number that returns {latitude, longitude, altitude, offset}, as a calendar's location/1 does.

Returns

  • A Geo.PointZ of the longitude, latitude and altitude, with the offset from UTC, in days, as its :offset property.

Examples

iex> location = &Calendrical.Chinese.location/1
iex> %Geo.PointZ{properties: %{offset: offset}} = Calendrical.Lunisolar.location(738_907, location)
iex> offset
0.3333333333333333

major_solar_term_on_or_after(iso_days, location_fun)

Returns the moment of the first major solar term (zhongqi) on or after iso_days. The major terms begin when the sun's longitude is a multiple of 30 degrees.

Arguments

  • iso_days is an ISO day number.

  • location_fun is a 1-arity function of the ISO day number that returns {latitude, longitude, altitude, offset}, the place the calendar is reckoned at, as a calendar's location/1 does.

Returns

  • The moment, in fractional ISO days.

Examples

iex> location = &Calendrical.Chinese.location/1
iex> Calendrical.Lunisolar.major_solar_term_on_or_after(739_038, location) |> floor()
739057

midnight_in_location(iso_days, location_fun)

Returns the universal time of the (clock) midnight that begins iso_days at the calendar's place.

Arguments

  • iso_days is an ISO day number.

  • location_fun is a 1-arity function of the ISO day number that returns {latitude, longitude, altitude, offset}, the place the calendar is reckoned at, as a calendar's location/1 does.

Returns

  • The moment, in fractional ISO days.

Examples

iex> location = &Calendrical.Chinese.location/1
iex> Calendrical.Lunisolar.midnight_in_location(738_907, location)
738906.6666666666

minor_solar_term_on_or_after(iso_days, location_fun)

Returns the moment of the first minor solar term (jieqi) on or after iso_days. The minor terms begin when the sun's longitude is an odd multiple of 15 degrees.

Arguments

  • iso_days is an ISO day number.

  • location_fun is a 1-arity function of the ISO day number that returns {latitude, longitude, altitude, offset}, the place the calendar is reckoned at, as a calendar's location/1 does.

Returns

  • The moment, in fractional ISO days.

Examples

iex> location = &Calendrical.Chinese.location/1
iex> Calendrical.Lunisolar.minor_solar_term_on_or_after(739_038, location) |> floor()
739042

month_name(month, year)

Returns the sexagesimal name of a month of a Chinese year.

Arguments

  • month is a month of the year.

  • year is a year of the Chinese calendar.

Returns

  • {stem, branch}.

Examples

iex> Calendrical.Lunisolar.month_name(1, 4660)
{1, 3}

name_difference(c_name1, c_name2)

Returns the number of names from one Lunar name to the next occurrence of another.

Arguments

  • c_name1 is a {stem, branch} name.

  • c_name2 is a {stem, branch} name.

Returns

  • The number of names, 1 to 60.

Examples

iex> Calendrical.Lunisolar.name_difference({1, 1}, {10, 4})
39

new(year, lunar_month, day, epoch, location_fun)

Creates a date in the lunisolar calendar as an ISO day number.

Arguments

  • year is a year of the lunisolar calendar.

  • lunar_month is a month of the year, as a positive integer or as {month, :leap} for the leap month.

  • day is a day of the month.

  • epoch is the ISO day number of the first day of the calendar's year 1, as a calendar's epoch/0 returns it.

  • location_fun is a 1-arity function of the ISO day number that returns {latitude, longitude, altitude, offset}, the place the calendar is reckoned at, as a calendar's location/1 does.

Returns

  • The ISO day number of the date.

  • {:error, %Calendrical.InvalidDateError{}} if the year has no such month or the month no such day.

Examples

iex> epoch = Calendrical.Chinese.epoch()
iex> location = &Calendrical.Chinese.location/1
iex> Calendrical.Lunisolar.new(4660, 1, 1, epoch, location)
738907

iex> epoch = Calendrical.Chinese.epoch()
iex> location = &Calendrical.Chinese.location/1
iex> Calendrical.Lunisolar.new(4660, 1, 31, epoch, location)
{:error, %Calendrical.InvalidDateError{value: {4660, 1, 31}}}

new_moon_before(iso_days, location_fun)

Returns the day of the first new moon before iso_days, at the calendar's place.

Arguments

  • iso_days is an ISO day number.

  • location_fun is a 1-arity function of the ISO day number that returns {latitude, longitude, altitude, offset}, the place the calendar is reckoned at, as a calendar's location/1 does.

Returns

  • The ISO day number of the new moon.

Raises Calendrical.UnsupportedDateRangeError for a day outside Gregorian years -20,005 to 20,005.

Examples

iex> location = &Calendrical.Chinese.location/1
iex> Calendrical.Lunisolar.new_moon_before(739_038, location)
739024

new_moon_on_or_after(iso_days, location_fun)

Returns the day of the first new moon on or after iso_days, at the calendar's place.

Arguments

  • iso_days is an ISO day number.

  • location_fun is a 1-arity function of the ISO day number that returns {latitude, longitude, altitude, offset}, the place the calendar is reckoned at, as a calendar's location/1 does.

Returns

  • The ISO day number of the new moon.

Raises Calendrical.UnsupportedDateRangeError for a day outside Gregorian years -20,005 to 20,005.

Examples

iex> location = &Calendrical.Chinese.location/1
iex> Calendrical.Lunisolar.new_moon_on_or_after(739_038, location)
739054

new_year_in_sui(iso_days, location_fun)

Returns the day of the Lunar New Year in the sui (the period from one winter solstice to the next) that contains iso_days.

Arguments

  • iso_days is an ISO day number.

  • location_fun is a 1-arity function of the ISO day number that returns {latitude, longitude, altitude, offset}, the place the calendar is reckoned at, as a calendar's location/1 does.

Returns

  • The ISO day number of the new year.

Examples

iex> location = &Calendrical.Chinese.location/1
iex> Calendrical.Lunisolar.new_year_in_sui(739_038, location)
738907

new_year_on_or_before(iso_days, location_fun)

Returns the day of the Lunar New Year on or before iso_days, at the calendar's place.

Arguments

  • iso_days is an ISO day number.

  • location_fun is a 1-arity function of the ISO day number that returns {latitude, longitude, altitude, offset}, the place the calendar is reckoned at, as a calendar's location/1 does.

Returns

  • The ISO day number of the new year.

Examples

iex> location = &Calendrical.Chinese.location/1
iex> Calendrical.Lunisolar.new_year_on_or_before(739_038, location)
738907

no_major_solar_term?(iso_days, location_fun)

Returns whether the lunar month starting on iso_days has no major solar term, as only a leap month has none.

Arguments

  • iso_days is the ISO day number of the first day of a lunar month.

  • location_fun is a 1-arity function of the ISO day number that returns {latitude, longitude, altitude, offset}, the place the calendar is reckoned at, as a calendar's location/1 does.

Returns

  • true if the month has no major solar term, otherwise false.

Examples

iex> location = &Calendrical.Chinese.location/1
iex> Calendrical.Lunisolar.no_major_solar_term?(Date.to_gregorian_days(~D[2023-03-22]), location)
true
iex> Calendrical.Lunisolar.no_major_solar_term?(Date.to_gregorian_days(~D[2023-04-20]), location)
false

prior_leap_month?(m_prime, m, location_fun)

Returns whether there is a leap month from the lunar month starting on m_prime up to and including the lunar month starting on m.

Arguments

  • m_prime is the ISO day number of the first day of the earlier lunar month.

  • m is the ISO day number of the first day of the later lunar month.

  • location_fun is a 1-arity function of the ISO day number that returns {latitude, longitude, altitude, offset}, the place the calendar is reckoned at, as a calendar's location/1 does.

Returns

  • true if a leap month falls in that span, otherwise false.

Examples

iex> location = &Calendrical.Chinese.location/1
iex> new_year = Date.to_gregorian_days(~D[2023-01-22])
iex> Calendrical.Lunisolar.prior_leap_month?(new_year, Date.to_gregorian_days(~D[2023-05-19]), location)
true
iex> Calendrical.Lunisolar.prior_leap_month?(new_year, Date.to_gregorian_days(~D[2023-02-20]), location)
false

solar_longitude_on_or_after(lambda, iso_days, location_fun)

@spec solar_longitude_on_or_after(Astro.angle(), number(), (Astro.Time.time() ->
                                                        {Astro.angle(),
                                                         Astro.angle(),
                                                         Astro.meters(),
                                                         Astro.Time.hours()})) ::
  Astro.Time.time()

Return moment at location of the first date on or after iso_days when the solar longitude will be lambda degrees.

Arguments

  • lambda is the solar longitude in degrees.

  • iso_days is the moment, in days since the ISO epoch, from which to search.

  • location_fun is a function that takes a moment and returns the {latitude, longitude, altitude, offset} of the calendar's reference location.

Returns

  • The moment, in days since the ISO epoch at the location's standard time, when the sun reaches lambda degrees.

Examples

# 大寒 (sun at 300 degrees) falls on 2026-01-20 Beijing time
iex> iso_days = Calendar.ISO.date_to_iso_days(2026, 1, 1)
iex> location = &Calendrical.Chinese.location/1
iex> moment = Calendrical.Lunisolar.solar_longitude_on_or_after(300, iso_days, location)
iex> Calendar.ISO.date_from_iso_days(floor(moment))
{2026, 1, 20}

solar_term(index, gregorian_year, location_fun)

The proleptic-Gregorian date on which the sun reaches the index-th solar term (jié-qì) during Gregorian year, observed at location_fun.

The 24 solar terms are numbered from lichun (立春, index 1) at 315° solar ecliptic longitude, then every 15° (qingming = 5 at 15°, dongzhi = 22 at 270°, …). The day is taken in the observer's local standard time, so the meridian — supplied by the calendar's location/1 — decides the civil date.

Arguments

  • index is the 1-based solar-term number, 1..24.

  • year is a proleptic-Gregorian year.

  • location_fun is the observer's location function, taken from the lunisolar calendar (e.g. &Calendrical.Chinese.location/1).

Returns

  • {:ok, t:Date.t/0} in Calendrical.Gregorian.

  • {:error, %Calendrical.InvalidSolarTermError{}} when index is not in 1..24.

Examples

iex> Calendrical.Lunisolar.solar_term(5, 2025, &Calendrical.Chinese.location/1)
{:ok, ~D[2025-04-04 Calendrical.Gregorian]}

solar_term_name(index)

@spec solar_term_name(integer()) :: {:ok, String.t()} | {:error, Exception.t()}

The name of the solar term (jié-qì) at a 1-based index.

The 24 solar terms are numbered from lichun (立春, index 1) at 315° solar ecliptic longitude, then every 15° — the same order solar_term/3 computes a term's date for, so an index that dates a term also names it.

Arguments

  • index is the 1-based solar-term number, 1..24.

Returns

  • {:ok, name} with the term's romanised (pinyin) name, such as "qingming".

  • {:error, %Calendrical.InvalidSolarTermError{}} when index is not in 1..24.

Examples

iex> Calendrical.Lunisolar.solar_term_name(1)
{:ok, "lichun"}

iex> Calendrical.Lunisolar.solar_term_name(5)
{:ok, "qingming"}

iex> Calendrical.Lunisolar.solar_term_name(22)
{:ok, "dongzhi"}

iex> Calendrical.Lunisolar.solar_term_name(0)
{:error, %Calendrical.InvalidSolarTermError{index: 0}}

stem_and_branch(n)

Returns the name of a year in the Lunar sexagesimal cycle, as its celestial stem and terrestrial branch.

Arguments

  • year is a position in the sexagesimal cycle, a lunisolar date as {year, month, day} or a traditional date as {cycle, cyclical_year, month, leap_month?, day}.

Returns

  • {stem, branch}, the stem 1 to 10 and the branch 1 to 12.

Examples

iex> Calendrical.Lunisolar.stem_and_branch(40)
{10, 4}
iex> Calendrical.Lunisolar.stem_and_branch({4660, 1, 1})
{10, 4}