From cf6b3d1cce18ff8f8d3083985f205a84412ab87d Mon Sep 17 00:00:00 2001 From: otegami Date: Wed, 12 Aug 2026 19:34:18 +0800 Subject: [PATCH] GH-50832: [Ruby] Add `ArrowFormat::FixedSizeBinaryArray.new(byte_width, values)` ### Rationale for this change Building a fixed size binary Arrow array from Ruby objects is convenient. ### What changes are included in this PR? Accept: * `ArrowFormat::FixedSizeBinaryArray.new(byte_width, values)` * `ArrowFormat::FixedSizeBinaryArray.new(type, values)` `ArrowFormat::FixedSizeBinaryType.try_convert` is added for the byte width shorthand, in the same way as `ArrowFormat::Time32Array.new(unit, values)`. The existing 4 arguments form is kept. Decimal arrays are out of scope. `ArrowFormat::DecimalArray#to_a` returns `BigDecimal`, so building one from Ruby objects needs a conversion between `BigDecimal` and two's complement. I would like to open a separate issue for it. ### Are these changes tested? Yes. ### Are there any user-facing changes? Yes. * GitHub Issue: #50832 --- .../lib/arrow-format/array.rb | 54 ++++++++++++- .../red-arrow-format/lib/arrow-format/type.rb | 14 ++++ .../test/test-fixed-size-binary-array.rb | 79 +++++++++++++++++++ 3 files changed, 146 insertions(+), 1 deletion(-) create mode 100644 ruby/red-arrow-format/test/test-fixed-size-binary-array.rb diff --git a/ruby/red-arrow-format/lib/arrow-format/array.rb b/ruby/red-arrow-format/lib/arrow-format/array.rb index d190cc29923e..20929493e906 100644 --- a/ruby/red-arrow-format/lib/arrow-format/array.rb +++ b/ruby/red-arrow-format/lib/arrow-format/array.rb @@ -852,7 +852,21 @@ def type end class FixedSizeBinaryArray < Array - def initialize(type, size, validity_buffer, values_buffer) + include BufferAlignable + + def initialize(type, *args) + unless type.is_a?(Type) + type = FixedSizeBinaryType.try_convert(type) || type + end + if args.size == 1 + args = build_data(args.first, type) + elsif args.size != 3 + raise ArgumentError, + "wrong number of arguments (given #{args.size + 1}, expected 2 or 4)" + end + + size, validity_buffer, values_buffer = args + super(type, size, validity_buffer) @values_buffer = values_buffer end @@ -875,6 +889,44 @@ def to_a end apply_validity(values) end + + private + def build_data(data, type) + n = 0 + validity_buffer_builder = nil + + values = +"".b + byte_width = type.byte_width + null_value = "\x00" * byte_width + + data.each_with_index do |value, i| + if value.nil? + validity_buffer_builder ||= SparseBitmapBuilder.new + validity_buffer_builder.unset(i) + values.append_as_bytes(null_value) + else + unless value.bytesize == byte_width + message = "value size must be #{byte_width}: #{value.inspect}" + raise ArgumentError, message + end + values.append_as_bytes(value) + end + + n += 1 + end + + validity_buffer = validity_buffer_builder&.finish(n) + + pad!(values, buffer_padding_size(values)) + values.freeze + values_buffer = IO::Buffer.for(values) + + [ + n, + validity_buffer, + values_buffer, + ] + end end class DecimalArray < FixedSizeBinaryArray diff --git a/ruby/red-arrow-format/lib/arrow-format/type.rb b/ruby/red-arrow-format/lib/arrow-format/type.rb index b52136769d47..9a6d1f3ed066 100644 --- a/ruby/red-arrow-format/lib/arrow-format/type.rb +++ b/ruby/red-arrow-format/lib/arrow-format/type.rb @@ -911,6 +911,20 @@ def to_flatbuffers end class FixedSizeBinaryType < Type + class << self + def try_convert(value) + case value + when Integer + byte_width = value + new(byte_width) + when self + value + else + nil + end + end + end + attr_reader :byte_width def initialize(byte_width) super() diff --git a/ruby/red-arrow-format/test/test-fixed-size-binary-array.rb b/ruby/red-arrow-format/test/test-fixed-size-binary-array.rb new file mode 100644 index 000000000000..bee0ae1f8eee --- /dev/null +++ b/ruby/red-arrow-format/test/test-fixed-size-binary-array.rb @@ -0,0 +1,79 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +class TestFixedSizeBinaryArray < Test::Unit::TestCase + sub_test_case("#initialize") do + def test_no_null + values = ["0124".b, "abcd".b] + assert_equal(values, + ArrowFormat::FixedSizeBinaryArray.new(4, values).to_a) + end + + def test_mixed + values = ["0124".b, nil, "abcd".b] + assert_equal(values, + ArrowFormat::FixedSizeBinaryArray.new(4, values).to_a) + end + + def test_type + type = ArrowFormat::FixedSizeBinaryType.new(4) + values = ["0124".b, nil, "abcd".b] + assert_equal(values, + ArrowFormat::FixedSizeBinaryArray.new(type, values).to_a) + end + + def test_too_small_value_size + error = ArgumentError.new("value size must be 4: \"012\"") + assert_raise(error) do + ArrowFormat::FixedSizeBinaryArray.new(4, ["012".b]) + end + end + + def test_too_large_value_size + error = ArgumentError.new("value size must be 4: \"01245\"") + assert_raise(error) do + ArrowFormat::FixedSizeBinaryArray.new(4, ["01245".b]) + end + end + end + + sub_test_case("#==") do + def test_no_slice + values = ["0124".b, nil, "abcd".b] + array1 = ArrowFormat::FixedSizeBinaryArray.new(4, values) + array2 = ArrowFormat::FixedSizeBinaryArray.new(4, values) + assert_equal(array1, array2) + end + + def test_sliced + pad = "0000".b + values = ["0124".b, nil, "abcd".b] + array1 = ArrowFormat::FixedSizeBinaryArray.new(4, values) + array2 = ArrowFormat::FixedSizeBinaryArray.new(4, [pad, *values, pad]) + assert_equal(array1, array2.slice(1, 3)) + end + + def test_sliced_different_content + pad = "0000".b + values = ["0124".b, nil, "abcd".b] + array1 = ArrowFormat::FixedSizeBinaryArray.new(4, values) + array2 = ArrowFormat::FixedSizeBinaryArray.new(4, + [pad, pad, *values, pad]) + assert_not_equal(array1, array2.slice(1, 3)) + end + end +end