Skip to content
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
47 changes: 43 additions & 4 deletions spec/std/range_spec.cr
Original file line number Diff line number Diff line change
Expand Up @@ -669,10 +669,49 @@ describe "Range" do
end

describe "#size" do
it "optimizes for int range" do
(5..12).size.should eq(8)
(5...12).size.should eq(7)
(5..4).size.should eq(0)
describe "Int" do
it { (5..12).size.should eq(8) }
it { (5...12).size.should eq(7) }
it { (5..4).size.should eq(0) }
it { (0..0).size.should eq(1) }
it { (0...0).size.should eq(0) }
it { (1..1).size.should eq(1) }
it { (1...1).size.should eq(0) }

it { (-12..-5).size.should eq(8) }
it { (-12...-5).size.should eq(7) }
it { (-4..-5).size.should eq(0) }
it { (-1..-1).size.should eq(1) }
it { (-1...-1).size.should eq(0) }

it { (-3..3).size.should eq(7) }
it { (-3...3).size.should eq(6) }
it { (-3..0).size.should eq(4) }
it { (-3...0).size.should eq(3) }
it { (3..-3).size.should eq(0) }
it { (3...-3).size.should eq(0) }
it { (-128_i8..0_i8).size.should eq(129) }
it { (-128_i8...0_i8).size.should eq(128) }

it { (Int32::MAX..Int32::MAX).size.should eq(1) }
it { (Int32::MAX...Int32::MAX).size.should eq(0) }
it { (-Int32::MAX..-Int32::MAX).size.should eq(1) }
it { (-Int32::MAX...-Int32::MAX).size.should eq(0) }

it { (5_u8..12_u8).size.should eq(8) }
it { (5_u8...12_u8).size.should eq(7) }
it { (5_u8..4_u8).size.should eq(0) }
it { (0_u8..0_u8).size.should eq(1) }
it { (0_u8...0_u8).size.should eq(0) }
it { (1_u8..1_u8).size.should eq(1) }
it { (1_u8...1_u8).size.should eq(0) }
it { (UInt8::MAX..UInt8::MAX).size.should eq(1) }
it { (UInt8::MAX...UInt8::MAX).size.should eq(0) }

it { (-32768_i16..254_u8).size.should eq(33023) }
it { (-128_i8..127_i8).size.should eq(256) }
it { (Int32::MIN..-127_i8).size.should eq(2_147_483_522) }
it { ((Int16::MIN.to_i32 - 1)..127_i16).size.should eq(32897) }
end

it "works for other types" do
Expand Down
14 changes: 11 additions & 3 deletions src/range.cr
Original file line number Diff line number Diff line change
Expand Up @@ -489,9 +489,17 @@ struct Range(B, E)

# Optimized implementation for int range
if b.is_a?(Int) && e.is_a?(Int)
e -= 1 if @exclusive
n = e - b + 1
n < 0 ? 0 : n.to_i32
return 0 if e < b

# Convert `e` to `Int32` in order to ensure that `e &- b` doesn't get
# truncated due to the smaller type of `e`.
if e.is_a?(UInt8 | Int8 | UInt16 | Int16)
e = e.to_i32!
end

diff = (e &- b).to_i32.abs

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.

This can't be right.. how can it be safe to just not check for overflows 🤔
We should double-check it, I'll probably do so today

@straight-shoota straight-shoota Sep 6, 2024

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Good catch. e < b only excludes the obvious case. 🤦
We can still end up with a difference that doesn't fit into E (e.g. (Int8::MIN..1i8).size). But it would fit into Int32.

I suppose we could use non-wrapping arithmetics and cast e to Int32 if it's a smaller type. That should probably cover every constellation where the result fits into Int32.

@straight-shoota straight-shoota Sep 6, 2024

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

There are so many edge cases to take into account 🤯
I'm wondering if we should collect a set of
for testing Range methods with optimized implementations for Int (i.e. the ones mentioned in #13648: size, sum, step.sum, map; maybe also sample).

diff &+= 1 unless @exclusive
diff
else
if b.nil? || e.nil?
raise ArgumentError.new("Can't calculate size of an open range")
Expand Down