#31104

math.decimal: rounding modes, minor-unit conversions, exponent parsing and scale preservation

Openalbriswopened 6d ago

Describe the feature

math.decimal (which closed #25010 and #21754) gets addition, subtraction, multiplication and comparison exactly right. For money work a few pieces are still missing. Checked on V 0.5.2 and on master at c64467d9, where the module is unchanged:

import math.decimal

one := decimal.from_int(1)
vat := decimal.from_string('19.99')! * decimal.from_string('0.21')! // 4.1979
println(vat.div_prec(one, 2)!)                                     // 4.2, not 4.20
println(decimal.from_string('5.00')!)                              // 5
println(decimal.from_string('0.125')!.div_prec(one, 2)!)           // 0.13; half-even gives 0.12
decimal.from_string('1e3') or { println(err) }                     // invalid decimal value
println(one / decimal.zero)                                        // panics

I'd like to add the following, as separate small PRs:

  1. Rounding modes. pub enum RoundingMode { half_up half_even half_down up down ceiling floor }, where half_up means half away from zero (what div_prec does today).

    • pub fn (d Decimal) round(places int, mode RoundingMode) !Decimal
    • pub fn (d Decimal) div(divisor Decimal, places int, mode RoundingMode) !Decimal. div_prec stays as the half_up shorthand.
  2. Minor units and small operations.

    • pub fn from_minor_units(units i64, places int) !Decimal
    • pub fn (d Decimal) to_minor_units(places int) !i64, which returns an error when the value has more places than places or does not fit in i64.
    • rem (result takes the dividend's sign; error on a zero divisor), sign() int, min, max.
  3. Exponent notation in from_string. Accept 1e3, 1.5E-2 and similar, with the exponent capped so that input like 1e999999999 cannot force a huge allocation.

  4. Keep the scale (behaviour change, needs agreement first). from_string('5.00') keeps scale 2 and prints 5.00. As in Java's BigDecimal:

    • + and - take the larger scale; * adds the scales; round and div produce exactly places digits.
    • == and < keep comparing by value, so 5.0 == 5.00.
    • A new normalize() strips trailing zeros on request.

    This changes str() for values that have trailing zeros today. Since the module is new, now seems the cheapest time to change it.

The / operator would still panic on a zero divisor. V cannot handle an operator's error with or {}, so the docs would point fallible code to div.

Use Case

Invoices, VAT and tax calculations, and storing amounts as PostgreSQL numeric or as integer cents. These need values rounded to a fixed number of places under a chosen rule, printed with those places, and converted exactly to and from integer minor units.

Proposed Solution

Items 1–3 only add API and are backward compatible. Item 4 changes output, so I would hold it until it's agreed here. Is the scale-preserving design acceptable, or would you prefer to keep normalization and add a str_fixed(places) formatter instead?

Acknowledgements

  • I may be able to implement this feature request
  • This feature might incur a breaking change (item 4 only)

Activity

1
verdy-p4d ago

Feature request:

  • Add floored division and remainder (like for floats, with the same contracts). For now you only provide .rem() with an integer division towards zero instead of minus infinity.
  • May be add also a "euclidian" variant (like for floats) with the remainder having the same sign as the dividend.

Rationale:

  • In some cases, the computing stability and convergence speed depends on stable signs and the sign of the result for remainders should be the same as the divisor, which is generally positive so that the remainder should alwo be positive, but in some high-precision expansions with polynomial series, the range of value must remain within strict stable intervals, not alternating intervals to get fast convergence and stability). The same remark applies to the "euclidian" variant.

Usage:

  • For signal processing, and high-precision evaluation of mathematical periodic functions (trigonometry, logarithms and exponentiation, complexes, and more generally linear algebras), and still with very fast serialisation/deserialisation with decimal strings preserving the same precision.

RFC:

  • Is there a way to limit the desired precision for such evaluations? Including basic divisions like 1/3 which is always inexact with numeric representation using base equal to any power of 2?
  • Is there a choice for other bases e.g. using 3 or other primes for the base, or other useful bases (such as some small product of primes like 60, 210, or 240 with 8 bits per digit, or even 60060 with 16 bits per digit), or using alternate representations using two integers for rationals (with a specified maximum range) and some exponent (with a power of 2 or of another prime used for the base of the mantissa, with a maximum size of exponent as well). Such extension would probably require another library than math.decimal, and may better fit in math.big. Note that base-3 (ternary) computing will soon come back (after the historic experiments in USSR that were abandoned in the 1970's for reason of costs in the chip industry) with native hardware implementations in ALU/FPU in some newer Chinese processors in development (that allow better density and lower power consumption with ternary transistors and memory storages, that could help bypass a limit for extending the "Moore's Law", even if there's a hardware binary conversion interface provided for compatiblity with binary devices and existing standard networking protocols).
  • Another extension would allow computing directly with one of two half-open ranges [r, r+delta[ or ]r-delta, r] (when delta is non-zero) or as [r,r] (for exact values, when delta is zero): this is useful for evaluating complex expressions and aggregates and determine the effective precision of the result (this also allows representing NaN/undetermined values, signed infinites and signed denormal zeroes, when delta has the maximum value) with a veriable stability of the computation (and the possibility to choose between alternate approximating computations, the one that is the most precise/stable). This can be implemented easily using a pair of floats (one for the rounded value r, and the other for the delta; this can be further improved for numeric stability by using the IEEE "even rounding mode" by default, which determines which half-open range to use, where the effective value is always at the middle of the stored half-open range). I've used such approach to compute and aggregate massive statistic data, and without any "error", "panic" or "exception", even for extreme cases like divisions by exact zero or even multiples of pi/2 for tangents, if such "exceptional" values are in the stored range (e.g. in physics measurements where you need to evaluate the "sigmas"): a pair f64 values, and I named this type "r64" (with the storage actually being 128-bit wide, with the "r" meaning "range" or "real" ).