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分(假设空闲时间是惩罚项)。
关键注意事项
- 时区问题:
LocalDateTime不带时区,如果你的业务涉及跨时区,建议改用ZonedDateTime,确保所有时间的时区一致。 - 增量计算的完整性:要考虑到Job的前后依赖关系,当一个Job的时间变化时,不仅要更新自身的分数影响,还要更新其后续Job的相关计算。
- 性能优化:如果业务日历计算频繁,可以缓存日期的工作状态(比如是否是工作日),避免重复计算。
内容的提问来源于stack exchange,提问作者Ricardo Mendes
相关产品推荐
相关产品推荐

