Skip to content

Commit fbf7513

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 fbf7513

16 files changed

Lines changed: 770 additions & 104 deletions

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

Lines changed: 77 additions & 5 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,28 @@ def empty?
6466
@size.zero?
6567
end
6668

69+
def ==(other)
70+
return false unless other.is_a?(self.class)
71+
return false unless @size == other.size
72+
return true if @validity_buffer.nil? and other.validity_buffer.nil?
73+
if @offset == other.offset and @validity_buffer == other.validity_buffer
74+
return true
75+
end
76+
validity_bitmap == other.validity_bitmap
77+
end
78+
6779
protected
6880
def slice!(offset, size)
6981
@offset = offset
7082
@size = size
7183
clear_cache
7284
end
7385

74-
private
7586
def validity_bitmap
7687
@validity_bitmap ||= Bitmap.new(@validity_buffer, @offset, @size)
7788
end
7889

90+
private
7991
def apply_validity(array)
8092
return array if @validity_buffer.nil?
8193
validity_bitmap.each_with_index do |is_valid, i|
@@ -159,11 +171,20 @@ def n_nulls
159171
def to_a
160172
[nil] * @size
161173
end
174+
175+
def each
176+
return to_enum(__method__) unless block_given?
177+
178+
@size.times do
179+
yield(nil)
180+
end
181+
end
162182
end
163183

164184
class PrimitiveArray < Array
165185
include BufferAlignable
166186

187+
attr_reader :values_buffer
167188
def initialize(*args)
168189
n_args = args.size
169190
if self.class.respond_to?(:type)
@@ -192,15 +213,47 @@ def to_a
192213
apply_validity(@values_buffer.values(@type.buffer_type, offset, @size))
193214
end
194215

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

198241
yield(slice_bitmap_buffer(:validity, @validity_buffer))
199242
yield(slice_fixed_element_size_buffer(:values,
200243
@values_buffer,
201244
element_size))
202245
end
203246

247+
def ==(other)
248+
return false unless super(other)
249+
if @offset == other.offset and @values_buffer == other.values_buffer
250+
return true
251+
end
252+
lazy.zip(other).all? do |value, other_value|
253+
value == other_value
254+
end
255+
end
256+
204257
private
205258
def element_size
206259
IO::Buffer.size_of(@type.buffer_type)
@@ -238,19 +291,38 @@ def type
238291
def to_a
239292
return [] if empty?
240293

241-
@values_bitmap ||= Bitmap.new(@values_buffer, @offset, @size)
242-
values = @values_bitmap.to_a
294+
values = values_bitmap.to_a
243295
apply_validity(values)
244296
end
245297

298+
def each(&block)
299+
return to_enum(__method__) {@size} unless block_given?
300+
301+
if @validity_buffer.nil?
302+
values_bitmap.each(&block)
303+
else
304+
validity_bitmap.zip(values_bitmap) do |is_valid, value|
305+
if is_valid
306+
yield(value)
307+
else
308+
yield(nil)
309+
end
310+
end
311+
end
312+
end
313+
246314
def each_buffer
247-
return to_enum(__method__) unless block_given?
315+
return to_enum(__method__) {2} unless block_given?
248316

249317
yield(slice_bitmap_buffer(:validity, @validity_buffer))
250318
yield(slice_bitmap_buffer(:values, @values_buffer))
251319
end
252320

253321
private
322+
def values_bitmap
323+
@values_bitmap ||= Bitmap.new(@values_buffer, @offset, @size)
324+
end
325+
254326
def clear_cache
255327
super
256328
@values_bitmap = nil

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+
lazy.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: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
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+
50+
def test_sliced_different_content
51+
array1 = ArrowFormat::BooleanArray.new([true, nil, false])
52+
array2 = ArrowFormat::BooleanArray.new([true, false, nil, false, true])
53+
assert_not_equal(array1, array2.slice(1, 3))
54+
end
55+
end
56+
end
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
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+
48+
def test_sliced_different_content
49+
values = [-Float::INFINITY, -0.0, nil, +0.0, +Float::INFINITY]
50+
array1 = ArrowFormat::Float32Array.new(values)
51+
array2 = ArrowFormat::Float32Array.new([0.0, 0.0] + values + [0.0])
52+
assert_not_equal(array1, array2.slice(1, 5))
53+
end
54+
end
55+
end

0 commit comments

Comments
 (0)