Showcase
Each example shows real output produced by schedulr itself, from files in the project repository. Input on the left, output on the right.
Workshop plan
examples/workshop_plan.rsDetail: Workshop plan →//! A two-day workshop plan: rooms, trainers and five activities placed on a
//! weekly slot calendar. Uses only the API released in schedulr 0.8.0.
//!
//! One time unit is one hour; hour 0 is Monday 00:00.
//!
//! Run with `cargo run --example workshop_plan`.
use schedulr::{
AcademicPeriod, Activity, ActivityId, Assignment, DayTemplate, GroupMembership, Participant,
ParticipantGroup, ParticipantGroupId, ParticipantId, ParticipantPool, ParticipantPoolId,
ParticipantRequirement, Resource, ResourceId, ResourcePool, ResourcePoolId,
ResourceRequirement, ScheduleTemplate, SchedulingProblem, ScoreLevel, ScoreRule, SlotTemplate,
TimeWindow, compile,
};
const DAYS: [&str; 2] = ["Mon", "Tue"];
const FIRST_HOUR: i64 = 9;
const LAST_HOUR: i64 = 16;
fn main() {
let resources = vec![
Resource::new(ResourceId(1), "Room Aurora", 24)
.with_type("room")
.with_feature("projector"),
Resource::new(ResourceId(2), "Room Birch", 12)
.with_type("room")
.with_feature("projector"),
Resource::new(ResourceId(3), "Lab 1", 12)
.with_type("lab")
.with_feature("workstations"),
];
let participants = vec![
Participant::new(ParticipantId(1), "Ana"),
Participant::new(ParticipantId(2), "Ben"),
Participant::new(ParticipantId(3), "Chris"),
];
let trainers = ParticipantGroupId(1);
let seminar_rooms = ResourcePoolId(1);
let rust_trainers = ParticipantPoolId(1);
let activities = vec![
Activity::new(ActivityId(1), "Kickoff", TimeWindow::new(0, 48), 1)
.with_requirement(ResourceRequirement::matching("room", 1).with_minimum_capacity(20))
.with_participant_group(trainers),
Activity::new(
ActivityId(2),
"Rust fundamentals",
TimeWindow::new(0, 48),
3,
)
.with_requirement(
ResourceRequirement::from_pool(seminar_rooms, 1).with_feature("projector"),
)
.with_participant_requirement(ParticipantRequirement::from_pool(rust_trainers)),
Activity::new(ActivityId(3), "Hands-on lab", TimeWindow::new(0, 48), 2)
.with_requirement(ResourceRequirement::matching("lab", 1).with_feature("workstations"))
.with_participant_requirement(ParticipantRequirement::from_pool(rust_trainers)),
Activity::new(ActivityId(4), "Solver deep dive", TimeWindow::new(0, 48), 2)
.with_requirement(ResourceRequirement::from_pool(seminar_rooms, 1))
.with_participant(ParticipantId(3)),
Activity::new(ActivityId(5), "Q&A", TimeWindow::new(24, 48), 1)
.with_requirement(ResourceRequirement::matching("room", 1).with_minimum_capacity(20))
.with_participant_group(trainers),
];
// Every day offers slots at 09, 10, 11 (ending by 12:00) and 13, 14, 15 (ending by 16:00).
// Tuesday 13:00-14:59 is closed as a one-off exception.
let mut day = DayTemplate::new(0);
for (offset, length) in [(9, 3), (10, 2), (11, 1), (13, 3), (14, 2), (15, 1)] {
day = day.with_slot(SlotTemplate::new(format!("{offset:02}:00"), offset, length));
}
let calendar = ScheduleTemplate::new(24)
.with_day(day)
.with_unavailable_range(37, 38);
let problem = SchedulingProblem::new(resources, participants, activities)
.with_resource_pool(ResourcePool::new(
seminar_rooms,
"Seminar rooms",
[ResourceId(1), ResourceId(2)],
))
.with_participant_pool(ParticipantPool::new(
rust_trainers,
"Rust trainers",
[ParticipantId(1), ParticipantId(2)],
))
.with_participant_group(ParticipantGroup::new(trainers, "Trainers"))
.with_group_membership(GroupMembership::participant(trainers, ParticipantId(1)))
.with_group_membership(GroupMembership::participant(trainers, ParticipantId(2)))
.with_group_membership(GroupMembership::participant(trainers, ParticipantId(3)))
.with_calendar(
AcademicPeriod {
window: TimeWindow::new(0, 48),
},
calendar,
)
.with_score_rule(ScoreRule::prefer_window(
"kickoff on Monday at 09:00",
ScoreLevel::Strong,
ActivityId(1),
TimeWindow::new(9, 10),
1,
))
.with_score_rule(ScoreRule::prefer_window(
"Q&A on Tuesday afternoon",
ScoreLevel::Medium,
ActivityId(5),
TimeWindow::new(37, 40),
1,
))
.with_score_rule(ScoreRule::prefer_window(
"lab on Tuesday morning",
ScoreLevel::Weak,
ActivityId(3),
TimeWindow::new(33, 36),
1,
));
println!(
"Input: {} resources, {} participants, {} activities, 1 resource pool, 1 participant pool, 1 group",
problem.resources.len(),
problem.participants.len(),
problem.activities.len(),
);
println!("Calendar: 24-hour cycle, starts at 09 10 11 13 14 15, Tue 13:00-14:59 closed");
println!();
let compiled = compile(&problem).expect("the workshop problem compiles");
let result = compiled.solve();
println!(
"Status: {:?} (optimal: {})",
result.status, result.statistics.optimal
);
let solution = result.solution.expect("the workshop problem is feasible");
println!();
println!(
"{:<17} {:<21} {:<13} Participants",
"When", "Activity", "Resource"
);
let mut ordered: Vec<&Assignment> = solution.assignments.iter().collect();
ordered.sort_by_key(|assignment| (assignment.window.start, assignment.activity));
for assignment in &ordered {
let activity = problem
.activities
.iter()
.find(|activity| activity.id() == assignment.activity)
.expect("assigned activity exists");
let resources = assignment
.resources
.iter()
.map(|id| resource_name(&problem, *id))
.collect::<Vec<_>>()
.join(", ");
let people = assignment
.participants
.iter()
.map(|id| participant_name(&problem, *id))
.collect::<Vec<_>>()
.join(", ");
println!(
"{:<17} {:<21} {:<13} {}",
format_window(assignment.window),
format!("{} {}", assignment.activity, activity.name()),
resources,
people
);
}
println!();
println!("Timeline (one column per hour, 09-16)");
let mut header = format!("{:<13}", "");
for name in DAYS {
header.push_str(&format!(" {:<21}", name));
}
println!("{}", header.trim_end());
let mut hours = format!("{:<13}", "");
for _ in DAYS {
hours.push(' ');
for hour in FIRST_HOUR..LAST_HOUR {
hours.push_str(&format!("{hour:02} "));
}
}
println!("{}", hours.trim_end());
for resource in &problem.resources {
let booked: Vec<&Assignment> = solution
.assignments
.iter()
.filter(|assignment| assignment.resources.contains(&resource.id()))
.collect();
println!("{}", timeline_row(resource.name(), &booked));
}
for participant in &problem.participants {
let booked: Vec<&Assignment> = solution
.assignments
.iter()
.filter(|assignment| assignment.participants.contains(&participant.id()))
.collect();
println!("{}", timeline_row(participant.name(), &booked));
}
println!();
println!(
"Score: hard {}, strong {}, medium {}, weak {}",
solution.score.hard, solution.score.strong, solution.score.medium, solution.score.weak
);
for component in &solution.score_components {
let activity = component
.activity
.map_or_else(String::new, |id| id.to_string());
println!(
" {:<7} {:<27} {:<3} {}",
format!("{:?}", component.level),
component.category,
activity,
component.value
);
}
}
fn format_window(window: TimeWindow) -> String {
let day = DAYS[usize::try_from(window.start / 24).expect("non-negative day")];
format!(
"{day} {:02}:00-{:02}:00",
window.start % 24,
window.end - window.start / 24 * 24
)
}
fn timeline_row(name: &str, booked: &[&Assignment]) -> String {
let mut row = format!("{name:<13}");
for day in 0..DAYS.len() as i64 {
row.push(' ');
for hour in FIRST_HOUR..LAST_HOUR {
let time = day * 24 + hour;
let cell = booked
.iter()
.find(|assignment| assignment.window.start <= time && time < assignment.window.end)
.map_or_else(
|| "..".to_string(),
|assignment| assignment.activity.to_string(),
);
row.push_str(&format!("{cell} "));
}
}
row.trim_end().to_string()
}
fn resource_name(problem: &SchedulingProblem, id: ResourceId) -> &str {
problem
.resources
.iter()
.find(|resource| resource.id() == id)
.map_or("?", Resource::name)
}
fn participant_name(problem: &SchedulingProblem, id: ParticipantId) -> &str {
problem
.participants
.iter()
.find(|participant| participant.id() == id)
.map_or("?", Participant::name)
}Input: 3 resources, 3 participants, 5 activities, 1 resource pool, 1 participant pool, 1 group
Calendar: 24-hour cycle, starts at 09 10 11 13 14 15, Tue 13:00-14:59 closed
Status: Feasible (optimal: true)
When Activity Resource Participants
Mon 09:00-10:00 a1 Kickoff Room Aurora Ana, Ben, Chris
Mon 10:00-12:00 a4 Solver deep dive Room Aurora Chris
Mon 13:00-16:00 a2 Rust fundamentals Room Aurora Ben
Tue 09:00-11:00 a3 Hands-on lab Lab 1 Ben
Tue 15:00-16:00 a5 Q&A Room Aurora Ana, Ben, Chris
Timeline (one column per hour, 09-16)
Mon Tue
09 10 11 12 13 14 15 09 10 11 12 13 14 15
Room Aurora a1 a4 a4 .. a2 a2 a2 .. .. .. .. .. .. a5
Room Birch .. .. .. .. .. .. .. .. .. .. .. .. .. ..
Lab 1 .. .. .. .. .. .. .. a3 a3 .. .. .. .. ..
Ana a1 .. .. .. .. .. .. .. .. .. .. .. .. a5
Ben a1 .. .. .. a2 a2 a2 a3 a3 .. .. .. .. a5
Chris a1 a4 a4 .. .. .. .. .. .. .. .. .. .. a5
Score: hard 0, strong 0, medium 0, weak 0
Strong kickoff on Monday at 09:00 a1 0
Medium Q&A on Tuesday afternoon a5 0
Weak lab on Tuesday morning a3 0Relations and breaks
examples/relations_and_breaks.rsDetail: Relations and breaks →//! One training day with a lunch break, a trainer who is only available in the
//! afternoon, and hard relations between activities.
//!
//! Breaks, availability ranges and activity relations were added after the
//! 0.8.0 release; this example needs the current master branch.
//!
//! One time unit is one hour; hour 0 is Monday 00:00.
//!
//! Run with `cargo run --example relations_and_breaks`.
use schedulr::{
AcademicPeriod, Activity, ActivityId, ActivityRelation, ActivityRelationConstraint,
BreakTemplate, DayTemplate, Participant, ParticipantId, Resource, ResourceId,
ResourceRequirement, ScheduleTemplate, SchedulingProblem, SlotTemplate, Solution, TimeWindow,
compile,
};
fn main() {
let aurora = ResourceId(1);
let birch = ResourceId(2);
let lab = ResourceId(3);
let library = ResourceId(4);
let resources = vec![
Resource::new(aurora, "Room Aurora", 24),
Resource::new(birch, "Room Birch", 12),
Resource::new(lab, "Lab 1", 12),
Resource::new(library, "Library", 6),
];
let ana = ParticipantId(1);
let ben = ParticipantId(2);
let chris = ParticipantId(3);
let participants = vec![
Participant::new(ana, "Ana"),
// Ben cannot start anything between 09:00 and 13:00 (inclusive).
Participant::new(ben, "Ben").with_unavailable_range(9, 13),
Participant::new(chris, "Chris"),
];
let day = TimeWindow::new(0, 24);
let activities = vec![
Activity::new(ActivityId(1), "Welcome", day, 1)
.with_requirement(ResourceRequirement::new(aurora, 1)),
Activity::new(ActivityId(2), "Theory", day, 2)
.with_requirement(ResourceRequirement::new(aurora, 1))
.with_participant(ana),
Activity::new(ActivityId(3), "Lab", day, 1)
.with_requirement(ResourceRequirement::new(lab, 1))
.with_participant(ana),
Activity::new(ActivityId(4), "Review", day, 1)
.with_requirement(ResourceRequirement::new(birch, 1))
.with_participant(ben),
Activity::new(ActivityId(5), "Parallel track", day, 2)
.with_requirement(ResourceRequirement::new(birch, 1))
.with_participant(chris),
Activity::new(ActivityId(6), "Office hour", day, 1)
.with_requirement(ResourceRequirement::new(library, 1))
.with_participant(ben),
];
// Slots start every hour from 09 to 16 and end by 17:00; lunch is 12:00-13:00.
let mut template = DayTemplate::new(0).with_break(BreakTemplate {
name: "Lunch".to_string(),
window: TimeWindow::new(12, 13),
});
for hour in 9..17 {
template = template.with_slot(SlotTemplate::new(
format!("{hour:02}:00"),
hour,
(17 - hour) as u64,
));
}
let relations = [
(1, 2, ActivityRelation::Precedence { min_gap: 1 }),
(2, 3, ActivityRelation::Consecutive),
(3, 4, ActivityRelation::Precedence { min_gap: 0 }),
(2, 5, ActivityRelation::SameStart),
(1, 6, ActivityRelation::NoOverlap),
];
let mut problem = SchedulingProblem::new(resources, participants, activities).with_calendar(
AcademicPeriod { window: day },
ScheduleTemplate::new(24).with_day(template),
);
for (first, second, relation) in relations {
problem = problem.with_relation(ActivityRelationConstraint::new(
ActivityId(first),
ActivityId(second),
relation,
));
}
let compiled = compile(&problem).expect("the training day compiles");
let result = compiled.solve();
println!("Status: {:?}", result.status);
let solution = result.solution.expect("the training day is feasible");
println!();
println!(
"{:<13} {:<19} {:<13} Participants",
"When", "Activity", "Resource"
);
let mut ordered = solution.assignments.clone();
ordered.sort_by_key(|assignment| (assignment.window.start, assignment.activity));
for assignment in &ordered {
let activity = problem
.activities
.iter()
.find(|activity| activity.id() == assignment.activity)
.expect("assigned activity exists");
let resource = assignment
.resources
.iter()
.map(|id| {
problem
.resources
.iter()
.find(|resource| resource.id() == *id)
.map_or("?", Resource::name)
})
.collect::<Vec<_>>()
.join(", ");
let people = assignment
.participants
.iter()
.map(|id| {
problem
.participants
.iter()
.find(|participant| participant.id() == *id)
.map_or("?", Participant::name)
})
.collect::<Vec<_>>()
.join(", ");
let line = format!(
"{:<13} {:<19} {:<13} {}",
format!(
"{}-{}",
clock(assignment.window.start),
clock(assignment.window.end)
),
format!("{} {}", assignment.activity, activity.name()),
resource,
people
);
println!("{}", line.trim_end());
}
println!();
println!("Checks against the solution");
for relation in &problem.relations {
let first = window_of(&solution, relation.first);
let second = window_of(&solution, relation.second);
let (name, detail) = match relation.relation {
ActivityRelation::SameStart => (
"SameStart".to_string(),
format!(
"{} and {} both start at {}",
relation.first,
relation.second,
clock(second.start)
),
),
ActivityRelation::Consecutive => (
"Consecutive".to_string(),
format!(
"{} ends {}, {} starts {}",
relation.first,
clock(first.end),
relation.second,
clock(second.start)
),
),
ActivityRelation::Precedence { min_gap } => (
format!("Precedence (gap {min_gap}h)"),
format!(
"{} ends {}, {} starts {}",
relation.first,
clock(first.end),
relation.second,
clock(second.start)
),
),
ActivityRelation::NoOverlap => (
"NoOverlap".to_string(),
format!(
"{} {}-{}, {} {}-{}",
relation.first,
clock(first.start),
clock(first.end),
relation.second,
clock(second.start),
clock(second.end)
),
),
};
println!(" {name:<22} {detail}");
}
let lunch = TimeWindow::new(12, 13);
let during_lunch = solution
.assignments
.iter()
.filter(|assignment| {
assignment.window.start < lunch.end && lunch.start < assignment.window.end
})
.count();
println!(
" {:<22} {during_lunch} activities overlap the break",
"Lunch 12:00-13:00"
);
println!(
" {:<22} a6 starts {}, a4 starts {}",
"Ben unavailable 09-13",
clock(window_of(&solution, ActivityId(6)).start),
clock(window_of(&solution, ActivityId(4)).start)
);
}
fn window_of(solution: &Solution, activity: ActivityId) -> TimeWindow {
solution
.assignments
.iter()
.find(|assignment| assignment.activity == activity)
.expect("every activity is assigned")
.window
}
fn clock(hour: i64) -> String {
format!("{:02}:00", hour % 24)
}Status: Feasible When Activity Resource Participants 09:00-10:00 a1 Welcome Room Aurora 13:00-15:00 a2 Theory Room Aurora Ana 13:00-15:00 a5 Parallel track Room Birch Chris 14:00-15:00 a6 Office hour Library Ben 15:00-16:00 a3 Lab Lab 1 Ana 16:00-17:00 a4 Review Room Birch Ben Checks against the solution Precedence (gap 1h) a1 ends 10:00, a2 starts 13:00 Consecutive a2 ends 15:00, a3 starts 15:00 Precedence (gap 0h) a3 ends 16:00, a4 starts 16:00 SameStart a2 and a5 both start at 13:00 NoOverlap a1 09:00-10:00, a6 14:00-15:00 Lunch 12:00-13:00 0 activities overlap the break Ben unavailable 09-13 a6 starts 14:00, a4 starts 16:00
Explaining conflicts
examples/explain_conflicts.rsDetail: Explaining conflicts →//! What schedulr reports when a plan cannot work: structured conflicts from
//! `explain` for infeasible problems, and `CompileError` messages for invalid
//! input. Uses only the API released in schedulr 0.8.0.
//!
//! Run with `cargo run --example explain_conflicts`.
use schedulr::{
Activity, ActivityId, Conflict, Participant, ParticipantId, Resource, ResourceId,
ResourceRequirement, SchedulingProblem, TimeWindow, compile,
};
fn main() {
println!("1. Two fixed lab sessions overlap in the same lab, with the same trainer");
let lab = Resource::new(ResourceId(1), "Lab 1", 1);
let chris = Participant::new(ParticipantId(1), "Chris");
let group_a = Activity::new(ActivityId(1), "Lab group A", TimeWindow::new(9, 11), 2)
.with_requirement(ResourceRequirement::new(lab.id(), 1))
.with_participant(chris.id());
let group_b = Activity::new(ActivityId(2), "Lab group B", TimeWindow::new(10, 12), 2)
.with_requirement(ResourceRequirement::new(lab.id(), 1))
.with_participant(chris.id());
let compiled = compile(&SchedulingProblem::new(
vec![lab],
vec![chris],
vec![group_a, group_b],
))
.expect("the problem compiles");
let result = compiled.solve();
println!(" Status: {:?}", result.status);
print_conflicts(&compiled.explain(&result));
println!();
println!("2. Three sessions at 09:00 need a room, but there are only two rooms");
let rooms = vec![
Resource::new(ResourceId(1), "Room Aurora", 1).with_type("room"),
Resource::new(ResourceId(2), "Room Birch", 1).with_type("room"),
];
let sessions = (1..=3)
.map(|id| {
Activity::new(
ActivityId(id),
format!("Session {id}"),
TimeWindow::new(9, 10),
1,
)
.with_requirement(ResourceRequirement::matching("room", 1))
})
.collect();
let compiled =
compile(&SchedulingProblem::new(rooms, vec![], sessions)).expect("the problem compiles");
let result = compiled.solve();
println!(" Status: {:?}", result.status);
print_conflicts(&compiled.explain(&result));
println!();
println!("3. Invalid input is rejected before any solving");
let room = Resource::new(ResourceId(1), "Room Aurora", 0);
let broken = vec![
Activity::new(ActivityId(1), "Too long", TimeWindow::new(9, 10), 2)
.with_requirement(ResourceRequirement::new(ResourceId(1), 1)),
Activity::new(ActivityId(2), "Missing room", TimeWindow::new(9, 12), 1)
.with_requirement(ResourceRequirement::new(ResourceId(9), 1)),
Activity::new(ActivityId(2), "Duplicate id", TimeWindow::new(9, 12), 1),
];
match compile(&SchedulingProblem::new(vec![room], vec![], broken)) {
Ok(_) => println!(" compiled unexpectedly"),
Err(error) => {
for message in error.messages() {
println!(" CompileError: {message}");
}
}
}
}
fn print_conflicts(conflicts: &[Conflict]) {
for conflict in conflicts {
let involved = conflict
.involved
.iter()
.map(ToString::to_string)
.collect::<Vec<_>>()
.join(", ");
println!(
" {:?} {} [{}]: {}",
conflict.severity, conflict.constraint_name, involved, conflict.message
);
}
}1. Two fixed lab sessions overlap in the same lab, with the same trainer Status: Infeasible Blocking NoOverlap [a1, a2]: Lab 1: Intervals [9, 11) and [10, 12) overlap Advisory NoOverlap [a1, a2]: Chris: Intervals [9, 11) and [10, 12) overlap 2. Three sessions at 09:00 need a room, but there are only two rooms Status: Infeasible Blocking AlternativeResourceCapacity [a1, a2, a3]: Room Aurora: capacity 1 is insufficient for the unresolved alternatives Blocking AlternativeResourceCapacity [a1, a2, a3]: Room Birch: capacity 1 is insufficient for the unresolved alternatives 3. Invalid input is rejected before any solving CompileError: resource r1 has zero capacity CompileError: activity a1 has an invalid time window CompileError: activity a2 requires unknown resource r9 CompileError: duplicate activity id a2
Booking desk
examples/booking_desk.rsDetail: Booking desk →//! A booking desk: create, check, move and cancel single appointments with
//! `SchedulingState`. No solver search runs; each check only evaluates the
//! constraints touching the proposed appointment. Uses only the API released
//! in schedulr 0.8.0.
//!
//! One time unit is one minute; minute 0 is 00:00.
//!
//! Run with `cargo run --example booking_desk`.
use schedulr::{
Conflict, Participant, ParticipantId, ProposedActivity, Resource, ResourceId,
ResourceRequirement, SchedulingState, TimeWindow,
};
fn main() {
let room = ResourceId(1);
let alex = ParticipantId(1);
let blair = ParticipantId(2);
let mut state = SchedulingState::new(
[Resource::new(room, "Consulting room", 1)],
[
Participant::new(alex, "Alex"),
Participant::new(blair, "Blair"),
],
[],
);
let consultation = |start: i64, end: i64| {
ProposedActivity::new("consultation", TimeWindow::new(start, end))
.with_requirement(ResourceRequirement::new(room, 1))
.with_participant(alex)
};
println!("1. Book a consultation with Alex, 10:00-11:00");
let first = state
.commit(consultation(600, 660))
.expect("room and Alex are free");
println!(" committed as {first}");
println!("2. Check a second consultation in the same room, 10:30-11:30");
print_conflicts(&state.check_feasibility(&consultation(630, 690)));
println!("3. Book a call for Blair without a room, 10:00-11:00");
let call = state
.commit(ProposedActivity::new("call", TimeWindow::new(600, 660)).with_participant(blair))
.expect("Blair is free");
println!(" committed as {call}");
println!("4. Add Blair to the consultation");
let mut with_blair = state.proposal_for(first).expect("consultation exists");
with_blair.add_participant(blair);
print_conflicts(&state.check_feasibility(&with_blair));
match state.commit(with_blair) {
Ok(id) => println!(" committed anyway: advisory conflicts do not block ({id})"),
Err(conflicts) => print_conflicts(&conflicts),
}
println!("5. Move the consultation to 10:30-11:30");
let moved = consultation(630, 690).excluding(first);
print_conflicts(&state.check_feasibility(&moved));
let id = state.commit(moved).expect("the new slot is free");
let window = state.assignment(id).expect("still booked").window;
println!(
" {id} now runs {}-{}",
clock(window.start),
clock(window.end)
);
println!("6. Cancel Blair's call");
state.cancel(call).expect("call exists");
println!(" {} activity left in the state", state.len());
}
fn print_conflicts(conflicts: &[Conflict]) {
if conflicts.is_empty() {
println!(" no conflicts");
}
for conflict in conflicts {
let involved = conflict
.involved
.iter()
.map(ToString::to_string)
.collect::<Vec<_>>()
.join(", ");
println!(
" {:?} {} [{}]: {}",
conflict.severity, conflict.constraint_name, involved, conflict.message
);
}
}
fn clock(minute: i64) -> String {
format!("{:02}:{:02}", minute / 60, minute % 60)
}1. Book a consultation with Alex, 10:00-11:00 committed as a0 2. Check a second consultation in the same room, 10:30-11:30 Blocking NoOverlap [a0, a1]: Consulting room: Intervals [600, 660) and [630, 690) overlap Advisory NoOverlap [a0, a1]: Alex: Intervals [600, 660) and [630, 690) overlap 3. Book a call for Blair without a room, 10:00-11:00 committed as a1 4. Add Blair to the consultation Advisory NoOverlap [a0, a1]: Blair: Intervals [600, 660) and [600, 660) overlap committed anyway: advisory conflicts do not block (a0) 5. Move the consultation to 10:30-11:30 no conflicts a0 now runs 10:30-11:30 6. Cancel Blair's call 1 activity left in the state