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 6542b630 fix(compute): detect temporal rounding overflow (#1127)
6542b630 is described below

commit 6542b630f84dda25715f6238027cc1eed9d114c6
Author: Minh Vu <[email protected]>
AuthorDate: Fri Sep 4 22:29:57 2026 +0200

    fix(compute): detect temporal rounding overflow (#1127)
    
    ### Rationale for this change
    
    Temporal rounding can overflow while converting timestamps to
    nanoseconds, calculating the rounding interval, or multiplying a rounded
    quotient. The current unchecked arithmetic can return an incorrect
    timestamp.
    
    ### What changes are included in this PR?
    
    Use checked arithmetic for temporal conversions, interval setup,
    rounding results, calendar conversion, and origin adjustments. Propagate
    overflow as an Arrow invalid-value error.
    
    ### Are these changes tested?
    
    - `go test ./arrow/compute`
    
    ### Are there any user-facing changes?
    
    Inputs and options that cannot be represented during temporal rounding
    now return an error instead of a wrapped result.
---
 arrow/compute/internal/kernels/rounding.go         | 1358 +++++++++++++++++---
 .../kernels/temporal_rounding_odd_multiple_test.go |    5 +-
 .../kernels/temporal_rounding_overflow_test.go     |  189 +++
 arrow/compute/temporal_rounding_origins_test.go    |  707 ++++++++++
 arrow/compute/temporal_rounding_overflow_test.go   |  741 +++++++++++
 arrow/compute/temporal_rounding_test.go            |  167 ++-
 6 files changed, 2999 insertions(+), 168 deletions(-)

diff --git a/arrow/compute/internal/kernels/rounding.go 
b/arrow/compute/internal/kernels/rounding.go
index 9f13d281..9e46573c 100644
--- a/arrow/compute/internal/kernels/rounding.go
+++ b/arrow/compute/internal/kernels/rounding.go
@@ -21,6 +21,7 @@ package kernels
 import (
        "fmt"
        "math"
+       "math/big"
        "time"
 
        "github.com/apache/arrow-go/v18/arrow"
@@ -847,10 +848,9 @@ type roundTemporalState struct {
        mode RoundMode
 
        // Pre-calculated values to avoid repeated computation
-       unitNanos         int64 // Duration of the unit in nanoseconds
-       roundingInterval  int64 // unitNanos * Multiple
-       isSubDay          bool  // true if this is a sub-day unit (can use fast 
path)
-       useCalendarOrigin bool  // true if using calendar-based origin
+       unitNanos        int64 // Duration of the unit in nanoseconds
+       roundingInterval int64 // unitNanos * Multiple
+       isSubDay         bool  // true if this is a sub-day unit (can use fast 
path)
 }
 
 func InitRoundTemporalState(_ *exec.KernelCtx, args exec.KernelInitArgs) 
(exec.KernelState, error) {
@@ -873,8 +873,11 @@ func InitRoundTemporalState(_ *exec.KernelCtx, args 
exec.KernelInitArgs) (exec.K
        // Pre-calculate constants for this rounding operation
        rs.unitNanos, rs.isSubDay = unitInNanos(rs.Unit)
        if rs.isSubDay {
-               rs.roundingInterval = rs.unitNanos * rs.Multiple
-               rs.useCalendarOrigin = rs.CalendarBasedOrigin && rs.Unit <= 
RoundTemporalDay
+               var err error
+               rs.roundingInterval, err = checkedMulInt64(rs.unitNanos, 
rs.Multiple)
+               if err != nil {
+                       return nil, err
+               }
        }
 
        return rs, nil
@@ -910,71 +913,546 @@ func roundTimestamp(ts int64, inputUnit arrow.TimeUnit, 
tz *time.Location, opts
                tz = time.UTC
        }
 
-       // Calendar units with variable duration (year, quarter, month, week) 
require date arithmetic
+       // Calendar units with variable duration (year, quarter, month, week) 
require date arithmetic.
        if !opts.isSubDay {
-               tsNanos := convertToNanos(ts, inputUnit)
-               return roundTimestampCalendar(tsNanos, inputUnit, tz, opts)
+               return roundTimestampCalendar(ts, inputUnit, tz, opts)
        }
 
-       // Day rounding with timezone requires calendar arithmetic (days vary: 
23/24/25 hours due to DST)
-       isUTC := tz == time.UTC || tz.String() == "UTC"
-       if !isUTC && opts.Unit == RoundTemporalDay {
-               tsNanos := convertToNanos(ts, inputUnit)
-               return roundTimestampCalendar(tsNanos, inputUnit, tz, opts)
+       // A day is a calendar unit when it is associated with a timezone. This 
also
+       // handles CalendarBasedOrigin for day multiples, whose origin is the 
start
+       // of the month rather than the Unix epoch.
+       if opts.Unit == RoundTemporalDay {
+               return roundTimestampCalendar(ts, inputUnit, tz, opts)
        }
 
-       // Sub-day units (hour, minute, second, etc.) use fixed-duration 
arithmetic
-       // Fast path: round directly in input unit if possible (no origin, 
compatible units)
-       if canRoundInInputUnit(inputUnit, opts.unitNanos) && 
!opts.useCalendarOrigin {
-               intervalInInputUnit := opts.roundingInterval / 
int64(inputUnit.Multiplier())
-               rounded := roundToMultipleInt64(ts, intervalInInputUnit, 
opts.mode, opts.CeilIsStrictlyGreater)
-               return rounded, nil
+       // Zoned timestamps are rounded in their local wall-clock timeline. 
This is
+       // observable for offsets that are not whole hours (for example, 
Kathmandu)
+       // and around daylight-saving transitions.
+       if tz != time.UTC && tz.String() != "UTC" {
+               if opts.CalendarBasedOrigin {
+                       return roundTimestampCalendarOrigin(ts, inputUnit, tz, 
opts)
+               }
+               return roundTimestampFixedTimezone(ts, inputUnit, tz, opts)
        }
 
-       // Slow path: convert to nanoseconds for calendar origin or 
incompatible units
-       tsNanos := convertToNanos(ts, inputUnit)
+       // CalendarBasedOrigin rounds fixed-duration units against the preceding
+       // calendar boundary (for example, minutes from the start of the hour).
+       // Use local wall-clock arithmetic here: time.Time.Sub measures elapsed 
time
+       // and therefore gives the wrong answer across daylight-saving 
transitions.
+       if opts.CalendarBasedOrigin {
+               return roundTimestampCalendarOrigin(ts, inputUnit, tz, opts)
+       }
 
-       var origin int64 = 0
-       if opts.useCalendarOrigin {
-               // Calendar origin: round relative to start of day 
(timezone-aware if tz != nil)
-               if tz != nil {
-                       t := time.Unix(0, tsNanos).In(tz)
-                       startOfDay := time.Date(t.Year(), t.Month(), t.Day(), 
0, 0, 0, 0, tz)
-                       origin = startOfDay.UnixNano()
-               } else {
-                       origin = tsNanos
+       // Sub-day units (hour, minute, second, etc.) use fixed-duration 
arithmetic.
+       // Fast path: round directly in input unit when possible. A finer 
interval that
+       // divides the input unit is already aligned and needs no calculation.
+       if canRoundInInputUnit(inputUnit, opts.roundingInterval) {
+               inputUnitNanos := int64(inputUnit.Multiplier())
+               if opts.roundingInterval < inputUnitNanos {
+                       // Every input value is already aligned to a finer 
interval that divides
+                       // the input unit. Keep the calculation in the input 
unit so wider
+                       // timestamp ranges do not overflow during a nanosecond 
conversion.
+                       return ts, nil
                }
+               intervalInInputUnit := opts.roundingInterval / inputUnitNanos
+               return roundToMultipleInt64(ts, intervalInInputUnit, opts.mode, 
opts.CeilIsStrictlyGreater)
+       }
+
+       // Slow path: calculate the floor in nanoseconds, then cast the floor 
and
+       // ceiling back to the input resolution before comparing them. This 
mirrors
+       // the C++ kernel's duration_cast behavior when the requested interval 
is not
+       // representable in the input unit.
+       return roundTimestampInInputUnit(ts, inputUnit, opts)
+}
+
+func roundTimestampInInputUnit(ts int64, inputUnit arrow.TimeUnit, opts 
roundTemporalState) (int64, error) {
+       // Keep the input in the target unit's arithmetic domain. Converting a 
wide
+       // timestamp to nanoseconds first would reject values that are valid in 
the
+       // requested unit (for example timestamp[s] in year 2300 rounded to 3 
ms).
+       inputUnitNanos := big.NewInt(int64(inputUnit.Multiplier()))
+       valueNanos := new(big.Int).Mul(big.NewInt(ts), inputUnitNanos)
+       floorNanos := floorBigIntToMultiple(valueNanos, opts.roundingInterval)
+       targetUnit := big.NewInt(opts.unitNanos)
+       floorTarget := new(big.Int).Quo(new(big.Int).Set(floorNanos), 
targetUnit)
+       if !floorTarget.IsInt64() {
+               return 0, overflowError()
+       }
+       floorInput := new(big.Int).Quo(floorNanos, inputUnitNanos)
+       if !floorInput.IsInt64() {
+               return 0, overflowError()
        }
+       floor := floorInput.Int64()
 
-       adjusted := tsNanos - origin
-       rounded := roundToMultipleInt64(adjusted, opts.roundingInterval, 
opts.mode, opts.CeilIsStrictlyGreater)
-       result := origin + rounded
+       ceil := floor
+       increment := opts.roundingInterval / inputUnitNanos.Int64()
+       if opts.CeilIsStrictlyGreater || floor < ts {
+               var err error
+               ceil, err = checkedAddInt64(floor, increment)
+               if err != nil {
+                       return 0, err
+               }
+       }
 
-       return convertFromNanos(result, inputUnit), nil
+       switch opts.mode {
+       case RoundDown:
+               return floor, nil
+       case RoundUp:
+               return ceil, nil
+       default:
+               fromFloor := new(big.Int).Sub(big.NewInt(ts), big.NewInt(floor))
+               toCeil := new(big.Int).Sub(big.NewInt(ceil), big.NewInt(ts))
+               if fromFloor.Cmp(toCeil) >= 0 {
+                       return ceil, nil
+               }
+               return floor, nil
+       }
 }
 
 // canRoundInInputUnit checks if rounding can be done in the input unit
-// without converting to nanoseconds (true when rounding interval is evenly 
divisible).
+// without converting to nanoseconds (true when either unit is evenly divisible
+// by the other).
 func canRoundInInputUnit(inputUnit arrow.TimeUnit, roundingIntervalNanos 
int64) bool {
-       return roundingIntervalNanos%int64(inputUnit.Multiplier()) == 0
+       if roundingIntervalNanos <= 0 {
+               return false
+       }
+
+       inputUnitNanos := int64(inputUnit.Multiplier())
+       return roundingIntervalNanos%inputUnitNanos == 0 ||
+               (roundingIntervalNanos < inputUnitNanos && 
inputUnitNanos%roundingIntervalNanos == 0)
+}
+
+// roundTimestampCalendarOrigin rounds a fixed-duration unit relative to the
+// preceding calendar boundary. Candidate boundaries are calculated in local
+// wall time, then compared as system times so daylight-saving transitions are
+// handled consistently with the temporal rounding reference implementation.
+func roundTimestampCalendarOrigin(ts int64, inputUnit arrow.TimeUnit, tz 
*time.Location, opts roundTemporalState) (int64, error) {
+       t, err := timestampToCalendarTime(ts, inputUnit, tz)
+       if err != nil {
+               return 0, err
+       }
+       origin, err := fixedCalendarOrigin(t, opts.Unit)
+       if err != nil {
+               return 0, err
+       }
+       return roundTimestampFromWallOrigin(t, inputUnit, tz, opts, origin)
+}
+
+func roundTimestampFixedTimezone(ts int64, inputUnit arrow.TimeUnit, tz 
*time.Location, opts roundTemporalState) (int64, error) {
+       t, err := timestampToCalendarTime(ts, inputUnit, tz)
+       if err != nil {
+               return 0, err
+       }
+       origin := time.Date(1970, 1, 1, 0, 0, 0, 0, time.UTC)
+       return roundTimestampFromWallOrigin(t, inputUnit, tz, opts, origin)
+}
+
+func roundTimestampFromWallOrigin(t time.Time, inputUnit arrow.TimeUnit, tz 
*time.Location, opts roundTemporalState, origin time.Time) (int64, error) {
+       inputTime := t
+       wallTime := calendarWallTime(t)
+
+       elapsed, err := wallClockDifference(wallTime, origin)
+       var floor time.Time
+       if err == nil {
+               flooredElapsed, roundErr := roundToMultipleInt64(elapsed, 
opts.roundingInterval, RoundDown, false)
+               if roundErr != nil {
+                       return 0, roundErr
+               }
+               floor, err = addWallNanos(origin, flooredElapsed, tz)
+       } else {
+               // Keep the wide timestamp range available for zoned 
timestamps. The
+               // reference implementation performs this arithmetic in the 
input
+               // duration, while an int64 nanosecond distance only spans 
about 292
+               // years.
+               wideElapsed, wideErr := wallClockDifferenceBig(wallTime, origin)
+               if wideErr != nil {
+                       return 0, wideErr
+               }
+               flooredElapsed := floorBigIntToMultiple(wideElapsed, 
opts.roundingInterval)
+               floor, err = addWallNanosBig(origin, flooredElapsed, tz)
+       }
+       if err != nil {
+               return 0, err
+       }
+       floorWall := calendarWallTime(floor.In(tz))
+       floorWallAtInputResolution, err := 
truncateWallToInputResolution(floorWall, origin, inputUnit)
+       if err != nil {
+               return 0, err
+       }
+       floorAtInputResolution, err := 
checkedLocalTime(floorWallAtInputResolution, tz)
+       if err != nil {
+               return 0, err
+       }
+
+       var rounded time.Time
+       switch opts.mode {
+       case RoundDown:
+               rounded = floorAtInputResolution
+       case RoundUp:
+               rounded, err = ceilCalendarOrigin(inputTime, 
floorAtInputResolution, inputUnit, opts, tz)
+               if err != nil {
+                       return 0, err
+               }
+       default:
+               ceil, ceilErr := ceilCalendarOrigin(inputTime, 
floorAtInputResolution, inputUnit, opts, tz)
+               if ceilErr != nil {
+                       return 0, ceilErr
+               }
+               rounded, err = halfRoundPeriod(inputTime, 
floorAtInputResolution, ceil)
+               if err != nil {
+                       return 0, err
+               }
+       }
+
+       result, err := timestampFromCalendarTime(rounded, inputUnit)
+       if err != nil {
+               return 0, err
+       }
+       return int64(result), nil
+}
+
+func truncateWallToInputResolution(value, origin time.Time, inputUnit 
arrow.TimeUnit) (time.Time, error) {
+       elapsed, err := wallClockDifferenceBig(value, origin)
+       if err != nil {
+               return time.Time{}, err
+       }
+       unitNanos := big.NewInt(int64(inputUnit.Multiplier()))
+       elapsed.Quo(elapsed, unitNanos)
+       elapsed.Mul(elapsed, unitNanos)
+       return addWallNanosBig(origin, elapsed, time.UTC)
+}
+
+func ceilCalendarOrigin(value, floor time.Time, inputUnit arrow.TimeUnit, opts 
roundTemporalState, tz *time.Location) (time.Time, error) {
+       // Convert the floored system time back to local wall time before 
deciding
+       // whether to advance. This matters when the period crosses a timezone
+       // transition because the elapsed system duration need not equal the 
wall
+       // clock duration.
+       floorWall := calendarWallTime(floor.In(tz))
+       cs, err := checkedLocalTime(floorWall, tz)
+       if err != nil {
+               return time.Time{}, err
+       }
+       if !opts.CeilIsStrictlyGreater && !cs.Before(value) {
+               return cs, nil
+       }
+
+       inputUnitNanos := int64(inputUnit.Multiplier())
+       increment := (opts.roundingInterval / inputUnitNanos) * inputUnitNanos
+       nextWall, err := addWallNanos(floorWall, increment, tz)
+       if err != nil {
+               return time.Time{}, err
+       }
+       return nextWall, nil
+}
+
+// Keep the origin's civil fields in UTC because its local time may not exist.
+// The requested location is applied only after rounding the wall-clock value.
+func fixedCalendarOrigin(value time.Time, unit RoundTemporalUnit) (time.Time, 
error) {
+       switch unit {
+       case RoundTemporalHour:
+               return time.Date(value.Year(), value.Month(), value.Day(), 0, 
0, 0, 0, time.UTC), nil
+       case RoundTemporalMinute:
+               return time.Date(value.Year(), value.Month(), value.Day(), 
value.Hour(), 0, 0, 0, time.UTC), nil
+       case RoundTemporalSecond:
+               return time.Date(value.Year(), value.Month(), value.Day(), 
value.Hour(), value.Minute(), 0, 0, time.UTC), nil
+       case RoundTemporalMillisecond:
+               return time.Date(value.Year(), value.Month(), value.Day(), 
value.Hour(), value.Minute(), value.Second(), 0, time.UTC), nil
+       case RoundTemporalMicrosecond:
+               return time.Date(value.Year(), value.Month(), value.Day(), 
value.Hour(), value.Minute(), value.Second(),
+                       (value.Nanosecond()/1_000_000)*1_000_000, time.UTC), nil
+       case RoundTemporalNanosecond:
+               return time.Date(value.Year(), value.Month(), value.Day(), 
value.Hour(), value.Minute(), value.Second(),
+                       (value.Nanosecond()/1_000)*1_000, time.UTC), nil
+       default:
+               return time.Time{}, fmt.Errorf("%w: unsupported fixed calendar 
unit", arrow.ErrNotImplemented)
+       }
+}
+
+func wallClockNanos(value time.Time) (int64, error) {
+       seconds := int64(value.Hour())*3600 + int64(value.Minute())*60 + 
int64(value.Second())
+       nanos, err := checkedMulInt64(seconds, int64(time.Second))
+       if err != nil {
+               return 0, err
+       }
+       return checkedAddInt64(nanos, int64(value.Nanosecond()))
+}
+
+func wallClockDifference(value, origin time.Time) (int64, error) {
+       days, err := calendarDayDifference(value, origin)
+       if err != nil {
+               return 0, err
+       }
+       daysNanos, err := checkedMulInt64(days, nanosPerDay)
+       if err != nil {
+               return 0, err
+       }
+       valueNanos, err := wallClockNanos(value)
+       if err != nil {
+               return 0, err
+       }
+       originNanos, err := wallClockNanos(origin)
+       if err != nil {
+               return 0, err
+       }
+       daysNanos, err = checkedAddInt64(daysNanos, valueNanos)
+       if err != nil {
+               return 0, err
+       }
+       return checkedSubInt64(daysNanos, originNanos)
+}
+
+func wallClockDifferenceBig(value, origin time.Time) (*big.Int, error) {
+       days, err := calendarDayDifference(value, origin)
+       if err != nil {
+               return nil, err
+       }
+       valueNanos, err := wallClockNanos(value)
+       if err != nil {
+               return nil, err
+       }
+       originNanos, err := wallClockNanos(origin)
+       if err != nil {
+               return nil, err
+       }
+
+       result := new(big.Int).Mul(big.NewInt(days), big.NewInt(nanosPerDay))
+       result.Add(result, big.NewInt(valueNanos))
+       result.Sub(result, big.NewInt(originNanos))
+       return result, nil
+}
+
+func floorBigIntToMultiple(value *big.Int, multiple int64) *big.Int {
+       divisor := big.NewInt(multiple)
+       quotient := new(big.Int)
+       remainder := new(big.Int)
+       quotient.QuoRem(value, divisor, remainder)
+       if remainder.Sign() < 0 {
+               quotient.Sub(quotient, big.NewInt(1))
+       }
+       return quotient.Mul(quotient, divisor)
 }
 
-// convertToNanos converts a timestamp value to nanoseconds
-func convertToNanos(ts int64, unit arrow.TimeUnit) int64 {
-       return ts * int64(unit.Multiplier())
+func calendarWallTime(value time.Time) time.Time {
+       return time.Date(value.Year(), value.Month(), value.Day(), 
value.Hour(), value.Minute(), value.Second(), value.Nanosecond(), time.UTC)
 }
 
-// convertFromNanos converts a nanosecond timestamp to the specified unit
-func convertFromNanos(nanos int64, unit arrow.TimeUnit) int64 {
-       return nanos / int64(unit.Multiplier())
+func checkedLocalTime(value time.Time, tz *time.Location) (time.Time, error) {
+       result := time.Date(value.Year(), value.Month(), value.Day(), 
value.Hour(), value.Minute(), value.Second(), value.Nanosecond(), tz)
+       if result.Year() != value.Year() || result.Month() != value.Month() || 
result.Day() != value.Day() ||
+               result.Hour() != value.Hour() || result.Minute() != 
value.Minute() || result.Second() != value.Second() ||
+               result.Nanosecond() != value.Nanosecond() {
+               return time.Time{}, fmt.Errorf("%w: local time does not exist", 
arrow.ErrInvalid)
+       }
+       if ambiguousLocalTime(value, result, tz) {
+               return time.Time{}, fmt.Errorf("%w: local time is ambiguous", 
arrow.ErrInvalid)
+       }
+       return result, nil
+}
+
+func ambiguousLocalTime(value, result time.Time, tz *time.Location) bool {
+       _, selectedOffset := result.Zone()
+       wall := calendarWallTime(value)
+       for _, delta := range []time.Duration{-2 * time.Hour, 2 * time.Hour, 
-48 * time.Hour, 48 * time.Hour} {
+               _, offset := result.Add(delta).Zone()
+               if offset == selectedOffset {
+                       continue
+               }
+               candidate := wall.Add(-time.Duration(offset) * 
time.Second).In(tz)
+               if candidate.Year() == value.Year() && candidate.Month() == 
value.Month() && candidate.Day() == value.Day() &&
+                       candidate.Hour() == value.Hour() && candidate.Minute() 
== value.Minute() &&
+                       candidate.Second() == value.Second() && 
candidate.Nanosecond() == value.Nanosecond() {
+                       return true
+               }
+       }
+       return false
+}
+
+// addWallNanos adds a duration to a local wall-clock value without using
+// time.Time.Add, which applies elapsed-time arithmetic across timezone
+// transitions. The result is normalized through calendar dates in UTC and then
+// reconstructed in the requested location.
+func addWallNanos(origin time.Time, nanos int64, tz *time.Location) 
(time.Time, error) {
+       days := floorDivInt64(nanos, nanosPerDay)
+       daysNanos, err := checkedMulInt64(days, nanosPerDay)
+       if err != nil {
+               return time.Time{}, err
+       }
+       remainder, err := checkedSubInt64(nanos, daysNanos)
+       if err != nil {
+               return time.Time{}, err
+       }
+
+       originNanos, err := wallClockNanos(origin)
+       if err != nil {
+               return time.Time{}, err
+       }
+       dayTimeNanos, err := checkedAddInt64(originNanos, remainder)
+       if err != nil {
+               return time.Time{}, err
+       }
+       if dayTimeNanos >= nanosPerDay {
+               days, err = checkedAddInt64(days, 1)
+               if err != nil {
+                       return time.Time{}, err
+               }
+               dayTimeNanos -= nanosPerDay
+       }
+
+       date := time.Date(origin.Year(), origin.Month(), origin.Day(), 0, 0, 0, 
0, time.UTC)
+       date, err = checkedCalendarAddDays(date, days)
+       if err != nil {
+               return time.Time{}, err
+       }
+
+       hour := dayTimeNanos / int64(time.Hour)
+       dayTimeNanos -= hour * int64(time.Hour)
+       minute := dayTimeNanos / int64(time.Minute)
+       dayTimeNanos -= minute * int64(time.Minute)
+       second := dayTimeNanos / int64(time.Second)
+       nanosecond := dayTimeNanos - second*int64(time.Second)
+       return checkedLocalTime(time.Date(date.Year(), date.Month(), 
date.Day(), int(hour), int(minute), int(second), int(nanosecond), time.UTC), tz)
+}
+
+func addWallNanosBig(origin time.Time, nanos *big.Int, tz *time.Location) 
(time.Time, error) {
+       days := new(big.Int)
+       remainder := new(big.Int)
+       days.QuoRem(nanos, big.NewInt(nanosPerDay), remainder)
+       if remainder.Sign() < 0 {
+               days.Sub(days, big.NewInt(1))
+               remainder.Add(remainder, big.NewInt(nanosPerDay))
+       }
+
+       originNanos, err := wallClockNanos(origin)
+       if err != nil {
+               return time.Time{}, err
+       }
+       dayTimeNanos := new(big.Int).Add(remainder, big.NewInt(originNanos))
+       if dayTimeNanos.Cmp(big.NewInt(nanosPerDay)) >= 0 {
+               days.Add(days, big.NewInt(1))
+               dayTimeNanos.Sub(dayTimeNanos, big.NewInt(nanosPerDay))
+       }
+       if !days.IsInt64() {
+               return time.Time{}, overflowError()
+       }
+
+       date := time.Date(origin.Year(), origin.Month(), origin.Day(), 0, 0, 0, 
0, time.UTC)
+       date, err = checkedCalendarAddDays(date, days.Int64())
+       if err != nil {
+               return time.Time{}, err
+       }
+       dayTime := dayTimeNanos.Int64()
+       hour := dayTime / int64(time.Hour)
+       dayTime -= hour * int64(time.Hour)
+       minute := dayTime / int64(time.Minute)
+       dayTime -= minute * int64(time.Minute)
+       second := dayTime / int64(time.Second)
+       nanosecond := dayTime - second*int64(time.Second)
+       return checkedLocalTime(time.Date(date.Year(), date.Month(), 
date.Day(), int(hour), int(minute), int(second), int(nanosecond), time.UTC), tz)
+}
+
+func overflowError() error {
+       return fmt.Errorf("%w: temporal rounding overflow", arrow.ErrInvalid)
+}
+
+// time.Time's calendar representation wraps for Unix seconds before this
+// boundary. time.Unix(math.MaxInt64, 0) also overflows its internal signed
+// seconds field. Keep both cases out of calendar arithmetic before 
constructing
+// a time.Time. The public timestamp conversion intentionally remains bijective
+// over the full int64 range, so this check belongs in the calendar kernel.
+const minCalendarTimestampSecond = -9223372028741760000
+
+func timestampToCalendarTime(ts int64, inputUnit arrow.TimeUnit, tz 
*time.Location) (time.Time, error) {
+       if inputUnit == arrow.Second && (ts < minCalendarTimestampSecond || ts 
== math.MaxInt64) {
+               return time.Time{}, overflowError()
+       }
+       return arrow.Timestamp(ts).ToTime(inputUnit).In(tz), nil
+}
+
+func checkedAddInt64(left, right int64) (int64, error) {
+       if (right > 0 && left > math.MaxInt64-right) || (right < 0 && left < 
math.MinInt64-right) {
+               return 0, overflowError()
+       }
+       return left + right, nil
+}
+
+func checkedSubInt64(left, right int64) (int64, error) {
+       if (right > 0 && left < math.MinInt64+right) || (right < 0 && left > 
math.MaxInt64+right) {
+               return 0, overflowError()
+       }
+       return left - right, nil
+}
+
+func checkedMulInt64(left, right int64) (int64, error) {
+       if left == 0 || right == 0 {
+               return 0, nil
+       }
+       if (left == math.MinInt64 && right == -1) || (right == math.MinInt64 && 
left == -1) {
+               return 0, overflowError()
+       }
+
+       result := left * right
+       if result/right != left {
+               return 0, overflowError()
+       }
+       return result, nil
 }
 
-func roundToMultipleInt64(value, multiple int64, mode RoundMode, strictCeil 
bool) int64 {
+func checkedInt64ToInt(value int64) (int, error) {
+       result := int(value)
+       if int64(result) != value {
+               return 0, overflowError()
+       }
+       return result, nil
+}
+
+// floorDivInt64 divides by a positive divisor and rounds towards negative
+// infinity. Go's integer division rounds towards zero, which would place
+// negative calendar values in the wrong period.
+func floorDivInt64(dividend, divisor int64) int64 {
+       quotient := dividend / divisor
+       if dividend%divisor < 0 {
+               quotient--
+       }
+       return quotient
+}
+
+func checkedCalendarDate(year, month int64, tz *time.Location) (time.Time, 
error) {
+       dateYear, err := checkedInt64ToInt(year)
+       if err != nil {
+               return time.Time{}, err
+       }
+       dateMonth, err := checkedInt64ToInt(month)
+       if err != nil {
+               return time.Time{}, err
+       }
+       wall := time.Date(dateYear, time.Month(dateMonth), 1, 0, 0, 0, 0, 
time.UTC)
+       if int64(wall.Year()) != year {
+               return time.Time{}, overflowError()
+       }
+       return checkedLocalTime(wall, tz)
+}
+
+func checkedCalendarMidnight(year int, month time.Month, day int, tz 
*time.Location) (time.Time, error) {
+       return checkedLocalTime(time.Date(year, month, day, 0, 0, 0, 0, 
time.UTC), tz)
+}
+
+func checkedCalendarAddDays(value time.Time, days int64) (time.Time, error) {
+       dayOffset, err := checkedInt64ToInt(days)
+       if err != nil {
+               return time.Time{}, err
+       }
+       result := value.AddDate(0, 0, dayOffset)
+       if (days > 0 && result.Before(value)) || (days < 0 && 
result.After(value)) {
+               return time.Time{}, overflowError()
+       }
+       return result, nil
+}
+
+func roundToMultipleInt64(value, multiple int64, mode RoundMode, strictCeil 
bool) (int64, error) {
        if multiple == 0 || value%multiple == 0 {
                if strictCeil && mode == RoundUp {
-                       return value + multiple
+                       return checkedAddInt64(value, multiple)
                }
-               return value
+               return value, nil
        }
 
        quotient := value / multiple
@@ -983,17 +1461,29 @@ func roundToMultipleInt64(value, multiple int64, mode 
RoundMode, strictCeil bool
        switch mode {
        case RoundDown:
                if remainder < 0 {
-                       return (quotient - 1) * multiple
+                       quotient, err := checkedSubInt64(quotient, 1)
+                       if err != nil {
+                               return 0, err
+                       }
+                       return checkedMulInt64(quotient, multiple)
                }
-               return quotient * multiple
+               return checkedMulInt64(quotient, multiple)
        case RoundUp:
                if remainder > 0 || (strictCeil && remainder == 0) {
-                       return (quotient + 1) * multiple
+                       quotient, err := checkedAddInt64(quotient, 1)
+                       if err != nil {
+                               return 0, err
+                       }
+                       return checkedMulInt64(quotient, multiple)
                }
                if remainder < 0 {
-                       return quotient * multiple
+                       return checkedMulInt64(quotient, multiple)
+               }
+               quotient, err := checkedAddInt64(quotient, 1)
+               if err != nil {
+                       return 0, err
                }
-               return (quotient + 1) * multiple
+               return checkedMulInt64(quotient, multiple)
        case HalfUp, HalfDown, HalfToEven:
                half := multiple / 2
                absRemainder := remainder
@@ -1005,204 +1495,741 @@ func roundToMultipleInt64(value, multiple int64, mode 
RoundMode, strictCeil bool
                // 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
+                       return checkedMulInt64(quotient, multiple)
                } else if absRemainder > half {
                        if remainder > 0 {
-                               return (quotient + 1) * multiple
+                               quotient, err := checkedAddInt64(quotient, 1)
+                               if err != nil {
+                                       return 0, err
+                               }
+                               return checkedMulInt64(quotient, multiple)
+                       }
+                       quotient, err := checkedSubInt64(quotient, 1)
+                       if err != nil {
+                               return 0, err
                        }
-                       return (quotient - 1) * multiple
+                       return checkedMulInt64(quotient, multiple)
                } else {
                        // Exactly on the halfway point
                        switch mode {
                        case HalfDown:
                                if remainder > 0 {
-                                       return quotient * multiple
+                                       return checkedMulInt64(quotient, 
multiple)
                                }
-                               return (quotient - 1) * multiple
+                               quotient, err := checkedSubInt64(quotient, 1)
+                               if err != nil {
+                                       return 0, err
+                               }
+                               return checkedMulInt64(quotient, multiple)
                        case HalfUp:
                                if remainder > 0 {
-                                       return (quotient + 1) * multiple
+                                       quotient, err := 
checkedAddInt64(quotient, 1)
+                                       if err != nil {
+                                               return 0, err
+                                       }
+                                       return checkedMulInt64(quotient, 
multiple)
                                }
-                               return quotient * multiple
+                               return checkedMulInt64(quotient, multiple)
                        case HalfToEven:
                                if quotient%2 == 0 {
-                                       return quotient * multiple
+                                       return checkedMulInt64(quotient, 
multiple)
                                }
                                if remainder > 0 {
-                                       return (quotient + 1) * multiple
+                                       quotient, err := 
checkedAddInt64(quotient, 1)
+                                       if err != nil {
+                                               return 0, err
+                                       }
+                                       return checkedMulInt64(quotient, 
multiple)
                                }
-                               return (quotient - 1) * multiple
+                               quotient, err := checkedSubInt64(quotient, 1)
+                               if err != nil {
+                                       return 0, err
+                               }
+                               return checkedMulInt64(quotient, multiple)
                        }
                }
        }
-       return quotient * multiple
+       return checkedMulInt64(quotient, multiple)
 }
 
-// halfRoundPeriod performs half-rounding by finding the midpoint between 
period start and end
-func halfRoundPeriod(t, periodStart, periodEnd time.Time) time.Time {
-       midPoint := periodStart.Add(periodEnd.Sub(periodStart) / 2)
-       if t.Before(midPoint) {
-               return periodStart
+// halfRoundPeriod performs half-rounding by finding the midpoint between 
period start and end.
+// It does not use time.Time.Sub or time.Time.Unix because those methods 
cannot represent
+// sufficiently large calendar periods and timestamps.
+func halfRoundPeriod(t, periodStart, periodEnd time.Time) (time.Time, error) {
+       startDays, startNanos, err := calendarTimeParts(periodStart)
+       if err != nil {
+               return time.Time{}, err
+       }
+       endDays, endNanos, err := calendarTimeParts(periodEnd)
+       if err != nil {
+               return time.Time{}, err
+       }
+       if endDays < startDays || (endDays == startDays && endNanos < 
startNanos) {
+               return time.Time{}, overflowError()
+       }
+       deltaDays, err := checkedSubInt64(endDays, startDays)
+       if err != nil {
+               return time.Time{}, err
+       }
+       deltaNanos, err := checkedSubInt64(endNanos, startNanos)
+       if err != nil {
+               return time.Time{}, err
+       }
+       if deltaNanos < 0 {
+               deltaDays, err = checkedSubInt64(deltaDays, 1)
+               if err != nil {
+                       return time.Time{}, err
+               }
+               deltaNanos += nanosPerDay
+       }
+
+       tDays, tNanos, err := calendarTimeParts(t)
+       if err != nil {
+               return time.Time{}, err
+       }
+       relativeDays, err := checkedSubInt64(tDays, startDays)
+       if err != nil {
+               return time.Time{}, err
+       }
+       relativeNanos, err := checkedSubInt64(tNanos, startNanos)
+       if err != nil {
+               return time.Time{}, err
+       }
+       if relativeNanos < 0 {
+               relativeDays, err = checkedSubInt64(relativeDays, 1)
+               if err != nil {
+                       return time.Time{}, err
+               }
+               relativeNanos += nanosPerDay
+       }
+
+       // Compare 2*(t-periodStart) with periodEnd-periodStart without ever
+       // converting the whole interval to a time.Duration.
+       doubledDays, err := checkedMulInt64(relativeDays, 2)
+       if err != nil {
+               return time.Time{}, err
+       }
+       doubledNanos, err := checkedMulInt64(relativeNanos, 2)
+       if err != nil {
+               return time.Time{}, err
+       }
+       if doubledNanos >= nanosPerDay {
+               doubledDays, err = checkedAddInt64(doubledDays, 1)
+               if err != nil {
+                       return time.Time{}, err
+               }
+               doubledNanos -= nanosPerDay
+       }
+
+       if doubledDays < deltaDays || (doubledDays == deltaDays && doubledNanos 
< deltaNanos) {
+               return periodStart, nil
+       }
+       return periodEnd, nil
+}
+
+const nanosPerDay = int64(24 * time.Hour)
+
+// calendarTimeParts returns a time instant as a day number and nanoseconds 
into
+// the corresponding UTC day. Unlike Unix timestamps, this representation
+// remains valid for all time.Time values supported by the time package.
+func calendarTimeParts(value time.Time) (int64, int64, error) {
+       days, err := calendarDays(value)
+       if err != nil {
+               return 0, 0, err
+       }
+
+       seconds := int64(value.Hour())*3600 + int64(value.Minute())*60 + 
int64(value.Second())
+       nanos, err := checkedMulInt64(seconds, int64(time.Second))
+       if err != nil {
+               return 0, 0, err
+       }
+       nanos, err = checkedAddInt64(nanos, int64(value.Nanosecond()))
+       if err != nil {
+               return 0, 0, err
+       }
+       _, offset := value.Zone()
+       offsetNanos, err := checkedMulInt64(int64(offset), int64(time.Second))
+       if err != nil {
+               return 0, 0, err
+       }
+       nanos, err = checkedSubInt64(nanos, offsetNanos)
+       if err != nil {
+               return 0, 0, err
+       }
+
+       if nanos < 0 {
+               days, err = checkedSubInt64(days, 1)
+               if err != nil {
+                       return 0, 0, err
+               }
+               nanos += nanosPerDay
+       } else if nanos >= nanosPerDay {
+               days, err = checkedAddInt64(days, 1)
+               if err != nil {
+                       return 0, 0, err
+               }
+               nanos -= nanosPerDay
+       }
+
+       return days, nanos, nil
+}
+
+// calendarDays returns the proleptic Gregorian day number for a local date.
+// The calculation is based on civil calendar fields, so it does not saturate
+// at time.Duration or int64 Unix-second boundaries.
+func calendarDays(value time.Time) (int64, error) {
+       year := int64(value.Year())
+       month := int64(value.Month())
+       day := int64(value.Day())
+       if month <= 2 {
+               var err error
+               year, err = checkedSubInt64(year, 1)
+               if err != nil {
+                       return 0, err
+               }
+       }
+
+       era := year / 400
+       if year < 0 && year%400 != 0 {
+               era--
+       }
+       eraYear, err := checkedMulInt64(era, 400)
+       if err != nil {
+               return 0, err
+       }
+       yearOfEra, err := checkedSubInt64(year, eraYear)
+       if err != nil {
+               return 0, err
+       }
+
+       monthOfMarch := month
+       if month > 2 {
+               monthOfMarch -= 3
+       } else {
+               monthOfMarch += 9
+       }
+       daysOfYear := (153*monthOfMarch+2)/5 + day - 1
+       daysOfEra := yearOfEra*365 + yearOfEra/4 - yearOfEra/100 + daysOfYear
+       days, err := checkedMulInt64(era, 146097)
+       if err != nil {
+               return 0, err
+       }
+       days, err = checkedAddInt64(days, daysOfEra)
+       if err != nil {
+               return 0, err
+       }
+       return checkedSubInt64(days, 719468)
+}
+
+func calendarDayDifference(left, right time.Time) (int64, error) {
+       leftDays, err := calendarDays(left)
+       if err != nil {
+               return 0, err
+       }
+       rightDays, err := calendarDays(right)
+       if err != nil {
+               return 0, err
+       }
+       return checkedSubInt64(leftDays, rightDays)
+}
+
+// floorCalendarIndex returns the start of the current calendar period as an
+// index in either months or quarters. Without a calendar origin, periods are
+// anchored at 1970-01-01. With one, multiple periods start at the beginning of
+// the current year.
+func floorCalendarIndex(year, offset, periodsPerYear, multiple int64, 
calendarOrigin bool) (int64, error) {
+       yearIndex, err := checkedMulInt64(year, periodsPerYear)
+       if err != nil {
+               return 0, err
+       }
+       currentIndex, err := checkedAddInt64(yearIndex, offset)
+       if err != nil {
+               return 0, err
+       }
+
+       if calendarOrigin && multiple != 1 {
+               roundedOffset, err := checkedMulInt64(floorDivInt64(offset, 
multiple), multiple)
+               if err != nil {
+                       return 0, err
+               }
+               return checkedAddInt64(yearIndex, roundedOffset)
+       }
+
+       epochIndex, err := checkedMulInt64(1970, periodsPerYear)
+       if err != nil {
+               return 0, err
+       }
+       relative, err := checkedSubInt64(currentIndex, epochIndex)
+       if err != nil {
+               return 0, err
+       }
+       roundedRelative, err := checkedMulInt64(floorDivInt64(relative, 
multiple), multiple)
+       if err != nil {
+               return 0, err
+       }
+       return checkedAddInt64(epochIndex, roundedRelative)
+}
+
+func calendarDateFromIndex(index, periodsPerYear, monthsPerPeriod int64, tz 
*time.Location) (time.Time, error) {
+       year := floorDivInt64(index, periodsPerYear)
+       yearIndex, err := checkedMulInt64(year, periodsPerYear)
+       if err != nil {
+               return time.Time{}, err
+       }
+       offset, err := checkedSubInt64(index, yearIndex)
+       if err != nil {
+               return time.Time{}, err
+       }
+       monthOffset, err := checkedMulInt64(offset, monthsPerPeriod)
+       if err != nil {
+               return time.Time{}, err
+       }
+       month, err := checkedAddInt64(monthOffset, 1)
+       if err != nil {
+               return time.Time{}, err
+       }
+       return checkedCalendarDate(year, month, tz)
+}
+
+func floorCalendarDay(value time.Time, multiple int64, calendarOrigin bool, tz 
*time.Location) (time.Time, error) {
+       valueDay := time.Date(value.Year(), value.Month(), value.Day(), 0, 0, 
0, 0, time.UTC)
+       if multiple == 1 {
+               return checkedCalendarMidnight(value.Year(), value.Month(), 
value.Day(), tz)
+       }
+
+       origin := time.Date(1970, 1, 1, 0, 0, 0, 0, time.UTC)
+       if calendarOrigin {
+               origin = time.Date(value.Year(), value.Month(), 1, 0, 0, 0, 0, 
time.UTC)
+       }
+       daysSinceOrigin, err := calendarDayDifference(valueDay, origin)
+       if err != nil {
+               return time.Time{}, err
+       }
+       roundedDays, err := checkedMulInt64(floorDivInt64(daysSinceOrigin, 
multiple), multiple)
+       if err != nil {
+               return time.Time{}, err
+       }
+       result, err := checkedCalendarAddDays(origin, roundedDays)
+       if err != nil {
+               return time.Time{}, err
+       }
+       return checkedCalendarMidnight(result.Year(), result.Month(), 
result.Day(), tz)
+}
+
+func floorCalendarWeek(value time.Time, multiple int64, weekStartsMonday bool, 
tz *time.Location) (time.Time, error) {
+       valueDay := time.Date(value.Year(), value.Month(), value.Day(), 0, 0, 
0, 0, time.UTC)
+       weekday := int(valueDay.Weekday())
+       targetWeekday := time.Wednesday
+       if weekStartsMonday {
+               weekday = (weekday + 6) % 7
+               targetWeekday = time.Thursday
+       }
+       weekStart, err := checkedCalendarAddDays(valueDay, -int64(weekday))
+       if err != nil {
+               return time.Time{}, err
+       }
+       // Use a fixed representative day from the input's week to identify the
+       // calendar week-year. Adding a constant to the input can cross into the
+       // following week for late-week values.
+       weekYearDate, err := checkedCalendarAddDays(weekStart, 3)
+       if err != nil {
+               return time.Time{}, err
+       }
+
+       previousYear, err := checkedSubInt64(int64(weekYearDate.Year()), 1)
+       if err != nil {
+               return time.Time{}, err
+       }
+       december := time.Date(int(previousYear), time.December, 31, 0, 0, 0, 0, 
time.UTC)
+       daysBack := (int(december.Weekday()) - int(targetWeekday) + 7) % 7
+       lastWeekday, err := checkedCalendarAddDays(december, -int64(daysBack))
+       if err != nil {
+               return time.Time{}, err
+       }
+       origin, err := checkedCalendarAddDays(lastWeekday, 4)
+       if err != nil {
+               return time.Time{}, err
+       }
+
+       daysSinceOrigin, err := calendarDayDifference(valueDay, origin)
+       if err != nil {
+               return time.Time{}, err
+       }
+       weeksSinceOrigin := floorDivInt64(daysSinceOrigin, 7)
+       roundedWeeks, err := checkedMulInt64(floorDivInt64(weeksSinceOrigin, 
multiple), multiple)
+       if err != nil {
+               return time.Time{}, err
+       }
+       roundedDays, err := checkedMulInt64(roundedWeeks, 7)
+       if err != nil {
+               return time.Time{}, err
+       }
+       result, err := checkedCalendarAddDays(origin, roundedDays)
+       if err != nil {
+               return time.Time{}, err
+       }
+       return checkedLocalTime(result, tz)
+}
+
+func floorFixedCalendarWeek(value time.Time, multiple int64, weekStartsMonday 
bool, tz *time.Location) (time.Time, error) {
+       weekdayOffset := int64(4)
+       if weekStartsMonday {
+               weekdayOffset = 3
+       }
+
+       // The reference algorithm shifts the local wall-clock timestamp by the
+       // weekday offset, floors fixed seven-day periods from the Unix epoch, 
and
+       // only then converts the candidate back to system time. Keep the 
wall-clock
+       // date in UTC while doing the arithmetic so a missing local midnight 
cannot
+       // move the period into a different civil date.
+       shifted, err := checkedCalendarAddDays(calendarWallTime(value), 
weekdayOffset)
+       if err != nil {
+               return time.Time{}, err
+       }
+       shiftedDay := time.Date(shifted.Year(), shifted.Month(), shifted.Day(), 
0, 0, 0, 0, time.UTC)
+       dayIndex, err := calendarDays(shiftedDay)
+       if err != nil {
+               return time.Time{}, err
+       }
+       weekIndex := floorDivInt64(dayIndex, 7)
+       roundedWeeks, err := checkedMulInt64(floorDivInt64(weekIndex, 
multiple), multiple)
+       if err != nil {
+               return time.Time{}, err
+       }
+       roundedDays, err := checkedMulInt64(roundedWeeks, 7)
+       if err != nil {
+               return time.Time{}, err
+       }
+       boundaryWall, err := checkedCalendarAddDays(time.Date(1970, 1, 1, 0, 0, 
0, 0, time.UTC), roundedDays)
+       if err != nil {
+               return time.Time{}, err
+       }
+       weekStartWall, err := checkedCalendarAddDays(boundaryWall, 
-weekdayOffset)
+       if err != nil {
+               return time.Time{}, err
+       }
+       return time.Date(weekStartWall.Year(), weekStartWall.Month(), 
weekStartWall.Day(), 0, 0, 0, 0, tz), nil
+}
+
+func ceilFixedCalendarWeek(value, floor time.Time, multiple int64, strict 
bool, tz *time.Location) (time.Time, error) {
+       floorWall := calendarWallTime(floor.In(tz))
+       cs, err := checkedLocalTime(floorWall, tz)
+       if err != nil {
+               return time.Time{}, err
+       }
+       if !strict && !cs.Before(value) {
+               return cs, nil
+       }
+
+       weekDays, err := checkedMulInt64(multiple, 7)
+       if err != nil {
+               return time.Time{}, err
+       }
+       nextWall, err := checkedCalendarAddDays(floorWall, weekDays)
+       if err != nil {
+               return time.Time{}, err
+       }
+       return checkedLocalTime(nextWall, tz)
+}
+
+func roundCalendarOriginWeek(value time.Time, opts roundTemporalState, tz 
*time.Location) (time.Time, error) {
+       floor, err := floorCalendarWeek(value, opts.Multiple, 
opts.WeekStartsMonday, tz)
+       if err != nil {
+               return time.Time{}, err
+       }
+       if opts.mode == RoundDown {
+               return floor, nil
+       }
+
+       var next time.Time
+       makeNext := func() (time.Time, error) {
+               if next.IsZero() {
+                       weekDays, err := checkedMulInt64(opts.Multiple, 7)
+                       if err != nil {
+                               return time.Time{}, err
+                       }
+                       nextWall, err := 
checkedCalendarAddDays(calendarWallTime(floor.In(tz)), weekDays)
+                       if err != nil {
+                               return time.Time{}, err
+                       }
+                       next, err = checkedLocalTime(nextWall, tz)
+                       if err != nil {
+                               return time.Time{}, err
+                       }
+               }
+               return next, nil
+       }
+
+       cs := floor
+
+       switch opts.mode {
+       case RoundUp:
+               if opts.CeilIsStrictlyGreater || cs.Before(value) {
+                       return makeNext()
+               }
+               return cs, nil
+       default:
+               if cs.After(value) {
+                       return cs, nil
+               }
+               next, err := makeNext()
+               if err != nil {
+                       return time.Time{}, err
+               }
+               return halfRoundPeriod(value, floor, next)
        }
-       return periodEnd
 }
 
 // roundTimestampCalendar handles calendar-based rounding (year, quarter, 
month, week, day).
 // Requires date arithmetic for variable-length periods and timezone-aware 
boundaries.
-func roundTimestampCalendar(tsNanos int64, inputUnit arrow.TimeUnit, tz 
*time.Location, opts roundTemporalState) (int64, error) {
+func roundTimestampCalendar(ts int64, inputUnit arrow.TimeUnit, tz 
*time.Location, opts roundTemporalState) (int64, error) {
        // Convert to time.Time for calendar operations in the specified 
timezone
-       secs := tsNanos / 1000000000
-       nanos := tsNanos % 1000000000
-       t := time.Unix(secs, nanos).In(tz)
+       t, err := timestampToCalendarTime(ts, inputUnit, tz)
+       if err != nil {
+               return 0, err
+       }
 
        var rounded time.Time
+       multiple := opts.Multiple
 
        switch opts.Unit {
        case RoundTemporalYear:
-               year := t.Year()
-               roundedYear := (year / int(opts.Multiple)) * int(opts.Multiple)
+               year := int64(t.Year())
+               roundedYear, err := checkedMulInt64(floorDivInt64(year, 
multiple), multiple)
+               if err != nil {
+                       return 0, err
+               }
                switch opts.mode {
                case RoundDown:
-                       rounded = time.Date(roundedYear, 1, 1, 0, 0, 0, 0, tz)
+                       var dateErr error
+                       rounded, dateErr = checkedCalendarDate(roundedYear, 1, 
tz)
+                       if dateErr != nil {
+                               return 0, dateErr
+                       }
                case RoundUp:
-                       periodStart := time.Date(roundedYear, 1, 1, 0, 0, 0, 0, 
tz)
-                       if opts.CeilIsStrictlyGreater || !t.Equal(periodStart) {
-                               roundedYear += int(opts.Multiple)
-                               rounded = time.Date(roundedYear, 1, 1, 0, 0, 0, 
0, tz)
-                       } else {
-                               rounded = periodStart
+                       roundedYear, err = checkedAddInt64(roundedYear, 
multiple)
+                       if err != nil {
+                               return 0, err
+                       }
+                       rounded, err = checkedCalendarDate(roundedYear, 1, tz)
+                       if err != nil {
+                               return 0, err
                        }
                default:
-                       yearStart := time.Date(roundedYear, 1, 1, 0, 0, 0, 0, 
tz)
-                       nextYear := roundedYear + int(opts.Multiple)
-                       yearEnd := time.Date(nextYear, 1, 1, 0, 0, 0, 0, tz)
-                       rounded = halfRoundPeriod(t, yearStart, yearEnd)
+                       yearStart, dateErr := checkedCalendarDate(roundedYear, 
1, tz)
+                       if dateErr != nil {
+                               return 0, dateErr
+                       }
+                       nextYear, dateErr := checkedAddInt64(roundedYear, 
multiple)
+                       if dateErr != nil {
+                               return 0, dateErr
+                       }
+                       yearEnd, dateErr := checkedCalendarDate(nextYear, 1, tz)
+                       if dateErr != nil {
+                               return 0, dateErr
+                       }
+                       rounded, err = halfRoundPeriod(t, yearStart, yearEnd)
+                       if err != nil {
+                               return 0, err
+                       }
                }
 
        case RoundTemporalQuarter:
                // Q1=Jan-Mar, Q2=Apr-Jun, Q3=Jul-Sep, Q4=Oct-Dec
-               month := int(t.Month())
-               year := t.Year()
-               totalQuarters := year*4 + (month-1)/3
-               roundedQuarters := (totalQuarters / int(opts.Multiple)) * 
int(opts.Multiple)
-               roundedYear := roundedQuarters / 4
-               roundedQuarter := roundedQuarters % 4
-               roundedMonth := roundedQuarter*3 + 1 // First month of the 
quarter
+               month := int64(t.Month())
+               year := int64(t.Year())
+               roundedQuarters, err := floorCalendarIndex(
+                       year, (month-1)/3, 4, multiple, 
opts.CalendarBasedOrigin,
+               )
+               if err != nil {
+                       return 0, err
+               }
 
                switch opts.mode {
                case RoundDown:
-                       rounded = time.Date(roundedYear, 
time.Month(roundedMonth), 1, 0, 0, 0, 0, tz)
+                       rounded, err = calendarDateFromIndex(roundedQuarters, 
4, 3, tz)
+                       if err != nil {
+                               return 0, err
+                       }
                case RoundUp:
-                       periodStart := time.Date(roundedYear, 
time.Month(roundedMonth), 1, 0, 0, 0, 0, tz)
-                       if opts.CeilIsStrictlyGreater || !t.Equal(periodStart) {
-                               roundedQuarters += int(opts.Multiple)
-                               roundedYear = roundedQuarters / 4
-                               roundedQuarter = roundedQuarters % 4
-                               roundedMonth = roundedQuarter*3 + 1
-                               rounded = time.Date(roundedYear, 
time.Month(roundedMonth), 1, 0, 0, 0, 0, tz)
-                       } else {
-                               rounded = periodStart
+                       nextQuarters, dateErr := 
checkedAddInt64(roundedQuarters, multiple)
+                       if dateErr != nil {
+                               return 0, dateErr
+                       }
+                       rounded, dateErr = calendarDateFromIndex(nextQuarters, 
4, 3, tz)
+                       if dateErr != nil {
+                               return 0, dateErr
                        }
                default:
-                       quarterStart := time.Date(roundedYear, 
time.Month(roundedMonth), 1, 0, 0, 0, 0, tz)
-                       nextQuarterNum := roundedQuarters + int(opts.Multiple)
-                       nextYear := nextQuarterNum / 4
-                       nextQuarter := nextQuarterNum % 4
-                       nextMonth := nextQuarter*3 + 1
-                       quarterEnd := time.Date(nextYear, 
time.Month(nextMonth), 1, 0, 0, 0, 0, tz)
-                       rounded = halfRoundPeriod(t, quarterStart, quarterEnd)
+                       quarterStart, dateErr := 
calendarDateFromIndex(roundedQuarters, 4, 3, tz)
+                       if dateErr != nil {
+                               return 0, dateErr
+                       }
+                       nextQuarterNum, dateErr := 
checkedAddInt64(roundedQuarters, multiple)
+                       if dateErr != nil {
+                               return 0, dateErr
+                       }
+                       quarterEnd, dateErr := 
calendarDateFromIndex(nextQuarterNum, 4, 3, tz)
+                       if dateErr != nil {
+                               return 0, dateErr
+                       }
+                       rounded, err = halfRoundPeriod(t, quarterStart, 
quarterEnd)
+                       if err != nil {
+                               return 0, err
+                       }
                }
 
        case RoundTemporalMonth:
-               month := int(t.Month())
-               year := t.Year()
-               totalMonths := year*12 + month - 1
-               roundedMonths := (totalMonths / int(opts.Multiple)) * 
int(opts.Multiple)
-               roundedYear := roundedMonths / 12
-               roundedMonth := (roundedMonths % 12) + 1
+               month := int64(t.Month())
+               year := int64(t.Year())
+               roundedMonths, err := floorCalendarIndex(
+                       year, month-1, 12, multiple, opts.CalendarBasedOrigin,
+               )
+               if err != nil {
+                       return 0, err
+               }
 
                switch opts.mode {
                case RoundDown:
-                       rounded = time.Date(roundedYear, 
time.Month(roundedMonth), 1, 0, 0, 0, 0, tz)
+                       rounded, err = calendarDateFromIndex(roundedMonths, 12, 
1, tz)
+                       if err != nil {
+                               return 0, err
+                       }
                case RoundUp:
-                       periodStart := time.Date(roundedYear, 
time.Month(roundedMonth), 1, 0, 0, 0, 0, tz)
-                       if opts.CeilIsStrictlyGreater || !t.Equal(periodStart) {
-                               roundedMonths += int(opts.Multiple)
-                               roundedYear = roundedMonths / 12
-                               roundedMonth = (roundedMonths % 12) + 1
-                               rounded = time.Date(roundedYear, 
time.Month(roundedMonth), 1, 0, 0, 0, 0, tz)
-                       } else {
-                               rounded = periodStart
+                       nextMonths, dateErr := checkedAddInt64(roundedMonths, 
multiple)
+                       if dateErr != nil {
+                               return 0, dateErr
+                       }
+                       rounded, dateErr = calendarDateFromIndex(nextMonths, 
12, 1, tz)
+                       if dateErr != nil {
+                               return 0, dateErr
                        }
                default:
-                       monthStart := time.Date(roundedYear, 
time.Month(roundedMonth), 1, 0, 0, 0, 0, tz)
-                       nextMonthNum := roundedMonths + int(opts.Multiple)
-                       nextYear := nextMonthNum / 12
-                       nextMonth := (nextMonthNum % 12) + 1
-                       monthEnd := time.Date(nextYear, time.Month(nextMonth), 
1, 0, 0, 0, 0, tz)
-                       rounded = halfRoundPeriod(t, monthStart, monthEnd)
+                       monthStart, dateErr := 
calendarDateFromIndex(roundedMonths, 12, 1, tz)
+                       if dateErr != nil {
+                               return 0, dateErr
+                       }
+                       nextMonthNum, dateErr := checkedAddInt64(roundedMonths, 
multiple)
+                       if dateErr != nil {
+                               return 0, dateErr
+                       }
+                       monthEnd, dateErr := 
calendarDateFromIndex(nextMonthNum, 12, 1, tz)
+                       if dateErr != nil {
+                               return 0, dateErr
+                       }
+                       rounded, err = halfRoundPeriod(t, monthStart, monthEnd)
+                       if err != nil {
+                               return 0, err
+                       }
                }
 
        case RoundTemporalWeek:
-               weekday := int(t.Weekday())
-               if opts.WeekStartsMonday {
-                       weekday = (weekday + 6) % 7
+               if opts.CalendarBasedOrigin && multiple != 1 {
+                       rounded, err = roundCalendarOriginWeek(t, opts, tz)
+                       if err != nil {
+                               return 0, err
+                       }
+                       break
                }
-               startOfWeek := t.AddDate(0, 0, -weekday)
-               startOfWeek = time.Date(startOfWeek.Year(), 
startOfWeek.Month(), startOfWeek.Day(), 0, 0, 0, 0, tz)
 
-               // Calculate N-week periods from epoch for Multiple > 1
-               epochInTz := time.Unix(0, 0).In(tz)
-               epochWeekday := int(epochInTz.Weekday())
-               if opts.WeekStartsMonday {
-                       epochWeekday = (epochWeekday + 6) % 7
+               floor, dateErr := floorFixedCalendarWeek(t, multiple, 
opts.WeekStartsMonday, tz)
+               if dateErr != nil {
+                       return 0, dateErr
+               }
+               ceil, dateErr := ceilFixedCalendarWeek(t, floor, multiple, 
opts.CeilIsStrictlyGreater, tz)
+               if dateErr != nil {
+                       return 0, dateErr
                }
-               epochWeekStart := epochInTz.AddDate(0, 0, -epochWeekday)
-               epochWeekStart = time.Date(epochWeekStart.Year(), 
epochWeekStart.Month(), epochWeekStart.Day(), 0, 0, 0, 0, tz)
-
-               daysSinceEpochWeek := 
int(startOfWeek.Sub(epochWeekStart).Hours() / 24)
-               weeksSinceEpoch := daysSinceEpochWeek / 7
-               roundedWeeks := (weeksSinceEpoch / int(opts.Multiple)) * 
int(opts.Multiple)
-               roundedWeekStart := epochWeekStart.AddDate(0, 0, roundedWeeks*7)
 
                switch opts.mode {
                case RoundDown:
-                       rounded = roundedWeekStart
+                       rounded = floor
                case RoundUp:
-                       if opts.CeilIsStrictlyGreater || 
!t.Equal(roundedWeekStart) {
-                               rounded = roundedWeekStart.AddDate(0, 0, 
7*int(opts.Multiple))
-                       } else {
-                               rounded = roundedWeekStart
-                       }
+                       rounded = ceil
                default:
-                       weekEnd := roundedWeekStart.AddDate(0, 0, 
7*int(opts.Multiple))
-                       rounded = halfRoundPeriod(t, roundedWeekStart, weekEnd)
+                       rounded, err = halfRoundPeriod(t, floor, ceil)
+                       if err != nil {
+                               return 0, err
+                       }
                }
 
        case RoundTemporalDay:
-               startOfDay := time.Date(t.Year(), t.Month(), t.Day(), 0, 0, 0, 
0, tz)
+               startOfDay, dateErr := floorCalendarDay(t, multiple, 
opts.CalendarBasedOrigin, tz)
+               if dateErr != nil {
+                       return 0, dateErr
+               }
+               makeNextDay := func() (time.Time, error) {
+                       nextWall, err := 
checkedCalendarAddDays(calendarWallTime(startOfDay), multiple)
+                       if err != nil {
+                               return time.Time{}, err
+                       }
+                       return checkedLocalTime(nextWall, tz)
+               }
 
                switch opts.mode {
                case RoundDown:
                        rounded = startOfDay
                case RoundUp:
                        if opts.CeilIsStrictlyGreater || !t.Equal(startOfDay) {
-                               rounded = startOfDay.AddDate(0, 0, 1)
+                               rounded, dateErr = makeNextDay()
+                               if dateErr != nil {
+                                       return 0, dateErr
+                               }
                        } else {
                                rounded = startOfDay
                        }
                default:
-                       nextDay := startOfDay.AddDate(0, 0, 1)
-                       rounded = halfRoundPeriod(t, startOfDay, nextDay)
+                       nextDay, nextErr := makeNextDay()
+                       if nextErr != nil {
+                               return 0, nextErr
+                       }
+                       rounded, err = halfRoundPeriod(t, startOfDay, nextDay)
+                       if err != nil {
+                               return 0, err
+                       }
                }
 
        default:
                return 0, fmt.Errorf("%w: unsupported calendar unit", 
arrow.ErrNotImplemented)
        }
+       if err != nil {
+               return 0, err
+       }
 
-       // Convert back to the input unit
-       roundedNanos := rounded.UnixNano()
-       return convertFromNanos(roundedNanos, inputUnit), nil
+       // Convert back to the input unit, validating only the selected result.
+       roundedTimestamp, err := timestampFromCalendarTime(rounded, inputUnit)
+       if err != nil {
+               return 0, err
+       }
+       return int64(roundedTimestamp), nil
+}
+
+func timestampFromCalendarTime(value time.Time, unit arrow.TimeUnit) 
(arrow.Timestamp, error) {
+       if unit != arrow.Second {
+               return arrow.TimestampFromTime(value, unit)
+       }
+
+       days, nanos, err := calendarTimeParts(value)
+       if err != nil {
+               return 0, err
+       }
+       seconds, err := checkedMulInt64(days, int64(24*time.Hour/time.Second))
+       if err != nil {
+               return 0, err
+       }
+       seconds, err = checkedAddInt64(seconds, nanos/int64(time.Second))
+       if err != nil {
+               return 0, err
+       }
+       return arrow.Timestamp(seconds), nil
+}
+
+func timeToNanos(value time.Time) (int64, error) {
+       timestamp, err := arrow.TimestampFromTime(value, arrow.Nanosecond)
+       if err != nil {
+               return 0, err
+       }
+       return int64(timestamp), nil
 }
 
 // Kernel execution functions for temporal rounding
@@ -1240,7 +2267,12 @@ func roundTemporalExec(ctx *exec.KernelCtx, batch 
*exec.ExecSpan, out *exec.Exec
                                return 0
                        }
                        // Convert back to days
-                       return int32(result / 86400)
+                       daysResult := result / 86400
+                       if daysResult < math.MinInt32 || daysResult > 
math.MaxInt32 {
+                               *e = overflowError()
+                               return 0
+                       }
+                       return int32(daysResult)
                }
                return ScalarUnaryNotNull(fn)(ctx, batch, out)
 
diff --git 
a/arrow/compute/internal/kernels/temporal_rounding_odd_multiple_test.go 
b/arrow/compute/internal/kernels/temporal_rounding_odd_multiple_test.go
index 3f445c1b..e356be8a 100644
--- a/arrow/compute/internal/kernels/temporal_rounding_odd_multiple_test.go
+++ b/arrow/compute/internal/kernels/temporal_rounding_odd_multiple_test.go
@@ -33,7 +33,10 @@ func TestRoundToMultipleInt64OddMultipleAcrossModes(t 
*testing.T) {
                                {value: -1, want: 0},
                                {value: -2, want: -3},
                        } {
-                               got := roundToMultipleInt64(tc.value, 3, mode, 
false)
+                               got, err := roundToMultipleInt64(tc.value, 3, 
mode, false)
+                               if err != nil {
+                                       t.Fatalf("roundToMultipleInt64(%d, 3, 
%s) returned an error: %v", tc.value, mode, err)
+                               }
                                if got != tc.want {
                                        t.Errorf("roundToMultipleInt64(%d, 3, 
%s) = %d, want %d", tc.value, mode, got, tc.want)
                                }
diff --git a/arrow/compute/internal/kernels/temporal_rounding_overflow_test.go 
b/arrow/compute/internal/kernels/temporal_rounding_overflow_test.go
new file mode 100644
index 00000000..9ba61b92
--- /dev/null
+++ b/arrow/compute/internal/kernels/temporal_rounding_overflow_test.go
@@ -0,0 +1,189 @@
+// 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 (
+       "errors"
+       "math"
+       "testing"
+       "time"
+
+       "github.com/apache/arrow-go/v18/arrow"
+)
+
+func TestTemporalCheckedArithmeticBoundaries(t *testing.T) {
+       tests := []struct {
+               name string
+               fn   func() (int64, error)
+       }{
+               {name: "positive multiply overflow", fn: func() (int64, error) {
+                       return checkedMulInt64(math.MaxInt64, 2)
+               }},
+               {name: "negative multiply overflow", fn: func() (int64, error) {
+                       return checkedMulInt64(math.MinInt64, 2)
+               }},
+               {name: "minimum times negative one", fn: func() (int64, error) {
+                       return checkedMulInt64(math.MinInt64, -1)
+               }},
+               {name: "subtraction below minimum", fn: func() (int64, error) {
+                       return checkedSubInt64(math.MinInt64, 1)
+               }},
+               {name: "subtraction above maximum", fn: func() (int64, error) {
+                       return checkedSubInt64(math.MaxInt64, -1)
+               }},
+       }
+
+       for _, tc := range tests {
+               t.Run(tc.name, func(t *testing.T) {
+                       _, err := tc.fn()
+                       if !errors.Is(err, arrow.ErrInvalid) {
+                               t.Fatalf("got error %v, want arrow.ErrInvalid", 
err)
+                       }
+               })
+       }
+}
+
+func TestTimeToNanosBoundaries(t *testing.T) {
+       minTime := time.Unix(0, math.MinInt64)
+       maxTime := time.Unix(0, math.MaxInt64)
+       tests := []struct {
+               name  string
+               value time.Time
+               want  int64
+               err   bool
+       }{
+               {name: "exact minimum", value: minTime, want: math.MinInt64},
+               {name: "exact maximum", value: maxTime, want: math.MaxInt64},
+               {name: "one below minimum", value: 
minTime.Add(-time.Nanosecond), err: true},
+               {name: "one above maximum", value: 
maxTime.Add(time.Nanosecond), err: true},
+       }
+
+       for _, tc := range tests {
+               t.Run(tc.name, func(t *testing.T) {
+                       got, err := timeToNanos(tc.value)
+                       if tc.err {
+                               if !errors.Is(err, arrow.ErrInvalid) {
+                                       t.Fatalf("got error %v, want 
arrow.ErrInvalid", err)
+                               }
+                               return
+                       }
+                       if err != nil {
+                               t.Fatalf("timeToNanos returned an unexpected 
error: %v", err)
+                       }
+                       if got != tc.want {
+                               t.Fatalf("timeToNanos(%s) = %d, want %d", 
tc.value, got, tc.want)
+                       }
+               })
+       }
+}
+
+func TestRoundToMultipleInt64OverflowBoundaries(t *testing.T) {
+       tests := []struct {
+               name     string
+               value    int64
+               multiple int64
+               mode     RoundMode
+               strict   bool
+               want     int64
+               err      bool
+       }{
+               {name: "strict ceiling past maximum", value: math.MaxInt64, 
multiple: 2, mode: RoundUp, strict: true, err: true},
+               {name: "strict ceiling exact past maximum", value: 
math.MaxInt64 - 1, multiple: 2, mode: RoundUp, strict: true, err: true},
+               {name: "floor at minimum", value: math.MinInt64, multiple: 2, 
mode: RoundDown, want: math.MinInt64},
+               {name: "odd multiple three", value: 2, multiple: 3, mode: 
HalfToEven, want: 3},
+               {name: "odd multiple five", value: 2, multiple: 5, mode: 
HalfDown, want: 0},
+       }
+
+       for _, tc := range tests {
+               t.Run(tc.name, func(t *testing.T) {
+                       got, err := roundToMultipleInt64(tc.value, tc.multiple, 
tc.mode, tc.strict)
+                       if tc.err {
+                               if !errors.Is(err, arrow.ErrInvalid) {
+                                       t.Fatalf("got error %v, want 
arrow.ErrInvalid", err)
+                               }
+                               return
+                       }
+                       if err != nil {
+                               t.Fatalf("roundToMultipleInt64 returned an 
unexpected error: %v", err)
+                       }
+                       if got != tc.want {
+                               t.Fatalf("roundToMultipleInt64(%d, %d) = %d, 
want %d", tc.value, tc.multiple, got, tc.want)
+                       }
+               })
+       }
+}
+
+func TestRoundToMultipleInt64HalfModes(t *testing.T) {
+       tests := []struct {
+               name  string
+               mode  RoundMode
+               value int64
+               want  int64
+       }{
+               {name: "half down positive", mode: HalfDown, value: 2, want: 0},
+               {name: "half down negative", mode: HalfDown, value: -2, want: 
-4},
+               {name: "half up positive", mode: HalfUp, value: 2, want: 4},
+               {name: "half up negative", mode: HalfUp, value: -2, want: 0},
+               {name: "half to even lower", mode: HalfToEven, value: 2, want: 
0},
+               {name: "half to even upper", mode: HalfToEven, value: 6, want: 
8},
+               {name: "half to even negative lower", mode: HalfToEven, value: 
-2, want: 0},
+               {name: "half to even negative upper", mode: HalfToEven, value: 
-6, want: -8},
+       }
+
+       for _, tc := range tests {
+               t.Run(tc.name, func(t *testing.T) {
+                       got, err := roundToMultipleInt64(tc.value, 4, tc.mode, 
false)
+                       if err != nil {
+                               t.Fatalf("roundToMultipleInt64 returned an 
unexpected error: %v", err)
+                       }
+                       if got != tc.want {
+                               t.Fatalf("roundToMultipleInt64(%d, 4, %s) = %d, 
want %d", tc.value, tc.mode, got, tc.want)
+                       }
+               })
+       }
+}
+
+func TestTemporalRoundingExtremeOrdering(t *testing.T) {
+       values := []int64{
+               math.MinInt64, math.MinInt64 + 1, minCalendarTimestampSecond - 
1,
+               minCalendarTimestampSecond, -1, 0, 1, math.MaxInt64 - 86400, 
math.MaxInt64 - 1, math.MaxInt64,
+       }
+       for _, inputUnit := range []arrow.TimeUnit{arrow.Second, 
arrow.Millisecond, arrow.Microsecond, arrow.Nanosecond} {
+               for _, unit := range []RoundTemporalUnit{RoundTemporalYear, 
RoundTemporalQuarter, RoundTemporalMonth, RoundTemporalWeek, RoundTemporalDay} {
+                       for _, multiple := range []int64{1, 2, 7, 300} {
+                               for _, value := range values {
+                                       state := 
roundTemporalState{RoundTemporalOptions: RoundTemporalOptions{Unit: unit, 
Multiple: multiple}}
+                                       state.unitNanos, state.isSubDay = 
unitInNanos(unit)
+                                       state.roundingInterval = 
state.unitNanos * multiple
+                                       state.mode = RoundDown
+                                       floor, err := roundTimestamp(value, 
inputUnit, time.UTC, state)
+                                       if err == nil && floor > value {
+                                               t.Errorf("floor(%d, %s, %d %v) 
= %d exceeds the input", value, inputUnit, multiple, unit, floor)
+                                       }
+                                       state.mode = RoundUp
+                                       ceil, err := roundTimestamp(value, 
inputUnit, time.UTC, state)
+                                       if err == nil && ceil < value {
+                                               t.Errorf("ceil(%d, %s, %d %v) = 
%d precedes the input", value, inputUnit, multiple, unit, ceil)
+                                       }
+                               }
+                       }
+               }
+       }
+}
diff --git a/arrow/compute/temporal_rounding_origins_test.go 
b/arrow/compute/temporal_rounding_origins_test.go
new file mode 100644
index 00000000..574de347
--- /dev/null
+++ b/arrow/compute/temporal_rounding_origins_test.go
@@ -0,0 +1,707 @@
+// 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"
+       "time"
+
+       "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/require"
+)
+
+type temporalRoundingFunc func(context.Context, compute.RoundTemporalOptions, 
compute.Datum) (compute.Datum, error)
+
+func requireTemporalRoundingTime(t *testing.T, fn temporalRoundingFunc, input, 
expected time.Time,
+       inputUnit arrow.TimeUnit, timezone string, opts 
compute.RoundTemporalOptions) {
+       t.Helper()
+
+       value, err := arrow.TimestampFromTime(input, inputUnit)
+       require.NoError(t, err)
+
+       builder := array.NewTimestampBuilder(memory.DefaultAllocator, 
&arrow.TimestampType{
+               Unit:     inputUnit,
+               TimeZone: timezone,
+       })
+       builder.Append(value)
+       inputArray := builder.NewArray()
+       builder.Release()
+       defer inputArray.Release()
+
+       inputDatum := compute.NewDatum(inputArray)
+       defer inputDatum.Release()
+       result, err := fn(context.Background(), opts, inputDatum)
+       require.NoError(t, err)
+       defer result.Release()
+
+       output := result.(*compute.ArrayDatum).MakeArray().(*array.Timestamp)
+       defer output.Release()
+
+       expectedValue, err := arrow.TimestampFromTime(expected, inputUnit)
+       require.NoError(t, err)
+       require.Equal(t, expectedValue, output.Value(0))
+}
+
+func requireTemporalRoundingError(t *testing.T, fn temporalRoundingFunc, input 
time.Time,
+       inputUnit arrow.TimeUnit, timezone string, opts 
compute.RoundTemporalOptions, expected error) {
+       t.Helper()
+
+       value, err := arrow.TimestampFromTime(input, inputUnit)
+       require.NoError(t, err)
+
+       builder := array.NewTimestampBuilder(memory.DefaultAllocator, 
&arrow.TimestampType{
+               Unit:     inputUnit,
+               TimeZone: timezone,
+       })
+       builder.Append(value)
+       inputArray := builder.NewArray()
+       builder.Release()
+       defer inputArray.Release()
+
+       inputDatum := compute.NewDatum(inputArray)
+       defer inputDatum.Release()
+       result, err := fn(context.Background(), opts, inputDatum)
+       if result != nil {
+               result.Release()
+       }
+       require.ErrorIs(t, err, expected)
+}
+
+func TestTemporalRoundingUsesUnixEpochForCalendarMultiples(t *testing.T) {
+       input := time.Date(2024, time.January, 15, 10, 37, 0, 0, time.UTC)
+       tests := []struct {
+               name     string
+               fn       temporalRoundingFunc
+               unit     compute.RoundTemporalUnit
+               multiple int64
+               expected time.Time
+       }{
+               {
+                       name:     "floor month",
+                       fn:       compute.FloorTemporal,
+                       unit:     compute.RoundTemporalMonth,
+                       multiple: 7,
+                       expected: time.Date(2023, time.September, 1, 0, 0, 0, 
0, time.UTC),
+               },
+               {
+                       name:     "ceil month",
+                       fn:       compute.CeilTemporal,
+                       unit:     compute.RoundTemporalMonth,
+                       multiple: 7,
+                       expected: time.Date(2024, time.April, 1, 0, 0, 0, 0, 
time.UTC),
+               },
+               {
+                       name:     "floor quarter",
+                       fn:       compute.FloorTemporal,
+                       unit:     compute.RoundTemporalQuarter,
+                       multiple: 3,
+                       expected: time.Date(2024, time.January, 1, 0, 0, 0, 0, 
time.UTC),
+               },
+               {
+                       name:     "ceil quarter",
+                       fn:       compute.CeilTemporal,
+                       unit:     compute.RoundTemporalQuarter,
+                       multiple: 3,
+                       expected: time.Date(2024, time.October, 1, 0, 0, 0, 0, 
time.UTC),
+               },
+       }
+
+       for _, test := range tests {
+               t.Run(test.name, func(t *testing.T) {
+                       requireTemporalRoundingTime(t, test.fn, input, 
test.expected, arrow.Microsecond, "", compute.RoundTemporalOptions{
+                               Multiple: test.multiple,
+                               Unit:     test.unit,
+                       })
+               })
+       }
+}
+
+func TestTemporalRoundingCalendarBasedOrigins(t *testing.T) {
+       tests := []struct {
+               name     string
+               fn       temporalRoundingFunc
+               input    time.Time
+               unit     compute.RoundTemporalUnit
+               multiple int64
+               expected time.Time
+       }{
+               {
+                       name:     "floor day from month",
+                       fn:       compute.FloorTemporal,
+                       input:    time.Date(2024, time.March, 10, 12, 0, 0, 0, 
time.UTC),
+                       unit:     compute.RoundTemporalDay,
+                       multiple: 2,
+                       expected: time.Date(2024, time.March, 9, 0, 0, 0, 0, 
time.UTC),
+               },
+               {
+                       name:     "ceil day from month",
+                       fn:       compute.CeilTemporal,
+                       input:    time.Date(2024, time.March, 10, 12, 0, 0, 0, 
time.UTC),
+                       unit:     compute.RoundTemporalDay,
+                       multiple: 2,
+                       expected: time.Date(2024, time.March, 11, 0, 0, 0, 0, 
time.UTC),
+               },
+               {
+                       name:     "floor minute from hour",
+                       fn:       compute.FloorTemporal,
+                       input:    time.Date(2024, time.January, 15, 10, 37, 30, 
123000, time.UTC),
+                       unit:     compute.RoundTemporalMinute,
+                       multiple: 7,
+                       expected: time.Date(2024, time.January, 15, 10, 35, 0, 
0, time.UTC),
+               },
+               {
+                       name:     "ceil minute from hour",
+                       fn:       compute.CeilTemporal,
+                       input:    time.Date(2024, time.January, 15, 10, 37, 30, 
123000, time.UTC),
+                       unit:     compute.RoundTemporalMinute,
+                       multiple: 7,
+                       expected: time.Date(2024, time.January, 15, 10, 42, 0, 
0, time.UTC),
+               },
+               {
+                       name:     "floor month from year",
+                       fn:       compute.FloorTemporal,
+                       input:    time.Date(2024, time.May, 15, 12, 0, 0, 0, 
time.UTC),
+                       unit:     compute.RoundTemporalMonth,
+                       multiple: 5,
+                       expected: time.Date(2024, time.January, 1, 0, 0, 0, 0, 
time.UTC),
+               },
+               {
+                       name:     "ceil month from year",
+                       fn:       compute.CeilTemporal,
+                       input:    time.Date(2024, time.May, 15, 12, 0, 0, 0, 
time.UTC),
+                       unit:     compute.RoundTemporalMonth,
+                       multiple: 5,
+                       expected: time.Date(2024, time.June, 1, 0, 0, 0, 0, 
time.UTC),
+               },
+               {
+                       name:     "floor quarter from year",
+                       fn:       compute.FloorTemporal,
+                       input:    time.Date(2024, time.May, 15, 12, 0, 0, 0, 
time.UTC),
+                       unit:     compute.RoundTemporalQuarter,
+                       multiple: 2,
+                       expected: time.Date(2024, time.January, 1, 0, 0, 0, 0, 
time.UTC),
+               },
+               {
+                       name:     "ceil quarter from year",
+                       fn:       compute.CeilTemporal,
+                       input:    time.Date(2024, time.May, 15, 12, 0, 0, 0, 
time.UTC),
+                       unit:     compute.RoundTemporalQuarter,
+                       multiple: 2,
+                       expected: time.Date(2024, time.July, 1, 0, 0, 0, 0, 
time.UTC),
+               },
+               {
+                       name:     "floor week from year",
+                       fn:       compute.FloorTemporal,
+                       input:    time.Date(2024, time.January, 8, 12, 0, 0, 0, 
time.UTC),
+                       unit:     compute.RoundTemporalWeek,
+                       multiple: 5,
+                       expected: time.Date(2024, time.January, 1, 0, 0, 0, 0, 
time.UTC),
+               },
+               {
+                       name:     "ceil week from year",
+                       fn:       compute.CeilTemporal,
+                       input:    time.Date(2024, time.January, 8, 12, 0, 0, 0, 
time.UTC),
+                       unit:     compute.RoundTemporalWeek,
+                       multiple: 5,
+                       expected: time.Date(2024, time.February, 5, 0, 0, 0, 0, 
time.UTC),
+               },
+       }
+
+       for _, test := range tests {
+               t.Run(test.name, func(t *testing.T) {
+                       requireTemporalRoundingTime(t, test.fn, test.input, 
test.expected, arrow.Microsecond, "", compute.RoundTemporalOptions{
+                               Multiple:            test.multiple,
+                               Unit:                test.unit,
+                               WeekStartsMonday:    test.unit == 
compute.RoundTemporalWeek,
+                               CalendarBasedOrigin: true,
+                       })
+               })
+       }
+}
+
+func TestTemporalRoundingCalendarOriginMultiWeek(t *testing.T) {
+       input := time.Date(1078, time.June, 14, 4, 11, 8, 0, time.UTC)
+       tests := []struct {
+               name             string
+               multiple         int64
+               weekStartsMonday bool
+               floor            time.Time
+               ceil             time.Time
+       }{
+               {
+                       name:             "monday start, two weeks",
+                       multiple:         2,
+                       weekStartsMonday: true,
+                       floor:            time.Date(1078, time.June, 3, 0, 0, 
0, 0, time.UTC),
+                       ceil:             time.Date(1078, time.June, 17, 0, 0, 
0, 0, time.UTC),
+               },
+               {
+                       name:     "sunday start, two weeks",
+                       multiple: 2,
+                       floor:    time.Date(1078, time.June, 2, 0, 0, 0, 0, 
time.UTC),
+                       ceil:     time.Date(1078, time.June, 16, 0, 0, 0, 0, 
time.UTC),
+               },
+               {
+                       name:             "monday start, three weeks",
+                       multiple:         3,
+                       weekStartsMonday: true,
+                       floor:            time.Date(1078, time.May, 27, 0, 0, 
0, 0, time.UTC),
+                       ceil:             time.Date(1078, time.June, 17, 0, 0, 
0, 0, time.UTC),
+               },
+       }
+
+       for _, test := range tests {
+               t.Run(test.name, func(t *testing.T) {
+                       opts := compute.RoundTemporalOptions{
+                               Multiple:            test.multiple,
+                               Unit:                compute.RoundTemporalWeek,
+                               WeekStartsMonday:    test.weekStartsMonday,
+                               CalendarBasedOrigin: true,
+                       }
+                       requireTemporalRoundingTime(t, compute.FloorTemporal, 
input, test.floor, arrow.Second, "", opts)
+                       requireTemporalRoundingTime(t, compute.CeilTemporal, 
input, test.ceil, arrow.Second, "", opts)
+               })
+       }
+}
+
+func TestTemporalRoundingCalendarOriginMultiWeekYearBoundaries(t *testing.T) {
+       tests := []struct {
+               name             string
+               input            time.Time
+               multiple         int64
+               weekStartsMonday bool
+               floor            time.Time
+               ceil             time.Time
+       }{
+               {
+                       name:     "sunday start at end of year",
+                       input:    time.Date(2025, time.December, 31, 4, 58, 46, 
0, time.UTC),
+                       multiple: 2,
+                       floor:    time.Date(2025, time.December, 28, 0, 0, 0, 
0, time.UTC),
+                       ceil:     time.Date(2026, time.January, 11, 0, 0, 0, 0, 
time.UTC),
+               },
+               {
+                       name:             "monday start at end of year",
+                       input:            time.Date(2025, time.December, 31, 4, 
58, 46, 0, time.UTC),
+                       multiple:         2,
+                       weekStartsMonday: true,
+                       floor:            time.Date(2025, time.December, 29, 0, 
0, 0, 0, time.UTC),
+                       ceil:             time.Date(2026, time.January, 12, 0, 
0, 0, 0, time.UTC),
+               },
+               {
+                       name:             "monday start before first week",
+                       input:            time.Date(2023, time.January, 1, 4, 
58, 46, 0, time.UTC),
+                       multiple:         2,
+                       weekStartsMonday: true,
+                       floor:            time.Date(2022, time.December, 19, 0, 
0, 0, 0, time.UTC),
+                       ceil:             time.Date(2023, time.January, 2, 0, 
0, 0, 0, time.UTC),
+               },
+       }
+
+       for _, test := range tests {
+               t.Run(test.name, func(t *testing.T) {
+                       opts := compute.RoundTemporalOptions{
+                               Multiple:            test.multiple,
+                               Unit:                compute.RoundTemporalWeek,
+                               WeekStartsMonday:    test.weekStartsMonday,
+                               CalendarBasedOrigin: true,
+                       }
+                       requireTemporalRoundingTime(t, compute.FloorTemporal, 
test.input, test.floor, arrow.Second, "", opts)
+                       requireTemporalRoundingTime(t, compute.FloorTemporal, 
test.floor, test.floor, arrow.Second, "", opts)
+                       requireTemporalRoundingTime(t, compute.CeilTemporal, 
test.input, test.ceil, arrow.Second, "", opts)
+               })
+       }
+}
+
+func TestTemporalRoundingCalendarOriginUsesWallClockAcrossDST(t *testing.T) {
+       location, err := time.LoadLocation("America/New_York")
+       require.NoError(t, err)
+
+       input := time.Date(2024, time.March, 10, 12, 0, 0, 0, time.UTC)
+       opts := compute.RoundTemporalOptions{
+               Multiple:            5,
+               Unit:                compute.RoundTemporalHour,
+               CalendarBasedOrigin: true,
+       }
+
+       requireTemporalRoundingTime(t, compute.FloorTemporal, input,
+               time.Date(2024, time.March, 10, 5, 0, 0, 0, location), 
arrow.Microsecond,
+               location.String(), opts)
+       requireTemporalRoundingTime(t, compute.CeilTemporal, input,
+               time.Date(2024, time.March, 10, 10, 0, 0, 0, location), 
arrow.Microsecond,
+               location.String(), opts)
+}
+
+func TestTemporalRoundingZonedFixedUnitsUseLocalTime(t *testing.T) {
+       location, err := time.LoadLocation("Asia/Kathmandu")
+       require.NoError(t, err)
+       input := time.Date(2024, time.July, 15, 10, 37, 0, 0, location)
+
+       requireTemporalRoundingTime(t, compute.FloorTemporal, input,
+               time.Date(2024, time.July, 15, 10, 0, 0, 0, location), 
arrow.Second,
+               location.String(), compute.RoundTemporalOptions{Multiple: 1, 
Unit: compute.RoundTemporalHour})
+       requireTemporalRoundingTime(t, compute.CeilTemporal, input,
+               time.Date(2024, time.July, 15, 11, 0, 0, 0, location), 
arrow.Second,
+               location.String(), compute.RoundTemporalOptions{Multiple: 1, 
Unit: compute.RoundTemporalHour})
+}
+
+func TestTemporalRoundingZonedFixedUnitsSupportWideDates(t *testing.T) {
+       location, err := time.LoadLocation("Asia/Kathmandu")
+       require.NoError(t, err)
+       input := time.Date(1000, time.July, 15, 10, 37, 0, 0, location)
+
+       for _, unit := range []arrow.TimeUnit{arrow.Second, arrow.Millisecond, 
arrow.Microsecond} {
+               t.Run(unit.String(), func(t *testing.T) {
+                       opts := compute.RoundTemporalOptions{Multiple: 1, Unit: 
compute.RoundTemporalHour}
+                       requireTemporalRoundingTime(t, compute.FloorTemporal, 
input,
+                               time.Date(1000, time.July, 15, 10, 0, 0, 0, 
location), unit,
+                               location.String(), opts)
+                       requireTemporalRoundingTime(t, compute.CeilTemporal, 
input,
+                               time.Date(1000, time.July, 15, 11, 0, 0, 0, 
location), unit,
+                               location.String(), opts)
+                       requireTemporalRoundingTime(t, compute.RoundTemporal, 
input,
+                               time.Date(1000, time.July, 15, 11, 0, 0, 0, 
location), unit,
+                               location.String(), opts)
+               })
+       }
+}
+
+func TestTemporalRoundingCalendarOriginAfterMidnightGap(t *testing.T) {
+       // Sao Paulo skipped midnight to 01:00. Monrovia skipped midnight to
+       // 00:44:30, so even an hour or minute boundary may not exist locally.
+       for _, tc := range []struct {
+               zone, input  string
+               unit         compute.RoundTemporalUnit
+               multiple     int64
+               interval     time.Duration
+               minInputUnit arrow.TimeUnit
+       }{
+               {"America/Sao_Paulo", "2018-11-04T02:00:00-02:00", 
compute.RoundTemporalHour, 2, 2 * time.Hour, arrow.Second},
+               {"America/Sao_Paulo", "2018-11-04T01:30:00-02:00", 
compute.RoundTemporalMinute, 30, 30 * time.Minute, arrow.Second},
+               {"Africa/Monrovia", "1972-01-07T01:00:00Z", 
compute.RoundTemporalHour, 1, time.Hour, arrow.Second},
+               {"Africa/Monrovia", "1972-01-07T00:45:00Z", 
compute.RoundTemporalMinute, 15, 15 * time.Minute, arrow.Second},
+               {"Africa/Monrovia", "1972-01-07T00:44:30Z", 
compute.RoundTemporalSecond, 10, 10 * time.Second, arrow.Second},
+               {"Africa/Monrovia", "1972-01-07T00:44:30.125Z", 
compute.RoundTemporalMillisecond, 5, 5 * time.Millisecond, arrow.Millisecond},
+               {"Africa/Monrovia", "1972-01-07T00:44:30.123455Z", 
compute.RoundTemporalMicrosecond, 5, 5 * time.Microsecond, arrow.Microsecond},
+               {"Africa/Monrovia", "1972-01-07T00:44:30.123456785Z", 
compute.RoundTemporalNanosecond, 5, 5 * time.Nanosecond, arrow.Nanosecond},
+       } {
+               t.Run(tc.zone+"/"+tc.input, func(t *testing.T) {
+                       input, err := time.Parse(time.RFC3339Nano, tc.input)
+                       require.NoError(t, err)
+                       for _, unit := range []arrow.TimeUnit{arrow.Second, 
arrow.Millisecond, arrow.Microsecond, arrow.Nanosecond} {
+                               if unit < tc.minInputUnit {
+                                       continue
+                               }
+                               t.Run(unit.String(), func(t *testing.T) {
+                                       opts := compute.RoundTemporalOptions{
+                                               Multiple:            
tc.multiple,
+                                               Unit:                tc.unit,
+                                               CalendarBasedOrigin: true,
+                                       }
+                                       for _, fn := range 
[]temporalRoundingFunc{compute.FloorTemporal, compute.CeilTemporal, 
compute.RoundTemporal} {
+                                               requireTemporalRoundingTime(t, 
fn, input, input, unit, tc.zone, opts)
+                                       }
+
+                                       unaligned := input.Add(tc.interval * 4 
/ 5)
+                                       requireTemporalRoundingTime(t, 
compute.FloorTemporal, unaligned, input, unit, tc.zone, opts)
+                                       for _, fn := range 
[]temporalRoundingFunc{compute.CeilTemporal, compute.RoundTemporal} {
+                                               requireTemporalRoundingTime(t, 
fn, unaligned, input.Add(tc.interval), unit, tc.zone, opts)
+                                       }
+                                       opts.CeilIsStrictlyGreater = true
+                                       requireTemporalRoundingTime(t, 
compute.CeilTemporal, input, input.Add(tc.interval), unit, tc.zone, opts)
+                               })
+                       }
+               })
+       }
+}
+
+func TestTemporalRoundingCalendarOriginCrossesMidnightGap(t *testing.T) {
+       input, err := time.Parse(time.RFC3339, "2018-11-03T23:30:00-03:00")
+       require.NoError(t, err)
+       floor, err := time.Parse(time.RFC3339, "2018-11-03T20:00:00-03:00")
+       require.NoError(t, err)
+       ceil, err := time.Parse(time.RFC3339, "2018-11-04T01:00:00-02:00")
+       require.NoError(t, err)
+       opts := compute.RoundTemporalOptions{
+               Multiple:            5,
+               Unit:                compute.RoundTemporalHour,
+               CalendarBasedOrigin: true,
+       }
+       for _, unit := range []arrow.TimeUnit{arrow.Second, arrow.Millisecond, 
arrow.Microsecond, arrow.Nanosecond} {
+               t.Run(unit.String(), func(t *testing.T) {
+                       requireTemporalRoundingTime(t, compute.FloorTemporal, 
input, floor, unit, "America/Sao_Paulo", opts)
+                       for _, fn := range 
[]temporalRoundingFunc{compute.CeilTemporal, compute.RoundTemporal} {
+                               requireTemporalRoundingTime(t, fn, input, ceil, 
unit, "America/Sao_Paulo", opts)
+                       }
+               })
+       }
+}
+
+func TestTemporalRoundingCalendarOriginRejectsDSTGap(t *testing.T) {
+       for _, tc := range []struct {
+               zone, input string
+               fn          temporalRoundingFunc
+               unit        compute.RoundTemporalUnit
+               multiple    int64
+       }{
+               {"America/New_York", "2024-03-10T01:30:00-05:00", 
compute.CeilTemporal, compute.RoundTemporalHour, 1},
+               {"America/Sao_Paulo", "2018-11-04T01:30:00-02:00", 
compute.FloorTemporal, compute.RoundTemporalHour, 2},
+               {"America/Sao_Paulo", "2018-11-03T23:30:00-03:00", 
compute.CeilTemporal, compute.RoundTemporalHour, 1},
+               {"Africa/Monrovia", "1972-01-07T00:45:00Z", 
compute.FloorTemporal, compute.RoundTemporalMinute, 30},
+               {"Africa/Monrovia", "1972-01-07T00:44:35Z", 
compute.FloorTemporal, compute.RoundTemporalSecond, 60},
+               {"Pacific/Apia", "2011-12-29T23:30:00-10:00", 
compute.CeilTemporal, compute.RoundTemporalHour, 5},
+       } {
+               t.Run(tc.zone+"/"+tc.input, func(t *testing.T) {
+                       input, err := time.Parse(time.RFC3339, tc.input)
+                       require.NoError(t, err)
+                       for _, unit := range []arrow.TimeUnit{arrow.Second, 
arrow.Millisecond, arrow.Microsecond, arrow.Nanosecond} {
+                               t.Run(unit.String(), func(t *testing.T) {
+                                       value, err := 
arrow.TimestampFromTime(input, unit)
+                                       require.NoError(t, err)
+
+                                       builder := 
array.NewTimestampBuilder(memory.DefaultAllocator, &arrow.TimestampType{
+                                               Unit:     unit,
+                                               TimeZone: tc.zone,
+                                       })
+                                       builder.Append(value)
+                                       inputArray := builder.NewArray()
+                                       builder.Release()
+                                       defer inputArray.Release()
+                                       inputDatum := 
compute.NewDatum(inputArray)
+                                       defer inputDatum.Release()
+
+                                       result, err := 
tc.fn(context.Background(), compute.RoundTemporalOptions{
+                                               Multiple:            
tc.multiple,
+                                               Unit:                tc.unit,
+                                               CalendarBasedOrigin: true,
+                                       }, inputDatum)
+                                       if err == nil && result != nil {
+                                               defer result.Release()
+                                       }
+                                       require.ErrorIs(t, err, 
arrow.ErrInvalid)
+                               })
+                       }
+               })
+       }
+}
+
+func TestTemporalRoundingRejectsMissingCalendarBoundary(t *testing.T) {
+       for _, tc := range []struct {
+               name, zone, input string
+               multiple          int64
+               unit              compute.RoundTemporalUnit
+               calendarOrigin    bool
+       }{
+               {
+                       name:           "month start",
+                       zone:           "America/Argentina/Buenos_Aires",
+                       input:          "1930-12-02T12:00:00-03:00",
+                       multiple:       1,
+                       unit:           compute.RoundTemporalMonth,
+                       calendarOrigin: true,
+               },
+               {
+                       name:     "day start",
+                       zone:     "America/Sao_Paulo",
+                       input:    "2018-11-04T01:30:00-02:00",
+                       multiple: 1,
+                       unit:     compute.RoundTemporalDay,
+               },
+               {
+                       name:           "calendar-origin week start",
+                       zone:           "America/Sao_Paulo",
+                       input:          "2018-11-05T12:00:00-02:00",
+                       multiple:       2,
+                       unit:           compute.RoundTemporalWeek,
+                       calendarOrigin: true,
+               },
+       } {
+               t.Run(tc.name, func(t *testing.T) {
+                       input, err := time.Parse(time.RFC3339, tc.input)
+                       require.NoError(t, err)
+                       value, err := arrow.TimestampFromTime(input, 
arrow.Second)
+                       require.NoError(t, err)
+
+                       builder := 
array.NewTimestampBuilder(memory.DefaultAllocator, &arrow.TimestampType{
+                               Unit:     arrow.Second,
+                               TimeZone: tc.zone,
+                       })
+                       builder.Append(value)
+                       inputArray := builder.NewArray()
+                       builder.Release()
+                       defer inputArray.Release()
+
+                       inputDatum := compute.NewDatum(inputArray)
+                       defer inputDatum.Release()
+                       result, err := 
compute.FloorTemporal(context.Background(), compute.RoundTemporalOptions{
+                               Multiple:            tc.multiple,
+                               Unit:                tc.unit,
+                               CalendarBasedOrigin: tc.calendarOrigin,
+                       }, inputDatum)
+                       if result != nil {
+                               defer result.Release()
+                       }
+                       require.ErrorIs(t, err, arrow.ErrInvalid)
+               })
+       }
+}
+
+func TestTemporalRoundingRejectsMissingNextCalendarDay(t *testing.T) {
+       input, err := time.Parse(time.RFC3339, "1995-10-14T23:37:29-03:00")
+       require.NoError(t, err)
+       value, err := arrow.TimestampFromTime(input, arrow.Second)
+       require.NoError(t, err)
+
+       builder := array.NewTimestampBuilder(memory.DefaultAllocator, 
&arrow.TimestampType{
+               Unit:     arrow.Second,
+               TimeZone: "America/Sao_Paulo",
+       })
+       builder.Append(value)
+       inputArray := builder.NewArray()
+       builder.Release()
+       defer inputArray.Release()
+
+       result, err := compute.CeilTemporal(context.Background(), 
compute.RoundTemporalOptions{
+               Multiple: 1,
+               Unit:     compute.RoundTemporalDay,
+       }, compute.NewDatum(inputArray))
+       if result != nil {
+               defer result.Release()
+       }
+       require.ErrorIs(t, err, arrow.ErrInvalid)
+}
+
+func TestTemporalRoundingWeekUsesFixedWallClockPeriods(t *testing.T) {
+       input, err := time.Parse(time.RFC3339, "2015-10-23T23:37:29-02:00")
+       require.NoError(t, err)
+       location, err := time.LoadLocation("America/Sao_Paulo")
+       require.NoError(t, err)
+       for _, tc := range []struct {
+               name     string
+               fn       temporalRoundingFunc
+               expected time.Time
+       }{
+               {
+                       name:     "floor",
+                       fn:       compute.FloorTemporal,
+                       expected: time.Date(2015, time.October, 17, 23, 0, 0, 
0, location),
+               },
+               {
+                       name:     "ceil",
+                       fn:       compute.CeilTemporal,
+                       expected: time.Date(2015, time.October, 24, 23, 0, 0, 
0, location),
+               },
+       } {
+               t.Run(tc.name, func(t *testing.T) {
+                       requireTemporalRoundingTime(t, tc.fn, input, 
tc.expected, arrow.Second, "America/Sao_Paulo", compute.RoundTemporalOptions{
+                               Multiple:         1,
+                               Unit:             compute.RoundTemporalWeek,
+                               WeekStartsMonday: false,
+                       })
+               })
+       }
+}
+
+func TestTemporalRoundingWeekPreservesWallBoundaryAcrossDST(t *testing.T) {
+       location, err := time.LoadLocation("America/New_York")
+       require.NoError(t, err)
+       input := time.Date(2024, time.March, 12, 12, 0, 0, 0, time.UTC)
+
+       for _, tc := range []struct {
+               name             string
+               weekStartsMonday bool
+               floor            time.Time
+               ceil             time.Time
+       }{
+               {
+                       name:  "sunday start",
+                       floor: time.Date(2024, time.March, 10, 0, 0, 0, 0, 
location),
+                       ceil:  time.Date(2024, time.March, 17, 0, 0, 0, 0, 
location),
+               },
+               {
+                       name:             "monday start",
+                       weekStartsMonday: true,
+                       floor:            time.Date(2024, time.March, 11, 0, 0, 
0, 0, location),
+                       ceil:             time.Date(2024, time.March, 18, 0, 0, 
0, 0, location),
+               },
+       } {
+               t.Run(tc.name, func(t *testing.T) {
+                       opts := compute.RoundTemporalOptions{
+                               Multiple:         1,
+                               Unit:             compute.RoundTemporalWeek,
+                               WeekStartsMonday: tc.weekStartsMonday,
+                       }
+                       requireTemporalRoundingTime(t, compute.FloorTemporal, 
input, tc.floor, arrow.Second, location.String(), opts)
+                       requireTemporalRoundingTime(t, compute.CeilTemporal, 
input, tc.ceil, arrow.Second, location.String(), opts)
+                       requireTemporalRoundingTime(t, compute.FloorTemporal, 
tc.floor, tc.floor, arrow.Second, location.String(), opts)
+               })
+       }
+}
+
+func TestTemporalRoundingRejectsAmbiguousCalendarBoundary(t *testing.T) {
+       for _, tc := range []struct {
+               zone, input string
+       }{
+               {"America/New_York", "2024-11-03T01:30:00-04:00"},
+               {"America/New_York", "2024-11-03T01:30:00-05:00"},
+               {"Europe/Berlin", "2024-10-27T02:30:00+02:00"},
+               {"Europe/Berlin", "2024-10-27T02:30:00+01:00"},
+               {"Australia/Lord_Howe", "2024-04-07T01:45:00+11:00"},
+               {"Australia/Lord_Howe", "2024-04-07T01:45:00+10:30"},
+       } {
+               t.Run(tc.zone+"/"+tc.input, func(t *testing.T) {
+                       input, err := time.Parse(time.RFC3339, tc.input)
+                       require.NoError(t, err)
+                       for _, unit := range []arrow.TimeUnit{arrow.Second, 
arrow.Millisecond, arrow.Microsecond, arrow.Nanosecond} {
+                               t.Run(unit.String(), func(t *testing.T) {
+                                       opts := compute.RoundTemporalOptions{
+                                               Multiple:            1,
+                                               Unit:                
compute.RoundTemporalMinute,
+                                               CalendarBasedOrigin: true,
+                                       }
+                                       for _, fn := range 
[]temporalRoundingFunc{compute.FloorTemporal, compute.CeilTemporal, 
compute.RoundTemporal} {
+                                               requireTemporalRoundingError(t, 
fn, input, unit, tc.zone, opts, arrow.ErrInvalid)
+                                       }
+                               })
+                       }
+               })
+       }
+}
+
+func TestTemporalRoundingTimezoneDayMultipleUsesEpochOrigin(t *testing.T) {
+       location, err := time.LoadLocation("America/New_York")
+       require.NoError(t, err)
+
+       input := time.Date(2024, time.January, 15, 10, 37, 0, 0, time.UTC)
+       opts := compute.RoundTemporalOptions{
+               Multiple: 2,
+               Unit:     compute.RoundTemporalDay,
+       }
+
+       requireTemporalRoundingTime(t, compute.FloorTemporal, input,
+               time.Date(2024, time.January, 14, 0, 0, 0, 0, location), 
arrow.Microsecond,
+               location.String(), opts)
+       requireTemporalRoundingTime(t, compute.CeilTemporal, input,
+               time.Date(2024, time.January, 16, 0, 0, 0, 0, location), 
arrow.Microsecond,
+               location.String(), opts)
+}
diff --git a/arrow/compute/temporal_rounding_overflow_test.go 
b/arrow/compute/temporal_rounding_overflow_test.go
new file mode 100644
index 00000000..cac85103
--- /dev/null
+++ b/arrow/compute/temporal_rounding_overflow_test.go
@@ -0,0 +1,741 @@
+// 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"
+       "math"
+       "testing"
+       "time"
+
+       "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/require"
+)
+
+func TestTemporalRoundingOverflow(t *testing.T) {
+       tests := []struct {
+               name  string
+               unit  arrow.TimeUnit
+               value arrow.Timestamp
+               opt   compute.RoundTemporalOptions
+       }{
+               {
+                       name:  "input conversion",
+                       unit:  arrow.Second,
+                       value: arrow.Timestamp(math.MaxInt64),
+                       opt:   compute.RoundTemporalOptions{Multiple: 3, Unit: 
compute.RoundTemporalNanosecond},
+               },
+               {
+                       name:  "millisecond input conversion",
+                       unit:  arrow.Millisecond,
+                       value: arrow.Timestamp(math.MaxInt64),
+                       opt:   compute.RoundTemporalOptions{Multiple: 3, Unit: 
compute.RoundTemporalNanosecond},
+               },
+               {
+                       name:  "microsecond input conversion",
+                       unit:  arrow.Microsecond,
+                       value: arrow.Timestamp(math.MaxInt64),
+                       opt:   compute.RoundTemporalOptions{Multiple: 3, Unit: 
compute.RoundTemporalNanosecond},
+               },
+               {
+                       name:  "interval conversion",
+                       unit:  arrow.Second,
+                       value: 1,
+                       opt:   compute.RoundTemporalOptions{Multiple: 
math.MaxInt64, Unit: compute.RoundTemporalSecond},
+               },
+               {
+                       name:  "rounded result",
+                       unit:  arrow.Nanosecond,
+                       value: arrow.Timestamp(math.MaxInt64),
+                       opt:   compute.RoundTemporalOptions{Multiple: 2, Unit: 
compute.RoundTemporalNanosecond},
+               },
+       }
+
+       for _, tc := range tests {
+               t.Run(tc.name, func(t *testing.T) {
+                       builder := 
array.NewTimestampBuilder(memory.DefaultAllocator, &arrow.TimestampType{Unit: 
tc.unit})
+                       builder.Append(tc.value)
+                       input := builder.NewArray()
+                       builder.Release()
+                       defer input.Release()
+
+                       _, err := compute.RoundTemporal(context.Background(), 
tc.opt, compute.NewDatum(input))
+                       require.ErrorIs(t, err, arrow.ErrInvalid)
+               })
+       }
+}
+
+func TestTemporalRoundingCalendarMultipleOverflow(t *testing.T) {
+       value, err := arrow.TimestampFromTime(time.Date(2024, time.January, 15, 
10, 37, 0, 0, time.UTC), arrow.Nanosecond)
+       require.NoError(t, err)
+
+       builder := array.NewTimestampBuilder(memory.DefaultAllocator, 
&arrow.TimestampType{Unit: arrow.Nanosecond})
+       builder.Append(value)
+       input := builder.NewArray()
+       builder.Release()
+       defer input.Release()
+
+       tests := []struct {
+               name string
+               fn   func(context.Context, compute.RoundTemporalOptions, 
compute.Datum) (compute.Datum, error)
+               unit compute.RoundTemporalUnit
+       }{
+               {name: "year ceiling", fn: compute.CeilTemporal, unit: 
compute.RoundTemporalYear},
+               {name: "year half-rounding", fn: compute.RoundTemporal, unit: 
compute.RoundTemporalYear},
+               {name: "quarter ceiling", fn: compute.CeilTemporal, unit: 
compute.RoundTemporalQuarter},
+               {name: "quarter half-rounding", fn: compute.RoundTemporal, 
unit: compute.RoundTemporalQuarter},
+               {name: "month ceiling", fn: compute.CeilTemporal, unit: 
compute.RoundTemporalMonth},
+               {name: "month half-rounding", fn: compute.RoundTemporal, unit: 
compute.RoundTemporalMonth},
+               {name: "week ceiling", fn: compute.CeilTemporal, unit: 
compute.RoundTemporalWeek},
+               {name: "week half-rounding", fn: compute.RoundTemporal, unit: 
compute.RoundTemporalWeek},
+       }
+
+       for _, tc := range tests {
+               t.Run(tc.name, func(t *testing.T) {
+                       _, err := tc.fn(context.Background(), 
compute.RoundTemporalOptions{
+                               Multiple: math.MaxInt64,
+                               Unit:     tc.unit,
+                       }, compute.NewDatum(input))
+                       require.ErrorIs(t, err, arrow.ErrInvalid)
+               })
+       }
+}
+
+func TestTemporalRoundingDate32Overflow(t *testing.T) {
+       tests := []struct {
+               name  string
+               value arrow.Date32
+               fn    func(context.Context, compute.RoundTemporalOptions, 
compute.Datum) (compute.Datum, error)
+               opts  compute.RoundTemporalOptions
+       }{
+               {
+                       name:  "ceiling past maximum",
+                       value: math.MaxInt32,
+                       fn:    compute.CeilTemporal,
+                       opts: compute.RoundTemporalOptions{
+                               Multiple:              1,
+                               Unit:                  compute.RoundTemporalDay,
+                               CeilIsStrictlyGreater: true,
+                       },
+               },
+               {
+                       name:  "floor below minimum",
+                       value: math.MinInt32,
+                       fn:    compute.FloorTemporal,
+                       opts: compute.RoundTemporalOptions{
+                               Multiple: 3,
+                               Unit:     compute.RoundTemporalDay,
+                       },
+               },
+       }
+
+       for _, tc := range tests {
+               t.Run(tc.name, func(t *testing.T) {
+                       builder := 
array.NewDate32Builder(memory.DefaultAllocator)
+                       builder.Append(tc.value)
+                       input := builder.NewArray()
+                       builder.Release()
+                       defer input.Release()
+
+                       _, err := tc.fn(context.Background(), tc.opts, 
compute.NewDatum(input))
+                       require.ErrorIs(t, err, arrow.ErrInvalid)
+               })
+       }
+}
+
+func TestTemporalRoundingTimezonePaths(t *testing.T) {
+       loc, err := time.LoadLocation("America/New_York")
+       require.NoError(t, err)
+
+       tests := []struct {
+               name     string
+               value    time.Time
+               unit     compute.RoundTemporalUnit
+               multiple int64
+               calendar bool
+               want     time.Time
+       }{
+               {
+                       name:     "timezone input",
+                       value:    time.Date(2024, time.January, 15, 10, 37, 0, 
0, loc),
+                       unit:     compute.RoundTemporalHour,
+                       multiple: 2,
+                       want:     time.Date(2024, time.January, 15, 10, 0, 0, 
0, loc),
+               },
+               {
+                       name:     "spring DST day",
+                       value:    time.Date(2024, time.March, 10, 12, 0, 0, 0, 
loc),
+                       unit:     compute.RoundTemporalDay,
+                       multiple: 1,
+                       want:     time.Date(2024, time.March, 10, 0, 0, 0, 0, 
loc),
+               },
+               {
+                       name:     "timezone calendar origin",
+                       value:    time.Date(2024, time.January, 15, 10, 37, 0, 
0, time.UTC),
+                       unit:     compute.RoundTemporalHour,
+                       multiple: 2,
+                       calendar: true,
+                       want:     time.Date(2024, time.January, 15, 9, 0, 0, 0, 
time.UTC),
+               },
+       }
+
+       for _, tc := range tests {
+               t.Run(tc.name, func(t *testing.T) {
+                       value, err := arrow.TimestampFromTime(tc.value, 
arrow.Nanosecond)
+                       require.NoError(t, err)
+                       builder := 
array.NewTimestampBuilder(memory.DefaultAllocator, &arrow.TimestampType{
+                               Unit:     arrow.Nanosecond,
+                               TimeZone: loc.String(),
+                       })
+                       builder.Append(value)
+                       input := builder.NewArray()
+                       builder.Release()
+                       defer input.Release()
+
+                       result, err := 
compute.FloorTemporal(context.Background(), compute.RoundTemporalOptions{
+                               Multiple:            tc.multiple,
+                               Unit:                tc.unit,
+                               CalendarBasedOrigin: tc.calendar,
+                       }, compute.NewDatum(input))
+                       require.NoError(t, err)
+                       defer result.Release()
+
+                       output := 
result.(*compute.ArrayDatum).MakeArray().(*array.Timestamp)
+                       defer output.Release()
+                       want, err := arrow.TimestampFromTime(tc.want, 
arrow.Nanosecond)
+                       require.NoError(t, err)
+                       require.Equal(t, want, output.Value(0))
+               })
+       }
+}
+
+func TestTemporalRoundingCalendarBoundaries(t *testing.T) {
+       tests := []struct {
+               name  string
+               value time.Time
+               want  time.Time
+               opts  compute.RoundTemporalOptions
+       }{
+               {
+                       name:  "year result before unused end boundary",
+                       value: time.Date(2262, time.January, 5, 0, 0, 0, 0, 
time.UTC),
+                       want:  time.Date(2262, time.January, 1, 0, 0, 0, 0, 
time.UTC),
+                       opts:  compute.RoundTemporalOptions{Multiple: 1, Unit: 
compute.RoundTemporalYear},
+               },
+               {
+                       name:  "quarter result before unused end boundary",
+                       value: time.Date(2262, time.January, 5, 0, 0, 0, 0, 
time.UTC),
+                       want:  time.Date(2262, time.January, 1, 0, 0, 0, 0, 
time.UTC),
+                       opts:  compute.RoundTemporalOptions{Multiple: 4, Unit: 
compute.RoundTemporalQuarter},
+               },
+               {
+                       name:  "month result before unused end boundary",
+                       value: time.Date(2262, time.January, 5, 0, 0, 0, 0, 
time.UTC),
+                       want:  time.Date(2262, time.January, 1, 0, 0, 0, 0, 
time.UTC),
+                       opts:  compute.RoundTemporalOptions{Multiple: 12, Unit: 
compute.RoundTemporalMonth},
+               },
+               {
+                       name:  "wide year multiple",
+                       value: time.Date(2201, time.January, 1, 0, 0, 0, 0, 
time.UTC),
+                       want:  time.Date(2200, time.January, 1, 0, 0, 0, 0, 
time.UTC),
+                       opts:  compute.RoundTemporalOptions{Multiple: 100, 
Unit: compute.RoundTemporalYear},
+               },
+       }
+
+       for _, tc := range tests {
+               t.Run(tc.name, func(t *testing.T) {
+                       value, err := arrow.TimestampFromTime(tc.value, 
arrow.Nanosecond)
+                       require.NoError(t, err)
+
+                       builder := 
array.NewTimestampBuilder(memory.DefaultAllocator, &arrow.TimestampType{Unit: 
arrow.Nanosecond})
+                       builder.Append(value)
+                       input := builder.NewArray()
+                       builder.Release()
+                       defer input.Release()
+
+                       result, err := 
compute.RoundTemporal(context.Background(), tc.opts, compute.NewDatum(input))
+                       require.NoError(t, err)
+                       defer result.Release()
+
+                       output := 
result.(*compute.ArrayDatum).MakeArray().(*array.Timestamp)
+                       defer output.Release()
+                       want, err := arrow.TimestampFromTime(tc.want, 
arrow.Nanosecond)
+                       require.NoError(t, err)
+                       require.Equal(t, want, output.Value(0))
+               })
+       }
+}
+
+func TestTemporalRoundingCalendarPreservesInputUnit(t *testing.T) {
+       value := time.Date(1500, time.June, 15, 12, 34, 56, 0, time.UTC)
+       want := time.Date(1500, time.January, 1, 0, 0, 0, 0, time.UTC)
+       opts := compute.RoundTemporalOptions{Multiple: 1, Unit: 
compute.RoundTemporalYear}
+
+       t.Run("date32", func(t *testing.T) {
+               builder := array.NewDate32Builder(memory.DefaultAllocator)
+               builder.Append(arrow.Date32FromTime(value))
+               input := builder.NewArray()
+               builder.Release()
+               defer input.Release()
+
+               result, err := compute.FloorTemporal(context.Background(), 
opts, compute.NewDatum(input))
+               require.NoError(t, err)
+               defer result.Release()
+
+               output := 
result.(*compute.ArrayDatum).MakeArray().(*array.Date32)
+               defer output.Release()
+               require.Equal(t, arrow.Date32FromTime(want), output.Value(0))
+       })
+
+       t.Run("date64", func(t *testing.T) {
+               builder := array.NewDate64Builder(memory.DefaultAllocator)
+               builder.Append(arrow.Date64FromTime(value))
+               input := builder.NewArray()
+               builder.Release()
+               defer input.Release()
+
+               result, err := compute.FloorTemporal(context.Background(), 
opts, compute.NewDatum(input))
+               require.NoError(t, err)
+               defer result.Release()
+
+               output := 
result.(*compute.ArrayDatum).MakeArray().(*array.Date64)
+               defer output.Release()
+               require.Equal(t, arrow.Date64FromTime(want), output.Value(0))
+       })
+
+       for _, unit := range []arrow.TimeUnit{arrow.Second, arrow.Millisecond, 
arrow.Microsecond} {
+               t.Run(unit.String(), func(t *testing.T) {
+                       value, err := arrow.TimestampFromTime(value, unit)
+                       require.NoError(t, err)
+
+                       builder := 
array.NewTimestampBuilder(memory.DefaultAllocator, &arrow.TimestampType{Unit: 
unit})
+                       builder.Append(value)
+                       input := builder.NewArray()
+                       builder.Release()
+                       defer input.Release()
+
+                       result, err := 
compute.FloorTemporal(context.Background(), opts, compute.NewDatum(input))
+                       require.NoError(t, err)
+                       defer result.Release()
+
+                       output := 
result.(*compute.ArrayDatum).MakeArray().(*array.Timestamp)
+                       defer output.Release()
+                       want, err := arrow.TimestampFromTime(want, unit)
+                       require.NoError(t, err)
+                       require.Equal(t, want, output.Value(0))
+               })
+       }
+}
+
+func TestTemporalRoundingFinerUnitPreservesInputPrecision(t *testing.T) {
+       value := time.Date(1500, time.June, 15, 12, 34, 56, 0, time.UTC)
+
+       for _, unit := range []arrow.TimeUnit{arrow.Second, arrow.Millisecond, 
arrow.Microsecond} {
+               t.Run(unit.String(), func(t *testing.T) {
+                       inputValue, err := arrow.TimestampFromTime(value, unit)
+                       require.NoError(t, err)
+
+                       builder := 
array.NewTimestampBuilder(memory.DefaultAllocator, &arrow.TimestampType{Unit: 
unit})
+                       builder.Append(inputValue)
+                       input := builder.NewArray()
+                       builder.Release()
+                       defer input.Release()
+
+                       result, err := 
compute.FloorTemporal(context.Background(), compute.RoundTemporalOptions{
+                               Multiple: 1,
+                               Unit:     compute.RoundTemporalNanosecond,
+                       }, compute.NewDatum(input))
+                       require.NoError(t, err)
+                       defer result.Release()
+
+                       output := 
result.(*compute.ArrayDatum).MakeArray().(*array.Timestamp)
+                       defer output.Release()
+                       require.Equal(t, inputValue, output.Value(0))
+               })
+       }
+
+       t.Run("date32", func(t *testing.T) {
+               builder := array.NewDate32Builder(memory.DefaultAllocator)
+               builder.Append(arrow.Date32FromTime(value))
+               input := builder.NewArray()
+               builder.Release()
+               defer input.Release()
+
+               result, err := compute.FloorTemporal(context.Background(), 
compute.RoundTemporalOptions{
+                       Multiple: 1,
+                       Unit:     compute.RoundTemporalNanosecond,
+               }, compute.NewDatum(input))
+               require.NoError(t, err)
+               defer result.Release()
+
+               output := 
result.(*compute.ArrayDatum).MakeArray().(*array.Date32)
+               defer output.Release()
+               require.Equal(t, arrow.Date32FromTime(value), output.Value(0))
+       })
+
+       t.Run("date64", func(t *testing.T) {
+               builder := array.NewDate64Builder(memory.DefaultAllocator)
+               builder.Append(arrow.Date64FromTime(value))
+               input := builder.NewArray()
+               builder.Release()
+               defer input.Release()
+
+               result, err := compute.FloorTemporal(context.Background(), 
compute.RoundTemporalOptions{
+                       Multiple: 1,
+                       Unit:     compute.RoundTemporalNanosecond,
+               }, compute.NewDatum(input))
+               require.NoError(t, err)
+               defer result.Release()
+
+               output := 
result.(*compute.ArrayDatum).MakeArray().(*array.Date64)
+               defer output.Release()
+               require.Equal(t, arrow.Date64FromTime(value), output.Value(0))
+       })
+}
+
+func TestTemporalRoundingCalendarOriginWideUnits(t *testing.T) {
+       valueTime := time.Date(1500, time.June, 15, 12, 34, 56, 0, time.UTC)
+       wantTime := time.Date(1500, time.June, 15, 12, 0, 0, 0, time.UTC)
+
+       for _, unit := range []arrow.TimeUnit{arrow.Second, arrow.Millisecond, 
arrow.Microsecond} {
+               t.Run(unit.String(), func(t *testing.T) {
+                       value, err := arrow.TimestampFromTime(valueTime, unit)
+                       require.NoError(t, err)
+
+                       builder := 
array.NewTimestampBuilder(memory.DefaultAllocator, &arrow.TimestampType{Unit: 
unit})
+                       builder.Append(value)
+                       input := builder.NewArray()
+                       builder.Release()
+                       defer input.Release()
+
+                       result, err := 
compute.FloorTemporal(context.Background(), compute.RoundTemporalOptions{
+                               Multiple:            2,
+                               Unit:                compute.RoundTemporalHour,
+                               CalendarBasedOrigin: true,
+                       }, compute.NewDatum(input))
+                       require.NoError(t, err)
+                       defer result.Release()
+
+                       output := 
result.(*compute.ArrayDatum).MakeArray().(*array.Timestamp)
+                       defer output.Release()
+                       want, err := arrow.TimestampFromTime(wantTime, unit)
+                       require.NoError(t, err)
+                       require.Equal(t, want, output.Value(0))
+               })
+       }
+}
+
+func TestTemporalRoundingZonedWideInputNoOp(t *testing.T) {
+       for _, tc := range []struct {
+               name           string
+               input          time.Time
+               calendarOrigin bool
+       }{
+               {
+                       name:  "before nanosecond range",
+                       input: time.Date(1500, time.June, 15, 12, 34, 56, 0, 
time.UTC),
+               },
+               {
+                       name:  "after nanosecond range",
+                       input: time.Date(2300, time.June, 15, 12, 34, 56, 0, 
time.UTC),
+               },
+               {
+                       name:           "before nanosecond range with calendar 
origin",
+                       input:          time.Date(1500, time.June, 15, 12, 34, 
56, 0, time.UTC),
+                       calendarOrigin: true,
+               },
+               {
+                       name:           "after nanosecond range with calendar 
origin",
+                       input:          time.Date(2300, time.June, 15, 12, 34, 
56, 0, time.UTC),
+                       calendarOrigin: true,
+               },
+       } {
+               t.Run(tc.name, func(t *testing.T) {
+                       for _, rounding := range []struct {
+                               name string
+                               fn   temporalRoundingFunc
+                       }{
+                               {name: "floor", fn: compute.FloorTemporal},
+                               {name: "ceil", fn: compute.CeilTemporal},
+                               {name: "round", fn: compute.RoundTemporal},
+                       } {
+                               t.Run(rounding.name, func(t *testing.T) {
+                                       requireTemporalRoundingTime(t, 
rounding.fn, tc.input, tc.input, arrow.Second, "Asia/Kathmandu", 
compute.RoundTemporalOptions{
+                                               Multiple:            1,
+                                               Unit:                
compute.RoundTemporalNanosecond,
+                                               CalendarBasedOrigin: 
tc.calendarOrigin,
+                                       })
+                               })
+                       }
+               })
+       }
+}
+
+func TestTemporalRoundingCalendarOriginAtMinimum(t *testing.T) {
+       minTime := arrow.Timestamp(math.MinInt64).ToTime(arrow.Nanosecond)
+       wantTime := time.Date(minTime.Year(), minTime.Month(), minTime.Day(), 
minTime.Hour()+1, 0, 0, 0, time.UTC)
+
+       builder := array.NewTimestampBuilder(memory.DefaultAllocator, 
&arrow.TimestampType{Unit: arrow.Nanosecond})
+       builder.Append(arrow.Timestamp(math.MinInt64))
+       input := builder.NewArray()
+       builder.Release()
+       defer input.Release()
+
+       result, err := compute.CeilTemporal(context.Background(), 
compute.RoundTemporalOptions{
+               Multiple:            1,
+               Unit:                compute.RoundTemporalHour,
+               CalendarBasedOrigin: true,
+       }, compute.NewDatum(input))
+       require.NoError(t, err)
+       defer result.Release()
+
+       output := result.(*compute.ArrayDatum).MakeArray().(*array.Timestamp)
+       defer output.Release()
+       want, err := arrow.TimestampFromTime(wantTime, arrow.Nanosecond)
+       require.NoError(t, err)
+       require.Equal(t, want, output.Value(0))
+}
+
+func TestTemporalRoundingCalendarLongRangeWeek(t *testing.T) {
+       value, err := arrow.TimestampFromTime(
+               time.Date(1500, time.June, 15, 12, 34, 56, 0, time.UTC),
+               arrow.Second,
+       )
+       require.NoError(t, err)
+
+       builder := array.NewTimestampBuilder(memory.DefaultAllocator, 
&arrow.TimestampType{Unit: arrow.Second})
+       builder.Append(value)
+       input := builder.NewArray()
+       builder.Release()
+       defer input.Release()
+
+       result, err := compute.FloorTemporal(context.Background(), 
compute.RoundTemporalOptions{
+               Multiple: 1,
+               Unit:     compute.RoundTemporalWeek,
+       }, compute.NewDatum(input))
+       require.NoError(t, err)
+       defer result.Release()
+
+       output := result.(*compute.ArrayDatum).MakeArray().(*array.Timestamp)
+       defer output.Release()
+       want, err := arrow.TimestampFromTime(time.Date(1500, time.June, 10, 0, 
0, 0, 0, time.UTC), arrow.Second)
+       require.NoError(t, err)
+       require.Equal(t, want, output.Value(0))
+}
+
+func TestTemporalRoundingCalendarSecondBoundary(t *testing.T) {
+       maxSecondTime := arrow.Timestamp(math.MaxInt64).ToTime(arrow.Second)
+       valueTime := time.Date(maxSecondTime.Year(), time.January, 2, 0, 0, 0, 
0, time.UTC)
+       value, err := arrow.TimestampFromTime(valueTime, arrow.Second)
+       require.NoError(t, err)
+
+       builder := array.NewTimestampBuilder(memory.DefaultAllocator, 
&arrow.TimestampType{Unit: arrow.Second})
+       builder.Append(value)
+       input := builder.NewArray()
+       builder.Release()
+       defer input.Release()
+
+       result, err := compute.RoundTemporal(context.Background(), 
compute.RoundTemporalOptions{
+               Multiple: 1,
+               Unit:     compute.RoundTemporalYear,
+       }, compute.NewDatum(input))
+       require.NoError(t, err)
+       defer result.Release()
+
+       output := result.(*compute.ArrayDatum).MakeArray().(*array.Timestamp)
+       defer output.Release()
+       want, err := arrow.TimestampFromTime(time.Date(maxSecondTime.Year(), 
time.January, 1, 0, 0, 0, 0, time.UTC), arrow.Second)
+       require.NoError(t, err)
+       require.Equal(t, want, output.Value(0))
+}
+
+func TestTemporalRoundingSecondOverflow(t *testing.T) {
+       builder := array.NewTimestampBuilder(memory.DefaultAllocator, 
&arrow.TimestampType{Unit: arrow.Second})
+       builder.Append(arrow.Timestamp(math.MaxInt64))
+       input := builder.NewArray()
+       builder.Release()
+       defer input.Release()
+
+       _, err := compute.CeilTemporal(context.Background(), 
compute.RoundTemporalOptions{
+               Multiple: 1,
+               Unit:     compute.RoundTemporalYear,
+       }, compute.NewDatum(input))
+       require.ErrorIs(t, err, arrow.ErrInvalid)
+}
+
+func TestTemporalRoundingSecondCalendarExtremes(t *testing.T) {
+       tests := []struct {
+               name  string
+               value arrow.Timestamp
+               fn    func(context.Context, compute.RoundTemporalOptions, 
compute.Datum) (compute.Datum, error)
+               opts  compute.RoundTemporalOptions
+       }{
+               {
+                       name:  "floor at minimum",
+                       value: arrow.Timestamp(math.MinInt64),
+                       fn:    compute.FloorTemporal,
+                       opts: compute.RoundTemporalOptions{
+                               Multiple: 1,
+                               Unit:     compute.RoundTemporalYear,
+                       },
+               },
+               {
+                       name:  "ceil at maximum to day",
+                       value: arrow.Timestamp(math.MaxInt64),
+                       fn:    compute.CeilTemporal,
+                       opts: compute.RoundTemporalOptions{
+                               Multiple: 1,
+                               Unit:     compute.RoundTemporalDay,
+                       },
+               },
+               {
+                       name:  "ceil immediately below maximum to day",
+                       value: arrow.Timestamp(math.MaxInt64 - 1),
+                       fn:    compute.CeilTemporal,
+                       opts: compute.RoundTemporalOptions{
+                               Multiple: 1,
+                               Unit:     compute.RoundTemporalDay,
+                       },
+               },
+       }
+
+       for _, tc := range tests {
+               t.Run(tc.name, func(t *testing.T) {
+                       builder := 
array.NewTimestampBuilder(memory.DefaultAllocator, &arrow.TimestampType{Unit: 
arrow.Second})
+                       builder.Append(tc.value)
+                       input := builder.NewArray()
+                       builder.Release()
+                       defer input.Release()
+
+                       _, err := tc.fn(context.Background(), tc.opts, 
compute.NewDatum(input))
+                       require.ErrorIs(t, err, arrow.ErrInvalid)
+               })
+       }
+}
+
+func TestTemporalRoundingNegativeCalendarMultiples(t *testing.T) {
+       tests := []struct {
+               name     string
+               value    time.Time
+               floor    time.Time
+               ceil     time.Time
+               multiple int64
+               unit     compute.RoundTemporalUnit
+       }{
+               {
+                       name:     "year",
+                       value:    time.Date(-5, time.June, 15, 12, 0, 0, 0, 
time.UTC),
+                       floor:    time.Date(-6, time.January, 1, 0, 0, 0, 0, 
time.UTC),
+                       ceil:     time.Date(-4, time.January, 1, 0, 0, 0, 0, 
time.UTC),
+                       multiple: 2,
+                       unit:     compute.RoundTemporalYear,
+               },
+               {
+                       name:     "quarter",
+                       value:    time.Date(-5, time.June, 15, 12, 0, 0, 0, 
time.UTC),
+                       floor:    time.Date(-5, time.January, 1, 0, 0, 0, 0, 
time.UTC),
+                       ceil:     time.Date(-5, time.July, 1, 0, 0, 0, 0, 
time.UTC),
+                       multiple: 2,
+                       unit:     compute.RoundTemporalQuarter,
+               },
+               {
+                       name:     "month",
+                       value:    time.Date(-5, time.June, 15, 12, 0, 0, 0, 
time.UTC),
+                       floor:    time.Date(-5, time.May, 1, 0, 0, 0, 0, 
time.UTC),
+                       ceil:     time.Date(-5, time.July, 1, 0, 0, 0, 0, 
time.UTC),
+                       multiple: 2,
+                       unit:     compute.RoundTemporalMonth,
+               },
+               {
+                       name:     "pre-epoch weeks",
+                       value:    time.Date(1969, time.December, 21, 12, 0, 0, 
0, time.UTC),
+                       floor:    time.Date(1969, time.December, 14, 0, 0, 0, 
0, time.UTC),
+                       ceil:     time.Date(1969, time.December, 28, 0, 0, 0, 
0, time.UTC),
+                       multiple: 2,
+                       unit:     compute.RoundTemporalWeek,
+               },
+       }
+
+       for _, tc := range tests {
+               t.Run(tc.name, func(t *testing.T) {
+                       value, err := arrow.TimestampFromTime(tc.value, 
arrow.Second)
+                       require.NoError(t, err)
+
+                       builder := 
array.NewTimestampBuilder(memory.DefaultAllocator, &arrow.TimestampType{Unit: 
arrow.Second})
+                       builder.Append(value)
+                       input := builder.NewArray()
+                       builder.Release()
+                       defer input.Release()
+
+                       for _, round := range []struct {
+                               name string
+                               fn   func(context.Context, 
compute.RoundTemporalOptions, compute.Datum) (compute.Datum, error)
+                               want time.Time
+                       }{
+                               {name: "floor", fn: compute.FloorTemporal, 
want: tc.floor},
+                               {name: "ceil", fn: compute.CeilTemporal, want: 
tc.ceil},
+                       } {
+                               t.Run(round.name, func(t *testing.T) {
+                                       result, err := 
round.fn(context.Background(), compute.RoundTemporalOptions{
+                                               Multiple: tc.multiple,
+                                               Unit:     tc.unit,
+                                       }, compute.NewDatum(input))
+                                       require.NoError(t, err)
+                                       defer result.Release()
+
+                                       output := 
result.(*compute.ArrayDatum).MakeArray().(*array.Timestamp)
+                                       defer output.Release()
+                                       want, err := 
arrow.TimestampFromTime(round.want, arrow.Second)
+                                       require.NoError(t, err)
+                                       require.Equal(t, want, output.Value(0))
+                               })
+                       }
+               })
+       }
+}
+
+func TestTemporalRoundingCalendarLongPeriod(t *testing.T) {
+       value, err := arrow.TimestampFromTime(
+               time.Date(1948, time.January, 1, 0, 0, 0, 0, time.UTC),
+               arrow.Nanosecond,
+       )
+       require.NoError(t, err)
+
+       builder := array.NewTimestampBuilder(memory.DefaultAllocator, 
&arrow.TimestampType{Unit: arrow.Nanosecond})
+       builder.Append(value)
+       input := builder.NewArray()
+       builder.Release()
+       defer input.Release()
+
+       result, err := compute.RoundTemporal(context.Background(), 
compute.RoundTemporalOptions{
+               Multiple: 300,
+               Unit:     compute.RoundTemporalYear,
+       }, compute.NewDatum(input))
+       require.NoError(t, err)
+       defer result.Release()
+
+       output := result.(*compute.ArrayDatum).MakeArray().(*array.Timestamp)
+       defer output.Release()
+       want, err := arrow.TimestampFromTime(time.Date(1800, time.January, 1, 
0, 0, 0, 0, time.UTC), arrow.Nanosecond)
+       require.NoError(t, err)
+       require.Equal(t, want, output.Value(0))
+}
diff --git a/arrow/compute/temporal_rounding_test.go 
b/arrow/compute/temporal_rounding_test.go
index 4de60730..e3fd6beb 100644
--- a/arrow/compute/temporal_rounding_test.go
+++ b/arrow/compute/temporal_rounding_test.go
@@ -385,9 +385,9 @@ func TestTemporalRoundingVectors(t *testing.T) {
                                time.Date(2023, 2, 1, 0, 0, 0, 0, time.UTC),
                                time.Date(2023, 2, 1, 0, 0, 0, 0, time.UTC),
                                time.Date(2023, 2, 1, 0, 0, 0, 0, time.UTC),
-                               time.Date(2023, 3, 1, 0, 0, 0, 0, time.UTC),
                                time.Date(2023, 4, 1, 0, 0, 0, 0, time.UTC),
-                               time.Date(1970, 1, 1, 0, 0, 0, 0, time.UTC),
+                               time.Date(2023, 4, 1, 0, 0, 0, 0, time.UTC),
+                               time.Date(1970, 2, 1, 0, 0, 0, 0, time.UTC),
                                time.Date(1970, 1, 1, 0, 0, 0, 0, time.UTC),
                                time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC),
                                time.Date(2024, 3, 1, 0, 0, 0, 0, time.UTC),
@@ -435,7 +435,7 @@ func TestTemporalRoundingVectors(t *testing.T) {
                                time.Date(2023, 4, 1, 0, 0, 0, 0, time.UTC), // 
-> Q2 2023 (not at quarter boundary)
                                time.Date(2023, 4, 1, 0, 0, 0, 0, time.UTC), // 
-> Q2 2023 (March 1 is in Q1)
                                time.Date(2023, 4, 1, 0, 0, 0, 0, time.UTC), // 
-> Q2 2023
-                               time.Date(1970, 1, 1, 0, 0, 0, 0, time.UTC), // 
already at Q1 start
+                               time.Date(1970, 4, 1, 0, 0, 0, 0, time.UTC), // 
exact Q1 start advances to Q2
                                time.Date(1970, 1, 1, 0, 0, 0, 0, time.UTC), // 
-> Q1 1970
                                time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC), // 
-> Q1 2024
                                time.Date(2024, 4, 1, 0, 0, 0, 0, time.UTC), // 
-> Q2 2024
@@ -603,6 +603,165 @@ func TestTemporalRoundingVectors(t *testing.T) {
        }
 }
 
+func TestTemporalRoundingInputResolutionCasting(t *testing.T) {
+       values := []int64{2, 1, 0, -1, -2}
+       tests := []struct {
+               name     string
+               timezone string
+               multiple int64
+               unit     compute.RoundTemporalUnit
+               floor    []int64
+               ceil     []int64
+               strict   []int64
+               round    []int64
+       }{
+               {
+                       name:     "nanosecond multiple in seconds UTC",
+                       multiple: 1_500_000_000,
+                       unit:     compute.RoundTemporalNanosecond,
+                       floor:    []int64{1, 0, 0, -1, -3},
+                       ceil:     []int64{2, 1, 0, -1, -2},
+                       strict:   []int64{2, 1, 1, 0, -2},
+                       round:    []int64{2, 1, 0, -1, -2},
+               },
+               {
+                       name:     "nanosecond multiple in seconds New York",
+                       timezone: "America/New_York",
+                       multiple: 1_500_000_000,
+                       unit:     compute.RoundTemporalNanosecond,
+                       floor:    []int64{2, 0, 0, -1, -3},
+                       ceil:     []int64{2, 1, 0, -1, -2},
+                       strict:   []int64{3, 1, 1, 0, -2},
+                       round:    []int64{2, 1, 0, -1, -2},
+               },
+               {
+                       name:     "nanosecond multiple in seconds Kathmandu",
+                       timezone: "Asia/Kathmandu",
+                       multiple: 1_500_000_000,
+                       unit:     compute.RoundTemporalNanosecond,
+                       floor:    []int64{1, 0, 0, -2, -3},
+                       ceil:     []int64{2, 1, 0, -1, -2},
+                       strict:   []int64{2, 1, 1, -1, -2},
+                       round:    []int64{2, 1, 0, -1, -2},
+               },
+               {
+                       name:     "millisecond multiple in seconds UTC",
+                       multiple: 999,
+                       unit:     compute.RoundTemporalMillisecond,
+                       floor:    []int64{1, 0, 0, -1, -2},
+                       ceil:     []int64{1, 0, 0, -1, -2},
+                       strict:   []int64{1, 0, 0, -1, -2},
+                       round:    []int64{1, 0, 0, -1, -2},
+               },
+               {
+                       name:     "millisecond multiple in seconds New York",
+                       timezone: "America/New_York",
+                       multiple: 999,
+                       unit:     compute.RoundTemporalMillisecond,
+                       floor:    []int64{2, 1, 0, -1, -2},
+                       ceil:     []int64{2, 1, 0, -1, -2},
+                       strict:   []int64{2, 1, 0, -1, -2},
+                       round:    []int64{2, 1, 0, -1, -2},
+               },
+               {
+                       name:     "millisecond multiple in seconds Kathmandu",
+                       timezone: "Asia/Kathmandu",
+                       multiple: 999,
+                       unit:     compute.RoundTemporalMillisecond,
+                       floor:    []int64{1, 0, -1, -2, -3},
+                       ceil:     []int64{1, 0, -1, -2, -3},
+                       strict:   []int64{1, 0, -1, -2, -3},
+                       round:    []int64{1, 0, -1, -2, -3},
+               },
+       }
+
+       for _, tc := range tests {
+               t.Run(tc.name, func(t *testing.T) {
+                       builder := 
array.NewTimestampBuilder(memory.DefaultAllocator, &arrow.TimestampType{
+                               Unit: arrow.Second, TimeZone: tc.timezone,
+                       })
+                       for _, value := range values {
+                               builder.Append(arrow.Timestamp(value))
+                       }
+                       input := builder.NewArray()
+                       builder.Release()
+                       defer input.Release()
+
+                       for _, rounding := range []struct {
+                               name   string
+                               fn     func(context.Context, 
compute.RoundTemporalOptions, compute.Datum) (compute.Datum, error)
+                               strict bool
+                               want   []int64
+                       }{
+                               {name: "floor", fn: compute.FloorTemporal, 
want: tc.floor},
+                               {name: "ceil", fn: compute.CeilTemporal, want: 
tc.ceil},
+                               {name: "ceil strictly greater", fn: 
compute.CeilTemporal, strict: true, want: tc.strict},
+                               {name: "round", fn: compute.RoundTemporal, 
want: tc.round},
+                       } {
+                               t.Run(rounding.name, func(t *testing.T) {
+                                       result, err := 
rounding.fn(context.Background(), compute.RoundTemporalOptions{
+                                               Multiple:              
tc.multiple,
+                                               Unit:                  tc.unit,
+                                               CeilIsStrictlyGreater: 
rounding.strict,
+                                       }, compute.NewDatum(input))
+                                       require.NoError(t, err)
+                                       defer result.Release()
+
+                                       output := 
result.(*compute.ArrayDatum).MakeArray().(*array.Timestamp)
+                                       defer output.Release()
+                                       for i, want := range rounding.want {
+                                               require.Equal(t, 
arrow.Timestamp(want), output.Value(i), "value %d", i)
+                                       }
+                               })
+                       }
+               })
+       }
+}
+
+func TestTemporalRoundingWideInputResolution(t *testing.T) {
+       for _, tc := range []struct {
+               name     string
+               input    time.Time
+               expected time.Time
+               timezone string
+       }{
+               {
+                       name:     "before epoch",
+                       input:    time.Date(1500, time.June, 15, 12, 34, 56, 0, 
time.UTC),
+                       expected: time.Date(1500, time.June, 15, 12, 34, 56, 0, 
time.UTC),
+               },
+               {
+                       name:     "after nanosecond range",
+                       input:    time.Date(2300, time.June, 15, 12, 34, 56, 0, 
time.UTC),
+                       expected: time.Date(2300, time.June, 15, 12, 34, 55, 0, 
time.UTC),
+               },
+               {
+                       name:     "after nanosecond range in Kathmandu",
+                       input:    time.Date(2300, time.June, 15, 12, 34, 56, 0, 
time.UTC),
+                       expected: time.Date(2300, time.June, 15, 12, 34, 55, 0, 
time.UTC),
+                       timezone: "Asia/Kathmandu",
+               },
+       } {
+               t.Run(tc.name, func(t *testing.T) {
+                       for _, rounding := range []struct {
+                               name string
+                               fn   temporalRoundingFunc
+                       }{
+                               {name: "floor", fn: compute.FloorTemporal},
+                               {name: "ceil", fn: compute.CeilTemporal},
+                               {name: "round", fn: compute.RoundTemporal},
+                       } {
+                               t.Run(rounding.name, func(t *testing.T) {
+                                       requireTemporalRoundingTime(t, 
rounding.fn, tc.input, tc.expected, arrow.Second, tc.timezone, 
compute.RoundTemporalOptions{
+                                               Multiple: 3,
+                                               Unit:     
compute.RoundTemporalMillisecond,
+                                       })
+                               })
+                       }
+               })
+       }
+}
+
 func TestTemporalWithNulls(t *testing.T) {
        ctx := context.Background()
        mem := memory.DefaultAllocator
@@ -758,7 +917,7 @@ func TestTemporalTimezoneAware(t *testing.T) {
                {name: "week_utc", tz: "UTC", unit: compute.RoundTemporalWeek,
                        expectedUTC: time.Date(2024, 7, 15, 0, 0, 0, 0, 
time.UTC)}, // Floor to Monday 00:00 UTC
                {name: "week_ny", tz: "America/New_York", unit: 
compute.RoundTemporalWeek,
-                       expectedUTC: time.Date(2024, 7, 8, 4, 0, 0, 0, 
time.UTC)}, // Floor to Monday 00:00 EDT (previous Monday in NY time)
+                       expectedUTC: time.Date(2024, 7, 15, 4, 0, 0, 0, 
time.UTC)}, // Floor to Monday 00:00 EDT
                {name: "week_tokyo", tz: "Asia/Tokyo", unit: 
compute.RoundTemporalWeek,
                        expectedUTC: time.Date(2024, 7, 14, 15, 0, 0, 0, 
time.UTC)}, // Floor to Monday 00:00 JST
 

Reply via email to