Set time part of DateTime in ruby - ruby

Say I have a datetime object eg DateTime.now. I want to set hours and minutes to 0 (midnight). How can I do that?

Within a Rails environment:
Thanks to ActiveSupport you can use:
DateTime.now.midnight
DateTime.now.beginning_of_day
OR
DateTime.now.change({ hour: 0, min: 0, sec: 0 })
# More concisely
DateTime.now.change({ hour: 0 })
Within a purely Ruby environment:
now = DateTime.now
DateTime.new(now.year, now.month, now.day, 0, 0, 0, now.zone)
OR
now = DateTime.now
DateTime.parse(now.strftime("%Y-%m-%dT00:00:00%z"))

Nevermind, got it. Need to create a new DateTime:
DateTime.new(now.year, now.month, now.day, 0, 0, 0, 0)

Warning: DateTime.now.midnight and DateTime.now.beginning_of_day return the same value (which is the zero hour of the current day - midnight does not return 24:00:00 as you would expect from its name).
So I am adding this as further info for anyone who might use the accepted answer to calculate midnight x days in the future.
For example, a 14 day free trial that should expire at midnight on the 14th day:
DateTime.now.midnight + 14.days
is the morning of the 14th day, which equates to a 13.x day trial (x is the part of the day left over - if now is noon, then it's 13.5 day trial).
You would actually need to do this:
DateTime.now.midnight + 15.days
to get midnight on the 14th day.
For this reason I always prefer to use beginning_of_day, since that is 00:00:00. Using midnight can be misleading/misunderstood.

If you use it often consider install this gem to improve date parse:
https://github.com/mojombo/chronic
require 'chronic'
Chronic.parse('this 0:00')

Related

Get the most recent "Monday 05:30" Unix time in Go?

Is there a Time library function or another good way to get the most recent weekday, hour, minute combination in Unix time? For example, given:
day: Monday
hour: 11
minute: 30
And it's currently Tuesday, I want the Unix time for yesterday at 11:30am. Some languages / libraries have support for thing but I cannot find something in Go that would make this easy. Any advice?
If you look at the definition of Weekday, you'll see that Sunday is 0, Monday is 1, etc. So to get to the last Monday, you have to do go back today-Monday, and move back to 11:30 that day. However, this doesn't work if it is already Monday and before 11:30, so you need to check for that:
now:=time.Now()
dayOffset:=now.Weekday()-time.Monday
targetDate:=now.AddDate(0,0,-int(dayOffset))
targetDate=time.Date(targetDate.Year(),targetDate.Month(),targetDate.Day(),11,30,0,0,targetDate.Location())
if targetDate.After(now) {
targetDate=targetDate.AddDate(0,0,-7)
}
My attempt at a simple and intuitive solution. See code comments for explanation
package main
import (
"fmt"
"time"
)
func main() {
// The target day / hour / minute
const (
day = time.Monday
hour = 21
minute = 0
)
// The starting time to work from
startTime := time.Now()
// Create the time on that day with the desired hour and minute
t := time.Date(startTime.Year(), startTime.Month(), startTime.Day(), hour, minute, 0, 0, startTime.Location())
// As long as we're on the wrong weekday, or not before the start time, back up one day
for t.Weekday() != day || !t.Before(startTime){
t = t.AddDate(0, 0, -1)
}
// Print the time, and it's Unix timestamp.
fmt.Println(t, t.Unix())
}

Get UTC offset for Timezone at given date via Ruby/tzinfo?

This is probably trivial for anybody who knows the tzinfo API:
Given a Timezone object from tzinfo, how can I get the UTC offset at a given point in time (given either in local time of the Timezone or UTC)?
You can use the period_for_local method. For these examples, I'm using the timezone I live in (America/Sao_Paulo), in where the offset is -03:00 during winter (March to October) and -02:00 during summer (Daylight Saving Time):
# Sao Paulo timezone
zone = TZInfo::Timezone.new('America/Sao_Paulo')
# date in January (Brazilia Summer Time - DST)
d = DateTime.new(2017, 1, 1, 10, 0)
period = zone.period_for_local(d)
puts period.offset.utc_total_offset / 3600.0
# date in July (Brazilia Standard Time - not in DST)
d = DateTime.new(2017, 7, 1, 10, 0)
period = zone.period_for_local(d)
puts period.offset.utc_total_offset / 3600.0
The output is:
-2.0
-3.0
The utc_total_offset method returns the offset in seconds, so I divided by 3600 to get the value in hours.
Note that I also used 3600.0 to force the results to be a float. If I just use 3600, the results will be rounded and timezones like Asia/Kolkata (which has an offset of +05:30) will give incorrect results (5 instead of 5.5).
Note that you must be aware of DST changes, because you can have either a gap or a overlap.
In São Paulo timezone, DST starts at October 15th 2017: at midnight, clocks shift forward to 1 AM (and offset changes from -03:00 to -02:00), so all the local times between 00:00 and 01:00 are not valid. In this case, if you try to get the offset, it will get a PeriodNotFound error:
# DST starts at October 15th, clocks shift from midnight to 1 AM
d = DateTime.new(2017, 10, 15, 0, 30)
period = zone.period_for_local(d) # error: TZInfo::PeriodNotFound
When DST ends, at February 18th 2018, at midnight clocks shift back to 11 PM of 17th (and offset changes from -02:00 to -03:00), so the local times between 11 PM and midnight exist twice (in both offsets).
In this case, you must specify which one you want (by setting the second parameter of period_for_local), indicating if you want the offset for DST or not:
# DST ends at February 18th, clocks shift from midnight to 11 PM of 17th
d = DateTime.new(2018, 2, 17, 23, 30)
period = zone.period_for_local(d, true) # get DST offset
puts period.offset.utc_total_offset / 3600.0 # -2.0
period = zone.period_for_local(d, false) # get non-DST offset
puts period.offset.utc_total_offset / 3600.0 # -3.0
If you don't specify the second parameter, you'll get a TZInfo::AmbiguousTime error:
# error: TZInfo::AmbiguousTime (local time exists twice due do DST overlap)
period = zone.period_for_local(d)
It seems in Ruby 1.9.3 there is some hackery (DateTime to Time) involved, with possible loss of precision, but this is my result based on the answer from #Hugo:
module TZInfo
class Timezone
def utc_to_local_zone(dateTime)
return dateTime.to_time.getlocal(self.period_for_utc(dateTime).utc_total_offset)
end
def offset_to_s(dateTime, format = "%z")
return utc_to_local_zone(dateTime).strftime(format)
end
end
end

Get seconds from the day to Thursday 10:00 at that week

How do I get the seconds from the day to Thursday 10:00 at that week? If later than Thursday 10:00, I want to get zero. For example:
seconds = (Thursday 10:00) - Time.now
Use Chronic:
require 'chronic'
Chronic.parse('this Thursday at 10:00 am') - Time.now
#=> 98688.251918432
You can subtract two time to get difference in seconds (see docs):
require 'time'
Time.parse(end_time) - Time.parse(time)
# => 57600.0
Update
To calculate difference between two time getting two fixed time is an absolute must. You can get time for next week simply by adding numeric time difference in seconds to existing time. Here:
next_week_time = Time.parse(end_time) + (1*7*24*60*60)
Or if you are on Rails, with ActiveSupport you can simply do:
next_week_time = Time.parse(end_time) + 1.weeks
(4-Time.now.wday-1)*24*3600: get the number of days from the day morning to Thursday of the week.
Time.now.seconds_until_end_of_day: get the rest seconds of the day.
seconds = (4-Time.now.wday-1)*24*3600 + Time.now.seconds_until_end_of_day + 10*3600
seconds = seconds > 0 ? seconds : 0

How to compare two Time objects only down to the hour

I want to compare two Time objects only down to the hour, while ignoring the difference of minutes and seconds.
I'm currently using t0.strftime("%Y%m%d%H") == t1.strftime("%Y%m%d%H"), but I don't think this is a good solution.
Is there better way to do this?
You can use this trick in pure Ruby
t0.to_a[2..9] == t1.to_a[2..9]
Where Time#to_a
$> Time.now.to_a
# => [7, 44, 2, 8, 3, 2014, 6, 67, false, "GMT"]
# [ sec, min, hour, day, month, year, wday, yday, isdst, zone ]
So you can check that the times are equals or not up to the level you want and without missing important components of the object like the zone, etc.
If you have ActiveSupport (either through Rails, or just installed as a gem), it includes an extension to the Time class that adds a change method which will truncate times:
$> require "active_support/core_ext/time"
# => true
$> t = Time.now
# => 2014-03-07 21:30:01 -0500
$> t.change(hour: 0)
# => 2014-03-07 00:00:00 -0500
This won't modify the original time value either. So you can do this:
t0.change(minute: 0) == t1.change(minute: 0)
It'll zero out everything at a lower granularity (seconds, etc.).
require 'time'
t1 = Time.new ; sleep 30 ; t2 = Time.new
t1.hour == t2.hour
This should give you a boolean answer.

How do I calculate the offset, in hours, of a given timezone from UTC in ruby?

I need to calculate the offset, in hours, of a given timezone from UTC in Ruby. This line of code had been working for me, or so I thought:
offset_in_hours = (TZInfo::Timezone.get(self.timezone).current_period.offset.utc_offset).to_f / 3600.0
But, it turns out that was returning to me the Standard Offset, not the DST offset. So for example, assume
self.timezone = "America/New_York"
If I run the above line, offset_in_hours = -5, not -4 as it should, given that the date today is April 1, 2012.
Can anyone advise me how to calculate offset_in_hours from UTC given a valid string TimeZone in Ruby that accounts for both standard time and daylight savings?
Thanks!
Update
Here is some output from IRB. Note that New York is 4 hours behind UTC, not 5, because of daylight savings:
>> require 'tzinfo'
=> false
>> timezone = "America/New_York"
=> "America/New_York"
>> offset_in_hours = TZInfo::Timezone.get(timezone).current_period.utc_offset / (60*60)
=> -5
>>
This suggests that there is a bug in TZInfo or it is not dst-aware
Update 2
Per joelparkerhender's comments, the bug in the above code is that I was using utc_offset, not utc_total_offset.
Thus, per my original question, the correct line of code is:
offset_in_hours = (TZInfo::Timezone.get(self.timezone).current_period.offset.utc_total_offset).to_f / 3600.0
Yes, use TZInfo like this:
require 'tzinfo'
tz = TZInfo::Timezone.get('America/Los_Angeles')
To get the current period:
current = tz.current_period
To find out if daylight savings time is active:
current.dst?
#=> true
To get the base offset of the timezone from UTC in seconds:
current.utc_offset
#=> -28800 which is -8 hours; this does NOT include daylight savings
To get the daylight savings offset from standard time:
current.std_offset
#=> 3600 which is 1 hour; this is because right now we're in daylight savings
To get the total offset from UTC:
current.utc_total_offset
#=> -25200 which is -7 hours
The total offset from UTC is equal to utc_offset + std_offset.
This is the offset from the local time where daylight savings is in effect, in seconds.

Resources