在 java 中获取非 UTC 时区的月份开始时间和结束时间

Get month's start time and end time in non UTC timezone in java

提问人:Sanjay Prajapat 提问时间:10/3/2023 更新时间:10/3/2023 访问量:104

问:

我正在尝试获取特定时区中特定月份的开始和结束时间,特别是“亚洲/加尔各答”(UTC+05:30)。

例如,以下是 2023 年 9 月的开始和结束时间。

开始时间: 2023-09-01 00:00:00

结束时间: 2023-09-30 23:59:59

我在 Java 中实现了以下功能:

public static Long getStartTimeStampOfMonth(Month month, Year year) {
    LocalDate startDate = LocalDate.of(year.getValue(), month, 1);
    LocalDateTime startDateTime = LocalDateTime.of(startDate, LocalTime.MIN);
    ZonedDateTime istDateTime = startDateTime.atZone(ZoneId.of("Asia/Kolkata"));
    return Timestamp.valueOf(istDateTime.toLocalDateTime()).getTime();
}

public static Long getEndTimeStampOfMonth(Month month, Year year) {
    LocalDate startDate = LocalDate.of(year.getValue(), month, 1);
    LocalDate endDate = startDate.withDayOfMonth(startDate.lengthOfMonth());
    LocalDateTime endDateTime = LocalDateTime.of(endDate, LocalTime.MAX);
    ZonedDateTime istDateTime = endDateTime.atZone(ZoneId.of("Asia/Kolkata"));
    return Timestamp.valueOf(istDateTime.toLocalDateTime()).getTime();
}

但是,我注意到上面的代码根据计算机上配置的时区返回开始和结束时间。即使我明确指定了 ZoneId.of(“Asia/Kolkata”),它也不会产生预期的结果。

当我在设置为 UTC 时区的机器上测试 2023 年 9 月的代码时,我得到了以下输出:

开始时间: 1693526400000

结束时间:1696118399999

但是,无论计算机的时区如何,我都希望输出保持一致:

开始时间: 1693506600000

结束时间:1696098599999

请帮助我确定我可能遗漏的内容或建议替代方法。

Java Spring 日期 时间

评论

0赞 lane.maxwell 10/3/2023
我已经读了几遍,但仍然无法理解被问到什么,不是敲门声,我只是不明白。我对为什么应该考虑时区感到困惑,9 月从每个时区的 1 日 00:00:00 开始。
0赞 Matt Johnson-Pint 10/3/2023
@lane.maxwell - 他们正在计算一个时间戳,对应于一个月的第一个时刻和一个月的最后一个时刻。这些时间戳是基于 UTC 的,但日期和时间是给定时区的本地时间。
0赞 Basil Bourque 10/3/2023
@lane.麦克斯韦 时区很重要,因为东部的天数甚至数月比西部早。当新的一天到达日本东京时,在法国图卢兹仍然是“昨天”。同样,在图卢兹的新一天,在美国俄亥俄州托莱多仍然是“昨天”。
0赞 Basil Bourque 10/3/2023
@lane.麦克斯韦 关于时区的另一件事:日子并不总是从 00:00 开始。某些时区的某些日期从其他时间开始,例如 01:00。
0赞 Anonymous 10/3/2023
避免骇人听闻和早已过时的课程。在当时,它也仅用于与 SQL 数据库之间的时间戳。而是通过 转换。您的问题是由您调用 引起的。在某些 java.time 类名中,local 表示没有时区,因此在此调用中,您将时区 Asia/Kolkata 扔掉。TimestampInstanttoLocalDateTime()

答:

5赞 Matt Johnson-Pint 10/3/2023 #1

当您从 转换为 时,将引入本地时区:ZonedDateTimeTimestamp

return Timestamp.valueOf(istDateTime.toLocalDateTime()).getTime();

由于您正在经历 ,因此该方法将假定为本地时区。LocalDateTimeTimestamp.valueOf

文档中

public static Timestamp valueOf(LocalDateTime dateTime)

获取 来自对象的实例,其年、月、月日、小时、分钟、秒和纳时间值与提供的相同。TimestampLocalDateTimeLocalDateTime

提供的 LocalDateTime 被解释为本地时区的本地日期时间。

相反,您可以从以下位置创建:TimestampInstant

return Timestamp.from(istDateTime.toInstant()).getTime();

不过,由于您似乎以毫秒为单位,因此您可以完全跳过该类型:longTimestamp

return istDateTime.toInstant().toEpochMilli();

评论

2赞 Anonymous 10/3/2023
一个非常好的答案。由于老式不是最终目标,我无条件同意跳过该课程的黑客。java.sql.Timestamp
5赞 Basil Bourque 10/3/2023 #2

TL的;博士

YearMonth
.of( 2023 , Month.NOVEMBER )
.atDay( 1 ) 
.atStartOfDay( ZoneId.of( "Asia/Kolkata" ) ) 
.toInstant()
.toEpochMilli()

...和:

YearMonth
.of( 2023 , Month.NOVEMBER )
.plusMonths( 1 )
.atDay( 1 ) 
.atStartOfDay( ZoneId.of( "Asia/Kolkata" ) ) 
.toInstant()
.toEpochMilli()

2023 年 9 月

使用 java.time.YearMonth 类来表示月份。

YearMonth ym = YearMonth.of( 2023 , Month.NOVEMBER ) ;

特定月份在特定时区的开始和结束时间,特别是“亚洲/加尔各答”(UTC+05:30)。

首先获取日期。

LocalDate startDate = ym.atDay ( 1 );
LocalDate endDate = ym.atEndOfMonth ( );

获得第一天的第一刻。这样做需要一个时区。不要假设一天从 00:00 开始,因为某些区域中的某些日期可能会在另一个时间开始,例如 01:00。让 java.time 确定一天中的第一个时刻。

ZoneId z = ZoneId.of ( "Asia/Kolkata" );
ZonedDateTime startZdt = startDate.atStartOfDay ( z );

结束时间: 2023-09-30 23:59:59

您的示例不正确。如果你紧挨着你的月经,你将错过最后一天的最后一秒。

最好将你的时间跨度定义为半开,其中开始是包容的,而结束是排他性的。因此,一个月从第一天的第一刻开始,一直到下个月的第一刻,但不包括下个月的第一天。使用这种安排,您不会失去最后一秒。

ZonedDateTime endZdt = endDate.plusDays ( 1 ).atStartOfDay ( z );

显然,您希望将这两个时刻表示为自 UTC 1970-01-01T00:00Z 中所示的 1970 年第一个时刻的纪元引用以来的毫秒数。

你试图得到这个计数......

Timestamp.valueOf(istDateTime.toLocalDateTime()).getTime();

...在两个方面不正确:

  • 您调用了该调用,该调用丢失了任何时区或与 UTC 的偏移量的概念。使用时刻时不要使用对象。有关更多解释,请参阅 Johnson-Pint 的正确答案toLocalDateTimeLocalDateTime
  • 你使用了一个有严重缺陷的遗留类,该类在几年前被 JSR 310 中定义的现代 java.time 类所取代。避免使用传统类,而只使用 java.time 类。Timestamp

要将我们的两个时刻调整为 UTC 时间子午线的偏移量为 0 小时-分钟-秒,请提取对象。Instant

Instant startInstant = startZdt.toInstant ( );
Instant endInstant = endZdt.toInstant ( );

从那里你可以得到你的毫秒数。

long start = startInstant.toEpochMilli ( );
long end = endInstant.toEpochMilli ( );

我不建议你以这种方式表现时刻。毫秒数是不明确的,因为它只是一个没有上下文的整数,这会产生歧义。1970-01-01T00:00Z 只是数十个常见纪元参考之一。此外,人类无法破译纪元的计数。这意味着错误可能很容易被忽视。

我建议您使用标准 ISO 8601 格式以文本方式传达日期时间值。在UTC中,例如。将日期部分与时间部分分开。是 的缩写,偏移量为零。2023-10-02T20:53:59ZTZ+00:00

下面是一个完整的代码示例。

package work.basil.example;

import java.time.*;

public class MonthMoments
{
    public static void main ( String[] args )
    {
        YearMonth ym = YearMonth.of ( 2023 , Month.NOVEMBER );
        LocalDate startDate = ym.atDay ( 1 );
        LocalDate endDate = ym.atEndOfMonth ( );

        ZoneId z = ZoneId.of ( "Asia/Kolkata" );
        ZonedDateTime startZdt = startDate.atStartOfDay ( z );
        ZonedDateTime endZdt = endDate.plusDays ( 1 ).atStartOfDay ( z );

        Instant startInstant = startZdt.toInstant ( );
        Instant endInstant = endZdt.toInstant ( );

        long start = startInstant.toEpochMilli ( );
        long end = endInstant.toEpochMilli ( );

        System.out.println ( "startZdt = " + startZdt );
        System.out.println ( "endZdt = " + endZdt );

        System.out.println ( "startInstant = " + startInstant );
        System.out.println ( "endInstant = " + endInstant );

        System.out.println ( "start = " + start );
        System.out.println ( "end = " + end );
    }
}

运行时:

startZdt = 2023-11-01T00:00+05:30[Asia/Kolkata]
endZdt = 2023-12-01T00:00+05:30[Asia/Kolkata]
startInstant = 2023-10-31T18:30:00Z
endInstant = 2023-11-30T18:30:00Z
start = 1698777000000
end = 1701369000000

要处理成对的时刻,我建议将 ThreeTen-Extra 库添加到您的项目中。该库包括 Interval 类,用于将时间跨度表示为一对对象。该类提供了几种方便的方法,例如 、 和 。该类解析/生成标准 ISO 8601 格式的文本,其中两个时刻用斜杠分隔。Instantcontainsabutsintersection

Interval monthInterval = Interval.of( startInstant , endInstant ) ;

评论

0赞 Sanjay Prajapat 10/3/2023
谢谢.不知道这一点,这可以使实现更清晰。但我不明白你为什么要在最后一天包含第二天的 00:00:00 秒。我想把它保持到 23:59:59,因为那是一天结束的时候。YearMonth
1赞 Basil Bourque 10/3/2023
@SanjayPrajapat 不,这一天在第二天的第一刻结束。在第 59 秒和第二天的第一个时刻之间有无限秒。如果对数据库查询进行时间戳,则永远不会检索存储的值为 23:59:59.001、23:59:59.333、23:59:59.789 等的行。
0赞 Basil Bourque 10/3/2023
@SanjayPrajapat 一系列错误定义的月份将导致一系列差距,每个月之间会错过一千毫秒。
1赞 Sanjay Prajapat 10/3/2023
@user85421 谢谢,现在这是有道理的。感谢 @Basil Bourque 指出这一点。我的实现以结束时间戳的形式返回,这确实包括,但是是的,将其保留到下一秒并使条件独占是一个更好的主意。169609859999930 September 2023 23:59:59.999 GMT+05:3023:59:59.1
0赞 Basil Bourque 11/23/2023
@SanjayPrajapat 不是要把这一点放在地上,而是要进一步解释:你的结束时间戳将错过像 Postgres 这样使用微秒的数据库中的值。因此,您的查询不会找到 、 、 等行。解析为纳秒的 java.time 对象之间的查询将遗漏 、 、 等值。 使用半开方法可以巧妙地解决整个问题。30 September 2023 23:59:59.999 GMT+05:30… 23:59:59.999001… 23:59:59.999002… 23:59:59.999003… 23:59:59.999000001… 23:59:59.999000002… 23:59:59.999000003