Super Rune Random code and web stuff

Shorthand date period parser in Rust

In my spare time I try to optimize my work day as Product Owner and this involves gathering a lot of data about the lives of my work items and try to visualize this in different diagrams and this often involves a date period or date range. And I was getting tired of writing from_date and to_date when parsing arguments to my programs. So I switched to a shorthand notation and found my self needing a parser.

I often find my self not needing a precise date range as for example, 2020-05-17 to 2020-06-11. I often work with date ranges in months, quarters or half years. So I started using this date range shorthand notation:

  • Get first or second half of year: YYH1 or YYH2
  • Get a quarter of the year: YYQ{Quarter number}, fx. 19Q3 will get you the period from 1st of July to 30th of September 2019.
  • Get a month of the year: YYM{Month number}, fx. 19M10 will get you the period from the 1st of October to the 31st of October. Month number start at 1 and ends at 12,

It is much easier to write and pretty easy to read as well.

Now onto the code it self.

In my Rust projects I have a period module which holds the struct returning the period and one function that parses a string to a date.

My period struct looks like this:

#[derive(Debug, Clone)]
pub struct Period {
    pub start: Option<Date<Utc>>,
    pub end: Option<Date<Utc>>,
    pub name: String,
    pub days: i64,
    pub modulo: i64,
}

It pretty straight forward, but then again maybe only for me the author. start and end is te real date range. name is the value getting parsed. days is the number of day in the date range. This has come in have several times, for example if I need to crate a time line it easier to loop over a number of integers. modulo is also used for presentation this value is used to determine when a date in the date range should be printed onto the screen. If modulo is equal to 6, then every time a day in days are day & module == 0 we print the day out. It's just another helper value and only for presentation.

This is the function that does all the parsing:

pub fn get_period(period: &str) -> Result<Period, &'static str> {
    let mut y: i32 = 2000;
    let mut start_m = 1;
    let mut end_m = 3;
    let start_d = 1;
    let mut end_d = 31;
    let mut modulo = 6;

    let period = period.to_lowercase();
    let period = period.as_str();

    if let Some(ch) = period.get(0..2) {
        match ch.parse::<i32>() {
            Ok(num) => {
                y = y + num;

                let mut last = 0;

                if let Some(ch) = period.get(3..5) {
                    if let Ok(num) = ch.parse::<i32>() {
                        last = num;
                    }
                }

                if last == 0 {
                    if let Some(ch) = period.get(3..4) {
                        if let Ok(num) = ch.parse::<i32>() {
                            last = num;
                        }
                    }
                }

                match period.get(2..3).unwrap() {
                    "h" => {
                        let half = last;
                        if half == 1 {
                            start_m = 1;
                            end_m = 6;
                        }

                        if half == 2 {
                            start_m = 7;
                            end_m = 12;
                        }
                        modulo = 12;

                        if half > 2 {
                            return Err("There can only be two halfs in a year");
                        }
                    }
                    "q" => {
                        if last == 1 {
                            start_m = 1;
                            end_m = 3;
                        }

                        if last == 2 {
                            start_m = 4;
                            end_m = 6;
                            end_d = (Utc.ymd(y, end_m + 1, 1) - Duration::days(1)).day()
                        }

                        if last == 3 {
                            start_m = 7;
                            end_m = 9;
                            end_d = (Utc.ymd(y, end_m + 1, 1) - Duration::days(1)).day()
                        }

                        if last == 4 {
                            start_m = 10;
                            end_m = 12;
                        }

                        modulo = 6;

                        if last < 1 {
                            return Err("quarter can not be lower than 1");
                        }

                        if last > 4 {
                            return Err("there can only be 4 quarters in a year");
                        }
                    }
                    "m" => {
                        if last < 1 {
                            return Err("Month number can not be lower than 1");
                        }

                        if last > 12 {
                            return Err("The are only 12 months in a year");
                        }

                        start_m = last;
                        end_m = start_m as u32;
                        if last < 12 {
                            end_d = (Utc.ymd(y, end_m + 1, 1) - Duration::days(1)).day()
                        }

                        if last == 12 {
                            end_d = (Utc.ymd(y + 1, 1, 1) - Duration::days(1)).day()
                        }

                        modulo = 3
                    }
                    _ => return Err("invalid char - period char"),
                }
            }
            Err(_) => return Err("invalid char - year char"),
        }
    }

    let start = Utc.ymd(y, start_m as u32, start_d);
    let end = Utc.ymd(y, end_m, end_d);

    Ok(Period {
        start: Some(start),
        end: Some(end),
        name: period.to_uppercase(),
        days: (end - start).num_days(),
        modulo: modulo,
    })
}

I could've used a tokenizer instead of looking up indexes in the string, but the format is very simple. It always starts with a year with the length of two. Then I expect there to be a keyword: H, Q or M. And then again I expect there be at least one number. The number check is performed in the top of the function and it might not be the best way of doing it. But i works

The function is rather long and it could be split into a function for every keyword to make it more readable. But that is for another evening of weekend morning of code.

And you have also noted that I do not perform any tests to verify my code and this is where my skill as a programmer is lacking.

Disclaimer

I'm sure there are bugs hidden in here, so if you ever copy my code be aware that it is probably flawed in some way. But feel free to do whatever you want with my code, just don't come crying if it has broken your production code because you didn't bother to review my implementation.