ActiveSupport::Duration incorrectly calculates interval? - ruby-on-rails

I have a very strange feeling that I am getting incorrect duration calculated by ActiveSupport::Duration. Here is the essence of the code I have
require 'time'
require 'active_support/duration'
require 'active_support/gem_version'
a = Time.parse('2044-11-18 01:00:00 -0600')
b = Time.parse('2045-03-05 04:00:00 -0600')
ActiveSupport::Duration.build(b - a).inspect
ActiveSupport.gem_version
And here is what I get
[30] pry(main)> require 'time'
=> false
[31] pry(main)> require 'active_support/duration'
=> false
[32] pry(main)> require 'active_support/gem_version'
=> false
[33] pry(main)> a = Time.parse('2044-11-18 01:00:00 -0600')
=> 2044-11-18 01:00:00 -0600
[34] pry(main)> b = Time.parse('2045-03-05 04:00:00 -0600')
=> 2045-03-05 04:00:00 -0600
[35] pry(main)> ActiveSupport::Duration.build(b - a).inspect
=> "3 months, 2 weeks, 1 day, 19 hours, 32 minutes, and 42.0 seconds"
[36] pry(main)> ActiveSupport.gem_version
=> Gem::Version.new("6.0.1")
I cross-checked the result with PostgreSQL
select justify_interval('2045-03-05 04:00:00 -0600'::timestamp - '2044-11-18 01:00:00 -0600'::timestamp)
and got 3 mons 17 days 03:00:00 (or 107 days and 3 hours). Also there is a web site that gives result consistent with PostgreSQL (although web page says 107 days are 3 months and 15 days).
Am I missing something? Where minutes and seconds are coming from? Is there a better interval calculator for Ruby/Rails?
Update
distance_of_time_in_words returns 4 months!
Update 2
I ended up with slightly modified Wizard's solution wrapped up to produce text
def nice_duration(seconds)
parts = duration_in_whms(seconds)
out = []
I18n.with_options(scope: 'datetime.distance_in_words') do |locale|
out.push locale.t(:x_days, count: parts[:days]) if parts.key?(:days)
out.push locale.t(:x_hours, count: parts[:hours]) if parts.key?(:hours)
out.push locale.t(:x_minutes, count: parts[:minutes]) if parts.key?(:minutes)
end
out.join ' '
end
private
def duration_in_whms(seconds)
parts_and_seconds_in_part = {:days => 86400, :hours => 3600, :minutes => 60}
result = {}
remainder = seconds
parts_and_seconds_in_part.each do |k, v|
out = (remainder / v).to_i
result[k] = out if out.positive?
remainder -= out * v
end
result.merge(seconds: remainder)
end
Apparently localization from Action View does not have hours without about. So I also had to add corresponding translation into my locales
en:
datetime:
distance_in_words:
x_hours:
one: "1 hour"
other: "%{count} hours"

ActiveSupport::Duration calculates its value using the following constants and algorithm (I have added the explanation on what it's doing below but here is a link to the source). As you can see below, the SECONDS_PER_YEAR constant is the average number of seconds in the gregorian calendar (which is then used to define SECONDS_PER_MONTH). It is because of this, "average definition" of SECONDS_PER_YEAR and SECONDS_PER_MONTH that you are getting the unexpected hours, minutes and seconds. It is defined as an average because a month and year is not a standard fixed amount of time.
SECONDS_PER_MINUTE = 60
SECONDS_PER_HOUR = 3600
SECONDS_PER_DAY = 86400
SECONDS_PER_WEEK = 604800
SECONDS_PER_MONTH = 2629746 # This is 1/12 of a Gregorian year
SECONDS_PER_YEAR = 31556952 # The length of a Gregorian year = 365.2425 days
# You pass ActiveSupport::Duration the number of seconds (b-a) = 9255600.0 seconds
remainder_seconds = 9255600.0
# Figure out how many years fit into the seconds using integer division.
years = (remainder_seconds/SECONDS_PER_YEAR).to_i # => 0
# Subtract the amount of years from the remaining_seconds
remainder_seconds -= years * SECONDS_PER_YEAR # => 9255600.0
months = (remainder_seconds/SECONDS_PER_MONTH).to_i # => 3
remainder_seconds -= months * SECONDS_PER_MONTH # => 1366362.0
weeks = (remainder_seconds/SECONDS_PER_WEEK).to_i # => 2
remainder_seconds -= weeks * SECONDS_PER_WEEK # => 156762.0
days = (remainder_seconds/SECONDS_PER_DAY).to_i # => 1
remainder_seconds -= days * SECONDS_PER_DAY # => 70362.0
hours = (remainder_seconds/SECONDS_PER_HOUR).to_i # => 19
remainder_seconds -= hours * SECONDS_PER_HOUR # => 1962.0
minutes = (remainder_seconds/SECONDS_PER_MINUTE).to_i # => 32
remainder_seconds -= minutes * SECONDS_PER_MINUTE # => 42
seconds = remainder_seconds # => 42
puts "#{years} years, #{months} months, #{weeks} weeks, #{days} days, #{hours} hours, #{minutes} minutes, #{seconds} seconds"
# 0 years, 3 months, 2 weeks, 1 days, 19 hours, 32 minutes, 42.0 seconds
To avoid the issue you are having, I would suggest to just represent the time in week, days, hours, minutes and seconds (basically anything excluding month & year).
The number of seconds in a month is complicated if you don't use an average since you will need to account for 28, 29, 30 and 31 days for each separate month. Similarly, for the year, you will need to account for leap/non-leap if you don't use the average.
I am not sure of any gems around which do this for you, however I can provide a function which can help you calculate the duration in days, hours, minutes and seconds below.
def duration_in_whms(seconds)
parts_and_seconds_in_part = {:weeks => 604800, :days => 86400, :hours => 3600, :minutes => 60}
result = {}
remainder = seconds
parts_and_seconds_in_part.each do |k, v|
result[k] = (remainder/v).to_i
remainder -= result[k]*v
end
result.merge(seconds: remainder)
end
duration_in_whms(9255600) => # {:weeks=>15, :days=>2, :hours=>3, :minutes=>0, :seconds=>0.0}

Related

Ruby: Calculate time difference between 2 times

I want to calculate the difference between 2 times.
start_time: 22:00 (Rails interprets this as 2015-12-31 22:00:00 +0100)
second_time: 02:00 (Rails interprets this as 2015-12-31 02:00:00 +0100). The second time is 4 hours later, so in the next day. Is there a way to calculate this difference?
I can not simply do this: second_time - first_time, because this gives me a difference of 22 hours instead of 4 hours.
Edit:
Some background information:
A job is starting at 22:00 and ending the next day at 02:00. Because i fill in the form of this job only times, this times for the above 2 values are 2015-12-31 22:00:00 +0100 and 2015-12-31 02:00:00 +0100. I don't want the user to fill in the time including the date. The real difference between the times should be 4 hours.
So what i actually want is calculate the difference between 22:00 and 02:00 (in the next day).
I don't understand why you think it should return 4 hours or why it does return 22 hours. 20 hours would be correct for your example:
require 'time'
a = Time.parse('2015-12-31 22:00:00 +0100')
b = Time.parse('2015-12-31 02:00:00 +0100')
a - b
#=> 72000.0 # difference in seconds
(a - b) / 3600
#=> 20.0 # difference in hours
Update: It seems like you are dealing only with the time portion and not with the actual date. And I assume the maximum difference you will have to deal with is 24 hours:
def time_difference(time_a, time_b)
difference = time_b - time_a
if difference > 0
difference
else
24 * 3600 + difference
end
end
a = Time.parse('2015-12-31 22:00:00 +0100')
b = Time.parse('2015-12-31 02:00:00 +0100')
time_difference(a, b) / 3600
# => 4 # hours
a = Time.parse('2015-12-31 02:00:00 +0100')
b = Time.parse('2015-12-31 22:00:00 +0100')
time_difference(a, b) / 3600
# => 20 # hours
Old question but I did a nice method to deal with it:
def time(start,ending)
if start != ending
medidas=["year","month","day","hour","minute","second"]
array=[1970,1,1,0,0,0]
text = ""
Time.at(ending-start).utc.to_a.take(6).reverse.each_with_index do |k,i|
text = "#{text} #{I18n.translate medidas[i].to_sym, count: k-array[i]}"
end
text = text.strip.squish
pos = text.rindex(" ",(text.rindex(" ")-1))
unless pos.nil?
text = text.insert(pos," and")
end
text = text.strip.squish #This shouldn't be needed but just in case
else
"0 seconds"
end
end
Then in config/locales/en.yml I added:
en:
año:
zero: ''
one: '1 year'
other: '%{count} years'
mes:
zero: ''
one: '1 month'
other: '%{count} months'
dia:
zero: ''
one: '1 day'
other: '%{count} days'
hora:
zero: ''
one: '1 hour'
other: '%{count} hours'
minuto:
zero: ''
one: '1 minute'
other: '%{count} minutes'
segundo:
zero: ''
one: '1 second'
other: '%{count} seconds'
So for example when you call:
start = Time.now
ending = start + (60*60)
time(start,ending)
=> "1 hour"
ending = start + (60*60*28)
time(start,ending)
=> "1 day and 4 hours"
ending = start + (53*60*5874)
time(start,ending)
=> "7 months 4 days 4 hours and 42 minutes"
Hope it's useful
I'd write it thusly (before adding data checks), in an attempt to make it self-documenting:
require 'time'
DT_FMT = '%Y-%m-%d %H:%M:%S %z'
SECONDS_PER_DAY = 24*60*60
def hours_elapsed(start_str, finish_str)
start = DateTime.strptime(start_str, DT_FMT).to_time
finish = DateTime.strptime(finish_str, DT_FMT).to_time
finish = same_time_tomorrow(finish) if finish < start
(finish-start)/3600
end
def same_time_tomorrow(time)
time + SECONDS_PER_DAY
end
hours_elapsed '2015-12-31 22:00:00 +0100',
'2015-12-31 02:00:00 +0100'
#=> 4.0
hours_elapsed '2015-12-31 02:00:00 +0100',
'2015-12-31 22:00:00 +0100'
#=> 20.0
It may be better for the arguments of hours_elapsed to be strings containing hours and minutes only, in which case we might rename the method as well. time_elapsed("18:00", "2:30") is an example of how this method might be invoked.
MINUTES_PER_DAY = 24*60
def time_elapsed(start_str, finish_str)
start_mins = time_str_to_minutes(start_str)
finish_mins = time_str_to_minutes(finish_str)
finish_mins += MINUTES_PER_DAY if
finish_mins < start_mins
(finish_mins-start_mins).divmod(60)
end
def time_str_to_minutes(str)
hrs, mins = str.split(':').map(&:to_i)
60 * hrs + mins
end
time_elapsed("8:00", "17:30")
#=> [9, 30]
time_elapsed("18:00", "2:30")
#=> [8, 30]

How do I convert a string into a Time object?

I searched for my problem and got a lot of solutions, but unfortunately none satisfy my need.
My problem is, I have two or more strings, and I want to convert those strings into times, and add them:
time1 = "10min 43s"
time2 = "32min 30s"
The output will be: 43min 13s
My attempted solution is:
time1 = "10min 43s"
d1=DateTime.strptime(time1, '%M ')
# Sat, 02 Nov 2013 00:10:00 +0000
time2 = "32min 30s"
d2=DateTime.strptime(time2, '%M ')
# Sat, 02 Nov 2013 00:32:00 +0000
Then I can't progress.
There are many ways to do this. Here's another:
time1 = "10min 43s"
time2 = "32min 30s"
def get_mins_and_secs(time_str)
time_str.scan(/\d+/).map(&:to_i)
#=> [10, 43] for time_str = time1, [32, 30] for time_str = time2
end
min, sec = get_mins_and_secs(time1)
min2, sec2 = get_mins_and_secs(time2)
min += min2
sec += sec2
if sec > 59
min += 1
sec -= 60
end
puts "#{min}min #{sec}sec"
Let's consider what's happening here. Firstly, you need to extract the minutes and seconds from the time strings. I made a method to do that:
def get_mins_and_secs(time_str)
time_str.scan(/\d+/).map(&:to_i)
#=> [10, 43] for time_str = time1, [32, 30] for time_str = time2
end
For time_str = "10min 43s", we apply the String#scan method to extract the two numbers as strings:
"10min 43s".scan(/\d+/) # => ["10", "43"]
Array#map is then used to convert these two strings to integers
["10", "43"].map {|e| e.to_i} # => [10, 43]
This can be written more succinctly as
["10", "43"].map(&:to_i} # => [10, 43]
By chaining map to to scan we obtain
"10min 43s".scan(/\d+/).map(&:to_i} # => [10, 43]
The array [10, 43] is returned and received (deconstructed) by the variables min and sec:
min, sec = get_mins_and_secs(time_str)
The rest is straightforward.
Here's a simple solution assuming that the format stays the same:
time1 = "10min 43s"
time2 = "32min 30s"
strings = [time1, time2]
total_time = strings.inject(0) do |sum, entry|
minutes, seconds = entry.split(' ')
minutes = minutes.gsub("min", "").to_i.send(:minutes)
seconds = seconds.gsub("s", "").to_i.send(:seconds)
sum + minutes + seconds
end
puts "#{total_time/60}min #{total_time%60}s"
Something like the following should do the trick:
# split the string on all the integers in the string
def to_seconds(time_string)
min, sec = time_string.gsub(/\d+/).map(&:to_i)
min.minutes + sec.seconds
end
# Divide the seconds with 60 to get minutes and format the output.
def to_time_str(seconds)
minutes = seconds / 60
seconds = seconds % 60
format("%02dmin %02dsec", minutes, seconds)
end
time_in_seconds1 = to_seconds("10min 43s")
time_in_seconds2 = to_seconds("32min 30s")
to_time_str(time_in_seconds1 + time_in_seconds2)
My solution that takes any number of time strings and return the sum in the same format:
def add_times(*times)
digits = /\d+/
total_time = times.inject(0){|sum, entry|
m, s = entry.scan(digits).map(&:to_i)
sum + m*60 + s
}.divmod(60)
times.first.gsub(digits){total_time.shift}
end
p add_times("10min 43s", "32min 55s", "1min 2s") #=> "44min, 40s"
p add_times("10:43", "32:55") #=> "38:43"

Ruby: given a date find the next 2nd or 4th Tuesday

I can't seem to find an elegant way to do this...
Given a date how can I find the next Tuesday that is either the 2nd or the 4th Tuesday of the calendar month?
For example:
Given 2012-10-19 then return 2012-10-23
or
Given 2012-10-31 then return 2012-11-13
October November
Su Mo Tu We Th Fr Sa Su Mo Tu We Th Fr Sa
1 2 3 4 5 6 1 2 3
7 8 9 10 11 12 13 4 5 6 7 8 9 10
14 15 16 17 18 19 20 11 12 13 14 15 16 17
21 22 23 24 25 26 27 18 19 20 21 22 23 24
28 29 30 31 25 26 27 28 29 30
Scroll to the bottom if you just want to see what the end result can look like..
Using code snippets from some date processing work I've done recently in ruby 1.9.3.
Some upgrades to DateTime:
require 'date'
class DateTime
ALL_DAYS = [ 'sunday', 'monday', 'tuesday',
'wednesday', 'thursday', 'friday', 'saturday' ]
def next_week
self + (7 - self.wday)
end
def next_wday (n)
n > self.wday ? self + (n - self.wday) : self.next_week.next_day(n)
end
def nth_wday (n, i)
current = self.next_wday(n)
while (i > 0)
current = current.next_wday(n)
i = i - 1
end
current
end
def first_of_month
self - self.mday + 1
end
def last_of_month
self.first_of_month.next_month - 1
end
end
method_missing Tricks:
I have also supplemented the class with some method missing tricks to map calls from next_tuesday to next_wday(2) andnth_tuesday(2)tonth_wday(2, 2)`, which makes the next snippet easier on the eyes.
class DateTime
# ...
def method_missing (sym, *args, &block)
day = sym.to_s.gsub(/^(next|nth)_(?<day>[a-zA-Z]+)$/i, '\k<day>')
dindex = ALL_DAYS.include?(day) ? ALL_DAYS.index(day.downcase) : nil
if (sym =~ /^next_[a-zA-Z]+$/i) && dindex
self.send(:next_wday, dindex)
elsif (sym =~ /^nth_[a-zA-Z]+$/i) && dindex
self.send(:nth_wday, dindex, args[0])
else
super(sym, *args, &block)
end
end
def respond_to? (sym)
day = sym.to_s.gsub(/^(next|nth)_(?<day>[a-zA-Z]+)$/i, '\k<day>')
(((sym =~ /^next_[a-zA-Z]+$/i) || (sym =~ /^nth_[a-zA-Z]+$/i)) && ALL_DAYS.include?(day)) || super(sym)
end
end
Example:
Given a date:
today = DateTime.now
second_tuesday = (today.first_of_month - 1).nth_tuesday(2)
fourth_tuesday = (today.first_of_month - 1).nth_tuesday(4)
if today == second_tuesday
puts "Today is the second tuesday of this month!"
elsif today == fourth_tuesday
puts "Today is the fourth tuesday of this month!"
else
puts "Today is not interesting."
end
You could also edit method_missing to handle calls such as :second_tuesday_of_this_month, :fourth_tuesday_of_this_month, etc. I'll post the code here if I decide to write it at a later date.
Take a look at Chronic or Tickle, both are gems for parsing complex times and dates. Tickle in particular will parse recurring times (I think it uses Chronic as well).
Check out this gem, you might be able to figure out your answer
https://github.com/mojombo/chronic/
Since you already use Rails, you don't need the includes, but this works in pure Ruby as well for reference.
require 'rubygems'
require 'active_support/core_ext'
d = DateTime.parse('2012-10-19')
result = nil
valid_weeks = [d.beginning_of_month.cweek + 1, d.beginning_of_month.cweek + 3]
if valid_weeks.include?(d.next_week(:tuesday).cweek)
result = d.next_week(:tuesday)
else
result = d.next_week.next_week(:tuesday)
end
puts result
I think you should probably use a library if you're needing to branch out into more interesting logic, but if what you've described is all you need, the code below should help
SECONDS_PER_DAY = 60 * 60 * 24
def find_tuesday_datenight(now)
tuesdays = [*-31..62].map { |i| now + (SECONDS_PER_DAY * i) }
.select { |d| d.tuesday? }
.group_by { |d| d.month }
[tuesdays[now.month][1], tuesdays[now.month][-2], tuesdays[(now.month + 1) % 12][1]]
.find {|d| d.yday > now.yday }
end
Loop through the last month and next month, grab the tuesdays, group by month, take the 2nd and the 2nd last tuesday of the current month (If you actually do want the 4th tuesday, just change -2 to 3) and the 2nd tuesday of the next month and then choose the first one after the provided date.
Here's some tests, 4 tuesdays in month, 5 tuesdays in month, random, and your examples:
[[2013, 5, 1], [2013, 12, 1], [2012, 10, 1], [2012, 10, 19], [2012, 10, 31]].each do |t|
puts "#{t} => #{find_tuesday_datenight(Time.new *t)}"
end
which produces
[2013, 5, 1] => 2013-05-14 00:00:00 +0800
[2013, 12, 1] => 2013-12-10 00:00:00 +0800
[2012, 10, 1] => 2012-10-09 00:00:00 +0800
[2012, 10, 19] => 2012-10-23 00:00:00 +0800
[2012, 10, 31] => 2012-11-13 00:00:00 +0800
I'm sure it could be simplified, and I'd love to hear some suggestions :) (way too late &tired to even bother figuring out what the actual range should be for valid dates i.e. smaller than -31..62)
so here is the code that will resolve a weekday for a given week in a month (what you asked for with little sugar). You should not have problems if you are running inside rails framework. Otherwise make sure you have active_support gem installed. Method name is stupid so feel free to change it :)
usage: get_next_day_of_week(some_date, :friday, 1)
require 'rubygems'
require 'active_support/core_ext'
def get_next_day_of_week(date, day_name, count)
next_date = date + (-date.days_to_week_start(day_name.to_sym) % 7)
while (next_date.mday / 7) != count - 1 do
next_date = next_date + 7
end
next_date
end
I use the following to calculate Microsoft's patch Tuesday date. It was adapted from some C# code.
require 'date'
#find nth iteration of given day (day specified in 'weekday' variable)
findnthday = 2
#Ruby wday number (days are numbered 0-7 beginning with Sunday)
weekday = 2
today = Time.now
todayM = today.month
todayY = today.year
StrtMonth = DateTime.new(todayY,todayM ,1)
while StrtMonth.wday != weekday do
StrtMonth = StrtMonth + 1;
end
PatchTuesday = StrtMonth + (7 * (findnthday - 1))

How to generate a human readable time range using ruby on rails

I'm trying to find the best way to generate the following output
<name> job took 30 seconds
<name> job took 1 minute and 20 seconds
<name> job took 30 minutes and 1 second
<name> job took 3 hours and 2 minutes
I started this code
def time_range_details
time = (self.created_at..self.updated_at).count
sync_time = case time
when 0..60 then "#{time} secs"
else "#{time/60} minunte(s) and #{time-min*60} seconds"
end
end
Is there a more efficient way of doing this. It seems like a lot of redundant code for something super simple.
Another use for this is:
<title> was posted 20 seconds ago
<title> was posted 2 hours ago
The code for this is similar, but instead i use Time.now:
def time_since_posted
time = (self.created_at..Time.now).count
...
...
end
If you need something more "precise" than distance_of_time_in_words, you can write something along these lines:
def humanize(secs)
[[60, :seconds], [60, :minutes], [24, :hours], [Float::INFINITY, :days]].map{ |count, name|
if secs > 0
secs, n = secs.divmod(count)
"#{n.to_i} #{name}" unless n.to_i==0
end
}.compact.reverse.join(' ')
end
p humanize 1234
#=>"20 minutes 34 seconds"
p humanize 12345
#=>"3 hours 25 minutes 45 seconds"
p humanize 123456
#=>"1 days 10 hours 17 minutes 36 seconds"
p humanize(Time.now - Time.local(2010,11,5))
#=>"4 days 18 hours 24 minutes 7 seconds"
Oh, one remark on your code:
(self.created_at..self.updated_at).count
is really bad way to get the difference. Use simply:
self.updated_at - self.created_at
There are two methods in DateHelper that might give you what you want:
time_ago_in_words
time_ago_in_words( 1234.seconds.from_now ) #=> "21 minutes"
time_ago_in_words( 12345.seconds.ago ) #=> "about 3 hours"
distance_of_time_in_words
distance_of_time_in_words( Time.now, 1234.seconds.from_now ) #=> "21 minutes"
distance_of_time_in_words( Time.now, 12345.seconds.ago ) #=> "about 3 hours"
chronic_duration parses numeric time to readable and vice versa
If you want to show significant durations in the seconds to days range, an alternative would be (as it doesn't have to perform the best):
def human_duration(secs, significant_only = true)
n = secs.round
parts = [60, 60, 24, 0].map{|d| next n if d.zero?; n, r = n.divmod d; r}.
reverse.zip(%w(d h m s)).drop_while{|n, u| n.zero? }
if significant_only
parts = parts[0..1] # no rounding, sorry
parts << '0' if parts.empty?
end
parts.flatten.join
end
start = Time.now
# perform job
puts "Elapsed time: #{human_duration(Time.now - start)}"
human_duration(0.3) == '0'
human_duration(0.5) == '1s'
human_duration(60) == '1m0s'
human_duration(4200) == '1h10m'
human_duration(3600*24) == '1d0h'
human_duration(3600*24 + 3*60*60) == '1d3h'
human_duration(3600*24 + 3*60*60 + 59*60) == '1d3h' # simple code, doesn't round
human_duration(3600*24 + 3*60*60 + 59*60, false) == '1d3h59m0s'
Alternatively you may be only interested in stripping the seconds part when it doesn't matter (also demonstrating another approach):
def human_duration(duration_in_seconds)
n = duration_in_seconds.round
parts = []
[60, 60, 24].each{|d| n, r = n.divmod d; parts << r; break if n.zero?}
parts << n unless n.zero?
pairs = parts.reverse.zip(%w(d h m s)[-parts.size..-1])
pairs.pop if pairs.size > 2 # do not report seconds when irrelevant
pairs.flatten.join
end
Hope that helps.
There is problem with distance_of_time_in_words if u ll pass there 1 hour 30 min it ll return about 2 hours
Simply add in helper:
PERIODS = {
'day' => 86400,
'hour' => 3600,
'minute' => 60
}
def formatted_time(total)
return 'now' if total.zero?
PERIODS.map do |name, span|
next if span > total
amount, total = total.divmod(span)
pluralize(amount, name)
end.compact.to_sentence
end
Basically just pass your data in seconds.
Rails has a DateHelper for views. If that is not exactly what you want, you may have to write your own.
#Mladen Jablanović has an answer with good sample code. However, if you don't mind continuing to customize a sample humanize method, this might be a good starting point.
def humanized_array_secs(sec)
[[60, 'minutes '], [60, 'hours '], [24, 'days ']].inject([[sec, 'seconds']]) do |ary, (count, next_name)|
div, prev_name = ary.pop
quot, remain = div.divmod(count)
ary.push([remain, prev_name])
ary.push([quot, next_name])
ary
end.reverse
end
This gives you an array of values and unit names that you can manipulate.
If the first element is non-zero, it is the number of days. You may want to write code to handle multiple days, like showing weeks, months, and years. Otherwise, trim off the leading 0 values, and take the next two.
def humanized_secs(sec)
return 'now' if 1 > sec
humanized_array = humanized_array_secs(sec.to_i)
days = humanized_array[-1][0]
case
when 366 <= days
"#{days / 365} years"
when 31 <= days
"#{days / 31} months"
when 7 <= days
"#{days / 7} weeks"
else
while humanized_array.any? && (0 == humanized_array[-1][0])
humanized_array.pop
end
humanized_array.reverse[0..1].flatten.join
end
end
The code even finds use for a ruby while statement.

Rails: distance_of_time_NOT_in_words

As in, distance_of_time(Time.now, Time.tomorrow).days = 1 or something along those lines? If not, what would be a good way to achieve this? I know there is "from_now" but why wouldn't there be a from_whenever?
I don't know of a built-in general solution but if it's only days you need to compare, you can do
d = DateTime.now
d2 = DateTime.now.advance(:days => 1)
days_diff = (d2-d).to_i # i.e., 1
Using Times you can do the same thing for seconds. From those seconds you can build a reasonable models of weeks, days, hours and minutes difference:
diff = (Time.now - Time.now.advance(:days => 38, :hours=>2, :minutes => 23)).abs
day_diff = diff % 1.week.seconds
weeks = (diff - day_diff) / 1.week.seconds
hour_diff = day_diff % 1.day.seconds
days = (day_diff - hour_diff) / 1.day.seconds
minute_diff = hour_diff % 1.hour.seconds
hours = (hour_diff - minute_diff) / 1.hour.seconds
second_diff = minute_diff % 1.minute.second
minutes = (minute_diff - second_diff) / 1.minute.seconds
fractions = second_diff % 1
seconds = (second_diff - fractions)
s=Struct.new(:weeks, :days, :hours, :minutes, :seconds)
s.new(weeks, days, hours, minutes, seconds)
s.weeks # 5
s.days # 2
s.minutes # 23
s.seconds # 0
(or something like this, I haven't really tested the code but you get the idea - I hope).

Resources