hpoydar-chronic_duration 0.6.0 → 0.6.1

Sign up to get free protection for your applications and to get access to all the features.
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'