【问题标题】:Is this code realiable for working out hours in Java, considering summer time changes?考虑到夏季时间的变化,这段代码在 Java 中工作时间是否可行?
【发布时间】:2021-10-13 00:14:29
【问题描述】:

有人可以帮我解决这个困惑吗?我需要显示每个工人的总工作时间。有不同的工作轮班。在这种情况下,最复杂的一个是黎明班,当人们从一天的 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;

 (...)

我的 Db 架构如下所示:

(...)

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; 

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

【问题讨论】:

  • 在重要时始终调用 setTimeZone(),您部署的服务器的默认时区可能不使用瑞士时间。
  • 通常应该避免“数学”时间操作,这就是创建 JodaTime 的原因,我们现在拥有 java.time API
  • 我建议你不要使用SimpleDateFormatDate。这些类设计不良且过时,尤其是前者,尤其是出了名的麻烦。在数据库端使用timestamp with time zone。在实体中使用ZonedDateTime 是可以的,OffsetDateTime 是你必须让它工作。在一段时间内使用来自java.time, the modern Java date and time APIDuration 类。
  • 您的实体存在冗余。看到你可以避免这种情况。工作时间可以从开始到结束计算(如果我理解正确的话),所以不应该存储。

标签: java spring-boot date java-time datetimeoffset


【解决方案1】:

java.time

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

// 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 小时工作。所以总工作时间是 6 + 5 小时 = 11 小时。所以代码计算实际经过的时间(不是时钟小时之间的差异)。

我想进一步说明

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

你的代码可靠吗?

只对您提出的问题提供部分答案。 Java 1.0 和 1.1 中的旧日期和时间类不太可靠。即使你得到了与他们一起工作的代码,它通常也会是不必要的复杂代码,不应忽视某些程序员后来引入错误的风险。

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

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

链接

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

【讨论】:

  • 谢谢 Ole V.V.对于惊人而清晰的解释和干净的代码。我在那里学到了很多。在没有太多经验的情况下,仅阅读文档以了解不同类的组合是非常令人困惑的。问题:在 10 月 31 日夏季结束的情况下,这是否意味着工人在实际工作 10 小时时似乎多工作了 1 小时(11 小时)?
  • 完美!非常感谢。
猜你喜欢
  • 1970-01-01
  • 2012-07-03
  • 2018-09-19
  • 1970-01-01
  • 2013-09-18
  • 2021-01-21
  • 2021-03-27
  • 2011-10-29
  • 1970-01-01
相关资源
最近更新 更多