Skip to content

Commit

Permalink
Normalization and rounding
Browse files Browse the repository at this point in the history
The PR replaces `round` and `normalize`.

Fixes #27
Fixes #36
Fixes #39
Fixes #50
  • Loading branch information
barucden committed Nov 6, 2024
1 parent 0d52f4d commit dbb051a
Show file tree
Hide file tree
Showing 9 changed files with 141 additions and 118 deletions.
15 changes: 1 addition & 14 deletions src/Decimals.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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
47 changes: 47 additions & 0 deletions src/conversion.jl
Original file line number Diff line number Diff line change
@@ -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

67 changes: 20 additions & 47 deletions src/decimal.jl
Original file line number Diff line number Diff line change
@@ -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)
Expand All @@ -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
16 changes: 0 additions & 16 deletions src/norm.jl

This file was deleted.

43 changes: 25 additions & 18 deletions src/round.jl
Original file line number Diff line number Diff line change
@@ -1,22 +1,29 @@
# 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 Base.round(x::Decimal, r::RoundingMode=RoundNearest;
digits::Union{Integer,Nothing}=nothing,
sigdigits::Union{Integer,Nothing}=nothing)
if !isnothing(digits) && !isnothing(sigdigits)
throw(ArgumentError("`round` cannot use both `digits` and `sigdigits` arguments"))
end

if isnothing(digits) && isnothing(sigdigits)
# If neither `digits` nor `sigdigits` was specified, remove all decimal
# digits
digits = 0
elseif isnothing(digits)
# If only `sigdigits` was specified, remove all digits besides
# `sigdigits` most significant
digits = -(ndigits(x.c) + x.q - sigdigits)
end
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)
@assert !isnothing(digits)

# `-x.q` is the number of digits after the decimal place
if digits -x.q
return x
end

trun_len = -(x.q + digits)
c = div(x.c, BigTen ^ trun_len, r)
return Decimal(x.s, c, x.q + trun_len)
end

1 change: 0 additions & 1 deletion test/runtests.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
6 changes: 6 additions & 0 deletions test/test_decimal.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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
15 changes: 0 additions & 15 deletions test/test_norm.jl

This file was deleted.

49 changes: 42 additions & 7 deletions test/test_round.jl
Original file line number Diff line number Diff line change
@@ -1,13 +1,48 @@
using Decimals
using Test

@testset "Rounding" begin
@testset "Round" begin
@test round(dec"1234.56789", digits=-5) == dec"0000.00000"
@test round(dec"1234.56789", digits=-4) == dec"0000.00000"
@test round(dec"1234.56789", digits=-3) == dec"1000.00000"
@test round(dec"1234.56789", digits=-2) == dec"1200.00000"
@test round(dec"1234.56789", digits=-1) == dec"1230.00000"
@test round(dec"1234.56789", digits=0) == dec"1235.00000"
@test round(dec"1234.56789", digits=1) == dec"1234.60000"
@test round(dec"1234.56789", digits=2) == dec"1234.57000"
@test round(dec"1234.56789", digits=3) == dec"1234.56800"
@test round(dec"1234.56789", digits=4) == dec"1234.56790"
@test round(dec"1234.56789", digits=5) == dec"1234.56789"
@test round(dec"1234.56789", digits=6) == dec"1234.56789"

@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"1234.56789", sigdigits=0) == dec"0000.00000"
@test round(dec"1234.56789", sigdigits=1) == dec"1000.00000"
@test round(dec"1234.56789", sigdigits=2) == dec"1200.00000"
@test round(dec"1234.56789", sigdigits=3) == dec"1230.00000"
@test round(dec"1234.56789", sigdigits=4) == dec"1235.00000"
@test round(dec"1234.56789", sigdigits=5) == dec"1234.60000"
@test round(dec"1234.56789", sigdigits=6) == dec"1234.57000"
@test round(dec"1234.56789", sigdigits=7) == dec"1234.56800"
@test round(dec"1234.56789", sigdigits=8) == dec"1234.56790"
@test round(dec"1234.56789", sigdigits=9) == dec"1234.56789"
@test round(dec"1234.56789", sigdigits=10) == dec"1234.56789"

@test round(dec"1234.56789", RoundUp, digits=-5) == dec"100000.00000"
@test round(dec"1234.56789", RoundUp, digits=-4) == dec"10000.00000"
@test round(dec"1234.56789", RoundUp, digits=-3) == dec"2000.00000"
@test round(dec"1234.56789", RoundUp, digits=-2) == dec"1300.00000"
@test round(dec"1234.56789", RoundUp, digits=-1) == dec"1240.00000"
@test round(dec"1234.56789", RoundUp, digits=0) == dec"1235.00000"
@test round(dec"1234.56789", RoundUp, digits=1) == dec"1234.60000"
@test round(dec"1234.56789", RoundUp, digits=2) == dec"1234.57000"
@test round(dec"1234.56789", RoundUp, digits=3) == dec"1234.56800"
@test round(dec"1234.56789", RoundUp, digits=4) == dec"1234.56790"
@test round(dec"1234.56789", RoundUp, digits=5) == dec"1234.56789"
@test round(dec"1234.56789", RoundUp, digits=6) == dec"1234.56789"

@test round(Int, dec"1234.56789") === Int(1235)
@test round(Int, dec"1234.56789", RoundUp) === Int(1235)
@test round(Int, dec"1234.56789", RoundDown) === Int(1233)

# TODO: test all rounding modes
end

0 comments on commit dbb051a

Please sign in to comment.