hpoydar-chronic_duration 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- data/LICENSE +22 -0
- data/README +40 -0
- data/Rakefile +17 -0
- data/lib/chronic_duration.rb +118 -0
- data/spec/chronic_duration_spec.rb +86 -0
- data/spec/spec_helper.rb +4 -0
- metadata +61 -0
data/LICENSE
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
ADDED
@@ -0,0 +1,40 @@
|
|
1
|
+
Chronic Duration
|
2
|
+
================
|
3
|
+
|
4
|
+
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.)
|
5
|
+
|
6
|
+
Installation
|
7
|
+
------------
|
8
|
+
|
9
|
+
$ sudo gem sources -a http://gems.github.com
|
10
|
+
$ sudo gem install hpoydar-chronic_duration
|
11
|
+
|
12
|
+
Usage
|
13
|
+
-----
|
14
|
+
|
15
|
+
>> require 'chronic_duration'
|
16
|
+
=> true
|
17
|
+
>> ChronicDuration.parse('4 minutes and 30 seconds')
|
18
|
+
=> 270
|
19
|
+
|
20
|
+
Nil is returned if the string can't be parsed
|
21
|
+
|
22
|
+
Examples of parse-able strings:
|
23
|
+
|
24
|
+
* '12.4 secs'
|
25
|
+
* '1:20'
|
26
|
+
* '1:20.51'
|
27
|
+
* '4:01:01'
|
28
|
+
* '3 mins 4 sec'
|
29
|
+
* '2 hrs 20 min'
|
30
|
+
* '2h20min'
|
31
|
+
* '6 mos 1 day'
|
32
|
+
* '47 yrs 6 mos and 4d'
|
33
|
+
|
34
|
+
|
35
|
+
TODO
|
36
|
+
----
|
37
|
+
|
38
|
+
* Benchmark and optimize
|
39
|
+
* Context specific matching (E.g., for '4m30s', assume 'm' is minutes)
|
40
|
+
* Smartly parse vacation-like durations (E.g., '4 days and 3 nights')
|
data/Rakefile
ADDED
@@ -0,0 +1,17 @@
|
|
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
|
@@ -0,0 +1,118 @@
|
|
1
|
+
module ChronicDuration
|
2
|
+
extend self
|
3
|
+
|
4
|
+
def parse(string)
|
5
|
+
result = calculate_from_words(cleanup(string))
|
6
|
+
result == 0 ? nil : result
|
7
|
+
end
|
8
|
+
|
9
|
+
private
|
10
|
+
|
11
|
+
def calculate_from_words(string)
|
12
|
+
val = 0
|
13
|
+
words = string.split(' ')
|
14
|
+
words.each_with_index do |v, k|
|
15
|
+
if v =~ float_matcher
|
16
|
+
val += (convert_to_number(v) * duration_units_seconds_multiplier(words[k + 1] || 'seconds'))
|
17
|
+
end
|
18
|
+
end
|
19
|
+
val
|
20
|
+
end
|
21
|
+
|
22
|
+
def cleanup(string)
|
23
|
+
res = filter_by_type(string)
|
24
|
+
res = res.gsub(float_matcher) {|n| " #{n} "}.squeeze(' ').strip
|
25
|
+
res = filter_through_white_list(res)
|
26
|
+
end
|
27
|
+
|
28
|
+
def convert_to_number(string)
|
29
|
+
string.to_f % 1 > 0 ? string.to_f : string.to_i
|
30
|
+
end
|
31
|
+
|
32
|
+
def duration_units_list
|
33
|
+
%w(seconds minutes hours days weeks months years)
|
34
|
+
end
|
35
|
+
def duration_units_seconds_multiplier(unit)
|
36
|
+
return 0 unless duration_units_list.include?(unit)
|
37
|
+
case unit
|
38
|
+
when 'years'; 31557600 # accounts for leap years
|
39
|
+
when 'months'; 3600 * 24 * 30
|
40
|
+
when 'weeks'; 3600 * 24 * 7
|
41
|
+
when 'days'; 3600 * 24
|
42
|
+
when 'hours'; 3600
|
43
|
+
when 'minutes'; 60
|
44
|
+
when 'seconds'; 1
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def error_message
|
49
|
+
'Sorry, that duration could not be parsed'
|
50
|
+
end
|
51
|
+
|
52
|
+
# Parse 3:41:59 and return 3 hours 41 minutes 59 seconds
|
53
|
+
def filter_by_type(string)
|
54
|
+
if string.gsub(' ', '') =~ /#{float_matcher}(:#{float_matcher})+/
|
55
|
+
res = []
|
56
|
+
string.gsub(' ', '').split(':').reverse.each_with_index do |v,k|
|
57
|
+
return unless duration_units_list[k]
|
58
|
+
res << "#{v} #{duration_units_list[k]}"
|
59
|
+
end
|
60
|
+
res = res.reverse.join(' ')
|
61
|
+
else
|
62
|
+
res = string
|
63
|
+
end
|
64
|
+
res
|
65
|
+
end
|
66
|
+
|
67
|
+
def float_matcher
|
68
|
+
/[0-9]*\.?[0-9]+/
|
69
|
+
end
|
70
|
+
|
71
|
+
# Get rid of unknown words and map found
|
72
|
+
# words to defined time units
|
73
|
+
def filter_through_white_list(string)
|
74
|
+
res = []
|
75
|
+
string.split(' ').each do |word|
|
76
|
+
if word =~ float_matcher
|
77
|
+
res << word.strip
|
78
|
+
next
|
79
|
+
end
|
80
|
+
res << mappings[word.strip] if mappings.has_key?(word.strip)
|
81
|
+
end
|
82
|
+
res.join(' ')
|
83
|
+
end
|
84
|
+
|
85
|
+
def mappings
|
86
|
+
{
|
87
|
+
'seconds' => 'seconds',
|
88
|
+
'second' => 'seconds',
|
89
|
+
'secs' => 'seconds',
|
90
|
+
'sec' => 'seconds',
|
91
|
+
's' => 'seconds',
|
92
|
+
'minutes' => 'minutes',
|
93
|
+
'minute' => 'minutes',
|
94
|
+
'mins' => 'minutes',
|
95
|
+
'min' => 'minutes',
|
96
|
+
'm' => 'minutes',
|
97
|
+
'hours' => 'hours',
|
98
|
+
'hour' => 'hours',
|
99
|
+
'hrs' => 'hours',
|
100
|
+
'hr' => 'hours',
|
101
|
+
'h' => 'hours',
|
102
|
+
'days' => 'days',
|
103
|
+
'day' => 'days',
|
104
|
+
'dy' => 'days',
|
105
|
+
'd' => 'days',
|
106
|
+
'months' => 'months',
|
107
|
+
'mos' => 'months',
|
108
|
+
'years' => 'years',
|
109
|
+
'yrs' => 'years',
|
110
|
+
'y' => 'years'
|
111
|
+
}
|
112
|
+
end
|
113
|
+
|
114
|
+
def white_list
|
115
|
+
self.mappings.map {|k, v| k}
|
116
|
+
end
|
117
|
+
|
118
|
+
end
|
@@ -0,0 +1,86 @@
|
|
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
|
+
'2 hrs 20 min' => 2 * 3600 + 20 * 60,
|
23
|
+
'2h20min' => 2 * 3600 + 20 * 60,
|
24
|
+
'6 mos 1 day' => 6 * 30 * 24 * 3600 + 24 * 3600,
|
25
|
+
'2.5 hrs' => 2.5 * 3600,
|
26
|
+
'47 yrs 6 mos and 4.5d' => 47 * 31557600 + 6 * 30 * 24 * 3600 + 4.5 * 24 * 3600
|
27
|
+
}
|
28
|
+
|
29
|
+
it "should return nil if the string can't be parsed" do
|
30
|
+
ChronicDuration.parse('gobblygoo').should be_nil
|
31
|
+
end
|
32
|
+
|
33
|
+
it "should return a float if seconds are in decimals" do
|
34
|
+
ChronicDuration.parse('12 mins 3.141 seconds').is_a?(Float).should be_true
|
35
|
+
end
|
36
|
+
|
37
|
+
it "should return an integer unless the seconds are in decimals" do
|
38
|
+
ChronicDuration.parse('12 mins 3 seconds').is_a?(Integer).should be_true
|
39
|
+
end
|
40
|
+
|
41
|
+
@exemplars.each do |k, v|
|
42
|
+
it "should properly parse a duration like #{k}" do
|
43
|
+
ChronicDuration.parse(k).should == v
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
end
|
48
|
+
|
49
|
+
# Some of the private methods deserve some spec'ing to aid
|
50
|
+
# us in development...
|
51
|
+
|
52
|
+
describe ChronicDuration, "private methods" do
|
53
|
+
|
54
|
+
describe ".filter_by_type" do
|
55
|
+
|
56
|
+
it "should take a chrono-formatted time like 3:14 and return a human time like 3 minutes 14 seconds" do
|
57
|
+
ChronicDuration.instance_eval("filter_by_type('3:14')").should == '3 minutes 14 seconds'
|
58
|
+
end
|
59
|
+
|
60
|
+
it "should take a chrono-formatted time like 12:10:14 and return a human time like 12 hours 10 minutes 14 seconds" do
|
61
|
+
ChronicDuration.instance_eval("filter_by_type('12:10:14')").should == '12 hours 10 minutes 14 seconds'
|
62
|
+
end
|
63
|
+
|
64
|
+
it "should return the input if it's not a chrono-formatted time" do
|
65
|
+
ChronicDuration.instance_eval("filter_by_type('4 hours')").should == '4 hours'
|
66
|
+
end
|
67
|
+
|
68
|
+
end
|
69
|
+
|
70
|
+
describe ".cleanup" do
|
71
|
+
|
72
|
+
it "should clean up extraneous words" do
|
73
|
+
ChronicDuration.instance_eval("cleanup('4 days and 11 hours')").should == '4 days 11 hours'
|
74
|
+
end
|
75
|
+
|
76
|
+
it "should cleanup extraneous spaces" do
|
77
|
+
ChronicDuration.instance_eval("cleanup(' 4 days and 11 hours')").should == '4 days 11 hours'
|
78
|
+
end
|
79
|
+
|
80
|
+
it "should insert spaces where there aren't any" do
|
81
|
+
ChronicDuration.instance_eval("cleanup('4m11.5s')").should == '4 minutes 11.5 seconds'
|
82
|
+
end
|
83
|
+
|
84
|
+
end
|
85
|
+
|
86
|
+
end
|
data/spec/spec_helper.rb
ADDED
metadata
ADDED
@@ -0,0 +1,61 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: hpoydar-chronic_duration
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.2.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Henry Poydar
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
|
12
|
+
date: 2009-01-11 00:00:00 -08:00
|
13
|
+
default_executable:
|
14
|
+
dependencies: []
|
15
|
+
|
16
|
+
description: 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.)
|
17
|
+
email: henry@poydar.com
|
18
|
+
executables: []
|
19
|
+
|
20
|
+
extensions: []
|
21
|
+
|
22
|
+
extra_rdoc_files: []
|
23
|
+
|
24
|
+
files:
|
25
|
+
- lib/chronic_duration.rb
|
26
|
+
- LICENSE
|
27
|
+
- Rakefile
|
28
|
+
- README
|
29
|
+
has_rdoc: true
|
30
|
+
homepage: http://github.com/hpoydar/chronic_duration
|
31
|
+
post_install_message:
|
32
|
+
rdoc_options:
|
33
|
+
- --line-numbers
|
34
|
+
- --inline-source
|
35
|
+
- --main
|
36
|
+
- README
|
37
|
+
require_paths:
|
38
|
+
- lib
|
39
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
40
|
+
requirements:
|
41
|
+
- - ">="
|
42
|
+
- !ruby/object:Gem::Version
|
43
|
+
version: "0"
|
44
|
+
version:
|
45
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
46
|
+
requirements:
|
47
|
+
- - ">="
|
48
|
+
- !ruby/object:Gem::Version
|
49
|
+
version: "0"
|
50
|
+
version:
|
51
|
+
requirements: []
|
52
|
+
|
53
|
+
rubyforge_project:
|
54
|
+
rubygems_version: 1.2.0
|
55
|
+
signing_key:
|
56
|
+
specification_version: 2
|
57
|
+
summary: A Ruby natural language parser for elapsed time
|
58
|
+
test_files:
|
59
|
+
- spec/chronic_duration_spec.rb
|
60
|
+
- spec/spec_helper.rb
|
61
|
+
- Rakefile
|