如何在Java中将时间四舍五入到最接近的15分钟?

71

如何将当前时间(例如下午2:24)四舍五入到2:30?

同样,如果时间是下午2:17,如何将其四舍五入到2:15?

17个回答

102

四舍五入

您需要使用模运算截取每个15分钟:

Date whateverDateYouWant = new Date();
Calendar calendar = Calendar.getInstance();
calendar.setTime(whateverDateYouWant);

int unroundedMinutes = calendar.get(Calendar.MINUTE);
int mod = unroundedMinutes % 15;
calendar.add(Calendar.MINUTE, mod < 8 ? -mod : (15-mod));

正如EJP所指出的,如果日历是宽松的,这也是可行的(仅替换最后一行):

calendar.set(Calendar.MINUTE, unroundedMinutes + mod);

改进

如果你想更精确,你还需要截断较小的字段:

calendar.set(Calendar.SECOND, 0);
calendar.set(Calendar.MILLISECOND, 0);

你也可以使用Apache Commons / Lang中的DateUtils.truncate()方法来实现:

calendar = DateUtils.truncate(calendar, Calendar.MINUTE);

2
此方法将把分钟向下舍入到最接近的一刻钟,如果它被舍入到下一个小时,则需要添加一些检查以便同时更新小时。 - Ido Weinstein
1
当前版本修复了这些问题。 - Sean Patrick Floyd
@EJP 您是正确的,但我认为在语义层面上将时间设置为“10:60”是一件可怕的事情,即使我知道日历类足够聪明,可以将其转换为“11:00”。 - Sean Patrick Floyd
A. 日历实例从哪里来? B. 如何在其中获取特定日期? - B T
@BT 日历实例来自第一个列表,特定日期不在问题的范围内。 - Sean Patrick Floyd
显示剩余5条评论

49

如果您只想向下取整,这是使用 Java Time API 的更易读版本:

LocalDateTime time = LocalDateTime.now();
LocalDateTime lastQuarter = time.truncatedTo(ChronoUnit.HOURS)
                                .plusMinutes(15 * (time.getMinute() / 15));

输出:

2016年11月4日10时58分10.228秒

2016年11月4日10时45分00秒


4
顺便说一下,“LocalDateTime” 不是用于表示时间轴上的时刻的正确类。因为它缺少任何区域/偏移概念,所以它们是不确定的。可以使用相同的逻辑,但改用“ZonedDateTime.now()”。 - Basil Bourque
5
顺便提一下,如果你想要四舍五入到最近的0.5,可以使用以下公式:(15 * (time.getMinute() + 7.5) / 15)。 - Shengfeng Li
不是对问题的回答。2.24将变成2.15,但提问者希望2.24变成2.30。 - undefined

13

一份针对Java 8的注释实现。支持任意舍入单位和增量:

 public static ZonedDateTime round(ZonedDateTime input, TemporalField roundTo, int roundIncrement) {
    /* Extract the field being rounded. */
    int field = input.get(roundTo);

    /* Distance from previous floor. */
    int r = field % roundIncrement;

    /* Find floor and ceiling. Truncate values to base unit of field. */
    ZonedDateTime ceiling = 
        input.plus(roundIncrement - r, roundTo.getBaseUnit())
        .truncatedTo(roundTo.getBaseUnit());

    ZonedDateTime floor = 
        input.plus(-r, roundTo.getBaseUnit())
        .truncatedTo(roundTo.getBaseUnit());

    /*
     * Do a half-up rounding.
     * 
     * If (input - floor) < (ceiling - input) 
     * (i.e. floor is closer to input than ceiling)
     *  then return floor, otherwise return ceiling.
     */
    return Duration.between(floor, input).compareTo(Duration.between(input, ceiling)) < 0 ? floor : ceiling;
  }

来源:本人


1
语义清晰:变量和计算明确。半上取整的替代方案:return r < roundIncrement/2 ? floor : ceiling,其中 roundIncrement/2 是阈值,即“半”。 - hc_dev
我使用了您的答案,并将其改编为单独的方法来向上取整或向下取整输入,并发现了一个小错误。对于与我有相同目标的任何人: 在给定的答案中,“ceiling”总是被向上取整,即使不必要。我按照以下方式进行了修改: ceiling = input.equals(floor) ? input : ceiling - kistlers

10

使用上面的答案,你最终会得到各种有趣的代码来处理溢出到小时、天等。

我会使用自纪元以来的毫秒数。

加上7.5分钟或7.5x60x1000 = 450000

然后将其截断为900000的倍数。

new Date(900000 * ((date.getTime() + 450000) / 900000))

这种方法有效是因为毫秒时间开始的时间恰好是00:00:00。由于世界上所有时区都以15分钟为步长更改,因此这不会影响四舍五入到15分钟。

(糟糕,我多写了一个0并忘记了一些重要括号:现在还为时过早)


4
你是完全正确的,我没有考虑闰秒。但上次我检查时,java.util.Date没有支持闰秒,因此人们会设置他们的电脑时钟来匹配墙上的时间(或原子钟),而JVM计算的纪元起始点则会偏移(与闰秒数量有关)。因此在实践中,这种方法可以使用,并且可以避免一堆需要处理闰秒的棘手代码。结论:使用NTP来保持您的电脑时钟同步。 - Peter Tillemans
3
“世界上所有时区都以30分钟为间隔更改”这种说法并不适用于所有时区。实际上有一些时区使用15分钟为间隔。 - B T
根据这篇维基百科文章,所有时区偏移量都是60、45或30分钟。 - Basil Bourque
1
@BT 我并不是想要争辩什么。我只是在自学这个令人困惑的日期时间主题。你提供的链接指向了一篇有趣的文章,谢谢。 - Basil Bourque
1
我并不是那个意思,也不是有意要表达那种感觉。我确实忘记了导致我写原始评论的信息。 - B T
显示剩余3条评论

10

很简单,找到自1970年以来的季度数量作为双精度数,四舍五入并乘以15分钟:

long timeMs = System.System.currentTimeMillis();

long roundedtimeMs = Math.round( (double)( (double)timeMs/(double)(15*60*1000) ) ) * (15*60*1000);

使用此方法设置您的日期或日历对象。 将您想要的时间乘以“n”来更改。


2
括号不匹配。我认为代码应该是: long timeMs = System.currentTimeMillis(); long roundedtimeMs = Math.round( (double)timeMs/(15*60*1000) ) * (15*60*1000); - jgrocha

6

非常棒的文章,非常感谢大家!这正是我所需要的 :)

这是基于你们工作的我的代码。

我的用例是“假设现在是上午11:47,我想设置两个日期来表示当前的5分钟范围:上午11:45和上午11:50”

            Calendar calendar = Calendar.getInstance();
            calendar.set(Calendar.SECOND, 0);
            calendar.set(Calendar.MILLISECOND, 0);

            int modulo = calendar.get(Calendar.MINUTE) % 5;
            if(modulo > 0) {

                calendar.add(Calendar.MINUTE, -modulo);
            }

            myObject.setStartDate(calendar.getTime());

            calendar.add(Calendar.MINUTE, 5);
            myObject.setDueDate(calendar.getTime()); 

帮助我得到了整洁的代码@Iboix。进行了一些小调整,我得到了所需的日期时间对象。谢谢。 - parishodak

4
您可以使用这个简单的代码...
int mode = min % 15;
if (mode > 15 / 2) {
    min = 15 - mode;
} else {
    min = 0 - mode;
}
cal.add(Calendar.MINUTE, min);

1
感谢您提供的提示,让我明白了数字 8 是从哪里来的(15/2)。这帮助我实现了其他舍入方式的版本,例如 5 分钟等! - 5im
简洁优雅的代码片段。作为对之前解决方案的改进,但缺少上下文(mincal定义在哪里)和解释。 - hc_dev

4

如果您需要将时间向下舍入到提供的任意级别,可以使用 Duration

static long truncateTo(long timeEpochMillis, Duration d) {
    long x = timeEpochMillis / d.toMillis();
    return x * d.toMillis();
}

4

使用Java Instant API的另一种替代方法。

Instant instant =  Instant.now();
int intervalInMinutes = 10;
instant.truncatedTo(ChronoUnit.MINUTES).minus(instant.atZone(ZoneId.of("UTC")).getMinute() % (1* intervalInMinutes),ChronoUnit.MINUTES);

2

java.time

我建议您使用现代日期时间API*来完成这项任务:
import java.time.LocalDate;
import java.time.LocalTime;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.time.temporal.ChronoUnit;
import java.util.stream.Stream;

public class Main {
    public static void main(String[] args) {
        
        // Change it to the applicable ZoneId e.g. ZoneId.of("Asia/Kolkata")
        ZoneId zoneId = ZoneId.systemDefault();
        
        Stream.of(
                        "10:00", 
                        "10:05", 
                        "10:10", 
                        "10:15", 
                        "10:20", 
                        "10:25", 
                        "10:30"
            ).forEach(t -> System.out.println(roundToNearestQuarter(t, zoneId)));
    }

    static ZonedDateTime roundToNearestQuarter(String strTime, ZoneId zoneId) {
        LocalTime time = LocalTime.parse(strTime);
        return LocalDate.now()
                        .atTime(time)
                        .atZone(zoneId)
                        .truncatedTo(ChronoUnit.HOURS)
                        .plusMinutes(15 * Math.round(time.getMinute() / 15.0));
    }
}

输出:

2021-04-02T10:00+01:00[Europe/London]
2021-04-02T10:00+01:00[Europe/London]
2021-04-02T10:15+01:00[Europe/London]
2021-04-02T10:15+01:00[Europe/London]
2021-04-02T10:15+01:00[Europe/London]
2021-04-02T10:30+01:00[Europe/London]
2021-04-02T10:30+01:00[Europe/London]

如果您只是想获取时间,请使用ZonedDateTime#toLocalTime从获得的ZonedDateTime中获取LocalTime
Trail: Date Time了解更多关于现代日期时间API的信息。
* Java.util日期时间API及其格式化API——SimpleDateFormat已经过时且容易出错。建议完全停止使用它们并转换到现代日期时间API。如果由于某种原因,您必须坚持使用Java 6或Java 7,则可以使用ThreeTen-Backport将大多数java.time功能回溯到Java 6和7。如果您正在为Android项目工作,并且您的Android API级别仍不符合Java-8,请检查通过解糖可用的Java 8+ API如何在Android项目中使用ThreeTenABP

顺便提一下,这个忽略了秒数,因此由于“15''”之间的中位数是“7''30'''”,所以在每个刻钟的“7''0'''”和“7''29'''”之间四舍五入将不正确。 - dualed

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