Rails calculate date range in months - ruby-on-rails

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.

Related

=IF(OR(AND - How to make gantt with automatic calendar handle start date prior to 31th of Dec and end date later than 1st of Jan

I made a gantt template where tasks are entered with a start date and an end date. Based on that I get a week number and use it to automatically fill cells in the calendar part of the sheet.
As our company have a fiscal year that start on the 1/7 and end on the 30/6. I run in to problems when the start date is less then or equal to 52 and end date is equal or bigger than 1. Can someone help me solve this?
https://docs.google.com/spreadsheets/d/1etVR3Y-l1hTVi7afdLJArq0juBFkXVI04r0SWwuAfCU/edit?usp=sharing
Try this:
=IF($A8 > $B8,
IF(OR(AE$3>=$A8,AE$3<=$B8),"Y",""),
IF(AND(AE$3>=$A8,AE$3<=$B8),"Y","")
)
I just added an outer IF to calculate if the start week was greater than the end week. If so, instead of using AND, it uses OR.
I got an answer from the Google community that I got to work
Here's the formula:
=if(($B4 >= $A4) * (C$3>=$A4) * (C$3<=$B4) + ($B4 < $A4) * ((C$3>=$A4) * (C$3 <= 52) + (C$3<=$B4)) = 0,, "Y")
or delete all the formulas in C4:BB and use this one formula in C4 without copying to the other cells.
=ArrayFormula(if((B4:B >= A4:A) * (C3:BB3>=A4:A) * (C3:BB3<=B4:B) + (B4:B < A4:A) * ((C3:BB3>=A4:A) * (C3:BB3 <= 52) + (C3:BB3<=B4:B)) = 0,, "Y"))

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.

How to get date of particular week in Rails

I am working on Rails application where I am trying to fetch date of particular week but week start from not 1 January of year but some fixed date.
Like, my week start from 8 july 2016 (08-07-2016) so now i want to fetch start date and end date of any week.
Means week 1 start date -> 08-07-2016 and end date -> 14-07-2016.
Now i want to fetch any week start date and end date but how? I already tried but got solution of year start date not like this.
Any one have idea?
Thanks in advance.
Ok friends,
I found my answer below
week = 3
number = (week-1) * 7
start_date = Date.new(2016, 7, 8) +number.to_i
d = start_date.to_s.split('-')
end_date = Date.new(d[0].to_i, d[1].to_i, d[2].to_i) + 6
end_date = end_date.strftime("%Y-%m-%d")
Let me know if any one have confusion in this answer.
There is no need to split the start date and create a new date object.
start_date_of_first_week = Date.new(2016, 7, 8)
week_number = 3
start_date = start_date_of_first_week + ( (week_number - 1) * 7 )
end_date = start_date + 6

How do you find what week in a month a day is on?

I can't seem to wrap my head around what might be an easy question..
Suppose I have the date..
Fri, 14 Sep 2012 18:37:50 +0200
How do I find out what week this date is on for this month? Is it the 1st, the 2nd..? The third?
Thanks!
Why use a library? Ruby has it by default:
Week number:
The week 1 of YYYY starts with a Sunday or Monday (according to %U
or %W). The days in the year before the first week are in week 0.
%U - Week number of the year. The week starts with Sunday. (00..53)
%W - Week number of the year. The week starts with Monday. (00..53)
> Time.zone.parse("2012-01-01").strftime("%U")
=> "01"
So, given that we can find what week a given date is in the year, we can do some math to figure out what week of the month it occurs in.
> week_of_year_for_first_of_month = Time.zone.parse("2012-07-01").strftime("%U").to_i
> week_of_target_date = Time.zone.parse("2012-07-14").strftime("%U").to_i
> week_occurs_in = week_of_target_date - week_of_year_for_first_of_month + 1
> week_occurs_in # => 2
Or a method:
def week_of_month_for_date(date)
my_date = Time.zone.parse(date)
week_of_target_date = my_date.strftime("%U").to_i
week_of_beginning_of_month = my_date.beginning_of_month.strftime("%U").to_i
week_of_target_date - week_of_beginning_of_month + 1
end
> week_of_month_for_date("2012-07-14") # => 2
> week_of_month_for_date("2012-07-15") # => 3
sachin87 has a library for determining such a thing.
Note that it depends how you count weeks. Let's say June 1 is on a Saturday. What week do you consider June 2 to be on? It might be the second week, or maybe it's the first if you consider a countable week to contain at least 4 days.
Or perhaps, given that June 2 is a Sunday, what's the week number of that Sunday? It's unambiguously the first Sunday. If this is what you mean, then it's actually simple. Dates 1 through 7 are always the first [weekday name] in the month. Dates 8-14 are always second. And so on. All you have to do is build a hash, and it will work for any month.
The week_of_month gem looks like it might be a bit overkill. That implementation uses a lot of array splitting and Array.include? checks.
Instead, here's a module that you can mixin to Date and Time to get the desired behavior.
require "active_support/core_ext/date"
require "active_support/core_ext/time"
module WeekCalculator
def week_of_year(mondays = false)
# Use %U for weeks starting on Sunday
# Use %W for weeks starting on Monday
strftime(mondays ? "%W" : "%U").to_i + 1
end
def week_of_month(mondays = false)
week_of_year(mondays) - beginning_of_month.week_of_year(mondays) + 1
end
end
class Date
include WeekCalculator
end
class Time
include WeekCalculator
end
Date.new(2014, 1, 1).week_of_year # => 1
Date.new(2014, 1, 1).week_of_month # => 1
Date.new(2014, 7, 1).week_of_year # => 27
Date.new(2014, 7, 1).week_of_month # => 1
Date.new(2014, 7, 27).week_of_year # => 31
Date.new(2014, 7, 27).week_of_month # => 5
Date.new(2014, 7, 27).week_of_year(:monday) # => 30
Date.new(2014, 7, 27).week_of_month(:monday) # => 4
I was challenged with answering "June 30th, 2017 is the nth Friday in June, 2017".
I solved this by building an array of all of the day names (Monday, Tuesday, etc) accrued up until the target date, and counting how many matched.
target_date = Date.new(2017, 6, 30)
(1..target_date.day).select do |day_of_month|
Date.new(target_date.year, target_date.month, day_of_month).strftime('%A') == target_date.strftime('%A')
end.length
Try this, find the week of a_date (considering first week is 1, and first day of week is monday):
week = (((a_date.mday + Date.new(a_date.year, a_date.month, 1).wday - 1) / 7) + 1)

In Rails, display time between two dates in English

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

Resources