gitlab_chronic_duration 0.10.6.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
+
[![Build Status](https://travis-ci.org/hpoydar/chronic_duration.png?branch=master)](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
|