ทดสอบว่าสตริงเป็นตัวเลขใน Ruby on Rails หรือไม่


103

ฉันมีสิ่งต่อไปนี้ในตัวควบคุมแอปพลิเคชันของฉัน:

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

และเงื่อนไขต่อไปนี้ในคอนโทรลเลอร์ของฉัน:

if mystring.is_number?

end

เงื่อนไขคือการโยนundefined methodข้อผิดพลาด ฉันเดาว่าฉันกำหนดis_numberผิด ... ?


4
ฉันรู้ว่าผู้คนมากมายมาที่นี่เพราะคลาส Rails for Zombies Testing ของ codechool เพียงแค่รอให้เขาอธิบายต่อไป การทดสอบไม่ควรผ่าน --- ตกลงถ้าคุณทดสอบล้มเหลวด้วยข้อผิดพลาดคุณสามารถแก้ไขรางเพื่อคิดค้นวิธีการเช่น self.is_number ได้ตลอดเวลา?
boulder_ruby

คำตอบที่ยอมรับจะล้มเหลวในกรณีเช่น "1,000" และช้ากว่าการใช้วิธี regex 39 เท่า ดูคำตอบของฉันด้านล่าง
pthamm

คำตอบ:


187

สร้าง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

2
นี่เป็นความคิดที่ไม่ดี "330.346.11" .to_f # => 330.346
epochwolf

11
ไม่มีto_fในข้างต้นและ Float () ไม่แสดงพฤติกรรมนั้น: Float("330.346.11")ยกArgumentError: invalid value for Float(): "330.346.11"
Jakob S

7
หากคุณใช้แพตช์นั้นฉันจะเปลี่ยนชื่อเป็นตัวเลข? เพื่อให้สอดคล้องกับหลักการตั้งชื่อทับทิม (คลาสตัวเลขที่สืบทอดมาจาก Numeric คำนำหน้า is_ เป็นภาษาชวา)
Konrad Reiche

10
ไม่เกี่ยวข้องกับคำถามเดิมจริงๆ แต่ฉันอาจใส่รหัสlib/core_ext/string.rbไว้
Jakob S

1
ฉันไม่คิดว่าis_number?(string)บิตทำงาน Ruby 1.9 นั่นอาจเป็นส่วนหนึ่งของ Rails หรือ 1.8? String.is_a?(Numeric)ได้ผล ดูstackoverflow.com/questions/2095493/…ด้วย
Ross Attrill

30

นี่คือเกณฑ์มาตรฐานสำหรับวิธีทั่วไปในการแก้ไขปัญหานี้ สังเกตว่าคุณควรใช้อันไหนขึ้นอยู่กับอัตราส่วนของกรณีเท็จที่คาดไว้

  1. หากพวกเขามีการคัดเลือกนักแสดงที่ค่อนข้างผิดปกติแน่นอน
  2. หากกรณีเท็จเป็นเรื่องปกติและคุณเพิ่งตรวจสอบ ints การเปรียบเทียบกับสถานะที่แปลงแล้วเป็นตัวเลือกที่ดี
  3. หากกรณีเท็จเป็นเรื่องปกติและคุณกำลังตรวจสอบการลอยอยู่ regexp น่าจะเป็นหนทางไป

หากประสิทธิภาพไม่สำคัญให้ใช้สิ่งที่คุณชอบ :-)

รายละเอียดการตรวจสอบจำนวนเต็ม:

# 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

30
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+$/ไม่ใช่ regexp ที่ปลอดภัยใน Ruby /\A\d+\Z/คือ (เช่น "42 \ nsome text" จะกลับมาtrue)
Timothee A

ชี้แจงกรณีการแสดงความคิดเห็น @ TimotheeA มันปลอดภัยที่จะใช้/^\d+$/ถ้าจัดการกับสาย /\A\d+\Z/แต่ในกรณีนี้มันเป็นเรื่องของจุดเริ่มต้นและจุดสิ้นสุดของสตริงจึง
Julio

1
ไม่ควรแก้ไขคำตอบเพื่อเปลี่ยนคำตอบจริงโดยผู้ตอบ? การเปลี่ยนคำตอบในการแก้ไขหากคุณไม่ใช่ผู้ตอบดูเหมือนว่า ... อาจไม่เหมาะสมและควรอยู่นอกขอบเขต
jaydel

2
\ Z อนุญาตให้มี \ n ต่อท้ายสตริงดังนั้น "123 \ n" จะผ่านการตรวจสอบความถูกต้องไม่ว่าจะเป็นตัวเลขไม่ครบ แต่ถ้าคุณใช้ \ z มันจะเป็น regexp ที่ถูกต้องมากกว่า: / \ A \ d + \ z /
SunnyMagadan

15

การอาศัยข้อยกเว้นที่เพิ่มขึ้นไม่ใช่วิธีแก้ปัญหาที่เร็วที่สุดอ่านได้และเชื่อถือได้
ฉันจะทำสิ่งต่อไปนี้:

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

1
อย่างไรก็ตามสิ่งนี้ใช้ได้กับจำนวนเต็มบวกเท่านั้น ค่าเช่น '-1', '0.0' หรือ '1_000' ทั้งหมดจะแสดงผลเท็จแม้ว่าจะเป็นค่าตัวเลขที่ถูกต้องก็ตาม คุณกำลังดูบางอย่างเช่น / ^ [- .0-9] + $ / แต่ยอมรับว่า "- -" ผิดพลาด
Jakob S

13
จาก Rails 'validates_numericality_of': raw_value.to_s = ~ / \ A [+ -]? \ d + \ Z /
Morten

NoMethodError: undefined method `should 'for" asd ": String
sergserg

ใน rspec ล่าสุดจะกลายเป็นexpect(my_string).to match(/^[0-9]+$/)
Damien MATHIEU

ฉันชอบ: my_string =~ /\A-?(\d+)?\.?\d+\Z/ให้คุณทำ '.1', '-0.1' หรือ '12' แต่ไม่ใช่ '' หรือ '-' หรือ '
Josh

9

ในฐานะของทับทิม 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

7

นี่คือวิธีที่ฉันทำ แต่ฉันคิดว่ามันต้องมีวิธีที่ดีกว่านี้

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

5
ไม่รู้จักสัญกรณ์ลอยตัวเช่น 1.2e + 35
hipertracker

1
ใน Ruby 2.4.0 ฉันวิ่งobject = "1.2e+35"; object.to_f.to_s == objectและใช้งานได้
Giovanni Benussi

7

Tl; dr:ใช้วิธี regex เร็วกว่าวิธีการช่วยเหลือ 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คำตอบที่ได้รับการยอมรับมีความได้เปรียบของปัจจัยการผลิตเช่นการยอมรับ ฉันเดาว่า regex ของคุณอาจได้รับการปรับแต่งให้ยอมรับสิ่งเหล่านี้ด้วย
Jodi

3
การจัดการเคสเช่น 1,000 นั้นยากมากเนื่องจากขึ้นอยู่กับความตั้งใจของผู้ใช้ มีหลายวิธีสำหรับมนุษย์ในการจัดรูปแบบตัวเลข 1,000 เท่ากับ 1,000 หรือเท่ากับ 1? คนทั่วโลกบอกว่ามันประมาณ 1 ไม่ใช่วิธีแสดงจำนวนเต็ม 1,000
James Moore

6

ไม่คุณแค่ใช้ผิด is_number ของคุณ? มีข้อโต้แย้ง คุณเรียกมันโดยไม่มีข้อโต้แย้ง

คุณควรจะทำ is_number? (mystring)


ขึ้นอยู่กับ is_number? วิธีการในคำถามโดยใช้ is_a? ไม่ได้ให้คำตอบที่ถูกต้อง ถ้าmystringเป็น String จริงmystring.is_a?(Integer)จะเป็นเท็จเสมอ ดูเหมือนว่าเขาต้องการผลลัพธ์เช่นis_number?("12.4") #=> true
Jakob S

Jakob S ถูกต้อง mystring เป็นสตริงเสมอ แต่อาจประกอบด้วยเพียงตัวเลข บางทีคำถามของฉันน่าจะเป็น is_numeric? เพื่อไม่ให้สับสนประเภทข้อมูล
Jamie Buchanan

4

ในราง 4 คุณต้องใส่ require File.expand_path('../../lib', __FILE__) + '/ext/string' config / application.rb ของคุณ


1
ที่จริงคุณไม่จำเป็นต้องทำเช่นนี้คุณสามารถใส่ string.rb ใน "initializers" ได้เลย!
mahatmanich

3

หากคุณไม่ต้องการใช้ข้อยกเว้นเป็นส่วนหนึ่งของตรรกะคุณอาจลองสิ่งนี้:

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

หรือถ้าคุณต้องการให้มันทำงานกับคลาสอ็อบเจ็กต์ทั้งหมดให้แทนที่class Stringด้วยclass Objectการแปลงตัวเองเป็นสตริง: !!(self.to_s =~ /^-?\d+(\.\d*)?$/)


จุดประสงค์ของการลบล้างและการทำnil?ศูนย์คือสิ่งที่น่าสนใจสำหรับทับทิมดังนั้นคุณสามารถทำได้!!(self =~ /^-?\d+(\.\d*)?$/)
Arnold Roa

ใช้!!งานได้อย่างแน่นอน อย่างน้อยหนึ่งคู่มือสไตล์ทับทิม ( github.com/bbatsov/ruby-style-guide ) ปัญหาการหลีกเลี่ยง!!ในความโปรดปรานของ.nil?สำหรับการอ่าน แต่ที่ผมเคยเห็น!!มาใช้ในการเก็บยอดนิยมและผมคิดว่ามันเป็นวิธีที่ดีที่จะแปลงเป็นบูล ฉันได้แก้ไขคำตอบแล้ว
Mark Schneider

-3

ใช้ฟังก์ชันต่อไปนี้:

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"ล้มเหลวด้วย
ย่า

-5

วิธีแก้ปัญหานี้เป็นใบ้แค่ไหน?

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

1
นี่เป็นสิ่งที่เหมาะสมที่สุดเนื่องจากการใช้ ". สิ่งนี้อาจล้มเหลวด้วยเหตุผลหลายประการเช่น Regex และวิธีการแปลงไม่ทำ
Sqeaky
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.