1use crate::column_types::{ColumnType, DerivedKind, TYPE_NAMES, dtype_label};
10use crate::widgets::ui::PickerState;
11use polars::prelude::DataType;
12
13pub const AS_READ: &str = "as read";
15
16#[derive(Debug, Clone, PartialEq, Eq)]
18pub enum Stage {
19 Type,
21 Format {
24 ty: DataType,
25 formats: Vec<Option<String>>,
26 },
27}
28
29#[derive(Debug, Clone)]
31pub struct RetypeModal {
32 pub column: String,
33 pub as_read: DataType,
35 pub examples: Vec<String>,
38 pub stage: Stage,
39 pub picker: PickerState,
40}
41
42#[derive(Debug, Clone, PartialEq, Eq)]
44pub enum Chosen {
45 AsRead,
47 Type(ColumnType),
48 Format,
50 Nothing,
51}
52
53impl RetypeModal {
54 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 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 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 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 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 self.picker
161 .select_original(if formats.len() > 1 { 1 } else { 0 });
162 self.stage = Stage::Format { ty: dtype, formats };
163 }
164
165 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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
207pub enum CombineField {
208 Date,
209 Time,
210 Offset,
211 Kind,
212 Name,
213}
214
215#[derive(Debug, Clone)]
217pub struct CombineModal {
218 pub focus: CombineField,
219 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 pub picker: Option<(CombineField, PickerState)>,
228 pub problem: Option<String>,
230}
231
232pub const NONE: &str = "none";
234
235impl CombineModal {
236 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 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 if self.time.is_some() {
269 from.extend(self.offset.clone());
270 }
271 }
272 from
273 }
274
275 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 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 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 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 let Chosen::Type(ty) = modal.choose() else {
417 panic!("a format");
418 };
419 assert_eq!(ty.format.as_deref(), Some("%d/%m/%Y"));
420 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}