Skip to main content

tiller_sync/model/
mod.rs

1//! Types that represent the core data model, such as `Transaction` and `Category`.
2mod amount;
3mod auto_cat;
4mod category;
5mod date;
6mod items;
7mod mapping;
8mod row_col;
9mod transaction;
10
11pub use amount::{Amount, AmountFormat};
12pub use auto_cat::{AutoCat, AutoCatUpdates, AutoCats};
13pub use category::{Categories, Category, CategoryUpdates};
14pub use date::Date;
15pub(crate) use date::{DateFromOpt, DateFromOptStr, DateToSheetStr};
16pub(crate) use items::Item;
17pub(crate) use mapping::Mapping;
18pub(crate) use row_col::RowCol;
19use serde::{Deserialize, Serialize};
20pub use transaction::{Transaction, TransactionColumn, TransactionUpdates, Transactions};
21
22/// Represents all the sheets of interest from a tiller Google sheet.
23#[derive(Default, Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
24#[serde(rename_all = "snake_case")]
25pub struct TillerData {
26    // TODO: make these private again
27    /// Rows of data from the Transactions sheet.
28    pub(crate) transactions: Transactions,
29    /// Rows of data from the Categories sheet.
30    pub(crate) categories: Categories,
31    /// Rows of data from the AutoCat sheet.
32    pub(crate) auto_cats: AutoCats,
33}
34
35impl TillerData {
36    /// Returns true if any of the sheets contain formulas.
37    pub(crate) fn has_formulas(&self) -> bool {
38        !self.transactions.formulas().is_empty()
39            || !self.categories.formulas().is_empty()
40            || !self.auto_cats.formulas().is_empty()
41    }
42
43    /// Checks if any of the sheets have gaps in their `original_order` sequences.
44    ///
45    /// Gaps indicate deleted rows (e.g., sequence 0, 1, 3 is missing 2).
46    /// This is important for formula preservation since formulas are position-dependent.
47    pub(crate) fn has_original_order_gaps(&self) -> bool {
48        // Check transactions
49        if Self::check_gaps(self.transactions.data().iter().map(|t| t.original_order)) {
50            return true;
51        }
52        // Check categories
53        if Self::check_gaps(self.categories.data().iter().map(|c| c.original_order)) {
54            return true;
55        }
56        // Check autocat
57        if Self::check_gaps(self.auto_cats.data().iter().map(|c| c.original_order)) {
58            return true;
59        }
60        false
61    }
62
63    /// Helper to check for gaps in a sequence of original_order values.
64    fn check_gaps(orders: impl Iterator<Item = Option<u64>>) -> bool {
65        let mut orders: Vec<u64> = orders.flatten().collect();
66
67        if orders.is_empty() {
68            return false;
69        }
70
71        orders.sort();
72
73        for (i, &order) in orders.iter().enumerate() {
74            if order != i as u64 {
75                return true;
76            }
77        }
78
79        false
80    }
81}