DateTime serialization and deserialization - ruby

I'd like to serialize a Ruby DateTime object to json. Unfortunately, my approach is not symetrical:
require 'date'
date = DateTime.now
DateTime.parse(date.to_s) == date
=> false
I could use some arbitrary strftime/parse string combination, but I believe there must be a better approach.

The accepted answer is not a good solution, unfortunately. As always, marshal/unmarshal is a tool you should only use as a last resort, but in this case it will probably break your app.
OP specifically mentioned serializing a date to JSON. Per RFC 7159:
JSON text SHALL be encoded in UTF-8, UTF-16, or UTF-32. The default encoding is UTF-8, and JSON texts that are encoded in UTF-8 are interoperable in the sense that they will be read successfully by the maximum number of implementations; there are many implementations that cannot successfully read texts in other encodings (such as UTF-16 and UTF-32).
Now let's look at what we get from Marshal:
marsh = Marshal.dump(DateTime.now)
# => "\x04\bU:\rDateTime[\vi\x00i\x03\xE0\x7F%i\x02s\xC9i\x04\xF8z\xF1\"i\xFE\xB0\xB9f\f2299161"
puts marsh.encoding
# -> #<Encoding:ASCII-8BIT>
marsh.encode(Encoding::UTF_8)
# -> Encoding::UndefinedConversionError: "\xE0" from ASCII-8BIT to UTF-8
In addition to returning a value that isn't human-readable, Marshal.dump gives us a value that can't be converted to UTF-8. That means the only way to put it into (valid) JSON is to encode it somehow, e.g. base-64.
There's no need to do that. There's already a very interoperable way to represent dates and times: ISO 8601. I won't go over why it's the best choice for JSON (and in general), but the answers here cover it well: What is the "right" JSON date format?.
Since Ruby 1.9.3 the DateTime class has had iso8601 class and instance methods to parse and format ISO 8601 dates, respectively. The latter takes an argument to specify precision for fractional seconds (e.g. 3 for milliseconds):
require "date"
date = DateTime.now
str = date.iso8601(9)
puts str
# -> 2016-06-28T09:35:58.311527000-05:00
DateTime.iso8601(str) == date
# => true
Note that if you specify a smaller precision, this might not work, because e.g. 58.311 is not equal to 58.311527. A precision of 9 (nanosecond) seems safe to me, since the DateTime docs say:
The fractional number’s precision is assumed at most nanosecond.
However, if you're interoperating with systems that might use greater precision, you should take that into consideration.
Finally, if you want to make Ruby's JSON library automatically use iso8601 for serialization, override the as_json and to_json methods:
unless defined?(::JSON::JSON_LOADED) and ::JSON::JSON_LOADED
require 'json'
end
require 'date'
class DateTime
def as_json(*)
iso8601(9)
end
def to_json(*args)
as_json.to_json(*args)
end
end
puts DateTime.now.to_json
# -> "2016-06-28T09:35:58.311527000-05:00"

Both the to_s method and the to_json method (provided require 'json') ignore the nanoseconds which are stored by the DateTime object date. Good old Marshal delivers:
require 'date'
date = DateTime.now
m_date = Marshal.dump(date)
p Marshal.load(m_date) == date # => true

It is because date has sub second value, and #to_s method will return ISO time format in seconds, the comparison don't succeed.
1.9.3p327 :021 > date = DateTime.now
=> #<DateTime: 2012-11-28T07:32:40+09:00 ((2456259j,81160s,283019000n),+32400s,2299161j)>
1.9.3p327 :022 > DateTime.parse(date.to_s)
=> #<DateTime: 2012-11-28T07:32:40+09:00 ((2456259j,81160s,0n),+32400s,2299161j)>
so they're actually different.
If you don't care about sub-seconds, just forget whether comparison succeed or not.
Or, you can use DateTime#marshal_load and DateTime#marshal_dump for 1.9.3.
(I didn't know this till now.. )
It work as:
date1 = DateTime.now
dump = date1.marshal_dump
date2 = DateTime.new.marshal_load(dump)
date1 == date2 # => true

Related

Format array of date_time in hash during hash to json conversion

so I have a class whose hash representation looks like this.
{"dateTime"=>[1484719381, 1484719381], "dateTime1"=>[1484719381, 1484719381]}
The dateTime here is is a unix formatted dateTime array.
I am trying to convert this hash to an equivalent of json_string for which I am using hash.to_json. Is there any way through which I can modify the format of date_time when calling to_json. The resulting json should look like this
'{"dateTime1":["2017-01-18T06:03:01+00:00","2017-01-18T06:03:01+00:00"]}'
Basically I am looking for an implementation that can be called during hash.to_json.
You cannot make this part of Hash#to_json without damaging that method dramatically because:
You would need to manipulate the #to_json for multiple other classes
Those are Integers which is valid JSON and changing this would be awful
That is not the string representation of a Time object in Ruby so you need to string format it anyway
Instead you would have to modify the Hash values to represent in the desired fashion e.g.
h= {"dateTime"=>[1484719381, 14848723546], "dateTime1"=>[1484234567, 1484719381]}
h.transform_values do |v|
v.map do |int|
Time.at(int, in: '+00:00').strftime("%Y-%m-%dT%H:%M:%S%z")
end
end
#=> {"dateTime"=>[
# "2017-01-18T06:03:01+0000",
# "2440-07-15T05:25:46+0000"],
# "dateTime1"=>[
# "2017-01-12T15:22:47+0000",
# "2017-01-18T06:03:01+0000"]}
You could then call to_json on the resulting object to get your desired result.

Ruby Time.parse incorrectly returns timestamp for String

I am facing a weird bug (?) in Ruby
Time.parse("David").to_i returns "no time information in "David"`
Time.parse("David1").to_i returns "no time information in "David1"`
However
Time.parse("David10").to_i returns 1570654800
It seems that any string with more than 2 numbers at the end manages to pass the Time conversion in Ruby. Is this a bug?
I am trying to create a single method than can handle conversion of strings to Timestamps where relevant or simply back to strings if conversion is not possible but for instances where my string includes 2+ numbers, it fails
if value.is_a? String
# if it's string of a date format
begin
Time.parse(value).to_i
rescue StandardError => e
value.downcase
end
# it's another object type - probably DateTime, Time or Date
else
value.nil? ? 0 : value.to_f
end
Internally time.rb uses, following,
def parse(date, now=self.now)
comp = !block_given?
d = Date._parse(date, comp)
year = d[:year]
year = yield(year) if year && !comp
make_time(date, year, d[:mon], d[:mday], d[:hour], d[:min], d[:sec], d[:sec_fraction], d[:zone], now)
end
It used to parse day, month later year by precision, Range of digits when exeed to 3, it consider it as year

Ruby Benign vale for nil DateTime

When comparing DateTimes, all objects being compared must be the same type. However, I have a data set that has nil dates. I want to treat these dates as older (or perhaps newer) than any other date. Is there a way to construct a benign value that will compare as older (or alternatively, newer) than any other date?
example:
data = [
{ name: :foo, timestamp: make_benign(some_valid_timestamp) },
{ name: :bar, timestamp: make_benign(nil)}
]
data.sort_by{|datum| datum[:timestamp]} #=> [<bar>, <foo>]
data.max_by {|datum| datum[:timestamp]} #=> <foo>
data.min_by {|datum| datum[:timestamp]} #=> <bar>
EDIT: I happen to be stuck on ruby 1.9 for this problem, so solutions for older versions of ruby would be nice. (But newer solutions also are nice for future reference)
From the docs, the requirement is not that "all objects are the same type". It says:
The other should be a date object or a numeric value as an astronomical Julian day number.
So for a benign value that is guaranteed to be before/after any date, you could use -Float::INFINITY and Float::INFINITY accordingly.
DateTime.now > Float::INFINITY #=> false
DateTime.now > -Float::INFINITY #=> true
EDIT:
So we need a solution that works in Ruby 1.9 and Rails 3.2.9, huh...
Well the reason the above won't work is because of this monkeypatch in ActiveSupport:
class DateTime
def <=>(other)
super other.to_datetime
end
end
This is particularly problematic. Unfortunately, you may need to just use a "very big/small number" instead...
However, if you're able to upgrade a little bit to Rails 3.2.13 (or apply this updated monkeypatch manually), where the method signature was changed to:
class DateTime
def <=>(other)
super other.kind_of?(Infinity) ? other : other.to_datetime
end
end
...Then you can use Date::Infinity (TIL that's a thing) instead of Float::Infinity, and this "fixed" version of the method now handles it correctly:
DateTime.now > Date::Infinity.new #=> false
DateTime.now > -Date::Infinity.new #=> true

Getting accurate Julian date number for current time in Ruby

Date.today.jd returns a rounded number. Is there a way to get more precision in Ruby?
I want to return a Julian date for the current time in UTC.
The Date#amjd method does what you're asking for, but it returns a Rational; converting to a Float gives you something easier to work with:
require 'date'
DateTime.now.amjd.to_f # => 56759.82092321331
require "date"
p jdate = DateTime.now.julian #=> #<DateTime: 2014-03-30T21:28:30+02:00 (...)
p jdate.julian? # => true

Ruby: comparing dates of two Time objects

What is the best way to compare the dates of two Time objects in Ruby?
I have two objects such as:
time_1 = Time.new(2012,12,10,10,10)
time_2 = Time.new(2012,12,11,10,10)
In this example, the date comparison should return false.
Otherwise, same date, but different times, should return true:
time_1 = Time.new(2012,12,10,10,10)
time_2 = Time.new(2012,12,10,11,10)
I have tried to use .to_date that works for DateTime objects, but it is not supported by Time.
Just require the 'date' part of stdlib, then compare the dates:
require "date"
time1.to_date == time2.to_date
Job done.
I have verified that this works for me:
time_1.strftime("%F") == time_2.strftime("%F")
The %F format returns the date portion only.
Maybe just testing this way:
time_1.year == time_2.year && time_1.yday == time_2.yday
It'll be less resource consuming than string comparison.
monkey patch the class Time with this method and it i'll be nice to read
class Time
def date_compare(time)
year == time.year && yday == time_2.yday
end
end
time_1.date_compare time_2
to_date works just fine in ruby 2.0 and ruby 1.9.3 and ruby 1.9.2 http://ruby-doc.org/stdlib-1.9.2/libdoc/date/rdoc/Time.html
>> time_1.to_date
=> #<Date: 2012-12-10 ((2456272j,0s,0n),+0s,2299161j)>
but it's not in the stdlib of ruby 1.8.7 http://ruby-doc.org/stdlib-1.8.7/libdoc/time/rdoc/Time.html - but then, your way of creating a time object doesn't work in that version either:
> time_1 = Time.new(2012,12,10,10,10)
ArgumentError: wrong number of arguments (5 for 0)

Resources