测试字符串在Ruby on Rails中是否为数字


103

我的应用程序控制器中有以下内容:

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

以及控制器中的以下情况:

if mystring.is_number?

end

条件抛出undefined method错误。我猜我已经定义了is_number在错误的地方了...?


4
我知道很多人在这里是因为codeschool的“僵尸测试Rails”课程。请等待他继续解释。测试不应该通过---确定测试失败是可以的,您始终可以修补rails来发明诸如self.is_number之类的方法?
boulder_ruby

在“ 1,000”之类的情况下,可接受的答案失败,并且比使用正则表达式的方法慢39倍。请参阅下面的答案。
pthamm '16

Answers:


186

创建is_number?方法。

创建一个辅助方法:

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

然后这样称呼它:

my_string = '12.34'

is_number?( my_string )
# => true

扩展String类。

如果您希望能够is_number?直接在字符串上调用,而不是将其作为参数传递给您的辅助函数,则需要将其定义is_number?String类的扩展,如下所示:

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

然后可以使用以下命令调用它:

my_string.is_number?
# => true

2
这是一个坏主意。“ 330.346.11” .to_f#=> 330.346
epochwolf 2011年

11
to_f上面没有任何内容,并且Float()没有表现出这种行为:Float("330.346.11")引发ArgumentError: invalid value for Float(): "330.346.11"
Jakob S

7
如果您使用该补丁程序,则将其重命名为numeric ?,以便与ruby命名约定保持一致(数值类继承自Numeric,is_前缀是Javaish)。
Konrad Reiche 2012年

10
与原始问题并不完全相关,但我可能会将代码放入中lib/core_ext/string.rb
雅各布·S

1
我认为is_number?(string)Ruby 1.9 并不适用。也许这是Rails或1.8的一部分? String.is_a?(Numeric)作品。另请参见stackoverflow.com/questions/2095493/…
Ross Attrill 2014年

30

这是解决此问题的常用方法的基准。请注意,应该使用哪一种可能取决于预期的错误案例比率。

  1. 如果它们相对不常见,铸造绝对是最快的。
  2. 如果错误的情况很普遍,而您只是检查整数,那么比较与转换状态是一个不错的选择。
  3. 如果错误的情况很普遍并且您正在检查浮点数,则可以使用regexp

如果性能无关紧要,请使用您喜欢的东西。:-)

整数检查详细信息:

# 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

浮动检查详细信息:

# 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

29
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

12
/^\d+$/在Ruby中不是安全的正则表达式,/\A\d+\Z/是。(例如,将返回“ 42 \ nsome text” true
Timothee

要澄清@TimotheeA的注释,可以安全地使用/^\d+$/行,但在这种情况下,它是关于字符串的开头和结尾的,因此/\A\d+\Z/
Julio

1
响应者是否不应该编辑答案来更改实际答案?如果您不是响应者,则在编辑中更改答案似乎...可能不足,应该超出范围。
jaydel

2
\ Z允许在字符串的末尾带有\ n,因此“ 123 \ n”将通过验证,无论它不是完全数字的。但是,如果使用\ z,则它将是更正确的正则表达式:/ \ A \ d + \ z /
SunnyMagadan

15

依靠引发的异常并不是最快,可读性也不可靠的解决方案。
我将执行以下操作:

my_string.should =~ /^[0-9]+$/

1
但是,这仅适用于正整数。像“ -1”,“ 0.0”或“ 1_000”之类的值均返回false,即使它们是有效的数值。您正在查看类似/ ^ [ -.0-9] + $ /的东西,但是错误地接受了' --'。
Jakob S

13
来自Rails的“ validates_numericality_of”:raw_value.to_s =〜/ \ A [+-]?\ d + \ Z /
Morten

NoMethodError:“应该”的“ asd”未定义方法“应该”:字符串
sergserg 2014年

在最新的rspec中,它变为expect(my_string).to match(/^[0-9]+$/)
Damien MATHIEU

我喜欢:my_string =~ /\A-?(\d+)?\.?\d+\Z/它可以让您执行'.1','-0.1'或'12',但不能执行','-'或'。
乔什(Josh)

8

从Ruby 2.6.0开始,数字转换方法具有可选的exception-argument [1]。这使我们能够使用内置方法,而无需将异常用作控制流:

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

因此,您不必定义自己的方法,而可以直接检查变量,例如

if Float(my_var, exception: false)
  # do something if my_var is a float
end

7

这就是我的做法,但我认为也必须有更好的方法

object.to_i.to_s == object || object.to_f.to_s == object

5
它不能识别浮动表示法,例如1.2e + 35。
hipertracker 2012年

1
在Ruby 2.4.0我跑object = "1.2e+35"; object.to_f.to_s == object和它的工作
乔瓦尼Benussi的

6

不,您只是在错误地使用它。您的is_number?有一个论点。你不带参数就叫它

你应该在做is_number?(mystring)


基于is_number?问题中的方法,使用is_a?没有给出正确的答案。如果mystring确实是String,mystring.is_a?(Integer)则始终为false。看起来他想要这样的结果is_number?("12.4") #=> true
Jakob S

Jakob S是正确的。mystring确实总是一个字符串,但可能只包含数字。也许我的问题应该是is_numeric?以免混淆数据类型
Jamie Buchanan

6

Tl; dr:使用正则表达式方法。在接受的答案中,它比救援方法快39倍,并且可以处理“ 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

-

@Jakob S接受的答案在大多数情况下都是有效的,但是捕获异常确实很慢。另外,救援方法在字符串“ 1,000”上失败。

让我们定义方法:

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

现在,一些测试用例:

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

和一些代码来运行测试用例:

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

这是测试用例的输出:

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

是时候做一些性能基准测试了:

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

结果:

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

感谢您提供基准。接受的答案的优点是可以接受输入5.4e-29。我想您的正则表达式可能会被调整为也接受那些。
乔迪

3
处理1000个这样的案例确实很困难,因为这取决于用户的意图。人们可以使用多种方式来格式化数字。1,000是大约等于1000,还是大约等于1?世界上大多数人说,这是约1,不显示整数1000的方式
詹姆斯·穆尔

4

在Rails 4中,您需要放入 require File.expand_path('../../lib', __FILE__) + '/ext/string' config / application.rb


1
实际上,您不需要这样做,只需将string.rb放在“初始化程序”中就可以了!
mahatmanich

3

如果您不希望将异常用作逻辑的一部分,则可以尝试以下操作:

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

或者,如果你想让它在所有对象类的工作,更换class Stringclass Object的转换自转换为字符串: !!(self.to_s =~ /^-?\d+(\.\d*)?$/)


否定nil?零零目的是什么,这对红宝石来说很重要,所以您可以做到!!(self =~ /^-?\d+(\.\d*)?$/)
Arnold Roa

使用!!肯定有效。至少有一个Ruby的风格指南(github.com/bbatsov/ruby-style-guide)建议避免!!有利于.nil?提高可读性,但我已经看到了!!在流行的仓库使用,我认为这是一个很好的方式转换为Boolean。我已经编辑了答案。
Mark Schneider

-3

使用以下功能:

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

所以,

is_numeric? "1.2f" =错误

is_numeric? "1.2" =真

is_numeric? "12f" =错误

is_numeric? "12" =真


如果val为,则将失败"0"。还要注意,该方法.try不是Ruby核心库的一部分,仅在包含ActiveSupport时可用。
GMA

实际上,它也对失败"12",因此您在该问题中的第四个示例是错误的。"12.10""12.00"失败了
GMA

-5

这个解决方案有多愚蠢?

def is_number?(i)
  begin
    i+0 == i
  rescue TypeError
    false
  end
end

1
这是次优的,因为使用'.respond_to?(:+)'总是比失败并捕获特定方法(:+)调用上的异常更好。由于正则表达式和转换方法不行,这也可能由于多种原因而失败。
Sqeaky
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.