From fa1e39eb24b3fb44281cda837076d4d6aa5bc36c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Denis=20Baru=C4=8Di=C4=87?= Date: Wed, 6 Nov 2024 22:46:11 +0100 Subject: [PATCH] Normalization and rounding The PR replaces `round` and `normalize`. Fixes #27 Fixes #36 Fixes #39 Fixes #50 --- src/Decimals.jl | 15 +--- src/conversion.jl | 47 +++++++++++ src/decimal.jl | 67 +++++----------- src/norm.jl | 16 ---- src/round.jl | 58 ++++++++++---- test/runtests.jl | 1 - test/test_decimal.jl | 6 ++ test/test_norm.jl | 15 ---- test/test_round.jl | 182 +++++++++++++++++++++++++++++++++++++++++-- 9 files changed, 290 insertions(+), 117 deletions(-) create mode 100644 src/conversion.jl delete mode 100644 src/norm.jl delete mode 100644 test/test_norm.jl diff --git a/src/Decimals.jl b/src/Decimals.jl index bc70dce..ea481a1 100644 --- a/src/Decimals.jl +++ b/src/Decimals.jl @@ -24,26 +24,13 @@ end include("bigint.jl") include("context.jl") - -# Convert between Decimal objects, numbers, and strings +include("conversion.jl") include("decimal.jl") - -# Decimal normalization -include("norm.jl") - -# Addition, subtraction, negation, multiplication include("arithmetic.jl") - -# Equality include("equals.jl") - -# Rounding include("round.jl") - include("hash.jl") - include("parse.jl") - include("show.jl") end diff --git a/src/conversion.jl b/src/conversion.jl new file mode 100644 index 0000000..3ba1307 --- /dev/null +++ b/src/conversion.jl @@ -0,0 +1,47 @@ +Decimal(x::Decimal) = x +Decimal(n::Integer) = Decimal(signbit(n), abs(n), 0) +function Decimal(x::AbstractFloat) + if !isfinite(x) + throw(ArgumentError("$x cannot be represented as a Decimal")) + end + + # Express `x` as a rational `u = n / 2^k`, where `k ≥ 0` + u = Rational(abs(x)) + + # u.den = 2^k + k = ndigits(u.den, base=2) - 1 + + # We can write + # + # x = n / 2^k + # = n / 2^k * 10^k * 10^-k + # = (n * 10^k / 2^k) * 10^-k + # = (n * 5^k) * 10^-k + + s = signbit(x) + c = u.num * BigInt(5)^k + q = -k + return Decimal(s, c, q) +end + +Base.convert(::Type{Decimal}, x::Real) = Decimal(x) + +function Base.BigFloat(x::Decimal) + y = BigFloat(x.c) + if x.q ≥ 0 + y *= BigTen^x.q + else + y /= BigTen^(-x.q) + end + return x.s ? -y : y +end + +(::Type{T})(x::Decimal) where {T<:Number} = T(BigFloat(x)) + +# String representation of Decimal +function Base.string(x::Decimal) + io = IOBuffer() + scientific_notation(io, x) + return String(take!(io)) +end + diff --git a/src/decimal.jl b/src/decimal.jl index d7071ef..4387903 100644 --- a/src/decimal.jl +++ b/src/decimal.jl @@ -1,50 +1,3 @@ -Decimal(x::Decimal) = x -Decimal(n::Integer) = Decimal(signbit(n), abs(n), 0) -function Decimal(x::AbstractFloat) - if !isfinite(x) - throw(ArgumentError("$x cannot be represented as a Decimal")) - end - - # Express `x` as a rational `u = n / 2^k`, where `k ≥ 0` - u = Rational(abs(x)) - - # u.den = 2^k - k = ndigits(u.den, base=2) - 1 - - # We can write - # - # x = n / 2^k - # = n / 2^k * 10^k * 10^-k - # = (n * 10^k / 2^k) * 10^-k - # = (n * 5^k) * 10^-k - - s = signbit(x) - c = u.num * BigInt(5)^k - q = -k - return Decimal(s, c, q) -end - -Base.convert(::Type{Decimal}, x::Real) = Decimal(x) - -function Base.BigFloat(x::Decimal) - y = BigFloat(x.c) - if x.q ≥ 0 - y *= BigTen^x.q - else - y /= BigTen^(-x.q) - end - return x.s ? -y : y -end - -(::Type{T})(x::Decimal) where {T<:Number} = T(BigFloat(x)) - -# String representation of Decimal -function Base.string(x::Decimal) - io = IOBuffer() - scientific_notation(io, x) - return String(take!(io)) -end - Base.signbit(x::Decimal) = x.s Base.zero(::Type{Decimal}) = Decimal(false, 0, 0) @@ -53,3 +6,23 @@ Base.one(::Type{Decimal}) = Decimal(false, 1, 0) Base.iszero(x::Decimal) = iszero(x.c) Base.isfinite(x::Decimal) = true Base.isnan(x::Decimal) = false + +""" + normalize(x::Decimal) + +Return an equal number reduced to its simplest form with all trailing zeros in +the coefficient removed. + +# Examples +```jldoctest +julia> normalize(dec"1.2000") +1.2 + +julia> normalize(dec"-10000") +-1E+4 +``` +""" +function normalize(x::Decimal) + c, e = cancelfactor(x.c, Val(10)) + return fix(Decimal(x.s, c, x.q + e)) +end diff --git a/src/norm.jl b/src/norm.jl deleted file mode 100644 index 17657ac..0000000 --- a/src/norm.jl +++ /dev/null @@ -1,16 +0,0 @@ -# Normalization: remove trailing zeros in coefficient -function normalize(x::Decimal; rounded::Bool=false) - p = 0 - if x.c != 0 - while x.c % 10^(p+1) == 0 - p += 1 - end - end - c = BigInt(x.c / 10^p) - q = (c == 0 && x.s == 0) ? 0 : x.q + p - if rounded - Decimal(x.s, abs(c), q) - else - round(Decimal(x.s, abs(c), q), digits=DIGITS, normal=true) - end -end diff --git a/src/round.jl b/src/round.jl index e6b1614..2574079 100644 --- a/src/round.jl +++ b/src/round.jl @@ -1,22 +1,46 @@ -# Rounding -function Base.round(x::Decimal; digits::Int=0, normal::Bool=false) - shift = BigInt(digits) + x.q - if shift > BigInt(0) || shift < x.q - (normal) ? x : normalize(x, rounded=true) - else - c = Base.round(x.c / BigInt(10)^(-shift)) - d = Decimal(x.s, BigInt(c), x.q - shift) - (normal) ? d : normalize(d, rounded=true) +function _round(x::Decimal, r::RoundingMode, digits::Integer) + dec_places = -x.q + if digits ≥ dec_places + return x end + + d = dec_places - digits + c = div(x.c, BigTen ^ d, r) + return Decimal(x.s, c, x.q + d) +end + +function _round(x::Decimal, r::RoundingMode, + digits::Union{Nothing, Integer}, + sigdigits::Union{Nothing, Integer}) + if !isnothing(digits) && !isnothing(sigdigits) + throw(ArgumentError("`round` cannot use both `digits` and `sigdigits` arguments")) + end + + if isnothing(digits) && isnothing(sigdigits) + digits = 0 + elseif isnothing(digits) + digits = -(ndigits(x.c) + x.q - sigdigits) + end + + return _round(x, r, digits) end -function Base.trunc(x::Decimal; digits::Int=0, normal::Bool=false) - shift = BigInt(digits) + x.q - if shift > BigInt(0) || shift < x.q - (normal) ? x : normalize(x, rounded=true) - else - c = Base.trunc(x.c / BigInt(10)^(-shift)) - d = Decimal(x.s, BigInt(c), x.q - shift) - (normal) ? d : normalize(d, rounded=true) +for r in (RoundingMode{:Up}, + RoundingMode{:Down}, + RoundingMode{:FromZero}, + RoundingMode{:Nearest}, + RoundingMode{:NearestTiesAway}, + RoundingMode{:ToZero}) + @eval function Base.round(x::Decimal, r::$r; + digits::Union{Nothing, Integer}=nothing, + sigdigits::Union{Nothing, Integer}=nothing) + return _round(x, r, digits, sigdigits) + end + + if VERSION < v"1.9" + @eval function Base.round(::Type{T}, x::Decimal, r::$r) where T<:Integer + return T(_round(x, r, 0)) + end end end + diff --git a/test/runtests.jl b/test/runtests.jl index e8a1560..1be97b4 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -19,7 +19,6 @@ include("test_context.jl") include("test_decimal.jl") include("test_equals.jl") include("test_hash.jl") -include("test_norm.jl") include("test_parse.jl") include("test_round.jl") include("test_show.jl") diff --git a/test/test_decimal.jl b/test/test_decimal.jl index 2a3f751..4a919a2 100644 --- a/test/test_decimal.jl +++ b/test/test_decimal.jl @@ -46,3 +46,9 @@ end @test isfinite(Decimal(0, 1, 1)) @test !isnan(Decimal(0, 1, 1)) end + +@testset "Normalize" begin + x = normalize(dec"-15.11000") + @test (x.c % 10) ≠ 0 + @test x == dec"-15.11" +end diff --git a/test/test_norm.jl b/test/test_norm.jl deleted file mode 100644 index 2bf3413..0000000 --- a/test/test_norm.jl +++ /dev/null @@ -1,15 +0,0 @@ -using Decimals -using Test - -@testset "Normalization" begin - -@test Decimal(true, 151100, -4) == Decimal(true, 1511, -2) -@test Decimal(false, 100100, -5) == Decimal(false, 1001, -3) -@test normalize(Decimal(1, 151100, -4)) == Decimal(true, 1511, -2) -@test normalize(Decimal(0, 100100, -5)) == Decimal(false, 1001, -3) -@test parse(Decimal, "3.0") == Decimal(false, 3, 0) -@test parse(Decimal, "3.0") == Decimal(false, 30, -1) -@test parse(Decimal, "3.1400") == Decimal(false, 314, -2) -@test parse(Decimal, "1234") == Decimal(false, 1234, 0) - -end diff --git a/test/test_round.jl b/test/test_round.jl index 9a2af28..9860d92 100644 --- a/test/test_round.jl +++ b/test/test_round.jl @@ -1,13 +1,181 @@ using Decimals using Test -@testset "Rounding" begin +@testset "Round" begin + @testset "RoundToZero" begin + @test round(dec"1928.37465", RoundToZero, digits=-5) == dec"0.0" + @test round(dec"1928.37465", RoundToZero, digits=-4) == dec"0.0" + @test round(dec"1928.37465", RoundToZero, digits=-3) == dec"1000.0" + @test round(dec"1928.37465", RoundToZero, digits=-2) == dec"1900.0" + @test round(dec"1928.37465", RoundToZero, digits=-1) == dec"1920.0" + @test round(dec"1928.37465", RoundToZero, digits=0) == dec"1928.0" + @test round(dec"1928.37465", RoundToZero, digits=1) == dec"1928.3" + @test round(dec"1928.37465", RoundToZero, digits=2) == dec"1928.37" + @test round(dec"1928.37465", RoundToZero, digits=3) == dec"1928.374" + @test round(dec"1928.37465", RoundToZero, digits=4) == dec"1928.3746" + @test round(dec"1928.37465", RoundToZero, digits=5) == dec"1928.37465" + @test round(dec"1928.37465", RoundToZero, digits=6) == dec"1928.37465" -@test round(Decimal(7.123456), digits=0) == dec"7" -@test round(Decimal(7.123456), digits=2) == dec"7.12" -@test round(Decimal(7.123456), digits=3) == dec"7.123" -@test round(Decimal(7.123456), digits=5) == dec"7.12346" -@test round(Decimal(7.123456), digits=6) == dec"7.123456" -@test trunc(Decimal(7.123456), digits=5) == dec"7.12345" + @test round(dec"1928.37465", RoundToZero, digits=-5) == dec"0.0" + @test round(dec"1928.37465", RoundToZero, digits=-4) == dec"0.0" + @test round(dec"1928.37465", RoundToZero, digits=-3) == dec"1000.0" + @test round(dec"1928.37465", RoundToZero, digits=-2) == dec"1900.0" + @test round(dec"1928.37465", RoundToZero, digits=-1) == dec"1920.0" + @test round(dec"1928.37465", RoundToZero, digits=0) == dec"1928.0" + @test round(dec"1928.37465", RoundToZero, digits=1) == dec"1928.3" + @test round(dec"1928.37465", RoundToZero, digits=2) == dec"1928.37" + @test round(dec"1928.37465", RoundToZero, digits=3) == dec"1928.374" + @test round(dec"1928.37465", RoundToZero, digits=4) == dec"1928.3746" + @test round(dec"1928.37465", RoundToZero, digits=5) == dec"1928.37465" + @test round(dec"1928.37465", RoundToZero, digits=6) == dec"1928.37465" + end + @testset "RoundNearestTiesAway" begin + @test round(dec"1928.37465", RoundNearestTiesAway, digits=-5) == dec"0.0" + @test round(dec"1928.37465", RoundNearestTiesAway, digits=-4) == dec"0.0" + @test round(dec"1928.37465", RoundNearestTiesAway, digits=-3) == dec"2000.0" + @test round(dec"1928.37465", RoundNearestTiesAway, digits=-2) == dec"1900.0" + @test round(dec"1928.37465", RoundNearestTiesAway, digits=-1) == dec"1930.0" + @test round(dec"1928.37465", RoundNearestTiesAway, digits=0) == dec"1928.0" + @test round(dec"1928.37465", RoundNearestTiesAway, digits=1) == dec"1928.4" + @test round(dec"1928.37465", RoundNearestTiesAway, digits=2) == dec"1928.37" + @test round(dec"1928.37465", RoundNearestTiesAway, digits=3) == dec"1928.375" + @test round(dec"1928.37465", RoundNearestTiesAway, digits=4) == dec"1928.3747" + @test round(dec"1928.37465", RoundNearestTiesAway, digits=5) == dec"1928.37465" + @test round(dec"1928.37465", RoundNearestTiesAway, digits=6) == dec"1928.37465" + + @test round(dec"1928.37465", RoundNearestTiesAway, digits=-5) == dec"0.0" + @test round(dec"1928.37465", RoundNearestTiesAway, digits=-4) == dec"0.0" + @test round(dec"1928.37465", RoundNearestTiesAway, digits=-3) == dec"2000.0" + @test round(dec"1928.37465", RoundNearestTiesAway, digits=-2) == dec"1900.0" + @test round(dec"1928.37465", RoundNearestTiesAway, digits=-1) == dec"1930.0" + @test round(dec"1928.37465", RoundNearestTiesAway, digits=0) == dec"1928.0" + @test round(dec"1928.37465", RoundNearestTiesAway, digits=1) == dec"1928.4" + @test round(dec"1928.37465", RoundNearestTiesAway, digits=2) == dec"1928.37" + @test round(dec"1928.37465", RoundNearestTiesAway, digits=3) == dec"1928.375" + @test round(dec"1928.37465", RoundNearestTiesAway, digits=4) == dec"1928.3747" + @test round(dec"1928.37465", RoundNearestTiesAway, digits=5) == dec"1928.37465" + @test round(dec"1928.37465", RoundNearestTiesAway, digits=6) == dec"1928.37465" + end + + @testset "RoundNearest" begin + @test round(dec"1928.37465", RoundNearest, digits=-5) == dec"0.0" + @test round(dec"1928.37465", RoundNearest, digits=-4) == dec"0.0" + @test round(dec"1928.37465", RoundNearest, digits=-3) == dec"2000.0" + @test round(dec"1928.37465", RoundNearest, digits=-2) == dec"1900.0" + @test round(dec"1928.37465", RoundNearest, digits=-1) == dec"1930.0" + @test round(dec"1928.37465", RoundNearest, digits=0) == dec"1928.0" + @test round(dec"1928.37465", RoundNearest, digits=1) == dec"1928.4" + @test round(dec"1928.37465", RoundNearest, digits=2) == dec"1928.37" + @test round(dec"1928.37465", RoundNearest, digits=3) == dec"1928.375" + @test round(dec"1928.37465", RoundNearest, digits=4) == dec"1928.3746" + @test round(dec"1928.37465", RoundNearest, digits=5) == dec"1928.37465" + @test round(dec"1928.37465", RoundNearest, digits=6) == dec"1928.37465" + + @test round(dec"1928.37465", RoundNearest, digits=-5) == dec"0.0" + @test round(dec"1928.37465", RoundNearest, digits=-4) == dec"0.0" + @test round(dec"1928.37465", RoundNearest, digits=-3) == dec"2000.0" + @test round(dec"1928.37465", RoundNearest, digits=-2) == dec"1900.0" + @test round(dec"1928.37465", RoundNearest, digits=-1) == dec"1930.0" + @test round(dec"1928.37465", RoundNearest, digits=0) == dec"1928.0" + @test round(dec"1928.37465", RoundNearest, digits=1) == dec"1928.4" + @test round(dec"1928.37465", RoundNearest, digits=2) == dec"1928.37" + @test round(dec"1928.37465", RoundNearest, digits=3) == dec"1928.375" + @test round(dec"1928.37465", RoundNearest, digits=4) == dec"1928.3746" + @test round(dec"1928.37465", RoundNearest, digits=5) == dec"1928.37465" + @test round(dec"1928.37465", RoundNearest, digits=6) == dec"1928.37465" + end + + @testset "RoundUp" begin + @test round(dec"1928.37465", RoundUp, digits=-5) == dec"100000.0" + @test round(dec"1928.37465", RoundUp, digits=-4) == dec"10000.0" + @test round(dec"1928.37465", RoundUp, digits=-3) == dec"2000.0" + @test round(dec"1928.37465", RoundUp, digits=-2) == dec"2000.0" + @test round(dec"1928.37465", RoundUp, digits=-1) == dec"1930.0" + @test round(dec"1928.37465", RoundUp, digits=0) == dec"1929.0" + @test round(dec"1928.37465", RoundUp, digits=1) == dec"1928.4" + @test round(dec"1928.37465", RoundUp, digits=2) == dec"1928.38" + @test round(dec"1928.37465", RoundUp, digits=3) == dec"1928.375" + @test round(dec"1928.37465", RoundUp, digits=4) == dec"1928.3747" + @test round(dec"1928.37465", RoundUp, digits=5) == dec"1928.37465" + @test round(dec"1928.37465", RoundUp, digits=6) == dec"1928.37465" + + @test round(dec"1928.37465", RoundUp, digits=-5) == dec"100000.0" + @test round(dec"1928.37465", RoundUp, digits=-4) == dec"10000.0" + @test round(dec"1928.37465", RoundUp, digits=-3) == dec"2000.0" + @test round(dec"1928.37465", RoundUp, digits=-2) == dec"2000.0" + @test round(dec"1928.37465", RoundUp, digits=-1) == dec"1930.0" + @test round(dec"1928.37465", RoundUp, digits=0) == dec"1929.0" + @test round(dec"1928.37465", RoundUp, digits=1) == dec"1928.4" + @test round(dec"1928.37465", RoundUp, digits=2) == dec"1928.38" + @test round(dec"1928.37465", RoundUp, digits=3) == dec"1928.375" + @test round(dec"1928.37465", RoundUp, digits=4) == dec"1928.3747" + @test round(dec"1928.37465", RoundUp, digits=5) == dec"1928.37465" + @test round(dec"1928.37465", RoundUp, digits=6) == dec"1928.37465" + end + + @testset "RoundDown" begin + @test round(dec"1928.37465", RoundDown, digits=-5) == dec"0.0" + @test round(dec"1928.37465", RoundDown, digits=-4) == dec"0.0" + @test round(dec"1928.37465", RoundDown, digits=-3) == dec"1000.0" + @test round(dec"1928.37465", RoundDown, digits=-2) == dec"1900.0" + @test round(dec"1928.37465", RoundDown, digits=-1) == dec"1920.0" + @test round(dec"1928.37465", RoundDown, digits=0) == dec"1928.0" + @test round(dec"1928.37465", RoundDown, digits=1) == dec"1928.3" + @test round(dec"1928.37465", RoundDown, digits=2) == dec"1928.37" + @test round(dec"1928.37465", RoundDown, digits=3) == dec"1928.374" + @test round(dec"1928.37465", RoundDown, digits=4) == dec"1928.3746" + @test round(dec"1928.37465", RoundDown, digits=5) == dec"1928.37465" + @test round(dec"1928.37465", RoundDown, digits=6) == dec"1928.37465" + + @test round(dec"1928.37465", RoundDown, digits=-5) == dec"0.0" + @test round(dec"1928.37465", RoundDown, digits=-4) == dec"0.0" + @test round(dec"1928.37465", RoundDown, digits=-3) == dec"1000.0" + @test round(dec"1928.37465", RoundDown, digits=-2) == dec"1900.0" + @test round(dec"1928.37465", RoundDown, digits=-1) == dec"1920.0" + @test round(dec"1928.37465", RoundDown, digits=0) == dec"1928.0" + @test round(dec"1928.37465", RoundDown, digits=1) == dec"1928.3" + @test round(dec"1928.37465", RoundDown, digits=2) == dec"1928.37" + @test round(dec"1928.37465", RoundDown, digits=3) == dec"1928.374" + @test round(dec"1928.37465", RoundDown, digits=4) == dec"1928.3746" + @test round(dec"1928.37465", RoundDown, digits=5) == dec"1928.37465" + @test round(dec"1928.37465", RoundDown, digits=6) == dec"1928.37465" + end + + @testset "RoundFromZero" begin + @test round(dec"1928.37465", RoundFromZero, digits=-5) == dec"100000.0" + @test round(dec"1928.37465", RoundFromZero, digits=-4) == dec"10000.0" + @test round(dec"1928.37465", RoundFromZero, digits=-3) == dec"2000.0" + @test round(dec"1928.37465", RoundFromZero, digits=-2) == dec"2000.0" + @test round(dec"1928.37465", RoundFromZero, digits=-1) == dec"1930.0" + @test round(dec"1928.37465", RoundFromZero, digits=0) == dec"1929.0" + @test round(dec"1928.37465", RoundFromZero, digits=1) == dec"1928.4" + @test round(dec"1928.37465", RoundFromZero, digits=2) == dec"1928.38" + @test round(dec"1928.37465", RoundFromZero, digits=3) == dec"1928.375" + @test round(dec"1928.37465", RoundFromZero, digits=4) == dec"1928.3747" + @test round(dec"1928.37465", RoundFromZero, digits=5) == dec"1928.37465" + @test round(dec"1928.37465", RoundFromZero, digits=6) == dec"1928.37465" + + @test round(dec"1928.37465", RoundFromZero, digits=-5) == dec"100000.0" + @test round(dec"1928.37465", RoundFromZero, digits=-4) == dec"10000.0" + @test round(dec"1928.37465", RoundFromZero, digits=-3) == dec"2000.0" + @test round(dec"1928.37465", RoundFromZero, digits=-2) == dec"2000.0" + @test round(dec"1928.37465", RoundFromZero, digits=-1) == dec"1930.0" + @test round(dec"1928.37465", RoundFromZero, digits=0) == dec"1929.0" + @test round(dec"1928.37465", RoundFromZero, digits=1) == dec"1928.4" + @test round(dec"1928.37465", RoundFromZero, digits=2) == dec"1928.38" + @test round(dec"1928.37465", RoundFromZero, digits=3) == dec"1928.375" + @test round(dec"1928.37465", RoundFromZero, digits=4) == dec"1928.3747" + @test round(dec"1928.37465", RoundFromZero, digits=5) == dec"1928.37465" + @test round(dec"1928.37465", RoundFromZero, digits=6) == dec"1928.37465" + end + + @testset "Round to Int" begin + @test round(Int, dec"1928.37465", RoundToZero) == dec"1928" + @test round(Int, dec"1928.37465", RoundNearestTiesAway) == dec"1928" + @test round(Int, dec"1928.37465", RoundNearest) == dec"1928" + @test round(Int, dec"1928.37465", RoundUp) == dec"1929" + @test round(Int, dec"1928.37465", RoundDown) == dec"1928" + @test round(Int, dec"1928.37465", RoundFromZero) == dec"1929" + end end