118

I have the following in my application controller:

def is_number?(object)
  true if Float(object) rescue false
end

and the following condition in my controller:

if mystring.is_number?

end

The condition is throwing an undefined method error. I'm guessing I've defined is_number in the wrong place...?

user664833
  • 18,397
  • 19
  • 91
  • 140
Jamie Buchanan
  • 3,868
  • 4
  • 22
  • 24
  • 4
    I know a lot of people are here because of codeschool's Rails for Zombies Testing class. Just wait for him to keep explaining. The tests aren't supposed to pass --- its OK to have you test fail in error, you can always patch rails to invent methods such as self.is_number? – boulder_ruby Feb 28 '13 at 19:33
  • The accepted answer fails on cases like "1,000" and is a 39x slower than using a regex approach. See my answer below. – pthamm Feb 19 '16 at 23:10

13 Answers13

199

Create is_number? Method.

Create a helper method:

def is_number? string
  true if Float(string) rescue false
end

And then call it like this:

my_string = '12.34'

is_number?( my_string )
# => true

Extend String Class.

If you want to be able to call is_number? directly on the string instead of passing it as a param to your helper function, then you need to define is_number? as an extension of the String class, like so:

class String
  def is_number?
    true if Float(self) rescue false
  end
end

And then you can call it with:

my_string.is_number?
# => true
Joshua Pinter
  • 45,245
  • 23
  • 243
  • 245
Jakob S
  • 19,575
  • 3
  • 40
  • 38
  • This is what I was after. It wasn't declaring it on the String class so wasn't able to call it as expected. Thanks Jakob! – Jamie Buchanan Apr 14 '11 at 10:24
  • 3
    This is a bad idea. "330.346.11".to_f # => 330.346 – epochwolf Nov 02 '11 at 23:15
  • 14
    There is no `to_f` in the above, and Float() doesn't exhibit that behavior: `Float("330.346.11")` raises `ArgumentError: invalid value for Float(): "330.346.11"` – Jakob S Nov 03 '11 at 10:29
  • 8
    If you use that patch, I'd rename it to numeric?, to stay in line with ruby naming conventions (Numeric classes inherit from Numeric, is_ prefixes are javaish). – Konrad Reiche Jun 24 '12 at 18:34
  • Where within your project would you do this String class extension? – Jason Swett Aug 06 '12 at 20:33
  • 10
    Not really relevant to the original question, but I'd probably put the code in `lib/core_ext/string.rb`. – Jakob S Aug 07 '12 at 08:48
  • 1
    I don't think the `is_number?(string)` bit works Ruby 1.9. Maybe that is part of Rails or 1.8? `String.is_a?(Numeric)` works. See also http://stackoverflow.com/questions/2095493/how-can-i-check-if-a-value-is-a-number. – Ross Attrill Jun 16 '14 at 07:10
  • @Ross + @Nathan: `is_number` isn't part of Ruby, it's a method that Jakob is defining. He is extending the String class to make it more convenient but he's saying you could also put it in a helper file but you would have to pass the "string" to the helper method as a param. – Joshua Pinter Jul 03 '15 at 11:39
  • This fails on cases like "1,000" and is a 39x slower than using a regex approach. See my answer below. – pthamm Feb 19 '16 at 23:09
33
class String
  def numeric?
    return true if self =~ /\A\d+\Z/
    true if Float(self) rescue false
  end
end  

p "1".numeric?  # => true
p "1.2".numeric? # => true
p "5.4e-29".numeric? # => true
p "12e20".numeric? # true
p "1a".numeric? # => false
p "1.2.3.4".numeric? # => false
Marius Butuc
  • 17,781
  • 22
  • 77
  • 111
hipertracker
  • 2,425
  • 26
  • 16
  • 12
    `/^\d+$/` is not a safe regexp in Ruby, `/\A\d+\Z/` is. (e.g. "42\nsome text" would return `true`) – Timothee A Sep 01 '14 at 15:18
  • 1
    To clarify on @TimotheeA's comment, it is safe to use `/^\d+$/` if dealing with lines but in this case it's about beginning and end of a string, thus `/\A\d+\Z/`. – Julio Oct 09 '14 at 20:09
  • 1
    Shouldn't answers be edited to change the actual answer BY the responder? changing the answer in an edit if you're not the responder seems...possibly underhanded and should be out of bounds. – jaydel Jul 20 '16 at 15:43
  • 2
    \Z allows to have \n at the end of the string, so "123\n" will pass validation, regardless that it's not fully numeric. But if you use \z then it will be more correct regexp: /\A\d+\z/ – SunnyMagadan Aug 14 '17 at 09:14
31

Here's a benchmark for common ways to address this problem. Note which one you should use probably depends on the ratio of false cases expected.

  1. If they are relatively uncommon casting is definitely fastest.
  2. If false cases are common and you are just checking for ints, comparison vs a transformed state is a good option.
  3. If false cases are common and you are checking floats, regexp is probably the way to go

If performance doesn't matter use what you like. :-)

Integer checking details:

# 1.9.3-p448
#
# Calculating -------------------------------------
#                 cast     57485 i/100ms
#            cast fail      5549 i/100ms
#                 to_s     47509 i/100ms
#            to_s fail     50573 i/100ms
#               regexp     45187 i/100ms
#          regexp fail     42566 i/100ms
# -------------------------------------------------
#                 cast  2353703.4 (±4.9%) i/s -   11726940 in   4.998270s
#            cast fail    65590.2 (±4.6%) i/s -     327391 in   5.003511s
#                 to_s  1420892.0 (±6.8%) i/s -    7078841 in   5.011462s
#            to_s fail  1717948.8 (±6.0%) i/s -    8546837 in   4.998672s
#               regexp  1525729.9 (±7.0%) i/s -    7591416 in   5.007105s
#          regexp fail  1154461.1 (±5.5%) i/s -    5788976 in   5.035311s

require 'benchmark/ips'

int = '220000'
bad_int = '22.to.2'

Benchmark.ips do |x|
  x.report('cast') do
    Integer(int) rescue false
  end

  x.report('cast fail') do
    Integer(bad_int) rescue false
  end

  x.report('to_s') do
    int.to_i.to_s == int
  end

  x.report('to_s fail') do
    bad_int.to_i.to_s == bad_int
  end

  x.report('regexp') do
    int =~ /^\d+$/
  end

  x.report('regexp fail') do
    bad_int =~ /^\d+$/
  end
end

Float checking details:

# 1.9.3-p448
#
# Calculating -------------------------------------
#                 cast     47430 i/100ms
#            cast fail      5023 i/100ms
#                 to_s     27435 i/100ms
#            to_s fail     29609 i/100ms
#               regexp     37620 i/100ms
#          regexp fail     32557 i/100ms
# -------------------------------------------------
#                 cast  2283762.5 (±6.8%) i/s -   11383200 in   5.012934s
#            cast fail    63108.8 (±6.7%) i/s -     316449 in   5.038518s
#                 to_s   593069.3 (±8.8%) i/s -    2962980 in   5.042459s
#            to_s fail   857217.1 (±10.0%) i/s -    4263696 in   5.033024s
#               regexp  1383194.8 (±6.7%) i/s -    6884460 in   5.008275s
#          regexp fail   723390.2 (±5.8%) i/s -    3613827 in   5.016494s

require 'benchmark/ips'

float = '12.2312'
bad_float = '22.to.2'

Benchmark.ips do |x|
  x.report('cast') do
    Float(float) rescue false
  end

  x.report('cast fail') do
    Float(bad_float) rescue false
  end

  x.report('to_s') do
    float.to_f.to_s == float
  end

  x.report('to_s fail') do
    bad_float.to_f.to_s == bad_float
  end

  x.report('regexp') do
    float =~ /^[-+]?[0-9]*\.?[0-9]+$/
  end

  x.report('regexp fail') do
    bad_float =~ /^[-+]?[0-9]*\.?[0-9]+$/
  end
end
Matt Sanders
  • 8,023
  • 3
  • 37
  • 49
26

As of Ruby 2.6.0, the numeric cast-methods have an optional exception-argument [1]. This enables us to use the built-in methods without using exceptions as control flow:

Float('x') # => ArgumentError (invalid value for Float(): "x")
Float('x', exception: false) # => nil

Therefore, you don't have to define your own method, but can directly check variables like e.g.

if Float(my_var, exception: false)
  # do something if my_var is a float
end
Timitry
  • 2,646
  • 20
  • 26
16

Relying on the raised exception is not the fastest, readable nor reliable solution.
I'd do the following :

my_string.should =~ /^[0-9]+$/
Damien MATHIEU
  • 31,924
  • 13
  • 86
  • 94
  • 1
    This only works for positive integers, however. Values like '-1', '0.0', or '1_000' all return false even though they are valid numeric values. You're looking at something like /^[-_.0-9]+$/, but that erroneously accepts '-_-'. – Jakob S Apr 14 '11 at 11:03
  • 13
    From Rails 'validates_numericality_of': raw_value.to_s =~ /\A[+-]?\d+\Z/ – Morten Aug 17 '12 at 20:09
  • NoMethodError: undefined method `should' for "asd":String – sergserg Oct 09 '14 at 22:08
  • In the latest rspec, this becomes `expect(my_string).to match(/^[0-9]+$/)` – Damien MATHIEU Oct 10 '14 at 08:55
  • I like: `my_string =~ /\A-?(\d+)?\.?\d+\Z/` it lets you do '.1', '-0.1', or '12' but not '' or '-' or '.' – Josh Sep 02 '16 at 00:05
  • @Morten the regex from Rails 'validates_numericality_of' is only for integers – Josh Sep 02 '16 at 00:13
7

Tl;dr: Use a regex approach. It is 39x faster than the rescue approach in the accepted answer and also handles cases like "1,000"

def regex_is_number? string
  no_commas =  string.gsub(',', '')
  matches = no_commas.match(/-?\d+(?:\.\d+)?/)
  if !matches.nil? && matches.size == 1 && matches[0] == no_commas
    true
  else
    false
  end
end

--

The accepted answer by @Jakob S works for the most part, but catching exceptions can be really slow. In addition, the rescue approach fails on a string like "1,000".

Let's define the methods:

def rescue_is_number? string
  true if Float(string) rescue false
end

def regex_is_number? string
  no_commas =  string.gsub(',', '')
  matches = no_commas.match(/-?\d+(?:\.\d+)?/)
  if !matches.nil? && matches.size == 1 && matches[0] == no_commas
    true
  else
    false
  end
end

And now some test cases:

test_cases = {
  true => ["5.5", "23", "-123", "1,234,123"],
  false => ["hello", "99designs", "(123)456-7890"]
}

And a little code to run the test cases:

test_cases.each do |expected_answer, cases|
  cases.each do |test_case|
    if rescue_is_number?(test_case) != expected_answer
      puts "**rescue_is_number? got #{test_case} wrong**"
    else
      puts "rescue_is_number? got #{test_case} right"
    end

    if regex_is_number?(test_case) != expected_answer
      puts "**regex_is_number? got #{test_case} wrong**"
    else
      puts "regex_is_number? got #{test_case} right"
    end  
  end
end

Here is the output of the test cases:

rescue_is_number? got 5.5 right
regex_is_number? got 5.5 right
rescue_is_number? got 23 right
regex_is_number? got 23 right
rescue_is_number? got -123 right
regex_is_number? got -123 right
**rescue_is_number? got 1,234,123 wrong**
regex_is_number? got 1,234,123 right
rescue_is_number? got hello right
regex_is_number? got hello right
rescue_is_number? got 99designs right
regex_is_number? got 99designs right
rescue_is_number? got (123)456-7890 right
regex_is_number? got (123)456-7890 right

Time to do some performance benchmarks:

Benchmark.ips do |x|

  x.report("rescue") { test_cases.values.flatten.each { |c| rescue_is_number? c } }
  x.report("regex") { test_cases.values.flatten.each { |c| regex_is_number? c } }

  x.compare!
end

And the results:

Calculating -------------------------------------
              rescue   128.000  i/100ms
               regex     4.649k i/100ms
-------------------------------------------------
              rescue      1.348k (±16.8%) i/s -      6.656k
               regex     52.113k (± 7.8%) i/s -    260.344k

Comparison:
               regex:    52113.3 i/s
              rescue:     1347.5 i/s - 38.67x slower
pthamm
  • 1,841
  • 1
  • 14
  • 17
  • Thanks for the benchmark. The accepted answer has the advantage of accepting inputs like `5.4e-29`. I guess your regex could be tweaked to accept those also. – Jodi Feb 24 '16 at 16:27
  • 4
    Handling cases like 1,000 is really hard, since it depends on user intention. There are many, many ways for humans to format numbers. Is 1,000 about equal to 1000, or about equal to 1? Most of the world says it's about 1, not a way to show the integer 1000. – James Moore Feb 25 '17 at 18:59
7

this is how i do it, but i think too there must be a better way

object.to_i.to_s == object || object.to_f.to_s == object
antpaw
  • 15,444
  • 11
  • 59
  • 88
6

no you're just using it wrong. your is_number? has an argument. you called it without the argument

you should be doing is_number?(mystring)

corroded
  • 21,406
  • 19
  • 83
  • 132
  • Based on the is_number? method in the question, using is_a? is not giving the correct answer. If `mystring` is indeed a String, `mystring.is_a?(Integer)` will always be false. It looks like he wants a result like `is_number?("12.4") #=> true` – Jakob S Apr 14 '11 at 10:15
  • Jakob S is correct. mystring is indeed always a string, but may be comprise of just numbers. perhaps my question should have been is_numeric? so as not to confuse the datatype – Jamie Buchanan Apr 14 '11 at 10:23
4

In rails 4, you need to put require File.expand_path('../../lib', __FILE__) + '/ext/string' in your config/application.rb

jcye
  • 61
  • 5
3

If you prefer not to use exceptions as part of the logic, you might try this:

class String
   def numeric?
    !!(self =~ /^-?\d+(\.\d*)?$/)
  end
end

Or, if you want it to work across all object classes, replace class String with class Object an convert self to a string: !!(self.to_s =~ /^-?\d+(\.\d*)?$/)

Mark Schneider
  • 360
  • 2
  • 10
  • Whats the purpose of negate and do `nil?` zero is trurthy on ruby, so you can do just `!!(self =~ /^-?\d+(\.\d*)?$/)` – Arnold Roa Apr 11 '17 at 14:37
  • Using `!!` certainly works. At least one Ruby style guide (https://github.com/bbatsov/ruby-style-guide) suggested avoiding `!!` in favor of `.nil?` for readability, but I've seen `!!` used in popular repositories, and I think it is a fine way to convert to boolean. I've edited the answer. – Mark Schneider Apr 13 '17 at 02:52
2

As Jakob S suggested in his answer, Kernel#Float can be used to validate numericality of the string, only thing that I can add is one-liner version of that, without using rescue block to control flow (which is considered as a bad practice sometimes)

  Float(my_string, exception: false).present?
Nkadze
  • 31
  • 4
-3

use the following function:

def is_numeric? val
    return val.try(:to_f).try(:to_s) == val
end

so,

is_numeric? "1.2f" = false

is_numeric? "1.2" = true

is_numeric? "12f" = false

is_numeric? "12" = true

Rajesh Paul
  • 6,793
  • 6
  • 40
  • 57
  • This will fail if val is `"0"`. Also note that the method `.try` isn't part of the Ruby core library and is only available if you're including ActiveSupport. – GMA Sep 17 '15 at 12:51
  • In fact, it also fails for `"12"`, so your fourth example in this question is wrong. `"12.10"` and `"12.00"` fail too. – GMA Sep 17 '15 at 12:52
-5

How dumb is this solution?

def is_number?(i)
  begin
    i+0 == i
  rescue TypeError
    false
  end
end
donvnielsen
  • 195
  • 1
  • 9
  • 1
    This is sub-optimal because using '.respond_to?(:+)' is always better then failing and catching an exception on a specific method (:+) call. This might also fail for a variety of reasons were the Regex and conversion methods don't. – Sqeaky Apr 28 '14 at 17:17