Skip to main content

datui_lib/
typed_value.rs

1//! Text typed as a value for a column, read as the column's own type.
2//!
3//! A filter and a Data Quality partition compare a column with a value someone
4//! typed. Compared as text it either fails (Polars will not compare a date with a
5//! string) or casts every row to text, which costs a pass over the column and
6//! blinds Parquet statistics and SQLite to the predicate. Read here into a literal
7//! of the column's type, `col op lit` stays a plain comparison.
8
9use chrono::{DateTime, NaiveDate, NaiveDateTime, NaiveTime, TimeZone as _};
10use polars::prelude::*;
11
12/// The forms a value of `dtype` is written in, for a message about one that is not.
13pub 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
25/// `text` as a literal of `dtype`: a number, a flag, a date, a date and time read as
26/// a clock in the column's zone, a time, a duration or a decimal at the column's
27/// scale. Text and categories stay text. The error says what `text` should look like.
28///
29/// Whole numbers are read as `i64` or `u64` whatever their width, so `< 300` on an
30/// `i8` column still compares; a float is read at the column's own precision, so
31/// `0.1` is the `f32` a Float32 column stores.
32pub 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            // Polars reads the text at the column's scale, as a cast of the column would.
86            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
98/// The text [`parse`] reads back to exactly `value`, a value of `dtype`: what `+` and
99/// `-` put in a filter. A float is its shortest exact decimal, a date and time its
100/// clock in the column's zone to the last digit (with its offset where that clock
101/// happens twice). `None` for a null, and for a type [`parse`] does not read.
102pub 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
174/// Whether `text` reads back to `value`.
175fn 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            // Read wide, stored narrow.
180            (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
187/// A literal as Python Polars: `pl.date(2024, 1, 1)`, `pl.datetime(...)`,
188/// `pl.duration(...)`, a number, a string.
189pub 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
285/// `YYYY-MM-DD`, or a date past the calendar as [`crate::exact::out_of_range`] writes
286/// it: days since the epoch.
287fn 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
295/// `HH:MM`, `HH:MM:SS` or `HH:MM:SS.f`, as nanoseconds since midnight.
296fn 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
313/// A date and time as a stored number in `unit`. The forms: a date alone (its
314/// midnight), then `T` or a space, `HH:MM`, `HH:MM:SS` or `HH:MM:SS.f`, and an
315/// optional offset (`+01:00`, `Z`). Without an offset the clock is read in `zone`.
316/// `Err(Some(why))` names a problem other than the form; `{text}` in it stands for
317/// the text.
318fn 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    // One separator, a space, whichever was typed; `Z` is the offset it stands for.
329    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
388/// The UTC clock of a value stored as `v` in `unit`.
389fn 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
398/// A duration as Polars writes one (`1d 2h 3m 4s 5µs`, `-1m -30s`, `1500ms`): parts
399/// of a whole number and a unit, `d h m s ms us µs ns`, each signed on its own, as
400/// nanoseconds.
401fn 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
427/// `ns` in `unit`, when it is a whole number of them.
428fn 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        // A clock in the column's zone: 05:00 in Paris is 04:00 UTC.
506        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        // Spring forward skips 02:30 in Paris; fall back repeats it.
552        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    /// The text `+` and `-` write reads back to the very value, every type.
578    #[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            // 02:30 on the night Paris falls back, the second time: only its offset tells it.
602            (
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}