Skip to content

Commit 6b519ae

Browse files
authored
Optimize Calendar.ISO.date_from_iso_days (#14976)
1 parent 6710d7e commit 6b519ae

File tree

1 file changed

+100
-90
lines changed

1 file changed

+100
-90
lines changed

lib/elixir/lib/calendar/iso.ex

Lines changed: 100 additions & 90 deletions
Original file line numberDiff line numberDiff line change
@@ -897,8 +897,14 @@ defmodule Calendar.ISO do
897897
@doc false
898898
def date_from_iso_days(days) do
899899
{year, day_of_year} = days_to_year(days)
900-
extra_day = if leap_year?(year), do: 1, else: 0
901-
{month, day_in_month} = year_day_to_year_date(extra_day, day_of_year)
900+
901+
{month, day_in_month} =
902+
if leap_year?(year) do
903+
year_day_to_year_date_leap(day_of_year)
904+
else
905+
year_day_to_year_date(day_of_year)
906+
end
907+
902908
{year, month, day_in_month + 1}
903909
end
904910

@@ -913,6 +919,9 @@ defmodule Calendar.ISO do
913919
end
914920
end
915921

922+
defp floor_div_positive_divisor(int1, int2) when int1 >= 0, do: div(int1, int2)
923+
defp floor_div_positive_divisor(int1, int2), do: -div(-int1 - 1, int2) - 1
924+
916925
@doc """
917926
Returns how many days there are in the given year-month.
918927
@@ -1848,7 +1857,7 @@ defmodule Calendar.ISO do
18481857
months_in_year = 12
18491858
total_months = year * months_in_year + month + months - 1
18501859

1851-
new_year = Integer.floor_div(total_months, months_in_year)
1860+
new_year = floor_div_positive_divisor(total_months, months_in_year)
18521861

18531862
new_month =
18541863
case rem(total_months, months_in_year) + 1 do
@@ -1971,7 +1980,7 @@ defmodule Calendar.ISO do
19711980

19721981
if total in @unix_range_microseconds do
19731982
microseconds = Integer.mod(total, @microseconds_per_second)
1974-
seconds = @unix_epoch + Integer.floor_div(total, @microseconds_per_second)
1983+
seconds = @unix_epoch + floor_div_positive_divisor(total, @microseconds_per_second)
19751984
precision = precision_for_unit(unit)
19761985
{date, time} = iso_seconds_to_datetime(seconds)
19771986
{:ok, date, time, {microseconds, precision}}
@@ -2093,64 +2102,71 @@ defmodule Calendar.ISO do
20932102
end
20942103
end
20952104

2096-
# Note that this function does not add the extra leap day for a leap year.
2097-
# If you want to add that leap day when appropriate,
2098-
# add the result of leap_day_offset/2 to the result of days_before_month/1.
2099-
defp days_before_month(1), do: 0
2100-
defp days_before_month(2), do: 31
2101-
defp days_before_month(3), do: 59
2102-
defp days_before_month(4), do: 90
2103-
defp days_before_month(5), do: 120
2104-
defp days_before_month(6), do: 151
2105-
defp days_before_month(7), do: 181
2106-
defp days_before_month(8), do: 212
2107-
defp days_before_month(9), do: 243
2108-
defp days_before_month(10), do: 273
2109-
defp days_before_month(11), do: 304
2110-
defp days_before_month(12), do: 334
2111-
21122105
defp leap_day_offset(_year, month) when month < 3, do: 0
21132106

21142107
defp leap_day_offset(year, _month) do
21152108
if leap_year?(year), do: 1, else: 0
21162109
end
21172110

21182111
defp days_to_year(days) when days < 0 do
2119-
year_estimate = -div(-days, @days_per_nonleap_year) - 1
2120-
2121-
{year, days_before_year} =
2122-
days_to_year(year_estimate, days, days_to_end_of_epoch(year_estimate))
2112+
y_min = floor_div_positive_divisor(days, @days_per_nonleap_year)
2113+
y_max = floor_div_positive_divisor(days, @days_per_leap_year)
2114+
2115+
{year, day_start} =
2116+
days_to_year_interpolated(
2117+
y_min,
2118+
y_max,
2119+
days,
2120+
days_in_previous_years(y_min),
2121+
days_in_previous_years(y_max)
2122+
)
21232123

2124-
leap_year_pad = if leap_year?(year), do: 1, else: 0
2125-
{year, leap_year_pad + @days_per_nonleap_year + days - days_before_year}
2124+
{year, days - day_start}
21262125
end
21272126

21282127
defp days_to_year(days) do
2129-
year_estimate = div(days, @days_per_nonleap_year)
2130-
2131-
{year, days_before_year} =
2132-
days_to_year(year_estimate, days, days_in_previous_years(year_estimate))
2128+
y_min = floor_div_positive_divisor(days, @days_per_leap_year)
2129+
y_max = floor_div_positive_divisor(days, @days_per_nonleap_year)
2130+
2131+
{year, day_start} =
2132+
days_to_year_interpolated(
2133+
y_min,
2134+
y_max,
2135+
days,
2136+
days_in_previous_years(y_min),
2137+
days_in_previous_years(y_max)
2138+
)
21332139

2134-
{year, days - days_before_year}
2140+
{year, days - day_start}
21352141
end
21362142

2137-
defp days_to_year(year, days1, days2) when year < 0 and days1 >= days2 do
2138-
days_to_year(year + 1, days1, days_to_end_of_epoch(year + 1))
2143+
defp days_to_year_interpolated(min, max, _days, d_min, _d_max) when min >= max do
2144+
{min, d_min}
21392145
end
21402146

2141-
defp days_to_year(year, days1, days2) when year >= 0 and days1 < days2 do
2142-
days_to_year(year - 1, days1, days_in_previous_years(year - 1))
2143-
end
2147+
defp days_to_year_interpolated(min, max, days, d_min, d_max) do
2148+
diff = max - min
2149+
d_diff = d_max - d_min
21442150

2145-
defp days_to_year(year, _days1, days2) do
2146-
{year, days2}
2147-
end
2151+
numerator = diff * (days - d_min)
2152+
offset = floor_div_positive_divisor(numerator, d_diff)
21482153

2149-
defp days_to_end_of_epoch(year) when year < 0 do
2150-
previous_year = year + 1
2154+
mid = min + max(0, min(offset, diff))
2155+
d_mid = days_in_previous_years(mid)
2156+
mid_length = if leap_year?(mid), do: @days_per_leap_year, else: @days_per_nonleap_year
21512157

2152-
div(previous_year, 4) - div(previous_year, 100) + div(previous_year, 400) +
2153-
previous_year * @days_per_nonleap_year
2158+
cond do
2159+
days < d_mid ->
2160+
new_max = mid - 1
2161+
days_to_year_interpolated(min, new_max, days, d_min, days_in_previous_years(new_max))
2162+
2163+
days - d_mid >= mid_length ->
2164+
new_min = mid + 1
2165+
days_to_year_interpolated(new_min, max, days, days_in_previous_years(new_min), d_max)
2166+
2167+
true ->
2168+
{mid, d_mid}
2169+
end
21542170
end
21552171

21562172
defp days_in_previous_years(0), do: 0
@@ -2174,54 +2190,48 @@ defmodule Calendar.ISO do
21742190
@days_per_leap_year
21752191
end
21762192

2177-
# Note that 0 is the first day of the month.
2178-
defp year_day_to_year_date(_extra_day, day_of_year) when day_of_year < 31 do
2179-
{1, day_of_year}
2180-
end
2181-
2182-
defp year_day_to_year_date(extra_day, day_of_year) when day_of_year < 59 + extra_day do
2183-
{2, day_of_year - 31}
2184-
end
2185-
2186-
defp year_day_to_year_date(extra_day, day_of_year) when day_of_year < 90 + extra_day do
2187-
{3, day_of_year - (59 + extra_day)}
2188-
end
2189-
2190-
defp year_day_to_year_date(extra_day, day_of_year) when day_of_year < 120 + extra_day do
2191-
{4, day_of_year - (90 + extra_day)}
2192-
end
2193-
2194-
defp year_day_to_year_date(extra_day, day_of_year) when day_of_year < 151 + extra_day do
2195-
{5, day_of_year - (120 + extra_day)}
2196-
end
2197-
2198-
defp year_day_to_year_date(extra_day, day_of_year) when day_of_year < 181 + extra_day do
2199-
{6, day_of_year - (151 + extra_day)}
2200-
end
2201-
2202-
defp year_day_to_year_date(extra_day, day_of_year) when day_of_year < 212 + extra_day do
2203-
{7, day_of_year - (181 + extra_day)}
2204-
end
2205-
2206-
defp year_day_to_year_date(extra_day, day_of_year) when day_of_year < 243 + extra_day do
2207-
{8, day_of_year - (212 + extra_day)}
2208-
end
2209-
2210-
defp year_day_to_year_date(extra_day, day_of_year) when day_of_year < 273 + extra_day do
2211-
{9, day_of_year - (243 + extra_day)}
2212-
end
2213-
2214-
defp year_day_to_year_date(extra_day, day_of_year) when day_of_year < 304 + extra_day do
2215-
{10, day_of_year - (273 + extra_day)}
2216-
end
2217-
2218-
defp year_day_to_year_date(extra_day, day_of_year) when day_of_year < 334 + extra_day do
2219-
{11, day_of_year - (304 + extra_day)}
2220-
end
2193+
# Note that this function does not add the extra leap day for a leap year.
2194+
# If you want to add that leap day when appropriate,
2195+
# add the result of leap_day_offset/2 to the result of days_before_month/1.
2196+
defp days_before_month(1), do: 0
2197+
defp days_before_month(2), do: 31
2198+
defp days_before_month(3), do: 59
2199+
defp days_before_month(4), do: 90
2200+
defp days_before_month(5), do: 120
2201+
defp days_before_month(6), do: 151
2202+
defp days_before_month(7), do: 181
2203+
defp days_before_month(8), do: 212
2204+
defp days_before_month(9), do: 243
2205+
defp days_before_month(10), do: 273
2206+
defp days_before_month(11), do: 304
2207+
defp days_before_month(12), do: 334
22212208

2222-
defp year_day_to_year_date(extra_day, day_of_year) do
2223-
{12, day_of_year - (334 + extra_day)}
2224-
end
2209+
# Note that 0 is the first day of the month.
2210+
defp year_day_to_year_date(day_of_year) when day_of_year < 31, do: {1, day_of_year}
2211+
defp year_day_to_year_date(day_of_year) when day_of_year < 59, do: {2, day_of_year - 31}
2212+
defp year_day_to_year_date(day_of_year) when day_of_year < 90, do: {3, day_of_year - 59}
2213+
defp year_day_to_year_date(day_of_year) when day_of_year < 120, do: {4, day_of_year - 90}
2214+
defp year_day_to_year_date(day_of_year) when day_of_year < 151, do: {5, day_of_year - 120}
2215+
defp year_day_to_year_date(day_of_year) when day_of_year < 181, do: {6, day_of_year - 151}
2216+
defp year_day_to_year_date(day_of_year) when day_of_year < 212, do: {7, day_of_year - 181}
2217+
defp year_day_to_year_date(day_of_year) when day_of_year < 243, do: {8, day_of_year - 212}
2218+
defp year_day_to_year_date(day_of_year) when day_of_year < 273, do: {9, day_of_year - 243}
2219+
defp year_day_to_year_date(day_of_year) when day_of_year < 304, do: {10, day_of_year - 273}
2220+
defp year_day_to_year_date(day_of_year) when day_of_year < 334, do: {11, day_of_year - 304}
2221+
defp year_day_to_year_date(day_of_year), do: {12, day_of_year - 334}
2222+
2223+
defp year_day_to_year_date_leap(day_of_year) when day_of_year < 31, do: {1, day_of_year}
2224+
defp year_day_to_year_date_leap(day_of_year) when day_of_year < 60, do: {2, day_of_year - 31}
2225+
defp year_day_to_year_date_leap(day_of_year) when day_of_year < 91, do: {3, day_of_year - 60}
2226+
defp year_day_to_year_date_leap(day_of_year) when day_of_year < 121, do: {4, day_of_year - 91}
2227+
defp year_day_to_year_date_leap(day_of_year) when day_of_year < 152, do: {5, day_of_year - 121}
2228+
defp year_day_to_year_date_leap(day_of_year) when day_of_year < 182, do: {6, day_of_year - 152}
2229+
defp year_day_to_year_date_leap(day_of_year) when day_of_year < 213, do: {7, day_of_year - 182}
2230+
defp year_day_to_year_date_leap(day_of_year) when day_of_year < 244, do: {8, day_of_year - 213}
2231+
defp year_day_to_year_date_leap(day_of_year) when day_of_year < 274, do: {9, day_of_year - 244}
2232+
defp year_day_to_year_date_leap(day_of_year) when day_of_year < 305, do: {10, day_of_year - 274}
2233+
defp year_day_to_year_date_leap(day_of_year) when day_of_year < 335, do: {11, day_of_year - 305}
2234+
defp year_day_to_year_date_leap(day_of_year), do: {12, day_of_year - 335}
22252235

22262236
defp iso_seconds_to_datetime(seconds) do
22272237
{days, rest_seconds} = div_rem(seconds, @seconds_per_day)

0 commit comments

Comments
 (0)