spanner-lfittl 0.0.3
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.
- 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: []
|