inscrutable behaviour of date in ruby - ruby

I have a variable which has some future date(for example I have taken 30 days after date here), I want to have a conditional statement based on if this future date is 10 days advanced, I was trying following code, but it doesn't work, what am I doing wrong here?
> future_date = Date.today + 30.day
=> Wed, 16 Jul 2014
> future_date - Date.today > 10.day
=> false # Shouldn't this has been true

future_date - Date.today will always give you an answer in days (30)
You can see this by
future_date - Date.today
=> 30
So you can do...
future_date - Date.today > 10
And that will work fine.
10.days converts the interval into seconds: 864_000 (number of seconds in ten days) and 30 is not greater than 864_000!

Related

Ruby Comparing Time objects considers date. How to avoid that?

I have a model with a Time attribute
create_table "opened_intervals", force: :cascade do |t|
t.time "start"
t.time "end"
An example of a value would be:
>> oi.start
=> Sat, 01 Jan 2000 06:26:00 UTC +00:00
(I am living in Germany)
If I use the current time, I get following value:
current_time = Time.now
>> current_time
=> 2019-02-14 18:36:12 +0100
If I compare the class of both objects, I have
>> current_time.class
=> Time
>> oi.start.class
=> ActiveSupport::TimeWithZone
In order to make both instances same class, I change the Time class with the .zone method
>> Time.zone.now
=> Thu, 14 Feb 2019 17:38:48 UTC +00:00
>> Time.zone.now.class
=> ActiveSupport::TimeWithZone
Now both instances have same Class.
If I compare them, I get wrong results because of the date:
>> oi.start
=> Sat, 01 Jan 2000 06:26:00 UTC +00:00
>> current_time
=> Thu, 14 Feb 2019 17:40:13 UTC +00:00
>> current_time < oi.end
=> false
I thought about creating a new Time with the hours, minutes and seconds, but then I have the same problem, Ruby always appends a Date.
Of course I could extract the hour, the minutes, the time, create an integer and compare them, but it feels too much.
How can I deal with this issue the Ruby way?
The Time object is always a time with a date? Is there no other way to achieve a Time just with the time?
What would be the best approach for this problem?
I solved it this way:
def to_integer(time)
hours = time.hour < 10 ? "0#{time.hour}" : time.hour
minutes = time.min < 10 ? "0#{time.min}" : time.min
seconds = time.sec < 10 ? "0#{time.sec}" : time.sec
"#{hours}#{minutes}#{seconds}"
end
It feels too much (I could refactor it, but still too much)
to_integer(current_time) < to_integer(oi.end)
You can just modulus the days off to get the remaining time value.
t1 = Time.now
t2 = t2 = Time.at(Time.now.to_i - (5*24*3600)) # later time in day but previous date
tod1 = t.to_i % (24*3600)
=> 69734
tod2 = t2.to_i % (24*3600)
=> 69912
we can clearly see that t2 is a later time of day or clock time if you will and the modulus operation is very clear if you know anything about the unix epoch.

Time ago in words convert into system date-time

Trying to convert strings like 9 weeks ago, 1 year, 6 months ago, 20 hours ago into a ruby time object like Tue, 10 Mar 2015 12:06:15 PDT -07:00.
I've been doing this:
eval("10 days ago".gsub(' ', '.'))
This works fine, but for strings like 1 year, 6 months ago blows up.
I just need to do comparisons like:
eval("10 days ago".gsub(' ', '.')) < (Time.now - 7.days)
I'm using sinatra so no fancy rails helpers.
Please never use eval in production code..
Converting from timeago notation would be quite complex and resource intensive.
However, this way seems the least error prone: It will convert a string like "5 seconds ago" to "5S" and use mapping to find what it means in time, after which it will subtract that time from the current time.
The parse string is dynamically built so it can accomodate most every timeago notation.
require('date')
mapping = {"D"=> "%d","W"=>"%U","H"=>"%T","Y"=>"%Y","M"=>"%m","S"=>"%S"}
timerel = "1 year, 6 months ago".split(",").map { |n| n.gsub(/\s+/, "").upcase()[0,2].split('')}
Date.strptime(
timerel.map {|n| n[0]}.join(" "),
timerel.map {|n| mapping[n[1]]}.join(" ")
)
date = Date.new(0) + (Date.today - Date.strptime(timerel.map {|n| n[0]}.join(" "), timerel.map {|n| mapping[n[1]]}.join(" ")))
=> #<Date: 2014-10-10 ((2456941j,0s,0n),+0s,2299161j)>
It goes without saying that is very error prone. Use at your own risk:
def parse(date:)
eval(date.gsub(/ ?(,|and) ?/, '+').tr(' ', '.').gsub(/^(.*)(\.ago)$/, '(\1)\2'))
end
parse(date: '1 year, 6 months ago') # => Wed, 10 Sep 2014 21:29:11 BST +01:00
parse(date: '1 year, 6 months, 3 weeks, 6 days, 9 hours and 12 seconds ago')
# => Thu, 14 Aug 2014 12:33:07 BST +01:00
The idea is to convert the original string to:
'(1.year+6.months).ago'

Rails 3.2.8 - How do I get the week number from Rails?

I would like to know how to get the current week number from Rails and how do I manipulate it:
Translate the week number into date.
Make an interval based on week number.
Thanks.
Use strftime:
%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.now.strftime("%U").to_i # 43
# Or...
Date.today.strftime("%U").to_i # 43
If you want to add 43 weeks (or days,years,minutes, etc...) to a date, you can use 43.weeks, provided by ActiveSupport:
irb(main):001:0> 43.weeks
=> 301 days
irb(main):002:0> Date.today + 43.weeks
=> Thu, 22 Aug 2013
irb(main):003:0> Date.today + 10.days
=> Sun, 04 Nov 2012
irb(main):004:0> Date.today + 1.years # or 1.year
=> Fri, 25 Oct 2013
irb(main):005:0> Date.today + 5.months
=> Mon, 25 Mar 2013
You are going to want to stay away from strftime("%U") and "%W".
Instead, use Date.cweek.
The problem is, if you ever want to take a week number and convert it to a date, strftime won't give you a value that you can pass back to Date.commercial.
Date.commercial expects a range of values that are 1 based.
Date.strftime("%U|%W") returns a value that is 0 based. You would think you could just +1 it and it would be fine. The problem will hit you at the end of a year when there are 53 weeks. (Like what just happened...)
For example, let's look at the end of Dec 2015 and the results from your two options for getting a week number:
Date.parse("2015-12-31").strftime("%W") = 52
Date.parse("2015-12-31").cweek = 53
Now, let's look at converting that week number to a date...
Date.commercial(2015, 52, 1) = Mon, 21 Dec 2015
Date.commercial(2015, 53, 1) = Mon, 28 Dec 2015
If you blindly just +1 the value you pass to Date.commercial, you'll end up with an invalid date in other situations:
For example, December 2014:
Date.commercial(2014, 53, 1) = ArgumentError: invalid date
If you ever have to convert that week number back to a date, the only surefire way is to use Date.cweek.
date.commercial([cwyear=-4712[, cweek=1[, cwday=1[, start=Date::ITALY]]]]) → date
Creates a date object denoting the given week date.
The week and the day of week should be a negative
or a positive number (as a relative week/day from the end of year/week when negative).
They should not be zero.
For the interval
require 'date'
def week_dates( week_num )
year = Time.now.year
week_start = Date.commercial( year, week_num, 1 )
week_end = Date.commercial( year, week_num, 7 )
week_start.strftime( "%m/%d/%y" ) + ' - ' + week_end.strftime("%m/%d/%y" )
end
puts week_dates(22)
EG: Input (Week Number): 22
Output: 06/12/08 - 06/19/08
credit: Siep Korteling http://www.ruby-forum.com/topic/125140
Date#cweek seems to get the ISO-8601 week number (a Monday-based week) like %V in strftime (mentioned by #Robban in a comment).
For example, the Monday and the Sunday of the week I'm writing this:
[ Date.new(2015, 7, 13), Date.new(2015, 7, 19) ].map { |date|
date.strftime("U: %U - W: %W - V: %V - cweek: #{date.cweek}")
}
# => ["U: 28 - W: 28 - V: 29 - cweek: 29", "U: 29 - W: 28 - V: 29 - cweek: 29"]

Number of days between two Time instances

How can I determine the number of days between two Time instances in Ruby?
> earlyTime = Time.at(123)
> laterTime = Time.now
> time_difference = laterTime - earlyTime
I'd like to determine the number of days in time_difference (I'm not worried about fractions of days. Rounding up or down is fine).
Difference of two times is in seconds. Divide it by number of seconds in 24 hours.
(t1 - t2).to_i / (24 * 60 * 60)
require 'date'
days_between = (Date.parse(laterTime.to_s) - Date.parse(earlyTime.to_s)).round
Edit ...or more simply...
require 'date'
(laterTime.to_date - earlyTime.to_date).round
earlyTime = Time.at(123)
laterTime = Time.now
time_difference = laterTime - earlyTime
time_difference_in_days = time_difference / 1.day # just divide by 1.day
[1] pry(main)> earlyTime = Time.at(123)
=> 1970-01-01 01:02:03 +0100
[2] pry(main)> laterTime = Time.now
=> 2014-04-15 11:13:40 +0200
[3] pry(main)> (laterTime.to_date - earlyTime.to_date).to_i
=> 16175
To account for DST (Daylight Saving Time), you'd have to count it by the days. Note that this assumes less than a day is counted as 1 (rounded up):
num = 0
cur = start_time
while cur < end_time
num += 1
cur = cur.advance(:days => 1)
end
return num
Here is a simple answer that works across DST:
numDays = ((laterTime - earlyTime)/(24.0*60*60)).round
60*60 is the number of seconds in an hour
24.0 is the number of hours in a day. It's a float because some days are a little more than 24 hours, some are less. So when we divide by the number of seconds in a day we still have a float, and round will round to the closest integer.
So if we go across DST, either way, we'll still round to the closest day. Even if you're in some weird timezone that changes more than an hour for DST.
in_days (Rails 6.1+)
Rails 6.1 introduces new ActiveSupport::Duration conversion methods like in_seconds, in_minutes, in_hours, in_days, in_weeks, in_months, and in_years.
As a result, now, your problem can be solved as:
date_1 = Time.parse('2020-10-18 00:00:00 UTC')
date_2 = Time.parse('2020-08-13 03:35:38 UTC')
(date_2 - date_1).seconds.in_days.to_i.abs
# => 65
Here is a link to the corresponding PR.
None of these answers will actually work if you don't want to estimate and you want to take into account daylight savings time.
For instance 10 AM on Wednesday before the fall change of clocks and 10 AM the Wednesday afterwards, the time between them would be 1 week and 1 hour. During the spring it would be 1 week minus 1 hour.
In order to get the accurate time you can use the following code
def self.days_between_two_dates later_time, early_time
days_between = (later_time.to_date-early_time.to_date).to_f
later_time_time_of_day_in_seconds = later_time.hour*3600+later_time.min*60+later_time.sec
earlier_time_time_of_day_in_seconds = early_time.hour*3600+early_time.min*60+early_time.sec
days_between + (later_time_time_of_day_in_seconds - early_time_time_of_day_in_seconds)/1.0.day
end

get decimal amount of months for date range

I try to get a decimal amount of months for a date range. Example:
ruby-1.9.2-p0 > from = Date.new(2011, 7, 6)
=> Wed, 06 Jul 2011
ruby-1.9.2-p0 > to = Date.new(2011, 8, 31)
=> Wed, 31 Aug 2011
ruby-1.9.2-p0 > to - from
=> (56/1)
So the difference is 56 days. But I want and need the amount of months: 1.83
I have created the following piece of code which returns the correct result but doesn't feel like the ruby way:
months = Hash.new
(from..to).each do |date|
unless months.key? date.beginning_of_month
months[date.beginning_of_month] = 1
else
months[date.beginning_of_month] += 1
end
end
multiplicator = 0.0
months.each do |month, days|
multiplicator += days.to_f/month.end_of_month.day
end
return multiplicator.floor_to(2)
To be honest: It looks ugly and really inefficient. But I just cannot figure out any easier way.
Can you help me to find a better solution?
For further questions feel free to ask me.
Many thanks in advance!
Update/Solution: Solved the problem with the following piece of code:
months = 0.0
months += ((date_to < date_from.end_of_month ? date_to : date_from.end_of_month) - date_from + 1) / Time.days_in_month(date_from.month)
unless date_to.month == date_from.month
months += (date_to - date_to.beginning_of_month + 1) / Time.days_in_month(date_to.month)
months += date_to.month - date_from.month - 1
end
return months.floor_to(2)
a better way to do would be summation of
number of days left in from / number of days in from
number of days completed in to / number of days in to
number of months between from and to (from, to excluded)
This way you wont have iterations to do

Resources