Download
include "all_equal.mzn";
include "global_cardinality.mzn";

% load parameters
include "parameters.dzn";

int: daysPerWeek = 7;

% the number of weeks
int: numberOfWeeks;

int: numberOfDays = numberOfWeeks * daysPerWeek;

% the minimum number of days in a row with the same shift
int: s_min;

% the maximum number of days in a row with the same shift
int: s_max; 

% 3 shifts: early = 1, late = 2 and night shift = 3 + day off (rest day) = 0
int: numberOfShifts = 3;

% the shift requirement matrix, including at M_{i,j} the needed employees for day i and shift j
array[1..daysPerWeek, 1..numberOfShifts+1] of int: shiftRequirements;


% the variables X 
array[1..numberOfWeeks * daysPerWeek] of var 0..numberOfShifts: plan1d;
array[1..numberOfWeeks,1..daysPerWeek] of var 0..numberOfShifts: plan2d;
array[1..daysPerWeek,1..numberOfWeeks] of var 0..numberOfShifts: plan2dT;

% helper arrays
array[1..s_min,1..s_min] of var 0..numberOfShifts: s_min_arrays;
array[1..s_max,1..s_max] of var 0..numberOfShifts: s_max_arrays;

% convert the 2D plan into 1D
constraint forall(week in 1..numberOfWeeks, day in 1..daysPerWeek) (
                 plan2d[week, day] == plan1d[(week-1) * daysPerWeek + day]
           );  

% transpose the 2D plan
constraint forall(week in 1..numberOfWeeks, day in 1..daysPerWeek) (
                 plan2d[week, day] == plan2dT[day, week]
           );

% C_equalDays: constrains that weekend days (Saturday and Sunday) always have the same shift
constraint forall(week in 1..numberOfWeeks) (
               plan2d[week, daysPerWeek - 1] == plan2d[week, daysPerWeek]
           );                        

                                                                                                                                
% create the sub arrays other the array bounds
constraint forall(i in 1..s_min, j in 1..s_min) (
                 s_min_arrays[i, j] == plan1d[((numberOfDays - s_min - 1 + i + j) mod numberOfDays) + 1]
           );                                            
                                            
% C_shiftRepetitions:for every shift type a minimum number of consecutive assignments to this shift is given
constraint forall(day in 1..numberOfDays - s_min) (
               plan1d[day] != plan1d[day+1] -> all_equal(plan1d[day+1..day+s_min])
           );     
% the constraints over the array bounds           
constraint forall(d in 1..s_min) (
               plan1d[d + numberOfDays - s_min] != plan1d[((d + numberOfDays - s_min) mod numberOfDays) +1] -> all_equal(s_min_arrays[d,1..s_min])
           );                     

           
% create the sub arrays other the array bounds
constraint forall(i in 1..s_max, j in 1..s_max) (
                 s_max_arrays[i, j] == plan1d[((numberOfDays - s_max - 2 + i + j) mod numberOfDays) + 1]
           );                      
                                            
% C_shiftRepetitions:for every shift type a maximum number of consecutive assignments to this shift is given           
constraint forall(day in 1..numberOfWeeks * daysPerWeek - s_max) (
               (all_equal(plan1d[day..day+s_max])) -> (plan1d[day] != plan1d[day + s_max])
           );               
% the constraints over the array bounds             
constraint forall(d in 1..s_max) (
               (all_equal(s_max_arrays[d, 1..s_max])) -> (plan1d[d + numberOfDays - s_max] != plan1d[d])
           );                     

                               
                                                                                                   
% C_restDays: at least 2 days must be rest days every 2 weeks.
constraint forall(day in 1..(numberOfWeeks - 2) * daysPerWeek) (
               count(j in plan1d[day..day + daysPerWeek * 2])(j=0) >= 2                   
           );            
constraint forall(i in 1..2 * daysPerWeek-1) (
               count(j in plan1d[numberOfWeeks * daysPerWeek-i .. numberOfWeeks * daysPerWeek] ++ plan1d[1..2*daysPerWeek-i])(j=0) >= 2    
           );         
                                            
% C_shiftOrder: restricts the order of shifts. There is a forward rotating principle. This means, that after an early shift there can only follow a shift with the same or a higher value, or a rest shift.
constraint forall(day in 2..numberOfWeeks * daysPerWeek - 1) (
               plan1d[day] <= plan1d[day + 1] \/ plan1d[day+1] == 0
           );         
constraint plan1d[1] >= plan1d[numberOfDays] \/ plan1d[1] == 0;
                      
% C_shiftRequirements: for every weekday for each shift the number of required staff is provided (e.g. usually less staff is required on the weekend) 
constraint forall(day in 1..daysPerWeek) (
              (global_cardinality(row(plan2dT,day),[0,1,2,3],row(shiftRequirements,day)))   
           );    
           
% solve the problem           
solve :: int_search(plan1d, input_order, indomain_min, complete)
      satisfy;           
      
% print the problem      
output [ show(row(plan2d,j)) ++ "\n" | j in 1..numberOfWeeks ] ++ ["\n"];