polars_plan/dsl/function_expr/
datetime.rs

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            // `replace_time_zone` may unset sorted flag. But, we're only taking the date
287            // part of the result, so we can safely preserve the sorted flag here. We may
288            // need to make an exception if a time zone introduces a change which involves
289            // "going back in time" and repeating a day, but we're not aware of that ever
290            // having happened.
291            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}