View Javadoc

1   /**
2    * Copyright 2004-2013 The Kuali Foundation
3    *
4    * Licensed under the Educational Community License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    *
8    * http://www.opensource.org/licenses/ecl2.php
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   * See the License for the specific language governing permissions and
14   * limitations under the License.
15   */
16  package org.kuali.hr.time.shiftdiff.rule.service;
17  
18  import org.apache.log4j.Logger;
19  import org.joda.time.*;
20  import org.kuali.hr.job.Job;
21  import org.kuali.hr.time.calendar.CalendarEntries;
22  import org.kuali.hr.time.principal.PrincipalHRAttributes;
23  import org.kuali.hr.time.service.base.TkServiceLocator;
24  import org.kuali.hr.time.shiftdiff.rule.ShiftDifferentialRule;
25  import org.kuali.hr.time.shiftdiff.rule.dao.ShiftDifferentialRuleDao;
26  import org.kuali.hr.time.timeblock.TimeBlock;
27  import org.kuali.hr.time.timeblock.TimeHourDetail;
28  import org.kuali.hr.time.timesheet.TimesheetDocument;
29  import org.kuali.hr.time.util.TKUtils;
30  import org.kuali.hr.time.util.TkConstants;
31  import org.kuali.hr.time.util.TkTimeBlockAggregate;
32  import org.kuali.hr.time.workflow.TimesheetDocumentHeader;
33  
34  import java.math.BigDecimal;
35  import java.sql.Date;
36  import java.util.*;
37  
38  
39  public class ShiftDifferentialRuleServiceImpl implements ShiftDifferentialRuleService {
40  
41  	@SuppressWarnings("unused")
42  	private static final Logger LOG = Logger.getLogger(ShiftDifferentialRuleServiceImpl.class);
43  	/**
44  	 * The maximum allowable time between timeblocks before we consider them to
45  	 * be day-boundary single time blocks.
46  	 */
47  	private ShiftDifferentialRuleDao shiftDifferentialRuleDao = null;
48  
49  	private Map<Long,List<ShiftDifferentialRule>> getJobNumberToShiftRuleMap(TimesheetDocument timesheetDocument) {
50  		Map<Long,List<ShiftDifferentialRule>> jobNumberToShifts = new HashMap<Long,List<ShiftDifferentialRule>>();
51  		PrincipalHRAttributes principalCal = TkServiceLocator.getPrincipalHRAttributeService().getPrincipalCalendar(timesheetDocument.getPrincipalId(),timesheetDocument.getCalendarEntry().getEndPeriodDate());
52  
53  		for (Job job : timesheetDocument.getJobs()) {
54  			List<ShiftDifferentialRule> shiftDifferentialRules = getShiftDifferentalRules(job.getLocation(),job.getHrSalGroup(),job.getPayGrade(),principalCal.getPayCalendar(),
55  					TKUtils.getTimelessDate(timesheetDocument.getCalendarEntry().getBeginPeriodDateTime()));
56  			if (shiftDifferentialRules.size() > 0)
57  				jobNumberToShifts.put(job.getJobNumber(), shiftDifferentialRules);
58  		}
59  
60  		return jobNumberToShifts;
61  	}
62  
63  	private Map<Long,List<TimeBlock>> getPreviousPayPeriodLastDayJobToTimeBlockMap(TimesheetDocument timesheetDocument, Map<Long,List<ShiftDifferentialRule>> jobNumberToShifts) {
64  		Map<Long, List<TimeBlock>> jobNumberToTimeBlocksPreviousDay = null;
65  
66  		// Get the last day of the last week of the previous pay period.
67  		// This is the only day that can have impact on the current day.
68  		List<TimeBlock> prevBlocks = TkServiceLocator.getTimesheetService().getPrevDocumentTimeBlocks(timesheetDocument.getPrincipalId(), timesheetDocument.getDocumentHeader().getBeginDate());
69  		if (prevBlocks.size() > 0) {
70  			TimesheetDocumentHeader prevTdh = TkServiceLocator.getTimesheetDocumentHeaderService().getPreviousDocumentHeader(timesheetDocument.getPrincipalId(), timesheetDocument.getDocumentHeader().getBeginDate());
71  			if (prevTdh != null) {
72  				CalendarEntries prevPayCalendarEntry = TkServiceLocator.getCalendarService().getCalendarDatesByPayEndDate(timesheetDocument.getPrincipalId(), prevTdh.getEndDate(), TkConstants.PAY_CALENDAR_TYPE);
73  				TkTimeBlockAggregate prevTimeAggregate = new TkTimeBlockAggregate(prevBlocks, prevPayCalendarEntry, prevPayCalendarEntry.getCalendarObj(), true);
74  				List<List<TimeBlock>> dayBlocks = prevTimeAggregate.getDayTimeBlockList();
75  				List<TimeBlock> previousPeriodLastDayBlocks = dayBlocks.get(dayBlocks.size() - 1);
76  				// Set back to null if there is nothing in the list.
77  				if (previousPeriodLastDayBlocks.size() > 0) {
78  					jobNumberToTimeBlocksPreviousDay = new HashMap<Long, List<TimeBlock>>();
79  
80  					for (TimeBlock block : previousPeriodLastDayBlocks) {
81  						// Job Number to TimeBlock for Last Day of Previous Time
82  						// Period
83  						Long jobNumber = block.getJobNumber();
84  						if (jobNumberToShifts.containsKey(jobNumber)) {
85  							// we have a useful timeblock.
86  							List<TimeBlock> jblist = jobNumberToTimeBlocksPreviousDay.get(jobNumber);
87  							if (jblist == null) {
88  								jblist = new ArrayList<TimeBlock>();
89  								jobNumberToTimeBlocksPreviousDay.put(jobNumber, jblist);
90  							}
91  							jblist.add(block);
92  						}
93  					}
94  				}
95  			}
96  		}
97  
98  		return jobNumberToTimeBlocksPreviousDay;
99  	}
100 
101 	private boolean timeBlockHasEarnCode(Set<String> earnCodes, TimeBlock block) {
102 		boolean present = false;
103 
104 		if (block != null && earnCodes != null)
105 			present = earnCodes.contains(block.getEarnCode());
106 
107 		return present;
108 	}
109 
110     /**
111      * Returns a BigDecimal representing the sum of all of the negative time
112      * hour detail types. In this case, only LUN is considered. This can be
113      * modified to add other considerations.
114      *
115      * @param block The Timeblock to inspect.
116      *
117      * @return A big decimal.
118      */
119     private BigDecimal negativeTimeHourDetailSum(TimeBlock block) {
120         BigDecimal sum = BigDecimal.ZERO;
121 
122         if (block != null) {
123             List<TimeHourDetail> details = block.getTimeHourDetails();
124             for (TimeHourDetail detail : details) {
125                 if (detail.getEarnCode().equals(TkConstants.LUNCH_EARN_CODE)) {
126                     sum = sum.add(detail.getHours());
127                 }
128             }
129         }
130 
131         return sum;
132     }
133 
134 	@Override
135 	public void processShiftDifferentialRules(TimesheetDocument timesheetDocument, TkTimeBlockAggregate aggregate) {
136         DateTimeZone zone = TkServiceLocator.getTimezoneService().getUserTimezoneWithFallback();
137 		List<List<TimeBlock>> blockDays = aggregate.getDayTimeBlockList();
138 		DateTime periodStartDateTime = timesheetDocument.getCalendarEntry().getBeginLocalDateTime().toDateTime(zone);
139 		Map<Long,List<ShiftDifferentialRule>> jobNumberToShifts = getJobNumberToShiftRuleMap(timesheetDocument);
140 
141 
142         // If there are no shift differential rules, we have an early exit.
143 		if (jobNumberToShifts.isEmpty()) {
144 			return;
145 		}
146 
147 		// Get the last day of the previous pay period. We need this to determine
148 		// if there are hours from the previous pay period that will effect the
149 		// shift rule on the first day of the currently-being-processed pay period.
150 		//
151         // Will be set to null if not applicable.
152         boolean previousPayPeriodPrevDay = true;
153 		Map<Long, List<TimeBlock>> jobNumberToTimeBlocksPreviousDay =
154                 getPreviousPayPeriodLastDayJobToTimeBlockMap(timesheetDocument, jobNumberToShifts);
155 
156 		// We are going to look at the time blocks grouped by Days.
157         //
158         // This is a very large outer loop.
159 		for (int pos = 0; pos < blockDays.size(); pos++) {
160 			List<TimeBlock> blocks = blockDays.get(pos); // Timeblocks for this day.
161 			if (blocks.isEmpty())
162 				continue; // No Time blocks, no worries.
163 
164 			DateTime currentDay = periodStartDateTime.plusDays(pos);
165 			Interval virtualDay = new Interval(currentDay, currentDay.plusHours(24));
166 
167 			// Builds our JobNumber to TimeBlock for Current Day List.
168             //
169             // Shift Differential Rules are also grouped by Job number, this
170             // provides a quick way to do the lookup / reference.
171             // We don't need every time block, only the ones that will be
172             // applicable to the shift rules.
173 			Map<Long, List<TimeBlock>> jobNumberToTimeBlocks = new HashMap<Long,List<TimeBlock>>();
174 			for (TimeBlock block : blocks) {
175 				Long jobNumber = block.getJobNumber();
176 				if (jobNumberToShifts.containsKey(jobNumber)) {
177 					List<TimeBlock> jblist = jobNumberToTimeBlocks.get(jobNumber);
178 					if (jblist == null) {
179 						jblist = new ArrayList<TimeBlock>();
180 						jobNumberToTimeBlocks.put(jobNumber, jblist);
181 					}
182 					jblist.add(block);
183 				}
184 			}
185 
186 
187             // Large Outer Loop to look at applying the Shift Rules based on
188             // the current JobNumber.
189             //
190 			// This loop will handle previous day boundary time as well as the
191             // current day.
192             //
193             // There is room for refactoring here!
194 			for (Long jobNumber: jobNumberToShifts.keySet()) {
195 				List<ShiftDifferentialRule> shiftDifferentialRules = jobNumberToShifts.get(jobNumber);
196 				// Obtain and sort our previous and current time blocks.
197 				List<TimeBlock> ruleTimeBlocksPrev = null;
198 				List<TimeBlock> ruleTimeBlocksCurr = jobNumberToTimeBlocks.get(jobNumber);
199 				if (ruleTimeBlocksCurr != null && ruleTimeBlocksCurr.size() > 0) {
200 					if (jobNumberToTimeBlocksPreviousDay != null)
201 						ruleTimeBlocksPrev = jobNumberToTimeBlocksPreviousDay.get(jobNumber);
202 					if (ruleTimeBlocksPrev != null && ruleTimeBlocksPrev.size() > 0)
203 						this.sortTimeBlocksInverse(ruleTimeBlocksPrev);
204 					this.sortTimeBlocksNatural(ruleTimeBlocksCurr);
205 				} else {
206 					// Skip to next job, there is nothing for this job
207 					// on this day, and because of this we don't care
208 					// about the previous day either.
209 					continue;
210 				}
211 
212 				for (ShiftDifferentialRule rule : shiftDifferentialRules) {
213 					Set<String> fromEarnGroup = TkServiceLocator.getEarnCodeGroupService().getEarnCodeListForEarnCodeGroup(rule.getFromEarnGroup(), TKUtils.getTimelessDate(timesheetDocument.getCalendarEntry().getBeginPeriodDateTime()));
214 
215                     LocalTime ruleStart = new LocalTime(rule.getBeginTime(), zone);
216                     LocalTime ruleEnd = new LocalTime(rule.getEndTime(), zone);
217 
218 
219 					DateTime shiftEnd = ruleEnd.toDateTime(currentDay);
220 					DateTime shiftStart = ruleStart.toDateTime(currentDay);
221 
222 					if (shiftEnd.isBefore(shiftStart) || shiftEnd.isEqual(shiftStart)) {
223 						shiftEnd = shiftEnd.plusDays(1);
224                     }
225 					Interval shiftInterval = new Interval(shiftStart, shiftEnd);
226 
227 					// Set up buckets to handle previous days time accumulations
228 					BigDecimal hoursBeforeVirtualDay = BigDecimal.ZERO;
229 
230 					// Check current day first block to see if start time gap from virtual day start is greater than max gap
231 					// if so, we can skip the previous day checks.
232 					TimeBlock firstBlockOfCurrentDay = null;
233 					for (TimeBlock b : ruleTimeBlocksCurr) {
234 						if (timeBlockHasEarnCode(fromEarnGroup, b)) {
235 							firstBlockOfCurrentDay = b;
236 							break;
237 						}
238 					}
239 
240 					// Previous Day :: We have prior block container of nonzero size, and the previous day is active.
241 					Interval previousDayShiftInterval = new Interval(shiftStart.minusDays(1), shiftEnd.minusDays(1));
242 
243                     // Blank initialization pointer for picking which interval to pass to applyPremium()
244                     Interval evalInterval = null;
245 					if (ruleTimeBlocksPrev != null && ruleTimeBlocksPrev.size() > 0 && dayIsRuleActive(currentDay.minusDays(1), rule)) {
246 						// Simple heuristic to see if we even need to worry about
247 						// the Shift rule for this set of data.
248 						if (shiftEnd.isAfter(virtualDay.getEnd())) {
249 							// Compare first block of previous day with first block of current day for max gaptitude.
250 							TimeBlock firstBlockOfPreviousDay = null;
251 							for (TimeBlock b : ruleTimeBlocksPrev) {
252 								if (timeBlockHasEarnCode(fromEarnGroup, b)) {
253 									firstBlockOfPreviousDay = b;
254 									break;
255 								}
256 							}
257 							// Only if we actually have at least one block.
258                             // Adding Assumption: We must have both a valid current and previous block. Max Gap can not be more than a virtual day.
259                             // If this assumption does not hold, additional logic will be needed to iteratively go back in time to figure out which
260                             // blocks are valid.
261 							if ( (firstBlockOfPreviousDay != null) && (firstBlockOfCurrentDay != null)) {
262 								Interval previousBlockInterval = new Interval(new DateTime(firstBlockOfPreviousDay.getEndTimestamp(), zone), new DateTime(firstBlockOfCurrentDay.getBeginTimestamp(), zone));
263 								Duration blockGapDuration = previousBlockInterval.toDuration();
264 								BigDecimal bgdHours = TKUtils.convertMillisToHours(blockGapDuration.getMillis());
265 								// if maxGap is 0, ignore gaps and assign shift to time blocks within the hours
266 								if (rule.getMaxGap().compareTo(BigDecimal.ZERO) == 0 || bgdHours.compareTo(rule.getMaxGap()) <= 0) {
267 									// If we are here, we know we have at least one valid time block to pull some hours forward from.
268 
269 
270 									// These are inversely sorted.
271 									for (int i=0; i<ruleTimeBlocksPrev.size(); i++) {
272 										TimeBlock b = ruleTimeBlocksPrev.get(i);
273 										if (timeBlockHasEarnCode(fromEarnGroup, b)) {
274 											Interval blockInterval = new Interval(new DateTime(b.getBeginTimestamp(), zone), new DateTime(b.getEndTimestamp(), zone));
275 
276 											// Calculate Block Gap, the duration between clock outs and clock ins of adjacent time blocks.
277 											if (previousBlockInterval != null) {
278 												blockGapDuration = new Duration(new DateTime(b.getEndTimestamp(), zone), previousBlockInterval.getStart());
279 												bgdHours = TKUtils.convertMillisToHours(blockGapDuration.getMillis());
280 											}
281 
282 											// Check Gap, if good, sum hours, if maxGap is 0, ignore gaps
283 											if (rule.getMaxGap().compareTo(BigDecimal.ZERO) == 0 || bgdHours.compareTo(rule.getMaxGap()) <= 0) {
284 												// Calculate Overlap and add it to hours before virtual day bucket.
285 												if (blockInterval.overlaps(previousDayShiftInterval)) {
286 													BigDecimal hrs = TKUtils.convertMillisToHours(blockInterval.overlap(previousDayShiftInterval).toDurationMillis());
287 													hoursBeforeVirtualDay = hoursBeforeVirtualDay.add(hrs);
288 												}
289 
290 											} else {
291 												// Time blocks are reverse sorted, we can jump out as soon as the max gap is exceeded.
292 												break;
293 											}
294 
295 											previousBlockInterval = blockInterval;
296 
297 										}
298 									}
299 								} else {
300 									// DO NOTHING!
301 								}
302 							}
303 						}
304 					}
305 
306 					BigDecimal hoursToApply = BigDecimal.ZERO;
307 					BigDecimal hoursToApplyPrevious = BigDecimal.ZERO;
308                     // If the hours before virtual day are less than or equal to
309                     // min hours, we have already applied the time, so we don't
310                     // set hoursToApplyPrevious
311 					if (hoursBeforeVirtualDay.compareTo(rule.getMinHours()) <= 0) {
312 						// we need to apply these hours.
313 						hoursToApplyPrevious = hoursBeforeVirtualDay;
314 					}
315 
316 
317 					//  Current Day
318 
319 					TimeBlock previous = null; // Previous Time Block
320 					List<TimeBlock> accumulatedBlocks = new ArrayList<TimeBlock>(); // TimeBlocks we MAY or MAY NOT apply Shift Premium to.
321                     List<Interval> accumulatedBlockIntervals = new ArrayList<Interval>(); // To save recompute time when checking timeblocks for application we store them as we create them.
322 					// Iterate over sorted list, checking time boundaries vs Shift Intervals.
323 					long accumulatedMillis = TKUtils.convertHoursToMillis(hoursBeforeVirtualDay);
324 
325                     boolean previousDayOnly = false; // IF the rule is not active today, but was on the previous day, we need to still look at time blocks.
326                     if (!dayIsRuleActive(currentDay, rule)) {
327                         if (dayIsRuleActive(currentDay.minusDays(1), rule)) {
328                             previousDayOnly = true;
329                         } else {
330                             // Nothing to see here, move to next rule.
331                             continue;
332                         }
333 
334                     }
335 
336 					/*
337 					 * We will touch each time block and accumulate time blocks that are applicable to
338 					 * the current rule we are on.
339 					 */
340 
341                     // These blocks are only used for detail application
342                     // We don't want to pass along the previous pay period,
343                     // because we don't want to modify the time blocks on that
344                     // period. If null is passed, time will be placed on the
345                     // first block of the first period if the previous period
346                     // block had influence.
347                     List<TimeBlock> previousBlocksFiltered = (previousPayPeriodPrevDay) ? null : filterBlocksByApplicableEarnGroup(fromEarnGroup, ruleTimeBlocksPrev);
348 
349 					for (TimeBlock current : ruleTimeBlocksCurr) {
350 						if (!timeBlockHasEarnCode(fromEarnGroup, current)) {
351                             // TODO: WorkSchedule considerations somewhere in here?
352                             continue;
353                         }
354 
355 						Interval blockInterval = new Interval(new DateTime(current.getBeginTimestamp(), zone), new DateTime(current.getEndTimestamp(), zone));
356 
357 						// Check both Intervals, since the time blocks could still
358 						// be applicable to the previous day.  These two intervals should
359 						// not have any overlap.
360 						if (previousDayShiftInterval.overlaps(shiftInterval))
361 							throw new RuntimeException("Interval of greater than 24 hours created in the rules processing.");
362 
363                         // This block of code handles cases where you have time
364                         // that spills to multiple days and a shift rule that
365                         // has a valid window on multiple consecutive days. Time
366                         // must be applied with the correct shift interval.
367 						Interval overlap = previousDayShiftInterval.overlap(blockInterval);
368                         evalInterval = previousDayShiftInterval;
369 						if (overlap == null) {
370                             if (hoursToApplyPrevious.compareTo(BigDecimal.ZERO) > 0) {
371                                 // we have hours from previous day, and the shift
372                                 // window is going to move to current day.
373                                 // Need to apply this now, and move window forward
374                                 // for current time block.
375                                 BigDecimal accumHours = TKUtils.convertMillisToHours(accumulatedMillis);
376                                 this.applyAccumulatedWrapper(accumHours, evalInterval, accumulatedBlockIntervals, accumulatedBlocks, previousBlocksFiltered, hoursToApplyPrevious, hoursToApply, rule);
377                                 accumulatedMillis = 0L; // reset accumulated hours..
378                                 hoursToApply = BigDecimal.ZERO;
379                                 hoursToApplyPrevious = BigDecimal.ZERO;
380                             }
381 
382                             // Because of our position in the loop, when we are at this point,
383                             // we know we've passed any previous day shift intervals, so we can
384                             // determine if we should skip the current day based on the boolean
385                             // we set earlier.
386                             if (previousDayOnly) {
387                                 continue;
388                             }
389 
390 							overlap = shiftInterval.overlap(blockInterval);
391                             evalInterval = shiftInterval;
392                         }
393 
394                         // Time bucketing and application as normal:
395                         //
396 						if (overlap != null) {
397 							// There IS overlap.
398 							if (previous != null) {
399 								// only check max gap if max gap of rule is not 0
400 								if (rule.getMaxGap().compareTo(BigDecimal.ZERO) != 0 && exceedsMaxGap(previous, current, rule.getMaxGap())) {
401 									BigDecimal accumHours = TKUtils.convertMillisToHours(accumulatedMillis);
402                                     this.applyAccumulatedWrapper(accumHours, evalInterval, accumulatedBlockIntervals, accumulatedBlocks, previousBlocksFiltered, hoursToApplyPrevious, hoursToApply, rule);
403                                     accumulatedMillis = 0L; // reset accumulated hours..
404 									hoursToApply = BigDecimal.ZERO;
405 									hoursToApplyPrevious = BigDecimal.ZERO;
406 								} else {
407 									long millis = overlap.toDurationMillis();
408 									accumulatedMillis  += millis;
409 									hoursToApply = hoursToApply.add(TKUtils.convertMillisToHours(millis));
410 								}
411 							} else {
412 								// Overlap shift at first time block.
413 								long millis = overlap.toDurationMillis();
414 								accumulatedMillis  += millis;
415 								hoursToApply = hoursToApply.add(TKUtils.convertMillisToHours(millis));
416 							}
417 							accumulatedBlocks.add(current);
418                             accumulatedBlockIntervals.add(blockInterval);
419 							previous = current; // current can still apply to next.
420 						} else {
421 							// No Overlap / Outside of Rule
422 							if (previous != null) {
423 								BigDecimal accumHours = TKUtils.convertMillisToHours(accumulatedMillis);
424                                 this.applyAccumulatedWrapper(accumHours, evalInterval, accumulatedBlockIntervals, accumulatedBlocks, previousBlocksFiltered, hoursToApplyPrevious, hoursToApply, rule);
425 								accumulatedMillis = 0L; // reset accumulated hours..
426 								hoursToApply = BigDecimal.ZERO;
427 								hoursToApplyPrevious = BigDecimal.ZERO;
428 							}
429 						}
430 
431 					}
432 
433 					// All time blocks are iterated over, check for remainders.
434 					// Check containers for time, and apply if needed.
435 					BigDecimal accumHours = TKUtils.convertMillisToHours(accumulatedMillis);
436                     this.applyAccumulatedWrapper(accumHours, evalInterval, accumulatedBlockIntervals, accumulatedBlocks, previousBlocksFiltered, hoursToApplyPrevious, hoursToApply, rule);
437                 }
438 			}
439 			// 	Keep track of previous as we move day by day.
440 			jobNumberToTimeBlocksPreviousDay = jobNumberToTimeBlocks;
441             previousPayPeriodPrevDay = false;
442 		}
443 
444 	}
445 
446     @Override
447     public List<ShiftDifferentialRule> getShiftDifferentialRules(String location, String hrSalGroup, String payGrade, Date fromEffdt, Date toEffdt, String active, String showHist) {
448         return shiftDifferentialRuleDao.getShiftDifferentialRules(location, hrSalGroup, payGrade, fromEffdt, toEffdt, active, showHist);
449     }
450 
451     private List<TimeBlock> filterBlocksByApplicableEarnGroup(Set<String> fromEarnGroup, List<TimeBlock> blocks) {
452         List<TimeBlock> filtered;
453 
454         if (blocks == null || blocks.size() == 0)
455             filtered = null;
456         else {
457             filtered = new ArrayList<TimeBlock>();
458             for (TimeBlock b : blocks) {
459                 if (timeBlockHasEarnCode(fromEarnGroup, b))
460                     filtered.add(b);
461             }
462         }
463 
464         return filtered;
465     }
466 
467     private void applyAccumulatedWrapper(BigDecimal accumHours,
468                                          Interval evalInterval,
469                                          List<Interval>accumulatedBlockIntervals,
470                                          List<TimeBlock>accumulatedBlocks,
471                                          List<TimeBlock> previousBlocks,
472                                          BigDecimal hoursToApplyPrevious,
473                                          BigDecimal hoursToApply,
474                                          ShiftDifferentialRule rule) {
475         if (accumHours.compareTo(rule.getMinHours()) >= 0) {
476             this.applyPremium(evalInterval, accumulatedBlockIntervals, accumulatedBlocks, previousBlocks, hoursToApplyPrevious, hoursToApply, rule.getEarnCode());
477         }
478         accumulatedBlocks.clear();
479         accumulatedBlockIntervals.clear();
480     }
481 
482 	private void sortTimeBlocksInverse(List<TimeBlock> blocks) {
483 		Collections.sort(blocks, new Comparator<TimeBlock>() { // Sort the Time Blocks
484 			public int compare(TimeBlock tb1, TimeBlock tb2) {
485 				if (tb1 != null && tb2 != null)
486 					return -1 * tb1.getBeginTimestamp().compareTo(tb2.getBeginTimestamp());
487 				return 0;
488 			}
489 		});
490 	}
491 
492 	private void sortTimeBlocksNatural(List<TimeBlock> blocks) {
493 		Collections.sort(blocks, new Comparator<TimeBlock>() { // Sort the Time Blocks
494 			public int compare(TimeBlock tb1, TimeBlock tb2) {
495 				if (tb1 != null && tb2 != null)
496 					return tb1.getBeginTimestamp().compareTo(tb2.getBeginTimestamp());
497 				return 0;
498 			}
499 		});
500 	}
501 
502     /**
503      *
504      * @param shift The shift interval - need to examine the time block to determine how many hours are eligible per block.
505      * @param blockIntervals Intervals for each block present in the blocks list. Passed here to avoid re computation.
506      * @param blocks The blocks we are applying hours to.
507      * @param previousBlocks If present, this is the list of time blocks from a previous "day", on which the initial hours (from previous day) should be placed.
508      * @param initialHours hours accumulated from a previous boundary that need to be applied here (NOT SUBJECT TO INTERVAL)
509      * @param hours hours to apply
510      * @param earnCode what earn code to create time hour detail entry for.
511      */
512 	void applyPremium(Interval shift, List<Interval> blockIntervals, List<TimeBlock> blocks, List<TimeBlock> previousBlocks, BigDecimal initialHours, BigDecimal hours, String earnCode) {
513         for (int i=0; i<blocks.size(); i++) {
514 			TimeBlock b = blocks.get(i);
515 
516             // Only apply initial hours to the first timeblock.
517 			if (i == 0 && (initialHours.compareTo(BigDecimal.ZERO) > 0)) {
518                 // ONLY if they're on the same document ID, do we apply to previous,
519                 // otherwise we dump all on the current document.
520                 if (previousBlocks != null && previousBlocks.size() > 0 && previousBlocks.get(0).getDocumentId().equals(b.getDocumentId())) {
521                     for (TimeBlock pb : previousBlocks) {
522                         BigDecimal lunchSub = this.negativeTimeHourDetailSum(pb); // A negative number
523                         initialHours = BigDecimal.ZERO.max(initialHours.add(lunchSub)); // We don't want negative premium hours!
524                         if (initialHours.compareTo(BigDecimal.ZERO) <= 0) // check here now as well, we may not have anything at all to apply.
525                             break;
526 
527                         // Adjust hours on the block by the lunch sub hours, so we're not over applying.
528                         BigDecimal hoursToApply = initialHours.min(pb.getHours().add(lunchSub));
529                         addPremiumTimeHourDetail(pb, hoursToApply, earnCode);
530                         initialHours = initialHours.subtract(hoursToApply, TkConstants.MATH_CONTEXT);
531                         if (initialHours.compareTo(BigDecimal.ZERO) <= 0)
532                             break;
533                     }
534                 } else {
535 				    addPremiumTimeHourDetail(b, initialHours, earnCode);
536                 }
537             }
538 
539             BigDecimal lunchSub = this.negativeTimeHourDetailSum(b); // A negative number
540             hours = BigDecimal.ZERO.max(hours.add(lunchSub)); // We don't want negative premium hours!
541 
542 			if (hours.compareTo(BigDecimal.ZERO) > 0) {
543                 Interval blockInterval = blockIntervals.get(i);
544                 Interval overlapInterval = shift.overlap(blockInterval);
545                 if (overlapInterval == null)
546                     continue;
547 
548                 long overlap = overlapInterval.toDurationMillis();
549                 BigDecimal hoursMax = TKUtils.convertMillisToHours(overlap); // Maximum number of possible hours applicable for this time block and shift rule
550                 // Adjust this time block's hoursMax (below) by lunchSub to
551                 // make sure the time applied is the correct amount per block.
552                 BigDecimal hoursToApply = hours.min(hoursMax.add(lunchSub));
553 
554                 addPremiumTimeHourDetail(b, hoursToApply, earnCode);
555 				hours = hours.subtract(hoursToApply, TkConstants.MATH_CONTEXT);
556 			}
557 		}
558 	}
559 
560 	void addPremiumTimeHourDetail(TimeBlock block, BigDecimal hours, String earnCode) {
561 		List<TimeHourDetail> details = block.getTimeHourDetails();
562 		TimeHourDetail premium = new TimeHourDetail();
563 		premium.setHours(hours);
564 		premium.setEarnCode(earnCode);
565 		premium.setTkTimeBlockId(block.getTkTimeBlockId());
566 		details.add(premium);
567 	}
568 
569 	/**
570 	 * Does the difference between the previous time blocks clock out time and the
571 	 * current time blocks clock in time exceed the max gap. max gap is in minutes
572 	 *
573 	 * @param previous
574 	 * @param current
575 	 * @param maxGap
576 	 * @return
577 	 */
578 	boolean exceedsMaxGap(TimeBlock previous, TimeBlock current, BigDecimal maxGap) {
579 		long difference = current.getBeginTimestamp().getTime() - previous.getEndTimestamp().getTime();
580 		BigDecimal gapMinutes = TKUtils.convertMillisToMinutes(difference);
581 
582 		return (gapMinutes.compareTo(maxGap) > 0);
583 	}
584 
585 	public void setShiftDifferentialRuleDao(ShiftDifferentialRuleDao shiftDifferentialRuleDao) {
586 		this.shiftDifferentialRuleDao = shiftDifferentialRuleDao;
587 	}
588 
589 	@Override
590 	public ShiftDifferentialRule getShiftDifferentialRule(String tkShiftDifferentialRuleId) {
591 		return this.shiftDifferentialRuleDao.findShiftDifferentialRule(tkShiftDifferentialRuleId);
592 	}
593 
594 	@Override
595 	public List<ShiftDifferentialRule> getShiftDifferentalRules(String location, String hrSalGroup, String payGrade, String pyCalendarGroup, Date asOfDate) {
596 		List<ShiftDifferentialRule> sdrs = new ArrayList<ShiftDifferentialRule>();
597 
598 		// location, sal group, pay grade
599 
600 	    sdrs.addAll(shiftDifferentialRuleDao.findShiftDifferentialRules(location, hrSalGroup, payGrade, pyCalendarGroup, asOfDate));
601 
602 		// location, sal group, *
603 		sdrs.addAll(shiftDifferentialRuleDao.findShiftDifferentialRules(location, hrSalGroup, "%", pyCalendarGroup, asOfDate));
604 
605 		// location, *, pay grade
606 		sdrs.addAll(shiftDifferentialRuleDao.findShiftDifferentialRules(location, "%", payGrade, pyCalendarGroup, asOfDate));
607 
608 		// location, *, *
609 		sdrs.addAll(shiftDifferentialRuleDao.findShiftDifferentialRules(location, "%", "%", pyCalendarGroup, asOfDate));
610 
611 		// *, sal group, pay grade
612 		sdrs.addAll(shiftDifferentialRuleDao.findShiftDifferentialRules("%", hrSalGroup, payGrade, pyCalendarGroup, asOfDate));
613 
614 		// *, sal group, *
615 		sdrs.addAll(shiftDifferentialRuleDao.findShiftDifferentialRules("%", hrSalGroup, "%", pyCalendarGroup, asOfDate));
616 
617 		// *, *, pay grade
618 		sdrs.addAll(shiftDifferentialRuleDao.findShiftDifferentialRules("%", "%", payGrade, pyCalendarGroup, asOfDate));
619 
620 		// *, *, *
621 		sdrs.addAll(shiftDifferentialRuleDao.findShiftDifferentialRules("%", "%", "%", pyCalendarGroup, asOfDate));
622 
623 		if (sdrs == null)
624 			sdrs = Collections.emptyList();
625 
626 		return sdrs;
627 	}
628 
629 	private boolean dayIsRuleActive(DateTime currentDate, ShiftDifferentialRule sdr) {
630 		boolean active = false;
631 
632 		switch (currentDate.getDayOfWeek()) {
633 		case DateTimeConstants.MONDAY:
634 			active = sdr.isMonday();
635 			break;
636 		case DateTimeConstants.TUESDAY:
637 			active = sdr.isTuesday();
638 			break;
639 		case DateTimeConstants.WEDNESDAY:
640 			active = sdr.isWednesday();
641 			break;
642 		case DateTimeConstants.THURSDAY:
643 			active = sdr.isThursday();
644 			break;
645 		case DateTimeConstants.FRIDAY:
646 			active = sdr.isFriday();
647 			break;
648 		case DateTimeConstants.SATURDAY:
649 			active = sdr.isSaturday();
650 			break;
651 		case DateTimeConstants.SUNDAY:
652 			active = sdr.isSunday();
653 			break;
654 		}
655 
656 		return active;
657 	}
658 
659 	@Override
660 	public void saveOrUpdate(List<ShiftDifferentialRule> shiftDifferentialRules) {
661 		shiftDifferentialRuleDao.saveOrUpdate(shiftDifferentialRules);
662 	}
663 
664 	@Override
665 	public void saveOrUpdate(ShiftDifferentialRule shiftDifferentialRule) {
666 		shiftDifferentialRuleDao.saveOrUpdate(shiftDifferentialRule);
667 	}
668 
669 }