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)

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

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

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

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

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,

  # 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(:*)



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

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!