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}