Ruby Time object for midnight UTC - ruby-on-rails

I need to be able to create UTC Time objects in Rails, but no matter what I try it ends up being a local Time object converted to UTC.
application.rb
config.time_zone = "UTC"
Examples
I try to create a Time object for midnight on New Years 2017:
Time.new(year, month, 1, 0, 0, 0)
=> 2017-03-01 00:00:00 -0500
Time.new(year, month, 1, 0, 0, 0).in_time_zone
=> Wed, 01 Mar 2017 05:00:00 UTC +00:00
Time.new(year, month, 1, 0, 0, 0).in_time_zone('UTC')
=> Wed, 01 Mar 2017 05:00:00 UTC +00:00

You should be able to use:
Time.utc(year, month, 1, 0, 0, 0)
or just
Time.utc(year, month, 1)

I think the following might do what you want:
Time.new(year, month, 1, 0, 0, 0, "+00:00")
See the documentation for more details.

Related

How to use DateComponents to represent the extra 1 hour period at the end of day light saving?

Usually, during end of day light saving, we will be gaining extra 1 hours.
Take Tehran timezone as an example.
During 22 September 2021, Tehran will backward by 1 hour from 00:00
AM, to 11:00 PM.
I wrote the following code to demonstrate such.
import UIKit
func date(year: Int, month: Int, day: Int, hour: Int, minute: Int, second: Int) -> Date {
var dateComponents = DateComponents()
dateComponents.year = year
dateComponents.month = month
dateComponents.day = day
dateComponents.hour = hour
dateComponents.minute = minute
dateComponents.second = second
let date = Calendar.current.date(from: dateComponents)!
return date
}
// During 22 September 2021, Tehran will backward by 1 hour from 00:00 AM, to 11:00 PM.
let tehranTimeZone = TimeZone(identifier: "Asia/Tehran")!
let oldDefault = NSTimeZone.default
NSTimeZone.default = tehranTimeZone
defer {
NSTimeZone.default = oldDefault
}
let date1 = date(year: 2021, month: 09, day: 21, hour: 23, minute: 59, second: 59)
let date2 = date(year: 2021, month: 09, day: 22, hour: 00, minute: 00, second: 00)
let date3 = date(year: 2021, month: 09, day: 22, hour: 00, minute: 00, second: 01)
// STEP 1: 2021 Sep 21 23:59:59 => 1632252599.0, Tuesday, September 21, 2021 at 11:59:59 PM Iran Daylight Time
print("STEP 1: 2021 Sep 21 23:59:59 => \(date1.timeIntervalSince1970), \(date1.description(with: .current))")
// STEP 2: 2021 Sep 22 00:00:00 => 1632256200.0, Wednesday, September 22, 2021 at 12:00:00 AM Iran Standard Time
print("STEP 2: 2021 Sep 22 00:00:00 => \(date2.timeIntervalSince1970), \(date2.description(with: .current))")
// STEP 3: 2021 Sep 22 00:00:01 => 1632256201.0, Wednesday, September 22, 2021 at 12:00:01 AM Iran Standard Time
print("STEP 3: 2021 Sep 22 00:00:01 => \(date3.timeIntervalSince1970), \(date3.description(with: .current))")
From STEP 1 transits to STEP 2, instead for their timeIntervalSince1970 different by +1 seconds, their difference are +3601 seconds, due to the extra 1 hour gain.
Now, my question is, how can we use DateComponents to represent the extra 1 hour period at the end of day light saving?
In another, how can I use DateComponents to generate a Date which is capable to print the following?
2021 Sep 21 23:00:00 => 1632252600.0, Tuesday, September 21, 2021 at 11:00:00 PM Iran Standard Time
Now, we understand that, in Tehran, during 2021 Sept 21, there are 2 type of 23:00:00 time
23:00:00 Iran Daylight time (Epoch is 1632249000)
23:00:00 Iran Standard time (Epoch is 1632252600)
23:00:00 Iran Daylight time (Epoch is 1632249000)
I can represent the above using
let date = date(year: 2021, month: 09, day: 21, hour: 23, minute: 00, second: 00)
23:00:00 Iran Standard time (Epoch is 1632252600)
I have no idea how to represent the above. As, I do not find a way in DateComponents, to enable us to specific whether the local time is belong to standard time, or daylight time.
DateComponents do not have a time zone. The time zone comes into it when you convert DateComponents to a Date, using the call Calendar.date(from:). It's the calendar's time zone that determines how those DateComponents are converted to a Date.
Instead of using Calendar.current, create a custom calendar and set it to the IRST time zone. (I couldn't figure out the time zone for Iran Daylight time. I would have expected it to have the abbreviation "IRDT", but that doesn't work.)
Let's say we have a calendar irstCalendar that's set to Iran Standard Time ("IRST").
If you use irstCalendar.date(from: dateComponents) you'll always get the Date based on standard time.
Consider this code:
func date(year: Int, month: Int, day: Int, hour: Int, minute: Int, second: Int, calendar: Calendar = Calendar.current) -> Date {
var dateComponents = DateComponents()
dateComponents.year = year
dateComponents.month = month
dateComponents.day = day
dateComponents.hour = hour
dateComponents.minute = minute
dateComponents.second = second
let date = calendar.date(from: dateComponents)!
return date
}
guard let tehranStandardTimeZone = TimeZone(abbreviation: "IRST") else {
fatalError("Can't create time zones")
}
var tehranSTCalendar = Calendar(identifier: .gregorian)
tehranSTCalendar.timeZone = tehranStandardTimeZone
let tehranDateFormatter = DateFormatter()
tehranDateFormatter.dateStyle = .medium
tehranDateFormatter.timeStyle = .medium
tehranDateFormatter.timeZone = tehranStandardTimeZone
let date1 = date(year: 2021, month: 09, day: 21, hour: 23, minute: 59, second: 59, calendar: tehranSTCalendar)
let date2 = date(year: 2021, month: 09, day: 22, hour: 00, minute: 00, second: 00, calendar: tehranSTCalendar)
let date3 = date(year: 2021, month: 09, day: 22, hour: 00, minute: 00, second: 01, calendar: tehranSTCalendar)
print("STEP 1: 2021 Sep 21 23:59:59 => \(date1.timeIntervalSince1970), \(tehranDateFormatter.string(from:date1))")
print("STEP 2: 2021 Sep 22 00:00:00 => \(date2.timeIntervalSince1970), \(tehranDateFormatter.string(from:date2))")
print("STEP 3: 2021 Sep 22 00:00:01 => \(date3.timeIntervalSince1970), \(tehranDateFormatter.string(from:date3))")
That outputs:
STEP 1: 2021 Sep 21 23:59:59 => 1632252599.0, Sep 21, 2021 at 11:59:59 PM
STEP 2: 2021 Sep 22 00:00:00 => 1632256200.0, Sep 22, 2021 at 12:00:00 AM
STEP 3: 2021 Sep 22 00:00:01 => 1632256201.0, Sep 22, 2021 at 12:00:01 AM

Wrong date from components

So this is interesting:
po Calendar.default.date(from: DateComponents(year: 2022, month: 1, hour: 16, minute: 1, second: 1, weekday: 1, weekOfMonth: 1))
▿ Optional<Date>
▿ some : 2021-12-26 23:01:01 +0000
- timeIntervalSinceReferenceDate : 662252461.0
I'm expecting January 1st 2022, but I'm getting December 26th 2021? Why is it doing this? Am I doing something wrong?
It's because the date components you are providing are contradictory. You have weekDay 1 (which will probably be a Sunday or Monday, depending on your locale) but the 1 jan 2022 is a Saturday.
(you also used Calendar.default when I think you meant Calendar.current?)
If you take out the weekDay term you will get the correct answer:
Calendar.current.date(from: DateComponents(year: 2022, month: 1, hour: 16, minute: 1, second: 1, weekOfMonth: 1))
// "Jan 1, 2022 at 4:01 PM"
You could also remove the weekOfMonth term as it is superfluous when you are specifying the actual date.

Calculate time from an array and convert to time zone

I have an array bed_times with Time instances in UTC format:
bed_times = [
Time.utc(2015, 12, 10, 5, 58, 24),
Time.utc(2015, 12, 9, 3, 35, 28),
Time.utc(2015, 12, 8, 6, 32, 26),
Time.utc(2015, 12, 7, 1, 43, 28),
Time.utc(2015, 12, 5, 7, 49, 30),
Time.utc(2015, 12, 04, 7, 2, 30)
]
#=> [2015-12-10 05:58:24 UTC,
# 2015-12-09 03:35:28 UTC,
# 2015-12-08 06:32:26 UTC,
# 2015-12-07 01:43:28 UTC,
# 2015-12-05 07:49:30 UTC,
# 2015-12-04 07:02:30 UTC]
I am trying to get the average bedtime, but I'm not getting the correct result
ave = Time.at(bed_times.map(&:to_f).inject(:+) / bed_times.size)
result is
2015-12-07 01:26:57 -0800
which is not correct. Also, I want to then convert the average time to a different time zone
I tried
Time.zone = 'Pacific Time (US & Canada)'
Time.zone.parse(ave.to_s)
2015-12-07 01:26:57 -0800
This is not correct either.
You have to calculate the average on the gap from midnight.
A not elegant (but fast) solution could be:
# Keep only time
bed_times.map! { |bt| Time.parse(bt.split(" ")[1]) }
# calculate the gap from 00:00:00
gap_from_midnight = bed_times.map do |bt|
if bt > Time.parse("12:00:00")
gap = (bt.to_f - Time.parse("24:00:00").to_f)
else
gap = (bt.to_f - Time.parse("00:00:00").to_f)
end
gap.to_i
end
# average in sec
avg_in_sec = gap_from_midnight.inject(:+) / bed_times.size
# average in UTC time zone
avg = Time.at(avg_in_sec).utc # => 1970-01-01 05:26:57 UTC (result for bed_times array)
# average in PST time zone (see note)
avg_pst = Time.parse(avg.to_s).in_time_zone("Pacific Time (US & Canada)") # => Wed, 31 Dec 1969 21:26:57 PST -08:00 (result for bed_times array)
# Keep only time
avg_pst.strftime("%H:%M:%S") # => "21:26:57" (result for bed_times array)
With your bed_times array (with the values as a string)
bed_times = [
"2015-12-10 05:58:24 UTC",
"2015-12-09 03:35:28 UTC",
"2015-12-08 06:32:26 UTC",
"2015-12-07 01:43:28 UTC",
"2015-12-05 07:49:30 UTC",
"2015-12-04 07:02:30 UTC"
]
the average is :
05:26:57 in UTC zone
21:26:57 in PST zone
With another array like this
bed_times = [
"2015-12-10 01:00:00 UTC",
"2015-12-09 23:00:00 UTC",
"2015-10-19 18:00:00 UTC",
]
the average is:
22:00:00 in UTC zone
14:00:00 in PST zone
note: .in_time_zone is a helper from ActiveSupport::TimeWithZone
http://api.rubyonrails.org/classes/ActiveSupport/TimeWithZone.html
As I understand, you are given an array arr that contains the UTC times of bedtimes in areas that are on PST time. You wish to compute the average bedtime.
Code
def avg_bedtime(arr)
avg = Time.at(arr.reduce(0) do |t,s|
lt = Time.parse(s).localtime("-08:00")
t + Time.new(2000, 1, (lt.hour < 12) ? 2 : 1, lt.hour, lt.min, lt.sec, "-08:00").to_f
end/arr.size)
"%d:%d:%d" % [avg.hour, avg.min, avg.sec]
end
Example
arr = ["2015-12-10 08:58:24 UTC", "2015-12-09 03:35:28 UTC",
"2015-12-08 06:32:26 UTC", "2015-12-07 01:43:28 UTC",
"2015-12-05 07:49:30 UTC", "2015-12-04 07:02:30 UTC"]
I've changed the first time in this array to make it more interesting.
avg_bedtime(arr)
#=> "21:56:57"
Explanation
Let's begin by converting these strings to time objects:
utc = arr.map { |s| Time.parse(s) }
#=> [2015-12-10 08:58:24 UTC, 2015-12-09 03:35:28 UTC, 2015-12-08 06:32:26 UTC,
# 2015-12-07 01:43:28 UTC, 2015-12-05 07:49:30 UTC, 2015-12-04 07:02:30 UTC]
Recalling that PST is 8 hours later than UTC, we can use Time.localtime to convert to PST:
pst = utc.map { |t| t.localtime("-08:00") }
#=> [2015-12-10 00:58:24 -0800, 2015-12-08 19:35:28 -0800,
# 2015-12-07 22:32:26 -0800, 2015-12-06 17:43:28 -0800,
# 2015-12-04 23:49:30 -0800, 2015-12-03 23:02:30 -0800]
I will refer to bedtimes before noon to be "late" bedtimes and those later in the day to be "early" bedtimes. (This is of course arbitrary. If, for example, some of the individuals are shift workers, this could be a problem.) As you see, the first element of pst is a late bedtime and all others are early bedtimes.
I will now convert these time objects to time objects having the same time of day but a different date. Early bedtime objects will be assigned an arbitrary date (say, January 1, 2000) and late bedtime objects will be one day later (January 2, 2000):
adj = pst.map { |t| Time.new(2000, 1, (t.hour < 12) ? 2 : 1, t.hour, t.min, t.sec, "-08:00") }
#=> [2000-01-02 00:58:24 -0800, 2000-01-01 19:35:28 -0800, 2000-01-01 22:32:26 -0800,
# 2000-01-01 17:43:28 -0800, 2000-01-01 23:49:30 -0800, 2000-01-01 23:02:30 -0800]
We can now convert the time objects to seconds since the epoch:
secs = adj.map { |t| t.to_f }
#=> [946803504.0, 946784128.0, 946794746.0, 946777408.0, 946799370.0, 946796550.0]
compute the average:
avg = secs.reduce(:+)/arr.size
#=> 946792617.6666666
convert back to a time object for the PST zone:
tavg = Time.at(avg)
#=> 2000-01-01 21:56:57 -0800
and, lastly, extract the time of day:
"%d:%d:%d" % [tavg.hour, tavg.min, tavg.sec]
# "21:56:57

Merge hash with another hash

I have two hash like
h1 = {
DateTime.new(2015, 7, 1),in_time_zone => 0,
DateTime.new(2015, 7, 2).in_time_zone => 10,
DateTime.new(2015, 7, 4).in_time_zone => 20,
DateTime.new(2015, 7, 5).in_time_zone => 5
}
h2 = {
DateTime.new(2015, 7, 1).in_time_zone => 0,
DateTime.new(2015, 7, 2).in_time_zone => 0,
DateTime.new(2015, 7, 3).in_time_zone => 0
}
I want to merge h1 and h2, don't merge if key already exist, so that will result look like (datetime format with time zone shortened for readability)
result
#=> {
# Wed, 01 Jul 2015 01:00:00 EST +01:00 => 0,
# Thu, 02 Jul 2015 01:00:00 EST +01:00 => 10,
# Fri, 03 Jul 2015 01:00:00 EST +01:00 => 0,
# Sat, 04 Jul 2015 01:00:00 EST +01:00 => 20,
# Sun, 05 Jul 2015 01:00:00 EST +01:00 => 5
# }
I have tried with h1.merge(h2) and h2.merge(h1) but it can be put key and value of h2 to h1.
arr = []
h = h1.merge(h2)
h.each{|k, v| arr.include?(v) ? h.delete(k) : arr << v }
#=> {#<DateTime: 2015-07-01T00:00:00+00:00 ((2457205j,0s,0n),+0s,2299161j)>=>0,
#<DateTime: 2015-07-04T00:00:00+00:00 ((2457208j,0s,0n),+0s,2299161j)>=>20,
#<DateTime: 2015-07-05T00:00:00+00:00 ((2457209j,0s,0n),+0s,2299161j)>=>5}
You will have only three key-value pairs, not 5 as you expect, because hash in Ruby is collection of unique keys and their values.

How to output a date range for each quarter of a year in Ruby?

Thanks to some people on this board I was able to come up with a function that returns a number of date ranges:
years = [2013, 2012, 2011, 2010, 2009]
def month_ranges
years.flat_map { |y|
12.downto(1).map { |m| Date.new(y,m,1)..Date.new(y,m,-1) }
}
end
# =>
[
01 Dec 2013..31 Dec 2013,
01 Nov 2013..31 Nov 2013,
01 Oct 2013..31 Oct 2013,
01 Sep 2013..31 Sep 2013,
01 Aug 2013..31 Aug 2013,
....
]
Now, is there a way to return the four quarters of a year as well?
So the output will be something like:
# =>
[
01 Oct 2013..31 Dec 2013,
01 Jul 2013..31 Sep 2013,
01 Apr 2013..31 Jun 2013,
01 Jan 2013..31 Mar 2013
]
(Note: If a month has 30 or 31 days doesn't really matter in this case.)
Thanks to anyone who can help.
This should work (based on month_ranges, i.e. last quarter comes first):
def quarter_ranges
years.flat_map { |y|
3.downto(0).map { |q|
Date.new(y, q * 3 + 1, 1)..Date.new(y, q * 3 + 3, -1)
}
}
end
Or a bit more verbose and maybe easier to understand:
def quarter_ranges
years.flat_map { |y|
[
Date.new(y, 10, 1)..Date.new(y, 12, -1),
Date.new(y, 7, 1)..Date.new(y, 9, -1),
Date.new(y, 4, 1)..Date.new(y, 6, -1),
Date.new(y, 1, 1)..Date.new(y, 3, -1)
]
}
end
You can use beginning_of_quarter and end_of_quarter to define quarters.
For example, if I want to group a date_range according to quarters I could do the following:
((Date.today - 1.year)..Date.today).group_by(&:beginning_of_quarter)
The keys in this case are the beginning of each quarter:
((Date.today - 1.year)..Date.today).group_by(&:beginning_of_quarter).keys
=> [Sun, 01 Jul 2012, Mon, 01 Oct 2012, Tue, 01 Jan 2013, Mon, 01 Apr 2013, Mon, 01 Jul 2013]
What about something like this:
> now = Time.now.beginning_of_month
=> 2013-09-01 00:00:00 +0200
> now..(now + 3.months)
=> 2013-09-01 00:00:00 +0200..2013-12-01 00:00:00 +0100
I'd do something like:
require 'date'
def quarters(y)
q = []
(1..4).each do |s|
q << (Date.new(y, s * 3 - 1, 1)..Date.new(y, s * 3, -1))
end
return q
end

Resources