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