Skip to content

Commit 5a05c88

Browse files
committed
Parse defaults to rounding input
1 parent 48fecf1 commit 5a05c88

File tree

2 files changed

+158
-51
lines changed

2 files changed

+158
-51
lines changed

src/FixedPointDecimals.jl

Lines changed: 47 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -293,29 +293,61 @@ function show{T, f}(io::IO, x::FD{T, f})
293293
end
294294

295295
# parsing
296-
function parse{T, f}(::Type{FD{T, f}}, str::AbstractString)
297-
# parse exponent information
298-
ex = findfirst(str, 'e')
299-
if ex > 0
300-
pow = parse(Int, str[(ex + 1):end])
301-
ending = ex - 1
296+
function parse{T, f}(::Type{FD{T, f}}, str::AbstractString, mode::RoundingMode=RoundNearest)
297+
if !(mode in [RoundNearest, RoundToZero])
298+
throw(ArgumentError("Unhandled rounding mode $mode"))
299+
end
300+
301+
# Parse exponent information
302+
exp_index = findfirst(str, 'e')
303+
if exp_index > 0
304+
exp = parse(Int, str[(exp_index + 1):end])
305+
sig_end = exp_index - 1
302306
else
303-
pow = 0
304-
ending = endof(str)
307+
exp = 0
308+
sig_end = endof(str)
305309
end
306310

307311
# Remove the decimal place from the string
308-
dp = findfirst(str, '.')
309-
if dp > 0
310-
int_str = str[1:(dp - 1)] * str[(dp + 1):min(dp + f, ending)]
311-
len = dp + f - 1
312+
sign = T(first(str) == '-' ? -1 : 1)
313+
dec_index = findfirst(str, '.')
314+
sig_start = sign < 0 ? 2 : 1
315+
if dec_index > 0
316+
int_str = str[sig_start:(dec_index - 1)] * str[(dec_index + 1):sig_end]
317+
exp -= sig_end - dec_index
312318
else
313-
int_str = str[1:ending]
314-
len = ending + f
319+
int_str = str[sig_start:sig_end]
315320
end
316321

317-
val = parse(T, rpad(int_str, len + pow, '0'))
322+
# Split the integer string into the value we can represent inside the FixedDecimal and
323+
# the remaining digits we'll use during rounding
324+
int_end = endof(int_str)
325+
pivot = int_end + exp - (-f)
326+
327+
a = rpad(int_str[1:min(pivot, int_end)], pivot, '0')
328+
b = lpad(int_str[max(pivot, 1):int_end], int_end - pivot + 1, '0')
329+
330+
# Parse the strings
331+
val = isempty(a) ? T(0) : sign * parse(T, a)
332+
if !isempty(b) && mode != RoundToZero
333+
val += sign * parse_round(T, b, mode)
334+
end
318335
reinterpret(FD{T, f}, val)
319336
end
320337

338+
function parse_round{T}(::Type{T}, fractional::AbstractString, ::RoundingMode{:Nearest})
339+
# Note: parsing each digit individually ensures we don't run into an OverflowError
340+
digits = Int8[parse(Int8, d) for d in fractional]
341+
for i in length(digits):-1:2
342+
if digits[i] > 5 || digits[i] == 5 && isodd(digits[i - 1])
343+
if i - 1 == 1
344+
return T(1)
345+
else
346+
digits[i - 1] += 1
347+
end
348+
end
349+
end
350+
return T(0)
351+
end
352+
321353
end

test/runtests.jl

Lines changed: 111 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -412,45 +412,120 @@ end
412412
end
413413
end
414414

415+
@testset "parse_round" begin
416+
@test FixedPointDecimals.parse_round(Int, "44", RoundNearest) == 0
417+
@test FixedPointDecimals.parse_round(Int, "45", RoundNearest) == 0
418+
@test FixedPointDecimals.parse_round(Int, "46", RoundNearest) == 1
419+
@test FixedPointDecimals.parse_round(Int, "54", RoundNearest) == 0
420+
@test FixedPointDecimals.parse_round(Int, "55", RoundNearest) == 1
421+
@test FixedPointDecimals.parse_round(Int, "56", RoundNearest) == 1
422+
423+
# Handle a number of digits that exceeds the storage capacity of Int128
424+
@test FixedPointDecimals.parse_round(Int8, "9"^40, RoundNearest) == 1
425+
end
426+
415427
@testset "parse" begin
416428
# Note: the underscore used in the reinterpreted integer is used to indicate the decimal
417429
# place.
418-
@test parse(FD2, "123") == reinterpret(FD2, 123_00)
419-
@test parse(FD2, ".123") == reinterpret(FD2, 0_12)
420-
@test parse(FD2, "1.23") == reinterpret(FD2, 1_23)
421-
@test parse(FD2, "12.3") == reinterpret(FD2, 12_30)
422-
@test parse(FD2, "123.") == reinterpret(FD2, 123_00)
423-
@test_skip parse(FD2, "123.456") == reinterpret(FD2, 123_46)
424-
@test_skip parse(FD2, "123.455") == reinterpret(FD2, 123_46)
425-
@test_skip parse(FD2, "123.465") == reinterpret(FD2, 123_46)
426-
427-
@test parse(FD2, "-123") == reinterpret(FD2, -123_00)
428-
@test parse(FD2, "-.123") == reinterpret(FD2, -0_12)
429-
@test parse(FD2, "-1.23") == reinterpret(FD2, -1_23)
430-
@test parse(FD2, "-12.3") == reinterpret(FD2, -12_30)
431-
@test parse(FD2, "-123.") == reinterpret(FD2, -123_00)
432-
433-
@test parse(FD4, "12e0") == reinterpret(FD4, 00012_0000)
434-
@test parse(FD4, "12e3") == reinterpret(FD4, 12000_0000)
435-
@test parse(FD4, "12e-3") == reinterpret(FD4, 00000_0120)
436-
@test parse(FD4, "1.2e0") == reinterpret(FD4, 00001_2000)
437-
@test parse(FD4, "1.2e3") == reinterpret(FD4, 01200_0000)
438-
@test parse(FD4, "1.2e-3") == reinterpret(FD4, 00000_0012)
439-
@test parse(FD4, "1.2e-4") == reinterpret(FD4, 00000_0001)
440-
@test_skip parse(FD4, "1.5e-4") == reinterpret(FD4, 00000_0002)
441-
@test_throws OverflowError parse("1.2e100")
442-
443-
@test parse(FD4, "-12e0") == reinterpret(FD4, -00012_0000)
444-
@test parse(FD4, "-12e3") == reinterpret(FD4, -12000_0000)
445-
@test parse(FD4, "-12e-3") == reinterpret(FD4, -00000_0120)
446-
@test parse(FD4, "-1.2e0") == reinterpret(FD4, -00001_2000)
447-
@test parse(FD4, "-1.2e3") == reinterpret(FD4, -01200_0000)
448-
@test parse(FD4, "-1.2e-3") == reinterpret(FD4, -00000_0012)
449-
450-
@test parse(FD2, "2.3") == reinterpret(FD2, 2_30)
451-
452-
@test_throws ArgumentError parse(FD4, "foo")
453-
@test_throws ArgumentError parse(FD4, "1.2.3")
430+
@testset "decimal position" begin
431+
@test parse(FD2, "123") == reinterpret(FD2, 123_00)
432+
@test parse(FD2, "0.123") == reinterpret(FD2, 0_12)
433+
@test parse(FD2, ".123") == reinterpret(FD2, 0_12)
434+
@test parse(FD2, "1.23") == reinterpret(FD2, 1_23)
435+
@test parse(FD2, "12.3") == reinterpret(FD2, 12_30)
436+
@test parse(FD2, "123.") == reinterpret(FD2, 123_00)
437+
@test parse(FD2, "123.0") == reinterpret(FD2, 123_00)
438+
439+
@test parse(FD2, "-123") == reinterpret(FD2, -123_00)
440+
@test parse(FD2, "-0.123") == reinterpret(FD2, -0_12)
441+
@test parse(FD2, "-.123") == reinterpret(FD2, -0_12)
442+
@test parse(FD2, "-1.23") == reinterpret(FD2, -1_23)
443+
@test parse(FD2, "-12.3") == reinterpret(FD2, -12_30)
444+
@test parse(FD2, "-123.") == reinterpret(FD2, -123_00)
445+
@test parse(FD2, "-123.0") == reinterpret(FD2, -123_00)
446+
end
447+
448+
@testset "scientific notation" begin
449+
@test parse(FD4, "12e0") == reinterpret(FD4, 00012_0000)
450+
@test parse(FD4, "12e3") == reinterpret(FD4, 12000_0000)
451+
@test parse(FD4, "12e-3") == reinterpret(FD4, 00000_0120)
452+
@test parse(FD4, "1.2e0") == reinterpret(FD4, 00001_2000)
453+
@test parse(FD4, "1.2e3") == reinterpret(FD4, 01200_0000)
454+
@test parse(FD4, "1.2e-3") == reinterpret(FD4, 00000_0012)
455+
@test parse(FD4, "1.2e-4") == reinterpret(FD4, 00000_0001)
456+
457+
@test parse(FD4, "-12e0") == reinterpret(FD4, -00012_0000)
458+
@test parse(FD4, "-12e3") == reinterpret(FD4, -12000_0000)
459+
@test parse(FD4, "-12e-3") == reinterpret(FD4, -00000_0120)
460+
@test parse(FD4, "-1.2e0") == reinterpret(FD4, -00001_2000)
461+
@test parse(FD4, "-1.2e3") == reinterpret(FD4, -01200_0000)
462+
@test parse(FD4, "-1.2e-3") == reinterpret(FD4, -00000_0012)
463+
464+
@test parse(FD2, "999e-1") == reinterpret(FD2, 99_90)
465+
@test parse(FD2, "999e-2") == reinterpret(FD2, 09_99)
466+
@test parse(FD2, "999e-3") == reinterpret(FD2, 01_00)
467+
@test parse(FD2, "999e-4") == reinterpret(FD2, 00_10)
468+
@test parse(FD2, "999e-5") == reinterpret(FD2, 00_01)
469+
@test parse(FD2, "999e-6") == reinterpret(FD2, 00_00)
470+
471+
@test parse(FD2, "-999e-1") == reinterpret(FD2, -99_90)
472+
@test parse(FD2, "-999e-2") == reinterpret(FD2, -09_99)
473+
@test parse(FD2, "-999e-3") == reinterpret(FD2, -01_00)
474+
@test parse(FD2, "-999e-4") == reinterpret(FD2, -00_10)
475+
@test parse(FD2, "-999e-5") == reinterpret(FD2, -00_01)
476+
@test parse(FD2, "-999e-6") == reinterpret(FD2, -00_00)
477+
478+
@test parse(FD4, "9"^96 * "e-100") == reinterpret(FD4, 0_001)
479+
end
480+
481+
@testset "round to nearest" begin
482+
@test parse(FD2, "0.444") == reinterpret(FD2, 0_44)
483+
@test parse(FD2, "0.445") == reinterpret(FD2, 0_44)
484+
@test parse(FD2, "0.446") == reinterpret(FD2, 0_45)
485+
@test parse(FD2, "0.454") == reinterpret(FD2, 0_45)
486+
@test parse(FD2, "0.455") == reinterpret(FD2, 0_46)
487+
@test parse(FD2, "0.456") == reinterpret(FD2, 0_46)
488+
489+
@test parse(FD2, "-0.444") == reinterpret(FD2, -0_44)
490+
@test parse(FD2, "-0.445") == reinterpret(FD2, -0_44)
491+
@test parse(FD2, "-0.446") == reinterpret(FD2, -0_45)
492+
@test parse(FD2, "-0.454") == reinterpret(FD2, -0_45)
493+
@test parse(FD2, "-0.455") == reinterpret(FD2, -0_46)
494+
@test parse(FD2, "-0.456") == reinterpret(FD2, -0_46)
495+
496+
@test parse(FD2, "0.009") == reinterpret(FD2, 0_01)
497+
@test parse(FD2, "-0.009") == reinterpret(FD2, -0_01)
498+
499+
@test parse(FD4, "1.5e-4") == reinterpret(FD4, 0_0002)
500+
end
501+
502+
@testset "round to zero" begin
503+
@test parse(FD2, "0.444", RoundToZero) == reinterpret(FD2, 0_44)
504+
@test parse(FD2, "0.445", RoundToZero) == reinterpret(FD2, 0_44)
505+
@test parse(FD2, "0.446", RoundToZero) == reinterpret(FD2, 0_44)
506+
@test parse(FD2, "0.454", RoundToZero) == reinterpret(FD2, 0_45)
507+
@test parse(FD2, "0.455", RoundToZero) == reinterpret(FD2, 0_45)
508+
@test parse(FD2, "0.456", RoundToZero) == reinterpret(FD2, 0_45)
509+
510+
@test parse(FD2, "-0.444", RoundToZero) == reinterpret(FD2, -0_44)
511+
@test parse(FD2, "-0.445", RoundToZero) == reinterpret(FD2, -0_44)
512+
@test parse(FD2, "-0.446", RoundToZero) == reinterpret(FD2, -0_44)
513+
@test parse(FD2, "-0.454", RoundToZero) == reinterpret(FD2, -0_45)
514+
@test parse(FD2, "-0.455", RoundToZero) == reinterpret(FD2, -0_45)
515+
@test parse(FD2, "-0.456", RoundToZero) == reinterpret(FD2, -0_45)
516+
517+
@test parse(FD2, "0.009", RoundToZero) == reinterpret(FD2, 0_00)
518+
@test parse(FD2, "-0.009", RoundToZero) == reinterpret(FD2, 0_00)
519+
520+
@test parse(FD4, "1.5e-4", RoundToZero) == reinterpret(FD4, 0_0001)
521+
end
522+
523+
@testset "invalid" begin
524+
@test_throws OverflowError parse(FD4, "1.2e100")
525+
@test_throws ArgumentError parse(FD4, "foo")
526+
@test_throws ArgumentError parse(FD4, "1.2.3")
527+
@test_throws ArgumentError parse(FD4, "1.2", RoundUp)
528+
end
454529
end
455530

456531
end # global testset

0 commit comments

Comments
 (0)