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 +4 -4
- data/.github/workflows/ruby.yml +37 -13
- data/.gitignore +1 -0
- data/Appraisals +13 -0
- data/CHANGELOG.md +26 -3
- data/README.markdown +53 -5
- data/RELEASING.md +71 -0
- data/benchmarks/distance_of_time_in_words_bench.rb +38 -0
- data/dotiw.gemspec +1 -1
- data/gemfiles/rails_8.0.gemfile +11 -0
- data/gemfiles/rails_8.1.gemfile +11 -0
- data/gemfiles/rails_8.1_i18n.gemfile +11 -0
- data/lib/dotiw/core.rb +45 -0
- data/lib/dotiw/methods.rb +53 -17
- data/lib/dotiw/time_hash.rb +119 -14
- data/lib/dotiw/version.rb +1 -1
- data/lib/dotiw.rb +1 -40
- data/spec/lib/dotiw_module_spec.rb +52 -0
- data/spec/lib/dotiw_spec.rb +273 -7
- data/spec/spec_helper.rb +19 -0
- metadata +22 -11
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: fa29e6fb27292383b6c52e90e638a79df342e512d2132fc43399fbdac2accce7
|
|
4
|
+
data.tar.gz: a2a5f6a9f954ffa985ecb48a5888f860191be892e451f5a25726d9c7e7bb58d2
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: d95cc6198e93afadd8a16b09125166a1b0a8474c21f44cb194ae1e77e0a1cfa2a1e35c2a752bf275b16c7d0ec54144d99c3a2940865db373f80d0b3668c77a39
|
|
7
|
+
data.tar.gz: 00cdcea3f7f60fed460de3992568f7bce85c680212cac766a55a2186782fa9e76d578326f4af29bee6dc877fd614fb875ed5430e9288be18209be0ac17638155
|
data/.github/workflows/ruby.yml
CHANGED
|
@@ -9,26 +9,33 @@ jobs:
|
|
|
9
9
|
fail-fast: false
|
|
10
10
|
matrix:
|
|
11
11
|
include:
|
|
12
|
-
- ruby-version:
|
|
13
|
-
- ruby-version: 3.
|
|
14
|
-
- ruby-version: 3.
|
|
15
|
-
- ruby-version: 3.
|
|
16
|
-
- ruby-version: 3.
|
|
17
|
-
- ruby-version:
|
|
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
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.
|
|
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
|
-
*
|
|
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 
|
|
1
|
+
# dotiw  [](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
|
-
=> "
|
|
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
|
-
=> "
|
|
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")
|
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 =
|
|
9
|
-
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
|
|
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
|
-
|
|
106
|
-
|
|
107
|
-
|
|
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
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
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
|
-
|
|
141
|
-
I18n.
|
|
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]
|