两个日期之间的时间偏移

Time offset between two dates

我有两个时间戳和一个本地化日期,用于查找时区偏移并将其添加到这些日期。如何更简单地计算日期之间的时间偏移量?我的方法不适用于负值(if (tsOffset.toSecondOfDay() > 0 总是 true)。

fun parseDateTime(startTs: Long, endTs: Long, localizedDateTime: String): Pair<String, String> {
    val dateUtcStart = LocalDateTime.ofInstant(Instant.ofEpochMilli(startTs), ZoneOffset.UTC)
    val dateUtcEnd = LocalDateTime.ofInstant(Instant.ofEpochMilli(endTs), ZoneOffset.UTC)

    val formatter = DateTimeFormatterBuilder()
        .parseCaseInsensitive()
        .parseDefaulting(ChronoField.YEAR_OF_ERA, dateUtcStart.year.toLong())
        .append(DateTimeFormatter.ofPattern("E dd MMM hh:mm a"))
        .toFormatter(Locale.ENGLISH)

    val localDateTime = LocalDateTime.parse(localizedDateTime, formatter)

    val localTs = Timestamp.valueOf(localDateTime).time
    val tsOffset = LocalTime.ofInstant(Instant.ofEpochMilli(localTs - startTs), ZoneOffset.systemDefault())
    val tzString = if (tsOffset.toSecondOfDay() > 0) "+$tsOffset" else tsOffset.toString()

    val startDate = dateUtcStart.toString() + tzString
    val endDate = dateUtcEnd.toString() + tzString

    return Pair(startDate, endDate)
}

@Test
fun parseDateTime() {
    val pair1 = parseDateTime(1626998400000, 1627005600000, "Fri 23 Jul 10:30 am")
    val pair2 = parseDateTime(1626998400000, 1627005600000, "Thu 22 Jul 11:30 pm")

    // pass
    assertEquals("2021-07-23T00:00+10:30", pair1.first)
    assertEquals("2021-07-23T02:00+10:30", pair1.second)
    // fails
    assertEquals("2021-07-23T00:00-00:30", pair2.first)
    assertEquals("2021-07-23T02:00-00:30", pair2.second)
}

我也试过了

val dur = Duration.between(dateUtcStart, localDateTime)

但不确定如何将其转换为字符串或正确添加到日期。

此处 startTs - 事件时间戳的开始。 endTs - 此事件时间戳结束。 localizedDateTime 用于显示实际时区(真实城市)的开始时间,而时间戳则显示 UTC 时间。我需要从 localizedDateTime 中提取这个时区并将其添加到开始和结束字符串 dateTimes(start = "2021-07-23T00:00+10:30", end = "2021-07-23T02:00+10:30" 相应地 startTs = 1626998400000endTs = 1627005600000)。

我在 Kotlin forum 上得到了答案:

    val duration = Duration.between(dateUtcStart, localDateTime)
    val offset = ZoneOffset.ofTotalSeconds(duration.seconds.toInt())
    val startDate = dateUtcStart.atOffset(offset).toString()
    val endDate = dateUtcEnd.atOffset(offset).toString()

相反,它并不比您的代码简单,但它修复了您遇到的几个问题。

免责声明:我没有你的 Pair class,我也不会写 运行 Kotlin。因此,我正在打印方法中的结果字符串,您必须根据自己的目的进行更改。你将不得不手翻译我的 Java.

private static void parseDateTime(long startTs, long endTs, String localizedDateTime) {
    // startTs and localizedDateTime are both representations of the event start time.
    // Use this information to obtain the UTC offset of the local time.
    
    Instant startInstant = Instant.ofEpochMilli(startTs);
    OffsetDateTime startUtc = startInstant.atOffset(ZoneOffset.UTC);
    
    // Corner case: the local year may be different from the UTC year if the event is close to New Year.
    // Check whether this is the case. First get the UTC month and the local month.
    Month utcMonth = startUtc.getMonth();

    DateTimeFormatter baseSyntaxFormatter = new DateTimeFormatterBuilder()
            .parseCaseInsensitive()
            .append(DateTimeFormatter.ofPattern("E dd MMM hh:mm a"))
            .toFormatter(Locale.ENGLISH);
    Month localMonth = baseSyntaxFormatter.parse(localizedDateTime, Month::from);
    
    int utcYear = startUtc.getYear();
    int localYear;
    if (utcMonth.equals(Month.DECEMBER) && localMonth.equals(Month.JANUARY)) {
        // Local date is in the following year
        localYear = utcYear + 1;
    } else if (utcMonth.equals(Month.JANUARY) && localMonth.equals(Month.DECEMBER)) {
        localYear = utcYear - 1;
    } else {
        localYear = utcYear;
    }
    
    DateTimeFormatter finalFormatter = new DateTimeFormatterBuilder()
            .append(baseSyntaxFormatter)
            .parseDefaulting(ChronoField.YEAR_OF_ERA, localYear)
            .toFormatter(Locale.ENGLISH);

    LocalDateTime startLocal = LocalDateTime.parse(localizedDateTime, finalFormatter);
    // Now calculate offset
    Duration durationOfOffset = Duration.between(startUtc.toLocalDateTime(), startLocal);
    ZoneOffset offset = ZoneOffset.ofTotalSeconds(Math.toIntExact(durationOfOffset.getSeconds()));

    String startString = startLocal.atOffset(offset).toString();
    
    // Calculate end date and time
    String endString = Instant.ofEpochMilli(endTs)
            .atOffset(offset)
            .toString();

    System.out.format("%s - %s%n", startString, endString);
}

让我们用您的示例数据试试看:

    parseDateTime(1_626_998_400_000L, 1_627_005_600_000L, "Fri 23 Jul 10:30 am");
    parseDateTime(1_626_998_400_000L, 1_627_005_600_000L, "Thu 22 Jul 11:30 pm");

输出:

2021-07-23T10:30+10:30 - 2021-07-23T12:30+10:30
2021-07-22T23:30-00:30 - 2021-07-23T01:30-00:30

您注意到本地开始时间现在是 10:30 和 23:30,与您输入的字符串一样,我相信这更正了您的错误。

我们也来举个桥接新年的例子:

    Instant start = Instant.parse("2021-01-01T00:00:00Z");
    parseDateTime(start.toEpochMilli(),
            start.plus(2, ChronoUnit.HOURS).toEpochMilli(),
            "Thu 31 Dec 10:30 pm");
2020-12-31T22:30-01:30 - 2021-01-01T00:30-01:30

我使用了与您在自己的答案中提供的基本相同的方法来计算偏移量。

您的代码有问题

首先,正如我所说,您的单元测试断言的时间与您输入的时间不一致。对于所需的输出,您采用了一天中的 UTC 时间并结合了本地偏移量,这给出了不同的时间点。 UTC 偏移量始终与该偏移量的一天中的时间一起使用(对于一天中的 UTC 时间,可以使用偏移量 Z+00:00)。

考虑让您的方法 return 成为 Pair<OffsetDateTime, OffsetDateTime>,而不是 Pair<String, String>。字符串用于呈现给用户,有时用于数据交换。在您的程序中,您应该使用正确的日期时间对象,而不是字符串。

正如我在评论中所说,新年不会在所有时区同时发生。因此,您从 dateUtcStart.year.toLong() 获得的年份不必是来自不同时区的字符串中假定的年份。我的代码考虑到了这一点。

不涉及Timestampclass。它设计不佳且早已过时。它给你的只是一个额外的转换,因此更加复杂。

您的基本问题是使用 LocalTime 持续时间可能是正数或负数。 LocalTime 是一天中的某个时间,而不是一段时间。您自己的解决方案已经解决了这个问题。

这是错误的:

    val startDate = dateUtcStart.toString() + tzString

首先,您不应该对日期和时间数学运算使用字符串操作。您正在使用的来自 java.time 的 classes 可以更轻松地生成您需要的字符串,并且出错的风险更小。其次,您确实在此处遇到错误:您将计算出的偏移量附加到 UTC 格式的字符串,即假定偏移量为 0(或 Z)。这就是为什么您实际上能够产生单元测试预期的错误结果的原因。