use solverforge::prelude::*;
use solverforge::stream::relational::operator::Pair;
use solverforge::stream::relational::Leaf;
use solverforge::stream::{source, ChangeSource};
use solverforge::IncrementalConstraint;
#[derive(Clone, Debug)]
struct RelShift {
id: u32,
night: bool,
}
#[derive(Clone, Debug)]
struct RelAssignment {
shift_id: u32,
employee_code: String,
}
#[derive(Clone, Debug)]
struct RelEmployee {
code: String,
}
#[derive(Clone)]
struct RelSchedule {
shifts: Vec<RelShift>,
assignments: Vec<RelAssignment>,
employees: Vec<RelEmployee>,
}
fn rel_shifts(schedule: &RelSchedule) -> &[RelShift] {
schedule.shifts.as_slice()
}
fn rel_assignments(schedule: &RelSchedule) -> &[RelAssignment] {
schedule.assignments.as_slice()
}
fn rel_employees(schedule: &RelSchedule) -> &[RelEmployee] {
schedule.employees.as_slice()
}
fn assignment_shift_id(assignment: &RelAssignment) -> u32 {
assignment.shift_id
}
fn shift_id(shift: &RelShift) -> u32 {
shift.id
}
fn row_employee_code(row: &Pair<Leaf<'_, RelAssignment>, Leaf<'_, RelShift>>) -> String {
row.left.entity.employee_code.clone()
}
fn employee_leaf_code(employee: &Leaf<'_, RelEmployee>) -> String {
employee.entity.code.clone()
}
fn night_filter(assignment: &RelAssignment, shift: &RelShift, _employee: &RelEmployee) -> bool {
let _ = assignment;
shift.night
}
fn unit_weight(
_assignment: &RelAssignment,
_shift: &RelShift,
_employee: &RelEmployee,
) -> HardSoftScore {
HardSoftScore::of(0, 1)
}
fn staffed_constraint() -> impl IncrementalConstraint<RelSchedule, HardSoftScore> {
ConstraintFactory::<RelSchedule, HardSoftScore>::new()
.for_each(source(
rel_assignments as fn(&RelSchedule) -> &[RelAssignment],
ChangeSource::Descriptor(1),
))
.join((
source(
rel_shifts as fn(&RelSchedule) -> &[RelShift],
ChangeSource::Descriptor(0),
),
joiner::equal_bi(
assignment_shift_id as fn(&RelAssignment) -> u32,
shift_id as fn(&RelShift) -> u32,
),
))
.join((
source(
rel_employees as fn(&RelSchedule) -> &[RelEmployee],
ChangeSource::Descriptor(2),
),
joiner::equal_on(
row_employee_code
as fn(&Pair<Leaf<'_, RelAssignment>, Leaf<'_, RelShift>>) -> String,
employee_leaf_code as fn(&Leaf<'_, RelEmployee>) -> String,
),
))
.filter(night_filter as fn(&RelAssignment, &RelShift, &RelEmployee) -> bool)
.penalize(unit_weight as fn(&RelAssignment, &RelShift, &RelEmployee) -> HardSoftScore)
.named("night shift staffed")
}
fn sample() -> RelSchedule {
RelSchedule {
shifts: vec![
RelShift {
id: 10,
night: true,
},
RelShift {
id: 11,
night: false,
},
],
assignments: vec![
RelAssignment {
shift_id: 10,
employee_code: "e0".to_string(),
},
RelAssignment {
shift_id: 11,
employee_code: "e1".to_string(),
},
RelAssignment {
shift_id: 10,
employee_code: "e0".to_string(),
},
],
employees: vec![
RelEmployee {
code: "e0".to_string(),
},
RelEmployee {
code: "e1".to_string(),
},
],
}
}
fn closure_chain_constraint() -> impl IncrementalConstraint<RelSchedule, SoftScore> {
ConstraintFactory::<RelSchedule, SoftScore>::new()
.for_each(source(
rel_assignments as fn(&RelSchedule) -> &[RelAssignment],
ChangeSource::Descriptor(1),
))
.join((
source(
rel_shifts as fn(&RelSchedule) -> &[RelShift],
ChangeSource::Descriptor(0),
),
joiner::equal_bi(|a: &RelAssignment| a.shift_id, |s: &RelShift| s.id),
))
.join((
source(
rel_employees as fn(&RelSchedule) -> &[RelEmployee],
ChangeSource::Descriptor(2),
),
joiner::equal_on(
|row: &Pair<Leaf<'_, RelAssignment>, Leaf<'_, RelShift>>| {
row.left.entity.employee_code.clone()
},
|e: &Leaf<'_, RelEmployee>| e.entity.code.clone(),
),
))
.filter(|_a: &RelAssignment, s: &RelShift, _e: &RelEmployee| s.night)
.penalize(SoftScore::of(1))
.named("night shift staffed (closures)")
}
#[test]
fn readme_closure_chain_compiles_and_scores() {
let schedule = sample();
let constraint = closure_chain_constraint();
assert_eq!(constraint.match_count(&schedule), 2);
assert_eq!(constraint.evaluate(&schedule), SoftScore::of(-2));
}
#[test]
fn relational_chain_publishes_hard_scores_through_facade() {
let schedule = sample();
let mut constraint = staffed_constraint();
assert_eq!(constraint.initialize(&schedule), HardSoftScore::of(0, -2));
assert_eq!(constraint.evaluate(&schedule), HardSoftScore::of(0, -2));
assert_eq!(constraint.match_count(&schedule), 2);
let delta = constraint.on_retract(&schedule, 0, 1);
assert_eq!(delta, HardSoftScore::of(0, 1));
let delta = constraint.on_insert(&schedule, 0, 1);
assert_eq!(delta, HardSoftScore::of(0, -1));
assert_eq!(constraint.evaluate(&schedule), HardSoftScore::of(0, -2));
}