polars_time/chunkedarray/string/
mod.rs1pub mod infer;
2use chrono::DateTime;
3mod patterns;
4mod strptime;
5pub use patterns::Pattern;
6#[cfg(feature = "dtype-time")]
7use polars_core::chunked_array::temporal::time_to_time64ns;
8use polars_core::prelude::arity::unary_elementwise;
9use polars_utils::cache::LruCachedFunc;
10
11use super::*;
12#[cfg(feature = "dtype-date")]
13use crate::chunkedarray::date::naive_date_to_date;
14use crate::prelude::string::strptime::StrpTimeState;
15
16#[cfg(feature = "dtype-time")]
17fn time_pattern<F, K>(val: &str, convert: F) -> Option<&'static str>
18where
20 F: Fn(&str, &str) -> chrono::ParseResult<K>,
21{
22 patterns::TIME_H_M_S
23 .iter()
24 .chain(patterns::TIME_H_M_S)
25 .find(|fmt| convert(val, fmt).is_ok())
26 .copied()
27}
28
29fn datetime_pattern<F, K>(val: &str, convert: F) -> Option<&'static str>
30where
32 F: Fn(&str, &str) -> chrono::ParseResult<K>,
33{
34 patterns::DATETIME_Y_M_D
35 .iter()
36 .chain(patterns::DATETIME_D_M_Y)
37 .find(|fmt| convert(val, fmt).is_ok())
38 .copied()
39}
40
41fn date_pattern<F, K>(val: &str, convert: F) -> Option<&'static str>
42where
44 F: Fn(&str, &str) -> chrono::ParseResult<K>,
45{
46 patterns::DATE_Y_M_D
47 .iter()
48 .chain(patterns::DATE_D_M_Y)
49 .find(|fmt| convert(val, fmt).is_ok())
50 .copied()
51}
52
53pub trait StringMethods: AsString {
54 #[cfg(feature = "dtype-time")]
55 fn as_time(&self, fmt: Option<&str>, use_cache: bool) -> PolarsResult<TimeChunked> {
57 let string_ca = self.as_string();
58 let fmt = match fmt {
59 Some(fmt) => fmt,
60 None => {
61 if string_ca.null_count() == string_ca.len() {
62 return Ok(
63 Int64Chunked::full_null(string_ca.name().clone(), string_ca.len())
64 .into_time(),
65 );
66 }
67 infer::infer_from_values(string_ca, |val| {
68 time_pattern(val, NaiveTime::parse_from_str)
69 })
70 .ok_or_else(|| polars_err!(parse_fmt_idk = "time"))?
71 },
72 };
73 let use_cache = use_cache && string_ca.len() > 50;
74
75 let mut convert = LruCachedFunc::new(
76 |s| {
77 let naive_time = NaiveTime::parse_from_str(s, fmt).ok()?;
78 Some(time_to_time64ns(&naive_time))
79 },
80 (string_ca.len() as f64).sqrt() as usize,
81 );
82 let ca = unary_elementwise(string_ca, |opt_s| convert.eval(opt_s?, use_cache));
83 Ok(ca.with_name(string_ca.name().clone()).into_time())
84 }
85
86 #[cfg(feature = "dtype-date")]
87 fn as_date_not_exact(&self, fmt: Option<&str>) -> PolarsResult<DateChunked> {
91 let string_ca = self.as_string();
92 let fmt = match fmt {
93 Some(fmt) => fmt,
94 None => {
95 if string_ca.null_count() == string_ca.len() {
96 return Ok(
97 Int32Chunked::full_null(string_ca.name().clone(), string_ca.len())
98 .into_date(),
99 );
100 }
101 infer::infer_from_values(string_ca, |val| {
102 date_pattern(val, NaiveDate::parse_from_str)
103 })
104 .ok_or_else(|| polars_err!(parse_fmt_idk = "date"))?
105 },
106 };
107 let ca = unary_elementwise(string_ca, |opt_s| {
108 let mut s = opt_s?;
109 while !s.is_empty() {
110 match NaiveDate::parse_and_remainder(s, fmt) {
111 Ok((nd, _)) => return Some(naive_date_to_date(nd)),
112 Err(_) => {
113 let mut it = s.chars();
114 it.next();
115 s = it.as_str();
116 },
117 }
118 }
119
120 None
121 });
122 Ok(ca.with_name(string_ca.name().clone()).into_date())
123 }
124
125 #[cfg(feature = "dtype-datetime")]
126 fn as_datetime_not_exact(
130 &self,
131 fmt: Option<&str>,
132 tu: TimeUnit,
133 tz_aware: bool,
134 tz: Option<&TimeZone>,
135 _ambiguous: &StringChunked,
136 ensure_matching_tz: bool,
138 ) -> PolarsResult<DatetimeChunked> {
139 let string_ca = self.as_string();
140 let had_format = fmt.is_some();
141 let fmt = match fmt {
142 Some(fmt) => fmt,
143 None => {
144 if string_ca.null_count() == string_ca.len() {
145 return Ok(
146 Int64Chunked::full_null(string_ca.name().clone(), string_ca.len())
147 .into_datetime(tu, tz.cloned()),
148 );
149 }
150 infer::infer_from_values(string_ca, |val| {
151 datetime_pattern(val, NaiveDateTime::parse_from_str)
152 .or_else(|| datetime_pattern(val, NaiveDate::parse_from_str))
153 })
154 .ok_or_else(|| polars_err!(parse_fmt_idk = "datetime"))?
155 },
156 };
157
158 let func = match tu {
159 TimeUnit::Nanoseconds => datetime_to_timestamp_ns,
160 TimeUnit::Microseconds => datetime_to_timestamp_us,
161 TimeUnit::Milliseconds => datetime_to_timestamp_ms,
162 };
163
164 let ca = unary_elementwise(string_ca, |opt_s| {
165 let mut s = opt_s?;
166 while !s.is_empty() {
167 let timestamp = if tz_aware {
168 DateTime::parse_and_remainder(s, fmt)
169 .ok()
170 .map(|(dt, _r)| func(dt.naive_utc()))
171 } else {
172 infer::parse_datetime_and_remainder(s, fmt).map(|(nd, _r)| func(nd))
173 };
174 match timestamp {
175 Some(ts) => return Some(ts),
176 None => {
177 let mut it = s.chars();
178 it.next();
179 s = it.as_str();
180 },
181 }
182 }
183 None
184 })
185 .with_name(string_ca.name().clone());
186
187 polars_ensure!(
188 !ensure_matching_tz || had_format || !(tz_aware && tz.is_none()),
189 to_datetime_tz_mismatch
190 );
191
192 match (tz_aware, tz) {
193 #[cfg(feature = "timezones")]
194 (false, Some(tz)) => polars_ops::prelude::replace_time_zone(
195 &ca.into_datetime(tu, None),
196 Some(tz),
197 _ambiguous,
198 NonExistent::Raise,
199 ),
200 #[cfg(feature = "timezones")]
201 (true, tz) => Ok(ca.into_datetime(tu, Some(tz.cloned().unwrap_or(TimeZone::UTC)))),
202 _ => Ok(ca.into_datetime(tu, None)),
203 }
204 }
205
206 #[cfg(feature = "dtype-date")]
207 fn as_date(&self, fmt: Option<&str>, use_cache: bool) -> PolarsResult<DateChunked> {
209 let string_ca = self.as_string();
210 let fmt = match fmt {
211 Some(fmt) => fmt,
212 None => return infer::to_date(string_ca),
213 };
214 let use_cache = use_cache && string_ca.len() > 50;
215 let fmt = strptime::compile_fmt(fmt)?;
216
217 let ca = if strptime::fast_parser_supported(fmt.as_bytes()) {
219 let mut strptime_cache = StrpTimeState::default();
220 let mut convert = LruCachedFunc::new(
221 |s: &str| {
222 match strptime_cache.parse(s.as_bytes(), fmt.as_bytes()) {
223 None => NaiveDate::parse_from_str(s, &fmt).ok(),
225 Some(ndt) => Some(ndt.date()),
226 }
227 .map(naive_date_to_date)
228 },
229 (string_ca.len() as f64).sqrt() as usize,
230 );
231 unary_elementwise(string_ca, |val| convert.eval(val?, use_cache))
232 } else {
233 let mut convert = LruCachedFunc::new(
234 |s| {
235 let naive_date = NaiveDate::parse_from_str(s, &fmt).ok()?;
236 Some(naive_date_to_date(naive_date))
237 },
238 (string_ca.len() as f64).sqrt() as usize,
239 );
240 unary_elementwise(string_ca, |val| convert.eval(val?, use_cache))
241 };
242
243 Ok(ca.with_name(string_ca.name().clone()).into_date())
244 }
245
246 #[cfg(feature = "dtype-datetime")]
247 fn as_datetime(
249 &self,
250 fmt: Option<&str>,
251 tu: TimeUnit,
252 use_cache: bool,
253 tz_aware: bool,
254 tz: Option<&TimeZone>,
255 ambiguous: &StringChunked,
256 ) -> PolarsResult<DatetimeChunked> {
257 let string_ca = self.as_string();
258 let fmt = match fmt {
259 Some(fmt) => fmt,
260 None => return infer::to_datetime(string_ca, tu, tz, ambiguous, true),
261 };
262 let fmt = strptime::compile_fmt(fmt)?;
263 let use_cache = use_cache && string_ca.len() > 50;
264
265 let func = match tu {
266 TimeUnit::Nanoseconds => datetime_to_timestamp_ns,
267 TimeUnit::Microseconds => datetime_to_timestamp_us,
268 TimeUnit::Milliseconds => datetime_to_timestamp_ms,
269 };
270
271 if tz_aware {
272 #[cfg(feature = "timezones")]
273 {
274 let mut convert = LruCachedFunc::new(
275 |s: &str| {
276 let dt = DateTime::parse_from_str(s, &fmt).ok()?;
277 Some(func(dt.naive_utc()))
278 },
279 (string_ca.len() as f64).sqrt() as usize,
280 );
281 Ok(
282 unary_elementwise(string_ca, |opt_s| convert.eval(opt_s?, use_cache))
283 .with_name(string_ca.name().clone())
284 .into_datetime(tu, Some(tz.cloned().unwrap_or(TimeZone::UTC))),
285 )
286 }
287 #[cfg(not(feature = "timezones"))]
288 {
289 panic!("activate 'timezones' feature")
290 }
291 } else {
292 let transform = match tu {
293 TimeUnit::Nanoseconds => infer::transform_datetime_ns,
294 TimeUnit::Microseconds => infer::transform_datetime_us,
295 TimeUnit::Milliseconds => infer::transform_datetime_ms,
296 };
297 let ca = if strptime::fast_parser_supported(fmt.as_bytes()) {
298 let mut strptime_cache = StrpTimeState::default();
299 let mut convert = LruCachedFunc::new(
300 |s: &str| match strptime_cache.parse(s.as_bytes(), fmt.as_bytes()) {
301 None => transform(s, &fmt),
302 Some(ndt) => Some(func(ndt)),
303 },
304 (string_ca.len() as f64).sqrt() as usize,
305 );
306 unary_elementwise(string_ca, |opt_s| convert.eval(opt_s?, use_cache))
307 } else {
308 let mut convert = LruCachedFunc::new(
309 |s| transform(s, &fmt),
310 (string_ca.len() as f64).sqrt() as usize,
311 );
312 unary_elementwise(string_ca, |opt_s| convert.eval(opt_s?, use_cache))
313 };
314 let dt = ca
315 .with_name(string_ca.name().clone())
316 .into_datetime(tu, None);
317 match tz {
318 #[cfg(feature = "timezones")]
319 Some(tz) => polars_ops::prelude::replace_time_zone(
320 &dt,
321 Some(tz),
322 ambiguous,
323 NonExistent::Raise,
324 ),
325 _ => Ok(dt),
326 }
327 }
328 }
329}
330
331pub trait AsString {
332 fn as_string(&self) -> &StringChunked;
333}
334
335impl AsString for StringChunked {
336 fn as_string(&self) -> &StringChunked {
337 self
338 }
339}
340
341impl StringMethods for StringChunked {}