1use chrono::{DateTime, NaiveDate, NaiveDateTime, NaiveTime, TimeZone as _};
10use polars::prelude::*;
11
12pub fn written_as(dtype: &DataType) -> &'static str {
14 match dtype {
15 DataType::Date => "a date written YYYY-MM-DD",
16 DataType::Datetime(..) => "a date and time written YYYY-MM-DD HH:MM[:SS[.f]]",
17 DataType::Time => "a time written HH:MM[:SS[.f]]",
18 DataType::Duration(_) => "a duration such as 1d 2h 30m or 1500ms",
19 DataType::Boolean => "true or false",
20 dtype if dtype.is_integer() => "a whole number",
21 _ => "a number",
22 }
23}
24
25pub fn parse(text: &str, dtype: &DataType) -> Result<Scalar, String> {
33 let bad = || format!("{text:?} is not {}", written_as(dtype));
34 let scalar = |value: AnyValue<'static>| Scalar::new(dtype.clone(), value);
35 match dtype {
36 DataType::String | DataType::Categorical(..) | DataType::Enum(..) => Ok(Scalar::new(
37 DataType::String,
38 AnyValue::StringOwned(text.into()),
39 )),
40 DataType::Boolean => text
41 .parse()
42 .map(|b| scalar(AnyValue::Boolean(b)))
43 .map_err(|_| bad()),
44 DataType::Int8 | DataType::Int16 | DataType::Int32 | DataType::Int64 => text
45 .parse()
46 .map(|i| Scalar::new(DataType::Int64, AnyValue::Int64(i)))
47 .map_err(|_| bad()),
48 DataType::UInt8 | DataType::UInt16 | DataType::UInt32 | DataType::UInt64 => text
49 .parse()
50 .map(|u| Scalar::new(DataType::UInt64, AnyValue::UInt64(u)))
51 .map_err(|_| bad()),
52 DataType::Float64 => text
53 .parse()
54 .map(|f| scalar(AnyValue::Float64(f)))
55 .map_err(|_| bad()),
56 DataType::Float32 => text
57 .parse()
58 .map(|f| scalar(AnyValue::Float32(f)))
59 .map_err(|_| bad()),
60 DataType::Date => parse_date(text.trim())
61 .map(|days| scalar(AnyValue::Date(days)))
62 .ok_or_else(bad),
63 DataType::Datetime(unit, zone) => {
64 let v = parse_datetime(text.trim(), *unit, zone.as_ref()).map_err(|why| {
65 why.unwrap_or_else(bad)
66 .replace("{text}", &format!("{text:?}"))
67 })?;
68 Ok(scalar(AnyValue::DatetimeOwned(
69 v,
70 *unit,
71 zone.clone().map(Arc::new),
72 )))
73 }
74 DataType::Time => parse_time(text.trim())
75 .map(|ns| scalar(AnyValue::Time(ns)))
76 .ok_or_else(bad),
77 DataType::Duration(unit) => {
78 let ns = parse_duration(text.trim()).ok_or_else(bad)?;
79 let v = in_unit(ns, *unit).ok_or_else(|| {
80 format!("{text:?} is finer than the column's {}", unit_name(*unit))
81 })?;
82 Ok(scalar(AnyValue::Duration(v, *unit)))
83 }
84 DataType::Decimal(..) => {
85 let read = Series::new(PlSmallStr::EMPTY, [text.trim()])
87 .strict_cast(dtype)
88 .map_err(|_| bad())?;
89 match read.get(0).map_err(|_| bad())? {
90 AnyValue::Null => Err(bad()),
91 value => Ok(scalar(value.into_static())),
92 }
93 }
94 _ => Err(format!("a {dtype} column has no value to compare with")),
95 }
96}
97
98pub fn text_of(value: &AnyValue, dtype: &DataType) -> Option<String> {
103 if value.is_null() {
104 return None;
105 }
106 let text = match (dtype, value) {
107 (DataType::String, _) => value.get_str()?.to_string(),
108 (DataType::Float64, AnyValue::Float64(f)) => crate::exact::f64_text(*f),
109 (DataType::Float32, AnyValue::Float32(f)) => crate::exact::f32_text(*f),
110 (DataType::Date | DataType::Time, _) => match crate::exact::out_of_range(value) {
111 Some(stored) => stored,
112 None => match value {
113 AnyValue::Date(days) => epoch_date()
114 .checked_add_signed(chrono::TimeDelta::days(i64::from(*days)))?
115 .format("%Y-%m-%d")
116 .to_string(),
117 AnyValue::Time(ns) => time_of(*ns)?.format("%H:%M:%S%.f").to_string(),
118 _ => return None,
119 },
120 },
121 (
122 DataType::Datetime(unit, zone),
123 AnyValue::Datetime(v, ..) | AnyValue::DatetimeOwned(v, ..),
124 ) => {
125 if let Some(stored) = crate::exact::out_of_range(value) {
126 stored
127 } else {
128 let utc = utc_of(*v, *unit)?;
129 let clock = "%Y-%m-%d %H:%M:%S%.f";
130 match zone {
131 None => utc.format(clock).to_string(),
132 Some(zone) => {
133 let local = zone.to_chrono().ok()?.from_utc_datetime(&utc);
134 let plain = local.format(clock).to_string();
135 if same(&plain, dtype, value) {
136 plain
137 } else {
138 local.format("%Y-%m-%d %H:%M:%S%.f%:z").to_string()
139 }
140 }
141 }
142 }
143 }
144 (DataType::Duration(unit), AnyValue::Duration(v, _)) => {
145 let shown = crate::exact::str_value(value).into_owned();
146 if same(&shown, dtype, value) {
147 shown
148 } else {
149 format!("{v}{}", unit_suffix(*unit))
150 }
151 }
152 (
153 DataType::Boolean
154 | DataType::Int8
155 | DataType::Int16
156 | DataType::Int32
157 | DataType::Int64
158 | DataType::UInt8
159 | DataType::UInt16
160 | DataType::UInt32
161 | DataType::UInt64
162 | DataType::Decimal(..)
163 | DataType::Categorical(..)
164 | DataType::Enum(..),
165 _,
166 ) => crate::exact::str_value(value).into_owned(),
167 _ => return None,
168 };
169 let reads_back = matches!(dtype, DataType::Categorical(..) | DataType::Enum(..))
170 || same(&text, dtype, value);
171 reads_back.then_some(text)
172}
173
174fn same(text: &str, dtype: &DataType, value: &AnyValue) -> bool {
176 parse(text, dtype).is_ok_and(|read| {
177 let read = read.into_value();
178 match (&read, value) {
179 (AnyValue::Int64(a), _) => value.extract::<i64>() == Some(*a),
181 (AnyValue::UInt64(a), _) => value.extract::<u64>() == Some(*a),
182 _ => read == value.clone().into_static(),
183 }
184 })
185}
186
187pub fn python(scalar: &Scalar) -> String {
190 use crate::python_script::{py_bool, py_float, py_str};
191 let unit_arg = |unit: TimeUnit| format!("time_unit={}", py_str(unit_name_short(unit)));
192 match (scalar.dtype(), scalar.value()) {
193 (_, AnyValue::Int64(i)) => i.to_string(),
194 (_, AnyValue::UInt64(u)) => u.to_string(),
195 (_, AnyValue::Float64(f)) => py_float(*f),
196 (_, AnyValue::Float32(f)) => py_float(f64::from(*f)),
197 (_, AnyValue::Boolean(b)) => py_bool(*b).to_string(),
198 (_, AnyValue::StringOwned(s)) => py_str(s),
199 (_, AnyValue::String(s)) => py_str(s),
200 (DataType::Date, AnyValue::Date(days)) => {
201 match epoch_date().checked_add_signed(chrono::TimeDelta::days(i64::from(*days))) {
202 Some(date) if crate::exact::out_of_range(scalar.value()).is_none() => {
203 use chrono::Datelike;
204 format!("pl.date({}, {}, {})", date.year(), date.month(), date.day())
205 }
206 _ => format!("pl.lit({days}).cast(pl.Date)"),
207 }
208 }
209 (DataType::Datetime(unit, zone), AnyValue::DatetimeOwned(v, ..)) => {
210 let zone_arg = zone
211 .as_ref()
212 .map_or(String::new(), |_| ", time_zone=\"UTC\"".to_string());
213 let convert = zone.as_ref().map_or(String::new(), |zone| {
214 format!(".dt.convert_time_zone({})", py_str(zone))
215 });
216 match utc_of(*v, *unit) {
217 Some(utc) if crate::exact::out_of_range(scalar.value()).is_none() => {
218 use chrono::{Datelike, Timelike};
219 let nanos = utc.nanosecond();
220 if nanos % 1000 == 0 {
221 format!(
222 "pl.datetime({}, {}, {}, {}, {}, {}, {}, {}{zone_arg}){convert}",
223 utc.year(),
224 utc.month(),
225 utc.day(),
226 utc.hour(),
227 utc.minute(),
228 utc.second(),
229 nanos / 1000,
230 unit_arg(*unit)
231 )
232 } else {
233 format!(
234 "pl.lit({}).str.to_datetime(\"%Y-%m-%d %H:%M:%S%.f\", {}{zone_arg}){convert}",
235 py_str(&utc.format("%Y-%m-%d %H:%M:%S%.f").to_string()),
236 unit_arg(*unit)
237 )
238 }
239 }
240 _ => format!(
241 "pl.lit({v}).cast(pl.Datetime({}{}))",
242 py_str(unit_name_short(*unit)),
243 zone.as_ref()
244 .map_or(String::new(), |zone| format!(", {}", py_str(zone)))
245 ),
246 }
247 }
248 (DataType::Time, AnyValue::Time(ns)) => match time_of(*ns) {
249 Some(time) if ns % 1000 == 0 => {
250 use chrono::Timelike;
251 format!(
252 "pl.time({}, {}, {}, {})",
253 time.hour(),
254 time.minute(),
255 time.second(),
256 time.nanosecond() / 1000
257 )
258 }
259 Some(time) => format!(
260 "pl.lit({}).str.to_time(\"%H:%M:%S%.f\")",
261 py_str(&time.format("%H:%M:%S%.f").to_string())
262 ),
263 None => format!("pl.lit({ns}).cast(pl.Time)"),
264 },
265 (DataType::Duration(unit), AnyValue::Duration(v, _)) => {
266 let name = match unit {
267 TimeUnit::Milliseconds => "milliseconds",
268 TimeUnit::Microseconds => "microseconds",
269 TimeUnit::Nanoseconds => "nanoseconds",
270 };
271 format!("pl.duration({name}={v}, {})", unit_arg(*unit))
272 }
273 (DataType::Decimal(precision, scale), value) => format!(
274 "pl.lit({}).cast(pl.Decimal({precision}, {scale}))",
275 py_str(&crate::exact::str_value(value))
276 ),
277 (_, value) => py_str(&crate::exact::str_value(value)),
278 }
279}
280
281fn epoch_date() -> NaiveDate {
282 NaiveDate::from_ymd_opt(1970, 1, 1).expect("the epoch is a date")
283}
284
285fn parse_date(text: &str) -> Option<i32> {
288 if let Some(days) = text.strip_suffix(" days since 1970-01-01") {
289 return days.trim().parse().ok();
290 }
291 let date = NaiveDate::parse_from_str(text, "%Y-%m-%d").ok()?;
292 i32::try_from((date - epoch_date()).num_days()).ok()
293}
294
295fn parse_time(text: &str) -> Option<i64> {
297 if let Some(ns) = text.strip_suffix(" ns since midnight") {
298 return ns.trim().parse().ok();
299 }
300 let time = NaiveTime::parse_from_str(text, "%H:%M:%S%.f")
301 .or_else(|_| NaiveTime::parse_from_str(text, "%H:%M"))
302 .ok()?;
303 use chrono::Timelike;
304 Some(i64::from(time.num_seconds_from_midnight()) * 1_000_000_000 + i64::from(time.nanosecond()))
305}
306
307fn time_of(ns: i64) -> Option<NaiveTime> {
308 let secs = u32::try_from(ns.div_euclid(1_000_000_000)).ok()?;
309 let nanos = u32::try_from(ns.rem_euclid(1_000_000_000)).ok()?;
310 NaiveTime::from_num_seconds_from_midnight_opt(secs, nanos)
311}
312
313fn parse_datetime(
319 text: &str,
320 unit: TimeUnit,
321 zone: Option<&TimeZone>,
322) -> Result<i64, Option<String>> {
323 if let Some(stored) = text.strip_suffix(" since 1970-01-01 UTC") {
324 let (v, written) = stored.trim().split_once(' ').ok_or(None)?;
325 let v: i64 = v.parse().map_err(|_| None)?;
326 return (written == unit_name_short(unit)).then_some(v).ok_or(None);
327 }
328 let mut text = text.to_string();
330 if text.len() > 10 && text.as_bytes()[10] == b'T' {
331 text.replace_range(10..11, " ");
332 }
333 if let Some(stripped) = text.strip_suffix('Z') {
334 text = format!("{stripped}+00:00");
335 }
336 let with_offset = ["%Y-%m-%d %H:%M:%S%.f%#z", "%Y-%m-%d %H:%M%#z"]
337 .iter()
338 .find_map(|format| DateTime::parse_from_str(&text, format).ok());
339 let utc = if let Some(at) = with_offset {
340 if zone.is_none() {
341 return Err(Some(
342 "{text} carries an offset, and the column has no time zone".to_string(),
343 ));
344 }
345 at.naive_utc()
346 } else {
347 let clock = ["%Y-%m-%d %H:%M:%S%.f", "%Y-%m-%d %H:%M"]
348 .iter()
349 .find_map(|format| NaiveDateTime::parse_from_str(&text, format).ok())
350 .or_else(|| {
351 NaiveDate::parse_from_str(&text, "%Y-%m-%d")
352 .ok()
353 .and_then(|date| date.and_hms_opt(0, 0, 0))
354 })
355 .ok_or(None)?;
356 match zone {
357 None => clock,
358 Some(zone) => {
359 let tz = zone.to_chrono().map_err(|e| Some(e.to_string()))?;
360 match tz.from_local_datetime(&clock) {
361 chrono::LocalResult::Single(at) => at.naive_utc(),
362 _ => {
363 return Err(Some(format!(
364 "{{text}} does not happen in {zone}, or happens twice there; \
365 add its offset, such as +01:00"
366 )));
367 }
368 }
369 }
370 }
371 };
372 let utc = utc.and_utc();
373 let nanos = i64::from(utc.timestamp_subsec_nanos());
374 let finer = || {
375 Some(format!(
376 "{{text}} is finer than the column's {}",
377 unit_name(unit)
378 ))
379 };
380 match unit {
381 TimeUnit::Nanoseconds => utc.timestamp_nanos_opt().ok_or(None),
382 TimeUnit::Microseconds if nanos % 1_000 == 0 => Ok(utc.timestamp_micros()),
383 TimeUnit::Milliseconds if nanos % 1_000_000 == 0 => Ok(utc.timestamp_millis()),
384 _ => Err(finer()),
385 }
386}
387
388fn utc_of(v: i64, unit: TimeUnit) -> Option<NaiveDateTime> {
390 let at = match unit {
391 TimeUnit::Milliseconds => DateTime::from_timestamp_millis(v)?,
392 TimeUnit::Microseconds => DateTime::from_timestamp_micros(v)?,
393 TimeUnit::Nanoseconds => DateTime::from_timestamp_nanos(v),
394 };
395 Some(at.naive_utc())
396}
397
398fn parse_duration(text: &str) -> Option<i128> {
402 let mut total: i128 = 0;
403 let mut parts = 0;
404 for part in text.split_whitespace() {
405 let digits_end = part
406 .char_indices()
407 .find(|(i, c)| !(c.is_ascii_digit() || (*i == 0 && *c == '-')))
408 .map(|(i, _)| i)?;
409 let (number, suffix) = part.split_at(digits_end);
410 let number: i128 = number.parse().ok()?;
411 let per: i128 = match suffix {
412 "d" => 86_400_000_000_000,
413 "h" => 3_600_000_000_000,
414 "m" => 60_000_000_000,
415 "s" => 1_000_000_000,
416 "ms" => 1_000_000,
417 "us" | "µs" => 1_000,
418 "ns" => 1,
419 _ => return None,
420 };
421 total = total.checked_add(number.checked_mul(per)?)?;
422 parts += 1;
423 }
424 (parts > 0).then_some(total)
425}
426
427fn in_unit(ns: i128, unit: TimeUnit) -> Option<i64> {
429 let per: i128 = match unit {
430 TimeUnit::Milliseconds => 1_000_000,
431 TimeUnit::Microseconds => 1_000,
432 TimeUnit::Nanoseconds => 1,
433 };
434 (ns % per == 0)
435 .then(|| i64::try_from(ns / per).ok())
436 .flatten()
437}
438
439fn unit_name(unit: TimeUnit) -> &'static str {
440 match unit {
441 TimeUnit::Milliseconds => "milliseconds",
442 TimeUnit::Microseconds => "microseconds",
443 TimeUnit::Nanoseconds => "nanoseconds",
444 }
445}
446
447fn unit_name_short(unit: TimeUnit) -> &'static str {
448 match unit {
449 TimeUnit::Milliseconds => "ms",
450 TimeUnit::Microseconds => "us",
451 TimeUnit::Nanoseconds => "ns",
452 }
453}
454
455fn unit_suffix(unit: TimeUnit) -> &'static str {
456 match unit {
457 TimeUnit::Milliseconds => "ms",
458 TimeUnit::Microseconds => "us",
459 TimeUnit::Nanoseconds => "ns",
460 }
461}
462
463#[cfg(test)]
464mod tests {
465 use super::*;
466
467 fn tz(name: &str) -> Option<TimeZone> {
468 TimeZone::opt_try_new(Some(name)).unwrap()
469 }
470
471 fn datetime(unit: TimeUnit, zone: Option<&str>) -> DataType {
472 DataType::Datetime(unit, zone.and_then(tz))
473 }
474
475 fn stored(text: &str, dtype: &DataType) -> i64 {
476 match parse(text, dtype).unwrap().into_value() {
477 AnyValue::DatetimeOwned(v, ..) | AnyValue::Duration(v, _) | AnyValue::Time(v) => v,
478 AnyValue::Date(days) => i64::from(days),
479 other => panic!("{other:?}"),
480 }
481 }
482
483 #[test]
484 fn dates_and_times_read_in_their_forms() {
485 assert_eq!(stored("2024-01-01", &DataType::Date), 19723);
486 assert_eq!(
487 stored("-800000 days since 1970-01-01", &DataType::Date),
488 -800000
489 );
490 let us = datetime(TimeUnit::Microseconds, None);
491 let midnight = 1_704_067_200_000_000;
492 assert_eq!(stored("2024-01-01", &us), midnight);
493 assert_eq!(
494 stored("2024-01-01 05:00", &us),
495 midnight + 5 * 3_600_000_000
496 );
497 assert_eq!(
498 stored("2024-01-01T05:00:00", &us),
499 midnight + 5 * 3_600_000_000
500 );
501 assert_eq!(
502 stored("2024-01-01 05:00:00.25", &us),
503 midnight + 5 * 3_600_000_000 + 250_000
504 );
505 let paris = datetime(TimeUnit::Microseconds, Some("Europe/Paris"));
507 assert_eq!(
508 stored("2024-01-01 05:00", &paris),
509 midnight + 4 * 3_600_000_000
510 );
511 assert_eq!(
512 stored("2024-01-01 05:00+00:00", &paris),
513 midnight + 5 * 3_600_000_000
514 );
515 assert_eq!(
516 stored("2024-01-01T05:00:00Z", &paris),
517 midnight + 5 * 3_600_000_000
518 );
519 assert_eq!(
520 stored("05:06", &DataType::Time),
521 (5 * 3600 + 6 * 60) * 1_000_000_000
522 );
523 assert_eq!(
524 stored("05:06:07.5", &DataType::Time),
525 (5 * 3600 + 6 * 60 + 7) * 1_000_000_000 + 500_000_000
526 );
527 let ms = DataType::Duration(TimeUnit::Milliseconds);
528 assert_eq!(stored("1d 2h", &ms), 26 * 3_600_000);
529 assert_eq!(stored("-1m -30s", &ms), -90_000);
530 assert_eq!(
531 stored("1500µs", &DataType::Duration(TimeUnit::Microseconds)),
532 1500
533 );
534 }
535
536 #[test]
537 fn what_does_not_read_says_why() {
538 let us = datetime(TimeUnit::Microseconds, None);
539 let ms = datetime(TimeUnit::Milliseconds, None);
540 let paris = datetime(TimeUnit::Microseconds, Some("Europe/Paris"));
541 let err = |text: &str, dtype: &DataType| parse(text, dtype).unwrap_err();
542 assert_eq!(
543 err("2024-13-01", &DataType::Date),
544 "\"2024-13-01\" is not a date written YYYY-MM-DD"
545 );
546 assert!(err("noon", &us).contains("is not a date and time"));
547 assert!(
548 err("2024-01-01 05:00:00.0001", &ms).contains("finer than the column's milliseconds")
549 );
550 assert!(err("2024-01-01 05:00+01:00", &us).contains("no time zone"));
551 assert!(err("2024-03-31 02:30", &paris).contains("does not happen in Europe/Paris"));
553 assert!(err("2024-10-27 02:30", &paris).contains("happens twice"));
554 assert!(err("25:00", &DataType::Time).contains("is not a time"));
555 assert!(
556 err("1 fortnight", &DataType::Duration(TimeUnit::Microseconds))
557 .contains("is not a duration")
558 );
559 assert!(err("1ns", &DataType::Duration(TimeUnit::Microseconds)).contains("finer"));
560 assert!(err("abc", &DataType::Decimal(10, 2)).contains("is not a number"));
561 assert!(err("yes", &DataType::Boolean).contains("true or false"));
562 }
563
564 #[test]
565 fn decimals_read_at_the_column_scale() {
566 let dtype = DataType::Decimal(10, 2);
567 assert_eq!(
568 parse("1.5", &dtype).unwrap(),
569 parse("1.50", &dtype).unwrap()
570 );
571 assert_eq!(
572 parse("1.5", &dtype).unwrap().into_value(),
573 AnyValue::Decimal(150, 10, 2)
574 );
575 }
576
577 #[test]
579 fn every_value_reads_back_from_its_text() {
580 let paris = tz("Europe/Paris");
581 let cases: Vec<(DataType, AnyValue<'static>)> = vec![
582 (DataType::Float64, AnyValue::Float64(0.1 + 0.2)),
583 (DataType::Float32, AnyValue::Float32(0.1)),
584 (DataType::Int8, AnyValue::Int8(-5)),
585 (DataType::UInt64, AnyValue::UInt64(u64::MAX)),
586 (DataType::Boolean, AnyValue::Boolean(true)),
587 (DataType::Date, AnyValue::Date(19723)),
588 (DataType::Date, AnyValue::Date(-800_000_000)),
589 (
590 DataType::Datetime(TimeUnit::Nanoseconds, None),
591 AnyValue::DatetimeOwned(1_704_085_200_123_456_789, TimeUnit::Nanoseconds, None),
592 ),
593 (
594 DataType::Datetime(TimeUnit::Microseconds, paris.clone()),
595 AnyValue::DatetimeOwned(
596 1_704_081_600_000_001,
597 TimeUnit::Microseconds,
598 paris.clone().map(Arc::new),
599 ),
600 ),
601 (
603 DataType::Datetime(TimeUnit::Microseconds, paris.clone()),
604 AnyValue::DatetimeOwned(
605 1_729_992_600_000_000,
606 TimeUnit::Microseconds,
607 paris.clone().map(Arc::new),
608 ),
609 ),
610 (DataType::Time, AnyValue::Time(18_367_123_456_789)),
611 (
612 DataType::Duration(TimeUnit::Microseconds),
613 AnyValue::Duration(93_784_000_005, TimeUnit::Microseconds),
614 ),
615 (
616 DataType::Duration(TimeUnit::Nanoseconds),
617 AnyValue::Duration(-1_500, TimeUnit::Nanoseconds),
618 ),
619 (DataType::Decimal(10, 2), AnyValue::Decimal(150, 10, 2)),
620 ];
621 for (dtype, value) in cases {
622 let text = text_of(&value, &dtype).unwrap_or_else(|| panic!("{dtype} {value:?}"));
623 assert!(same(&text, &dtype, &value), "{dtype}: {text}");
624 }
625 }
626
627 #[test]
628 fn literals_read_back_in_python() {
629 let py = |text: &str, dtype: DataType| python(&parse(text, &dtype).unwrap());
630 assert_eq!(py("2024-01-01", DataType::Date), "pl.date(2024, 1, 1)");
631 assert_eq!(
632 py(
633 "2024-01-01 05:00:00.25",
634 datetime(TimeUnit::Microseconds, None)
635 ),
636 "pl.datetime(2024, 1, 1, 5, 0, 0, 250000, time_unit=\"us\")"
637 );
638 assert_eq!(
639 py(
640 "2024-01-01 05:00",
641 datetime(TimeUnit::Microseconds, Some("Europe/Paris"))
642 ),
643 "pl.datetime(2024, 1, 1, 4, 0, 0, 0, time_unit=\"us\", time_zone=\"UTC\")\
644 .dt.convert_time_zone(\"Europe/Paris\")"
645 );
646 assert_eq!(
647 py(
648 "2024-01-01 05:00:00.000000001",
649 datetime(TimeUnit::Nanoseconds, None)
650 ),
651 "pl.lit(\"2024-01-01 05:00:00.000000001\").str.to_datetime(\"%Y-%m-%d %H:%M:%S%.f\", time_unit=\"ns\")"
652 );
653 assert_eq!(py("05:06:07.5", DataType::Time), "pl.time(5, 6, 7, 500000)");
654 assert_eq!(
655 py("1d", DataType::Duration(TimeUnit::Milliseconds)),
656 "pl.duration(milliseconds=86400000, time_unit=\"ms\")"
657 );
658 assert_eq!(
659 py("1.5", DataType::Decimal(10, 2)),
660 "pl.lit(\"1.50\").cast(pl.Decimal(10, 2))"
661 );
662 assert_eq!(py("0.1", DataType::Float32), "0.10000000149011612");
663 }
664}