Fix Humanized.format/2 crash when finish precedes start - #794
Open
dblock wants to merge 2 commits into
Open
Conversation
Timex.Format.Duration.Formatters.Humanized.format/1 only ever receives a raw elapsed Duration (a plain second/microsecond count), so it has no way to know whether the underlying calendar distance between two datetimes is clean. Any UTC offset change spanned by the interval (a DST transition, or a timezone's UTC offset changing permanently) gets misattributed as spurious trailing hours/minutes, because years/months/days are bucketed from fixed 365/30-day constants rather than real calendar arithmetic. For example, formatting the distance between 2015-01-15 and 2016-03-15 in Pacific/Norfolk (whose UTC offset permanently changed from +11:30 to +11:00 on 4 October 2015) currently produces "1 year, 2 months, 30 minutes" instead of the correct "1 year, 2 months". This adds format/2 and lformat/3, which accept both datetimes directly. Years/months are computed via Timex.diff/3 + Timex.shift/2 (real calendar arithmetic that already correctly ignores offset noise), and only the true leftover duration is passed to the existing week/day/hour/minute bucketing logic. format/1 (duration-only) is unchanged and still available for callers who only have an elapsed duration and no reference datetimes. This is the same class of bug (and the same fix strategy) as two issues found and fixed in Ruby's distance_of_time_in_words gem: - radar/distance_of_time_in_words#63 (dst? flag instead of comparing actual UTC offsets, Europe/Dublin) - radar/distance_of_time_in_words#153 (assuming any offset change is a recurring +/-1 hour DST transition, breaking on Pacific/Norfolk's permanent offset change) Write-up with full technical detail on both original bugs, and cross-language testing across JS, Python, Go, Rust, PHP, C#, Java, and Elixir that led to finding this Timex bug: https://code.dblock.org/2026/08/28/adventures-in-daylight-saving-norfolk-island-and-time-zone-math-in-ruby.html Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
format/2 (added in bitwalker#793) assumed start <= finish and passed the resulting negative :years/:months counts straight into Gettext's plural translation, which raises a FunctionClauseError because it requires n >= 0. format/1 has always returned the same output regardless of the sign of the underlying duration (see the 'format negative duration' test), so format/2 should behave the same way when finish is chronologically before start. Fix by normalizing the order (swapping start/finish when needed) before doing the calendar-aware diff/shift arithmetic. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
dblock
added a commit
to dblock/tz_test
that referenced
this pull request
Aug 28, 2026
Timex is largely unmaintained (last push mid-2025, 70+ open issues). humanizer's relative_time/2,3 is clean on both the Norfolk and Dublin cases, and also handles reversed argument order correctly, unlike Timex's format/2 (bitwalker/timex#794). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
dblock
added a commit
to dblock/code.dblock.org
that referenced
this pull request
Aug 28, 2026
Also links the format/2 reversed-argument-order fix (bitwalker/timex#794). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This file contains hidden or 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
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
Humanized.format/2(added in #793, not yet merged) assumedstart <= finishand passed the resulting negative:years/:monthscounts straight into Gettext's plural translation, which raises aFunctionClauseErrorbecause it requiresn >= 0:format/1has always returned the same output regardless of the sign of the underlying duration (see the existing"format negative duration"test).format/2should behave the same way whenfinishis chronologically beforestart.Fix
Normalize the order of
start/finish(swap them iffinishprecedesstart) before doing the calendar-aware diff/shift arithmetic, matchingformat/1's sign-independent behavior.Found this while going back through
distance_of_time_in_words's test suite to check whether Timex's newformat/2(from #793) covers the same range of edge cases dotiw does -- it didn't have a reversed-order case.Note: this is a follow-up to #793 and is based on that branch, since
format/2doesn't exist onmainyet.Note: This fix was AI-generated (with human review/testing along the way). Please review carefully.