Version 7 of Doing arithmetic on currency

Updated 2008-07-03 17:27:29 by jmn

Samy Zafrany wrote:

 >
 > Anyone has an explanation why the following happens?
 > Is it possible to rewrite the int function so it be completely robust?
 >
 >       tcl> expr int(8299.46 * 1000.0)
 >        result => 8299459

KBK The short answer is, "no." The desired result is application dependent.

The form of these numbers looks as if you may be dealing with currency. In this case, contemplate keeping only the cents (pence, centimes, pfennigs, groschen, ...) internally, and convert to floating point only for display. Remember that the floating point engine will represent all sufficiently small integers exactly!

Look at the following code for how to deal with currency properly. Remember that bankers think in integers: integer number of pennies, integer number of percentage points, etc. Scale everything to be an integer and work on it that way. Round to the nearest penny when appropriate.

US AFAIK bankers calculate with a precision of four decimal digits and round to two digits (cents) on output.

JMN 2008-07-03 despite this page having displayed the functions below for 7 years or so - this does not seem to be the 'proper' way to do things after all. The code suffers from the old octal problem.

 %dollarsToCents 10.18
 can't use invalid octal number as operand of "+"

Actually there seems to be surprisingly little Tcl-specific information relating to dealing with currency. In particular there doesn't seem to be anything finance-specific in Tcllib, and I haven't been able to find any code to do 'statistical rounding', aka 'bankers rounding' or 'round-to-even'. Is everyone just 'rolling their own' for accounting/finance stuff, or is hardly anyone ever dealing with money in Tcl?

I'm a little surprised because I happened to notice a while back that there's a even a company in my home town of Sydney that has an accounting package implemented in Tcl/Tk.

NEM It also doesn't seem to handle negative amounts either:

 % dollarsToCents -10.18
 -982

Here is a slightly better version (needs optimising):

proc dollarsToCents2 dollars {
  if {[scan $dollars %lld.%2d bucks cents] == 2} {
    set sign [expr {$bucks < 0 ? -1 : 1}]
    return [expr {$sign * (abs($bucks)*100 + $cents)}]
  } else {
    return -code error "not a valid dollar amount \"$dollars\""
  }
}

JMN It's actually faster than the other version, but even this one is not quite right:

 %dollarsToCents2 -0.42
 42


 # Take a dollar amount, expressed as nnn.nn, and convert to an
 # integer number of cents.

 proc dollarsToCents { dollars } {
     if { [regexp -expanded {
         ^                         # Match the start of the string
         ([-+]?[[:digit:]]*)       # Match the dollars, with an optional sign
         [.]                       # Match the decimal point
         ([[:digit:]][[:digit:]])  # Match the cents
         $                         # Match the end of the string
     } $dollars -> bucks pennies] } {
         return [expr { 100. * $bucks + $pennies }]
     } else {
         return -code error "Dollar amount must be a decimal number with\
                             two digits after the decimal point."
     }
 }

 # Take an integer number of cents and reformat as a dollar amount.

 proc centsToDollars { cents } {
     return [format {%d.%02d} [expr { $cents / 100 }] [expr { $cents % 100 }]]
 }

 # Convert a dollar amount to pennies.

 set amount [dollarsToCents "8299.56"]
 puts "8299.56 dollars are $amount cents"

 # Multiply a dollar amount by 1000.

 set result [expr { round( 1000.0 * $amount ) }]
 set resultString [centsToDollars $result]
 puts "One thousand times 8299.56 is $resultString"

 # Apply a percentage to a dollar amount. Round the result to the
 # nearest cent.

 set result2 [expr { round( 0.01 * $amount ) }]
 set result2String [centsToDollars $result2]
 puts "One percent of 8299.56 is $result2String"

See also A real problem.


Category Mathematics