问题
I have rows of hashes imported from several different XML database dumps that look like this (but with varying keys):
{"Id"=>"1", "Name"=>"Cat", "Description"=>"Feline", "Count"=>"123"}
I tried using #to_i
but it converts a non-number string to 0
:
"Feline".to_i
# => 0
But what I'd like is a way for "Feline"
to remain a string, while Id
and Count
in the above example become integers 1
and 123
.
Is there an easy way to convert only the strings values that are numbers into integers?
回答1:
use Kernel#Integer:
my_hash = {"Id"=>"1", "Name"=>"Cat", "Description"=>"Feline", "Count"=>"123"}
Hash[ my_hash.map{ |a, b| [ a,
begin
Integer b
rescue ArgumentError
b
end ] } ]
ADDED LATER: With my y_support
gem, you can make hash operations even more concise.
require 'y_support/core_ext/hash'
my_hash.with_values { |v| begin
Integer b
rescue ArgumentError
b
end }
YSupport
can be installed by gem install y_support
and also offers Hash#with_keys
, Hash#with_values!
, Hash#with_keys!
that do what you expect they do, and Hash#modify
that expects a binary block returning a pair of values, modifying the hash in place. There have been proposals to add such methods directly to the Ruby core in the future.
回答2:
One line answer: Using regex approach
h.merge(h) { |k, v| v.match(/\A[+-]?\d+?(\.\d+)?\Z/) ? v.to_i : v }
Using Integer approach
h.merge(h) { |k, v| Integer(v) rescue v }
回答3:
I think you know what fields should be integers (your consuming code probably depends on it), so I would recommend you convert the specific fields.
c = Hash[h.map { |k,v| [k, %w(Id Count).include?(k) ? Integer(v) : v ] }]
回答4:
I had a similar problem to solve, where results for pesticide analysis came into the system as a heterogeneous (bad design!) format... negative integers as special codes (not detected, not tested, not quantified etc...), nil
as synonym as not detected, floats for quantified compounds and strings for pass/fail boolean... Hold your horses, this is a 10 years old-running in production, never greenfield-ed highly patched app ;)
Two things that I learned from top rubists:
0) DON'T ITERATE-MODIFY AN ENUMERABLE (return a copy)
1) YOUR REGEX WON'T COVER ALL CASES
While I am not a big fan of rescue
, I think it fits the purpose of keeping the code clean. So, I've been using this to mitigate my input:
ha = {
"p_permethrin" => nil,
"p_acequinocyl"=>"0.124",
"p_captan"=>"2.12",
"p_cypermethrin"=>"-6",
"p_cyfluthrin"=>"-6",
"p_fenhexamid"=>"-1",
"p_spinetoram"=>"-6",
"p_pentachloronitrobenzene"=>"-6",
"p_zpass"=>"true"
}
Hash[ha.map{|k,v| [k, (Float(v) rescue v)]}] # allows nil
Hash[ha.map{|k,v| [k, (Float(v) rescue v.to_s)]}] # nit to empty string
I would even
class Hash
# return a copy of the hash, where values are evaluated as Integer and Float
def evaluate_values
Hash[self.map{|k,v| [k, (Float(v) rescue v)]}]
end
end
回答5:
Using a regex and the ternary operator, you could incorporate this into the logic somewhere:
string =~ /^\d+$/ ? string.to_i : string
回答6:
This will handle not only integers but all numbers.
my_hash = {"Id"=>"1", "Name"=>"Cat", "Description"=>"Feline", "Count"=>"123"}
result = my_hash.inject({}) { |result,(key,value)|
if value.match(/^\s*[+-]?((\d+_?)*\d+(\.(\d+_?)*\d+)?|\.(\d+_?)*\d+)(\s*|([eE][+-]?(\d+_?)*\d+)\s*)$/)
result[key.to_sym] = value.to_i
else
result[key.to_sym] = value
end
result
}
Thanks to Determine if a string is a valid float value for regexp
回答7:
Define a new method for String: String#to_number
class String
def to_number
Integer(self) rescue Float(self) rescue self
end
end
Test it:
"1".to_number => 1
"Cat".to_number => "Cat"
来源:https://stackoverflow.com/questions/12318021/how-to-convert-only-strings-hash-values-that-are-numbers-to-integers