Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

DecTests: script and documentation #76

Merged
merged 1 commit into from
Oct 28, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 17 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@

## Decimals.jl: Arbitrary precision decimal floating point arithmetics in Julia
# Decimals.jl: Arbitrary precision decimal floating point arithmetics in Julia

[![Coverage Status](https://coveralls.io/repos/github/JuliaMath/Decimals.jl/badge.svg?branch=master)](https://coveralls.io/github/JuliaMath/Decimals.jl?branch=master)

Expand Down Expand Up @@ -195,6 +194,22 @@ Unlike another Julia package called [`DecFP`](https://github.com/JuliaMath/DecFP

The closest equivalent (and inspiration) for the present package in Python is the standard built-in [`decimal`](https://docs.python.org/3.7/library/decimal.html) package, which is based on [General Decimal Arithmetic Specification by IBM](http://speleotrove.com/decimal/decarith.html). Since version 3.3 of Python, it is actually [`libmpdec`](http://www.bytereef.org/mpdecimal/index.html)/[`cdecimal`](https://www.bytereef.org/mpdecimal/doc/cdecimal/index.html) that is under the hood.

## Development

### Standard tests

There is a standard test suite called
[DecTests](https://speleotrove.com/decimal/dectest.html). The test suite is
provided in a [custom format](https://speleotrove.com/decimal/dtfile.html). We
have a script `scripts/dectest.jl` for translating test cases from the custom
format to common Julia tests. The script should be called like this:
```
julia scripts/dectest.jl <testset name> <dectest path> <output path>
```
For example:
`julia scripts/dectest.jl Plus dectests/plus.decTest test/dectests/test_plus.jl`.
We put these test files into the `test/dectests` subdirectory.

## Further reading

1. What Every Programmer Should Know About Floating-Point Arithmetic!?!
Expand Down
134 changes: 134 additions & 0 deletions scripts/dectest.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
function _precision(line)
m = match(r"^precision:\s*(\d+)$", line)
return parse(Int, m[1])
end

function _rounding(line)
m = match(r"^rounding:\s*(\w+)$", line)
return Symbol(m[1])
end

function _maxexponent(line)
m = match(r"^maxexponent:\s*(\d+)$", line)
return parse(Int, m[1])
end

function _minexponent(line)
m = match(r"^minexponent:\s*(-\d+)$", line)
return parse(Int, m[1])
end

function _test(line)
lhs, rhs = split(line, "->")
id, operation, operands... = split(lhs)
result, conditions... = split(rhs)
conditions = Symbol.(lowercase.(conditions))
return (;id, operation, operands, result, conditions)
end

function decimal(x)
x = strip(x, ['\'', '\"'])
return "dec\"$x\""
end

print_precision(io, p::Int) = println(io, " setprecision(Decimal, $p)")
print_maxexponent(io, e::Int) = println(io, " Decimals.CONTEXT.Emax = $e")
print_minexponent(io, e::Int) = println(io, " Decimals.CONTEXT.Emin = $e")
function print_rounding(io, r::Symbol)
modes = Dict(:ceiling => "RoundUp",
:down => "RoundToZero",
:floor => "RoundDown",
:half_even => "RoundNearest",
:half_up => "RoundNearestTiesAway",
:up => "RoundFromZero",
:half_down => "RoundHalfDownUnsupported",
Symbol("05up") => "Round05UpUnsupported")
haskey(modes, r) || throw(ArgumentError(r))
rmod = modes[r]
println(io, " setrounding(Decimal, $rmod)")
end

function print_operation(io, operation, operands)
if operation == "plus"
print_plus(io, operands...)
elseif operation == "minus"
print_minus(io, operands...)
elseif operation == "compare"
print_compare(io, operands...)
else
throw(ArgumentError(operation))
end
end
print_compare(io, x, y) = print(io, "cmp(", decimal(x), ", ", decimal(y), ")")
print_minus(io, x) = print(io, "-(", decimal(x), ")")
print_plus(io, x) = print(io, "+(", decimal(x), ")")

function print_test(io, test)
println(io, " # $(test.id)")

if :overflow ∈ test.conditions
print(io, " @test_throws OverflowError ")
print_operation(io, test.operation, test.operands)
println(io)
else
print(io, " @test ")
print_operation(io, test.operation, test.operands)
print(io, " == ")
println(io, decimal(test.result))
end
end

function isspecial(value)
value = lowercase(value)
return occursin(r"(inf|nan|#)", value)
end

function translate(io, line)
isempty(line) && return
startswith(line, "--") && return

line = lowercase(line)

if startswith(line, "version:")
# ...
elseif startswith(line, "extended:")
# ...
elseif startswith(line, "clamp:")
# ...
elseif startswith(line, "precision:")
precision = _precision(line)
print_precision(io, precision)
elseif startswith(line, "rounding:")
rounding = _rounding(line)
print_rounding(io, rounding)
elseif startswith(line, "maxexponent:")
maxexponent = _maxexponent(line)
print_maxexponent(io, maxexponent)
elseif startswith(line, "minexponent:")
minexponent = _minexponent(line)
print_minexponent(io, minexponent)
else
test = _test(line)
any(isspecial, test.operands) && return
print_test(io, test)
end
end

function (@main)(args=ARGS)
name, dectest_path, output_path = args

open(output_path, "w") do io
println(io, """
using Decimals
using Test

@testset \"$name\" begin""")

for line in eachline(dectest_path)
translate(io, line)
end

println(io, "end")
end
end

Loading