use std::sync::Arc;
use solverforge_core::score::SoftScore;
use solverforge_core::{ConstraintRef, ImpactType, Score};
use crate::api::constraint_set::IncrementalConstraint;
use crate::constraint::cross_tri_incremental::Tri as CrossTriConstraint;
use crate::stream::collection_extract::{source, ChangeSource};
#[derive(Clone, Debug, Hash, PartialEq, Eq)]
struct Shift {
id: usize,
employee_id: Option<usize>,
day: u32,
}
#[derive(Clone, Debug, Hash, PartialEq, Eq)]
struct Employee {
id: usize,
}
#[derive(Clone, Debug, Hash, PartialEq, Eq)]
struct DayOff {
employee_id: Option<usize>,
day: u32,
}
#[derive(Clone)]
struct Schedule {
shifts: Vec<Shift>,
employees: Vec<Employee>,
days_off: Vec<DayOff>,
}
fn shifts_src(schedule: &Schedule) -> &[Shift] {
schedule.shifts.as_slice()
}
fn employees_src(schedule: &Schedule) -> &[Employee] {
schedule.employees.as_slice()
}
fn days_off_src(schedule: &Schedule) -> &[DayOff] {
schedule.days_off.as_slice()
}
fn tri_constraint() -> impl IncrementalConstraint<Schedule, SoftScore> {
CrossTriConstraint::new(
ConstraintRef::new("", "Shift on day off"),
ImpactType::Penalty,
source(
shifts_src as fn(&Schedule) -> &[Shift],
ChangeSource::Descriptor(0),
),
source(
employees_src as fn(&Schedule) -> &[Employee],
ChangeSource::Descriptor(1),
),
source(
days_off_src as fn(&Schedule) -> &[DayOff],
ChangeSource::Descriptor(2),
),
|shift: &Shift| shift.employee_id,
|employee: &Employee| Some(employee.id),
|day_off: &DayOff| day_off.employee_id,
|_s: &Schedule,
shift: &Shift,
_employee: &Employee,
day_off: &DayOff,
_a: usize,
_b: usize,
_c: usize| { shift.day == day_off.day },
|_s: &Schedule, _a: usize, _b: usize, _c: usize| SoftScore::of(1),
false,
)
}
fn sample_schedule() -> Schedule {
Schedule {
shifts: vec![
Shift {
id: 0,
employee_id: Some(0),
day: 1,
},
Shift {
id: 1,
employee_id: Some(1),
day: 2,
},
Shift {
id: 2,
employee_id: Some(0),
day: 3,
},
],
employees: vec![Employee { id: 0 }, Employee { id: 1 }],
days_off: vec![
DayOff {
employee_id: Some(0),
day: 1,
},
DayOff {
employee_id: Some(1),
day: 5,
},
],
}
}
#[test]
fn cross_tri_evaluate_matches_expected_rows() {
let constraint = tri_constraint();
let schedule = sample_schedule();
assert_eq!(constraint.evaluate(&schedule), SoftScore::of(-1));
assert_eq!(constraint.match_count(&schedule), 1);
}
#[test]
fn cross_tri_initialize_then_incremental_matches_evaluate() {
let schedule = sample_schedule();
let mut constraint = tri_constraint();
assert_eq!(constraint.initialize(&schedule), SoftScore::of(-1));
let extended = Schedule {
shifts: schedule.shifts.clone(),
employees: vec![Employee { id: 0 }, Employee { id: 1 }, Employee { id: 2 }],
days_off: schedule.days_off.clone(),
};
let delta = constraint.on_insert(&extended, 2, 1);
assert_eq!(delta, SoftScore::zero());
assert_eq!(constraint.evaluate(&extended), SoftScore::of(-1));
let with_day_off = Schedule {
shifts: schedule.shifts.clone(),
employees: schedule.employees.clone(),
days_off: vec![
DayOff {
employee_id: Some(0),
day: 1,
},
DayOff {
employee_id: Some(1),
day: 5,
},
DayOff {
employee_id: Some(0),
day: 3,
},
],
};
let delta = constraint.on_insert(&with_day_off, 2, 2);
assert_eq!(delta, SoftScore::of(-1));
assert_eq!(constraint.evaluate(&with_day_off), SoftScore::of(-2));
assert_eq!(constraint.match_count(&with_day_off), 2);
let delta = constraint.on_retract(&with_day_off, 2, 2);
assert_eq!(delta, SoftScore::of(1));
assert_eq!(constraint.evaluate(&schedule), SoftScore::of(-1));
assert_eq!(constraint.match_count(&schedule), 1);
}
#[test]
fn cross_tri_unrelated_descriptor_is_noop() {
let schedule = sample_schedule();
let mut constraint = tri_constraint();
constraint.initialize(&schedule);
assert_eq!(constraint.on_insert(&schedule, 0, 9), SoftScore::zero());
assert_eq!(constraint.on_retract(&schedule, 0, 9), SoftScore::zero());
}
#[test]
#[should_panic(expected = "cannot localize entity indexes")]
fn cross_tri_unknown_source_panics_on_localized_callback() {
let constraint = CrossTriConstraint::new(
ConstraintRef::new("", "Shift on day off"),
ImpactType::Penalty,
(|schedule: &Schedule| schedule.shifts.as_slice()) as fn(&Schedule) -> &[Shift],
source(
employees_src as fn(&Schedule) -> &[Employee],
ChangeSource::Descriptor(1),
),
source(
days_off_src as fn(&Schedule) -> &[DayOff],
ChangeSource::Descriptor(2),
),
|shift: &Shift| shift.employee_id,
|employee: &Employee| Some(employee.id),
|day_off: &DayOff| day_off.employee_id,
|_s: &Schedule,
_shift: &Shift,
_employee: &Employee,
_day_off: &DayOff,
_a: usize,
_b: usize,
_c: usize| true,
|_s: &Schedule, _a: usize, _b: usize, _c: usize| SoftScore::of(1),
false,
);
let schedule = sample_schedule();
let mut constraint = constraint;
constraint.initialize(&schedule);
constraint.on_insert(&schedule, 0, 0);
}
#[test]
fn cross_tri_filter_receives_semantic_source_indexes() {
let seen = Arc::new(std::sync::Mutex::new(Vec::new()));
let sink = seen.clone();
let constraint = CrossTriConstraint::new(
ConstraintRef::new("", "Indexed tri rows"),
ImpactType::Penalty,
source(
shifts_src as fn(&Schedule) -> &[Shift],
ChangeSource::Descriptor(0),
),
source(
employees_src as fn(&Schedule) -> &[Employee],
ChangeSource::Descriptor(1),
),
source(
days_off_src as fn(&Schedule) -> &[DayOff],
ChangeSource::Descriptor(2),
),
|shift: &Shift| shift.employee_id,
|employee: &Employee| Some(employee.id),
|day_off: &DayOff| day_off.employee_id,
move |_s: &Schedule,
_shift: &Shift,
_employee: &Employee,
_day_off: &DayOff,
a_idx: usize,
b_idx: usize,
c_idx: usize| {
sink.lock().unwrap().push((a_idx, b_idx, c_idx));
true
},
|_s: &Schedule, _a: usize, _b: usize, _c: usize| SoftScore::of(1),
false,
);
let schedule = sample_schedule();
let _ = constraint.evaluate(&schedule);
let rows = seen.lock().unwrap().clone();
assert_eq!(rows, vec![(0, 0, 0), (1, 1, 1), (2, 0, 0)]);
}