001 /** 002 * Copyright 2004-2012 The Kuali Foundation 003 * 004 * Licensed under the Educational Community License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * http://www.opensource.org/licenses/ecl2.php 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016 package org.kuali.hr.time.flsa; 017 018 import org.apache.commons.lang.StringUtils; 019 import org.joda.time.DateTime; 020 import org.joda.time.DateTimeZone; 021 import org.joda.time.Interval; 022 import org.joda.time.LocalDateTime; 023 import org.kuali.hr.time.timeblock.TimeBlock; 024 import org.kuali.hr.time.timeblock.TimeHourDetail; 025 import org.kuali.hr.time.util.TKUtils; 026 import org.kuali.hr.time.util.TkConstants; 027 028 import java.math.BigDecimal; 029 import java.util.ArrayList; 030 import java.util.HashMap; 031 import java.util.List; 032 import java.util.Map; 033 034 public class FlsaDay { 035 private Map<String,BigDecimal> earnCodeToHours = new HashMap<String,BigDecimal>(); 036 private Map<String,List<TimeBlock>> earnCodeToTimeBlocks = new HashMap<String,List<TimeBlock>>(); 037 private List<TimeBlock> appliedTimeBlocks = new ArrayList<TimeBlock>(); 038 039 Interval flsaDateInterval; 040 LocalDateTime flsaDate; 041 DateTimeZone timeZone; 042 043 /** 044 * 045 * @param flsaDate A LocalDateTime because we want to be conscious of the 046 * relative nature of this flsa/window 047 * @param timeBlocks 048 * @param timeZone The timezone we are constructing, relative. 049 */ 050 public FlsaDay(LocalDateTime flsaDate, List<TimeBlock> timeBlocks, DateTimeZone timeZone) { 051 this.flsaDate = flsaDate; 052 this.timeZone = timeZone; 053 flsaDateInterval = new Interval(flsaDate.toDateTime(timeZone), flsaDate.toDateTime(timeZone).plusHours(24)); 054 this.setTimeBlocks(timeBlocks); 055 } 056 057 /** 058 * Handles the breaking apart of existing time blocks around FLSA boundaries. 059 * 060 * This method will compare the FLSA interval against the timeblock interval 061 * to determine how many hours overlap. It will then examine the time hour 062 * details 063 * 064 * @param timeBlocks a sorted list of time blocks. 065 */ 066 public void setTimeBlocks(List<TimeBlock> timeBlocks) { 067 for (TimeBlock block : timeBlocks) 068 if (!applyBlock(block, this.appliedTimeBlocks)) 069 break; 070 } 071 072 /** 073 * This method will compute the mappings present for this object: 074 * 075 * earnCodeToTimeBlocks 076 * earnCodeToHours 077 * 078 */ 079 public void remapTimeHourDetails() { 080 List<TimeBlock> reApplied = new ArrayList<TimeBlock>(appliedTimeBlocks.size()); 081 earnCodeToHours.clear(); 082 earnCodeToTimeBlocks.clear(); 083 for (TimeBlock block : appliedTimeBlocks) { 084 applyBlock(block, reApplied); 085 } 086 } 087 088 /** 089 * This method determines if the provided TimeBlock is applicable to this 090 * FLSA day, and if so will add it to the applyList. It could be the case 091 * that a TimeBlock is on the boundary of the FLSA day so that only a 092 * partial amount of the hours for that TimeBlock will count towards this 093 * day. 094 * 095 * |---------+------------------+---------| 096 * | Day 1 | Day 1/2 Boundary | Day 2 | 097 * |---------+------------------+---------| 098 * | Block 1 | | Block 2 | 099 * |---------+------------------+---------| 100 * 101 * The not so obvious ascii diagram above is intended to illustrate the case 102 * where on day one you have 1 fully overlapping time block (block1) and one 103 * partially overlapping time block (block2). Block 2 belongs to both FLSA 104 * Day 1 and Day 2. 105 * 106 * @param block A time block that we want to check and apply to this day. 107 * @param applyList A list of time blocks we want to add applicable time blocks to. 108 * 109 * @return True if the block is applicable, false otherwise. The return 110 * value can be used as a quick exit for the setTimeBlocks() method. 111 * 112 * TODO : Bucketing of partial FLSA days is still suspect, however real life examples of this are likely non-existent to rare. 113 * 114 * Danger may still lurk in day-boundary overlapping time blocks that have multiple Time Hour Detail entries. 115 */ 116 private boolean applyBlock(TimeBlock block, List<TimeBlock> applyList) { 117 DateTime beginDateTime = new DateTime(block.getBeginTimestamp(), this.timeZone); 118 DateTime endDateTime = new DateTime(block.getEndTimestamp(), this.timeZone); 119 120 if (beginDateTime.isAfter(flsaDateInterval.getEnd())) 121 return false; 122 123 Interval timeBlockInterval = null; 124 //Requested to have zero hour time blocks be able to be added to the GUI 125 boolean zeroHoursTimeBlock = false; 126 if(endDateTime.getMillis() > beginDateTime.getMillis()){ 127 timeBlockInterval = new Interval(beginDateTime,endDateTime); 128 } 129 130 if(flsaDateInterval.contains(beginDateTime)){ 131 zeroHoursTimeBlock = true; 132 } 133 134 Interval overlapInterval = flsaDateInterval.overlap(timeBlockInterval); 135 long overlap = (overlapInterval == null) ? 0L : overlapInterval.toDurationMillis(); 136 BigDecimal overlapHours = TKUtils.convertMillisToHours(overlap); 137 if((overlapHours.compareTo(BigDecimal.ZERO) == 0) && flsaDateInterval.contains(beginDateTime) && flsaDateInterval.contains(endDateTime)){ 138 if(block.getHours().compareTo(BigDecimal.ZERO) > 0){ 139 overlapHours = block.getHours(); 140 } 141 } 142 143 // Local lookup for this time-block to ensure we are not over applicable hours. 144 // You will notice below we are earn codes globally per day, and also locally per timeblock. 145 // The local per-time block mapping is used only to verify that we have not gone over allocated overlap time 146 // for the individual time block. 147 Map<String,BigDecimal> localEarnCodeToHours = new HashMap<String,BigDecimal>(); 148 149 if (zeroHoursTimeBlock || overlapHours.compareTo(BigDecimal.ZERO) > 0 || (flsaDateInterval.contains(beginDateTime) && StringUtils.equals(block.getEarnCodeType(),TkConstants.EARN_CODE_AMOUNT))) { 150 151 List<TimeHourDetail> details = block.getTimeHourDetails(); 152 for (TimeHourDetail thd : details) { 153 BigDecimal ecHours = earnCodeToHours.containsKey(thd.getEarnCode()) ? earnCodeToHours.get(thd.getEarnCode()) : BigDecimal.ZERO; 154 BigDecimal localEcHours = localEarnCodeToHours.containsKey(thd.getEarnCode()) ? localEarnCodeToHours.get(thd.getEarnCode()) : BigDecimal.ZERO; 155 //NOTE adding this in the last few hours before release.. remove if side effects are noticed 156 if (overlapHours.compareTo(localEcHours) >= 0 || thd.getAmount().compareTo(BigDecimal.ZERO) == 0) { 157 ecHours = ecHours.add(thd.getHours(), TkConstants.MATH_CONTEXT); 158 localEcHours = localEcHours.add(thd.getHours(), TkConstants.MATH_CONTEXT); 159 earnCodeToHours.put(thd.getEarnCode(), ecHours); 160 localEarnCodeToHours.put(thd.getEarnCode(), localEcHours); 161 } 162 } 163 164 List<TimeBlock> blocks = earnCodeToTimeBlocks.get(block.getEarnCode()); 165 if (blocks == null) { 166 blocks = new ArrayList<TimeBlock>(); 167 earnCodeToTimeBlocks.put(block.getEarnCode(), blocks); 168 } 169 blocks.add(block); 170 applyList.add(block); 171 } 172 173 return true; 174 } 175 176 public Map<String, BigDecimal> getEarnCodeToHours() { 177 return earnCodeToHours; 178 } 179 180 public Map<String, List<TimeBlock>> getEarnCodeToTimeBlocks() { 181 return earnCodeToTimeBlocks; 182 } 183 184 public void setEarnCodeToTimeBlocks(Map<String, List<TimeBlock>> earnCodeToTimeBlocks) { 185 this.earnCodeToTimeBlocks = earnCodeToTimeBlocks; 186 } 187 188 public List<TimeBlock> getAppliedTimeBlocks() { 189 return appliedTimeBlocks; 190 } 191 192 public void setAppliedTimeBlocks(List<TimeBlock> appliedTimeBlocks) { 193 this.appliedTimeBlocks = appliedTimeBlocks; 194 } 195 196 197 }