Перевірте, чи рядок є числом у 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
Я знаю, що тут багато людей із-за навчальних курсів «Рейки для тестування зомбі». Просто зачекайте, поки він продовжує пояснювати. Тести не повинні проходити --- все гаразд, щоб ви не помилилися з тестом, ви завжди можете виправити рейки, щоб винайти такі методи, як self.is_number?
boulder_ruby

Прийнята відповідь не відповідає у випадках, таких як "1000", і на 39 разів повільніше, ніж використання підходу регулярного вираження. Дивіться мою відповідь нижче.
птамм

Відповіді:


186

Створити 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
Якщо ви використовуєте цей патч, я перейменував би його на числовий ?, щоб залишатися у відповідність із умовами іменування рубіном (числові класи успадковуються з числа, префікси is_ є явними).
Конрад Рейче

10
Не дуже стосується початкового питання, але я, мабуть, поставив код lib/core_ext/string.rb.
Якоб S

1
Я не думаю, що is_number?(string)біт працює Ruby 1.9. Можливо, це частина Rails або 1.8? String.is_a?(Numeric)працює. Дивіться також stackoverflow.com/questions/2095493/… .
Росс Attrill

30

Ось орієнтир для поширених способів вирішення цієї проблеми. Зауважте, який із них ви повинні використовувати, ймовірно, залежить від співвідношення очікуваних помилкових випадків.

  1. Якщо вони відносно рідкісні, кастинг, безумовно, найшвидший.
  2. Якщо помилкові випадки поширені, і ви просто перевіряєте на ints, порівняння проти перетвореного стану є хорошим варіантом.
  3. Якщо помилкові випадки поширені, і ви перевіряєте floats, ймовірно, дорога перейти до регулярного вибору

Якщо продуктивність не має значення, використовуйте те, що вам подобається. :-)

Деталі перевірки цілого числа:

# 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

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, /\A\d+\Z/є. (наприклад, "42 \ nокремий текст" повернеться true)
Timothee A

Щоб уточнити коментар @ TimotheeA, це безпечно використовувати, /^\d+$/якщо мати справу з рядками, але в цьому випадку це стосується початку і кінця рядка /\A\d+\Z/.
Хуліо

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", але не "" або "-" або "."
Джош

8

За станом на Ruby 2.6.0, чисельні методи відтворення мають необов'язковий exceptionаргумент [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і це спрацювало
Джованні Бенуссі

6

ні, ви просто неправильно використовуєте ваш номер_кілька? має аргумент. ви називали це без аргументу

вам слід робити is_number? (mystring)


На основі is_number? метод у запитанні, використовуючи is_a? не дає правильної відповіді. Якщо mystringце дійсно Рядок, mystring.is_a?(Integer)завжди буде помилковим. Схоже, він хоче такого результату, якis_number?("12.4") #=> true
Jakob S

Якоб S правильний. mystring насправді завжди є рядком, але може містити просто числа. можливо, моє запитання повинно було бути числовим? щоб не переплутати тип даних
Джеймі Бюкенан

6

Tl; dr: Використовуйте підхід з регулярними виразками. Це в 39 разів швидше, ніж рятувальний підхід у прийнятій відповіді, а також обробляє випадки типу "1000"

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 здебільшого працює, але вилов винятків може бути дуже повільним. Крім того, рятувальний підхід провалюється на рядку типу "1000".

Давайте визначимо методи:

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. Я думаю, ваш регекс може бути налаштований, щоб прийняти їх також.
Джоді

3
Поводження з випадками, як 1000, справді важке, оскільки це залежить від наміру користувача. Існує багато, багато способів форматування чисел. 1.000 приблизно дорівнює 1000, або приблизно дорівнює 1? Більшість країн світу говорить, що це приблизно 1, а не спосіб показати ціле число 1000.
Джеймс Мур,

4

У рейки 4 потрібно поставити require File.expand_path('../../lib', __FILE__) + '/ext/string' свою config / application.rb


1
насправді цього не потрібно робити, ви можете просто поставити string.rb в "ініціалізатори", і це працює!
махатманіч

3

Якщо ви не хочете використовувати винятки як частину логіки, ви можете спробувати це:

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

Або, якщо ви хочете , щоб працювати по всіх класах об'єктів, замінити class Stringз class Objectна новонаверненої себе в рядок: !!(self.to_s =~ /^-?\d+(\.\d*)?$/)


nil?Яка мета заперечувати та робити нульове значення - рубін, тож ви можете зробити просто!!(self =~ /^-?\d+(\.\d*)?$/)
Арнольд Роа,

Використання, !!безумовно, працює. Принаймні один стиль керівництва Ruby ( github.com/bbatsov/ruby-style-guide ) запропонував уникати !!на користь .nil?для зручності читання, але я бачив !!використовується в популярних репозиторіях, і я думаю , що це прекрасний спосіб перетворити в булево. Я відредагував відповідь.
Марк Шнайдер

-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.
GMA

Насправді це також не вдається "12", тому ваш четвертий приклад у цьому питанні - неправильний. "12.10"і "12.00"невдачі теж.
GMA

-5

Наскільки тупо це рішення?

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

1
Це недооптимально, тому що використання ".respo_to? (: +)" Завжди краще, ніж невдало та вилучення виключення для конкретного виклику методу (: +). Це може також не вдатися з різних причин, якщо методи Regex та конверсії не мають.
Sqeaky
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.