Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to check if a value is valid input for Integer(), Float() or Rational()?

This is loosely based on "How to convert a String to Integer or Float".

If I wanted to convert a numerical string input to its "most appropriate type" using Ruby's built-in conversion mechanics, I could do something like this:

def convert(input)
  value = Integer(input) rescue nil
  value ||= Float(input) rescue nil
  value ||= Rational(input) rescue nil
  value
end

convert('1')     #=> 1
convert('1_000') #=> 1000
convert('0xff')  #=> 255
convert('0.5')   #=> 0.5
convert('1e2')   #=> 100.0
convert('1/2')   #=> (1/2)
convert('foo')   #=> nil

But this brute-force method-calling looks dirty. Is there a more elegant way to approach this? Can I check whether a value is a valid input for Integer(), Float() or Rational() so I could call these methods in a more controlled manner?

like image 717
Stefan Avatar asked Nov 21 '16 14:11

Stefan


2 Answers

Using a trailing rescue makes me cringe as it can obscure problems with the underlying code since it traps Exception not ArgumentError, which is what the failed attempted conversions would raise. This isn't as concise but it'd handle the appropriate exception:

def convert(input)
  value = begin
            Integer(input)
          rescue ArgumentError
            nil
          end

  value ||= begin
              Float(input)
            rescue ArgumentError
              nil
            end

  value ||= begin
              Rational(input)
            rescue ArgumentError
              nil
            end

  value
end

convert('1')     # => 1
convert('1_000') # => 1000
convert('0xff')  # => 255
convert('0.5')   # => 0.5
convert('1e2')   # => 100.0
convert('1/2')   # => (1/2)
convert('foo')   # => nil

After thinking about it a bit it seems like that can be DRY'd down to:

def convert(input)
  [:Integer, :Float, :Rational].each do |m| 
    begin
      return Kernel.method(m).call(input)
    rescue ArgumentError
    end
  end
  nil
end

convert('1')     # => 1
convert('1_000') # => 1000
convert('0xff')  # => 255
convert('0.5')   # => 0.5
convert('1e2')   # => 100.0
convert('1/2')   # => (1/2)
convert('foo')   # => nil

As pointed out by Jörn, the above wasn't a good example. I was using Kernel to get at Integer(), Float() and Rational because that's where they're defined, but really Object was the place to look since it inherits from Kernel.

And it'd been one of those days when I knew there was a good way to call the method indirectly, but call was sticking in my mind, not send as Stephan pointed out. So, here's a cleaner way of doing it, starting with:

  return Object.send(m, input)

But, that could be reduced to:

 return send(m, input)

resulting in:

def convert(input)
  [:Integer, :Float, :Rational].each do |m| 
    begin
      return send(m, input)
    rescue ArgumentError
    end
  end
  nil
end

convert('1')     # => 1
convert('1_000') # => 1000
convert('0xff')  # => 255
convert('0.5')   # => 0.5
convert('1e2')   # => 100.0
convert('1/2')   # => (1/2)
convert('foo')   # => nil
like image 137
the Tin Man Avatar answered Oct 08 '22 11:10

the Tin Man


Since for some reason you prefer "0.3" to be converted to 3e-1 rather than to 3/10, this might be done in more explicit manner. After all, under the hood, there is the same recognition mechanism in ruby parser:

def convert input
  raise unless String === input && input[/\A_|_\z|__/].nil?
  input = input.strip.delete('_')
  case input
  when /\A-?\d+\z/ then Integer(input)
  when /\A-?0x[\da-f]+\z/i then Integer(input)
  when /\A-?(\d*\.)?\d+(e-?\d+)?\z/i then Float(input)
  when /\A-?(\d*\.)?\d+(e-?\d+)?\/\d+\z/i then Rational(input)
  end
end

And that works as expected :)

like image 2
Aleksei Matiushkin Avatar answered Oct 08 '22 12:10

Aleksei Matiushkin