This is an automated email from the ASF dual-hosted git repository.

zeroshade pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/arrow-go.git


The following commit(s) were added to refs/heads/main by this push:
     new f85b0b25 fix(compute): handle odd temporal multiples (#1119)
f85b0b25 is described below

commit f85b0b258aface21fb5e9454e4da37d6b794b959
Author: Minh Vu <[email protected]>
AuthorDate: Tue Aug 11 18:20:44 2026 +0200

    fix(compute): handle odd temporal multiples (#1119)
    
    ### Rationale for this change
    
    Half-up temporal rounding treats the lower remainder as an exact tie
    when Multiple is odd. For example, rounding 1 second to multiples of 3
    seconds returns 3 seconds instead of 0.
    
    ### What changes are included in this PR?
    
    Only even multiples use an exact halfway tie. Odd multiples compare the
    remainder to the two neighboring multiples, including negative values.
    
    ### Are these changes tested?
    
    - `go test ./arrow/compute`
    
    ### Are there any user-facing changes?
    
    No API changes. Temporal rounding now returns the nearest result for odd
    multiples.
---
 arrow/compute/internal/kernels/rounding.go         |   5 +-
 .../kernels/temporal_rounding_odd_multiple_test.go |  43 +++++++++
 .../compute/temporal_rounding_odd_multiple_test.go | 106 +++++++++++++++++++++
 3 files changed, 153 insertions(+), 1 deletion(-)

diff --git a/arrow/compute/internal/kernels/rounding.go 
b/arrow/compute/internal/kernels/rounding.go
index 88794edb..605a55ea 100644
--- a/arrow/compute/internal/kernels/rounding.go
+++ b/arrow/compute/internal/kernels/rounding.go
@@ -1001,7 +1001,10 @@ func roundToMultipleInt64(value, multiple int64, mode 
RoundMode, strictCeil bool
                        absRemainder = -absRemainder
                }
 
-               if absRemainder < half {
+               // Odd multiples do not have an exact halfway point. For 
example,
+               // a remainder of 1 when rounding to multiples of 3 is closer 
to 0
+               // than to 3, so it must not be treated as a tie.
+               if absRemainder < half || (multiple%2 != 0 && absRemainder == 
half) {
                        return quotient * multiple
                } else if absRemainder > half {
                        if remainder > 0 {
diff --git 
a/arrow/compute/internal/kernels/temporal_rounding_odd_multiple_test.go 
b/arrow/compute/internal/kernels/temporal_rounding_odd_multiple_test.go
new file mode 100644
index 00000000..3f445c1b
--- /dev/null
+++ b/arrow/compute/internal/kernels/temporal_rounding_odd_multiple_test.go
@@ -0,0 +1,43 @@
+// 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.
+
+//go:build go1.18
+
+package kernels
+
+import "testing"
+
+func TestRoundToMultipleInt64OddMultipleAcrossModes(t *testing.T) {
+       for _, mode := range []RoundMode{HalfDown, HalfUp, HalfToEven} {
+               t.Run(mode.String(), func(t *testing.T) {
+                       for _, tc := range []struct {
+                               value int64
+                               want  int64
+                       }{
+                               {value: 1, want: 0},
+                               {value: 2, want: 3},
+                               {value: -1, want: 0},
+                               {value: -2, want: -3},
+                       } {
+                               got := roundToMultipleInt64(tc.value, 3, mode, 
false)
+                               if got != tc.want {
+                                       t.Errorf("roundToMultipleInt64(%d, 3, 
%s) = %d, want %d", tc.value, mode, got, tc.want)
+                               }
+                       }
+               })
+       }
+}
diff --git a/arrow/compute/temporal_rounding_odd_multiple_test.go 
b/arrow/compute/temporal_rounding_odd_multiple_test.go
new file mode 100644
index 00000000..ebbe16fc
--- /dev/null
+++ b/arrow/compute/temporal_rounding_odd_multiple_test.go
@@ -0,0 +1,106 @@
+// 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.
+
+//go:build go1.18
+
+package compute_test
+
+import (
+       "context"
+       "testing"
+
+       "github.com/apache/arrow-go/v18/arrow"
+       "github.com/apache/arrow-go/v18/arrow/array"
+       "github.com/apache/arrow-go/v18/arrow/compute"
+       "github.com/apache/arrow-go/v18/arrow/memory"
+       "github.com/stretchr/testify/assert"
+       "github.com/stretchr/testify/require"
+)
+
+func TestTemporalOddMultipleRounding(t *testing.T) {
+       tests := []struct {
+               name      string
+               unit      arrow.TimeUnit
+               roundUnit compute.RoundTemporalUnit
+               multiple  int64
+               values    []arrow.Timestamp
+               want      []arrow.Timestamp
+       }{
+               {
+                       name:      "seconds multiple three",
+                       unit:      arrow.Second,
+                       roundUnit: compute.RoundTemporalSecond,
+                       multiple:  3,
+                       values:    []arrow.Timestamp{1, 2, -1, -2},
+                       want:      []arrow.Timestamp{0, 3, 0, -3},
+               },
+               {
+                       name:      "seconds multiple five",
+                       unit:      arrow.Second,
+                       roundUnit: compute.RoundTemporalSecond,
+                       multiple:  5,
+                       values:    []arrow.Timestamp{1, 2, 3, 4, -1, -2, -3, 
-4},
+                       want:      []arrow.Timestamp{0, 0, 5, 5, 0, 0, -5, -5},
+               },
+               {
+                       name:      "exact multiples and zero",
+                       unit:      arrow.Second,
+                       roundUnit: compute.RoundTemporalSecond,
+                       multiple:  5,
+                       values:    []arrow.Timestamp{0, 5, -5, 10, -10},
+                       want:      []arrow.Timestamp{0, 5, -5, 10, -10},
+               },
+               {
+                       name:      "multiple one",
+                       unit:      arrow.Second,
+                       roundUnit: compute.RoundTemporalSecond,
+                       multiple:  1,
+                       values:    []arrow.Timestamp{0, 1, -1, 2, -2},
+                       want:      []arrow.Timestamp{0, 1, -1, 2, -2},
+               },
+               {
+                       name:      "milliseconds",
+                       unit:      arrow.Millisecond,
+                       roundUnit: compute.RoundTemporalMillisecond,
+                       multiple:  3,
+                       values:    []arrow.Timestamp{1, 2, -1, -2},
+                       want:      []arrow.Timestamp{0, 3, 0, -3},
+               },
+       }
+
+       for _, tt := range tests {
+               t.Run(tt.name, func(t *testing.T) {
+                       builder := 
array.NewTimestampBuilder(memory.DefaultAllocator, &arrow.TimestampType{Unit: 
tt.unit})
+                       defer builder.Release()
+                       builder.AppendValues(tt.values, nil)
+
+                       input := builder.NewArray()
+                       defer input.Release()
+
+                       result, err := 
compute.RoundTemporal(context.Background(), compute.RoundTemporalOptions{
+                               Multiple: tt.multiple,
+                               Unit:     tt.roundUnit,
+                       }, compute.NewDatum(input))
+                       require.NoError(t, err)
+                       defer result.Release()
+
+                       output := 
result.(*compute.ArrayDatum).MakeArray().(*array.Timestamp)
+                       defer output.Release()
+
+                       assert.Equal(t, tt.want, output.Values())
+               })
+       }
+}

Reply via email to