dotiw 5.5.0 → 5.6.0
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 +19 -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 +34 -14
- data/lib/dotiw/time_hash.rb +55 -9
- 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 +169 -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: 1b9778f6b8dfb3fa5013f9e365c91be2fcb955f040e19dde50d8f70b1fb28fcd
|
|
4
|
+
data.tar.gz: 5213a53264dd238ea225939d53c40a63523749b92331ef4a899fa3ca190d5bce
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 2efac079381c10ed7584f921dbeab2c3b4ae96f63a4b32c470a986a2a521f0a91c94e26871e92bcdcd9718d05d2bb6c41bc051155868b9c3684695b5b662d27e
|
|
7
|
+
data.tar.gz: 1506fe3f4715fd1f85b72d8de416d4dfdff144e02bce2837c741ab82d7928b2fed38e6c08fac19e1a5a0e155a9ec4c947407d1878107e5c15cc7947618f731dd
|
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,28 @@
|
|
|
1
|
-
## 5.
|
|
1
|
+
## 5.6.0 (2026/8/28)
|
|
2
|
+
|
|
3
|
+
* [#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).
|
|
4
|
+
* [#146](https://github.com/radar/distance_of_time_in_words/pull/146): Add support for Rails 8 - [@dblock](https://github.com/dblock).
|
|
5
|
+
* [#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).
|
|
6
|
+
* [#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).
|
|
7
|
+
* [#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).
|
|
8
|
+
* [#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).
|
|
9
|
+
* [#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).
|
|
10
|
+
* [#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).
|
|
11
|
+
* [#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).
|
|
12
|
+
* [#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).
|
|
13
|
+
* [#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).
|
|
14
|
+
|
|
15
|
+
## 5.5.0 (2026/03/15)
|
|
2
16
|
|
|
3
17
|
* [#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
18
|
* [#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
19
|
* [#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
20
|
* [#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
21
|
* [#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
|
-
*
|
|
22
|
+
* [#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).
|
|
23
|
+
* [#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).
|
|
24
|
+
* [#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).
|
|
25
|
+
* Fix bug when accumulating on years - [@radar](https://github.com/radar).
|
|
10
26
|
|
|
11
27
|
## 5.3.3 (2022/04/25)
|
|
12
28
|
|
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
|
@@ -102,9 +102,23 @@ module DOTIW
|
|
|
102
102
|
options[:except] = Array.wrap(options[:except]).map!(&:to_sym) if options[:except]
|
|
103
103
|
options[:only] = Array.wrap(options[:only]).map!(&:to_sym) if options[:only]
|
|
104
104
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
105
|
+
if options[:except] || options[:only]
|
|
106
|
+
# Fold excluded fractions into the next smaller fraction instead of discarding their
|
|
107
|
+
# duration outright, e.g. weeks excluded via only: [:years, :months, :days] are converted
|
|
108
|
+
# to days and added to the days count, rather than losing those weeks entirely. Larger
|
|
109
|
+
# fractions are processed first so that a run of consecutive excluded fractions cascades
|
|
110
|
+
# down to the largest fraction that's still included (or is dropped if none remain).
|
|
111
|
+
DOTIW::TimeHash::TIME_FRACTIONS.reverse_each do |fraction|
|
|
112
|
+
next unless hash.key?(fraction)
|
|
113
|
+
next unless options[:except]&.include?(fraction) || (options[:only] && !options[:only].include?(fraction))
|
|
114
|
+
|
|
115
|
+
value = hash.delete(fraction)
|
|
116
|
+
discarded_hash[fraction] = value
|
|
117
|
+
|
|
118
|
+
next_fraction = _next_smaller_fraction(fraction)
|
|
119
|
+
next unless next_fraction
|
|
120
|
+
|
|
121
|
+
hash[next_fraction] = hash.fetch(next_fraction, 0) + (value * ROLLUP_THRESHOLDS[next_fraction])
|
|
108
122
|
end
|
|
109
123
|
end
|
|
110
124
|
|
|
@@ -117,12 +131,13 @@ module DOTIW
|
|
|
117
131
|
fractions &= options[:only] if options[:only]
|
|
118
132
|
fractions -= options[:except] if options[:except]
|
|
119
133
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
134
|
+
locale = options[:locale]
|
|
135
|
+
# e.g. try to format 'less than 1 days', fallback to '0 days'
|
|
136
|
+
return I18n.translate :less_than_x,
|
|
137
|
+
locale: locale,
|
|
138
|
+
scope: i18n_scope,
|
|
139
|
+
distance: I18n.translate(fractions.first, locale: locale, scope: i18n_scope, count: 1),
|
|
140
|
+
default: I18n.translate(fractions.first, locale: locale, scope: i18n_scope, count: 0)
|
|
126
141
|
end
|
|
127
142
|
|
|
128
143
|
options.delete(:except)
|
|
@@ -137,10 +152,8 @@ module DOTIW
|
|
|
137
152
|
_maybe_round! hash, discarded_hash, highest_measures[:remainder]
|
|
138
153
|
end
|
|
139
154
|
|
|
140
|
-
|
|
141
|
-
I18n.
|
|
142
|
-
phrases = hash.map { |key, value| locale.t(key, count: value) }
|
|
143
|
-
end
|
|
155
|
+
locale = options[:locale]
|
|
156
|
+
phrases = hash.map { |key, value| I18n.translate(key, locale: locale, scope: i18n_scope, count: value) }
|
|
144
157
|
|
|
145
158
|
options[:words_connector] ||= I18n.translate :"#{i18n_scope}.words_connector",
|
|
146
159
|
default: :'support.array.words_connector',
|
|
@@ -152,7 +165,7 @@ module DOTIW
|
|
|
152
165
|
default: :'support.array.last_word_connector',
|
|
153
166
|
locale: options[:locale]
|
|
154
167
|
|
|
155
|
-
phrases.to_sentence(options.except(:accumulate_on, :compact))
|
|
168
|
+
phrases.to_sentence(options.except(:accumulate_on, :max_unit, :compact))
|
|
156
169
|
end
|
|
157
170
|
|
|
158
171
|
def _compute_highest_measures!(options)
|
|
@@ -192,6 +205,13 @@ module DOTIW
|
|
|
192
205
|
end
|
|
193
206
|
end
|
|
194
207
|
|
|
208
|
+
def _next_smaller_fraction(fraction)
|
|
209
|
+
index = DOTIW::TimeHash::TIME_FRACTIONS.index(fraction)
|
|
210
|
+
return nil if index.nil? || index.zero?
|
|
211
|
+
|
|
212
|
+
DOTIW::TimeHash::TIME_FRACTIONS[index - 1]
|
|
213
|
+
end
|
|
214
|
+
|
|
195
215
|
def _rollup!(hash, smallest_measure_index)
|
|
196
216
|
DOTIW::TimeHash::TIME_FRACTIONS[smallest_measure_index..-1].each_with_index do |fraction, index|
|
|
197
217
|
if hash.fetch(fraction, 0) >= ROLLUP_THRESHOLDS[fraction]
|
data/lib/dotiw/time_hash.rb
CHANGED
|
@@ -13,15 +13,9 @@ module DOTIW
|
|
|
13
13
|
@from_time = from_time || Time.current
|
|
14
14
|
@to_time = to_time || (@to_time_not_given = true && @from_time + distance.seconds)
|
|
15
15
|
@smallest, @largest = [@from_time, @to_time].minmax
|
|
16
|
-
@to_time
|
|
17
|
-
@to_time -= 1.hour if @to_time_not_given && !smallest.dst? && largest.dst?
|
|
16
|
+
@to_time += offset_delta(smallest, largest) if @to_time_not_given
|
|
18
17
|
@smallest, @largest = [@from_time, @to_time].minmax
|
|
19
|
-
@distance ||=
|
|
20
|
-
d = largest - smallest
|
|
21
|
-
d -= 1.hour if smallest.dst? && !largest.dst?
|
|
22
|
-
d += 1.hour if !smallest.dst? && largest.dst?
|
|
23
|
-
d
|
|
24
|
-
end
|
|
18
|
+
@distance ||= (largest - smallest) + offset_delta(smallest, largest)
|
|
25
19
|
|
|
26
20
|
build_time_hash
|
|
27
21
|
end
|
|
@@ -40,8 +34,22 @@ module DOTIW
|
|
|
40
34
|
ONE_WEEK = 7.days.freeze
|
|
41
35
|
FOUR_WEEKS = 28.days.freeze
|
|
42
36
|
|
|
37
|
+
# The distance between two Time objects computed via subtraction already
|
|
38
|
+
# reflects any change in UTC offset between them (whether from a DST
|
|
39
|
+
# transition or a permanent tzdata rule change, e.g. Pacific/Norfolk's
|
|
40
|
+
# 2015 UTC offset change), which is what we want when reporting a raw
|
|
41
|
+
# elapsed duration. However, when we split that distance into calendar
|
|
42
|
+
# fields (years/months/weeks/days), we want the offset difference
|
|
43
|
+
# folded away so the leftover hours/minutes/seconds reflect only actual
|
|
44
|
+
# elapsed wall-clock time, not artifacts of an offset shift.
|
|
45
|
+
def offset_delta(smallest, largest)
|
|
46
|
+
largest.utc_offset - smallest.utc_offset
|
|
47
|
+
end
|
|
48
|
+
|
|
43
49
|
def build_time_hash
|
|
44
|
-
if
|
|
50
|
+
if (max_unit = options[:max_unit])
|
|
51
|
+
build_max_unit(max_unit.to_sym)
|
|
52
|
+
elsif accumulate_on = options[:accumulate_on]
|
|
45
53
|
accumulate_on = accumulate_on.to_sym
|
|
46
54
|
TIME_FRACTIONS.index(accumulate_on).downto(0) { |i| send("build_#{TIME_FRACTIONS[i]}") }
|
|
47
55
|
else
|
|
@@ -86,6 +94,44 @@ module DOTIW
|
|
|
86
94
|
output[:weeks], @distance = distance.divmod(ONE_WEEK.to_i) unless output[:weeks]
|
|
87
95
|
end
|
|
88
96
|
|
|
97
|
+
# Expresses the entire distance as a single number in the given unit, discarding
|
|
98
|
+
# (flooring) any remainder smaller than that unit, unlike +accumulate_on+ which keeps
|
|
99
|
+
# showing the smaller units below the target.
|
|
100
|
+
def build_max_unit(unit)
|
|
101
|
+
case unit
|
|
102
|
+
when :seconds
|
|
103
|
+
output[:seconds] = distance.to_i
|
|
104
|
+
when :minutes
|
|
105
|
+
output[:minutes] = (distance / ONE_MINUTE).floor
|
|
106
|
+
when :hours
|
|
107
|
+
output[:hours] = (distance / ONE_HOUR).floor
|
|
108
|
+
when :days
|
|
109
|
+
output[:days] = (distance / ONE_DAY).floor
|
|
110
|
+
when :weeks
|
|
111
|
+
output[:weeks] = (distance / ONE_WEEK).floor
|
|
112
|
+
when :months
|
|
113
|
+
output[:months] = whole_months_between(smallest, largest)
|
|
114
|
+
when :years
|
|
115
|
+
output[:years] = whole_years_between(smallest, largest)
|
|
116
|
+
else
|
|
117
|
+
raise ArgumentError, "unrecognized max_unit #{unit.inspect}"
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
@distance = 0
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def whole_months_between(from, to)
|
|
124
|
+
months = (to.year - from.year) * 12 + (to.month - from.month)
|
|
125
|
+
months -= 1 if to.advance(months: -months) < from
|
|
126
|
+
months
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def whole_years_between(from, to)
|
|
130
|
+
years = to.year - from.year
|
|
131
|
+
years -= 1 if to.advance(years: -years) < from
|
|
132
|
+
years
|
|
133
|
+
end
|
|
134
|
+
|
|
89
135
|
def build_months
|
|
90
136
|
build_years_months_weeks_days
|
|
91
137
|
|
data/lib/dotiw/version.rb
CHANGED
data/lib/dotiw.rb
CHANGED
|
@@ -1,45 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
|
|
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
|
-
def init_i18n!
|
|
23
|
-
I18n.load_path.unshift(*locale_files)
|
|
24
|
-
I18n.reload!
|
|
25
|
-
end
|
|
26
|
-
|
|
27
|
-
def languages
|
|
28
|
-
@languages ||= (locale_files.map { |path| path.split(%r{[/.]})[-2].to_sym })
|
|
29
|
-
end
|
|
30
|
-
|
|
31
|
-
def locale_files
|
|
32
|
-
files 'dotiw/locale', '*.yml'
|
|
33
|
-
end
|
|
34
|
-
|
|
35
|
-
protected
|
|
36
|
-
|
|
37
|
-
def files(directory, ext)
|
|
38
|
-
Dir[File.join File.dirname(__FILE__), directory, ext]
|
|
39
|
-
end
|
|
40
|
-
end
|
|
41
|
-
|
|
42
|
-
DOTIW.init_i18n!
|
|
3
|
+
require_relative 'dotiw/core'
|
|
43
4
|
|
|
44
5
|
begin
|
|
45
6
|
require 'action_view'
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'spec_helper'
|
|
4
|
+
|
|
5
|
+
describe DOTIW do
|
|
6
|
+
START_TIME_MODULE = '01-08-2009'.to_time(:utc)
|
|
7
|
+
|
|
8
|
+
before do
|
|
9
|
+
I18n.locale = :en
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
describe '.distance_of_time_in_words' do
|
|
13
|
+
it 'returns the exact distance' do
|
|
14
|
+
expect(
|
|
15
|
+
described_class.distance_of_time_in_words(START_TIME_MODULE, START_TIME_MODULE + 1.hour + 30.minutes)
|
|
16
|
+
).to eq('1 hour and 30 minutes')
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
it 'accepts options' do
|
|
20
|
+
expect(
|
|
21
|
+
described_class.distance_of_time_in_words(
|
|
22
|
+
START_TIME_MODULE, START_TIME_MODULE + 1.hour + 30.minutes, highest_measures: 1
|
|
23
|
+
)
|
|
24
|
+
).to eq('1 hour')
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
describe '.time_ago_in_words' do
|
|
29
|
+
it 'returns the distance to now' do
|
|
30
|
+
allow(Time).to receive(:now).and_return(START_TIME_MODULE)
|
|
31
|
+
allow(Time.zone).to receive(:now).and_return(START_TIME_MODULE)
|
|
32
|
+
|
|
33
|
+
expect(
|
|
34
|
+
described_class.time_ago_in_words(START_TIME_MODULE - 90, include_seconds: true)
|
|
35
|
+
).to eq('1 minute and 30 seconds')
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
describe '.distance_of_time' do
|
|
40
|
+
it 'returns the distance for a number of seconds' do
|
|
41
|
+
expect(described_class.distance_of_time(90)).to eq('1 minute and 30 seconds')
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
describe '.distance_of_time_in_words_hash' do
|
|
46
|
+
it 'returns the distance as a hash' do
|
|
47
|
+
expect(
|
|
48
|
+
described_class.distance_of_time_in_words_hash(START_TIME_MODULE, START_TIME_MODULE + 1.hour + 30.minutes)
|
|
49
|
+
).to include(hours: 1, minutes: 30)
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
data/spec/lib/dotiw_spec.rb
CHANGED
|
@@ -17,7 +17,11 @@ describe 'A better distance_of_time_in_words' do
|
|
|
17
17
|
|
|
18
18
|
before do
|
|
19
19
|
I18n.locale = :en
|
|
20
|
-
|
|
20
|
+
# This config is deprecated as of Rails 8.1 (it becomes the permanent,
|
|
21
|
+
# non-configurable behavior) and is removed entirely in Rails 8.2.
|
|
22
|
+
if Gem::Version.new(ActiveSupport::VERSION::STRING) < Gem::Version.new('8.1')
|
|
23
|
+
ActiveSupport.to_time_preserves_timezone = :zone
|
|
24
|
+
end
|
|
21
25
|
|
|
22
26
|
allow(Time).to receive(:now).and_return(START_TIME)
|
|
23
27
|
allow(Time.zone).to receive(:now).and_return(START_TIME)
|
|
@@ -193,6 +197,63 @@ describe 'A better distance_of_time_in_words' do
|
|
|
193
197
|
end
|
|
194
198
|
end
|
|
195
199
|
|
|
200
|
+
context 'in timezones with an inverted DST scheme (#63)' do
|
|
201
|
+
# Real, non-mocked reproduction from #63: running the suite under
|
|
202
|
+
# certain timezones (e.g. Europe/Dublin, where IST is "standard" time
|
|
203
|
+
# and GMT is technically the DST offset) previously failed because
|
|
204
|
+
# Time#dst? disagreed between a Time built via Time.at(datetime) and
|
|
205
|
+
# one built via datetime.to_time for the exact same instant, even
|
|
206
|
+
# though their utc_offset was identical. This caused a spurious +/-1
|
|
207
|
+
# hour "DST correction" to be applied.
|
|
208
|
+
around do |example|
|
|
209
|
+
original_tz = ENV.fetch('TZ', nil)
|
|
210
|
+
ENV['TZ'] = tz
|
|
211
|
+
example.run
|
|
212
|
+
ENV['TZ'] = original_tz
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
%w[
|
|
216
|
+
Europe/Dublin
|
|
217
|
+
Africa/Casablanca
|
|
218
|
+
Asia/Tehran
|
|
219
|
+
Europe/Moscow
|
|
220
|
+
Asia/Gaza
|
|
221
|
+
].each do |timezone|
|
|
222
|
+
context "TZ=#{timezone}" do
|
|
223
|
+
let(:tz) { timezone }
|
|
224
|
+
|
|
225
|
+
it 'is 1 minute' do
|
|
226
|
+
start = Time.at(DateTime.now)
|
|
227
|
+
finish = DateTime.now + 1.minute
|
|
228
|
+
|
|
229
|
+
expect(distance_of_time_in_words(start, finish)).to eq('1 minute')
|
|
230
|
+
end
|
|
231
|
+
end
|
|
232
|
+
end
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
context 'across a historical UTC offset change (#153)' do
|
|
236
|
+
# Real, non-mocked reproduction from #153: Pacific/Norfolk permanently
|
|
237
|
+
# changed its UTC offset from +11:30 to +11:00 on 2015-10-04 (a
|
|
238
|
+
# tzdata rule change, not a recurring DST transition - both endpoints
|
|
239
|
+
# report dst? == false). The calendar distance (years/months/weeks/
|
|
240
|
+
# days) was correct, but the leftover hours/minutes leaked the 30
|
|
241
|
+
# minute offset difference as a spurious "23 hours and 30 minutes".
|
|
242
|
+
around do |example|
|
|
243
|
+
original_tz = ENV.fetch('TZ', nil)
|
|
244
|
+
ENV['TZ'] = 'Pacific/Norfolk'
|
|
245
|
+
example.run
|
|
246
|
+
ENV['TZ'] = original_tz
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
it 'is 1 year and 2 months, not 1 year, 2 months, 23 hours, and 30 minutes' do
|
|
250
|
+
start = '2015-1-15'.to_time
|
|
251
|
+
finish = '2016-3-15'.to_time
|
|
252
|
+
|
|
253
|
+
expect(distance_of_time_in_words(start, finish, true)).to eq('1 year and 2 months')
|
|
254
|
+
end
|
|
255
|
+
end
|
|
256
|
+
|
|
196
257
|
describe 'accumulate_on:' do
|
|
197
258
|
[
|
|
198
259
|
[START_TIME,
|
|
@@ -228,6 +289,55 @@ describe 'A better distance_of_time_in_words' do
|
|
|
228
289
|
end
|
|
229
290
|
end
|
|
230
291
|
|
|
292
|
+
describe 'max_unit:' do
|
|
293
|
+
[
|
|
294
|
+
[START_TIME,
|
|
295
|
+
START_TIME + 10.minute,
|
|
296
|
+
:seconds,
|
|
297
|
+
{ seconds: 600 },
|
|
298
|
+
'600 seconds'],
|
|
299
|
+
[START_TIME,
|
|
300
|
+
START_TIME + 10.hour + 10.minute + 1.second,
|
|
301
|
+
:minutes,
|
|
302
|
+
{ minutes: 610 },
|
|
303
|
+
'610 minutes'],
|
|
304
|
+
[START_TIME,
|
|
305
|
+
START_TIME + 2.day + 10_000.hour + 10.second,
|
|
306
|
+
:hours,
|
|
307
|
+
{ hours: 10_048 },
|
|
308
|
+
'10048 hours'],
|
|
309
|
+
[START_TIME,
|
|
310
|
+
START_TIME + 2.day + 10_000.hour + 10.second,
|
|
311
|
+
:days,
|
|
312
|
+
{ days: 418 },
|
|
313
|
+
'418 days'],
|
|
314
|
+
[START_TIME,
|
|
315
|
+
START_TIME + 2.day + 10_000.hour + 10.second,
|
|
316
|
+
:weeks,
|
|
317
|
+
{ weeks: 59 },
|
|
318
|
+
'59 weeks'],
|
|
319
|
+
['2015-1-15'.to_time, '2016-3-15'.to_time, :months, { months: 14 }, '14 months'],
|
|
320
|
+
['2015-1-15'.to_time, '2016-3-15'.to_time, :years, { years: 1 }, '1 year']
|
|
321
|
+
].each do |start, finish, unit, hash, output|
|
|
322
|
+
it "should be #{output}" do
|
|
323
|
+
expect(distance_of_time_in_words_hash(start, finish, max_unit: unit)).to eq(hash)
|
|
324
|
+
expect(distance_of_time_in_words(start, finish, true, max_unit: unit)).to eq(output)
|
|
325
|
+
end
|
|
326
|
+
end
|
|
327
|
+
|
|
328
|
+
it 'floors and discards any remainder smaller than the unit' do
|
|
329
|
+
expect(
|
|
330
|
+
distance_of_time_in_words_hash(START_TIME, START_TIME + 1.year - 1.day, max_unit: :years)
|
|
331
|
+
).to eq(years: 0)
|
|
332
|
+
end
|
|
333
|
+
|
|
334
|
+
it 'raises for an unrecognized unit' do
|
|
335
|
+
expect do
|
|
336
|
+
distance_of_time_in_words_hash(START_TIME, START_TIME + 1.day, max_unit: :fortnights)
|
|
337
|
+
end.to raise_error(ArgumentError, /unrecognized max_unit/)
|
|
338
|
+
end
|
|
339
|
+
end
|
|
340
|
+
|
|
231
341
|
describe 'without finish time' do
|
|
232
342
|
# A missing finish argument should default to zero, essentially returning
|
|
233
343
|
# the equivalent of distance_of_time in order to be backwards-compatible
|
|
@@ -297,22 +407,28 @@ describe 'A better distance_of_time_in_words' do
|
|
|
297
407
|
[START_TIME,
|
|
298
408
|
START_TIME + 1.year + 2.months + 3.days + 4.hours + 5.minutes + 6.seconds,
|
|
299
409
|
{ except: 'minutes' },
|
|
300
|
-
|
|
410
|
+
# 5 excluded minutes are folded into seconds (5 * 60 + 6 = 306) instead of being discarded.
|
|
411
|
+
'1 year, 2 months, 3 days, 4 hours, and 306 seconds'],
|
|
301
412
|
[START_TIME,
|
|
302
413
|
START_TIME + 1.hour + 1.minute,
|
|
303
|
-
{ except: 'minutes' },
|
|
414
|
+
{ except: 'minutes' },
|
|
415
|
+
# The excluded minute is folded into seconds (1 * 60 = 60) instead of being discarded.
|
|
416
|
+
'1 hour and 60 seconds'],
|
|
304
417
|
[START_TIME,
|
|
305
418
|
START_TIME + 1.hour + 1.day + 1.minute,
|
|
306
419
|
{ except: %w[minutes hours] },
|
|
307
|
-
|
|
420
|
+
# The excluded hour and minute cascade down into seconds (1 * 24 * 60 * 60 + 1 * 60 = 3660).
|
|
421
|
+
'1 day and 3660 seconds'],
|
|
308
422
|
[START_TIME,
|
|
309
423
|
START_TIME + 1.hour + 1.day + 1.minute,
|
|
310
424
|
{ only: %w[minutes hours] },
|
|
311
|
-
|
|
425
|
+
# The excluded day is folded into hours (1 * 24 + 1 = 25) instead of being discarded.
|
|
426
|
+
'25 hours and 1 minute'],
|
|
312
427
|
[START_TIME,
|
|
313
428
|
START_TIME + 1.year + 2.months + 3.weeks + 4.days + 5.hours + 6.minutes + 7.seconds,
|
|
314
429
|
{ except: 'minutes' },
|
|
315
|
-
|
|
430
|
+
# 6 excluded minutes are folded into seconds (6 * 60 + 7 = 367) instead of being discarded.
|
|
431
|
+
'1 year, 2 months, 3 weeks, 4 days, 5 hours, and 367 seconds'],
|
|
316
432
|
[START_TIME,
|
|
317
433
|
START_TIME + 1.hour + 2.minutes + 3.seconds,
|
|
318
434
|
{ highest_measure_only: true },
|
|
@@ -395,6 +511,34 @@ describe 'A better distance_of_time_in_words' do
|
|
|
395
511
|
end.to raise_error(ArgumentError)
|
|
396
512
|
end
|
|
397
513
|
|
|
514
|
+
describe 'folding excluded fractions into included ones (regression for #77 and #120)' do
|
|
515
|
+
it 'folds excluded weeks into days rather than discarding them' do
|
|
516
|
+
from = '2019-01-02'.to_time
|
|
517
|
+
to = '2020-12-28'.to_time
|
|
518
|
+
expect(distance_of_time_in_words(from, to, false, only: %i[years months days]))
|
|
519
|
+
.to eq('1 year, 11 months, and 26 days')
|
|
520
|
+
end
|
|
521
|
+
|
|
522
|
+
it 'folds excluded weeks into days when using except' do
|
|
523
|
+
from = '2019-01-02'.to_time
|
|
524
|
+
to = '2020-12-28'.to_time
|
|
525
|
+
expect(distance_of_time_in_words(from, to, false, except: :weeks))
|
|
526
|
+
.to eq('1 year, 11 months, and 26 days')
|
|
527
|
+
end
|
|
528
|
+
|
|
529
|
+
it 'folds an excluded month into days' do
|
|
530
|
+
expect(distance_of_time_in_words(START_TIME, START_TIME + 1.month + 9.days, false, only: %i[months days]))
|
|
531
|
+
.to eq('1 month and 9 days')
|
|
532
|
+
end
|
|
533
|
+
|
|
534
|
+
it 'converts a negative day count into a week when the day-of-month matches but the hour rolls back' do
|
|
535
|
+
from = Time.new(2024, 5, 1, 10, 0, 0)
|
|
536
|
+
to = Time.new(2024, 6, 1, 9, 0, 0)
|
|
537
|
+
expect(distance_of_time_in_words(from, to, false, only: %i[months weeks days]))
|
|
538
|
+
.to eq('4 weeks and 2 days')
|
|
539
|
+
end
|
|
540
|
+
end
|
|
541
|
+
|
|
398
542
|
if defined?(ActionView)
|
|
399
543
|
describe 'ActionView without include seconds argument' do
|
|
400
544
|
[
|
|
@@ -440,13 +584,31 @@ describe 'A better distance_of_time_in_words' do
|
|
|
440
584
|
[START_TIME,
|
|
441
585
|
START_TIME + 1.year + 2.months + 3.weeks + 4.days + 5.hours + 6.minutes + 7.seconds,
|
|
442
586
|
{ vague: nil },
|
|
443
|
-
'1 year, 2 months, 3 weeks, 4 days, 5 hours, 6 minutes, and 7 seconds']
|
|
587
|
+
'1 year, 2 months, 3 weeks, 4 days, 5 hours, 6 minutes, and 7 seconds'],
|
|
588
|
+
[START_TIME,
|
|
589
|
+
START_TIME + 1.year + 2.months,
|
|
590
|
+
{ vague: true, locale: :en },
|
|
591
|
+
'about 1 year']
|
|
444
592
|
].each do |start, finish, options, output|
|
|
445
593
|
it "should be #{output}" do
|
|
446
594
|
expect(distance_of_time_in_words(start, finish, true, options)).to eq(output)
|
|
447
595
|
end
|
|
448
596
|
end
|
|
449
597
|
|
|
598
|
+
# requires the rails-i18n gem for non-English datetime.distance_in_words translations, see #44
|
|
599
|
+
if defined?(RailsI18n)
|
|
600
|
+
it 'should be rundt 1 år for vague: true, locale: :nb' do
|
|
601
|
+
finish = START_TIME + 1.year + 2.months
|
|
602
|
+
expect(distance_of_time_in_words(START_TIME, finish, true, vague: true, locale: :nb)).to eq('rundt 1 år')
|
|
603
|
+
end
|
|
604
|
+
else
|
|
605
|
+
it 'returns a translation missing message for vague: true, locale: :nb without the rails-i18n gem' do
|
|
606
|
+
finish = START_TIME + 1.year + 2.months
|
|
607
|
+
expect(distance_of_time_in_words(START_TIME, finish, true, vague: true, locale: :nb))
|
|
608
|
+
.to eq('Translation missing: nb.datetime.distance_in_words.about_x_years')
|
|
609
|
+
end
|
|
610
|
+
end
|
|
611
|
+
|
|
450
612
|
context 'via ActionController::Base.helpers' do
|
|
451
613
|
it '#distance_of_time_in_words' do
|
|
452
614
|
end_time = START_TIME + 1.year + 2.months + 3.weeks + 4.days + 5.hours + 6.minutes + 7.seconds
|
data/spec/spec_helper.rb
CHANGED
|
@@ -3,8 +3,27 @@
|
|
|
3
3
|
ROOT_PATH = File.join(File.dirname(__FILE__), '..')
|
|
4
4
|
$LOAD_PATH.unshift ROOT_PATH unless $LOAD_PATH.include? ROOT_PATH
|
|
5
5
|
|
|
6
|
+
require 'simplecov'
|
|
7
|
+
SimpleCov.start do
|
|
8
|
+
add_filter '/spec/'
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
# Ruby 3.4 dropped concurrent-ruby's implicit require of logger, which older
|
|
12
|
+
# Rails versions rely on being already loaded, see https://github.com/rails/rails/issues/54271.
|
|
13
|
+
require 'logger'
|
|
14
|
+
require 'bundler'
|
|
15
|
+
Bundler.require
|
|
16
|
+
|
|
6
17
|
require 'dotiw'
|
|
7
18
|
|
|
19
|
+
# Loads Rails' own datetime.distance_in_words translations for non-English
|
|
20
|
+
# locales, needed by the vague: true option, which falls back to Rails'
|
|
21
|
+
# native distance_of_time_in_words instead of dotiw's own translations, see #44.
|
|
22
|
+
# Only present when the rails-i18n gem is in the Gemfile (see gemfiles/rails_8.1_i18n.gemfile).
|
|
23
|
+
if defined?(RailsI18n)
|
|
24
|
+
I18n.load_path += Dir[File.join(Gem.loaded_specs['rails-i18n'].gem_dir, 'rails', 'locale', '*.yml')]
|
|
25
|
+
end
|
|
26
|
+
|
|
8
27
|
Time.zone = 'UTC'
|
|
9
28
|
|
|
10
29
|
I18n.locale = :en
|
metadata
CHANGED
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: dotiw
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 5.
|
|
4
|
+
version: 5.6.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Ryan Bigg
|
|
8
8
|
- Lauran Jansen
|
|
9
|
+
autorequire:
|
|
9
10
|
bindir: bin
|
|
10
11
|
cert_chain: []
|
|
11
|
-
date:
|
|
12
|
+
date: 2026-08-28 00:00:00.000000000 Z
|
|
12
13
|
dependencies:
|
|
13
14
|
- !ruby/object:Gem::Dependency
|
|
14
15
|
name: activesupport
|
|
@@ -109,21 +110,21 @@ dependencies:
|
|
|
109
110
|
- !ruby/object:Gem::Version
|
|
110
111
|
version: '3.0'
|
|
111
112
|
- !ruby/object:Gem::Dependency
|
|
112
|
-
name:
|
|
113
|
+
name: mutex_m
|
|
113
114
|
requirement: !ruby/object:Gem::Requirement
|
|
114
115
|
requirements:
|
|
115
|
-
- - "
|
|
116
|
+
- - ">="
|
|
116
117
|
- !ruby/object:Gem::Version
|
|
117
|
-
version:
|
|
118
|
+
version: '0'
|
|
118
119
|
type: :development
|
|
119
120
|
prerelease: false
|
|
120
121
|
version_requirements: !ruby/object:Gem::Requirement
|
|
121
122
|
requirements:
|
|
122
|
-
- - "
|
|
123
|
+
- - ">="
|
|
123
124
|
- !ruby/object:Gem::Version
|
|
124
|
-
version:
|
|
125
|
+
version: '0'
|
|
125
126
|
- !ruby/object:Gem::Dependency
|
|
126
|
-
name:
|
|
127
|
+
name: base64
|
|
127
128
|
requirement: !ruby/object:Gem::Requirement
|
|
128
129
|
requirements:
|
|
129
130
|
- - ">="
|
|
@@ -137,7 +138,7 @@ dependencies:
|
|
|
137
138
|
- !ruby/object:Gem::Version
|
|
138
139
|
version: '0'
|
|
139
140
|
- !ruby/object:Gem::Dependency
|
|
140
|
-
name:
|
|
141
|
+
name: bigdecimal
|
|
141
142
|
requirement: !ruby/object:Gem::Requirement
|
|
142
143
|
requirements:
|
|
143
144
|
- - ">="
|
|
@@ -151,7 +152,7 @@ dependencies:
|
|
|
151
152
|
- !ruby/object:Gem::Version
|
|
152
153
|
version: '0'
|
|
153
154
|
- !ruby/object:Gem::Dependency
|
|
154
|
-
name:
|
|
155
|
+
name: simplecov
|
|
155
156
|
requirement: !ruby/object:Gem::Requirement
|
|
156
157
|
requirements:
|
|
157
158
|
- - ">="
|
|
@@ -187,7 +188,9 @@ files:
|
|
|
187
188
|
- Gemfile
|
|
188
189
|
- MIT-LICENSE
|
|
189
190
|
- README.markdown
|
|
191
|
+
- RELEASING.md
|
|
190
192
|
- Rakefile
|
|
193
|
+
- benchmarks/distance_of_time_in_words_bench.rb
|
|
191
194
|
- benchmarks/time_hash_bench.rb
|
|
192
195
|
- dotiw.gemspec
|
|
193
196
|
- gemfiles/.bundle/config
|
|
@@ -198,8 +201,12 @@ files:
|
|
|
198
201
|
- gemfiles/rails_7.0.gemfile
|
|
199
202
|
- gemfiles/rails_7.1.gemfile
|
|
200
203
|
- gemfiles/rails_7.2.gemfile
|
|
204
|
+
- gemfiles/rails_8.0.gemfile
|
|
205
|
+
- gemfiles/rails_8.1.gemfile
|
|
206
|
+
- gemfiles/rails_8.1_i18n.gemfile
|
|
201
207
|
- lib/dotiw.rb
|
|
202
208
|
- lib/dotiw/action_view/helpers/date_helper.rb
|
|
209
|
+
- lib/dotiw/core.rb
|
|
203
210
|
- lib/dotiw/locale/ar.yml
|
|
204
211
|
- lib/dotiw/locale/da.yml
|
|
205
212
|
- lib/dotiw/locale/de.yml
|
|
@@ -223,6 +230,7 @@ files:
|
|
|
223
230
|
- lib/dotiw/methods.rb
|
|
224
231
|
- lib/dotiw/time_hash.rb
|
|
225
232
|
- lib/dotiw/version.rb
|
|
233
|
+
- spec/lib/dotiw_module_spec.rb
|
|
226
234
|
- spec/lib/dotiw_spec.rb
|
|
227
235
|
- spec/lib/i18n/ar.yml
|
|
228
236
|
- spec/lib/i18n/da.yml
|
|
@@ -249,6 +257,7 @@ homepage: https://github.com/radar/distance_of_time_in_words
|
|
|
249
257
|
licenses:
|
|
250
258
|
- MIT
|
|
251
259
|
metadata: {}
|
|
260
|
+
post_install_message:
|
|
252
261
|
rdoc_options: []
|
|
253
262
|
require_paths:
|
|
254
263
|
- lib
|
|
@@ -263,10 +272,12 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
263
272
|
- !ruby/object:Gem::Version
|
|
264
273
|
version: '0'
|
|
265
274
|
requirements: []
|
|
266
|
-
rubygems_version: 3.
|
|
275
|
+
rubygems_version: 3.5.16
|
|
276
|
+
signing_key:
|
|
267
277
|
specification_version: 4
|
|
268
278
|
summary: Better distance_of_time_in_words for Rails
|
|
269
279
|
test_files:
|
|
280
|
+
- spec/lib/dotiw_module_spec.rb
|
|
270
281
|
- spec/lib/dotiw_spec.rb
|
|
271
282
|
- spec/lib/i18n/ar.yml
|
|
272
283
|
- spec/lib/i18n/da.yml
|