16

I'm trying to get NSDate from UIDatePicker, but it constantly returns me a date time with trailing 20 seconds. How can I manually set NSDate's second to zero?

Wyetro
  • 8,439
  • 9
  • 46
  • 64
deukyun
  • 163
  • 1
  • 6

7 Answers7

17

NSDate is immutable, so you cannot modify its time. But you can create a new date object that snaps to the nearest minute:

NSTimeInterval time = floor([date timeIntervalSinceReferenceDate] / 60.0) * 60.0;
NSDate *minute = [NSDate dateWithTimeIntervalSinceReferenceDate:time];

Edit to answer Uli's comment

The reference date for NSDate is January 1, 2001, 0:00 GMT. There have been two leap seconds added since then: 2005 and 2010, so the value returned by [NSDate timeIntervalSinceReferenceDate] should be off by two seconds.

This is not the case: timeIntervalSinceReferenceDate is exactly synchronous to the wall time.

When answering the question I did not make sure that this is actually true. I just assumed that Mac OS would behave as UNIX time (1970 epoch) does: POSIX guarantees that each day starts at a multiple of 86,400 seconds.

Looking at the values returned from NSDate this assumption seems to be correct but it sure would be nice to find a definite (documented) statement of that.

Nikolai Ruhe
  • 81,520
  • 17
  • 180
  • 200
  • 4
    This won't work. The time interval since reference date is not guaranteed to be an even multiple of 60 for a time that has 00 seconds. It is the number of seconds since the reference date, and as such, leap seconds that occasionally get inserted will throw you off. I think you will have to use NSCalendar to find out what seconds your date has, then subtract *that* from the time interval. – uliwitness Jan 27 '12 at 11:03
  • 1
    The time you get may be one minute in the future, if the current minute is past the 30 seconds mark. This happens because round might round up. Try with `floor([date timeIntervalSinceReferenceDate] / 60.0) * 60.0;` – flo von der uni Apr 04 '14 at 13:29
  • 1
    @flovonderuni Good catch. The results from `floor` switch at the same time wall clocks do. Edited answer, thanks! – Nikolai Ruhe Apr 07 '14 at 08:17
11

You can't directly manipulate the NSTimeInterval since that is the distance in seconds since the reference date, which isn't guaranteed to be a 00-second-time when divided by 60. After all, leap seconds may have been inserted to adjust for differences between solar time and UTC. Each leap second would throw you off by 1. What I do to fix the seconds of my date to 0 is:

NSDate              *   startDateTime = [NSDate date];
NSDateComponents    *   startSeconds = [[NSCalendar currentCalendar] components: NSSecondCalendarUnit fromDate: startDateTime];

startDateTime = [NSDate dateWithTimeIntervalSinceReferenceDate: [startDateTime timeIntervalSinceReferenceDate] -[startSeconds second]];

This takes care of leap seconds. I guess an even cleaner way would be to use -dateByAddingComponents:

NSDate              *   startDateTime = [NSDate date];
NSDateComponents    *   startSeconds = [[NSCalendar currentCalendar] components: NSSecondCalendarUnit fromDate: startDateTime];
[startSeconds setSecond: -[startSeconds second]];

startDateTime = [[NSCalendar currentCalendar] dateByAddingComponents: startSeconds toDate: startDateTime options: 0];

That way you're guaranteed that whatever special things -dateByAddingComponents: takes care of is accounted for as well.

uliwitness
  • 8,532
  • 36
  • 58
  • 2
    I believe your claim about leap seconds affecting the `timeIntervalSinceReferenceDate` is not true. While Apple's Date and Time Programming Guide does not explicitly define the behavior, POSIX does so for the UNIX time: Leap seconds are ignored (see http://en.wikipedia.org/wiki/Unix_time). It seems that this is true for Cocoa as well. – Nikolai Ruhe Jan 27 '12 at 19:49
  • 2
    Another thought: Both your proposed solutions do not take fractions of a second into account. You calculate the (integer) number of seconds and subtract them from the original date. The result refers to a time with zero seconds but non-zero milliseconds, which is a bit odd. – Nikolai Ruhe Jan 27 '12 at 20:11
5

Here is a Swift extension for anyone who is interested:

extension Date {
    public mutating func floorSeconds() {
        let calendar = Calendar.current
        let components = calendar.dateComponents([.year, .month, .day, .hour, .minute], from: self)
        self = calendar.date(from: components) ?? self // you can handle nil however you choose, probably safe to force unwrap in most cases anyway
    }
}

Example usage:

let date = Date()
date.floorSeconds()

Using DateComponents is much more robust than adding a time interval to a date.

shim
  • 9,289
  • 12
  • 69
  • 108
3

Although this is an old question and Uli has given the "correct" answer, the simplest solution IMHO is to just subtract the seconds from the date, as obtained from the calendar. Mind that this may still leave milliseconds in place.

NSDate *date = [NSDate date];
NSDateComponents *comp = [[NSCalendar currentCalendar] components:NSCalendarUnitSecond
                                                         fromDate:date];
date = [date dateByAddingTimeInterval:-comp.second];
Pascal
  • 16,846
  • 4
  • 60
  • 69
1

Here is an extension to do this in Swift:

extension NSDate {
    func truncateSeconds() -> NSDate {
        let roundedTime = floor(self.timeIntervalSinceReferenceDate / 60) * 60
        return NSDate(timeIntervalSinceReferenceDate: roundedTime)
    }
}
Jeff Lewis
  • 2,846
  • 1
  • 19
  • 24
1

NSDate records a single moment in time. If what you want to do is store a specific day, don't use NSDate. You'll get lots of unexpected head-aches related to time-zones, daylight savings time e.tc.

One alternative solution is to store the day as an integer in quasi-ISO standard format, like 20110915 for the 15th of September, 2011. This is guaranteed to sort in the same way as NSDate would sort.

Aderstedt
  • 6,301
  • 5
  • 28
  • 44
0

Also Swift ...

extension Date {

  func floored() -> Date {
    let flooredSeconds = DateComponents(second: 0, nanosecond: 0)
    return Calendar.current.nextDate(after: self, 
                                     matching: flooredSeconds,
                                     matchingPolicy: .strict, 
                                     direction: Calendar.SearchDirection.backward)!
  }

}
Dirk
  • 1
  • 1