Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
15 changes: 15 additions & 0 deletions spec/std/big/big_decimal_spec.cr
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,21 @@ describe BigDecimal do
1.5.to_big_f.to_big_d.should eq (BigDecimal.new(15, 1))
end

it "can be converted from scientific notation" do
"-0.123e12".to_big_d.should eq (BigDecimal.new("-123000000000"))
"0.123e12".to_big_d.should eq (BigDecimal.new("123000000000"))
"0.123e+12".to_big_d.should eq (BigDecimal.new("123000000000"))
"-0.123e-7".to_big_d.should eq (BigDecimal.new("-0.0000000123"))
"-0.1e-7".to_big_d.should eq (BigDecimal.new("-0.00000001"))
"0.1e-7".to_big_d.should eq (BigDecimal.new("0.00000001"))
"1.0e-8".to_big_d.should eq (BigDecimal.new("0.00000001"))
"10e-8".to_big_d.should eq (BigDecimal.new("0.0000001"))
"1.0e+8".to_big_d.should eq (BigDecimal.new("100000000"))
"10e+8".to_big_d.should eq (BigDecimal.new("1000000000"))
"10E+8".to_big_d.should eq (BigDecimal.new("1000000000"))
"10E8".to_big_d.should eq (BigDecimal.new("1000000000"))

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

What about tests for 10.01e1 == 100.1 and 10.01e-1 == 1.001? Not all exponents are large

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Good catch! 🎉 Former case (10.01e1) uncovered bug in the implementation for which fix is included in the following commit.

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")
Expand Down
55 changes: 50 additions & 5 deletions src/big/big_decimal.cr
Original file line number Diff line number Diff line change
Expand Up @@ -47,34 +47,79 @@ struct BigDecimal < Number
def initialize(str : String)
raise InvalidBigDecimalException.new(str, "Zero size") if str.bytesize == 0

# Check str's validity and find index of .
# Check str's validity and find index of '.'
decimal_index = nil
# Check str's validity and find index of 'e'
exponent_index = nil

str.each_char_with_index do |char, index|
case char
when '-'
if index != 0
unless index == 0 || exponent_index == index - 1
raise InvalidBigDecimalException.new(str, "Unexpected '-' character")
end
when '+'
unless exponent_index == index - 1
raise InvalidBigDecimalException.new(str, "Unexpected '+' character")
end
when '.'
if decimal_index
raise InvalidBigDecimalException.new(str, "Unexpected '.' character")
end

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

why?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

why not?

@RX14 RX14 Jan 13, 2018

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

because you're making an unneccesary change which makes the code harder to read? Blank lines are an invaluable tool in writing readable code.

decimal_index = index
when 'e', 'E'
if exponent_index
raise InvalidBigDecimalException.new(str, "Unexpected #{char.inspect} character")
end
exponent_index = index
when '0'..'9'
# Pass
else
raise InvalidBigDecimalException.new(str, "Unexpected #{char.inspect} character")
end
end

if decimal_index
case
when exponent_index
exponent_postfix = str[exponent_index + 1]
case exponent_postfix
when '+', '-'
exponent_positive = exponent_postfix == '+'
exponent = str[exponent_index + 2..-1].to_u64
else
exponent_positive = true
exponent = str[exponent_index + 1..-1].to_u64
end
if decimal_index

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

you could leave some whitespace before this line

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Is that bikeshedding?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

yeah

decimals = (exponent_index - decimal_index - 1).to_u64

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

By defining

decimals = ((exponent_index || str.bytesize) - decimal_index - 1)

I think we can remove a lot of duplication here. We don't need to duplicate the @value calculation for each branch, just have

if decimal_index
  # the existing code but with the above calculation
end

if exponent_index
  # ...
end

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I'm not sure if I follow... to define

decimals = ((exponent_index || str.bytesize) - decimal_index - 1)

you'd need to do it inside if decimal_index branch, otherwise compiler will complain about possible nil in decimal_index - 1. that's where I'm lost... perhaps some code could to illustrate your point could be a help here.

@RX14 RX14 Jan 14, 2018

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I absolutely haven't tested this:

decimal_end_index = (exponent_index || str.bytesize) - 1
if decimal_index # rename to decimal_start_index ?
  decimal_count = decimal_end_index - decimal_index

  value_str = String.build do |builder|
    # We know this is ASCII, so we can slice by index
    builder.write(str.to_slice[0, decimal_index])
    builder.write(str.to_slice[decimal_index + 1, decimal_count])
  end
  @value = value_str.to_big_i
else
  decimal_count = 0
  @value = str[0..decimal_end_index].to_big_i
end

if exponent_index
  exponent_postfix = str[exponent_index + 1]
  case exponent_postfix
  when '+', '-'
    exponent_positive = exponent_postfix == '+'
    exponent = str[(exponent_index + 2)..-1].to_u64
  else
    exponent_positive = true
    exponent = str[(exponent_index + 1)..-1].to_u64
  end

  @scale = exponent
  if exponent_positive
    @scale -= decimal_count
    @value *= 10.to_big_i ** @scale
    @scale = 0_u64
  else
    @scale += decimal_count
  end
else
  @scale = decimal_count.to_u64
end

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I don't entirely understand why you do

    @value *= 10.to_big_i ** @scale
    @scale = 0_u64

in your code, couldn't you just take these lines out or am I being stupid?

value_str = String.build do |builder|
# We know this is ASCII, so we can slice by index
builder.write(str.to_slice[0, decimal_index])
builder.write(str.to_slice[decimal_index + 1, decimals])
end
@value = value_str.to_big_i
@scale = exponent
if exponent_positive
@scale -= decimals
@value *= 10.to_big_i ** @scale

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

My question still stands: why do we have to apply @scale to @value here? Isn't the point of scale to represent what you just calculated by hand?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Because @scale in BigDecimal is an UInt which represents negative scale, so we can't use it for representing the positive, we need to multiple value by it.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

oh, OK, fair enough.

@scale = 0_u64
else
@scale += decimals
end
else
@value = str[0...exponent_index].to_big_i
@scale = exponent
if exponent_positive
@value *= 10.to_big_i ** @scale
@scale = 0_u64
end
end
when decimal_index
value_str = String.build do |builder|
# We know this is ASCII, so we can slice by index
builder.write(str.to_slice[0, decimal_index])
builder.write(str.to_slice[decimal_index + 1, str.bytesize - decimal_index - 1])
end

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

why?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

it's more consistent with lack of blank lines in rest of the code in this method. I'd add them in places like before next when keyword in multiple case statement (for example on line with when decimal_index).

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Please, just leave it as-is and don't bloat the diff.

@value = value_str.to_big_i
@scale = (str.bytesize - decimal_index - 1).to_u64
else
Expand Down