hpoydar-chronic_duration 0.6.0 → 0.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.
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: hpoydar-chronic_duration
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.6.0
4
+ version: 0.6.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Henry Poydar
@@ -9,7 +9,7 @@ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
11
 
12
- date: 2009-02-12 00:00:00 -08:00
12
+ date: 2009-02-17 00:00:00 -08:00
13
13
  default_executable:
14
14
  dependencies: []
15
15
 
@@ -21,12 +21,8 @@ extensions: []
21
21
 
22
22
  extra_rdoc_files: []
23
23
 
24
- files:
25
- - lib/chronic_duration.rb
26
- - lib/numerizer.rb
27
- - MIT-LICENSE
28
- - Rakefile
29
- - README.rdoc
24
+ files: []
25
+
30
26
  has_rdoc: true
31
27
  homepage: http://github.com/hpoydar/chronic_duration
32
28
  post_install_message:
@@ -56,7 +52,5 @@ rubygems_version: 1.2.0
56
52
  signing_key:
57
53
  specification_version: 2
58
54
  summary: A Ruby natural language parser for elapsed time
59
- test_files:
60
- - spec/chronic_duration_spec.rb
61
- - spec/spec_helper.rb
62
- - Rakefile
55
+ test_files: []
56
+
@@ -1,22 +0,0 @@
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.
@@ -1,52 +0,0 @@
1
- = Chronic Duration
2
-
3
- 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.)
4
-
5
- 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.
6
-
7
- == Installation
8
-
9
- $ sudo gem sources -a http://gems.github.com
10
- $ sudo gem install hpoydar-chronic_duration
11
-
12
- == Usage
13
-
14
- >> require 'chronic_duration'
15
- => true
16
- >> ChronicDuration.parse('4 minutes and 30 seconds')
17
- => 270
18
- >> ChronicDuration.output(270)
19
- => 4 mins 30 secs
20
- >> ChronicDuration.output(270, :short)
21
- => 4m 30s
22
- >> ChronicDuration.output(270, :long)
23
- => 4 minutes 30 seconds
24
- >> ChronicDuration.output(270, :chrono)
25
- => 4:30
26
-
27
- Nil is returned if the string can't be parsed
28
-
29
- Examples of parse-able strings:
30
-
31
- * '12.4 secs'
32
- * '1:20'
33
- * '1:20.51'
34
- * '4:01:01'
35
- * '3 mins 4 sec'
36
- * '2 hrs 20 min'
37
- * '2h20min'
38
- * '6 mos 1 day'
39
- * '47 yrs 6 mos and 4d'
40
- * 'two hours and twenty minutes'
41
-
42
- == Contributors
43
-
44
- jduff
45
-
46
- == TODO
47
-
48
- * Benchmark, optimize
49
- * Context specific matching (E.g., for '4m30s', assume 'm' is minutes not months)
50
- * Smartly parse vacation-like durations (E.g., '4 days and 3 nights')
51
- * :chrono output option should probably change to something like 4 days 4:00:12 instead of 4:04:00:12
52
- * Correct rdoc whine on gem install
data/Rakefile DELETED
@@ -1,17 +0,0 @@
1
- require 'rake/rdoctask'
2
- require 'spec/rake/spectask'
3
-
4
- task :default => :spec
5
-
6
- desc 'Run specs'
7
- Spec::Rake::SpecTask.new('spec') do |task|
8
- task.spec_files = FileList['spec/**/*_spec.rb']
9
- end
10
-
11
- Rake::RDocTask.new do |task|
12
- task.rdoc_dir = 'doc'
13
- task.title = 'chronic_duration'
14
- task.options << '--line-numbers' << '--inline-source' << '--main' << 'README'
15
- task.rdoc_files.include 'README'
16
- task.rdoc_files.include 'lib/**/*.rb'
17
- end
@@ -1,198 +0,0 @@
1
- require 'numerizer'
2
- module ChronicDuration
3
- extend self
4
-
5
- def parse(string)
6
- result = calculate_from_words(cleanup(string))
7
- result == 0 ? nil : result
8
- end
9
-
10
- # Refactor, DRY up, make recursive
11
- def output(seconds, opts = {})
12
-
13
- opts[:format] ||= :default
14
-
15
- years = months = days = hours = minutes = 0
16
-
17
- if seconds >= 60
18
- minutes = (seconds / 60).to_i
19
- seconds = seconds % 60
20
- if minutes >= 60
21
- hours = (minutes / 60).to_i
22
- minutes = (minutes % 60).to_i
23
- if hours >= 24
24
- days = (hours / 24).to_i
25
- hours = (hours % 24).to_i
26
- if days >= 30
27
- months = (days / 30).to_i
28
- days = (days % 30).to_i
29
- if months >= 12
30
- years = (months / 12).to_i
31
- months = (months % 12).to_i
32
- end
33
- end
34
- end
35
- end
36
- end
37
-
38
- joiner = ' '
39
- process = nil
40
-
41
- case opts[:format]
42
- when :short
43
- dividers = {
44
- :years => 'y', :months => 'm', :days => 'd', :hours => 'h', :minutes => 'm', :seconds => 's' }
45
- when :default
46
- dividers = {
47
- :years => ' yr', :months => ' mo', :days => ' day', :hours => ' hr', :minutes => ' min', :seconds => ' sec',
48
- :pluralize => true }
49
- when :long
50
- dividers = {
51
- :years => ' year', :months => ' month', :days => ' day', :hours => ' hour', :minutes => ' minute', :seconds => ' second',
52
- :pluralize => true }
53
- when :chrono
54
- dividers = {
55
- :years => ':', :months => ':', :days => ':', :hours => ':', :minutes => ':', :seconds => ':', :keep_zero => true }
56
- process = lambda do |str|
57
- # Pad zeros
58
- # Get rid of lead off times if they are zero
59
- # Get rid of lead off zero
60
- # Get rid of trailing :
61
- str.gsub(/\b\d\b/) { |d| ("%02d" % d) }.gsub(/^(00:)+/, '').gsub(/^0/, '').gsub(/:$/, '')
62
- end
63
- joiner = ''
64
- end
65
-
66
- result = []
67
- [:years, :months, :days, :hours, :minutes, :seconds].each do |t|
68
- result << humanize_time_unit( eval(t.to_s), dividers[t], dividers[:pluralize], dividers[:keep_zero] )
69
- end
70
-
71
- result = result.join(joiner).squeeze(' ').strip
72
-
73
- if process
74
- result = process.call(result)
75
- end
76
-
77
- result.length == 0 ? nil : result
78
-
79
- end
80
-
81
- private
82
-
83
- def humanize_time_unit(number, unit, pluralize, keep_zero)
84
- return '' if number == 0 && !keep_zero
85
- res = "#{number}#{unit}"
86
- # A poor man's pluralizer
87
- res << 's' if !(number == 1) && pluralize
88
- res
89
- end
90
-
91
- def calculate_from_words(string)
92
- val = 0
93
- words = string.split(' ')
94
- words.each_with_index do |v, k|
95
- if v =~ float_matcher
96
- val += (convert_to_number(v) * duration_units_seconds_multiplier(words[k + 1] || 'seconds'))
97
- end
98
- end
99
- val
100
- end
101
-
102
- def cleanup(string)
103
- res = filter_by_type(Numerizer.numerize(string))
104
- res = res.gsub(float_matcher) {|n| " #{n} "}.squeeze(' ').strip
105
- res = filter_through_white_list(res)
106
- end
107
-
108
- def convert_to_number(string)
109
- string.to_f % 1 > 0 ? string.to_f : string.to_i
110
- end
111
-
112
- def duration_units_list
113
- %w(seconds minutes hours days weeks months years)
114
- end
115
- def duration_units_seconds_multiplier(unit)
116
- return 0 unless duration_units_list.include?(unit)
117
- case unit
118
- when 'years'; 31557600 # accounts for leap years
119
- when 'months'; 3600 * 24 * 30
120
- when 'weeks'; 3600 * 24 * 7
121
- when 'days'; 3600 * 24
122
- when 'hours'; 3600
123
- when 'minutes'; 60
124
- when 'seconds'; 1
125
- end
126
- end
127
-
128
- def error_message
129
- 'Sorry, that duration could not be parsed'
130
- end
131
-
132
- # Parse 3:41:59 and return 3 hours 41 minutes 59 seconds
133
- def filter_by_type(string)
134
- if string.gsub(' ', '') =~ /#{float_matcher}(:#{float_matcher})+/
135
- res = []
136
- string.gsub(' ', '').split(':').reverse.each_with_index do |v,k|
137
- return unless duration_units_list[k]
138
- res << "#{v} #{duration_units_list[k]}"
139
- end
140
- res = res.reverse.join(' ')
141
- else
142
- res = string
143
- end
144
- res
145
- end
146
-
147
- def float_matcher
148
- /[0-9]*\.?[0-9]+/
149
- end
150
-
151
- # Get rid of unknown words and map found
152
- # words to defined time units
153
- def filter_through_white_list(string)
154
- res = []
155
- string.split(' ').each do |word|
156
- if word =~ float_matcher
157
- res << word.strip
158
- next
159
- end
160
- res << mappings[word.strip] if mappings.has_key?(word.strip)
161
- end
162
- res.join(' ')
163
- end
164
-
165
- def mappings
166
- {
167
- 'seconds' => 'seconds',
168
- 'second' => 'seconds',
169
- 'secs' => 'seconds',
170
- 'sec' => 'seconds',
171
- 's' => 'seconds',
172
- 'minutes' => 'minutes',
173
- 'minute' => 'minutes',
174
- 'mins' => 'minutes',
175
- 'min' => 'minutes',
176
- 'm' => 'minutes',
177
- 'hours' => 'hours',
178
- 'hour' => 'hours',
179
- 'hrs' => 'hours',
180
- 'hr' => 'hours',
181
- 'h' => 'hours',
182
- 'days' => 'days',
183
- 'day' => 'days',
184
- 'dy' => 'days',
185
- 'd' => 'days',
186
- 'months' => 'months',
187
- 'mos' => 'months',
188
- 'years' => 'years',
189
- 'yrs' => 'years',
190
- 'y' => 'years'
191
- }
192
- end
193
-
194
- def white_list
195
- self.mappings.map {|k, v| k}
196
- end
197
-
198
- end
@@ -1,97 +0,0 @@
1
- require 'strscan'
2
-
3
- class Numerizer
4
-
5
- DIRECT_NUMS = [
6
- ['eleven', '11'],
7
- ['twelve', '12'],
8
- ['thirteen', '13'],
9
- ['fourteen', '14'],
10
- ['fifteen', '15'],
11
- ['sixteen', '16'],
12
- ['seventeen', '17'],
13
- ['eighteen', '18'],
14
- ['nineteen', '19'],
15
- ['ninteen', '19'], # Common mis-spelling
16
- ['zero', '0'],
17
- ['one', '1'],
18
- ['two', '2'],
19
- ['three', '3'],
20
- ['four(\W|$)', '4\1'], # The weird regex is so that it matches four but not fourty
21
- ['five', '5'],
22
- ['six(\W|$)', '6\1'],
23
- ['seven(\W|$)', '7\1'],
24
- ['eight(\W|$)', '8\1'],
25
- ['nine(\W|$)', '9\1'],
26
- ['ten', '10'],
27
- ['\ba[\b^$]', '1'] # doesn't make sense for an 'a' at the end to be a 1
28
- ]
29
-
30
- TEN_PREFIXES = [ ['twenty', 20],
31
- ['thirty', 30],
32
- ['fourty', 40],
33
- ['fifty', 50],
34
- ['sixty', 60],
35
- ['seventy', 70],
36
- ['eighty', 80],
37
- ['ninety', 90]
38
- ]
39
-
40
- BIG_PREFIXES = [ ['hundred', 100],
41
- ['thousand', 1000],
42
- ['million', 1_000_000],
43
- ['billion', 1_000_000_000],
44
- ['trillion', 1_000_000_000_000],
45
- ]
46
-
47
- def self.numerize(string)
48
- string = string.dup
49
-
50
- # preprocess
51
- string.gsub!(/ +|([^\d])-([^\d])/, '\1 \2') # will mutilate hyphenated-words but shouldn't matter for date extraction
52
- string.gsub!(/a half/, 'haAlf') # take the 'a' out so it doesn't turn into a 1, save the half for the end
53
-
54
- # easy/direct replacements
55
-
56
- DIRECT_NUMS.each do |dn|
57
- string.gsub!(/#{dn[0]}/i, '<num>' + dn[1])
58
- end
59
-
60
- # ten, twenty, etc.
61
-
62
- TEN_PREFIXES.each do |tp|
63
- string.gsub!(/(?:#{tp[0]}) *<num>(\d(?=[^\d]|$))*/i) { '<num>' + (tp[1] + $1.to_i).to_s }
64
- end
65
-
66
- TEN_PREFIXES.each do |tp|
67
- string.gsub!(/#{tp[0]}/i) { '<num>' + tp[1].to_s }
68
- end
69
-
70
- # hundreds, thousands, millions, etc.
71
-
72
- BIG_PREFIXES.each do |bp|
73
- string.gsub!(/(?:<num>)?(\d*) *#{bp[0]}/i) { '<num>' + (bp[1] * $1.to_i).to_s}
74
- andition(string)
75
- end
76
-
77
- # fractional addition
78
- # I'm not combining this with the previous block as using float addition complicates the strings
79
- # (with extraneous .0's and such )
80
- string.gsub!(/(\d+)(?: | and |-)*haAlf/i) { ($1.to_f + 0.5).to_s }
81
-
82
- string.gsub(/<num>/, '')
83
- end
84
-
85
- private
86
-
87
- def self.andition(string)
88
- sc = StringScanner.new(string)
89
- while(sc.scan_until(/<num>(\d+)( | and )<num>(\d+)(?=[^\w]|$)/i))
90
- if sc[2] =~ /and/ || sc[1].size > sc[3].size
91
- string[(sc.pos - sc.matched_size)..(sc.pos-1)] = '<num>' + (sc[1].to_i + sc[3].to_i).to_s
92
- sc.reset
93
- end
94
- end
95
- end
96
-
97
- end
@@ -1,150 +0,0 @@
1
- require File.dirname(__FILE__) + '/spec_helper'
2
-
3
- describe ChronicDuration, 'gem' do
4
-
5
- it "should build" do
6
- spec = eval(File.read("#{File.dirname(__FILE__)}/../chronic_duration.gemspec"))
7
- FileUtils.rm_f(File.dirname(__FILE__) + "/../chronic_duration-#{spec.version}.gem")
8
- system "cd #{File.dirname(__FILE__)}/.. && gem build chronic_duration.gemspec -q --no-verbose"
9
- File.exists?(File.dirname(__FILE__) + "/../chronic_duration-#{spec.version}.gem").should be_true
10
- FileUtils.rm_f(File.dirname(__FILE__) + "/../chronic_duration-#{spec.version}.gem")
11
- end
12
-
13
- end
14
-
15
- describe ChronicDuration, '.parse' do
16
-
17
- @exemplars = {
18
- '1:20' => 60 + 20,
19
- '1:20.51' => 60 + 20.51,
20
- '4:01:01' => 4 * 3600 + 60 + 1,
21
- '3 mins 4 sec' => 3 * 60 + 4,
22
- 'three mins four sec' => 3 * 60 + 4,
23
- '2 hrs 20 min' => 2 * 3600 + 20 * 60,
24
- '2h20min' => 2 * 3600 + 20 * 60,
25
- '6 mos 1 day' => 6 * 30 * 24 * 3600 + 24 * 3600,
26
- '2.5 hrs' => 2.5 * 3600,
27
- '47 yrs 6 mos and 4.5d' => 47 * 31557600 + 6 * 30 * 24 * 3600 + 4.5 * 24 * 3600,
28
- 'two hours and twenty minutes' => 2 * 3600 + 20 * 60
29
- }
30
-
31
- it "should return nil if the string can't be parsed" do
32
- ChronicDuration.parse('gobblygoo').should be_nil
33
- end
34
-
35
- it "should return a float if seconds are in decimals" do
36
- ChronicDuration.parse('12 mins 3.141 seconds').is_a?(Float).should be_true
37
- end
38
-
39
- it "should return an integer unless the seconds are in decimals" do
40
- ChronicDuration.parse('12 mins 3 seconds').is_a?(Integer).should be_true
41
- end
42
-
43
- @exemplars.each do |k, v|
44
- it "should properly parse a duration like #{k}" do
45
- ChronicDuration.parse(k).should == v
46
- end
47
- end
48
-
49
- end
50
-
51
- describe ChronicDuration, '.output' do
52
-
53
- it "should return nil if the input can't be parsed" do
54
- ChronicDuration.parse('gobblygoo').should be_nil
55
- end
56
-
57
- @exemplars = {
58
- (60 + 20) =>
59
- {
60
- :short => '1m 20s',
61
- :default => '1 min 20 secs',
62
- :long => '1 minute 20 seconds',
63
- :chrono => '1:20'
64
- },
65
- (60 + 20.51) =>
66
- {
67
- :short => '1m 20.51s',
68
- :default => '1 min 20.51 secs',
69
- :long => '1 minute 20.51 seconds',
70
- :chrono => '1:20.51'
71
- },
72
- (4 * 3600 + 60 + 1) =>
73
- {
74
- :short => '4h 1m 1s',
75
- :default => '4 hrs 1 min 1 sec',
76
- :long => '4 hours 1 minute 1 second',
77
- :chrono => '4:01:01'
78
- },
79
- (2 * 3600 + 20 * 60) =>
80
- {
81
- :short => '2h 20m',
82
- :default => '2 hrs 20 mins',
83
- :long => '2 hours 20 minutes',
84
- :chrono => '2:20'
85
- },
86
- (2 * 3600 + 20 * 60) =>
87
- {
88
- :short => '2h 20m',
89
- :default => '2 hrs 20 mins',
90
- :long => '2 hours 20 minutes',
91
- :chrono => '2:20:00'
92
- },
93
- (6 * 30 * 24 * 3600 + 24 * 3600) =>
94
- {
95
- :short => '6m 1d',
96
- :default => '6 mos 1 day',
97
- :long => '6 months 1 day',
98
- :chrono => '6:01:00:00:00' # Yuck. FIXME
99
- }
100
- }
101
-
102
- @exemplars.each do |k, v|
103
- v.each do |key, val|
104
- it "should properly output a duration of #{k} seconds as #{val} using the #{key.to_s} format option" do
105
- ChronicDuration.output(k, :format => key).should == val
106
- end
107
- end
108
- end
109
-
110
- end
111
-
112
-
113
- # Some of the private methods deserve some spec'ing to aid
114
- # us in development...
115
-
116
- describe ChronicDuration, "private methods" do
117
-
118
- describe ".filter_by_type" do
119
-
120
- it "should take a chrono-formatted time like 3:14 and return a human time like 3 minutes 14 seconds" do
121
- ChronicDuration.instance_eval("filter_by_type('3:14')").should == '3 minutes 14 seconds'
122
- end
123
-
124
- it "should take a chrono-formatted time like 12:10:14 and return a human time like 12 hours 10 minutes 14 seconds" do
125
- ChronicDuration.instance_eval("filter_by_type('12:10:14')").should == '12 hours 10 minutes 14 seconds'
126
- end
127
-
128
- it "should return the input if it's not a chrono-formatted time" do
129
- ChronicDuration.instance_eval("filter_by_type('4 hours')").should == '4 hours'
130
- end
131
-
132
- end
133
-
134
- describe ".cleanup" do
135
-
136
- it "should clean up extraneous words" do
137
- ChronicDuration.instance_eval("cleanup('4 days and 11 hours')").should == '4 days 11 hours'
138
- end
139
-
140
- it "should cleanup extraneous spaces" do
141
- ChronicDuration.instance_eval("cleanup(' 4 days and 11 hours')").should == '4 days 11 hours'
142
- end
143
-
144
- it "should insert spaces where there aren't any" do
145
- ChronicDuration.instance_eval("cleanup('4m11.5s')").should == '4 minutes 11.5 seconds'
146
- end
147
-
148
- end
149
-
150
- end
@@ -1,4 +0,0 @@
1
- require 'rubygems'
2
- require 'spec'
3
- $LOAD_PATH.unshift File.dirname(__FILE__) + '/../lib'
4
- require 'chronic_duration'