-
-
Notifications
You must be signed in to change notification settings - Fork 1.6k
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
Implement BigDecimal #4876
Conversation
src/big/big_decimal.cr
Outdated
# Set maximum iterations used in division operation. This implicitly | ||
# defines the maximum precision of divisions in cases where the | ||
# division is not exact. | ||
def self.set_max_div_iterations(x : Int) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please use descriptive names for arguments.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
=> new_max_div
src/big/big_decimal.cr
Outdated
# Create a new `BigDecimal` from a String. | ||
# | ||
# Allows only valid number strings with an optional negative sign. | ||
def initialize(s : String) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ditto
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Use
ditto
to use the same comment as in the previous declaration.
https://crystal-lang.org/docs/conventions/documenting_code.html
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
=> str
@makenowjust I guess this comment didn't apply here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@makenowjust Quoted remark applies to documentation, not code-review comments, mind you.
src/big/big_decimal.cr
Outdated
end | ||
|
||
# from Int | ||
def initialize(i : Int) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ditto
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
=> num
src/big/big_decimal.cr
Outdated
end | ||
|
||
# from Float is not supported due to precision loss risks. This call fails at compile time. | ||
def initialize(f : Float) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ditto
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
=> num
src/big/big_decimal.cr
Outdated
end | ||
|
||
private def power_ten_to(x : Int) : Int | ||
BigInt.new(10) ** x |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
TEN ** x
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
=> TEN
def36c3
to
6e9a815
Compare
src/big/big_decimal.cr
Outdated
end | ||
|
||
struct Float | ||
# from Float is not supported due to precision loss risks. This call fails at compile time. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd use full sentence here, like: "Casting from Float
is not supported due to ..."
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So changed.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@vegai you should use `
around Float
in your comment: Casting from `Float` ...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Marked this Float and all the other similar ones too.
src/big/big_decimal.cr
Outdated
include Comparable(BigDecimal) | ||
|
||
# Convert `Int` to `BigDecimal` | ||
def to_big_d |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks like you forget to declare
struct BigDecimal
def to_big_d
self
end
end
to optimize BigDecimal
case of to_big_d
(we need no copy here).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes. Added.
src/big/big_decimal.cr
Outdated
elsif @scale > s.size | ||
io << "0." | ||
(@scale - s.size).times do | ||
io << "0" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You could use a Char
here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No, string is better here because need no conversion from Char to utf8 bytes.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@Sija I'm wrong! Unbelievable!
➜ crystal git:(bigfloat-frexp) ✗ bin/crystal 1.cr --no-debug --release
Using compiled compiler at `.build/crystal'
char 370.13 ( 2.7ms) (± 0.91%) fastest
string 150.32 ( 6.65ms) (± 1.97%) 2.46× slower
byte from char 12.61 ( 79.32ms) (± 2.65%) 29.36× slower
byte from string 133.63 ( 7.48ms) (± 1.40%) 2.77× slower
byte 132.72 ( 7.53ms) (± 1.82%) 2.79× slower
require "benchmark"
Benchmark.ips do |x|
x.report("char") { io = IO::Memory.new; 1_000_000.times { io << '.' } }
x.report("string") { io = IO::Memory.new; 1_000_000.times { io << "." } }
x.report("byte from char") { io = IO::Memory.new; 1_000_000.times { io << '.'.bytes } }
x.report("byte from string") { io = IO::Memory.new; 1_000_000.times { io << ".".byte_at(0) } }
x.report("byte") { b = ".".byte_at(0); io = IO::Memory.new; 1_000_000.times { io << b } }
end
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@vegai Could you please include above tweaks?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Huh, interesting :) Seems almost like a performance bug somewhere.
But will change these to chars then.
src/big/big_decimal.cr
Outdated
io << s | ||
else | ||
offset = s.size - @scale | ||
io << s[0...offset] << "." << s[offset..-1] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ditto
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done
LGTM |
|
||
(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 |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added < tests
There was a problem hiding this comment.
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
spec/std/big/big_decimal_spec.cr
Outdated
(BigDecimal.new("-1") < BigDecimal.new("1")).should be_true | ||
end | ||
|
||
it "arithmetic that beats float precision" do |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
it "keeps precision"
? In general these it
names don't flow well.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, I wasn't thinking at all there. Changed all the descriptions.
src/big/big_decimal.cr
Outdated
|
||
include Comparable(BigDecimal) | ||
include Comparable(Int) | ||
include Comparable(String) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We shouldn't be comparable with string (at least I don't think any other numeric is) but we should be comparable with all other numbers not just int. We shouldn't need multiple comparable includes here, just Comparable(Numeric)
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Replaced Int and String with Comparable(Number)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oops, misremembered the name. Thanks!
src/big/big_decimal.cr
Outdated
struct BigDecimal | ||
ZERO = BigInt.new(0) | ||
TEN = BigInt.new(10) | ||
@@max_div_iterations = 100_u64 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please use class_property
. I'm not entirely sure I like that this is global, maybe we should make it configurable per div call with the default being this property.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, I thought it was a bit awkward too. The original algorithm in Rust just had this value hardcoded inside the div method. I'll take a look at configuring it per div call. I cannot make this an optional parameter of /
though, can I?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Made it configurable per div call as suggested: added a div function with the optional parameter, and made / call that one. DEFAULT_MAX_DIV_ITERATIONS is now a class constant.
src/big/big_decimal.cr
Outdated
|
||
private property value : BigInt | ||
private property scale : UInt64 | ||
getter value, scale |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please don't do this. Use a public getter and simply use @value
when you want to set.
For that matter, you should never need to set these values because this struct should be immutable.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm, I didn't quite get the gist here. Doesn't this combo make the value immutable from the outside world? Or does Crystal have better immutability protections that I've missed?
src/big/big_decimal.cr
Outdated
check_division_by_zero other | ||
|
||
scale = @scale - other.scale | ||
n, d = @value, other.@value |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please don't use single character variable names.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed.
src/big/big_decimal.cr
Outdated
scale = @scale - other.scale | ||
n, d = @value, other.@value | ||
|
||
quotient, remainder = n.tdiv(d), n.remainder(d) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could you use divmod here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
They're not quite the same. divmod
floors negative integers down, which doesn't work here without additional changes.
1.divmod(-2) => {-1, -1}
1.tdiv(-2) => 0
1.remainder(-2) => 1
But I'll think a bit if it would be a good idea to adapt the algo to divmod.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Huh, unearthed a bug here. -1 / -2 gives us -0.5 currently.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed bugs, added bunch of div tests, and moved to divmod.
src/big/big_decimal.cr
Outdated
end | ||
|
||
# from `Int` | ||
def initialize(num : Int) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There should be a far faster way to initialize from integers than via to_s.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh, whoops. Somebody was lazy :P Fixed.
src/big/big_decimal.cr
Outdated
remainder = remainder * TEN | ||
|
||
i = 0 | ||
while remainder != ZERO && i < @@max_div_iterations |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Shouldn't you raise if you reach max_div_iterations, not give the wrong result.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's not very exceptional, since simple calculations like 1/3 hit this.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh never mind then. I didn't read the code properly.
src/big/big_decimal.cr
Outdated
struct Float | ||
# Casting from `Float` is not supported due to precision loss risks. This call fails at compile time. | ||
def to_big_d | ||
{% raise "Initializing from Float is risky due to loss of precision -- convert rather from Int or String" %} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ditto
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same as before, I guess?
b8b2025
to
8a73f37
Compare
spec/std/big/big_decimal_spec.cr
Outdated
end | ||
|
||
it "keeps precision" do | ||
oneThousandth = BigDecimal.new("0.001") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
please, use snake_case
for variable names; we ain't in JavaScript Land Dorothy ;)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Heh. Fixed.
e1c41d9
to
d3728f0
Compare
src/big/big_decimal.cr
Outdated
return BigDecimal.new(quotient, scale) if remainder == ZERO | ||
# quotient, remainder = n.tdiv(d), n.remainder(d) | ||
quotient, remainder = n.divmod(d) | ||
puts "n #{n} d #{d} q #{quotient} r #{remainder}" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Debug leftovers.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, intermediate commits to save work. Removed afterwards
struct BigDecimal | ||
ZERO = BigInt.new(0) | ||
TEN = BigInt.new(10) | ||
DEFAULT_MAX_DIV_ITERATIONS = 100_u64 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Personally I wouldn't bother making this a constant but it's not worth any effort making it not a constant.
So ignore this comment.
5470f7b
to
9346020
Compare
Looks like You forgot about struct BigDecimal
# Returns *num*. Useful for generic code that does `T.new(...)` with `T`
# being a `Number`.
def self.new(num : BigDecimal)
num
end Some info here: #2292. |
e050e69
to
1b202bb
Compare
@akzhan Thanks, added that. |
Added to_f/u/i implementations and a few specs. Also rebased against master and changed |
src/big/big_decimal.cr
Outdated
(@value / TEN ** @scale) | ||
else | ||
-(@value.abs / TEN ** @scale) | ||
end.to_i |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No, we never chain a method call after end
. The correct way to write this is to stick the to_i
on the end of the if branches, which works because of explicit return.
If this wasn't the final (only) statement in the method, we simply assign to a variable inside if:
if @value >= 0
int_value = (@value / TEN ** @scale).to_i
else
int_value = -(@value.abs / TEN ** @scale).to_i
end
# use int_value
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Righto. Fixed.
Should this PR be in the "Numbers" project? |
src/big/big_decimal.cr
Outdated
# defines a maximum number of iterations in case the division is not exact. | ||
# | ||
# ``` | ||
# BigDecimal(1).div(BigDecimal(2)) => BigDecimal(@value=5, @scale=2) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Missing #
before =>
. On the line below as well.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Roger, fixed.
src/big/big_decimal.cr
Outdated
div other | ||
end | ||
|
||
# Divides self with another `BigDecimal`, with a optionally configurable max_div_iterations, which |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
max_div_iterations
-> *max_div_iterations*
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed.
src/big/big_decimal.cr
Outdated
hasher.string(self.to_s) | ||
end | ||
|
||
# Returns the quotient as absolutely negative if self and other have different signs, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
quotient
-> *quotient*
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed (also line below).
Thanks!
Thank you! 🎉 |
Thank you @vegai! Amazing work, and outstanding patience to bear with reviews :) |
end | ||
|
||
def hash(hasher) | ||
hasher.string(self.to_s) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Of course it must be rewritten using number normalization.
But it will be anyway later.
This PR added |
btw, why |
This PR adds a BigDecimal implementation.
The motivation for having BigDecimals is to have an arbitrary precision exact decimal type. This is required at least in financial calculations, where the precision of Floats and is not enough and BigRationals may have other problems due to denominator potentially being something else than 10**x. At least Ruby, Python and Java contain something like this in their standard libs.
The design is mostly adapted from bigdecimal-rs written for Rust. It holds its actual value in a BigInt, and a scale (decimal point place) in UInt64. It currently contains basic + - * / arithmetics. Possible future needs: implementation of ** (power) and supporting scientific "123E45" notation.
Please critique liberally. This would be my first larger Crystal contribution.