Skip to content

Commit

Permalink
Documentation, tests and bug fixes for std/misc/decimal (mighty-gerbi…
Browse files Browse the repository at this point in the history
…ls#990)

Also fix build-spec for srfi 141, and a typo in char-set.
  • Loading branch information
fare authored Oct 7, 2023
1 parent c68aabf commit 4574928
Show file tree
Hide file tree
Showing 6 changed files with 454 additions and 150 deletions.
1 change: 1 addition & 0 deletions doc/.vuepress/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,7 @@ module.exports = {
path: '/reference/std/misc/',
children: [
'misc/',
'misc/decimal',
'misc/list',
'misc/list-builder',
'misc/alist',
Expand Down
235 changes: 235 additions & 0 deletions doc/reference/std/misc/decimal.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,235 @@
# Decimal Numbers

The `:std/misc/decimal` library provides support for
arbitrary-precision decimal numbers and conversion between them and strings.
This can notably be important for handling financial data without losing precision.

::: tip To use bindings from this module
``` Scheme
(import :std/misc/decimal)
```
:::

Decimal numbers are "just" a subset of rational numbers, and so
all arithmetic operations are already implemented by Gerbil Scheme's
underlying generic number arithmetics (itself handled by Gambit Scheme).
The main remaining issue then is parsing and printing,
conversion to and from strings, since going through
regular floating point parsing and printing will drop decimals.

Acknowledgements: The code was written with
inspiration from two Common Lisp libraries,
QUUX (see the [snapshot at QITAB](https://qitab.common-lisp.dev/) and
[wu-decimal](https://github.com/Wukix/wu-decimal),
with its own design and implementation improvements.

## decimal?
``` Scheme
(decimal? x) -> bool
```

Given any value `x`, return true if that object is a decimal number,
i.e. a rational number that is not a floating-point number.

::: tip Examples:
``` Scheme
> (decimal? 13/10)
#t
> (decimal? 1.3)
#f
> (decimal? 13/125)
#t
> (decimal? 'foo)
#f
```
:::

## parse-decimal
``` Scheme
(parse-decimal
input
sign-allowed?: (sign-allowed? #t)
decimal-mark: (decimal-mark #\.)
group-separator: (group-separator_ #f)
exponent-allowed: (exponent-allowed_ #f)) -> decimal
```

`parse-decimal` expects and parses a decimal number on an `input`,
with the options specifed via keyword arguments.

The `input` will be cast to a `BufferedStringReader` using
[`open-buffered-string-reader`](../stdio.md#open-buffered-string-reader).
`parse-decimal` will then side-effect this reader as it parses,
and finally return the decimal number,
or raises a `parse-error` (from `:std/parser/base`).

The keyword arguments `decimal-mark` and `group-separator` are each a character or false,
and specify optional allowed decimal mark and group separator characters,
to support for different (typically cultural) numerical conventions.
For convenience, `group-separator` can also be `#t`, designating the comma character `#\,`.
These two arguments cannot designate the same character.
If `decimal-mark` is false, then and you can only parse integer numbers before the exponent,
and so can only parse integers if `exponent-allowed` is false (the default),
though you can still use exponents if allowed to denote a fractional number (a weird use case).

The boolean `sign-allowed` controls whether a `+` or `-` sign is accepted or must be omitted.

`exponent-allowed` is a boolean or a string controlling exponent notation.
Exponent notation follows the syntax for Scheme floats,
with the exception that the exponent marker must be `#\e` or `#\E` when `exponent-allowed` is `#t`,
or the exponent marker must be `char=` to some element of `exponent-allowed`
when `exponent-allowed` is a string.
Exponents, when allowed, can always be signed.

It is up to the caller to provide an actual `BufferedStringReader`
and process any leading or trailing whitespace and check for `#!eof`
before and/or after calling `parse-decimal`.

`: PeekableStringReader sign-allowed?:Bool decimal-mark:Char group-separator:(Or Char Bool) exponent-allowed:(or Bool String) -> Decimal`

You may use utilities from [:std/text/basic-parsers](../text/basic-parsers)
to parse decimals as part of something bigger, or just use `string->decimal` below.

## string->decimal
``` Scheme
(string->decimal s
sign-allowed?: (sign-allowed? #t)
decimal-mark: (decimal-mark #\.)
group-separator: (group-separator #f)
exponent-allowed: (exponent-allowed #f)
allow-leading-whitespace?: (allow-leading-whitespace? #f)
allow-trailing-whitespace?: (allow-trailing-whitespace? #f)
start: (start 0)
end: (end #f))
```
Parse a decimal number from given string `s`.

The `start` and `end` arguments specify which slice of the string to use
as an interval `[start, end)` of the string indexes
(if `end` is unspecified or false, it designates the length of the string).

The `sign-allowed?`, `decimal-mark`, `group-separator` and `exponent-allowed`
arguments are as per `parse-decimal`.

The `allow-leading-whitespace?` (respectively `allow-trailing-whitespace?`)
arguments specify whether whitespace is allowed to be parsed and skipped
before (respectively after) the decimal number as part of the string:
- if the value is `#f` (the default) then no whitespace is allowed
before or after the decimal;
- if the value is `#t`, then any of the strict whitespaces
(space, tab, newline, return) is accepted;
- if the value is a procedure, then this procedure is assumed to be
a unary predicate accepting any character or the `#!eof` marker and
returning true if its argument is a character considered whitespace
for the purpose of skipping before to parse a decimal
(include digits and signs in the list at your peril);
see [:std/text/char-set](../text/char-set.md)
for other whitespace predicates.

## write-decimal
``` Scheme
(write-decimal number (port (current-output-port))
scale: (scale #f)
width: (width #f)
integral-digits: (integral-digits #f)
fractional-digits: (fractional-digits #f)
pad: (pad_ #f)
always-decimal?: (always-decimal? #f)
always-sign?: (always-sign? #f)
decimal-mark: (decimal-mark #\.)
precision-loss-behavior: (precision-loss-behavior 'error))
```

Write a decimal `number` to the specified `port` with the given keyword options.

The `port` is designated as per [`with-output`](../ports.md#with-output).

The keyword options are as follow:
- An integer `scale` (or `#f` meaning `0`, the default), such that
the number actually printed is the argument `number` notionally multiplied
by ten to that `scale` (default `#f`).
- A natural integer `width` within which to fit the number
or `#f` (the default) for no limitation.
- A minimum number of `integral-digits` to display
or `#f` (the default, same as `0`) for no minimum;
the minimum can notably be 1 to force `#\0` to be printed
before a decimal point even if there are already fractional digits after.
- A minimum number of `fractional-digits` to display
or `#f` (the default, same as `0`) for no minimum;
the minimum can notably be 1 to force `#\0` to be printed
after a decimal point even if there are already integral digits before.
- A character `pad` to print when left-padding for desired width
or `#f` (the default, same as `#\space`);
- A boolean `always-decimal?` (defaults to `#f`) for whether
a decimal mark will always be printed even for integers.
- A boolean `always-sign?` (defaults to `#f`) for whether
a sign will always be printed even for positive numbers.
- A character `decimal-mark` to use as the decimal mark.
- A symbol `precision-loss-behavior` for the behavior on precision loss,
one of `error` (the default), `truncate` or `round`,
in case the digits cannot fit in the space.

Note that even if `precision-loss-behavior` is `truncate` or `round`,
`write-decimal` may throw an error if the integral part of the `number` is
too large to fit within the given width.

## decimal->string
``` Scheme
(decimal->string number
scale: (scale #f) width: (width #f)
integral-digits: (integral-digits #f) fractional-digits: (fractional-digits #f)
pad: (pad #f) always-decimal?: (always-decimal? #f) always-sign?: (always-sign? #f)
decimal-mark: (decimal-mark #\.)
precision-loss-behavior: (precision-loss-behavior 'error))
```

`decimal->string` converts a decimal `number` to a string by
calling `write-decimal` with the same number and options.

## LossOfPrecision LossOfPrecision?

An error class and its recognizer predicate, for the sake of handling cases when
printing a decimal number results in loss of precision.

## power-of-5
``` Scheme
(power-of-5 x) -> nat or false
```
If `x` is an exact integer that is the `n`th power of 5, return `n`,
otherwise return false.

## find-decimal-multiplier
``` Scheme
(find-decimal-multiplier d) -> (values integer integer)
```
Given a positive integer `d`, the reduced denominator of a decimal number,
thus a number of the form `2**m * 5**n`,
compute `c` such that `c*d = c*(2^m*5^n) = 10^max(m,n)`
and returns the two values `c` and `max(m,n)`,
respectively the multiplier required to make the denominator a power of 10,
and which power of 10 you will thus have reached.

## count-significant-digits
``` Scheme
(count-significant-digits n) -> nat
```

Count the number of significant digits to represent the natural integer `n`.

Exception: for `0`, return `0`, which defies convention for writing integers,
but is the right thing in the context of figuring out how many decimals to use

## decimal->digits-exponent
``` Scheme
(decimal->digits-exponent decimal) -> (values integer integer)
```
Given a decimal number `decimal`, return two values:
- the absolute smallest integer with all its non-zero digits, of same sign as decimal.
- the power of ten by which the decimal had to be multiplied to get this integer
(can be positive, zero or negative).

## digits-exponent->decimal
``` Scheme
(digits-exponent->decimal digits exponent) -> decimal
```
Given an integer `digits` and an `exponent`, multiply `digits` by 10 to the given power.
2 changes: 1 addition & 1 deletion src/std/build-spec.ss
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@
(gxc: "srfi/srfi-135/macros" (extra-inputs: ("srfi/srfi-135/macros.scm")))
(gxc: "srfi/srfi-135/text" (extra-inputs: ("srfi/srfi-135/text.scm")))
"srfi/135"
(gxc: "srfi/141" (extra-inputs: ("srfi/srfi-141.scm")))
(gxc: "srfi/141")
(gxc: "srfi/143" (extra-inputs: ("srfi/srfi-143/carries.scm")))
(gxc: "srfi/144" (extra-inputs: ("srfi/srfi-144/144.constants.scm" "srfi/srfi-144/144.body0.scm"
"srfi/srfi-144/144.body.scm" "srfi/srfi-144/144.special.scm")))
Expand Down
Loading

0 comments on commit 4574928

Please sign in to comment.