Version 8 of Delimiting Numbers

Updated 2005-11-30 09:54:27

WJP I find large numbers hard to read without group delimiters, but Tcl's format command doesn't provide automatic delimiter insertion the way some recent versions of C printf do. Here's a Tcl procedure that does the job. It defaults to a group size of 3 and comma as the delimiter but has optional arguments that allow other choices for locales that use other delimiters and group sizes.


See also commas added to numbers which has the following:

 proc commify { num {sep ,} {groupSize 3}} {
    while {[regsub "^(\[-+]?\\d+)(\\d{$groupSize})" $num "\\1$sep\\2" num]} {}
    return $num
 }

 # Given a number represented as a string, insert delimiters to break it up for
 # readability. Normally, the delimiter will be a comma which will be inserted every
 # three digits. However, the delimiter and groupsize are optional arguments,
 # permitting use in other locales.
 #
 # The string is assumed to consist of digits, possibly preceded by spaces,
 # and possibly containing a decimal point, i.e.: [:space:]*[:digit:]*\.[:digit:]*

 proc DelimitNumber {number {delim ","} {GroupSize 3}} {
     # First, extract right hand part of number, up to and including decimal point
     set point [string last "." $number];
     if {$point >= 0} {
         set PostDecimal [string range $number [expr $point + 1] end];
         set PostDecimalP 1;
     } else {
         set point end;
         set PostDecimal "";
         set PostDecimalP 0;
     }

     # Now extract any leading spaces.
     set ind 0;
     while {[string equal [string index $number $ind] \u0020]} {
         incr ind;
     }
     set FirstNonSpace $ind;
     set LastSpace [expr $FirstNonSpace - 1];
     set LeadingSpaces [string range $number 0 $LastSpace];

     # Now extract the non-fractional part of the number, omitting leading spaces.
     set MainNumber [string range $number $FirstNonSpace [expr $point -1]];

     # Insert commas into the non-fractional part.
     set Length [string length $MainNumber];
     set Phase  [expr $Length % $GroupSize]
     set PhaseMinusOne  [expr $Phase -1];
     set DelimitedMain "";

     #First we deal with the extra stuff.
     if {$Phase > 0} {
         append DelimitedMain [string range $MainNumber 0 $PhaseMinusOne];
     }
     set FirstInGroup $Phase;
     set LastInGroup [expr $FirstInGroup + $GroupSize -1];
     while {$LastInGroup < $Length} {
         if {$FirstInGroup > 0} {
             append DelimitedMain $delim;
         }
         append DelimitedMain [string range $MainNumber $FirstInGroup $LastInGroup];
         incr FirstInGroup $GroupSize
         incr LastInGroup  $GroupSize
     }

     # Reassemble the number.
     if {$PostDecimalP} {
         return [format "%s%s.%s" $LeadingSpaces $DelimitedMain $PostDecimal];
     } else {
         return [format "%s%s" $LeadingSpaces $DelimitedMain];
     }
 }

LES: I cannot understand the purpose of this proc or it simply doesn't work for me:

 % DelimitNumber 45869797940
 45,869,797,940

OK. But...

 % DelimitNumber 45869797940.4352543
 458,697,979,40...4352543
 % DelimitNumber 45869797940,4352543
 4,586,979,794,0,4,352,543
 % DelimitNumber 45869797940,4352543 .
 4.586.979.794.0,4.352.543
 % DelimitNumber 4586979794043525,43 "."
 4.586.979.794.043.525.,43

The way I understand the idea, this would be my proc:

 proc  DelimitNumber  { _number {_delim ","}  {_decimals 0} }          { 

         if  {[regexp {[0-9.,]+} $_number] == 0}  {error "This is not a number!"}

         set _dec {}
         set _delim2 [expr {$_delim == "," ? "." : ","}]
         regexp "(\[0-9]+)($_delim2\(\[0-9]+))?"  $_number => _int => _dec

         while          {[regexp {([0-9]+)([0-9]{3})} $_int] == 1}          {
                 regsub {([0-9]+)([0-9]{3})} $_int "\\1$_delim\\2" _int
         }

         if          {$_decimals <= 0}  {set _delim2 ""}

         return [join "$_int $_delim2 $_dec" {}]
 }

Testing:

 % DelimitNumber 45869797940.4352543 , 7
 45,869,797,940.4352543
 % DelimitNumber 45869797940,4352543 . 7
 45.869.797.940,4352543
 % DelimitNumber 4586979794043525,43 "." 2
 4.586.979.794.043.525,43

Mine seems to work better, but you have to tell it how many decimal digits the original number has (and the result is supposed to have). Improving the proc to guess that automatically must be feasible, but tricky.


WJP The problem is that I copied an earlier version that didn't quite work. Sorry. I just moved and am going back and forth between desktop and laptop and can't always keep things straight. I've edited the above to fix it. It's just a matter of adding 1 to point in computing PostDecimal and substracting 1 in computing MainNumber. I think you'll find it works correctly and doesn't any additional information.

Determining the number of decimal digits is not hard at all. The reason its hard using LES's approach is that, while regular expressions are good at many things, they are not very good when you need to count things. This is probably a case in which direct parsing is a better approach than using regular expressions.