Skip to content

Commit

Permalink
refactor, cleen up, iso8601
Browse files Browse the repository at this point in the history
  • Loading branch information
Paxa committed Nov 22, 2011
1 parent 09be2cb commit 40195c6
Show file tree
Hide file tree
Showing 4 changed files with 121 additions and 84 deletions.
1 change: 1 addition & 0 deletions Gemfile
Expand Up @@ -3,6 +3,7 @@ source "http://rubygems.org"
gem "numerizer", "~> 0.1.1"

group :development do
gem 'activesupport', '3.1.3'
gem "rspec", "~> 2.3.0"
gem "bundler", "~> 1.0.0"
gem "jeweler", "~> 1.5.2"
Expand Down
4 changes: 4 additions & 0 deletions Gemfile.lock
@@ -1,12 +1,15 @@
GEM
remote: http://rubygems.org/
specs:
activesupport (3.1.3)
multi_json (~> 1.0)
diff-lcs (1.1.2)
git (1.2.5)
jeweler (1.5.2)
bundler (~> 1.0.0)
git (>= 1.2.5)
rake
multi_json (1.0.3)
numerizer (0.1.1)
rake (0.8.7)
rcov (0.9.9)
Expand All @@ -23,6 +26,7 @@ PLATFORMS
ruby

DEPENDENCIES
activesupport (= 3.1.3)
bundler (~> 1.0.0)
jeweler (~> 1.5.2)
numerizer (~> 0.1.1)
Expand Down
138 changes: 70 additions & 68 deletions lib/chronic_duration.rb
@@ -1,5 +1,43 @@
require 'numerizer' unless defined?(Numerizer)
module ChronicDuration
FORMATS = {
:micro => {
:names => {:years => 'y', :months => 'm', :days => 'd', :hours => 'h', :minutes => 'm', :seconds => 's'},
:joiner => ''
},

:short => {
:names => {:years => 'y', :months => 'm', :days => 'd', :hours => 'h', :minutes => 'm', :seconds => 's'}
},

:default => {
:names => {:years => ' yr', :months => ' mo', :days => ' day', :hours => ' hr', :minutes => ' min', :seconds => ' sec',
:pluralize => true}
},

:long => {
:names => {:years => ' year', :months => ' month', :days => ' day', :hours => ' hour', :minutes => ' minute', :seconds => ' second',
:pluralize => true}
},

:chrono => {
:names => {:years => ':', :months => ':', :days => ':', :hours => ':', :minutes => ':', :seconds => ':', :keep_zero => true},
:joiner => '',
:process => lambda do |str|
# Pad zeros
# Get rid of lead off times if they are zero
# Get rid of lead off zero
# Get rid of trailing :
str.gsub(/\b\d\b/) { |d| ("%02d" % d) }.gsub(/^(00:)+/, '').gsub(/^0/, '').gsub(/:$/, '')
end
},

:iso8601 => {
:names => {:years => 'Y', :months => 'M', :days => 'D', :hours => 'H', :minutes => 'M', :seconds => 'S'},
:joiner => '',
:process => lambda {|str| "P#{str}" }
}
}
extend self

class DurationParseError < StandardError
Expand All @@ -15,6 +53,14 @@ def self.raise_exceptions=(value)
@@raise_exceptions = !!value
end

@@rates = {
:minutes => 60,
:hours => 60 * 60,
:days => 60 * 60 * 24,
:months => 60 * 60 * 24 * 30,
:years => 60 * 60 * 24 * 365.25
}

# Given a string representation of elapsed time,
# return an integer (or float, if fractions of a
# second are input)
Expand All @@ -26,90 +72,51 @@ def parse(string, opts = {})
# Given an integer and an optional format,
# returns a formatted string representing elapsed time
def output(seconds, opts = {})
date = { :years => 0, :months => 0, :days => 0, :hours => 0, :minutes => 0 }

opts[:format] ||= :default

years = months = days = hours = minutes = 0

decimal_places = seconds.to_s.split('.').last.length if seconds.is_a?(Float)

if seconds >= 60
minutes = (seconds / 60).to_i
seconds = seconds % 60
if minutes >= 60
hours = (minutes / 60).to_i
minutes = (minutes % 60).to_i
if hours >= 24
days = (hours / 24).to_i
hours = (hours % 24).to_i
if days >= 30
months = (days / 30).to_i
days = (days % 30).to_i
if months >= 12
years = (months / 12).to_i
months = (months % 12).to_i
end
end
end
end
# drop tail zero (5.0 => 5)
if seconds.is_a?(Float) && seconds % 1 == 0.0
seconds = seconds.to_i
end

joiner = ' '
process = nil
decimal_places = seconds.to_s.split('.').last.length if seconds.is_a?(Float)

case opts[:format]
when :micro
dividers = {
:years => 'y', :months => 'm', :days => 'd', :hours => 'h', :minutes => 'm', :seconds => 's' }
joiner = ''
when :short
dividers = {
:years => 'y', :months => 'm', :days => 'd', :hours => 'h', :minutes => 'm', :seconds => 's' }
when :default
dividers = {
:years => ' yr', :months => ' mo', :days => ' day', :hours => ' hr', :minutes => ' min', :seconds => ' sec',
:pluralize => true }
when :long
dividers = {
:years => ' year', :months => ' month', :days => ' day', :hours => ' hour', :minutes => ' minute', :seconds => ' second',
:pluralize => true }
when :chrono
dividers = {
:years => ':', :months => ':', :days => ':', :hours => ':', :minutes => ':', :seconds => ':', :keep_zero => true }
process = lambda do |str|
# Pad zeros
# Get rid of lead off times if they are zero
# Get rid of lead off zero
# Get rid of trailing :
str.gsub(/\b\d\b/) { |d| ("%02d" % d) }.gsub(/^(00:)+/, '').gsub(/^0/, '').gsub(/:$/, '')
end
joiner = ''
@@rates.to_a.sort_by(&:last).reverse.each do |key, value|
date[key] = (seconds / value).to_i
seconds = seconds % value
end
date[:seconds] = seconds

format_info = FORMATS[opts[:format]] || FORMATS[:default]
dividers = format_info[:names]
joiner = format_info[:joiner] || ' '
process = format_info[:joiner] || nil

result = []
[:years, :months, :days, :hours, :minutes, :seconds].each do |t|
num = eval(t.to_s)
num = ("%.#{decimal_places}f" % num) if num.is_a?(Float) && t == :seconds
num = date[t]
num = ("%.#{decimal_places}f" % num) if num.is_a?(Float) && t == :seconds
result << humanize_time_unit( num, dividers[t], dividers[:pluralize], dividers[:keep_zero] )
end

result = result.join(joiner).squeeze(' ').strip

if process
result = process.call(result)
# insert 'T' if its iso8601 && and time is not zero
if opts[:format] == :iso8601 && !result[3..5].join.empty?
result.insert(3, 'T')
end

result = result.join(joiner).squeeze(' ').strip
result = format_info[:process].call(result) if format_info[:process]

result.length == 0 ? nil : result

end

private

def humanize_time_unit(number, unit, pluralize, keep_zero)
return '' if number == 0 && !keep_zero
return '' if number.to_s == '0' && !keep_zero
res = "#{number}#{unit}"
# A poor man's pluralizer
res << 's' if !(number == 1) && pluralize
res << 's' if !(number.to_s == '1') && pluralize
res
end

Expand Down Expand Up @@ -230,9 +237,4 @@ def mappings
def join_words
['and', 'with', 'plus']
end

def white_list
self.mappings.map {|k, v| k}
end

end
62 changes: 46 additions & 16 deletions spec/chronic_duration_spec.rb
@@ -1,4 +1,6 @@
require 'chronic_duration'
require 'active_support/core_ext/numeric/time'
require 'active_support/core_ext/integer/time'

describe ChronicDuration, '.parse' do

Expand Down Expand Up @@ -60,6 +62,11 @@
it "should return nil if the input can't be parsed" do
ChronicDuration.parse('gobblygoo').should be_nil
end
min = 60
hour = 60 * min
day = 24 * hour
month = day * 30
year = (day * 365.25).to_i

@exemplars = {
#(0) =>
Expand All @@ -70,61 +77,86 @@
#:long => '0 seconds',
#:chrono => '0'
#},
(60 + 20) =>
(min + 20) =>
{
:micro => '1m20s',
:short => '1m 20s',
:default => '1 min 20 secs',
:long => '1 minute 20 seconds',
:chrono => '1:20'
:chrono => '1:20',
:iso8601 => 'PT1M20S'
},
(60 + 20.51) =>
(min + 20.51) =>
{
:micro => '1m20.51s',
:short => '1m 20.51s',
:default => '1 min 20.51 secs',
:long => '1 minute 20.51 seconds',
:chrono => '1:20.51'
:chrono => '1:20.51',
:iso8601 => 'PT1M20.51S'
},
(60 + 20.51928) =>
(min + 20.51928) =>
{
:micro => '1m20.51928s',
:short => '1m 20.51928s',
:default => '1 min 20.51928 secs',
:long => '1 minute 20.51928 seconds',
:chrono => '1:20.51928'
:chrono => '1:20.51928',
:iso8601 => 'PT1M20.51928S'
},
(4 * 3600 + 60 + 1) =>
(4 * hour + 1 * min + 1) =>
{
:micro => '4h1m1s',
:short => '4h 1m 1s',
:default => '4 hrs 1 min 1 sec',
:long => '4 hours 1 minute 1 second',
:chrono => '4:01:01'
:chrono => '4:01:01',
:iso8601 => 'PT4H1M1S'
},
(2 * 3600 + 20 * 60) =>
(2 * hour + 20 * min) =>
{
:micro => '2h20m',
:short => '2h 20m',
:default => '2 hrs 20 mins',
:long => '2 hours 20 minutes',
:chrono => '2:20'
:chrono => '2:20',
:iso8601 => 'PT2H20M'
},
(2 * 3600 + 20 * 60) =>
(2 * hour + 20 * min) =>
{
:micro => '2h20m',
:short => '2h 20m',
:default => '2 hrs 20 mins',
:long => '2 hours 20 minutes',
:chrono => '2:20:00'
:chrono => '2:20:00',
:iso8601 => 'PT2H20M'
},
(6 * 30 * 24 * 3600 + 24 * 3600) =>
(6 * month + 1 * day) =>
{
:micro => '6m1d',
:short => '6m 1d',
:default => '6 mos 1 day',
:long => '6 months 1 day',
:chrono => '6:01:00:00:00' # Yuck. FIXME
:chrono => '6:01:00:00:00', # Yuck. FIXME
:iso8601 => 'P6M1D'
},
(2*year + 6*month + 1*day + 5*hour + 20*min + 13) =>
{
:micro => '2y6m1d5h20m13s',
:short => '2y 6m 1d 5h 20m 13s',
:default => '2 yrs 6 mos 1 day 5 hrs 20 mins 13 secs', # 13 sex
:long => '2 years 6 months 1 day 5 hours 20 minutes 13 seconds',
:chrono => '2:06:01:05:20:13', # Yuck. FIXME
:iso8601 => 'P2Y6M1DT5H20M13S'
},
(2.years + 6.months + 1.day + 5.hours + 20.minutes + 13.seconds) =>
{
:micro => '2y6m1d5h20m13s',
:short => '2y 6m 1d 5h 20m 13s',
:default => '2 yrs 6 mos 1 day 5 hrs 20 mins 13 secs', # 13 sex
:long => '2 years 6 months 1 day 5 hours 20 minutes 13 seconds',
:chrono => '2:06:01:05:20:13', # Yuck. FIXME
:iso8601 => 'P2Y6M1DT5H20M13S'
}
}

Expand All @@ -139,8 +171,6 @@
it "should use the default format when the format is not specified" do
ChronicDuration.output(2 * 3600 + 20 * 60).should == '2 hrs 20 mins'
end


end


Expand Down

0 comments on commit 40195c6

Please sign in to comment.