New version of Decimals.jl released!

I am pleased to announce that today, almost four years after the last version, a new version of Decimals.jl has been released!

Decimals.jl is a package for arbitrary-precision decimal floating point arithmetics written in Julia.

The new version, number 0.5.0, comes with a lot of changes, yet almost no new features. This release primarily fixes severe bugs in arithmetic operations, ensures compliance with the floating-point arithmetic standard, and generally stabilizes the package. The correctness of the implemented operations is now guarded by almost 5,000 verified test cases.

One of the major changes is that users can now set the precision, among other parameters, through so-called “context”. This feature is conveniently implemented via ScopedValues.

As of now, we are out of issues! Put the package under load and bring us new ones!

Links

23 Likes

This is great! Any plans to make it to 1.0 after a time for stabilization and testing?

1 Like

Congratulations on your successful implementation of Decimals.

I too have implementated my version of Decimal Floating Point called SimpleDFPs

It was base on something I learned in high school called “Scientific Notation”.

Which in my case is

-1^signbit * mantissa * 10^exponent

Represented by

struct DFP{N} <: AbstractFloat
    s::Int8
    expo::BigInt
    m::Array{Int8,1}
end

It allows me to do things like

julia> using SimpleDFPs
[ Info: Precompiling SimpleDFPs [top-level] 

julia> x = sqrt(DFP{80}(2))
DFP{80}(0, 0, [1, 4, 1, 4, 2, 1, 3, 5, 6, 2, 3, 7, 3, 0, 9, 5, 0, 4, 8, 8, 0, 1, 6, 8, 8, 7, 2, 4, 2, 0, 9, 6, 9, 8, 0, 7, 8, 5, 6, 9, 6, 7, 1, 8, 7, 5, 3, 7, 6, 9, 4, 8, 0, 7, 3, 1, 7, 6, 6, 7, 9, 7, 3, 7, 9, 9, 0, 7, 3, 2, 4, 7, 8, 4, 6, 2, 1, 0, 7, 0])

julia> DFP_toCommonString(x)
"1.4142135623730950488016887242096980785696718753769480731766797379907324784621070"

julia> DFP_setShowStyle()
Error! Invalid symbol or no argument given.
Controls how Base.show displays a DFP object
The valid values are
1.  :default  or  :canon         for canonical string output
2.  :normal   or  :string        for complete string output
3.  :short    or  :shortstring   for short string output
4.  :tiny     or  :tinystring    for tiny string output
5.  <num>                        for <num> digits string output

julia> DFP_setShowStyle(:string)
:string

julia> x
1.4142135623730950488016887242096980785696718753769480731766797379907324784621070

julia> yf = 1.0 - 1e-78
1.0

julia> zf = yf^200
1.0

julia> y = 1.0 - DFP{80}("1e-78")
0.99999999999999999999999999999999999999999999999999999999999999999999999999999900

julia> z=y^200
0.99999999999999999999999999999999999999999999999999999999999999999999999999980000

You can only add two DFP number together if they have the same precision level

julia> b = DFP{8}(1) + DFP{80}(2)
ERROR: StackOverflowError:
Stacktrace:
      [1] promote_result(::Type, ::Type, ::Type{DFP{8}}, ::Type{DFP{80}})
        @ Base ./promotion.jl:338
      [2] promote_type
        @ ./promotion.jl:318 [inlined]--- the above 2 lines are repeated 79982 more times ---

julia> b = DFP_setPrecision(  DFP{8}(1), 80  ) + DFP{80}(2)
3.0000000000000000000000000000000000000000000000000000000000000000000000000000000

And of course the famous 0.1 + 0.2

julia> 0.1 + 0.2
0.30000000000000004

julia> DFP{20}("0.1") + DFP{20}("0.2")
0.30000000000000000000

julia> d"0.1,20" + d"0.2,20"
0.30000000000000000000

And I am particularly proud of this “to infinity and beyond!”

julia> DFP{80}(123)^DFP{80}(456)^DFP{80}(789)
1.0000000000000000000000000000000000000000000000000000000000000000000000000000000E17514084084174459149096354922773857364961962420130964921802135680652567734798252513464722139170197727902974481668239344702993641815117735899674454129713578163422602680000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000

1 Like

I think so, yes.

Most of the API is the same to what we are used to with Base floats in Julia (e.g., hash(x), round(x, sigdigits=3), or x + y works for Decimals). I am not worried about the stability (as in “interface might still change”) of this code.

What I am concerned about is the Context type, which is new. People might dislike the with_context/@with_context way of setting it. The introduction of context has consequences in the arithmetic operations too, because it influences Overflow/Underflow behavior. Right now, the operations throw an exception in case overflow occurs, but underflow is silently ignored (we should probably make this behavior parametrized through Context too). I, personally, would like to hear what the users think of all this before releasing 1.0.

1 Like

Thank you! Although is is not mine. I have joined the project only recently. Many people contributed before me, and several people contributed to this release too.

Nice! Your representation is very similar to what Decimals.jl uses (we represent mantissa with a BigInt though). If you have gained any experience that you’d like to share, you are more than welcome to contribute to Decimals.jl too. For example, I’d be interested to hear what motivated you to make precision a parameter of the type, and what the consequences were.

Yeah, we can’t raise to a power yet :laughing:

2 Likes

“Yeah, we can’t raise to a power yet”

Raising DFP to a power is difficult, so I used log10 logarithm to do so


    # Integer Plus Fraction is used to perform calculations
    # with base 10 logarithmns
    struct IpfType
        int::BigInt
        d::DFP
    end


    @inline function takelog10ToIpfType(a::DFP)
        if a.s == 1
            throw(DomainError(-1,"Cannot take the log10 of a negative number in takelog10ToIpfType"))
        end
        prec = DFP_lengthMantissa(a)
        newprec = prec + GUARD_DIGITS
        numofbits = Int64(  ceil(3.322 * newprec)  )
        v = setprecision(numofbits) do
                log10(  parse(BigFloat,MantissaStringInScientificFormat(a))  )
            end
        IpfType(a.expo,DFP(v,prec))
    end

    @inline function straightToIpfType(a::DFP)
        frac = DFP_fraction(a)
        expo = parse(BigInt,DFP_toIntegerString(a))
        IpfType(expo,frac)
    end

    @inline function straightToIpfType(a::Integer)
        frac = DFP_createZero(DFP_getDefaultPrecision())
        IpfType(a,frac)
    end

    @inline function straightToIpfType(a::Integer,prec::Int64)
        frac = DFP_createZero(prec)
        IpfType(a,frac)
    end

    function +(a::IpfType,b::IpfType)::IpfType
        c = a.d + b.d
        if c.expo < 0 || DFP_isZero(c)
            return IpfType(a.int + b.int,c)
        end
        if c.expo == 0
            newexpo = a.int + b.int + c.m[1]
            c = c << 1
            return IpfType(newexpo,c)
        end
    end

    function -(a::IpfType,b::IpfType)::IpfType
        c = a.d - b.d
        if c.s == 1
            prec = DFP_lengthMantissa(c)
            one = DFP_createOne(prec)
            newc = one + c
            return IpfType(a.int - b.int - 1,newc)
        else
            return IpfType(a.int - b.int,c)
        end
    end

    function *(a::IpfType,b::IpfType)::IpfType
        # Stage 1
        stage1_int = a.int * b.int

        # Stage 2
        stringa = string(a.int)
        aintprec = length(stringa)
        bfracprec = DFP_lengthMantissa(b.d)
        newprec = max(aintprec,bfracprec) + GUARD_DIGITS
        aa = DFP(stringa,newprec)
        bb = DFP_setPrecision(b.d,newprec)
        rr = aa * bb
        intrr = DFP_integer(rr)
        stage2_int = parse(BigInt,DFP_toIntegerString(intrr))
        stage2_frac = DFP_setPrecision(DFP_fraction(rr),bfracprec)

        # Stage 3
        afracprec = DFP_lengthMantissa(a.d)
        stringb = string(b.int)
        bintprec = length(stringb)
        newprec = max(afracprec,bintprec) + GUARD_DIGITS
        aa = DFP_setPrecision(a.d,newprec)
        bb = DFP(stringb,newprec)
        rr = aa * bb
        intrr = DFP_integer(rr)
        stage3_int = parse(BigInt,DFP_toIntegerString(intrr))
        stage3_frac = DFP_setPrecision(DFP_fraction(rr),afracprec)

        # Stage 4
        afracprec = DFP_lengthMantissa(a.d)
        bfracprec = DFP_lengthMantissa(b.d)
        newprec = max(afracprec,bfracprec) + GUARD_DIGITS
        aa = DFP_setPrecision(a.d,newprec)
        bb = DFP_setPrecision(b.d,newprec)
        rr = aa * bb
        intrr = DFP_integer(rr)
        stage4_int = parse(BigInt,DFP_toIntegerString(intrr))
        stage4_frac = DFP_setPrecision(DFP_fraction(rr),afracprec)

        # Final Stage
        r = stage2_frac + stage3_frac + stage4_frac
        intr = DFP_integer(r)
        finalfrac = DFP_fraction(r)
        finalint = stage1_int + stage2_int +
                   stage3_int + stage4_int +
                   parse(BigInt,DFP_toIntegerString(intr))
        return IpfType(finalint,finalfrac)
    end

    function /(a::IpfType,b::IpfType)::IpfType
        prec = DFP_lengthMantissa(a.d)
        a_int_prec = length(string(a.int))
        b_int_prec = length(string(b.int))
        newprec = max(a_int_prec + 2 * (prec + GUARD_DIGITS), b_int_prec + 2 * (prec + GUARD_DIGITS))
        newa = DFP(a.int,newprec) + DFP(a.d,newprec)
        newb = DFP(b.int,newprec) + DFP(b.d,newprec)
        newc = newa / newb
        frac = DFP_setPrecision(DFP_fraction(newc),prec)
        expo = parse(BigInt,DFP_toIntegerString(newc))
        IpfType(expo,frac)
    end

    @inline function takeexp10OfIpfType(a::IpfType)
        prec = DFP_lengthMantissa(a.d)
        newprec = prec + GUARD_DIGITS
        numofbits = Int64(  ceil(3.322 * newprec)  )
        r = setprecision(numofbits) do
                exp10(  parse(BigFloat,MantissaStringInLog10Format(a.d))  )
            end
        temp = DFP(r,newprec)
        DFP_setPrecision(DFP{newprec}(0,a.int,temp.m),prec)
    end

    function DFP_power(a::DFP,b::Integer)
        # Check if a is an Error
        if DFP_isError(a)
            return a
        end
        prec = DFP_lengthMantissa(a)
        if iszero(b)
            return DFP_createOne(prec)
        end
        if DFP_isZero(a)
            return DFP_createZero(prec)
        end
        if DFP_isOne(a)
            return a
        end
        newprec = prec + GUARD_DIGITS
        if DFP_isPositive(a)
            aa = DFP_setPrecision(a,newprec)
            ipfaa = takelog10ToIpfType(aa)
            ipfbb = straightToIpfType(b,newprec)
            result = ipfbb * ipfaa
            # Handle the special case when the logarithmn
            # is negative by taking the fractional part
            # and add one to it and take the integer part
            # and subtract one from it
            if result.d.s == 1
                f = result.d
                len = DFP_lengthMantissa(f)
                newf = DFP_createOne(len) + f
                newint = result.int - 1
                result = IpfType(newint,newf)
            end
            return DFP_setPrecision( takeexp10OfIpfType( result ),prec )
        else
            # Since it is not Zero then it must be negative
            positive_a = DFP{prec}(0,a.expo,a.m)
            result = DFP_power(positive_a,b)
            if isodd(b)
                # If the power is an odd integer then the
                # result is negative
                result = DFP{prec}(1,result.expo,result.m)
            end
            return result
        end
    end

    function DFP_power(a::DFP{N},b::DFP{N}) where {N}
        # Check if a or b is an Error
        if DFP_isError(a)
            return a
        end
        if DFP_isError(b)
            return b
        end
        if DFP_isZero(b)
            return DFP_createOne(N)
        end
        if DFP_isZero(a)
            return DFP_createZero(N)
        end
        if DFP_isOne(a)
            return a
        end
        if DFP_isPositive(a)
            newprec = N + GUARD_DIGITS
            aa = DFP_setPrecision(a,newprec)
            bb = DFP_setPrecision(b,newprec)
            ipfaa = takelog10ToIpfType(aa)
            ipfbb = straightToIpfType(bb)
            result = ipfbb * ipfaa
            # Handle the special case when the logarithmn
            # is negative by taking the fractional part
            # and add one to it and take the integer part
            # and subtract one from it
            if result.d.s == 1
                f = result.d
                len = DFP_lengthMantissa(f)
                newf = DFP_createOne(len) + f
                newint = result.int - 1
                result = IpfType(newint,newf)
            end
            return DFP_setPrecision( takeexp10OfIpfType( result ),N )
        else
            # Since it is not Zero then a must be negative
            if DFP_isInteger(b)
                positive_a = DFP{N}(0,a.expo,a.m)
                result = DFP_power(positive_a,b)
                unitdigit = DFP_getUnitDigit(b)
                if unitdigit != nothing && isodd(unitdigit)
                    # If the power is an odd integer then the
                    # result is negative
                    result = DFP{N}(1,result.expo,result.m)
                end
                return result
            else
                # b is not an integer so the result is complex
                # return a domain error as we only deal in the Real domain
                return DFP_createError(N,3)
            end
        end # if DFP_isPositive(a)
    end

example:

julia> DFP{16}(1.23456)^DFP{16}(9.87654)
8.013530973203226

I’ll try and post the full source code below:

Sorry it didn’t work.
“An error occurred: Body is limited to 32000 characters; you entered 153580.”

Simple, I couldn’t decide how many digits of precision to make the DFP so I thought why not let the user decide and make N the number of decimal digits a part of DFP.

Then I had to figure out how to add together two DFP of different precisions and the simplest answer is to disallow it and force the user to change the precision to make both arguments (to addition) have the same precision.

This solves a lot of problems.

I don’t know what the maximum number of precision N could be. I only tested for N up to 2000.

I see, I see. Well, a downside is that different precision means different type then, and so changing precision leads to compilation.

After a failure to upload the source code, I have put the source code on GitHub

So you can get a copy via this url

2 Likes

Most users will just stick to one type. The default precision is 16 decimal places which was choosen to emulate Float64. I like my version because it allows the user to have multiple precision at the same time. Warning: a 2000 digit precision DFP takes a long time to do calculations.

I have upload the source via Git Hub so that you can download it to play around with it.

sample code:

include("/Users/ssiew/juliascript/SimpleDFP/SimpleDFPs.jl")

using .SimpleDFPs

a = DFP(12345.6789)

println(DFP_toCommonString(a))

cs = SimpleDFPs.DFP_toCommonString

println(cs(a^2))
1 Like

Here is how to do exponentiation in Decimals

using Decimals
import Base.sqrt, Base.^

function SciNota(x::Decimal)
    prec = precision(Decimal)
    str = string(x.c)
    strlen = length(str)
    expo_adj = strlen - 1
    if strlen > prec
        str = str[1:prec]
    else
        while length(str) < prec
            str = str * "0"
        end
    end
    return (Int64(x.s),str,x.q + expo_adj)
end

function SciString(x::Decimal)
    (sign,str,expo) = SciNota(x)
    result = sign == 1 ? "-" : ""
    result = result * str[1] * "." * str[2:end] * "e" * string(expo)
    return result
end

function sqrt(x::Decimal;guard_digits=6)
    x_str = SciString(x)
    local z_str
    setprecision(BigFloat, precision(Decimal) + guard_digits; base=10) do
        z_str      = string(  Base.sqrt(parse(BigFloat,x_str))  )
    end
    z = parse(Decimal,z_str)
    return z
end

function power(x::Decimal,y::Decimal;guard_digits=6)
    x_str = SciString(x)
    y_str = SciString(y)
    local z_str
    setprecision(BigFloat, precision(Decimal) + guard_digits; base=10) do
        z_str      = string(  parse(BigFloat,x_str) ^ parse(BigFloat,y_str)  )
    end
    z = parse(Decimal,z_str)
    return z
end

^(x::Decimal,y::Decimal;guard_digits=6) = power(x,y,guard_digits=guard_digits)

x = Decimal(0,5,-1)
y = sqrt(dec"3")

println("start of program")
println( SciNota(x) )
println( SciString(x) )
println( string(power(x,y)) )

with the output

Starting Julia...
               _
   _       _ _(_)_     |  Documentation: https://docs.julialang.org
  (_)     | (_) (_)    |
   _ _   _| |_  __ _   |  Type "?" for help, "]?" for Pkg help.
  | | | | | | |/ _` |  |
  | | |_| | | | (_| |  |  Version 1.8.5 (2023-01-08)
 _/ |\__'_|_|_|\__'_|  |  Official https://julialang.org/ release
|__/                   |

(0, "5000000000000000000000000000", -1)
5.000000000000000000000000000e-1
0.301023743930928451027062486480388091
julia> 
julia> dec"5.1" ^ dec"7.1"
105620.50759235738803361081790798746

julia> 5.1^7.1
105620.50759235727

compare with Mathematica of 5.1^7.1