Round NSDate to the nearest 5 minutes
IphoneObjective CCocoaIphone Problem Overview
For example I have
NSDate *curDate = [NSDate date];
and its value is 9:13 am. I am not using year, month and day parts of curDate.
What I want to get is date with 9:15 time value; If I have time value 9:16 I want to advance it to 9:20 and so on.
How can I do that with NSDate?
Iphone Solutions
Solution 1 - Iphone
Here's my solution:
NSTimeInterval seconds = round([date timeIntervalSinceReferenceDate]/300.0)*300.0;
NSDate *rounded = [NSDate dateWithTimeIntervalSinceReferenceDate:seconds];
I did some testing and it is about ten times as fast as Voss's solution. With 1M iterations it took about 3.39 seconds. This one performed in 0.38 seconds. J3RM's solution took 0.50 seconds. Memory usage should be the lowest also.
Not that the performance is everything but it's a one-liner. Also you can easily control the rounding with division and multiplication.
EDIT: To answer the question, you can use ceil
to round up properly:
NSTimeInterval seconds = ceil([date timeIntervalSinceReferenceDate]/300.0)*300.0;
NSDate *rounded = [NSDate dateWithTimeIntervalSinceReferenceDate:seconds];
EDIT: An extension in 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)
}
}
Solution 2 - Iphone
Take the minute value, divide by 5 rounding up to get the next highest 5 minute unit, multiply to 5 to get that back into in minutes, and construct a new 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];
Solution 3 - Iphone
How about this based on Chris' and 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)
Solution 4 - Iphone
Wowsers, I see a lot of answers here, but many are long or difficult to understand, so I'll try to throw in my 2 cents in case it helps. The NSCalendar
class provides the functionality needed, in a safe and concise manner. Here is a solution that works for me, without multiplying time interval seconds, rounding, or anything. NSCalendar
takes into account leap days/years, and other time and date oddities. (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()
It can be added to an extension on NSDate
if needed, or as a free-form function returning a new NSDate
instance, whatever you need. Hope this helps anyone who needs it.
Swift 3 Update
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()
Solution 5 - Iphone
I think this is the best solution, but just my opinion, based on previous poster code. rounds to nearest 5 min mark. This code should use a lot less memory than the date components solutions. Brilliant, Thanks for the direction.
+(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;
}
Solution 6 - Iphone
https://forums.developer.apple.com/thread/92399
see link for full and detailed answer from an Apple staff member. To save you a click, the solution:
let original = Date()
let rounded = Date(timeIntervalSinceReferenceDate:
(original.timeIntervalSinceReferenceDate / 300.0).rounded(.toNearestOrEven) * 300.0)
Solution 7 - Iphone
Thanks for the sample. Below I have added some code the round to nearest 5 minutes
-(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;
}
Solution 8 - Iphone
Most replies here are unfortunately not perfectly correct (even though they seem to work quite well for most users), as they either rely on the current active system calendar to be a Gregorian calendar (which may not be the case) or upon the fact that leap seconds don't exist and/or will always be ignored by OS X an iOS. The following code works copy&paste, is guaranteed to be correct and it makes no such assumptions (and thus will also not break in the future if Apple changes leap seconds support, as in that case NSCalendar will have to correctly support them as well):
{
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];
}
If the current time is already a multiple of 5 minutes, the code will not change it. The original question did not specify this case explicitly. If the code shall always round up to the next multiple of 5 minutes, just remove the test if (minutes % 5) {
and it will always round up.
Solution 9 - Iphone
The answer from @ipje did the trick for the next 5 minutes but I needed something more flexible and I wanted to get rid of all the magic numbers.
I found a solution thanks to an answer to a similar question
My solution uses the Swift 5.2 and Measurement
to avoid using magic numbers:
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)
}
}
In my playground :
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
Solution 10 - Iphone
I just started experimenting with this for an app of mine, and came up with the following. It is in Swift, but the concept should be understandable enough, even if you don't know 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)!
}
The result looks correct in my playground, where I inject various custom dates, close to midnight, close to a new year etc.
Edit: Swift2 support:
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)!
}
Solution 11 - Iphone
Here's my solution to the original problem (rounding up) using ayianni's wrapper idea.
-(NSDate *)roundDateToCeiling5Minutes:(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;
// Add the remainder of time to the date to round it up evenly
mydate = [mydate addTimeInterval:60*(5-remain)];
return mydate;
}
Solution 12 - Iphone
I know this is an older thread, but since there are more recent answers I will share the utility method that I use to round an NSDate to the nearest 5 minute interval.
I use this to populate a UITextField with the current UIDatePicker date when it becomes FirstResponder. You can't just use [NSDate date] when the UIDatePicker is configured with something other than a 1 minute interval. Mine are configured with 5 minute intervals.
+ (NSDate *)roundToNearest5MinuteInterval {
NSDate *ceilingDate = [NSDate dateWithTimeIntervalSinceReferenceDate:ceil([[NSDate date] timeIntervalSinceReferenceDate]/300.0)*300.0];
NSDate *floorDate = [NSDate dateWithTimeIntervalSinceReferenceDate:floor([[NSDate date] timeIntervalSinceReferenceDate]/300.0)*300.0];
NSTimeInterval ceilingInterval = [ceilingDate timeIntervalSinceNow];
NSTimeInterval floorInterval = [floorDate timeIntervalSinceNow];
if (fabs(ceilingInterval) < fabs(floorInterval)) {
return ceilingDate;
} else {
return floorDate;
}
}
Ignoring the title of the question and reading what @aler really wants to accomplish (rounding UP to the nearest 5 minute). All you have to do is the following:
NSDate *ceilingDate = [NSDate dateWithTimeIntervalSinceReferenceDate:ceil([[NSDate date] timeIntervalSinceReferenceDate]/300.0)*300.0];
Solution 13 - Iphone
One more Swift generic solution, which works up to 30 minutes rounding using NSCalendar
extension NSDate {
func nearest(minutes: Int) -> NSDate {
assert(minutes <= 30, "nearest(m) suppport rounding up to 30 minutes");
let cal = NSCalendar.currentCalendar();
var time = cal.components(.CalendarUnitMinute | .CalendarUnitSecond, fromDate: self);
let rem = time.minute % minutes
if rem > 0 {
time.minute = minutes - rem;
}
time.second = -time.second;
time.nanosecond = -time.nanosecond //updated 7.07.15
let date = cal.dateByAddingComponents(time, toDate: self, options: NSCalendarOptions(0));
return date!;
}
}
Solution 14 - Iphone
Had been looking for this myself, but using the example above gave me from year 0001 dates.
Here's my alternative, incorporated with smorgan's more elegant mod suggestion though beware I haven't leak tested this yet:
NSDate *myDate = [NSDate date];
// Get the nearest 5 minute block
NSDateComponents *time = [[NSCalendar currentCalendar] components:NSHourCalendarUnit | NSMinuteCalendarUnit
fromDate:myDate];
NSInteger minutes = [time minute];
int remain = minutes % 5;
// Add the remainder of time to the date to round it up evenly
myDate = [myDate addTimeInterval:60*(5-remain)];
Solution 15 - Iphone
I rewrote @J3RM 's solution as an extension in Swift on the NSDate class. Here it is for rounding a date to the nearest 15th minute interval:
extension NSDate
{
func nearestFifteenthMinute() -> NSDate!
{
let referenceTimeInterval = Int(self.timeIntervalSinceReferenceDate)
let remainingSeconds = referenceTimeInterval % 900
var timeRoundedTo5Minutes = referenceTimeInterval - remainingSeconds
if remainingSeconds > 450
{
timeRoundedTo5Minutes = referenceTimeInterval + (900 - remainingSeconds)
}
let roundedDate = NSDate.dateWithTimeIntervalSinceReferenceDate(NSTimeInterval(timeRoundedTo5Minutes))
return roundedDate
}
}
Solution 16 - Iphone
I'm not sure how efficient NSDateComponents are, but if you just want to deal with the NSDate itself it can give you values based on seconds which can then be manipulated.
For example, this method rounds down to the nearest minute. Change the 60 to 300 and it will round down to nearest 5 minutes.
+ (NSDate *)dateRoundedDownToMinutes:(NSDate *)date {
// Strip miliseconds by converting to int
int referenceTimeInterval = (int)[date timeIntervalSinceReferenceDate];
int remainingSeconds = referenceTimeInterval % 60;
int timeRoundedDownToMinutes = referenceTimeInterval - remainingSeconds;
NSDate *roundedDownDate = [NSDate dateWithTimeIntervalSinceReferenceDate:(NSTimeInterval)timeRoundedDownToMinutes];
return roundedDownDate;
}
Solution 17 - Iphone
This is a generic solution which rounds up to the nearest input 'mins':
+(NSDate *)roundUpDate:(NSDate *)aDate toNearestMins:(NSInteger)mins
{
NSDateComponents *components = [[NSCalendar currentCalendar] components:NSUIntegerMax fromDate:aDate];
NSInteger dateMins = components.minute;
dateMins = ((dateMins+mins)/mins)*mins;
[components setMinute:dateMins];
[components setSecond:0];
return [[NSCalendar currentCalendar] dateFromComponents:components];
}
Solution 18 - Iphone
- (NSDate *)roundDateToNearestFiveMinutes:(NSDate *)date
{
NSDateComponents *time = [[NSCalendar currentCalendar]
components:NSHourCalendarUnit | NSMinuteCalendarUnit
fromDate:date];
NSInteger minutes = [time minute];
float minuteUnit = ceil((float) minutes / 5.0);
minutes = minuteUnit * 5.0;
[time setMinute: minutes];
return [[NSCalendar currentCalendar] dateFromComponents:time];
}
Solution 19 - Iphone
Even shorter... limit to seconds:
let seconds = ceil(Date().timeIntervalSinceReferenceDate/300.0)*300.0
let roundedDate = Date(timeIntervalSinceReferenceDate: seconds)