3
votes

I have been struggling with a requirement for some days now, and i think i am making it harder than it is. I want to make some kind of fluid rota/shift system. This should be a wizard / randomizer function just to help the user to create a "one time" timetable (facility management). I'll like to make it as generic as possible.

X amount of staff

X amount of daily shifts (ex. morning, lunch, evening and night)

X amount of periods (ex. 4 weeks - a period is one week)

This will result in the following setup:

Week 1: Morning (*), Lunch, Evening and Night

Week 2: Morning, Lunch (*), Evening and Night

Week 3: Morning, Lunch, Evening (*) and Night

Week 4: Morning, Lunch, Evening and Night (*)

Each staff must take a shift each week, but the shift must not be the same the following weeks. I can make the requirement that there should be enough staff to fill every shift for all weeks. The result should be random on every execution.

Example:

If i have 8 employees and 4 shifts per week. There should be at least 2 employees per shift per week.

If i have 4 employees and 4 shifts per week. There should be at least 1 employee per shift per week.

The amount of employees must always be equal or more that the amount of shifts.

Is there any "leftovers" this is OK, when the amount of employees doesn't match with the amount of shifts, then the users either must defined more shifts or employees.

Example can be seen here: http://v2.iclean.dk/shifts

I have the following code:

Classes

[DebuggerDisplay("ID: {ID}, Name: {Name}")]
public class Staff
{
    public Staff(int id, string name)
    {
        ID = id;
        Name = name;
        Schedulers = new List<StaffScheduler>();
    }
    public int ID { get; set; }
    public string Name { get; set; }
    public List<StaffScheduler> Schedulers { get; set; }
}
public class StaffMap
{
    public StaffMap(Staff staff, StaffScheduler scheduler)
    {
        Staff = staff;
        Scheduler = scheduler;
    }
    public Staff Staff { get; set; }
    public StaffScheduler Scheduler { get; set; }
}

[DebuggerDisplay("ID: {ID}, Name: {Name}")]
public class StaffScheduler
{
    public StaffScheduler(int id, string name)
    {
        ID = id;
        Name = name;
    }

    public int ID { get; set; }
    public string Name { get; set; }
}

[DebuggerDisplay("Name: {Name}, Start: {DateStart}, End: {DateEnd}")]
public class DateRangeShift
{
    public DateRangeShift()
    {
        Staff = new List<StaffMap>();
    }
    public DateRangeShift(string name, DateTime start, DateTime end)
        : this()
    {
        Name = name;
        DateStart = start;
        DateEnd = new DateTime(end.Year, end.Month, end.Day, 23, 59, 59);
    }

    public string Name { get; set; }
    public DateTime DateStart { get; set; }
    public DateTime DateEnd { get; set; }

    public List<StaffMap> Staff { get; set; }
}

MVC Controller code

private readonly int SHIFTS_MAX_SHUFFLE_RETRIES = 50;

public ActionResult Index()
{
    var dailySchedules = new List<StaffScheduler>() { 
        new StaffScheduler(1, "Morning"), 
        new StaffScheduler(2, "Lunch"), 
        new StaffScheduler(3, "Evening"), 
        new StaffScheduler(4, "Night")
    };

    List<DateRangeShift> shiftList = new List<DateRangeShift>();
    shiftList.Add(new DateRangeShift("Week 1", new DateTime(2013, 3, 4), new DateTime(2013, 3, 10)));
    shiftList.Add(new DateRangeShift("Week 2", new DateTime(2013, 3, 11), new DateTime(2013, 3, 17)));
    shiftList.Add(new DateRangeShift("Week 3", new DateTime(2013, 3, 18), new DateTime(2013, 3, 24)));
    shiftList.Add(new DateRangeShift("Week 4", new DateTime(2013, 3, 25), new DateTime(2013, 3, 31)));

    List<Staff> staffList = new List<Staff>();
    staffList.Add(new Staff(1, "Fred Smith"));
    staffList.Add(new Staff(2, "Charlie Brown"));
    staffList.Add(new Staff(3, "Samantha Green"));
    staffList.Add(new Staff(4, "Bash Malik"));
    staffList.Add(new Staff(5, "Bryan Griffiths"));
    staffList.Add(new Staff(6, "Akaash Patel"));
    staffList.Add(new Staff(7, "Kang-Hyun Kim"));
    staffList.Add(new Staff(8, "Pedro Morales"));

    var shiftSegmentSize = (int)Math.Floor((double)staffList.Count / (double)dailySchedules.Count)

    int shuffleCount = 0;
    bool shuffleMatchedSize = true;
    do
    {
        shiftList.ForEach(e => e.Staff.Clear());
        shuffleMatchedSize = true;

        foreach (var shift in shiftList)
        {
            foreach (var scheduler in dailySchedules)
            {
                var schedulerEmployees = staffList.OrderByDescending(e => e.Schedulers.Count()).ThenBy(e => Guid.NewGuid()).Where(e => !shiftList.Any(sl => sl.Staff.Any(s => s.Scheduler.ID == scheduler.ID && s.Staff.ID == e.ID)) && !shift.Staff.Any(s => s.Staff.ID == e.ID)).Take(shiftSegmentSize).ToList();
                if (schedulerEmployees.Count < shiftSegmentSize)
                {
                    shuffleMatchedSize = false;
                    shuffleCount++;
                    break;
                }

                while (schedulerEmployees.Count > 0)
                {
                    var staffSelector = schedulerEmployees.FirstOrDefault();

                    var staff = staffList.FirstOrDefault(e => e.ID == staffSelector.ID);
                    staff.Schedulers.Add(scheduler);

                    shift.Staff.Add(new StaffMap(staff, scheduler));

                    schedulerEmployees.Remove(staffSelector);
                }
            }
            if (!shuffleMatchedSize)
                break;
        }

    } while (!shuffleMatchedSize && shuffleCount < SHIFTS_MAX_SHUFFLE_RETRIES);

    ViewData["Iterations"] = shuffleCount;

    return View(shiftList);
}

But since i order by the amount of shifts assigned to a staff, and order random afterwards .. i have a make retries until i get a min. match of staff / shift.

<h2>Week 1 <span>(04-03-2013 - 10-03-2013)</span></h2>
<table>
    <tr>
        <td>Morning</td>
        <td>Akaash Patel, Bash Malik</td>
    </tr>
    <tr>
        <td>Lunch</td>
        <td>Fred Smith, Kang-Hyun Kim</td>
    </tr>
    <tr>
        <td>Evening</td>
        <td>Charlie Brown, Samantha Green</td>
    </tr>
    <tr>
        <td>Night</td>
        <td>Bryan Griffiths, Pedro Morales</td>
    </tr>
</table>
<h2>Week 2 <span>(11-03-2013 - 17-03-2013)</span></h2>
<table>
    <tr>
        <td>Morning</td>
        <td>Fred Smith, Kang-Hyun Kim</td>
    </tr>
    <tr>
        <td>Lunch</td>
        <td>Akaash Patel, Bash Malik</td>
    </tr>
    <tr>
        <td>Evening</td>
        <td>Bryan Griffiths, Pedro Morales</td>
    </tr>
    <tr>
        <td>Night</td>
        <td>Charlie Brown, Samantha Green</td>
    </tr>
</table>
<h2>Week 3 <span>(18-03-2013 - 24-03-2013)</span></h2>
<table>
    <tr>
        <td>Morning</td>
        <td>Charlie Brown, Samantha Green</td>
    </tr>
    <tr>
        <td>Lunch</td>
        <td>Bryan Griffiths, Pedro Morales</td>
    </tr>
    <tr>
        <td>Evening</td>
        <td>Akaash Patel, Fred Smith</td>
    </tr>
    <tr>
        <td>Night</td>
        <td>Bash Malik, Kang-Hyun Kim</td>
    </tr>
</table>
<h2>Week 4 <span>(25-03-2013 - 31-03-2013)</span></h2>
<table>
    <tr>
        <td>Morning</td>
        <td>Bryan Griffiths, Pedro Morales</td>
    </tr>
    <tr>
        <td>Lunch</td>
        <td>Charlie Brown, Samantha Green</td>
    </tr>
    <tr>
        <td>Evening</td>
        <td>Bash Malik, Kang-Hyun Kim</td>
    </tr>
    <tr>
        <td>Night</td>
        <td>Akaash Patel, Fred Smith</td>
    </tr>
</table>

I can't get my head around how to do this. Hope someone can guide me.

3
This may not be a valid approach, but I would use Switch Statements and issue a particular schedule type to an integer value. Then in your code when you input that integer value it could assign that type of schedule to that employee. Just a thought. - Greg
Hi Greg. I think this would make the code too static or locked to specific rules. - Carsten Petersen
I think you should consider renaming the title of this question. From the title it sounds like a real-world people-management issue, rather than a software development issue! - Mike Baxter
Teifi you're right. Thanks. I hope it is better now? - Carsten Petersen
@CarstenPetersen, can you update your question with expected precondition. Is staff.Count <= shift.Count? Otherwise you won't be able to satisfy Each staff must take a shift each week. Anything else? At the moment it is not completely clear what is the shape of input and how collection sizes correspond to each other. - Snowbear

3 Answers

1
votes

I think this work for you:

        int maxLength = Math.Max(shiftList.Count, dailySchedules.Count);

        var shiftSegmentSize = staffList.Count / maxLength;

        var schedulerEmployees = staffList.OrderBy(e => Guid.NewGuid()) 
                                          .Take(shiftSegmentSize * maxLength) 
                                          .ToArray(); //

        for (int k = 0; k < shiftList.Count; k++)
        {
            for (int j = 0; j < dailySchedules.Count; j++)
            {
                int staffindex = (maxLength + j - k) % maxLength;

                for (int i = 0; i < shiftSegmentSize; i++)
                {
                    schedulerEmployees[staffindex + (i * maxLength)].Schedulers.Add(dailySchedules[j]);
                    shiftList[k].Staff.Add(new StaffMap(schedulerEmployees[staffindex + i * maxLength], dailySchedules[j]));
                }
            }
        }
0
votes

I war playing with your code here, and I think it is correct now:

       var dailySchedules = new List<StaffScheduler>() { 
            new StaffScheduler(1, "Morning"), 
            new StaffScheduler(2, "Lunch"), 
            new StaffScheduler(3, "Evening"), 
            new StaffScheduler(4, "Night")
        };

        List<DateRangeShift> shiftList = new List<DateRangeShift>();
        shiftList.Add(new DateRangeShift("Week 1", new DateTime(2013, 3, 4), new DateTime(2013, 3, 10)));
        shiftList.Add(new DateRangeShift("Week 2", new DateTime(2013, 3, 11), new DateTime(2013, 3, 17)));
        shiftList.Add(new DateRangeShift("Week 3", new DateTime(2013, 3, 18), new DateTime(2013, 3, 24)));
        shiftList.Add(new DateRangeShift("Week 4", new DateTime(2013, 3, 25), new DateTime(2013, 3, 31)));

        List<Staff> staffList = new List<Staff>();
        staffList.Add(new Staff(1, "Fred Smith"));
        staffList.Add(new Staff(2, "Charlie Brown"));
        staffList.Add(new Staff(3, "Samantha Green"));
        staffList.Add(new Staff(4, "Bash Malik"));
        staffList.Add(new Staff(5, "Bryan Griffiths"));
        staffList.Add(new Staff(6, "Akaash Patel"));
        staffList.Add(new Staff(7, "Kang-Hyun Kim"));
        staffList.Add(new Staff(8, "Pedro Morales"));

        var shiftSegmentSize = (int)Math.Floor((double)staffList.Count / (double)dailySchedules.Count);

        Dictionary<StaffScheduler, Staff[]> map = new Dictionary<StaffScheduler, Staff[]>();

        foreach (var schedule in dailySchedules)
        {
            map.Add(schedule, staffList.ToArray());
        }

        Random r = new Random();

        foreach (var shift in shiftList)
        {
            shift.Staff = new List<StaffMap>();

            foreach(var schedule in dailySchedules)
            {
                int randomStaff_Index = r.Next(map[schedule].Length);

                Staff randStaff = map[schedule][randomStaff_Index];

                map[schedule] = map[schedule].Where(x => x != randStaff).ToArray();

                shift.Staff.Add(new StaffMap(randStaff, schedule));
            }
        }
0
votes

Final result became:

Models:

[DebuggerDisplay("ID: {ID}, Name: {Name}")]
public class Employee
{
    public Employee(string name)
    {
        Name = name;
        Schedules = new List<Schedule>();
    }
    public string Name { get; set; }
    public List<Schedule> Schedules { get; set; }
}
public class EmployeeSchedule
{
    public EmployeeSchedule(Employee employee, Schedule schedule)
    {
        Employee = employee;
        Schedule = schedule;
    }
    public Employee Employee { get; set; }
    public Schedule Schedule { get; set; }
}

[DebuggerDisplay("ID: {ID}, Name: {Name}")]
public class Schedule
{
    public Schedule(string name)
    {
        Name = name;
    }

    public string Name { get; set; }
}

[DebuggerDisplay("Name: {Name}, Start: {DateStart}, End: {DateEnd}")]
public class Shift
{
    public Shift()
    {
        EmployeeSchedules = new List<EmployeeSchedule>();
    }
    public Shift(string name, DateTime start, DateTime end) : this()
    {
        Name = name;
        DateStart = start.Date;
        DateEnd = new DateTime(end.Year, end.Month, end.Day, 23, 59, 59, 999);
    }

    public string Name { get; set; }
    public DateTime DateStart { get; set; }
    public DateTime DateEnd { get; set; }
    public List<EmployeeSchedule> EmployeeSchedules { get; set; }
}

Console App code:

class Program
{
    static void Main(string[] args)
    {
        var schedules = new List<Schedule>();
        schedules.Add(new Schedule("Morning"));
        schedules.Add(new Schedule("Lunch"));
        schedules.Add(new Schedule("Evening"));
        schedules.Add(new Schedule("Night"));

        var shifts = new List<Shift>();
        shifts.Add(new Shift("Week 1", new DateTime(2013, 3, 4), new DateTime(2013, 3, 10)));
        shifts.Add(new Shift("Week 2", new DateTime(2013, 3, 11), new DateTime(2013, 3, 17)));
        shifts.Add(new Shift("Week 3", new DateTime(2013, 3, 18), new DateTime(2013, 3, 24)));
        shifts.Add(new Shift("Week 4", new DateTime(2013, 3, 25), new DateTime(2013, 3, 31)));

        var employees = new List<Employee>();
        employees.Add(new Employee("Fred Smith"));
        employees.Add(new Employee("Charlie Brown"));
        employees.Add(new Employee("Samantha Green"));
        employees.Add(new Employee("Bash Malik"));
        employees.Add(new Employee("Bryan Griffiths"));
        employees.Add(new Employee("Akaash Patel"));
        employees.Add(new Employee("Kang-Hyun Kim"));
        employees.Add(new Employee("Pedro Morales"));

        ShuffleEmployees(ref shifts, schedules, employees);

        var scheduleNameMaxLength = schedules.Max(e => e.Name.Length);
        foreach (var shift in shifts)
        {
            Console.WriteLine($"{shift.Name} ({shift.DateStart:D} - {shift.DateEnd:D})");
            foreach (var map in shift.EmployeeSchedules)
            {
                Console.WriteLine($"- {map.Schedule.Name.PadRight(scheduleNameMaxLength + 1, ' ')}: {map.Employee.Name}");
            }
            Console.WriteLine();
        }

        Console.WriteLine("Press any key to continue ...");
        Console.ReadKey();
    }

    static void ShuffleEmployees(ref List<Shift> shifts, List<Schedule> schedules, List<Employee> employees)
    {
        var maxLength = Math.Max(shifts.Count, schedules.Count);
        var shiftSegmentSize = (employees.Count / maxLength);
        var employeeList = employees.OrderBy(e => Guid.NewGuid()).Take(shiftSegmentSize * maxLength).ToArray();

        var shiftsCount = shifts.Count;
        var schedulesCount = schedules.Count;

        for (int shiftIndex = 0; shiftIndex < shiftsCount; shiftIndex++)
        {
            for (int scheduleIndex = 0; scheduleIndex < schedulesCount; scheduleIndex++)
            {
                var employeeIndex = (maxLength + scheduleIndex - shiftIndex) % maxLength;
                for (int segment = 0; segment < shiftSegmentSize; segment++)
                {
                    employeeList[employeeIndex + (segment * maxLength)].Schedules.Add(schedules[scheduleIndex]);
                    shifts[shiftIndex].EmployeeSchedules.Add(new EmployeeSchedule(employeeList[employeeIndex + segment * maxLength], schedules[scheduleIndex]));
                }
            }
        }
    }
}