risingwave_expr_impl/scalar/
make_time.rs

1// Copyright 2025 RisingWave Labs
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15use chrono::{NaiveDate, NaiveDateTime, NaiveTime};
16use risingwave_common::types::{Date, F64, FloatExt, Time, Timestamp, Timestamptz};
17use risingwave_expr::expr_context::TIME_ZONE;
18use risingwave_expr::{ExprError, Result, capture_context, function};
19
20use crate::scalar::timestamptz::timestamp_at_time_zone;
21
22pub fn make_naive_date(mut year: i32, month: i32, day: i32) -> Result<NaiveDate> {
23    if year == 0 {
24        return Err(ExprError::InvalidParam {
25            name: "year, month, day",
26            reason: format!("invalid date: {}-{}-{}", year, month, day).into(),
27        });
28    }
29    if year < 0 {
30        year += 1
31    }
32    NaiveDate::from_ymd_opt(year, month as u32, day as u32).ok_or_else(|| ExprError::InvalidParam {
33        name: "year, month, day",
34        reason: format!("invalid date: {}-{}-{}", year, month, day).into(),
35    })
36}
37
38fn make_naive_time(hour: i32, min: i32, sec: F64) -> Result<NaiveTime> {
39    if !sec.is_finite() || sec.0.is_sign_negative() {
40        return Err(ExprError::InvalidParam {
41            name: "sec",
42            reason: format!("invalid sec: {}", sec).into(),
43        });
44    }
45    let sec_u32 = sec.0.trunc() as u32;
46    let nanosecond_u32 = ((sec.0 - sec.0.trunc()) * 1_000_000_000.0).round_ties_even() as u32;
47    NaiveTime::from_hms_nano_opt(hour as u32, min as u32, sec_u32, nanosecond_u32).ok_or_else(
48        || ExprError::InvalidParam {
49            name: "hour, min, sec",
50            reason: format!("invalid time: {}:{}:{}", hour, min, sec).into(),
51        },
52    )
53}
54
55// year int, month int, day int
56#[function("make_date(int4, int4, int4) -> date")]
57pub fn make_date(year: i32, month: i32, day: i32) -> Result<Date> {
58    Ok(Date(make_naive_date(year, month, day)?))
59}
60
61// hour int, min int, sec double precision
62#[function("make_time(int4, int4, float8) -> time")]
63pub fn make_time(hour: i32, min: i32, sec: F64) -> Result<Time> {
64    Ok(Time(make_naive_time(hour, min, sec)?))
65}
66
67// year int, month int, day int, hour int, min int, sec double precision
68#[function("make_timestamp(int4, int4, int4, int4, int4, float8) -> timestamp")]
69pub fn make_timestamp(
70    year: i32,
71    month: i32,
72    day: i32,
73    hour: i32,
74    min: i32,
75    sec: F64,
76) -> Result<Timestamp> {
77    Ok(Timestamp(NaiveDateTime::new(
78        make_naive_date(year, month, day)?,
79        make_naive_time(hour, min, sec)?,
80    )))
81}
82
83// year int, month int, day int, hour int, min int, sec double precision
84#[function("make_timestamptz(int4, int4, int4, int4, int4, float8) -> timestamptz")]
85pub fn make_timestamptz(
86    year: i32,
87    month: i32,
88    day: i32,
89    hour: i32,
90    min: i32,
91    sec: F64,
92) -> Result<Timestamptz> {
93    make_timestamptz_impl_captured(year, month, day, hour, min, sec)
94}
95
96// year int, month int, day int, hour int, min int, sec double precision, timezone text
97#[function("make_timestamptz(int4, int4, int4, int4, int4, float8, varchar) -> timestamptz")]
98pub fn make_timestamptz_with_time_zone(
99    year: i32,
100    month: i32,
101    day: i32,
102    hour: i32,
103    min: i32,
104    sec: F64,
105    time_zone: &str,
106) -> Result<Timestamptz> {
107    make_timestamptz_impl(time_zone, year, month, day, hour, min, sec)
108}
109
110#[capture_context(TIME_ZONE)]
111fn make_timestamptz_impl(
112    time_zone: &str,
113    year: i32,
114    month: i32,
115    day: i32,
116    hour: i32,
117    min: i32,
118    sec: F64,
119) -> Result<Timestamptz> {
120    let naive_date_time = NaiveDateTime::new(
121        make_naive_date(year, month, day)?,
122        make_naive_time(hour, min, sec)?,
123    );
124    timestamp_at_time_zone(Timestamp(naive_date_time), time_zone)
125}
126
127#[cfg(test)]
128mod tests {
129    use chrono::{NaiveDate, NaiveDateTime, NaiveTime};
130    use risingwave_common::types::{Date, Timestamp};
131
132    #[test]
133    fn test_naive_date_and_time() {
134        let year = -1973;
135        let month = 2;
136        let day = 2;
137        let hour = 12;
138        let min = 34;
139        let sec: f64 = 56.789;
140        let naive_date = NaiveDate::from_ymd_opt(year, month as u32, day as u32).unwrap();
141        let naive_time = NaiveTime::from_hms_micro_opt(
142            hour as u32,
143            min as u32,
144            sec.trunc() as u32,
145            ((sec - sec.trunc()) * 1_000_000.0).round() as u32,
146        )
147        .unwrap();
148        assert_eq!(naive_date.to_string(), String::from("-1973-02-02"));
149        let date = Date(naive_date);
150        assert_eq!(date.to_string(), String::from("1974-02-02 BC"));
151        assert_eq!(naive_time.to_string(), String::from("12:34:56.789"));
152        let date_time = Timestamp(NaiveDateTime::new(naive_date, naive_time));
153        assert_eq!(
154            date_time.to_string(),
155            String::from("1974-02-02 12:34:56.789 BC")
156        );
157    }
158}