Formatting Number Strings in Ruby

Data comes in all shapes and sizes, and often when sourcing data from API’s, like the famous Tom Hanks adage, “You never know what you’re gonna get”. Well, that’s not always true, and part of our role as software engineers is to anticipate the way data will reach our code, and design our code to manage that data in a useful way.

Here, I’ll go through some of the common solutions to format number strings to correctly represent large numbers, money and dates using Ruby.

Large Numbers:

Photo by Nick Hillier on Unsplash

One common issue when loading a value from an API is receiving a large number, and counting through the digits to know how big of a value you are dealing with. For the user, a number of items: “2499738630” is much less meaningful than “2,499,738,630”, however, you would likely store that value in a database without the separating commas, and you would likely receive a value unformatted like this from an API.

When tackling a problem, I always start out with the question “What patterns do I see between the prompt and the solution, and what tools do I have to implement that pattern?” I typically talk through the solution in plain english, and then later apply that “algorithmic” thinking to coding out a solution.

I receive “2499738630" as a value from and API, and I’d like to display it with comma separators in my application: “2,499,738,630”

So, we need to take a number string with a length of more that 3 digits, count from the end of the string and insert a comma after every three digits. Simple enough. First we’ll convert whatever we receive into a string (in case we want to use this function on an integer), select each character in that string, and create an array of numbers. Now we can reverse that array, so we can work from the 1’s place, to the 10’s, etc:

2499738630.to_s.chars.to_a.reverse 
=> ["0", "3", "6", "8", "3", "7", "9", "9", "4", "2"]

Great, now we want to somehow group these elements into groups of 3:

2499738630.to_s.chars.to_a.reverse.each_slice(3).map(&:join)
=> ["036", "837", "994", "2"]
  • note: .map(&:join) is shorthand for .map{|x| x.join}

Now that we know the mechanics to use, let’s build out our function. We can take those groups of 3 digits, join them with the separating commas, and reverse the order back to our original:

number = 2499738630def format_number(number)
num_groups = number.to_s.chars.to_a.reverse.each_slice(3)
num_groups.map(&:join).join(',').reverse
end
format_number(number)
=> "2,499,738,630"

And there we have our return value! With that solved, let’s try to think of data we might receive that would break this code. What if we pass in a long number with a decimal?

number = "2499738630.2332"
format_number(number)
=> "249,973,863,0.2,332"

Oops! That’s not going to work for us! It just considered the decimal point a character in the string. Let’s account for this in our code. We need to first split up the whole numbers from the decimals, apply our comma separating code to the whole numbers side, and rejoin it with the decimals side, untouched.

number_one = '2499738630'
number_two = '2499738630.2332'
def format_number(number)
whole, decimal = number.to_s.split(".")
num_groups = whole.chars.to_a.reverse.each_slice(3)
whole_with_commas = num_groups.map(&:join).join(',').reverse
[whole_with_commas, decimal].compact.join(".")
end
format_number(number_one)
=> "2,499,738,630"
format_number(number_two)
=> "2,499,738,630.2332"

Now we have a flexible function that can handle formatting integers and floats correctly.

Alternatively, we could achieve the same goal using Regular Expressions:

def format_number(number)
whole, decimal = number.to_s.split('.')
if whole.to_i < -999 || whole.to_i > 999
whole.reverse!.gsub!(/(\d{3})(?=\d)/, '\\1,').reverse!
end
[whole, decimal].compact.join('.')
end

Here, the reverse! and gsub! functions will destructively alter the whole numbers inside the function. Though, in this case, the .gsub! function returns nil if whole is less than 3 characters, so we can add an if statement to account for whole numbers from -999 and 999. However, this function is effective in formatting our large numbers into more readable strings.

Money:

Photo by Macau Photo Agency on Unsplash

If you’re building an application that handles currency, you are probably best off exploring a Ruby gem called “Money”, located at this github page:

This gem provides the tools to create money objects, which include the name, symbol, and all the formatting rules used in a particular currency.

In the case of receiving an unformatted string from an API, along with a currency code (‘usd’, ‘cad’, ‘jpy’), the ‘Monetize’ gem is very helpful for parsing:

In this example, we receive a hash where the keys are currency codes and the values are the amounts:

currency_hash = {
"usd"=>"10947.34",
"jpy"=>"1123591.19",
"huf"=>"3195944.54"
}

If you were receiving the value of a stock for example, there is a good chance an API would provide a hash like this, including the price in several national currencies. We’d like to view these amounts formatted correctly with their correct symbols and comma separators, and remove any monetary units that are too small to be significant. ‘Money’ provides the tools to accomplish this:

require 'money'
require 'monetize'
currency_hash.each do |key, value|
currency = key.upcase
amount = value
puts Monetize.parse("#{currency} #{amount}").format
end
#output#
$10,947.34
¥1,123,591
3 195 945 Ft
=> {"usd"=>"10947.34", "jpy"=>"1123591.19", "huf"=>"3195944.54"}

This test function shows thatMonetize.parse() will take in a currency in uppercase and an unformatted number (as one string), and apply the formatting associated with that currency. It will even round off the smallest unit: 3195944.54 in HUF got rounded up to 3 195 945 Ft.

We can use this syntax to write a simple formatter function for parsing our money values:

def format_currency(currency = 'USD', amount)  
Monetize.parse("#{currency.upcase} #{amount}").format
end
format_currency(13784)
=> "$13,784.00"
format_currency("JPY", 27382)
=> "¥27,382"

Lovely! Now we can rest assured that our currency displays will always look nice and correct.

As we can see below, we are creating a #<Money> object using our data which can be formatted with the .format function:

Monetize.parse("1000 USD")
=> #<Money fractional:100000 currency:USD>
Monetize.parse("1000 USD").format
=> "$1,000.00"

If we set that object to a variable, we can dig in to the object and see what’s under the hood:

money = Monetize.parse("1000 USD")
=> #<Money fractional:100000 currency:USD>
money.currency
=> #<Money::Currency
id: usd,
priority: 1,
symbol_first: true,
thousands_separator: ,,
html_entity: $,
decimal_mark: .,
name: United States Dollar,
symbol: $,
subunit_to_unit: 100,
exponent: 2,
iso_code: USD,
iso_numeric: 840,
subunit: Cent,
smallest_denomination: 1>

Using the built-in currency functions, we can even set our own currency parameters for our money objects to use, allowing the formatting to be flexible. More information on these attributes can be found in the gem docs.

If needed, this gem can also handle currency exchanges:

Money.add_rate("USD", "CAD", 1.24515)
Money.add_rate("CAD", "USD", 0.803115)

Money.us_dollar(100).exchange_to("CAD")
=> Money.new(124, "CAD")
Money.ca_dollar(100).exchange_to("USD")
=> Money.new(80, "USD")
  • Note: Currently, to avoid deprecation in upcoming releases, you will receive a warning in your code if you don’t explicitly set the following parameters in your environment:
Money.rounding_mode = BigDecimal::ROUND_HALF_EVEN
Money.locale_backend = :currency
Money.default_currency = "USD"

Dates:

Photo by Estée Janssens on Unsplash

When receiving date/time information, we can once again call upon a useful gem to help us out:

require 'date'

“Date” allows us to parse date information, make calculations on those data points, and display that data in our application. The gem allows us to parse incoming values and create #<Date> objects, which hold information we need regarding that timestamp:

Date.new(2001,2,3)
=> #<Date: 2001-02-03 ((2451944j,0s,0n),+0s,2299161j)>
Date.parse("2021-01-06T04:30:29.969Z")
=> #<Date: 2021-01-06 ((2459221j,0s,0n),+0s,2299161j)>

More information can be found in the gem docs on all the different ways these date objects can be created, but for our formatting purposes, we have a few useful methods at our disposal:

The simplest solution is to use puts:

date = Date.parse("2021-01-06T04:30:29.969Z")
=> #<Date: 2021-01-06 ((2459221j,0s,0n),+0s,2299161j)>
puts date#output#
2021-01-06
=> nil

We can also separate out the individual elements of the date:

date.year
=> 2021
date.month
=> 1
date.day
=> 6

However, the most useful formatting method for Date objects is .strftime()

As shown below, this method allows us to create a custom output using the data stored in a #<Date> object:

date.strftime("Today is %B %e, %Y")
=> "Today is January 6, 2021"

Extended usage notes on .strftime() can be found in the additional resources.

  • Note: .strftime can also be used on #<DateTime> objects, which are also very useful when referencing time information in your program.

For more information on DateTime and Time, check out this helpful article:

Full Stack Web Developer and Software Engineer. React/Redux, Ruby on Rails, JavaScript.