Проверить, является ли строка числом в 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
Принятый ответ не работает в таких случаях, как «1000», и он в 39 раз медленнее, чем при использовании подхода с регулярным выражением. Смотрите мой ответ ниже.
pthamm

Ответы:

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")raisesArgumentError: invalid value for Float(): "330.346.11"
Jakob S
7
Если вы используете этот патч, я бы переименовал его в numeric?, Чтобы соответствовать соглашениям об именах рубинов (числовые классы наследуются от Numeric, префиксы is_ являются javaish).
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/… .
Росс Аттрилл
30

Вот эталон распространенных способов решения этой проблемы. Обратите внимание, какой из них вам следует использовать, вероятно, зависит от ожидаемого количества ложных случаев.

  1. Если они относительно необычны, кастинг определенно самый быстрый.
  2. Если ложные случаи распространены, и вы просто проверяете целые числа, сравнение с преобразованным состоянием - хороший вариант.
  3. Если ложные случаи встречаются часто, и вы проверяете числа с плавающей запятой, вероятно, вам подойдет регулярное выражение.

Если производительность не имеет значения, используйте то, что вам нравится. :-)

Детали проверки целых чисел:

# 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 \ nsome text" вернется true)
Timothee A
Чтобы уточнить комментарий @ TimotheeA, его можно безопасно использовать /^\d+$/при работе со строками, но в этом случае речь идет о начале и конце строки, таким образом /\A\d+\Z/.
Хулио
1
Разве ответы не следует редактировать, чтобы изменить фактический ответ респондента? изменение ответа при редактировании, если вы не отвечающий, кажется ... возможно закулисным и должно быть вне поля зрения.
jaydel
2
\ Z позволяет иметь \ n в конце строки, поэтому "123 \ n" пройдет проверку, даже если оно не является полностью числовым. Но если использовать \ z, то более правильным будет регулярное выражение: / \ A \ d + \ z /
SunnyMagadan
15

Полагаться на возникшее исключение - не самое быстрое, удобочитаемое и надежное решение.
Я бы сделал следующее:

my_string.should =~ /^[0-9]+$/
Дэмьен МАТЬЕ
источник
1
Однако это работает только для положительных целых чисел. Такие значения, как «-1», «0,0» или «1_000», возвращают false, даже если они являются допустимыми числовыми значениями. Вы смотрите на что-то вроде / ^ [- .0-9] + $ /, но это ошибочно принимает '- -'.
Якоб С.
13
Из Rails 'validates_numericality_of': raw_value.to_s = ~ / \ A [+ -]? \ D + \ Z /
Мортен
NoMethodError: неопределенный метод `should 'для 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? есть аргумент. вы назвали это без аргументов

вы должны делать is_number? (mystring)

ржавый
источник
На основе is_number? метод в вопросе, используя is_a? не дает правильный ответ. Если mystringдействительно String, mystring.is_a?(Integer)всегда будет false. Похоже, он хочет результата вродеis_number?("12.4") #=> true
Якоб С.
Якоб С. прав. mystring действительно всегда является строкой, но может состоять только из чисел. возможно, мой вопрос должен был быть is_numeric? чтобы не путать тип данных
Джейми Бьюкенен
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, действительно сложно, поскольку это зависит от намерения пользователя. У людей есть много способов форматировать числа. 1000 примерно равно 1000 или примерно равно 1? Большая часть мира говорит, что это примерно 1, а не способ показать целое число 1000.
Джеймс Мур,
4

В rails 4 вам нужно поместить require File.expand_path('../../lib', __FILE__) + '/ext/string' в свой config / application.rb

jcye
источник
1
на самом деле вам не нужно этого делать, вы можете просто поместить string.rb в "инициализаторы", и это сработает!
mahatmanich
3

Если вы предпочитаете не использовать исключения как часть логики, вы можете попробовать следующее:

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

Или, если вы хотите , чтобы работать по всем классам объектов, заменить class Stringс class Objectна новообращенной себя в строку: !!(self.to_s =~ /^-?\d+(\.\d*)?$/)

Марк Шнайдер
источник
Какова цель отрицания и nil?обнуления, это правда на Ruby, так что вы можете сделать просто!!(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
Donvnielsen
источник
1
Это неоптимально, потому что использование '.respond_to? (: +)' Всегда лучше, чем сбой и перехват исключения при вызове определенного метода (: +). Это также может не работать по разным причинам, если не использовать Regex и методы преобразования.
Sqeaky