Search code examples
iosswiftnsdate

Swift - NSDate and last week of year


I am making a TimeTable app, and i have a method that adds 1 week to the current date, this works as it is supposed to, however if the week transitions from December to January, it adds 1 day extra.

Here is my code:

func getWeekDates(var date: NSDate) -> [NSDate] {
    var dates: [NSDate] = [NSDate]()
    for var i = 0; i < 5; i++ {
        date = date.dateAtWeekStart() + 1.day - 1.week
        date += i.day
        dates.append(date)
    }
    return dates
}

And dateAtWeekStart():

func dateAtWeekStart() -> NSDate {
    let flags : NSCalendarUnit = [NSCalendarUnit.Year,NSCalendarUnit.Month ,
        NSCalendarUnit.WeekOfYear,
        NSCalendarUnit.Weekday]
    let components = NSCalendar.currentCalendar().components(flags, fromDate: self)
    components.weekday = 1 // Sunday
    components.hour = self.hour
    components.minute = self.minute
    components.second = self.second
    return NSCalendar.currentCalendar().dateFromComponents(components)!
}

(dateAtWeekStart() is a function made in an extension to NSDate)

The reason i am adding 1 day and removing 1 week, is because dateAtWeekStart returns next sunday, so for example 08-10-2015.dateAtWeekStart() returns 11-10-2015.

So this works fine normally, however if we take this year as an example, 29-12-2015.dateAtWeekStart() returns 04-01-2015 instead of 03-01-2016.

By the way, the region on the device is set to Denmark.

dateAtWeekStart, comes from a helper class called SwiftDate made by malcommac: https://github.com/malcommac/SwiftDate

UPDATE EDIT: I am still having trouble figuring out how to fix this, i tried adding year to components like so: components.year = self.year, but it sets the year to 2014 for some reason when returning the components..


Solution

  • That dateAtWeekStart() method simply does not work. [.YearForWeekOfYear, .WeekOfYear] are sufficient as calendar units to determine the (start of a) week uniquely. The additional units can make the calculation undetermined. Also you can not just set components.weekday = 1 because in some regions Monday (2) is the first day of the week.

    So it is actually a bit easier:

    extension NSDate {
        func dateAtWeekStart() -> NSDate {
            let cal = NSCalendar.currentCalendar()
            // cal.firstWeekday = 1 // If you insist on Sunday being the first day of the week.
            let flags : NSCalendarUnit = [.YearForWeekOfYear, .WeekOfYear]
            let components = cal.components(flags, fromDate: self)
            return cal.dateFromComponents(components)!
        }
    }
    

    This should work in all cases and give the start of the week (at midnight) for the given date. There are also other methods one could use, such as rangeOfUnit().

    If you want Sunday to be considered as the first day of the week instead of using the user's regional settings then you have to set the firstWeekday property of the calendar.

    The code to add days or weeks to a date also looks highly suspicious. The extensions method for Int in the SwiftDate project treats a day as 24*60*60 seconds. This is not correct, because in regions with daylight saving times, a day can have 23 or 25 hours when the clocks are adjusted. The correct way to add one week to a date is to use calendar components again:

    date = cal.dateByAddingUnit(.WeekOfYear, value: 1, toDate: date, options: [])!
    

    Update for Swift 3:

    extension Date {
        func dateAtWeekStart() -> Date {
            var cal = Calendar.current
            // cal.firstWeekday = 1 // If you insist on Sunday being the first day of the week.
            let components = cal.dateComponents([.yearForWeekOfYear, .weekOfYear], from: self)
            return cal.date(from: components)!
        }
    }