Skip to content

Commit de71398

Browse files
committed
GH-50432: [Ruby] Add ArrowFormat::{Array,Bitmap}#==
Comparing 2 bitmaps/arrays is a common operation. We should provide content based (not identity based) comparition feature.
1 parent 463682d commit de71398

16 files changed

Lines changed: 695 additions & 103 deletions

ruby/red-arrow-format/lib/arrow-format/array.rb

Lines changed: 78 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ module ArrowFormat
2424
using FlatBuffers::AppendAsBytes if FlatBuffers.const_defined?(:AppendAsBytes)
2525

2626
class Array
27+
include Enumerable
28+
2729
attr_reader :type
2830
attr_reader :size
2931
alias_method :length, :size
@@ -64,18 +66,29 @@ def empty?
6466
@size.zero?
6567
end
6668

69+
def ==(other)
70+
return false unless other.is_a?(self.class)
71+
return true if @validity_buffer.nil? and other.validity_buffer.nil?
72+
if @offset == other.offset and
73+
@size == other.size and
74+
@validity_buffer == other.validity_buffer
75+
return true
76+
end
77+
validity_bitmap == other.validity_bitmap
78+
end
79+
6780
protected
6881
def slice!(offset, size)
6982
@offset = offset
7083
@size = size
7184
clear_cache
7285
end
7386

74-
private
7587
def validity_bitmap
7688
@validity_bitmap ||= Bitmap.new(@validity_buffer, @offset, @size)
7789
end
7890

91+
private
7992
def apply_validity(array)
8093
return array if @validity_buffer.nil?
8194
validity_bitmap.each_with_index do |is_valid, i|
@@ -159,11 +172,20 @@ def n_nulls
159172
def to_a
160173
[nil] * @size
161174
end
175+
176+
def each
177+
return to_enum(__method__) unless block_given?
178+
179+
@size.times do
180+
yield(nil)
181+
end
182+
end
162183
end
163184

164185
class PrimitiveArray < Array
165186
include BufferAlignable
166187

188+
attr_reader :values_buffer
167189
def initialize(*args)
168190
n_args = args.size
169191
if self.class.respond_to?(:type)
@@ -173,7 +195,7 @@ def initialize(*args)
173195
type = args.shift
174196
expected_n_args = "2 or 4"
175197
end
176-
args = build_data(args[0], type) if args.size == 1
198+
args = build_data(args[0], type) if n_args == 1
177199
if args.size != 3
178200
message =
179201
"wrong number of arguments " +
@@ -192,15 +214,49 @@ def to_a
192214
apply_validity(@values_buffer.values(@type.buffer_type, offset, @size))
193215
end
194216

217+
def each(&block)
218+
return to_enum(__method__) {@size} unless block_given?
219+
220+
each_value = Enumerator.new(@size) do |yielder|
221+
offset = element_size * @offset
222+
@values_buffer.each(@type.buffer_type, offset, @size) do |_, value|
223+
yielder << value
224+
end
225+
end
226+
if @validity_buffer.nil?
227+
each_value.each(&block)
228+
else
229+
validity_bitmap.zip(each_value) do |is_valid, value|
230+
if is_valid
231+
yield(value)
232+
else
233+
yield(nil)
234+
end
235+
end
236+
end
237+
end
238+
195239
def each_buffer
196-
return to_enum(__method__) unless block_given?
240+
return to_enum(__method__) {2} unless block_given?
197241

198242
yield(slice_bitmap_buffer(:validity, @validity_buffer))
199243
yield(slice_fixed_element_size_buffer(:values,
200244
@values_buffer,
201245
element_size))
202246
end
203247

248+
def ==(other)
249+
return false unless super(other)
250+
if @offset == other.offset and
251+
@size == other.size and
252+
@values_buffer == other.values_buffer
253+
return true
254+
end
255+
zip(other).all? do |value, other_value|
256+
value == other_value
257+
end
258+
end
259+
204260
private
205261
def element_size
206262
IO::Buffer.size_of(@type.buffer_type)
@@ -243,8 +299,26 @@ def to_a
243299
apply_validity(values)
244300
end
245301

302+
303+
def each(&block)
304+
return to_enum(__method__) {@size} unless block_given?
305+
306+
@values_bitmap ||= Bitmap.new(@values_buffer, @offset, @size)
307+
if @validity_buffer.nil?
308+
@values_bitmap.each(&block)
309+
else
310+
validity_bitmap.zip(@values_bitmap) do |is_valid, value|
311+
if is_valid
312+
yield(value)
313+
else
314+
yield(nil)
315+
end
316+
end
317+
end
318+
end
319+
246320
def each_buffer
247-
return to_enum(__method__) unless block_given?
321+
return to_enum(__method__) {2} unless block_given?
248322

249323
yield(slice_bitmap_buffer(:validity, @validity_buffer))
250324
yield(slice_bitmap_buffer(:values, @values_buffer))

ruby/red-arrow-format/lib/arrow-format/bitmap.rb

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,21 @@ module ArrowFormat
1919
class Bitmap
2020
include Enumerable
2121

22-
def initialize(buffer, offset, n_values)
22+
attr_reader :offset
23+
attr_reader :size
24+
alias_method :length, :size
25+
def initialize(buffer, offset, size)
2326
@buffer = buffer
2427
@offset = offset
25-
@n_values = n_values
28+
@size = size
29+
end
30+
31+
def ==(other)
32+
return false unless other.is_a?(self.class)
33+
return false unless @size == other.size
34+
zip(other).all? do |bit, other_bit|
35+
bit == other_bit
36+
end
2637
end
2738

2839
def [](i)
@@ -31,19 +42,19 @@ def [](i)
3142
end
3243

3344
def each
34-
return to_enum(__method__) unless block_given?
45+
return to_enum(__method__) {@size} unless block_given?
3546

3647
# TODO: Optimize
3748
current = -1
38-
n_bytes = (@offset + @n_values) / 8
49+
n_bytes = (@offset + @size) / 8
3950
@buffer.each(:U8, 0, n_bytes) do |offset, value|
4051
8.times do |i|
4152
current += 1
4253
next if current < @offset
4354
yield((value & (1 << (i % 8))) > 0)
4455
end
4556
end
46-
remained_bits = (@offset + @n_values) % 8
57+
remained_bits = (@offset + @size) % 8
4758
unless remained_bits.zero?
4859
value = @buffer.get_value(:U8, n_bytes)
4960
remained_bits.times do |i|
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
# Licensed to the Apache Software Foundation (ASF) under one
2+
# or more contributor license agreements. See the NOTICE file
3+
# distributed with this work for additional information
4+
# regarding copyright ownership. The ASF licenses this file
5+
# to you under the Apache License, Version 2.0 (the
6+
# "License"); you may not use this file except in compliance
7+
# with the License. You may obtain a copy of the License at
8+
#
9+
# http://www.apache.org/licenses/LICENSE-2.0
10+
#
11+
# Unless required by applicable law or agreed to in writing,
12+
# software distributed under the License is distributed on an
13+
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14+
# KIND, either express or implied. See the License for the
15+
# specific language governing permissions and limitations
16+
# under the License.
17+
18+
class TestBitmap < Test::Unit::TestCase
19+
sub_test_case("#==") do
20+
def test_no_slice
21+
buffer1 = IO::Buffer.for("\xf0")
22+
buffer2 = IO::Buffer.for("\xf0")
23+
assert_equal(ArrowFormat::Bitmap.new(buffer1, 0, 8),
24+
ArrowFormat::Bitmap.new(buffer2, 0, 8))
25+
end
26+
27+
def test_sliced
28+
buffer1 = IO::Buffer.for("\xf0")
29+
buffer2 = IO::Buffer.for("\x00\xf0")
30+
assert_equal(ArrowFormat::Bitmap.new(buffer1, 0, 8),
31+
ArrowFormat::Bitmap.new(buffer2, 8, 8))
32+
end
33+
end
34+
end
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
# Licensed to the Apache Software Foundation (ASF) under one
2+
# or more contributor license agreements. See the NOTICE file
3+
# distributed with this work for additional information
4+
# regarding copyright ownership. The ASF licenses this file
5+
# to you under the Apache License, Version 2.0 (the
6+
# "License"); you may not use this file except in compliance
7+
# with the License. You may obtain a copy of the License at
8+
#
9+
# http://www.apache.org/licenses/LICENSE-2.0
10+
#
11+
# Unless required by applicable law or agreed to in writing,
12+
# software distributed under the License is distributed on an
13+
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14+
# KIND, either express or implied. See the License for the
15+
# specific language governing permissions and limitations
16+
# under the License.
17+
18+
class TestBooleanArray < Test::Unit::TestCase
19+
sub_test_case("#initialize") do
20+
def test_no_null
21+
assert_equal([true, false],
22+
ArrowFormat::BooleanArray.new([true, false]).to_a)
23+
end
24+
25+
def test_null_multiple_bytes
26+
values = [true] * 8 + [nil, false]
27+
assert_equal(values,
28+
ArrowFormat::BooleanArray.new(values).to_a)
29+
end
30+
31+
def test_mixed
32+
assert_equal([true, nil, false],
33+
ArrowFormat::BooleanArray.new([true, nil, false]).to_a)
34+
end
35+
end
36+
37+
sub_test_case("#==") do
38+
def test_no_slice
39+
array1 = ArrowFormat::BooleanArray.new([true, nil, false])
40+
array2 = ArrowFormat::BooleanArray.new([true, nil, false])
41+
assert_equal(array1, array2)
42+
end
43+
44+
def test_sliced
45+
array1 = ArrowFormat::BooleanArray.new([true, nil, false])
46+
array2 = ArrowFormat::BooleanArray.new([true, true, nil, false, true])
47+
assert_equal(array1, array2.slice(1, 3))
48+
end
49+
end
50+
end
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
# Licensed to the Apache Software Foundation (ASF) under one
2+
# or more contributor license agreements. See the NOTICE file
3+
# distributed with this work for additional information
4+
# regarding copyright ownership. The ASF licenses this file
5+
# to you under the Apache License, Version 2.0 (the
6+
# "License"); you may not use this file except in compliance
7+
# with the License. You may obtain a copy of the License at
8+
#
9+
# http://www.apache.org/licenses/LICENSE-2.0
10+
#
11+
# Unless required by applicable law or agreed to in writing,
12+
# software distributed under the License is distributed on an
13+
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14+
# KIND, either express or implied. See the License for the
15+
# specific language governing permissions and limitations
16+
# under the License.
17+
18+
class TestFloat32Array < Test::Unit::TestCase
19+
sub_test_case("#initialize") do
20+
def test_no_null
21+
values = [-Float::INFINITY, -0.0, +0.0, +Float::INFINITY]
22+
assert_equal(values,
23+
ArrowFormat::Float32Array.new(values).to_a)
24+
end
25+
26+
def test_mixed
27+
values = [-Float::INFINITY, -0.0, nil, +0.0, +Float::INFINITY]
28+
assert_equal(values,
29+
ArrowFormat::Float32Array.new(values).to_a)
30+
end
31+
end
32+
33+
sub_test_case("#==") do
34+
def test_no_slice
35+
values = [-Float::INFINITY, -0.0, nil, +0.0, +Float::INFINITY]
36+
array1 = ArrowFormat::Float32Array.new(values)
37+
array2 = ArrowFormat::Float32Array.new(values)
38+
assert_equal(array1, array2)
39+
end
40+
41+
def test_sliced
42+
values = [-Float::INFINITY, -0.0, nil, +0.0, +Float::INFINITY]
43+
array1 = ArrowFormat::Float32Array.new(values)
44+
array2 = ArrowFormat::Float32Array.new([0.0] + values + [0.0])
45+
assert_equal(array1, array2.slice(1, 5))
46+
end
47+
end
48+
end
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
# Licensed to the Apache Software Foundation (ASF) under one
2+
# or more contributor license agreements. See the NOTICE file
3+
# distributed with this work for additional information
4+
# regarding copyright ownership. The ASF licenses this file
5+
# to you under the Apache License, Version 2.0 (the
6+
# "License"); you may not use this file except in compliance
7+
# with the License. You may obtain a copy of the License at
8+
#
9+
# http://www.apache.org/licenses/LICENSE-2.0
10+
#
11+
# Unless required by applicable law or agreed to in writing,
12+
# software distributed under the License is distributed on an
13+
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14+
# KIND, either express or implied. See the License for the
15+
# specific language governing permissions and limitations
16+
# under the License.
17+
18+
class TestFloat64Array < Test::Unit::TestCase
19+
sub_test_case("#initialize") do
20+
def test_no_null
21+
values = [-Float::INFINITY, -0.0, +0.0, +Float::INFINITY]
22+
assert_equal(values,
23+
ArrowFormat::Float64Array.new(values).to_a)
24+
end
25+
26+
def test_mixed
27+
values = [-Float::INFINITY, -0.0, nil, +0.0, +Float::INFINITY]
28+
assert_equal(values,
29+
ArrowFormat::Float64Array.new(values).to_a)
30+
end
31+
end
32+
33+
sub_test_case("#==") do
34+
def test_no_slice
35+
values = [-Float::INFINITY, -0.0, nil, +0.0, +Float::INFINITY]
36+
array1 = ArrowFormat::Float64Array.new(values)
37+
array2 = ArrowFormat::Float64Array.new(values)
38+
assert_equal(array1, array2)
39+
end
40+
41+
def test_sliced
42+
values = [-Float::INFINITY, -0.0, nil, +0.0, +Float::INFINITY]
43+
array1 = ArrowFormat::Float64Array.new(values)
44+
array2 = ArrowFormat::Float64Array.new([0.0] + values + [0.0])
45+
assert_equal(array1, array2.slice(1, 5))
46+
end
47+
end
48+
end

0 commit comments

Comments
 (0)