From 56a940b838d6fbc2e005306dcd598e82a33e63fc Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Sun, 18 Oct 2026 22:44:12 +0200 Subject: [PATCH] Fix Duration.now/1 for minutes/hours/days/weeks and correct several typespecs --- CHANGELOG.md | 2 ++ lib/calendar/julian.ex | 2 +- lib/datetime/datetime.ex | 3 ++- lib/parse/zoneinfo/parser.ex | 2 +- lib/time/duration.ex | 8 ++++---- lib/timex.ex | 4 ++-- test/duration_test.exs | 11 +++++++++++ 7 files changed, 23 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c1b0057e..83995aa5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,8 @@ This project adheres to [Semantic Versioning](http://semver.org/). ### Fixed +- Fixed `Timex.Duration.now/1` raising for `:minutes`, `:hours`, `:days` and `:weeks` +- Corrected typespecs of `Timex.Calendar.Julian.julian_date/1`, `Timex.normalize/2`, `Timex.Protocol.DateTime.shift/2` and `Timex.Parse.ZoneInfo.Parser.parse/1` - Updated `Timex.now/1` typespec to remove the `AmbiguousDateTime` - Corrected pluralization rules for bg/cs/he/id/ro/ru - Fixed documentation formatting of `Timex.TimezoneInfo.create/6` diff --git a/lib/calendar/julian.ex b/lib/calendar/julian.ex index aba4855d..c15999cc 100644 --- a/lib/calendar/julian.ex +++ b/lib/calendar/julian.ex @@ -24,7 +24,7 @@ defmodule Timex.Calendar.Julian do do: julian_date(year, month, day) # Same as julian_date/1, except takes an Erlang datetime, and returns a more precise Julian date number - @spec julian_date(Types.datetime()) :: integer + @spec julian_date(Types.datetime()) :: float def julian_date({{year, month, day}, {hour, minute, second}}) do julian_date(year, month, day, hour, minute, second) end diff --git a/lib/datetime/datetime.ex b/lib/datetime/datetime.ex index 25cb8e9e..2663307b 100644 --- a/lib/datetime/datetime.ex +++ b/lib/datetime/datetime.ex @@ -436,7 +436,8 @@ defimpl Timex.Protocol, for: DateTime do See docs for Timex.shift/2 for details. """ - @spec shift(DateTime.t(), list({atom(), term})) :: DateTime.t() | {:error, term} + @spec shift(DateTime.t(), list({atom(), term})) :: + DateTime.t() | AmbiguousDateTime.t() | {:error, term} def shift(%DateTime{} = datetime, shifts) when is_list(shifts) do {logical_shifts, shifts} = Keyword.split(shifts, [:years, :months, :weeks, :days]) shift = calculate_shift(shifts) diff --git a/lib/parse/zoneinfo/parser.ex b/lib/parse/zoneinfo/parser.ex index add5b322..5967e283 100644 --- a/lib/parse/zoneinfo/parser.ex +++ b/lib/parse/zoneinfo/parser.ex @@ -108,7 +108,7 @@ defmodule Timex.Parse.ZoneInfo.Parser do Parses the timezone information inside, and returns it as a Zone struct. """ - @spec parse(binary) :: {:ok, Zone.t()} | {:error, binary} + @spec parse(binary) :: {:ok, Zone.t()} | {:error, binary | :invalid_zoneinfo_content} def parse(<>) do version = case version do diff --git a/lib/time/duration.ex b/lib/time/duration.ex index 7f7da7ea..d3c726fe 100644 --- a/lib/time/duration.ex +++ b/lib/time/duration.ex @@ -625,10 +625,10 @@ defmodule Timex.Duration do do: System.system_time(:millisecond) def now(s) when s in [:second, :seconds], do: System.system_time(:second) - def now(:minutes), do: to_minutes(now(:microsecond)) - def now(:hours), do: to_hours(now(:microsecond)) - def now(:days), do: to_days(now(:microsecond)) - def now(:weeks), do: to_weeks(now(:microsecond)) + def now(:minutes), do: to_minutes(now(), truncate: true) + def now(:hours), do: to_hours(now(), truncate: true) + def now(:days), do: to_days(now(), truncate: true) + def now(:weeks), do: to_weeks(now(), truncate: true) @doc """ An alias for `Duration.diff/3` diff --git a/lib/timex.ex b/lib/timex.ex index 8c7c8ea9..c5bb0a5c 100644 --- a/lib/timex.ex +++ b/lib/timex.ex @@ -1898,8 +1898,8 @@ defmodule Timex do 23 """ @spec normalize(:date, {integer, integer, integer}) :: Types.date() - @spec normalize(:time, {integer, integer, integer} | {integer, integer, integer, integer}) :: - Types.time() + @spec normalize(:time, {integer, integer, integer}) :: Types.time() + @spec normalize(:time, {integer, integer, integer, integer}) :: Types.microsecond_time() @spec normalize(:day, {integer, integer, integer}) :: non_neg_integer @spec normalize( :year | :month | :day | :hour | :minute | :second | :millisecond | :microsecond, diff --git a/test/duration_test.exs b/test/duration_test.exs index bedb0a2f..f3f47b27 100644 --- a/test/duration_test.exs +++ b/test/duration_test.exs @@ -150,6 +150,17 @@ defmodule DurationTests do difference_seconds = timex_now_seconds - erlang_now_seconds assert_in_delta(difference_seconds, 0.5, 1) end + + test "now/1 returns whole minutes, hours, days and weeks" do + for {unit, divisor} <- [minutes: 60, hours: 3_600, days: 86_400, weeks: 604_800] do + before_seconds = System.system_time(:second) + result = Timex.Duration.now(unit) + after_seconds = System.system_time(:second) + + assert is_integer(result) + assert result in div(before_seconds, divisor)..div(after_seconds, divisor) + end + end end def something_to_measure(x), do: x