ncalendar

Reimplementation of the BSD calendar(1) program in Rust

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

  1use crate::util::*;
  2use crate::*;
  3
  4use nom::{
  5    branch::alt,
  6    character::complete::{char, line_ending, not_line_ending, one_of},
  7    combinator::{map_res, opt},
  8    multi::many0,
  9    sequence::{preceded, terminated, tuple},
 10    IResult,
 11};
 12use std::num::TryFromIntError;
 13
 14////////////////////////////////////////////////////////////////////////
 15
 16fn parse_weekday(input: &str) -> IResult<&str, time::Weekday> {
 17    alt((
 18        str("Monday", "Mon", time::Weekday::Monday),
 19        str("Tuesday", "Tue", time::Weekday::Tuesday),
 20        str("Wednesday", "Wed", time::Weekday::Wednesday),
 21        str("Thursday", "Thu", time::Weekday::Thursday),
 22        str("Friday", "Fri", time::Weekday::Friday),
 23        str("Saturday", "Sat", time::Weekday::Saturday),
 24        str("Sunday", "Sun", time::Weekday::Sunday),
 25    ))(input)
 26}
 27
 28fn parse_offset(input: &str) -> IResult<&str, WeekOffset> {
 29    let (input, prefix) = one_of("+-")(input)?;
 30    let (input, amount) = alt((
 31        bind(char('1'), WeekOffsetAmount::First),
 32        bind(char('2'), WeekOffsetAmount::Second),
 33        bind(char('3'), WeekOffsetAmount::Third),
 34        bind(char('4'), WeekOffsetAmount::Fourth),
 35        bind(char('5'), WeekOffsetAmount::Fifth),
 36    ))(input)?;
 37
 38    Ok((
 39        input,
 40        WeekOffset {
 41            from_start: prefix == '+',
 42            amount: amount,
 43        },
 44    ))
 45}
 46
 47fn parse_month_str(input: &str) -> IResult<&str, time::Month> {
 48    alt((
 49        str("January", "Jan", time::Month::January),
 50        str("February", "Feb", time::Month::February),
 51        str("March", "Mar", time::Month::March),
 52        str("April", "Apr", time::Month::April),
 53        str("May", "May", time::Month::May),
 54        str("June", "Jun", time::Month::June),
 55        str("July", "Jul", time::Month::July),
 56        str("August", "Aug", time::Month::August),
 57        str("September", "Sep", time::Month::September),
 58        str("October", "Oct", time::Month::October),
 59        str("November", "Nov", time::Month::November),
 60        str("December", "Dec", time::Month::December),
 61    ))(input)
 62}
 63
 64fn parse_month_num(input: &str) -> IResult<&str, time::Month> {
 65    map_res(
 66        digits,
 67        |n| -> Result<time::Month, time::error::ComponentRange> {
 68            // XXX: If there is a u32 → u8 conversion error then use 0xff
 69            // as the month value which will result in a ComponentRange error.
 70            // Unfourtunately, can't create a ComponentRange directly and use .map_err().
 71            let m: u8 = n.try_into().unwrap_or(0xff);
 72            time::Month::try_from(m)
 73        },
 74    )(input)
 75}
 76
 77fn parse_month(input: &str) -> IResult<&str, time::Month> {
 78    alt((parse_month_str, parse_month_num))(input)
 79}
 80
 81fn parse_day(input: &str) -> IResult<&str, Day> {
 82    map_res(digits, |n| -> Result<Day, TryFromIntError> { n.try_into() })(input)
 83}
 84
 85fn parse_year(input: &str) -> IResult<&str, Year> {
 86    map_res(digits, |n| -> Result<Year, TryFromIntError> {
 87        n.try_into()
 88    })(input)
 89}
 90
 91fn parse_reminder(input: &str) -> IResult<&str, Reminder> {
 92    alt((
 93        map_res(
 94            tuple((parse_weekday, parse_offset)),
 95            |(wday, off)| -> Result<Reminder, ()> { Ok(Reminder::SemiWeekly(wday, off)) },
 96        ),
 97        map_res(parse_weekday, |wday| -> Result<Reminder, ()> {
 98            Ok(Reminder::Weekly(wday))
 99        }),
100        map_res(
101            tuple((parse_day, ws(char('*')), opt(parse_year))),
102            |(day, _, year)| -> Result<Reminder, ()> { Ok(Reminder::Monthly(day, year)) },
103        ),
104        map_res(
105            tuple((opt(parse_day), ws(parse_month), opt(parse_year))),
106            move |(day, mon, year)| -> Result<Reminder, time::error::ComponentRange> {
107                let day = day.unwrap_or(1);
108                Ok(match year {
109                    Some(y) => Reminder::Date(time::Date::from_calendar_date(y, mon, day)?),
110                    None => Reminder::Yearly(day, mon),
111                })
112            },
113        ),
114    ))(input)
115}
116
117fn parse_desc(input: &str) -> IResult<&str, String> {
118    let (input, (desc, ext)) = tuple((
119        terminated(not_line_ending, line_ending),
120        many0(terminated(
121            preceded(char('\t'), not_line_ending),
122            line_ending,
123        )),
124    ))(input)?;
125
126    if ext.is_empty() {
127        Ok((input, desc.to_string()))
128    } else {
129        Ok((input, desc.to_owned() + "\n\t" + &ext.join("\n\t")))
130    }
131}
132
133fn parse_entry(input: &str) -> IResult<&str, Entry> {
134    let (input, (day, _, desc)) = tuple((parse_reminder, char('\t'), parse_desc))(input)?;
135
136    Ok((input, Entry { day, desc: desc }))
137}
138
139pub fn parse_entries(input: &str) -> IResult<&str, Vec<Entry>> {
140    many0(empty_lines(parse_entry))(input)
141}
142
143////////////////////////////////////////////////////////////////////////
144
145#[cfg(test)]
146mod tests {
147    use super::*;
148    use nom::error::Error;
149    use nom::error::ErrorKind;
150    use nom::Err;
151    use time::macros::date;
152
153    #[test]
154    fn weekday() {
155        assert_eq!(parse_weekday("Monday"), Ok(("", time::Weekday::Monday)));
156        assert_eq!(parse_weekday("Mon"), Ok(("", time::Weekday::Monday)));
157        assert_eq!(parse_weekday("Tuesday"), Ok(("", time::Weekday::Tuesday)));
158    }
159
160    #[test]
161    fn month() {
162        assert_eq!(parse_month("April"), Ok(("", time::Month::April)));
163        assert_eq!(parse_month("04"), Ok(("", time::Month::April)));
164        assert_eq!(parse_month("4"), Ok(("", time::Month::April)));
165        assert_eq!(
166            parse_month("13"),
167            Err(Err::Error(Error::new("13", ErrorKind::MapRes)))
168        );
169        assert_eq!(
170            parse_month("2342"),
171            Err(Err::Error(Error::new("2342", ErrorKind::MapRes)))
172        );
173    }
174
175    #[test]
176    fn reminder() {
177        assert_eq!(
178            parse_reminder("25 Feb"),
179            Ok(("", Reminder::Yearly(25, time::Month::February)))
180        );
181        assert_eq!(
182            parse_reminder("Fri"),
183            Ok(("", Reminder::Weekly(time::Weekday::Friday)))
184        );
185        assert_eq!(
186            parse_reminder("Fri+2"),
187            Ok((
188                "",
189                Reminder::SemiWeekly(time::Weekday::Friday, 2i8.try_into().unwrap())
190            )),
191        );
192        assert_eq!(
193            parse_reminder("Mon-4"),
194            Ok((
195                "",
196                Reminder::SemiWeekly(time::Weekday::Monday, (-4i8).try_into().unwrap())
197            )),
198        );
199        assert_eq!(
200            parse_reminder("Jan 1990"),
201            Ok(("", Reminder::Date(date!(1990 - 01 - 01))))
202        );
203        assert_eq!(
204            parse_reminder("06 July 2020"),
205            Ok(("", Reminder::Date(date!(2020 - 07 - 06))))
206        );
207        assert_eq!(
208            parse_reminder("12 Dec 1950"),
209            Ok(("", Reminder::Date(date!(1950 - 12 - 12))))
210        );
211        assert_eq!(
212            parse_reminder("10 *"),
213            Ok(("", Reminder::Monthly(10, None)))
214        );
215        assert_eq!(
216            parse_reminder("10 * 1989"),
217            Ok(("", Reminder::Monthly(10, Some(1989))))
218        );
219    }
220
221    #[test]
222    fn desc() {
223        assert_eq!(parse_desc("foo bar\n"), Ok(("", "foo bar".to_string())));
224        assert_eq!(
225            parse_desc("foo\n\tbar\n"),
226            Ok(("", "foo\n\tbar".to_string()))
227        );
228        assert_eq!(
229            parse_desc("foo\n\tbar\n\tbaz\n"),
230            Ok(("", "foo\n\tbar\n\tbaz".to_string()))
231        );
232    }
233
234    #[test]
235    fn event() {
236        assert_eq!(
237            parse_entry("12 Mar 2015\tDo some stuff\n"),
238            Ok((
239                "",
240                Entry {
241                    day: Reminder::Date(date!(2015 - 03 - 12)),
242                    desc: "Do some stuff".to_string(),
243                }
244            ))
245        );
246
247        assert_eq!(
248            parse_entry("Mon\tMonday\n"),
249            Ok((
250                "",
251                Entry {
252                    day: Reminder::Weekly(time::Weekday::Monday),
253                    desc: "Monday".to_string(),
254                }
255            ))
256        );
257    }
258}