@@ -196,14 +196,28 @@ defmodule Calendar.ISO do
196196 @ ext_date_sep ?-
197197 @ ext_time_sep ?:
198198
199- @ days_per_nonleap_year 365
200- @ days_per_leap_year 366
201-
202199 # The ISO epoch starts, in this implementation,
203200 # with ~D[0000-01-01]. Era "1" starts
204201 # on ~D[0001-01-01] which is 366 days later.
205202 @ iso_epoch 366
206203
204+ # Constants for date calculations using 400-year era cycles.
205+ # The algorithm uses a March-based year where March 1 is day 0.
206+ # Reference: Neri C, Schneider L. "Euclidean Affine Functions and
207+ # their Application to Calendar Algorithms". Softw Pract Exper. 2022.
208+ @ days_per_year 365
209+ @ years_per_era 400
210+ @ days_per_era @ years_per_era * @ days_per_year + 97
211+ @ days_per_4_years 4 * @ days_per_year
212+ @ days_per_100_years 100 * @ days_per_year + 24
213+ @ march_1_offset 31 + 29
214+ @ unix_epoch_days 719_528
215+
216+ # Month calculation constants: in a March-based year, each 5-month
217+ # cycle has exactly 153 days (31+30+31+30+31 or 31+30+31+30+31).
218+ @ days_per_5_months 153
219+ @ months_per_cycle 5
220+
207221 [ match_basic_date , match_ext_date , guard_date , read_date ] =
208222 quote do
209223 [
@@ -878,34 +892,49 @@ defmodule Calendar.ISO do
878892
879893 # Converts year, month, day to count of days since 0000-01-01.
880894 @ doc false
881- def date_to_iso_days ( 0 , 1 , 1 ) do
882- 0
883- end
884-
885- def date_to_iso_days ( 1970 , 1 , 1 ) do
886- 719_528
887- end
895+ def date_to_iso_days ( 0 , 1 , 1 ) , do: 0
896+ def date_to_iso_days ( 1970 , 1 , 1 ) , do: @ unix_epoch_days
888897
889898 def date_to_iso_days ( year , month , day ) do
890899 ensure_day_in_month! ( year , month , day )
891900
892- days_in_previous_years ( year ) + days_before_month ( month ) + leap_day_offset ( year , month ) + day -
893- 1
901+ y = if month <= 2 , do: year - 1 , else: year
902+ era = if y >= 0 , do: div ( y , @ years_per_era ) , else: div ( y - 399 , @ years_per_era )
903+ year_of_era = y - era * @ years_per_era
904+ month_prime = if month > 2 , do: month - 3 , else: month + 9
905+ day_of_year = div ( @ days_per_5_months * month_prime + 2 , @ months_per_cycle ) + day - 1
906+
907+ day_of_era =
908+ @ days_per_year * year_of_era + div ( year_of_era , 4 ) - div ( year_of_era , 100 ) + day_of_year
909+
910+ era * @ days_per_era + day_of_era + @ march_1_offset
894911 end
895912
896913 # Converts count of days since 0000-01-01 to {year, month, day} tuple.
897914 @ doc false
898915 def date_from_iso_days ( days ) do
899- { year , day_of_year } = days_to_year ( days )
916+ z = days - @ march_1_offset
917+ era = if z >= 0 , do: div ( z , @ days_per_era ) , else: div ( z - @ days_per_era + 1 , @ days_per_era )
918+ day_of_era = z - era * @ days_per_era
919+
920+ year_of_era =
921+ div (
922+ day_of_era - div ( day_of_era , @ days_per_4_years ) + div ( day_of_era , @ days_per_100_years ) -
923+ div ( day_of_era , @ days_per_era - 1 ) ,
924+ @ days_per_year
925+ )
900926
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
927+ day_of_year =
928+ day_of_era -
929+ ( @ days_per_year * year_of_era + div ( year_of_era , 4 ) - div ( year_of_era , 100 ) )
907930
908- { year , month , day_in_month + 1 }
931+ month_prime = div ( @ months_per_cycle * day_of_year + 2 , @ days_per_5_months )
932+ day = day_of_year - div ( @ days_per_5_months * month_prime + 2 , @ months_per_cycle ) + 1
933+ month = if month_prime < 10 , do: month_prime + 3 , else: month_prime - 9
934+ year = year_of_era + era * @ years_per_era
935+ year = if month <= 2 , do: year + 1 , else: year
936+
937+ { year , month , day }
909938 end
910939
911940 defp div_rem ( int1 , int2 ) do
@@ -2108,91 +2137,6 @@ defmodule Calendar.ISO do
21082137 if leap_year? ( year ) , do: 1 , else: 0
21092138 end
21102139
2111- defp days_to_year ( days ) when days < 0 do
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- )
2123-
2124- { year , days - day_start }
2125- end
2126-
2127- defp days_to_year ( days ) do
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- )
2139-
2140- { year , days - day_start }
2141- end
2142-
2143- defp days_to_year_interpolated ( min , max , _days , d_min , _d_max ) when min >= max do
2144- { min , d_min }
2145- end
2146-
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
2150-
2151- numerator = diff * ( days - d_min )
2152- offset = floor_div_positive_divisor ( numerator , d_diff )
2153-
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
2157-
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
2170- end
2171-
2172- defp days_in_previous_years ( 0 ) , do: 0
2173-
2174- # A concise version of the algorithm would use floor_div instead of div.
2175- # However, floor_div would check the operands on every operation.
2176- # We optimize this by providing a positive and negative version of each algorithm.
2177- defp days_in_previous_years ( year ) when year > 0 do
2178- previous_year = year - 1
2179-
2180- div ( previous_year , 4 ) - div ( previous_year , 100 ) +
2181- div ( previous_year , 400 ) + previous_year * @ days_per_nonleap_year +
2182- @ days_per_leap_year
2183- end
2184-
2185- defp days_in_previous_years ( year ) when year < 0 do
2186- previous_year = year - 1
2187-
2188- div ( year , 4 ) - div ( year , 100 ) +
2189- div ( year , 400 ) - 1 + previous_year * @ days_per_nonleap_year +
2190- @ days_per_leap_year
2191- end
2192-
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.
21962140 defp days_before_month ( 1 ) , do: 0
21972141 defp days_before_month ( 2 ) , do: 31
21982142 defp days_before_month ( 3 ) , do: 59
@@ -2206,33 +2150,6 @@ defmodule Calendar.ISO do
22062150 defp days_before_month ( 11 ) , do: 304
22072151 defp days_before_month ( 12 ) , do: 334
22082152
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 }
2235-
22362153 defp iso_seconds_to_datetime ( seconds ) do
22372154 { days , rest_seconds } = div_rem ( seconds , @ seconds_per_day )
22382155
0 commit comments