Daniel Segovia's personal website and portfolio

Awesome Traits

Reusing code with traits and generics

Created:

tags: dev


I find it fascinating when a small amount of code, can hold most if not all the business logic of an application. In the case of my open source Calendly alternative project, its the following Rust trait.

pub trait GenericWindowComparison<T: PartialOrd> {
    fn start_time(&self) -> T;
    fn end_time(&self) -> T;

    /// Check if this window clashes with any other window in the list.
    fn clash_check(&self, others: &[impl GenericWindowComparison<T>]) -> bool {
        others.iter().any(|other| {
            // Check if `self.start_time` is between other
            // `start_time` and `end_time`
            if self.start_time() >= other.start_time() 
            && self.start_time() < other.end_time() {
                return true;
            }
            // Check if `self.end_time` is between other
            // `start_time` and `end_time`
            if self.end_time() > other.start_time() 
            && self.end_time() <= other.end_time() {
                return true;
            }
            // Check if `self` is wrapping around other
            if self.start_time() <= other.start_time() 
            && self.end_time() >= other.end_time() {
                return true;
            }
            false
        })
    }
}

If we implement this trait on a struct (or multiple, with the same type T) and set the start_time and end_time functions, we can compare any time window vs a set of time windows that implement the same T, where T implements PartialOrd itself. More on PartialOrd later.

Lets see a quick example. Lets say WeeklyAvailabilityInMinutes symbolizes when a user is available in a regular week.

/// Weekly availability duration in minutes from Monday 00:00 until Sunday 23:59
pub struct WeeklyAvailabilityInMinutes {
    pub from: u32,
    pub to: u32,
}

Some examples of this weekly availability would be:

let monday_morning_availability = WeeklyAvailabilityInMinutes {
    from: 540, // 9:00 am Monday
    to: 780, // 1:00 pm Monday
}
let wednesday_midday_availability = WeeklyAvailabilityInMinutes {
    from: 3660, // 1:00 pm Wednesday
    to: 3780, // 3:00 pm Wednesday
}

Now, implementing the trait:

impl GenericWindowComparison<u32> for WeeklyAvailabilityInMinutes {
    fn start_time(&self) -> u32 {
        self.from
    }
    fn end_time(&self) -> u32 {
        self.to
    }
}

We can use the function clash_check to figure out if we can insert a new availability window that does not collide with an existing one.

let current_availability_windows = vec![
    monday_morning_availability, 
    wednesday_midday_availability
];

let new_failing_availability_window = WeeklyAvailabilityInMinutes {
    from: 720, // 12:00 pm Monday
    to: 1080, // 6:00 pm Monday
}
let fails = new_failing_availability_window.clash_check(
    &current_availability_windows
); // This would return `true`

let new_passing_availability_window = WeeklyAvailabilityInMinutes {
    from: 960, // 4:00 pm Monday
    to: 1080, // 6:00 pm Monday
}
let passes = new_passing_availability_window.clash_check(
    &current_availability_windows
); // This would return `false`

That seems simple enough, right? Both comparisons come from the same struct, so naturally, both implement T, in this case u32. Here’s where things get a bit interesting. Lets now assume that we have some data coming from the frontend with the following structs:

/// Normalizes availability to 0 minutes min and 1440 minutes max
pub struct NormalizedAvailabilityDay {
    pub from: u32,
    pub to: u32,
}

pub struct WeeklyAvailabilitiesCreateUpdateParams {
    pub normalized: NormalizedAvailabilityDay,
    /// Zero indexed weekday (0 = Monday, 1 = Tuesday, etc.)
    pub weekday: u32,
}

Lets implement GenericWindowComparison<u32>:

impl GenericWindowComparison<u32> for WeeklyAvailabilitiesCreateUpdateParams {
    fn start_time(&self) -> u32 {
        self.normalized.from + self.weekday * (60 * 24)
    }
    fn end_time(&self) -> u32 {
        self.normalized.to + self.weekday * (60 * 24)
    }
}

Now, we can use both interchangeably:

let params_from_frontend: WeeklyAvailabilitiesCreateUpdateParams = ...; //
params_from_frontend.clash_check(&current_availability_windows);

We can now clash_check between two different structs, so long as the implement GenericWindowComparison<T>, in this case GenericWindowComparison<u32>. u32 is PartialOrd by default, see here.

What PartialOrd allows us to do, is run comparisons with <, >, >= and <=.

What else uses PartialOrd that would be useful to compare time windows? chrono::DateTime<Tz>!

This allows us to store appointments in the application with a model like so:

pub struct AppointmentModel {
    pub start_time: DateTime<Utc>,
    pub end_time: DateTime<Utc>,
    // other fields...
}

impl GenericWindowComparison<DateTime<Utc>> for AppointmentModel {
    fn start_time(&self) -> DateTime<Utc> {
        self.start_time
    }
    fn end_time(&self) -> DateTime<Utc> {
        self.end_time
    }
}

And if we have data coming from an API, lets say Google Calendar’s freebusy endpoint:

pub struct TimePeriod {
    pub end: DateTime<Utc>,
    pub start: DateTime<Utc>,
}

pub struct FreeBusyCalendar {
    pub busy: Vec<TimePeriod>,
    pub errors: Vec<Error>,
}

We impl our trait:

impl GenericWindowComparison<DateTime<Utc>> for TimePeriod {
    fn start_time(&self) -> DateTime<Utc> {
        self.start
    }
    fn end_time(&self) -> DateTime<Utc> {
        self.end
    }
}

And we can now compare the two:

let appointment_to_schedule: AppointmentModel = ... //
let gcal_free_busy: FreeBusyCalendar = get_gcal_freebusy_api_call().await?;

if appointment_to_schedule.clash_check(&gcal_free_busy.busy) {
    return Err(...)
}

In reality, things are a bit more complex. But the premise stays the same. We can even impl GenericWindowComparison<T> on something that we need to convert first. A quick example would be chrono::TimeDelta.

struct SomeTimeWindowFromNow {
    start: chrono::TimeDelta,
    end: chrono::TimeDelta
}

impl GenericWindowComparison<i64> for SomeTimeWindowFromNow {
    fn start_time(&self) -> i64 {
        self.start.num_minutes()
    }
    fn end_time(&self) -> i64 {
        self.end.num_minutes()
    }
}

Using this trait has allowed me to isolate the core business logic of comparing time windows, and use it in multiple places across the repo. Even if my logic is faulty in that trait, I only have to fix it in one place, and all the time window comparisons are fixed.

If you read up to here, I really appreciate your time. Have any thoughts? Please let me know!

If you want to find out more about the open source appointments repo, head over to github.