5

I want to put in a space at every third character when formatting a number. According to this spec:

  it "should format an amount" do
    spaces_on( 1202003 ).should == "1 202 003"
  end

and I came up with this piece of code that does the job

  def spaces_on amount
    thousands = amount / 1000
    remainder = amount % 1000
    if thousands == 0
      "#{remainder}"
    else
      zero_padded_remainder = '%03.f' % remainder
      "#{spaces_on thousands} #{zero_padded_remainder}"
    end
  end

So my question is if this was the best way to do it. I suspect that there may be a regex way around it but I am not sure I will like the readability of that. (On the other hand - the %03.f magic is not very readable either....)

Phrogz
  • 296,393
  • 112
  • 651
  • 745
froderik
  • 4,642
  • 3
  • 33
  • 43

4 Answers4

5
>> def spaces_on number
>>   number.to_s.gsub(/\D/, '').reverse.gsub(/.{3}/, '\0 ').reverse
>> end
=> nil
>> spaces_on 12345678
=> "12 345 678"

Maybe there's an argument that regular expressions aren't amazingly readable, but personally I think they're simpler to understand than having to think in recursion.

Gareth
  • 133,157
  • 36
  • 148
  • 157
  • nice piece. I find that many developers don't know about regex at all which makes me avoid it - but the same is probably true about recursion as well! Point taken. – froderik Feb 06 '12 at 20:17
  • Of course the magic here isn't the regular expression - once you know the goal of the method it's easy to pick out the 'adding spaces every 3 characters' bit. The `reverse`/`reverse` is the only real mental leap. – Gareth Feb 06 '12 at 20:21
  • 4
    While this solution is correct for the given spec, it won't handle other input such as `-1000` or `12345678.0`. – Mark Thomas Feb 06 '12 at 20:32
  • @Gareth well, you may go other way and get rid of reverse / reverse but use more complex regexp... – Alexis Feb 06 '12 at 20:49
5

A small modification of the Gareth's solution (a more complex regexp, but no string reversing):

def spaces_on number
  number.to_s.gsub(/\d(?=(...)+$)/, '\0 ')
end

It works for negative numbers too (but doesn't work for floats).

Alexis
  • 4,317
  • 1
  • 25
  • 34
5

def spaces_on(number,sep=" ")
  number.to_s.tap do |s|
    :go while s.gsub!(/^([^.]*)(\d)(?=(\d{3})+)/, "\\1\\2#{sep}")
  end
end

NUMBERS = [ 1, 12, 123, 1234, 12345, 123456, 1234567,
            1.0, 1.2, 1.23, 1.234, 1.2345, 1.23456, 1.234567,
            12.3, 12.34, 12.345, 12.3456,
            123.4, 123.45, 123.456, 123.4567,
            1234.5, 1234.5, 1234.56, 1234.567,  1234.5678 ]

NUMBERS.each do |n|
  puts "%10s: %s" % [  n, spaces_on(n).inspect ]
  puts "%10s: %s" % [ -n, spaces_on(-n).inspect ]
end

Produces:

         1: "1"
        -1: "-1"
        12: "12"
       -12: "-12"
       123: "123"
      -123: "-123"
      1234: "1 234"
     -1234: "-1 234"
     12345: "12 345"
    -12345: "-12 345"
    123456: "123 456"
   -123456: "-123 456"
   1234567: "1 234 567"
  -1234567: "-1 234 567"
       1.0: "1.0"
      -1.0: "-1.0"
       1.2: "1.2"
      -1.2: "-1.2"
      1.23: "1.23"
     -1.23: "-1.23"
     1.234: "1.234"
    -1.234: "-1.234"
    1.2345: "1.2345"
   -1.2345: "-1.2345"
   1.23456: "1.23456"
  -1.23456: "-1.23456"
  1.234567: "1.234567"
 -1.234567: "-1.234567"
      12.3: "12.3"
     -12.3: "-12.3"
     12.34: "12.34"
    -12.34: "-12.34"
    12.345: "12.345"
   -12.345: "-12.345"
   12.3456: "12.3456"
  -12.3456: "-12.3456"
     123.4: "123.4"
    -123.4: "-123.4"
    123.45: "123.45"
   -123.45: "-123.45"
   123.456: "123.456"
  -123.456: "-123.456"
  123.4567: "123.4567"
 -123.4567: "-123.4567"
    1234.5: "1 234.5"
   -1234.5: "-1 234.5"
    1234.5: "1 234.5"
   -1234.5: "-1 234.5"
   1234.56: "1 234.56"
  -1234.56: "-1 234.56"
  1234.567: "1 234.567"
 -1234.567: "-1 234.567"
 1234.5678: "1 234.5678"
-1234.5678: "-1 234.5678"
Phrogz
  • 296,393
  • 112
  • 651
  • 745
  • yay - getting into deep regex space! – froderik Feb 06 '12 at 21:05
  • In the ActionView docs the thing you call `sep` is called a delimiter. separator is used for the char between the fractional and integer digits. (I don't have an opinion on what's best, but I was confused at first.) Otherwise +1 – steenslag Feb 06 '12 at 21:20
  • Oh, my try was called `to_s_with_delim`, defined on `Numeric`, that's why i noticed. `spaces_on` is not a very good method name IMHO. But still +1 :) – steenslag Feb 06 '12 at 21:24
  • @steenslag I agree on the name issue; I (and others) were coding to the specs of the OP – Phrogz Feb 06 '12 at 22:59
3

Is this Rails?

number_with_delimiter(@number, :delimiter => ' ')

This comes from ActionView::Helpers::NumberHelper.

If you want a standalone version that works with negative numbers and decimal point, use this:

def spaces_on number
  parts = number.to_s.split('.')
  parts[0].gsub!(/(\d)(?=(\d\d\d)+(?!\d))/, "\\1 ")
  parts.join(".")
end

The logic in the regex is this: Capture each digit that is followed only by groups of 3 digits (with no remaining digits), and output it with the space.

Mark Thomas
  • 37,131
  • 11
  • 74
  • 101
  • Yours fails for `123.4567`, producing `"123.4 567"`. However, +1 for mentioning the Rails version, where the source code can be viewed. (Once you throw away the i18n "fluff", the core of splitting on the `.`, using `gsub!` on the integer half and `join`ing again is sort of elegant.) – Phrogz Feb 06 '12 at 21:03
  • Good catch. I've updated mine to mimic the helper. Should work in that case now. – Mark Thomas Feb 06 '12 at 21:13