From db600fe285078ba7bb3716e62d2d3de242b8c934 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Denis=20Baru=C4=8Di=C4=87?= Date: Wed, 16 Oct 2024 21:19:10 +0200 Subject: [PATCH] Initial hashing support --- src/Decimals.jl | 4 ++++ src/bigint.jl | 55 +++++++++++++++++++++++++++++++++++++++++++++ src/decimal.jl | 1 - src/equals.jl | 2 -- src/hash.jl | 16 +++++++++++++ test/runtests.jl | 6 +++-- test/test_bigint.jl | 44 ++++++++++++++++++++++++++++++++++++ test/test_hash.jl | 21 +++++++++++++++++ 8 files changed, 144 insertions(+), 5 deletions(-) create mode 100644 src/bigint.jl create mode 100644 src/hash.jl create mode 100644 test/test_bigint.jl create mode 100644 test/test_hash.jl diff --git a/src/Decimals.jl b/src/Decimals.jl index 04cb620..ad02568 100644 --- a/src/Decimals.jl +++ b/src/Decimals.jl @@ -20,6 +20,8 @@ struct Decimal <: AbstractFloat Decimal(s::Integer, c::Integer, e::Integer) = new(Bool(s), c, e) end +include("bigint.jl") + # Convert between Decimal objects, numbers, and strings include("decimal.jl") @@ -35,4 +37,6 @@ include("equals.jl") # Rounding include("round.jl") +include("hash.jl") + end diff --git a/src/bigint.jl b/src/bigint.jl new file mode 100644 index 0000000..08d8827 --- /dev/null +++ b/src/bigint.jl @@ -0,0 +1,55 @@ +@static if VERSION < v"1.10-" + const libgmp = :libgmp +else + const libgmp = Base.GMP.libgmp +end + +function isdivisible(x::BigInt, n::Int) + r = ccall((:__gmpz_divisible_ui_p, libgmp), Cint, + (Base.GMP.MPZ.mpz_t, Culong), x, n) + return r != 0 +end + +function exactdiv(x::BigInt, n::Int) + y = BigInt() + ccall((:__gmpz_divexact_ui, libgmp), Cvoid, + (Base.GMP.MPZ.mpz_t, Base.GMP.MPZ.mpz_t, Culong), y, x, n) + return y +end + +""" + maxexp(n) + +Return maximum exponent E such that n^E is representable both as Int and +Culong (i.e., n^(E+1) would overflow). +""" +function maxexp(n::Int) + maxval = min(typemax(Culong), typemax(Int)) + return ceil(Int, log(n, maxval)) - 1 +end + +""" + cancelfactor(x::BigInt, ::Val{N}) + +Remove all occurrences of the factor `N` from `x`. The result is pair `(y, E)` +such that `x = y * N^E`. +""" +function cancelfactor(x::BigInt, ::Val{N}) where {N} + if iszero(x) + return x, 0 + end + + q = 0 + while isdivisible(x, N) + d = N + q += 1 + for e in 2:maxexp(N) + isdivisible(x, d * N) || break + d *= N + q += 1 + end + x = exactdiv(x, d) + end + + return x, q +end diff --git a/src/decimal.jl b/src/decimal.jl index 4ff71b0..1f0029f 100644 --- a/src/decimal.jl +++ b/src/decimal.jl @@ -82,4 +82,3 @@ end # sign Base.signbit(x::Decimal) = x.s - diff --git a/src/equals.jl b/src/equals.jl index e1b5367..3ecab68 100644 --- a/src/equals.jl +++ b/src/equals.jl @@ -15,8 +15,6 @@ function Base.:(==)(x::Decimal, y::Decimal) a.c == b.c && a.q == b.q && a.s == b.s end -Base.iszero(x::Decimal) = iszero(x.c) - function Base.:(<)(x::Decimal, y::Decimal) # return early on zero if iszero(x) && iszero(y) diff --git a/src/hash.jl b/src/hash.jl new file mode 100644 index 0000000..698a99e --- /dev/null +++ b/src/hash.jl @@ -0,0 +1,16 @@ +function Base.decompose(x::Decimal) + if iszero(x) + return (big(0), 0, big((-1)^x.s)) + end + + coef = (-1)^x.s * x.c + + if x.q ≥ 0 + return (coef * big(5)^x.q, x.q, big(1)) + else + coef, exp = cancelfactor(coef, Val(5)) + q = -x.q - exp + return (coef, x.q, big(5) ^ q) + end +end + diff --git a/test/runtests.jl b/test/runtests.jl index 6da70af..d15616c 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -12,11 +12,13 @@ global d = [ Decimal(true, 4, -6) ] +include("test_arithmetic.jl") +include("test_bigint.jl") include("test_constructor.jl") include("test_decimal.jl") -include("test_norm.jl") -include("test_arithmetic.jl") include("test_equals.jl") +include("test_hash.jl") +include("test_norm.jl") include("test_round.jl") end diff --git a/test/test_bigint.jl b/test/test_bigint.jl new file mode 100644 index 0000000..2b8f24c --- /dev/null +++ b/test/test_bigint.jl @@ -0,0 +1,44 @@ +@testset "BigInt" begin + @testset "isdivisible" begin + @test Decimals.isdivisible(big(12), 1) + @test Decimals.isdivisible(big(12), 2) + @test Decimals.isdivisible(big(12), 3) + @test Decimals.isdivisible(big(12), 4) + @test Decimals.isdivisible(big(12), 6) + + prime = big(1709) + @test Decimals.isdivisible(prime, 1) + @test Decimals.isdivisible(prime, Int(prime)) + @test !any(n -> Decimals.isdivisible(prime, n), 2:1708) + end + + @testset "exactdiv" begin + @test Decimals.exactdiv(big(12), 2) == big(6) + @test Decimals.exactdiv(big(12), 3) == big(4) + @test Decimals.exactdiv(big(12), 4) == big(3) + @test Decimals.exactdiv(big(12), 6) == big(2) + end + + @testset "maxexp" begin + T = ifelse(typemax(Culong) > typemax(Int), Int, Culong) + + E = Decimals.maxexp(2) + @test T(2)^E == big(2)^E + @test T(2)^(E + 1) != big(2)^(E+1) + + E = Decimals.maxexp(5) + @test T(5)^E == big(5)^E + @test T(5)^(E + 1) != big(5)^(E+1) + + E = Decimals.maxexp(10) + @test T(10)^E == big(10)^E + @test T(10)^(E + 1) != big(10)^(E+1) + end + + @testset "cancelfactor" begin + @test Decimals.cancelfactor(big(0), Val(3)) == (big(0), 0) + @test Decimals.cancelfactor(big(3)^8, Val(3)) == (big(1), 8) + @test Decimals.cancelfactor(big(3)^128, Val(3)) == (big(1), 128) + @test Decimals.cancelfactor(big(948659)^8, Val(948659)) == (big(1), 8) + end +end diff --git a/test/test_hash.jl b/test/test_hash.jl new file mode 100644 index 0000000..4c65f5d --- /dev/null +++ b/test/test_hash.jl @@ -0,0 +1,21 @@ +using Decimals +using Test + +@testset "Hashing" begin + @testset "hash" begin + @test hash(Decimal(0.)) == hash(0.) + @test hash(Decimal(-0.)) == hash(-0.) + @test hash(Decimal(3)) == hash(3) + @test hash(Decimal(0.09375)) == hash(0.09375) + @test hash(Decimal(-3)) == hash(-3) + @test hash(Decimal(-0.09375)) == hash(-0.09375) + + # Equality implies same hash + @test hash(Decimal(0, 100, 0)) == hash(Decimal(0, 10, 1)) + @test hash(Decimal(0, 100, 0)) == hash(Decimal(0, 1, 2)) + @test hash(Decimal(1, 100, 0)) == hash(Decimal(1, 10, 1)) + @test hash(Decimal(1, 100, 0)) == hash(Decimal(1, 1, 2)) + + @test hash(Decimal(0, 310, -2)) == hash(Decimal(0, 31, -1)) + end +end