You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

工作日历工时调整问题: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. 日期推进逻辑错误:仅在当前日为非工作日时+1天,未循环检查后续日期是否仍为非工作日,导致非工作日被错误计入工作时长计算。
  2. 时间溢出处理混乱:溢出时仅调整分钟数,未正确推进日期并重置到次日工作开始时间。
  3. 起始时间修正时机错误:处理早于工作开始时间的逻辑放在日期推进之后,非工作日的起始时间未被正确修正。
  4. 浮点数精度丢失:用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
}

修正说明

  1. 改用MonthDay存储年度节假日:避免原代码中LocalDate绑定年份的问题,符合年度重复节假日的定义。
  2. 新增isWorkingDay方法:统一工作日判断逻辑,代码更清晰易维护。
  3. 使用BigDecimal处理时长:彻底避免浮点数精度丢失问题。
  4. 循环跳转到工作日:确保非工作日被完全跳过,不会参与工作时长计算。
  5. 修正时间溢出逻辑:当天时长用完后自动跳转到下一个工作日的开始时间,再继续累加剩余时长。
  6. 调整时间修正时机:先处理非工作日跳转,再修正时间到工作时段内,逻辑更合理。

内容的提问来源于stack exchange,提问作者user565

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.29 13:25:56