schedulrv0.8.0

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  0
  • batch solve
  • pools
  • groups
  • calendar
  • scores

Relations 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
  • relations
  • breaks
  • availability
  • unreleased

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
  • explain
  • infeasible
  • compile errors

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
  • SchedulingState
  • no search
  • move
  • cancel