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())
+ })
+ }
+}