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.getPayCalendarEntry().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.getPayCalendarEntry().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().getPayBeginDate());
69  		if (prevBlocks.size() > 0) {
70  			TimesheetDocumentHeader prevTdh = TkServiceLocator.getTimesheetDocumentHeaderService().getPreviousDocumentHeader(timesheetDocument.getPrincipalId(), timesheetDocument.getDocumentHeader().getPayBeginDate());
71  			if (prevTdh != null) {
72  				CalendarEntries prevPayCalendarEntry = TkServiceLocator.getCalendarService().getCalendarDatesByPayEndDate(timesheetDocument.getPrincipalId(), prevTdh.getPayEndDate(), null);
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.getPayCalendarEntry().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.getEarnGroupService().getEarnCodeListForEarnGroup(rule.getFromEarnGroup(), TKUtils.getTimelessDate(timesheetDocument.getPayCalendarEntry().getBeginPeriodDateTime()));
214 
215                     // Because of the way java.sql.Time are stored, we need to first
216                     // construct a LocalTime in the System Time Zone, then convert that
217                     // time to the users time zone.
218                     LocalTime ruleStart = new LocalTime(rule.getBeginTime(), TKUtils.getSystemDateTimeZone());
219                     LocalTime ruleEnd = new LocalTime(rule.getEndTime(), TKUtils.getSystemDateTimeZone());
220                     ruleStart = new LocalTime(ruleStart, zone);
221                     ruleEnd = new LocalTime(ruleEnd, zone);
222 
223 
224 					DateTime shiftEnd = ruleEnd.toDateTime(currentDay);
225 					DateTime shiftStart = ruleStart.toDateTime(currentDay);
226 
227 					if (shiftEnd.isBefore(shiftStart) || shiftEnd.isEqual(shiftStart))
228 						shiftEnd = shiftEnd.plusDays(1);
229 					Interval shiftInterval = new Interval(shiftStart, shiftEnd);
230 
231 					// Set up buckets to handle previous days time accumulations
232 					BigDecimal hoursBeforeVirtualDay = BigDecimal.ZERO;
233 
234 					// Check current day first block to see if start time gap from virtual day start is greater than max gap
235 					// if so, we can skip the previous day checks.
236 					TimeBlock firstBlockOfCurrentDay = null;
237 					for (TimeBlock b : ruleTimeBlocksCurr) {
238 						if (timeBlockHasEarnCode(fromEarnGroup, b)) {
239 							firstBlockOfCurrentDay = b;
240 							break;
241 						}
242 					}
243 
244 					// Previous Day :: We have prior block container of nonzero size, and the previous day is active.
245 					Interval previousDayShiftInterval = new Interval(shiftStart.minusDays(1), shiftEnd.minusDays(1));
246 
247                     // Blank initialization pointer for picking which interval to pass to applyPremium()
248                     Interval evalInterval = null;
249 					if (ruleTimeBlocksPrev != null && ruleTimeBlocksPrev.size() > 0 && dayIsRuleActive(currentDay.minusDays(1), rule)) {
250 						// Simple heuristic to see if we even need to worry about
251 						// the Shift rule for this set of data.
252 						if (shiftEnd.isAfter(virtualDay.getEnd())) {
253 							// Compare first block of previous day with first block of current day for max gaptitude.
254 							TimeBlock firstBlockOfPreviousDay = null;
255 							for (TimeBlock b : ruleTimeBlocksPrev) {
256 								if (timeBlockHasEarnCode(fromEarnGroup, b)) {
257 									firstBlockOfPreviousDay = b;
258 									break;
259 								}
260 							}
261 							// Only if we actually have at least one block.
262                             // Adding Assumption: We must have both a valid current and previous block. Max Gap can not be more than a virtual day.
263                             // If this assumption does not hold, additional logic will be needed to iteratively go back in time to figure out which
264                             // blocks are valid.
265 							if ( (firstBlockOfPreviousDay != null) && (firstBlockOfCurrentDay != null)) {
266 								Interval previousBlockInterval = new Interval(new DateTime(firstBlockOfPreviousDay.getEndTimestamp(), zone), new DateTime(firstBlockOfCurrentDay.getBeginTimestamp(), zone));
267 								Duration blockGapDuration = previousBlockInterval.toDuration();
268 								BigDecimal bgdHours = TKUtils.convertMillisToHours(blockGapDuration.getMillis());
269 								// if maxGap is 0, ignore gaps and assign shift to time blocks within the hours
270 								if (rule.getMaxGap().compareTo(BigDecimal.ZERO) == 0 || bgdHours.compareTo(rule.getMaxGap()) <= 0) {
271 									// If we are here, we know we have at least one valid time block to pull some hours forward from.
272 
273 
274 									// These are inversely sorted.
275 									for (int i=0; i<ruleTimeBlocksPrev.size(); i++) {
276 										TimeBlock b = ruleTimeBlocksPrev.get(i);
277 										if (timeBlockHasEarnCode(fromEarnGroup, b)) {
278 											Interval blockInterval = new Interval(new DateTime(b.getBeginTimestamp(), zone), new DateTime(b.getEndTimestamp(), zone));
279 
280 											// Calculate Block Gap, the duration between clock outs and clock ins of adjacent time blocks.
281 											if (previousBlockInterval != null) {
282 												blockGapDuration = new Duration(new DateTime(b.getEndTimestamp(), zone), previousBlockInterval.getStart());
283 												bgdHours = TKUtils.convertMillisToHours(blockGapDuration.getMillis());
284 											}
285 
286 											// Check Gap, if good, sum hours, if maxGap is 0, ignore gaps
287 											if (rule.getMaxGap().compareTo(BigDecimal.ZERO) == 0 || bgdHours.compareTo(rule.getMaxGap()) <= 0) {
288 												// Calculate Overlap and add it to hours before virtual day bucket.
289 												if (blockInterval.overlaps(previousDayShiftInterval)) {
290 													BigDecimal hrs = TKUtils.convertMillisToHours(blockInterval.overlap(previousDayShiftInterval).toDurationMillis());
291 													hoursBeforeVirtualDay = hoursBeforeVirtualDay.add(hrs);
292 												}
293 
294 											} else {
295 												// Time blocks are reverse sorted, we can jump out as soon as the max gap is exceeded.
296 												break;
297 											}
298 
299 											previousBlockInterval = blockInterval;
300 
301 										}
302 									}
303 								} else {
304 									// DO NOTHING!
305 								}
306 							}
307 						}
308 					}
309 
310 					BigDecimal hoursToApply = BigDecimal.ZERO;
311 					BigDecimal hoursToApplyPrevious = BigDecimal.ZERO;
312                     // If the hours before virtual day are less than or equal to
313                     // min hours, we have already applied the time, so we don't
314                     // set hoursToApplyPrevious
315 					if (hoursBeforeVirtualDay.compareTo(rule.getMinHours()) <= 0) {
316 						// we need to apply these hours.
317 						hoursToApplyPrevious = hoursBeforeVirtualDay;
318 					}
319 
320 
321 					//  Current Day
322 
323 					TimeBlock previous = null; // Previous Time Block
324 					List<TimeBlock> accumulatedBlocks = new ArrayList<TimeBlock>(); // TimeBlocks we MAY or MAY NOT apply Shift Premium to.
325                     List<Interval> accumulatedBlockIntervals = new ArrayList<Interval>(); // To save recompute time when checking timeblocks for application we store them as we create them.
326 					// Iterate over sorted list, checking time boundaries vs Shift Intervals.
327 					long accumulatedMillis = TKUtils.convertHoursToMillis(hoursBeforeVirtualDay);
328 
329                     boolean previousDayOnly = false; // IF the rule is not active today, but was on the previous day, we need to still look at time blocks.
330                     if (!dayIsRuleActive(currentDay, rule)) {
331                         if (dayIsRuleActive(currentDay.minusDays(1), rule)) {
332                             previousDayOnly = true;
333                         } else {
334                             // Nothing to see here, move to next rule.
335                             continue;
336                         }
337 
338                     }
339 
340 					/*
341 					 * We will touch each time block and accumulate time blocks that are applicable to
342 					 * the current rule we are on.
343 					 */
344 
345                     // These blocks are only used for detail application
346                     // We don't want to pass along the previous pay period,
347                     // because we don't want to modify the time blocks on that
348                     // period. If null is passed, time will be placed on the
349                     // first block of the first period if the previous period
350                     // block had influence.
351                     List<TimeBlock> previousBlocksFiltered = (previousPayPeriodPrevDay) ? null : filterBlocksByApplicableEarnGroup(fromEarnGroup, ruleTimeBlocksPrev);
352 
353 					for (TimeBlock current : ruleTimeBlocksCurr) {
354 						if (!timeBlockHasEarnCode(fromEarnGroup, current)) {
355                             // TODO: WorkSchedule considerations somewhere in here?
356                             continue;
357                         }
358 
359 						Interval blockInterval = new Interval(new DateTime(current.getBeginTimestamp(), zone), new DateTime(current.getEndTimestamp(), zone));
360 
361 						// Check both Intervals, since the time blocks could still
362 						// be applicable to the previous day.  These two intervals should
363 						// not have any overlap.
364 						if (previousDayShiftInterval.overlaps(shiftInterval))
365 							throw new RuntimeException("Interval of greater than 24 hours created in the rules processing.");
366 
367                         // This block of code handles cases where you have time
368                         // that spills to multiple days and a shift rule that
369                         // has a valid window on multiple consecutive days. Time
370                         // must be applied with the correct shift interval.
371 						Interval overlap = previousDayShiftInterval.overlap(blockInterval);
372                         evalInterval = previousDayShiftInterval;
373 						if (overlap == null) {
374                             if (hoursToApplyPrevious.compareTo(BigDecimal.ZERO) > 0) {
375                                 // we have hours from previous day, and the shift
376                                 // window is going to move to current day.
377                                 // Need to apply this now, and move window forward
378                                 // for current time block.
379                                 BigDecimal accumHours = TKUtils.convertMillisToHours(accumulatedMillis);
380                                 this.applyAccumulatedWrapper(accumHours, evalInterval, accumulatedBlockIntervals, accumulatedBlocks, previousBlocksFiltered, hoursToApplyPrevious, hoursToApply, rule);
381                                 accumulatedMillis = 0L; // reset accumulated hours..
382                                 hoursToApply = BigDecimal.ZERO;
383                                 hoursToApplyPrevious = BigDecimal.ZERO;
384                             }
385 
386                             // Because of our position in the loop, when we are at this point,
387                             // we know we've passed any previous day shift intervals, so we can
388                             // determine if we should skip the current day based on the boolean
389                             // we set earlier.
390                             if (previousDayOnly) {
391                                 continue;
392                             }
393 
394 							overlap = shiftInterval.overlap(blockInterval);
395                             evalInterval = shiftInterval;
396                         }
397 
398                         // Time bucketing and application as normal:
399                         //
400 						if (overlap != null) {
401 							// There IS overlap.
402 							if (previous != null) {
403 								// only check max gap if max gap of rule is not 0
404 								if (rule.getMaxGap().compareTo(BigDecimal.ZERO) != 0 && exceedsMaxGap(previous, current, rule.getMaxGap())) {
405 									BigDecimal accumHours = TKUtils.convertMillisToHours(accumulatedMillis);
406                                     this.applyAccumulatedWrapper(accumHours, evalInterval, accumulatedBlockIntervals, accumulatedBlocks, previousBlocksFiltered, hoursToApplyPrevious, hoursToApply, rule);
407                                     accumulatedMillis = 0L; // reset accumulated hours..
408 									hoursToApply = BigDecimal.ZERO;
409 									hoursToApplyPrevious = BigDecimal.ZERO;
410 								} else {
411 									long millis = overlap.toDurationMillis();
412 									accumulatedMillis  += millis;
413 									hoursToApply = hoursToApply.add(TKUtils.convertMillisToHours(millis));
414 								}
415 							} else {
416 								// Overlap shift at first time block.
417 								long millis = overlap.toDurationMillis();
418 								accumulatedMillis  += millis;
419 								hoursToApply = hoursToApply.add(TKUtils.convertMillisToHours(millis));
420 							}
421 							accumulatedBlocks.add(current);
422                             accumulatedBlockIntervals.add(blockInterval);
423 							previous = current; // current can still apply to next.
424 						} else {
425 							// No Overlap / Outside of Rule
426 							if (previous != null) {
427 								BigDecimal accumHours = TKUtils.convertMillisToHours(accumulatedMillis);
428                                 this.applyAccumulatedWrapper(accumHours, evalInterval, accumulatedBlockIntervals, accumulatedBlocks, previousBlocksFiltered, hoursToApplyPrevious, hoursToApply, rule);
429 								accumulatedMillis = 0L; // reset accumulated hours..
430 								hoursToApply = BigDecimal.ZERO;
431 								hoursToApplyPrevious = BigDecimal.ZERO;
432 							}
433 						}
434 
435 					}
436 
437 					// All time blocks are iterated over, check for remainders.
438 					// Check containers for time, and apply if needed.
439 					BigDecimal accumHours = TKUtils.convertMillisToHours(accumulatedMillis);
440                     this.applyAccumulatedWrapper(accumHours, evalInterval, accumulatedBlockIntervals, accumulatedBlocks, previousBlocksFiltered, hoursToApplyPrevious, hoursToApply, rule);
441                 }
442 			}
443 			// 	Keep track of previous as we move day by day.
444 			jobNumberToTimeBlocksPreviousDay = jobNumberToTimeBlocks;
445             previousPayPeriodPrevDay = false;
446 		}
447 
448 	}
449 
450     @Override
451     public List<ShiftDifferentialRule> getShiftDifferentialRules(String location, String hrSalGroup, String payGrade, Date fromEffdt, Date toEffdt, String active, String showHist) {
452         return shiftDifferentialRuleDao.getShiftDifferentialRules(location, hrSalGroup, payGrade, fromEffdt, toEffdt, active, showHist);
453     }
454 
455     private List<TimeBlock> filterBlocksByApplicableEarnGroup(Set<String> fromEarnGroup, List<TimeBlock> blocks) {
456         List<TimeBlock> filtered;
457 
458         if (blocks == null || blocks.size() == 0)
459             filtered = null;
460         else {
461             filtered = new ArrayList<TimeBlock>();
462             for (TimeBlock b : blocks) {
463                 if (timeBlockHasEarnCode(fromEarnGroup, b))
464                     filtered.add(b);
465             }
466         }
467 
468         return filtered;
469     }
470 
471 
472     private void applyAccumulatedWrapper(BigDecimal accumHours, Interval evalInterval, List<Interval>accumulatedBlockIntervals, List<TimeBlock>accumulatedBlocks, List<TimeBlock> previousBlocks, BigDecimal hoursToApplyPrevious, BigDecimal hoursToApply, ShiftDifferentialRule rule) {
473         if (accumHours.compareTo(rule.getMinHours()) >= 0) {
474             this.applyPremium(evalInterval, accumulatedBlockIntervals, accumulatedBlocks, previousBlocks, hoursToApplyPrevious, hoursToApply, rule.getEarnCode());
475         }
476         accumulatedBlocks.clear();
477         accumulatedBlockIntervals.clear();
478     }
479 
480 	private void sortTimeBlocksInverse(List<TimeBlock> blocks) {
481 		Collections.sort(blocks, new Comparator<TimeBlock>() { // Sort the Time Blocks
482 			public int compare(TimeBlock tb1, TimeBlock tb2) {
483 				if (tb1 != null && tb2 != null)
484 					return -1 * tb1.getBeginTimestamp().compareTo(tb2.getBeginTimestamp());
485 				return 0;
486 			}
487 		});
488 	}
489 
490 	private void sortTimeBlocksNatural(List<TimeBlock> blocks) {
491 		Collections.sort(blocks, new Comparator<TimeBlock>() { // Sort the Time Blocks
492 			public int compare(TimeBlock tb1, TimeBlock tb2) {
493 				if (tb1 != null && tb2 != null)
494 					return tb1.getBeginTimestamp().compareTo(tb2.getBeginTimestamp());
495 				return 0;
496 			}
497 		});
498 	}
499 
500     /**
501      *
502      * @param shift The shift interval - need to examine the time block to determine how many hours are eligible per block.
503      * @param blockIntervals Intervals for each block present in the blocks list. Passed here to avoid re computation.
504      * @param blocks The blocks we are applying hours to.
505      * @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.
506      * @param initialHours hours accumulated from a previous boundary that need to be applied here (NOT SUBJECT TO INTERVAL)
507      * @param hours hours to apply
508      * @param earnCode what earn code to create time hour detail entry for.
509      */
510 	void applyPremium(Interval shift, List<Interval> blockIntervals, List<TimeBlock> blocks, List<TimeBlock> previousBlocks, BigDecimal initialHours, BigDecimal hours, String earnCode) {
511 		for (int i=0; i<blocks.size(); i++) {
512 			TimeBlock b = blocks.get(i);
513 
514             // Only apply initial hours to the first timeblock.
515 			if (i == 0 && (initialHours.compareTo(BigDecimal.ZERO) > 0)) {
516                 // ONLY if they're on the same document ID, do we apply to previous,
517                 // otherwise we dump all on the current document.
518                 if (previousBlocks != null && previousBlocks.size() > 0 && previousBlocks.get(0).getDocumentId().equals(b.getDocumentId())) {
519                     for (TimeBlock pb : previousBlocks) {
520                         BigDecimal lunchSub = this.negativeTimeHourDetailSum(pb); // A negative number
521                         initialHours = BigDecimal.ZERO.max(initialHours.add(lunchSub)); // We don't want negative premium hours!
522                         if (initialHours.compareTo(BigDecimal.ZERO) <= 0) // check here now as well, we may not have anything at all to apply.
523                             break;
524 
525                         // Adjust hours on the block by the lunch sub hours, so we're not over applying.
526                         BigDecimal hoursToApply = initialHours.min(pb.getHours().add(lunchSub));
527                         addPremiumTimeHourDetail(pb, hoursToApply, earnCode);
528                         initialHours = initialHours.subtract(hoursToApply, TkConstants.MATH_CONTEXT);
529                         if (initialHours.compareTo(BigDecimal.ZERO) <= 0)
530                             break;
531                     }
532                 } else {
533 				    addPremiumTimeHourDetail(b, initialHours, earnCode);
534                 }
535             }
536 
537             BigDecimal lunchSub = this.negativeTimeHourDetailSum(b); // A negative number
538             hours = BigDecimal.ZERO.max(hours.add(lunchSub)); // We don't want negative premium hours!
539 
540 			if (hours.compareTo(BigDecimal.ZERO) > 0) {
541                 Interval blockInterval = blockIntervals.get(i);
542                 Interval overlapInterval = shift.overlap(blockInterval);
543                 if (overlapInterval == null)
544                     continue;
545 
546                 long overlap = overlapInterval.toDurationMillis();
547                 BigDecimal hoursMax = TKUtils.convertMillisToHours(overlap); // Maximum number of possible hours applicable for this time block and shift rule
548                 // Adjust this time block's hoursMax (below) by lunchSub to
549                 // make sure the time applied is the correct amount per block.
550                 BigDecimal hoursToApply = hours.min(hoursMax.add(lunchSub));
551 
552                 addPremiumTimeHourDetail(b, hoursToApply, earnCode);
553 				hours = hours.subtract(hoursToApply, TkConstants.MATH_CONTEXT);
554 			}
555 		}
556 	}
557 
558 	void addPremiumTimeHourDetail(TimeBlock block, BigDecimal hours, String earnCode) {
559 		List<TimeHourDetail> details = block.getTimeHourDetails();
560 		TimeHourDetail premium = new TimeHourDetail();
561 		premium.setHours(hours);
562 		premium.setEarnCode(earnCode);
563 		premium.setTkTimeBlockId(block.getTkTimeBlockId());
564 		details.add(premium);
565 	}
566 
567 	/**
568 	 * Does the difference between the previous time blocks clock out time and the
569 	 * current time blocks clock in time exceed the max gap. max gap is in minutes
570 	 *
571 	 * @param previous
572 	 * @param current
573 	 * @param maxGap
574 	 * @return
575 	 */
576 	boolean exceedsMaxGap(TimeBlock previous, TimeBlock current, BigDecimal maxGap) {
577 		long difference = current.getBeginTimestamp().getTime() - previous.getEndTimestamp().getTime();
578 		BigDecimal gapMinutes = TKUtils.convertMillisToMinutes(difference);
579 
580 		return (gapMinutes.compareTo(maxGap) > 0);
581 	}
582 
583 	public void setShiftDifferentialRuleDao(ShiftDifferentialRuleDao shiftDifferentialRuleDao) {
584 		this.shiftDifferentialRuleDao = shiftDifferentialRuleDao;
585 	}
586 
587 	@Override
588 	public ShiftDifferentialRule getShiftDifferentialRule(String tkShiftDifferentialRuleId) {
589 		return this.shiftDifferentialRuleDao.findShiftDifferentialRule(tkShiftDifferentialRuleId);
590 	}
591 
592 	@Override
593 	public List<ShiftDifferentialRule> getShiftDifferentalRules(String location, String hrSalGroup, String payGrade, String pyCalendarGroup, Date asOfDate) {
594 		List<ShiftDifferentialRule> sdrs = new ArrayList<ShiftDifferentialRule>();
595 
596 		// location, sal group, pay grade
597 
598 	    sdrs.addAll(shiftDifferentialRuleDao.findShiftDifferentialRules(location, hrSalGroup, payGrade, pyCalendarGroup, asOfDate));
599 
600 		// location, sal group, *
601 		sdrs.addAll(shiftDifferentialRuleDao.findShiftDifferentialRules(location, hrSalGroup, "%", pyCalendarGroup, asOfDate));
602 
603 		// location, *, pay grade
604 		sdrs.addAll(shiftDifferentialRuleDao.findShiftDifferentialRules(location, "%", payGrade, pyCalendarGroup, asOfDate));
605 
606 		// location, *, *
607 		sdrs.addAll(shiftDifferentialRuleDao.findShiftDifferentialRules(location, "%", "%", pyCalendarGroup, asOfDate));
608 
609 		// *, sal group, pay grade
610 		sdrs.addAll(shiftDifferentialRuleDao.findShiftDifferentialRules("%", hrSalGroup, payGrade, pyCalendarGroup, asOfDate));
611 
612 		// *, sal group, *
613 		sdrs.addAll(shiftDifferentialRuleDao.findShiftDifferentialRules("%", hrSalGroup, "%", pyCalendarGroup, asOfDate));
614 
615 		// *, *, pay grade
616 		sdrs.addAll(shiftDifferentialRuleDao.findShiftDifferentialRules("%", "%", payGrade, pyCalendarGroup, asOfDate));
617 
618 		// *, *, *
619 		sdrs.addAll(shiftDifferentialRuleDao.findShiftDifferentialRules("%", "%", "%", pyCalendarGroup, asOfDate));
620 
621 		if (sdrs == null)
622 			sdrs = Collections.emptyList();
623 
624 		return sdrs;
625 	}
626 
627 	private boolean dayIsRuleActive(DateTime currentDate, ShiftDifferentialRule sdr) {
628 		boolean active = false;
629 
630 		switch (currentDate.getDayOfWeek()) {
631 		case DateTimeConstants.MONDAY:
632 			active = sdr.isMonday();
633 			break;
634 		case DateTimeConstants.TUESDAY:
635 			active = sdr.isTuesday();
636 			break;
637 		case DateTimeConstants.WEDNESDAY:
638 			active = sdr.isWednesday();
639 			break;
640 		case DateTimeConstants.THURSDAY:
641 			active = sdr.isThursday();
642 			break;
643 		case DateTimeConstants.FRIDAY:
644 			active = sdr.isFriday();
645 			break;
646 		case DateTimeConstants.SATURDAY:
647 			active = sdr.isSaturday();
648 			break;
649 		case DateTimeConstants.SUNDAY:
650 			active = sdr.isSunday();
651 			break;
652 		}
653 
654 		return active;
655 	}
656 
657 	@Override
658 	public void saveOrUpdate(List<ShiftDifferentialRule> shiftDifferentialRules) {
659 		shiftDifferentialRuleDao.saveOrUpdate(shiftDifferentialRules);
660 	}
661 
662 	@Override
663 	public void saveOrUpdate(ShiftDifferentialRule shiftDifferentialRule) {
664 		shiftDifferentialRuleDao.saveOrUpdate(shiftDifferentialRule);
665 	}
666 
667 }