Skip to content

Commit 044a4a7

Browse files
Omega359alamb
andauthored
Add make_time function (#19183)
## Which issue does this PR close? <!-- We generally require a GitHub issue to be filed for all bug fixes and enhancements and this helps us generate change logs for our releases. You can link an issue to this PR using the GitHub syntax. For example `Closes #123` indicates that this PR will close issue #123. --> - Part of #19025 ## Rationale for this change There wasn't a good way to make a time from component parts. ## What changes are included in this PR? Code, test, docs ## Are these changes tested? Yes ## Are there any user-facing changes? New function. --------- Co-authored-by: Andrew Lamb <andrew@nerdnetworks.org>
1 parent c8add5c commit 044a4a7

File tree

4 files changed

+584
-0
lines changed

4 files changed

+584
-0
lines changed
Lines changed: 270 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,270 @@
1+
// Licensed to the Apache Software Foundation (ASF) under one
2+
// or more contributor license agreements. See the NOTICE file
3+
// distributed with this work for additional information
4+
// regarding copyright ownership. The ASF licenses this file
5+
// to you under the Apache License, Version 2.0 (the
6+
// "License"); you may not use this file except in compliance
7+
// with the License. You may obtain a copy of the License at
8+
//
9+
// http://www.apache.org/licenses/LICENSE-2.0
10+
//
11+
// Unless required by applicable law or agreed to in writing,
12+
// software distributed under the License is distributed on an
13+
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14+
// KIND, either express or implied. See the License for the
15+
// specific language governing permissions and limitations
16+
// under the License.
17+
18+
use std::any::Any;
19+
use std::sync::Arc;
20+
21+
use arrow::array::builder::PrimitiveBuilder;
22+
use arrow::array::cast::AsArray;
23+
use arrow::array::types::Int32Type;
24+
use arrow::array::{Array, PrimitiveArray};
25+
use arrow::datatypes::DataType::Time32;
26+
use arrow::datatypes::{DataType, Time32SecondType, TimeUnit};
27+
use chrono::prelude::*;
28+
29+
use datafusion_common::types::{NativeType, logical_int32, logical_string};
30+
use datafusion_common::{Result, ScalarValue, exec_err, utils::take_function_args};
31+
use datafusion_expr::{
32+
ColumnarValue, Documentation, ScalarUDFImpl, Signature, Volatility,
33+
};
34+
use datafusion_expr_common::signature::{Coercion, TypeSignatureClass};
35+
use datafusion_macros::user_doc;
36+
37+
#[user_doc(
38+
doc_section(label = "Time and Date Functions"),
39+
description = "Make a time from hour/minute/second component parts.",
40+
syntax_example = "make_time(hour, minute, second)",
41+
sql_example = r#"```sql
42+
> select make_time(13, 23, 1);
43+
+-------------------------------------------+
44+
| make_time(Int64(13),Int64(23),Int64(1)) |
45+
+-------------------------------------------+
46+
| 13:23:01 |
47+
+-------------------------------------------+
48+
> select make_time('23', '01', '31');
49+
+-----------------------------------------------+
50+
| make_time(Utf8("23"),Utf8("01"),Utf8("31")) |
51+
+-----------------------------------------------+
52+
| 23:01:31 |
53+
+-----------------------------------------------+
54+
```
55+
56+
Additional examples can be found [here](https://github.com/apache/datafusion/blob/main/datafusion-examples/examples/builtin_functions/date_time.rs)
57+
"#,
58+
argument(
59+
name = "hour",
60+
description = "Hour to use when making the time. Can be a constant, column or function, and any combination of arithmetic operators."
61+
),
62+
argument(
63+
name = "minute",
64+
description = "Minute to use when making the time. Can be a constant, column or function, and any combination of arithmetic operators."
65+
),
66+
argument(
67+
name = "second",
68+
description = "Second to use when making the time. Can be a constant, column or function, and any combination of arithmetic operators."
69+
)
70+
)]
71+
#[derive(Debug, PartialEq, Eq, Hash)]
72+
pub struct MakeTimeFunc {
73+
signature: Signature,
74+
}
75+
76+
impl Default for MakeTimeFunc {
77+
fn default() -> Self {
78+
Self::new()
79+
}
80+
}
81+
82+
impl MakeTimeFunc {
83+
pub fn new() -> Self {
84+
let int = Coercion::new_implicit(
85+
TypeSignatureClass::Native(logical_int32()),
86+
vec![
87+
TypeSignatureClass::Integer,
88+
TypeSignatureClass::Native(logical_string()),
89+
],
90+
NativeType::Int32,
91+
);
92+
Self {
93+
signature: Signature::coercible(
94+
vec![int.clone(), int.clone(), int.clone()],
95+
Volatility::Immutable,
96+
),
97+
}
98+
}
99+
}
100+
101+
impl ScalarUDFImpl for MakeTimeFunc {
102+
fn as_any(&self) -> &dyn Any {
103+
self
104+
}
105+
106+
fn name(&self) -> &str {
107+
"make_time"
108+
}
109+
110+
fn signature(&self) -> &Signature {
111+
&self.signature
112+
}
113+
114+
fn return_type(&self, _arg_types: &[DataType]) -> Result<DataType> {
115+
Ok(Time32(TimeUnit::Second))
116+
}
117+
118+
fn invoke_with_args(
119+
&self,
120+
args: datafusion_expr::ScalarFunctionArgs,
121+
) -> Result<ColumnarValue> {
122+
let [hours, minutes, seconds] = take_function_args(self.name(), args.args)?;
123+
124+
match (hours, minutes, seconds) {
125+
(ColumnarValue::Scalar(h), _, _) if h.is_null() => {
126+
Ok(ColumnarValue::Scalar(ScalarValue::Time32Second(None)))
127+
}
128+
(_, ColumnarValue::Scalar(m), _) if m.is_null() => {
129+
Ok(ColumnarValue::Scalar(ScalarValue::Time32Second(None)))
130+
}
131+
(_, _, ColumnarValue::Scalar(s)) if s.is_null() => {
132+
Ok(ColumnarValue::Scalar(ScalarValue::Time32Second(None)))
133+
}
134+
(
135+
ColumnarValue::Scalar(ScalarValue::Int32(Some(hours))),
136+
ColumnarValue::Scalar(ScalarValue::Int32(Some(minutes))),
137+
ColumnarValue::Scalar(ScalarValue::Int32(Some(seconds))),
138+
) => {
139+
let mut value = 0;
140+
make_time_inner(hours, minutes, seconds, |seconds: i32| value = seconds)?;
141+
Ok(ColumnarValue::Scalar(ScalarValue::Time32Second(Some(
142+
value,
143+
))))
144+
}
145+
(hours, minutes, seconds) => {
146+
let len = args.number_rows;
147+
let hours = hours.into_array(len)?;
148+
let minutes = minutes.into_array(len)?;
149+
let seconds = seconds.into_array(len)?;
150+
151+
let hours = hours.as_primitive::<Int32Type>();
152+
let minutes = minutes.as_primitive::<Int32Type>();
153+
let seconds = seconds.as_primitive::<Int32Type>();
154+
155+
let mut builder: PrimitiveBuilder<Time32SecondType> =
156+
PrimitiveArray::builder(len);
157+
158+
for i in 0..len {
159+
// match postgresql behaviour which returns null for any null input
160+
if hours.is_null(i) || minutes.is_null(i) || seconds.is_null(i) {
161+
builder.append_null();
162+
} else {
163+
make_time_inner(
164+
hours.value(i),
165+
minutes.value(i),
166+
seconds.value(i),
167+
|seconds: i32| builder.append_value(seconds),
168+
)?;
169+
}
170+
}
171+
172+
Ok(ColumnarValue::Array(Arc::new(builder.finish())))
173+
}
174+
}
175+
}
176+
177+
fn documentation(&self) -> Option<&Documentation> {
178+
self.doc()
179+
}
180+
}
181+
182+
/// Converts the hour/minute/second fields to an `i32` representing the seconds from
183+
/// midnight and invokes `time_consumer_fn` with the value
184+
fn make_time_inner<F: FnMut(i32)>(
185+
hour: i32,
186+
minute: i32,
187+
second: i32,
188+
mut time_consumer_fn: F,
189+
) -> Result<()> {
190+
let h = match hour {
191+
0..=24 => hour as u32,
192+
_ => return exec_err!("Hour value '{hour:?}' is out of range"),
193+
};
194+
let m = match minute {
195+
0..=60 => minute as u32,
196+
_ => return exec_err!("Minute value '{minute:?}' is out of range"),
197+
};
198+
let s = match second {
199+
0..=60 => second as u32,
200+
_ => return exec_err!("Second value '{second:?}' is out of range"),
201+
};
202+
203+
if let Some(time) = NaiveTime::from_hms_opt(h, m, s) {
204+
time_consumer_fn(time.num_seconds_from_midnight() as i32);
205+
Ok(())
206+
} else {
207+
exec_err!("Unable to parse time from {hour}, {minute}, {second}")
208+
}
209+
}
210+
211+
#[cfg(test)]
212+
mod tests {
213+
use crate::datetime::make_time::MakeTimeFunc;
214+
use arrow::array::{Array, Int32Array, Time32SecondArray};
215+
use arrow::datatypes::TimeUnit::Second;
216+
use arrow::datatypes::{DataType, Field};
217+
use datafusion_common::DataFusionError;
218+
use datafusion_common::config::ConfigOptions;
219+
use datafusion_expr::{ColumnarValue, ScalarUDFImpl};
220+
use std::sync::Arc;
221+
222+
fn invoke_make_time_with_args(
223+
args: Vec<ColumnarValue>,
224+
number_rows: usize,
225+
) -> Result<ColumnarValue, DataFusionError> {
226+
let arg_fields = args
227+
.iter()
228+
.map(|arg| Field::new("a", arg.data_type(), true).into())
229+
.collect::<Vec<_>>();
230+
let args = datafusion_expr::ScalarFunctionArgs {
231+
args,
232+
arg_fields,
233+
number_rows,
234+
return_field: Field::new("f", DataType::Time32(Second), true).into(),
235+
config_options: Arc::new(ConfigOptions::default()),
236+
};
237+
238+
MakeTimeFunc::new().invoke_with_args(args)
239+
}
240+
241+
#[test]
242+
fn test_make_time() {
243+
let hours = Arc::new((4..8).map(Some).collect::<Int32Array>());
244+
let minutes = Arc::new((1..5).map(Some).collect::<Int32Array>());
245+
let seconds = Arc::new((11..15).map(Some).collect::<Int32Array>());
246+
let batch_len = hours.len();
247+
let res = invoke_make_time_with_args(
248+
vec![
249+
ColumnarValue::Array(hours),
250+
ColumnarValue::Array(minutes),
251+
ColumnarValue::Array(seconds),
252+
],
253+
batch_len,
254+
)
255+
.unwrap();
256+
257+
if let ColumnarValue::Array(array) = res {
258+
assert_eq!(array.len(), 4);
259+
260+
let mut builder = Time32SecondArray::builder(4);
261+
builder.append_value(14_471);
262+
builder.append_value(18_132);
263+
builder.append_value(21_793);
264+
builder.append_value(25_454);
265+
assert_eq!(&builder.finish() as &dyn Array, array.as_ref());
266+
} else {
267+
panic!("Expected a columnar array")
268+
}
269+
}
270+
}

datafusion/functions/src/datetime/mod.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ pub mod date_part;
2929
pub mod date_trunc;
3030
pub mod from_unixtime;
3131
pub mod make_date;
32+
pub mod make_time;
3233
pub mod now;
3334
pub mod planner;
3435
pub mod to_char;
@@ -44,6 +45,7 @@ make_udf_function!(date_bin::DateBinFunc, date_bin);
4445
make_udf_function!(date_part::DatePartFunc, date_part);
4546
make_udf_function!(date_trunc::DateTruncFunc, date_trunc);
4647
make_udf_function!(make_date::MakeDateFunc, make_date);
48+
make_udf_function!(make_time::MakeTimeFunc, make_time);
4749
make_udf_function!(from_unixtime::FromUnixtimeFunc, from_unixtime);
4850
make_udf_function!(to_char::ToCharFunc, to_char);
4951
make_udf_function!(to_date::ToDateFunc, to_date);
@@ -90,6 +92,10 @@ pub mod expr_fn {
9092
make_date,
9193
"make a date from year, month and day component parts",
9294
year month day
95+
),(
96+
make_time,
97+
"make a time from hour, minute and second component parts",
98+
hour minute second
9399
),(
94100
now,
95101
"returns the current timestamp in nanoseconds, using the same value for all instances of now() in same statement",
@@ -267,6 +273,7 @@ pub fn functions() -> Vec<Arc<ScalarUDF>> {
267273
date_trunc(),
268274
from_unixtime(),
269275
make_date(),
276+
make_time(),
270277
now(&ConfigOptions::default()),
271278
to_char(),
272279
to_date(),

0 commit comments

Comments
 (0)