Skip to main content

datui_lib/
retype_modal.rs

1//! A column's type changed in the table, and a datetime made from columns: the same
2//! names, formats and rules a delimited spec's `[columns]` takes ([`crate::column_types`]),
3//! as a view step before the filters.
4//!
5//! [`RetypeModal`] picks the type, then, for a date, time or datetime, a format that
6//! reads the column's first values, or one typed. [`CombineModal`] is the spec's
7//! `{ from = [...], as = "datetime" }`.
8
9use crate::column_types::{ColumnType, DerivedKind, TYPE_NAMES, dtype_label};
10use crate::widgets::ui::PickerState;
11use polars::prelude::DataType;
12
13/// The first choice of the type list: the column as the read gave it.
14pub const AS_READ: &str = "as read";
15
16/// Where the type picker is.
17#[derive(Debug, Clone, PartialEq, Eq)]
18pub enum Stage {
19    /// Which type.
20    Type,
21    /// Which format for `ty`, a temporal type: `formats[i]` for line `i`, `None` for
22    /// the line that infers it from the values.
23    Format {
24        ty: DataType,
25        formats: Vec<Option<String>>,
26    },
27}
28
29/// The type picker over the table.
30#[derive(Debug, Clone)]
31pub struct RetypeModal {
32    pub column: String,
33    /// The column's type as read, before the view's.
34    pub as_read: DataType,
35    /// Values of the column on screen, the first that are not blank: what the formats
36    /// are judged by and previewed on.
37    pub examples: Vec<String>,
38    pub stage: Stage,
39    pub picker: PickerState,
40}
41
42/// What Enter on the picker chose.
43#[derive(Debug, Clone, PartialEq, Eq)]
44pub enum Chosen {
45    /// The column as read: the view's type taken away.
46    AsRead,
47    Type(ColumnType),
48    /// A temporal type: its format is next.
49    Format,
50    Nothing,
51}
52
53impl RetypeModal {
54    /// The picker for `column`, now `current` (the view's type, or none), read as
55    /// `as_read`, with `examples` from the rows on screen.
56    pub fn new(
57        column: String,
58        as_read: DataType,
59        current: Option<&ColumnType>,
60        examples: Vec<String>,
61    ) -> Self {
62        let mut items = vec![AS_READ.to_string()];
63        items.extend(TYPE_NAMES.iter().map(|n| n.to_string()));
64        let mut picker = PickerState::new(items);
65        let at = current
66            .and_then(|ty| TYPE_NAMES.iter().position(|n| *n == ty.name()))
67            .map_or(0, |i| i + 1);
68        picker.select_original(at);
69        Self {
70            column,
71            as_read,
72            examples,
73            stage: Stage::Type,
74            picker,
75        }
76    }
77
78    /// Enter: the type chosen, or the format.
79    pub fn choose(&mut self) -> Chosen {
80        match &self.stage {
81            Stage::Type => {
82                let Some(i) = self.picker.selected_original() else {
83                    return Chosen::Nothing;
84                };
85                if i == 0 {
86                    return Chosen::AsRead;
87                }
88                let Ok(ty) = ColumnType::named(TYPE_NAMES[i - 1], None) else {
89                    return Chosen::Nothing;
90                };
91                if ty.is_temporal() {
92                    self.offer_formats(ty.dtype);
93                    return Chosen::Format;
94                }
95                Chosen::Type(ty)
96            }
97            Stage::Format { ty, formats } => {
98                let typed = self.picker.filter.trim();
99                let format = match self.picker.selected_original() {
100                    // What was typed narrowed to nothing: it is the format.
101                    None if typed.contains('%') => Some(typed.to_string()),
102                    None => return Chosen::Nothing,
103                    Some(i) => formats[i].clone(),
104                };
105                Chosen::Type(ColumnType {
106                    dtype: ty.clone(),
107                    format,
108                })
109            }
110        }
111    }
112
113    /// Esc: back from the formats to the types. `false` when there is nowhere back to.
114    pub fn back(&mut self) -> bool {
115        if matches!(self.stage, Stage::Type) {
116            return false;
117        }
118        *self = Self::new(
119            std::mem::take(&mut self.column),
120            self.as_read.clone(),
121            None,
122            std::mem::take(&mut self.examples),
123        );
124        true
125    }
126
127    /// The formats of `dtype` that read the first example, each with what it makes of
128    /// it, after a line that infers the format; every format when none reads it.
129    fn offer_formats(&mut self, dtype: DataType) {
130        let first = self.examples.first().cloned().unwrap_or_default();
131        let mut fitting = crate::column_types::formats_reading(&dtype, &first);
132        if fitting.is_empty() {
133            fitting = match dtype {
134                DataType::Date => crate::column_types::DATE_FORMATS.to_vec(),
135                DataType::Time => crate::column_types::TIME_FORMATS.to_vec(),
136                _ => crate::column_types::DATETIME_FORMATS.to_vec(),
137            };
138        }
139        let arrow = crate::glyphs::get().arrow_right;
140        let shown = |ty: &ColumnType| match crate::column_types::preview(ty, &first) {
141            Some(read) if !first.is_empty() => format!("  {first} {arrow} {read}"),
142            _ => String::new(),
143        };
144        let infer = ColumnType {
145            dtype: dtype.clone(),
146            format: None,
147        };
148        let mut items = vec![format!("infer from the values{}", shown(&infer))];
149        let mut formats = vec![None];
150        for format in fitting {
151            let ty = ColumnType {
152                dtype: dtype.clone(),
153                format: Some(format.to_string()),
154            };
155            items.push(format!("{format}{}", shown(&ty)));
156            formats.push(Some(format.to_string()));
157        }
158        self.picker = PickerState::new(items);
159        // The first that reads, ahead of inferring.
160        self.picker
161            .select_original(if formats.len() > 1 { 1 } else { 0 });
162        self.stage = Stage::Format { ty: dtype, formats };
163    }
164
165    /// The format typed, and what it makes of the first example, while it is one.
166    pub fn typed_format(&self) -> Option<(String, Option<String>)> {
167        let Stage::Format { ty, .. } = &self.stage else {
168            return None;
169        };
170        let typed = self.picker.filter.trim();
171        if !typed.contains('%') {
172            return None;
173        }
174        let first = self.examples.first()?;
175        let read = crate::column_types::preview(
176            &ColumnType {
177                dtype: ty.clone(),
178                format: Some(typed.to_string()),
179            },
180            first,
181        );
182        Some((typed.to_string(), read))
183    }
184
185    pub fn title(&self) -> String {
186        match &self.stage {
187            Stage::Type => format!(
188                "Type of {}, read as {}",
189                self.column,
190                dtype_label(&self.as_read)
191            ),
192            Stage::Format { ty, .. } => format!("{} Format", title_case(&dtype_label(ty))),
193        }
194    }
195}
196
197fn title_case(word: &str) -> String {
198    let mut chars = word.chars();
199    chars
200        .next()
201        .map(|c| c.to_uppercase().chain(chars).collect())
202        .unwrap_or_default()
203}
204
205/// The fields of the combine form.
206#[derive(Debug, Clone, Copy, PartialEq, Eq)]
207pub enum CombineField {
208    Date,
209    Time,
210    Offset,
211    Kind,
212    Name,
213}
214
215/// "Combine into datetime": the columns a datetime is made from, and its name.
216#[derive(Debug, Clone)]
217pub struct CombineModal {
218    pub focus: CombineField,
219    /// The columns that can be a date, a time or an offset: text, dates and times.
220    pub columns: Vec<String>,
221    pub date: String,
222    pub time: Option<String>,
223    pub offset: Option<String>,
224    pub kind: DerivedKind,
225    pub name: String,
226    /// The open picker and the field it is for.
227    pub picker: Option<(CombineField, PickerState)>,
228    /// Why Enter did not apply, said on the form's own line.
229    pub problem: Option<String>,
230}
231
232/// What a column picker offers before the columns where a column may be left out.
233pub const NONE: &str = "none";
234
235impl CombineModal {
236    /// The form, made from `date`, among `columns`. A column whose name says time is
237    /// the time, if there is one; the result is named so no column is.
238    pub fn new(date: String, columns: Vec<String>, taken: &[String]) -> Self {
239        let time = columns
240            .iter()
241            .find(|c| **c != date && c.to_lowercase().contains("time"))
242            .cloned();
243        let mut name = "datetime".to_string();
244        let mut n = 2;
245        while taken.contains(&name) {
246            name = format!("datetime_{n}");
247            n += 1;
248        }
249        Self {
250            focus: CombineField::Date,
251            columns,
252            date,
253            time,
254            offset: None,
255            kind: DerivedKind::Datetime,
256            name,
257            picker: None,
258            problem: None,
259        }
260    }
261
262    /// The columns the result is made from, in the order `as` reads them.
263    pub fn from(&self) -> Vec<String> {
264        let mut from = vec![self.date.clone()];
265        if self.kind == DerivedKind::Datetime {
266            from.extend(self.time.clone());
267            // An offset needs a time before it: `from` is date, time, offset.
268            if self.time.is_some() {
269                from.extend(self.offset.clone());
270            }
271        }
272        from
273    }
274
275    /// The derived column it makes, as a spec's `[columns]` entry would.
276    pub fn derived(&self) -> Result<crate::column_types::Derived, String> {
277        let name = self.name.trim();
278        if name.is_empty() {
279            return Err("the new column needs a name".to_string());
280        }
281        Ok(crate::column_types::Derived {
282            name: name.to_string(),
283            from: self.from(),
284            kind: self.kind,
285            format: None,
286        })
287    }
288
289    /// What Enter makes, in a line: `datetime = datetime from Lcl Date, Lcl Time`.
290    pub fn spec_line(&self) -> String {
291        format!(
292            "{} = {} from {}",
293            self.name.trim(),
294            self.kind.name(),
295            self.from().join(", ")
296        )
297    }
298
299    /// The open picker's choices for `field`: the columns, after `none` where the
300    /// column may be left out.
301    pub fn open_picker(&mut self) {
302        let field = self.focus;
303        let optional = matches!(field, CombineField::Time | CombineField::Offset);
304        let mut items = Vec::new();
305        if optional {
306            items.push(NONE.to_string());
307        }
308        items.extend(self.columns.iter().cloned());
309        let current = match field {
310            CombineField::Date => Some(self.date.clone()),
311            CombineField::Time => self.time.clone(),
312            CombineField::Offset => self.offset.clone(),
313            _ => return,
314        };
315        let mut picker = PickerState::new(items);
316        let at = current
317            .and_then(|c| picker.items().iter().position(|i| *i == c))
318            .unwrap_or(0);
319        picker.select_original(at);
320        self.picker = Some((field, picker));
321    }
322
323    /// The picker's choice, into its field.
324    pub fn picker_choose(&mut self) {
325        let Some((field, picker)) = self.picker.take() else {
326            return;
327        };
328        let Some(chosen) = picker
329            .selected_original()
330            .map(|i| picker.items()[i].clone())
331        else {
332            return;
333        };
334        let column = (chosen != NONE).then_some(chosen);
335        match field {
336            CombineField::Date => {
337                if let Some(c) = column {
338                    self.date = c;
339                }
340            }
341            CombineField::Time => self.time = column,
342            CombineField::Offset => self.offset = column,
343            _ => {}
344        }
345    }
346
347    pub fn step_kind(&mut self, delta: i8) {
348        self.kind = crate::form::step_value(&DerivedKind::ALL, self.kind, delta);
349        crate::form::Form::settle_focus(self);
350    }
351}
352
353impl crate::form::Form for CombineModal {
354    type Field = CombineField;
355
356    fn fields(&self) -> Vec<(CombineField, crate::form::FieldKind)> {
357        use crate::form::FieldKind;
358        let mut fields = vec![
359            (CombineField::Date, FieldKind::Picker { multi: false }),
360            (CombineField::Kind, FieldKind::Choice),
361        ];
362        if self.kind == DerivedKind::Datetime {
363            fields.insert(1, (CombineField::Time, FieldKind::Picker { multi: false }));
364            fields.insert(
365                2,
366                (CombineField::Offset, FieldKind::Picker { multi: false }),
367            );
368        }
369        fields.push((CombineField::Name, FieldKind::Text));
370        fields
371    }
372
373    fn focused(&self) -> CombineField {
374        self.focus
375    }
376
377    fn set_focused(&mut self, field: CombineField) {
378        self.focus = field;
379    }
380}
381
382#[cfg(test)]
383mod tests {
384    use super::*;
385
386    #[test]
387    fn a_number_type_is_chosen_at_once_and_a_date_asks_its_format() {
388        let mut modal = RetypeModal::new(
389            "when".into(),
390            DataType::String,
391            None,
392            vec!["03/04/2024".into()],
393        );
394        assert_eq!(modal.picker.items()[0], "as read");
395        assert_eq!(modal.title(), "Type of when, read as str");
396        modal
397            .picker
398            .select_original(TYPE_NAMES.iter().position(|n| *n == "i64").unwrap() + 1);
399        assert_eq!(
400            modal.choose(),
401            Chosen::Type(ColumnType::named("i64", None).unwrap())
402        );
403        modal
404            .picker
405            .select_original(TYPE_NAMES.iter().position(|n| *n == "date").unwrap() + 1);
406        assert_eq!(modal.choose(), Chosen::Format);
407        let items = modal.picker.items().to_vec();
408        assert!(
409            items
410                .iter()
411                .any(|i| i.starts_with("%d/%m/%Y") && i.ends_with("2024-04-03")),
412            "{items:?}"
413        );
414        assert!(items.iter().any(|i| i.starts_with("%m/%d/%Y")), "{items:?}");
415        // The first that reads is chosen by default.
416        let Chosen::Type(ty) = modal.choose() else {
417            panic!("a format");
418        };
419        assert_eq!(ty.format.as_deref(), Some("%d/%m/%Y"));
420        // A format typed that no line has is the format.
421        modal.picker.filter = "%d.%m.%Y".into();
422        modal.picker.clear_filter();
423        for c in "%d %m %Y".chars() {
424            modal.picker.type_char(c);
425        }
426        assert_eq!(
427            modal.choose(),
428            Chosen::Type(ColumnType::named("date", Some("%d %m %Y".into())).unwrap())
429        );
430        assert!(modal.back());
431        assert!(matches!(modal.stage, Stage::Type));
432        assert!(!modal.back());
433    }
434
435    #[test]
436    fn the_combine_form_makes_the_specs_derived_column() {
437        let columns = vec![
438            "Lcl Date".to_string(),
439            "Lcl Time".to_string(),
440            "UTCOfst".to_string(),
441        ];
442        let mut modal = CombineModal::new("Lcl Date".into(), columns.clone(), &columns);
443        assert_eq!(modal.time.as_deref(), Some("Lcl Time"));
444        modal.offset = Some("UTCOfst".into());
445        let derived = modal.derived().unwrap();
446        assert_eq!(derived.from, columns);
447        assert_eq!(derived.kind, DerivedKind::Datetime);
448        assert_eq!(derived.name, "datetime");
449        modal.step_kind(1);
450        assert_eq!(modal.kind, DerivedKind::Date);
451        assert_eq!(modal.from(), ["Lcl Date"], "a date from one column");
452        modal.name = " ".into();
453        assert!(modal.derived().is_err());
454    }
455}