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

Optaplanner ProjectJobScheduling日期转LocalDateTime的分数计算问题咨询

方案可行性与解决思路

首先明确一点:你的方案完全可行!OptaPlanner原生支持LocalDateTime这类日期时间类型作为规划变量,只要把「基于资源业务日历的有效工作时长计算」逻辑整合到增量分数计算器中,就能精准算出符合需求的soft分数。

接下来我会一步步拆解实现步骤,再给你带真实日期的代码示例。


核心实现步骤

1. 替换实体类的日期类型

把原来用时间戳(Long)存储的开始/结束时间,改成LocalDateTime类型。比如在Job实体中:

public class Job {
    // 原字段:private Long startTime;
    private LocalDateTime startTime;
    private LocalDateTime endTime;
    private Resource resource;
    // 其他字段和getter/setter...
}

同时给Resource实体添加业务日历相关配置,比如工作时段、节假日列表:

public class Resource {
    private String name;
    // 工作时段,比如每天9:00-18:00
    private LocalTime workStartTime;
    private LocalTime workEndTime;
    // 节假日列表,比如2024年国庆、元旦
    private Set<LocalDate> holidays;
    // getter/setter...
}

2. 实现业务日历工具类

这个工具类是核心,用来计算两个LocalDateTime之间扣除非工作时间(周末、节假日、工作时段外)的有效工作分钟数。示例如下:

public class BusinessCalendarUtil {

    // 计算两个时间之间的有效工作分钟数
    public static long calculateWorkingMinutes(LocalDateTime start, LocalDateTime end, Resource resource) {
        if (start.isAfter(end)) {
            return 0;
        }

        long totalMinutes = 0;
        LocalDateTime current = start;

        while (current.isBefore(end)) {
            LocalDate currentDate = current.toLocalDate();
            // 跳过周末和节假日
            if (isWeekend(currentDate) || resource.getHolidays().contains(currentDate)) {
                current = current.with(LocalTime.MAX).plusNanos(1);
                continue;
            }

            // 计算当天的工作时段交集
            LocalDateTime workStart = currentDate.atTime(resource.getWorkStartTime());
            LocalDateTime workEnd = currentDate.atTime(resource.getWorkEndTime());

            LocalDateTime periodStart = current.isAfter(workStart) ? current : workStart;
            LocalDateTime periodEnd = end.isBefore(workEnd) ? end : workEnd;

            if (periodStart.isBefore(periodEnd)) {
                totalMinutes += Duration.between(periodStart, periodEnd).toMinutes();
            }

            current = workEnd.plusNanos(1);
        }

        return totalMinutes;
    }

    private static boolean isWeekend(LocalDate date) {
        DayOfWeek day = date.getDayOfWeek();
        return day == DayOfWeek.SATURDAY || day == DayOfWeek.SUNDAY;
    }
}

3. 改造增量分数计算器

在ProjectJobSchedulingIncrementalScoreCalculator中,替换原来的时间戳逻辑,调用上面的工具类计算有效时长,更新soft分数。

初始化阶段(全量计算)

@Override
public void resetWorkingScore(ScoreManager<ProjectJobSchedulingSolution> scoreManager) {
    super.resetWorkingScore(scoreManager);
    ProjectJobSchedulingSolution solution = scoreManager.getWorkingSolution();
    long soft1Score = 0;

    // 遍历所有Job,计算你需要的时间差(比如资源空闲时长、延迟交付的工作时长等)
    for (Job job : solution.getJobList()) {
        if (job.getPredecessor() != null && job.getStartTime() != null) {
            // 示例:计算当前Job与前序Job结束时间之间的资源空闲工作分钟数,作为惩罚项
            LocalDateTime predecessorEnd = job.getPredecessor().getEndTime();
            long idleMinutes = BusinessCalendarUtil.calculateWorkingMinutes(predecessorEnd, job.getStartTime(), job.getResource());
            // 假设空闲时间越长,soft分数越低(惩罚)
            soft1Score -= idleMinutes;
        }
    }

    setWorkingSoftScore(SoftScore.of(soft1Score));
}

增量更新阶段(处理规划变量变化)

当某个Job的startTime变化时,只更新受影响的分数,避免全量计算:

@Override
public void beforeVariableChanged(Object entity, String variableName) {
    if (entity instanceof Job job && "startTime".equals(variableName)) {
        // 移除旧值对应的分数影响
        if (job.getStartTime() != null && job.getPredecessor() != null) {
            LocalDateTime predecessorEnd = job.getPredecessor().getEndTime();
            long oldIdleMinutes = BusinessCalendarUtil.calculateWorkingMinutes(predecessorEnd, job.getStartTime(), job.getResource());
            addWorkingSoftScore(-oldIdleMinutes);
        }
        // 还要处理后续Job的影响,如果有的话
        for (Job successor : job.getSuccessorList()) {
            if (successor.getStartTime() != null) {
                long oldIdleMinutes = BusinessCalendarUtil.calculateWorkingMinutes(job.getEndTime(), successor.getStartTime(), successor.getResource());
                addWorkingSoftScore(-oldIdleMinutes);
            }
        }
    }
}

@Override
public void afterVariableChanged(Object entity, String variableName) {
    if (entity instanceof Job job && "startTime".equals(variableName)) {
        // 添加新值对应的分数影响
        if (job.getStartTime() != null && job.getPredecessor() != null) {
            LocalDateTime predecessorEnd = job.getPredecessor().getEndTime();
            long newIdleMinutes = BusinessCalendarUtil.calculateWorkingMinutes(predecessorEnd, job.getStartTime(), job.getResource());
            addWorkingSoftScore(-newIdleMinutes);
        }
        // 更新后续Job的分数影响
        for (Job successor : job.getSuccessorList()) {
            if (successor.getStartTime() != null) {
                long newIdleMinutes = BusinessCalendarUtil.calculateWorkingMinutes(job.getEndTime(), successor.getStartTime(), successor.getResource());
                addWorkingSoftScore(-newIdleMinutes);
            }
        }
    }
}

真实日期示例场景

假设我们有以下配置:

  • 资源ResourceA的工作时段是9:00-18:00,节假日包含2024-10-01(国庆节)
  • 前序Job结束时间:2024-09-30 17:30
  • 当前Job开始时间:2024-10-02 09:30

调用calculateWorkingMinutes计算的话:

  • 9月30日17:30到18:00:30分钟
  • 10月1日是节假日,跳过
  • 10月2日9:00到9:30:30分钟
  • 总有效工作分钟数是30+30=60分钟

对应的分数会扣除60分(假设空闲时间是惩罚项)。


关键注意事项

  1. 时区问题:LocalDateTime不带时区,如果你的业务涉及跨时区,建议改用ZonedDateTime,确保所有时间的时区一致。
  2. 增量计算的完整性:要考虑到Job的前后依赖关系,当一个Job的时间变化时,不仅要更新自身的分数影响,还要更新其后续Job的相关计算。
  3. 性能优化:如果业务日历计算频繁,可以缓存日期的工作状态(比如是否是工作日),避免重复计算。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.15 04:05:02