1+ use std:: sync:: {
2+ atomic:: { AtomicUsize , Ordering } ,
3+ Arc ,
4+ } ;
5+
16use crate :: api:: constraint_set:: IncrementalConstraint ;
27use crate :: constraint:: IncrementalCrossBiConstraint ;
3- use crate :: stream:: collection_extract:: { source, ChangeSource } ;
8+ use crate :: stream:: collection_extract:: { source, ChangeSource , CollectionExtract } ;
49use crate :: stream:: joiner:: equal_bi;
510use crate :: stream:: ConstraintFactory ;
611use solverforge_core:: score:: { Score , SoftScore } ;
@@ -24,6 +29,42 @@ struct Schedule {
2429 employees : Vec < Employee > ,
2530}
2631
32+ #[ derive( Clone ) ]
33+ struct CountingShiftExtract {
34+ calls : Arc < AtomicUsize > ,
35+ }
36+
37+ impl CollectionExtract < Schedule > for CountingShiftExtract {
38+ type Item = Shift ;
39+
40+ fn extract < ' s > ( & self , schedule : & ' s Schedule ) -> & ' s [ Self :: Item ] {
41+ self . calls . fetch_add ( 1 , Ordering :: Relaxed ) ;
42+ schedule. shifts . as_slice ( )
43+ }
44+
45+ fn change_source ( & self ) -> ChangeSource {
46+ ChangeSource :: Descriptor ( 0 )
47+ }
48+ }
49+
50+ #[ derive( Clone ) ]
51+ struct CountingEmployeeExtract {
52+ calls : Arc < AtomicUsize > ,
53+ }
54+
55+ impl CollectionExtract < Schedule > for CountingEmployeeExtract {
56+ type Item = Employee ;
57+
58+ fn extract < ' s > ( & self , schedule : & ' s Schedule ) -> & ' s [ Self :: Item ] {
59+ self . calls . fetch_add ( 1 , Ordering :: Relaxed ) ;
60+ schedule. employees . as_slice ( )
61+ }
62+
63+ fn change_source ( & self ) -> ChangeSource {
64+ ChangeSource :: Descriptor ( 1 )
65+ }
66+ }
67+
2768fn create_unavailable_employee_constraint ( ) -> impl IncrementalConstraint < Schedule , SoftScore > {
2869 IncrementalCrossBiConstraint :: new (
2970 ConstraintRef :: new ( "" , "Unavailable employee" ) ,
@@ -65,6 +106,40 @@ fn sample_schedule() -> Schedule {
65106 }
66107}
67108
109+ #[ test]
110+ fn cross_bi_unrelated_insert_skips_extractors ( ) {
111+ let shift_extract_calls = Arc :: new ( AtomicUsize :: new ( 0 ) ) ;
112+ let employee_extract_calls = Arc :: new ( AtomicUsize :: new ( 0 ) ) ;
113+ let mut constraint = IncrementalCrossBiConstraint :: new (
114+ ConstraintRef :: new ( "" , "Unavailable employee" ) ,
115+ ImpactType :: Penalty ,
116+ CountingShiftExtract {
117+ calls : Arc :: clone ( & shift_extract_calls) ,
118+ } ,
119+ CountingEmployeeExtract {
120+ calls : Arc :: clone ( & employee_extract_calls) ,
121+ } ,
122+ |shift : & Shift | shift. employee_id ,
123+ |employee : & Employee | Some ( employee. id ) ,
124+ |_schedule : & Schedule , shift : & Shift , employee : & Employee | {
125+ shift. employee_id . is_some ( ) && employee. unavailable_days . contains ( & shift. day )
126+ } ,
127+ |_schedule : & Schedule , _shift_idx : usize , _employee_idx : usize | SoftScore :: of ( 1 ) ,
128+ false ,
129+ ) ;
130+ let schedule = sample_schedule ( ) ;
131+
132+ assert_eq ! ( constraint. initialize( & schedule) , SoftScore :: of( -1 ) ) ;
133+ shift_extract_calls. store ( 0 , Ordering :: Relaxed ) ;
134+ employee_extract_calls. store ( 0 , Ordering :: Relaxed ) ;
135+
136+ let delta = constraint. on_insert ( & schedule, 0 , 2 ) ;
137+
138+ assert_eq ! ( delta, SoftScore :: zero( ) ) ;
139+ assert_eq ! ( shift_extract_calls. load( Ordering :: Relaxed ) , 0 ) ;
140+ assert_eq ! ( employee_extract_calls. load( Ordering :: Relaxed ) , 0 ) ;
141+ }
142+
68143#[ test]
69144fn test_cross_bi_evaluate_works_without_initialize ( ) {
70145 let constraint = create_unavailable_employee_constraint ( ) ;
0 commit comments