Skip to content

Commit 4dd5492

Browse files
authored
Merge pull request #1 from JuliaMath/cv/parse
Add support for creating FixedDecimals from strings
2 parents 9627c91 + 0ebb0ce commit 4dd5492

File tree

2 files changed

+213
-2
lines changed

2 files changed

+213
-2
lines changed

src/FixedPointDecimals.jl

Lines changed: 73 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,13 +25,13 @@ __precompile__()
2525

2626
module FixedPointDecimals
2727

28-
export FixedDecimal
28+
export FixedDecimal, RoundThrows
2929

3030
using Compat
3131

3232
import Base: reinterpret, zero, one, abs, sign, ==, <, <=, +, -, /, *, div,
3333
rem, divrem, fld, mod, fldmod, fld1, mod1, fldmod1, isinteger,
34-
typemin, typemax, realmin, realmax, print, show, string, convert,
34+
typemin, typemax, realmin, realmax, print, show, string, convert, parse,
3535
promote_rule, min, max, trunc, round, floor, ceil, eps, float, widemul
3636

3737
import Base.Checked: checked_mul
@@ -311,4 +311,75 @@ function show{T, f}(io::IO, x::FD{T, f})
311311
end
312312
end
313313

314+
# parsing
315+
316+
"""
317+
RoundThrows
318+
319+
Raises an `InexactError` if any rounding is necessary.
320+
"""
321+
const RoundThrows = RoundingMode{:Throw}()
322+
323+
function parse{T, f}(::Type{FD{T, f}}, str::AbstractString, mode::RoundingMode=RoundNearest)
324+
if !(mode in [RoundThrows, RoundNearest, RoundToZero])
325+
throw(ArgumentError("Unhandled rounding mode $mode"))
326+
end
327+
328+
# Parse exponent information
329+
exp_index = findfirst(str, 'e')
330+
if exp_index > 0
331+
exp = parse(Int, str[(exp_index + 1):end])
332+
sig_end = exp_index - 1
333+
else
334+
exp = 0
335+
sig_end = endof(str)
336+
end
337+
338+
# Remove the decimal place from the string
339+
sign = T(first(str) == '-' ? -1 : 1)
340+
dec_index = findfirst(str, '.')
341+
sig_start = sign < 0 ? 2 : 1
342+
if dec_index > 0
343+
int_str = str[sig_start:(dec_index - 1)] * str[(dec_index + 1):sig_end]
344+
exp -= sig_end - dec_index
345+
else
346+
int_str = str[sig_start:sig_end]
347+
end
348+
349+
# Split the integer string into the value we can represent inside the FixedDecimal and
350+
# the remaining digits we'll use during rounding
351+
int_end = endof(int_str)
352+
pivot = int_end + exp - (-f)
353+
354+
a = rpad(int_str[1:min(pivot, int_end)], pivot, '0')
355+
b = lpad(int_str[max(pivot, 1):int_end], int_end - pivot + 1, '0')
356+
357+
# Parse the strings
358+
val = isempty(a) ? T(0) : sign * parse(T, a)
359+
if !isempty(b) && any(collect(b[2:end]) .!= '0')
360+
if mode == RoundThrows
361+
throw(InexactError())
362+
elseif mode == RoundNearest
363+
val += sign * parse_round(T, b, mode)
364+
end
365+
end
366+
367+
reinterpret(FD{T, f}, val)
368+
end
369+
370+
function parse_round{T}(::Type{T}, fractional::AbstractString, ::RoundingMode{:Nearest})
371+
# Note: parsing each digit individually ensures we don't run into an OverflowError
372+
digits = Int8[parse(Int8, d) for d in fractional]
373+
for i in length(digits):-1:2
374+
if digits[i] > 5 || digits[i] == 5 && isodd(digits[i - 1])
375+
if i - 1 == 1
376+
return T(1)
377+
else
378+
digits[i - 1] += 1
379+
end
380+
end
381+
end
382+
return T(0)
383+
end
384+
314385
end

test/runtests.jl

Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -454,4 +454,144 @@ end
454454
end
455455
end
456456

457+
@testset "parse_round" begin
458+
@test FixedPointDecimals.parse_round(Int, "44", RoundNearest) == 0
459+
@test FixedPointDecimals.parse_round(Int, "45", RoundNearest) == 0
460+
@test FixedPointDecimals.parse_round(Int, "46", RoundNearest) == 1
461+
@test FixedPointDecimals.parse_round(Int, "54", RoundNearest) == 0
462+
@test FixedPointDecimals.parse_round(Int, "55", RoundNearest) == 1
463+
@test FixedPointDecimals.parse_round(Int, "56", RoundNearest) == 1
464+
465+
# Handle a number of digits that exceeds the storage capacity of Int128
466+
@test FixedPointDecimals.parse_round(Int8, "9"^40, RoundNearest) == 1
467+
end
468+
469+
@testset "parse" begin
470+
# Note: the underscore used in the reinterpreted integer is used to indicate the decimal
471+
# place.
472+
@testset "decimal position" begin
473+
@test parse(FD2, "123") == reinterpret(FD2, 123_00)
474+
@test parse(FD2, "0.123") == reinterpret(FD2, 0_12)
475+
@test parse(FD2, ".123") == reinterpret(FD2, 0_12)
476+
@test parse(FD2, "1.23") == reinterpret(FD2, 1_23)
477+
@test parse(FD2, "12.3") == reinterpret(FD2, 12_30)
478+
@test parse(FD2, "123.") == reinterpret(FD2, 123_00)
479+
@test parse(FD2, "123.0") == reinterpret(FD2, 123_00)
480+
481+
@test parse(FD2, "-123") == reinterpret(FD2, -123_00)
482+
@test parse(FD2, "-0.123") == reinterpret(FD2, -0_12)
483+
@test parse(FD2, "-.123") == reinterpret(FD2, -0_12)
484+
@test parse(FD2, "-1.23") == reinterpret(FD2, -1_23)
485+
@test parse(FD2, "-12.3") == reinterpret(FD2, -12_30)
486+
@test parse(FD2, "-123.") == reinterpret(FD2, -123_00)
487+
@test parse(FD2, "-123.0") == reinterpret(FD2, -123_00)
488+
end
489+
490+
@testset "scientific notation" begin
491+
@test parse(FD4, "12e0") == reinterpret(FD4, 00012_0000)
492+
@test parse(FD4, "12e3") == reinterpret(FD4, 12000_0000)
493+
@test parse(FD4, "12e-3") == reinterpret(FD4, 00000_0120)
494+
@test parse(FD4, "1.2e0") == reinterpret(FD4, 00001_2000)
495+
@test parse(FD4, "1.2e3") == reinterpret(FD4, 01200_0000)
496+
@test parse(FD4, "1.2e-3") == reinterpret(FD4, 00000_0012)
497+
@test parse(FD4, "1.2e-4") == reinterpret(FD4, 00000_0001)
498+
499+
@test parse(FD4, "-12e0") == reinterpret(FD4, -00012_0000)
500+
@test parse(FD4, "-12e3") == reinterpret(FD4, -12000_0000)
501+
@test parse(FD4, "-12e-3") == reinterpret(FD4, -00000_0120)
502+
@test parse(FD4, "-1.2e0") == reinterpret(FD4, -00001_2000)
503+
@test parse(FD4, "-1.2e3") == reinterpret(FD4, -01200_0000)
504+
@test parse(FD4, "-1.2e-3") == reinterpret(FD4, -00000_0012)
505+
506+
@test parse(FD2, "999e-1") == reinterpret(FD2, 99_90)
507+
@test parse(FD2, "999e-2") == reinterpret(FD2, 09_99)
508+
@test parse(FD2, "999e-3") == reinterpret(FD2, 01_00)
509+
@test parse(FD2, "999e-4") == reinterpret(FD2, 00_10)
510+
@test parse(FD2, "999e-5") == reinterpret(FD2, 00_01)
511+
@test parse(FD2, "999e-6") == reinterpret(FD2, 00_00)
512+
513+
@test parse(FD2, "-999e-1") == reinterpret(FD2, -99_90)
514+
@test parse(FD2, "-999e-2") == reinterpret(FD2, -09_99)
515+
@test parse(FD2, "-999e-3") == reinterpret(FD2, -01_00)
516+
@test parse(FD2, "-999e-4") == reinterpret(FD2, -00_10)
517+
@test parse(FD2, "-999e-5") == reinterpret(FD2, -00_01)
518+
@test parse(FD2, "-999e-6") == reinterpret(FD2, -00_00)
519+
520+
@test parse(FD4, "9"^96 * "e-100") == reinterpret(FD4, 0_001)
521+
end
522+
523+
@testset "round to nearest" begin
524+
@test parse(FD2, "0.444") == reinterpret(FD2, 0_44)
525+
@test parse(FD2, "0.445") == reinterpret(FD2, 0_44)
526+
@test parse(FD2, "0.446") == reinterpret(FD2, 0_45)
527+
@test parse(FD2, "0.454") == reinterpret(FD2, 0_45)
528+
@test parse(FD2, "0.455") == reinterpret(FD2, 0_46)
529+
@test parse(FD2, "0.456") == reinterpret(FD2, 0_46)
530+
531+
@test parse(FD2, "-0.444") == reinterpret(FD2, -0_44)
532+
@test parse(FD2, "-0.445") == reinterpret(FD2, -0_44)
533+
@test parse(FD2, "-0.446") == reinterpret(FD2, -0_45)
534+
@test parse(FD2, "-0.454") == reinterpret(FD2, -0_45)
535+
@test parse(FD2, "-0.455") == reinterpret(FD2, -0_46)
536+
@test parse(FD2, "-0.456") == reinterpret(FD2, -0_46)
537+
538+
@test parse(FD2, "0.009") == reinterpret(FD2, 0_01)
539+
@test parse(FD2, "-0.009") == reinterpret(FD2, -0_01)
540+
541+
@test parse(FD4, "1.5e-4") == reinterpret(FD4, 0_0002)
542+
end
543+
544+
@testset "round to zero" begin
545+
@test parse(FD2, "0.444", RoundToZero) == reinterpret(FD2, 0_44)
546+
@test parse(FD2, "0.445", RoundToZero) == reinterpret(FD2, 0_44)
547+
@test parse(FD2, "0.446", RoundToZero) == reinterpret(FD2, 0_44)
548+
@test parse(FD2, "0.454", RoundToZero) == reinterpret(FD2, 0_45)
549+
@test parse(FD2, "0.455", RoundToZero) == reinterpret(FD2, 0_45)
550+
@test parse(FD2, "0.456", RoundToZero) == reinterpret(FD2, 0_45)
551+
552+
@test parse(FD2, "-0.444", RoundToZero) == reinterpret(FD2, -0_44)
553+
@test parse(FD2, "-0.445", RoundToZero) == reinterpret(FD2, -0_44)
554+
@test parse(FD2, "-0.446", RoundToZero) == reinterpret(FD2, -0_44)
555+
@test parse(FD2, "-0.454", RoundToZero) == reinterpret(FD2, -0_45)
556+
@test parse(FD2, "-0.455", RoundToZero) == reinterpret(FD2, -0_45)
557+
@test parse(FD2, "-0.456", RoundToZero) == reinterpret(FD2, -0_45)
558+
559+
@test parse(FD2, "0.009", RoundToZero) == reinterpret(FD2, 0_00)
560+
@test parse(FD2, "-0.009", RoundToZero) == reinterpret(FD2, 0_00)
561+
562+
@test parse(FD4, "1.5e-4", RoundToZero) == reinterpret(FD4, 0_0001)
563+
end
564+
565+
@testset "round throws" begin
566+
@test parse(FD2, "0.44", RoundThrows) == reinterpret(FD2, 0_44)
567+
@test parse(FD2, "0.440", RoundThrows) == reinterpret(FD2, 0_44)
568+
569+
@test_throws InexactError parse(FD2, "0.444", RoundThrows)
570+
@test_throws InexactError parse(FD2, "0.445", RoundThrows)
571+
@test_throws InexactError parse(FD2, "0.446", RoundThrows)
572+
@test_throws InexactError parse(FD2, "0.454", RoundThrows)
573+
@test_throws InexactError parse(FD2, "0.455", RoundThrows)
574+
@test_throws InexactError parse(FD2, "0.456", RoundThrows)
575+
576+
@test_throws InexactError parse(FD2, "-0.444", RoundThrows)
577+
@test_throws InexactError parse(FD2, "-0.445", RoundThrows)
578+
@test_throws InexactError parse(FD2, "-0.446", RoundThrows)
579+
@test_throws InexactError parse(FD2, "-0.454", RoundThrows)
580+
@test_throws InexactError parse(FD2, "-0.455", RoundThrows)
581+
@test_throws InexactError parse(FD2, "-0.456", RoundThrows)
582+
583+
@test_throws InexactError parse(FD2, "0.009", RoundThrows)
584+
@test_throws InexactError parse(FD2, "-0.009", RoundThrows)
585+
586+
@test_throws InexactError parse(FD4, "1.5e-4", RoundThrows)
587+
end
588+
589+
@testset "invalid" begin
590+
@test_throws OverflowError parse(FD4, "1.2e100")
591+
@test_throws ArgumentError parse(FD4, "foo")
592+
@test_throws ArgumentError parse(FD4, "1.2.3")
593+
@test_throws ArgumentError parse(FD4, "1.2", RoundUp)
594+
end
595+
end
596+
457597
end # global testset

0 commit comments

Comments
 (0)