-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
3 changed files
with
279 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,151 @@ | ||
defmodule TimeZoneInfo.IanaDateTime do | ||
@moduledoc """ | ||
Some functions to handle datetimes in `TimeZoneInfo`. | ||
""" | ||
|
||
alias Calendar.ISO | ||
alias TimeZoneInfo.IanaParser | ||
|
||
@seconds_per_minute 60 | ||
@seconds_per_hour 60 * @seconds_per_minute | ||
@seconds_per_day 24 * @seconds_per_hour | ||
|
||
@microsecond {0, 0} | ||
|
||
@typedoc "The number of gregorian seconds starting with year 0" | ||
@type gregorian_seconds :: non_neg_integer() | ||
|
||
@doc """ | ||
Builds a new ISO naive datetime. | ||
This function differs in the types and arity from `NaiveDateTime.new/6`, | ||
because it handles also day formats from the IANA DB. | ||
""" | ||
@spec new( | ||
Calendar.year(), | ||
Calendar.month(), | ||
IanaParser.day(), | ||
Calendar.hour(), | ||
Calendar.minute(), | ||
Calendar.second() | ||
) :: NaiveDateTime.t() | ||
def new(year, month \\ 1, day \\ 1, hour \\ 0, minute \\ 0, second \\ 0) | ||
|
||
def new(year, month, day, hour, minute, second) when is_integer(day) do | ||
with {:ok, naive_datetime} <- NaiveDateTime.new(year, month, day, 0, 0, 0) do | ||
seconds = hour * @seconds_per_hour + minute * @seconds_per_minute + second | ||
NaiveDateTime.add(naive_datetime, seconds, :second) | ||
end | ||
end | ||
|
||
def new(year, month, day, hour, minute, second) do | ||
year | ||
|> new(month, 1, hour, minute, second) | ||
|> NaiveDateTime.add(to_day(year, month, day) * @seconds_per_day, :second) | ||
end | ||
|
||
defp to_day(_year, _month, day) when is_integer(day), do: day | ||
|
||
defp to_day(year, month, last_day_of_week: last_day_of_week), | ||
do: to_last_day_of_week(year, month, last_day_of_week) | ||
|
||
defp to_day(year, month, day: day, op: op, day_of_week: day_of_week), | ||
do: to_day_of_week(year, month, day, day_of_week, op) | ||
|
||
defp to_last_day_of_week(year, month, day_of_week) do | ||
days_in_month = ISO.days_in_month(year, month) | ||
last = ISO.day_of_week(year, month + 1, 1) - 1 | ||
|
||
days_in_month - rem(7 - (day_of_week - last), 7) | ||
end | ||
|
||
defp to_day_of_week(year, month, day, day_of_week, op) do | ||
current = ISO.day_of_week(year, month, day) | ||
|
||
case op do | ||
:ge -> day + rem(7 + (day_of_week - current), 7) | ||
:le -> day - rem(7 + (current - day_of_week), 7) | ||
end | ||
end | ||
|
||
@doc """ | ||
Builds a new ISO naive datetime from parsed IANA data. | ||
""" | ||
@spec from_iana(tuple()) :: NaiveDateTime.t() | ||
def from_iana(tuple) do | ||
apply(__MODULE__, :new, Tuple.to_list(tuple)) | ||
end | ||
|
||
@spec from_iana( | ||
Calendar.year(), | ||
Calendar.month(), | ||
IanaParser.day(), | ||
IanaParser.time() | ||
) :: NaiveDateTime.t() | ||
def from_iana(year, month, day, {hour, minute, second}) do | ||
new(year, month, day, hour, minute, second) | ||
end | ||
|
||
@doc """ | ||
Returns the Calendar.iso_days/0 format of the specified date. | ||
""" | ||
@spec to_iso_days(NaiveDateTime.t()) :: Calendar.iso_days() | ||
def to_iso_days(%NaiveDateTime{year: yr, month: mo, day: dy, hour: hr, minute: m, second: s}), | ||
do: ISO.naive_datetime_to_iso_days(yr, mo, dy, hr, m, s, @microsecond) | ||
|
||
@doc """ | ||
Builds a new ISO naive datetime from iso days. | ||
""" | ||
@spec from_iso_days(Calendar.iso_days()) :: NaiveDateTime.t() | ||
def from_iso_days(iso_days) do | ||
naive_datetime = iso_days |> ISO.naive_datetime_from_iso_days() | ||
{:ok, naive_datetime} = NaiveDateTime |> apply(:new, Tuple.to_list(naive_datetime)) | ||
naive_datetime | ||
end | ||
|
||
def to_gregorian_seconds(date) do | ||
case date do | ||
{year} -> | ||
to_gregorian_seconds(year, 1, 1, {0, 0, 0}) | ||
|
||
{year, month} -> | ||
to_gregorian_seconds(year, month, 1, {0, 0, 0}) | ||
|
||
{year, month, day} -> | ||
to_gregorian_seconds(year, month, day, {0, 0, 0}) | ||
|
||
{year, month, day, hour} -> | ||
to_gregorian_seconds(year, month, day, {hour, 0, 0}) | ||
|
||
{year, month, day, hour, minute} -> | ||
to_gregorian_seconds(year, month, day, {hour, minute, 0}) | ||
|
||
{year, month, day, hour, minute, second} -> | ||
to_gregorian_seconds(year, month, day, {hour, minute, second}) | ||
end | ||
end | ||
|
||
def to_gregorian_seconds(year, month, day, time) do | ||
day = to_day(year, month, day) | ||
|
||
do_to_gregorian_seconds({{year, month, day}, time}) | ||
end | ||
|
||
defp do_to_gregorian_seconds({date, time}) do | ||
date = update(date) | ||
:calendar.datetime_to_gregorian_seconds({date, time}) | ||
end | ||
|
||
defp update({year, month, day}) do | ||
case day > 0 && day <= ISO.days_in_month(year, month) do | ||
true -> | ||
{year, month, day} | ||
|
||
false -> | ||
{:ok, date} = NaiveDateTime.new(year, month, 1, 0, 0, 0) | ||
date = NaiveDateTime.add(date, (day - 1) * @seconds_per_day) | ||
|
||
{date.year, date.month, date.day} | ||
end | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,39 @@ | ||
defmodule TimeZoneInfo.IsoDays do | ||
@moduledoc """ | ||
A module to handle ISO days. | ||
""" | ||
|
||
alias Calendar.ISO | ||
alias TimeZoneInfo.GregorianSeconds | ||
|
||
@seconds_per_day 24 * 60 * 60 | ||
@microseconds_per_second 1_000_000 | ||
@parts_per_day @seconds_per_day * @microseconds_per_second | ||
|
||
@doc """ | ||
Converts an ISO day to gregorian seconds. | ||
""" | ||
@spec to_gregorian_seconds(Calendar.iso_days()) :: GregorianSeconds.t() | ||
def to_gregorian_seconds({days, {parts_in_day, @parts_per_day}}) do | ||
div(days * @parts_per_day + parts_in_day, @microseconds_per_second) | ||
end | ||
|
||
@doc """ | ||
Converts an ISO day to a naive datetime. | ||
""" | ||
@spec to_naive(Calendar.iso_days()) :: NaiveDateTime.t() | ||
def to_naive(iso_days) do | ||
naive_datetime = ISO.naive_datetime_from_iso_days(iso_days) | ||
{:ok, naive_datetime} = apply(NaiveDateTime, :new, Tuple.to_list(naive_datetime)) | ||
naive_datetime | ||
end | ||
|
||
@doc """ | ||
Converts an ISO day to year. | ||
""" | ||
@spec to_year(Calendar.iso_days()) :: Calendar.year() | ||
def to_year(iso_days) do | ||
{year, _, _, _, _, _, _} = ISO.naive_datetime_from_iso_days(iso_days) | ||
year | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,89 @@ | ||
defmodule TimeZoneInfo.IanaDateTimeTest do | ||
use ExUnit.Case, async: true | ||
|
||
alias TimeZoneInfo.IanaDateTime | ||
|
||
describe "to_gregorian_seconds/4" do | ||
test "returns seconds for datetimes with [day: y, op: :ge, day_of_week: y]" do | ||
assert IanaDateTime.to_gregorian_seconds( | ||
1932, | ||
10, | ||
[day: 1, op: :ge, day_of_week: 7], | ||
{0, 0, 0} | ||
) == to_gregorian_seconds(~N[1932-10-02 00:00:00]) | ||
end | ||
|
||
test "returns seconds for datetimes with [day: 31, op: :ge, day_of_week: y]" do | ||
assert IanaDateTime.to_gregorian_seconds( | ||
1973, | ||
10, | ||
[day: 31, op: :ge, day_of_week: 7], | ||
{2, 0, 0} | ||
) == to_gregorian_seconds(~N[1973-11-04 02:00:00]) | ||
end | ||
end | ||
|
||
describe "to_gregorian_seconds/1" do | ||
test "with year" do | ||
assert IanaDateTime.to_gregorian_seconds({1999}) == | ||
to_gregorian_seconds(~N[1999-01-01 00:00:00]) | ||
end | ||
|
||
test "with year, month, day, hour, minute and second" do | ||
assert IanaDateTime.to_gregorian_seconds({1999, 2, 3, 10, 11, 12}) == | ||
to_gregorian_seconds(~N[1999-02-03 10:11:12]) | ||
end | ||
|
||
test "with day as last day of week" do | ||
# The last Monday in February 2019. | ||
day = [last_day_of_week: 1] | ||
|
||
assert IanaDateTime.to_gregorian_seconds({2019, 2, day, 10}) == | ||
to_gregorian_seconds(~N[2019-02-25 10:00:00]) | ||
end | ||
|
||
test "with day as day of week with op ge" do | ||
# The first Sunday in February 2019. | ||
day = [day: 1, op: :ge, day_of_week: 7] | ||
|
||
assert IanaDateTime.to_gregorian_seconds({2019, 2, day, 10}) == | ||
to_gregorian_seconds(~N[2019-02-03 10:00:00]) | ||
end | ||
|
||
test "with day as day of week with op ge and equal day" do | ||
# The first Sunday in February 2019. | ||
day = [day: 3, op: :ge, day_of_week: 7] | ||
|
||
assert IanaDateTime.to_gregorian_seconds({2019, 2, day, 10}) == | ||
to_gregorian_seconds(~N[2019-02-03 10:00:00]) | ||
end | ||
|
||
test "with day as day of week with op ge and result in next month" do | ||
# The first Sunday in February 2019. | ||
day = [day: 31, op: :ge, day_of_week: 7] | ||
|
||
assert IanaDateTime.to_gregorian_seconds({2019, 1, day, 10}) == | ||
to_gregorian_seconds(~N[2019-02-03 10:00:00]) | ||
end | ||
|
||
test "with day as day of week with op le" do | ||
# Saturday before 15 February 2019. | ||
day = [day: 15, op: :le, day_of_week: 6] | ||
|
||
assert IanaDateTime.to_gregorian_seconds({2019, 2, day, 10}) == | ||
to_gregorian_seconds(~N[2019-02-09 10:00:00]) | ||
end | ||
|
||
test "with day as day of week with op le and equal day" do | ||
# Saturday before 15 February 2019. | ||
day = [day: 9, op: :le, day_of_week: 6] | ||
|
||
assert IanaDateTime.to_gregorian_seconds({2019, 2, day, 10}) == | ||
to_gregorian_seconds(~N[2019-02-09 10:00:00]) | ||
end | ||
end | ||
|
||
defp to_gregorian_seconds(naive_datetime) do | ||
naive_datetime |> NaiveDateTime.to_erl() |> :calendar.datetime_to_gregorian_seconds() | ||
end | ||
end |