spanner-lfittl 0.0.3
Sign up to get free protection for your applications and to get access to all the features.
- data/Gemfile +3 -0
- data/Gemfile.lock +43 -0
- data/README.rdoc +24 -0
- data/Rakefile +28 -0
- data/VERSION +1 -0
- data/lib/spanner.rb +115 -0
- data/spec/spanner_spec.rb +55 -0
- data/spec/spec.opts +7 -0
- data/spec/spec_helper.rb +6 -0
- metadata +176 -0
data/Gemfile
ADDED
data/Gemfile.lock
ADDED
@@ -0,0 +1,43 @@
|
|
1
|
+
PATH
|
2
|
+
remote: .
|
3
|
+
specs:
|
4
|
+
spanner-lfittl (0.0.3)
|
5
|
+
activesupport
|
6
|
+
spanner-lfittl
|
7
|
+
|
8
|
+
GEM
|
9
|
+
remote: http://rubygems.org/
|
10
|
+
specs:
|
11
|
+
activesupport (3.2.3)
|
12
|
+
i18n (~> 0.6)
|
13
|
+
multi_json (~> 1.0)
|
14
|
+
diff-lcs (1.1.3)
|
15
|
+
git (1.2.5)
|
16
|
+
i18n (0.6.0)
|
17
|
+
jeweler (1.8.3)
|
18
|
+
bundler (~> 1.0)
|
19
|
+
git (>= 1.2.5)
|
20
|
+
rake
|
21
|
+
rdoc
|
22
|
+
json (1.7.3)
|
23
|
+
multi_json (1.3.6)
|
24
|
+
rake (0.9.2.2)
|
25
|
+
rdoc (3.12)
|
26
|
+
json (~> 1.4)
|
27
|
+
rspec (2.11.0)
|
28
|
+
rspec-core (~> 2.11.0)
|
29
|
+
rspec-expectations (~> 2.11.0)
|
30
|
+
rspec-mocks (~> 2.11.0)
|
31
|
+
rspec-core (2.11.1)
|
32
|
+
rspec-expectations (2.11.2)
|
33
|
+
diff-lcs (~> 1.1.3)
|
34
|
+
rspec-mocks (2.11.1)
|
35
|
+
|
36
|
+
PLATFORMS
|
37
|
+
ruby
|
38
|
+
|
39
|
+
DEPENDENCIES
|
40
|
+
jeweler
|
41
|
+
rake
|
42
|
+
rspec
|
43
|
+
spanner-lfittl!
|
data/README.rdoc
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
= Spanner
|
2
|
+
|
3
|
+
Easy way to parse natural language time spans as periods expressed in seconds. Supports float point notions of spans as well.
|
4
|
+
|
5
|
+
== Usage
|
6
|
+
|
7
|
+
require 'spanner'
|
8
|
+
|
9
|
+
Spanner.parse('1s')
|
10
|
+
=> 1
|
11
|
+
|
12
|
+
Spanner.parse('23 hours 12 minutes')
|
13
|
+
=> 83520
|
14
|
+
|
15
|
+
Spanner.format(83520)
|
16
|
+
=> '23 hours 12 minutes'
|
17
|
+
|
18
|
+
== Authors
|
19
|
+
|
20
|
+
Original author:
|
21
|
+
Joshua Hull <joshbuddy@gmail.com>
|
22
|
+
|
23
|
+
Newer contributions:
|
24
|
+
Lukas Fittl <lukas@fittl.com>
|
data/Rakefile
ADDED
@@ -0,0 +1,28 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'rake'
|
3
|
+
|
4
|
+
require 'rspec/core/rake_task'
|
5
|
+
$:.push File.expand_path("../lib", __FILE__)
|
6
|
+
|
7
|
+
task :default => :spec
|
8
|
+
|
9
|
+
RSpec::Core::RakeTask.new(:spec)
|
10
|
+
|
11
|
+
begin
|
12
|
+
require 'jeweler'
|
13
|
+
Jeweler::Tasks.new do |s|
|
14
|
+
s.name = "spanner-lfittl"
|
15
|
+
s.description = s.summary = "Natural language time span parsing & formatting"
|
16
|
+
s.email = "lukas@fittl.com"
|
17
|
+
s.homepage = "http://github.com/lfittl/spanner"
|
18
|
+
s.authors = ["Lukas Fittl", "Joshua Hull"]
|
19
|
+
s.files = FileList["[A-Z]*", "{lib,spec}/**/*"]
|
20
|
+
s.add_dependency 'activesupport'
|
21
|
+
s.add_development_dependency 'jeweler'
|
22
|
+
s.add_development_dependency 'rake'
|
23
|
+
s.add_development_dependency 'rspec'
|
24
|
+
end
|
25
|
+
Jeweler::GemcutterTasks.new
|
26
|
+
rescue LoadError
|
27
|
+
puts "Jeweler not available. Run 'bundle install'."
|
28
|
+
end
|
data/VERSION
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
0.0.3
|
data/lib/spanner.rb
ADDED
@@ -0,0 +1,115 @@
|
|
1
|
+
require 'date'
|
2
|
+
require 'active_support/inflector'
|
3
|
+
|
4
|
+
class Spanner
|
5
|
+
|
6
|
+
ParseError = Class.new(RuntimeError)
|
7
|
+
|
8
|
+
def self.parse(str, opts = nil)
|
9
|
+
Spanner.new(opts).parse(str)
|
10
|
+
end
|
11
|
+
|
12
|
+
def self.format(distance, opts = nil)
|
13
|
+
Spanner.new(opts).format(distance)
|
14
|
+
end
|
15
|
+
|
16
|
+
attr_reader :value, :raise_on_error, :from
|
17
|
+
|
18
|
+
def initialize(opts)
|
19
|
+
@value = value
|
20
|
+
@on_error = opts && opts.key?(:on_error) ? opts[:on_error] : :raise
|
21
|
+
@length_of_month = opts && opts[:length_of_month]
|
22
|
+
|
23
|
+
@from = if opts && opts.key?(:from)
|
24
|
+
case opts[:from]
|
25
|
+
when :now
|
26
|
+
Time.new.to_i
|
27
|
+
else
|
28
|
+
opts[:from].to_i
|
29
|
+
end
|
30
|
+
else
|
31
|
+
0
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def self.days_in_month(year, month)
|
36
|
+
(Date.new(year, 12, 31) << (12-month)).day
|
37
|
+
end
|
38
|
+
|
39
|
+
def length_of_month
|
40
|
+
@length_of_month ||= Spanner.parse("#{Spanner.days_in_month(Time.new.year, Time.new.month)} days")
|
41
|
+
end
|
42
|
+
|
43
|
+
def error(err)
|
44
|
+
if on_error == :raise
|
45
|
+
raise ParseError.new(err)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def parse(value)
|
50
|
+
parts = []
|
51
|
+
part_contextualized = nil
|
52
|
+
value.to_s.scan(/[\+\-]?(?:\d*\.\d+|\d+)|[a-z]+/i).each do |part|
|
53
|
+
part_as_float = Float(part) rescue nil
|
54
|
+
if part_as_float
|
55
|
+
parts << part_as_float
|
56
|
+
part_contextualized = nil
|
57
|
+
else
|
58
|
+
if part_contextualized
|
59
|
+
error "Part has already been contextualized with #{part_contextualized}"
|
60
|
+
return nil
|
61
|
+
end
|
62
|
+
|
63
|
+
if parts.empty?
|
64
|
+
parts << 1
|
65
|
+
end
|
66
|
+
|
67
|
+
# part is context
|
68
|
+
multiplier = case part
|
69
|
+
when 's', 'sec', 'second', 'seconds' then 1
|
70
|
+
when 'h', 'hour', 'hours', 'hrs' then 3600
|
71
|
+
when 'm', 'min', 'minute', 'minutes' then 60
|
72
|
+
when 'd', 'day', 'days' then 86_400
|
73
|
+
when 'w', 'wks', 'week', 'weeks' then 604_800
|
74
|
+
when 'months', 'month', 'M' then length_of_month
|
75
|
+
when 'years', 'year', 'y' then 31_556_926
|
76
|
+
when /\As/ then 1
|
77
|
+
when /\Am/ then 60
|
78
|
+
when /\Ah/ then 3600
|
79
|
+
when /\Ad/ then 86_400
|
80
|
+
when /\Aw/ then 604_800
|
81
|
+
when /\AM/ then length_of_month
|
82
|
+
when /\Ay/ then 31_556_926
|
83
|
+
end
|
84
|
+
|
85
|
+
part_contextualized = part
|
86
|
+
parts << (parts.pop * multiplier) if multiplier
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
if parts.empty?
|
91
|
+
nil
|
92
|
+
else
|
93
|
+
value = parts.inject(from) {|s, p| s += p}
|
94
|
+
value.ceil == value ? value.ceil : value
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
def format(distance)
|
99
|
+
parts = {}
|
100
|
+
parts[:years], distance = distance.divmod(31_556_926)
|
101
|
+
parts[:months], distance = distance.divmod(length_of_month)
|
102
|
+
parts[:weeks], distance = distance.divmod(604_800)
|
103
|
+
parts[:days], distance = distance.divmod(86_400)
|
104
|
+
parts[:hours], distance = distance.divmod(3600)
|
105
|
+
parts[:minutes], parts[:seconds] = distance.divmod(60)
|
106
|
+
|
107
|
+
output = []
|
108
|
+
parts.each do |name, value|
|
109
|
+
next if value == 0
|
110
|
+
name = name.to_s.singularize if value.between?(-1, 1)
|
111
|
+
output << "%d %s" % [value, name]
|
112
|
+
end
|
113
|
+
output.join(" ")
|
114
|
+
end
|
115
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Spanner do
|
4
|
+
|
5
|
+
it "should return nil for empty strings" do
|
6
|
+
Spanner.parse('').should be_nil
|
7
|
+
end
|
8
|
+
|
9
|
+
it "should assume seconds" do
|
10
|
+
Spanner.parse('1').should == 1
|
11
|
+
Spanner.parse(1).should == 1
|
12
|
+
end
|
13
|
+
|
14
|
+
#simple
|
15
|
+
{ '.5s' => 0.5, '1s' => 1, '1.5s' => 1.5, '1m' => 60, '1.5m' => 90, '1d' => 86400, '1.7233312d' => 148895.81568, '1M' => Spanner.days_in_month(Time.new.year, Time.new.month) * 24 * 60 * 60 }.each do |input, output|
|
16
|
+
it "should parse #{input} and return #{output}" do
|
17
|
+
Spanner.parse(input).should == output
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
#complex
|
22
|
+
{ '1m23s' => 83, '3h20min' => 3600*3+20*60 }.each do |input, output|
|
23
|
+
it "should parse #{input} and return #{output}" do
|
24
|
+
Spanner.parse(input).should == output
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
#singular
|
29
|
+
{ '1 day' => 3600*24, '5 day' => 3600*24*5, '1 hour' => 3600 }.each do |input, output|
|
30
|
+
it "should parse #{input} and return #{output}" do
|
31
|
+
Spanner.parse(input).should == output
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
it "should let you set the length of a month" do
|
36
|
+
Spanner.parse("4 months", :length_of_month => 1234).should == 4936
|
37
|
+
end
|
38
|
+
|
39
|
+
it "should accept time as from option" do
|
40
|
+
now = Time.new
|
41
|
+
Spanner.parse('23s', :from => now).should == now.to_i + 23
|
42
|
+
end
|
43
|
+
|
44
|
+
it "should accept special :now as from option" do
|
45
|
+
Spanner.parse('23s', :from => :now).should == Time.new.to_i + 23
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
describe Spanner, "format" do
|
50
|
+
{ 3600*24 => '1 day', 3600*24*5 => '5 days', 3620 => '1 hour 20 seconds', 175814631 => '5 years 6 months 3 weeks 1 day 16 hours 20 minutes 1 second' }.each do |input, output|
|
51
|
+
it "should format #{input} as #{output}" do
|
52
|
+
Spanner.format(input).should == output
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
data/spec/spec.opts
ADDED
data/spec/spec_helper.rb
ADDED
metadata
ADDED
@@ -0,0 +1,176 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: spanner-lfittl
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.3
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Lukas Fittl
|
9
|
+
- Joshua Hull
|
10
|
+
autorequire:
|
11
|
+
bindir: bin
|
12
|
+
cert_chain: []
|
13
|
+
date: 2012-08-07 00:00:00.000000000Z
|
14
|
+
dependencies:
|
15
|
+
- !ruby/object:Gem::Dependency
|
16
|
+
name: spanner-lfittl
|
17
|
+
requirement: &2152789000 !ruby/object:Gem::Requirement
|
18
|
+
none: false
|
19
|
+
requirements:
|
20
|
+
- - ! '>='
|
21
|
+
- !ruby/object:Gem::Version
|
22
|
+
version: '0'
|
23
|
+
type: :runtime
|
24
|
+
prerelease: false
|
25
|
+
version_requirements: *2152789000
|
26
|
+
- !ruby/object:Gem::Dependency
|
27
|
+
name: jeweler
|
28
|
+
requirement: &2152788380 !ruby/object:Gem::Requirement
|
29
|
+
none: false
|
30
|
+
requirements:
|
31
|
+
- - ! '>='
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: *2152788380
|
37
|
+
- !ruby/object:Gem::Dependency
|
38
|
+
name: rake
|
39
|
+
requirement: &2152787660 !ruby/object:Gem::Requirement
|
40
|
+
none: false
|
41
|
+
requirements:
|
42
|
+
- - ! '>='
|
43
|
+
- !ruby/object:Gem::Version
|
44
|
+
version: '0'
|
45
|
+
type: :development
|
46
|
+
prerelease: false
|
47
|
+
version_requirements: *2152787660
|
48
|
+
- !ruby/object:Gem::Dependency
|
49
|
+
name: rspec
|
50
|
+
requirement: &2152787160 !ruby/object:Gem::Requirement
|
51
|
+
none: false
|
52
|
+
requirements:
|
53
|
+
- - ! '>='
|
54
|
+
- !ruby/object:Gem::Version
|
55
|
+
version: '0'
|
56
|
+
type: :development
|
57
|
+
prerelease: false
|
58
|
+
version_requirements: *2152787160
|
59
|
+
- !ruby/object:Gem::Dependency
|
60
|
+
name: jeweler
|
61
|
+
requirement: &2152786500 !ruby/object:Gem::Requirement
|
62
|
+
none: false
|
63
|
+
requirements:
|
64
|
+
- - ! '>='
|
65
|
+
- !ruby/object:Gem::Version
|
66
|
+
version: '0'
|
67
|
+
type: :development
|
68
|
+
prerelease: false
|
69
|
+
version_requirements: *2152786500
|
70
|
+
- !ruby/object:Gem::Dependency
|
71
|
+
name: rake
|
72
|
+
requirement: &2152785780 !ruby/object:Gem::Requirement
|
73
|
+
none: false
|
74
|
+
requirements:
|
75
|
+
- - ! '>='
|
76
|
+
- !ruby/object:Gem::Version
|
77
|
+
version: '0'
|
78
|
+
type: :development
|
79
|
+
prerelease: false
|
80
|
+
version_requirements: *2152785780
|
81
|
+
- !ruby/object:Gem::Dependency
|
82
|
+
name: rspec
|
83
|
+
requirement: &2152784800 !ruby/object:Gem::Requirement
|
84
|
+
none: false
|
85
|
+
requirements:
|
86
|
+
- - ! '>='
|
87
|
+
- !ruby/object:Gem::Version
|
88
|
+
version: '0'
|
89
|
+
type: :development
|
90
|
+
prerelease: false
|
91
|
+
version_requirements: *2152784800
|
92
|
+
- !ruby/object:Gem::Dependency
|
93
|
+
name: activesupport
|
94
|
+
requirement: &2152784200 !ruby/object:Gem::Requirement
|
95
|
+
none: false
|
96
|
+
requirements:
|
97
|
+
- - ! '>='
|
98
|
+
- !ruby/object:Gem::Version
|
99
|
+
version: '0'
|
100
|
+
type: :runtime
|
101
|
+
prerelease: false
|
102
|
+
version_requirements: *2152784200
|
103
|
+
- !ruby/object:Gem::Dependency
|
104
|
+
name: jeweler
|
105
|
+
requirement: &2152783360 !ruby/object:Gem::Requirement
|
106
|
+
none: false
|
107
|
+
requirements:
|
108
|
+
- - ! '>='
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: '0'
|
111
|
+
type: :development
|
112
|
+
prerelease: false
|
113
|
+
version_requirements: *2152783360
|
114
|
+
- !ruby/object:Gem::Dependency
|
115
|
+
name: rake
|
116
|
+
requirement: &2152782160 !ruby/object:Gem::Requirement
|
117
|
+
none: false
|
118
|
+
requirements:
|
119
|
+
- - ! '>='
|
120
|
+
- !ruby/object:Gem::Version
|
121
|
+
version: '0'
|
122
|
+
type: :development
|
123
|
+
prerelease: false
|
124
|
+
version_requirements: *2152782160
|
125
|
+
- !ruby/object:Gem::Dependency
|
126
|
+
name: rspec
|
127
|
+
requirement: &2152780400 !ruby/object:Gem::Requirement
|
128
|
+
none: false
|
129
|
+
requirements:
|
130
|
+
- - ! '>='
|
131
|
+
- !ruby/object:Gem::Version
|
132
|
+
version: '0'
|
133
|
+
type: :development
|
134
|
+
prerelease: false
|
135
|
+
version_requirements: *2152780400
|
136
|
+
description: Natural language time span parsing & formatting
|
137
|
+
email: lukas@fittl.com
|
138
|
+
executables: []
|
139
|
+
extensions: []
|
140
|
+
extra_rdoc_files:
|
141
|
+
- README.rdoc
|
142
|
+
files:
|
143
|
+
- Gemfile
|
144
|
+
- Gemfile.lock
|
145
|
+
- README.rdoc
|
146
|
+
- Rakefile
|
147
|
+
- VERSION
|
148
|
+
- lib/spanner.rb
|
149
|
+
- spec/spanner_spec.rb
|
150
|
+
- spec/spec.opts
|
151
|
+
- spec/spec_helper.rb
|
152
|
+
homepage: http://github.com/lfittl/spanner
|
153
|
+
licenses: []
|
154
|
+
post_install_message:
|
155
|
+
rdoc_options: []
|
156
|
+
require_paths:
|
157
|
+
- lib
|
158
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
159
|
+
none: false
|
160
|
+
requirements:
|
161
|
+
- - ! '>='
|
162
|
+
- !ruby/object:Gem::Version
|
163
|
+
version: '0'
|
164
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
165
|
+
none: false
|
166
|
+
requirements:
|
167
|
+
- - ! '>='
|
168
|
+
- !ruby/object:Gem::Version
|
169
|
+
version: '0'
|
170
|
+
requirements: []
|
171
|
+
rubyforge_project:
|
172
|
+
rubygems_version: 1.8.15
|
173
|
+
signing_key:
|
174
|
+
specification_version: 3
|
175
|
+
summary: Natural language time span parsing & formatting
|
176
|
+
test_files: []
|