In Rails, display time between two dates in English - ruby-on-rails

In a Rails project I want to find the difference between two dates and then display it in natural language. Something like
>> (date1 - date2).to_natural_language
"3 years, 2 months, 1 week, 6 days"
Basically this for ruby.
Google and the Rails API haven't turned up anything. I've found some things that will give you the difference in one unit (ie, how many weeks between two dates) but not something that will accurately calculate years, months, weeks, days all together.

The Rails' ActionView module includes two methods that may do what you require:
distance_of_time_in_words
distance_of_time_in_words_to_now

The other answers may not give the type of output that you're looking for, because instead of giving a string of years, months, etc., the Rails helpers just show the largest unit. If you're looking for something more broken down, here's another option. Stick this method into a helper:
def time_diff_in_natural_language(from_time, to_time)
from_time = from_time.to_time if from_time.respond_to?(:to_time)
to_time = to_time.to_time if to_time.respond_to?(:to_time)
distance_in_seconds = ((to_time - from_time).abs).round
components = []
%w(year month week day).each do |interval|
# For each interval type, if the amount of time remaining is greater than
# one unit, calculate how many units fit into the remaining time.
if distance_in_seconds >= 1.send(interval)
delta = (distance_in_seconds / 1.send(interval)).floor
distance_in_seconds -= delta.send(interval)
components << pluralize(delta, interval)
# if above line give pain. try below one
# components << interval.pluralize(delta)
end
end
components.join(", ")
end
And then in a view you can say something like:
<%= time_diff_in_natural_language(Time.now, 2.5.years.ago) %>
=> 2 years, 6 months, 2 days
The given method only goes down to days, but can be easily extended to add in smaller units if desired.

I tried Daniel's solution and found some incorrect results for a few test cases, due to the fact that it doesn't correctly handle the variable number of days found in months:
> 30.days < 1.month
=> false
So, for example:> d1 = DateTime.civil(2011,4,4)
> d2 = d1 + 1.year + 5.months
> time_diff_in_natural_language(d1,d2)
=> "1 year, 5 months, 3 days"
The following will give you the correct number of {years,months,days,hours,minutes,seconds}:
def time_diff(from_time, to_time)
%w(year month day hour minute second).map do |interval|
distance_in_seconds = (to_time.to_i - from_time.to_i).round(1)
delta = (distance_in_seconds / 1.send(interval)).floor
delta -= 1 if from_time + delta.send(interval) > to_time
from_time += delta.send(interval)
delta
end
end
> time_diff(d1,d2)
=> [1, 5, 0, 0, 0, 0]

distance_of_time_in_words is the most accurate here. Daniel's answer is actully wrong: 2.5 years ago should produce exactly 2 years, 6 months. The issue is that months contain 28-31 day, and years might be leap.
I wish I knew how to fix this :(

DateHelper#distance_of_time_in_words

def date_diff_in_natural_language(date_from, date_to)
components = []
%w(years months days).each do |interval_name|
interval = 1.send(interval_name)
count_intervals = 0
while date_from + interval <= date_to
date_from += interval
count_intervals += 1
end
components << pluralize(count_intervals, interval_name) if count_intervals > 0
end
components.join(', ')
end

Related

Show objects based on selected week

I have an active relation Bar object with an attribute shift_date. shift_date represents each day between March and June. March and June comes from Foo which has attributes start_month and end_month:
f = Foo.find(1)
days = (f.end_month - f.start_month).to_i
weeks = (days * 0.142857).round(2)
f.bars will give me days objects. Where days is the total amount of objetcs.
My trouble is to get Bars objects, objects for week 1, 2 or 3 etc:
f.bars.where('shift_date >= ?', (weeks/7.days)).group_by{ |result| result }
operator does not exist: timestamp without time zone >= numeric
So what am I saying? Give me all objects on week 1 or week 5, if any. How do I go about this, please?
Im on to something but not right:
f.bars.where('shift_date >= ?', Date.today).group_by{ |result| result}
Edit:
Im almost there. I could splat out the days with:
days_array = *(f.start_month..f.end_month)
then
f.bars.where(shift_date: days_array[0]..days_array[7])
That would be the answer! But...not really. For my views, I need to group the splatted days in a 7 days interval as week, so days_array[0] to days_array[7] would be week 1 and days_array[8] to days_array[14] would be week 2 etc. How to show that in the view? This will give me everything I need.

Display time remaining with distance_of_time_in_words

So i want item to display to remaining on Items after 7 days the item will be deleted. ive tried
<%= distance_of_time_in_words(item.created_at, item.created_at + 7.days) %>
but all i get is "7 Days" on all items. Can anyone simply how this helper method works ?
Lets looks at the documentation to see what distance_of_time_in_words does:
distance_of_time_in_words(from_time, to_time = 0, options = {})
Reports the approximate distance in time between two Time, Date or DateTime objects or integers as seconds.
So it reports the time difference of the first argument and the second argument. Now, you're doing:
distance_of_time_in_words(item.created_at, item.created_at + 7.days)
The difference between item.created_at and item.created_at plus seven days is always ... seven days ;-)
I assume that this is something that will always be deleted after seven days? In that case, what you want, is the difference between the current date and the creation date plus seven days, which you can get with:
distance_of_time_in_words(Time.now, item.created_at + 7.days)

Shoter time_ago_in_words eg. 1d ago

In my design i have a latest posts block, in this block there is very little space to say when the post was posted. Therefore, I did it as "1d" instead of "1 day ago". I would like it to be either "Today", "Yesterday" or "xd"(1d, 2d, 3d, etc)
The main reason for not using hours and minutes(and seconds..) is that my format is like this posted_date="2013-01-04", so no hours, minutes, seconds etc.
Is this posible?
def days_ago(date)
days = ((Time.now - date) / 24 / 60 / 60).round
case days
when 0 then 'Today'
when 1 then 'Yesterday'
else "#{days}d"
end
end

Rails calculate date range in months

How do I calculate the difference of two dates in months? Also, incase it makes a difference, I am working with Date objects, not DateTime. Also, some rounding options might be nice so I can control if I want to round up or down on partial months.
Thanks!
Subtracting one Date or DateTime from another will yield the number of days as a fraction, but this can be evaluated as a Float or Fixnum as required.
For instance:
(Date.today - Date.today.advance(:months => -3)).to_f
# => 89.0
There were 89.0 days between today and the same calendar date three months ago. If you work this using 30-day months, or 30.4375 as they are on average, you end up with 2.92 months elapsed between then and now, or rounded up to the nearest integer, 3.
If you want to compute the precise number of calendar months, that is trickier, but can be done.
Something like this is more readable than figuring out seconds, and will give you the actual calendar difference:
# Calculate differnce between two dates in months
# Produces b - a
def month_difference(a, b)
difference = 0.0
if a.year != b.year
difference += 12 * (b.year - a.year)
end
difference + b.month - a.month
end
If you need to work out the difference based on days as well, you can just follow the pattern
We needed something along these lines, but inclusive of partial months. So 1/31 to 2/1 would still yield 2 months. Might help!
def self.month_count(range)
12 * (range.end.year - range.begin.year) + range.end.month - range.begin.month
end
This should give an o.k. approximation:
Date1 - Date2 = difference_in_days
(difference_in_days/30).round = difference_in_months
This answer is late to the party, builds on previous answers, and could probably be written more concisely, however, it does give the calendar difference between two dates taking days into account.
def difference_in_months(start_date, today)
date_to_months(today) - date_to_months(start_date) + adjustment_for_days(start_date, today)
end
def date_to_months(date)
date.year * 12 + date.month
end
def adjustment_for_days(earlier_date, later_date)
if later_date.day == earlier_date.day
0
elsif later_date.day > earlier_date.day
1
else
-1
end
end
There is a rails helper for this functionality:
http://api.rubyonrails.org/classes/ActionView/Helpers/DateHelper.html#method-i-distance_of_time_in_words
how about this practice?
current_date = start_date
while current_date < end_date
# something
current_date = current_date.next_month
end
I needed the exact number of months (including decimals) between two dates and wrote the following method for it.
def months_difference(period_start, period_end)
period_end = period_end + 1.day
months = (period_end.year - period_start.year) * 12 + period_end.month - period_start.month - (period_end.day >= period_start.day ? 0 : 1)
remains = period_end - (period_start + months.month)
(months + remains/period_end.end_of_month.day).to_f.round(2)
end
If comparing let's say September 26th to September 26th (same day) I calculate it as 1 day. If you don't need that you can remove the first line in the method: period_end = period_end + 1.day
It passes the following specs:
expect(months_difference(Date.new(2017, 8, 1), Date.new(2017, 8, 31))).to eq 1.0
expect(months_difference(Date.new(2017, 8, 1), Date.new(2017, 8, 30))).to eq 0.97
expect(months_difference(Date.new(2017, 8, 1), Date.new(2017, 10, 31))).to eq 3.0
# Overlapping february (28 days) still counts Feb as a full month
expect(months_difference(Date.new(2017, 1, 1), Date.new(2017, 3, 31))).to eq 3.0
expect(months_difference(Date.new(2017, 2, 10), Date.new(2017, 3, 9))).to eq 1.0
# Leap year
expect(months_difference(Date.new(2016, 2, 1), Date.new(2016, 2, 29))).to eq 1.0
It relies on Rails' ActiveSupport.

Generate Time object given weekday and minutes since midnight

I'm trying to generate a Time object initialized to the next instance of a specific weekday and number of minutes past midnight.
My data (representing a schedule) looks like: weekday, start, finish. Weekday is the day of the week between 0 and 6, and start/finish are integers representing minutes past midnight on that particular weekday.
What I'd like to be able to do is get a Time object for the next time that this will be so that I can work more flexibly with the start/end times for frontend purposes.
Does anyone know how to do this? I've tinkered with Time.utc without much success.
Not sure if I'm interpreting this correctly... Is it where given a weekday, you will find the next available date that is on that weekday? For example, if today is tuesday, and your schedule is set for thursday, then the time object will be at midnight on this thursday? If that's the case, something like this should work:
day_difference = #weekday - Time.now.wday
# If the difference is in the past, then add 7 so that it is next week.
if day_difference < 0
day_difference = day_difference + 7
end
next_date = Time.now.midnight + day_difference.days
start_time = next_date + #start.minutes
end_time = next_date + #finish.minutes
Where weekday is the value in your Schedule object. Once again, I'm not 100% sure what you're asking. Here start_time and end_time are time objects representing the next weekday with the corresponding amount of minutes added to them.
You can do something basic along these lines for this with something like the following:
class Schedule
attr_accessor :day_of_week, :start_minute, :finish_minute
def start(current_date = Time.zone.now)
current_date.beginning_of_week + day_of_week.days + start_minute.minutes
end
def finish(current_date = Time.zone.now)
current_date.beginning_of_week + day_of_week.days + finish_minute.minutes
end
def length
(finish_minute - start_minute).minutes
end
end
Since that's pretty rudimentary and I'm not sure exactly what you're looking to use it for, I suggest looking at some of the libraries mentioned in this post ice_cube, and business_time look like they're applicable for what you might be wanting

Resources