views:

429

answers:

4

Hi,

I am trying to come up with an efficient method for truncating Ruby Time objects according to a given resolution.

class Time
  def truncate resolution
    t = to_a
    case resolution
    when :min   
      t[0] = 0
    when :hour
      t[0] = t[1] = 0
    when :day
      t[0] = t[1] = 0
      t[2] = 1
    when :month 
      t[0] = t[1] = 0
      t[2] = t[3] = 1
    when :week  
      t[0] = t[1] = 0
      t[2] = 1
      t[3] -= t[6] - 1
    when :year
      t[0] = t[1] = 0
      t[2] = t[3] = t[4] = 1
    end

    Time.local *t
  end
end

Does anyone know a faster version that achieves the same task?

+3  A: 

This has already been implemented for you in Rails' ActiveSupport library's Time extensions. See the change method, as well as the various at_beginning_of_* methods.

Greg Campbell
+1  A: 

If you don't want ActiveSupport as Greg suggests, I think you could do something like this

t = to_a
if resolution==:week
  t[0] = t[1] = 0
  t[2] = 1
  t[3] -= t[6] - 1
else
  len = [:sec, :min, :hour, :day, :month, :year].index(resolution)
  t.fill(0, 0,len)
  t.fill(1, 3,len-3)
end
Time.local *t

Don't know if it's faster though...

AShelly
+1  A: 

Thanks to both of you. Since I don't use Rails I copied the code to perform a performance benchmark.

require 'benchmark'

class Time
  def truncate resolution
    t = to_a
    case resolution
    when :min   
      t[0] = 0
    when :hour
      t[0] = t[1] = 0
    when :day
      t[0] = t[1] = t[2] = 0
    when :week  
      t[0] = t[1] = t[2] = 0
      t[3] -= t[6] - 1
    when :month 
      t[0] = t[1] = t[2] = 0
      t[3] = 1
    when :year
      t[0] = t[1] = t[2] = 0
      t[3] = t[4] = 1
    end

    Time.local *t 
  end

  def truncate2 resolution
    opts = {}

    case resolution
    when :min   
      opts[:sec] = 0
    when :hour
      opts[:sec] = opts[:min] = 0
    when :day
      opts[:sec] = opts[:min] = opts[:hour] = 0
    when :week  
      opts[:sec] = opts[:min] = opts[:hour] = 0
      opts[:day] = wday - 1 if wday != 1
    when :month 
      opts[:sec] = opts[:min] = opts[:hour] = 0
      opts[:day] = 1
    when :year
      opts[:sec] = opts[:min] = opts[:hour] = 0
      opts[:day] = opts[:month] = 1
    end

    change opts 
  end

  def truncate3 resolution
    t = to_a
    if resolution == :week
      t[0] = t[1] = 0
      t[2] = 1
      t[3] -= t[6] - 1
    else
      len = [:sec, :min, :hour, :day, :month, :year].index(resolution)
      t.fill(0, 0, len)
      t.fill(1, 3, len-3)
    end

    Time.local *t
  end    

  def change opts 
    Time.local(
      opts[:year]  || year,
      opts[:month] || month,
      opts[:day]   || day,
      opts[:hour]  || hour,
      opts[:min]   || (opts[:hour] ? 0 : min),
      opts[:sec]   || ((opts[:hour] || opts[:min]) ? 0 : sec),
    )
  end
end

Resolutions = [:sec, :min, :hour, :day, :week, :month, :year]

# Correctness check.
puts Resolutions.map { |r| "#{r}: #{Time.now.truncate r}" } << "\n"
puts Resolutions.map { |r| "#{r}: #{Time.now.truncate2 r}" } << "\n"
puts Resolutions.map { |r| "#{r}: #{Time.now.truncate3 r}" } << "\n"

n = 100000
now = Time.now

Benchmark.bm(10) do |x|
  x.report("truncate") { n.times { Resolutions.each { |r| now.truncate  r } } }
  x.report("truncate2") { n.times { Resolutions.each { |r| now.truncate2 r } } }
  x.report("truncate3") { n.times { Resolutions.each { |r| now.truncate3 r } } }
end

Benchmark.bm(10) do |x|
  Resolutions.each do |unit| 
    x.report("#{unit}") { n.times { now.truncate unit } }
    x.report("#{unit}2") { n.times { now.truncate2 unit } }
    x.report("#{unit}3") { n.times { now.truncate3 unit } }
  end
end

Here are the results:

sec: 2009-05-26 13:44:20 -0700
min: 2009-05-26 13:44:00 -0700
hour: 2009-05-26 13:00:00 -0700
day: 2009-05-26 00:00:00 -0700
week: 2009-05-25 00:00:00 -0700
month: 2009-05-01 00:00:00 -0700
year: 2008-12-31 23:00:00 -0800

sec: 2009-05-26 13:44:20 -0700
min: 2009-05-26 13:44:00 -0700
hour: 2009-05-26 13:00:00 -0700
day: 2009-05-26 00:00:00 -0700
week: 2009-05-01 00:00:00 -0700
month: 2009-05-01 00:00:00 -0700
year: 2009-01-01 00:00:00 -0800

sec: 2009-05-26 13:44:20 -0700
min: 2009-05-26 13:44:00 -0700
hour: 2009-05-26 13:00:00 -0700
day: 2009-05-26 00:00:00 -0700
week: 2009-05-25 01:00:00 -0700
month: 2009-05-01 00:00:00 -0700
year: 2008-12-31 23:00:00 -0800

                user     system      total        real
truncate    5.910000   0.020000   5.930000 (  5.947453)
truncate2   6.180000   0.020000   6.200000 (  6.232918)
truncate3   6.150000   0.020000   6.170000 (  6.253931)
                user     system      total        real
sec         0.720000   0.000000   0.720000 (  0.749040)
sec2        0.830000   0.010000   0.840000 (  0.863029)
sec3        0.800000   0.000000   0.800000 (  0.820477)
min         0.700000   0.000000   0.700000 (  0.709238)
min2        0.860000   0.010000   0.870000 (  0.860168)
min3        0.770000   0.000000   0.770000 (  0.795734)
hour        0.680000   0.000000   0.680000 (  0.705306)
hour2       0.850000   0.010000   0.860000 (  0.867235)
hour3       0.740000   0.000000   0.740000 (  0.746338)
day         0.720000   0.000000   0.720000 (  0.724324)
day2        0.890000   0.010000   0.900000 (  0.894312)
day3        0.780000   0.000000   0.780000 (  0.788007)
week        0.730000   0.000000   0.730000 (  0.736604)
week2       0.910000   0.000000   0.910000 (  0.910925)
week3       0.600000   0.000000   0.600000 (  0.611683)
month       0.720000   0.000000   0.720000 (  0.719515)
month2      0.880000   0.010000   0.890000 (  0.888045)
month3      0.780000   0.000000   0.780000 (  0.789726)
year        1.540000   0.010000   1.550000 (  1.565335)
year2       0.830000   0.000000   0.830000 (  0.849737)
year3       1.600000   0.010000   1.610000 (  1.644958)

Seems like my first truncation version is still most efficient, except for the year case. There is a small quirk in the year for truncate and truncate3 though. It is displayed as

2008-12-31 23:00:00 -0800

rather than

2009-01-01 00:00:00 -0700

Any ideas why?

Matthias Vallentin
The issue with the year case may be that Time.local with 10 args is overspecified, and I'm not sure how the Time library deals with conflicts: YDAY, WDAY are redundant, and isDst may be out of sync with "PDT" vs "PST". One way to get rid of the error is to do `Time.local t.reverse[4,6]`
AShelly
+1  A: 

You don't have to use Rails to use the ActiveSupport goodies. I ran into this exact problem in addition to wanting to use stuff like 1.day or 3.minutes or Time.now.beginning__of__day

irb(main):001:0> require 'rubygems'
=> true
irb(main):002:0> require 'activesupport'
=> true
irb(main):003:0> 1.day
=> 1 day
irb(main):004:0> 3.minutes
=> 180 seconds
irb(main):005:0> Time.now.beginning_of_day
=> Sun Jun 28 00:00:00 -0400 2009
irb(main):006:0>
ulver