How to convert human readable number to float in Ruby

During my 30 days of code challenge I’m working now on fun project which convert amount of money from user input to amount of subways, which you may build in Omsk. For example: some politician had bought the watches for 12 billion roubles. Cost of Omsk Subway project is about 24 billion roubles. So, instead of buying such expensive stuff our subject could build half of Omsk Subway.

It is to easy to restrict user input by integers only. And I decided to accept whatever user wants. For example it may be “3 billions 542 millions” or “3.42 billions”. And I need to convert this string to float, and that’s interesting task.

For this project I’m using Ruby on Rails, full repository available at github.

Failing tests for service object

It’s easy: we need to throw different strings into our object called StringDecoder.

require 'rails_helper'

RSpec.describe StringDecoder do
  let(:decoder) { StringDecoder.new }

  it 'decodes numeric string' do
    input = '1231412353'
    expect(decoder.call(input)).to equal(1231412353.0)
  end

  it 'decodes numeric string with spaces' do
    input = '1 412 935 315'
    expect(decoder.call(input)).to equal(1412935315.0)
  end

  it 'decodes numeric string with punctuation' do
    input = '1,426,233,234.43'
    expect(decoder.call(input)).to equal(1426233234.43)
  end

  it 'decodes string with миллиард миллион тысяча' do
    input = '1 billion 149 millions 51 thousand'
    expect(decoder.call(input)).to equal(1149051000.0)
  end

  it 'decodes string with punctuation and words' do
    input = '1.44 billions'
    expect(decoder.call(input)).to equal(1440000000.0)
  end
end

Writing StringDecoder service object

Next part is more interesting. We need to parse user input, remove unnecessary symbols, convert words to numbers, etc.

I thought this part will be hard, but I was wrong. When I started coding, everything I need just appeared in my head. Just read this code, it has comments (sic!).

# Convert human entered string into number
class StringDecoder
  # Hash with powers of 10 by names
  POWERS = {
    'trillion' => 10**12,
    'trillions' => 10**12,
    'billion' => 10**9,
    'billions' => 10**9,
    'million' => 10**6,
    'millions' => 10**6,
    'thousand' => 10**3,
    'thousands' => 10**3,
  }.freeze

  # Only one public method for service object: #call
  def call(input)
    # Return if input is numeric (and don't forget to strip spaces and commas)
    return input.gsub(/[\s,]/, '').to_f if float? input.gsub(/[\s,]/, '')

    result = 0

    # First line: insert spacebar if there is no one between integer and word
    # Second line: split array by space
    # Third line: convert each word to its numeric format
    # Fourth line: slice array by 2 elements
    # Fifth line: multiply each chunk and sum everything
    input.gsub(/(?<=[\d])(?=[а-яА-Я])/, ' ')
         .split(' ')
         .map { |i| POWERS[i] ? POWERS[i].to_f : i.to_f }
         .each_slice(2) do |slice|
      result += slice.inject(:*)
    end

    result
  end

  private

  # Check if given string may be converted to float
  def float?(input)
    input[/\A[\d]+[\.]?[\d]*\z/] == input
  end
end

Of course I have to do more:

  • I need to convert words like one, two, three, etc to number
  • I need to log wrong user inputs to upgrade this service object
  • I need to return something if converting failed
  • Profit

But for now it’s working.

So Long, and Thanks for all the Fish!

Dmitry Zuev

Dmitry has over two years of experience developing web applications on Ruby (mostly on Rails), and about five years of web development total. Now he is a part of development team at Rambler & Co, a Russian media company with an audience of around 40 million. Dmitry prefers back-end web development.