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
Related
I have the following complicated hash structure (among many) that looks like the following:
hash = {"en-us"=>
{:learn_more=>"Learn more",
:non_apple_desktop=>"To redeem, open this link.",
:value_prop_safety=>"",
:storage_size=>
{:apple_tv_1_month_tms=>
{:cta=>"Offer",
:details=>"Get a 1-month subscription!.",
:disclaimer=>"This offer expires on December 10, 2021.",
:header=>"Watch The Morning Show ",
:normal_price=>"$2.99"}
}
}
}
What I'd like to do is to have a function that will produce the following string output based off the hash structure:
en-us.storage_size.apple_tv_1_month_tms.cta: Offer
en-us.storage_size.apple_tv_1_month_tms.details: Get a 1-month subscription!.
en-us.storage_size.apple_tv_1_month_tms.disclaimer: This offer expires on December 10, 2021.
en-us.storage_size.apple_tv_1_month_tms.header: Watch The Morning Show
en-us.storage_size.apple_tv_1_month_tms.normal_price: $2.99
en-us.learn_more: Learn more
en-us.non_apple_desktop: To redeem, open this link.
en-us.value_prop_safety:
I've used this recursive function from another stackoverflow question that somewhat accomplishes this:
def show(hash, current_path = '')
string = ''
hash.each do |k,v|
if v.respond_to?(:each)
current_path += "#{k}."
show v, current_path
else
string += "#{current_path}#{k}: #{v}" + "\n"
end
end
string
end
If I place a puts statement in the body of the method I can see the desired result but its line by line. What I need is to obtain the entirety of the output because I will be writing it to a csv. I can't seem to get it to work in its current incarnation.
If I were to place a puts show(hash) into my irb, then I won't get any output. So in summary, I am trying to do the following:
show(hash) ----->
en-us.storage_size.apple_tv_1_month_tms.cta: Offer
en-us.storage_size.apple_tv_1_month_tms.details: Get a 1-month subscription!.
en-us.storage_size.apple_tv_1_month_tms.disclaimer: This offer expires on December 10, 2021.
en-us.storage_size.apple_tv_1_month_tms.header: Watch The Morning Show
en-us.storage_size.apple_tv_1_month_tms.normal_price: $2.99
en-us.learn_more: Learn more
en-us.non_apple_desktop: To redeem, open this link.
en-us.value_prop_safety:
This should be an easy recursive task but I can't pinpoint what exactly I've got wrong. Help would be greatly appreciated. Thank you.
In my opinion it is much more convenient to use i18n gem
It has I18n::Backend::Flatten#flatten_translations method. It receives a hash of translations (where the key is a locale and the value is another hash) and return a hash with all translations flattened, just as you need
Just convert the resulting hash to a string and you're done
require "i18n/backend/flatten"
include I18n::Backend::Flatten
locale_hash = {"en-us"=>
{:learn_more=>"Learn more",
:non_apple_desktop=>"To redeem, open this link.",
:value_prop_safety=>"",
:storage_size=>
{:apple_tv_1_month_tms=>
{:cta=>"Offer",
:details=>"Get a 1-month subscription!.",
:disclaimer=>"This offer expires on December 10, 2021.",
:header=>"Watch The Morning Show ",
:normal_price=>"$2.99"}
}
}
}
puts flatten_translations(nil, locale_hash, nil, nil).
map { |k, v| "#{k}: #{v}" }.
join("\n")
# will print
# en-us.learn_more: Learn more
# en-us.non_apple_desktop: To redeem, open this link.
# en-us.value_prop_safety:
# en-us.storage_size.apple_tv_1_month_tms.cta: Offer
# en-us.storage_size.apple_tv_1_month_tms.details: Get a 1-month subscription!.
# en-us.storage_size.apple_tv_1_month_tms.disclaimer: This offer expires on December 10, 2021.
# en-us.storage_size.apple_tv_1_month_tms.header: Watch The Morning Show
# en-us.storage_size.apple_tv_1_month_tms.normal_price: $2.99
Of course it's better to include not in main object, but in some service object
require "i18n/backend/flatten"
class StringifyLocaleHash
include I18n::Backend::Flatten
attr_reader :locale_hash
def self.call(locale_hash)
new(locale_hash).call
end
def initialize(locale_hash)
#locale_hash = locale_hash
end
def call
flatten_translations(nil, locale_hash, nil, nil).
map { |k, v| "#{k}: #{v}" }.
join("\n")
end
end
# To get string call such way
StringifyLocaleHash.(locale_hash)
To answer your literal question:
show v, current_path
should be
string += show v, current_path
otherwise you lose any work that your recursive call has done.
Note that a += b replaces a with the new string a + b. It does not change a. Thus, preserving the return value of show is critical.
If you want to rely on strings being mutable, here is a mutable string version; however, note that it may not always work, since string immutability is an option added to Ruby. If frozen_string_literals is on, the mutable concatenation operator << will fail. In the mutable string version, you can't initialise string in each iteration, because you'd be discarding the work your caller has done; so it is passed as another parameter, and initialised by the default value only on its initial call.
def show(hash, current_path = '', string = '')
hash.each do |k,v|
if v.respond_to?(:each)
current_path += "#{k}."
show v, current_path, string
else
string << "#{current_path}#{k}: #{v}" + "\n"
end
end
string
end
Considerations on the Approach
First, some issues:
Ruby is not JavaScript. Hashes don't have properties, so chained dots aren't going to work unless you do a lot of work to split and join the paths to your data after the fact. The juice is probably not worth the squeeze.
en-us is not a valid LANG value. You can use it a a key, but you probably want something more technically accurate like en_US or en_US.UTF-8 as a key if the locale matters.
Secondly, if you already know the structure of your JSON, then you should just make each JSON key a column value and populate it. This is likely a lot easier.
Thirdly, while Ruby can do recursion, most of its built-in methods are designed for iteration. Iterating is faster and easier for a lot of things, so unless you're doing this for other reasons, just iterate over the information you need.
Building CSV from Structured Data with a Predefined Format
Finally, if you just want the data to populate a CSV or create strings, then maybe you just want to extract the data and then munge it with information you already know anyway. For example:
require "hashie"
# assume your hash is already defined
offer_values =
hash.extend(Hashie::Extensions::DeepFind)
.deep_find(:apple_tv_1_month_tms).values
#=>
# ["Offer",
# "Get a 1-month subscription!.",
# "This offer expires on December 10, 2021.",
# "Watch The Morning Show ",
# "$2.99"]
To get the values of the top-level keys, you can can do it like this:
linking = hash["en-us"].keys[0..-2].map { hash.dig "en-us", _1 }
#=> ["Learn more", "To redeem, open this link.", ""]
With more work, you could do similar things with ActiveSupport or by using Ruby 3.1 pattern matching with a find pattern, but this will work on older Ruby versions too. The only downside is that it relies on the Hashie gem for Hashie::DeepFind, but this is one of those cases where Ruby doesn't have a simple built-in method for finding nested structures other than pattern matching and I think it's worth it.
You'll still have to do some work to convert the extracted data into the exact format you want, but this gets you all the values you're after into a pair of variables, offer_values and linking. You can then do whatever you want with them.
See Also
In addition to Hashie::DeepFind and Ruby 3 pattern matching, you also have a number of query languages that might be useful in extracting data from JSON, some of which have Ruby gems or can be easily integrated in your Ruby application:
JSONPath
https://github.com/joshbuddy/jsonpath
https://www.ruby-toolbox.com/projects/remap
XPath 3.1+
XQuery 3.1+
This is a relatively straightforward recursion as each value is a string or a hash. It can be written as follows.
def recurse(h)
h.each_with_object([]) do |(k,v),a|
case v
when Hash
recurse(v).each { |ss| a << "#{k}.#{ss}" }
else
a << "#{k}: #{v}"
end
end
end
recurse hash
#=> ["en-us.learn_more: Learn more",
# "en-us.non_apple_desktop: To redeem, open this link.",
# "en-us.value_prop_safety: ",
# "en-us.storage_size.apple_tv_1_month_tms.cta: Offer",
# "en-us.storage_size.apple_tv_1_month_tms.details: Get a 1-month subscription!.",
# "en-us.storage_size.apple_tv_1_month_tms.disclaimer: This offer expires on December 10, 2021.",
# "en-us.storage_size.apple_tv_1_month_tms.header: Watch The Morning Show ",
# "en-us.storage_size.apple_tv_1_month_tms.normal_price: $2.99"]
I am writing a little updater for a app that will update the last time someone logs in to the app and then saves it. its in rails 3.2 and ruby 1.9.3p327
def update_last_seen
if current_account.present?
if (Date.current - 1.day) > current_account.last_login_at
current_account.last_login_at = Date.current
current_account.save
end
end
end
I stuck that into the application controller and call it with a before filter. The only thing is that sometimes i have dates that are nil. so comparing date to nil gives errors. you cant call to_date on a nil.
nil.to_f => 0.0
nil.to_i => 0
nil.to_s => ""
nil.to_date => NoMethodError: undefined method `to_date' for nil:NilClass
"2013/07/26".to_date => Fri, 26 Jul 2013
how can i have it set it to be accepted as a blank date as it were.
i could always do
if current_account.last_login_at.blank? || (Date.current - 1.day) > current_account.last_login_at
that way it will set it if its not there but is there a semantic way of doing it?
UPDATE:
You might think this is has no point. the reason i ask is because there are some engines that have a nil for a date. for example excel will return dates two ways 1. as text as in "06/12/2013" or 2. an integer as the number of days from 01/01/1900. that date is excels nil date i was hoping that there was a default date for nils for Ruby. if there isn't you can just comment nope there isn't sorry man. giving a downvote without explanation as to why means that you really don't care about helping/teaching anything you're just there pushing buttons. if i did something wrong with this question you can tell me ill try fix it, if it doesnt make sence?
You could add in another method to clean up the code a little bit.
def new_login_since?(last_login)
last_login.blank? || (Date.current - 1.day) > last_login
end
def update_last_seen
if current_account.present? && new_login_since?(current_account.last_login_at)
current_account.update_attributes { last_login_at: Date.current }
end
end
To answer the actual question ... you can monkey-patch the NilClass like this
class NilClass
def to_date
Date.today
end
end
nil.to_date # => #<Date: 2013-09-26 ((2456562j,0s,0n),+0s,2299161j)>
Of course, the accepted answer shows the better approach.
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
Is there any standard way to avoid truthiness in Ruby, or would I need to roll my own solution, such as
class FalseClass
def to_bool
self
end
end
class TrueClass
def to_bool
self
end
end
true.to_bool # => true
false.to_bool # => false
nil.to_bool # => NoMethodError
42.to_bool # => NoMethodError
Background: I know that to_bool would go against the permissive nature of Ruby, but I'm playing around with ternary logic, and want to avoid accidentally doing something like
require "ternary_logic"
x = UNKNOWN
do_something if x
I'm using ternary logic because I'm writing a parser of a flatmate-share web site (for personal, not commercial, use) and it's possible for some fields to be missing information, and therefore it'd be unknown whether the place meets my criteria or not. I'd try to limit the amount of code that uses the ternary logic, however.
It is not possible to influence truthiness or falsiness in Ruby. nil and false are falsy, everything else is truthy.
It's a feature that comes up every couple of years or so, but is always rejected. (For reasons that I personally don't find convincing, but I'm not the one calling the shots.)
You will have to implement your own logic system, but you cannot prohibit someone from using Ruby's logical operators on an unknown value.
I re-implemented Ruby's logic system once, for fun and to show it can be done. It should be fairly easy to extend this to ternary logic. (When I wrote this, I actually took the conformance tests from RubySpec and ported them to my implementation, and they all passed, so I'm fairly confident that it matches Ruby's semantics.)
You can take advantage of the overridable ! operator in 1.9 and the !! idiom to redefine truthiness.
Let's have a Pythonesque truthiness:
class Numeric
def !
zero?
end
end
class Array
def !
empty?
end
end
!![] #=> false
!!0 #=> false
I also made my own logic system in Ruby (for fun), and you can easily redefine truthiness:
Note, the analogs of the normal conditionals are if!/else_if!/else!
# redefine truthiness with the `truth_test` method
CustomBoolean.truth_test = proc { |b| b && b != 0 && b != [] }
if!(0) {
puts 'true'
}.
else! {
puts 'false'
}
#=> false
see: http://github.com/banister/custom_boolean
According to the axe book (2nd edition), we can use to_s as follows.
class Song
def to_s
"Song"
end
end
song = Song.new()
song.to_s
But, it doesn't give me anything, in order to print something to the stdout, I should run
def to_s
p "Song"
end
Is there any change since the ruby 1.8.2 when the book was written for?
My ruby version info is 1.8.7 for Mac.
ruby 1.8.7 (2009-06-08 patchlevel 173) [universal-darwin10.0]
The Pickaxe book uses a common notation of e.g.
song.to_s → "Song: My Way--Sinatra (225)"
to indicate that "Song: My Way--Sinatra (225)" is the value of the expression on the left. However it isn't printed out unless you explictly use a puts.
An appropriate implementation of to_s on an object will return a string representation of that object (e.g. the name for a person or the title and ID for a blog post) but won't actually have any side effects itself (such as outputting something.)
If you use irb, the interactive Ruby interpreter it prints out the value of each expression for you without an explicit puts. e.g.
$ irb
irb(main):001:0> 2 + 2
=> 4
That example might be meant to be typed into a REPL like irb, which would look like this:
irb> class Song
.. def to_s
.. "Song"
.. end
.. end
=> nil
irb> song = Song.new()
=> Song
irb> song.to_s
=> "Song"
In your case you would want your last lines to be:
song = Song.new
puts song.to_s
Right, to_s has always just converted the object to a string. It is up to your program to then use that string - for example, by writing it to stdout.
This is because the above method simply returns "song", it doesn't print it. Your program should do the printing itself.