1extern crate nom;2extern crate time;34mod cpp;5pub mod error;6mod format;7mod util;8mod weekday;910use std::convert;11use std::path;1213use crate::error::Error;14use crate::format::*;1516////////////////////////////////////////////////////////////////////////1718///19pub type Day = u8; // Day of the month20pub type Year = i32;2122#[derive(Copy, Clone, Debug, PartialEq, PartialOrd)]23pub enum WeekOffsetAmount {24 First = 1,25 Second,26 Third,27 Fourth,28 Fifth,29}3031impl TryFrom<usize> for WeekOffsetAmount {32 type Error = ();3334 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}4546#[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,5051 // Offset in weeks.52 amount: WeekOffsetAmount,53}5455impl TryFrom<i8> for WeekOffset {56 type Error = ();5758 fn try_from(v: i8) -> Result<Self, Self::Error> {59 let amount: usize = v.abs() as usize;60 let amount: WeekOffsetAmount = amount.try_into()?;6162 Ok(WeekOffset {63 from_start: v > 0,64 amount: amount,65 })66 }67}6869impl 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 - 174 } else {75 if off > days.len() {76 return None;77 }78 days.len() - off79 };8081 if idx >= days.len() {82 None83 } else {84 Some(days[idx])85 }86 }87}8889///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}9899impl 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 false107 } 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}120121/// 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}128129impl 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}137138////////////////////////////////////////////////////////////////////////139140pub 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}149150////////////////////////////////////////////////////////////////////////151152#[cfg(test)]153mod tests {154 use super::*;155 use time::macros::date;156157 #[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)));162163 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)));166167 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)));170171 // Maximum negative172 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)));175176 // Maximum positive177 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}