ncalendar

Reimplementation of the BSD calendar(1) program in Rust

git clone https://git.8pit.net/ncalendar.git

  1extern crate nom;
  2extern crate time;
  3
  4mod cpp;
  5pub mod error;
  6mod format;
  7mod util;
  8mod weekday;
  9
 10use std::convert;
 11use std::path;
 12
 13use crate::error::Error;
 14use crate::format::*;
 15
 16////////////////////////////////////////////////////////////////////////
 17
 18///
 19pub type Day = u8; // Day of the month
 20pub type Year = i32;
 21
 22#[derive(Copy, Clone, Debug, PartialEq, PartialOrd)]
 23pub enum WeekOffsetAmount {
 24    First = 1,
 25    Second,
 26    Third,
 27    Fourth,
 28    Fifth,
 29}
 30
 31impl TryFrom<usize> for WeekOffsetAmount {
 32    type Error = ();
 33
 34    fn try_from(v: usize) -> Result<Self, Self::Error> {
 35        match v {
 36            1 => Ok(WeekOffsetAmount::First),
 37            2 => Ok(WeekOffsetAmount::Second),
 38            3 => Ok(WeekOffsetAmount::Third),
 39            4 => Ok(WeekOffsetAmount::Fourth),
 40            5 => Ok(WeekOffsetAmount::Fifth),
 41            _ => Err(()),
 42        }
 43    }
 44}
 45
 46#[derive(Debug, PartialEq)]
 47pub struct WeekOffset {
 48    // Whether the offset is relative to the start or the end of the month.
 49    from_start: bool,
 50
 51    // Offset in weeks.
 52    amount: WeekOffsetAmount,
 53}
 54
 55impl TryFrom<i8> for WeekOffset {
 56    type Error = ();
 57
 58    fn try_from(v: i8) -> Result<Self, Self::Error> {
 59        let amount: usize = v.abs() as usize;
 60        let amount: WeekOffsetAmount = amount.try_into()?;
 61
 62        Ok(WeekOffset {
 63            from_start: v > 0,
 64            amount: amount,
 65        })
 66    }
 67}
 68
 69impl WeekOffset {
 70    pub fn get(&self, days: Vec<time::Date>) -> Option<time::Date> {
 71        let off = self.amount as usize;
 72        let idx = if self.from_start {
 73            off - 1
 74        } else {
 75            if off > days.len() {
 76                return None;
 77            }
 78            days.len() - off
 79        };
 80
 81        if idx >= days.len() {
 82            None
 83        } else {
 84            Some(days[idx])
 85        }
 86    }
 87}
 88
 89///
 90#[derive(Debug, PartialEq)]
 91pub enum Reminder {
 92    Weekly(time::Weekday),
 93    SemiWeekly(time::Weekday, WeekOffset),
 94    Monthly(Day, Option<Year>),
 95    Yearly(Day, time::Month),
 96    Date(time::Date),
 97}
 98
 99impl Reminder {
100    pub fn matches(&self, date: time::Date) -> bool {
101        match self {
102            Reminder::Weekly(wday) => date.weekday() == *wday,
103            Reminder::SemiWeekly(wday, off) => weekday::filter(date.year(), date.month(), *wday)
104                .map(|wdays| -> bool {
105                    if date.weekday() != *wday {
106                        false
107                    } else {
108                        off.get(wdays) == Some(date)
109                    }
110                })
111                .unwrap_or(false),
112            Reminder::Monthly(day, year) => {
113                date.day() == *day && year.map(|y| date.year() == y).unwrap_or(true)
114            }
115            Reminder::Yearly(day, mon) => date.month() == *mon && date.day() == *day,
116            Reminder::Date(d) => date == *d,
117        }
118    }
119}
120
121/// Represents a single appointment from the calendar file.
122#[derive(Debug, PartialEq)]
123pub struct Entry {
124    pub day: Reminder,
125    pub desc: String,
126    //pub time: time::Time,
127}
128
129impl Entry {
130    pub fn is_fixed(&self) -> bool {
131        match self.day {
132            Reminder::SemiWeekly(_, _) | Reminder::Weekly(_) | Reminder::Monthly(_, _) => false,
133            _ => true,
134        }
135    }
136}
137
138////////////////////////////////////////////////////////////////////////
139
140pub fn parse_file<'a, P: convert::AsRef<path::Path>>(fp: P) -> Result<Vec<Entry>, Error> {
141    let out = cpp::preprocess(fp)?;
142    let (input, entries) = parse_entries(&out)?;
143    if input != "" {
144        Err(Error::IncompleteParse)
145    } else {
146        Ok(entries)
147    }
148}
149
150////////////////////////////////////////////////////////////////////////
151
152#[cfg(test)]
153mod tests {
154    use super::*;
155    use time::macros::date;
156
157    #[test]
158    fn match_semiweekly() {
159        let rem0 = Reminder::SemiWeekly(time::Weekday::Friday, 1.try_into().unwrap());
160        assert!(rem0.matches(date!(2023 - 12 - 01)));
161        assert!(rem0.matches(date!(2023 - 07 - 07)));
162
163        let rem1 = Reminder::SemiWeekly(time::Weekday::Monday, 2.try_into().unwrap());
164        assert!(rem1.matches(date!(2023 - 02 - 13)));
165        assert!(!rem1.matches(date!(2023 - 02 - 06)));
166
167        let rem2 = Reminder::SemiWeekly(time::Weekday::Sunday, 4.try_into().unwrap());
168        assert!(rem2.matches(date!(2023 - 02 - 26)));
169        assert!(!rem2.matches(date!(2023 - 02 - 05)));
170
171        // Maximum negative
172        let rem3 = Reminder::SemiWeekly(time::Weekday::Tuesday, (-5).try_into().unwrap());
173        assert!(rem3.matches(date!(2023 - 05 - 02)));
174        assert!(rem3.matches(date!(2023 - 08 - 01)));
175
176        // Maximum positive
177        let rem4 = Reminder::SemiWeekly(time::Weekday::Tuesday, 5.try_into().unwrap());
178        assert!(rem4.matches(date!(2023 - 05 - 30)));
179        assert!(rem4.matches(date!(2023 - 08 - 29)));
180    }
181}