diff --git a/src/Decimals.jl b/src/Decimals.jl index e0a152e..30ca997 100644 --- a/src/Decimals.jl +++ b/src/Decimals.jl @@ -5,9 +5,9 @@ module Decimals export Decimal, - decimal, number, - normalize + normalize, + @dec_str const DIGITS = 20 @@ -35,7 +35,7 @@ include("equals.jl") # Rounding include("round.jl") -include("conversion.jl") +include("parse.jl") include("show.jl") diff --git a/src/decimal.jl b/src/decimal.jl index 5facb33..4408d85 100644 --- a/src/decimal.jl +++ b/src/decimal.jl @@ -1,7 +1,28 @@ +Decimal(x::Decimal) = x + +# From real numbers to Decimal +Decimal(x::Real) = parse(Decimal, string(x)) +Base.convert(::Type{Decimal}, x::Real) = Decimal(x) + +# From Decimal to numbers +(::Type{T})(x::Decimal) where {T<:Number} = parse(T, string(x)) + +# String representation of Decimal +function Base.string(x::Decimal) + io = IOBuffer() + show_plain(io, x) + return String(take!(io)) +end + # Zero/one value Base.zero(::Type{Decimal}) = Decimal(false, 0, 0) Base.one(::Type{Decimal}) = Decimal(false, 1, 0) +Base.iszero(x::Decimal) = iszero(x.c) + +# As long as we do not support Inf/NaN +Base.isfinite(x::Decimal) = true +Base.isnan(x::Decimal) = false + # 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/conversion.jl b/src/parse.jl similarity index 51% rename from src/conversion.jl rename to src/parse.jl index 28c118e..7987ba3 100644 --- a/src/conversion.jl +++ b/src/parse.jl @@ -1,4 +1,32 @@ -function Base.parse(::Type{Decimal}, str::AbstractString) +macro dec_str(s) + # Taken from @big_str in Base + msg = "Invalid decimal: $s" + throw_error = :(throw(ArgumentError($msg))) + + if '_' in s + # remove _ in s[2:end-1] + bf = IOBuffer(maxsize=lastindex(s)) + c = s[1] + print(bf, c) + is_prev_underscore = (c == '_') + is_prev_dot = (c == '.') + for c in SubString(s, 2, lastindex(s) - 1) + c != '_' && print(bf, c) + c == '_' && is_prev_dot && return throw_error + c == '.' && is_prev_underscore && return throw_error + is_prev_underscore = (c == '_') + is_prev_dot = (c == '.') + end + print(bf, s[end]) + s = String(take!(bf)) + end + + x = tryparse(Decimal, s) + x === nothing || return x + return throw_error +end + +function Base.tryparse(::Type{Decimal}, str::AbstractString) regex = Regex(string( "^", # Optional sign @@ -12,13 +40,14 @@ function Base.parse(::Type{Decimal}, str::AbstractString) m = match(regex, str) if isnothing(m) - throw(ArgumentError("Invalid decimal number: $str")) + return nothing end sign = m[:sign] deci_part = m[:dec] expo_part = m[:exp] + # expo_part[1] is 'e' or 'E' exponent = isnothing(expo_part) ? 0 : parse(Int, @view expo_part[2:end]) int_frac = split(deci_part, ".", keepempty=true) @@ -39,21 +68,9 @@ function Base.parse(::Type{Decimal}, str::AbstractString) return Decimal(negative, coef, exponent) end -Decimal(x::Decimal) = x -Decimal(num::Real) = parse(Decimal, string(num)) - -decimal(x::Real) = Decimal(x) -decimal(str::AbstractString) = parse(Decimal, str) - -Base.convert(::Type{Decimal}, num::Real) = Decimal(num::Real) - -# TODO: This is broken now, because of `string(::Decimal)` -# convert a decimal to any subtype of Real -(::Type{T})(x::Decimal) where {T<:Real} = parse(T, string(x)) - -# TODO: This is "deliberately" type-unstable -# Convert a decimal to an integer if possible, a float if not -function number(x::Decimal) - ix = (str = string(x) ; fx = parse(Float64, str); round(Int64, fx)) - (ix == fx) ? ix : fx +function Base.parse(::Type{Decimal}, str::AbstractString) + x = tryparse(Decimal, str) + isnothing(x) && throw(ArgumentError("Invalid decimal: $str")) + return x end + diff --git a/src/show.jl b/src/show.jl index be4b173..5b39f85 100644 --- a/src/show.jl +++ b/src/show.jl @@ -1,32 +1,33 @@ # Show x without using exponential notation -function _show_plain(io::IO, x::Decimal) - @assert x.q ≤ 0 - +function show_plain(io::IO, x::Decimal) if x.s print(io, '-') end - # If exponent is zero, do not add decimal point. - # Otherwise (exponent is negative), add a decimal point so that there - # are `-x.q` digits after it - if iszero(x.q) - print(io, x.c) - else + if x.q ≥ 0 + # Print coefficient and `x.q` zeros + print(io, x.c, repeat('0', x.q)) + else # x.q < 0 coef = string(x.c) coefdigits = ncodeunits(coef) - pointpos = -x.q + pointpos = -x.q # How many digits should go after the decimal point + + # If there are some (non-zero) digits before the decimal point, + # print them, then print the decimal point, and then the digits after + # the decimal point + # Otherwise, print "0." and then print zeros so that the number of + # zeros plus `coefdigits` is `pointpos` if pointpos < coefdigits - # If there are some (non-zero) digits before the decimal point - print(io, @view(coef[1:end - pointpos]), ".") + print(io, @view(coef[1:end - pointpos]), ".", + @view(coef[end - pointpos + 1:end])) else - print(io, "0.") + print(io, "0.", repeat('0', pointpos - coefdigits), coef) end - print(io, @view(coef[end - pointpos + 1:end])) end end # Show x using exponential notation -function _show_exponential(io::IO, x::Decimal) +function show_exponential(io::IO, x::Decimal) coef = string(x.c) coefdigits = ncodeunits(coef) @@ -44,16 +45,20 @@ function _show_exponential(io::IO, x::Decimal) adjusted_exp = x.q + coefdigits - 1 exp_sign = adjusted_exp > 0 ? '+' : '-' - print(io, "E", exp_sign, adjusted_exp) + print(io, "E", exp_sign, abs(adjusted_exp)) end -function Base.show(io::IO, ::MIME"text/plain", x::Decimal) +# Prints `x` using the scientific notation +function scientific_notation(io::IO, x::Decimal) adjusted_exp = x.q + ndigits(x.c) - 1 # Decide whether to use the exponential notation if x.q ≤ 0 && adjusted_exp ≥ -6 - _show_plain(io, x) + show_plain(io, x) else - _show_exponential(io, x) + show_exponential(io, x) end end + +Base.show(io::IO, x::Decimal) = print(io, "Decimal($(Int(x.s)), $(x.c), $(x.q))") +Base.show(io::IO, ::MIME"text/plain", x::Decimal) = scientific_notation(io, x) diff --git a/test/runtests.jl b/test/runtests.jl index 6da70af..4965ee5 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -13,6 +13,7 @@ global d = [ ] include("test_constructor.jl") +include("test_parse.jl") include("test_decimal.jl") include("test_norm.jl") include("test_arithmetic.jl") diff --git a/test/test_arithmetic.jl b/test/test_arithmetic.jl index c07413d..7c7fa71 100644 --- a/test/test_arithmetic.jl +++ b/test/test_arithmetic.jl @@ -21,7 +21,7 @@ end @testset "Negation" begin @test -Decimal.([0.3 0.2]) == [-Decimal(0.3) -Decimal(0.2)] @test -Decimal(0.3) == zero(Decimal) - Decimal(0.3) - @test iszero(decimal(12.1) - decimal(12.1)) + @test iszero(Decimal(12.1) - Decimal(12.1)) end @testset "Multiplication" begin diff --git a/test/test_decimal.jl b/test/test_decimal.jl index 57c7c3c..0f2de6c 100644 --- a/test/test_decimal.jl +++ b/test/test_decimal.jl @@ -29,12 +29,6 @@ using Test @test parse(Decimal, "0.1234567891") == Decimal(0.1234567891) == Decimal(false,1234567891, -10) @test parse(Decimal, "0.12345678912") == Decimal(0.12345678912) == Decimal(false,12345678912, -11) end - - @testset "Using `decimal`" begin - @test decimal("1.0") == Decimal(false, 1, 0) - @test decimal(8.1) == Decimal(false, 81, -1) - @test decimal.(Float64.(d)) == d - end end @testset "Array{<:Number} to Array{Decimal}" begin @@ -65,19 +59,6 @@ end @test BigInt(Decimal(false, 2001, 2)) == 200100 @test BigFloat(Decimal(true, 123, -3)) == big"-0.123" @test Float64(Decimal(false, 123, -2)) == 1.23 - @test number(Decimal(false, 1, -2)) == 0.01 - @test number(Decimal(false, 1, -3)) == 0.001 - @test number(Decimal(false, 1523, -2)) == 15.23 - @test number(Decimal(false, 543, 0)) == 543 - @test number(Decimal(true, 345, 0)) == -345 - @test number(Decimal(false, 123, 0)) == 123 - @test number(Decimal(true, 32, 0)) == -32 - @test number(Decimal(false, 2001, 2)) == 200100 - @test number(Decimal(true, 123, -3)) == -0.123 - @test number(Decimal(false, 123, -2)) == 1.23 - @test string(Float64(Decimal(false, 543, 0))) == "543.0" - @test string(number(Decimal(false, 543, 0))) == "543" - @test string(number(Decimal(false, 543, -1))) == "54.3" end end diff --git a/test/test_equals.jl b/test/test_equals.jl index 07c6529..b850af3 100644 --- a/test/test_equals.jl +++ b/test/test_equals.jl @@ -31,8 +31,6 @@ end @test Decimal(bi) == bi @test bi == Decimal(bi) - @test decimal(12.1) == decimal(12.1) - @test Decimal(true, 0, -1) == Decimal(false, 0, 0) end @@ -43,7 +41,6 @@ end @test !(Decimal(true, 0, 1) < Decimal(true, 1, 1)) @test Decimal(false, 2, -3) < Decimal(false, 2, 3) @test !(Decimal(false, 2, 3) < Decimal(false, 2, -3)) - @test !(decimal(12.1) < decimal(12.1)) @test !(Decimal(true, 0, -1) < Decimal(false, 0, 0)) @test !(Decimal(false, 0, 0) < Decimal(true, 0, -1)) end @@ -55,7 +52,6 @@ end @test Decimal(1, 0, 1) > Decimal(1, 1, 1) @test !(Decimal(0, 2, -3) > Decimal(0, 2, 3)) @test Decimal(0, 2, 3) > Decimal(0, 2, -3) - @test !(decimal(12.1) > decimal(12.1)) @test !(Decimal(1, 0, -1) > Decimal(0, 0, 0)) @test !(Decimal(0, 0, 0) > Decimal(1, 0, -1)) end @@ -67,7 +63,6 @@ end @test !(Decimal(1, 0, 1) <= Decimal(1, 1, 1)) @test Decimal(0, 2, -3) <= Decimal(0, 2, 3) @test !(Decimal(0, 2, 3) <= Decimal(0, 2, -3)) - @test decimal(12.1) <= decimal(12.1) @test Decimal(1, 0, -1) <= Decimal(0, 0, 0) @test Decimal(0, 0, 0) <= Decimal(1, 0, -1) end @@ -79,7 +74,6 @@ end @test Decimal(1, 0, 1) >= Decimal(1, 1, 1) @test !(Decimal(0, 2, -3) >= Decimal(0, 2, 3)) @test Decimal(0, 2, 3) >= Decimal(0, 2, -3) - @test decimal(12.1) >= decimal(12.1) @test Decimal(1, 0, -1) >= Decimal(0, 0, 0) @test Decimal(0, 0, 0) >= Decimal(1, 0, -1) end diff --git a/test/test_parse.jl b/test/test_parse.jl new file mode 100644 index 0000000..54cecba --- /dev/null +++ b/test/test_parse.jl @@ -0,0 +1,40 @@ +@testset "Parsing" begin + @testset "tryparse" begin + @test tryparse(Decimal, "0.01") == Decimal(false, 1, -2) + @test tryparse(Decimal, ".001") == Decimal(false, 1, -3) + @test tryparse(Decimal, "15.23") == Decimal(false, 1523, -2) + @test tryparse(Decimal, "543") == Decimal(false, 543, 0) + @test tryparse(Decimal, "-345") == Decimal(true, 345, 0) + @test tryparse(Decimal, "000123") == Decimal(false, 123, 0) + @test tryparse(Decimal, "-00032") == Decimal(true, 32, 0) + @test tryparse(Decimal, "200100") == Decimal(false, 2001, 2) + @test tryparse(Decimal, "-.123") == Decimal(true, 123, -3) + @test tryparse(Decimal, "1.23000") == Decimal(false, 123, -2) + @test tryparse(Decimal, "4734.612") == Decimal(false, 4734612, -3) + @test tryparse(Decimal, "541724.2") == Decimal(false,5417242,-1) + @test tryparse(Decimal, "2.5e6") == Decimal(false, 25, 5) + @test tryparse(Decimal, "2.385350e8") == Decimal(false, 238535, 3) + @test tryparse(Decimal, "12.3e-4") == Decimal(false, 123, -5) + @test tryparse(Decimal, "-12.3e4") == Decimal(true, 123, 3) + @test tryparse(Decimal, "-12.3e-4") == Decimal(true, 123, -5) + @test tryparse(Decimal, "-12.3E-4") == Decimal(true, 123, -5) + @test tryparse(Decimal, "0.1234567891") == Decimal(false,1234567891, -10) + @test tryparse(Decimal, "0.12345678912") == Decimal(false,12345678912, -11) + + @test isnothing(tryparse(Decimal, "1.1.1")) + @test isnothing(tryparse(Decimal, "1f2")) + @test isnothing(tryparse(Decimal, " 1")) + @test isnothing(tryparse(Decimal, "1e1e1")) + @test isnothing(tryparse(Decimal, "1-1")) + end + + @testset "@dec_str" begin + @test dec"1.123" == Decimal(0, 1123, -3) + @test dec"-1.123" == Decimal(1, 1123, -3) + @test dec"123" == Decimal(0, 123, 0) + @test dec"-123" == Decimal(1, 123, 0) + @test dec"1_000.002" == Decimal(0, 1000002, -3) + @test dec"1_000_000.002" == Decimal(0, 1000000002, -3) + end + +end