将字符串日期转换为长值并执行计算的问题

Issues Converting String dates into Long values and performing calculations

我有一个字符串值映射,代表不同组件的停机时间。

dependencyMap.put ("sut", "14:26:12,14:27:19,00:01:07;15:01:54,15:02:54,00:01:00;15:44:30,15:46:30,00:02:00;16:10:30,16:11:30,00:01:00");
dependencyMap.put ("jms", "14:26:12,14:28:12,00:02:00;15:10:50,15:12:55,00:02:05;15:42:30,15:43:30,00:01:00;16:25:30,16:27:30,00:02:00");

字符串代表停机时间的开始、结束和持续时间。

(start)14:26:12,(end)14:27:19,(duration)00:01:07

我读取其中的值,然后将它们添加到 DependencyDownTime 个对象的列表中,这些对象包含 Long 个值 startTime、endTime 和 duration。

jArray.forEach (dependency ->{
            String downTimeValues = knownDowntimesMap.get(dependency);
            final String[] downtime = downTimeValues.split (";");
            for (final String str : downtime) {
                final DependencyDownTime depDownTime = new DependencyDownTime ();
                final String[] strings = str.split (",");
                if (strings.length == 3) {
                    final DateFormat dateFormat = new SimpleDateFormat ("HH:mm:ss");
                    try {
                        depDownTime.setStartTime(dateFormat.parse (strings[0]).getTime ());
                        depDownTime.setEndTime (dateFormat.parse (strings[1]).getTime ());
                        depDownTime.setDuration (dateFormat.parse (strings[2]).getTime ());
                        downTimes.add (depDownTime);
                    } catch (final ParseException e) {
                        //logger.warn (e.getMessage (), e);
                    }
                } else {
                    //logger.warn ("");
                }
            }

然后我对这些值执行简单的算术运算,计算出每个组件的总停机时间。

// sort the list by start time
        Collections.sort(downTimes, Comparator.comparing (DependencyDownTime::getStartTime));

        int i = 1;
        Long duration = 0L;
        for(DependencyDownTime dts: downTimes){
            Long curStart = dts.getStartTime ();
            Long curEnd = dts.getEndTime();

            Long nextStart = downTimes.get(i).getStartTime ();
            Long nextEnd = downTimes.get(i).getEndTime ();

            if(duration == 0){
                duration = dts.getDuration();
            }
            if(curStart.equals(nextStart) && curEnd < nextEnd){
                duration += (nextEnd - curEnd);
            }
            else if(nextStart > curEnd){
                duration += downTimes.get(i).getDuration();
            }
            else if( curStart < nextStart && curEnd > nextStart){
                duration += (nextEnd - curEnd);
            }
            else if(curEnd == nextStart){
                duration += downTimes.get(i).getDuration();
            }
            i++;
            if(i == downTimes.size ()){
                componentDTimeMap.put (application, duration);
                return;
            }

预期值应该类似于 1970-01-01T 00:14:35 .000+0100,只需几分钟。实际结果通常相差几小时非常高 1969-12-31T 15:13:35 .000+0100

我有两个问题。

  1. 我是否正确解析了这些值?

  2. 如果我的计算在加减长值时有点偏差。当我将值转换回日期格式时,预期值会有很大差异吗?

中所述,请不要误解这两个不同的概念:

  • a time of the day:表示一天中的特定时间点,如10 AM14:45:50
  • a duration:表示一个,如“1小时10分钟”或“2年3个月4天”。持续时间不会告诉您它何时开始或结束(“1 小时 10 分钟”相对于什么?),它不依附于年表,它不对应于时间轴中的特定点。这只是时间本身。

在您的输入中,您有:

(start)14:26:12,(end)14:27:19,(duration)00:01:07

startend代表一天中的duration代表的时间量SimpleDateFormat 旨在处理一天中的日期和时间,但不适用于持续时间。将持续时间视为一天中的某个时间可能会奏效,但如 .

中所述,这是一种 hack

另一个问题是,当SimpleDateFormat只解析一个时间时,它默认日期是JVM默认时区的January 1st 1970,导致所有奇怪的你看到的结果。不幸的是,没有办法避免这种情况,因为 java.util.Date 使用完整的时间戳。更好的选择是使用新的 date/time API.

一样,您使用的是 Java 8,我假设您也可以在这里使用它(但如果您使用的是 Java <= 7,您可以使用 ThreeTen Backport,这是 Java 8 的新 date/time 类 的一个很好的 backport。不同之处在于 包名称 (在 Java 8 中是 java.time,在 ThreeTen Backport(或 Android 的 ThreeTenABP)中是 org.threeten.bp ),但 类 和方法 names 是相同的)。

由于您只处理时间,因此无需考虑日期字段 (day/month/year),我们可以使用 LocalTime。你可以直接解析字符串,因为它们在 ISO861 compliant format:

LocalTime start = LocalTime.parse("14:26:12");
LocalTime end = LocalTime.parse("14:27:19");

很遗憾,一段时间内没有内置解析器,因此您必须手动解析它:

// parse the duration manually
String[] parts = "00:01:07".split(":");
Duration d = Duration
    // get hours
    .ofHours(Long.parseLong(parts[0]))
    // plus minutes
    .plusMinutes(Long.parseLong(parts[1]))
    // plus seconds
    .plusSeconds(Long.parseLong(parts[2]));

另一种方法是从输入中删除持续时间(或忽略它们)并使用开始和结束计算它:

Duration d = Duration.between(start, end);

两者都会为您提供 1 分 7 秒的持续时间。

我的建议是更改 DependencyDownTime 以将开始和结束存储为 LocalTime 对象,并将持续时间存储为 Duration 对象。有了这个,你的算法将是这样的:

Duration total = Duration.ZERO;
for (...) {
    LocalTime curStart = ...
    LocalTime curEnd = ...
    LocalTime nextStart = ...
    LocalTime nextEnd = ...

    if (total.toMillis() == 0) {
        duration = dts.getDuration();
    }
    if (curStart.equals(nextStart) && curEnd.isBefore(nextEnd)) {
        total = total.plus(Duration.between(curEnd, nextEnd));
    } else if (nextStart.isAfter(curEnd)) {
        total = total.plus(downTimes.get(i).getDuration());
    } else if (curStart.isBefore(nextStart) && curEnd.isAfter(nextStart)) {
        total = total.plus(Duration.between(curEnd, nextEnd));
    } else if (curEnd.equals(nextStart)) {
        total = total.plus(downTimes.get(i).getDuration());
    }
    i++;
    if (i == downTimes.size()) {
        // assuming you want the duration as a total of milliseconds
        componentDTimeMap.put(application, total.toMillis());
        return;
    }
}

您可以存储 Duration 对象,或相应的毫秒值。不要尝试将其转换为 Date,因为日期不是设计的,也不应该与持续时间一起使用。如果需要,您可以 adapt this code 格式化持续时间(不幸的是,持续时间没有本机格式化程序)。


限制

上面的代码假定所有 startend 时间都在同一天。但是,如果 start23:50end00:10,持续时间应该是 20 分钟吗?

如果是这样,那就有点棘手了,因为 LocalTime 不知道日期(所以它认为 23:50 > 00:10 并且它们之间的持续时间是 "minus 23 hours and 40 minutes")。

在这种情况下,你可以做一个技巧,假设日期都是当前日期,但是当 start 大于 end 时,这意味着 end 时间在第二天:

LocalTime start = LocalTime.parse("23:50");
LocalTime end = LocalTime.parse("00:10");
// calculate duration
Duration d;
if (start.isAfter(end)) {
    // start is after end, it means end is in the next day

    // current date
    LocalDate now = LocalDate.now();
    // start is at the current day
    LocalDateTime startDt = now.atTime(start);
    // end is at the next day
    LocalDateTime endDt = now.plusDays(1).atTime(end);
    d = Duration.between(startDt, endDt);
} else {
    // both start and end are in the same day
    // just calculate the duration in the usual way
    d = Duration.between(start, end);
}

在上面的代码中,结果将是 Duration 20 分钟。


不要将日期格式化为持续时间

这里有一些例子说明为什么 SimpleDateFormatDate 不适合处理持续时间。

假设我有 10 秒的持续时间。如果我尝试使用值 10 将其转换为 java.util.Date 日期(也就是将持续时间视为日期):

// a 10 second duration (10000 milliseconds), treated as a date
Date date = new Date(10 * 1000);
System.out.println(date);

这将得到一个对应于“unix 纪元后 10000 毫秒 (1970-01-01T00:00Z)”的日期,即 1970-01-01T00:00:10Z。但是当我打印日期对象时,toString() 方法被隐式调用(如 here 所解释)。并且此方法将此毫秒值转换为 JVM 默认时区。

在我使用的 JVM 中,默认时区是 America/Sao_Paulo,所以上面的代码输出:

Wed Dec 31 21:00:10 BRT 1969

这不是预期的:UTC 时刻 1970-01-01T00:00:10Z 对应圣保罗时区 12 月 31st 1969 年晚上 9 点。

发生这种情况是因为我错误地将持续时间视为日期(并且输出会有所不同,具体取决于 JVM 中配置的默认时区)。

A java.util.Date 不能(不得)用于持续时间。实际上,既然我们有了这个 better API's, it should be avoided whenever possible. There are too many problems and design issues,如果可以就不要使用它。


如果您将持续时间作为日期处理,

SimpleDateFormat 也将无法正常工作。在这段代码中:

SimpleDateFormat dateFormat = new SimpleDateFormat("HH:mm:ss");
Date d = dateFormat.parse("10:00:00");

输入只有时间字段(小时、分钟和秒),因此 SimpleDateFormat 将日期设置为 JVM 默认时区的 January 1st 1970。如果我 System.out.println 这个日期,结果将是:

Thu Jan 01 10:00:00 BRT 1970

那是圣保罗时区 1970 年 1 月 1 日st 上午 10 点,在 UTC 中相当于 1970-01-01T13:00:00Z - 所以 d.getTime() returns 46800000.

如果我将 JVM 默认时区更改为 Europe/London,它将创建一个对应于 1970 年 1 月 1 日 st 伦敦上午 10 点(或 UTC 1970-01-01T09:00:00Z) - 和 d.getTime() 现在 returns 32400000(因为伦敦上午 10 点和圣保罗上午 10 点发生在不同的时刻)。

SimpleDateFormat 不是处理持续时间的正确工具 - it isn't even the best tool to work with dates,实际上。