Skip to content

Commit

Permalink
Add files
Browse files Browse the repository at this point in the history
  • Loading branch information
NickNeck committed Apr 13, 2020
1 parent 0224ee4 commit 46878e5
Show file tree
Hide file tree
Showing 3 changed files with 279 additions and 0 deletions.
151 changes: 151 additions & 0 deletions lib/time_zone_info/iana_datetime.ex
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
39 changes: 39 additions & 0 deletions lib/time_zone_info/iso_days.ex
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
89 changes: 89 additions & 0 deletions test/time_zone_info/iana_datetime_test.exs
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

0 comments on commit 46878e5

Please sign in to comment.