periodic 1.2.3
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +11 -0
- data/.travis.yml +9 -0
- data/Gemfile +4 -0
- data/LICENSE +20 -0
- data/README.rdoc +50 -0
- data/Rakefile +10 -0
- data/lib/periodic.rb +7 -0
- data/lib/periodic/duration.rb +83 -0
- data/lib/periodic/parser.rb +59 -0
- data/lib/periodic/version.rb +3 -0
- data/periodic.gemspec +32 -0
- metadata +126 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 694acb05f38ad1b232aa218884749f8c1971989c
|
4
|
+
data.tar.gz: 5e106d42184155b580315f880c8d4c53c4030b42
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 0ea08bb314c34e90f1eef36b91ecf6e1185a55d787b40f43a389abc6c7d22e2fe5fea17fa9c9c40ee2f0aa5cbbff8ad2c216f9eb54b3f5d591173b849b92bb8a
|
7
|
+
data.tar.gz: 2bc0d3b6258d72ccb15a8da94e417d8a455078bb1738ea711d0cb550af85f09e45ea27081b12ed5518b26fa3455c78138326ac8653cd3f9d523df8f9dd29f63a
|
data/.gitignore
ADDED
data/.travis.yml
ADDED
data/Gemfile
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2016 Chris Kalafarski
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.rdoc
ADDED
@@ -0,0 +1,50 @@
|
|
1
|
+
= Periodic
|
2
|
+
|
3
|
+
|
4
|
+
== Installation
|
5
|
+
|
6
|
+
$ sudo gem sources -a http://gems.github.com
|
7
|
+
$ sudo gem install farski-periodic
|
8
|
+
|
9
|
+
== Usage
|
10
|
+
|
11
|
+
==== Parser
|
12
|
+
|
13
|
+
Periodic will parse out natural language strings representing durations using different units of time, and return the total number of seconds. When using text labels, it will do it's best to look for any of the following units: seconds, minutes, hours, days, weeks, months, years, decades, centuries, millennia. Units can appear in any order in the string, and may appear more than once.
|
14
|
+
|
15
|
+
When using a digital format (e.g. 10:30), the parser will default to the smallest sensible units (e.g. 10 minutes 30 seconds), but this can be overridden using the :bias option (e.g. :bias => centuries, 10 centuries 30 decades). Valid options for the :bias are symbols of those units mentioned in the previous paragraph. If a bias is explicitly defined that is too precise for the given string, the smallest sensible unit will be substituted
|
16
|
+
|
17
|
+
>> Periodic.parse('1 minute')
|
18
|
+
=> 60
|
19
|
+
>> Periodic.parse('60min')
|
20
|
+
=> 3600
|
21
|
+
>> Periodic.parse('1:30')
|
22
|
+
=> 90
|
23
|
+
>> Periodic.parse('1:30', :bias => :hours)
|
24
|
+
=> 5400
|
25
|
+
|
26
|
+
==== Formatting
|
27
|
+
|
28
|
+
The #format method of Periodic::Duration objects lets you format the number of seconds into different units. Any combination of units can be used to express precise values, the the precision is optioned (i.e. 90 seconds can be output as '1 minute' or '1.5 minutes', or '1 minute 30 seconds'). If you use text labels in the format, they can come either before or after the values, and in both cases the resulting string by default will have zero-value value-label pairs removed (e.g. with a value of 30, and a format of '%m minute %s seconds', by default it will print '30 seconds', not '0 minutes 30 seconds'). Pairs can be forced into the result with a '!' before the directive. When using a digital format (like the default '%y:%d:%h:%m:%s'), you should be sure to include all directives between the largest and smallest unit, though it will work even with missing directives.
|
29
|
+
|
30
|
+
|
31
|
+
The available directives
|
32
|
+
/%s/, /%m/, /%h/, /%d/, /%y/
|
33
|
+
|
34
|
+
>> Duration.new(125).format
|
35
|
+
=> '2:05'
|
36
|
+
>> Duration.new(125).format('%y:%d:!%h:%m:%s')
|
37
|
+
=> '00:02:05'
|
38
|
+
>> Duration.new(125).format('%h hours %m minutes %s seconds')
|
39
|
+
=> '2 minutes 5 seconds'
|
40
|
+
>> Duration.new(125).format('!%h hours %m minutes %s seconds')
|
41
|
+
=> '0 hours 2 minutes 5 seconds'
|
42
|
+
>> Duration.new(125).format('!%h hours %s seconds')
|
43
|
+
=> '0 hours 125 seconds'
|
44
|
+
|
45
|
+
== TODO
|
46
|
+
[ ] plural text labels should automatically be singularized with 1
|
47
|
+
|
48
|
+
== COPYRIGHT
|
49
|
+
|
50
|
+
Copyright (c) 2008 Chris Kalafarski. See LICENSE for details.
|
data/Rakefile
ADDED
data/lib/periodic.rb
ADDED
@@ -0,0 +1,83 @@
|
|
1
|
+
require 'bigdecimal'
|
2
|
+
|
3
|
+
module Periodic
|
4
|
+
module Duration
|
5
|
+
module Units
|
6
|
+
TIME = Hash.new
|
7
|
+
TIME[:seconds] = { :factor => 1, :directive => /%s/ }
|
8
|
+
TIME[:minutes] = { :factor => 60, :directive => /%m/ }
|
9
|
+
TIME[:hours] = { :factor => 3600, :directive => /%h/ }
|
10
|
+
TIME[:days] = { :factor => 3600*24, :directive => /%d/ }
|
11
|
+
# TIME[:weeks] = { :factor => 3600*24*7, :directive => /%w/ }
|
12
|
+
# TIME[:months] = { :factor => 3600*24*30, :directive => /%n/ }
|
13
|
+
TIME[:years] = { :factor => 3600*24*365, :directive => /%y/ }
|
14
|
+
|
15
|
+
TIME_ORDER = [:seconds, :minutes, :hours, :days, :years] # not working with weeks and months...
|
16
|
+
end
|
17
|
+
|
18
|
+
def self.sanitize_formatted_string(string)
|
19
|
+
if string.match(/:/) && !string.match(/[a-zA-Z ]/)
|
20
|
+
# add leading zeros where missing...
|
21
|
+
string.gsub!(/!(\d):/, '!0\1:')
|
22
|
+
string.gsub!(/^(\d):/, '0\1:')
|
23
|
+
string.gsub!(/:(\d):/, ':0\1:')
|
24
|
+
string.gsub!(/:(\d):/, ':0\1:') # needs to happen twice??
|
25
|
+
string.gsub!(/:(\d(.\d)*)$/, ':0\1')
|
26
|
+
|
27
|
+
# remove leading zero-value digitals
|
28
|
+
string.sub!(/[0:]*/, '')
|
29
|
+
else
|
30
|
+
# if the string starts with a number we can assume the value-label pairs are like '10 minutes'
|
31
|
+
if string[0,1].match(/\d/) || string[0,1] == "!"
|
32
|
+
string = string.split(/(!?\d[.\d]*[-_:, a-zA-Z]+)/).delete_if{|x| x == ""}.inject(String.new) { |memo, s| memo << ((s.match(/!/) || s.match(/[1-9]/)) ? s : "") }
|
33
|
+
|
34
|
+
# if starts with a letter we can assume the value-label pairs are like 'minutes: 10'
|
35
|
+
else
|
36
|
+
string = string.split(/([-A-Za-z: ,]+\d[.\d]*)/).delete_if{|x| x == ""}.inject(String.new) { |memo, s| memo << ((s.match(/!/) || s.match(/[1-9]/)) ? s : "") }
|
37
|
+
string.sub!(/([ ,])*([a-zA-Z]+)/, '\2')
|
38
|
+
end
|
39
|
+
|
40
|
+
# remove leading zero-value digitals
|
41
|
+
string.sub!(/[0:]*/, '')
|
42
|
+
end
|
43
|
+
string.strip.gsub(/!/, '')
|
44
|
+
end
|
45
|
+
|
46
|
+
class Duration
|
47
|
+
def initialize(seconds)
|
48
|
+
@seconds = (seconds.is_a?(Float) ? seconds.to_f : seconds)
|
49
|
+
end
|
50
|
+
|
51
|
+
def format(format = '%y:%d:%h:%m:%s', precision = nil)
|
52
|
+
string, nondirective_units, values, smallest_unit_directive = format, [], Hash.new, nil
|
53
|
+
|
54
|
+
Periodic::Duration::Units::TIME_ORDER.reverse.each_with_index do |unit, i|
|
55
|
+
if format =~ Periodic::Duration::Units::TIME[unit][:directive]
|
56
|
+
values[unit] = send(unit) + nondirective_units.inject(0) { |total, u| total += (send(u) * (Periodic::Duration::Units::TIME[u][:factor] / Periodic::Duration::Units::TIME[unit][:factor])) }
|
57
|
+
smallest_unit_directive = unit
|
58
|
+
nondirective_units.clear
|
59
|
+
else
|
60
|
+
nondirective_units << unit if (send(unit) > 0)
|
61
|
+
end
|
62
|
+
|
63
|
+
# correct for any left over time that's is fractional for all the included units
|
64
|
+
values[smallest_unit_directive] += nondirective_units.inject(0) { |total, u| total += (send(u).to_f * Periodic::Duration::Units::TIME[u][:factor] / Periodic::Duration::Units::TIME[smallest_unit_directive][:factor]) } if (!Periodic::Duration::Units::TIME_ORDER.reverse[i+1] && !nondirective_units.empty?)
|
65
|
+
end
|
66
|
+
|
67
|
+
values[smallest_unit_directive] = case precision
|
68
|
+
when nil then (values[smallest_unit_directive] % 1 == 0) && !@seconds.is_a?(Float) ? values[smallest_unit_directive].to_i : values[smallest_unit_directive]
|
69
|
+
when 0 then values[smallest_unit_directive].to_i
|
70
|
+
else (values[smallest_unit_directive] * (10 ** precision)).round / (10 ** precision).to_f
|
71
|
+
end
|
72
|
+
|
73
|
+
return Periodic::Duration.sanitize_formatted_string(values.inject(string) { |str, data| str.sub!(Periodic::Duration::Units::TIME[data[0]][:directive], data[1].to_s) })
|
74
|
+
end
|
75
|
+
|
76
|
+
Periodic::Duration::Units::TIME_ORDER.each_with_index do |unit, i|
|
77
|
+
define_method("in_" + unit.to_s) { @seconds.to_f / Periodic::Duration::Units::TIME[unit][:factor] }
|
78
|
+
define_method("whole_" + unit.to_s) { (@seconds.to_f / Periodic::Duration::Units::TIME[unit][:factor]).floor }
|
79
|
+
define_method(unit) { ((Periodic::Duration::Units::TIME_ORDER[i+1] ? BigDecimal.new(@seconds.to_f.to_s) % BigDecimal.new(Periodic::Duration::Units::TIME[Periodic::Duration::Units::TIME_ORDER[i+1]][:factor].to_f.to_s) : @seconds.to_f) / Periodic::Duration::Units::TIME[unit][:factor].to_f).send(unit == :seconds ? :to_f : :floor) }
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
module Periodic
|
2
|
+
module Parser
|
3
|
+
def parse(string, options = { :bias => :seconds})
|
4
|
+
return Parseable.new(string, options[:bias]).seconds
|
5
|
+
end
|
6
|
+
|
7
|
+
private
|
8
|
+
|
9
|
+
class Parseable
|
10
|
+
def initialize(string, bias)
|
11
|
+
@string = string
|
12
|
+
validates_inclusion_of_numeral_in_string
|
13
|
+
@bias = bias
|
14
|
+
|
15
|
+
extract_time_parts_from_string
|
16
|
+
end
|
17
|
+
|
18
|
+
def seconds
|
19
|
+
units = { :seconds => 1, :minutes => 60, :hours => 3600, :days => 3600*24, :weeks => 3600*24*7, :months => 3600*24*30, :years => 3600*24*365.25, :decades => 3600*24*365.25*10, :centuries => 3600*24*365.25*100, :millennia => 3600*24*365.25*1000 }
|
20
|
+
seconds = @time_parts.inject(0) { |total, part| total = total + (part[1] * units[part[0]]) }
|
21
|
+
return seconds % 1 == 0 ? seconds.to_i : seconds
|
22
|
+
end
|
23
|
+
|
24
|
+
private
|
25
|
+
|
26
|
+
def validates_inclusion_of_numeral_in_string
|
27
|
+
raise ArgumentError, "String contains no numbers", caller unless @string.match(/\d/)
|
28
|
+
end
|
29
|
+
|
30
|
+
def digital?
|
31
|
+
@string.match(/:/)
|
32
|
+
end
|
33
|
+
|
34
|
+
def extract_time_parts_from_string
|
35
|
+
@time_parts = Hash.new
|
36
|
+
digital? ? extract_time_parts_from_digital : extract_time_parts_from_text
|
37
|
+
end
|
38
|
+
|
39
|
+
def extract_time_parts_from_digital
|
40
|
+
units = [:seconds, :minutes, :hours, :days, :weeks, :months, :years, :decades, :centuries, :millennia]
|
41
|
+
@string.split(":").reverse.each_with_index do |part, i|
|
42
|
+
@time_parts[units[i + ((units.index(@bias) >= @string.split(":").size) ? units.index(@bias) - @string.split(":").size + 1 : 0)]] = part.to_f
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def extract_time_parts_from_text
|
47
|
+
normalize_string
|
48
|
+
units = { :s => :seconds, :m => :minutes, :h => :hours, :d => :days, :w => :weeks, :n => :months, :y => :years, :a => :decades, :c => :centuries, :b => :millennia }
|
49
|
+
@string.split(' ').each { |part| @time_parts[part.match(/([a-z])/) ? units[part.match(/([a-z])/)[1].to_sym] : @bias] = (@time_parts[part.match(/([a-z])/) ? units[part.match(/([a-z])/)[1].to_sym] : @bias]||0) + part.to_f }
|
50
|
+
end
|
51
|
+
|
52
|
+
def normalize_string
|
53
|
+
[/( )/, /(,)/, /(and)/].each{ |m| @string.gsub!(m, '') }
|
54
|
+
@string.gsub!(/(\d)([a-zA-Z]+)/, '\1\2 ')
|
55
|
+
[{:n=>/(mo\w*)/,:b=>/(m\w*l\w*)/,:a=>/(d\w*c\w*)/}, {:m=>/(m\w*)/,:h=>/(h\w*)/,:d=>/(d\w*)/,:w=>/(w\w*)/,:y=>/(y\w*)/,:c=>/(c\w*)/}, {:s=>/(s\w*)/}].each { |set| set.each{ |k,v| @string.gsub!(v, k.to_s) } }
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
data/periodic.gemspec
ADDED
@@ -0,0 +1,32 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path("../lib", __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require "periodic/version"
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "periodic"
|
8
|
+
spec.version = Periodic::VERSION
|
9
|
+
spec.authors = ["Chris Kalafarski"]
|
10
|
+
spec.email = ["chris@farski.com"]
|
11
|
+
|
12
|
+
spec.summary = "Natural language parser and output formating for durations"
|
13
|
+
spec.description = "Natural language parser and output formating for durations in Ruby"
|
14
|
+
spec.homepage = "https://github.com/farski/periodic"
|
15
|
+
spec.license = "MIT"
|
16
|
+
|
17
|
+
spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
|
18
|
+
spec.bindir = "exe"
|
19
|
+
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
20
|
+
spec.require_paths = ["lib"]
|
21
|
+
spec.required_ruby_version = '~> 1.9'
|
22
|
+
|
23
|
+
if spec.respond_to?(:metadata)
|
24
|
+
spec.metadata["allowed_push_host"] = "https://rubygems.org"
|
25
|
+
end
|
26
|
+
|
27
|
+
spec.add_development_dependency "bundler", "~> 1.8"
|
28
|
+
spec.add_development_dependency "test-unit", "~> 2.5"
|
29
|
+
spec.add_development_dependency "rake", "~> 10.0"
|
30
|
+
spec.add_development_dependency "coveralls", "~> 0"
|
31
|
+
spec.add_development_dependency "rubocop", "~> 0"
|
32
|
+
end
|
metadata
ADDED
@@ -0,0 +1,126 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: periodic
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 1.2.3
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Chris Kalafarski
|
8
|
+
autorequire:
|
9
|
+
bindir: exe
|
10
|
+
cert_chain: []
|
11
|
+
date: 2016-01-13 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: bundler
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '1.8'
|
20
|
+
type: :development
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '1.8'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: test-unit
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '2.5'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '2.5'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: rake
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '10.0'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '10.0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: coveralls
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - "~>"
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - "~>"
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '0'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: rubocop
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - "~>"
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '0'
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - "~>"
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '0'
|
83
|
+
description: Natural language parser and output formating for durations in Ruby
|
84
|
+
email:
|
85
|
+
- chris@farski.com
|
86
|
+
executables: []
|
87
|
+
extensions: []
|
88
|
+
extra_rdoc_files: []
|
89
|
+
files:
|
90
|
+
- ".gitignore"
|
91
|
+
- ".travis.yml"
|
92
|
+
- Gemfile
|
93
|
+
- LICENSE
|
94
|
+
- README.rdoc
|
95
|
+
- Rakefile
|
96
|
+
- lib/periodic.rb
|
97
|
+
- lib/periodic/duration.rb
|
98
|
+
- lib/periodic/parser.rb
|
99
|
+
- lib/periodic/version.rb
|
100
|
+
- periodic.gemspec
|
101
|
+
homepage: https://github.com/farski/periodic
|
102
|
+
licenses:
|
103
|
+
- MIT
|
104
|
+
metadata:
|
105
|
+
allowed_push_host: https://rubygems.org
|
106
|
+
post_install_message:
|
107
|
+
rdoc_options: []
|
108
|
+
require_paths:
|
109
|
+
- lib
|
110
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
111
|
+
requirements:
|
112
|
+
- - "~>"
|
113
|
+
- !ruby/object:Gem::Version
|
114
|
+
version: '1.9'
|
115
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
116
|
+
requirements:
|
117
|
+
- - ">="
|
118
|
+
- !ruby/object:Gem::Version
|
119
|
+
version: '0'
|
120
|
+
requirements: []
|
121
|
+
rubyforge_project:
|
122
|
+
rubygems_version: 2.5.1
|
123
|
+
signing_key:
|
124
|
+
specification_version: 4
|
125
|
+
summary: Natural language parser and output formating for durations
|
126
|
+
test_files: []
|