Path: blob/main/crates/polars-time/src/chunkedarray/datetime.rs
6939 views
use arrow::array::{Array, PrimitiveArray};1use arrow::compute::temporal;2use polars_compute::cast::{CastOptionsImpl, cast};3use polars_core::prelude::*;4#[cfg(feature = "timezones")]5use polars_ops::chunked_array::datetime::replace_time_zone;67use super::*;89fn cast_and_apply<10F: Fn(&dyn Array) -> PolarsResult<PrimitiveArray<T::Native>>,11T: PolarsNumericType,12>(13ca: &DatetimeChunked,14func: F,15) -> ChunkedArray<T> {16let dtype = ca.dtype().to_arrow(CompatLevel::newest());17let chunks = ca.physical().downcast_iter().map(|arr| {18let arr = cast(19arr,20&dtype,21CastOptionsImpl {22wrapped: true,23partial: false,24},25)26.unwrap();27func(&*arr).unwrap()28});29ChunkedArray::from_chunk_iter(ca.name().clone(), chunks)30}3132pub trait DatetimeMethods: AsDatetime {33/// Extract month from underlying NaiveDateTime representation.34/// Returns the year number in the calendar date.35fn year(&self) -> Int32Chunked {36cast_and_apply(self.as_datetime(), temporal::year)37}3839/// Extract year from underlying NaiveDate representation.40/// Returns whether the year is a leap year.41fn is_leap_year(&self) -> BooleanChunked {42let ca = self.as_datetime();43let f = match ca.time_unit() {44TimeUnit::Nanoseconds => datetime_to_is_leap_year_ns,45TimeUnit::Microseconds => datetime_to_is_leap_year_us,46TimeUnit::Milliseconds => datetime_to_is_leap_year_ms,47};48let ca_local = match ca.dtype() {49#[cfg(feature = "timezones")]50DataType::Datetime(_, Some(_)) => &polars_ops::chunked_array::replace_time_zone(51ca,52None,53&StringChunked::new("".into(), ["raise"]),54NonExistent::Raise,55)56.expect("Removing time zone is infallible"),57_ => ca,58};59ca_local.physical().apply_kernel_cast::<BooleanType>(&f)60}6162fn iso_year(&self) -> Int32Chunked {63let ca = self.as_datetime();64let f = match ca.time_unit() {65TimeUnit::Nanoseconds => datetime_to_iso_year_ns,66TimeUnit::Microseconds => datetime_to_iso_year_us,67TimeUnit::Milliseconds => datetime_to_iso_year_ms,68};69let ca_local = match ca.dtype() {70#[cfg(feature = "timezones")]71DataType::Datetime(_, Some(_)) => &polars_ops::chunked_array::replace_time_zone(72ca,73None,74&StringChunked::new("".into(), ["raise"]),75NonExistent::Raise,76)77.expect("Removing time zone is infallible"),78_ => ca,79};80ca_local.physical().apply_kernel_cast::<Int32Type>(&f)81}8283/// Extract quarter from underlying NaiveDateTime representation.84/// Quarters range from 1 to 4.85fn quarter(&self) -> Int8Chunked {86let months = self.month();87months_to_quarters(months)88}8990/// Extract month from underlying NaiveDateTime representation.91/// Returns the month number starting from 1.92///93/// The return value ranges from 1 to 12.94fn month(&self) -> Int8Chunked {95cast_and_apply(self.as_datetime(), temporal::month)96}9798/// Returns the number of days in the month of the underlying NaiveDateTime99/// representation.100fn days_in_month(&self) -> Int8Chunked {101let ca = self.as_datetime();102let f = match ca.time_unit() {103TimeUnit::Nanoseconds => datetime_to_days_in_month_ns,104TimeUnit::Microseconds => datetime_to_days_in_month_us,105TimeUnit::Milliseconds => datetime_to_days_in_month_ms,106};107let ca_local = match ca.dtype() {108#[cfg(feature = "timezones")]109DataType::Datetime(_, Some(_)) => &polars_ops::chunked_array::replace_time_zone(110ca,111None,112&StringChunked::new("".into(), ["raise"]),113NonExistent::Raise,114)115.expect("Removing time zone is infallible"),116_ => ca,117};118ca_local.physical().apply_kernel_cast::<Int8Type>(&f)119}120121/// Extract ISO weekday from underlying NaiveDateTime representation.122/// Returns the weekday number where monday = 1 and sunday = 7123fn weekday(&self) -> Int8Chunked {124cast_and_apply(self.as_datetime(), temporal::weekday)125}126127/// Returns the ISO week number starting from 1.128/// The return value ranges from 1 to 53. (The last week of year differs by years.)129fn week(&self) -> Int8Chunked {130cast_and_apply(self.as_datetime(), temporal::iso_week)131}132133/// Extract day from underlying NaiveDateTime representation.134/// Returns the day of month starting from 1.135///136/// The return value ranges from 1 to 31. (The last day of month differs by months.)137fn day(&self) -> Int8Chunked {138cast_and_apply(self.as_datetime(), temporal::day)139}140141/// Extract hour from underlying NaiveDateTime representation.142/// Returns the hour number from 0 to 23.143fn hour(&self) -> Int8Chunked {144cast_and_apply(self.as_datetime(), temporal::hour)145}146147/// Extract minute from underlying NaiveDateTime representation.148/// Returns the minute number from 0 to 59.149fn minute(&self) -> Int8Chunked {150cast_and_apply(self.as_datetime(), temporal::minute)151}152153/// Extract second from underlying NaiveDateTime representation.154/// Returns the second number from 0 to 59.155fn second(&self) -> Int8Chunked {156cast_and_apply(self.as_datetime(), temporal::second)157}158159/// Extract second from underlying NaiveDateTime representation.160/// Returns the number of nanoseconds since the whole non-leap second.161/// The range from 1,000,000,000 to 1,999,999,999 represents the leap second.162fn nanosecond(&self) -> Int32Chunked {163cast_and_apply(self.as_datetime(), temporal::nanosecond)164}165166/// Returns the day of year starting from 1.167///168/// The return value ranges from 1 to 366. (The last day of year differs by years.)169fn ordinal(&self) -> Int16Chunked {170let ca = self.as_datetime();171let f = match ca.time_unit() {172TimeUnit::Nanoseconds => datetime_to_ordinal_ns,173TimeUnit::Microseconds => datetime_to_ordinal_us,174TimeUnit::Milliseconds => datetime_to_ordinal_ms,175};176let ca_local = match ca.dtype() {177#[cfg(feature = "timezones")]178DataType::Datetime(_, Some(_)) => &polars_ops::chunked_array::replace_time_zone(179ca,180None,181&StringChunked::new("".into(), ["raise"]),182NonExistent::Raise,183)184.expect("Removing time zone is infallible"),185_ => ca,186};187ca_local.physical().apply_kernel_cast::<Int16Type>(&f)188}189190fn parse_from_str_slice(191name: PlSmallStr,192v: &[&str],193fmt: &str,194tu: TimeUnit,195) -> DatetimeChunked {196let func = match tu {197TimeUnit::Nanoseconds => datetime_to_timestamp_ns,198TimeUnit::Microseconds => datetime_to_timestamp_us,199TimeUnit::Milliseconds => datetime_to_timestamp_ms,200};201202Int64Chunked::from_iter_options(203name,204v.iter()205.map(|s| NaiveDateTime::parse_from_str(s, fmt).ok().map(func)),206)207.into_datetime(tu, None)208}209210/// Construct a datetime ChunkedArray from individual time components.211#[allow(clippy::too_many_arguments)]212fn new_from_parts(213year: &Int32Chunked,214month: &Int8Chunked,215day: &Int8Chunked,216hour: &Int8Chunked,217minute: &Int8Chunked,218second: &Int8Chunked,219nanosecond: &Int32Chunked,220ambiguous: &StringChunked,221time_unit: &TimeUnit,222time_zone: Option<TimeZone>,223name: PlSmallStr,224) -> PolarsResult<DatetimeChunked> {225let ca: Int64Chunked = year226.into_iter()227.zip(month)228.zip(day)229.zip(hour)230.zip(minute)231.zip(second)232.zip(nanosecond)233.map(|((((((y, m), d), h), mnt), s), ns)| {234if let (Some(y), Some(m), Some(d), Some(h), Some(mnt), Some(s), Some(ns)) =235(y, m, d, h, mnt, s, ns)236{237NaiveDate::from_ymd_opt(y, m as u32, d as u32).map_or_else(238// We have an invalid date.239|| Err(polars_err!(ComputeError: format!("Invalid date components ({}, {}, {}) supplied", y, m, d))),240// We have a valid date.241|date| {242date.and_hms_nano_opt(h as u32, mnt as u32, s as u32, ns as u32)243.map_or_else(244// We have invalid time components for the specified date.245|| Err(polars_err!(ComputeError: format!("Invalid time components ({}, {}, {}, {}) supplied", h, mnt, s, ns))),246// We have a valid time.247|ndt| {248let t = ndt.and_utc();249Ok(Some(match time_unit {250TimeUnit::Milliseconds => t.timestamp_millis(),251TimeUnit::Microseconds => t.timestamp_micros(),252TimeUnit::Nanoseconds => {253t.timestamp_nanos_opt().unwrap()254},255}))256},257)258},259)260} else {261Ok(None)262}263})264.try_collect_ca_with_dtype(name, DataType::Int64)?;265266let ca = match time_zone {267#[cfg(feature = "timezones")]268Some(_) => {269let mut ca = ca.into_datetime(*time_unit, None);270ca = replace_time_zone(&ca, time_zone.as_ref(), ambiguous, NonExistent::Raise)?;271ca272},273_ => {274polars_ensure!(275time_zone.is_none(),276ComputeError: "cannot make use of the `time_zone` argument without the 'timezones' feature enabled."277);278ca.into_datetime(*time_unit, None)279},280};281Ok(ca)282}283}284285pub trait AsDatetime {286fn as_datetime(&self) -> &DatetimeChunked;287}288289impl AsDatetime for DatetimeChunked {290fn as_datetime(&self) -> &DatetimeChunked {291self292}293}294295impl DatetimeMethods for DatetimeChunked {}296297#[cfg(test)]298mod test {299use super::*;300301#[test]302fn from_datetime() {303let datetimes: Vec<_> = [304"1988-08-25 00:00:16",305"2015-09-05 23:56:04",306"2012-12-21 00:00:00",307]308.iter()309.map(|s| NaiveDateTime::parse_from_str(s, "%Y-%m-%d %H:%M:%S").unwrap())310.collect();311312// NOTE: the values are checked and correct.313let dt = DatetimeChunked::from_naive_datetime(314"name".into(),315datetimes.iter().copied(),316TimeUnit::Nanoseconds,317);318assert_eq!(319[320588_470_416_000_000_000,3211_441_497_364_000_000_000,3221_356_048_000_000_000_000323],324dt.physical().cont_slice().unwrap()325);326}327}328329330