如何获取一天的开始时间和结束时间?

174

如何获取一天的开始时间和结束时间?

像这样的代码并不准确:

 private Date getStartOfDay(Date date) {
    Calendar calendar = Calendar.getInstance();
    int year = calendar.get(Calendar.YEAR);
    int month = calendar.get(Calendar.MONTH);
    int day = calendar.get(Calendar.DATE);
    calendar.set(year, month, day, 0, 0, 0);
    return calendar.getTime();
}

private Date getEndOfDay(Date date) {
    Calendar calendar = Calendar.getInstance();
    int year = calendar.get(Calendar.YEAR);
    int month = calendar.get(Calendar.MONTH);
    int day = calendar.get(Calendar.DATE);
    calendar.set(year, month, day, 23, 59, 59);
    return calendar.getTime();
}

精度不到毫秒级。


8
“不准确”是什么意思? - Kirk Woll
5
上面的代码甚至没有使用传递进方法的日期。在创建日历后应该执行:calendar.setTime(date)。 - Jerry Destremps
2
这个问题假设日期时间值的分辨率为毫秒。对于java.util.Date和Joda-Time来说是正确的。但对于Java 8中的java.time包(纳秒)、一些数据库(如Postgres)(微秒)、Unix库(整秒)和其他一些情况则不正确。请参见[我的答案](https://dev59.com/uWkv5IYBdhLWcg3w1kWr#20536041),使用半开区间更加安全。 - Basil Bourque
一天的开始完全准确到毫秒。对于一天的结束,为什么不只是使用TimeUnit.DAYS.toMiliseconds(1) - 1l来获取一天的结束呢?简单地说,就是“下一天的开始减去一”。很容易。 - astryk
2
实际上,您应该获取下一天的开头,并在迭代当天项目时(假设您想这样做),通过使用 < 而不是 <= 来指定上限。这将排除下一天,但包括前一天的所有内容,直到纳秒。 - Daniel F
显示剩余2条评论
17个回答

231

Java 8


public static Date atStartOfDay(Date date) {
    LocalDateTime localDateTime = dateToLocalDateTime(date);
    LocalDateTime startOfDay = localDateTime.with(LocalTime.MIN);
    return localDateTimeToDate(startOfDay);
}

public static Date atEndOfDay(Date date) {
    LocalDateTime localDateTime = dateToLocalDateTime(date);
    LocalDateTime endOfDay = localDateTime.with(LocalTime.MAX);
    return localDateTimeToDate(endOfDay);
}

private static LocalDateTime dateToLocalDateTime(Date date) {
    return LocalDateTime.ofInstant(date.toInstant(), ZoneId.systemDefault());
}

private static Date localDateTimeToDate(LocalDateTime localDateTime) {
    return Date.from(localDateTime.atZone(ZoneId.systemDefault()).toInstant());
}
更新: 我已经将这两个方法添加到我的Java工具类这里

它在Maven中央仓库中:

<dependency>
  <groupId>com.github.rkumsher</groupId>
  <artifactId>utils</artifactId>
  <version>1.3</version>
</dependency>

Java 7及更早版本


使用Apache Commons

public static Date atEndOfDay(Date date) {
    return DateUtils.addMilliseconds(DateUtils.ceiling(date, Calendar.DATE), -1);
}

public static Date atStartOfDay(Date date) {
    return DateUtils.truncate(date, Calendar.DATE);
}

不使用Apache Commons

public Date atEndOfDay(Date date) {
    Calendar calendar = Calendar.getInstance();
    calendar.setTime(date);
    calendar.set(Calendar.HOUR_OF_DAY, 23);
    calendar.set(Calendar.MINUTE, 59);
    calendar.set(Calendar.SECOND, 59);
    calendar.set(Calendar.MILLISECOND, 999);
    return calendar.getTime();
}

public Date atStartOfDay(Date date) {
    Calendar calendar = Calendar.getInstance();
    calendar.setTime(date);
    calendar.set(Calendar.HOUR_OF_DAY, 0);
    calendar.set(Calendar.MINUTE, 0);
    calendar.set(Calendar.SECOND, 0);
    calendar.set(Calendar.MILLISECOND, 0);
    return calendar.getTime();
}

5
我建议不要采用这种方法,因为你会遇到一些边缘情况问题,比如时区、夏令时等等,而像Joda这样的时间库可以为你解决这些问题。 - Edward Anderson
8
我认为在 getStartOfDay 中应该是:LocalDateTime startOfDay = localDateTime.with(LocalTime.MIN);,也就是说应该使用 LocalTime 而不是 LocalDateTime。 - Konstantin Kulagin
正如@KonstantinKulagin所说,答案必须更正为LocalDateTime startOfDay = localDateTime.with(LocalTime.MIN);否则会出现我上面发布的错误。 - sanluck
1
@GlenMazza Java 8的解决方案使用LocalTime.MAX,而不是LocalDateTime.MAX。该解决方案是正确的。 - Frederico Pantuzza
2
看起来LocalDateTime没有atStartOfDay方法。 - Eugene
显示剩余3条评论

173

简而言之

LocalDate                       // Represents an entire day, without time-of-day and without time zone.
.now(                           // Capture the current date.
    ZoneId.of( "Asia/Tokyo" )   // Returns a `ZoneId` object.
)                               // Returns a `LocalDate` object.
.atStartOfDay(                  // Determines the first moment of the day as seen on that date in that time zone. Not all days start at 00:00!
    ZoneId.of( "Asia/Tokyo" ) 
)                               // Returns a `ZonedDateTime` object.

开始一天

获取今天在特定时区中的完整长度。

使用半开放方法,其中开始时间是包容性的,而结束时间是排除性的。这种方法解决了代码中无法考虑到一天中最后一秒的缺陷。

ZoneId zoneId = ZoneId.of( "Africa/Tunis" ) ;
LocalDate today = LocalDate.now( zoneId  ) ;

ZonedDateTime zdtStart = today.atStartOfDay( zoneId ) ;
ZonedDateTime zdtStop = today.plusDays( 1 ).atStartOfDay( zoneId ) ;

zdtStart.toString() = 2020年1月30日00:00+01:00[非洲/突尼斯]

zdtStop.toString() = 2020年1月31日00:00+01:00[非洲/突尼斯]

在UTC中查看相同的时刻。

Instant start = zdtStart.toInstant() ;
Instant stop = zdtStop.toInstant() ;

start.toString() = 2020-01-29T23:00:00Z

stop.toString() = 2020-01-30T23:00:00Z

如果您想要以UTC格式获得日期的整天而不是时区,请使用OffsetDateTime

LocalDate today = LocalDate.now( ZoneOffset.UTC  ) ;

OffsetDateTime odtStart = today.atTime( OffsetTime.MIN ) ;
OffsetDateTime odtStop = today.plusDays( 1 ).atTime( OffsetTime.MIN ) ;

odtStart.toString() = 2020-01-30T00:00+18:00

odtStop.toString() = 2020-01-31T00:00+18:00

这些 OffsetDateTime 对象已经是UTC时间,但是如果您需要始终处于UTC时间的对象,可以调用 toInstant 方法。

Instant start = odtStart.toInstant() ;
Instant stop = odtStop.toInstant() ;

start.toString() = 2020-01-29T06:00:00Z

stop.toString() = 2020-01-30T06:00:00Z

提示:您可能需要将ThreeTen-Extra库添加到您的项目中,以使用其Interval类来表示这对Instant对象。该类提供了有用的比较方法,如abutsoverlapscontains等等。

Interval interval = Interval.of( start , stop ) ;

interval.toString() = 2020-01-29T06:00:00Z/2020-01-30T06:00:00Z

半开放

mprivat的答案是正确的。他的观点是不要试图获取一天的结束时间,而是比较“下一天开始之前”。他的想法被称为“半开放”方法,其中一段时间有一个包含开始时间不包括结束时间的部分。

  • Java的当前日期时间框架(java.util.Date/Calendar和Joda-Time)都使用自epoch以来的毫秒。但是在Java 8中,新的JSR 310 java.time.*类使用纳秒分辨率。如果切换到新类并基于强制一天中最后一刻的毫秒计数编写的任何代码都将不正确。
  • 如果其他来源的数据采用其他分辨率,则比较数据会出现故障。例如,Unix库通常使用整秒,并且像Postgres这样的数据库将日期时间解析为微秒。
  • 有些夏令时更改发生在午夜之后,这可能会进一步混淆事情。

enter image description here

Joda-Time 2.3提供了一个方法来获取一天中的第一个时刻:withTimeAtStartOfDay()。在java.time中,类似的方法是LocalDate::atStartOfDay

搜索StackOverflow以获取“joda半开放”以查看更多讨论和示例。

请参阅Bill Schneider的文章时间间隔和其他范围应该是半开放的

避免使用旧版日期时间类

java.util.Date和.Calendar类非常麻烦。请避免使用它们。

使用java.time类。 java.time框架是高度成功的Joda-Time库的官方继承者。

java.time

java.time框架内置于Java 8及更高版本中。在ThreeTen-Backport项目中向Java 6和7进行了后移,进一步适配了ThreeTenABP项目中的Android。

Instant是一个时间线上以UTC为基准的瞬间,分辨率为nanoseconds

Instant instant = Instant.now();

应用时区以获取某个地区的挂钟时间
ZoneId zoneId = ZoneId.of( "America/Montreal" );
ZonedDateTime zdt = ZonedDateTime.ofInstant( instant , zoneId );

要获取一天的第一个时刻,请使用LocalDate类及其atStartOfDay方法。

ZonedDateTime zdtStart = zdt.toLocalDate().atStartOfDay( zoneId );

使用半开区间方法,获取以下一天的第一个时刻。
ZonedDateTime zdtTomorrowStart = zdtStart.plusDays( 1 );

Table of all date-time types in Java, both modern and legacy

目前,java.time框架缺少像Joda-Time中描述的Interval类。然而,ThreeTen-Extra项目通过添加额外的类扩展了java.time。该项目是可能未来添加到java.time的试验场。其中一个类是Interval。通过传递一对Instant对象来构造Interval。我们可以从我们的ZonedDateTime对象中提取一个Instant

Interval today = Interval.of( zdtStart.toInstant() , zdtTomorrowStart.toInstant() );

关于java.time

java.time框架是Java 8及更高版本内置的。这些类取代了老旧的legacy日期时间类,如java.util.Date, Calendar, 和 SimpleDateFormat

要了解更多,请参阅Oracle教程。并在Stack Overflow上搜索许多示例和解释。规范是JSR 310

Joda-Time项目现在处于维护模式,建议迁移到java.time类。

你可以直接在数据库中使用 java.time 对象进行交换。使用符合 JDBC 4.2 或更高版本的 JDBC driver。无需字符串,也不需要 java.sql.* 类。Hibernate 5 和 JPA 2.2 支持 java.time
在哪里获取 java.time 类?

Joda-Time

更新:Joda-Time项目现在处于维护模式,并建议迁移到java.time类。我保留这个部分作为历史记录。

Joda-Time有三个类来以各种方式表示时间跨度:IntervalPeriodDuration。一个Interval在宇宙时间线上具有特定的开始和结束。 这符合我们表示“一天”的需求。

我们调用withTimeAtStartOfDay方法而不是将时间设置为零。由于夏令时和其他异常情况,一天的第一刻钟可能不是00:00:00

使用Joda-Time 2.3的示例代码。

DateTimeZone timeZone = DateTimeZone.forID( "America/Montreal" );
DateTime now = DateTime.now( timeZone );
DateTime todayStart = now.withTimeAtStartOfDay();
DateTime tomorrowStart = now.plusDays( 1 ).withTimeAtStartOfDay();
Interval today = new Interval( todayStart, tomorrowStart );

如果必须的话,你可以转换为java.util.Date。
java.util.Date date = todayStart.toDate();

我需要使用 LocalDateTime - user924
但是,如果我确实需要使用 java.time 获取一天中的最后一纳秒,该怎么办?我应该使用接受的答案、接受的答案中的评论(指出我们应该编写 .plusDays(1).atStartOfDay().minusNanos(1))还是其他什么方法? - Philippe Gioseffi
@PhilippeGioseffi 不需要知道最后一分钟/秒/纳秒。所需要知道的只是“以下一天的第一个时刻”。如果您想知道一个时刻是否在明天结束之前发生:myInstant.isBefore( LocalDate.now( zoneId ).plusDays( 2 ).atStartOfDay( zoneId ).toInstant() )。请注意,我们添加了两天,以获取超过明天的时间,到达后天的第一时刻。我建议您转变思路,采用Half-Open方法来处理日期和时间,这样处理日期和时间会更容易,且错误较少。 - Basil Bourque
@BasilBourque,我一直在阅读这个特定的主题,并且被说服你是正确的。除此之外,用户接受了解决方案,仅在一天的最后一秒钟使用。 - Philippe Gioseffi

32

在 getEndOfDay 方法中,你可以添加:

calendar.set(Calendar.MILLISECOND, 999);

虽然从数学上来说,你不能指定一天的结束时间,只能通过说明它在“下一天的开始之前”来表示。

因此,不要使用 if(date >= getStartOfDay(today) && date <= getEndOfDay(today)) 这样的语句,而应该使用 if(date >= getStartOfDay(today) && date < getStartOfDay(tomorrow))。这是一个更加严谨的定义(而且您无需担心毫秒精度)。


1
这个答案的后半部分是最好的方式。这种方法被称为“半开放”。我在我的回答中进一步解释。 - Basil Bourque

21

java.time

使用 Java 8 内置的 java.time 框架。

import java.time.LocalTime;
import java.time.LocalDateTime;

LocalDateTime now = LocalDateTime.now(); // 2015-11-19T19:42:19.224
// start of a day
now.with(LocalTime.MIN); // 2015-11-19T00:00
now.with(LocalTime.MIDNIGHT); // 2015-11-19T00:00
// end of a day
now.with(LocalTime.MAX); // 2015-11-19T23:59:59.999999999

4
这个答案忽略了异常情况,如夏令时(DST)。 Local …类没有时间区域的概念,也不会调整异常情况。例如,一些时区在午夜进行夏令时切换,因此一天中的第一个时刻是在01:00:00.0(上午1点)而不是此代码产生的00:00:00.0值。请改用ZonedDateTime - Basil Bourque

6

使用Java8中的java.time.ZonedDateTime寻找一天的开始时间的另一种方法,而不是通过LocalDateTime,只需将输入的ZonedDateTime截断到DAYS:

zonedDateTimeInstance.truncatedTo( ChronoUnit.DAYS );

5

#Java 8 或者 ThreeTenABP

###ZonedDateTime

ZonedDateTime curDate = ZonedDateTime.now();

public ZonedDateTime startOfDay() {
    return curDate
    .toLocalDate()
    .atStartOfDay()
    .atZone(curDate.getZone())
    .withEarlierOffsetAtOverlap();
}

public ZonedDateTime endOfDay() {

    ZonedDateTime startOfTomorrow =
        curDate
        .toLocalDate()
        .plusDays(1)
        .atStartOfDay()
        .atZone(curDate.getZone())
        .withEarlierOffsetAtOverlap();

    return startOfTomorrow.minusSeconds(1);
}

// based on https://dev59.com/XV4b5IYBdhLWcg3wbRIf#29145886

###本地日期时间

LocalDateTime curDate = LocalDateTime.now();

public LocalDateTime startOfDay() {
    return curDate.atStartOfDay();
}

public LocalDateTime endOfDay() {
    return curDate.atTime(LocalTime.MAX);  //23:59:59.999999999;
}

// based on https://dev59.com/_FoV5IYBdhLWcg3wTdXN#36408726

希望这对某人有所帮助。


“atTime”方法与常量“LocalTime.MAX”结合使用是非常好的处理方式,可以用于表示一天的结束。不错! - Mr. Anderson
我在Java 8的LocalDateTime中找不到atStartOfDay方法。 - fudy
@sudoplz 请描述一下 startOfTomorrow 函数? - Arnab Dutta
1
@ArnabDutta 我修改了代码,请看一下。 - SudoPlz

3

另一种不依赖于任何框架的解决方案是:

static public Date getStartOfADay(Date day) {
    final long oneDayInMillis = 24 * 60 * 60 * 1000;
    return new Date(day.getTime() / oneDayInMillis * oneDayInMillis);
}

static public Date getEndOfADay(Date day) {
    final long oneDayInMillis = 24 * 60 * 60 * 1000;
    return new Date((day.getTime() / oneDayInMillis + 1) * oneDayInMillis - 1);
}

请注意,它返回基于协调世界时的时间。

3
我尝试了这段代码,它运行良好!
final ZonedDateTime now = ZonedDateTime.now(ZoneOffset.UTC);
final ZonedDateTime startofDay =
    now.toLocalDate().atStartOfDay(ZoneOffset.UTC);
final ZonedDateTime endOfDay =
    now.toLocalDate().atTime(LocalTime.MAX).atZone(ZoneOffset.UTC);

如果您可以使用ZonedDateTimeInstant,就没有必要再使用java.sql.Timestamp。那个类是可怕的旧遗留类之一,现在已被java.time类所取代。从JDBC 4.2开始,我们可以直接与数据库交换java.time对象。您混合使用旧版和现代类对我来说毫无意义。 - Basil Bourque

2

对于Java 8,以下单行语句适用。在本例中,我使用UTC时区。请考虑更改您当前使用的时区。

System.out.println(new Date());

final LocalDateTime endOfDay       = LocalDateTime.of(LocalDate.now(), LocalTime.MAX);
final Date          endOfDayAsDate = Date.from(endOfDay.toInstant(ZoneOffset.UTC));

System.out.println(endOfDayAsDate);

final LocalDateTime startOfDay       = LocalDateTime.of(LocalDate.now(), LocalTime.MIN);
final Date          startOfDayAsDate = Date.from(startOfDay.toInstant(ZoneOffset.UTC));

System.out.println(startOfDayAsDate);

如果输出没有时间差,请尝试使用:ZoneOffset.ofHours(0)

1
这样做有什么超越现有答案的价值吗?而且,你忽略了时区这个关键问题。将UTC常量传递给toInstant是非法语法,从概念上讲也是多余的。 - Basil Bourque
@BasilBourque “这个有什么超越现有答案的价值?” 这就是stackoverflow的工作方式。顺便说一句,这只是一个模板。人们可以按照自己的方式进行更改。将UTC传递给toInstant是完全合法的,您可以检查输出结果。 - Fırat Küçük

2

针对您的时区TZ,最简短的答案如下:

LocalDateTime start = LocalDate.now(TZ).atStartOfDay()
LocalDateTime end = start.plusDays(1)

比较使用isAfter()isBefore()方法,或者使用toEpochSecond()toInstant()方法进行转换。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接