考虑到夏令时的变化,此代码是否适用于 Java 的工作时间?

Is this code realiable for working out hours in Java, considering summer time changes?

有人可以帮我解决这个困惑吗?我需要显示每个工人的总工作时间。有不同的工作班次。在这种情况下,最复杂的是黎明班,当这个人从一天的 21:00 工作到第二天的 7:00 时,考虑到日期偏移,即在 10 月 31 日有一个时间瑞士的变化。我打算只在国内应用这个逻辑。以下代码是否是这种情况的可靠解决方案?我试图理解 java.time 和许多针对这个特定问题的帖子,但它们都变得更加混乱。

public String saveWorkTime(@Valid @ModelAttribute("workCalc") WorkCalculator workCalculator,
                               BindingResult bindingResult, ModelMap modelMap) throws ParseException {

        (...)

        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm");

        Date startDT = sdf.parse(workCalculator.getStartDate() + " " + workCalculator.getStartTime());
        Date endDT = sdf.parse(workCalculator.getEndDate() + " " + workCalculator.getEndTime());

        long timeDiffMilli = endDT.getTime() - startDT.getTime();
        long timeDiffMin = TimeUnit.MILLISECONDS.toMinutes(timeDiffMilli) % 60;
        long timeDiffH = TimeUnit.MILLISECONDS.toHours(timeDiffMilli) % 24;

        workCalculator.setTotalWorkH(timeDiffH);
        workCalculator.setTotalWorkMin(timeDiffMin);

        workCalculatorService.save(workCalculator);
        return "redirect:/index";
}

我的实体模型如下所示:

 (...)

 @Id
 @GeneratedValue(strategy = GenerationType.IDENTITY)
 @Column(name = "id")
 private Integer id;

 @Column(name = "start_date")
 private String startDate;

 @Column(name = "end_date")
 private String endDate;

 @Column(name = "total_work_h")
 private long totalWorkH;

 @Column(name = "total_work_min")
 private long totalWorkMin;

 @Column(name = "end_time")
 private String endTime;

 @Column(name = "start_time")
 private String startTime;

 (...)

我的数据库架构如下所示:

(...)

CREATE TABLE `work_calc` (
    `id` int(11) NOT NULL AUTO_INCREMENT,
    `start_date` varchar(30) DEFAULT NULL,
    `end_date` varchar(30) DEFAULT NULL,
    `start_time` varchar(20) DEFAULT NULL,
    `end_time` varchar(20) DEFAULT NULL,
    `total_work_h` int(10) DEFAULT NULL,
    `total_work_min` int(10) DEFAULT NULL,

    PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=latin1; 

提前感谢大家的努力和时间。

java.time

很简单。如果你愿意的话。我建议您使用 java.time,现代 Java 日期和时间 API,用于所有日期和时间工作。

// This should be your entity class
public class Work {
    
    private static final DateTimeFormatter FORMATTER
            = DateTimeFormatter.ofPattern("uuuu-MM-dd HH:mm", Locale.ROOT);
    
    private OffsetDateTime start;
    private OffsetDateTime end;
    
    public Work(OffsetDateTime start, OffsetDateTime end) {
        this.start = start;
        this.end = end;
    }

    public Duration getTotalWorkTime() {
        return Duration.between(start, end);
    }

    @Override
    public String toString() {
        return start.format(FORMATTER) + " - " + end.format(FORMATTER);
    }
    
}

我们来试试吧。为了构建一些 Work 对象,我编写了以下辅助方法。它接受日期和时间的字符串。

/** Switzerland time */
private static final ZoneId ZONE = ZoneId.of("Europe/Zurich");

private static Work buildWork(String startDate, String startTime,
        String endDate, String endTime) {
    ZonedDateTime start = ZonedDateTime.of(
            LocalDate.parse(startDate), LocalTime.parse(startTime), ZONE);
    ZonedDateTime end = ZonedDateTime.of(
            LocalDate.parse(endDate), LocalTime.parse(endTime), ZONE);
    
    if (start.isAfter(end)) {
        throw new IllegalArgumentException("Start must not be after end");
    }
    
    return new Work(start.toOffsetDateTime(), end.toOffsetDateTime());
}

示范:

    Work[] exampleEntities = {
            buildWork("2021-10-13", "07:00", "2021-10-14", "17:45"),
            buildWork("2021-10-15", "07:00", "2021-10-15", "21:00"),
            buildWork("2021-10-30", "21:00", "2021-10-31", "07:00")
    };

    for (Work work : exampleEntities) {
        Duration totalWork = work.getTotalWorkTime();
        System.out.format("%s: %-8s or %2d hours %2d minutes%n", work,
                totalWork, totalWork.toHours(), totalWork.toMinutesPart());
    }

输出:

2021-10-13 07:00 - 2021-10-14 17:45: PT34H45M or 34 hours 45 minutes
2021-10-15 07:00 - 2021-10-15 21:00: PT14H    or 14 hours  0 minutes
2021-10-30 21:00 - 2021-10-31 07:00: PT11H    or 11 hours  0 minutes

在正常的夜晚,我们预计从 21:00 到 7:00 的时间为 10 小时。您可以在上面的输出中看到,在 10 月 30 日至 31 日的晚上,欧洲在夏令时 (DST) 之后将时钟调回 11 小时。编辑:如果有疑问,可以通过以下方式查看:工人首先工作 21 到 3 = 6 小时。然后时钟调回 2。然后工人继续工作 2 到 7 = 5 小时。所以总工作时间 i 6 + 5 小时 = 11 小时。所以代码计算实际经过的时间(而不是时钟时间之间的差异)。

我想进一步说明

  1. 使用适当的日期时间对象来存储日期和时间。你不应该在字符串中存储数字和布尔值(我当然希望),你也不应该在字符串中存储日期和时间。我真的很想将 ZonedDateTime 用于带时区的日期和时间(如 Europe/Zurich),但许多 SQL 数据库无法存储 ZonedDateTime 的值实时时区,所以我们可能必须像上面的代码一样使用 OffsetDateTimeOffsetDateTime 非常适合常见 SQL 数据库的 timestamp with time zone 列,尽管供应商之间的差异很大。在一段时间内使用 Java 中的 Duration class(不是单独的数字)。
  2. 不要在您的实体中存储冗余信息。某些数据库更新导致未检测到的数据不一致的风险太大。总工作时间可以从开始和结束计算,所以我们只是按需计算它们(这可能不经常,但你知道得更多)。
  3. 正如 MadProgrammer 在评论中所说,留给标准库方法来执行日期和时间数学运算。它通常比你想象的要复杂,所以不要像在你自己的代码中那样手工完成。

你的代码可靠吗?

仅部分回答您的问题。 Java 1.0 和 1.1 中的旧日期和时间 classes 没有太多可靠的信息。即使你得到了与他们一起工作的代码,也往往是不必要的复杂代码,一些程序员后来引入错误的风险不容忽视。

我没有使用回退(例如 2021 年 10 月 30 日至 31 日)和 spring 前进(预计 2022 年 3 月 26 日至 27 日)之夜的数据测试您的代码。你可以自己做。

您的工作班次很长,从 2021-10-13 07:00 到 2021-10-14 17:45。可能是数据有误,但应该正确计算工作时间,这样也有助于用户发现是否确实有误。正确的持续时间是 34 小时 45 分钟。从您的屏幕截图看来,您的代码计算出的时间仅为 10 小时 45 分钟。

Link

Oracle tutorial: Date Time 解释如何使用 java.time.