Currency types - a small include (updated)

Share your advanced PureBasic knowledge/code with the community.
srod
PureBasic Expert
PureBasic Expert
Posts: 10589
Joined: Wed Oct 29, 2003 4:35 pm
Location: Beyond the pale...

Currency types - a small include (updated)

Post by srod »

Bug fix - 7th May 2008.
ValC() didn't check that the given value was a valid number! Doh! :oops:

Updated code is below.


=======================================

Update - 25th April 2008.
Have altered the (Windows only) FormatCurrency() function to allow the user to over-ride a couple of the user's default locale currency format settings. You can now specify the number of digits to be displayed after the decimal point, so, for example, you can opt to display all 4 fractional digits etc. Also, you can opt to remove the currency symbol from the formatted string; £, $ etc.

Updated code is below.


=======================================

Hi,

in need of a simple mechanism for using 'currency' type variables I quickly hacked up the following little library. All the existing solutions I found in these forums didn't quite give me what I needed.

The advantage of having a currency type is that there is no reliance whatsoever on floating point arithmetic and so there will be no rounding errors. This is essential when dealing with financial calculations.

My implementation simply utilises 64-bit signed integers (quads) in which the least significant 4 digits represent the fractional part. (I chose 4 digits because this should cover just about every kind of currency).

The point is that all calculations using the currency types are exact up to an accuracy of 4 decimal places, i.e. there will be no rounding errors.

List of functions :
  • ValC(value$) - Converts a string representation of a currency value to a true currency type.
  • StrC(cur.currency) - Converts a true currency type to a string.
  • MultC(a.currency, b.currency) - Multiplys two currency values and returns the result in currency form.
  • DivC(a.currency, b.currency) - Divides two currency values and returns the result in currency form (it doesn't really make sense to do this!)
  • FormatCurrency(cur.currency) - WINDOWS ONLY! Returns a string in which the given currency value has been formatted depending on the computer's locale etc.
There is a discussion within the source code comments which discusses all the arithmetic operations possible with currency types. For example, you can simply add 2 currency variables using the usual + operator; no special function is required etc!

Code: Select all

;/////////////////////////////////////////////////////////////////////////////////
;***Currency***
;
;©nxSoftWare 2008.
;=================
;   Stephen Rodriguez (srod)
;   Created with Purebasic 4.2 for Windows.
;
;   Platforms:  ALL.
;               Only the FormatCurrency() function is Windows specific, but is only included
;               in the compilation under Windows platforms.
;
;   Fully Unicode compliant and threadsafe.
;/////////////////////////////////////////////////////////////////////////////////

;/////////////////////////////////////////////////////////////////////////////////
;-NOTES.
;
; i)    A currency type variable is a signed quad type in which the least significant 4 digits
;       represent the fractional part of the value.
;       Basic arithmetic operations are thus not subject to rounding.
;
; ii)   All operations on currency values (including conversion to/from strings) operate
;       on all 4 decimal digits. That is, no account is taken of the currency format specified
;       in the underlying locale etc. The (Windows only) function FormatCurrency() can be used
;       for this purpose.
;
; iii)  The following operations can be performed on currency values directly, with the result
;       being another valid currency value :
;         -addition of two currency values (use the + operator as usual)
;         -subtraction of two currency values (use the - operator as usual)
;         -multiplication/division of a currency value by an INTEGER (use the usual * and / operators)
;
; iv)   Multiplying two currency values requires the use of the MultC() function, which
;       returns another currency value.
;
; v)    Division of two currency values  - there are 2 cases to consider.
;       --------------------------------
;         a)  Use of the normal / operator upon two currency values will return an INTEGER quotient.
;             E.g. £6.50 / £2 will return the integer value 3.
;         b)  The function DivC() will return a currency value. Such a division is actually
;             quite meaningless, but is included for completeness!
;             E.g. DivC(6.50, 2) will return the currency value 3.25.
;/////////////////////////////////////////////////////////////////////////////////


;/////////////////////////////////////////////////////////////////////////////////
;-MACROS.
  ;Set up a currency type.
    Macro currency
      q
    EndMacro
;/////////////////////////////////////////////////////////////////////////////////


EnableExplicit

;/////////////////////////////////////////////////////////////////////////////////
;The following function divides two currency values and returns an integer result.
;E.g. DivC(6.50, 2) will return the currency value of 3.25.
Procedure.currency DivC(a.currency, b.currency)
  If b
    a*10000
    a/b
    ProcedureReturn a
  EndIf  
  ProcedureReturn 0
EndProcedure
;/////////////////////////////////////////////////////////////////////////////////


;/////////////////////////////////////////////////////////////////////////////////
;The following function multiplies two currency values and returns a third currency value.
Procedure.currency MultC(a.currency, b.currency)
  Protected result.currency = a*b/10000
  ProcedureReturn result
EndProcedure
;/////////////////////////////////////////////////////////////////////////////////


;/////////////////////////////////////////////////////////////////////////////////
;The following function converts a currency value to a string.
Procedure.s StrC(cur.currency)
  Protected result$
  If cur<0
    result$="-"
    cur=-cur
  EndIf
  result$ + StrQ(cur/10000) + "." + RSet(StrQ(cur%10000),4,"0")
  ProcedureReturn result$
EndProcedure
;/////////////////////////////////////////////////////////////////////////////////


;/////////////////////////////////////////////////////////////////////////////////
;The following function converts a string representation of a currency value to a
;currency type.
;Any string with more than 4 places of decimals is rounded.
Procedure.currency ValC(value$)
  Protected result.currency, i, char
  Protected blnNegative, posDec, len, count, int$, dec$
  value$ = Trim(value$)
  count = CountString(value$,".")
  If value$ And count<=1
    len = Len(value$)
    ;Check a valid decimal number.
      For i = 1 To len
        char = Asc(Mid(value$,i,1))
        If char<>'.' And (char<'0' Or char>'9') And (char<>'-' Or i > 1)
          ProcedureReturn 0
        EndIf
      Next
    If count = 0
      result = ValQ(value$)*10000
    Else
      If Left(value$,1)="-"
        blnNegative = #True
        value$ = Right(value$,len-1)
        len-1
      EndIf
      posDec = FindString(value$,".", 1)
      int$ = Left(value$, posDec-1)
      dec$ = Right(value$, len-posDec)
      result = ValQ(int$)*10000 + ValQ(LSet(dec$,4,"0"))
      If Asc(Mid(dec$,5,1)) >= '5'
        result+1
      EndIf
      If blnNegative
        result=-result
      EndIf
    EndIf
  EndIf
  ProcedureReturn result
EndProcedure
;/////////////////////////////////////////////////////////////////////////////////


;/////////////////////////////////////////////////////////////////////////////////
;The following (Windows only) function formats the given currency value depending on
;the computer's locale etc. We allow certain fields to be over-ridden with the optional parameters.
;Leave the optional parameters blank in order to use the user's defaults.
;Otherwise, numDecimals specifies how many digits will appear after the decimal point,
;and blnShowCurrencySymbol specifies whether the currency symbol will appear or not?
CompilerIf #PB_Compiler_OS = #PB_OS_Windows
  Procedure.s FormatCurrency(cur.currency, numDecimals=-1, blnShowCurrencySymbol=-1)
    Protected val$, numChars, buffer$
    Protected currency.CURRENCYFMT
    Protected decimal$, thousands$, currency$
    val$ = StrC(cur)
    If numDecimals = -1 And blnShowCurrencySymbol = -1
      numChars = GetCurrencyFormat_(0,0,val$,0,0,0)
      buffer$ = Space(numChars+1)
      GetCurrencyFormat_(0,0,val$,0,@buffer$,numChars) 
      ProcedureReturn buffer$
    Else ;First load the defaults.
      currency\Grouping = 3
      ;LOCALE_IDIGITS
        currency\NumDigits = numDecimals
        If numDecimals = -1
          numChars = GetLocaleInfo_(#LOCALE_USER_DEFAULT,#LOCALE_IDIGITS,buffer$,0) 
          buffer$ = Space(numChars+1)
          GetLocaleInfo_(#LOCALE_USER_DEFAULT,#LOCALE_IDIGITS,buffer$,numChars) 
          currency\NumDigits = Val(buffer$)
        EndIf
      ;LOCALE_ILZERO
        numChars = GetLocaleInfo_(#LOCALE_USER_DEFAULT,#LOCALE_ILZERO,buffer$,0) 
        buffer$ = Space(numChars+1)
        GetLocaleInfo_(#LOCALE_USER_DEFAULT,#LOCALE_ILZERO,buffer$,numChars) 
        currency\LeadingZero = Val(buffer$)
      ;LOCALE_SDECIMAL
        numChars = GetLocaleInfo_(#LOCALE_USER_DEFAULT,#LOCALE_SDECIMAL,decimal$,0) 
        decimal$ = Space(numChars+1)
        GetLocaleInfo_(#LOCALE_USER_DEFAULT,#LOCALE_SDECIMAL,decimal$,numChars) 
        currency\lpDecimalSep = @decimal$
      ;LOCALE_STHOUSAND
        numChars = GetLocaleInfo_(#LOCALE_USER_DEFAULT,#LOCALE_STHOUSAND,thousands$,0) 
        thousands$ = Space(numChars+1)
        GetLocaleInfo_(#LOCALE_USER_DEFAULT,#LOCALE_STHOUSAND,thousands$,numChars) 
        currency\lpThousandSep = @thousands$
      ;LOCALE_INEGCURR
        numChars = GetLocaleInfo_(#LOCALE_USER_DEFAULT,#LOCALE_INEGCURR,buffer$,0) 
        buffer$ = Space(numChars+1)
        GetLocaleInfo_(#LOCALE_USER_DEFAULT,#LOCALE_INEGCURR,buffer$,numChars) 
        currency\NegativeOrder = Val(buffer$)
      ;LOCALE_ICURRENCY
        numChars = GetLocaleInfo_(#LOCALE_USER_DEFAULT,#LOCALE_ICURRENCY,buffer$,0) 
        buffer$ = Space(numChars+1)
        GetLocaleInfo_(#LOCALE_USER_DEFAULT,#LOCALE_ICURRENCY,buffer$,numChars) 
        currency\PositiveOrder = Val(buffer$)
      ;LOCALE_SCURRENCY
        If blnShowCurrencySymbol = 0
          currency\lpCurrencySymbol = @""
        Else
          numChars = GetLocaleInfo_(#LOCALE_USER_DEFAULT,#LOCALE_SCURRENCY,currency$,0) 
          currency$ = Space(numChars+1)
          GetLocaleInfo_(#LOCALE_USER_DEFAULT,#LOCALE_SCURRENCY,currency$,numChars) 
          currency\lpCurrencySymbol = @currency$
        EndIf
      ;Format the string.
        numChars = GetCurrencyFormat_(0,0,val$,currency,0,0)
        buffer$ = Space(numChars+1)
        GetCurrencyFormat_(0,0,val$,currency,@buffer$,numChars) 
        ProcedureReturn buffer$
    EndIf
  EndProcedure
CompilerEndIf
;/////////////////////////////////////////////////////////////////////////////////

DisableExplicit


;=================================================================================
;TEST.

;Set up two currency values, in my locale, £6.50 and £2.00.
  a.currency = ValC("6.5005")
  b.currency = ValC("2")

;A couple of simple manipulations using our standard operators.
  Debug "Half of £6.50 (unformatted) = " + StrC(a/2)
  CompilerIf #PB_Compiler_OS = #PB_OS_Windows
    Debug "Half of £6.50 (formatted) = " + FormatCurrency(a/2)
  CompilerEndIf

;Some binary currency operations.
  c.currency = a+b
  Debug "The sum of £6.50 and £2 (unformatted) = " + StrC(c)
  c = a-b
  Debug "The difference of £6.50 and £2 (unformatted) = " + StrC(c)
  c = MultC(a, b)
  Debug "£6.50 and £2 multiplied as currencies (unformatted) = " + StrC(c)
  c = DivC(a, b)
  Debug "£6.50 and £2 divided as currencies (unformatted) = " + StrC(c)
  x.l = a/b
  Debug "£6.50 and £2 divided NOT as currencies (INTEGER return) = " + Str(x)
;=================================================================================

Last edited by srod on Thu May 08, 2008 11:10 am, edited 3 times in total.
I may look like a mule, but I'm not a complete ass.
User avatar
Rook Zimbabwe
Addict
Addict
Posts: 4322
Joined: Tue Jan 02, 2007 8:16 pm
Location: Cypress TX
Contact:

Post by Rook Zimbabwe »

Srod, this is great! How do you figure to convert values returned by MSDB as the currency values there are bankers precision ie: 1.0000 instead of 1.00? Will your FormatCurrency() just round up or ignore the extra?

:D

I plan on using this in my next version of Blue Mesa Restaurant POS if I can work that out! 8)
Binarily speaking... it takes 10 to Tango!!!

Image
http://www.bluemesapc.com/
srod
PureBasic Expert
PureBasic Expert
Posts: 10589
Joined: Wed Oct 29, 2003 4:35 pm
Location: Beyond the pale...

Post by srod »

Hi,

I do no conversions whatsoever! :wink: The library will always use 4 places of decimals regardless of what format the local currency is and so it will be suitable for MSDB etc.

The formatCurrency() function simply calls the GetCurrencyFormat() api function which you can easily modify to give you the output you desire. I have simply left it with the user's default locale settings.
I may look like a mule, but I'm not a complete ass.
User avatar
graves
Enthusiast
Enthusiast
Posts: 160
Joined: Wed Oct 03, 2007 2:38 pm
Location: To the deal with a pepper

Post by graves »

HI,
I've my own mult/div procedure

Code: Select all

;-------------------------------------------------------------------------------
; SHIFT REGISTER
;-------------------------------------------------------------------------------
;  "op1" and "op2" operators are always integers 
;   i.e value 58,00 is stocked as 5800
;
;  "dec" field have operators decimals and an optional operation
;     "xxx" OR "xxx*x" "xxx/x"
;
;  MULTIPLY operators  result = op1*op2
; shreg = (op1 decimals + op2 decimals - result decimals)
;
;  DIVIDE operators    result = op1/op2
; shreg = (-op1 decimals + op2 decimals + result decimals)
;
;-------------------------------------------------------------------------------
#Mult = 1
#Divide = 2
macro DecMult(op1, op2, dec)  : Comun_ShiftReg(#Mult, op1, op2, dec)  : endmacro
macro DecDivide(op1, op2, dec): Comun_ShiftReg(#Divide, op1, op2, dec): endmacro

Procedure Comun_ShiftReg(ope, op1, op2, dec.s)
  InternalQ.q = op1
  shreg = 0
  if len(dec) > 3
    select mid(dec,4,1)
      case "*" : shreg = -val(mid(dec,5,1))
      case "/" : shreg =  val(mid(dec,5,1))
    endselect
  endif

  select ope
    case #Mult
        shreg + val(left(dec,1)) + val(mid(dec,2,1)) - val(mid(dec,3,1))
        InternalQ = op1 * op2
        if shreg < 1: InternalQ * Pow(10,abs(shreg)+1)
                else: InternalQ / Pow(10,shreg-1): endif

    case #Divide
      if op2
        shreg - val(left(dec,1)) + val(mid(dec,2,1)) + val(mid(dec,3,1))
        if shreg < 1: InternalQ = op1 / Pow(10,abs(shreg)-1)
                else: InternalQ = op1 * Pow(10,shreg+1): endif
        InternalQ / op2
      endif
  endselect
  InternalQ + 5
  ReturnL = InternalQ / 10
  ProcedureReturn ReturnL
EndProcedure
srod
PureBasic Expert
PureBasic Expert
Posts: 10589
Joined: Wed Oct 29, 2003 4:35 pm
Location: Beyond the pale...

Post by srod »

Graves, is this for use with my currency library? If not, then you should really have started another thread. If it is for use with my lib then I apologise, you are more than welcome to add to the code. However, an example of it's use would probably be of benefit.
I may look like a mule, but I'm not a complete ass.
User avatar
graves
Enthusiast
Enthusiast
Posts: 160
Joined: Wed Oct 03, 2007 2:38 pm
Location: To the deal with a pepper

Post by graves »

srod:
Yes, you can use it in your library if you want.
Mi goal is to extend their scope, using up to 9 decimal positions in every operator
If you don't believe it interesting, ignore it.

examples:

Code: Select all

; Money change
dolar = 1234500     ; 12,345.00 edited
change = 1645000   ; 1.645 dolar by euro
euro = DecMult(dolar, change, "262")
debug euro

; Percentil VAT
baseVAT = 12345678    ; 123,456.78 euro
percVAT = 170             ; 17,0 %
valueVAT = DecMult(baseVAT, percVAT, "232")  ; percVAT has only 1 dec. but using "3" divide result by 100 (you can  use also "212/2")
debug valueVAT

; Base VAT
valueVAT = 12345678   ; 123,456.78 euro
percVAT = 170             ; 17,0 %
baseVAT = DecDivide(valueVAT, (1000+percVAT), "232") ; Add 1000 to obtain 1.170 (3 decs)
debug baseVAT

srod
PureBasic Expert
PureBasic Expert
Posts: 10589
Joined: Wed Oct 29, 2003 4:35 pm
Location: Beyond the pale...

Post by srod »

Update - 25th April 2008.
Have altered the (Windows only) FormatCurrency() function to allow the user to over-ride a couple of the user's default locale currency format settings. You can now specify the number of digits to be displayed after the decimal point, so, for example, you can opt to display all 4 fractional digits etc. Also, you can opt to remove the currency symbol from the formatted string; £, $ etc.

Updated code is in the first post.

@Rook : does this address your query regarding MSDB ?
I may look like a mule, but I'm not a complete ass.
User avatar
Rook Zimbabwe
Addict
Addict
Posts: 4322
Joined: Tue Jan 02, 2007 8:16 pm
Location: Cypress TX
Contact:

Post by Rook Zimbabwe »

Oh yes! This is superfine! 8)

I am groovin on the righteous code my man! Fantastic work (as always!) :D
Binarily speaking... it takes 10 to Tango!!!

Image
http://www.bluemesapc.com/
srod
PureBasic Expert
PureBasic Expert
Posts: 10589
Joined: Wed Oct 29, 2003 4:35 pm
Location: Beyond the pale...

Post by srod »

Bug fix - 7th May 2008.
ValC() didn't check that the given value was a valid number! Doh! :oops:

Updated code is in the first post.
I may look like a mule, but I'm not a complete ass.
Post Reply