Java tutorial
/** * Copyright 2004-2013 The Kuali Foundation * * Licensed under the Educational Community License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.opensource.org/licenses/ecl2.php * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.kuali.kpme.tklm.time.rules.shiftdifferential.service; import java.math.BigDecimal; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import org.apache.log4j.Logger; import org.joda.time.DateTime; import org.joda.time.DateTimeConstants; import org.joda.time.DateTimeZone; import org.joda.time.Duration; import org.joda.time.Interval; import org.joda.time.LocalDate; import org.joda.time.LocalTime; import org.kuali.kpme.core.KPMENamespace; import org.kuali.kpme.core.calendar.entry.CalendarEntry; import org.kuali.kpme.core.job.Job; import org.kuali.kpme.core.permission.KPMEPermissionTemplate; import org.kuali.kpme.core.principal.PrincipalHRAttributes; import org.kuali.kpme.core.role.KPMERoleMemberAttribute; import org.kuali.kpme.core.service.HrServiceLocator; import org.kuali.kpme.core.util.HrConstants; import org.kuali.kpme.core.util.TKUtils; import org.kuali.kpme.tklm.time.rules.shiftdifferential.ShiftDifferentialRule; import org.kuali.kpme.tklm.time.rules.shiftdifferential.dao.ShiftDifferentialRuleDao; import org.kuali.kpme.tklm.time.service.TkServiceLocator; import org.kuali.kpme.tklm.time.timeblock.TimeBlock; import org.kuali.kpme.tklm.time.timehourdetail.TimeHourDetail; import org.kuali.kpme.tklm.time.timesheet.TimesheetDocument; import org.kuali.kpme.tklm.time.util.TkTimeBlockAggregate; import org.kuali.kpme.tklm.time.workflow.TimesheetDocumentHeader; import org.kuali.rice.kim.api.KimConstants; import org.kuali.rice.kim.api.services.KimApiServiceLocator; public class ShiftDifferentialRuleServiceImpl implements ShiftDifferentialRuleService { @SuppressWarnings("unused") private static final Logger LOG = Logger.getLogger(ShiftDifferentialRuleServiceImpl.class); /** * The maximum allowable time between timeblocks before we consider them to * be day-boundary single time blocks. */ private ShiftDifferentialRuleDao shiftDifferentialRuleDao = null; private Map<Long, Set<ShiftDifferentialRule>> getJobNumberToShiftRuleMap(TimesheetDocument timesheetDocument) { Map<Long, Set<ShiftDifferentialRule>> jobNumberToShifts = new HashMap<Long, Set<ShiftDifferentialRule>>(); PrincipalHRAttributes principalCal = HrServiceLocator.getPrincipalHRAttributeService().getPrincipalCalendar( timesheetDocument.getPrincipalId(), timesheetDocument.getCalendarEntry().getEndPeriodFullDateTime().toLocalDate()); for (Job job : timesheetDocument.getJobs()) { List<ShiftDifferentialRule> shiftDifferentialRules = getShiftDifferentalRules(job.getLocation(), job.getHrSalGroup(), job.getPayGrade(), principalCal.getPayCalendar(), timesheetDocument.getCalendarEntry().getEndPeriodFullDateTime().toLocalDate()); if (shiftDifferentialRules.size() > 0) jobNumberToShifts.put(job.getJobNumber(), new HashSet<ShiftDifferentialRule>(shiftDifferentialRules)); } return jobNumberToShifts; } private Map<Long, List<TimeBlock>> getPreviousPayPeriodLastDayJobToTimeBlockMap( TimesheetDocument timesheetDocument, Map<Long, Set<ShiftDifferentialRule>> jobNumberToShifts) { Map<Long, List<TimeBlock>> jobNumberToTimeBlocksPreviousDay = null; // Get the last day of the last week of the previous pay period. // This is the only day that can have impact on the current day. List<TimeBlock> prevBlocks = TkServiceLocator.getTimesheetService().getPrevDocumentTimeBlocks( timesheetDocument.getPrincipalId(), timesheetDocument.getDocumentHeader().getBeginDateTime()); if (prevBlocks.size() > 0) { TimesheetDocumentHeader prevTdh = TkServiceLocator.getTimesheetDocumentHeaderService() .getPreviousDocumentHeader(timesheetDocument.getPrincipalId(), timesheetDocument.getDocumentHeader().getBeginDateTime().toDateTime()); if (prevTdh != null) { CalendarEntry prevPayCalendarEntry = HrServiceLocator.getCalendarEntryService() .getCalendarDatesByPayEndDate(timesheetDocument.getPrincipalId(), prevTdh.getEndDateTime(), HrConstants.PAY_CALENDAR_TYPE); TkTimeBlockAggregate prevTimeAggregate = new TkTimeBlockAggregate(prevBlocks, prevPayCalendarEntry, prevPayCalendarEntry.getCalendarObj(), true); List<List<TimeBlock>> dayBlocks = prevTimeAggregate.getDayTimeBlockList(); List<TimeBlock> previousPeriodLastDayBlocks = dayBlocks.get(dayBlocks.size() - 1); // Set back to null if there is nothing in the list. if (previousPeriodLastDayBlocks.size() > 0) { jobNumberToTimeBlocksPreviousDay = new HashMap<Long, List<TimeBlock>>(); for (TimeBlock block : previousPeriodLastDayBlocks) { // Job Number to TimeBlock for Last Day of Previous Time // Period Long jobNumber = block.getJobNumber(); if (jobNumberToShifts.containsKey(jobNumber)) { // we have a useful timeblock. List<TimeBlock> jblist = jobNumberToTimeBlocksPreviousDay.get(jobNumber); if (jblist == null) { jblist = new ArrayList<TimeBlock>(); jobNumberToTimeBlocksPreviousDay.put(jobNumber, jblist); } jblist.add(block); } } } } } return jobNumberToTimeBlocksPreviousDay; } private boolean timeBlockHasEarnCode(Set<String> earnCodes, TimeBlock block) { boolean present = false; if (block != null && earnCodes != null) present = earnCodes.contains(block.getEarnCode()); return present; } /** * Returns a BigDecimal representing the sum of all of the negative time * hour detail types. In this case, only LUN is considered. This can be * modified to add other considerations. * * @param block The Timeblock to inspect. * * @return A big decimal. */ private BigDecimal negativeTimeHourDetailSum(TimeBlock block) { BigDecimal sum = BigDecimal.ZERO; if (block != null) { List<TimeHourDetail> details = block.getTimeHourDetails(); for (TimeHourDetail detail : details) { if (detail.getEarnCode().equals(HrConstants.LUNCH_EARN_CODE)) { sum = sum.add(detail.getHours()); } } } return sum; } @Override public void processShiftDifferentialRules(TimesheetDocument timesheetDocument, TkTimeBlockAggregate aggregate) { DateTimeZone zone = HrServiceLocator.getTimezoneService().getUserTimezoneWithFallback(); List<List<TimeBlock>> blockDays = aggregate.getDayTimeBlockList(); DateTime periodStartDateTime = timesheetDocument.getCalendarEntry().getBeginPeriodLocalDateTime() .toDateTime(zone); Map<Long, Set<ShiftDifferentialRule>> jobNumberToShifts = getJobNumberToShiftRuleMap(timesheetDocument); // If there are no shift differential rules, we have an early exit. if (jobNumberToShifts.isEmpty()) { return; } // Get the last day of the previous pay period. We need this to determine // if there are hours from the previous pay period that will effect the // shift rule on the first day of the currently-being-processed pay period. // // Will be set to null if not applicable. boolean previousPayPeriodPrevDay = true; Map<Long, List<TimeBlock>> jobNumberToTimeBlocksPreviousDay = getPreviousPayPeriodLastDayJobToTimeBlockMap( timesheetDocument, jobNumberToShifts); // We are going to look at the time blocks grouped by Days. // // This is a very large outer loop. for (int pos = 0; pos < blockDays.size(); pos++) { List<TimeBlock> blocks = blockDays.get(pos); // Timeblocks for this day. if (blocks.isEmpty()) continue; // No Time blocks, no worries. DateTime currentDay = periodStartDateTime.plusDays(pos); Interval virtualDay = new Interval(currentDay, currentDay.plusHours(24)); // Builds our JobNumber to TimeBlock for Current Day List. // // Shift Differential Rules are also grouped by Job number, this // provides a quick way to do the lookup / reference. // We don't need every time block, only the ones that will be // applicable to the shift rules. Map<Long, List<TimeBlock>> jobNumberToTimeBlocks = new HashMap<Long, List<TimeBlock>>(); for (TimeBlock block : blocks) { Long jobNumber = block.getJobNumber(); if (jobNumberToShifts.containsKey(jobNumber)) { List<TimeBlock> jblist = jobNumberToTimeBlocks.get(jobNumber); if (jblist == null) { jblist = new ArrayList<TimeBlock>(); jobNumberToTimeBlocks.put(jobNumber, jblist); } jblist.add(block); } } // Large Outer Loop to look at applying the Shift Rules based on // the current JobNumber. // // This loop will handle previous day boundary time as well as the // current day. // // There is room for refactoring here! for (Map.Entry<Long, Set<ShiftDifferentialRule>> entry : jobNumberToShifts.entrySet()) { Set<ShiftDifferentialRule> shiftDifferentialRules = entry.getValue(); // Obtain and sort our previous and current time blocks. List<TimeBlock> ruleTimeBlocksPrev = null; List<TimeBlock> ruleTimeBlocksCurr = jobNumberToTimeBlocks.get(entry.getKey()); if (ruleTimeBlocksCurr != null && ruleTimeBlocksCurr.size() > 0) { if (jobNumberToTimeBlocksPreviousDay != null) ruleTimeBlocksPrev = jobNumberToTimeBlocksPreviousDay.get(entry.getKey()); if (ruleTimeBlocksPrev != null && ruleTimeBlocksPrev.size() > 0) this.sortTimeBlocksInverse(ruleTimeBlocksPrev); this.sortTimeBlocksNatural(ruleTimeBlocksCurr); } else { // Skip to next job, there is nothing for this job // on this day, and because of this we don't care // about the previous day either. continue; } for (ShiftDifferentialRule rule : shiftDifferentialRules) { Set<String> fromEarnGroup = HrServiceLocator.getEarnCodeGroupService() .getEarnCodeListForEarnCodeGroup(rule.getFromEarnGroup(), timesheetDocument .getCalendarEntry().getBeginPeriodFullDateTime().toLocalDate()); LocalTime ruleStart = new LocalTime(rule.getBeginTime(), zone); LocalTime ruleEnd = new LocalTime(rule.getEndTime(), zone); DateTime shiftEnd = ruleEnd.toDateTime(currentDay); DateTime shiftStart = ruleStart.toDateTime(currentDay); if (shiftEnd.isBefore(shiftStart) || shiftEnd.isEqual(shiftStart)) { shiftEnd = shiftEnd.plusDays(1); } Interval shiftInterval = new Interval(shiftStart, shiftEnd); // Set up buckets to handle previous days time accumulations BigDecimal hoursBeforeVirtualDay = BigDecimal.ZERO; // Check current day first block to see if start time gap from virtual day start is greater than max gap // if so, we can skip the previous day checks. TimeBlock firstBlockOfCurrentDay = null; for (TimeBlock b : ruleTimeBlocksCurr) { if (timeBlockHasEarnCode(fromEarnGroup, b)) { firstBlockOfCurrentDay = b; break; } } // Previous Day :: We have prior block container of nonzero size, and the previous day is active. Interval previousDayShiftInterval = new Interval(shiftStart.minusDays(1), shiftEnd.minusDays(1)); // Blank initialization pointer for picking which interval to pass to applyPremium() Interval evalInterval = null; if (ruleTimeBlocksPrev != null && ruleTimeBlocksPrev.size() > 0 && dayIsRuleActive(currentDay.minusDays(1), rule)) { // Simple heuristic to see if we even need to worry about // the Shift rule for this set of data. if (shiftEnd.isAfter(virtualDay.getEnd())) { // Compare first block of previous day with first block of current day for max gaptitude. TimeBlock firstBlockOfPreviousDay = null; for (TimeBlock b : ruleTimeBlocksPrev) { if (timeBlockHasEarnCode(fromEarnGroup, b)) { firstBlockOfPreviousDay = b; break; } } // Only if we actually have at least one block. // Adding Assumption: We must have both a valid current and previous block. Max Gap can not be more than a virtual day. // If this assumption does not hold, additional logic will be needed to iteratively go back in time to figure out which // blocks are valid. if ((firstBlockOfPreviousDay != null) && (firstBlockOfCurrentDay != null)) { Interval previousBlockInterval = new Interval( firstBlockOfPreviousDay.getEndDateTime().withZone(zone), firstBlockOfCurrentDay.getBeginDateTime().withZone(zone)); Duration blockGapDuration = previousBlockInterval.toDuration(); BigDecimal bgdHours = TKUtils.convertMillisToHours(blockGapDuration.getMillis()); // if maxGap is 0, ignore gaps and assign shift to time blocks within the hours if (rule.getMaxGap().compareTo(BigDecimal.ZERO) == 0 || bgdHours.compareTo(rule.getMaxGap()) <= 0) { // If we are here, we know we have at least one valid time block to pull some hours forward from. // These are inversely sorted. for (int i = 0; i < ruleTimeBlocksPrev.size(); i++) { TimeBlock b = ruleTimeBlocksPrev.get(i); if (timeBlockHasEarnCode(fromEarnGroup, b)) { Interval blockInterval = new Interval( b.getBeginDateTime().withZone(zone), b.getEndDateTime().withZone(zone)); // Calculate Block Gap, the duration between clock outs and clock ins of adjacent time blocks. if (previousBlockInterval != null) { blockGapDuration = new Duration(b.getEndDateTime().withZone(zone), previousBlockInterval.getStart()); bgdHours = TKUtils .convertMillisToHours(blockGapDuration.getMillis()); } // Check Gap, if good, sum hours, if maxGap is 0, ignore gaps if (rule.getMaxGap().compareTo(BigDecimal.ZERO) == 0 || bgdHours.compareTo(rule.getMaxGap()) <= 0) { // Calculate Overlap and add it to hours before virtual day bucket. if (blockInterval.overlaps(previousDayShiftInterval)) { BigDecimal hrs = TKUtils.convertMillisToHours(blockInterval .overlap(previousDayShiftInterval).toDurationMillis()); hoursBeforeVirtualDay = hoursBeforeVirtualDay.add(hrs); } } else { // Time blocks are reverse sorted, we can jump out as soon as the max gap is exceeded. break; } previousBlockInterval = blockInterval; } } } else { // DO NOTHING! } } } } BigDecimal hoursToApply = BigDecimal.ZERO; BigDecimal hoursToApplyPrevious = BigDecimal.ZERO; // If the hours before virtual day are less than or equal to // min hours, we have already applied the time, so we don't // set hoursToApplyPrevious if (hoursBeforeVirtualDay.compareTo(rule.getMinHours()) <= 0) { // we need to apply these hours. hoursToApplyPrevious = hoursBeforeVirtualDay; } // Current Day TimeBlock previous = null; // Previous Time Block List<TimeBlock> accumulatedBlocks = new ArrayList<TimeBlock>(); // TimeBlocks we MAY or MAY NOT apply Shift Premium to. List<Interval> accumulatedBlockIntervals = new ArrayList<Interval>(); // To save recompute time when checking timeblocks for application we store them as we create them. // Iterate over sorted list, checking time boundaries vs Shift Intervals. long accumulatedMillis = TKUtils.convertHoursToMillis(hoursBeforeVirtualDay); boolean previousDayOnly = false; // IF the rule is not active today, but was on the previous day, we need to still look at time blocks. if (!dayIsRuleActive(currentDay, rule)) { if (dayIsRuleActive(currentDay.minusDays(1), rule)) { previousDayOnly = true; } else { // Nothing to see here, move to next rule. continue; } } /* * We will touch each time block and accumulate time blocks that are applicable to * the current rule we are on. */ // These blocks are only used for detail application // We don't want to pass along the previous pay period, // because we don't want to modify the time blocks on that // period. If null is passed, time will be placed on the // first block of the first period if the previous period // block had influence. List<TimeBlock> previousBlocksFiltered = (previousPayPeriodPrevDay) ? null : filterBlocksByApplicableEarnGroup(fromEarnGroup, ruleTimeBlocksPrev); for (TimeBlock current : ruleTimeBlocksCurr) { if (!timeBlockHasEarnCode(fromEarnGroup, current)) { // TODO: WorkSchedule considerations somewhere in here? continue; } Interval blockInterval = new Interval(current.getBeginDateTime().withZone(zone), current.getEndDateTime().withZone(zone)); // Check both Intervals, since the time blocks could still // be applicable to the previous day. These two intervals should // not have any overlap. if (previousDayShiftInterval.overlaps(shiftInterval)) { LOG.error("Interval of greater than 24 hours created in the rules processing."); return; // throw new RuntimeException("Interval of greater than 24 hours created in the rules processing."); } // This block of code handles cases where you have time // that spills to multiple days and a shift rule that // has a valid window on multiple consecutive days. Time // must be applied with the correct shift interval. Interval overlap = previousDayShiftInterval.overlap(blockInterval); evalInterval = previousDayShiftInterval; if (overlap == null) { if (hoursToApplyPrevious.compareTo(BigDecimal.ZERO) > 0) { // we have hours from previous day, and the shift // window is going to move to current day. // Need to apply this now, and move window forward // for current time block. BigDecimal accumHours = TKUtils.convertMillisToHours(accumulatedMillis); this.applyAccumulatedWrapper(accumHours, evalInterval, accumulatedBlockIntervals, accumulatedBlocks, previousBlocksFiltered, hoursToApplyPrevious, hoursToApply, rule); accumulatedMillis = 0L; // reset accumulated hours.. hoursToApply = BigDecimal.ZERO; hoursToApplyPrevious = BigDecimal.ZERO; } // Because of our position in the loop, when we are at this point, // we know we've passed any previous day shift intervals, so we can // determine if we should skip the current day based on the boolean // we set earlier. if (previousDayOnly) { continue; } overlap = shiftInterval.overlap(blockInterval); evalInterval = shiftInterval; } // Time bucketing and application as normal: // if (overlap != null) { // There IS overlap. if (previous != null) { // only check max gap if max gap of rule is not 0 if (rule.getMaxGap().compareTo(BigDecimal.ZERO) != 0 && exceedsMaxGap(previous, current, rule.getMaxGap())) { BigDecimal accumHours = TKUtils.convertMillisToHours(accumulatedMillis); this.applyAccumulatedWrapper(accumHours, evalInterval, accumulatedBlockIntervals, accumulatedBlocks, previousBlocksFiltered, hoursToApplyPrevious, hoursToApply, rule); accumulatedMillis = 0L; // reset accumulated hours.. hoursToApply = BigDecimal.ZERO; hoursToApplyPrevious = BigDecimal.ZERO; } else { long millis = overlap.toDurationMillis(); accumulatedMillis += millis; hoursToApply = hoursToApply.add(TKUtils.convertMillisToHours(millis)); } } else { // Overlap shift at first time block. long millis = overlap.toDurationMillis(); accumulatedMillis += millis; hoursToApply = hoursToApply.add(TKUtils.convertMillisToHours(millis)); } accumulatedBlocks.add(current); accumulatedBlockIntervals.add(blockInterval); previous = current; // current can still apply to next. } else { // No Overlap / Outside of Rule if (previous != null) { BigDecimal accumHours = TKUtils.convertMillisToHours(accumulatedMillis); this.applyAccumulatedWrapper(accumHours, evalInterval, accumulatedBlockIntervals, accumulatedBlocks, previousBlocksFiltered, hoursToApplyPrevious, hoursToApply, rule); accumulatedMillis = 0L; // reset accumulated hours.. hoursToApply = BigDecimal.ZERO; hoursToApplyPrevious = BigDecimal.ZERO; } } } // All time blocks are iterated over, check for remainders. // Check containers for time, and apply if needed. BigDecimal accumHours = TKUtils.convertMillisToHours(accumulatedMillis); this.applyAccumulatedWrapper(accumHours, evalInterval, accumulatedBlockIntervals, accumulatedBlocks, previousBlocksFiltered, hoursToApplyPrevious, hoursToApply, rule); } } // Keep track of previous as we move day by day. jobNumberToTimeBlocksPreviousDay = jobNumberToTimeBlocks; previousPayPeriodPrevDay = false; } } @Override public List<ShiftDifferentialRule> getShiftDifferentialRules(String userPrincipalId, String location, String hrSalGroup, String payGrade, LocalDate fromEffdt, LocalDate toEffdt, String active, String showHist) { List<ShiftDifferentialRule> results = new ArrayList<ShiftDifferentialRule>(); List<ShiftDifferentialRule> shiftDifferentialRuleObjs = shiftDifferentialRuleDao .getShiftDifferentialRules(location, hrSalGroup, payGrade, fromEffdt, toEffdt, active, showHist); for (ShiftDifferentialRule shiftDifferentialRuleObj : shiftDifferentialRuleObjs) { Map<String, String> roleQualification = new HashMap<String, String>(); roleQualification.put(KimConstants.AttributeConstants.PRINCIPAL_ID, userPrincipalId); roleQualification.put(KPMERoleMemberAttribute.LOCATION.getRoleMemberAttributeName(), shiftDifferentialRuleObj.getLocation()); if (!KimApiServiceLocator.getPermissionService().isPermissionDefinedByTemplate( KPMENamespace.KPME_WKFLW.getNamespaceCode(), KPMEPermissionTemplate.VIEW_KPME_RECORD.getPermissionTemplateName(), new HashMap<String, String>()) || KimApiServiceLocator.getPermissionService().isAuthorizedByTemplate(userPrincipalId, KPMENamespace.KPME_WKFLW.getNamespaceCode(), KPMEPermissionTemplate.VIEW_KPME_RECORD.getPermissionTemplateName(), new HashMap<String, String>(), roleQualification)) { results.add(shiftDifferentialRuleObj); } } return results; } private List<TimeBlock> filterBlocksByApplicableEarnGroup(Set<String> fromEarnGroup, List<TimeBlock> blocks) { List<TimeBlock> filtered; if (blocks == null || blocks.size() == 0) filtered = null; else { filtered = new ArrayList<TimeBlock>(); for (TimeBlock b : blocks) { if (timeBlockHasEarnCode(fromEarnGroup, b)) filtered.add(b); } } return filtered; } private void applyAccumulatedWrapper(BigDecimal accumHours, Interval evalInterval, List<Interval> accumulatedBlockIntervals, List<TimeBlock> accumulatedBlocks, List<TimeBlock> previousBlocks, BigDecimal hoursToApplyPrevious, BigDecimal hoursToApply, ShiftDifferentialRule rule) { if (accumHours.compareTo(rule.getMinHours()) >= 0) { this.applyPremium(evalInterval, accumulatedBlockIntervals, accumulatedBlocks, previousBlocks, hoursToApplyPrevious, hoursToApply, rule.getEarnCode()); } accumulatedBlocks.clear(); accumulatedBlockIntervals.clear(); } private void sortTimeBlocksInverse(List<TimeBlock> blocks) { Collections.sort(blocks, new Comparator<TimeBlock>() { // Sort the Time Blocks public int compare(TimeBlock tb1, TimeBlock tb2) { if (tb1 != null && tb2 != null) return -1 * tb1.getBeginTimestamp().compareTo(tb2.getBeginTimestamp()); return 0; } }); } private void sortTimeBlocksNatural(List<TimeBlock> blocks) { Collections.sort(blocks, new Comparator<TimeBlock>() { // Sort the Time Blocks public int compare(TimeBlock tb1, TimeBlock tb2) { if (tb1 != null && tb2 != null) return tb1.getBeginTimestamp().compareTo(tb2.getBeginTimestamp()); return 0; } }); } /** * * @param shift The shift interval - need to examine the time block to determine how many hours are eligible per block. * @param blockIntervals Intervals for each block present in the blocks list. Passed here to avoid re computation. * @param blocks The blocks we are applying hours to. * @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. * @param initialHours hours accumulated from a previous boundary that need to be applied here (NOT SUBJECT TO INTERVAL) * @param hours hours to apply * @param earnCode what earn code to create time hour detail entry for. */ void applyPremium(Interval shift, List<Interval> blockIntervals, List<TimeBlock> blocks, List<TimeBlock> previousBlocks, BigDecimal initialHours, BigDecimal hours, String earnCode) { for (int i = 0; i < blocks.size(); i++) { TimeBlock b = blocks.get(i); // Only apply initial hours to the first timeblock. if (i == 0 && (initialHours.compareTo(BigDecimal.ZERO) > 0)) { // ONLY if they're on the same document ID, do we apply to previous, // otherwise we dump all on the current document. if (previousBlocks != null && previousBlocks.size() > 0 && previousBlocks.get(0).getDocumentId().equals(b.getDocumentId())) { for (TimeBlock pb : previousBlocks) { BigDecimal lunchSub = this.negativeTimeHourDetailSum(pb); // A negative number initialHours = BigDecimal.ZERO.max(initialHours.add(lunchSub)); // We don't want negative premium hours! if (initialHours.compareTo(BigDecimal.ZERO) <= 0) // check here now as well, we may not have anything at all to apply. break; // Adjust hours on the block by the lunch sub hours, so we're not over applying. BigDecimal hoursToApply = initialHours.min(pb.getHours().add(lunchSub)); addPremiumTimeHourDetail(pb, hoursToApply, earnCode); initialHours = initialHours.subtract(hoursToApply, HrConstants.MATH_CONTEXT); if (initialHours.compareTo(BigDecimal.ZERO) <= 0) break; } } else { addPremiumTimeHourDetail(b, initialHours, earnCode); } } BigDecimal lunchSub = this.negativeTimeHourDetailSum(b); // A negative number hours = BigDecimal.ZERO.max(hours.add(lunchSub)); // We don't want negative premium hours! if (hours.compareTo(BigDecimal.ZERO) > 0) { Interval blockInterval = blockIntervals.get(i); Interval overlapInterval = shift.overlap(blockInterval); if (overlapInterval == null) continue; long overlap = overlapInterval.toDurationMillis(); BigDecimal hoursMax = TKUtils.convertMillisToHours(overlap); // Maximum number of possible hours applicable for this time block and shift rule // Adjust this time block's hoursMax (below) by lunchSub to // make sure the time applied is the correct amount per block. BigDecimal hoursToApply = hours.min(hoursMax.add(lunchSub)); addPremiumTimeHourDetail(b, hoursToApply, earnCode); hours = hours.subtract(hoursToApply, HrConstants.MATH_CONTEXT); } } } void addPremiumTimeHourDetail(TimeBlock block, BigDecimal hours, String earnCode) { List<TimeHourDetail> details = block.getTimeHourDetails(); TimeHourDetail premium = new TimeHourDetail(); premium.setHours(hours); premium.setEarnCode(earnCode); premium.setTkTimeBlockId(block.getTkTimeBlockId()); details.add(premium); } /** * Does the difference between the previous time blocks clock out time and the * current time blocks clock in time exceed the max gap. max gap is in minutes * * @param previous * @param current * @param maxGap * @return */ boolean exceedsMaxGap(TimeBlock previous, TimeBlock current, BigDecimal maxGap) { long difference = current.getBeginTimestamp().getTime() - previous.getEndTimestamp().getTime(); BigDecimal gapMinutes = TKUtils.convertMillisToMinutes(difference); return (gapMinutes.compareTo(maxGap) > 0); } public void setShiftDifferentialRuleDao(ShiftDifferentialRuleDao shiftDifferentialRuleDao) { this.shiftDifferentialRuleDao = shiftDifferentialRuleDao; } @Override public ShiftDifferentialRule getShiftDifferentialRule(String tkShiftDifferentialRuleId) { return this.shiftDifferentialRuleDao.findShiftDifferentialRule(tkShiftDifferentialRuleId); } @Override public List<ShiftDifferentialRule> getShiftDifferentalRules(String location, String hrSalGroup, String payGrade, String pyCalendarGroup, LocalDate asOfDate) { List<ShiftDifferentialRule> sdrs = new ArrayList<ShiftDifferentialRule>(); // location, sal group, pay grade, calendar sdrs.addAll(shiftDifferentialRuleDao.findShiftDifferentialRules(location, hrSalGroup, payGrade, pyCalendarGroup, asOfDate)); // location, sal group, *, calendar sdrs.addAll(shiftDifferentialRuleDao.findShiftDifferentialRules(location, hrSalGroup, "%", pyCalendarGroup, asOfDate)); // location, *, pay grade, calendar sdrs.addAll(shiftDifferentialRuleDao.findShiftDifferentialRules(location, "%", payGrade, pyCalendarGroup, asOfDate)); // location, *, *, calendar sdrs.addAll( shiftDifferentialRuleDao.findShiftDifferentialRules(location, "%", "%", pyCalendarGroup, asOfDate)); // *, sal group, pay grade, calendar sdrs.addAll(shiftDifferentialRuleDao.findShiftDifferentialRules("%", hrSalGroup, payGrade, pyCalendarGroup, asOfDate)); // *, sal group, *, calendar sdrs.addAll(shiftDifferentialRuleDao.findShiftDifferentialRules("%", hrSalGroup, "%", pyCalendarGroup, asOfDate)); // *, *, pay grade, calendar sdrs.addAll( shiftDifferentialRuleDao.findShiftDifferentialRules("%", "%", payGrade, pyCalendarGroup, asOfDate)); // *, *, *, calendar sdrs.addAll(shiftDifferentialRuleDao.findShiftDifferentialRules("%", "%", "%", pyCalendarGroup, asOfDate)); // location, sal group, pay grade, * sdrs.addAll( shiftDifferentialRuleDao.findShiftDifferentialRules(location, hrSalGroup, payGrade, "%", asOfDate)); // location, sal group, *, * sdrs.addAll(shiftDifferentialRuleDao.findShiftDifferentialRules(location, hrSalGroup, "%", "%", asOfDate)); // location, *, pay grade, * sdrs.addAll(shiftDifferentialRuleDao.findShiftDifferentialRules(location, "%", payGrade, "%", asOfDate)); // location, *, *, * sdrs.addAll(shiftDifferentialRuleDao.findShiftDifferentialRules(location, "%", "%", "%", asOfDate)); // *, sal group, pay grade, * sdrs.addAll(shiftDifferentialRuleDao.findShiftDifferentialRules("%", hrSalGroup, payGrade, "%", asOfDate)); // *, sal group, *, * sdrs.addAll(shiftDifferentialRuleDao.findShiftDifferentialRules("%", hrSalGroup, "%", "%", asOfDate)); // *, *, pay grade, * sdrs.addAll(shiftDifferentialRuleDao.findShiftDifferentialRules("%", "%", payGrade, "%", asOfDate)); // *, *, *, * sdrs.addAll(shiftDifferentialRuleDao.findShiftDifferentialRules("%", "%", "%", "%", asOfDate)); return sdrs; } private boolean dayIsRuleActive(DateTime currentDate, ShiftDifferentialRule sdr) { boolean active = false; switch (currentDate.getDayOfWeek()) { case DateTimeConstants.MONDAY: active = sdr.isMonday(); break; case DateTimeConstants.TUESDAY: active = sdr.isTuesday(); break; case DateTimeConstants.WEDNESDAY: active = sdr.isWednesday(); break; case DateTimeConstants.THURSDAY: active = sdr.isThursday(); break; case DateTimeConstants.FRIDAY: active = sdr.isFriday(); break; case DateTimeConstants.SATURDAY: active = sdr.isSaturday(); break; case DateTimeConstants.SUNDAY: active = sdr.isSunday(); break; } return active; } @Override public void saveOrUpdate(List<ShiftDifferentialRule> shiftDifferentialRules) { shiftDifferentialRuleDao.saveOrUpdate(shiftDifferentialRules); } @Override public void saveOrUpdate(ShiftDifferentialRule shiftDifferentialRule) { shiftDifferentialRuleDao.saveOrUpdate(shiftDifferentialRule); } }