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

Implement BigDecimal #4876

Merged
merged 24 commits into from
Nov 7, 2017
Merged
Show file tree
Hide file tree
Changes from 23 commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
f0e011c
Implement BigDecimal
vegai Aug 19, 2017
4701432
Add a few missing >= <= tests
vegai Sep 19, 2017
7aea1ba
Fix BigInt <=> Int case
vegai Sep 19, 2017
094fee4
Make init from Float be a warning instead
vegai Sep 21, 2017
0a89e94
Implement Float.to_big_d test
vegai Sep 21, 2017
9a13f99
Remove warning, fix comments
vegai Sep 22, 2017
319ecbc
Improve docstrings
vegai Oct 3, 2017
34268c3
Add a failing test for hashing contract
vegai Oct 10, 2017
9b313b4
Add a failing test for a BigDecimal.to_s that has trailing zeros
vegai Oct 10, 2017
5a2f543
Implement hasher by a simple to_s
vegai Oct 10, 2017
15c107f
Add a factoring powers of ten simplifier, make private funs public
vegai Oct 10, 2017
a717b6c
Add test for normalize_quotient
vegai Oct 10, 2017
811eef5
Factor powers of ten away before doing rest of to_s
vegai Oct 10, 2017
12cfa4e
Move methods around for better structure.
vegai Oct 10, 2017
9bf8f89
Change require big_int to big as per #5121
vegai Oct 16, 2017
c2e7846
Add a failing to_s test
vegai Oct 16, 2017
a1fabe4
Fix to_s on negative numbers > -1.0
vegai Oct 16, 2017
3758be1
Add failing to_i/u/f conversion specs
vegai Oct 16, 2017
9cb9664
Fix few to_s edge cases. Implement to_f/i/u
vegai Oct 16, 2017
d80a81b
More concise hash invariant check
vegai Oct 16, 2017
10e9a05
Document to_i and to_u's truncation behaviour
vegai Oct 16, 2017
e761944
Don't call to_i on a block
vegai Oct 16, 2017
70b2a7b
Combine redundant branches away
vegai Oct 16, 2017
3a36303
CR fixes
vegai Nov 7, 2017
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
326 changes: 326 additions & 0 deletions spec/std/big/big_decimal_spec.cr
Original file line number Diff line number Diff line change
@@ -0,0 +1,326 @@
require "spec"
require "big_decimal"

describe BigDecimal do
it "initializes from valid input" do
BigDecimal.new
.should eq(BigDecimal.new(BigInt.new(0)))

BigDecimal.new("41.0123")
.should eq(BigDecimal.new(BigInt.new(410123), 4))

BigDecimal.new(1)
.should eq(BigDecimal.new(BigInt.new(1)))

BigDecimal.new(-1)
.should eq(BigDecimal.new(BigInt.new(-1)))

BigDecimal.new(0)
.should eq(BigDecimal.new(BigInt.new(0)))

BigDecimal.new("42.0123")
.should eq(BigDecimal.new(BigInt.new(420123), 4))

BigDecimal.new("0.0")
.should eq(BigDecimal.new(BigInt.new(0)))

BigDecimal.new(".2")
.should eq(BigDecimal.new(BigInt.new(2), 1))

BigDecimal.new("2.")
.should eq(BigDecimal.new(BigInt.new(2)))

BigDecimal.new("-.2")
.should eq(BigDecimal.new(BigInt.new(-2), 1))

BigDecimal.new("-2.")
.should eq(BigDecimal.new(BigInt.new(-2)))

BigDecimal.new("-0.1")
.should eq(BigDecimal.new(BigInt.new(-1), 1))

BigDecimal.new("-1.1")
.should eq(BigDecimal.new(BigInt.new(-11), 1))

BigDecimal.new("123871293879123790874230984702938470917238971298379127390182739812739817239087123918273098.1029387192083710928371092837019283701982370918237")
.should eq(BigDecimal.new(BigInt.new("1238712938791237908742309847029384709172389712983791273901827398127398172390871239182730981029387192083710928371092837019283701982370918237".to_big_i), 49))

BigDecimal.new("-123871293879123790874230984702938470917238971298379127390182739812739817239087123918273098.1029387192083710928371092837019283701982370918237")
.should eq(BigDecimal.new(BigInt.new("-1238712938791237908742309847029384709172389712983791273901827398127398172390871239182730981029387192083710928371092837019283701982370918237".to_big_i), 49))

BigDecimal.new("-0.1029387192083710928371092837019283701982370918237")
.should eq(BigDecimal.new(BigInt.new("-1029387192083710928371092837019283701982370918237".to_big_i), 49))

BigDecimal.new("2")
.should eq(BigDecimal.new(BigInt.new(2)))

BigDecimal.new("-1")
.should eq(BigDecimal.new(BigInt.new(-1)))

BigDecimal.new("0")
.should eq(BigDecimal.new(BigInt.new(0)))

BigDecimal.new("-0")
.should eq(BigDecimal.new(BigInt.new(0)))

BigDecimal.new(BigDecimal.new(2))
.should eq(BigDecimal.new(2.to_big_i))
end

it "raises InvalidBigDecimalException when initializing from invalid input" do
expect_raises(InvalidBigDecimalException) do
BigDecimal.new("derp")
end

expect_raises(InvalidBigDecimalException) do
BigDecimal.new("")
end

expect_raises(InvalidBigDecimalException) do
BigDecimal.new("1.2.3")
end

expect_raises(InvalidBigDecimalException) do
BigDecimal.new("..2")
end

expect_raises(InvalidBigDecimalException) do
BigDecimal.new("1..2")
end

expect_raises(InvalidBigDecimalException) do
BigDecimal.new("a1.2")
end

expect_raises(InvalidBigDecimalException) do
BigDecimal.new("1a.2")
end

expect_raises(InvalidBigDecimalException) do
BigDecimal.new("1.a2")
end

expect_raises(InvalidBigDecimalException) do
BigDecimal.new("1.2a")
end
end

it "performs arithmetic with bigdecimals" do
BigDecimal.new(0).should eq(BigDecimal.new(0) + BigDecimal.new(0))
BigDecimal.new(1).should eq(BigDecimal.new(0) + BigDecimal.new(1))
BigDecimal.new(1).should eq(BigDecimal.new(1) + BigDecimal.new(0))
BigDecimal.new(0).should eq(BigDecimal.new(1) + BigDecimal.new(-1))
BigDecimal.new(0).should eq(BigDecimal.new(-1) + BigDecimal.new(1))
BigDecimal.new("0.1").should eq(BigDecimal.new("-1.1") + BigDecimal.new("1.2"))
BigDecimal.new("0.076543211").should eq(BigDecimal.new("-1.123456789") + BigDecimal.new("1.2"))
BigDecimal.new("0.13456789").should eq(BigDecimal.new("-1.1") + BigDecimal.new("1.23456789"))

BigDecimal.new(0).should eq(BigDecimal.new(0) - BigDecimal.new(0))
BigDecimal.new(-1).should eq(BigDecimal.new(0) - BigDecimal.new(1))
BigDecimal.new(1).should eq(BigDecimal.new(1) - BigDecimal.new(0))
BigDecimal.new(2).should eq(BigDecimal.new(1) - BigDecimal.new(-1))
BigDecimal.new(-2).should eq(BigDecimal.new(-1) - BigDecimal.new(1))
BigDecimal.new(1).should eq(BigDecimal.new("1.12345") - BigDecimal.new("0.12345"))
BigDecimal.new("1.0000067").should eq(BigDecimal.new("1.1234567") - BigDecimal.new("0.12345"))
BigDecimal.new("0.9999933").should eq(BigDecimal.new("1.12345") - BigDecimal.new("0.1234567"))

BigDecimal.new(0).should eq(BigDecimal.new(0) * BigDecimal.new(0))
BigDecimal.new(0).should eq(BigDecimal.new(0) * BigDecimal.new(1))
BigDecimal.new(0).should eq(BigDecimal.new(1) * BigDecimal.new(0))
BigDecimal.new(-1).should eq(BigDecimal.new(1) * BigDecimal.new(-1))
BigDecimal.new(-1).should eq(BigDecimal.new(-1) * BigDecimal.new(1))
BigDecimal.new("1.2621466432").should eq(BigDecimal.new("1.12345") * BigDecimal.new("1.123456"))
BigDecimal.new("1.2621466432").should eq(BigDecimal.new("1.123456") * BigDecimal.new("1.12345"))

expect_raises(DivisionByZero) do
BigDecimal.new(0) / BigDecimal.new(0)
end
expect_raises(DivisionByZero) do
BigDecimal.new(1) / BigDecimal.new(0)
end
expect_raises(DivisionByZero) do
BigDecimal.new(-1) / BigDecimal.new(0)
end
BigDecimal.new(1).should eq(BigDecimal.new(1) / BigDecimal.new(1))
BigDecimal.new(5.to_big_i, 1_u64).should eq(BigDecimal.new(1) / BigDecimal.new(2))
BigDecimal.new(-5.to_big_i, 1_u64).should eq(BigDecimal.new(1) / BigDecimal.new(-2))
BigDecimal.new(-5.to_big_i, 4_u64).should eq(BigDecimal.new(1) / BigDecimal.new(-2000))
BigDecimal.new(-500.to_big_i, 0).should eq(BigDecimal.new(1000) / BigDecimal.new(-2))
BigDecimal.new(-500.to_big_i, 0).should eq(BigDecimal.new(-1000) / BigDecimal.new(2))
BigDecimal.new(500.to_big_i, 0).should eq(BigDecimal.new(-1000) / BigDecimal.new(-2))
BigDecimal.new(5.to_big_i, 1_u64).should eq(BigDecimal.new(-1) / BigDecimal.new(-2))
BigDecimal.new(5.to_big_i, 4_u64).should eq(BigDecimal.new(-1) / BigDecimal.new(-2000))
BigDecimal.new(500.to_big_i, 0).should eq(BigDecimal.new(-1000) / BigDecimal.new(-2))
BigDecimal.new(0).should eq(BigDecimal.new(0) / BigDecimal.new(1))
BigDecimal.new("3333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333".to_big_i, 100_u64).should eq(BigDecimal.new(1) / BigDecimal.new(3))

BigDecimal.new(33333.to_big_i, 5_u64).should eq(BigDecimal.new(1).div(BigDecimal.new(3), 5))
BigDecimal.new(33.to_big_i, 5_u64).should eq(BigDecimal.new(1).div(BigDecimal.new(3000), 5))

BigDecimal.new(3333333.to_big_i, 7_u64).should eq(BigDecimal.new(1).div(BigDecimal.new(3), 7))
BigDecimal.new(3333.to_big_i, 7_u64).should eq(BigDecimal.new(1).div(BigDecimal.new(3000), 7))
end

it "can be converted from other types" do
1.to_big_d.should eq (BigDecimal.new(1))
"1.5".to_big_d.should eq (BigDecimal.new(15, 1))
BigInt.new(15).to_big_d.should eq (BigDecimal.new(15, 0))
1.5.to_big_d.should eq (BigDecimal.new(15, 1))
end

it "is comparable with other types" do
BigDecimal.new("1.0").should eq BigDecimal.new("1")
BigDecimal.new("1").should eq BigDecimal.new("1.0")
BigDecimal.new(1, 10).should eq BigDecimal.new(10, 11)
BigDecimal.new(10, 11).should eq BigDecimal.new(1, 10)

(BigDecimal.new(1) > BigDecimal.new(1)).should be_false
(BigDecimal.new("1.00000000000000000000000000000000000001") > BigDecimal.new(1)).should be_true
(BigDecimal.new("0.99999999999999999999999999999999999999") > BigDecimal.new(1)).should be_false
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You should test less than here.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added < tests

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(BigDecimal.new("0.99999999999999999999999999999999999999") < BigDecimal.new(1)).should be_true

BigDecimal.new("1.00000000000000000000000000000000000000").should eq BigDecimal.new(1)

(1 < BigDecimal.new(1)).should be_false

(1 < BigDecimal.new(1)).should be_false
(BigDecimal.new(1) < 1).should be_false
(2 < BigDecimal.new(1)).should be_false
(BigDecimal.new(2) < 1).should be_false
(BigDecimal.new("-1") > BigDecimal.new("1")).should be_false

(1 > BigDecimal.new(1)).should be_false
(BigDecimal.new(1) > 1).should be_false
(2 > BigDecimal.new(1)).should be_true
(BigDecimal.new(2) > 1).should be_true
(BigDecimal.new("-1") < BigDecimal.new("1")).should be_true

(1 >= BigDecimal.new(1)).should be_true

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One more test for <=? :).

(2 >= BigDecimal.new(1)).should be_true

(1 <= BigDecimal.new(1)).should be_true
(0 <= BigDecimal.new(1)).should be_true

(BigDecimal.new("6.5") > 7).should be_false
(BigDecimal.new("7.5") > 6).should be_true
end

it "keeps precision" do
one_thousandth = BigDecimal.new("0.001")
one = BigDecimal.new("1")

x = BigDecimal.new
1000.times do
x += one_thousandth
end
one.should eq(x)

x = BigDecimal.new("2")
1000.times do
x -= one_thousandth
end
one.should eq(x)
end

it "converts to string" do
BigDecimal.new.to_s.should eq "0"
BigDecimal.new(0).to_s.should eq "0"
BigDecimal.new(1).to_s.should eq "1"
BigDecimal.new(-1).to_s.should eq "-1"
BigDecimal.new("0.01").to_s.should eq "0.01"
BigDecimal.new("-0.01").to_s.should eq "-0.01"
BigDecimal.new("0.00123").to_s.should eq "0.00123"
BigDecimal.new("-0.00123").to_s.should eq "-0.00123"
BigDecimal.new("1.0").to_s.should eq "1"
BigDecimal.new("-1.0").to_s.should eq "-1"
BigDecimal.new("1.000").to_s.should eq "1"
BigDecimal.new("-1.000").to_s.should eq "-1"
BigDecimal.new("1.0001").to_s.should eq "1.0001"
BigDecimal.new("-1.0001").to_s.should eq "-1.0001"

(BigDecimal.new(1).div(BigDecimal.new(3), 9)).to_s.should eq "0.333333333"
(BigDecimal.new(1000).div(BigDecimal.new(3000), 9)).to_s.should eq "0.333333333"
(BigDecimal.new(1).div(BigDecimal.new(3000), 9)).to_s.should eq "0.000333333"

(BigDecimal.new("112839719283").div(BigDecimal.new("3123779"), 9)).to_s.should eq "36122.824080384"
(BigDecimal.new("112839719283").div(BigDecimal.new("3123779"), 14)).to_s.should eq "36122.8240803846879"

BigDecimal.new(1, 2).to_s.should eq "0.01"
BigDecimal.new(100, 4).to_s.should eq "0.01"
end

it "converts to other number types" do
bd1 = BigDecimal.new(123, 5)
bd2 = BigDecimal.new(-123, 5)
bd3 = BigDecimal.new(123, 0)
bd4 = BigDecimal.new(-123, 0)

bd1.to_i.should eq 0
bd2.to_i.should eq 0
bd3.to_i.should eq 123
bd4.to_i.should eq -123

bd1.to_u.should eq 0
bd2.to_u.should eq 0
bd3.to_u.should eq 123
bd4.to_u.should eq 123

bd1.to_f.should eq 0.00123
bd2.to_f.should eq -0.00123
bd3.to_f.should eq 123.0
bd4.to_f.should eq -123.0
end

it "hashes" do
bd1 = BigDecimal.new("123.456")
bd2 = BigDecimal.new("0.12345")
bd3 = BigDecimal.new("1.23456")
bd4 = BigDecimal.new("123456")
bd5 = BigDecimal.new("0")

hash = {} of BigDecimal => String
hash[bd1] = "bd1"
hash[bd2] = "bd2"
hash[bd3] = "bd3"
hash[bd4] = "bd4"
hash[bd5] = "bd5"

# regular cases
hash[BigDecimal.new("123.456")].should eq "bd1"
hash[BigDecimal.new("0.12345")].should eq "bd2"
hash[BigDecimal.new("1.23456")].should eq "bd3"
hash[BigDecimal.new("123456")].should eq "bd4"
hash[BigDecimal.new("0")].should eq "bd5"
Copy link
Member

@oprypin oprypin Oct 10, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you explain what this test actually checks? The hashing method of BigDecimal could literally always return 0 and the test would still pass. Seems like more of a Hash test than a #hash test.

What I think this checks:

  • That BigDecimal#hash does not raise an exception.
  • That BigDecimal#hash outputs the same value every time (not randomized).
  • That BigDecimal#== works correctly.

But there are more direct ways to check that.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, seems to me like this is a useful test, even if indeed a bit redundant. It's checking that basic hashing scenarios with BigDecimal keys is working.


# not found
expect_raises(KeyError) do
hash[BigDecimal.new("4")]
end
end

it "upkeeps hashing invariant" do
# a == b => h[a] == h[b]
bd1 = BigDecimal.new(1, 2)
bd2 = BigDecimal.new(100, 4)

bd1.hash.should eq bd2.hash
end

it "can normalize quotient" do
positive_one = BigDecimal.new("1.0")
negative_one = BigDecimal.new("-1.0")

positive_ten = BigInt.new(10)
negative_ten = BigInt.new(-10)

positive_one.normalize_quotient(positive_one, positive_ten).should eq(positive_ten)
positive_one.normalize_quotient(positive_one, negative_ten).should eq(negative_ten)

positive_one.normalize_quotient(negative_one, positive_ten).should eq(negative_ten)
positive_one.normalize_quotient(negative_one, negative_ten).should eq(negative_ten)

negative_one.normalize_quotient(positive_one, positive_ten).should eq(negative_ten)
negative_one.normalize_quotient(positive_one, negative_ten).should eq(negative_ten)

negative_one.normalize_quotient(negative_one, positive_ten).should eq(positive_ten)
negative_one.normalize_quotient(negative_one, negative_ten).should eq(negative_ten)
end
end
1 change: 1 addition & 0 deletions src/big.cr
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,4 @@ require "./big/lib_gmp"
require "./big/big_int"
require "./big/big_float"
require "./big/big_rational"
require "./big/big_decimal"
Loading