Ruby on Rails'de dizenin bir sayı olup olmadığını test edin


103

Uygulama denetleyicimde aşağıdakiler var:

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

ve denetleyicimde aşağıdaki durum:

if mystring.is_number?

end

Durum bir undefined methodhata veriyor . Sanırım is_numberyanlış yerde tanımladım ...?


4
Kod okulunun Rails for Zombies Testing dersi nedeniyle burada birçok insan olduğunu biliyorum. Sadece açıklamaya devam etmesini bekleyin. Testlerin geçmemesi gerekiyor - hata ile başarısız testi yaptırmanız sorun değil, self.is_number gibi yöntemler icat etmek için her zaman rayları yamayabilirsiniz.
boulder_ruby

Kabul edilen yanıt "1.000" gibi durumlarda başarısız olur ve normal ifade yaklaşımı kullanmaktan 39 kat daha yavaştır. Aşağıdaki cevabıma bakın.
pthamm

Yanıtlar:


186

is_number?Yöntem Oluşturun .

Yardımcı bir yöntem oluşturun:

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

Ve sonra şöyle diyelim:

my_string = '12.34'

is_number?( my_string )
# => true

StringSınıfı Genişletin .

is_number?Yardımcı işlevinize bir parametre olarak iletmek yerine doğrudan dizeyi arayabilmek istiyorsanız is_number?, Stringsınıfın bir uzantısı olarak tanımlamanız gerekir , örneğin:

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

Ve sonra onu arayabilirsin:

my_string.is_number?
# => true

2
Bu kötü bir fikir. "330.346.11" .to_f # => 330.346
epochwolf

11
to_fYukarıdakiler yoktur ve Float () bu davranışı göstermez: Float("330.346.11")artışlarArgumentError: invalid value for Float(): "330.346.11"
Jakob S

7
Bu yamayı kullanırsanız, Ruby adlandırma kurallarına uymak için onu sayısal?
Konrad Reiche

10
Orijinal soruyla pek alakalı değil, ama muhtemelen kodu ekleyeceğim lib/core_ext/string.rb.
Jakob S

1
is_number?(string)Bitin Ruby 1.9'da çalıştığını sanmıyorum . Belki bu Rails'in veya 1.8'in bir parçasıdır? String.is_a?(Numeric)İşler. Ayrıca stackoverflow.com/questions/2095493/… sayfasına bakın .
Ross Attrill

30

İşte bu sorunu çözmenin yaygın yolları için bir kıyaslama. Muhtemelen hangisini kullanmanız gerektiğine dikkat edin, beklenen yanlış vakaların oranına bağlıdır.

  1. Nispeten nadir iseler, döküm kesinlikle en hızlısıdır.
  2. Yanlış vakalar yaygınsa ve yalnızca girişleri kontrol ediyorsanız, dönüştürülmüş bir duruma karşı karşılaştırma iyi bir seçenektir.
  3. Yanlış vakalar yaygınsa ve kayan sayıları kontrol ediyorsanız, regexp muhtemelen gitmenin yoludur

Performans önemli değilse, sevdiğiniz şeyi kullanın. :-)

Tam sayı denetimi ayrıntıları:

# 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

Şamandıra kontrol detayları:

# 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'de güvenli bir normal ifade değildir, /\A\d+\Z/öyle. (örneğin, "42 \ nbir metin" döndürür true)
Timothee A

@ TimotheeA'nın yorumunu açıklığa kavuşturmak için /^\d+$/, satırlarla ilgileniyorsanız kullanmak güvenlidir, ancak bu durumda bu, bir dizenin başlangıcı ve sonuyla ilgilidir /\A\d+\Z/.
Julio

1
Yanıt veren tarafından gerçek yanıtı değiştirmek için yanıtlar düzenlenmemeli mi? Cevap veren siz değilseniz bir düzenlemede cevabı değiştirmek ... muhtemelen gizli görünüyor ve sınırların dışında olmalıdır.
jaydel

2
\ Z dizenin sonunda \ n olmasına izin verir, bu nedenle "123 \ n", tam olarak sayısal olmamasına bakılmaksızın doğrulamayı geçecektir. Ancak \ z kullanırsanız, o zaman daha doğru regexp olacaktır: / \ A \ d + \ z /
SunnyMagadan

15

Belirtilen istisnaya güvenmek en hızlı, okunabilir veya güvenilir çözüm değildir.
Aşağıdakileri yapardım:

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

1
Ancak bu yalnızca pozitif tam sayılar için işe yarar. "-1", "0.0" veya "1_000" gibi değerlerin tümü, geçerli sayısal değerler olsa bile yanlış döndürür. / ^ [- .0-9] + $ / gibi bir şeye bakıyorsunuz , ancak bu yanlışlıkla '- -' kabul ediyor .
Jakob S

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

NoMethodError: "asd" için tanımlanmamış yöntem "gerekir ': String
sergserg

Son rspec'te, bu şu olurexpect(my_string).to match(/^[0-9]+$/)
Damien MATHIEU

Seviyorum: my_string =~ /\A-?(\d+)?\.?\d+\Z/".1", "-0.1" veya "12" yapmanıza izin verir, "veya" - "veya" "yapmanıza izin vermez.
Josh

8

Ruby exception2.6.0'dan itibaren , sayısal dönüştürme yöntemlerinin isteğe bağlı bir -argument [1] vardır . Bu, kontrol akışı olarak istisnaları kullanmadan yerleşik yöntemleri kullanmamızı sağlar:

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

Bu nedenle, kendi yönteminizi tanımlamanız gerekmez, ancak örn.

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

7

ben böyle yaparım ama bence daha iyi bir yol olmalı

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

5
Yüzen gösterimi tanımaz, örneğin 1.2e + 35.
hipertracker

1
Ruby 2.4.0'da koştum object = "1.2e+35"; object.to_f.to_s == objectve işe yaradı
Giovanni Benussi

6

hayır sadece yanlış kullanıyorsun. is_number? bir argüman var. tartışmasız çağırdın

is_number? (mystring) yapıyor olmalısın


İs_number temel alınarak mı? is_a? doğru cevabı vermiyor. Eğer mystringgerçekten bir String ise mystring.is_a?(Integer), her zaman yanlış olacaktır. Görünüşe göreis_number?("12.4") #=> true
Jakob S

Jakob S doğru. mystring aslında her zaman bir dizedir, ancak yalnızca sayılardan oluşabilir. belki sorum is_numeric olmalıydı? veri türünü karıştırmamak için
Jamie Buchanan

6

Tl; dr: Bir normal ifade yaklaşımı kullanın. Kabul edilen yanıtta kurtarma yaklaşımından 39 kat daha hızlıdır ve ayrıca "1.000" gibi durumları ele alır

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 tarafından kabul edilen cevap çoğunlukla işe yarıyor, ancak istisnaları yakalamak gerçekten yavaş olabilir. Ek olarak, kurtarma yaklaşımı "1.000" gibi bir dizede başarısız olur.

Yöntemleri tanımlayalım:

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

Ve şimdi bazı test durumları:

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

Ve test senaryolarını çalıştırmak için küçük bir kod:

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

İşte test senaryolarının çıktıları:

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

Bazı performans karşılaştırmaları yapma zamanı:

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

Ve sonuçlar:

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

Kıyaslama için teşekkürler. Kabul edilen yanıt, gibi girdileri kabul etme avantajına sahiptir 5.4e-29. Sanırım normal ifadeniz bunları da kabul edecek şekilde değiştirilebilir.
Jodi

3
1.000 gibi vakalarla ilgilenmek, kullanıcının niyetine bağlı olduğu için gerçekten zordur. İnsanların sayıları biçimlendirmesinin pek çok yolu vardır. 1000, 1000'e eşit mi, yoksa yaklaşık 1'e eşit mi? Dünyanın çoğu bunun yaklaşık 1 olduğunu söylüyor, 1000 tamsayısını göstermenin bir yolu değil.
James Moore

4

Raylarda 4, require File.expand_path('../../lib', __FILE__) + '/ext/string' config / application.rb dosyanızı girmeniz gerekir.


1
aslında bunu yapmanıza gerek yok, string.rb'yi "başlatıcılara" koyabilirsiniz ve işe yarıyor!
mahatmanich

3

Mantığın bir parçası olarak istisnaları kullanmamayı tercih ederseniz, şunu deneyebilirsiniz:

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

Veya, tüm nesne sınıflarında çalışmasını istiyorsanız, bir dizeye dönüştürülen ben class Stringile değiştirin class Object: !!(self.to_s =~ /^-?\d+(\.\d*)?$/)


nil?İnkar etmenin ve sıfır yapmanın amacı yakutta gerçektir, bu yüzden sadece yapabilirsiniz!!(self =~ /^-?\d+(\.\d*)?$/)
Arnold Roa

Kullanmak !!kesinlikle işe yarar. En az bir Ruby stili kılavuz ( github.com/bbatsov/ruby-style-guide ) okunabilirlik !!lehine kaçınmayı önerdi .nil?, ancak !!popüler depolarda kullanıldığını gördüm ve bence boolean'a dönüştürmek için iyi bir yol. Cevabı düzenledim.
Mark Schneider

-3

aşağıdaki işlevi kullanın:

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

yani,

is_numeric? "1.2f" = yanlış

is_numeric? "1.2" = true

is_numeric? "12f" = yanlış

is_numeric? "12" = true


Val ise bu başarısız olacaktır "0". Ayrıca yöntemin .tryRuby çekirdek kitaplığının bir parçası olmadığını ve yalnızca ActiveSupport'u dahil ediyorsanız kullanılabileceğini unutmayın.
GMA

Aslında bu da başarısız oluyor "12", bu yüzden bu sorudaki dördüncü örneğiniz yanlış. "12.10"ve de "12.00"başarısız.
GMA

-5

Bu çözüm ne kadar aptalca?

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

1
Bu yetersizdir çünkü '.respond_to? (: +)' Kullanmak, belirli bir yöntem (: +) çağrısında başarısız olmaktan ve bir istisna yakalamadan her zaman daha iyidir. Bu, Regex ve dönüştürme yöntemlerinin olmadığı çeşitli nedenlerle de başarısız olabilir.
Sqeaky
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.