From d780d325aca383a6a8b010a6622bddbc09652da5 Mon Sep 17 00:00:00 2001 From: Oliver Ford Date: Mon, 2 Nov 2026 15:48:05 +0000 Subject: [PATCH 1/2] Roll `TimeWithZone`s forward in DST-aware manner Presently, `roll_forward`, `next_business_day`, et al. use `day_interval_for`'s definition of `1` (days) for `Date` objects, or `60 * 60 * 24` (seconds) otherwise. Falling into the latter category, `ActiveSupport::TimeWithZone` objects in Rails environments have their date component adjusted in a manner which is not timezone-aware, and thus corrupts the time component when the change spans a DST transition. For example: t = TimeWithZone.new(Time.new(2026,10,23,12,0,0), ActiveSupport::TimeZone::country_zones(:gb)[0]) constructs `t` as noon UTC in `Europe/London` while BST is active, i.e. `t` is 1pm local time. next_business_day(t) gives `2026-10-26 12:00:00.000000000 GMT +00:00`, that is, still noon UTC, but now also noon local time, as the next business day was post transition to GMT. This commit addresses the issue by detecting `TimeWithZone`s, and using ActiveSupport's `Integer#day` as for the interval in such cases; in the above example giving `2026-10-26 13:00:00.000000000 GMT +00:00` as expected. --- lib/business/calendar.rb | 8 +++++++- spec/business/calendar_spec.rb | 14 ++++++++++++++ 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/lib/business/calendar.rb b/lib/business/calendar.rb index 3fbae69..fc21f23 100644 --- a/lib/business/calendar.rb +++ b/lib/business/calendar.rb @@ -209,7 +209,13 @@ def business_days_between(date1, date2) end def day_interval_for(date) - date.is_a?(Date) ? 1 : 3600 * 24 + if date.is_a?(Date) + 1 + elsif defined?(ActiveSupport::TimeWithZone) && date.is_a?(ActiveSupport::TimeWithZone) + 1.day + else + 3600 * 24 + end end # Internal method for assigning working days from a calendar config. diff --git a/spec/business/calendar_spec.rb b/spec/business/calendar_spec.rb index 9ee37aa..26050d1 100644 --- a/spec/business/calendar_spec.rb +++ b/spec/business/calendar_spec.rb @@ -1049,4 +1049,18 @@ it_behaves_like "common" end + + context "(using TimeWithZone objects)" do + before do + stub_const("ActiveSupport::TimeWithZone", Class.new(Time)) + Integer.define_method(:day) { self * 60 * 60 * 24 } + end + + after { Integer.remove_method(:day) } + + let(:date_class) { ActiveSupport::TimeWithZone } + let(:day_interval) { 1.day } + + it_behaves_like "common" + end end From 10ade9d356a49efc17822950f748592b272c89dc Mon Sep 17 00:00:00 2001 From: Oliver Ford Date: Tue, 3 Nov 2026 12:27:52 +0000 Subject: [PATCH 2/2] Bump to v3.2.0 --- CHANGELOG.md | 4 ++++ lib/business/version.rb | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index aa7385e..0b0ac6b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ ## Upcoming +## 3.2.0 - Nov 3, 2026 + +- Advance `ActiveSupport::TimeWithZone`s whole days across DST boundaries #339 + ## 3.1.0 - Sept 28, 2026 - Fix `business_days_between` not counting extra working dates in full weeks #34 - thanks @anatoliliotych! diff --git a/lib/business/version.rb b/lib/business/version.rb index 09ba1f3..097d139 100644 --- a/lib/business/version.rb +++ b/lib/business/version.rb @@ -1,5 +1,5 @@ # frozen_string_literal: true module Business - VERSION = "3.1.0" + VERSION = "3.2.0" end