59

I have an NSDate object and I want to set it to an arbitrary time (say, midnight) so that I can use the timeIntervalSince1970 function to retrieve data consistently without worrying about the time when the object is created.

I've tried using an NSCalendar and modifying its components by using some Objective-C methods, like this:

let date: NSDate = NSDate()
let cal: NSCalendar = NSCalendar(calendarIdentifier: NSGregorianCalendar)!

let components: NSDateComponents = cal.components(NSCalendarUnit./* a unit of time */CalendarUnit, fromDate: date)
let newDate: NSDate = cal.dateFromComponents(components)

The problem with the above method is that you can only set one unit of time (/* a unit of time */), so you could only have one of the following be accurate:

  1. Day
  2. Month
  3. Year
  4. Hours
  5. Minutes
  6. Seconds

Is there a way to set hours, minutes, and seconds at the same time and retain the date (day/month/year)?

AstroCB
  • 12,337
  • 20
  • 57
  • 73

9 Answers9

96

Your statement

The problem with the above method is that you can only set one unit of time ...

is not correct. NSCalendarUnit conforms to the RawOptionSetType protocol which inherits from BitwiseOperationsType. This means that the options can be bitwise combined with & and |.

In Swift 2 (Xcode 7) this was changed again to be an OptionSetType which offers a set-like interface, see for example Error combining NSCalendarUnit with OR (pipe) in Swift 2.0.

Therefore the following compiles and works in iOS 7 and iOS 8:

let date = NSDate()
let cal = NSCalendar(calendarIdentifier: NSCalendarIdentifierGregorian)!

// Swift 1.2:
let components = cal.components(.CalendarUnitDay | .CalendarUnitMonth | .CalendarUnitYear, fromDate: date)
// Swift 2:
let components = cal.components([.Day , .Month, .Year ], fromDate: date)

let newDate = cal.dateFromComponents(components)

(Note that I have omitted the type annotations for the variables, the Swift compiler infers the type automatically from the expression on the right hand side of the assignments.)

Determining the start of the given day (midnight) can also done with the rangeOfUnit() method (iOS 7 and iOS 8):

let date = NSDate()
let cal = NSCalendar(calendarIdentifier: NSCalendarIdentifierGregorian)!
var newDate : NSDate?

// Swift 1.2:
cal.rangeOfUnit(.CalendarUnitDay, startDate: &newDate, interval: nil, forDate: date)
// Swift 2:
cal.rangeOfUnit(.Day, startDate: &newDate, interval: nil, forDate: date)

If your deployment target is iOS 8 then it is even simpler:

let date = NSDate()
let cal = NSCalendar(calendarIdentifier: NSCalendarIdentifierGregorian)!
let newDate = cal.startOfDayForDate(date)

Update for Swift 3 (Xcode 8):

let date = Date()
let cal = Calendar(identifier: .gregorian)
let newDate = cal.startOfDay(for: date)
Community
  • 1
  • 1
Martin R
  • 529,903
  • 94
  • 1,240
  • 1,382
  • 1
    It's strange that the `startOfDayForDate:` method is undocumented although it is exposed in the header file among with a lot of new methods also undocumented. Thanks for this. – Desdenova Oct 04 '14 at 09:20
  • 3
    @Desdenova: It is also listed in the [iOS 8.0 API Diffs](https://developer.apple.com/library/ios/releasenotes/General/iOS80APIDiffs/frameworks/Foundation.html), but you are right, the `NSCalendar` documentation is indeed not up to date. – Martin R Oct 04 '14 at 09:26
  • I knew that there was something I was missing. Thanks; this is what I needed. – AstroCB Oct 04 '14 at 14:03
  • 1
    @Desdenova The same is true for the `dateBySettingHour` method. – AstroCB Oct 04 '14 at 14:04
  • Be careful using `startOfDayForDate()`, because it works right only in UTC locale. – adnako Jun 29 '16 at 15:22
  • 1
    @adnako: I am pretty sure that startOfDayForDate works correctly in all time zones. Do you have a concrete example? – Martin R Jun 29 '16 at 15:25
  • 1
    @MartinR My time zone is UTC+3, value is '2016-06-28 15:35:44 UTC', if I call startOfDayForDate on this date the result is '2016-06-27 21:00:00 UTC'. It's not exactly what I need. – adnako Jun 29 '16 at 15:28
  • 1
    @adnako: Note that '2016-06-27 21:00:00 UTC' is '2016-06-28 00:00:00' in your time zone, so the result is correct. To *print* the result according to your time zone you need an NSDateFormatter. – Martin R Jun 29 '16 at 15:52
  • @MartinR I did say that user should consider that function works as expected in UTC time zone. – adnako Jun 30 '16 at 07:25
38

Yes.

You don't need to fiddle with the components of the NSCalendar at all; you can simply call the dateBySettingHour method and use the ofDate parameter with your existing date.

let date: NSDate = NSDate()
let cal: NSCalendar = NSCalendar(calendarIdentifier: NSGregorianCalendar)!

let newDate: NSDate = cal.dateBySettingHour(0, minute: 0, second: 0, ofDate: date, options: NSCalendarOptions())!

For Swift 3:

let date: Date = Date()
let cal: Calendar = Calendar(identifier: .gregorian)

let newDate: Date = cal.date(bySettingHour: 0, minute: 0, second: 0, of: date)!

Then, to get your time since 1970, you can just do

let time: NSTimeInterval = newDate.timeIntervalSince1970

dateBySettingHour was introduced in OS X Mavericks (10.9) and gained iOS support with iOS 8.

Declaration in NSCalendar.h:

/*
    This API returns a new NSDate object representing the date calculated by setting hour, minute, and second to a given time.
    If no such time exists, the next available time is returned (which could, for example, be in a different day than the nominal target date).
    The intent is to return a date on the same day as the original date argument.  This may result in a date which is earlier than the given date, of course.
 */
- (NSDate *)dateBySettingHour:(NSInteger)h minute:(NSInteger)m second:(NSInteger)s ofDate:(NSDate *)date options:(NSCalendarOptions)opts NS_AVAILABLE(10_9, 8_0);
AstroCB
  • 12,337
  • 20
  • 57
  • 73
8

Here's an example of how you would do it, without using the dateBySettingHour function (to make sure your code is still compatible with iOS 7 devices):

NSDate* now = [NSDate date];
NSCalendar *gregorian = [[NSCalendar alloc] initWithCalendarIdentifier:NSGregorianCalendar];
NSDateComponents *dateComponents = [gregorian components:(NSYearCalendarUnit | NSMonthCalendarUnit | NSDayCalendarUnit) fromDate:now];
NSDate* midnightLastNight = [gregorian dateFromComponents:dateComponents];

Yuck.

There is a reason why I prefer coding in C#...

Anyone fancy some readable code..?

DateTime midnightLastNight = DateTime.Today;

;-)

Mike Gledhill
  • 27,846
  • 7
  • 149
  • 159
5

Swift iOS 8 and up: People tend to forget that the Calendar and DateFormatter objects have a TimeZone. If you do not set the desired timzone and the default timezone value is not ok for you, then the resulting hours and minutes could be off.

Note: In a real app you could optimize this code some more.

Note: When not caring about timezones, the results could be OK on one device, and bad on an other device just because of different timezone settings.

Note: Be sure to add an existing timezone identifier! This code does not handle a missing or misspelled timezone name.

func dateTodayZeroHour() -> Date {
    var cal = Calendar.current
    cal.timeZone = TimeZone(identifier: "Europe/Paris")!
    return cal.startOfDay(for: Date())   
}

You could even extend the language. If the default timezone is fine for you, do not set it.

extension Date {
    var midnight: Date {
        var cal = Calendar.current
        cal.timeZone = TimeZone(identifier: "Europe/Paris")!
        return cal.startOfDay(for: self)
    }
    var midday: Date {
        var cal = Calendar.current
        cal.timeZone = TimeZone(identifier: "Europe/Paris")!
        return cal.date(byAdding: .hour, value: 12, to: self.midnight)!
    }
}

And use it like this:

let formatter = DateFormatter()
formatter.timeZone = TimeZone(identifier: "Europe/Paris")
formatter.dateFormat = "yyyy/MM/dd HH:mm:ss"

let midnight = Date().midnight
let midnightString = formatter.string(from: midnight)
let midday = Date().midday
let middayString = formatter.string(from: midday)

let wheneverMidnight = formatter.date(from: "2018/12/05 08:08:08")!.midnight
let wheneverMidnightString = formatter.string(from: wheneverMidnight)

print("dates: \(midnightString) \(middayString) \(wheneverMidnightString)")

The string conversions and the DateFormatter are needed in our case for some formatting and to move the timezone since the date object in itself does not keep or care about a timezone value.

Watch out! The resulting value could differ because of a timezone offset somewhere in your calculating chain!

t1ser
  • 1,574
  • 19
  • 18
5

Swift 5+

let date = Calendar.current.date(bySettingHour: 0, minute: 0, second: 0, of: Date())
William Hu
  • 15,423
  • 11
  • 100
  • 121
2

Just in case someone is looking for this:

Using SwiftDate you could just do this:

Date().atTime(hour: 0, minute: 0, second: 0)
Francois Nadeau
  • 7,023
  • 2
  • 49
  • 58
1

In my opinion, the solution, which is easiest to verify, but perhaps not the quickest, is to use strings.

func set( hours: Int, minutes: Int, seconds: Int, ofDate date: Date ) -> Date {

    let dateFormatter = DateFormatter()
    dateFormatter.dateFormat = "yyyy-MM-dd"

    let newDateString = "\(dateFormatter.string(from: date)) \(hours):\(minutes):\(seconds)"

    dateFormatter.dateFormat = "yyyy-MM-dd HH:mm:ss"

    return dateFormatter.date(from: newDateString)
}
Jason
  • 413
  • 5
  • 11
0
func resetHourMinuteSecond(date: NSDate, hour: Int, minute: Int, second: Int) -> NSDate{
    let nsdate = NSCalendar.currentCalendar().dateBySettingHour(hour, minute: minute, second: second, ofDate: date, options: NSCalendarOptions(rawValue: 0))
    return nsdate!
}
Frank Hou
  • 1,688
  • 1
  • 16
  • 11
0

Use the current calendar to get the start of the day for the current time.

let today = Calendar.current.startOfDay(for: Date())
Edward Brey
  • 40,302
  • 20
  • 199
  • 253