【问题标题】:Test if string is a number in Ruby on Rails在 Ruby on Rails 中测试字符串是否为数字
【发布时间】:2011-08-05 09:51:24
【问题描述】:

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

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

以及我的控制器中的以下条件:

if mystring.is_number?

end

条件引发undefined method 错误。我猜我在错误的地方定义了is_number...?

【问题讨论】:

  • 我知道很多人都在这里,因为 codeschool 的 Rails for Zombies 测试课程。等他继续解释。测试不应该通过 --- 可以让您错误地测试失败,您可以随时修补 rails 以发明诸如 self.is_number 之类的方法?
  • 接受的答案在“1,000”之类的情况下失败,并且比使用正则表达式方法慢 39 倍。请参阅下面的答案。

标签: ruby-on-rails ruby string integer


【解决方案1】:

创建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

【讨论】:

  • 这是个坏主意。 "330.346.11".to_f # => 330.346
  • 上面没有to_f,并且 Float() 没有表现出这种行为:Float("330.346.11") raises ArgumentError: invalid value for Float(): "330.346.11"
  • 如果你使用那个补丁,我会把它重命名为 numeric?,以符合 ruby​​ 命名约定(Numeric 类继承自 Numeric,is_ 前缀是 javaish)。
  • 与原来的问题不太相关,但我可能会将代码放在lib/core_ext/string.rb
  • 我认为is_number?(string) 位不适用于 Ruby 1.9。也许这是 Rails 或 1.8 的一部分? String.is_a?(Numeric) 有效。另请参阅stackoverflow.com/questions/2095493/…
【解决方案2】:

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

  1. 如果它们相对不常见,铸造肯定是最快的。
  2. 如果错误情况很常见并且您只是检查整数,则比较与转换状态是一个不错的选择。
  3. 如果错误情况很常见并且您正在检查浮点数,那么正则表达式可能是要走的路

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

整数校验细节:

# 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

【讨论】:

    【解决方案3】:
    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
    

    【讨论】:

    • /^\d+$/ 在 Ruby 中不是安全的正则表达式,/\A\d+\Z/ 是。 (例如“42\nsome text”将返回true
    • 为了澄清@TimotheeA 的评论,如果处理行,使用/^\d+$/ 是安全的,但在这种情况下,它是关于字符串的开头和结尾,因此/\A\d+\Z/
    • 不应该编辑答案以更改响应者的实际答案吗?如果您不是响应者,则在编辑中更改答案似乎……可能是卑鄙的,应该越界。
    • \Z 允许在字符串末尾有 \n,因此 "123\n" 将通过验证,无论它不是完全数字。但是如果你使用 \z 那么它会更正确的正则表达式: /\A\d+\z/
    【解决方案4】:

    从 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
    

    【讨论】:

      【解决方案5】:

      依赖引发的异常并不是最快、可读且可靠的解决方案。
      我会做以下事情:

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

      【讨论】:

      • 不过,这只适用于正整数。 '-1'、'0.0' 或 '1_000' 等值都返回 false,即使它们是有效的数值。您正在查看 /^[-.0-9]+$/ 之类的内容,但它错误地接受了 '--'。
      • 来自 Rails 'validates_numericality_of': raw_value.to_s =~ /\A[+-]?\d+\Z/
      • NoMethodError: "asd":String 的未定义方法“应该”
      • 在最新的 rspec 中,这变成了expect(my_string).to match(/^[0-9]+$/)
      • 我喜欢:my_string =~ /\A-?(\d+)?\.?\d+\Z/ 它可以让你做 '.1'、'-0.1' 或 '12' 但不能做 '' 或 '-' 或 '.'
      【解决方案6】:

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

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

      【讨论】:

      • 它不识别浮动符号,例如1.2e+35.
      • 在 Ruby 2.4.0 中,我运行了 object = "1.2e+35"; object.to_f.to_s == object,它成功了
      【解决方案7】:

      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 等输入的优势。我想你的正则表达式也可以调整以接受这些。
      • 处理 1000 个这样的案例真的很难,因为它取决于用户的意图。人类格式化数字有很多很多方法。 1,000 大约等于 1000,还是大约等于 1?世界上大多数人都说它大约是 1,而不是显示整数 1000 的方式。
      【解决方案8】:

      不,你只是用错了。你的 is_number?有论据。你在没有参数的情况下调用它

      你应该做 is_number?(mystring)

      【讨论】:

      • 基于is_number?问题中的方法,使用 is_a?没有给出正确的答案。如果mystring 确实是一个字符串,mystring.is_a?(Integer) 将始终为假。看起来他想要is_number?("12.4") #=> true 这样的结果
      • Jakob S 是正确的。 mystring 确实总是一个字符串,但可能只包含数字。也许我的问题应该是 is_numeric?以免混淆数据类型
      【解决方案9】:

      在 rails 4 中,你需要把 require File.expand_path('../../lib', __FILE__) + '/ext/string' 在你的 config/application.rb

      【讨论】:

      • 其实你不需要这样做,你只要把string.rb放在“initializers”中就可以了!
      【解决方案10】:

      如果您不想将异常用作逻辑的一部分,您可以试试这个:

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

      或者,如果您希望它适用于所有对象类,请将 class String 替换为 class Object 并将 self 转换为字符串:!!(self.to_s =~ /^-?\d+(\.\d*)?$/)

      【讨论】:

      • 否定和执行nil? 零的目的是什么在 ruby​​ 上是真实的,所以您可以只执行 !!(self =~ /^-?\d+(\.\d*)?$/)
      • 使用!! 确实有效。至少有一个 Ruby 风格指南 (github.com/bbatsov/ruby-style-guide) 建议避免使用 !! 以支持 .nil? 以提高可读性,但我已经看到 !! 在流行的存储库中使用,我认为这是转换为布尔值的好方法.我已经编辑了答案。
      【解决方案11】:

      由于Jakob S suggested in his answerKernel#Float 可用于验证字符串的数字,我唯一可以添加的是单行版本,而不使用rescue 块来控制流(这被认为是有时是不好的做法)

        Float(my_string, exception: false).present?
      

      【讨论】:

        【解决方案12】:

        使用以下函数:

        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 时才可用。
        • 事实上,"12" 也失败了,所以你在这个问题中的第四个例子是错误的。 "12.10""12.00" 也失败了。
        【解决方案13】:

        这个解决方案有多愚蠢?

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

        【讨论】:

        • 这是次优的,因为使用 '.respond_to?(:+)' 总是比在特定方法 (:+) 调用上失败并捕获异常要好。如果正则表达式和转换方法没有,这也可能由于多种原因而失败。
        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2012-04-25
        • 2013-08-05
        • 1970-01-01
        相关资源
        最近更新 更多