2
\$\begingroup\$

@angus on Unix.SE implemented a tool in C to list upcoming cron jobs in response to a question. I've re-implemented the basic functionality (showing only the next cron job) in Rust, using rustfmt, clippy and TDD to keep it reasonably clean, but it's my first non-trivial program:

use std::ops::RangeInclusive;
pub struct DateTimeFieldParser {
 range: RangeInclusive<u8>,
 wrap_around_at_end: bool,
}
impl DateTimeFieldParser {
 pub fn new(min: u8, max: u8) -> DateTimeFieldParser {
 DateTimeFieldParser {
 range: min..=max,
 wrap_around_at_end: false,
 }
 }
 pub fn new_with_wrap_around(min: u8, max: u8) -> DateTimeFieldParser {
 DateTimeFieldParser {
 range: min..=max,
 wrap_around_at_end: true,
 }
 }
 pub fn parse_field(&self, string_value: &str) -> Vec<u8> {
 let mut values = Vec::with_capacity((self.range.end() - self.range.start()) as usize);
 string_value
 .split(',')
 .for_each(|part| values.append(&mut self.parse_list_entry(part)));
 values.sort_unstable();
 values.dedup();
 values
 }
 fn parse_list_entry(&self, string_value: &str) -> Vec<u8> {
 let mut parts = string_value.splitn(2, '/');
 let values = parts
 .next()
 .unwrap()
 .replace("*", &format!("{}-{}", self.range.start(), self.range.end() - 1));
 let values = match values.to_lowercase().as_ref() {
 "sun" => "0",
 "jan" | "mon" => "1",
 "feb" | "tue" => "2",
 "mar" | "wed" => "3",
 "apr" | "thu" => "4",
 "may" | "fri" => "5",
 "jun" | "sat" => "6",
 "jul" => "7",
 "aug" => "8",
 "sep" => "9",
 "oct" => "10",
 "nov" => "11",
 "dec" => "12",
 _ => &values,
 };
 let values: RangeInclusive<u8> = self.parse_range(values);
 let step = match parts.next() {
 Some(string_value) => string_value.parse::<u8>().unwrap(),
 None => 1,
 };
 let mut values: Vec<u8> = values.step_by(step as usize).collect();
 let last_value = values.pop().unwrap();
 if last_value == *self.range.end() + 1 && self.wrap_around_at_end {
 values.push(0);
 } else {
 values.push(last_value);
 }
 self.verify_range(values[0]);
 self.verify_range(values[values.len() - 1]);
 values
 }
 fn parse_range(&self, values: &str) -> RangeInclusive<u8> {
 let mut range_or_value = values.splitn(2, '-').map(|part| part.parse().unwrap());
 let first = range_or_value.next().expect("Empty range");
 let last = match range_or_value.next() {
 Some(value) => value,
 None => first,
 };
 first..=last
 }
 fn verify_range(&self, value: u8) {
 assert!(*self.range.start() <= value);
 assert!(*self.range.end() >= value);
 }
}

Tests:

#[cfg(test)]
mod tests {
 use super::DateTimeFieldParser;
 #[test]
 fn should_parse_complex_pattern() {
 let parser = DateTimeFieldParser::new(1, 12);
 assert_eq!(parser.parse_field("5-9/2,1,*/5"), vec![1, 5, 6, 7, 9, 11]);
 }
 #[test]
 fn should_parse_comma_separated_numbers() {
 let parser = DateTimeFieldParser::new(0, 23);
 assert_eq!(parser.parse_field("0,23"), vec![0, 23]);
 }
 #[test]
 fn should_parse_range_with_step() {
 let parser = DateTimeFieldParser::new(0, 23);
 assert_eq!(parser.parse_list_entry("1-7/2"), vec![1, 3, 5, 7]);
 }
 #[test]
 fn should_parse_asterisk() {
 let parser = DateTimeFieldParser::new(1, 12);
 assert_eq!(parser.parse_list_entry("*/4"), vec![1, 5, 9]);
 }
 #[test]
 fn should_parse_january_name() {
 let parser = DateTimeFieldParser::new(1, 12);
 assert_eq!(parser.parse_list_entry("Jan"), vec![1]);
 }
 #[test]
 fn should_parse_february_name() {
 let parser = DateTimeFieldParser::new(1, 12);
 assert_eq!(parser.parse_list_entry("Feb"), vec![2]);
 }
 #[test]
 fn should_parse_march_name() {
 let parser = DateTimeFieldParser::new(1, 12);
 assert_eq!(parser.parse_list_entry("Mar"), vec![3]);
 }
 #[test]
 fn should_parse_april_name() {
 let parser = DateTimeFieldParser::new(1, 12);
 assert_eq!(parser.parse_list_entry("Apr"), vec![4]);
 }
 #[test]
 fn should_parse_may_name() {
 let parser = DateTimeFieldParser::new(1, 12);
 assert_eq!(parser.parse_list_entry("May"), vec![5]);
 }
 #[test]
 fn should_parse_june_name() {
 let parser = DateTimeFieldParser::new(1, 12);
 assert_eq!(parser.parse_list_entry("Jun"), vec![6]);
 }
 #[test]
 fn should_parse_july_name() {
 let parser = DateTimeFieldParser::new(1, 12);
 assert_eq!(parser.parse_list_entry("Jul"), vec![7]);
 }
 #[test]
 fn should_parse_august_name() {
 let parser = DateTimeFieldParser::new(1, 12);
 assert_eq!(parser.parse_list_entry("Aug"), vec![8]);
 }
 #[test]
 fn should_parse_september_name() {
 let parser = DateTimeFieldParser::new(1, 12);
 assert_eq!(parser.parse_list_entry("Sep"), vec![9]);
 }
 #[test]
 fn should_parse_october_name() {
 let parser = DateTimeFieldParser::new(1, 12);
 assert_eq!(parser.parse_list_entry("Oct"), vec![10]);
 }
 #[test]
 fn should_parse_november_name() {
 let parser = DateTimeFieldParser::new(1, 12);
 assert_eq!(parser.parse_list_entry("Nov"), vec![11]);
 }
 #[test]
 fn should_parse_december_name() {
 let parser = DateTimeFieldParser::new(1, 12);
 assert_eq!(parser.parse_list_entry("Dec"), vec![12]);
 }
 #[test]
 fn should_parse_month_name_case_insensitively() {
 let parser = DateTimeFieldParser::new(1, 12);
 assert_eq!(parser.parse_list_entry("dEC"), vec![12]);
 }
 #[test]
 fn should_parse_sunday_name() {
 let parser = DateTimeFieldParser::new_with_wrap_around(0, 6);
 assert_eq!(parser.parse_list_entry("Sun"), vec![0]);
 }
 #[test]
 fn should_parse_monday_name() {
 let parser = DateTimeFieldParser::new_with_wrap_around(0, 6);
 assert_eq!(parser.parse_list_entry("Mon"), vec![1]);
 }
 #[test]
 fn should_parse_tuesday_name() {
 let parser = DateTimeFieldParser::new_with_wrap_around(0, 6);
 assert_eq!(parser.parse_list_entry("Tue"), vec![2]);
 }
 #[test]
 fn should_parse_wednesday_name() {
 let parser = DateTimeFieldParser::new_with_wrap_around(0, 6);
 assert_eq!(parser.parse_list_entry("Wed"), vec![3]);
 }
 #[test]
 fn should_parse_thursday_name() {
 let parser = DateTimeFieldParser::new_with_wrap_around(0, 6);
 assert_eq!(parser.parse_list_entry("Thu"), vec![4]);
 }
 #[test]
 fn should_parse_friday_name() {
 let parser = DateTimeFieldParser::new_with_wrap_around(0, 6);
 assert_eq!(parser.parse_list_entry("Fri"), vec![5]);
 }
 #[test]
 fn should_parse_saturday_name() {
 let parser = DateTimeFieldParser::new_with_wrap_around(0, 6);
 assert_eq!(parser.parse_list_entry("Sat"), vec![6]);
 }
 #[test]
 fn should_parse_week_day_name_case_insensitively() {
 let parser = DateTimeFieldParser::new_with_wrap_around(0, 6);
 assert_eq!(parser.parse_list_entry("sAT"), vec![6]);
 }
 #[test]
 fn should_remove_duplicates() {
 let parser = DateTimeFieldParser::new(1, 2);
 assert_eq!(parser.parse_field("1,1,2,2,2"), vec![1, 2]);
 }
 #[test]
 fn should_sort_values() {
 let parser = DateTimeFieldParser::new(1, 2);
 assert_eq!(parser.parse_field("2,1"), vec![1, 2]);
 }
 #[test]
 fn should_parse_wraparound_sunday() {
 let parser = DateTimeFieldParser::new_with_wrap_around(0, 6);
 assert_eq!(parser.parse_field("7"), vec![0]);
 }
 #[test]
 fn should_parse_week_range_with_sunday_at_end() {
 let parser = DateTimeFieldParser::new_with_wrap_around(0, 6);
 assert_eq!(parser.parse_field("4-7"), vec![0, 4, 5, 6]);
 }
 #[test]
 fn should_parse_week_range_with_sunday_at_both_sides() {
 let parser = DateTimeFieldParser::new_with_wrap_around(0, 6);
 assert_eq!(parser.parse_field("0-7"), vec![0, 1, 2, 3, 4, 5, 6]);
 }
 #[test]
 fn should_parse_range() {
 let parser = DateTimeFieldParser::new(0, 23);
 assert_eq!(parser.parse_range("1-3"), { 1..=3 });
 }
 #[test]
 #[should_panic]
 fn should_fail_verification_below_min() {
 let parser = DateTimeFieldParser::new(1, 12);
 parser.verify_range(0)
 }
 #[test]
 fn should_verify_at_min() {
 let parser = DateTimeFieldParser::new(0, 23);
 parser.verify_range(0);
 }
 #[test]
 fn should_verify_between_min_and_max() {
 let parser = DateTimeFieldParser::new(0, 23);
 parser.verify_range(12);
 }
 #[test]
 fn should_verify_at_both_min_and_max() {
 let parser = DateTimeFieldParser::new(1, 1);
 parser.verify_range(1);
 }
 #[test]
 fn should_verify_at_max() {
 let parser = DateTimeFieldParser::new(0, 23);
 parser.verify_range(23);
 }
 #[test]
 #[should_panic]
 fn should_fail_verification_above_max() {
 let parser = DateTimeFieldParser::new(0, 23);
 parser.verify_range(24);
 }
}
200_success
145k22 gold badges190 silver badges478 bronze badges
asked Sep 22, 2018 at 1:21
\$\endgroup\$

1 Answer 1

1
\$\begingroup\$

I’d go out on a limb and call this Good CodeTM️. I’m easily able to follow all the logic. It’s fairly idiomatic, using map/reduce instead of loops and pattern matching. The tests are all super tight and clear.

The only thing that catches my eye are those [should_panic] annotations. I would expect a parse function to return an Ok/Err Result instead of hard panic. Personally, I prefer to defer decisions about how to handle an error as long as possible and push it as high up into the call stack as I can. Embrace the monad. It’s common in rust to have two variants of a function, one that may panic and one that returns a Result<T>. Then the client code can decide which behavior it wants.

answered Sep 23, 2018 at 11:26
\$\endgroup\$

Your Answer

Draft saved
Draft discarded

Sign up or log in

Sign up using Google
Sign up using Email and Password

Post as a guest

Required, but never shown

Post as a guest

Required, but never shown

By clicking "Post Your Answer", you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.