Skip to content

Commit d88600e

Browse files
committed
Fix days in month calculation for the last month
1 parent 1cf9008 commit d88600e

File tree

4 files changed

+119
-4
lines changed

4 files changed

+119
-4
lines changed

CHANGELOG.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,18 @@
22

33
**Note that `ex_cldr_calendars` version 1.24.0 and later are supported on Elixir 1.12 and later only.**
44

5+
## Cldr.Calendars v2.4.0
6+
7+
This is the changelog for Cldr Calendars v2.4.0 released on _____, 2025. For older changelogs please consult the release tag on [GitHub](https://github.com/elixir-cldr/cldr_calendars/tags)
8+
9+
### Bug Fixes
10+
11+
### Enhancements
12+
13+
* Adds `use Cldr.Calendar.Julian, new_year_starting_month_and_day: {month_of_year, day_of_month}` to allow modelling Julian calendars that don't start on January 1st.
14+
15+
* Adds `Cldr.Calendar.convert/2` which converts either a date or a date range to another calendar.
16+
517
## Cldr.Calendars v2.3.1
618

719
This is the changelog for Cldr Calendars v2.3.1 released on September 10th, 2025. For older changelogs please consult the release tag on [GitHub](https://github.com/elixir-cldr/cldr_calendars/tags)

lib/cldr/calendar.ex

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -798,6 +798,46 @@ defmodule Cldr.Calendar do
798798
end
799799
end
800800

801+
@doc """
802+
Converts a date or a date range to another calendar.
803+
804+
If the argument is a `t:Date.t/0`, `Date.convert/2` is used to
805+
do the conversion.
806+
807+
If the argument is a `t:/Date.Range.t/0` then `Date.convert/2` is
808+
applied to the start and end of the range and a new range returned.
809+
810+
### Arguments
811+
812+
* `date_or_range` is any `t:Date.t/0` or ` t:Date.Range.t/0`.
813+
814+
* `calendar` is any valid calendar module.
815+
816+
### Returns
817+
818+
* `{:ok, converted_date_or_range}` or
819+
820+
* `{:error, :incompatible_calendars}`
821+
822+
### Example
823+
824+
"""
825+
@doc since: "2.4.0"
826+
@spec convert(Date.t() | Date.Range.t(), Calendar.calendar()) ::
827+
{:ok, Date.t() | Date.Range.t()} | {:error, :incompatible_calendars}
828+
829+
def convert(%Date{} = date, calendar) do
830+
Date.convert(date, calendar)
831+
end
832+
833+
def convert(%Date.Range{first: first, last: last, step: step}, calendar) do
834+
with {:ok, new_first} <- Date.convert(first, calendar),
835+
{:ok, new_last} <- Date.convert(last, calendar) do
836+
{:ok, Date.range(new_first, new_last, step)}
837+
end
838+
end
839+
840+
801841
@doc """
802842
Formats the given date, time, or datetime into a string.
803843

lib/cldr/calendar/calendars/julian.ex

Lines changed: 66 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,9 @@ defmodule Cldr.Calendar.Julian do
1818
@new_year_starting_month start_month
1919
@new_year_starting_day start_day
2020

21+
@months_in_year Cldr.Calendar.Julian.months_in_year(0)
22+
@last_month_of_year rem(start_month + (@months_in_year - 1), @months_in_year)
23+
2124
# Adjust the year to be a Jan 1st starting year and carry
2225
# on
2326
def date_to_iso_days(year, month, day) when month <= @new_year_starting_month and day < @new_year_starting_day do
@@ -28,6 +31,10 @@ defmodule Cldr.Calendar.Julian do
2831
Cldr.Calendar.Julian.date_to_iso_days(year, month, day)
2932
end
3033

34+
def naive_datetime_to_iso_days(year, month, day, 0, 0, 0, {0, 0}) do
35+
{date_to_iso_days(year, month, day), {0, 0}}
36+
end
37+
3138
# Adjust the year to be this calendars starting year
3239
def date_from_iso_days(iso_days) do
3340
{year, month, day} = Cldr.Calendar.Julian.date_from_iso_days(iso_days)
@@ -39,22 +46,78 @@ defmodule Cldr.Calendar.Julian do
3946
end
4047
end
4148

49+
# Here we use month to mean ordinal month. Therefore if the calendar
50+
# starts on March 25th, then days in month for March will be seen as
51+
# days if month for month 1.
52+
53+
def days_in_month(year, ordinal_month) do
54+
adjusted_month =
55+
Cldr.Math.amod(ordinal_month + @new_year_starting_month - 1, @months_in_year)
56+
57+
adjusted_year =
58+
if adjusted_month < @new_year_starting_month, do: year + 1, else: year
59+
60+
cond do
61+
# The first month of the year will be short since the year starts
62+
# part way through the month
63+
adjusted_month == @new_year_starting_month ->
64+
days_in_julian_month = Cldr.Calendar.Julian.days_in_month(year, ordinal_month)
65+
days_in_julian_month - @new_year_starting_day
66+
67+
# The last month of the year will be "long" since the first part of the
68+
# first month that is before the start of year will be included
69+
adjusted_month == @last_month_of_year ->
70+
start_of_month =
71+
date_to_iso_days(adjusted_year, adjusted_month, 1)
72+
start_of_next_month =
73+
date_to_iso_days(adjusted_year, @new_year_starting_month, @new_year_starting_day)
74+
start_of_next_month - start_of_month
75+
76+
true ->
77+
Cldr.Calendar.Julian.days_in_month(year, adjusted_month)
78+
end
79+
end
80+
81+
def month(year, ordinal_month) do
82+
adjusted_month =
83+
Cldr.Math.amod(ordinal_month + @new_year_starting_month - 1, @months_in_year)
84+
85+
adjusted_year =
86+
if adjusted_month < @new_year_starting_month, do: year + 1, else: year
87+
88+
first_day =
89+
if adjusted_month == @new_year_starting_month, do: @new_year_starting_day, else: 1
90+
91+
{:ok, first} = Date.new(adjusted_year, adjusted_month, first_day, __MODULE__)
92+
first_iso_days = date_to_iso_days(year, adjusted_month, first_day)
93+
days_in_month = days_in_month(year, ordinal_month)
94+
95+
last_iso_days = first_iso_days + days_in_month - 1
96+
{year, month, day} = date_from_iso_days(last_iso_days)
97+
{:ok, last} = Date.new(year, month, day, __MODULE__)
98+
99+
Date.range(first, last, 1)
100+
end
101+
102+
# Returns the ordinal month
103+
def month_of_year(_year, ordinal_month, _day) do
104+
Cldr.Math.amod(ordinal_month - @new_year_starting_month + 1, @months_in_year)
105+
end
106+
42107
defdelegate valid_date?(year, month, day), to: Cldr.Calendar.Julian
43108
defdelegate leap_year?(year), to: Cldr.Calendar.Julian
44109
defdelegate plus(year, month, day, part, years, options), to: Cldr.Calendar.Julian
45110
defdelegate week(year, week), to: Cldr.Calendar.Julian
46-
defdelegate month(year, month), to: Cldr.Calendar.Julian
47111
defdelegate weeks_in_year(year), to: Cldr.Calendar.Julian
48-
defdelegate days_in_month(year, month), to: Cldr.Calendar.Julian
49112
defdelegate day_of_week(year, month, day, starts_on), to: Cldr.Calendar.Julian
50113
defdelegate day_of_year(year, month, day), to: Cldr.Calendar.Julian
51114
defdelegate day_of_era(year, month, day), to: Cldr.Calendar.Julian
52115
defdelegate iso_week_of_year(year, month, day), to: Cldr.Calendar.Julian
53116
defdelegate week_of_year(year, month, day), to: Cldr.Calendar.Julian
54-
defdelegate month_of_year(year, month, day), to: Cldr.Calendar.Julian
55117
defdelegate quarter_of_year(year, month, day), to: Cldr.Calendar.Julian
56118
defdelegate year_of_era(year), to: Cldr.Calendar.Julian
57119
defdelegate parse_date(date), to: Cldr.Calendar.Julian
120+
defdelegate date_to_string(year, month, day), to: Cldr.Calendar.Julian
58121
end
59122
end
60123

mix.exs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
defmodule Cldr.Calendar.MixProject do
22
use Mix.Project
33

4-
@version "2.3.1"
4+
@version "2.4.0"
55

66
def project do
77
[

0 commit comments

Comments
 (0)