1use arrow::array::PrimitiveArray;
2use chrono::format::ParseErrorKind;
3use chrono::{DateTime, NaiveDate, NaiveDateTime, NaiveTime};
4use polars_core::prelude::*;
5
6use super::patterns::{self, Pattern};
7#[cfg(feature = "dtype-date")]
8use crate::chunkedarray::date::naive_date_to_date;
9use crate::prelude::string::strptime::StrpTimeState;
10
11polars_utils::regex_cache::cached_regex! {
12 static DATETIME_DMY_RE = r#"(?x)
13 ^
14 ['"]? # optional quotes
15 (?:\d{1,2}) # day
16 [-/\.] # separator
17 (?P<month>[01]?\d{1}) # month
18 [-/\.] # separator
19 (?:\d{4,}) # year
20 (?:
21 [T\ ] # separator
22 (?:\d{1,2}) # hour
23 :? # separator
24 (?:\d{1,2}) # minute
25 (?:
26 :? # separator
27 (?:\d{1,2}) # second
28 (?:
29 \.(?:\d{1,9}) # subsecond
30 )?
31 )?
32 )?
33 ['"]? # optional quotes
34 $
35 "#;
36
37 static DATETIME_YMD_RE = r#"(?x)
38 ^
39 ['"]? # optional quotes
40 (?:\d{4,}) # year
41 [-/\.]? # separator
42 (?P<month>[01]?\d{1}) # month
43 [-/\.]? # separator
44 (?:\d{1,2}) # day
45 (?:
46 [T\ ] # separator
47 (?:\d{1,2}) # hour
48 :? # separator
49 (?:\d{1,2}) # minute
50 (?:
51 :? # separator
52 (?:\d{1,2}) # seconds
53 (?:
54 \.(?:\d{1,9}) # subsecond
55 )?
56 )?
57 )?
58 ['"]? # optional quotes
59 $
60 "#;
61
62 static DATETIME_YMDZ_RE = r#"(?x)
63 ^
64 ['"]? # optional quotes
65 (?:\d{4,}) # year
66 [-/\.]? # separator
67 (?P<month>[01]?\d{1}) # month
68 [-/\.]? # separator
69 (?:\d{1,2}) # year
70 [T\ ] # separator
71 (?:\d{2}) # hour
72 :? # separator
73 (?:\d{2}) # minute
74 (?:
75 :? # separator
76 (?:\d{2}) # second
77 (?:
78 \.(?:\d{1,9}) # subsecond
79 )?
80 )?
81 (?:
82 # offset (e.g. +01:00, +0100, or +01)
83 [+-](?:\d{2})
84 (?::?\d{2})?
85 # or Zulu suffix
86 |Z
87 )
88 ['"]? # optional quotes
89 $
90 "#;
91}
92
93impl Pattern {
94 pub fn is_inferable(&self, val: &str) -> bool {
95 match self {
96 Pattern::DateDMY => true, Pattern::DateYMD => true, Pattern::Time => true,
99 Pattern::DatetimeDMY => match DATETIME_DMY_RE.captures(val) {
100 Some(search) => (1..=12).contains(
101 &search
102 .name("month")
103 .unwrap()
104 .as_str()
105 .parse::<u8>()
106 .unwrap(),
107 ),
108 None => false,
109 },
110 Pattern::DatetimeYMD => match DATETIME_YMD_RE.captures(val) {
111 Some(search) => (1..=12).contains(
112 &search
113 .name("month")
114 .unwrap()
115 .as_str()
116 .parse::<u8>()
117 .unwrap(),
118 ),
119 None => false,
120 },
121 Pattern::DatetimeYMDZ => match DATETIME_YMDZ_RE.captures(val) {
122 Some(search) => (1..=12).contains(
123 &search
124 .name("month")
125 .unwrap()
126 .as_str()
127 .parse::<u8>()
128 .unwrap(),
129 ),
130 None => false,
131 },
132 }
133 }
134}
135
136pub trait StrpTimeParser<T> {
137 fn parse_bytes(&mut self, val: &[u8], time_unit: Option<TimeUnit>) -> Option<T>;
138}
139
140#[cfg(feature = "dtype-datetime")]
141impl StrpTimeParser<i64> for DatetimeInfer<Int64Type> {
142 fn parse_bytes(&mut self, val: &[u8], time_unit: Option<TimeUnit>) -> Option<i64> {
143 let transform = match time_unit {
144 Some(TimeUnit::Nanoseconds) => datetime_to_timestamp_ns,
145 Some(TimeUnit::Microseconds) => datetime_to_timestamp_us,
146 Some(TimeUnit::Milliseconds) => datetime_to_timestamp_ms,
147 _ => unreachable!(), };
149 self.transform_bytes
150 .parse(val, self.latest_fmt.as_bytes())
151 .map(transform)
152 .or_else(|| {
153 for fmt in self.patterns {
156 if let Some(parsed) = self
157 .transform_bytes
158 .parse(val, fmt.as_bytes())
159 .map(datetime_to_timestamp_us)
160 {
161 self.latest_fmt = fmt;
162 return Some(parsed);
163 }
164 }
165 None
166 })
167 }
168}
169
170#[cfg(feature = "dtype-date")]
171impl StrpTimeParser<i32> for DatetimeInfer<Int32Type> {
172 fn parse_bytes(&mut self, val: &[u8], _time_unit: Option<TimeUnit>) -> Option<i32> {
173 self.transform_bytes
174 .parse(val, self.latest_fmt.as_bytes())
175 .map(|ndt| naive_date_to_date(ndt.date()))
176 .or_else(|| {
177 for fmt in self.patterns {
180 if let Some(parsed) = self
181 .transform_bytes
182 .parse(val, fmt.as_bytes())
183 .map(|ndt| naive_date_to_date(ndt.date()))
184 {
185 self.latest_fmt = fmt;
186 return Some(parsed);
187 }
188 }
189 None
190 })
191 }
192}
193
194#[derive(Clone)]
195pub struct DatetimeInfer<T: PolarsNumericType> {
196 pub pattern: Pattern,
197 patterns: &'static [&'static str],
198 latest_fmt: &'static str,
199 transform: fn(&str, &str) -> Option<T::Native>,
200 transform_bytes: StrpTimeState,
201 pub logical_type: DataType,
202}
203
204pub trait TryFromWithUnit<T>: Sized {
205 type Error;
206 fn try_from_with_unit(pattern: T, time_unit: Option<TimeUnit>) -> PolarsResult<Self>;
207}
208
209#[cfg(feature = "dtype-datetime")]
210impl TryFromWithUnit<Pattern> for DatetimeInfer<Int64Type> {
211 type Error = PolarsError;
212
213 fn try_from_with_unit(value: Pattern, time_unit: Option<TimeUnit>) -> PolarsResult<Self> {
214 let time_unit = time_unit.expect("time_unit must be provided for datetime");
215
216 let transform = match (time_unit, value) {
217 (TimeUnit::Milliseconds, Pattern::DatetimeYMDZ) => transform_tzaware_datetime_ms,
218 (TimeUnit::Milliseconds, _) => transform_datetime_ms,
219 (TimeUnit::Microseconds, Pattern::DatetimeYMDZ) => transform_tzaware_datetime_us,
220 (TimeUnit::Microseconds, _) => transform_datetime_us,
221 (TimeUnit::Nanoseconds, Pattern::DatetimeYMDZ) => transform_tzaware_datetime_ns,
222 (TimeUnit::Nanoseconds, _) => transform_datetime_ns,
223 };
224 let (pattern, patterns) = match value {
225 Pattern::DatetimeDMY | Pattern::DateDMY => {
226 (Pattern::DatetimeDMY, patterns::DATETIME_D_M_Y)
227 },
228 Pattern::DatetimeYMD | Pattern::DateYMD => {
229 (Pattern::DatetimeYMD, patterns::DATETIME_Y_M_D)
230 },
231 Pattern::DatetimeYMDZ => (Pattern::DatetimeYMDZ, patterns::DATETIME_Y_M_D_Z),
232 Pattern::Time => (Pattern::Time, patterns::TIME_H_M_S),
233 };
234
235 Ok(DatetimeInfer {
236 pattern,
237 patterns,
238 latest_fmt: patterns[0],
239 transform,
240 transform_bytes: StrpTimeState::default(),
241 logical_type: DataType::Datetime(time_unit, None),
242 })
243 }
244}
245
246#[cfg(feature = "dtype-date")]
247impl TryFromWithUnit<Pattern> for DatetimeInfer<Int32Type> {
248 type Error = PolarsError;
249
250 fn try_from_with_unit(value: Pattern, _time_unit: Option<TimeUnit>) -> PolarsResult<Self> {
251 match value {
252 Pattern::DateDMY => Ok(DatetimeInfer {
253 pattern: Pattern::DateDMY,
254 patterns: patterns::DATE_D_M_Y,
255 latest_fmt: patterns::DATE_D_M_Y[0],
256 transform: transform_date,
257 transform_bytes: StrpTimeState::default(),
258 logical_type: DataType::Date,
259 }),
260 Pattern::DateYMD => Ok(DatetimeInfer {
261 pattern: Pattern::DateYMD,
262 patterns: patterns::DATE_Y_M_D,
263 latest_fmt: patterns::DATE_Y_M_D[0],
264 transform: transform_date,
265 transform_bytes: StrpTimeState::default(),
266 logical_type: DataType::Date,
267 }),
268 _ => polars_bail!(ComputeError: "could not convert pattern"),
269 }
270 }
271}
272
273impl<T: PolarsNumericType> DatetimeInfer<T> {
274 pub fn parse(&mut self, val: &str) -> Option<T::Native> {
275 match (self.transform)(val, self.latest_fmt) {
276 Some(parsed) => Some(parsed),
277 None => {
279 if !self.pattern.is_inferable(val) {
280 return None;
281 }
282 for fmt in self.patterns {
283 if let Some(parsed) = (self.transform)(val, fmt) {
284 self.latest_fmt = fmt;
285 return Some(parsed);
286 }
287 }
288 None
289 },
290 }
291 }
292}
293
294impl<T: PolarsNumericType> DatetimeInfer<T> {
295 pub fn coerce_string(&mut self, ca: &StringChunked) -> Series {
296 let chunks = ca.downcast_iter().map(|array| {
297 let iter = array
298 .into_iter()
299 .map(|opt_val| opt_val.and_then(|val| self.parse(val)));
300 PrimitiveArray::from_trusted_len_iter(iter)
301 });
302 ChunkedArray::<T>::from_chunk_iter(ca.name().clone(), chunks)
303 .into_series()
304 .cast(&self.logical_type)
305 .unwrap()
306 .with_name(ca.name().clone())
307 }
308}
309
310#[cfg(feature = "dtype-date")]
311fn transform_date(val: &str, fmt: &str) -> Option<i32> {
312 NaiveDate::parse_from_str(val, fmt)
313 .ok()
314 .map(naive_date_to_date)
315}
316
317pub(crate) fn parse_datetime(val: &str, fmt: &str) -> Option<NaiveDateTime> {
318 NaiveDateTime::parse_from_str(val, fmt)
319 .or_else(|parse_error| match parse_error.kind() {
320 ParseErrorKind::NotEnough => {
321 NaiveDate::parse_from_str(val, fmt).map(|nd| nd.and_hms_opt(0, 0, 0).unwrap())
322 },
323 _ => Err(parse_error),
324 })
325 .ok()
326}
327
328pub(crate) fn parse_datetime_and_remainder<'a>(
329 val: &'a str,
330 fmt: &str,
331) -> Option<(NaiveDateTime, &'a str)> {
332 NaiveDateTime::parse_and_remainder(val, fmt)
333 .or_else(|parse_error| match parse_error.kind() {
334 ParseErrorKind::NotEnough => NaiveDate::parse_and_remainder(val, fmt)
335 .map(|(nd, r)| (nd.and_hms_opt(0, 0, 0).unwrap(), r)),
336 _ => Err(parse_error),
337 })
338 .ok()
339}
340
341#[cfg(feature = "dtype-datetime")]
342pub(crate) fn transform_datetime_ns(val: &str, fmt: &str) -> Option<i64> {
343 parse_datetime(val, fmt).map(datetime_to_timestamp_ns)
344}
345
346#[cfg(feature = "dtype-datetime")]
347pub(crate) fn transform_datetime_us(val: &str, fmt: &str) -> Option<i64> {
348 parse_datetime(val, fmt).map(datetime_to_timestamp_us)
349}
350
351#[cfg(feature = "dtype-datetime")]
352pub(crate) fn transform_datetime_ms(val: &str, fmt: &str) -> Option<i64> {
353 parse_datetime(val, fmt).map(datetime_to_timestamp_ms)
354}
355
356fn transform_tzaware_datetime_ns(val: &str, fmt: &str) -> Option<i64> {
357 let dt = DateTime::parse_from_str(val, fmt);
358 dt.ok().map(|dt| datetime_to_timestamp_ns(dt.naive_utc()))
359}
360
361fn transform_tzaware_datetime_us(val: &str, fmt: &str) -> Option<i64> {
362 let dt = DateTime::parse_from_str(val, fmt);
363 dt.ok().map(|dt| datetime_to_timestamp_us(dt.naive_utc()))
364}
365
366fn transform_tzaware_datetime_ms(val: &str, fmt: &str) -> Option<i64> {
367 let dt = DateTime::parse_from_str(val, fmt);
368 dt.ok().map(|dt| datetime_to_timestamp_ms(dt.naive_utc()))
369}
370
371pub fn infer_pattern_single(val: &str) -> Option<Pattern> {
372 infer_pattern_date_single(val)
374 .or_else(|| infer_pattern_time_single(val))
375 .or_else(|| infer_pattern_datetime_single(val))
376}
377
378pub fn infer_pattern_datetime_single(val: &str) -> Option<Pattern> {
379 if patterns::DATETIME_D_M_Y.iter().any(|fmt| {
380 NaiveDateTime::parse_from_str(val, fmt).is_ok()
381 || NaiveDate::parse_from_str(val, fmt).is_ok()
382 }) {
383 Some(Pattern::DatetimeDMY)
384 } else if patterns::DATETIME_Y_M_D.iter().any(|fmt| {
385 NaiveDateTime::parse_from_str(val, fmt).is_ok()
386 || NaiveDate::parse_from_str(val, fmt).is_ok()
387 }) {
388 Some(Pattern::DatetimeYMD)
389 } else if patterns::DATETIME_Y_M_D_Z
390 .iter()
391 .any(|fmt| NaiveDateTime::parse_from_str(val, fmt).is_ok())
392 {
393 Some(Pattern::DatetimeYMDZ)
394 } else {
395 None
396 }
397}
398
399pub fn infer_pattern_date_single(val: &str) -> Option<Pattern> {
400 if patterns::DATE_D_M_Y
401 .iter()
402 .any(|fmt| NaiveDate::parse_from_str(val, fmt).is_ok())
403 {
404 Some(Pattern::DateDMY)
405 } else if patterns::DATE_Y_M_D
406 .iter()
407 .any(|fmt| NaiveDate::parse_from_str(val, fmt).is_ok())
408 {
409 Some(Pattern::DateYMD)
410 } else {
411 None
412 }
413}
414
415pub fn infer_pattern_time_single(val: &str) -> Option<Pattern> {
416 sniff_time_fmt(val).is_some().then_some(Pattern::Time)
417}
418
419pub fn sniff_time_fmt(val: &str) -> Option<&'static str> {
421 patterns::TIME_H_M_S
422 .iter()
423 .copied()
424 .find(|fmt| NaiveTime::parse_from_str(val, fmt).is_ok())
425}
426
427#[cfg(feature = "dtype-datetime")]
428pub fn to_datetime_with_inferred_tz(
429 ca: &StringChunked,
430 tu: TimeUnit,
431 strict: bool,
432 exact: bool,
433 ambiguous: &StringChunked,
434) -> PolarsResult<DatetimeChunked> {
435 use super::StringMethods;
436
437 let out = if exact {
438 to_datetime(ca, tu, None, ambiguous, false)
439 } else {
440 ca.as_datetime_not_exact(None, tu, false, None, ambiguous, false)
441 }?;
442
443 if strict && ca.null_count() != out.null_count() {
444 polars_core::utils::handle_casting_failures(
445 &ca.clone().into_series(),
446 &out.clone().into_series(),
447 )?;
448 }
449
450 Ok(out)
451}
452
453#[cfg(feature = "dtype-datetime")]
454pub fn to_datetime(
455 ca: &StringChunked,
456 tu: TimeUnit,
457 tz: Option<&TimeZone>,
458 ambiguous: &StringChunked,
459 ensure_matching_time_zone: bool,
461) -> PolarsResult<DatetimeChunked> {
462 match ca.first_non_null() {
463 None => {
464 Ok(Int64Chunked::full_null(ca.name().clone(), ca.len()).into_datetime(tu, tz.cloned()))
465 },
466 Some(idx) => {
467 let subset = ca.slice(idx as i64, ca.len());
468 let pattern = subset
469 .iter()
470 .find_map(|opt_val| opt_val.and_then(infer_pattern_datetime_single))
471 .ok_or_else(|| polars_err!(parse_fmt_idk = "date"))?;
472 let mut infer = DatetimeInfer::<Int64Type>::try_from_with_unit(pattern, Some(tu))?;
473 #[cfg(feature = "timezones")]
474 if matches!(pattern, Pattern::DatetimeYMDZ) {
475 polars_ensure!(
476 !ensure_matching_time_zone || tz.is_some(),
477 to_datetime_tz_mismatch
478 );
479 }
480 coerce_string_to_datetime(&mut infer, ca, tz, ambiguous)
481 },
482 }
483}
484#[cfg(feature = "dtype-date")]
486pub fn coerce_string_to_date(
487 infer: &mut DatetimeInfer<Int32Type>,
488 ca: &StringChunked,
489) -> PolarsResult<DateChunked> {
490 infer.coerce_string(ca).date().cloned()
491}
492
493#[cfg(feature = "dtype-datetime")]
496pub fn coerce_string_to_datetime(
497 infer: &mut DatetimeInfer<Int64Type>,
498 ca: &StringChunked,
499 tz: Option<&TimeZone>,
500 ambiguous: &StringChunked,
501) -> PolarsResult<DatetimeChunked> {
502 let DataType::Datetime(tu, _) = &infer.logical_type else {
503 unreachable!()
504 };
505 let tu = *tu;
506 match infer.pattern {
507 #[cfg(feature = "timezones")]
508 Pattern::DatetimeYMDZ => infer.coerce_string(ca).datetime().map(|ca| {
509 let mut ca = ca.clone();
510 ca.set_time_unit_and_time_zone(tu, tz.cloned().unwrap_or(TimeZone::UTC))?;
511 Ok(ca)
512 })?,
513 _ => infer.coerce_string(ca).datetime().map(|ca| {
514 let mut ca = ca.clone();
515 ca.set_time_unit(tu);
516 match tz {
517 #[cfg(feature = "timezones")]
518 Some(tz) => polars_ops::prelude::replace_time_zone(
519 &ca,
520 Some(tz),
521 ambiguous,
522 NonExistent::Raise,
523 ),
524 _ => Ok(ca),
525 }
526 })?,
527 }
528}
529
530#[cfg(feature = "dtype-date")]
531pub(crate) fn to_date(ca: &StringChunked) -> PolarsResult<DateChunked> {
532 match ca.first_non_null() {
533 None => Ok(Int32Chunked::full_null(ca.name().clone(), ca.len()).into_date()),
534 Some(idx) => {
535 let subset = ca.slice(idx as i64, ca.len());
536 let pattern = subset
537 .iter()
538 .find_map(|opt_val| opt_val.and_then(infer_pattern_date_single))
539 .ok_or_else(|| polars_err!(parse_fmt_idk = "date"))?;
540 let mut infer = DatetimeInfer::<Int32Type>::try_from_with_unit(pattern, None).unwrap();
541 coerce_string_to_date(&mut infer, ca)
542 },
543 }
544}