# Employee Scheduler version 1.51 # # Copyleft 2004 Brendan Byrd and Resonator Software # # Web site: http://www.ResonatorSoft.org/employee/ # # E-mail: Brendan Byrd/SineSwiper <SineSwiper@ResonatorSoft.org> #
It would be prefered that the script goes in its own directory, as it requires one or two text files (to read data from). You can also use this directory to store your schedule spreadsheets, etc.
The script itself requires Perl. On Windows, you can grab ActivePerl at www.activestate.com. From there, you can either run the script on a command prompt, or (better yet) you can associate *.pl files with the perl.exe program.
You'll need a decent machine with at least 256MB of RAM. The pre-caching can take up anywhere from 20-200MB of RAM, depending on how many different shift types you have. (It's much better to just find a machine with enough RAM, instead of subtracting your shift types.) The schedules generally get spit out about once every 3-5 seconds.
This is a REQUIRED file, containing a 7x24 matrix of the number of employees required per weekday per hour. Hours would be the rows; weekdays would be the columns. It can be delimited with any non-word character you want: commas, spaces, tabs, etc. The script will try to remove any headers, but it's recommended that you write the file with the numbers only. In Excel, you can do this by copying the numbers, and then pasting it in Notepad. A CSV file should work, too, but again, remove the headers. total.txt:
This is only needed if you are building off of an existing schedule. The format is the same as the goal.txt file. This is a matrix of the already existing employee schedules. The best way to get these numbers is to use the employee matrix that the script spits out. Delete this file if you are trying to create a new schedule.
On both of these files, you can use a five-minute matrix (7x288) if the file contains the words "FIVE MINUTE MATRIX" (all-caps).
The script itself has some variables (at the beginning) you can change that affect the behavior of the results:
$fulltime_min & $parttime_min:
The minimum number of full-time and part-time employees. If one reaches its minimum, it switches to the other. If both are at/past their minimum, it will continue as normal. $fulltime_max & $parttime_max: The maximum total number of full-time and part-time employees. If one runs out, it switches to the other. If both runs out before the goals are met, it will start to enter correction loops until the problem is solved. (NOTE: 0 means to disable the max, -1 means to set the max TO zero.) @fulltime_pos_min/max & @parttime_pos_min/max: These are the same as the FT/PT min/max variables above, but are confined to a specific shift. These values don't negate the total min/max variables, so it's recommended that you use those first and then set up shift position min/maxs as necessary. Shift positions are defined by the starting hour: 1st = 5AM-11PM, 2nd = 12PM-6PM, 3rd = 7PM-4AM. @ft_hr & @pt_hr: These are the different possible starting hours, for full-time and part-time respectively. Removing any hours will slightly affect the efficiency of the script, but can be used to remove any undesirable hours, like starting work at 4 in the morning, etc. Setting an hour will also set its X:30 mark. (For example, 6 would set 6:00AM and 6:30AM.)
The maximum number of total employees at any given hour. Use this if you have a limited number of places to put employees. $hourly_min: The minimum number of total employees at any given hour. Best used to make sure that there are, for example, at least two people working so that lunch/breaks/vacations are properly covered without any lapse. If this isn't a 24/7 workplace, you maybe want to set this to 0. $dt_step: This affects the "politeness" of the schedules. The higher the number, the less accurate it will be, but it will tend to have less fragmented and "double-weekend" schedules. At 0, it will be as efficient as possible. (More details on the specifics of how this works on the Behavior section.) $dt_step_breaks: The same as $dt_step, but only affects the breaks. $refuse_nconsec: If set to 1, this will completely disable non-consecutive (fragmented) schedules. Behavior-wise, this tends to increase the number of double-weekend schedules, though the overlap and number of employees stays about the same. $refuse_bothwkends: If set to 1, this will completely disable double-weekend schedules. Behavior-wise, this tends to increase the number of non-consecutive schedules, as well as overlap and number of employees. This option is not recommended, unless you have employees to waste. $defrag_weekends: If set to 1, this will cause the defragmentation loop to try to give some of its weekend days to other schedules. The result of this is that it will remove around 20% of the single-weekend schedules and split them up among 10/10% of no/double-weekend schedules. (The specifics on the defragmentation loop is covered in the Behavior section.)
All of the @shifts* variables set the type of shifts you will offer. To ensure that the schedule is efficient and that your employees have many options to choose from, be sure to include as many different type of shifts as you can. (Obviously, there's only a couple of 40-hour shifts you can offer, but you can have quite a few types of part-time shifts.) Full-time shifts are considered to be any shift with 40 or more hours. (The rest are part-times.)
The numbers need to match up with the rest of the @shift* variables. For example, the default shifts reads:
@shifts = (10, 8,10,10, 9, 9, 8, 8, 7, 7, 7, 6, 6, 5, 5, 4); @shifts_days = ( 4, 5, 3, 2, 4, 3, 4, 3, 5, 4, 3, 5, 4, 5, 4, 5); @shifts_fbreak = (15,15,15,15,15,15,15,15, 0, 0, 0, 0, 0,15,15,15); @shifts_lunch = (60,60,60,60,60,60,60,60,30,30,30,20,20, 0, 0, 0); @shifts_sbreak = (15,15,15,15,15,15,15,15, 0, 0, 0, 0, 0, 0, 0, 0); @shifts_lpaid = ( 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0);
Which means that there are these type of shifts available:
10hX4d = 40 hour (FT), 1hr lunch, two 15min breaks
8hX5d = 40 hour (FT), 1hr lunch, two 15min breaks
10hX3d = 30 hour (PT), 1hr lunch, two 15min breaks
10hX2d = 20 hour (PT), 1hr lunch, two 15min breaks
9hX4d = 36 hour (PT), 1hr lunch, two 15min breaks
9hX3d = 27 hour (PT), 1hr lunch, two 15min breaks
8hX4d = 32 hour (PT), 1hr lunch, two 15min breaks
8hX3d = 24 hour (PT), 1hr lunch, two 15min breaks
7hX5d = 35 hour (PT), 30min "paid lunch"
7hX4d = 28 hour (PT), 30min "paid lunch"
7hX3d = 21 hour (PT), 30min "paid lunch"
6hX5d = 30 hour (PT), 20min "paid lunch"
6hX4d = 24 hour (PT), 20min "paid lunch"
5hX5d = 25 hour (PT), 15min break
5hX4d = 20 hour (PT), 15min break
4hX5d = 20 hour (PT), 15min break
More on how the lunch/breaks work in the Lunch/Breaks section.
- Populate your goal matrix into goal.txt file.
- If you are trying to build off of an existing schedule, then populate the total.txt file with your current employee matrix. Otherwise, delete the file, if it exists.
- Edit the script variables to your liking.
- Go into the current directory and run the script. This process takes anywhere from 30 seconds to 20 minutes, depending on how big the schedule is, how strict the efficiency is set to, etc.
- When it is finished, it will generate a schd.csv file (the finished schedule), a schd-stats.txt file (ending stats), and a schd-newtotal.txt (new "total.txt" file for new schedule).
- If you don't like the output, try tweaking the options. (See the Problem/Solutions section.) Then, go back to step #4.
The first time you use the script, you should probably want to create a schedule first, instead of trying to add to a human-generated schedule. The script is most effective with a fresh start. Once you have a good and efficient schedule in place, you can start adding more schedules to your current one.
After adding a bunch of times, the schedule will start to become more wasteful of man-hours, so you will want to recreate the schedule every few months. This fixes any excessive overlaps, adjusts any shifts in workload, and gives employees a chance to pick a better schedule. Just like a hard drive, the schedule needs to be "defragged" a few times a year.
First, it will output the total.txt and goal.txt matrices, as it reads them. If they are incorrect, remove any garbage out of the files, and try again. There will also be warnings, if there is a problem with your settings. In most of these cases, it will attempt to correct the situation, but you should still pay attention to them.
It will begin to pre-cache all possible shift hours/days. This is the main memory hog, but this keeps the scheduler from taking eons to complete.
Then, it will start printing schedules:
Shift Position Type Days of Schd Time of Schd Breaks Ratio (GDS)
1st (late) 10hX4d Mon-Thu 1100-2200 L1400 1230,1645 40:0 (1452) 1st (late) 10hX4d Mon-Thu 1100-2200 L1400 1230,1630 40:0 (1414) 1st (late) 10hX4d Mon-Thu 1000-2100 L1300 1130,1545 40:0 (1378) 2nd (early) 8hX5d Mon-Fri 1300-2200 L1600 1430,2000 40:0 (1352) 1st (late) 10hX4d Mon-Tue,Thu,Sat 1000-2100 L1300 1130,1530 40:0 (1331) 2nd (early) 8hX5d Mon-Fri 1300-2200 L1600 1430,2015 40:0 (1297) 1st (late) 10hX4d Mon-Tue,Thu,Sat 1000-2100 L1300 1130,1545 40:0 (1277) 1st (late) 10hX4d Mon-Thu 1130-2230 L1445 1315,1745 40:0 (1249) 1st (late) 10hX4d Mon,Thu-Sat 1000-2100 L1300 1130,1545 40:0 (1233) 2nd (early) 8hX5d Mon-Fri 1300-2200 L1700 1445,2030 40:0 (1201) 1st (late) 10hX4d Mon,Thu-Sat 1000-2100 L1330 1145,1615 40:0 (1179) 2nd (early) 10hX4d Mon-Thu 1200-2300 L1500 1330,1730 40:0 (1159) 1st shift 10hX4d Mon,Wed,Fri-Sat 0930-2030 L1300 1115,1600 40:0 (1138) 1st (late) 10hX4d Sun-Tue,Thu 1100-2200 L1400 1230,1900 40:0 (1114) 1st shift 10hX4d Mon-Tue,Fri-Sat 0900-2000 L1200 1030,1515 40:0 (1097) 2nd shift 8hX5d Mon-Fri 1400-2300 L1700 1530,2045 40:0 (1076) ^^^^^^^^^^^ ^^^^^^ ^^^^^^^^^^^^^^^ ^^^^^^^^^ ^^^^^ ^^^^^^^^^ ^^^^ ^^^^ Shift Position | | | | | | | Shift Type -------/ | | | | | | Days of Schd ------------------/ | | | | | Time of Schd -------------------------------/ | | | | Lunch Hour -----------------------------------------/ | | | 1st/2nd Breaks ---------------------------------------------/ | | Underlap ratio ----------------------------------------------------/ |
Goal delta score --------------------------------------------------------/
The underlap ratio and goal delta score are covered in the Behavior section.
The script may need to go into correction loops, in order to fix problems. The reasons for these problems are listed as it goes into a loop. In a correction loop, wasteful schedules are deleted, so that they can be allocated for better spots.
After the schedules are finished (or it can't create any more, due to restrictions), it will spit out a summary of totals, as well as employee and overlap matrices. You can read the actual schedule in the schd.csv file.
Problem : Too much underlap
Solution: Make sure the full-time/part-time/hourly caps are not too low. The
script will only stop if there are enough schedules or if it runs into too many correction loops, due to overly restrictive caps. If the caps can't be raised, try reducing the goals a bit. (Also, see "Too much overlap" below.)
Problem : Too much overlap
Solution: If not set already, turn on all of the most efficient options, in
order of importance: both weekends on, dt_step(breaks) = 0, @fthr & @pt_hr set to all hours, non-consec schds on/off (play around with it). Also, remove any minimums, in case you're asking for more than what is required. Please note that some overlap will have to be there, due to the peaks in the goals.
Problem : Part-times/full-times aren't filling up the way they should Solution: All of the control for this should be in the PT/FT variables. The
script will make sure it fills up the minimums, unless it's not possible with the goals. (See "Too much overlap" above.) If you want -EXACTLY- X number of PT/FT employees, set the main min/max variables both to X.
Problem : Schedules aren't polite enough Solution: Play with the Flow Control options (under "Configuration" above),
Problem : Not enough variety of the different shifts Solution: Raising dt_step tends to have a big effect on the diversity of the
Problem : One shift has too much dominance over another Solution: The script will attempt to place the best schedule for the first
shift over the others. If you move the one you want to be first (leftmost) in the @shift* variables, it will tend to balance the shifts out. (NOTE: This only applies to shifts with the same number of hours, ie: 10hX4d and 8hX5d are both 40-hour shifts.)
The lunches and breaks are assigned by a specific set of rules, in order to conform with (my) state laws and to have a reasonable gap between one break/lunch and another. These rules are:
Assigned between 90min mark and 4th hour Must be 90mins before the start of a lunch Set in 5 minute marks (8:00, 8:05, 8:10, etc.) Lunches:
Assigned between the 3rd and 5th hour (4th hour if there are no breaks)
Set in 15 minute marks (8:00, 8:15, 8:30, etc.)
Lunches are assigned before breaks
Assigned between the 4rd and 8th hour Must be 90mins after the end of lunch Must be 90mins before the end of the shift Must be 2hr after the end of the first break Set in 5 minute marks (8:00, 8:05, 8:10, etc.)
Paid lunchs are just like breaks (in that they are considered "paid by the company" and don't increase the work hours), except they are assigned like lunches. This is useful for longer (houred) shifts that don't have a regular lunch.
Of course, these rules conform to my state laws (Kentucky; which are fairly well-designed and rightfully strict). If you prefer changes to these rules, e-mail me the changes, and I'll try to add variables to accommodate the changes.
(NOTE: This section is verbose and technical on purpose. You don't have to read it if you don't want to, though I would recommend at least skimming it over to get an idea on how it works.)
--- Main Priorities
The script works by a set of priorities. These priorities are slowly eroded as schedules become impossible using the current priorities. For example, if it cannot create a schedule without both weekends, it will look for ones with both weekends. If neither are possible, it will erode the next highest priority, and start over.
The list of priorities are, in order of highest to lowest:
Goal delta score
Double weekends off/on Non-consec schds off/on Shift types Weekdays (in order of highest combined goal delta) Hours (in order of highest combined goal delta) Lunch Hours
Most of the priorities are self-explainatory, but I'll go over the first two:
--- Overlap/Underlap Delta
The overlap/underlap delta is simply a count of the number of overlaps (hours that go over the goal, and thus, waste manhours) subtracted from the number of underlaps (hours that are required to fill in the goal). Therefore, the highest priority is to have as little waste in manhours as possible. The output will display the underlap ratio as "36:4", where 36 is the number of underlap hours and 4 is the number of overlap hours.
--- Goal Delta Score (GDS)
A goal delta is the total number of employees in a given hour subtracted from the goal of that hour. For example, if there are only 2 employees at 2PM Sunday and the goal is 25, the goal delta would be 23. The goal delta score (GDS) is the sum of all of the goal deltas in a possible schedule (44/45 different hours in a full-time schedule). Because full-time schedules would have a higher GDS than part-times, it will start to fill the full-times first. (Use of the FT/PT minimums/maximums can affect this behavior, however.)
The dt_step variable controls how slow or fast it should scan the best possible GDS. If set to a high number, it will start skipping schedules with a better GDS, in favor of schedules with no double weekends, consec schds, etc.
--- Choosing Breaks
After a schedule is picked, it will find the best breaks, based on the breaks' OL/UL delta and GDS. Breaks are analyzed in order of the middle of the period (ie: the ideal break), and spread on out, with the invalid breaks removed (ie: the ones that break the rules in "Lunch/Breaks"). For example: a 8hX5d schedule at 8AM-5PM, with an hour lunch at 12PM and two breaks, would try out a first break at 10AM, 10:05, 9:55, 10:10, 9:50, etc.
The ideal break depends on how many breaks/lunchs exist. If you have two breaks and a lunch, it will try to put the break on the 1/4th marks (2hr marks for an 8hr shift); for two breaks only, on the 1/3rd marks; etc. This ensures that the breaks are generally evenly spread out.
--- Correction Loops
Correction loops are used to correct problems that arise from FT/PT limitations put in place, by removing useless or nearly useless schedules. (The schedule may have been useful at first, but when other schedules came in play, their overlaps actually made the original schedule obsolete.) It's not made to be completely perfect (at least not as "perfect" as the main scheduler loop is), but to merely remove enough schedules to get the main loop back in action.
The correction loop will always remove schedules that are deemed totally useless. (In other words, they have an underlap of zero.) Beyond that, it will remove schedules in order of lowest GDS. The number of schedules it removes depends on the lowest underlap on the overlap map. For example, if the lowest underlap is -4, it will remove 4 FT and 4 PT schedules, or just 4 FT, or 4 PT, depending on which limiter the main loop is getting hung up. (Zero UL schedules don't count towards the number of schedules removed.)
After schedules are removed, it will resume itself back into the main loop, resetting the priorities back to the top, and hopefully straighten itself out this round. The script will enter as many as 10 correction loops, before it declares the effort a lost cause and gives up.
--- Defragmentation Loop
The defragmentation loop is only executed right before it's completely finished with the schedules. It's main purpose is to take two fragmented schedules that both have the same hours (but different days), and try to combine them to make two defragmented schedules, by checking for gaps and "island" days. Lunch and breaks are factored to make sure that the change doesn't cause any underlap. (The lunches stay the same, but the breaks are chosen again after the defragmentation.)
Weekends are normally never traded, but if $defrag_weekends is set, it will try to move as many weekends from the first schedule to the second one. (This is done only on the single-weekend schedules.)