Kiểm tra xem chuỗi có phải là một số trong Ruby on Rails không


103

Tôi có những thứ sau trong bộ điều khiển ứng dụng của mình:

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

và điều kiện sau trong bộ điều khiển của tôi:

if mystring.is_number?

end

Điều kiện là ném một undefined methodlỗi. Tôi đoán tôi đã xác định is_numbersai chỗ ...?


4
Tôi biết rất nhiều người ở đây vì lớp học Rails for Zombies Testing của Codechool. Chỉ đợi anh ta tiếp tục giải thích. Các bài kiểm tra không được phép vượt qua --- bạn có thể kiểm tra lỗi không thành công, bạn luôn có thể vá các đường ray để phát minh ra các phương pháp như self.is_number?
boulder_ruby

Câu trả lời được chấp nhận không thành công trong các trường hợp như "1,000" và chậm hơn 39 lần so với việc sử dụng phương pháp regex. Xem câu trả lời của tôi dưới đây.
pthamm 19/02/16

Câu trả lời:


186

Tạo is_number?phương pháp.

Tạo một phương thức trợ giúp:

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

Và sau đó gọi nó như thế này:

my_string = '12.34'

is_number?( my_string )
# => true

Mở rộng Stringlớp học.

Nếu bạn muốn có thể gọi is_number?trực tiếp trên chuỗi thay vì chuyển nó dưới dạng tham số cho hàm trợ giúp của mình, thì bạn cần phải xác định is_number?như một phần mở rộng của Stringlớp, như sau:

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

Và sau đó bạn có thể gọi nó bằng:

my_string.is_number?
# => true

2
Đây là một ý tưởng tồi. "330.346.11" .to_f # => 330.346
epochwolf

11
Không có to_fở trên, và phao () không thể hiện rằng hành vi: Float("330.346.11")tăng lươngArgumentError: invalid value for Float(): "330.346.11"
Jakob S

7
Nếu bạn sử dụng bản vá đó, tôi sẽ đổi tên nó thành số ?, để phù hợp với quy ước đặt tên ruby ​​(Các lớp số kế thừa từ Numeric, tiền tố is_ là javaish).
Konrad Reiche

10
Không thực sự liên quan đến câu hỏi ban đầu, nhưng có lẽ tôi sẽ đưa mã vào lib/core_ext/string.rb.
Jakob S

1
Tôi không nghĩ rằng is_number?(string)bit hoạt động Ruby 1.9. Có lẽ đó là một phần của Rails hoặc 1.8? String.is_a?(Numeric)làm. Xem thêm stackoverflow.com/questions/2095493/… .
Ross Attrill

30

Đây là điểm chuẩn cho những cách phổ biến để giải quyết vấn đề này. Lưu ý rằng bạn nên sử dụng cái nào có lẽ phụ thuộc vào tỷ lệ trường hợp sai dự kiến.

  1. Nếu chúng tương đối không phổ biến thì việc casting chắc chắn là nhanh nhất.
  2. Nếu trường hợp sai là phổ biến và bạn chỉ đang kiểm tra số nguyên, so sánh với trạng thái đã chuyển đổi là một lựa chọn tốt.
  3. Nếu các trường hợp sai là phổ biến và bạn đang kiểm tra phao, thì regexp có thể là cách để đi

Nếu hiệu suất không quan trọng, hãy sử dụng những gì bạn thích. :-)

Chi tiết kiểm tra số nguyên:

# 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

Chi tiết kiểm tra phao:

# 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+$/không phải là một regexp an toàn trong Ruby, /\A\d+\Z/là. (ví dụ: "42 \ nsome text" sẽ trả lại true)
Timothee A

Để làm rõ về nhận xét của @ TimotheeA, nó là an toàn để sử dụng /^\d+$/nếu xử lý các dòng nhưng trong trường hợp này là về phần đầu và phần cuối của một chuỗi, do đó /\A\d+\Z/.
Julio

1
Không nên chỉnh sửa câu trả lời để thay đổi câu trả lời thực tế BỞI người trả lời? thay đổi câu trả lời trong một bản chỉnh sửa nếu bạn không phải là người trả lời có vẻ như ... có thể bị ám chỉ và nên vượt quá giới hạn.
jaydel vào

2
\ Z cho phép có \ n ở cuối chuỗi, vì vậy "123 \ n" sẽ vượt qua xác thực, bất kể nó không phải là số hoàn toàn. Nhưng nếu bạn sử dụng \ z thì nó sẽ chính xác hơn regexp: / \ A \ d + \ z /
SunnyMagadan 14/08/17

15

Dựa vào ngoại lệ nêu ra không phải là giải pháp nhanh nhất, dễ đọc và đáng tin cậy.
Tôi sẽ làm như sau:

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

1
Tuy nhiên, điều này chỉ hoạt động đối với các số nguyên dương. Các giá trị như '-1', '0.0' hoặc '1_000' đều trả về false mặc dù chúng là các giá trị số hợp lệ. Bạn đang xem một cái gì đó như / ^ [- .0-9] + $ /, nhưng nó chấp nhận sai '- -'.
Jakob S

13
Từ Rails 'validates_numericality_of': raw_value.to_s = ~ / \ A [+ -]? \ D + \ Z /
Morten

NoMethodError: phương thức không xác định `should 'cho" asd ": String
sergserg

Trong rspec mới nhất, điều này trở thànhexpect(my_string).to match(/^[0-9]+$/)
Damien Mathieu

Tôi thích: my_string =~ /\A-?(\d+)?\.?\d+\Z/nó cho phép bạn làm '.1', '-0.1', hoặc '12' nhưng không phải '' hoặc '-' hoặc '.'
Josh

8

Kể từ Ruby 2.6.0, các phương thức exceptionép kiểu số có một đối số tùy chọn [1] . Điều này cho phép chúng tôi sử dụng các phương thức tích hợp sẵn mà không sử dụng ngoại lệ làm luồng điều khiển:

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

Do đó, bạn không phải xác định phương thức của riêng mình mà có thể kiểm tra trực tiếp các biến như vd

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

7

đây là cách tôi làm, nhưng tôi cũng nghĩ phải có một cách tốt hơn

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

5
Nó không nhận dạng ký hiệu nổi, ví dụ: 1.2e + 35.
hipertracker

1
Trong Ruby 2.4.0 Tôi chạy object = "1.2e+35"; object.to_f.to_s == objectvà nó làm việc
Giovanni Benussi

6

không, bạn chỉ đang sử dụng sai. is_number của bạn? có một lập luận. bạn đã gọi nó mà không cần tranh luận

bạn nên làm is_number? (mystring)


Dựa trên is_number? trong câu hỏi, sử dụng is_a? không đưa ra câu trả lời chính xác. Nếu mystringthực sự là một chuỗi, mystring.is_a?(Integer)sẽ luôn luôn là false. Dường như ông muốn có một kết quả nhưis_number?("12.4") #=> true
Jakob S

Jakob S đúng. mystring thực sự luôn là một chuỗi, nhưng có thể chỉ bao gồm các số. có lẽ câu hỏi của tôi nên là is_numeric? để không nhầm lẫn giữa datatype
Jamie Buchanan

6

Tl; dr: Sử dụng cách tiếp cận regex. Nó nhanh hơn 39 lần so với cách tiếp cận giải cứu trong câu trả lời được chấp nhận và cũng xử lý các trường hợp như "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

-

Câu trả lời được chấp nhận bởi @Jakob S hầu hết hoạt động, nhưng việc bắt các ngoại lệ có thể thực sự chậm. Ngoài ra, cách tiếp cận giải cứu không thành công trên một chuỗi như "1.000".

Hãy xác định các phương thức:

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

Và bây giờ là một số trường hợp thử nghiệm:

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

Và một đoạn mã nhỏ để chạy các trường hợp thử nghiệm:

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

Đây là kết quả của các trường hợp thử nghiệm:

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

Đã đến lúc thực hiện một số điểm chuẩn hiệu suất:

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

Và kết quả:

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

Cảm ơn về điểm chuẩn. Câu trả lời được chấp nhận có lợi thế của việc chấp nhận đầu vào như 5.4e-29. Tôi đoán regex của bạn có thể được điều chỉnh để chấp nhận những điều đó.
Jodi

3
Xử lý các trường hợp như 1.000 là thực sự khó khăn, vì nó phụ thuộc vào ý định của người dùng. Có rất nhiều cách để con người định dạng số. 1.000 có bằng 1000 hay khoảng bằng 1? Hầu hết thế giới nói rằng đó là khoảng 1, không phải là một cách để hiển thị số nguyên 1000.
James Moore

4

Trong rails 4, bạn cần đưa require File.expand_path('../../lib', __FILE__) + '/ext/string' vào config / application.rb của mình


1
thực sự bạn không cần phải làm điều này, bạn chỉ có thể đặt string.rb trong "khởi tạo" và nó hoạt động!
mahatmanich

3

Nếu bạn không muốn sử dụng các ngoại lệ như một phần của logic, bạn có thể thử cách này:

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

Hoặc, nếu bạn muốn nó hoạt động trên tất cả các lớp đối tượng, hãy thay thế class Stringbằng class Objectmột tự chuyển đổi thành một chuỗi: !!(self.to_s =~ /^-?\d+(\.\d*)?$/)


Mục đích của việc phủ định và làm nil?bằng không là gì trên ruby, vì vậy bạn có thể làm chỉ!!(self =~ /^-?\d+(\.\d*)?$/)
Arnold Roa

Sử dụng !!chắc chắn hiệu quả. Ít nhất một hướng dẫn kiểu Ruby ( github.com/bbatsov/ruby-style-guide ) đã đề xuất tránh để có !!lợi .nil?cho tính dễ đọc, nhưng tôi đã thấy !!được sử dụng trong các kho lưu trữ phổ biến và tôi nghĩ rằng đó là một cách tốt để chuyển đổi sang boolean. Tôi đã chỉnh sửa câu trả lời.
Mark Schneider

-3

sử dụng chức năng sau:

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

vì thế,

is_numeric? "1.2f" = sai

is_numeric? "1.2" = true

is_numeric? "12f" = sai

is_numeric? "12" = true


Điều này sẽ không thành công nếu val được "0". Cũng lưu ý rằng phương pháp .trynày không phải là một phần của thư viện lõi Ruby và chỉ khả dụng nếu bạn bao gồm ActiveSupport.
GMA

Trong thực tế, nó cũng không thành công "12", vì vậy ví dụ thứ tư của bạn trong câu hỏi này là sai. "12.10""12.00"thất bại quá.
GMA

-5

Làm thế nào là ngu ngốc giải pháp này?

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

1
Điều này là không tối ưu vì sử dụng '.respond_to? (: +)' Luôn tốt hơn sau đó không thành công và bắt một ngoại lệ trên một lệnh gọi phương thức (: +) cụ thể. Điều này cũng có thể không thành công vì nhiều lý do khác nhau như Regex và các phương pháp chuyển đổi thì không.
Sqeaky
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.