Lighthouse has a new layout. Prefer the old one? Return to the old layout, and switch back any time from the link at the top of each page.

This project is archived and is in readonly mode.

[PATCH] Improvements in number_helper.rb

#4239

Hi all,

I've made several small refactorings and added some new functionality to the existing number helpers (especially number_to_human_size and number_with_precision). And added a new one, number_to_human. All is well tested and documented. I've put a considerable amount of thought in every change, I think it is "ready to commit", but please review and suggest possible improvements.

Rationale

What I needed to do was print large numbers in a rounded and readable way for human reading (instead of printing 10,232,423,423, printing "10.2 billion"). I thought Rails' NumberHelper would rescue me, but then I saw #number_to_human_size would only print file size. I though of throwing in a quick helper, if Rails would only help me round numbers to a certain amount of significant digits with number_with_precision, but then I was upset to discover what rails called precision (rounding the number of fractional digits) was far from precise/correct, according to the standard math definition of the name precision (the number of significant digits in a number - see what wikipedia says ).

So I reached the conclusion Rails could do better, and decided to add this functionality.

Improvements

First I changed number_with_precision by adding the :significant option, that if set to true will make number_with_precision round to the number with significant digits:

    number_with_precision(3.1415, :precision => 2)                            # => "3.14"
    number_with_precision(3.1415, :precision => 2, :significant => true)      # => "3.2"
    number_with_precision(1232.2342, :precision => 2)                         # => "1232.23"
    number_with_precision(1232, :precision => 2, :significant => true)        #  => "1200"
    number_with_precision(12, :precision => 4)                                # => "12.0000"
    number_with_precision(12, :precision => 4, :significant => true)          # => "12.00"

Then I noticed number_to_human_size stripped unsignificant zeros after the decimal period by default, and thought this ought to be in number_with_precision, and not there. So I refactored.

    number_with_precision(12, :precision => 4, :strip_unsignificant_zeros => true)                          # => "12"
    number_with_precision(12, :precision => 4, :significant => true, :strip_unsignificant_zeros => true)    # => "12"

Then I decided number_to_human_size should use :strip_unsignificant_zeros => true by default. It makes no sense to use precision => 1 and display "1.3 KB" and "657.6 Kb" (2 digits versus 4 digits). It would be more reasonable (and less overwhelming to the end user) to display "1.3 KB" and "650 KB" (using :precision => 2, :strip_unsignificant_zeros => true). We are humans and our brains can't process too many digits (we always round). I know this can lead to some controversy, since it changes previous behavior. But come on, Rails is opinionated, its view code (won't break anything) and it is the right thing to do. In the paranoid scenario, we could add something to initializers/new_rails_defaults.rb, but I think it would be overkill (in this case).

    number_to_human_size(kilobytes(543.43), :precision => 2)   # => "540 KB"
    number_to_human_size(kilobytes(543.34), :precision => 2, :strip_unsignificant_zeros => false)   # => "543.34 KB"

Besides that, I refactored the other number helpers to benefit from those changes (eg.: number_with_percentage can also have :significant => true), while keeping their defaults. I also made some improvements to the code (it's more clean and rubyish now), and fixed some minor issues with helpers not actually returning the untouched first parameter if it was not a valid number.

number_to_human

I decided to call it number_to_human since it is more general than number_to_human_size, yet similar. It prints stuff like "1.2 million" and "3.4 trillion" by default, but it can be easily tweaked to print anything that uses a decimal scale, like 12 centimeters, 34 meters, 76 kilometers (No, it won't convert ounces to pounds.).

By default I set in actionview/locale/en.yml a list of the decimal quantifiers:

human:
  format:
    delimiter: ""
    precision: 3
    significant: true
  # Used in number_to_human_size()
  storage_units:
    format: "%n %u"
    units:
      byte:
        one:   "Byte"
        other: "Bytes"
      kb: "KB"
      mb: "MB"
      gb: "GB"
      tb: "TB"
  # Used in number_to_human() 
  decimal_units:
    format: "%n %u"
    units:
      unit: ""     
      thousand: thousand
      million: million
      billion: billion
      trillion: trillion
      quadrillion: quadrillion

So it will do by default:

  number_to_human(123456789)  # => 120 million
  number_to_human(123456789012, :precision => 4)  # => 123.4 billion

But this can be easily customized by the user:

  number_to_human(2780, :units => {:hundred => "hundred"})  # =>  28 hundred

And he can also define it in his locale:

distance:
  centi:
    one: "centimeter"
    other: "centimeters" 
  unit:
    one: "meter"
    other: "meters"
  thousand:
    one: "kilometer"
    other: "kilometers"
  billion: "gazilion-distance"

And then do:

  number_to_human(2780, :units => :distance)  # =>  2.8 kilometers
  number_to_human(523, :units => :distance)  # =>  520 meters
  number_to_human(0.238, :units => :distance)  # =>  24 centimeters
  number_to_human(0.238, :units => :distance)  # =>  24 centimeters

The possible unit quantifiers that can be used is defined in the DECIMAL_UNITS constant hash, and is currently set to:

{0 => :unit, 1 => :ten, 2 => :hundred, 3 => :thousand, 6 => :million, 9 => :billion, 12 => :trillion, 15 => :quadrillion, -1 => :deci, -2 => :centi, -3 => :mili, -6 => :micro, -9 => :nano, -12 => :pico}.freeze

Adding support for a new one (eg.: femto or quintillion) as simple as adding a new key => value pair to this hash with the proper exponent.

Patch will follow (I need the ticket number before I make the commit)

Reported by Bernardo Padua · March 19th, 2010 @ 09:40 PM

State: resolved
Milestone: 3.0.2
Assigned to: José Valim José Valim
Importance: Low

Activity

  1. Bernardo Padua
  2. Bernardo Padua
    Bernardo Padua
    • Assigned user set to José Valim

    BTW, somebody please close #1340 Rounding problem in number_with_precision as it looks like it was already fixed.

    March 19th, 2010 @ 10:11 PM

  3. José Valim
    José Valim
    • Assigned user cleared.
    • Milestone cleared.

    March 19th, 2010 @ 10:37 PM

  4. Jeremy Kemper
    Jeremy Kemper
    • State changed from new to open
    • Assigned user set to José Valim

    Great patch, Bernardo!

    March 19th, 2010 @ 10:44 PM

  5. José Valim
    José Valim
    • State changed from open to new

    March 19th, 2010 @ 10:47 PM

  6. Bernardo Padua
    Bernardo Padua

    Thanks for the nice feedback, José and Jeremy. Following some suggestions by José (exchanged in portuguese, I won't bother printing them here... :)), I refactored the patch (and also found/fixed some minor issues), here is the 2nd take. In the process I rewrote the number_helper_i18n_test.rb, since it used mocking in a very brittle way and broke completely with the slightest refactoring.

    Something I forgot to write in the ticket (but it is in the docs), is that you can also supply your custom formatting for number_to_human:

      number_to_human(2000, :format => "Y%n%u", :units => {:thousand => "k"})  # =>  "Y2k"
    

    Any questions/requests let me know.

    March 20th, 2010 @ 08:21 PM

  7. Bernardo Padua
    Bernardo Padua

    I just discovered there was a line/hack in number_to_currency which wasn't doing anything useful (its probably from when it didn't rely on number_with_precision), so I yanked it:

      separator = '' if precision == 0
    

    Now number_to_currency looks much better. Hope its not too late.

    March 20th, 2010 @ 08:44 PM

  8. Bernardo Padua
    Bernardo Padua

    Ops, me one more time!

    I just figured out rails 3 will have this html_safe thing (I wasn't aware, since I am still working with 2.3), and most of the outputs from NumberHelper were not html_safe (only number_to_currency was, for some reason).

    I am trying to fix that, hold until I send the updated patch.

    March 20th, 2010 @ 09:36 PM

  9. Bernardo Padua
    Bernardo Padua

    Hi,

    I finally added support for html_safe throughout NumberHelper methods. The updated patch follows (there are two commits, one for the previous improvements, other for this).

    We have to make sure the number (which may also be a string in all helpers) does not have any unsafe stuff, so it was not just a matter of adding .html_safe to everything that is returned. Most number helpers return the number param unchanged if they are not a valid number (sometimes prepended/appended by something, like number + "%" in number_to_percentage), so this is where the danger lies. To accommodate that I had to be a little bit more strict when parsing the numbers in some helpers (which were not doing any parsing) and had to add a way for the helpers to throw exceptions so they could be "bubbled up" to helpers that in turn relied on other helpers (eg.: number_to_currency relies on number_with_precision).

    Of course it is tested. I also structured the tests a little bit more to make a few of those things more clear (methods returning number param unchanged, returning nil when number is nil, etc).

    Waiting for your reviews now.

    March 22nd, 2010 @ 07:46 PM

  10. Repository
    Repository
    • State changed from new to resolved

    March 22nd, 2010 @ 08:42 PM

  11. Jeremy Kemper
    Jeremy Kemper
    • Milestone set to 3.0.2
    • Importance changed from to Low

    October 15th, 2010 @ 11:01 PM