Rails - Convert time to string - ruby-on-rails

This is the time format I want to convert
time = Time.parse('2020-07-02 03:59:59.999 UTC')
#=> 2020-07-02 03:59:59 UTC
I want to convert to string in this format.
"2020-07-02T03:59:59.999Z"
I have tried.
time.strftime("%Y-%m-%dT%H:%M:%S.999Z")
Is this correct? Any better way?

You can just use Time#iso8601 with the desired number of fraction digits as an argument:
time = Time.current.end_of_hour
time.iso8601(3) #=> "2020-07-01T10:59:59.999Z"

If you want to handle the output format explicitly via strftime, there are some things to keep in mind:
Instead of hard-coding 999, you should use %L to get the actual milliseconds:
time = Time.parse('2020-07-02 03:59:59.999 UTC')
#=> 2020-07-02 03:59:59 UTC
time.strftime('%Y-%m-%dT%H:%M:%S.%LZ')
#=> "2020-07-02T03:59:59.999Z"
Use combinations for common formats, e.g. %F for %Y-%m-%d and %T for %H:%M:%S:
time.strftime('%FT%T.%LZ')
#=> "2020-07-02T03:59:59.999Z"
If you are dealing with time zones other than UTC (maybe your machine's local time zone), make sure to convert your time instance to utc first:
time = Time.parse('2020-07-02 05:59:59.999+02:00')
#=> 2020-07-02 05:59:59 +0200
time.utc
#=> 2020-07-02 03:59:59 UTC
time.strftime('%FT%T.%LZ')
#=> "2020-07-02T03:59:59.999Z"
or to use %z / %:z to append the actual time zone offset:
time = Time.parse('2020-07-02 05:59:59.999+02:00')
time.strftime('%FT%T.%L%:z')
#=> "2020-07-02T05:59:59.999+02:00"

For APIs you should use utc.iso8601:
> timestamp = Time.now.utc.iso8601
=> "2015-07-04T21:53:23Z"
See:
https://thoughtbot.com/blog/its-about-time-zones#working-with-apis

Related

Moment Display Date according to ISO-8601 Timezone Offset

I am getting a ISO-8601 date string from an API response as follows :
var x1 = 2022-06-22T05:30:00+05:30
or it could be
var x2 = 2022-06-22T08:30:00-05:00
Irrespective of browser timezone I should display the dates as
X1 - 2022-06-22 05:30:30 IST
X2 - 2022-06-22 08:30:00 EST
How can i parse timezone from the offset like -05:00 or +05:30 using moment or luxon?
I tried moment.tz(timestamp) but it defaults to UTC since it needs the second argument.
So i did a bit more digging.
Logically what i want is not possible.
Many timezones shares UTC offset. Hence, there could be ambiguity, if we try to convert an offset to a TimeZone without any other additional info
Hence, I am opting for a solution, where my API response sends a timezone metadata for each date/time field. (I have lat/long info to convert in Tz in my backend)
In front End, i will simply use the timezone info to format my moment object into a desired date-time String.
For example
const timestring = '2022-06-22T00:00:00+00:00';
const timezoneName = "America/Chicago" ;
moment.tz(timestring, timezoneName).format('YYYY-DD-MM hh:mm:ss z');
// Output: 2022-06-21 07:00:00 CDT
Source : https://stackoverflow.com/tags/timezone/info

Parse a date with too many days in month

Is there any way to correctly parse a date that has too many days in the month? E.g.
'2016-01-32' = '2016-02-01'
'2015-12-32' = '2016-01-01'
This is to support easy date manipulation on some of my JS based front-end.
This is with Ruby 2.3.0 and Rails 4.2.5. I think this was working in an earlier release - perhaps the parsing rules were tightened?
I'd rather rely on a standard library and it's knowledge of leap years etc, rather than implementing this myself.
You can use Date::_parse (or Date::_iso8601) to parse a date string without validation:
require 'date'
h = Date._parse('2016-01-32')
#=> {:mday=>32, :year=>2016, :mon=>1}
And create a date instance using Simone Carletti's suggestion:
Date.new(h[:year], h[:mon]).next_day(h[:mday] - 1)
#=> #<Date: 2016-02-01 ...>
If both, days and months can be out of range, you could use:
h = Date._parse('2016-14-32')
#=> {:mday=>32, :year=>2016, :mon=>14}
Date.new(h[:year]).next_month(h[:mon] - 1).next_day(h[:mday] - 1)
#=> #<Date: 2017-03-04 ...>
next_day and next_month are equivalent to + and << respectively, so the last line can also be written as:
Date.new(h[:year]) << (h[:mon] - 1) + (h[:mday] - 1)
or:
Date.new(h[:year]) << h[:mon].pred + h[:mday].pred
require 'date'
y, m, d = '2016-01-32'.split('-').map(&:to_i)
# => [2016, 01, 32]
last_day_of_month = Date.civil(y, m, -1)
# => #<Date: 2016-01-31 ((2457419j,0s,0n),+0s,2299161j)>
date = last_day_of_month + (d - last_day_of_month.day)
# => #<Date: 2016-02-01 ((2457420j,0s,0n),+0s,2299161j)>
Trying to parse a date like the one you mentioned will raise an error
DateTime.parse('2016-01-32')
ArgumentError: invalid date
One possible solution is to create a custom function/class for date parsing. You forward the parse to the DateTime object, if it fails with that error, you can retry by stripping the day number and replacing it with 1.
If it succeeds, you add the remaining days to the date.
date = DateTime.parse('2016-01-1')
=> Fri, 01 Jan 2016 00:00:00 +0000
date + (32-1).days
=> Mon, 01 Feb 2016 00:00:00 +0000
No existing core or Rails library will do that automatically for you. Hence you need a custom function for that.
You should be able to easily write the method following the instructions and the example above.
Here is one way to do it:
require 'date'
date_string = '2016-01-50'
year,month,day = date_string.split('-').map(&:to_i)
date = Date.new(year,month)
(day-1).times{date = date.next}
p date #=> 2016-02-19

Parsing JSON datetime?

I am consuming a JSON API that returns datetime values as a string in the following format:
/Date(1370651105153)/
How can I parse a value like this into a datetime variable in rails?
That appears to be a UNIX timestamp (seconds since epoch). Additionally it appears to be milliseconds since epoch.
So you can convert it like so - given that value is a String that looks like:
value = "/Date(1370651105153)/"
if value =~ /\/Date\((\d+)\)\//
timestamp = $1.to_i
time = Time.at(timestamp / 1000)
# time is now a Time object
end
You need to divide by 1000 since Time#at expects its argument to be seconds and not milliseconds since the epoch.
There are a few errors in andyisnowskynet's answer (https://stackoverflow.com/a/25817039/5633460): There's a minor RegEx error which means it won't detect positive timezones; and it won't support timezones that aren't aligned to full hours such as -0430 (VET). Also, the Time object it returns could be honouring proper Ruby timezone handling instead of adding a time offset, which would be the more-conventional way to do this.
This is my improved version of that answer...
First, for datetime values expressed in this format, the time serial part (e.g. 1370651105153 in OP's example) is ALWAYS the number of milliseconds since 1970-01-01 00:00:00 GMT, and that is not influenced by the presence or absence of a timezone suffix. Hence, any timezone included as part of the string does not change the point in history that this represents. It only serves to state which timezone the "observer" was in at the time.
Ruby Time objects are able to handle these two pieces of information (i.e. the actual datetime "value" and the timezone "metadata"). To demonstrate:
a = Time.at(-1).utc
# => 1969-12-31 23:59:59 UTC
b = Time.at(-1).getlocal('+09:30')
# => 1970-01-01 09:29:59 +0930
a == b
# => true
As you can see, even though both look like different values (owing to their different timezones in which they are expressed), the == equality operator shows they actually reference the same moment in time.
Knowing this, we could improve on parse_json_datetime as follows (correcting the other errors too):
def parse_json_datetime(datetime)
# "/Date(-62135575200000-0600)/" seems to be the default date returned
# if the value is null:
if datetime == "/Date(-62135575200000-0600)/"
# Return nil because it is probably a null date that is defaulting to 0.
# To be more technically correct you could just return 0 here if you wanted:
return nil
elsif datetime =~ %r{/Date\(([-+]?\d+)([-+]\d+)?\)/}
# We've now parsed the string into:
# - $1: Number of milliseconds since the 1/1/1970 epoch.
# - $2: [Optional] timezone offset.
# Divide $1 by 1000 because it is in milliseconds and Time uses seconds:
seconds_since_epoch = $1.to_i / 1000.0
time = Time.at(seconds_since_epoch.to_i).utc
# We now have the exact moment in history that this represents,
# stored as a UTC-based "Time" object.
if $2
# We have a timezone, so convert its format (adding a colon)...
timezone = $2.gsub(/(...)(..)/, '\1:\2')
# ...then apply it to the Time object:
time = time.getlocal(timezone)
end
time
else
raise "Unrecognized date format."
end
end
Then we can test:
# See: http://momentjs.com/docs/#/parsing/asp-net-json-date/
parse_json_datetime("/Date(1198908717056-0700)/")
# => 2007-12-28 23:11:57 -0700
# 1 minute before the epoch, but in ACST:
parse_json_datetime("/Date(-60000+0930)/")
# => 1970-01-01 09:29:00 +0930
# Same as above, but converted naturally to its UTC equivalent:
parse_json_datetime("/Date(-60000+0930)/").utc
# => 1969-12-31 23:59:00 UTC
# Same as above, with timezone unspecified (implying UTC):
parse_json_datetime("/Date(-60000)/")
# => 1969-12-31 23:59:00 UTC
# OP's example:
parse_json_datetime("/Date(1370651105153)/")
# => 2013-06-08 00:25:05 UTC
# Same as above, but stated in two different timezones:
aaa = parse_json_datetime("/Date(1370651105153-0200)/")
# => 2013-06-07 22:25:05 -0200
bbb = parse_json_datetime("/Date(1370651105153-0800)/")
# => 2013-06-07 16:25:05 -0800
# As "rendered" strings they're not the same:
aaa.to_s == bbb.to_s
# => false
# But as moments in time, they are equivalent:
aaa == bbb
# => true
# And, for the sake of the argument, if they're both expressed in the
# same timezone (arbitrary '-04:00' in this case) then they also render
# as equal strings:
aaa.getlocal('-04:00').to_s == bbb.getlocal('-04:00').to_s
The integer seems to be a unixtime (in milliseconds). Just cut the last three digits off and feed the rest to Time.at:
Time.at(1370651105) # => 2013-06-08 04:25:05 +0400
For anyone who needed a bit more robust solution that included a timezone, this is the parser I came up with:
def parse_json_datetime(datetime)
# "/Date(-62135575200000-0600)/" seems to be the default date returned if the value is null
if datetime == "/Date(-62135575200000-0600)/"
# return nil because it is probably a null date that is defaulting to 0.
# to be more technically correct you could just return 0 here if you wanted.
return nil
elsif datetime =~ /\/Date\(([-+]?\d+)(-+\d+)?\)\// # parse the seconds and timezone (if present)
milliseconds_since_epoch = $1
time = Time.at(milliseconds_since_epoch.to_i/1000.0).utc # divide by 1000 because it is in milliseconds and Time uses seconds
if timezone_hourly_offset = $2
timezone_hourly_offset = timezone_hourly_offset.gsub("0","").to_i
time = time+(timezone_hourly_offset*60*60) # convert hourly timezone offset into seconds and add onto time
end
time
else
raise "Unrecognized date format."
end
end
Active Support makes it easy to do this:
#!/usr/bin/env ruby
require 'active_support/json'
require 'active_support/time'
Time.zone = 'Eastern Time (US & Canada)'
ActiveSupport.parse_json_times = true
puts ActiveSupport::JSON.decode('{"hi":"2009-08-10T19:01:02Z"}')
Which will output:
{"hi"=>Mon, 10 Aug 2009 15:01:02 EDT -04:00}
Once you turn the ActiveSupport.parse_json_times flag on, then any JSON payload you decode will get any times automatically convered into ActiveSupport::TimeWithZone objects.
Active Support uses a regex to detect times. You can read the tests to get a good idea of the time formats that it supports. Looks like it supports ISO 8601, but does not support Unix/POSIX/Epoch timestamps.
I was informed of this feature in Active Support when I created this issue against Rails.

How to convert a long to date?

I have a long value in Rails, 134740800, which is the number of milliseconds since the epoch.
How do I convert that to a date in mm-dd-yyyy format?
I figure the formatting would be done with something like strftime but I can't seem to find the right method to convert the long into a valid date.
secs = 134740800/1000 # millisecs / 1000
t = Time.at(secs)
t.strftime("%m-%d-%Y")
Output
"01-02-1970"
Try this:
require 'date'
DateTime.strptime("1318996912",'%s')
I assume you mean seconds since the epoch.
Time.at seconds_since_epoch
You can also pass a float. If you have milliseconds, divide by 1000.0 first.
You can then call strftime on the returned Time object.
Use Time.at:
irb(main):003:0> Time.at(134740800)
=> Tue Apr 09 08:00:00 -0400 1974
This is an advisory... It's often a good idea to look at how fast some answers run. Here's a simple benchmark:
require 'benchmark'
require 'date'
SECS = 134740800
LOOPS = 1_000_000
puts Time.at(SECS).strftime('%m-%d-%Y')
puts Date.strptime(SECS.to_s, '%s').strftime('%m-%d-%Y')
Benchmark.bm(14) do |x|
x.report('Time.at:') { LOOPS.times { Time.at(SECS) }}
x.report('Date.strptime:') { LOOPS.times { Date.strptime(SECS.to_s, '%s') }}
end
And the output is:
04-09-1974
04-09-1974
user system total real
Time.at: 0.370000 0.020000 0.390000 ( 0.392761)
Date.strptime: 6.320000 0.050000 6.370000 ( 6.373248)

How do I convert a 'Fixnum' to a date in Rails 3.1.3?

I am trying to display this output, as a date:
1296524384
But when I call .to_date on it, I am getting this error:
undefined method `to_date' for 1296524384:Fixnum
You can just do:
the_time = Time.at(1296524384)
That gives you:
2011-01-31 20:39:44 -0500
By the way, 1296524384 is referred to as UNIX or epoch time. It measures the number of seconds elapsed since January 1, 1970.
To format it a bit better, you can use Ruby's strftime method.
the_time = Time.at(1296524384).strftime("The date is %m/%d/%Y")
More info here: http://apidock.com/ruby/DateTime/strftime

Resources