1#[cfg(feature = "timezones")]
2use chrono_tz::Tz;
3#[cfg(feature = "timezones")]
4use polars_core::chunked_array::temporal::validate_time_zone;
5#[cfg(feature = "timezones")]
6use polars_time::base_utc_offset as base_utc_offset_fn;
7#[cfg(feature = "timezones")]
8use polars_time::dst_offset as dst_offset_fn;
9#[cfg(feature = "offset_by")]
10use polars_time::impl_offset_by;
11#[cfg(any(feature = "dtype-date", feature = "dtype-datetime"))]
12use polars_time::replace::{replace_date, replace_datetime};
13#[cfg(feature = "serde")]
14use serde::{Deserialize, Serialize};
15
16use super::*;
17
18#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
19#[derive(Clone, PartialEq, Debug, Eq, Hash)]
20pub enum TemporalFunction {
21 Millennium,
22 Century,
23 Year,
24 IsLeapYear,
25 IsoYear,
26 Quarter,
27 Month,
28 Week,
29 WeekDay,
30 Day,
31 OrdinalDay,
32 Time,
33 Date,
34 Datetime,
35 Duration(TimeUnit),
36 Hour,
37 Minute,
38 Second,
39 Millisecond,
40 Microsecond,
41 Nanosecond,
42 TotalDays,
43 TotalHours,
44 TotalMinutes,
45 TotalSeconds,
46 TotalMilliseconds,
47 TotalMicroseconds,
48 TotalNanoseconds,
49 ToString(String),
50 CastTimeUnit(TimeUnit),
51 WithTimeUnit(TimeUnit),
52 #[cfg(feature = "timezones")]
53 ConvertTimeZone(TimeZone),
54 TimeStamp(TimeUnit),
55 Truncate,
56 #[cfg(feature = "offset_by")]
57 OffsetBy,
58 #[cfg(feature = "month_start")]
59 MonthStart,
60 #[cfg(feature = "month_end")]
61 MonthEnd,
62 #[cfg(feature = "timezones")]
63 BaseUtcOffset,
64 #[cfg(feature = "timezones")]
65 DSTOffset,
66 Round,
67 Replace,
68 #[cfg(feature = "timezones")]
69 ReplaceTimeZone(Option<TimeZone>, NonExistent),
70 Combine(TimeUnit),
71 DatetimeFunction {
72 time_unit: TimeUnit,
73 time_zone: Option<TimeZone>,
74 },
75}
76
77impl TemporalFunction {
78 pub(super) fn get_field(&self, mapper: FieldsMapper) -> PolarsResult<Field> {
79 use TemporalFunction::*;
80 match self {
81 Millennium | Century => mapper.with_dtype(DataType::Int8),
82 Year | IsoYear => mapper.with_dtype(DataType::Int32),
83 OrdinalDay => mapper.with_dtype(DataType::Int16),
84 Month | Quarter | Week | WeekDay | Day | Hour | Minute | Second => {
85 mapper.with_dtype(DataType::Int8)
86 },
87 Millisecond | Microsecond | Nanosecond => mapper.with_dtype(DataType::Int32),
88 TotalDays | TotalHours | TotalMinutes | TotalSeconds | TotalMilliseconds
89 | TotalMicroseconds | TotalNanoseconds => mapper.with_dtype(DataType::Int64),
90 ToString(_) => mapper.with_dtype(DataType::String),
91 WithTimeUnit(_) => mapper.with_same_dtype(),
92 CastTimeUnit(tu) => mapper.try_map_dtype(|dt| match dt {
93 DataType::Duration(_) => Ok(DataType::Duration(*tu)),
94 DataType::Datetime(_, tz) => Ok(DataType::Datetime(*tu, tz.clone())),
95 dtype => polars_bail!(ComputeError: "expected duration or datetime, got {}", dtype),
96 }),
97 #[cfg(feature = "timezones")]
98 ConvertTimeZone(tz) => mapper.try_map_dtype(|dt| match dt {
99 DataType::Datetime(tu, _) => Ok(DataType::Datetime(*tu, Some(tz.clone()))),
100 dtype => polars_bail!(ComputeError: "expected Datetime, got {}", dtype),
101 }),
102 TimeStamp(_) => mapper.with_dtype(DataType::Int64),
103 IsLeapYear => mapper.with_dtype(DataType::Boolean),
104 Time => mapper.with_dtype(DataType::Time),
105 Duration(tu) => mapper.with_dtype(DataType::Duration(*tu)),
106 Date => mapper.with_dtype(DataType::Date),
107 Datetime => mapper.try_map_dtype(|dt| match dt {
108 DataType::Datetime(tu, _) => Ok(DataType::Datetime(*tu, None)),
109 dtype => polars_bail!(ComputeError: "expected Datetime, got {}", dtype),
110 }),
111 Truncate => mapper.with_same_dtype(),
112 #[cfg(feature = "offset_by")]
113 OffsetBy => mapper.with_same_dtype(),
114 #[cfg(feature = "month_start")]
115 MonthStart => mapper.with_same_dtype(),
116 #[cfg(feature = "month_end")]
117 MonthEnd => mapper.with_same_dtype(),
118 #[cfg(feature = "timezones")]
119 BaseUtcOffset => mapper.with_dtype(DataType::Duration(TimeUnit::Milliseconds)),
120 #[cfg(feature = "timezones")]
121 DSTOffset => mapper.with_dtype(DataType::Duration(TimeUnit::Milliseconds)),
122 Round => mapper.with_same_dtype(),
123 Replace => mapper.with_same_dtype(),
124 #[cfg(feature = "timezones")]
125 ReplaceTimeZone(tz, _non_existent) => mapper.map_datetime_dtype_timezone(tz.as_ref()),
126 DatetimeFunction {
127 time_unit,
128 time_zone,
129 } => Ok(Field::new(
130 PlSmallStr::from_static("datetime"),
131 DataType::Datetime(*time_unit, time_zone.clone()),
132 )),
133 Combine(tu) => mapper.try_map_dtype(|dt| match dt {
134 DataType::Datetime(_, tz) => Ok(DataType::Datetime(*tu, tz.clone())),
135 DataType::Date => Ok(DataType::Datetime(*tu, None)),
136 dtype => {
137 polars_bail!(ComputeError: "expected Date or Datetime, got {}", dtype)
138 },
139 }),
140 }
141 }
142}
143
144impl Display for TemporalFunction {
145 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
146 use TemporalFunction::*;
147 let s = match self {
148 Millennium => "millennium",
149 Century => "century",
150 Year => "year",
151 IsLeapYear => "is_leap_year",
152 IsoYear => "iso_year",
153 Quarter => "quarter",
154 Month => "month",
155 Week => "week",
156 WeekDay => "weekday",
157 Day => "day",
158 OrdinalDay => "ordinal_day",
159 Time => "time",
160 Date => "date",
161 Datetime => "datetime",
162 Duration(_) => "duration",
163 Hour => "hour",
164 Minute => "minute",
165 Second => "second",
166 Millisecond => "millisecond",
167 Microsecond => "microsecond",
168 Nanosecond => "nanosecond",
169 TotalDays => "total_days",
170 TotalHours => "total_hours",
171 TotalMinutes => "total_minutes",
172 TotalSeconds => "total_seconds",
173 TotalMilliseconds => "total_milliseconds",
174 TotalMicroseconds => "total_microseconds",
175 TotalNanoseconds => "total_nanoseconds",
176 ToString(_) => "to_string",
177 #[cfg(feature = "timezones")]
178 ConvertTimeZone(_) => "convert_time_zone",
179 CastTimeUnit(_) => "cast_time_unit",
180 WithTimeUnit(_) => "with_time_unit",
181 TimeStamp(tu) => return write!(f, "dt.timestamp({tu})"),
182 Truncate => "truncate",
183 #[cfg(feature = "offset_by")]
184 OffsetBy => "offset_by",
185 #[cfg(feature = "month_start")]
186 MonthStart => "month_start",
187 #[cfg(feature = "month_end")]
188 MonthEnd => "month_end",
189 #[cfg(feature = "timezones")]
190 BaseUtcOffset => "base_utc_offset",
191 #[cfg(feature = "timezones")]
192 DSTOffset => "dst_offset",
193 Round => "round",
194 Replace => "replace",
195 #[cfg(feature = "timezones")]
196 ReplaceTimeZone(_, _) => "replace_time_zone",
197 DatetimeFunction { .. } => return write!(f, "dt.datetime"),
198 Combine(_) => "combine",
199 };
200 write!(f, "dt.{s}")
201 }
202}
203
204pub(super) fn millennium(s: &Column) -> PolarsResult<Column> {
205 s.as_materialized_series()
206 .millennium()
207 .map(|ca| ca.into_column())
208}
209pub(super) fn century(s: &Column) -> PolarsResult<Column> {
210 s.as_materialized_series()
211 .century()
212 .map(|ca| ca.into_column())
213}
214pub(super) fn year(s: &Column) -> PolarsResult<Column> {
215 s.as_materialized_series().year().map(|ca| ca.into_column())
216}
217pub(super) fn is_leap_year(s: &Column) -> PolarsResult<Column> {
218 s.as_materialized_series()
219 .is_leap_year()
220 .map(|ca| ca.into_column())
221}
222pub(super) fn iso_year(s: &Column) -> PolarsResult<Column> {
223 s.as_materialized_series()
224 .iso_year()
225 .map(|ca| ca.into_column())
226}
227pub(super) fn month(s: &Column) -> PolarsResult<Column> {
228 s.as_materialized_series()
229 .month()
230 .map(|ca| ca.into_column())
231}
232pub(super) fn quarter(s: &Column) -> PolarsResult<Column> {
233 s.as_materialized_series()
234 .quarter()
235 .map(|ca| ca.into_column())
236}
237pub(super) fn week(s: &Column) -> PolarsResult<Column> {
238 s.as_materialized_series().week().map(|ca| ca.into_column())
239}
240pub(super) fn weekday(s: &Column) -> PolarsResult<Column> {
241 s.as_materialized_series()
242 .weekday()
243 .map(|ca| ca.into_column())
244}
245pub(super) fn day(s: &Column) -> PolarsResult<Column> {
246 s.as_materialized_series().day().map(|ca| ca.into_column())
247}
248pub(super) fn ordinal_day(s: &Column) -> PolarsResult<Column> {
249 s.as_materialized_series()
250 .ordinal_day()
251 .map(|ca| ca.into_column())
252}
253pub(super) fn time(s: &Column) -> PolarsResult<Column> {
254 match s.dtype() {
255 #[cfg(feature = "timezones")]
256 DataType::Datetime(_, Some(_)) => polars_ops::prelude::replace_time_zone(
257 s.datetime().unwrap(),
258 None,
259 &StringChunked::from_iter(std::iter::once("raise")),
260 NonExistent::Raise,
261 )?
262 .cast(&DataType::Time)
263 .map(Column::from),
264 DataType::Datetime(_, _) => s
265 .datetime()
266 .unwrap()
267 .cast(&DataType::Time)
268 .map(Column::from),
269 DataType::Time => Ok(s.clone()),
270 dtype => polars_bail!(ComputeError: "expected Datetime or Time, got {}", dtype),
271 }
272}
273pub(super) fn date(s: &Column) -> PolarsResult<Column> {
274 match s.dtype() {
275 #[cfg(feature = "timezones")]
276 DataType::Datetime(_, Some(_)) => {
277 let mut out = {
278 polars_ops::chunked_array::replace_time_zone(
279 s.datetime().unwrap(),
280 None,
281 &StringChunked::from_iter(std::iter::once("raise")),
282 NonExistent::Raise,
283 )?
284 .cast(&DataType::Date)?
285 };
286 out.set_sorted_flag(s.is_sorted_flag());
292 Ok(out.into())
293 },
294 DataType::Datetime(_, _) => s
295 .datetime()
296 .unwrap()
297 .cast(&DataType::Date)
298 .map(Column::from),
299 DataType::Date => Ok(s.clone()),
300 dtype => polars_bail!(ComputeError: "expected Datetime or Date, got {}", dtype),
301 }
302}
303pub(super) fn datetime(s: &Column) -> PolarsResult<Column> {
304 match s.dtype() {
305 #[cfg(feature = "timezones")]
306 DataType::Datetime(tu, Some(_)) => polars_ops::chunked_array::replace_time_zone(
307 s.datetime().unwrap(),
308 None,
309 &StringChunked::from_iter(std::iter::once("raise")),
310 NonExistent::Raise,
311 )?
312 .cast(&DataType::Datetime(*tu, None))
313 .map(|x| x.into()),
314 DataType::Datetime(tu, _) => s
315 .datetime()
316 .unwrap()
317 .cast(&DataType::Datetime(*tu, None))
318 .map(Column::from),
319 dtype => polars_bail!(ComputeError: "expected Datetime, got {}", dtype),
320 }
321}
322pub(super) fn hour(s: &Column) -> PolarsResult<Column> {
323 s.as_materialized_series().hour().map(|ca| ca.into_column())
324}
325pub(super) fn minute(s: &Column) -> PolarsResult<Column> {
326 s.as_materialized_series()
327 .minute()
328 .map(|ca| ca.into_column())
329}
330pub(super) fn second(s: &Column) -> PolarsResult<Column> {
331 s.as_materialized_series()
332 .second()
333 .map(|ca| ca.into_column())
334}
335pub(super) fn millisecond(s: &Column) -> PolarsResult<Column> {
336 s.as_materialized_series()
337 .nanosecond()
338 .map(|ca| (ca.wrapping_trunc_div_scalar(1_000_000)).into_column())
339}
340pub(super) fn microsecond(s: &Column) -> PolarsResult<Column> {
341 s.as_materialized_series()
342 .nanosecond()
343 .map(|ca| (ca.wrapping_trunc_div_scalar(1_000)).into_column())
344}
345pub(super) fn nanosecond(s: &Column) -> PolarsResult<Column> {
346 s.as_materialized_series()
347 .nanosecond()
348 .map(|ca| ca.into_column())
349}
350#[cfg(feature = "dtype-duration")]
351pub(super) fn total_days(s: &Column) -> PolarsResult<Column> {
352 s.as_materialized_series()
353 .duration()
354 .map(|ca| ca.days().into_column())
355}
356#[cfg(feature = "dtype-duration")]
357pub(super) fn total_hours(s: &Column) -> PolarsResult<Column> {
358 s.as_materialized_series()
359 .duration()
360 .map(|ca| ca.hours().into_column())
361}
362#[cfg(feature = "dtype-duration")]
363pub(super) fn total_minutes(s: &Column) -> PolarsResult<Column> {
364 s.as_materialized_series()
365 .duration()
366 .map(|ca| ca.minutes().into_column())
367}
368#[cfg(feature = "dtype-duration")]
369pub(super) fn total_seconds(s: &Column) -> PolarsResult<Column> {
370 s.as_materialized_series()
371 .duration()
372 .map(|ca| ca.seconds().into_column())
373}
374#[cfg(feature = "dtype-duration")]
375pub(super) fn total_milliseconds(s: &Column) -> PolarsResult<Column> {
376 s.as_materialized_series()
377 .duration()
378 .map(|ca| ca.milliseconds().into_column())
379}
380#[cfg(feature = "dtype-duration")]
381pub(super) fn total_microseconds(s: &Column) -> PolarsResult<Column> {
382 s.as_materialized_series()
383 .duration()
384 .map(|ca| ca.microseconds().into_column())
385}
386#[cfg(feature = "dtype-duration")]
387pub(super) fn total_nanoseconds(s: &Column) -> PolarsResult<Column> {
388 s.as_materialized_series()
389 .duration()
390 .map(|ca| ca.nanoseconds().into_column())
391}
392pub(super) fn timestamp(s: &Column, tu: TimeUnit) -> PolarsResult<Column> {
393 s.as_materialized_series()
394 .timestamp(tu)
395 .map(|ca| ca.into_column())
396}
397pub(super) fn to_string(s: &Column, format: &str) -> PolarsResult<Column> {
398 TemporalMethods::to_string(s.as_materialized_series(), format).map(Column::from)
399}
400
401#[cfg(feature = "timezones")]
402pub(super) fn convert_time_zone(s: &Column, time_zone: &TimeZone) -> PolarsResult<Column> {
403 match s.dtype() {
404 DataType::Datetime(_, _) => {
405 let mut ca = s.datetime()?.clone();
406 validate_time_zone(time_zone)?;
407 ca.set_time_zone(time_zone.clone())?;
408 Ok(ca.into_column())
409 },
410 dtype => polars_bail!(ComputeError: "expected Datetime, got {}", dtype),
411 }
412}
413pub(super) fn with_time_unit(s: &Column, tu: TimeUnit) -> PolarsResult<Column> {
414 match s.dtype() {
415 DataType::Datetime(_, _) => {
416 let mut ca = s.datetime()?.clone();
417 ca.set_time_unit(tu);
418 Ok(ca.into_column())
419 },
420 #[cfg(feature = "dtype-duration")]
421 DataType::Duration(_) => {
422 let mut ca = s.as_materialized_series().duration()?.clone();
423 ca.set_time_unit(tu);
424 Ok(ca.into_column())
425 },
426 dt => polars_bail!(ComputeError: "dtype `{}` has no time unit", dt),
427 }
428}
429pub(super) fn cast_time_unit(s: &Column, tu: TimeUnit) -> PolarsResult<Column> {
430 match s.dtype() {
431 DataType::Datetime(_, _) => {
432 let ca = s.datetime()?;
433 Ok(ca.cast_time_unit(tu).into_column())
434 },
435 #[cfg(feature = "dtype-duration")]
436 DataType::Duration(_) => {
437 let ca = s.as_materialized_series().duration()?;
438 Ok(ca.cast_time_unit(tu).into_column())
439 },
440 dt => polars_bail!(ComputeError: "dtype `{}` has no time unit", dt),
441 }
442}
443
444pub(super) fn truncate(s: &[Column]) -> PolarsResult<Column> {
445 let time_series = &s[0];
446 let every = s[1].str()?;
447
448 let mut out = match time_series.dtype() {
449 DataType::Datetime(_, tz) => match tz {
450 #[cfg(feature = "timezones")]
451 Some(tz) => time_series
452 .datetime()?
453 .truncate(tz.parse::<Tz>().ok().as_ref(), every)?
454 .into_column(),
455 _ => time_series.datetime()?.truncate(None, every)?.into_column(),
456 },
457 DataType::Date => time_series.date()?.truncate(None, every)?.into_column(),
458 dt => polars_bail!(opq = round, got = dt, expected = "date/datetime"),
459 };
460 out.set_sorted_flag(time_series.is_sorted_flag());
461 Ok(out)
462}
463
464#[cfg(feature = "offset_by")]
465pub(super) fn offset_by(s: &[Column]) -> PolarsResult<Column> {
466 impl_offset_by(s[0].as_materialized_series(), s[1].as_materialized_series()).map(Column::from)
467}
468
469#[cfg(feature = "month_start")]
470pub(super) fn month_start(s: &Column) -> PolarsResult<Column> {
471 Ok(match s.dtype() {
472 DataType::Datetime(_, tz) => match tz {
473 #[cfg(feature = "timezones")]
474 Some(tz) => s
475 .datetime()
476 .unwrap()
477 .month_start(tz.parse::<Tz>().ok().as_ref())?
478 .into_column(),
479 _ => s.datetime().unwrap().month_start(None)?.into_column(),
480 },
481 DataType::Date => s.date().unwrap().month_start(None)?.into_column(),
482 dt => polars_bail!(opq = month_start, got = dt, expected = "date/datetime"),
483 })
484}
485
486#[cfg(feature = "month_end")]
487pub(super) fn month_end(s: &Column) -> PolarsResult<Column> {
488 Ok(match s.dtype() {
489 DataType::Datetime(_, tz) => match tz {
490 #[cfg(feature = "timezones")]
491 Some(tz) => s
492 .datetime()
493 .unwrap()
494 .month_end(tz.parse::<Tz>().ok().as_ref())?
495 .into_column(),
496 _ => s.datetime().unwrap().month_end(None)?.into_column(),
497 },
498 DataType::Date => s.date().unwrap().month_end(None)?.into_column(),
499 dt => polars_bail!(opq = month_end, got = dt, expected = "date/datetime"),
500 })
501}
502
503#[cfg(feature = "timezones")]
504pub(super) fn base_utc_offset(s: &Column) -> PolarsResult<Column> {
505 match s.dtype() {
506 DataType::Datetime(time_unit, Some(tz)) => {
507 let tz = tz
508 .parse::<Tz>()
509 .expect("Time zone has already been validated");
510 Ok(base_utc_offset_fn(s.datetime().unwrap(), time_unit, &tz).into_column())
511 },
512 dt => polars_bail!(
513 opq = base_utc_offset,
514 got = dt,
515 expected = "time-zone-aware datetime"
516 ),
517 }
518}
519#[cfg(feature = "timezones")]
520pub(super) fn dst_offset(s: &Column) -> PolarsResult<Column> {
521 match s.dtype() {
522 DataType::Datetime(time_unit, Some(tz)) => {
523 let tz = tz
524 .parse::<Tz>()
525 .expect("Time zone has already been validated");
526 Ok(dst_offset_fn(s.datetime().unwrap(), time_unit, &tz).into_column())
527 },
528 dt => polars_bail!(
529 opq = dst_offset,
530 got = dt,
531 expected = "time-zone-aware datetime"
532 ),
533 }
534}
535
536pub(super) fn round(s: &[Column]) -> PolarsResult<Column> {
537 let time_series = &s[0];
538 let every = s[1].str()?;
539
540 Ok(match time_series.dtype() {
541 DataType::Datetime(_, tz) => match tz {
542 #[cfg(feature = "timezones")]
543 Some(tz) => time_series
544 .datetime()
545 .unwrap()
546 .round(every, tz.parse::<Tz>().ok().as_ref())?
547 .into_column(),
548 _ => time_series
549 .datetime()
550 .unwrap()
551 .round(every, None)?
552 .into_column(),
553 },
554 DataType::Date => time_series
555 .date()
556 .unwrap()
557 .round(every, None)?
558 .into_column(),
559 dt => polars_bail!(opq = round, got = dt, expected = "date/datetime"),
560 })
561}
562
563pub(super) fn replace(s: &[Column]) -> PolarsResult<Column> {
564 let time_series = &s[0];
565 let s_year = &s[1].strict_cast(&DataType::Int32)?;
566 let s_month = &s[2].strict_cast(&DataType::Int8)?;
567 let s_day = &s[3].strict_cast(&DataType::Int8)?;
568 let year = s_year.i32()?;
569 let month = s_month.i8()?;
570 let day = s_day.i8()?;
571
572 match time_series.dtype() {
573 DataType::Datetime(_, _) => {
574 let s_hour = &s[4].strict_cast(&DataType::Int8)?;
575 let s_minute = &s[5].strict_cast(&DataType::Int8)?;
576 let s_second = &s[6].strict_cast(&DataType::Int8)?;
577 let s_microsecond = &s[7].strict_cast(&DataType::Int32)?;
578 let hour = s_hour.i8()?;
579 let minute = s_minute.i8()?;
580 let second = s_second.i8()?;
581 let nanosecond = &(s_microsecond.i32()? * 1_000);
582 let s_ambiguous = &s[8].strict_cast(&DataType::String)?;
583 let ambiguous = s_ambiguous.str()?;
584
585 let out = replace_datetime(
586 time_series.datetime().unwrap(),
587 year,
588 month,
589 day,
590 hour,
591 minute,
592 second,
593 nanosecond,
594 ambiguous,
595 );
596 out.map(|s| s.into_column())
597 },
598 DataType::Date => {
599 let out = replace_date(time_series.date().unwrap(), year, month, day);
600 out.map(|s| s.into_column())
601 },
602 dt => polars_bail!(opq = round, got = dt, expected = "date/datetime"),
603 }
604}