From d28594479563748a1b366627d3085a754d5aa3b1 Mon Sep 17 00:00:00 2001 From: Sutou Kouhei Date: Fri, 17 Jul 2026 09:52:15 +0900 Subject: [PATCH] GH-50528: [Ruby] Add `ArrowFormat::ArrayBuilder` --- .../lib/arrow-format/array-builder.rb | 140 ++++++++++++++++ .../lib/arrow-format/array.rb | 49 +++++- .../red-arrow-format/lib/arrow-format/type.rb | 28 +++- .../test/test-array-builder.rb | 150 ++++++++++++++++++ 4 files changed, 360 insertions(+), 7 deletions(-) create mode 100644 ruby/red-arrow-format/lib/arrow-format/array-builder.rb create mode 100644 ruby/red-arrow-format/test/test-array-builder.rb diff --git a/ruby/red-arrow-format/lib/arrow-format/array-builder.rb b/ruby/red-arrow-format/lib/arrow-format/array-builder.rb new file mode 100644 index 00000000000..f6b420e7761 --- /dev/null +++ b/ruby/red-arrow-format/lib/arrow-format/array-builder.rb @@ -0,0 +1,140 @@ +# 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. + +module ArrowFormat + class ArrayBuilder + def initialize(values) + @values = values + end + + def build + target = nil + @values.each do |value| + target = detect_target(value, target) + break if target and target[:detected] + end + if target + array_class = target[:array_class] + array_class ||= finalize_array_class(target) + type = target[:type] + if type + array_class.new(type, @values) + else + array_class.new(@values) + end + else + UTF8Array.new(@values) + end + end + + private + def detect_target(value, target) + case value + when nil + target + when true, false + { + array_class: BooleanArray, + detected: true, + } + when String + { + array_class: UTF8Array, + detected: true, + } + when Float + { + array_class: Float64Array, + detected: true, + } + when Integer + target ||= {} + min = target[:min] || value + max = target[:max] || value + min = value if value < min + max = value if value > max + + if target[:value_type] == :int or value < 0 + { + value_type: :int, + min: min, + max: max, + } + else + { + value_type: :uint, + min: min, + max: max, + } + end + when Time + { + array_class: TimestampArray, + type: TimestampType.new(:nanosecond), + detected: true, + } + when Date + { + array_class: Date32Array, + detected: true, + } + else + { + array_class: UTF8Array, + detected: true, + } + end + end + + def finalize_array_class(target) + value_type = target[:value_type] + case value_type + when :int + min = target[:min] + max = target[:max] + + if Int8Type::MIN_VALUE <= min and max <= Int8Type::MAX_VALUE + Int8Array + elsif Int16Type::MIN_VALUE <= min and max <= Int16Type::MAX_VALUE + Int16Array + elsif Int32Type::MIN_VALUE <= min and max <= Int32Type::MAX_VALUE + Int32Array + elsif Int64Type::MIN_VALUE <= min and max <= Int64Type::MAX_VALUE + Int64Array + else + UTF8Array + end + when :uint + max = target[:max] + + if max <= UInt8Type::MAX_VALUE + UInt8Array + elsif max <= UInt16Type::MAX_VALUE + UInt16Array + elsif max <= UInt32Type::MAX_VALUE + UInt32Array + elsif max <= UInt64Type::MAX_VALUE + UInt64Array + else + UTF8Array + end + else + UTF8Array + end + end + end +end diff --git a/ruby/red-arrow-format/lib/arrow-format/array.rb b/ruby/red-arrow-format/lib/arrow-format/array.rb index b3aceecd8fa..0a25ebf6b3b 100644 --- a/ruby/red-arrow-format/lib/arrow-format/array.rb +++ b/ruby/red-arrow-format/lib/arrow-format/array.rb @@ -17,6 +17,7 @@ require "bigdecimal" +require_relative "array-builder" require_relative "bitmap" require_relative "bitmap-builder" @@ -26,6 +27,12 @@ module ArrowFormat class Array include Enumerable + class << self + def build(values) + ArrayBuilder.new(values).build + end + end + attr_reader :type attr_reader :size alias_method :length, :size @@ -271,9 +278,9 @@ def build_data(data, type) if value.nil? validity_buffer_builder ||= SparseBitmapBuilder.new validity_buffer_builder.unset(i) - buffer.append_as_bytes(pack_value(nil, pack_template)) + buffer.append_as_bytes(pack_value(nil, pack_template, type)) else - buffer.append_as_bytes(pack_value(value, pack_template)) + buffer.append_as_bytes(pack_value(value, pack_template, type)) end n += 1 end @@ -283,7 +290,7 @@ def build_data(data, type) return n, validity_buffer, IO::Buffer.for(buffer) end - def pack_value(value, template) + def pack_value(value, template, type) value = 0 if value.nil? [value].pack(template) end @@ -455,6 +462,17 @@ def type Date32Type.singleton end end + + private + def pack_value(value, template, type) + if value.nil? + [0].pack(template) + elsif value.is_a?(Date) + [value.day].pack(template) + else + [value].pack(template) + end + end end class Date64Array < DateArray @@ -490,6 +508,27 @@ def type_class TimestampType end end + + private + def pack_value(value, template, type) + if value.nil? + [0, 0].pack(template) + elsif value.is_a?(Time) + case type.unit + when :second + value = value.to_i + when :millisecond + value = (value.to_i * 1_000) + (value.nsec / 1_000_000) + when :microsecond + value = (value.to_i * 1_000_000) + (value.nsec / 1_000) + when :nanosecond + value = (value.to_i * 1_000_000_000) + value.nsec + end + [value].pack(template) + else + [value].pack(template) + end + end end class IntervalArray < TemporalArray @@ -552,7 +591,7 @@ def element_size super * 2 end - def pack_value(value, template) + def pack_value(value, template, type) if value.nil? [0, 0].pack(template) elsif value.is_a?(Hash) @@ -613,7 +652,7 @@ def element_size IO::Buffer.size_of(@type.buffer_types) end - def pack_value(value, template) + def pack_value(value, template, type) if value.nil? [0, 0, 0].pack(template) elsif value.is_a?(Hash) diff --git a/ruby/red-arrow-format/lib/arrow-format/type.rb b/ruby/red-arrow-format/lib/arrow-format/type.rb index 28d7104638b..4ba989515e8 100644 --- a/ruby/red-arrow-format/lib/arrow-format/type.rb +++ b/ruby/red-arrow-format/lib/arrow-format/type.rb @@ -88,6 +88,9 @@ def to_flatbuffers end class Int8Type < IntType + MIN_VALUE = -(2 ** 7) + MAX_VALUE = (2 ** 7) - 1 + class << self def singleton @singleton ||= new @@ -116,6 +119,9 @@ def build_array(...) end class UInt8Type < IntType + MIN_VALUE = 0 + MAX_VALUE = (2 ** 8) - 1 + class << self def singleton @singleton ||= new @@ -144,6 +150,9 @@ def build_array(...) end class Int16Type < IntType + MIN_VALUE = -(2 ** 15) + MAX_VALUE = (2 ** 15) - 1 + class << self def singleton @singleton ||= new @@ -172,6 +181,9 @@ def build_array(...) end class UInt16Type < IntType + MIN_VALUE = 0 + MAX_VALUE = (2 ** 16) - 1 + class << self def singleton @singleton ||= new @@ -200,6 +212,9 @@ def build_array(...) end class Int32Type < IntType + MIN_VALUE = -(2 ** 31) + MAX_VALUE = (2 ** 31) - 1 + class << self def singleton @singleton ||= new @@ -228,6 +243,9 @@ def build_array(...) end class UInt32Type < IntType + MIN_VALUE = 0 + MAX_VALUE = (2 ** 32) - 1 + class << self def singleton @singleton ||= new @@ -256,6 +274,9 @@ def build_array(...) end class Int64Type < IntType + MIN_VALUE = -(2 ** 63) + MAX_VALUE = (2 ** 63) - 1 + class << self def singleton @singleton ||= new @@ -284,6 +305,9 @@ def build_array(...) end class UInt64Type < IntType + MIN_VALUE = 0 + MAX_VALUE = (2 ** 64) - 1 + class << self def singleton @singleton ||= new @@ -559,7 +583,7 @@ def try_convert(value) case value when Symbol unit = value - new(unit, nil) + new(unit) when ::Array unit, time_zone = value new(unit, time_zone) @@ -573,7 +597,7 @@ def try_convert(value) attr_reader :unit attr_reader :time_zone - def initialize(unit, time_zone) + def initialize(unit, time_zone=nil) super() @unit = unit @time_zone = time_zone diff --git a/ruby/red-arrow-format/test/test-array-builder.rb b/ruby/red-arrow-format/test/test-array-builder.rb new file mode 100644 index 00000000000..bae8e88c029 --- /dev/null +++ b/ruby/red-arrow-format/test/test-array-builder.rb @@ -0,0 +1,150 @@ +# 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 TestArrayBuilder < Test::Unit::TestCase + def test_boolean + values = [false, true] + assert_equal(ArrowFormat::BooleanArray.new(values), + ArrowFormat::Array.build(values)) + end + + def test_uint8 + values = [0, (2 ** 8) - 1] + assert_equal(ArrowFormat::UInt8Array.new(values), + ArrowFormat::Array.build(values)) + end + + def test_int8 + values = [-(2 ** 7), (2 ** 7) - 1] + assert_equal(ArrowFormat::Int8Array.new(values), + ArrowFormat::Array.build(values)) + end + + def test_uint16_min + values = [0, 2 ** 8] + assert_equal(ArrowFormat::UInt16Array.new(values), + ArrowFormat::Array.build(values)) + end + + def test_uint16_max + values = [0, (2 ** 16) - 1] + assert_equal(ArrowFormat::UInt16Array.new(values), + ArrowFormat::Array.build(values)) + end + + def test_int16_min + values = [-(2 ** 7) - 1, 2 ** 7] + assert_equal(ArrowFormat::Int16Array.new(values), + ArrowFormat::Array.build(values)) + end + + def test_int16_max + values = [-(2 ** 15), (2 ** 15) - 1] + assert_equal(ArrowFormat::Int16Array.new(values), + ArrowFormat::Array.build(values)) + end + + def test_uint32_min + values = [0, 2 ** 16] + assert_equal(ArrowFormat::UInt32Array.new(values), + ArrowFormat::Array.build(values)) + end + + def test_uint32_max + values = [0, (2 ** 32) - 1] + assert_equal(ArrowFormat::UInt32Array.new(values), + ArrowFormat::Array.build(values)) + end + + def test_int32_min + values = [-(2 ** 15) - 1, 2 ** 15] + assert_equal(ArrowFormat::Int32Array.new(values), + ArrowFormat::Array.build(values)) + end + + def test_int32_max + values = [-(2 ** 31), (2 ** 31) - 1] + assert_equal(ArrowFormat::Int32Array.new(values), + ArrowFormat::Array.build(values)) + end + + def test_uint64_min + values = [0, 2 ** 32] + assert_equal(ArrowFormat::UInt64Array.new(values), + ArrowFormat::Array.build(values)) + end + + def test_uint64_max + values = [0, (2 ** 64) - 1] + assert_equal(ArrowFormat::UInt64Array.new(values), + ArrowFormat::Array.build(values)) + end + + def test_int64_min + values = [-(2 ** 31) - 1, 2 ** 31] + assert_equal(ArrowFormat::Int64Array.new(values), + ArrowFormat::Array.build(values)) + end + + def test_int64_max + values = [-(2 ** 63), (2 ** 63) - 1] + assert_equal(ArrowFormat::Int64Array.new(values), + ArrowFormat::Array.build(values)) + end + + def test_float64 + values = [-Float::INFINITY, -0.0, +0.0, +Float::INFINITY] + assert_equal(ArrowFormat::Float64Array.new(values), + ArrowFormat::Array.build(values)) + end + + def test_float64_mixed + values = [-Float::INFINITY, 0, 0, +Float::INFINITY] + assert_equal(ArrowFormat::Float64Array.new(values), + ArrowFormat::Array.build(values)) + end + + def test_utf8 + values = ["Hello", "World"] + assert_equal(ArrowFormat::UTF8Array.new(values), + ArrowFormat::Array.build(values)) + end + + def test_utf8_mixed + values = ["Hello", 100, "World"] + assert_equal(ArrowFormat::UTF8Array.new(values), + ArrowFormat::Array.build(values)) + end + + def test_nil + values = ["Hello", nil, "World"] + assert_equal(ArrowFormat::UTF8Array.new(values), + ArrowFormat::Array.build(values)) + end + + def test_time + values = [Time.at(0)] + assert_equal(ArrowFormat::TimestampArray.new(:nanosecond, values), + ArrowFormat::Array.build(values)) + end + + def test_date + values = [Date.new(2026, 7, 17)] + assert_equal(ArrowFormat::Date32Array.new(values), + ArrowFormat::Array.build(values)) + end +end