1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
//! Handling of habit databases.
mod sqlite_datafile;
use anyhow::Result;
use chrono::{Duration, NaiveDate};
use std::path::Path;

/// Format of the dates used in the program.
pub const DATE_FORMAT: &str = "%Y-%m-%d";

/// Result of an update to a `DiaryDataConnection` instance.
pub enum SuccessfulUpdate {
    /// The new date was not present in the instance, but it was added.
    AddedNew,

    /// The date was already present in the instance, but was replaced.
    ReplacedExisting,
}

/// Result from the call to `add_category`
#[derive(Debug, PartialEq)]
pub enum AddCategoryResult {
    /// Created a new category
    AddedNew,

    /// Made a previously hidden category visible again
    Unhide,

    /// The category is already present and is visible
    AlreadyPresent,
}

/// Result from the call to `hide_category`
#[derive(Debug, PartialEq)]
pub enum HideCategoryResult {
    /// The specified category was visible previously and was hidden
    Hidden,

    /// The specified category is already hidden, nothing was changed
    AlreadyHidden,

    /// The specified category does not exist
    NonExistingCategory,
}

/// Represents a connection to the diary database.
pub trait DiaryDataConnection {
    fn into_any(self: Box<Self>) -> Box<dyn std::any::Any>;

    /// Calculates the occurences of all habits over multiple periods of date ranges.
    fn calculate_data_counts_per_iter(
        &self,
        date_ranges: &[(NaiveDate, NaiveDate)],
    ) -> Result<Vec<Vec<usize>>>;

    /// Modifies the provided `DiaryDataConnection` instance with the provided data row and date.
    fn update_data(&mut self, date: &NaiveDate, new_row: &[usize]) -> Result<SuccessfulUpdate>;

    /// Modifies the provided `DiaryDataConnection` instance with the provided row-date pairs.
    fn update_data_batch(&mut self, new_items: &[(NaiveDate, Vec<usize>)]) -> Result<()>;

    /// Returns a vector of missing dates between the first date in the database until specified date.
    fn get_missing_dates(
        &self,
        from: &Option<NaiveDate>,
        until: &NaiveDate,
    ) -> Result<Vec<NaiveDate>>;

    /// Get the list of habits tracked by the database.
    fn get_header(&self) -> Result<Vec<(String, usize)>>;

    /// Gets the category IDs of the habits associated with the specified date,
    /// or None, if the date is not present in the database.
    fn get_row(&self, date: &NaiveDate) -> Result<Option<Vec<usize>>>;

    /// Gets the category IDs of habits of dates that are no earlier as `from` and not more recent
    /// than `until`. If the date is not in the database, None is returned for the specified date.
    /// The rows are returned in date-descending order.
    fn get_rows(&self, from: &NaiveDate, until: &NaiveDate) -> Result<Vec<Option<Vec<usize>>>>;

    /// Returns if the database contains any records.
    fn is_empty(&self) -> Result<bool>;

    /// Returns the earliest and latest date among the database records.
    fn get_date_range(&self) -> Result<(NaiveDate, NaiveDate)>;

    /// Adds or unhides the specified category in the database.
    fn add_category(&self, name: &str) -> Result<AddCategoryResult>;

    /// Hides the specified category in the database.
    fn hide_category(&self, name: &str) -> Result<HideCategoryResult>;

    /// Returns the most frequent day "signatures" in the specified date interval (inclusive).
    fn get_most_frequent_daily_data(
        &self,
        from: &Option<NaiveDate>,
        until: &NaiveDate,
        max_count: Option<usize>,
    ) -> Result<Vec<(Vec<usize>, usize)>>;
}

/// Tries to read data file to memory.
pub fn open_datafile(path: &Path) -> Result<Box<dyn DiaryDataConnection>> {
    sqlite_datafile::open_sqlite_datafile(path)
}

/// Calculates the date ranges according to the parameters.
/// For example when `range_size == 30`, `iters == 3` and `from_date` is today,
/// the result is a 3-element vector containing ranges of the last 30 days,
/// the 30 days before that, and the 30 days before the latter one.
pub fn get_date_ranges(
    from_date: &NaiveDate,
    range_size: usize,
    iters: usize,
) -> Vec<(NaiveDate, NaiveDate)> {
    let start_offsets = (0..range_size * iters).step_by(range_size);
    let end_offsets = (range_size - 1..range_size * (iters + 1)).step_by(range_size);
    start_offsets
        .zip(end_offsets)
        .map(|(start, end)| {
            (
                *from_date - Duration::try_days(start as i64).unwrap(),
                *from_date - Duration::try_days(end as i64).unwrap(),
            )
        })
        .collect()
}

/// Create a new database on the prescribed path, using the prescribed headers.
pub fn create_new_datafile(path: &Path, headers: &[String]) -> Result<()> {
    sqlite_datafile::create_new_sqlite(path, headers)?;
    Ok(())
}

#[test]
fn test_get_date_ranges() {
    let result = get_date_ranges(&NaiveDate::from_ymd_opt(2000, 5, 30).unwrap(), 5, 3);
    assert_eq!(
        vec![
            (
                NaiveDate::from_ymd_opt(2000, 5, 30).unwrap(),
                NaiveDate::from_ymd_opt(2000, 5, 26).unwrap()
            ),
            (
                NaiveDate::from_ymd_opt(2000, 5, 25).unwrap(),
                NaiveDate::from_ymd_opt(2000, 5, 21).unwrap()
            ),
            (
                NaiveDate::from_ymd_opt(2000, 5, 20).unwrap(),
                NaiveDate::from_ymd_opt(2000, 5, 16).unwrap()
            ),
        ],
        result
    );
}