Skip to content

Commit

Permalink
Reworked parsing & conversion
Browse files Browse the repository at this point in the history
  • Loading branch information
barucden committed Oct 16, 2024
1 parent 90748c1 commit 5d10758
Show file tree
Hide file tree
Showing 4 changed files with 122 additions and 72 deletions.
4 changes: 4 additions & 0 deletions src/Decimals.jl
Original file line number Diff line number Diff line change
Expand Up @@ -35,4 +35,8 @@ include("equals.jl")
# Rounding
include("round.jl")

include("conversion.jl")

include("show.jl")

end
59 changes: 59 additions & 0 deletions src/conversion.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
function Base.parse(::Type{Decimal}, str::AbstractString)
regex = Regex(string(
"^",
# Optional sign
"(?<sign>[+-])?",
# Decimal part: either 1[234].[567] or [.]1[234]
"(?<dec>(?:\\d+\\.\\d*)|(?:\\.?\\d+))",
# Optional exponent part: e or E followed by optional sign and digits
"(?<exp>[eE][+-]?\\d+)?",
"\$"
))

m = match(regex, str)
if isnothing(m)
throw(ArgumentError("Invalid decimal number: $str"))
end

sign = m[:sign]
deci_part = m[:dec]
expo_part = m[:exp]

exponent = isnothing(expo_part) ? 0 : parse(Int, @view expo_part[2:end])

int_frac = split(deci_part, ".", keepempty=true)
if length(int_frac) == 1
# There is no decimal point
coef = parse(BigInt, deci_part)
elseif length(int_frac) == 2
# There is a decimal point
int, frac = int_frac
coef = parse(BigInt, int * frac)
exponent -= length(frac)
else
@assert false # should never happen
end

negative = sign == "-"

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
end
72 changes: 0 additions & 72 deletions src/decimal.jl
Original file line number Diff line number Diff line change
@@ -1,79 +1,7 @@
# Convert a string to a decimal, e.g. "0.01" -> Decimal(0, 1, -2)
function Base.parse(::Type{Decimal}, str::AbstractString)
if 'e' str
return parse(Decimal, scinote(str))
elseif 'E' str
return parse(Decimal, scinote(lowercase(str)))
end
c, q = parameters(('.' in str) ? split(str, '.') : str)
normalize(Decimal(str[1] == '-', c, q))
end

decimal(str::AbstractString) = parse(Decimal, str)

# Convert a number to a decimal
Decimal(num::Real) = parse(Decimal, string(num))
Base.convert(::Type{Decimal}, num::Real) = Decimal(num::Real)
decimal(x::Real) = Decimal(x)
Decimal(x::Decimal) = x

# Get Decimal constructor parameters from string
parameters(x::AbstractString) = (abs(parse(BigInt, x)), 0)

# Get Decimal constructor parameters from array
function parameters(x::Array)
c = parse(BigInt, join(x))
(abs(c), -length(x[2]))
end

# Get decimal() argument from scientific notation
function scinote(str::AbstractString)
s = (str[1] == '-') ? "-" : ""
n, expo = split(str, 'e')
n = split(n, '.')
if s == "-"
n[1] = n[1][2:end]
end
if parse(Int64, expo) >= 0
shift = parse(Int64, expo) - ((length(n) == 2) ? length(n[2]) : 0)
s * join(n) * repeat("0", shift)
else
shift = -parse(Int64, expo) - ((length(n) == 2) ? length(n[1]) : length(n))
s * "0." * repeat("0", shift) * join(n)
end
end

# Convert a decimal to a string
function Base.print(io::IO, x::Decimal)
c = string(x.c)
negative = (x.s == 1) ? "-" : ""
if x.q > 0
print(io, negative, c, repeat("0", x.q))
elseif x.q < 0
shift = x.q + length(c)
if shift > 0
print(io, negative, c[1:shift], ".", c[(shift+1):end])
else
print(io, negative, "0.", repeat("0", -shift), c)
end
else
print(io, negative, c)
end
end

# Zero/one value
Base.zero(::Type{Decimal}) = Decimal(false, 0, 0)
Base.one(::Type{Decimal}) = Decimal(false, 1, 0)

# convert a decimal to any subtype of Real
(::Type{T})(x::Decimal) where {T<:Real} = parse(T, string(x))

# 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
end

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

59 changes: 59 additions & 0 deletions src/show.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
# Show x without using exponential notation
function _show_plain(io::IO, x::Decimal)
@assert x.q 0

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
coef = string(x.c)
coefdigits = ncodeunits(coef)
pointpos = -x.q
if pointpos < coefdigits
# If there are some (non-zero) digits before the decimal point
print(io, @view(coef[1:end - pointpos]), ".")
else
print(io, "0.")
end
print(io, @view(coef[end - pointpos + 1:end]))
end
end

# Show x using exponential notation
function _show_exponential(io::IO, x::Decimal)
coef = string(x.c)
coefdigits = ncodeunits(coef)

if x.s
print(io, '-')
end

# If there are more than one digit,
# put a decimal point right after the first digit
if coefdigits > 1
print(io, coef[1], ".", @view coef[2:end])
else
print(io, coef)
end

adjusted_exp = x.q + coefdigits - 1
exp_sign = adjusted_exp > 0 ? '+' : '-'
print(io, "E", exp_sign, adjusted_exp)
end

function Base.show(io::IO, ::MIME"text/plain", 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)
else
_show_exponential(io, x)
end
end

0 comments on commit 5d10758

Please sign in to comment.