Category talk:Wren-money

From Rosetta Code
Revision as of 12:36, 3 November 2023 by PureFox (talk | contribs) (→‎Source code: Now uses Wren S/H lexer.)
(diff) ← Older revision | Latest revision (diff) | Newer revision → (diff)

Financial arithmetic

Wren's only native numeric type (Num) is represented by a 64 bit floating point number. Most decimal numbers can not be represented exactly by this type and it is not therefore ideal for monetary calculations because of the anomalies which this loss of precision can produce.

The usual solutions to this are either to use integer arithmetic throughout or to introduce a 'decimal' type which can represent decimal numbers exactly up to a certain precision.

The first solution (i.e. working in cents or whatever the fractional unit is called) tends to be rather tedious in practice because of the manual conversions needed and awkwardness in displaying results in the correct form.

I have therefore decided to create a Money type which can be used in a reasonably natural fashion and does the necessary conversions using integer arithmetic under the hood. This only supports currencies whose fractional unit is 1/100th of the principal unit which accounts for the majority of currencies in use today and simplifies the implementation considerably.

Given that integer arithmetic in Wren is only precise up to ± 2^53 this still gives a reasonable range for the principal currency unit of about ± 90 quadrillion which should be sufficient for most practical purposes. It would be possible to extend this range by using either the Long or BigInt classes for internal calculations rather than Num. However, this would make calculations much slower and does not therefore seem worthwhile.

Where calculations would result in amounts with 3 or more decimal places, these are rounded to 2 decimal places using one of two rounding modes - nearest (with 0.5 rounded up) or lower. It is believed that accountants normally use the first of these and that therefore is the default.

As far as displaying results are concerned, the Money class supports different options for currency symbol, negative value, thousands separator and decimal point. The respective defaults are: (none), "-", "," and ".".

There are some currencies which uses different multiples than 100 for their fractional units (such as 5, 10 or 1000) or have no fractional unit at all and anyone interested in writing financial applications for such currencies may be able to create a special Money class for them by modifying the present one accordingly.

I don't expect this module to see much use on RC as tasks involving financial arithmetic are rare. However, as I've been finding it useful in my own efforts, I thought I would share it with others who are interested in writing this type of application.

Source code

/* Module "money.wren" */

import "./trait" for Comparable

/*
    Money represents a safe integer scaled by 100 to repesent an amount of money
    expressed in units of a decimal currency plus 100ths of the currency unit
    (referred to as 'cents' whatever its actual name). It is designed to always give
    exact results for basic arithmetic operations except that, where those operations
    produce more than 2 decimal places, they are rounded to either :

    1. the nearer second decimal place with 0.5 rounded up and away from zero; or
    2. the lower second decimal place, depending on the 'roundDown' property.
 
    Money objects are immutable and may be negative.
*/
class Money is Comparable {
    // Constants
    static minusOne  { Money.new_( -100) }
    static minusCent { Money.new_(   -1) }
    static zero      { Money.new_(    0) }
    static one       { Money.new_(  100) }
    static two       { Money.new_(  200) }
    static three     { Money.new_(  300) }
    static four      { Money.new_(  400) }
    static five      { Money.new_(  500) }
    static six       { Money.new_(  600) }
    static seven     { Money.new_(  700) }
    static eight     { Money.new_(  800) }
    static nine      { Money.new_(  900) }
    static ten       { Money.new_( 1000) }
    static half      { Money.new_(   50) }
    static quarter   { Money.new_(   25) }
    static fifth     { Money.new_(   20) }
    static tenth     { Money.new_(   10) }
    static nineCent  { Money.new_(    9) }
    static eightCent { Money.new_(    8) }
    static sevenCent { Money.new_(    7) }
    static sixCent   { Money.new_(    6) }
    static fiveCent  { Money.new_(    5) }
    static fourCent  { Money.new_(    4) }
    static threeCent { Money.new_(    3) }
    static twoCent   { Money.new_(    2) }
    static cent      { Money.new_(    1) }

    // Common non-ascii currency symbols
    static euro  { "€" }
    static pound { "£" }
    static rupee { "₹" }
    static won   { "₩" }

    // Gets or sets the default currency symbol. If "" no symbol.
    static symbol     { __symbol }
    static symbol=(s) { __symbol = s }

    // Gets or sets the characters used to parenthesize negative Money objects. If 'null' uses '-'.
    static negParens      { __negParens }
    static negParens =(p) { __negParens = p }

    // Gets or sets the character used to commatize thousands within Money objects. If "" no commatization.
    static comma      { __comma }
    static comma =(c) { __comma = c }

    // Gets or sets the character used for the decimal point within Money objects. If 'null' uses '.'.
    static point      { __point }
    static point =(p) { __point = p }

    // Gets or sets the default rounding mode. If 'true' rounds to the lower second decimal place.
    // Otherwise rounds to the nearer second decimal place with 0.5 rounded up and away from zero.
    static roundDown      { __down }
    static roundDown =(r) { __down = r }

    // Return largest and smallest Money objects.
    static largest  { Money.new_(Num.maxSafeInteger) }
    static smallest { Money.new_(Num.minSafeInteger) }

    // Returns the greater of two Money objects.
    static max(m1, m2) {
         if (!(m1 is Money) || !(m2 is Money)) Fiber.abort("Arguments must both be Money objects.") 
         return (m1 > m2) ? m1 : m2
    }

    // Returns the smaller of two Money objects.
    static min(m1, m2) {
         if (!(m1 is Money) || !(m2 is Money)) Fiber.abort("Arguments must both be Money objects.")
         return (m1 < m2) ? m1 : m2
    }

    // Returns the positive difference of two Money objects.
    static dim(m1, m2) { max(m1 - m2, zero) }

    // Returns a list of Money objects from 'from' to 'to'
    // with a given 'step' which must be positive
    static range (from, to, step) {
        if (!(from is Money) || !(to is Money) || !(step is Money)) {
            Fiber.abort("Arguments must all be Money objects.")
        }
        if (!step.isPositive) Fiber.abort("Step must be positive.") 
        var res = []
        var m = from.copy()
        if (from < to) {
            while (m <= to) {
                res.add(m)
                m = m + step
            }
        } else {
            while (m >= to) {
                res.add(m)
                m = m - step
            }
        }
        return res
    }

    // Creates a new Money object from a numeric string.
    static fromString(s) { Money.new(Num.fromString(s)) }

    // Creates a new Money object from a Num rounded to 2 decimal places
    // and ensures the result is 'safe'.
    construct new(n) {
        if (!(n is Num) || n.isInfinity || n.isNan) Fiber.abort("Argument must be a finite number.")
        if (n.isInteger) {
            _m = n * 100
        } else {
            _m = !__down ? (n * 100).round : (n * 100).floor
        }
        if (_m.abs > Num.maxSafeInteger) Fiber.abort("Argument is unsafe.")
    }

    // Private constructor whose argument is an amount in cents.
    construct new_(m) {
        if (m.abs > Num.maxSafeInteger) Fiber.abort("Argument is unsafe.")
        _m = m
    }

    // Returns the value of this instance in cents.
    cents { _m }

    // Other self-explanatory properties.
    isInteger  { _m % 100 == 0 } // checks if integral or not
    isPositive { _m > 0 }        // checks if positive
    isNegative { _m < 0 }        // checks if negative
    isUnit     { _m.abs == 1 }   // checks if plus or minus one
    isZero     { _m == 0 }       // checks if zero

    // Rounding methods (similar to those in Num class).
    ceil     { Money.new(toNum.ceil) }      // higher integer
    floor    { Money.new(toNum.floor) }     // lower integer
    truncate { Money.new(toNum.truncate) }  // lower integer, towards zero
    round    { Money.new(toNum.round) }     // nearer integer
    fraction { this - truncate }            // fractional part (same sign as this)

    // Returns a copy of this instance.
    copy() { Money.new_(_m) }

    // Basic methods.
    inc  { Money.new_(_m + 100) }  // increases this instance by one unit
    dec  { Money.new_(_m - 100) }  // decreases this instance by one unit
    abs  { Money.new_(_m.abs) }    // returns the absolute value of this instance
    sign { _m.sign }               // returns the sign of this instance (1 if +ve, 0 if zero and -1 if -ve)
    sqrt { Money.new(toNum.sqrt) } // returns the square root of the current instance (may not round trip)

    square  { this * this }        // returns the square of the current instance
    inverse { Money.new(100/_m) }  // inverts the current instance

    /* Operators. */

    // Reverses the sign of this instance.
    - { Money.new_(-_m) }

    // Adds another Money object or Num to this instance.
    +(other) {
        if (other is Money) return Money.new_(_m + other.cents)
        if (other is Num)   return Money.new_(_m + Money.new(other).cents)
        Fiber.abort("Argument must be a Money object or a number.")
    }

    // Subtracts another Money object or Num from this instance.
    -(other) { this + (-other) }

    // Multiplies this instance by a Num or another Money object.
    *(other) {
        if (!(other is Num) && !(other is Money)) Fiber.abort("Argument must be a number or a Money object.")
        var prod = (other is Num) ? _m * other : _m / 100 * other.cents
        return Money.new_(!__down ? prod.round : prod.floor)
    }

    // Divides this instance by a Num or another Money object.
    /(other) {
        if (!(other is Num) && !(other is Money)) Fiber.abort("Argument must be a number or a Money object.")
        var div = (other is Num) ? _m / other : _m / other.cents * 100
        return Money.new_(!__down ? div.round : div.floor)
    }

    // Returns the remainder after dividing this instance by a Num or another Money object.
    %(other) {
        var d = this / other
        return this - d * other
    }

    // Divides this instance by a Num or another Money object
    // and truncates the result to the lower integer towards zero.
    idiv(other)  { (this/other).truncate }

    // Returns the remainder after 'idiv' of this and a Num or another Money object.
    irem(other) {
        var d = this.idiv(other)
        return this - d * other
    }

    /* Other methods */

    // Returns 'p' per cent' of this instance.
    perCent(p) {
        if (p is Money) p = p.toNum
        var pc = _m / 100 * p
        return Money.new_(!__down ? pc.round : pc.floor)
    }

    // Returns (as a Num) 'this' expressed as a percentage of 'p'.
    isPerCentOf(p) { this.toNum / p.toNum * 100 }

    // Returns 'p' per mille of this instance.
    perMille(p) {
        if (p is Money) p = p.toNum
        var pm = _m / 1000 * p
        return Money.new_(!__down ? pm.round : pm.floor)
    }

    // Returns (as a Num) 'this' expressed as a per mille of 'p'.
    isPerMilleOf(p) { this.toNum / p.toNum * 1000 }

    // Compares the current instance with another Money object or Num. If they are equal returns 0.
    // If 'this' is greater, returns 1. Otherwise returns -1.
    compare(other) {
        if (other is Money) return (_m - other.cents).sign
        if (other is Num)   return (_m - other * 100).sign
        Fiber.abort("Argument must be a Money object or a number.")
    }

    // Converts the current instance to a Num.
    toNum { _m / 100 }

    // Returns the string representation of this instance having regard to the default settings for
    // currency symbol, negative parenthesization, thousands separator and decimal point.
    toString {
        var pt = __point ? __point : "."
        if (_m == 0 || _m == -0) return "0%(pt)00"
        var neg = _m < 0
        var s = _m.abs.toString
        if (s.contains("e")) {
            var u = (_m.abs / 100).floor
            var c = _m.abs % 100
            var cs = c.toString
            if (cs.count == 1) cs = "0" + cs
            s = "%(u)%(cs)"
        }
        var sign = neg ? (__negParens ? __negParens[0] :"-") : ""
        var sym = __symbol ? __symbol : ""
        var ret = sym + sign
        var sc = s.count
        if (sc == 1) {
            ret = ret + "0%(pt)0" + s
        } else if (sc == 2) {
            ret = ret + "0%(pt)" + s
        } else {
            s = s[0..-3] + pt + s[-2..-1]
            if (sc > 5 && __comma) {
                var i = sc - 5
                while (i >= 1) {
                    s = s[0...i] + __comma + s[i..-1]
                    i = i - 3
                }
            }
            ret = ret + s
        }
        return (neg && __negParens) ? ret + __negParens[1] : ret
    }
}

/*  Moneys contains various routines applicable to lists of Money objects. */
class Moneys {
    static sum(a)  { a.reduce(Money.zero) { |acc, x| acc + x } }
    static mean(a) { sum(a)/a.count }
    static prod(a) { a.reduce(Money.one) { |acc, x| acc * x } }
    static max(a)  { a.reduce { |acc, x| (x > acc) ? x : acc } }
    static min(a)  { a.reduce { |acc, x| (x < acc) ? x : acc } }
}

/* Set defaults */
Money.symbol = ""
Money.negParens = null
Money.comma = ","
Money.point = "."
Money.roundDown = false