【问题标题】:Determine if a string is a valid float value确定字符串是否为有效的浮点值
【发布时间】:2009-06-23 18:49:14
【问题描述】:

有没有办法简单地检查一个字符串值是否是一个有效的浮点值。如果字符串不是数值,则对字符串调用 to_f 会将其转换为 0.0。并且使用 Float() 会在传递一个更接近我想要的无效浮点字符串时引发异常,但我不想处理捕获异常。我真正想要的是诸如nan之类的方法?它确实存在于 Float 类中,但这无济于事,因为非数字字符串无法在不更改为 0.0(使用 to_f)的情况下转换为浮点数。

"a".to_f => 0.0

"a".to_f.nan? => false

Float("a") => ArgumentError: invalid value for Float(): "a"

是否有一个简单的解决方案或者我需要编写代码来检查一个字符串是否是一个有效的浮点值?

【问题讨论】:

    标签: ruby-on-rails ruby


    【解决方案1】:

    这是一种方法:

    class String
      def valid_float?
        # The double negation turns this into an actual boolean true - if you're 
        # okay with "truthy" values (like 0.0), you can remove it.
        !!Float(self) rescue false
      end
    end
    
    "a".valid_float? #false
    "2.4".valid_float? #true
    

    如果你想避免 String 的猴子补丁,你总是可以把它作为你控制的某个模块的类方法,当然:

    module MyUtils
      def self.valid_float?(str)
        !!Float(str) rescue false
      end
    end
    MyUtils.valid_float?("a") #false
    

    【讨论】:

    • 你不应该避免在它的修饰符形式中使用rescue吗?
    • 不错的答案。作为一个小建议,我会避免使用双重否定,因为它建议here
    【解决方案2】:

    关于 Ruby 世界的一个有趣事实是 Rubinius 项目的存在,该项目主要使用纯 Ruby 实现 Ruby 及其标准库。因此,他们有一个 Kernel#Float 的纯 Ruby 实现,如下所示:

    def Float(obj)
      raise TypeError, "can't convert nil into Float" if obj.nil?
    
      if obj.is_a?(String)
        if obj !~ /^\s*[+-]?((\d+_?)*\d+(\.(\d+_?)*\d+)?|\.(\d+_?)*\d+)(\s*|([eE][+-]?(\d+_?)*\d+)\s*)$/
          raise ArgumentError, "invalid value for Float(): #{obj.inspect}"
        end
      end
    
      Type.coerce_to(obj, Float, :to_f)
    end
    

    这为您提供了一个正则表达式,它与 Ruby 在运行 Float() 时所做的内部工作相匹配,但没有例外。所以你现在可以这样做:

    class String
      def nan?
        self !~ /^\s*[+-]?((\d+_?)*\d+(\.(\d+_?)*\d+)?|\.(\d+_?)*\d+)(\s*|([eE][+-]?(\d+_?)*\d+)\s*)$/
      end
    end
    

    这个解决方案的好处是,由于 Rubinius 运行并通过了 RubySpec,你知道这个正则表达式处理 Ruby 本身处理的边缘情况,你可以毫无顾忌地调用字符串上的 to_f!

    【讨论】:

    【解决方案3】:
    # Edge Cases:
    # numeric?"Infinity" => true is_numeric?"Infinity" => false
    
    
    def numeric?(object)
    true if Float(object) rescue false
    end
    
    #Possibly faster alternative
    def is_numeric?(i)
    i.to_i.to_s == i || i.to_f.to_s == i
    end
    

    【讨论】:

    • NB 'faster alternative' 将返回 false '5.00'
    【解决方案4】:

    Ruby 2.6+

    Ruby 2.6 向Float 添加了一个新的exception keyword argument

    所以,现在检查一个字符串是否包含一个有效的浮点数很简单:

    Float('22.241234', exception: false)
    # => 22.241234
    
    Float('abcd', exception: false)
    # => nil
    

    这里是a link 文档。

    【讨论】:

    【解决方案5】:

    我看到了关于 cast+exceptions 与正则表达式的未解决讨论,我想我会尝试对所有内容进行基准测试并得出一个客观的答案:

    以下是此处尝试的每种方法的最佳情况和最差情况的来源:

    require "benchmark"
    n = 500000
    
    def is_float?(fl)
      !!Float(fl) rescue false
    end
    
    def is_float_reg(fl)
      fl =~ /(^(\d+)(\.)?(\d+)?)|(^(\d+)?(\.)(\d+))/
    end
    
    class String
      def to_float
        Float self rescue (0.0 / 0.0)
      end
    end
    
    
    Benchmark.bm(7) do |x|
      x.report("Using cast best case") {
        n.times do |i|
          temp_fl = "#{i + 0.5}"
          is_float?(temp_fl)
        end
      }
      x.report("Using cast worst case") {
        n.times do |i|
          temp_fl = "asdf#{i + 0.5}"
          is_float?(temp_fl)
        end
      }
      x.report("Using cast2 best case") {
        n.times do |i|
          "#{i + 0.5}".to_float
        end
      }
      x.report("Using cast2 worst case") {
        n.times do |i|
          "asdf#{i + 0.5}".to_float
        end
      }
      x.report("Using regexp short") {
        n.times do |i|
          temp_fl = "#{i + 0.5}"
          is_float_reg(temp_fl)
        end
      }
      x.report("Using regexp long") {
        n.times do |i|
          temp_fl = "12340918234981234#{i + 0.5}"
          is_float_reg(temp_fl)
        end
      }
        x.report("Using regexp short fail") {
        n.times do |i|
          temp_fl = "asdf#{i + 0.5}"
          is_float_reg(temp_fl)
        end
      }
      x.report("Using regexp long fail") {
        n.times do |i|
          temp_fl = "12340918234981234#{i + 0.5}asdf"
          is_float_reg(temp_fl)
        end
      }
    
    end
    

    mri193 的结果如下:

                  user     system      total        real
    Using cast best case  0.608000   0.000000   0.608000 (  0.615000)
    Using cast worst case  5.647000   0.094000   5.741000 (  5.745000)
    Using cast2 best case  0.593000   0.000000   0.593000 (  0.586000)
    Using cast2 worst case  5.788000   0.047000   5.835000 (  5.839000)
    Using regexp short  0.951000   0.000000   0.951000 (  0.952000)
    Using regexp long  1.217000   0.000000   1.217000 (  1.214000)
    Using regexp short fail  1.201000   0.000000   1.201000 (  1.202000)
    Using regexp long fail  1.295000   0.000000   1.295000 (  1.284000)
    

    由于我们只处理线性时间算法,我认为我们使用经验测量来进行概括。很明显,正则表达式更加一致,并且只会根据传递的字符串的长度略有波动。演员在没有失败时显然更快,而在失败时则慢得多。

    如果我们比较成功时间,我们可以看到强制转换的最佳情况比正则表达式的最佳情况快约 0.3 秒。如果我们将其除以最坏情况下的时间量,我们可以估计需要多少次运行才能达到收支平衡,但例外情况会减慢投射速度以匹配正则表达式速度。 0.3 的大约 6 秒为我们提供了大约 20 秒。因此,如果性能很重要,并且您预计不到 20 分之一的测试失败,那么使用 cast+exceptions。

    JRuby 1.7.4 的结果完全不同:

                  user     system      total        real
    Using cast best case  2.575000   0.000000   2.575000 (  2.575000)
    Using cast worst case 53.260000   0.000000  53.260000 ( 53.260000)
    Using cast2 best case  2.375000   0.000000   2.375000 (  2.375000)
    Using cast2 worst case 53.822000   0.000000  53.822000 ( 53.822000)
    Using regexp short  2.637000   0.000000   2.637000 (  2.637000)
    Using regexp long  3.395000   0.000000   3.395000 (  3.396000)
    Using regexp short fail  3.072000   0.000000   3.072000 (  3.073000)
    Using regexp long fail  3.375000   0.000000   3.375000 (  3.374000)
    

    在最佳情况下,Cast 只会稍微快一点(大约 10%)。假设这种差异适合进行概括(我不认为是),那么盈亏平衡点在 200 到 250 次运行之间,只有 1 次导致异常。

    因此,只有在发生真正异常的事情时才应使用异常,这是您和您的代码库的决定。当它们不被使用时,它们所在的代码可以更简单、更快。

    如果性能无关紧要,您可能应该只遵循您的团队或代码库已有的任何约定,而忽略整个答案。

    【讨论】:

      【解决方案6】:
      def float?(string)
        true if Float(string) rescue false
      end
      

      这支持1.55123.4561_000,但不支持1 0001,000等(例如与String#to_f相同)。

      >> float?("1.2")
      => true
      >> float?("1")
      => true
      >> float?("1 000")
      => false
      >> float?("abc")
      => false
      >> float?("1_000")
      => true
      

      来源:https://github.com/ruby/ruby/blob/trunk/object.c#L2934-L2959

      【讨论】:

        【解决方案7】:

        嗯,如果你不想要异常,那么也许:

        def is_float?(fl) fl =~ /(^(\d+)(\.)?(\d+)?)|(^(\d+)?(\.)(\d+))/ 结尾

        由于 OP 专门要求无例外的解决方案。基于正则表达式的解决方案有点慢:

        需要“基准” n = 500000 def is_float?(fl) !!Float(fl) 救援假 结尾 def is_float_reg(fl) fl =~ /(^(\d+)(\.)?(\d+)?)|(^(\d+)?(\.)(\d+))/ 结尾 Benchmark.bm(7) 做 |x| x.report("使用演员表") { n.次做|i| temp_fl = "#{i + 0.5}" is_float?(temp_fl) 结尾 } x.report("使用正则表达式") { n.次做|i| temp_fl = "#{i + 0.5}" is_float_reg(temp_fl) 结尾 } 结尾

        结果:

        5286 sn-ps:主人!? % 用户系统总真实 使用演员表 3.000000 0.000000 3.000000 ( 3.010926) 使用正则表达式 5.020000 0.000000 5.020000 (5.021762)

        【讨论】:

        • Float 不是投射原生例程,而上面的正则表达式要慢得多吗?
        • “基于正则表达式的解决方案有点慢” - 再次检查你的数字 3/5 等于 60%。我不会将损失 40% 称为边际下跌。
        • 另外,请记住,如果您的转换会更频繁地引发异常而不是,那么它会比正则表达式慢得多。这是因为从异常中救援非常慢,如下所示:simonecarletti.com/blog/2010/01/how-slow-are-ruby-exceptions
        【解决方案8】:

        试试这个

        def is_float(val)
          fval = !!Float(val) rescue false
          # if val is "1.50" for instance
          # we need to lop off the trailing 0(s) with gsub else no match
          return fval && Float(val).to_s == val.to_s.gsub(/0+$/,'') ? true:false
        end 
        
        s = "1000"
        is_float s
         => false 
        
        s = "1.5"
        is_float s
         => true 
        
        s = "Bob"
        is_float s
         => false
        
        n = 1000
        is_float n
         => false 
        
        n = 1.5
        is_float n
         => true
        

        【讨论】:

          【解决方案9】:

          我尝试将此添加为评论,但显然 cmets 中没有格式:

          另一方面,为什么不把它用作你的转换函数,比如

          class String
            def to_float
              Float self rescue (0.0 / 0.0)
            end
          end
          "a".to_float.nan? => true
          

          这当然是你一开始不想做的事情。我想答案是,“如果你真的不想使用异常处理,你必须编写自己的函数,但是,你为什么要这样做呢?”

          【讨论】:

          • 我只是想明确一点,使用 0.0 / 0.0 是一种肮脏的技巧,但如果你想获得 NaN,它是目前唯一的方法(据我所知)。如果这是我的程序,我会强烈考虑改用 nil。
          猜你喜欢
          • 2019-03-15
          • 2023-01-10
          • 1970-01-01
          • 2011-01-08
          • 1970-01-01
          • 2023-03-21
          • 2011-01-11
          • 2012-04-02
          • 1970-01-01
          相关资源
          最近更新 更多