1use std::cmp::Ordering;
2use std::collections::HashMap;
3
4use chrono::NaiveDate;
5use serde::{Deserialize, Serialize};
6
7#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
9pub enum DataValue {
10 Text(String),
11 Number(f64),
12 Bool(bool),
13 Date(NaiveDate),
14 Category(String),
15 Null,
16}
17
18#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
20pub enum DataType {
21 #[default]
22 Text,
23 Number,
24 Bool,
25 Date,
26 Category,
27}
28
29#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
31pub enum CompareHint {
32 #[default]
33 Text,
34 Number,
35 Date,
36 Boolean,
37 SingleSelect,
38}
39
40impl DataValue {
41 pub fn display_string(&self) -> String {
43 match self {
44 Self::Text(s) | Self::Category(s) => s.clone(),
45 Self::Number(n) => n.to_string(),
46 Self::Bool(b) => b.to_string(),
47 Self::Date(d) => d.format("%Y-%m-%d").to_string(),
48 Self::Null => String::new(),
49 }
50 }
51
52 pub fn is_empty_value(&self) -> bool {
54 matches!(self, Self::Null) || matches!(self, Self::Text(s) if s.is_empty())
55 }
56
57 pub fn filter_text(&self) -> String {
59 self.display_string().to_lowercase()
60 }
61
62 pub fn partial_cmp_typed(&self, other: &Self, hint: CompareHint) -> Option<Ordering> {
64 match (self, other) {
65 (Self::Null, Self::Null) => Some(Ordering::Equal),
66 (Self::Null, _) => Some(Ordering::Greater),
67 (_, Self::Null) => Some(Ordering::Less),
68 (Self::Number(a), Self::Number(b)) if matches!(hint, CompareHint::Number) => {
69 a.partial_cmp(b)
70 }
71 (Self::Date(a), Self::Date(b)) if matches!(hint, CompareHint::Date) => Some(a.cmp(b)),
72 (Self::Bool(a), Self::Bool(b)) if matches!(hint, CompareHint::Boolean) => {
73 Some(a.cmp(b))
74 }
75 (Self::Text(a) | Self::Category(a), Self::Text(b) | Self::Category(b))
76 if matches!(hint, CompareHint::Text | CompareHint::SingleSelect) =>
77 {
78 Some(a.to_lowercase().cmp(&b.to_lowercase()))
79 }
80 (Self::Number(a), Self::Number(b)) => a.partial_cmp(b),
81 (Self::Date(a), Self::Date(b)) => Some(a.cmp(b)),
82 (Self::Bool(a), Self::Bool(b)) => Some(a.cmp(b)),
83 (Self::Text(a) | Self::Category(a), Self::Text(b) | Self::Category(b)) => {
84 Some(a.to_lowercase().cmp(&b.to_lowercase()))
85 }
86 _ => None,
87 }
88 }
89
90 pub fn data_type(&self) -> DataType {
92 match self {
93 Self::Text(_) => DataType::Text,
94 Self::Number(_) => DataType::Number,
95 Self::Bool(_) => DataType::Bool,
96 Self::Date(_) => DataType::Date,
97 Self::Category(_) => DataType::Category,
98 Self::Null => DataType::Text,
99 }
100 }
101}
102
103impl From<String> for DataValue {
104 fn from(value: String) -> Self {
105 Self::Text(value)
106 }
107}
108
109impl From<&str> for DataValue {
110 fn from(value: &str) -> Self {
111 Self::Text(value.to_string())
112 }
113}
114
115impl From<f64> for DataValue {
116 fn from(value: f64) -> Self {
117 Self::Number(value)
118 }
119}
120
121impl From<bool> for DataValue {
122 fn from(value: bool) -> Self {
123 Self::Bool(value)
124 }
125}
126
127pub fn text_map_from_strings(cells: HashMap<String, String>) -> HashMap<String, DataValue> {
129 cells
130 .into_iter()
131 .map(|(k, v)| (k, DataValue::Text(v)))
132 .collect()
133}
134
135#[cfg(test)]
136mod tests {
137 use super::*;
138
139 #[test]
140 fn number_sort_order() {
141 let two = DataValue::Number(2.0);
142 let ten = DataValue::Number(10.0);
143 assert_eq!(
144 two.partial_cmp_typed(&ten, CompareHint::Number),
145 Some(Ordering::Less)
146 );
147 }
148
149 #[test]
150 fn null_sorts_last() {
151 let text = DataValue::Text("a".into());
152 let null = DataValue::Null;
153 assert_eq!(
154 text.partial_cmp_typed(&null, CompareHint::Text),
155 Some(Ordering::Less)
156 );
157 }
158}