gitlab_chronic_duration 0.10.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 +7 -0
- data/.gitignore +17 -0
- data/.gitlab-ci.yml +12 -0
- data/CHANGELOG.md +4 -0
- data/Gemfile +2 -0
- data/LICENSE.txt +22 -0
- data/README.md +81 -0
- data/Rakefile +6 -0
- data/gitlab_chronic_duration.gemspec +28 -0
- data/lib/chronic_duration/version.rb +5 -0
- data/lib/gitlab_chronic_duration.rb +302 -0
- data/spec/lib/chronic_duration_spec.rb +331 -0
- data/spec/spec_helper.rb +8 -0
- metadata +103 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 2a08e26fec03acc92f7690fe833452920d67d6c28bda9b5d106e020fc3fef390
|
4
|
+
data.tar.gz: bcd9abbbb3e510283748ebc7566705c32cd97f18f0c052b92f3b262fcccf7d94
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: c86d746140760a7785907f2e19d263bbfe53bbc2a10987246990035a0f391ec2995f534f9ab7c377ca6dccea41178063b600e65e54e0e26831c1bfd6141d60ea
|
7
|
+
data.tar.gz: 60f11fe463617d2853a37a3268af643e28785aad1d689f78b49ae718fc7d1db1e35cf230e240e18fbe367e571b58a51ad2b05a34e90adb8dcc89efe8f6dd27f7
|
data/.gitignore
ADDED
data/.gitlab-ci.yml
ADDED
data/CHANGELOG.md
ADDED
@@ -0,0 +1,4 @@
|
|
1
|
+
## v0.10.6.1
|
2
|
+
|
3
|
+
- Allow to pass `days_per_month`, `hours_per_day` as part of `opts={}` for `.parse` and `.output`. For internal calculations, use `days_per_month` to calculate `days_per_week`. Replace `days_per_week` getter/setter with `days_per_month` !2
|
4
|
+
- Configure GitLab CI !1
|
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) Henry Poydar
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person
|
4
|
+
obtaining a copy of this software and associated documentation
|
5
|
+
files (the "Software"), to deal in the Software without
|
6
|
+
restriction, including without limitation the rights to use,
|
7
|
+
copy, modify, merge, publish, distribute, sublicense, and/or sell
|
8
|
+
copies of the Software, and to permit persons to whom the
|
9
|
+
Software is furnished to do so, subject to the following
|
10
|
+
conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be
|
13
|
+
included in all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
16
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
|
17
|
+
OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
18
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
|
19
|
+
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
20
|
+
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
21
|
+
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
22
|
+
OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,81 @@
|
|
1
|
+
[](https://travis-ci.org/hpoydar/chronic_duration)
|
2
|
+
|
3
|
+
# Chronic Duration
|
4
|
+
|
5
|
+
A simple Ruby natural language parser for elapsed time. (For example, 4 hours and 30 minutes, 6 minutes 4 seconds, 3 days, etc.) Returns all results in seconds. Will return an integer unless you get tricky and need a float. (4 minutes and 13.47 seconds, for example.)
|
6
|
+
|
7
|
+
The reverse can also be accomplished with the output method. So pass in seconds and you can get strings like 4 mins 31.51 secs (default format), 4h 3m 30s, or 4:01:29.
|
8
|
+
|
9
|
+
## Usage
|
10
|
+
|
11
|
+
>> require 'chronic_duration'
|
12
|
+
=> true
|
13
|
+
>> ChronicDuration.parse('4 minutes and 30 seconds')
|
14
|
+
=> 270
|
15
|
+
>> ChronicDuration.parse('0 seconds')
|
16
|
+
=> nil
|
17
|
+
>> ChronicDuration.parse('0 seconds', :keep_zero => true)
|
18
|
+
=> 0
|
19
|
+
>> ChronicDuration.output(270)
|
20
|
+
=> 4 mins 30 secs
|
21
|
+
>> ChronicDuration.output(0)
|
22
|
+
=> nil
|
23
|
+
>> ChronicDuration.output(0, :keep_zero => true)
|
24
|
+
=> 0 secs
|
25
|
+
>> ChronicDuration.output(270, :format => :short)
|
26
|
+
=> 4m 30s
|
27
|
+
>> ChronicDuration.output(270, :format => :long)
|
28
|
+
=> 4 minutes 30 seconds
|
29
|
+
>> ChronicDuration.output(270, :format => :chrono)
|
30
|
+
=> 4:30
|
31
|
+
>> ChronicDuration.output(1299600, :weeks => true)
|
32
|
+
=> 2 wks 1 day 1 hr
|
33
|
+
>> ChronicDuration.output(1299600, :weeks => true, :units => 2)
|
34
|
+
=> 2 wks 1 day
|
35
|
+
>> ChronicDuration.output(45*24*60*60 + 15*60, :limit_to_hours => true)
|
36
|
+
=> 1080 hrs 15 mins
|
37
|
+
>> ChronicDuration.output(1299600, :weeks => true, :units => 2, :joiner => ', ')
|
38
|
+
=> 2 wks, 1 day
|
39
|
+
>> ChronicDuration.output(1296000)
|
40
|
+
=> 15 days
|
41
|
+
|
42
|
+
Nil is returned if the string can't be parsed
|
43
|
+
|
44
|
+
Examples of parse-able strings:
|
45
|
+
|
46
|
+
* '12.4 secs'
|
47
|
+
* '1:20'
|
48
|
+
* '1:20.51'
|
49
|
+
* '4:01:01'
|
50
|
+
* '3 mins 4 sec'
|
51
|
+
* '2 hrs 20 min'
|
52
|
+
* '2h20min'
|
53
|
+
* '6 mos 1 day'
|
54
|
+
* '47 yrs 6 mos and 4d'
|
55
|
+
* 'two hours and twenty minutes'
|
56
|
+
* '3 weeks and 2 days'
|
57
|
+
|
58
|
+
ChronicDuration.raise_exceptions can be set to true to raise exceptions when the string can't be parsed.
|
59
|
+
|
60
|
+
>> ChronicDuration.raise_exceptions = true
|
61
|
+
=> true
|
62
|
+
>> ChronicDuration.parse('4 elephants and 3 Astroids')
|
63
|
+
ChronicDuration::DurationParseError: An invalid word "elephants" was used in the string to be parsed.
|
64
|
+
|
65
|
+
## Contributing
|
66
|
+
|
67
|
+
Fork and pull request after your specs are green. Add your handle to the list below.
|
68
|
+
Also looking for additional maintainers.
|
69
|
+
|
70
|
+
## Contributors
|
71
|
+
|
72
|
+
errm,pdf, brianjlandau, jduff, olauzon, roboman, ianlevesque, bolandrm
|
73
|
+
|
74
|
+
## TODO
|
75
|
+
|
76
|
+
* Benchmark, optimize
|
77
|
+
* Context specific matching (E.g., for '4m30s', assume 'm' is minutes not months)
|
78
|
+
* Smartly parse vacation-like durations (E.g., '4 days and 3 nights')
|
79
|
+
* :chrono output option should probably change to something like 4 days 4:00:12 instead of 4:04:00:12
|
80
|
+
* Other locale support
|
81
|
+
|
data/Rakefile
ADDED
@@ -0,0 +1,28 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'chronic_duration/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |gem|
|
7
|
+
|
8
|
+
gem.name = "gitlab_chronic_duration"
|
9
|
+
gem.version = ChronicDuration::VERSION
|
10
|
+
gem.authors = ["hpoydar"]
|
11
|
+
gem.email = ["henry@poydar.com"]
|
12
|
+
gem.description = %q{A simple Ruby natural language parser for elapsed time. (For example, 4 hours and 30 minutes, 6 minutes 4 seconds, 3 days, etc.) Returns all results in seconds. Will return an integer unless you get tricky and need a float. (4 minutes and 13.47 seconds, for example.) The reverse can also be performed via the output method.}
|
13
|
+
gem.summary = %q{A simple Ruby natural language parser for elapsed time}
|
14
|
+
gem.homepage = "https://gitlab.com/gitlab-org/gitlab-chronic-duration"
|
15
|
+
gem.license = "MIT"
|
16
|
+
|
17
|
+
gem.files = `git ls-files`.split($/)
|
18
|
+
gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
|
19
|
+
gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
|
20
|
+
gem.require_paths = ["lib"]
|
21
|
+
|
22
|
+
gem.add_runtime_dependency "numerizer", "~> 0.1.1"
|
23
|
+
|
24
|
+
gem.add_development_dependency "rake", "~> 10.0.3"
|
25
|
+
gem.add_development_dependency "rspec", "~> 2.12.0"
|
26
|
+
|
27
|
+
|
28
|
+
end
|
@@ -0,0 +1,302 @@
|
|
1
|
+
require 'numerizer' unless defined?(Numerizer)
|
2
|
+
|
3
|
+
module ChronicDuration
|
4
|
+
|
5
|
+
extend self
|
6
|
+
|
7
|
+
class DurationParseError < StandardError
|
8
|
+
end
|
9
|
+
|
10
|
+
# On average, there's a little over 4 weeks in month.
|
11
|
+
FULL_WEEKS_PER_MONTH = 4
|
12
|
+
|
13
|
+
@@raise_exceptions = false
|
14
|
+
@@hours_per_day = 24
|
15
|
+
@@days_per_month = 30
|
16
|
+
|
17
|
+
def self.raise_exceptions
|
18
|
+
!!@@raise_exceptions
|
19
|
+
end
|
20
|
+
|
21
|
+
def self.raise_exceptions=(value)
|
22
|
+
@@raise_exceptions = !!value
|
23
|
+
end
|
24
|
+
|
25
|
+
def self.hours_per_day
|
26
|
+
@@hours_per_day
|
27
|
+
end
|
28
|
+
|
29
|
+
def self.hours_per_day=(value)
|
30
|
+
@@hours_per_day = value
|
31
|
+
end
|
32
|
+
|
33
|
+
def self.days_per_month
|
34
|
+
@@days_per_month
|
35
|
+
end
|
36
|
+
|
37
|
+
def self.days_per_month=(value)
|
38
|
+
@@days_per_month = value
|
39
|
+
end
|
40
|
+
|
41
|
+
# Given a string representation of elapsed time,
|
42
|
+
# return an integer (or float, if fractions of a
|
43
|
+
# second are input)
|
44
|
+
def parse(string, opts = {})
|
45
|
+
result = calculate_from_words(cleanup(string), opts)
|
46
|
+
(!opts[:keep_zero] and result == 0) ? nil : result
|
47
|
+
end
|
48
|
+
|
49
|
+
# Given an integer and an optional format,
|
50
|
+
# returns a formatted string representing elapsed time
|
51
|
+
def output(seconds, opts = {})
|
52
|
+
int = seconds.to_i
|
53
|
+
seconds = int if seconds - int == 0 # if seconds end with .0
|
54
|
+
|
55
|
+
opts[:format] ||= :default
|
56
|
+
opts[:keep_zero] ||= false
|
57
|
+
|
58
|
+
hours_per_day = opts[:hours_per_day] || ChronicDuration.hours_per_day
|
59
|
+
days_per_month = opts[:days_per_month] || ChronicDuration.days_per_month
|
60
|
+
days_per_week = days_per_month / FULL_WEEKS_PER_MONTH
|
61
|
+
|
62
|
+
years = months = weeks = days = hours = minutes = 0
|
63
|
+
|
64
|
+
decimal_places = seconds.to_s.split('.').last.length if seconds.is_a?(Float)
|
65
|
+
|
66
|
+
minute = 60
|
67
|
+
hour = 60 * minute
|
68
|
+
day = hours_per_day * hour
|
69
|
+
month = days_per_month * day
|
70
|
+
year = 31557600
|
71
|
+
|
72
|
+
if seconds >= 31557600 && seconds%year < seconds%month
|
73
|
+
years = seconds / year
|
74
|
+
months = seconds % year / month
|
75
|
+
days = seconds % year % month / day
|
76
|
+
hours = seconds % year % month % day / hour
|
77
|
+
minutes = seconds % year % month % day % hour / minute
|
78
|
+
seconds = seconds % year % month % day % hour % minute
|
79
|
+
elsif seconds >= 60
|
80
|
+
minutes = (seconds / 60).to_i
|
81
|
+
seconds = seconds % 60
|
82
|
+
if minutes >= 60
|
83
|
+
hours = (minutes / 60).to_i
|
84
|
+
minutes = (minutes % 60).to_i
|
85
|
+
if !opts[:limit_to_hours]
|
86
|
+
if hours >= hours_per_day
|
87
|
+
days = (hours / hours_per_day).to_i
|
88
|
+
hours = (hours % hours_per_day).to_i
|
89
|
+
if opts[:weeks]
|
90
|
+
if days >= days_per_week
|
91
|
+
weeks = (days / days_per_week).to_i
|
92
|
+
days = (days % days_per_week).to_i
|
93
|
+
if weeks >= FULL_WEEKS_PER_MONTH
|
94
|
+
months = (weeks / FULL_WEEKS_PER_MONTH).to_i
|
95
|
+
weeks = (weeks % FULL_WEEKS_PER_MONTH).to_i
|
96
|
+
end
|
97
|
+
end
|
98
|
+
else
|
99
|
+
if days >= days_per_month
|
100
|
+
months = (days / days_per_month).to_i
|
101
|
+
days = (days % days_per_month).to_i
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
joiner = opts.fetch(:joiner) { ' ' }
|
110
|
+
process = nil
|
111
|
+
|
112
|
+
case opts[:format]
|
113
|
+
when :micro
|
114
|
+
dividers = {
|
115
|
+
:years => 'y', :months => 'mo', :weeks => 'w', :days => 'd', :hours => 'h', :minutes => 'm', :seconds => 's' }
|
116
|
+
joiner = ''
|
117
|
+
when :short
|
118
|
+
dividers = {
|
119
|
+
:years => 'y', :months => 'mo', :weeks => 'w', :days => 'd', :hours => 'h', :minutes => 'm', :seconds => 's' }
|
120
|
+
when :default
|
121
|
+
dividers = {
|
122
|
+
:years => ' yr', :months => ' mo', :weeks => ' wk', :days => ' day', :hours => ' hr', :minutes => ' min', :seconds => ' sec',
|
123
|
+
:pluralize => true }
|
124
|
+
when :long
|
125
|
+
dividers = {
|
126
|
+
:years => ' year', :months => ' month', :weeks => ' week', :days => ' day', :hours => ' hour', :minutes => ' minute', :seconds => ' second',
|
127
|
+
:pluralize => true }
|
128
|
+
when :chrono
|
129
|
+
dividers = {
|
130
|
+
:years => ':', :months => ':', :weeks => ':', :days => ':', :hours => ':', :minutes => ':', :seconds => ':', :keep_zero => true }
|
131
|
+
process = lambda do |str|
|
132
|
+
# Pad zeros
|
133
|
+
# Get rid of lead off times if they are zero
|
134
|
+
# Get rid of lead off zero
|
135
|
+
# Get rid of trailing :
|
136
|
+
divider = ':'
|
137
|
+
str.split(divider).map { |n|
|
138
|
+
# add zeros only if n is an integer
|
139
|
+
n.include?('.') ? ("%04.#{decimal_places}f" % n) : ("%02d" % n)
|
140
|
+
}.join(divider).gsub(/^(00:)+/, '').gsub(/^0/, '').gsub(/:$/, '')
|
141
|
+
end
|
142
|
+
joiner = ''
|
143
|
+
end
|
144
|
+
|
145
|
+
result = [:years, :months, :weeks, :days, :hours, :minutes, :seconds].map do |t|
|
146
|
+
next if t == :weeks && !opts[:weeks]
|
147
|
+
num = eval(t.to_s)
|
148
|
+
num = ("%.#{decimal_places}f" % num) if num.is_a?(Float) && t == :seconds
|
149
|
+
keep_zero = dividers[:keep_zero]
|
150
|
+
keep_zero ||= opts[:keep_zero] if t == :seconds
|
151
|
+
humanize_time_unit( num, dividers[t], dividers[:pluralize], keep_zero )
|
152
|
+
end.compact!
|
153
|
+
|
154
|
+
result = result[0...opts[:units]] if opts[:units]
|
155
|
+
|
156
|
+
result = result.join(joiner)
|
157
|
+
|
158
|
+
if process
|
159
|
+
result = process.call(result)
|
160
|
+
end
|
161
|
+
|
162
|
+
result.length == 0 ? nil : result
|
163
|
+
|
164
|
+
end
|
165
|
+
|
166
|
+
private
|
167
|
+
|
168
|
+
def humanize_time_unit(number, unit, pluralize, keep_zero)
|
169
|
+
return nil if number == 0 && !keep_zero
|
170
|
+
res = "#{number}#{unit}"
|
171
|
+
# A poor man's pluralizer
|
172
|
+
res << 's' if !(number == 1) && pluralize
|
173
|
+
res
|
174
|
+
end
|
175
|
+
|
176
|
+
def calculate_from_words(string, opts)
|
177
|
+
val = 0
|
178
|
+
words = string.split(' ')
|
179
|
+
words.each_with_index do |v, k|
|
180
|
+
if v =~ float_matcher
|
181
|
+
val += (convert_to_number(v) * duration_units_seconds_multiplier(words[k + 1] || (opts[:default_unit] || 'seconds'), opts))
|
182
|
+
end
|
183
|
+
end
|
184
|
+
val
|
185
|
+
end
|
186
|
+
|
187
|
+
def cleanup(string)
|
188
|
+
res = string.downcase
|
189
|
+
res = filter_by_type(Numerizer.numerize(res))
|
190
|
+
res = res.gsub(float_matcher) {|n| " #{n} "}.squeeze(' ').strip
|
191
|
+
res = filter_through_white_list(res)
|
192
|
+
end
|
193
|
+
|
194
|
+
def convert_to_number(string)
|
195
|
+
string.to_f % 1 > 0 ? string.to_f : string.to_i
|
196
|
+
end
|
197
|
+
|
198
|
+
def duration_units_list
|
199
|
+
%w(seconds minutes hours days weeks months years)
|
200
|
+
end
|
201
|
+
|
202
|
+
def duration_units_seconds_multiplier(unit, opts)
|
203
|
+
return 0 unless duration_units_list.include?(unit)
|
204
|
+
|
205
|
+
hours_per_day = opts[:hours_per_day] || ChronicDuration.hours_per_day
|
206
|
+
days_per_month = opts[:days_per_month] || ChronicDuration.days_per_month
|
207
|
+
days_per_week = days_per_month / FULL_WEEKS_PER_MONTH
|
208
|
+
|
209
|
+
case unit
|
210
|
+
when 'years'; 31557600
|
211
|
+
when 'months'; 3600 * hours_per_day * days_per_month
|
212
|
+
when 'weeks'; 3600 * hours_per_day * days_per_week
|
213
|
+
when 'days'; 3600 * hours_per_day
|
214
|
+
when 'hours'; 3600
|
215
|
+
when 'minutes'; 60
|
216
|
+
when 'seconds'; 1
|
217
|
+
end
|
218
|
+
end
|
219
|
+
|
220
|
+
# Parse 3:41:59 and return 3 hours 41 minutes 59 seconds
|
221
|
+
def filter_by_type(string)
|
222
|
+
chrono_units_list = duration_units_list.reject {|v| v == "weeks"}
|
223
|
+
if string.gsub(' ', '') =~ /#{float_matcher}(:#{float_matcher})+/
|
224
|
+
res = []
|
225
|
+
string.gsub(' ', '').split(':').reverse.each_with_index do |v,k|
|
226
|
+
return unless chrono_units_list[k]
|
227
|
+
res << "#{v} #{chrono_units_list[k]}"
|
228
|
+
end
|
229
|
+
res = res.reverse.join(' ')
|
230
|
+
else
|
231
|
+
res = string
|
232
|
+
end
|
233
|
+
res
|
234
|
+
end
|
235
|
+
|
236
|
+
def float_matcher
|
237
|
+
/[0-9]*\.?[0-9]+/
|
238
|
+
end
|
239
|
+
|
240
|
+
# Get rid of unknown words and map found
|
241
|
+
# words to defined time units
|
242
|
+
def filter_through_white_list(string)
|
243
|
+
res = []
|
244
|
+
string.split(' ').each do |word|
|
245
|
+
if word =~ float_matcher
|
246
|
+
res << word.strip
|
247
|
+
next
|
248
|
+
end
|
249
|
+
stripped_word = word.strip.gsub(/^,/, '').gsub(/,$/, '')
|
250
|
+
if mappings.has_key?(stripped_word)
|
251
|
+
res << mappings[stripped_word]
|
252
|
+
elsif !join_words.include?(stripped_word) and ChronicDuration.raise_exceptions
|
253
|
+
raise DurationParseError, "An invalid word #{word.inspect} was used in the string to be parsed."
|
254
|
+
end
|
255
|
+
end
|
256
|
+
# add '1' at front if string starts with something recognizable but not with a number, like 'day' or 'minute 30sec'
|
257
|
+
res.unshift(1) if res.length > 0 && mappings[res[0]]
|
258
|
+
res.join(' ')
|
259
|
+
end
|
260
|
+
|
261
|
+
def mappings
|
262
|
+
{
|
263
|
+
'seconds' => 'seconds',
|
264
|
+
'second' => 'seconds',
|
265
|
+
'secs' => 'seconds',
|
266
|
+
'sec' => 'seconds',
|
267
|
+
's' => 'seconds',
|
268
|
+
'minutes' => 'minutes',
|
269
|
+
'minute' => 'minutes',
|
270
|
+
'mins' => 'minutes',
|
271
|
+
'min' => 'minutes',
|
272
|
+
'm' => 'minutes',
|
273
|
+
'hours' => 'hours',
|
274
|
+
'hour' => 'hours',
|
275
|
+
'hrs' => 'hours',
|
276
|
+
'hr' => 'hours',
|
277
|
+
'h' => 'hours',
|
278
|
+
'days' => 'days',
|
279
|
+
'day' => 'days',
|
280
|
+
'dy' => 'days',
|
281
|
+
'd' => 'days',
|
282
|
+
'weeks' => 'weeks',
|
283
|
+
'week' => 'weeks',
|
284
|
+
'wks' => 'weeks',
|
285
|
+
'wk' => 'weeks',
|
286
|
+
'w' => 'weeks',
|
287
|
+
'months' => 'months',
|
288
|
+
'mo' => 'months',
|
289
|
+
'mos' => 'months',
|
290
|
+
'month' => 'months',
|
291
|
+
'years' => 'years',
|
292
|
+
'year' => 'years',
|
293
|
+
'yrs' => 'years',
|
294
|
+
'yr' => 'years',
|
295
|
+
'y' => 'years'
|
296
|
+
}
|
297
|
+
end
|
298
|
+
|
299
|
+
def join_words
|
300
|
+
['and', 'with', 'plus']
|
301
|
+
end
|
302
|
+
end
|
@@ -0,0 +1,331 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe ChronicDuration do
|
4
|
+
|
5
|
+
describe ".parse" do
|
6
|
+
|
7
|
+
@exemplars = {
|
8
|
+
'1:20' => 60 + 20,
|
9
|
+
'1:20.51' => 60 + 20.51,
|
10
|
+
'4:01:01' => 4 * 3600 + 60 + 1,
|
11
|
+
'3 mins 4 sec' => 3 * 60 + 4,
|
12
|
+
'3 Mins 4 Sec' => 3 * 60 + 4,
|
13
|
+
'three mins four sec' => 3 * 60 + 4,
|
14
|
+
'2 hrs 20 min' => 2 * 3600 + 20 * 60,
|
15
|
+
'2h20min' => 2 * 3600 + 20 * 60,
|
16
|
+
'6 mos 1 day' => 6 * 30 * 24 * 3600 + 24 * 3600,
|
17
|
+
'1 year 6 mos 1 day' => 1 * 31557600 + 6 * 30 * 24 * 3600 + 24 * 3600,
|
18
|
+
'2.5 hrs' => 2.5 * 3600,
|
19
|
+
'47 yrs 6 mos and 4.5d' => 47 * 31557600 + 6 * 30 * 24 * 3600 + 4.5 * 24 * 3600,
|
20
|
+
'two hours and twenty minutes' => 2 * 3600 + 20 * 60,
|
21
|
+
'four hours and forty minutes' => 4 * 3600 + 40 * 60,
|
22
|
+
'four hours, and fourty minutes' => 4 * 3600 + 40 * 60,
|
23
|
+
'3 weeks and, 2 days' => 3600 * 24 * 7 * 3 + 3600 * 24 * 2,
|
24
|
+
'3 weeks, plus 2 days' => 3600 * 24 * 7 * 3 + 3600 * 24 * 2,
|
25
|
+
'3 weeks with 2 days' => 3600 * 24 * 7 * 3 + 3600 * 24 * 2,
|
26
|
+
'1 month' => 3600 * 24 * 30,
|
27
|
+
'2 months' => 3600 * 24 * 30 * 2,
|
28
|
+
'18 months' => 3600 * 24 * 30 * 18,
|
29
|
+
'1 year 6 months' => (3600 * 24 * (365.25 + 6 * 30)).to_i,
|
30
|
+
'day' => 3600 * 24,
|
31
|
+
'minute 30s' => 90
|
32
|
+
}
|
33
|
+
|
34
|
+
context "when string can't be parsed" do
|
35
|
+
|
36
|
+
it "returns nil" do
|
37
|
+
ChronicDuration.parse('gobblygoo').should be_nil
|
38
|
+
end
|
39
|
+
|
40
|
+
it "cannot parse zero" do
|
41
|
+
ChronicDuration.parse('0').should be_nil
|
42
|
+
end
|
43
|
+
|
44
|
+
context "when @@raise_exceptions set to true" do
|
45
|
+
|
46
|
+
it "raises with ChronicDuration::DurationParseError" do
|
47
|
+
ChronicDuration.raise_exceptions = true
|
48
|
+
expect { ChronicDuration.parse('23 gobblygoos') }.to raise_error(ChronicDuration::DurationParseError)
|
49
|
+
ChronicDuration.raise_exceptions = false
|
50
|
+
end
|
51
|
+
|
52
|
+
end
|
53
|
+
|
54
|
+
end
|
55
|
+
|
56
|
+
it "should return zero if the string parses as zero and the keep_zero option is true" do
|
57
|
+
ChronicDuration.parse('0', :keep_zero => true).should == 0
|
58
|
+
end
|
59
|
+
|
60
|
+
it "should return a float if seconds are in decimals" do
|
61
|
+
ChronicDuration.parse('12 mins 3.141 seconds').is_a?(Float).should be_true
|
62
|
+
end
|
63
|
+
|
64
|
+
it "should return an integer unless the seconds are in decimals" do
|
65
|
+
ChronicDuration.parse('12 mins 3 seconds').is_a?(Integer).should be_true
|
66
|
+
end
|
67
|
+
|
68
|
+
it "should be able to parse minutes by default" do
|
69
|
+
ChronicDuration.parse('5', :default_unit => "minutes").should == 300
|
70
|
+
end
|
71
|
+
|
72
|
+
@exemplars.each do |k, v|
|
73
|
+
it "parses a duration like #{k}" do
|
74
|
+
ChronicDuration.parse(k).should == v
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
context 'with :hours_per_day and :days_per_month params' do
|
79
|
+
it 'uses provided :hours_per_day' do
|
80
|
+
ChronicDuration.parse('1d', hours_per_day: 24).should == 24 * 60 * 60
|
81
|
+
ChronicDuration.parse('1d', hours_per_day: 8).should == 8 * 60 * 60
|
82
|
+
end
|
83
|
+
|
84
|
+
it 'uses provided :days_per_month' do
|
85
|
+
ChronicDuration.parse('1mo', days_per_month: 30).should == 30 * 24 * 60 * 60
|
86
|
+
ChronicDuration.parse('1mo', days_per_month: 20).should == 20 * 24 * 60 * 60
|
87
|
+
|
88
|
+
ChronicDuration.parse('1w', days_per_month: 30).should == 7 * 24 * 60 * 60
|
89
|
+
ChronicDuration.parse('1w', days_per_month: 20).should == 5 * 24 * 60 * 60
|
90
|
+
end
|
91
|
+
|
92
|
+
it 'uses provided both :hours_per_day and :days_per_month' do
|
93
|
+
ChronicDuration.parse('1mo', days_per_month: 30, hours_per_day: 24).should == 30 * 24 * 60 * 60
|
94
|
+
ChronicDuration.parse('1mo', days_per_month: 20, hours_per_day: 8).should == 20 * 8 * 60 * 60
|
95
|
+
|
96
|
+
ChronicDuration.parse('1w', days_per_month: 30, hours_per_day: 24).should == 7 * 24 * 60 * 60
|
97
|
+
ChronicDuration.parse('1w', days_per_month: 20, hours_per_day: 8).should == 5 * 8 * 60 * 60
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
describe '.output' do
|
103
|
+
|
104
|
+
@exemplars = {
|
105
|
+
(60 + 20) =>
|
106
|
+
{
|
107
|
+
:micro => '1m20s',
|
108
|
+
:short => '1m 20s',
|
109
|
+
:default => '1 min 20 secs',
|
110
|
+
:long => '1 minute 20 seconds',
|
111
|
+
:chrono => '1:20'
|
112
|
+
},
|
113
|
+
(60 + 20.51) =>
|
114
|
+
{
|
115
|
+
:micro => '1m20.51s',
|
116
|
+
:short => '1m 20.51s',
|
117
|
+
:default => '1 min 20.51 secs',
|
118
|
+
:long => '1 minute 20.51 seconds',
|
119
|
+
:chrono => '1:20.51'
|
120
|
+
},
|
121
|
+
(60 + 20.51928) =>
|
122
|
+
{
|
123
|
+
:micro => '1m20.51928s',
|
124
|
+
:short => '1m 20.51928s',
|
125
|
+
:default => '1 min 20.51928 secs',
|
126
|
+
:long => '1 minute 20.51928 seconds',
|
127
|
+
:chrono => '1:20.51928'
|
128
|
+
},
|
129
|
+
(4 * 3600 + 60 + 1) =>
|
130
|
+
{
|
131
|
+
:micro => '4h1m1s',
|
132
|
+
:short => '4h 1m 1s',
|
133
|
+
:default => '4 hrs 1 min 1 sec',
|
134
|
+
:long => '4 hours 1 minute 1 second',
|
135
|
+
:chrono => '4:01:01'
|
136
|
+
},
|
137
|
+
(2 * 3600 + 20 * 60) =>
|
138
|
+
{
|
139
|
+
:micro => '2h20m',
|
140
|
+
:short => '2h 20m',
|
141
|
+
:default => '2 hrs 20 mins',
|
142
|
+
:long => '2 hours 20 minutes',
|
143
|
+
:chrono => '2:20'
|
144
|
+
},
|
145
|
+
(2 * 3600 + 20 * 60) =>
|
146
|
+
{
|
147
|
+
:micro => '2h20m',
|
148
|
+
:short => '2h 20m',
|
149
|
+
:default => '2 hrs 20 mins',
|
150
|
+
:long => '2 hours 20 minutes',
|
151
|
+
:chrono => '2:20:00'
|
152
|
+
},
|
153
|
+
(6 * 30 * 24 * 3600 + 24 * 3600) =>
|
154
|
+
{
|
155
|
+
:micro => '6mo1d',
|
156
|
+
:short => '6mo 1d',
|
157
|
+
:default => '6 mos 1 day',
|
158
|
+
:long => '6 months 1 day',
|
159
|
+
:chrono => '6:01:00:00:00' # Yuck. FIXME
|
160
|
+
},
|
161
|
+
(365.25 * 24 * 3600 + 24 * 3600 ).to_i =>
|
162
|
+
{
|
163
|
+
:micro => '1y1d',
|
164
|
+
:short => '1y 1d',
|
165
|
+
:default => '1 yr 1 day',
|
166
|
+
:long => '1 year 1 day',
|
167
|
+
:chrono => '1:00:01:00:00:00'
|
168
|
+
},
|
169
|
+
(3 * 365.25 * 24 * 3600 + 24 * 3600 ).to_i =>
|
170
|
+
{
|
171
|
+
:micro => '3y1d',
|
172
|
+
:short => '3y 1d',
|
173
|
+
:default => '3 yrs 1 day',
|
174
|
+
:long => '3 years 1 day',
|
175
|
+
:chrono => '3:00:01:00:00:00'
|
176
|
+
},
|
177
|
+
(3600 * 24 * 30 * 18) =>
|
178
|
+
{
|
179
|
+
:micro => '18mo',
|
180
|
+
:short => '18mo',
|
181
|
+
:default => '18 mos',
|
182
|
+
:long => '18 months',
|
183
|
+
:chrono => '18:00:00:00:00'
|
184
|
+
}
|
185
|
+
}
|
186
|
+
|
187
|
+
@exemplars.each do |k, v|
|
188
|
+
v.each do |key, val|
|
189
|
+
it "properly outputs a duration of #{k} seconds as #{val} using the #{key.to_s} format option" do
|
190
|
+
ChronicDuration.output(k, :format => key).should == val
|
191
|
+
end
|
192
|
+
end
|
193
|
+
end
|
194
|
+
|
195
|
+
@keep_zero_exemplars = {
|
196
|
+
(true) =>
|
197
|
+
{
|
198
|
+
:micro => '0s',
|
199
|
+
:short => '0s',
|
200
|
+
:default => '0 secs',
|
201
|
+
:long => '0 seconds',
|
202
|
+
:chrono => '0'
|
203
|
+
},
|
204
|
+
(false) =>
|
205
|
+
{
|
206
|
+
:micro => nil,
|
207
|
+
:short => nil,
|
208
|
+
:default => nil,
|
209
|
+
:long => nil,
|
210
|
+
:chrono => '0'
|
211
|
+
},
|
212
|
+
}
|
213
|
+
|
214
|
+
@keep_zero_exemplars.each do |k, v|
|
215
|
+
v.each do |key, val|
|
216
|
+
it "should properly output a duration of 0 seconds as #{val.nil? ? "nil" : val} using the #{key.to_s} format option, if the keep_zero option is #{k.to_s}" do
|
217
|
+
ChronicDuration.output(0, :format => key, :keep_zero => k).should == val
|
218
|
+
end
|
219
|
+
end
|
220
|
+
end
|
221
|
+
|
222
|
+
it "returns weeks when needed" do
|
223
|
+
ChronicDuration.output(45*24*60*60, :weeks => true).should =~ /.*wk.*/
|
224
|
+
end
|
225
|
+
|
226
|
+
it "returns hours and minutes only when :hours_only option specified" do
|
227
|
+
ChronicDuration.output(395*24*60*60 + 15*60, :limit_to_hours => true).should == '9480 hrs 15 mins'
|
228
|
+
end
|
229
|
+
|
230
|
+
context 'with :hours_per_day and :days_per_month params' do
|
231
|
+
it 'uses provided :hours_per_day' do
|
232
|
+
ChronicDuration.output(24 * 60 * 60, hours_per_day: 24).should == '1 day'
|
233
|
+
ChronicDuration.output(24 * 60 * 60, hours_per_day: 8).should == '3 days'
|
234
|
+
end
|
235
|
+
|
236
|
+
it 'uses provided :days_per_month' do
|
237
|
+
ChronicDuration.output(7 * 24 * 60 * 60, weeks: true, days_per_month: 30).should == '1 wk'
|
238
|
+
ChronicDuration.output(7 * 24 * 60 * 60, weeks: true, days_per_month: 20).should == '1 wk 2 days'
|
239
|
+
end
|
240
|
+
|
241
|
+
it 'uses provided both :hours_per_day and :days_per_month' do
|
242
|
+
ChronicDuration.output(7 * 24 * 60 * 60, weeks: true, days_per_month: 30, hours_per_day: 24).should == '1 wk'
|
243
|
+
ChronicDuration.output(5 * 8 * 60 * 60, weeks: true, days_per_month: 20, hours_per_day: 8).should == '1 wk'
|
244
|
+
end
|
245
|
+
|
246
|
+
it 'uses provided params alonside with :weeks when converting to months' do
|
247
|
+
ChronicDuration.output(30 * 24 * 60 * 60, days_per_month: 30, hours_per_day: 24).should == '1 mo'
|
248
|
+
ChronicDuration.output(30 * 24 * 60 * 60, days_per_month: 30, hours_per_day: 24, weeks: true).should == '1 mo 2 days'
|
249
|
+
|
250
|
+
ChronicDuration.output(20 * 8 * 60 * 60, days_per_month: 20, hours_per_day: 8).should == '1 mo'
|
251
|
+
ChronicDuration.output(20 * 8 * 60 * 60, days_per_month: 20, hours_per_day: 8, weeks: true).should == '1 mo'
|
252
|
+
end
|
253
|
+
end
|
254
|
+
|
255
|
+
it "returns the specified number of units if provided" do
|
256
|
+
ChronicDuration.output(4 * 3600 + 60 + 1, units: 2).should == '4 hrs 1 min'
|
257
|
+
ChronicDuration.output(6 * 30 * 24 * 3600 + 24 * 3600 + 3600 + 60 + 1, units: 3, format: :long).should == '6 months 1 day 1 hour'
|
258
|
+
end
|
259
|
+
|
260
|
+
context "when the format is not specified" do
|
261
|
+
|
262
|
+
it "uses the default format" do
|
263
|
+
ChronicDuration.output(2 * 3600 + 20 * 60).should == '2 hrs 20 mins'
|
264
|
+
end
|
265
|
+
|
266
|
+
end
|
267
|
+
|
268
|
+
@exemplars.each do |seconds, format_spec|
|
269
|
+
format_spec.each do |format, _|
|
270
|
+
it "outputs a duration for #{seconds} that parses back to the same thing when using the #{format.to_s} format" do
|
271
|
+
ChronicDuration.parse(ChronicDuration.output(seconds, :format => format)).should == seconds
|
272
|
+
end
|
273
|
+
end
|
274
|
+
end
|
275
|
+
|
276
|
+
it "uses user-specified joiner if provided" do
|
277
|
+
ChronicDuration.output(2 * 3600 + 20 * 60, joiner: ', ').should == '2 hrs, 20 mins'
|
278
|
+
end
|
279
|
+
|
280
|
+
end
|
281
|
+
|
282
|
+
describe ".filter_by_type" do
|
283
|
+
|
284
|
+
it "receives a chrono-formatted time like 3:14 and return a human time like 3 minutes 14 seconds" do
|
285
|
+
ChronicDuration.instance_eval("filter_by_type('3:14')").should == '3 minutes 14 seconds'
|
286
|
+
end
|
287
|
+
|
288
|
+
it "receives chrono-formatted time like 12:10:14 and return a human time like 12 hours 10 minutes 14 seconds" do
|
289
|
+
ChronicDuration.instance_eval("filter_by_type('12:10:14')").should == '12 hours 10 minutes 14 seconds'
|
290
|
+
end
|
291
|
+
|
292
|
+
it "returns the input if it's not a chrono-formatted time" do
|
293
|
+
ChronicDuration.instance_eval("filter_by_type('4 hours')").should == '4 hours'
|
294
|
+
end
|
295
|
+
|
296
|
+
end
|
297
|
+
|
298
|
+
describe ".cleanup" do
|
299
|
+
|
300
|
+
it "cleans up extraneous words" do
|
301
|
+
ChronicDuration.instance_eval("cleanup('4 days and 11 hours')").should == '4 days 11 hours'
|
302
|
+
end
|
303
|
+
|
304
|
+
it "cleans up extraneous spaces" do
|
305
|
+
ChronicDuration.instance_eval("cleanup(' 4 days and 11 hours')").should == '4 days 11 hours'
|
306
|
+
end
|
307
|
+
|
308
|
+
it "inserts spaces where there aren't any" do
|
309
|
+
ChronicDuration.instance_eval("cleanup('4m11.5s')").should == '4 minutes 11.5 seconds'
|
310
|
+
end
|
311
|
+
|
312
|
+
end
|
313
|
+
|
314
|
+
describe "work week" do
|
315
|
+
before(:all) do
|
316
|
+
ChronicDuration.hours_per_day = 8
|
317
|
+
ChronicDuration.days_per_month = 20
|
318
|
+
end
|
319
|
+
|
320
|
+
after(:all) do
|
321
|
+
ChronicDuration.hours_per_day = 24
|
322
|
+
ChronicDuration.days_per_month = 30
|
323
|
+
end
|
324
|
+
|
325
|
+
it "should parse knowing the work week" do
|
326
|
+
week = ChronicDuration.parse('5d')
|
327
|
+
ChronicDuration.parse('40h').should == week
|
328
|
+
ChronicDuration.parse('1w').should == week
|
329
|
+
end
|
330
|
+
end
|
331
|
+
end
|
data/spec/spec_helper.rb
ADDED
metadata
ADDED
@@ -0,0 +1,103 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: gitlab_chronic_duration
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.10.6.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- hpoydar
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2019-09-11 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: numerizer
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: 0.1.1
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: 0.1.1
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: rake
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: 10.0.3
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: 10.0.3
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: rspec
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: 2.12.0
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: 2.12.0
|
55
|
+
description: A simple Ruby natural language parser for elapsed time. (For example,
|
56
|
+
4 hours and 30 minutes, 6 minutes 4 seconds, 3 days, etc.) Returns all results in
|
57
|
+
seconds. Will return an integer unless you get tricky and need a float. (4 minutes
|
58
|
+
and 13.47 seconds, for example.) The reverse can also be performed via the output
|
59
|
+
method.
|
60
|
+
email:
|
61
|
+
- henry@poydar.com
|
62
|
+
executables: []
|
63
|
+
extensions: []
|
64
|
+
extra_rdoc_files: []
|
65
|
+
files:
|
66
|
+
- ".gitignore"
|
67
|
+
- ".gitlab-ci.yml"
|
68
|
+
- CHANGELOG.md
|
69
|
+
- Gemfile
|
70
|
+
- LICENSE.txt
|
71
|
+
- README.md
|
72
|
+
- Rakefile
|
73
|
+
- gitlab_chronic_duration.gemspec
|
74
|
+
- lib/chronic_duration/version.rb
|
75
|
+
- lib/gitlab_chronic_duration.rb
|
76
|
+
- spec/lib/chronic_duration_spec.rb
|
77
|
+
- spec/spec_helper.rb
|
78
|
+
homepage: https://gitlab.com/gitlab-org/gitlab-chronic-duration
|
79
|
+
licenses:
|
80
|
+
- MIT
|
81
|
+
metadata: {}
|
82
|
+
post_install_message:
|
83
|
+
rdoc_options: []
|
84
|
+
require_paths:
|
85
|
+
- lib
|
86
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
87
|
+
requirements:
|
88
|
+
- - ">="
|
89
|
+
- !ruby/object:Gem::Version
|
90
|
+
version: '0'
|
91
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
92
|
+
requirements:
|
93
|
+
- - ">="
|
94
|
+
- !ruby/object:Gem::Version
|
95
|
+
version: '0'
|
96
|
+
requirements: []
|
97
|
+
rubygems_version: 3.0.3
|
98
|
+
signing_key:
|
99
|
+
specification_version: 4
|
100
|
+
summary: A simple Ruby natural language parser for elapsed time
|
101
|
+
test_files:
|
102
|
+
- spec/lib/chronic_duration_spec.rb
|
103
|
+
- spec/spec_helper.rb
|