dotiw 5.5.0 → 5.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 32864f97b3b06dc77f8c793e11a5b17b1d2af506bdfac0c559354f3e4eef7998
4
- data.tar.gz: 20efed42fb0802a836f078f2791d78e7b863005b73780243ad4867bbf653103e
3
+ metadata.gz: fa29e6fb27292383b6c52e90e638a79df342e512d2132fc43399fbdac2accce7
4
+ data.tar.gz: a2a5f6a9f954ffa985ecb48a5888f860191be892e451f5a25726d9c7e7bb58d2
5
5
  SHA512:
6
- metadata.gz: 6bb97a0d7bc860b57432dc81cb3d8a7921c6364c5271db648d30984164f26d6f4003e21de28252f958294960df1b0270cc4fbf83ae7602be23096829404ee2fe
7
- data.tar.gz: e2d4f70428937b7e5c409666612dd7aefbe63e71d884a3738b240c222fc2f36a8df7a66b83fa4d52fef96cd27e7edd3a9fdecb15e497fa46853e9b3922f83663
6
+ metadata.gz: d95cc6198e93afadd8a16b09125166a1b0a8474c21f44cb194ae1e77e0a1cfa2a1e35c2a752bf275b16c7d0ec54144d99c3a2940865db373f80d0b3668c77a39
7
+ data.tar.gz: 00cdcea3f7f60fed460de3992568f7bce85c680212cac766a55a2186782fa9e76d578326f4af29bee6dc877fd614fb875ed5430e9288be18209be0ac17638155
@@ -9,26 +9,33 @@ jobs:
9
9
  fail-fast: false
10
10
  matrix:
11
11
  include:
12
- - ruby-version: 3.4
13
- - ruby-version: 3.3
14
- - ruby-version: 3.2
15
- - ruby-version: 3.1
16
- - ruby-version: 3.0
17
- - ruby-version: 2.7
18
- - ruby-version: 2.7
12
+ - ruby-version: '4.0'
13
+ - ruby-version: '3.4'
14
+ - ruby-version: '3.3'
15
+ - ruby-version: '3.2'
16
+ - ruby-version: '3.1'
17
+ - ruby-version: '3.0'
18
+ - ruby-version: '2.7'
19
+ - ruby-version: '2.7'
19
20
  gemfile: gemfiles/rails_5.0.gemfile
20
- - ruby-version: 2.7
21
+ - ruby-version: '2.7'
21
22
  gemfile: gemfiles/rails_5.1.gemfile
22
- - ruby-version: 2.7
23
+ - ruby-version: '2.7'
23
24
  gemfile: gemfiles/rails_5.2.gemfile
24
- - ruby-version: 3.4
25
+ - ruby-version: '3.4'
25
26
  gemfile: gemfiles/rails_6.0.gemfile
26
- - ruby-version: 3.4
27
+ - ruby-version: '3.4'
27
28
  gemfile: gemfiles/rails_7.0.gemfile
28
- - ruby-version: 3.4
29
+ - ruby-version: '3.4'
29
30
  gemfile: gemfiles/rails_7.1.gemfile
30
- - ruby-version: 3.4
31
+ - ruby-version: '3.4'
31
32
  gemfile: gemfiles/rails_7.2.gemfile
33
+ - ruby-version: '3.4'
34
+ gemfile: gemfiles/rails_8.0.gemfile
35
+ - ruby-version: '3.4'
36
+ gemfile: gemfiles/rails_8.1.gemfile
37
+ - ruby-version: '3.4'
38
+ gemfile: gemfiles/rails_8.1_i18n.gemfile
32
39
  steps:
33
40
  - uses: actions/checkout@v2
34
41
  - name: Set up Ruby
@@ -44,3 +51,20 @@ jobs:
44
51
  gem install bundler -v ${BUNDLER:-2.1.4}
45
52
  bundle install --jobs 4 --retry 3
46
53
  bundle exec rake
54
+ - name: Upload coverage to Coveralls
55
+ if: matrix.ruby-version == '3.4'
56
+ uses: coverallsapp/github-action@v2
57
+ with:
58
+ github-token: ${{ secrets.GITHUB_TOKEN }}
59
+ flag-name: ruby-${{ matrix.ruby-version }}-${{ matrix.gemfile }}
60
+ parallel: true
61
+
62
+ finish:
63
+ needs: build
64
+ runs-on: ubuntu-latest
65
+ steps:
66
+ - name: Close parallel build
67
+ uses: coverallsapp/github-action@v2
68
+ with:
69
+ github-token: ${{ secrets.GITHUB_TOKEN }}
70
+ parallel-finished: true
data/.gitignore CHANGED
@@ -1,5 +1,6 @@
1
1
  tmp
2
2
  pkg
3
+ coverage
3
4
  spec/fixtures/database.yml
4
5
  .rvmrc
5
6
  Gemfile.lock
data/Appraisals CHANGED
@@ -25,3 +25,16 @@ end
25
25
  appraise 'rails_7.2' do
26
26
  gem 'rails', '~> 7.2.0'
27
27
  end
28
+
29
+ appraise 'rails_8.0' do
30
+ gem 'rails', '~> 8.0.0'
31
+ end
32
+
33
+ appraise 'rails_8.1' do
34
+ gem 'rails', '~> 8.1.0'
35
+ end
36
+
37
+ appraise 'rails_8.1_i18n' do
38
+ gem 'rails', '~> 8.1.0'
39
+ gem 'rails-i18n'
40
+ end
data/CHANGELOG.md CHANGED
@@ -1,12 +1,35 @@
1
- ## 5.4.0 (Next)
1
+ ## 5.6.1 (2026/09/09)
2
+
3
+ * [#160](https://github.com/radar/distance_of_time_in_words/issues/160): Fix `offset_delta` incorrectly folding the full UTC offset difference into the distance when comparing timestamps with different, unrelated offsets (e.g. one in UTC, one with an explicit `-08:00` offset) instead of only when they're the same clock across a real DST/tzdata transition - [@dblock](https://github.com/dblock).
4
+ * [#162](https://github.com/radar/distance_of_time_in_words/issues/162): Fix `same_clock?` incorrectly treating two `Time` values converted from `ActiveSupport::TimeWithZone` in different zones as the same clock on Rails >= 8.0, where `#to_time` returns a `Time` whose `#zone` is the `ActiveSupport::TimeZone` object itself rather than a `nil`/String abbreviation - [@dblock](https://github.com/dblock).
5
+ * [#165](https://github.com/radar/distance_of_time_in_words/issues/165): Fix `offset_delta` folding the full UTC offset change into the top-level distance, which could overcorrect past zero and report "less than 1 second" for a case where a small real elapsed time (e.g. 1 minute) crosses a much larger DST transition (e.g. Europe/Dublin's 1 hour fall-back) - the offset correction is now only applied to the sub-day leftover once the distance has already been split into calendar fields - [@dblock](https://github.com/dblock).
6
+ * [#170](https://github.com/radar/distance_of_time_in_words/issues/170): Fix `distance_of_time_in_words` unnecessarily converting an `ActiveSupport::TimeWithZone` argument to a plain `Time` via `#to_time`, which discarded its real zone on Rails < 8.0 and prevented `same_clock?` from correctly folding in a historical UTC offset change (e.g. Pacific/Norfolk's 2015 offset change, #153) on those versions - [@dblock](https://github.com/dblock).
7
+
8
+ ## 5.6.0 (2026/08/28)
9
+
10
+ * [#145](https://github.com/radar/distance_of_time_in_words/pull/145): Add `dotiw/core` require path to use dotiw without overriding the Rails date helpers, `DOTIW.distance_of_time_in_words` shortcut - [@ermolaev](https://github.com/ermolaev).
11
+ * [#146](https://github.com/radar/distance_of_time_in_words/pull/146): Add support for Rails 8 - [@dblock](https://github.com/dblock).
12
+ * [#147](https://github.com/radar/distance_of_time_in_words/pull/147): Add support for Rails 8.1, drop the `tzinfo ~> 1.2.7` development pin - [@ermolaev](https://github.com/ermolaev).
13
+ * [#148](https://github.com/radar/distance_of_time_in_words/pull/148): Add `max_unit` option to express the entire distance as a single number in one unit - [@dblock](https://github.com/dblock).
14
+ * [#79](https://github.com/radar/distance_of_time_in_words/issues/79): Speed up `distance_of_time_in_words` by avoiding `I18n.with_options`/`ActiveSupport::OptionMerger` allocation for translations, using `I18n.translate` directly instead - [@dblock](https://github.com/dblock).
15
+ * [#77](https://github.com/radar/distance_of_time_in_words/issues/77), [#120](https://github.com/radar/distance_of_time_in_words/issues/120): Fix `only`/`except` discarding the duration of excluded fractions instead of folding them into the next smaller included fraction - [@dblock](https://github.com/dblock).
16
+ * [#63](https://github.com/radar/distance_of_time_in_words/issues/63): Fix incorrect DST correction in timezones with an inverted DST scheme (e.g. Europe/Dublin) by comparing `utc_offset` instead of the unreliable `Time#dst?` - [@dblock](https://github.com/dblock).
17
+ * [#153](https://github.com/radar/distance_of_time_in_words/issues/153): Fix leftover hours/minutes leaking a historical UTC offset change (e.g. Pacific/Norfolk's 2015 offset change) into the result instead of folding it into the calendar distance - [@dblock](https://github.com/dblock).
18
+ * [#44](https://github.com/radar/distance_of_time_in_words/issues/44): Document that `vague: true` combined with a non-English `locale:` requires the host application to load Rails' own translations for that locale - [@dblock](https://github.com/dblock).
19
+ * [#156](https://github.com/radar/distance_of_time_in_words/pull/156): Add Ruby 4.0 to the test matrix - [@dblock](https://github.com/dblock).
20
+ * [#158](https://github.com/radar/distance_of_time_in_words/pull/158): Add test coverage reporting with SimpleCov and Coveralls, merged across all Ruby 3.4 CI jobs (default + pinned Rails gemfiles) using parallel builds - [@dblock](https://github.com/dblock).
21
+
22
+ ## 5.5.0 (2026/03/15)
2
23
 
3
24
  * [#131](https://github.com/radar/distance_of_time_in_words/pull/131): Deprecates `highest_measure_only`. Adds alternate form of `highest_measures` option to permit rounding up whatever part of the duration was previously silently discarded - [@seansfkelley](https://github.com/seansfkelley).
4
25
  * [#133](https://github.com/radar/distance_of_time_in_words/pull/133): Test on Ruby 3.0 and 3.1 - [@dblock](https://github.com/dblock).
5
26
  * [#134](https://github.com/radar/distance_of_time_in_words/pull/134): Add support for Dzongkha, the National language of Bhutan - [@KinWang-2013](https://github.com/KinWang-2013).
6
27
  * [#135](https://github.com/radar/distance_of_time_in_words/pull/135): Add support for Ruby 2.7 and 3.2, removed support for ruby 2.4, 2.5, 2.6 and Rails 4 - [@KinWang-2013](https://github.com/KinWang-2013).
7
28
  * [#136](https://github.com/radar/distance_of_time_in_words/pull/136): Add support for Rails 7 - [@KinWang-2013](https://github.com/KinWang-2013).
8
- * [#139](https://github.com/radar/distance_of_time_in_words/pull/139): Fix bug with pluralization of Russian translations for numbers ending in 1 - [bitberry-dev](https://github.com/bitberry-dev).
9
- * Your contribution here.
29
+ * [#139](https://github.com/radar/distance_of_time_in_words/pull/139): Fix bug with pluralization of Russian translations for numbers ending in 1 - [@bitberry-dev](https://github.com/bitberry-dev).
30
+ * [#140](https://github.com/radar/distance_of_time_in_words/pull/140): Add Ruby 3.3 and 3.4 support, added logger - [@dblock](https://github.com/dblock).
31
+ * [#141](https://github.com/radar/distance_of_time_in_words/pull/141): Fix flaky time calculation test by explicitly setting timezone - [@bitberry-dev](https://github.com/bitberry-dev).
32
+ * Fix bug when accumulating on years - [@radar](https://github.com/radar).
10
33
 
11
34
  ## 5.3.3 (2022/04/25)
12
35
 
data/README.markdown CHANGED
@@ -1,4 +1,4 @@
1
- # dotiw ![Ruby](https://github.com/radar/distance_of_time_in_words/workflows/Ruby/badge.svg)
1
+ # dotiw ![Ruby](https://github.com/radar/distance_of_time_in_words/workflows/Ruby/badge.svg) [![Coverage Status](https://coveralls.io/repos/github/radar/distance_of_time_in_words/badge.svg?branch=master)](https://coveralls.io/github/radar/distance_of_time_in_words?branch=master)
2
2
 
3
3
  The `dotiw` library that adds `distance_of_time_in_words` to any Ruby project, or overrides the default implementation in Rails with more accurate output.
4
4
 
@@ -32,6 +32,23 @@ include ActionView::Helpers::TextHelper
32
32
  include ActionView::Helpers::NumberHelper
33
33
  ```
34
34
 
35
+ ### Rails, without overriding the built-in helpers
36
+
37
+ Requiring `dotiw` replaces the Rails implementation of `distance_of_time_in_words`
38
+ and `time_ago_in_words`. If you would rather keep the built-in Rails helpers and
39
+ call dotiw explicitly, require `dotiw/core` instead:
40
+
41
+ ```ruby
42
+ gem 'dotiw', require: 'dotiw/core'
43
+ ```
44
+
45
+ This loads `DOTIW::Methods` and the dotiw translations, but leaves
46
+ `ActionView::Helpers::DateHelper` untouched:
47
+
48
+ ```ruby
49
+ DOTIW.distance_of_time_in_words(from_time, to_time, highest_measures: 1)
50
+ ```
51
+
35
52
  ## distance\_of\_time\_in\_words
36
53
 
37
54
  Take this for a totally kick-ass example:
@@ -117,6 +134,8 @@ This will also be passed to `to_sentence`.
117
134
 
118
135
  Specify this if you want it to use the old `distance_of_time_in_words`. The value can be anything except `nil` or `false`.
119
136
 
137
+ Note that `:vague` falls back to Rails/ActionView's own `distance_of_time_in_words`, which looks up its translations under `datetime.distance_in_words`. This namespace is provided by Rails core translations (e.g. via the [rails-i18n](https://github.com/svenfuchs/rails-i18n) gem) for non-English locales, not by dotiw. If you combine `:vague` with a non-English `:locale` and see a "translation missing" error, make sure your application also loads the corresponding Rails translations for that locale (see [#44](https://github.com/radar/distance_of_time_in_words/issues/44)).
138
+
120
139
  #### :include_seconds
121
140
 
122
141
  As described above this option is the equivalent to the third argument whether to include seconds.
@@ -130,38 +149,67 @@ Specifies the maximum output unit which will accumulate all the surplus. Say you
130
149
  => "121 minutes and 10 seconds"
131
150
  ```
132
151
 
152
+ Note that units below `accumulate_on` are still shown separately. If you want the entire distance expressed as a single number in one unit, use `:max_unit` instead.
153
+
154
+ #### :max_unit
155
+
156
+ Expresses the entire distance as a single number in the given unit, discarding (flooring) any remainder smaller than that unit. Unlike `:accumulate_on`, no other units are shown in the output.
157
+
158
+ ```ruby
159
+ >> distance_of_time_in_words(Time.now, Time.now + 1.day, true, max_unit: :seconds)
160
+ => "86400 seconds"
161
+
162
+ >> distance_of_time_in_words(Time.now, Time.now + 3.days + 2.hours, true, max_unit: :hours)
163
+ => "74 hours"
164
+
165
+ >> distance_of_time_in_words_hash(Time.now, Time.now + 3.days + 2.hours, max_unit: :hours)
166
+ => {hours: 74}
167
+ ```
168
+
169
+ Valid units are `:seconds`, `:minutes`, `:hours`, `:days`, `:weeks`, `:months` and `:years`.
170
+
133
171
  #### :only
134
172
 
135
173
  Only want a specific measurement of time? No problem!
136
174
 
137
175
  ```ruby
138
176
  >> distance_of_time_in_words(Time.now, Time.now + 1.hour + 1.minute, false, only: :minutes)
139
- => "1 minute"
177
+ => "61 minutes"
140
178
  ```
141
179
 
180
+ Excluded measurements are folded into the next smaller included measurement instead of being
181
+ discarded, so the returned duration is always accurate (1 hour + 1 minute = 61 minutes, not 1
182
+ minute).
183
+
142
184
  You only want some? No problem too!
143
185
 
144
186
  ```ruby
145
187
  >> distance_of_time_in_words(Time.now, Time.now + 1.hour + 1.day + 1.minute, false, only: [:minutes, :hours])
146
- => "1 hour and 1 minute"
188
+ => "25 hours and 1 minute"
147
189
  ```
148
190
 
191
+ Here the excluded day is folded into hours (1 day + 1 hour = 25 hours).
192
+
149
193
  #### :except
150
194
 
151
195
  Don't want a measurement of time? No problem!
152
196
 
153
197
  ```ruby
154
198
  >> distance_of_time_in_words(Time.now, Time.now + 1.hour + 1.minute, false, except: :minutes)
155
- => "1 hour"
199
+ => "1 hour and 60 seconds"
156
200
  ```
157
201
 
202
+ The excluded minute is folded into seconds (1 minute = 60 seconds) rather than discarded.
203
+
158
204
  Culling a whole group of measurements of time:
159
205
 
160
206
  ```ruby
161
207
  >> distance_of_time_in_words(Time.now, Time.now + 1.hour + 1.day + 1.minute, false, except: [:minutes, :hours])
162
- => "1 day"
208
+ => "1 day and 3660 seconds"
163
209
  ```
164
210
 
211
+ The excluded hour and minute cascade down into seconds (1 hour + 1 minute = 3660 seconds).
212
+
165
213
  #### :highest\_measure\_only
166
214
 
167
215
  > **Deprecated**. Use `highest_measures: 1` instead.
data/RELEASING.md ADDED
@@ -0,0 +1,71 @@
1
+ # Releasing DOTIW
2
+
3
+ There are no hard rules about when to release dotiw. Release bug fixes frequently, features not so frequently and breaking API changes rarely.
4
+
5
+ ## Release
6
+
7
+ Run tests, check that all tests succeed locally.
8
+
9
+ ```
10
+ bundle install
11
+ rake
12
+ ```
13
+
14
+ Check that the last build succeeded in [GitHub Actions](https://github.com/radar/distance_of_time_in_words/actions) for all supported platforms.
15
+
16
+ Add a date to this release in [CHANGELOG.md](CHANGELOG.md).
17
+
18
+ ```
19
+ ## 5.5.1 (2026/03/28)
20
+ ```
21
+
22
+ Remove the line with "Your contribution here.", since there will be no more contributions to this release.
23
+
24
+ Commit your changes.
25
+
26
+ ```
27
+ git add CHANGELOG.md lib/dotiw/version.rb
28
+ git commit -m "Preparing for release, 5.5.1."
29
+ git push origin master
30
+ ```
31
+
32
+ Release.
33
+
34
+ ```
35
+ rake release
36
+ ```
37
+
38
+ This builds the gem, creates and pushes a git tag, and pushes the gem to [RubyGems](https://rubygems.org/gems/dotiw).
39
+
40
+ ```
41
+ dotiw 5.5.1 built to pkg/dotiw-5.5.1.gem.
42
+ Tagged v5.5.1.
43
+ Pushed git commits and tags.
44
+ Pushed dotiw 5.5.1 to rubygems.org.
45
+ ```
46
+
47
+ Create a GitHub release for the tag using the `gh` CLI, passing the CHANGELOG entries for this version as the release notes.
48
+
49
+ ```
50
+ gh release create v5.5.1 --title "5.5.1" --notes "$(sed -n '/^## 5.5.1/,/^## /p' CHANGELOG.md | sed '$d')"
51
+ ```
52
+
53
+ ## Prepare for the Next Version
54
+
55
+ Add the next release to [CHANGELOG.md](CHANGELOG.md).
56
+
57
+ ```
58
+ ## 5.5.2 (Next)
59
+
60
+ * Your contribution here.
61
+ ```
62
+
63
+ Increment the patch version number in [lib/dotiw/version.rb](lib/dotiw/version.rb).
64
+
65
+ Commit your changes.
66
+
67
+ ```
68
+ git add CHANGELOG.md lib/dotiw/version.rb
69
+ git commit -m "Preparing for next developer iteration, 5.5.2."
70
+ git push origin master
71
+ ```
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Benchmark for https://github.com/radar/distance_of_time_in_words/issues/79
4
+ #
5
+ # Compares dotiw's distance_of_time_in_words against Rails' original
6
+ # ActionView::Helpers::DateHelper#distance_of_time_in_words, using the
7
+ # scenario from the issue and its follow-up comments.
8
+ #
9
+ # Run with `ruby benchmarks/distance_of_time_in_words_bench.rb` after
10
+ # `gem install benchmark-ips benchmark-memory actionview`.
11
+
12
+ require 'active_support'
13
+ require 'active_support/core_ext'
14
+ require 'benchmark/ips'
15
+ require 'benchmark/memory'
16
+
17
+ require_relative '../lib/dotiw'
18
+
19
+ begin
20
+ require 'action_view'
21
+ rescue LoadError
22
+ warn "This benchmark requires actionview: gem install actionview"
23
+ exit 1
24
+ end
25
+
26
+ include ActionView::Helpers::DateHelper
27
+
28
+ now = Time.now
29
+ future_time = Time.now + 100.years + 30.days + 50.seconds
30
+
31
+ %i[ips memory].each do |type|
32
+ Benchmark.public_send(type) do |x|
33
+ x.report('dotiw') { DOTIW.distance_of_time_in_words(now, future_time) }
34
+ x.report('rails') { distance_of_time_in_words(now, future_time, include_seconds: true) }
35
+
36
+ x.compare!
37
+ end
38
+ end
data/dotiw.gemspec CHANGED
@@ -26,10 +26,10 @@ Gem::Specification.new do |s|
26
26
  s.add_development_dependency 'pry'
27
27
  s.add_development_dependency 'rake'
28
28
  s.add_development_dependency 'rspec', '~> 3.0'
29
- s.add_development_dependency 'tzinfo', '~> 1.2.7'
30
29
  s.add_development_dependency 'mutex_m'
31
30
  s.add_development_dependency 'base64'
32
31
  s.add_development_dependency 'bigdecimal'
32
+ s.add_development_dependency 'simplecov'
33
33
 
34
34
  s.files = `git ls-files`.split("\n")
35
35
  s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ # This file was generated by Appraisal
4
+
5
+ source 'http://rubygems.org'
6
+
7
+ gem 'rails', '~> 8.0.0'
8
+ gem 'tzinfo', '~> 2.0'
9
+
10
+
11
+ gemspec path: '../'
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ # This file was generated by Appraisal
4
+
5
+ source 'http://rubygems.org'
6
+
7
+ gem 'rails', '~> 8.1.0'
8
+ gem 'tzinfo', '~> 2.0'
9
+
10
+
11
+ gemspec path: '../'
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ # This file was generated by Appraisal
4
+
5
+ source 'http://rubygems.org'
6
+
7
+ gem 'rails', '~> 8.1.0'
8
+ gem 'rails-i18n'
9
+ gem 'tzinfo', '~> 2.0'
10
+
11
+ gemspec path: '../'
data/lib/dotiw/core.rb ADDED
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'i18n'
4
+ require 'logger'
5
+ require 'active_support'
6
+ require 'active_support/core_ext'
7
+
8
+ module DOTIW
9
+ extend ActiveSupport::Autoload
10
+
11
+ eager_autoload do
12
+ autoload :VERSION, 'dotiw/version'
13
+ autoload :TimeHash, 'dotiw/time_hash'
14
+ autoload :Methods, 'dotiw/methods'
15
+ end
16
+
17
+ extend self
18
+
19
+ DEFAULT_I18N_SCOPE = :'datetime.dotiw'
20
+ DEFAULT_I18N_SCOPE_COMPACT = :'datetime.dotiw_compact'
21
+
22
+ # DOTIW.distance_of_time_in_words(...), DOTIW.time_ago_in_words(...), etc.
23
+ extend Methods
24
+
25
+ def init_i18n!
26
+ I18n.load_path.unshift(*locale_files)
27
+ I18n.reload!
28
+ end
29
+
30
+ def languages
31
+ @languages ||= (locale_files.map { |path| path.split(%r{[/.]})[-2].to_sym })
32
+ end
33
+
34
+ def locale_files
35
+ files 'locale', '*.yml'
36
+ end
37
+
38
+ protected
39
+
40
+ def files(directory, ext)
41
+ Dir[File.join File.dirname(__FILE__), directory, ext]
42
+ end
43
+ end
44
+
45
+ DOTIW.init_i18n!
data/lib/dotiw/methods.rb CHANGED
@@ -5,8 +5,8 @@ module DOTIW
5
5
  extend self
6
6
 
7
7
  def distance_of_time_in_words_hash(from_time, to_time, options = {})
8
- from_time = from_time.to_time if !from_time.is_a?(Time) && from_time.respond_to?(:to_time)
9
- to_time = to_time.to_time if !to_time.is_a?(Time) && to_time.respond_to?(:to_time)
8
+ from_time = coerce_to_time(from_time)
9
+ to_time = coerce_to_time(to_time)
10
10
 
11
11
  DOTIW::TimeHash.new(nil, from_time, to_time, options).to_hash
12
12
  end
@@ -73,12 +73,28 @@ module DOTIW
73
73
  if value.is_a?(Numeric)
74
74
  Time.at(value)
75
75
  elsif value.respond_to?(:to_time)
76
- value.to_time
76
+ coerce_to_time(value)
77
77
  else
78
78
  raise ArgumentError, "#{value.inspect} can't be converted to a Time value"
79
79
  end
80
80
  end
81
81
 
82
+ # An ActiveSupport::TimeWithZone already carries its zone explicitly and
83
+ # supports every operation TimeHash needs (arithmetic, #advance, calendar
84
+ # accessors, #utc_offset), so there's no reason to convert it to a plain
85
+ # Time first. Doing so is actively harmful: on Rails < 8.0 (before
86
+ # to_time_preserves_timezone defaulted to :zone), TimeWithZone#to_time
87
+ # discards the real zone and returns a Time in the process's local zone
88
+ # instead, which loses the information TimeHash's same_clock? needs to
89
+ # correctly fold a historical UTC offset change into the distance
90
+ # (#153/#162/#165) on Rails < 8.0 (#170) - the loss isn't inherent to
91
+ # older Rails, it's an unforced conversion we don't need to make.
92
+ def coerce_to_time(value)
93
+ return value if value.is_a?(Time) || value.respond_to?(:time_zone)
94
+
95
+ value.to_time
96
+ end
97
+
82
98
  def options_with_scope(options)
83
99
  if options.key?(:compact)
84
100
  options.merge(scope: DOTIW::DEFAULT_I18N_SCOPE_COMPACT)
@@ -102,9 +118,23 @@ module DOTIW
102
118
  options[:except] = Array.wrap(options[:except]).map!(&:to_sym) if options[:except]
103
119
  options[:only] = Array.wrap(options[:only]).map!(&:to_sym) if options[:only]
104
120
 
105
- DOTIW::TimeHash::TIME_FRACTIONS.each do |fraction|
106
- if options[:except]&.include?(fraction) || (options[:only] && !options[:only].include?(fraction))
107
- discarded_hash[fraction] = hash.delete fraction
121
+ if options[:except] || options[:only]
122
+ # Fold excluded fractions into the next smaller fraction instead of discarding their
123
+ # duration outright, e.g. weeks excluded via only: [:years, :months, :days] are converted
124
+ # to days and added to the days count, rather than losing those weeks entirely. Larger
125
+ # fractions are processed first so that a run of consecutive excluded fractions cascades
126
+ # down to the largest fraction that's still included (or is dropped if none remain).
127
+ DOTIW::TimeHash::TIME_FRACTIONS.reverse_each do |fraction|
128
+ next unless hash.key?(fraction)
129
+ next unless options[:except]&.include?(fraction) || (options[:only] && !options[:only].include?(fraction))
130
+
131
+ value = hash.delete(fraction)
132
+ discarded_hash[fraction] = value
133
+
134
+ next_fraction = _next_smaller_fraction(fraction)
135
+ next unless next_fraction
136
+
137
+ hash[next_fraction] = hash.fetch(next_fraction, 0) + (value * ROLLUP_THRESHOLDS[next_fraction])
108
138
  end
109
139
  end
110
140
 
@@ -117,12 +147,13 @@ module DOTIW
117
147
  fractions &= options[:only] if options[:only]
118
148
  fractions -= options[:except] if options[:except]
119
149
 
120
- I18n.with_options locale: options[:locale], scope: i18n_scope do |locale|
121
- # e.g. try to format 'less than 1 days', fallback to '0 days'
122
- return locale.translate :less_than_x,
123
- distance: locale.translate(fractions.first, count: 1),
124
- default: locale.translate(fractions.first, count: 0)
125
- end
150
+ locale = options[:locale]
151
+ # e.g. try to format 'less than 1 days', fallback to '0 days'
152
+ return I18n.translate :less_than_x,
153
+ locale: locale,
154
+ scope: i18n_scope,
155
+ distance: I18n.translate(fractions.first, locale: locale, scope: i18n_scope, count: 1),
156
+ default: I18n.translate(fractions.first, locale: locale, scope: i18n_scope, count: 0)
126
157
  end
127
158
 
128
159
  options.delete(:except)
@@ -137,10 +168,8 @@ module DOTIW
137
168
  _maybe_round! hash, discarded_hash, highest_measures[:remainder]
138
169
  end
139
170
 
140
- phrases = []
141
- I18n.with_options locale: options[:locale], scope: i18n_scope do |locale|
142
- phrases = hash.map { |key, value| locale.t(key, count: value) }
143
- end
171
+ locale = options[:locale]
172
+ phrases = hash.map { |key, value| I18n.translate(key, locale: locale, scope: i18n_scope, count: value) }
144
173
 
145
174
  options[:words_connector] ||= I18n.translate :"#{i18n_scope}.words_connector",
146
175
  default: :'support.array.words_connector',
@@ -152,7 +181,7 @@ module DOTIW
152
181
  default: :'support.array.last_word_connector',
153
182
  locale: options[:locale]
154
183
 
155
- phrases.to_sentence(options.except(:accumulate_on, :compact))
184
+ phrases.to_sentence(options.except(:accumulate_on, :max_unit, :compact))
156
185
  end
157
186
 
158
187
  def _compute_highest_measures!(options)
@@ -192,6 +221,13 @@ module DOTIW
192
221
  end
193
222
  end
194
223
 
224
+ def _next_smaller_fraction(fraction)
225
+ index = DOTIW::TimeHash::TIME_FRACTIONS.index(fraction)
226
+ return nil if index.nil? || index.zero?
227
+
228
+ DOTIW::TimeHash::TIME_FRACTIONS[index - 1]
229
+ end
230
+
195
231
  def _rollup!(hash, smallest_measure_index)
196
232
  DOTIW::TimeHash::TIME_FRACTIONS[smallest_measure_index..-1].each_with_index do |fraction, index|
197
233
  if hash.fetch(fraction, 0) >= ROLLUP_THRESHOLDS[fraction]