From fac9bc883deed88903be40b897e1d5b861baeeae Mon Sep 17 00:00:00 2001 From: Denis Barucic Date: Wed, 13 Nov 2024 21:42:27 +0100 Subject: [PATCH] Docs: Add documentation page (#90) --- .github/workflows/documentation.yml | 29 ++++ .gitignore | 2 + README.md | 197 +--------------------- docs/.gitignore | 2 + docs/Project.toml | 3 + docs/make.jl | 8 + docs/src/contrib.md | 15 ++ docs/src/index.md | 72 ++++++++ docs/src/operations.md | 247 ++++++++++++++++++++++++++++ src/context.jl | 24 +++ src/decimal.jl | 15 +- 11 files changed, 419 insertions(+), 195 deletions(-) create mode 100644 .github/workflows/documentation.yml create mode 100644 docs/.gitignore create mode 100644 docs/Project.toml create mode 100644 docs/make.jl create mode 100644 docs/src/contrib.md create mode 100644 docs/src/index.md create mode 100644 docs/src/operations.md diff --git a/.github/workflows/documentation.yml b/.github/workflows/documentation.yml new file mode 100644 index 0000000..4948d50 --- /dev/null +++ b/.github/workflows/documentation.yml @@ -0,0 +1,29 @@ +name: Documentation + +on: + push: + branches: + - master + tags: '*' + pull_request: + +jobs: + build: + permissions: + actions: write + contents: write + pull-requests: read + statuses: write + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: julia-actions/setup-julia@v2 + with: + version: '1' + - uses: julia-actions/cache@v2 + - name: Install dependencies + run: julia --project=docs/ -e 'using Pkg; Pkg.develop(PackageSpec(path=pwd())); Pkg.instantiate()' + - name: Build and deploy + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: julia --project=docs/ docs/make.jl diff --git a/.gitignore b/.gitignore index 717fcea..04bd7ab 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,3 @@ *.cov + +docs/build/ diff --git a/README.md b/README.md index f223788..9ce475e 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,10 @@ # Decimals.jl: Arbitrary precision decimal floating point arithmetics in Julia -[![Coverage Status](https://coveralls.io/repos/github/JuliaMath/Decimals.jl/badge.svg?branch=master)](https://coveralls.io/github/JuliaMath/Decimals.jl?branch=master) +[![CI Status](https://github.com/JuliaMath/Decimals.jl/workflows/CI/badge.svg)]( https://github.com/JuliaMath/Decimals.jl/actions?query=workflows/CI) +[![Coverage Status](https://codecov.io/github/JuliaMath/Decimals.jl/branch/master/graph/badge.svg)](https://codecov.io/github/JuliaMath/Decimals.jl) +[![Documentation](https://img.shields.io/badge/docs-master-blue.svg)](http://juliamath.github.io/Decimals.jl) -The `Decimals` package provides basic data type and functions for arbitrary precision [decimal floating point](https://en.wikipedia.org/wiki/Decimal_floating_point) arithmetic in Julia. It supports addition, subtraction, negation, multiplication, division, and equality operations. +The `Decimals` package provides basic data type and functions for arbitrary precision [decimal floating point](https://en.wikipedia.org/wiki/Decimal_floating_point) arithmetic in Julia. Why is this needed? The following code in Julia gives an answer @@ -11,182 +13,8 @@ Why is this needed? The following code in Julia gives an answer In words, the binary floating point arithmetics implemented in computers has finite resolution - not all real numbers (even within the limits) can be expressed exactly. While many scientific and engineering fields can handle this behavior, it is not acceptable in fields like finance, where it's important to be able to trust that $0.30 is actually 30 cents, rather than 30.000000000000004 cents. -## Installation +See [documentation](http://juliamath.github.io/Decimals.jl). -```julia -julia> Pkg.add("Decimals") -``` -or just `Ctrl`+`]` and -```julia -(v1.2) pkg> add Decimals -``` - -## Usage - -```julia -julia> using Decimals -``` - -### Construction - - -`Decimal` is constructed by passing values `s`, `c`, `q` such that -`x = (-1)^s * c * 10^q`: -```julia -julia> Decimal(0, 1, -1) -0.1 - -julia> Decimal(1, 1, -1) --0.1 -``` - - -### Parsing from string - -You can parse `Decimal` objects from strings: - -```julia -julia> x = "0.2" -"0.2" - -julia> parse(Decimal, x) -0.2 - -julia> tryparse(Decimal, x) -0.2 -``` -Parsing support scientific notation. Alternatively, you can use the `@dec_str` -macro, which also supports the thousands separator `_`: -```julia -julia> dec"0.2" -0.2 - -julia> dec"1_000.000_001" -1000.000001 -``` - -### Conversion - -Any real number can be converted to a `Decimal`: -```julia -julia> Decimal(0.2) -0.2 - -julia> Decimal(-10) --10 -``` - -A `Decimal` can be converted to numeric types that can represent it: -```julia -julia> Float64(Decimal(0.2)) -0.2 - -julia> Int(Decimal(10)) -10 - -julia> Float64(Decimal(0, 1, 512)) -ERROR: ArgumentError: cannot parse "100[...]" as Float64 - -julia> Int(Decimal(0.4)) -ERROR: ArgumentError: invalid base 10 digit '.' in "0.4" -``` - -### String representation - -A string in the decimal form of a `Decimal` can be obtained via -`string(::Decimal)`: -```julia -julia> string(Decimal(0.2)) -"0.2" -``` - -The 2- and 3-args methods for `show` are implemented: -```julia -julia> repr(Decimal(1000000.0)) -"Decimal(0, 10, 5)" - -julia> repr("text/plain", Decimal(1000000.0)) -"1.0E+6" -``` - -### Operations -```julia -julia> x, y = decimal("0.2"), decimal("0.1"); -``` -#### Addition -```julia -julia> string(x + y) -"0.3" -``` - -#### Subtraction -```julia -julia> string(x - y) -"0.1" -``` - -#### Negation -```julia -julia> string(-x) -"-0.2" -``` -#### Multiplication -```julia -julia> string(x * y) -"0.02" -``` - -#### Division -```julia -julia> string(x / y) -"2" -``` - -#### Inversion -```julia -julia> string(inv(x)) -"5" -``` - -#### Broadcasting -```julia -julia> [x y] .* 2 -2-element Array{Decimal,1}: - Decimal(0,1,-1) - Decimal(0,5,-2) -``` -#### Equals (`==` and `isequal`) -```julia -julia> x == decimal("0.2") -true - -julia> x != decimal("0.1") -true -``` - -#### Inequality -```julia -julia> x >= y -true - -julia> isless(x, y) -false -``` - -#### `==` returns true for Decimal vs. Number comparisons -```julia -julia> x == 0.2 -true -``` - -#### Rounding -```julia -julia> round(decimal(3.1415), digits=2) -Decimal(0,314,-2) - -julia> string(ans) -"3.14" -``` ## Comparison with other packages @@ -194,21 +22,6 @@ Unlike another Julia package called [`DecFP`](https://github.com/JuliaMath/DecFP The closest equivalent (and inspiration) for the present package in Python is the standard built-in [`decimal`](https://docs.python.org/3.7/library/decimal.html) package, which is based on [General Decimal Arithmetic Specification by IBM](http://speleotrove.com/decimal/decarith.html). Since version 3.3 of Python, it is actually [`libmpdec`](http://www.bytereef.org/mpdecimal/index.html)/[`cdecimal`](https://www.bytereef.org/mpdecimal/doc/cdecimal/index.html) that is under the hood. -## Development - -### Standard tests - -There is a standard test suite called -[DecTests](https://speleotrove.com/decimal/dectest.html). The test suite is -provided in a [custom format](https://speleotrove.com/decimal/dtfile.html). We -have a script `scripts/dectest.jl` for translating test cases from the custom -format to common Julia tests. The script should be called like this: -``` -julia scripts/dectest.jl -``` -For example: -`julia scripts/dectest.jl Plus dectests/plus.decTest test/dectests/test_plus.jl`. -We put these test files into the `test/dectests` subdirectory. ## Further reading diff --git a/docs/.gitignore b/docs/.gitignore new file mode 100644 index 0000000..a5eabd5 --- /dev/null +++ b/docs/.gitignore @@ -0,0 +1,2 @@ +build +Manifest.toml diff --git a/docs/Project.toml b/docs/Project.toml new file mode 100644 index 0000000..bfd80fa --- /dev/null +++ b/docs/Project.toml @@ -0,0 +1,3 @@ +[deps] +Decimals = "abce61dc-4473-55a0-ba07-351d65e31d42" +Documenter = "e30172f5-a6a5-5a46-863b-614d45cd2de4" diff --git a/docs/make.jl b/docs/make.jl new file mode 100644 index 0000000..361367d --- /dev/null +++ b/docs/make.jl @@ -0,0 +1,8 @@ +using Decimals +using Documenter + +makedocs(sitename="Decimals.jl", + pages=["index.md", + "operations.md"]) +deploydocs(repo="github.com/JuliaMath/Decimals.jl.git", + push_preview=true) diff --git a/docs/src/contrib.md b/docs/src/contrib.md new file mode 100644 index 0000000..986011d --- /dev/null +++ b/docs/src/contrib.md @@ -0,0 +1,15 @@ +## Development + +### Standard tests + +There is a standard test suite called +[DecTests](https://speleotrove.com/decimal/dectest.html). The test suite is +provided in a [custom format](https://speleotrove.com/decimal/dtfile.html). We +have a script `scripts/dectest.jl` for translating test cases from the custom +format to common Julia tests. The script should be called like this: +``` +julia scripts/dectest.jl +``` +For example: +`julia scripts/dectest.jl Plus dectests/plus.decTest test/dectests/test_plus.jl`. +We put these test files into the `test/dectests` subdirectory. diff --git a/docs/src/index.md b/docs/src/index.md new file mode 100644 index 0000000..137f65c --- /dev/null +++ b/docs/src/index.md @@ -0,0 +1,72 @@ +```@meta +DocTestSetup = quote +using Decimals +end +``` + +# Introduction + +This package is a Julia implementation of arbitrary precision decimal floating +point arithmetic, where numbers $x \in \mathbb{R}$ are represented using the +`Decimal` type as +```math +x = (-1)^s \cdot c \cdot 10^q, +``` +where $s \in \{0, 1\}$ is the signbit, $c \in \mathbb{N}$ is the coefficient, and $q \in \mathbb{Z}$ is the exponent. + + +## Construction + +The `Decimal` object can be created directly, or by conversion from a number, +or by parsing from a string: + +```jldoctest +julia> Decimal(0, 5, -1) # (-1)^0 * 5 * 10^-1 +0.5 + +julia> Decimal(0.5) +0.5 + +julia> Decimal(5E-1) +0.5 + +julia> parse(Decimal, "0.5") +0.5 + +julia> parse(Decimal, "5E-1") +0.5 +``` + +Alternatively, a `Decimal` can be parsed from a string literal via the +`@dec_str` macro, which also supports the thousands separator `_`: +```jldoctest +julia> dec"1_000.000_000_1" +1000.0000001 +``` +The thousands separator is not supported by the `parse` function: +```jldoctest +julia> parse(Decimal, "1_000") +ERROR: ArgumentError: Invalid decimal: 1_000 +``` + +!!! warning "Conversion from numbers is exact" + The constructor `Decimal(::Real)` converts the given number to a `Decimal` + exactly. The consequence is that `Decimal(x)` is not generally equal to + `parse(Decimal, string(x))`. For example, + ```jldoctest + julia> Decimal(0.1) + 0.1000000000000000055511151231257827021181583404541015625 + + julia> parse(Decimal, "0.1") + 0.1 + + julia> dec"0.1" # Alternative to parse + 0.1 + + julia> big(0.1) # Exact value of 0.1 represented by a binary floating-point number + 0.1000000000000000055511151231257827021181583404541015625 + + julia> string(0.1) # The string representation of 0.1 is deceiving + "0.1" + ``` + diff --git a/docs/src/operations.md b/docs/src/operations.md new file mode 100644 index 0000000..8f32ef8 --- /dev/null +++ b/docs/src/operations.md @@ -0,0 +1,247 @@ +```@meta +DocTestSetup = quote +using Decimals +using Decimals: @with_context +end +``` + +# Operations + +Users can parametrize the decimal arithmetic by setting +[`Decimals.Context`](@ref) using the [`Decimals.with_context`](@ref) function +or the [`Decimals.@with_context`](@ref) macro. Operations affected by context +are denoted by the badge ![Affected by +context](https://img.shields.io/badge/ctxt-affected-blue). + + +```@docs +Decimals.Context +Decimals.with_context +Decimals.@with_context +``` + +## Arithmetic operations + +### Unary plus, minus + +![Affected by context](https://img.shields.io/badge/ctxt-affected-blue) + +The unary plus, `+x`, and unary minus, `-x`, is equivalent to `0 + x` and +`0 - x`, respectively. + +```jldoctest +julia> x = dec"1.1" +1.1 + +julia> +x +1.1 + +julia> -x +-1.1 +``` + +### Addition, subtraction + +![Affected by context](https://img.shields.io/badge/ctxt-affected-blue) + +Addition and subtraction are implemented via the binary operators `+` and `y`. + +```jldoctest +julia> x = dec"1.1" +1.1 + +julia> y = dec"2.2" +2.2 + +julia> x + y +3.3 + +julia> x - y +-1.1 +``` + +### Multiplication, division + +![Affected by context](https://img.shields.io/badge/ctxt-affected-blue) + +Multiplication and division are implemented via the binary operators `*` and +`/`. + +Division by zero throws [`DivisionByZeroError`](@ref) unless the dividend is also zero, in which case [`UndefinedDivisionError`](@ref) is thrown. + +```jldoctest +julia> x = dec"2" +2 + +julia> y = dec"3" +3 + +julia> x * y +6 + +julia> x / y +0.6666666666666666666666666667 + +julia> x / dec"0" +ERROR: DivisionByZeroError() + +julia> dec"0" / dec"0" +ERROR: UndefinedDivisionError() +``` + +### Absolute value + +![Affected by context](https://img.shields.io/badge/ctxt-affected-blue) + +Absolute value is implemented via the `Base.abs` function. + +```jldoctest +julia> abs(dec"1") +1 + +julia> abs(dec"-1") +1 +``` + +### Minimum, maximum + +![Affected by context](https://img.shields.io/badge/ctxt-affected-blue) + +Selecting the minimum and maximum of two `Decimal`s is implemented via the +`Base.min` and `Base.max` functions. + +```jldoctest +julia> x = dec"0.123" +0.123 + +julia> y = dec"4.567" +4.567 + +julia> min(x, y) +0.123 + +julia> max(x, y) +4.567 +``` + +## Comparison operations + +The basic comparison operation is implemented via `Base.cmp`, which compares +`x` and `y` and returns `-1`, `0`, or `+1` if `x` is less than, equal to, or +greater than `y`. + +```jldoctest +julia> x = dec"0.123" +0.123 + +julia> y = dec"4.567" +4.567 + +julia> cmp(x, y) +-1 + +julia> cmp(y, x) +1 +``` + +The binary operators `==`, `<`, and `<=` are also implemented. + +```jldoctest +julia> x = dec"0.123" +0.123 + +julia> y = dec"4.567" +4.567 + +julia> x == y +false + +julia> x < y +true + +julia> x > y +false + +julia> x <= y +true + +julia> x >= y +false +``` + +## Miscellaneous + +### Hashing + +Hashing of `Decimal`s is supported via the `Base.hash` function. It holds for +the output hash code that `x == y` implies `hash(x) == hash(y)`, and this is +true even if one of the operands is not a `Decimal`. + +```jldoctest +julia> x = Decimal(0, 2125, -3) +2.125 + +julia> y = Decimal(0, 212500, -5) +2.12500 + +julia> hash(x) == hash(y) +true + +julia> hash(x) == hash(2.125) +true +``` + +### Rounding + +The standard interface for rounding floating-point numbers is supported: +```jldoctest +julia> x = dec"123.45678" +123.45678 + +julia> round(x) # Round to the nearest integer +123 + +julia> round(Int, x) # Round to the nearest integer and convert to Int +123 + +julia> round(x, RoundUp) # Round using a particular rounding mode +124 + +julia> round(x, digits=4) # Round to four decimal places +123.4568 + +julia> round(x, sigdigits=4) # Round to four significant digits +123.5 +``` + +Our test suite covers six rounding modes that are specified by the Decimal +arithmetic specification. The rounding modes are + + - `RoundNearest` (*round-half-even* in the specification), + - `RoundNearestTiesAway` (*round-half-up*), + - `RoundUp` (*round-ceiling*), + - `RoundDown` (*round-floor*), + - `RoundFromZero` (*round-up*), + - `RoundToZero` (*round-down*). + +The default mode is `RoundNearest`. + +## Normalization + +![Affected by context](https://img.shields.io/badge/ctxt-affected-blue) + +Normalization of a `Decimal` removes all trailing zeros of the coefficient $c$ +while adjusting the exponent $q$ so that the `Decimal` remains the same (up to +the precision given by context). The operation is semantically equivalent to +unary plus. + +```@docs +normalize +``` + +## Exception types + +```@docs +DivisionByZeroError +UndefinedDivisionError +``` diff --git a/src/context.jl b/src/context.jl index 825fcbf..3e9ab2d 100644 --- a/src/context.jl +++ b/src/context.jl @@ -1,5 +1,29 @@ using ScopedValues +""" + Context + +User-selectable parametrization of the decimal arithmetics. The parameters are: + +| Parameter | Description | +|:-------------------------|:------------------------------------------------------------------| +| `precision::Int` | Maximum number of significand digits (default: `28`) | +| `rounding::RoundingMode` | Rounding mode to be used when necessary (default: `RoundNearest`) | +| `Emax::Int` | Maximum adjusted exponent (default: `999999`) | +| `Emin::Int` | Minimum adjusted exponent (default: `-999999`) | + +The parameters can be set via the (unexported) function [`with_context`](@ref) +or macro [`@with_context`](@ref): +```jldoctest +julia> Decimals.with_context(precision=1) do + +dec"0.1234" + end +0.1 + +julia> Decimals.@with_context (precision=1,) +dec"0.1234" +0.1 +``` +""" Base.@kwdef struct Context precision::Int=28 rounding::RoundingMode=RoundNearest diff --git a/src/decimal.jl b/src/decimal.jl index 4387903..3778b4b 100644 --- a/src/decimal.jl +++ b/src/decimal.jl @@ -15,11 +15,20 @@ the coefficient removed. # Examples ```jldoctest -julia> normalize(dec"1.2000") +julia> x = dec"1.2000" +1.2000 + +julia> x.c # Coefficient of `x` +12000 + +julia> y = normalize(dec"1.2000") 1.2 -julia> normalize(dec"-10000") --1E+4 +julia> y.c # Coefficient of `y` +12 + +julia> x == y +true ``` """ function normalize(x::Decimal)