Skip to content

Commit

Permalink
Fix #537 - precision not preserved in some situations
Browse files Browse the repository at this point in the history
  • Loading branch information
bitwalker committed Jun 27, 2019
1 parent 04dc72b commit 59650d4
Show file tree
Hide file tree
Showing 13 changed files with 131 additions and 82 deletions.
14 changes: 11 additions & 3 deletions lib/convert/convert.ex
Original file line number Diff line number Diff line change
Expand Up @@ -27,11 +27,19 @@ defmodule Timex.Convert do
second = Map.get(datetime_map, :second, 0)
us = Map.get(datetime_map, :microsecond, {0, 0})
tz = Map.get(datetime_map, :time_zone, nil)
{us, precision} =
case us do
{_us, _precision} = val ->
val

us when is_integer(us) ->
Timex.DateTime.Helpers.construct_microseconds(us, -1)
end
case tz do
s when is_binary(s) ->
Timex.DateTime.Helpers.construct({{year,month,day},{hour,minute,second,us}}, tz)
Timex.DateTime.Helpers.construct({{year,month,day},{hour,minute,second,us}}, precision, tz)
nil ->
{:ok, nd} = NaiveDateTime.new(year, month, day, hour, minute, second, us)
{:ok, nd} = NaiveDateTime.new(year, month, day, hour, minute, second, {us, precision})
nd
end
end
Expand Down Expand Up @@ -76,7 +84,7 @@ defmodule Timex.Convert do
{k, v}, acc when k in [:milliseconds, "milliseconds", :ms, "ms", :millisecond, "millisecond"] ->
case v do
n when is_integer(n) ->
us = Timex.DateTime.Helpers.construct_microseconds(n*1_000)
us = Timex.DateTime.Helpers.construct_microseconds(n*1_000, -1)
Map.put(acc, :microsecond, us)
:error ->
{:error, {:expected_integer, for: k, got: v}}
Expand Down
35 changes: 18 additions & 17 deletions lib/datetime/datetime.ex
Original file line number Diff line number Diff line change
Expand Up @@ -102,19 +102,23 @@ defimpl Timex.Protocol, for: DateTime do
end

@spec end_of_year(DateTime.t) :: DateTime.t
def end_of_year(%DateTime{year: year, time_zone: tz}),
do: %{Timex.to_datetime({{year, 12, 31}, {23, 59, 59}}, tz) | :microsecond => {999_999, 6}}
def end_of_year(%DateTime{year: year, time_zone: tz, microsecond: {_, precision}}) do
us = Timex.DateTime.Helpers.to_precision(999_999, precision)
%{Timex.to_datetime({{year, 12, 31}, {23, 59, 59}}, tz) | :microsecond => {us, precision}}
end

@spec beginning_of_quarter(DateTime.t) :: DateTime.t
def beginning_of_quarter(%DateTime{year: year, month: month, time_zone: tz}) do
def beginning_of_quarter(%DateTime{year: year, month: month, time_zone: tz} = date) do
month = 1 + (3 * (Timex.quarter(month) - 1))
Timex.DateTime.Helpers.construct({year, month, 1}, tz)
{_, precision} = date.microsecond
Timex.DateTime.Helpers.construct({{year, month, 1}, {0,0,0,0}}, precision, tz)
end

@spec end_of_quarter(DateTime.t) :: DateTime.t | AmbiguousDateTime.t
def end_of_quarter(%DateTime{year: year, month: month, time_zone: tz}) do
def end_of_quarter(%DateTime{year: year, month: month, time_zone: tz} = date) do
month = 3 * Timex.quarter(month)
case Timex.DateTime.Helpers.construct({year,month,1}, tz) do
{_, precision} = date.microsecond
case Timex.DateTime.Helpers.construct({{year,month,1}, {23,59,59,999_999}}, precision, tz) do
{:error, _} = err -> err
%DateTime{} = d -> end_of_month(d)
%AmbiguousDateTime{:before => b, :after => a} ->
Expand All @@ -124,12 +128,14 @@ defimpl Timex.Protocol, for: DateTime do
end

@spec beginning_of_month(DateTime.t) :: DateTime.t
def beginning_of_month(%DateTime{year: year, month: month, time_zone: tz}),
do: Timex.DateTime.Helpers.construct({{year, month, 1}, {0, 0, 0, 0}}, tz)
def beginning_of_month(%DateTime{year: year, month: month, time_zone: tz, microsecond: {_, precision}}) do
Timex.DateTime.Helpers.construct({{year, month, 1}, {0, 0, 0, 0}}, precision, tz)
end

@spec end_of_month(DateTime.t) :: DateTime.t
def end_of_month(%DateTime{year: year, month: month, time_zone: tz} = date),
do: Timex.DateTime.Helpers.construct({{year, month, days_in_month(date)},{23,59,59,999_999}}, tz)
def end_of_month(%DateTime{year: year, month: month, time_zone: tz, microsecond: {_, precision}} = date) do
Timex.DateTime.Helpers.construct({{year, month, days_in_month(date)},{23,59,59,999_999}}, precision, tz)
end

@spec quarter(DateTime.t) :: 1..4
def quarter(%DateTime{month: month}), do: Timex.quarter(month)
Expand All @@ -138,7 +144,7 @@ defimpl Timex.Protocol, for: DateTime do

def week_of_month(%DateTime{:year => y, :month => m, :day => d}), do: Timex.week_of_month(y,m,d)

def weekday(%DateTime{:year => y, :month => m, :day => d}), do: :calendar.day_of_the_week({y, m, d})
def weekday(%DateTime{:year => y, :month => m, :day => d}), do: :calendar.day_of_the_week({y, m, d})

def day(%DateTime{} = date) do
ref = beginning_of_year(date)
Expand Down Expand Up @@ -272,12 +278,7 @@ defimpl Timex.Protocol, for: DateTime do

defp raw_convert(secs, {us, precision}) do
{date,{h,mm,s}} = :calendar.gregorian_seconds_to_datetime(secs)
if precision == 0 do
Timex.DateTime.Helpers.construct({date, {h,mm,s,us}}, "Etc/UTC")
else
%DateTime{microsecond: {us, _}} = dt = Timex.DateTime.Helpers.construct({date, {h,mm,s,us}}, "Etc/UTC")
%DateTime{dt | microsecond: {us, precision}}
end
Timex.DateTime.Helpers.construct({date, {h,mm,s,us}}, precision, "Etc/UTC")
end

defp logical_shift(datetime, []), do: datetime
Expand Down
2 changes: 1 addition & 1 deletion lib/datetime/erlang.ex
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ defimpl Timex.Protocol, for: Tuple do

@spec to_naive_datetime(Types.date | Types.datetime | Types.microsecond_datetime) :: NaiveDateTime.t
def to_naive_datetime({{y,m,d},{h,mm,s,us}}) when is_datetime(y,m,d,h,mm,s) do
us = Timex.DateTime.Helpers.construct_microseconds(us)
us = Timex.DateTime.Helpers.construct_microseconds(us, -1)
%NaiveDateTime{year: y, month: m, day: d, hour: h, minute: mm, second: s, microsecond: us}
end
def to_naive_datetime(date) do
Expand Down
51 changes: 40 additions & 11 deletions lib/datetime/helpers.ex
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ defmodule Timex.DateTime.Helpers do

alias Timex.{Types, Timezone, TimezoneInfo, AmbiguousDateTime, AmbiguousTimezoneInfo}

@type precision :: -1 | 0..6

@doc """
Constructs an empty DateTime, for internal use only
"""
Expand All @@ -23,13 +25,17 @@ defmodule Timex.DateTime.Helpers do
@spec construct(Types.date, Types.valid_timezone) :: DateTime.t | AmbiguousDateTime.t | {:error, term}
@spec construct(Types.datetime, Types.valid_timezone) :: DateTime.t | AmbiguousDateTime.t | {:error, term}
@spec construct(Types.microsecond_datetime, Types.valid_timezone) :: DateTime.t | AmbiguousDateTime.t | {:error, term}
@spec construct(Types.microsecond_datetime, precision, Types.valid_timezone) :: DateTime.t | AmbiguousDateTime.t | {:error, term}
def construct({_, _, _} = date, timezone) do
construct({date, {0,0,0,0}}, timezone)
construct({date, {0,0,0,0}}, 0, timezone)
end
def construct({{_,_,_} = date, {h,mm,s}}, timezone) do
construct({date,{h,mm,s,0}}, timezone)
construct({date,{h,mm,s,0}}, 0, timezone)
end
def construct({{_,_,_} = date, {_,_,_,_} = time}, timezone) do
construct({date,time}, -1, timezone)
end
def construct({{y,m,d} = date, {h,mm,s,us}}, timezone) do
def construct({{y,m,d} = date, {h,mm,s,us}}, precision, timezone) do
seconds_from_zeroyear = :calendar.datetime_to_gregorian_seconds({date,{h,mm,s}})
case Timezone.name_of(timezone) do
{:error, _} = err ->
Expand All @@ -41,35 +47,58 @@ defmodule Timex.DateTime.Helpers do
%TimezoneInfo{} = tz ->
%DateTime{:year => y, :month => m, :day => d,
:hour => h, :minute => mm, :second => s,
:microsecond => construct_microseconds(us),
:microsecond => construct_microseconds(us, precision),
:time_zone => tz.full_name, :zone_abbr => tz.abbreviation,
:utc_offset => tz.offset_utc, :std_offset => tz.offset_std}
%AmbiguousTimezoneInfo{before: b, after: a} ->
bd = %DateTime{:year => y, :month => m, :day => d,
:hour => h, :minute => mm, :second => s,
:microsecond => construct_microseconds(us),
:microsecond => construct_microseconds(us, precision),
:time_zone => b.full_name, :zone_abbr => b.abbreviation,
:utc_offset => b.offset_utc, :std_offset => b.offset_std}
ad = %DateTime{:year => y, :month => m, :day => d,
:hour => h, :minute => mm, :second => s,
:microsecond => construct_microseconds(us),
:microsecond => construct_microseconds(us, precision),
:time_zone => a.full_name, :zone_abbr => a.abbreviation,
:utc_offset => a.offset_utc, :std_offset => a.offset_std}
%AmbiguousDateTime{before: bd, after: ad}
end
end
end

def construct_microseconds({us, p} = us_tuple) when is_integer(us) and is_integer(p), do: us_tuple
def construct_microseconds(0), do: {0,0}
def construct_microseconds(n), do: {n, precision(n)}
def construct_microseconds({us, p}) when is_integer(us) and is_integer(p) do
construct_microseconds(us, p)
end
# Input precision of -1 means it should be recalculated based on the value
def construct_microseconds(0, -1), do: {0,0}
def construct_microseconds(0, p), do: {0,p}
def construct_microseconds(n, -1), do: {n, precision(n)}
def construct_microseconds(n, p), do: {to_precision(n, p), p}

def to_precision(0, _p), do: 0
def to_precision(us, p) do
case precision(us) do
detected_p when detected_p > p ->
# Convert to lower precision
pow = trunc(:math.pow(10, detected_p - p))
Integer.floor_div(us, pow) * pow

_detected_p ->
# Already correct precision or less precise
us
end
end

defp precision(0), do: 0
defp precision(n) when is_integer(n) do
ns = Integer.to_string(n)
n_width = byte_size(ns)
trimmed = byte_size(String.trim_trailing(ns, "0"))
p = 6 - (n_width - trimmed)
if p > 6, do: 6, else: p
new_p = 6 - (n_width - trimmed)
if new_p >= 6 do
6
else
new_p
end
end
end
44 changes: 31 additions & 13 deletions lib/datetime/naivedatetime.ex
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,9 @@ defimpl Timex.Protocol, for: NaiveDateTime do
def to_date(date), do: NaiveDateTime.to_date(date)

@spec to_datetime(NaiveDateTime.t, timezone :: Types.valid_timezone) :: DateTime.t | {:error, term}
def to_datetime(%NaiveDateTime{:microsecond => {us,_}} = d, timezone) do
def to_datetime(%NaiveDateTime{:microsecond => {us,precision}} = d, timezone) do
{date,{h,mm,s}} = NaiveDateTime.to_erl(d)
Timex.DateTime.Helpers.construct({date,{h,mm,s,us}}, timezone)
Timex.DateTime.Helpers.construct({date,{h,mm,s,us}}, precision, timezone)
end

@spec to_naive_datetime(NaiveDateTime.t) :: NaiveDateTime.t
Expand All @@ -51,13 +51,18 @@ defimpl Timex.Protocol, for: NaiveDateTime do
def is_leap?(%NaiveDateTime{year: year}), do: :calendar.is_leap_year(year)

@spec beginning_of_day(NaiveDateTime.t) :: NaiveDateTime.t
def beginning_of_day(%NaiveDateTime{:microsecond => {_, _precision}} = datetime) do
%{datetime | :hour => 0, :minute => 0, :second => 0, :microsecond => {0, 0}}
def beginning_of_day(%NaiveDateTime{:microsecond => {_, precision}} = datetime) do
%{datetime | :hour => 0, :minute => 0, :second => 0, :microsecond => {0, precision}}
end

@spec end_of_day(NaiveDateTime.t) :: NaiveDateTime.t
def end_of_day(%NaiveDateTime{microsecond: {_, _precision}} = datetime) do
%{datetime | :hour => 23, :minute => 59, :second => 59, :microsecond => {999_999, 6}}
def end_of_day(%NaiveDateTime{microsecond: {_, precision}} = datetime) do
if precision > 0 do
us = Timex.DateTime.Helpers.to_precision(999_999, precision)
%{datetime | :hour => 23, :minute => 59, :second => 59, :microsecond => {us, precision}}
else
%{datetime | :hour => 23, :minute => 59, :second => 59, :microsecond => {0, 0}}
end
end

@spec beginning_of_week(NaiveDateTime.t, Types.weekstart) :: NaiveDateTime.t
Expand Down Expand Up @@ -85,8 +90,14 @@ defimpl Timex.Protocol, for: NaiveDateTime do
end

@spec end_of_year(NaiveDateTime.t) :: NaiveDateTime.t
def end_of_year(%NaiveDateTime{} = date),
do: %{date | :month => 12, :day => 31, :hour => 23, :minute => 59, :second => 59, :microsecond => {999_999, 6}}
def end_of_year(%NaiveDateTime{microsecond: {_, precision}} = date) do
if precision > 0 do
us = Timex.DateTime.Helpers.to_precision(999_999, precision)
%{date | :month => 12, :day => 31, :hour => 23, :minute => 59, :second => 59, :microsecond => {us, precision}}
else
%{date | :month => 12, :day => 31, :hour => 23, :minute => 59, :second => 59, :microsecond => {0,0}}
end
end

@spec beginning_of_quarter(NaiveDateTime.t) :: NaiveDateTime.t
def beginning_of_quarter(%NaiveDateTime{month: month} = date) do
Expand All @@ -101,12 +112,18 @@ defimpl Timex.Protocol, for: NaiveDateTime do
end

@spec beginning_of_month(NaiveDateTime.t) :: NaiveDateTime.t
def beginning_of_month(%NaiveDateTime{} = datetime),
do: %{datetime | :day => 1, :hour => 0, :minute => 0, :second => 0, :microsecond => {0,0}}
def beginning_of_month(%NaiveDateTime{microsecond: {_, precision}} = datetime),
do: %{datetime | :day => 1, :hour => 0, :minute => 0, :second => 0, :microsecond => {0,precision}}

@spec end_of_month(NaiveDateTime.t) :: NaiveDateTime.t
def end_of_month(%NaiveDateTime{} = date),
do: %{date | :day => days_in_month(date), :hour => 23, :minute => 59, :second => 59, :microsecond => {999_999, 6}}
def end_of_month(%NaiveDateTime{microsecond: {_, precision}} = date) do
if precision > 0 do
us = Timex.DateTime.Helpers.to_precision(999_999, precision)
%{date | :day => days_in_month(date), :hour => 23, :minute => 59, :second => 59, :microsecond => {us, precision}}
else
%{date | :day => days_in_month(date), :hour => 23, :minute => 59, :second => 59, :microsecond => {0,0}}
end
end

@spec quarter(NaiveDateTime.t) :: 1..4
def quarter(%NaiveDateTime{month: month}), do: Timex.quarter(month)
Expand Down Expand Up @@ -283,11 +300,12 @@ defimpl Timex.Protocol, for: NaiveDateTime do
else
seconds_from_zero = div(microseconds_from_zero, 1_000_000)
rem_microseconds = rem(microseconds_from_zero, 1_000_000)
us = Timex.DateTime.Helpers.to_precision(rem_microseconds, current_precision)

seconds_from_zero
|> :calendar.gregorian_seconds_to_datetime
|> Timex.to_naive_datetime
|> Map.put(:microsecond, {rem_microseconds, current_precision})
|> Map.put(:microsecond, {us, current_precision})
end
end
defp shift_by(_datetime, _value, units),
Expand Down
2 changes: 1 addition & 1 deletion lib/parse/datetime/helpers.ex
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ defmodule Timex.Parse.DateTime.Helpers do
n = ms |> String.trim_leading("0")
n = if n == "", do: 0, else: String.to_integer(n)
n = n * 1_000
[sec_fractional: Timex.DateTime.Helpers.construct_microseconds(n)]
[sec_fractional: Timex.DateTime.Helpers.construct_microseconds(n, -1)]
end
def parse_microseconds(us) do
n_width = byte_size(us)
Expand Down
6 changes: 3 additions & 3 deletions lib/parse/datetime/parser.ex
Original file line number Diff line number Diff line change
Expand Up @@ -323,12 +323,12 @@ defmodule Timex.Parse.DateTime.Parser do
case value do
"" -> date
n when is_number(n) ->
%{date | :microsecond => Timex.DateTime.Helpers.construct_microseconds(n)}
%{date | :microsecond => Timex.DateTime.Helpers.construct_microseconds(n, -1)}
{_n, _precision} = us ->
%{date | :microsecond => us}
end
:us -> %{date | :microsecond => Timex.DateTime.Helpers.construct_microseconds(value)}
:ms -> %{date | :microsecond => Timex.DateTime.Helpers.construct_microseconds(value*1_000)}
:us -> %{date | :microsecond => Timex.DateTime.Helpers.construct_microseconds(value, -1)}
:ms -> %{date | :microsecond => Timex.DateTime.Helpers.construct_microseconds(value*1_000, -1)}
:sec_epoch ->
DateTime.from_unix!(value)
am_pm when am_pm in [:am, :AM] ->
Expand Down
4 changes: 2 additions & 2 deletions lib/time/duration.ex
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ defmodule Timex.Duration do
@spec to_time(__MODULE__.t) :: {:ok, Time.t} | {:error, atom}
def to_time(%__MODULE__{} = d) do
{h,m,s,us} = to_clock(d)
Time.from_erl({h,m,s}, Timex.DateTime.Helpers.construct_microseconds(us))
Time.from_erl({h,m,s}, Timex.DateTime.Helpers.construct_microseconds(us, -1))
end

@doc """
Expand All @@ -86,7 +86,7 @@ defmodule Timex.Duration do
@spec to_time!(__MODULE__.t) :: Time.t | no_return
def to_time!(%__MODULE__{} = d) do
{h,m,s,us} = to_clock(d)
Time.from_erl!({h,m,s}, Timex.DateTime.Helpers.construct_microseconds(us))
Time.from_erl!({h,m,s}, Timex.DateTime.Helpers.construct_microseconds(us, -1))
end

@doc """
Expand Down
10 changes: 5 additions & 5 deletions lib/timex.ex
Original file line number Diff line number Diff line change
Expand Up @@ -1324,7 +1324,7 @@ defmodule Timex do
iex> date = ~N[2015-06-15T12:30:00Z]
iex> Timex.end_of_month(date)
~N[2015-06-30T23:59:59.999999Z]
~N[2015-06-30T23:59:59Z]
"""
@spec end_of_month(Types.valid_datetime()) :: Types.valid_datetime() | {:error, term}
Expand Down Expand Up @@ -1382,7 +1382,7 @@ defmodule Timex do
iex> date = ~N[2015-06-15T12:30:00]
...> Timex.end_of_quarter(date)
~N[2015-06-30T23:59:59.999999]
~N[2015-06-30T23:59:59]
iex> Timex.end_of_quarter(2015, 4)
~D[2015-06-30]
Expand Down Expand Up @@ -1427,7 +1427,7 @@ defmodule Timex do
iex> date = ~N[2015-06-15T00:00:00]
iex> Timex.end_of_year(date)
~N[2015-12-31T23:59:59.999999]
~N[2015-12-31T23:59:59]
iex> Timex.end_of_year(2015)
~D[2015-12-31]
Expand Down Expand Up @@ -1542,7 +1542,7 @@ defmodule Timex do
iex> date = ~N[2015-11-30T13:30:30] # Monday 30th November
...> Timex.end_of_week(date)
~N[2015-12-06T23:59:59.999999]
~N[2015-12-06T23:59:59]
iex> date = ~D[2015-11-30] # Monday 30th November
...> Timex.end_of_week(date, :sun)
Expand Down Expand Up @@ -1577,7 +1577,7 @@ defmodule Timex do
iex> date = ~N[2015-01-01T13:14:15]
...> Timex.end_of_day(date)
~N[2015-01-01T23:59:59.999999]
~N[2015-01-01T23:59:59]
"""
@spec end_of_day(Types.valid_datetime()) :: Types.valid_datetime() | {:error, term}
Expand Down
Loading

0 comments on commit 59650d4

Please sign in to comment.