Uji apakah string adalah angka di Ruby on Rails

103

Saya memiliki yang berikut ini di pengontrol aplikasi saya:

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

dan kondisi berikut di pengontrol saya:

if mystring.is_number?

end

Kondisinya menimbulkan undefined methodkesalahan. Saya kira saya telah mendefinisikan is_numberdi tempat yang salah ...?

Jamie Buchanan
sumber
4
Saya tahu banyak orang di sini karena kelas Rails for Zombies Testing di sekolah kode. Tunggu saja dia terus menjelaskan. Tes tidak seharusnya lulus --- tidak apa-apa jika Anda menguji gagal karena kesalahan, Anda selalu dapat menambal rel untuk menemukan metode seperti self.is_number?
boulder_ruby
Jawaban yang diterima gagal pada kasus seperti "1.000" dan 39x lebih lambat daripada menggunakan pendekatan regex. Lihat jawaban saya di bawah.
pthamm

Jawaban:

186

Buat is_number?Metode.

Buat metode pembantu:

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

Dan kemudian menyebutnya seperti ini:

my_string = '12.34'

is_number?( my_string )
# => true

Memperpanjang StringKelas.

Jika Anda ingin dapat memanggil is_number?langsung pada string alih-alih meneruskannya sebagai param ke fungsi helper Anda, maka Anda perlu mendefinisikannya is_number?sebagai ekstensi Stringkelas, seperti:

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

Dan kemudian Anda dapat menyebutnya dengan:

my_string.is_number?
# => true
Jakob S
sumber
2
Ini ide yang buruk. "330.346.11" .to_f # => 330.346
epochwolf
11
Tidak ada to_fdi atas, dan Float () tidak menunjukkan perilaku itu: Float("330.346.11")menaikkanArgumentError: invalid value for Float(): "330.346.11"
Jakob S
7
Jika Anda menggunakan tambalan itu, saya akan mengganti namanya menjadi numerik ?, agar tetap sejalan dengan konvensi penamaan ruby ​​(Kelas numerik diturunkan dari Numeric, prefiks is_ adalah javaish).
Konrad Reiche
10
Tidak terlalu relevan dengan pertanyaan awal, tapi saya mungkin akan memasukkan kodenya lib/core_ext/string.rb.
Jakob S
1
Saya tidak berpikir is_number?(string)bit berfungsi Ruby 1.9. Mungkin itu bagian dari Rails atau 1.8? String.is_a?(Numeric)bekerja. Lihat juga stackoverflow.com/questions/2095493/… .
Ross Attrill
30

Berikut ini patokan cara umum untuk mengatasi masalah ini. Perhatikan mana yang harus Anda gunakan mungkin bergantung pada rasio kasus palsu yang diharapkan.

  1. Jika mereka relatif jarang casting pasti tercepat.
  2. Jika kasus palsu sering terjadi dan Anda hanya memeriksa int, perbandingan vs keadaan yang diubah adalah pilihan yang baik.
  3. Jika kasus palsu sering terjadi dan Anda memeriksa float, regexp mungkin adalah cara yang tepat

Jika kinerja tidak penting, gunakan apa yang Anda suka. :-)

Detail pengecekan bilangan bulat:

# 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

Rincian pemeriksaan float:

# 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
sumber
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
hipertracker
sumber
12
/^\d+$/bukan regexp yang aman di Ruby /\A\d+\Z/. (misalnya "42 \ nbeberapa teks" akan kembali true)
Timothee A
Untuk memperjelas komentar @TimotheeA, aman digunakan /^\d+$/jika berurusan dengan baris tetapi dalam kasus ini tentang awal dan akhir string /\A\d+\Z/.
Julio
1
Bukankah jawaban harus diedit untuk mengubah jawaban yang sebenarnya OLEH responden? mengubah jawaban dalam edit jika Anda bukan penjawab tampaknya ... mungkin curang dan harus di luar batas.
jaydel
2
\ Z memungkinkan untuk memiliki \ n di akhir string, jadi "123 \ n" akan lolos validasi, terlepas dari itu tidak sepenuhnya numerik. Tetapi jika Anda menggunakan \ z maka regexp akan lebih tepat: / \ A \ d + \ z /
SunnyMagadan
15

Mengandalkan pengecualian yang dimunculkan bukanlah solusi tercepat, terbaca, atau andal.
Saya akan melakukan hal berikut:

my_string.should =~ /^[0-9]+$/
Damien MATHIEU
sumber
1
Namun, ini hanya berfungsi untuk bilangan bulat positif. Nilai seperti '-1', '0.0', atau '1_000' semuanya kembali salah meskipun merupakan nilai numerik yang valid. Anda melihat sesuatu seperti / ^ [- .0-9] + $ /, tetapi menerima '- -' secara keliru .
Jakob S
13
Dari Rails 'validates_numericality_of': raw_value.to_s = ~ / \ A [+ -]? \ D + \ Z /
Morten
NoMethodError: metode tidak terdefinisi `harus 'untuk" asd ": String
sergserg
Dalam rspec terbaru, ini menjadiexpect(my_string).to match(/^[0-9]+$/)
Damien MATHIEU
Saya suka: my_string =~ /\A-?(\d+)?\.?\d+\Z/ini memungkinkan Anda melakukan '.1', '-0.1', atau '12' tetapi tidak '' atau '-' atau '.'
Josh
8

Pada Ruby 2.6.0, metode-cor numerik memiliki exceptionargumen- opsional [1] . Ini memungkinkan kami untuk menggunakan metode bawaan tanpa menggunakan pengecualian sebagai aliran kontrol:

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

Oleh karena itu, Anda tidak harus menentukan metode Anda sendiri, tetapi dapat langsung memeriksa variabel seperti mis

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

begitulah cara saya melakukannya, tetapi saya pikir juga pasti ada cara yang lebih baik

object.to_i.to_s == object || object.to_f.to_s == object
antpaw
sumber
5
Itu tidak mengenali notasi mengambang, misalnya 1.2e + 35.
hipertracker
1
Di Ruby 2.4.0 saya menjalankan object = "1.2e+35"; object.to_f.to_s == objectdan berhasil
Giovanni Benussi
6

tidak, Anda hanya salah menggunakannya. is_number Anda? bertengkar. Anda menyebutnya tanpa argumen

Anda harus melakukan is_number? (mystring)

berkarat
sumber
Berdasarkan is_number? metode dalam pertanyaan, menggunakan is_a? tidak memberikan jawaban yang benar. Jika mystringmemang sebuah String, mystring.is_a?(Integer)akan selalu salah. Sepertinya dia menginginkan hasil sepertiis_number?("12.4") #=> true
Jakob S
Jakob S benar. mystring memang selalu berupa string, tetapi mungkin hanya terdiri dari angka. mungkin pertanyaan saya seharusnya is_numeric? agar tidak membingungkan tipe data
Jamie Buchanan
6

Tl; dr: Gunakan pendekatan regex. Ini 39x lebih cepat daripada pendekatan penyelamatan dalam jawaban yang diterima dan juga menangani kasus seperti "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

-

Jawaban yang diterima oleh @Jakob S berfungsi untuk sebagian besar, tetapi menangkap pengecualian bisa sangat lambat. Selain itu, pendekatan penyelamatan gagal pada string seperti "1.000".

Mari tentukan metode:

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

Dan sekarang beberapa kasus uji:

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

Dan sedikit kode untuk menjalankan kasus uji:

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

Berikut adalah hasil dari test case:

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

Saatnya melakukan beberapa tolok ukur kinerja:

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

Dan hasilnya:

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
sumber
Terima kasih untuk patokannya. Jawaban yang diterima memiliki keuntungan menerima masukan seperti 5.4e-29. Saya rasa regex Anda dapat disesuaikan untuk menerima itu juga.
Jodi
3
Menangani kasus seperti 1.000 sangat sulit, karena bergantung pada niat pengguna. Ada banyak sekali cara bagi manusia untuk memformat angka. Apakah 1.000 kira-kira sama dengan 1000, atau kira-kira sama dengan 1? Sebagian besar dunia mengatakan ini tentang 1, bukan cara untuk menunjukkan bilangan bulat 1000.
James Moore
4

Di rel 4, Anda perlu memasukkan require File.expand_path('../../lib', __FILE__) + '/ext/string' config / application.rb Anda

jcye
sumber
1
sebenarnya Anda tidak perlu melakukan ini, Anda cukup meletakkan string.rb di "penginisialisasi" dan berhasil!
mahatmanich
3

Jika Anda memilih untuk tidak menggunakan pengecualian sebagai bagian dari logika, Anda dapat mencoba ini:

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

Atau, jika Anda ingin itu bekerja di semua kelas objek, ganti class Stringdengan class Objectself convert to a string: !!(self.to_s =~ /^-?\d+(\.\d*)?$/)

Mark Schneider
sumber
Apa tujuan meniadakan dan melakukan nil?nol adalah benar pada ruby, jadi Anda bisa melakukannya!!(self =~ /^-?\d+(\.\d*)?$/)
Arnold Roa
Menggunakan !!pasti berhasil. Setidaknya satu panduan gaya Ruby ( github.com/bbatsov/ruby-style-guide ) menyarankan menghindari !!demi .nil?keterbacaan, tetapi saya telah melihat !!digunakan di repositori populer, dan saya pikir ini adalah cara yang bagus untuk mengonversi ke boolean. Saya sudah mengedit jawabannya.
Mark Schneider
-3

gunakan fungsi berikut:

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

begitu,

is_numeric? "1.2f" = salah

is_numeric? "1.2" = benar

is_numeric? "12f" = salah

is_numeric? "12" = benar

Rajesh Paul
sumber
Ini akan gagal jika val "0". Perhatikan juga bahwa metode .tryini bukan bagian dari pustaka inti Ruby dan hanya tersedia jika Anda menyertakan ActiveSupport.
GMA
Faktanya, itu juga gagal "12", jadi contoh keempat Anda dalam pertanyaan ini salah. "12.10"dan "12.00"gagal juga.
GMA
-5

Seberapa bodoh solusi ini?

def is_number?(i)
  begin
    i+0 == i
  rescue TypeError
    false
  end
end
donvnielsen
sumber
1
Ini kurang optimal karena menggunakan '.respond_to? (: +)' Selalu lebih baik daripada gagal dan menangkap pengecualian pada panggilan metode tertentu (: +). Ini mungkin juga gagal karena berbagai alasan jika Regex dan metode konversi tidak.
Sqeaky