1use std::borrow::Cow;
4use std::cmp::Ordering;
5use std::fmt;
6use std::rc::Rc;
7
8use ravenrs_reactive::State;
9
10use crate::AnyView;
11use crate::date::Date;
12
13#[derive(Clone, Debug, Default, PartialEq)]
15#[non_exhaustive]
16#[cfg_attr(feature = "live", ravenrs_live::expose(part = "variants"))]
17pub enum Value {
18 #[default]
20 Empty,
21 Text(String),
23 Number(f64),
25 Bool(bool),
27 Date(Date),
29}
30
31#[cfg_attr(feature = "live", ravenrs_live::expose)]
32impl Value {
33 pub fn as_f64(&self) -> Option<f64> {
35 match self {
36 Value::Number(n) => Some(*n),
37 Value::Bool(b) => Some(f64::from(u8::from(*b))),
38 _ => None,
39 }
40 }
41
42 pub fn as_date(&self) -> Option<Date> {
44 if let Value::Date(date) = self { Some(*date) } else { None }
45 }
46
47 pub fn key_text(&self) -> String {
50 match self {
51 Value::Empty => String::new(),
52 Value::Text(text) => text.clone(),
53 Value::Number(n) => n.to_string(),
54 Value::Bool(b) => b.to_string(),
55 Value::Date(date) => date.to_string(),
56 }
57 }
58
59 pub fn is_empty(&self) -> bool {
61 matches!(self, Value::Empty)
62 }
63}
64
65impl From<Date> for Value {
66 fn from(date: Date) -> Self {
67 Value::Date(date)
68 }
69}
70impl From<&str> for Value {
71 fn from(s: &str) -> Self {
72 Value::Text(s.to_owned())
73 }
74}
75impl From<String> for Value {
76 fn from(s: String) -> Self {
77 Value::Text(s)
78 }
79}
80impl From<f64> for Value {
81 fn from(n: f64) -> Self {
82 Value::Number(n)
83 }
84}
85impl From<f32> for Value {
86 fn from(n: f32) -> Self {
87 Value::Number(f64::from(n))
88 }
89}
90impl From<i64> for Value {
91 fn from(n: i64) -> Self {
92 Value::Number(n as f64)
93 }
94}
95impl From<i32> for Value {
96 fn from(n: i32) -> Self {
97 Value::Number(f64::from(n))
98 }
99}
100impl From<u32> for Value {
101 fn from(n: u32) -> Self {
102 Value::Number(f64::from(n))
103 }
104}
105impl From<usize> for Value {
106 fn from(n: usize) -> Self {
107 Value::Number(n as f64)
108 }
109}
110impl From<bool> for Value {
111 fn from(b: bool) -> Self {
112 Value::Bool(b)
113 }
114}
115
116pub type Row = Vec<Value>;
118
119#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
121#[cfg_attr(feature = "live", ravenrs_live::expose(part = "variants"))]
122pub enum SortDirection {
123 Ascending,
125 Descending,
127}
128
129#[cfg_attr(feature = "live", ravenrs_live::expose)]
130impl SortDirection {
131 pub(crate) fn apply(self, o: Ordering) -> Ordering {
132 match self {
133 SortDirection::Ascending => o,
134 SortDirection::Descending => o.reverse(),
135 }
136 }
137}
138
139#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
141#[non_exhaustive]
142#[cfg_attr(feature = "live", ravenrs_live::expose(part = "variants"))]
143pub enum Summary {
144 Count,
146 Sum,
148 Average,
150 Min,
152 Max,
154}
155
156#[cfg_attr(feature = "live", ravenrs_live::expose)]
157impl Summary {
158 pub fn label(self) -> &'static str {
160 match self {
161 Summary::Count => "Count",
162 Summary::Sum => "Sum",
163 Summary::Average => "Avg",
164 Summary::Min => "Min",
165 Summary::Max => "Max",
166 }
167 }
168}
169
170#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
172#[non_exhaustive]
173#[cfg_attr(feature = "live", ravenrs_live::expose(part = "variants"))]
174pub enum CellAlign {
175 #[default]
177 Start,
178 Center,
180 End,
182}
183
184#[derive(Clone, Debug, PartialEq)]
186#[cfg_attr(feature = "live", ravenrs_live::expose(part = "fields"))]
187pub struct NumberFormat {
188 pub decimals: usize,
190 pub prefix: String,
192 pub suffix: String,
194 pub thousands: char,
196 pub decimal: char,
198}
199
200impl Default for NumberFormat {
201 fn default() -> Self {
202 Self { decimals: 0, prefix: String::new(), suffix: String::new(), thousands: ',', decimal: '.' }
203 }
204}
205
206#[cfg_attr(feature = "live", ravenrs_live::expose)]
207impl NumberFormat {
208 pub fn format(&self, n: f64) -> String {
210 if !n.is_finite() {
211 return String::new();
212 }
213 ravenrs_criteria::NumberStyle {
214 decimals: self.decimals,
215 group: Some(self.thousands),
216 decimal: self.decimal,
217 prefix: &self.prefix,
218 suffix: &self.suffix,
219 }
220 .format(n)
221 }
222}
223
224pub type CellFormat = Rc<dyn Fn(&Value) -> String>;
226pub type CellTemplate = Rc<dyn Fn(&Value) -> AnyView>;
228
229#[derive(Clone)]
231#[cfg_attr(feature = "live", ravenrs_live::expose(part = "fields"))]
232pub struct GridColumn {
233 pub(crate) title: String,
234 pub(crate) field: Option<String>,
235 pub(crate) width: f32,
236 pub(crate) min_width: f32,
237 pub(crate) align: CellAlign,
238 pub(crate) number: Option<NumberFormat>,
239 pub(crate) summary: Option<Summary>,
240 pub(crate) sortable: bool,
241 pub(crate) filterable: bool,
242 pub(crate) groupable: bool,
243 pub(crate) editable: bool,
244 pub(crate) visible: bool,
245 pub(crate) fill: bool,
246 pub(crate) pinned: bool,
247 pub(crate) resizable: bool,
248 pub(crate) formula: Option<String>,
249 pub(crate) format: Option<CellFormat>,
250 pub(crate) template: Option<CellTemplate>,
251}
252
253impl fmt::Debug for GridColumn {
254 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
255 f.debug_struct("GridColumn")
256 .field("title", &self.title)
257 .field("width", &self.width)
258 .finish_non_exhaustive()
259 }
260}
261
262#[cfg_attr(feature = "live", ravenrs_live::expose)]
263impl GridColumn {
264 pub fn text(title: impl Into<String>) -> Self {
266 Self {
267 title: title.into(),
268 field: None,
269 width: 160.0,
270 min_width: 60.0,
271 align: CellAlign::Start,
272 number: None,
273 summary: None,
274 sortable: true,
275 filterable: true,
276 groupable: true,
277 editable: false,
278 visible: true,
279 fill: false,
280 pinned: false,
281 resizable: true,
282 formula: None,
283 format: None,
284 template: None,
285 }
286 }
287
288 pub fn number(title: impl Into<String>) -> Self {
290 Self {
291 align: CellAlign::End,
292 number: Some(NumberFormat::default()),
293 width: 120.0,
294 ..Self::text(title)
295 }
296 }
297
298 pub fn currency(title: impl Into<String>, prefix: &str, decimals: usize) -> Self {
300 let format = NumberFormat { decimals, prefix: prefix.to_owned(), ..NumberFormat::default() };
301 Self { number: Some(format), ..Self::number(title) }
302 }
303
304 pub fn boolean(title: impl Into<String>) -> Self {
306 Self { align: CellAlign::Center, width: 100.0, ..Self::text(title) }
307 }
308
309 pub fn date(title: impl Into<String>) -> Self {
311 Self { width: 120.0, ..Self::text(title) }
312 }
313
314 pub fn field(mut self, field: impl Into<String>) -> Self {
316 self.field = Some(field.into());
317 self
318 }
319
320 pub fn field_name(&self) -> Option<&str> {
322 self.field.as_deref()
323 }
324
325 pub fn width(mut self, width: f32) -> Self {
327 self.width = width.max(self.min_width);
328 self
329 }
330
331 pub fn resizable(mut self, resizable: bool) -> Self {
335 self.resizable = resizable;
336 self
337 }
338
339 pub fn align(mut self, align: CellAlign) -> Self {
341 self.align = align;
342 self
343 }
344
345 pub fn number_format(mut self, format: NumberFormat) -> Self {
347 self.number = Some(format);
348 self
349 }
350
351 pub fn summary(mut self, summary: Summary) -> Self {
353 self.summary = Some(summary);
354 self
355 }
356
357 pub fn sortable(mut self, sortable: bool) -> Self {
359 self.sortable = sortable;
360 self
361 }
362
363 pub fn filterable(mut self, filterable: bool) -> Self {
365 self.filterable = filterable;
366 self
367 }
368
369 pub fn groupable(mut self, groupable: bool) -> Self {
371 self.groupable = groupable;
372 self
373 }
374
375 pub fn editable(mut self) -> Self {
377 self.editable = true;
378 self
379 }
380
381 pub fn summary_formula(mut self, formula: impl Into<String>) -> Self {
385 self.formula = Some(formula.into());
386 self
387 }
388
389 pub fn pinned(mut self, pinned: bool) -> Self {
391 self.pinned = pinned;
392 self
393 }
394
395 pub fn fill(mut self) -> Self {
398 self.fill = true;
399 self
400 }
401
402 pub fn hidden(mut self) -> Self {
404 self.visible = false;
405 self
406 }
407
408 pub fn format(mut self, f: impl Fn(&Value) -> String + 'static) -> Self {
410 self.format = Some(Rc::new(f));
411 self
412 }
413
414 pub fn template(mut self, f: impl Fn(&Value) -> AnyView + 'static) -> Self {
416 self.template = Some(Rc::new(f));
417 self
418 }
419
420 pub fn title(&self) -> &str {
422 &self.title
423 }
424
425 pub fn display(&self, value: &Value) -> String {
427 self.display_cow(value).into_owned()
428 }
429
430 pub(crate) fn display_cow<'a>(&self, value: &'a Value) -> Cow<'a, str> {
432 if let Some(f) = &self.format {
433 return Cow::Owned(f(value));
434 }
435 match value {
436 Value::Empty => Cow::Borrowed(""),
437 Value::Text(s) => Cow::Borrowed(s),
438 Value::Number(n) => {
439 Cow::Owned(self.number.as_ref().map_or_else(|| n.to_string(), |f| f.format(*n)))
440 }
441 Value::Bool(b) => Cow::Borrowed(if *b { "Yes" } else { "No" }),
442 Value::Date(date) => Cow::Owned(date.to_string()),
443 }
444 }
445
446 pub fn display_summary(&self, summary: Summary, value: f64) -> String {
448 match summary {
449 Summary::Count => NumberFormat::default().format(value),
450 _ => match &self.number {
451 Some(f) => f.format(value),
452 None => NumberFormat { decimals: 2, ..NumberFormat::default() }.format(value),
453 },
454 }
455 }
456
457 pub fn parse(&self, text: &str, previous: &Value) -> Value {
459 let t = text.trim();
460 match previous {
461 Value::Number(_) | Value::Empty if self.number.is_some() => {
462 let f = self.number.clone().unwrap_or_default();
463 let cleaned: String = t
464 .trim_start_matches(f.prefix.as_str())
465 .trim_end_matches(f.suffix.as_str())
466 .chars()
467 .filter(|c| *c != f.thousands)
468 .map(|c| if c == f.decimal { '.' } else { c })
469 .collect();
470 cleaned.trim().parse::<f64>().map_or(previous.clone(), Value::Number)
471 }
472 Value::Date(_) if !t.is_empty() => parse_date(t).map_or(previous.clone(), Value::Date),
473 Value::Bool(_) => {
474 Value::Bool(matches!(t.to_lowercase().as_str(), "yes" | "true" | "1" | "y" | "sim"))
475 }
476 _ if t.is_empty() => Value::Empty,
477 _ => Value::Text(t.to_owned()),
478 }
479 }
480}
481
482fn parse_date(text: &str) -> Option<Date> {
484 let number = |part: &str| part.trim().parse::<u32>().ok();
485 if let Some((year, rest)) = text.split_once('-') {
486 let (month, day) = rest.split_once('-')?;
487 return Date::new(year.parse().ok()?, number(month)? as u8, number(day)? as u8);
488 }
489 let (day, rest) = text.split_once('/')?;
490 let (month, year) = rest.split_once('/')?;
491 Date::new(year.trim().parse().ok()?, number(month)? as u8, number(day)? as u8)
492}
493
494#[derive(Clone, Debug)]
497#[cfg_attr(feature = "live", ravenrs_live::expose(part = "fields"))]
498pub struct GridData {
499 rows: State<Rc<Vec<Row>>>,
500}
501
502#[cfg_attr(feature = "live", ravenrs_live::expose)]
503impl GridData {
504 pub fn new(rows: Vec<Row>) -> Self {
506 Self { rows: State::new(Rc::new(rows)) }
507 }
508
509 pub fn rows(&self) -> Rc<Vec<Row>> {
511 self.rows.get()
512 }
513
514 pub fn len(&self) -> usize {
516 self.rows.peek().len()
517 }
518
519 pub fn is_empty(&self) -> bool {
521 self.len() == 0
522 }
523
524 pub fn set_cell(&self, row: usize, column: usize, value: Value) {
526 self.rows.update(|rows| {
527 if let Some(cell) = Rc::make_mut(rows).get_mut(row).and_then(|r| r.get_mut(column)) {
528 *cell = value;
529 }
530 });
531 }
532
533 pub fn push(&self, row: Row) {
535 self.rows.update(|rows| Rc::make_mut(rows).push(row));
536 }
537
538 pub fn insert(&self, index: usize, row: Row) {
540 self.rows.update(|rows| {
541 let rows = Rc::make_mut(rows);
542 rows.insert(index.min(rows.len()), row);
543 });
544 }
545
546 pub fn remove(&self, index: usize) -> Option<Row> {
548 let mut removed = None;
549 self.rows.update(|rows| {
550 let rows = Rc::make_mut(rows);
551 if index < rows.len() {
552 removed = Some(rows.remove(index));
553 }
554 });
555 removed
556 }
557
558 pub fn replace(&self, rows: Vec<Row>) {
560 self.rows.set(Rc::new(rows));
561 }
562}
563
564#[cfg(test)]
565mod tests {
566 use super::*;
567
568 #[test]
569 fn number_format_groups_and_rounds() {
570 let f = NumberFormat {
571 decimals: 2,
572 prefix: "R$ ".into(),
573 thousands: '.',
574 decimal: ',',
575 ..Default::default()
576 };
577 assert_eq!(f.format(1234567.891), "R$ 1.234.567,89");
578 assert_eq!(f.format(-12.5), "-R$ 12,50");
579 assert_eq!(NumberFormat::default().format(999.0), "999");
580 assert_eq!(NumberFormat::default().format(1000.0), "1,000");
581 assert_eq!(NumberFormat::default().format(-0.2), "0");
582 }
583
584 #[test]
585 fn parse_round_trips_formatted_numbers() {
586 let col = GridColumn::currency("MRR", "$", 2);
587 assert_eq!(col.parse("$1,234.50", &Value::Number(0.0)), Value::Number(1234.5));
588 assert_eq!(
589 col.parse("oops", &Value::Number(7.0)),
590 Value::Number(7.0),
591 "invalid input keeps the old value"
592 );
593 let due = GridColumn::date("Due");
594 let day = Date::new(2026, 11, 2).map(Value::Date).unwrap_or_default();
595 assert_eq!(due.parse("2026-11-02", &Value::Date(Date::from_days(0))), day);
596 assert_eq!(due.parse("02/11/2026", &Value::Date(Date::from_days(0))), day);
597 assert_eq!(due.parse("soon", &day), day, "invalid input keeps the old value");
598 assert_eq!(due.display(&day), "2026-11-02");
599 assert_eq!(GridColumn::boolean("Active").parse("yes", &Value::Bool(false)), Value::Bool(true));
600 }
601}