when3meet 0.1.6

CLI tool to find the optimal time to meet given a when2meet URL
Documentation
use chrono::{DateTime, Duration, TimeZone, Utc};
use chrono_tz::{OffsetName, Tz};
use iana_time_zone::get_timezone;

#[derive(Debug, PartialEq)]
pub struct Slot {
    pub start_time: DateTime<Utc>,
    pub end_time: DateTime<Utc>,
    pub people: Vec<Person>,
}

#[derive(Debug, PartialEq)]
pub struct Person {
    pub name: Box<str>,
    pub available: bool,
}

impl Slot {
    pub fn new(start_time: DateTime<Utc>, people: Vec<Person>) -> Self {
        Self {
            start_time,
            end_time: start_time + Duration::minutes(15),
            people,
        }
    }
}

pub fn fold(slots: Vec<Slot>) -> Vec<Slot> {
    slots
        .into_iter()
        .fold(Vec::new(), |mut folded_slots, slot| {
            if let Some(last_slot) = folded_slots.last_mut() {
                if last_slot.end_time == slot.start_time && last_slot.people == slot.people {
                    last_slot.end_time = slot.end_time;
                    return folded_slots;
                }
            }
            folded_slots.push(slot);
            folded_slots
        })
}

impl std::fmt::Display for Slot {
    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
        let tz_str = match get_timezone() {
            Ok(tz) => tz,
            Err(_) => return Err(std::fmt::Error),
        };

        let tz: Tz = match tz_str.parse() {
            Ok(tz) => tz,
            Err(_) => return Err(std::fmt::Error),
        };

        let offset = tz.offset_from_utc_date(&Utc::now().date_naive());
        let abbreviation = offset.abbreviation();

        let formatted_timestamp = format!(
            "{} - {} {}",
            self.start_time.format("%A %I:%M%P"),
            self.end_time.format("%I:%M%P"),
            abbreviation
        );

        writeln!(f, "Timestamp: {}", formatted_timestamp)?;

        let (available_people, unavailable_people): (Vec<&Person>, Vec<&Person>) =
            self.people.iter().partition(|&person| person.available);

        if !available_people.is_empty() {
            writeln!(f, "Available People:")?;

            for person in available_people {
                writeln!(f, "- {}", person.name)?;
            }
        }

        if !unavailable_people.is_empty() {
            writeln!(f, "Unavailable People:")?;

            for person in unavailable_people {
                writeln!(f, "- {}", person.name)?;
            }
        }

        Ok(())
    }
}

#[cfg(test)]
mod tests {
    use crate::fetch_availability::fold;
    use crate::fetch_availability::model::{Person, Slot};
    use chrono::{DateTime, Duration, TimeZone, Utc};
    use chrono_tz::{OffsetName, Tz};
    use iana_time_zone::get_timezone;

    #[test]
    fn test_fold() {
        let slots = vec![
            Slot::new(
                DateTime::parse_from_str("1693746000", "%s")
                    .unwrap()
                    .with_timezone(&Utc),
                vec![
                    Person {
                        name: "Muneer".to_string().into_boxed_str(),
                        available: false,
                    },
                    Person {
                        name: "Brian".to_string().into_boxed_str(),
                        available: false,
                    },
                    Person {
                        name: "Garrett".to_string().into_boxed_str(),
                        available: false,
                    },
                ],
            ),
            Slot::new(
                DateTime::parse_from_str("1693746900", "%s")
                    .unwrap()
                    .with_timezone(&Utc),
                vec![
                    Person {
                        name: "Muneer".to_string().into_boxed_str(),
                        available: false,
                    },
                    Person {
                        name: "Brian".to_string().into_boxed_str(),
                        available: false,
                    },
                    Person {
                        name: "Garrett".to_string().into_boxed_str(),
                        available: false,
                    },
                ],
            ),
            Slot::new(
                DateTime::parse_from_str("1693747800", "%s")
                    .unwrap()
                    .with_timezone(&Utc),
                vec![
                    Person {
                        name: "Muneer".to_string().into_boxed_str(),
                        available: true,
                    },
                    Person {
                        name: "Brian".to_string().into_boxed_str(),
                        available: false,
                    },
                    Person {
                        name: "Garrett".to_string().into_boxed_str(),
                        available: false,
                    },
                ],
            ),
        ];

        let folded_slots = fold(slots);

        assert_eq!(
            folded_slots,
            vec![
                Slot {
                    start_time: DateTime::parse_from_str("1693746000", "%s")
                        .unwrap()
                        .with_timezone(&Utc),
                    end_time: DateTime::parse_from_str("1693747800", "%s")
                        .unwrap()
                        .with_timezone(&Utc),
                    people: vec![
                        Person {
                            name: "Muneer".to_string().into_boxed_str(),
                            available: false,
                        },
                        Person {
                            name: "Brian".to_string().into_boxed_str(),
                            available: false,
                        },
                        Person {
                            name: "Garrett".to_string().into_boxed_str(),
                            available: false,
                        },
                    ],
                },
                Slot::new(
                    DateTime::parse_from_str("1693747800", "%s")
                        .unwrap()
                        .with_timezone(&Utc),
                    vec![
                        Person {
                            name: "Muneer".to_string().into_boxed_str(),
                            available: true,
                        },
                        Person {
                            name: "Brian".to_string().into_boxed_str(),
                            available: false,
                        },
                        Person {
                            name: "Garrett".to_string().into_boxed_str(),
                            available: false,
                        },
                    ],
                ),
            ]
        );
    }

    #[test]
    fn test_fold_empty() {
        assert_eq!(fold(vec![]), vec![]);
    }

    #[test]
    fn test_slot_display() {
        let slot = Slot {
            start_time: Utc::now(),
            end_time: Utc::now() + Duration::hours(2),
            people: vec![
                Person {
                    name: "Muneer".to_string().into_boxed_str(),
                    available: true,
                },
                Person {
                    name: "Garrett".to_string().into_boxed_str(),
                    available: false,
                },
            ],
        };

        let tz_str = get_timezone().unwrap();

        let tz: Tz = tz_str.parse().unwrap();

        let offset = tz.offset_from_utc_date(&Utc::now().date_naive());
        let abbreviation = offset.abbreviation();

        let expected_output = format!(
            "Timestamp: {} - {} {}\nAvailable People:\n- Muneer\nUnavailable People:\n- Garrett\n",
            slot.start_time.format("%A %I:%M%P"),
            slot.end_time.format("%I:%M%P"),
            abbreviation
        );

        let actual_output = format!("{}", slot);

        assert_eq!(expected_output, actual_output);
    }
}