001package com.identityworksllc.iiq.common; 002 003import java.time.DayOfWeek; 004import java.time.LocalDate; 005import java.time.temporal.Temporal; 006import java.time.temporal.TemporalAdjuster; 007import java.util.Calendar; 008import java.util.EnumSet; 009import java.util.HashSet; 010import java.util.Set; 011 012/** 013 * A {@link TemporalAdjuster} that adds or subtracts a specified number of business days to a date. 014 * A business day is a date that is not a weekend or in the list of holidays. 015 * 016 * For example: 017 * {@code} 018 * LocalDate now = LocalDate.now(); 019 * LocalDate nextBusinessDay = now.with(BusinessDateAdjuster.addWorkDays(1)); 020 * {@code} 021 * 022 * You can also use it with a {@link Calendar} object via the static helper methods. 023 * 024 * {@code} 025 * Calendar now = Calendar.getInstance(); 026 * Calendar nextBusinessDay = BusinessDateAdjuster.adjustCalendar(now, 1); 027 * {@code} 028 */ 029public class BusinessDayAdjuster implements TemporalAdjuster { 030 /** 031 * The default set of weekend days (Saturday and Sunday). 032 */ 033 public static final Set<DayOfWeek> DEFAULT_WEEKEND_DAYS = EnumSet.of(DayOfWeek.SATURDAY, DayOfWeek.SUNDAY); 034 035 /** 036 * The number of business days to add (positive) or subtract (negative). 037 */ 038 private final int days; 039 040 /** 041 * The set of dates to be considered holidays (non-work days). 042 */ 043 private final Set<LocalDate> holidays; 044 045 /** 046 * The set of days of the week to be considered part of the weekend. 047 */ 048 private final Set<DayOfWeek> weekendDays; 049 050 /** 051 * Creates a new BusinessDayAdjuster with no holidays and the default set of weekend days. 052 * @param days The number of business days to add (positive) or subtract (negative) 053 */ 054 public BusinessDayAdjuster(int days) { 055 this(days, null, DEFAULT_WEEKEND_DAYS); 056 } 057 058 /** 059 * Creates a new BusinessDayAdjuster with the given list of holidays and the default set of weekend days. 060 * @param days The number of business days to add (positive) or subtract (negative) 061 * @param holidays A set of dates to be considered holidays (non-work days) 062 */ 063 public BusinessDayAdjuster(int days, Set<LocalDate> holidays) { 064 this(days, holidays, DEFAULT_WEEKEND_DAYS); 065 } 066 067 /** 068 * Creates a new BusinessDayAdjuster. 069 * 070 * @param days The number of business days to add (positive) or subtract (negative) 071 * @param holidays A set of dates to be considered holidays (non-work days) 072 * @param weekendDays A set of days to be considered weekends 073 */ 074 public BusinessDayAdjuster(int days, Set<LocalDate> holidays, Set<DayOfWeek> weekendDays) { 075 if (weekendDays != null && weekendDays.size() == 7) { 076 throw new IllegalArgumentException("Weekend days cannot include all days of the week"); 077 } 078 this.days = days; 079 this.holidays = holidays != null ? holidays : new HashSet<>(); 080 this.weekendDays = weekendDays != null ? weekendDays : EnumSet.of(DayOfWeek.SATURDAY, DayOfWeek.SUNDAY); 081 } 082 083 /** 084 * Creates a BusinessDayAdjuster that adds the specified number of work days. 085 * 086 * @param days The number of work days to add 087 * @return A BusinessDayAdjuster for use with {@link LocalDate#with(TemporalAdjuster)} 088 */ 089 public static BusinessDayAdjuster addWorkDays(int days) { 090 return new BusinessDayAdjuster(days, null, DEFAULT_WEEKEND_DAYS); 091 } 092 093 /** 094 * Creates a BusinessDayAdjuster that adds the specified number of work days. 095 * 096 * @param days The number of work days to add 097 * @param holidays A set of dates to be considered holidays 098 * @return A BusinessDayAdjuster for use with {@link LocalDate#with(TemporalAdjuster)} 099 */ 100 public static BusinessDayAdjuster addWorkDays(int days, Set<LocalDate> holidays) { 101 return new BusinessDayAdjuster(days, holidays, DEFAULT_WEEKEND_DAYS); 102 } 103 104 /** 105 * Creates a BusinessDayAdjuster that adds the specified number of work days. 106 * 107 * @param days The number of work days to add 108 * @param holidays A set of dates to be considered holidays 109 * @param weekendDays A set of days to be considered weekends 110 * @return A BusinessDayAdjuster for use with {@link LocalDate#with(TemporalAdjuster)} 111 */ 112 public static BusinessDayAdjuster addWorkDays(int days, Set<LocalDate> holidays, Set<DayOfWeek> weekendDays) { 113 return new BusinessDayAdjuster(days, holidays, weekendDays); 114 } 115 116 /** 117 * Adjusts the specified calendar by the required number of business days. 118 * The input calendar is not modified. 119 * 120 * @param input The input calendar 121 * @param days The number of business days to add (positive) or subtract (negative) 122 * @return The adjusted calendar, a separate object from the original 123 */ 124 public static Calendar adjustCalendar(Calendar input, int days) { 125 return adjustCalendar(input, days, new HashSet<>()); 126 } 127 128 /** 129 * Adjusts the specified calendar by the required number of business days. 130 * The input calendar is not modified. 131 * 132 * @param input The input calendar 133 * @param days The number of business days to add (positive) or subtract (negative) 134 * @param holidays A set of dates to be considered holidays 135 * @return The adjusted calendar, a separate object from the original 136 */ 137 public static Calendar adjustCalendar(Calendar input, int days, Set<LocalDate> holidays) { 138 return adjustCalendar(input, days, holidays, DEFAULT_WEEKEND_DAYS); 139 } 140 141 /** 142 * Adjusts the specified calendar by the required number of business days. 143 * The input calendar is not modified. 144 * 145 * @param input The input calendar 146 * @param days The number of business days to add (positive) or subtract (negative) 147 * @param holidays A set of dates to be considered holidays 148 * @param weekendDays A set of days to be considered weekends 149 * @return The adjusted calendar, a separate object from the original 150 */ 151 public static Calendar adjustCalendar(Calendar input, int days, Set<LocalDate> holidays, Set<DayOfWeek> weekendDays) { 152 // Convert Calendar to LocalDate 153 LocalDate date = LocalDate.of(input.get(Calendar.YEAR), input.get(Calendar.MONTH) + 1, input.get(Calendar.DAY_OF_MONTH)); 154 155 // Adjust the LocalDate 156 LocalDate adjustedDate = date.with(BusinessDayAdjuster.addWorkDays(days, holidays, weekendDays)); 157 158 // Convert LocalDate back to Calendar 159 Calendar output = Calendar.getInstance(); 160 output.set(adjustedDate.getYear(), adjustedDate.getMonthValue() - 1, adjustedDate.getDayOfMonth()); 161 162 // Restore the time component of the Calendar 163 output.set(Calendar.HOUR_OF_DAY, input.get(Calendar.HOUR_OF_DAY)); 164 output.set(Calendar.MINUTE, input.get(Calendar.MINUTE)); 165 output.set(Calendar.SECOND, input.get(Calendar.SECOND)); 166 output.set(Calendar.MILLISECOND, input.get(Calendar.MILLISECOND)); 167 168 return output; 169 } 170 171 /** 172 * Creates a BusinessDayAdjuster that subtracts the specified number of work days. 173 * 174 * @param days The number of work days to add 175 * @param holidays A set of dates to be considered holidays 176 * @return A BusinessDayAdjuster for use with {@link LocalDate#with(TemporalAdjuster)} 177 */ 178 public static BusinessDayAdjuster subtractWorkDays(int days, Set<LocalDate> holidays) { 179 return new BusinessDayAdjuster(-Math.abs(days), holidays, DEFAULT_WEEKEND_DAYS); 180 } 181 182 /** 183 * Creates a BusinessDayAdjuster that subtracts the specified number of work days. 184 * 185 * @param days The number of work days to add 186 * @return A BusinessDayAdjuster for use with {@link LocalDate#with(TemporalAdjuster)} 187 */ 188 public static BusinessDayAdjuster subtractWorkDays(int days) { 189 return new BusinessDayAdjuster(-Math.abs(days), null, DEFAULT_WEEKEND_DAYS); 190 } 191 192 /** 193 * Creates a WorkDayAdjuster that subtracts the specified number of work days. 194 * 195 * @param days The number of work days to subtract 196 * @param holidays A set of dates to be considered holidays 197 * @param weekendDays A set of days to be considered weekends 198 * @return A BusinessDayAdjuster for use with {@link LocalDate#with(TemporalAdjuster)} 199 */ 200 public static BusinessDayAdjuster subtractWorkDays(int days, Set<LocalDate> holidays, Set<DayOfWeek> weekendDays) { 201 return new BusinessDayAdjuster(-Math.abs(days), holidays, weekendDays); 202 } 203 204 /** 205 * Adjusts the specified temporal object by the required number of business days. 206 * 207 * @param temporal the temporal object to adjust, not null 208 * @return the adjusted temporal object, not null 209 * @throws NullPointerException if the input object is null 210 * @see TemporalAdjuster#adjustInto(Temporal) 211 */ 212 @Override 213 public Temporal adjustInto(Temporal temporal) { 214 if (temporal == null) { 215 throw new NullPointerException("temporal is null"); 216 } 217 218 LocalDate date = LocalDate.from(temporal); 219 220 // If no adjustment needed, return original date 221 if (days == 0) { 222 return date; 223 } 224 225 // Determine direction (adding or subtracting days) 226 int direction = Integer.signum(days); 227 int absWorkDays = Math.abs(days); 228 229 int weekendDayCount = 7 - this.weekendDays.size(); 230 231 // Step 1: Calculate naive target date (ignoring holidays, including weekends) 232 // For every {weekendDayCount} work days, we add 7 calendar days (i.e., a full week) 233 int fullWeeks = absWorkDays / weekendDayCount; 234 int remainingWorkDays = absWorkDays % weekendDayCount; 235 236 // Calculate calendar days from full weeks (7 days per week) 237 int calendarDays = fullWeeks * 7; 238 239 // Add remaining work days, accounting for weekends 240 LocalDate tempDate = date.plusDays(direction * calendarDays); 241 int addedWorkDays = 0; 242 243 while (addedWorkDays < remainingWorkDays) { 244 tempDate = tempDate.plusDays(direction); 245 DayOfWeek dayOfWeek = tempDate.getDayOfWeek(); 246 if (!weekendDays.contains(dayOfWeek)) { 247 addedWorkDays++; 248 } 249 } 250 251 while (weekendDays.contains(tempDate.getDayOfWeek())) { 252 tempDate = tempDate.plusDays(direction); 253 } 254 255 // Step 2: Adjust for holidays 256 // Get all holidays that fall on work days between start and end dates 257 LocalDate startDate = direction > 0 ? date : tempDate; 258 LocalDate endDate = direction > 0 ? tempDate : date; 259 260 long holidaysInRange = holidays.stream() 261 .filter(holiday -> !holiday.isBefore(startDate) && !holiday.isAfter(endDate)) 262 .filter(holiday -> { 263 DayOfWeek dow = holiday.getDayOfWeek(); 264 return !weekendDays.contains(dow); 265 }) 266 .count(); 267 268 // Add or subtract extra days to account for holidays 269 if (holidaysInRange > 0) { 270 for (int i = 0; i < holidaysInRange; i++) { 271 // For each holiday, add days in the specified direction 272 // until we find a non-holiday work day. 273 while (true) { 274 tempDate = tempDate.plusDays(direction); 275 DayOfWeek dayOfWeek = tempDate.getDayOfWeek(); 276 277 // Skip weekends 278 if (weekendDays.contains(dayOfWeek)) { 279 continue; 280 } 281 282 // Skip if this additional day is also a holiday 283 if (holidays.contains(tempDate)) { 284 continue; 285 } 286 287 break; 288 } 289 } 290 } 291 292 return tempDate; 293 } 294}