将NSDate舍入到最近的5分钟

65

例如我有

NSDate *curDate = [NSDate date];

它的值为上午9:13。我没有使用curDate的年、月和日部分。

我想要获取时间值为9:15的日期;如果我有时间值为9:16,我想将其提前到9:20等等。

我该如何使用NSDate实现这个目标?


你可以作弊,获取timeIntervalSince...并四舍五入。由于所有(有理数)时区至少在5分钟边界上对齐,因此即使这会让纯粹主义者感到不适,它也能正常工作。(尤其是如果这让纯粹主义者感到不适的话。;) - Hot Licks
@HotLicks 我怀疑如果考虑到闰秒之类的因素,那么这种方法可能行不通。壁钟时间并不像线性和可预测的那样改变。 - Mecki
@Mecki - NSDate不考虑闰秒。 - Hot Licks
NSDate本身并不会四舍五入,但现实世界中需要这样做。当您使用NSCalendar将NSDate转换为可打印日期字符串时,“四舍五入”的五分钟时间可能并不完全是5分钟的倍数,而是相差几秒钟。NSDate只是一个围绕参考日期的秒数整数的封装器,但它与日历和挂钟上的实际日期/时间并不相同。简单的四舍五入永远准确到分钟,但我不确定它是否总是准确到秒。 - Mecki
@HotLicks 但在这种情况下,您可能是正确的:由于Cocoa根据NTP标准而不是UTC标准处理闰秒(Apple的文档明确说明了这一点!),因此闰秒仅在计算“过去日期”时起作用。由于只有未来日期相关(四舍五入),因此闰秒目前可能不会影响向上舍入。请参见http://www.eecis.udel.edu/~mills/leap.html - “3.如何使用NTP和POSIX计算闰秒”的第三段。请注意,如果在构建GlibC时启用了它,则Linux确实具有真正的闰秒支持。 - Mecki
我在寻找类似的东西,但花了15分钟。最终我选择了这个人的方案:http://bdunagan.com/2010/09/24/cocoa-tip-nsdate-to-the-nearest-15-minutes/ - NYC Tech Engineer
19个回答

63

这是我的解决方案:

NSTimeInterval seconds = round([date timeIntervalSinceReferenceDate]/300.0)*300.0;
NSDate *rounded = [NSDate dateWithTimeIntervalSinceReferenceDate:seconds];

我进行了一些测试,它比Voss的解决方案快大约10倍。用1百万次迭代,大约需要3.39秒。这个解决方案只需0.38秒就能执行完。J3RM的解决方案则需要0.50秒。它的内存使用量也应该是最低的。

并不是性能就是一切,但这个解决方案只需要一行代码。而且你可以通过除法和乘法轻松控制舍入。

编辑:为了回答问题,你可以使用 ceil 来正确地向上舍入:

NSTimeInterval seconds = ceil([date timeIntervalSinceReferenceDate]/300.0)*300.0;
NSDate *rounded = [NSDate dateWithTimeIntervalSinceReferenceDate:seconds];

编辑:一段用Swift编写的扩展:

public extension Date {

    public func round(precision: TimeInterval) -> Date {
        return round(precision: precision, rule: .toNearestOrAwayFromZero)
    }

    public func ceil(precision: TimeInterval) -> Date {
        return round(precision: precision, rule: .up)
    }

    public func floor(precision: TimeInterval) -> Date {
        return round(precision: precision, rule: .down)
    }

    private func round(precision: TimeInterval, rule: FloatingPointRoundingRule) -> Date {
        let seconds = (self.timeIntervalSinceReferenceDate / precision).rounded(rule) *  precision;
        return Date(timeIntervalSinceReferenceDate: seconds)
    }
}

先加上299秒。 - Hot Licks
2
@vikingosegundo 你可以使用 ceil 而不是 round - mkko
2
这很好 - 也可以与UIDatePickerminuteInterval属性一起使用。 - Evan R
1
300.0 是从哪里来的?60(秒/分钟)* 5(分钟)= 300(秒)吗? - Tommy K
1
请注意,这将丢失时区并计算以GMT(UTC)为基础的日期舍入,与日历结果不同。 - Grzegorz Krukowski
显示剩余2条评论

55

获取分钟值,将其除以5并向上舍入以得到下一个最高的5分钟单位,乘以5以将其转换回分钟,并构建一个新的NSDate对象。

NSDateComponents *time = [[NSCalendar currentCalendar]
                          components:NSHourCalendarUnit | NSMinuteCalendarUnit
                            fromDate:curDate];
NSInteger minutes = [time minute];
float minuteUnit = ceil((float) minutes / 5.0);
minutes = minuteUnit * 5.0;
[time setMinute: minutes];
curDate = [[NSCalendar currentCalendar] dateFromComponents:time];

4
关于舍入本身的另一个选项是:remainder = minutes % 5; 如果(余数),则分钟数加上5减去余数。 - smorgan
4
可能是iOS 5.x的问题,但这个功能绝对不能正确运行。'curDate'给出了一个日期为'0001-01-01'加上时间。 - mpemburn
1
为什么这个答案被接受了?它是错误的。复制/粘贴这段代码并不能得到所请求的结果。 - Mecki
2
@Sulthan 它依赖于currentCalendar是一个公历,但这可能不是情况(用户可以在系统偏好中配置),因此组件拆分可能无法按预期工作。此外,它没有获取年、月和日到组件中,因此创建的日期不是未来最近的5分钟的日期,而是十多年前的日期(NSDate不是时间,它是一个时间点,您不能使用NSDate来表示忽略当前日期的某个时间)。不能使用结果安排计时器在下一个5分钟倍数上运行。 - Mecki
1
@Sulthan 这只是因为您告诉格式化程序忽略了日期部分。但原帖作者在哪里声明他想在UI中打印时间呢?他说他想“将NSDate四舍五入到最近的5分钟”,可能是因为他想在那个日期安排一个事件(例如更新UI,从服务器拉取数据,触发警报事件)。在上面的代码中,他会将其安排在10年前。当我说将当前日期(2014-05-15 16:56)四舍五入到最近的5分钟时,我不希望您将其四舍五入到0001-01-01 17:00,可能没有人会期望这样!意外的行为是错误的。 - Mecki
显示剩余5条评论

43

基于Chris和Swift3,这个怎么样?

import UIKit

enum DateRoundingType {
    case round
    case ceil
    case floor
}

extension Date {
    func rounded(minutes: TimeInterval, rounding: DateRoundingType = .round) -> Date {
        return rounded(seconds: minutes * 60, rounding: rounding)
    }
    func rounded(seconds: TimeInterval, rounding: DateRoundingType = .round) -> Date {
        var roundedInterval: TimeInterval = 0
        switch rounding  {
        case .round:
            roundedInterval = (timeIntervalSinceReferenceDate / seconds).rounded() * seconds
        case .ceil:
            roundedInterval = ceil(timeIntervalSinceReferenceDate / seconds) * seconds
        case .floor:
            roundedInterval = floor(timeIntervalSinceReferenceDate / seconds) * seconds
        }
        return Date(timeIntervalSinceReferenceDate: roundedInterval)
    }
}

// Example

let nextFiveMinuteIntervalDate = Date().rounded(minutes: 5, rounding: .ceil)
print(nextFiveMinuteIntervalDate)

3
这是我对这个问题最好且最新的解决方案的投票。简洁且通用! - timgcarlson
完美运行。 - Andreas Kraft
非常棒 :) 非常整洁 - N.Raval

34

哇塞,我在这里看到很多答案,但许多答案太长或难以理解,所以我尝试提供我的意见,希望能有所帮助。 NSCalendar 类以安全而简洁的方式提供所需的功能。以下是我使用的可行解决方案,无需乘以时间间隔秒数、四舍五入或任何其他处理。 NSCalendar 考虑了闰年/闰日和其他时间和日期问题。(Swift 2.2)

let calendar = NSCalendar.currentCalendar()
let rightNow = NSDate()
let interval = 15
let nextDiff = interval - calendar.component(.Minute, fromDate: rightNow) % interval
let nextDate = calendar.dateByAddingUnit(.Minute, value: nextDiff, toDate: rightNow, options: []) ?? NSDate()

如果需要的话,可以将其添加到NSDate扩展中,或作为自由形式的函数返回一个新的NSDate实例,具体取决于您的需求。希望这可以帮助需要它的人。

Swift 3更新

let calendar = Calendar.current  
let rightNow = Date()  
let interval = 15  
let nextDiff = interval - calendar.component(.minute, from: rightNow) % interval  
let nextDate = calendar.date(byAdding: .minute, value: nextDiff, to: rightNow) ?? Date()

4
是的,我刚刚查看了所有代码 - 这是最简单、最好的代码,并且有效。 :) - legel
1
顶级分数!非常高效地解决了问题!感谢您的发布!Swift 3代码片段运行得非常好! - H2wk
1
很棒的解决方案,唯一的缺点是它不能处理与时间间隔相符的时间实例。如果您有一个15分钟的时间间隔,并传递给它10:30AM,它将返回10:45AM,而理想情况下应该返回10:30AM,因为它已经四舍五入了。 - William T.
1
@WilliamT。是的,你是正确的。 应该是: 如果nextDiff!= interval { 让nextDate =日历。date(byAdding: .minute, value: nextDiff, to: rightNow) ?? Date() } - Mehul Sojitra

14

13

我认为这是最好的解决方案,但这只是基于前面回答者的代码而言的个人意见。将时间四舍五入到最接近的5分钟标记。这段代码应该比日期组件解决方案使用更少的内存。太棒了,感谢指导。

+(NSDate *) dateRoundedDownTo5Minutes:(NSDate *)dt{
    int referenceTimeInterval = (int)[dt timeIntervalSinceReferenceDate];
    int remainingSeconds = referenceTimeInterval % 300;
    int timeRoundedTo5Minutes = referenceTimeInterval - remainingSeconds; 
    if(remainingSeconds>150)
    {/// round up
         timeRoundedTo5Minutes = referenceTimeInterval +(300-remainingSeconds);            
    }
    NSDate *roundedDate = [NSDate dateWithTimeIntervalSinceReferenceDate:(NSTimeInterval)timeRoundedTo5Minutes];
    return roundedDate;
}

+1 - 看到性能比较会很有趣,但这绝对是最有效率的解决方案。其他解决方案至少需要一个新对象,我认为在这种低级实用程序中总是不好的。 - mkko
除非他只想要四舍五入,就像我读到的那样。只需添加299秒并截断即可。 - Hot Licks

6

感谢提供示例。下面我添加了一些代码,用于将时间四舍五入到最接近的5分钟。

 -(NSDate *)roundDateTo5Minutes:(NSDate *)mydate{
    // Get the nearest 5 minute block
    NSDateComponents *time = [[NSCalendar currentCalendar]
                              components:NSHourCalendarUnit | NSMinuteCalendarUnit
                              fromDate:mydate];
    NSInteger minutes = [time minute];
    int remain = minutes % 5;
    // if less then 3 then round down
    if (remain<3){
        // Subtract the remainder of time to the date to round it down evenly
        mydate = [mydate addTimeInterval:-60*(remain)];
    }else{
        // Add the remainder of time to the date to round it up evenly
        mydate = [mydate addTimeInterval:60*(5-remain)];
    }
    return mydate;
}

你不需要跳过那么多的步骤来进行四舍五入。只需将4:59加上并截断即可。 - Hot Licks

6

很不幸,这里的大多数回复并不完全正确(尽管它们似乎对大多数用户都非常有效),因为它们要么依赖于当前活动系统日历是公历(这可能并非如此),要么基于闰秒不存在和/或将始终被OS X和iOS忽略的事实。以下代码可以正常复制粘贴,保证是正确的,并且不做任何假设(因此,即使Apple更改闰秒支持的情况下,它也不会在未来出现故障,因为NSCalendar也必须正确支持它们):

{
    NSDate * date;
    NSUInteger units;
    NSCalendar * cal;
    NSInteger minutes;
    NSDateComponents * comp;

    // Get current date
    date = [NSDate date];

    // Don't rely that `currentCalendar` is a
    // Gregorian calendar that works the way we are used to.
    cal = [[NSCalendar alloc]
        initWithCalendarIdentifier:NSGregorianCalendar
    ];
    [cal autorelease]; // Delete that line if using ARC

    // Units for the day
    units = NSYearCalendarUnit | NSMonthCalendarUnit | NSDayCalendarUnit;
    // Units for the time (seconds are irrelevant)
    units |= NSHourCalendarUnit | NSMinuteCalendarUnit;

    // Split current date into components
    comp = [cal components:units fromDate:date];

    // Get the minutes,
    // will be a number between 0 and 59.
    minutes = [comp minute];
    // Unless it is a multiple of 5...
    if (minutes % 5) {
        // ... round up to the nearest multiple of 5.
        minutes = ((minutes / 5) + 1) * 5;
    }

    // Set minutes again.
    // Minutes may now be a value between 0 and 60,
    // but don't worry, NSCalendar knows how to treat overflows!
    [comp setMinute:minutes];

    // Convert back to date
    date = [cal dateFromComponents:comp];
}

如果当前时间已经是5分钟的倍数,代码将不会更改它。原始问题没有明确指定这种情况。如果代码应该总是向上舍入到下一个5分钟的倍数,则只需删除测试if (minutes%5) {,它将始终向上舍入。


5

ipje的回答在接下来的5分钟内解决了问题,但我需要更加灵活的解决方案,并且想要摆脱所有魔法数字。

幸运的是,我在类似问题的答案中找到了一个解决方案。

我的解决方案使用Swift 5.2和Measurement避免使用魔法数字:

extension UnitDuration {
    var upperUnit: Calendar.Component? {
        if self == .nanoseconds {
            return .second
        }

        if self == .seconds {
            return .minute
        }
        if self == .minutes {
            return .hour
        }
        if self == .hours {
            return .day
        }
        return nil
    }
}
extension Date {
    func roundDate(to value: Int, in unit: UnitDuration, using rule: FloatingPointRoundingRule, and calendar: Calendar = Calendar.current) -> Date? {
        guard unit != .picoseconds && unit != .nanoseconds,
            let upperUnit = unit.upperUnit else { return nil }
        let value = Double(value)
        let unitMeasurement = Measurement(value: value, unit: unit)
        let interval = unitMeasurement.converted(to: .seconds).value

        let startOfPeriod = calendar.dateInterval(of: upperUnit, for: self)!.start
        var seconds = self.timeIntervalSince(startOfPeriod)
        seconds = (seconds / interval).rounded(rule) * interval
        return startOfPeriod.addingTimeInterval(seconds)
    }

    func roundDate(toNearest value: Int, in unit: UnitDuration, using calendar: Calendar = Calendar.current) -> Date? {
        return roundDate(to: value, in: unit, using: .toNearestOrEven)
    }

    func roundDate(toNext value: Int, in unit: UnitDuration, using calendar: Calendar = Calendar.current) -> Date? {
        return roundDate(to: value, in: unit, using: .up)
    }
}

在我的游乐场中:

let calendar = Calendar.current
let date = Calendar.current.date(from: DateComponents(timeZone: TimeZone.current, year: 2020, month: 6, day: 12, hour: 00, minute: 24, second: 17, nanosecond: 577881))! // 12 Jun 2020 at 00:24

var roundedDate = date.roundDate(toNext: 5, in: .seconds)!
//"12 Jun 2020 at 00:24"
calendar.dateComponents([.nanosecond, .second, .minute, .hour, .day, .month], from: roundedDate) 
// month: 6 day: 12 hour: 0 minute: 24 second: 20 nanosecond: 0 isLeapMonth: false 

roundedDate = date.roundDate(toNearest: 5, in: .seconds)!
// "12 Jun 2020 at 00:24"
calendar.dateComponents([.nanosecond, .second, .minute, .hour, .day, .month], from: roundedDate)
// month: 6 day: 12 hour: 0 minute: 24 second: 15 nanosecond: 0 isLeapMonth: false 

roundedDate = date.roundDate(toNext: 5, in: .minutes)!
// "12 Jun 2020 at 00:25"
calendar.dateComponents([.nanosecond, .second, .minute, .hour, .day, .month], from: roundedDate)
// month: 6 day: 12 hour: 0 minute: 25 second: 0 nanosecond: 0 isLeapMonth: false 

roundedDate = date.roundDate(toNearest: 5, in: .minutes)!
// "12 Jun 2020 at 00:25"
calendar.dateComponents([.nanosecond, .second, .minute, .hour, .day, .month], from: roundedDate)
// month: 6 day: 12 hour: 0 minute: 25 second: 0 nanosecond: 0 isLeapMonth: false 

roundedDate = date.roundDate(toNext: 5, in: .hours)!
// "12 Jun 2020 at 05:00"
calendar.dateComponents([.nanosecond, .second, .minute, .hour, .day, .month], from: roundedDate)
// month: 6 day: 12 hour: 5 minute: 0 second: 0 nanosecond: 0 isLeapMonth: false 

roundedDate = date.roundDate(toNearest: 5, in: .hours)!
// "12 Jun 2020 at 00:00"
calendar.dateComponents([.nanosecond, .second, .minute, .hour, .day, .month], from: roundedDate)
// month: 6 day: 12 hour: 0 minute: 0 second: 0 nanosecond: 0 isLeapMonth: false 



1
这太棒了! - the Reverend

3

我刚刚开始为我的一个应用程序做实验,并想到了以下内容。它是用Swift编写的,但即使您不懂Swift,这个概念也应该足够易懂。

func skipToNextEvenFiveMinutesFromDate(date: NSDate) -> NSDate {
   var componentMask : NSCalendarUnit = (NSCalendarUnit.CalendarUnitYear | NSCalendarUnit.CalendarUnitMonth | NSCalendarUnit.CalendarUnitDay | NSCalendarUnit.CalendarUnitHour | NSCalendarUnit.CalendarUnitMinute)
   var components = NSCalendar.currentCalendar().components(componentMask, fromDate: date)

   components.minute += 5 - components.minute % 5
   components.second = 0
   if (components.minute == 0) {
      components.hour += 1
   }

   return NSCalendar.currentCalendar().dateFromComponents(components)!
}

在我的测试环境中,我注入了各种自定义日期,接近午夜、新年等,结果看起来是正确的。
编辑:支持Swift2。
 func skipToNextEvenFiveMinutesFromDate(date: NSDate) -> NSDate {
    let componentMask : NSCalendarUnit = ([NSCalendarUnit.Year , NSCalendarUnit.Month , NSCalendarUnit.Day , NSCalendarUnit.Hour ,NSCalendarUnit.Minute])
    let components = NSCalendar.currentCalendar().components(componentMask, fromDate: date)

    components.minute += 5 - components.minute % 5
    components.second = 0
    if (components.minute == 0) {
        components.hour += 1
    }

    return NSCalendar.currentCalendar().dateFromComponents(components)!
}

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