Skip to content

Commit

Permalink
tryparse, @dec_str, printing
Browse files Browse the repository at this point in the history
  • Loading branch information
barucden committed Oct 16, 2024
1 parent 5d10758 commit fd69b8d
Show file tree
Hide file tree
Showing 10 changed files with 127 additions and 70 deletions.
6 changes: 3 additions & 3 deletions src/Decimals.jl
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@
module Decimals

export Decimal,
decimal,
number,
normalize
normalize,
@dec_str

const DIGITS = 20

Expand Down Expand Up @@ -35,7 +35,7 @@ include("equals.jl")
# Rounding
include("round.jl")

include("conversion.jl")
include("parse.jl")

include("show.jl")

Expand Down
23 changes: 22 additions & 1 deletion src/decimal.jl
Original file line number Diff line number Diff line change
@@ -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

Check warning on line 24 in src/decimal.jl

View check run for this annotation

Codecov / codecov/patch

src/decimal.jl#L24

Added line #L24 was not covered by tests
Base.isnan(x::Decimal) = false

# sign
Base.signbit(x::Decimal) = x.s

2 changes: 0 additions & 2 deletions src/equals.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
55 changes: 36 additions & 19 deletions src/conversion.jl → src/parse.jl
Original file line number Diff line number Diff line change
@@ -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

Check warning on line 26 in src/parse.jl

View check run for this annotation

Codecov / codecov/patch

src/parse.jl#L26

Added line #L26 was not covered by tests
end

function Base.tryparse(::Type{Decimal}, str::AbstractString)
regex = Regex(string(
"^",
# Optional sign
Expand All @@ -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)
Expand All @@ -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

43 changes: 24 additions & 19 deletions src/show.jl
Original file line number Diff line number Diff line change
@@ -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)

Check warning on line 32 in src/show.jl

View check run for this annotation

Codecov / codecov/patch

src/show.jl#L30-L32

Added lines #L30 - L32 were not covered by tests

Expand All @@ -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))

Check warning on line 48 in src/show.jl

View check run for this annotation

Codecov / codecov/patch

src/show.jl#L46-L48

Added lines #L46 - L48 were not covered by tests
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

Check warning on line 53 in src/show.jl

View check run for this annotation

Codecov / codecov/patch

src/show.jl#L52-L53

Added lines #L52 - L53 were not covered by tests

# Decide whether to use the exponential notation
if x.q 0 && adjusted_exp -6
_show_plain(io, x)
show_plain(io, x)

Check warning on line 57 in src/show.jl

View check run for this annotation

Codecov / codecov/patch

src/show.jl#L56-L57

Added lines #L56 - L57 were not covered by tests
else
_show_exponential(io, x)
show_exponential(io, x)

Check warning on line 59 in src/show.jl

View check run for this annotation

Codecov / codecov/patch

src/show.jl#L59

Added line #L59 was not covered by tests
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)

Check warning on line 64 in src/show.jl

View check run for this annotation

Codecov / codecov/patch

src/show.jl#L63-L64

Added lines #L63 - L64 were not covered by tests
1 change: 1 addition & 0 deletions test/runtests.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
2 changes: 1 addition & 1 deletion test/test_arithmetic.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
19 changes: 0 additions & 19 deletions test/test_decimal.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
6 changes: 0 additions & 6 deletions test/test_equals.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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
Expand Down
40 changes: 40 additions & 0 deletions test/test_parse.jl
Original file line number Diff line number Diff line change
@@ -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

0 comments on commit fd69b8d

Please sign in to comment.