risingwave_expr_impl/scalar/
tumble.rs1use num_traits::Zero;
16use risingwave_common::types::{Date, Interval, Timestamp, Timestamptz};
17use risingwave_expr::{ExprError, Result, function};
18
19#[inline(always)]
20fn interval_to_micro_second(t: Interval) -> Result<i64> {
21 let checked_interval_to_micro_second = || {
22 (t.months() as i64)
23 .checked_mul(Interval::USECS_PER_MONTH)?
24 .checked_add(
25 (t.days() as i64)
26 .checked_mul(Interval::USECS_PER_DAY)?
27 .checked_add(t.usecs())?,
28 )
29 };
30
31 checked_interval_to_micro_second().ok_or(ExprError::NumericOutOfRange)
32}
33
34#[function("tumble_start(date, interval) -> timestamp")]
35pub fn tumble_start_date(timestamp: Date, window_size: Interval) -> Result<Timestamp> {
36 tumble_start_date_time(timestamp.into(), window_size)
37}
38
39#[function("tumble_start(timestamp, interval) -> timestamp")]
40pub fn tumble_start_date_time(timestamp: Timestamp, window_size: Interval) -> Result<Timestamp> {
41 let timestamp_micro_second = timestamp.0.and_utc().timestamp_micros();
42 let window_start_micro_second = get_window_start(timestamp_micro_second, window_size)?;
43 Timestamp::with_micros(window_start_micro_second).map_err(|_| ExprError::NumericOutOfRange)
44}
45
46#[function("tumble_start(timestamptz, interval) -> timestamptz")]
47pub fn tumble_start_timestamptz(tz: Timestamptz, window_size: Interval) -> Result<Timestamptz> {
48 get_window_start(tz.timestamp_micros(), window_size)
49 .and_then(|us| Timestamptz::from_micros(us).ok_or(ExprError::NumericOutOfRange))
50}
51
52#[inline(always)]
54fn get_window_start(timestamp_micro_second: i64, window_size: Interval) -> Result<i64> {
55 get_window_start_with_offset(timestamp_micro_second, window_size, Interval::zero())
56}
57
58#[function("tumble_start(date, interval, interval) -> timestamp")]
59pub fn tumble_start_offset_date(
60 timestamp_date: Date,
61 window_size: Interval,
62 offset: Interval,
63) -> Result<Timestamp> {
64 tumble_start_offset_date_time(timestamp_date.into(), window_size, offset)
65}
66
67#[function("tumble_start(timestamp, interval, interval) -> timestamp")]
68pub fn tumble_start_offset_date_time(
69 time: Timestamp,
70 window_size: Interval,
71 offset: Interval,
72) -> Result<Timestamp> {
73 let timestamp_micro_second = time.0.and_utc().timestamp_micros();
74 let window_start_micro_second =
75 get_window_start_with_offset(timestamp_micro_second, window_size, offset)?;
76
77 Timestamp::with_micros(window_start_micro_second).map_err(|_| ExprError::NumericOutOfRange)
78}
79
80#[inline(always)]
81fn get_window_start_with_offset(
82 timestamp_micro_second: i64,
83 window_size: Interval,
84 offset: Interval,
85) -> Result<i64> {
86 let window_size_micro_second = interval_to_micro_second(window_size)?;
87 let offset_micro_second = interval_to_micro_second(offset)?;
88
89 let remainder = timestamp_micro_second
91 .checked_sub(offset_micro_second)
92 .ok_or(ExprError::NumericOutOfRange)?
93 .checked_rem(window_size_micro_second)
94 .ok_or(ExprError::DivisionByZero)?;
95 if remainder < 0 {
96 timestamp_micro_second
97 .checked_sub(remainder + window_size_micro_second)
98 .ok_or(ExprError::NumericOutOfRange)
99 } else {
100 timestamp_micro_second
101 .checked_sub(remainder)
102 .ok_or(ExprError::NumericOutOfRange)
103 }
104}
105
106#[function("tumble_start(timestamptz, interval, interval) -> timestamptz")]
107pub fn tumble_start_offset_timestamptz(
108 tz: Timestamptz,
109 window_size: Interval,
110 offset: Interval,
111) -> Result<Timestamptz> {
112 get_window_start_with_offset(tz.timestamp_micros(), window_size, offset)
113 .and_then(|us| Timestamptz::from_micros(us).ok_or(ExprError::NumericOutOfRange))
114}
115
116#[cfg(test)]
117mod tests {
118 use chrono::{Datelike, Timelike};
119 use risingwave_common::types::test_utils::IntervalTestExt;
120 use risingwave_common::types::{Date, Interval, Timestamp};
121
122 use super::tumble_start_offset_date_time;
123 use crate::scalar::tumble::{
124 get_window_start, interval_to_micro_second, tumble_start_date_time,
125 };
126
127 #[test]
128 fn test_tumble_start_date_time() {
129 let dt = Date::from_ymd_uncheck(2022, 2, 22).and_hms_uncheck(22, 22, 22);
130 let interval = Interval::from_minutes(30);
131 let w = tumble_start_date_time(dt, interval).unwrap().0;
132 assert_eq!(w.year(), 2022);
133 assert_eq!(w.month(), 2);
134 assert_eq!(w.day(), 22);
135 assert_eq!(w.hour(), 22);
136 assert_eq!(w.minute(), 0);
137 assert_eq!(w.second(), 0);
138 }
139
140 #[test]
141 fn test_tumble_start_negative_fractional() {
142 let dt = "1969-12-31 23:59:58.450".parse::<Timestamp>().unwrap();
144 let w = tumble_start_date_time(dt, Interval::from_millis(100)).unwrap();
145 assert_eq!(w, "1969-12-31 23:59:58.400".parse::<Timestamp>().unwrap());
146 }
147
148 #[test]
149 fn test_tumble_start_offset_date_time() {
150 let dt = Date::from_ymd_uncheck(2022, 2, 22).and_hms_uncheck(22, 22, 22);
151 let window_size = 30;
152 for offset in 0..window_size {
153 for coefficient in 0..5 {
154 let w = tumble_start_date_time(dt, Interval::from_minutes(window_size))
155 .unwrap()
156 .0;
157 println!("{}", w);
158 let w = tumble_start_offset_date_time(
159 dt,
160 Interval::from_minutes(window_size),
161 Interval::from_minutes(coefficient * window_size + offset),
162 )
163 .unwrap()
164 .0;
165 assert_eq!(w.year(), 2022);
166 assert_eq!(w.month(), 2);
167 assert_eq!(w.day(), 22);
168 if offset > 22 {
169 assert_eq!(w.hour(), 21);
170 assert_eq!(w.minute(), 30 + offset as u32);
171 } else {
172 assert_eq!(w.hour(), 22);
173 assert_eq!(w.minute(), offset as u32);
174 }
175
176 assert_eq!(w.second(), 0);
177 }
178 }
179 }
180
181 #[test]
182 fn test_remainder_necessary() {
183 let mut wrong_cnt = 0;
184 for i in -30..30 {
185 let timestamp_micro_second = Interval::from_minutes(i).usecs();
186 let window_size = Interval::from_minutes(5);
187 let window_start = get_window_start(timestamp_micro_second, window_size).unwrap();
188
189 let window_size_micro_second = interval_to_micro_second(window_size).unwrap();
190 let default_window_start = timestamp_micro_second
191 - (timestamp_micro_second + window_size_micro_second) % window_size_micro_second;
192
193 if timestamp_micro_second < default_window_start {
194 wrong_cnt += 1;
196 }
197
198 assert!(timestamp_micro_second >= window_start)
199 }
200 assert_ne!(wrong_cnt, 0);
201 }
202
203 #[test]
204 fn test_window_start_overflow() {
205 get_window_start(i64::MIN, Interval::from_millis(20)).unwrap_err();
206 interval_to_micro_second(Interval::from_month_day_usec(1, 1, i64::MAX)).unwrap_err();
207 }
208}