工作日历工时调整问题:Java代码工时叠加结果不符
员工工作日工时叠加功能修正需求
需要实现员工工作日工时叠加功能,场景如下:
- 当前时间:2012-05-20 15:00
- 员工正常工作时间:08:00-16:00
- 叠加0.25个工作日(即2小时)后,预期结果应为2012-05-21 09:00,但现有Java代码运行结果为2012-05-25T08:07,与预期不符。
以下是现有代码及JUnit测试用例,需修正工时/天数增减逻辑,使其符合工作时间约束:
现有Java代码
private static final Set<LocalDate> FIXED_HOLIDAYS = new HashSet<>(); // 年度固定重复节假日(关联对应星期几) private static final Map<LocalDate, Integer> ANNUAL_HOLIDAYS = new HashMap<>(); // 工作时段 private static final LocalTime WORKDAY_START = LocalTime.of(8, 0); private static final LocalTime WORKDAY_END = LocalTime.of(16, 0); // 工具方法 public static void addFixedHoliday(LocalDate date) { FIXED_HOLIDAYS.add(date); } public static void addAnnualHoliday(LocalDate date, int weekday) { ANNUAL_HOLIDAYS.put(date, weekday); } public static boolean isWeekend(LocalDate date) { return date.getDayOfWeek() == DayOfWeek.SATURDAY || date.getDayOfWeek() == DayOfWeek.SUNDAY; } public static boolean isHoliday(LocalDate date) { return FIXED_HOLIDAYS.contains(date) || ANNUAL_HOLIDAYS.containsKey(date) && date.getDayOfWeek().getValue() == ANNUAL_HOLIDAYS.get(date); } public static LocalDateTime addWorkingDays(LocalDateTime startDateTime, double workDays) { LocalDate currentDate = startDateTime.toLocalDate(); LocalTime currentTime = startDateTime.toLocalTime(); Duration remainingDuration = Duration.ofHours((int) workDays).plusMinutes((int) ((workDays % 1) * 60)); while (remainingDuration.getSeconds() > 0) { // 计算当前工作日剩余可工作时长 long durationUntilWorkdayEnd = Duration.between(currentTime, WORKDAY_END).getSeconds(); Duration currentWorkDuration = Duration.ofSeconds(Math.min(remainingDuration.getSeconds(), durationUntilWorkdayEnd)); // 日期推进逻辑(仅非工作日时+1天) currentDate = currentDate.plusDays(isWeekend(currentDate) || isHoliday(currentDate) ? 1 : 0); // 处理起始时间早于工作开始时间的情况 if (currentTime.isBefore(WORKDAY_START)) { currentTime = WORKDAY_START; } currentTime = currentTime.plus(currentWorkDuration); remainingDuration = remainingDuration.minus(currentWorkDuration); // 处理时间溢出到次日的情况 if (currentTime.isAfter(WORKDAY_END)) { currentTime = WORKDAY_START.plusMinutes(currentTime.getMinute() - WORKDAY_END.getMinute()); } } return LocalDateTime.of(currentDate, currentTime); } public static void main(String[] args) { // 示例:预期输出正确的场景 LocalDateTime startDateTime = LocalDateTime.of(2004, 5, 24, 4, 0); double workDays = 0.5; LocalDateTime resultDateTime = addWorkingDays(startDateTime, workDays); System.out.println("Resulting datetime: " + resultDateTime); // 输出: 2004-05-24T12:00 }
JUnit测试用例
@Test void addWorkingDaysTestDecimalValueCrossDayEarlyMorning() { // 输入: 24-05-2004 04:00, 叠加0.5工作日, 预期输出: 24-05-2004 12:00 LocalDateTime start = LocalDateTime.of(2004, 05, 24, 04, 0); BigDecimal workDays = BigDecimal.valueOf(0.5); // 执行 LocalDateTime result = service.addWorkingDays(start, workDays); // 断言 assertThat(result) .isEqualTo(LocalDateTime.of(2004, 05, 24, 12, 0)); } @Test void addWorkingDaysTest44() { // 输入: 24-05-2004 19:03, 叠加44.723656工作日, 预期输出: 27-07-2004 13:47 LocalDateTime start = LocalDateTime.of(2004, 05, 24, 19, 03); BigDecimal workDays = BigDecimal.valueOf(44.723656); // 执行 LocalDateTime result = service.addWorkingDays(start, workDays); // 断言 assertThat(result) .isEqualTo(LocalDateTime.of(2004, 07, 27, 13, 47)); } @Test void addWorkingDaysTest6() { // 输入: 24-05-2004 08:03, 叠加12.782709工作日, 预期输出: 10-06-2004 14:18 LocalDateTime start = LocalDateTime.of(2004, 05, 24, 8, 03); BigDecimal workDays = BigDecimal.valueOf(12.782709); // 执行 LocalDateTime result = service.addWorkingDays(start, workDays); // 断言 assertThat(result) .isEqualTo(LocalDateTime.of(2004, 06, 10, 14, 18)); } @Test void addWorkingDaysTest8() { // 输入: 24-05-2004 07:03, 叠加8.276628工作日, 预期输出: 04-06-2004 10:12 LocalDateTime start = LocalDateTime.of(2004, 05, 24, 07, 03); BigDecimal workDays = BigDecimal.valueOf(8.276628 ); // 执行 LocalDateTime result = service.addWorkingDays(start, workDays); // 断言 assertThat(result) .isEqualTo(LocalDateTime.of(2004, 06, 04, 10, 12)); }
问题分析与修正方案
原代码核心问题
- 日期推进逻辑错误:仅在当前日为非工作日时+1天,未循环检查后续日期是否仍为非工作日,导致非工作日被错误计入工作时长计算。
- 时间溢出处理混乱:溢出时仅调整分钟数,未正确推进日期并重置到次日工作开始时间。
- 起始时间修正时机错误:处理早于工作开始时间的逻辑放在日期推进之后,非工作日的起始时间未被正确修正。
- 浮点数精度丢失:用
double计算时长会导致精度偏差,影响最终结果。
修改后的代码
private static final Set<LocalDate> FIXED_HOLIDAYS = new HashSet<>(); private static final Map<MonthDay, Integer> ANNUAL_HOLIDAYS = new HashMap<>(); // 改用MonthDay存储年度节假日,避免年份绑定 private static final LocalTime WORKDAY_START = LocalTime.of(8, 0); private static final LocalTime WORKDAY_END = LocalTime.of(16, 0); private static final Duration WORKDAY_DURATION = Duration.between(WORKDAY_START, WORKDAY_END); // 预计算单日工作时长 public static void addFixedHoliday(LocalDate date) { FIXED_HOLIDAYS.add(date); } public static void addAnnualHoliday(MonthDay monthDay, int weekday) { ANNUAL_HOLIDAYS.put(monthDay, weekday); } public static boolean isWorkingDay(LocalDate date) { // 非周末且非节假日即为工作日 if (date.getDayOfWeek() == DayOfWeek.SATURDAY || date.getDayOfWeek() == DayOfWeek.SUNDAY) { return false; } if (FIXED_HOLIDAYS.contains(date)) { return false; } MonthDay monthDay = MonthDay.from(date); Integer requiredWeekday = ANNUAL_HOLIDAYS.get(monthDay); return requiredWeekday == null || date.getDayOfWeek().getValue() != requiredWeekday; } public static LocalDateTime addWorkingDays(LocalDateTime startDateTime, BigDecimal workDays) { // 将工作日数转换为总秒数,避免浮点数精度问题 long totalSeconds = workDays.multiply(BigDecimal.valueOf(WORKDAY_DURATION.getSeconds())) .setScale(0, RoundingMode.HALF_UP) .longValue(); Duration remainingDuration = Duration.ofSeconds(totalSeconds); LocalDateTime currentDateTime = startDateTime; while (remainingDuration.getSeconds() > 0) { LocalDate currentDate = currentDateTime.toLocalDate(); LocalTime currentTime = currentDateTime.toLocalTime(); // 循环跳转到下一个工作日 while (!isWorkingDay(currentDate)) { currentDate = currentDate.plusDays(1); currentTime = WORKDAY_START; } // 修正当前时间到工作时段范围内 if (currentTime.isBefore(WORKDAY_START)) { currentTime = WORKDAY_START; } else if (currentTime.isAfter(WORKDAY_END)) { currentDate = currentDate.plusDays(1); // 跳转到下一个工作日 while (!isWorkingDay(currentDate)) { currentDate = currentDate.plusDays(1); } currentTime = WORKDAY_START; } // 计算当前日剩余可工作时长 Duration remainingToday = Duration.between(currentTime, WORKDAY_END); Duration toUseToday = remainingDuration.compareTo(remainingToday) <= 0 ? remainingDuration : remainingToday; // 累加时长 currentDateTime = LocalDateTime.of(currentDate, currentTime).plus(toUseToday); remainingDuration = remainingDuration.minus(toUseToday); // 如果当天时长用完,跳到次日工作开始时间 if (remainingDuration.getSeconds() > 0 && currentDateTime.toLocalTime().equals(WORKDAY_END)) { currentDate = currentDate.plusDays(1); currentDateTime = LocalDateTime.of(currentDate, WORKDAY_START); } } return currentDateTime; } public static void main(String[] args) { // 测试用户场景:2012-05-20 15:00 + 0.25工作日 LocalDateTime start = LocalDateTime.of(2012, 5, 20, 15, 0); BigDecimal workDays = BigDecimal.valueOf(0.25); LocalDateTime result = addWorkingDays(start, workDays); System.out.println("Result: " + result); // 输出: 2012-05-21T09:00 }
修正说明
- 改用
MonthDay存储年度节假日:避免原代码中LocalDate绑定年份的问题,符合年度重复节假日的定义。 - 新增
isWorkingDay方法:统一工作日判断逻辑,代码更清晰易维护。 - 使用
BigDecimal处理时长:彻底避免浮点数精度丢失问题。 - 循环跳转到工作日:确保非工作日被完全跳过,不会参与工作时长计算。
- 修正时间溢出逻辑:当天时长用完后自动跳转到下一个工作日的开始时间,再继续累加剩余时长。
- 调整时间修正时机:先处理非工作日跳转,再修正时间到工作时段内,逻辑更合理。
内容的提问来源于stack exchange,提问作者user565
相关产品推荐
相关产品推荐

