blob: ec95d190446383a23393a4a04038ee6baace6c28 [file]
// 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.
use arrow::array::{Array, Decimal128Array, Int32Array, Time64NanosecondArray};
use arrow::compute::cast;
use arrow::datatypes::{DataType, TimeUnit};
use datafusion::common::{utils::take_function_args, DataFusionError, Result};
use datafusion::logical_expr::{
ColumnarValue, ScalarFunctionArgs, ScalarUDFImpl, Signature, Volatility,
};
use std::sync::Arc;
const MICROS_PER_SECOND: i128 = 1_000_000;
const NANOS_PER_MICRO: i64 = 1_000;
const NANOS_PER_SECOND: i64 = 1_000_000_000;
#[derive(Debug, PartialEq, Eq, Hash)]
pub struct SparkMakeTime {
signature: Signature,
}
impl SparkMakeTime {
pub fn new() -> Self {
Self {
signature: Signature::any(3, Volatility::Immutable),
}
}
}
impl Default for SparkMakeTime {
fn default() -> Self {
Self::new()
}
}
/// Converts hours, minutes, and fractional seconds (Decimal(16,6)) to nanoseconds from midnight.
/// Returns an error for invalid inputs (matching Spark's always-throw behavior).
fn make_time(hours: i32, minutes: i32, secs_and_micros_unscaled: i128) -> Result<i64> {
let full_secs = secs_and_micros_unscaled.div_euclid(MICROS_PER_SECOND);
let frac_micros = secs_and_micros_unscaled.rem_euclid(MICROS_PER_SECOND);
if full_secs > i32::MAX as i128 || full_secs < 0 {
return Err(DataFusionError::Execution(format!(
"Invalid value for SecondOfMinute (valid values 0 - 59): {}",
secs_and_micros_unscaled / MICROS_PER_SECOND
)));
}
let secs = full_secs as i32;
let nanos = (frac_micros as i64) * NANOS_PER_MICRO;
if !(0..=23).contains(&hours) {
return Err(DataFusionError::Execution(format!(
"Invalid value for HourOfDay (valid values 0 - 23): {hours}"
)));
}
if !(0..=59).contains(&minutes) {
return Err(DataFusionError::Execution(format!(
"Invalid value for MinuteOfHour (valid values 0 - 59): {minutes}"
)));
}
if !(0..=59).contains(&secs) {
return Err(DataFusionError::Execution(format!(
"Invalid value for SecondOfMinute (valid values 0 - 59): {secs}"
)));
}
let total_nanos = hours as i64 * 3_600 * NANOS_PER_SECOND
+ minutes as i64 * 60 * NANOS_PER_SECOND
+ secs as i64 * NANOS_PER_SECOND
+ nanos;
Ok(total_nanos)
}
impl ScalarUDFImpl for SparkMakeTime {
fn name(&self) -> &str {
"make_time"
}
fn signature(&self) -> &Signature {
&self.signature
}
fn return_type(&self, _: &[DataType]) -> Result<DataType> {
Ok(DataType::Time64(TimeUnit::Nanosecond))
}
fn invoke_with_args(&self, args: ScalarFunctionArgs) -> Result<ColumnarValue> {
let [hours, minutes, secs_and_micros] = take_function_args(self.name(), args.args)?;
let num_rows = [&hours, &minutes, &secs_and_micros]
.iter()
.find_map(|arg| match arg {
ColumnarValue::Array(array) => Some(array.len()),
ColumnarValue::Scalar(_) => None,
})
.unwrap_or(1);
let hours_arr = hours.into_array(num_rows)?;
let minutes_arr = minutes.into_array(num_rows)?;
let secs_arr = secs_and_micros.into_array(num_rows)?;
let hours_arr = cast_to_int32(&hours_arr)?;
let minutes_arr = cast_to_int32(&minutes_arr)?;
let hours_array = hours_arr
.as_any()
.downcast_ref::<Int32Array>()
.ok_or_else(|| {
DataFusionError::Execution("make_time: failed to cast hours to Int32".to_string())
})?;
let minutes_array = minutes_arr
.as_any()
.downcast_ref::<Int32Array>()
.ok_or_else(|| {
DataFusionError::Execution("make_time: failed to cast minutes to Int32".to_string())
})?;
let secs_array = secs_arr
.as_any()
.downcast_ref::<Decimal128Array>()
.ok_or_else(|| {
DataFusionError::Execution(
"make_time: expected Decimal128 for seconds argument".to_string(),
)
})?;
let len = hours_array.len();
let mut builder = Time64NanosecondArray::builder(len);
for i in 0..len {
if hours_array.is_null(i) || minutes_array.is_null(i) || secs_array.is_null(i) {
builder.append_null();
} else {
let h = hours_array.value(i);
let m = minutes_array.value(i);
let s = secs_array.value(i);
let nanos = make_time(h, m, s)?;
builder.append_value(nanos);
}
}
Ok(ColumnarValue::Array(Arc::new(builder.finish())))
}
}
fn cast_to_int32(arr: &Arc<dyn Array>) -> Result<Arc<dyn Array>> {
if arr.data_type() == &DataType::Int32 {
Ok(Arc::clone(arr))
} else {
cast(arr.as_ref(), &DataType::Int32)
.map_err(|e| DataFusionError::Execution(format!("Failed to cast to Int32: {e}")))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_make_time_valid() {
// Midnight
assert_eq!(make_time(0, 0, 0).unwrap(), 0);
// 1 hour
assert_eq!(make_time(1, 0, 0).unwrap(), 3_600_000_000_000);
// 1 minute
assert_eq!(make_time(0, 1, 0).unwrap(), 60_000_000_000);
// 1 second (unscaled: 1_000_000)
assert_eq!(make_time(0, 0, 1_000_000).unwrap(), 1_000_000_000);
// 1.5 seconds (unscaled: 1_500_000)
assert_eq!(make_time(0, 0, 1_500_000).unwrap(), 1_500_000_000);
// 23:59:59.999999 (unscaled: 59_999_999)
assert_eq!(make_time(23, 59, 59_999_999).unwrap(), 86_399_999_999_000);
// 12:30:45.123456 (unscaled: 45_123_456)
assert_eq!(
make_time(12, 30, 45_123_456).unwrap(),
12 * 3_600_000_000_000 + 30 * 60_000_000_000 + 45_123_456_000
);
}
#[test]
fn test_make_time_invalid_hours() {
assert!(make_time(24, 0, 0).is_err());
assert!(make_time(25, 0, 0).is_err());
assert!(make_time(-1, 0, 0).is_err());
}
#[test]
fn test_make_time_invalid_minutes() {
assert!(make_time(0, 60, 0).is_err());
assert!(make_time(0, -1, 0).is_err());
}
#[test]
fn test_make_time_invalid_seconds() {
// 60 seconds (unscaled: 60_000_000)
assert!(make_time(0, 0, 60_000_000).is_err());
// 100.5 seconds (unscaled: 100_500_000)
assert!(make_time(0, 0, 100_500_000).is_err());
// negative seconds (unscaled: -1_000_000)
assert!(make_time(0, 0, -1_000_000).is_err());
}
#[test]
fn test_make_time_overflow_seconds() {
// Very large value that overflows i32
let large = (i32::MAX as i128 + 1) * MICROS_PER_SECOND;
assert!(make_time(0, 0, large).is_err());
}
}