5

I believe I have a good answer to this issue, but I wanted to make sure ruby-philes didn't have a much better way to do this.

Basically, given an input string, I would like to convert the string to an integer, where appropriate, or a float, where appropriate. Otherwise, just return the string.

I'll post my answer below, but I'd like to know if there is a better way out there.

Ex:

to_f_or_i_or_s("0523.49") #=> 523.49
to_f_or_i_or_s("0000029") #=> 29
to_f_or_i_or_s("kittens") #=> "kittens"

6 Answers 6

9

I would avoid using regex whenever possible in Ruby. It's notoriously slow.

def to_f_or_i_or_s(v)
  ((float = Float(v)) && (float % 1.0 == 0) ? float.to_i : float) rescue v
end

# Proof of Ruby's slow regex
def regex_float_detection(input)
  input.match('\.')
end

def math_float_detection(input)
  input % 1.0 == 0
end

n = 100_000
Benchmark.bm(30) do |x|
  x.report("Regex") { n.times { regex_float_detection("1.1") } }
  x.report("Math") { n.times { math_float_detection(1.1) } }
end

#                                     user     system      total        real
# Regex                           0.180000   0.000000   0.180000 (  0.181268)
# Math                            0.050000   0.000000   0.050000 (  0.048692)

A more comprehensive benchmark:

def wattsinabox(input)
  input.match('\.').nil? ? Integer(input) : Float(input) rescue input.to_s
end

def jaredonline(input)
  ((float = Float(input)) && (float % 1.0 == 0) ? float.to_i : float) rescue input
end

def muistooshort(input)
  case(input)
  when /\A\s*[+-]?\d+\.\d+\z/
      input.to_f
  when /\A\s*[+-]?\d+(\.\d+)?[eE]\d+\z/
      input.to_f
  when /\A\s*[+-]?\d+\z/ 
      input.to_i     
  else  
      input
  end
end

n = 1_000_000
Benchmark.bm(30) do |x|
  x.report("wattsinabox") { n.times { wattsinabox("1.1") } }
  x.report("jaredonline") { n.times { jaredonline("1.1") } }
  x.report("muistooshort") { n.times { muistooshort("1.1") } }
end

#                                     user     system      total        real
# wattsinabox                     3.600000   0.020000   3.620000 (  3.647055)
# jaredonline                     1.400000   0.000000   1.400000 (  1.413660)
# muistooshort                    2.790000   0.010000   2.800000 (  2.803939)
Sign up to request clarification or add additional context in comments.

Comments

5
def to_f_or_i_or_s(v)
    v.match('\.').nil? ? Integer(v) : Float(v) rescue v.to_s
end

Comments

2

A pile of regexes might be a good idea if you want to handle numbers in scientific notation (which String#to_f does):

def to_f_or_i_or_s(v)
    case(v)
    when /\A\s*[+-]?\d+\.\d+\z/
        v.to_f
    when /\A\s*[+-]?\d+(\.\d+)?[eE]\d+\z/
        v.to_f
    when /\A\s*[+-]?\d+\z/ 
        v.to_i     
    else  
        v
    end
end

You could mash both to_f cases into one regex if you wanted.

This will, of course, fail when fed '3,14159' in a locale that uses a comma as a decimal separator.

3 Comments

In my function, that would actually get returned as a string, since the float conversion would fail and throw an exception and then the rescue would run to_s and return.
@WattsInABox: Right you are (shows you how much I use Float). But you still have scientific notation to worry about.
This is tru, @muistooshort. Thanks!
2

Depends on security requirements.

def to_f_or_i_or_s s
    eval(s) rescue s
end

Comments

0

I used this method

  def to_f_or_i_or_s(value)
    return value if value[/[a-zA-Z]/]

    i = value.to_i
    f = value.to_f

    i == f ? i : f
  end

Comments

0

CSV has converters which do this.

require "csv"
strings = ["0523.49", "29","kittens"]
strings.each{|s|p s.parse_csv(converters: :numeric).first}

#523.49
#29
#"kittens"

However for some reason it converts "00029" to a float.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.