how to calculate relative time in ruby? - ruby

How to calculate relative_time for seconds,minutes, days, months and year in ruby?
Given a DateTime object, implement relative time
d = DateTime.now
d.relative_time # few seconds ago
Possible outputs
# few seconds ago
# few minutes ago - till 3 minutes
# x minutes ago (here x starts from 3 till 59)
# x hours ago (x starts from 1 - 24) and so on for the below outputs
# x days ago
# x weeks ago
# x months ago
# x years ago
how to implement this in ruby? please help

Use GNU Date
In Ruby's core and standard library, there are no convenience methods for the type of output you want, so you'd have to implement your own logic. Without ActiveSupport extensions, you'd be better off calling out to the GNU (not BSD) date utility. For example:
now = Time.now
last_week = %x(date -d "#{now} - 1 week")
If you pass a format flag to GNU date such as +"%s", the date command will provide the date as seconds since epoch. This allows you to create case statements for whatever units of time you want to compare, such whether it's more than 360 seconds ago or less than 86,400.
Please see the date input formats defined by GNU coreutils for a more comprehensive look at how to use the various date string options.
Using Rails Extensions
If you're willing to install and require a couple of Rails extensions in your code, you can use ActionView::Helpers::DateHelper#distance_of_time_in_words. For example:
require "active_support/core_ext/integer/time"
require "active_support/core_ext/numeric/time"
require "action_view"
include ActionView::Helpers::DateHelper
2.weeks.ago
#=> 2020-07-12 09:34:20.178526 -0400
distance_of_time_in_words Date.current, 1.month.ago
#=> "30 days"
sprintf "%s ago", distance_of_time_in_words(Time.now, 1.hour.ago)
#=> "about 1 hour ago"
Some combination of the various #ago methods coupled with date/time calculations, and calls to the distance helper will do what you want, but you'll have to implement some of your own logic if you want to cast the results differently (e.g. specifying that you want the output of large distances as weeks or months instead of in years).
Other Gems
There are certainly other Ruby gems that could help. The Ruby Toolbox shows a number of gems for determining time differences, but none seem to be well-maintained. Caveat emptor, and your mileage may vary.
See Also
ActiveSupport Core Extensions
DateAndTime::Calculations

First of all, DateTime is intended for historical dates, whereas Time is used for current dates, so you probably want the latter. (see When should you use DateTime and when should you use Time?)
Given a time instance 45 minutes ago: (45 × 60 = 2,700)
t = Time.now - 2700
You can get the difference in seconds to the current time via Time#-:
Time.now - t
#=> 2701.360482
# wait 10 seconds
Time.now - t
#=> 2711.148882
This difference can be used in a case expression along with ranges to generate the corresponding duration string:
diff = Time.now - t
case diff
when 0...60 then "few seconds ago"
when 60...180 then "few minutes ago"
when 180...3600 then "#{diff.div(60)} minutes ago"
when 3600...7200 then "1 hour ago"
when 7200...86400 then "#{diff.div(3600)} hours ago"
# ...
end
You might have to adjust the numbers, but that should get you going.

I will construct a method relative_time that has two arguments:
date_time an instance of DateTime that specifies a time in the past; and
time_unit, one of :SECONDS, :MINUTES, :HOURS, :DAYS, :WEEKS, :MONTHS or :YEARS, specifying the unit of time for which the difference in time between the current time and date_time is to be expressed.
Code
require 'date'
TIME_UNIT_TO_SECS = { SECONDS:1, MINUTES:60, HOURS:3600, DAYS:24*3600,
WEEKS: 7*24*3600 }
TIME_UNIT_LBLS = { SECONDS:"seconds", MINUTES:"minutes", HOURS:"hours",
DAYS:"days", WEEKS: "weeks", MONTHS:"months",
YEARS: "years" }
def relative_time(date_time, time_unit)
now = DateTime.now
raise ArgumentError, "'date_time' cannot be in the future" if
date_time > now
v = case time_unit
when :SECONDS, :MINUTES, :HOURS, :DAYS, :WEEKS
(now.to_time.to_i-date_time.to_time.to_i)/
TIME_UNIT_TO_SECS[time_unit]
when :MONTHS
0.step.find { |n| (date_time >> n) > now } -1
when :YEARS
0.step.find { |n| (date_time >> 12*n) > now } -1
else
raise ArgumentError, "Invalid value for 'time_unit'"
end
puts "#{v} #{TIME_UNIT_LBLS[time_unit]} ago"
end
Examples
date_time = DateTime.parse("2020-5-20")
relative_time(date_time, :SECONDS)
5870901 seconds ago
relative_time(date_time, :MINUTES)
97848 minutes ago
relative_time(date_time, :HOURS)
1630 hours ago
relative_time(date_time, :DAYS)
67 days ago
relative_time(date_time, :WEEKS)
9 weeks ago
relative_time(date_time, :MONTHS)
2 months ago
relative_time(date_time, :YEARS)
0 years ago
Explanation
If time_unit equals :SECONDS, :MINUTES, :HOURS, :DAYS or :WEEKS I simply compute the number of seconds elapsed between date_time and the current time, and divide that by the number of seconds per the given unit of time. For example, if time_unit equals :DAYS the elapsed time in seconds is divided by 24*3600, as there are that many seconds per day.
If time_unit equals :MONTHS, I use the method Date#>> (which is inherited by DateTime) to determine the number of months that elapse from date_time until a time is reached that is after the current time, then subtract 1.
The calculation is similar if time_unit equals :YEARS: determine the number of years that elapse from date_time until a time is reached that is after the current time, then subtract 1.
One could require the user to enter a Time instance (rather than a DateTime instance) as the first argument. That would not simplify the method, however, as the Time instance would have to be converted to a DateTime instance when time_unit equals :MONTH or :YEAR, to use the method Date#>>.

The following function uses the power of Date#<< to represent past dates relative to today.
def natural_past_date(date)
date_today = Date.today
date_then = Date.parse(date)
if (date_today << 12) >= date_then
dy = (1..100).detect { (date_today << (_1.next * 12)) < date_then }
"#{dy} year#{dy > 1 ? 's' : ''} ago"
elsif (date_today << 1) >= date_then
dm = (1..12).detect { (date_today << _1.next) < date_then }
"#{dm} month#{dm > 1 ? 's' : ''} ago"
elsif (dw = (dd = (date_today - date_then).to_i)/7) > 0
"#{dw} week#{dw > 1 ? 's' : ''} ago"
else
if (2...7) === dd
"#{dd} days ago"
elsif (1...2) === dd
'yesterday'
else
'today'
end
end
end
Some sample usages of the function:
Date.today
=> #<Date: 2022-03-16 ...
natural_past_date '2020/01/09'
=> "2 years ago"
natural_past_date '2021/09/09'
=> "6 months ago"
natural_past_date '2022/03/08'
=> "1 week ago"
natural_past_date '2022/03/14'
=> "2 days ago"
natural_past_date '2022/03/15'
=> "yesterday"
natural_past_date '2022/03/16'
=> "today"

Related

Finding the date for a given week number

I am trying to do some date math based on the week number of a given year. For example:
date = Date.today # Monday, March 5, 2012
puts date.cwyear # 2012
puts date.cweek # 10 (10th week of 2012)
Now that I know what the current week is, I want to figure out what the next week and previous week are. I need to take the year (2012) and the week number (10) and turn it back into a date object so I can calculate the value for the next/previous week. How can I do this?
You want Date.commercial:
require 'date'
now = Date.today #=> 2012-03-05
monday_next_week = Date.commercial(now.cwyear,now.cweek+1) #=> 2012-03-12
next_sunday_or_today = monday_next_week - 1 #=> 2012-03-11
Note that weeks start on Monday, so if you are on a Sunday and ask for next monday - 1 you'll get the same day.
Note also that if you don't want Mondays you can also specify the day number in the method:
thursday_next_week = Date.commercial(now.cwyear,now.cweek+1,4) #=> 2012-03-15
Calculating on a day basis is pretty simple with Date objects. If you just want to get the previous / next week from a given Date object use the following:
date = Date.today
previous_week = (date - 7).cweek
next_week = (date + 7).cweek
In ActiveSupport you have helper to convert Fixnum to time http://as.rubyonrails.org/classes/ActiveSupport/CoreExtensions/Numeric/Time.html use:
date = Date.today
week_ago = date - 1.week
next_week = date + 1.week
I have created some methods to get week number of a given date
something like this:
def self.get_week(date)
year = date.year
first_monday_of_the_year = self.get_first_monday_of_the_year(year)
# The first days of January may belong to the previous year!
if date < first_monday_of_the_year
year -= 1
first_monday_of_the_year = self.get_first_monday_of_the_year(year)
end
day_difference = date - first_monday_of_the_year
week = (day_difference / 7).to_i + 1
week
end
def self.get_monday_of_year_week(year, week)
d = self.get_first_monday_of_the_year(year)
d + ((week - 1) * 7).days
end
def self.get_first_monday_of_the_year(year)
d = Date.new(year, 1, 7) # 7 Jan is always in the first week
self.get_monday_of_week(d)
end
def self.get_monday_of_week(date)
wday = (date.wday + 6) % 7
date - wday.days
end
Assuming you mean "a given week number in the current year", you can do the following:
2.weeks.since(Time.gm(Time.now.year))
=> Fri Jan 15 00:00:00 UTC 2010
Substitute (week_number - 1) for the 1 in the above, and you'll get a date in the desired week.

Calculating a time before 1970 (fails with negative seconds)

I am new in ruby,and when I learn the Time class in ruby(In fact I follow the VTC video) I found something I can not make ,I want to caculate the born year of one person according to his age,
for example,when a person tell his age is "20",then I should caculate his born year.
class Person
attr_accessor :name,:age,:year_born
def initialize(name,age)
#name=name
#age=age
#year_born=(Time.now - age*31556962).year
end
def days_alive
#age*365
end
end
In the following code everything works well except the
#year_born=(Time.now - age*31556962).year
I got an error when I try
Person.new("name",43).year_born
which says:
ArgumentError: time must be positive
./person.rb:6:in `-'
./person.rb:6:in `initialize'
I know Time.now will return the seconds from 1970,that's to say
(2011-1970)<43
So the Time.now-43*31556962 return a invalid value,but I want to know how to implement my requirement?
According to Programming Ruby:
Time is an abstraction of dates and
times. Time is stored internally as
the number of seconds and microseconds
since the epoch, January 1, 1970 00:00
UTC. On some operating systems, this
offset is allowed to be negative. Also
see the Date library module on page
742. (emphasis mine)
Which implies that on some operating systems, the offset is not allowed to be negative. So any of us elderly folks who were born before 1970 may blow up your code. Also keep in mind you're actually calculating number_of_seconds_per_year * age_in_years, which won't be very accurate.
why not do it like this:
note that i am using
(Time.now.year - age)
and that i have year_born as a method.
class Person
attr_accessor :name,:age
def initialize(name,age)
#name=name
#age=age
end
def year_born
(Time.now.year - age)
end
def days_alive
#age*365
end
end
However do not store the age in your DB(if you are going to save this in your DB). Just save the birth date.
The problem is you're using Time, but should be using either Date or DateTime, which have a greater range. Date doesn't know about times, which might fit your application better since you want day granularity.
require 'date'
Date.today - 20 * 365 # => #<Date: 1991-05-07 (4896767/2,0,2299161)>
Date.today - 50 * 365 # => #<Date: 1961-05-14 (4874867/2,0,2299161)>
(Date.today - 50 * 365).year # => 1961

Max and min time checks in case statement using Ruby?

I what to check a time with a case statement. How to do?
Use ranges:
case time
when (Time.now - 60)..(Time.now) then puts 'within the last minute'
when (Time.now - 3600)..(Time.now) then puts 'within the last hour'
end
Ranges work with all sorts of values. You can use Dates too:
case date
when (Date.today - 1)..(Date.today) then puts 'less than a day ago'
when (Date.today - 30)..(Date.today) then puts 'less than a month ago'
end
Update: Ruby 1.9 broke Time ranges, so that example works only in Ruby 1.8.7. The Date example works in both versions though. In 1.9 you can use this code to match a Time:
case time.to_i
when ((Time.now - 60).to_i)..(Time.now.to_i) then puts 'within the last minute'
when ((Time.now - 3600).to_i)..(Time.now.to_i) then puts 'within the last hour'
end
Just use the version that doesn't have a defined variable at the top...
t = event.time # arbitrary example.
case
when t <= Time.now
# Event is in the past.
else
# Event is in the future.
end

how to convert 270921sec into days + hours + minutes + sec ? (ruby)

I have a number of seconds. Let's say 270921. How can I display that number saying it is xx days, yy hours, zz minutes, ww seconds?
It can be done pretty concisely using divmod:
t = 270921
mm, ss = t.divmod(60) #=> [4515, 21]
hh, mm = mm.divmod(60) #=> [75, 15]
dd, hh = hh.divmod(24) #=> [3, 3]
puts "%d days, %d hours, %d minutes and %d seconds" % [dd, hh, mm, ss]
#=> 3 days, 3 hours, 15 minutes and 21 seconds
You could probably DRY it further by getting creative with collect, or maybe inject, but when the core logic is three lines it may be overkill.
I was hoping there would be an easier way than using divmod, but this is the most DRY and reusable way I found to do it:
def seconds_to_units(seconds)
'%d days, %d hours, %d minutes, %d seconds' %
# the .reverse lets us put the larger units first for readability
[24,60,60].reverse.inject([seconds]) {|result, unitsize|
result[0,0] = result.shift.divmod(unitsize)
result
}
end
The method is easily adjusted by changing the format string and the first inline array (ie the [24,60,60]).
Enhanced version
class TieredUnitFormatter
# if you set this, '%d' must appear as many times as there are units
attr_accessor :format_string
def initialize(unit_names=%w(days hours minutes seconds), conversion_factors=[24, 60, 60])
#unit_names = unit_names
#factors = conversion_factors
#format_string = unit_names.map {|name| "%d #{name}" }.join(', ')
# the .reverse helps us iterate more effectively
#reversed_factors = #factors.reverse
end
# e.g. seconds
def format(smallest_unit_amount)
parts = split(smallest_unit_amount)
#format_string % parts
end
def split(smallest_unit_amount)
# go from smallest to largest unit
#reversed_factors.inject([smallest_unit_amount]) {|result, unitsize|
# Remove the most significant item (left side), convert it, then
# add the 2-element array to the left side of the result.
result[0,0] = result.shift.divmod(unitsize)
result
}
end
end
Examples:
fmt = TieredUnitFormatter.new
fmt.format(270921) # => "3 days, 3 hours, 15 minutes, 21 seconds"
fmt = TieredUnitFormatter.new(%w(minutes seconds), [60])
fmt.format(5454) # => "90 minutes, 54 seconds"
fmt.format_string = '%d:%d'
fmt.format(5454) # => "90:54"
Note that format_string won't let you change the order of the parts (it's always the most significant value to least). For finer grained control, you can use split and manipulate the values yourself.
Needed a break. Golfed this up:
s = 270921
dhms = [60,60,24].reduce([s]) { |m,o| m.unshift(m.shift.divmod(o)).flatten }
# => [3, 3, 15, 21]
Rails has an helper which converts distance of time in words.
You can look its implementation: distance_of_time_in_words
If you're using Rails, there is an easy way if you don't need the precision:
time_ago_in_words 270921.seconds.from_now
# => 3 days
You can use the simplest method I found for this problem:
def formatted_duration total_seconds
hours = total_seconds / (60 * 60)
minutes = (total_seconds / 60) % 60
seconds = total_seconds % 60
"#{ hours } h #{ minutes } m #{ seconds } s"
end
You can always adjust returned value to your needs.
2.2.2 :062 > formatted_duration 3661
=> "1 h 1 m 1 s"
I modified the answer given by #Mike to add dynamic formatting based on the size of the result
def formatted_duration(total_seconds)
dhms = [60, 60, 24].reduce([total_seconds]) { |m,o| m.unshift(m.shift.divmod(o)).flatten }
return "%d days %d hours %d minutes %d seconds" % dhms unless dhms[0].zero?
return "%d hours %d minutes %d seconds" % dhms[1..3] unless dhms[1].zero?
return "%d minutes %d seconds" % dhms[2..3] unless dhms[2].zero?
"%d seconds" % dhms[3]
end
I just start writing ruby. i guess this is only for 1.9.3
def dateBeautify(t)
cute_date=Array.new
tables=[ ["day", 24*60*60], ["hour", 60*60], ["minute", 60], ["sec", 1] ]
tables.each do |unit, value|
o = t.divmod(value)
p_unit = o[0] > 1 ? unit.pluralize : unit
cute_date.push("#{o[0]} #{unit}") unless o[0] == 0
t = o[1]
end
return cute_date.join(', ')
end
Number of days = 270921/86400 (Number of seconds in day) = 3 days this is the absolute number
seconds remaining (t) = 270921 - 3*86400 = 11721
3.to_s + Time.at(t).utc.strftime(":%H:%M:%S")
Which will produce something like 3:03:15:21
Not a direct answer to the OP but it might help someone who lands here.
I had this string
"Sorry, you cannot change team leader in the last #{freeze_period_time} of a #{competition.kind}"
freeze_period_time resolved to 5 days inside irb, but inside the string, it resolved to time in seconds eg 47200, so the string became something ugly
"Sorry, you cannot change team leader in the last 47200 of a hackathon"
To fix it, I had to use .inspect on the freeze_period_time object.
So the following made it work
"Sorry, you cannot change team leader in the last #{freeze_period_time.inspect} of a #{competition.kind}"
Which returned the correct sentence
"Sorry, you cannot change team leader in the last 5 days of a hackathon"
TLDR
You might need time.inspect - https://www.geeksforgeeks.org/ruby-time-inspect-function/

How do I get elapsed time in milliseconds in Ruby?

If I have a Time object got from :
Time.now
and later I instantiate another object with that same line, how can I see how many milliseconds have passed? The second object may be created that same minute, over the next minutes or even hours.
As stated already, you can operate on Time objects as if they were numeric (or floating point) values. These operations result in second resolution which can easily be converted.
For example:
def time_diff_milli(start, finish)
(finish - start) * 1000.0
end
t1 = Time.now
# arbitrary elapsed time
t2 = Time.now
msecs = time_diff_milli t1, t2
You will need to decide whether to truncate that or not.
You can add a little syntax sugar to the above solution with the following:
class Time
def to_ms
(self.to_f * 1000.0).to_i
end
end
start_time = Time.now
sleep(3)
end_time = Time.now
elapsed_time = end_time.to_ms - start_time.to_ms # => 3004
I think the answer is incorrectly chosen, that method gives seconds, not milliseconds.
t = Time.now.t­o_f
=> 1382471965.146
Here I suppose the floating value are the milliseconds
DateTime.now.strftime("%Q")
Example usage:
>> DateTime.now.strftime("%Q")
=> "1541433332357"
>> DateTime.now.strftime("%Q").to_i
=> 1541433332357
To get time in milliseconds, it's better to add .round(3), so it will be more accurate in some cases:
puts Time.now.to_f # => 1453402722.577573
(Time.now.to_f.round(3)*1000).to_i # => 1453402722578
ezpz's answer is almost perfect, but I hope I can add a little more.
Geo asked about time in milliseconds; this sounds like an integer quantity, and I wouldn't take the detour through floating-point land. Thus my approach would be:
t8 = Time.now
# => Sun Nov 01 15:18:04 +0100 2009
t9 = Time.now
# => Sun Nov 01 15:18:18 +0100 2009
dif = t9 - t8
# => 13.940166
(1000 * dif).to_i
# => 13940
Multiplying by an integer 1000 preserves the fractional number perfectly and may be a little faster too.
If you're dealing with dates and times, you may need to use the DateTime class. This works similarly but the conversion factor is 24 * 3600 * 1000 = 86400000 .
I've found DateTime's strptime and strftime functions invaluable in parsing and formatting date/time strings (e.g. to/from logs). What comes in handy to know is:
The formatting characters for these functions (%H, %M, %S, ...) are almost the same as for the C functions found on any Unix/Linux system; and
There are a few more: In particular, %L does milliseconds!
The answer is something like:
t_start = Time.now
# time-consuming operation
t_end = Time.now
milliseconds = (t_start - t_end) * 1000.0
However, the Time.now approach risks to be inaccurate. I found this post by Luca Guidi:
https://blog.dnsimple.com/2018/03/elapsed-time-with-ruby-the-right-way/
system clock is constantly floating and it doesn't move only forwards. If your calculation of elapsed time is based on it, you're very likely to run into calculation errors or even outages.
So, it is recommended to use Process.clock_gettime instead. Something like:
def measure_time
start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
yield
end_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
elapsed_time = end_time - start_time
elapsed_time.round(3)
end
Example:
elapsed = measure_time do
# your time-consuming task here:
sleep 2.2321
end
=> 2.232
%L gives milliseconds in ruby
require 'time'
puts Time.now.strftime("%Y-%m-%dT%H:%M:%S.%L")
or
puts Time.now.strftime("%Y-%m-%d %H:%M:%S.%L")
will give you current timestamp in milliseconds.
Time.now.to_f can help you but it returns seconds.
In general, when working with benchmarks I:
put in variable the current time;
insert the block to test;
put in a variable the current time, subtracting the preceding current-time value;
It's a very simple process, so I'm not sure you were really asking this...
Try subtracting the first Time.now from the second. Like so:
a = Time.now
sleep(3)
puts Time.now - a # about 3.0
This gives you a floating-point number of the seconds between the two times (and with that, the milliseconds).
If you want something precise, unaffected by other part of your app (Timecop) or other programs (like NTP), use Process#clock_gettime with Process::CLOCK_MONOTONIC to directly get the processor time.
t1 = Process.clock_gettime(Process::CLOCK_MONOTONIC)
# other code
t2 = Process.clock_gettime(Process::CLOCK_MONOTONIC)
Also, if you are trying to benchmark some code tho, there is the Benchmark module for that!
require "benchmark"
time = Benchmark.realtime do
# code to measure
end

Resources