1

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..

Jonathan
  • 67
  • 2
  • 10
  • have you tried `NSCalendar.autoupdatingCurrentCalendar().dateFromComponents(components)!`? – Francis Yeap Oct 08 '15 at 10:16
  • Nope, i had not tried that. Just tried it, and sadly, it doesn't work. – Jonathan Oct 08 '15 at 10:35
  • i think you might need to consider the year as well in your components, since your flag includes `NSCalendarUnit.Year`. I suspecting it is actually taking the first Sunday of 2015 - which is 04-01 – Francis Yeap Oct 08 '15 at 11:39
  • When there are insufficient components provided to completely specify an absolute time, a calendar uses default values of its choice - [Apple Doc](https://developer.apple.com/library/mac/documentation/Cocoa/Reference/Foundation/Classes/NSCalendar_Class/#//apple_ref/occ/instm/NSCalendar/dateFromComponents:), which can then be quite unpredictable – Francis Yeap Oct 08 '15 at 11:42
  • That actually sounds like it may be it, i will try, thanks. – Jonathan Oct 08 '15 at 12:00
  • Why are you setting the hour, minute and second on your components? EDIT: also, could you clarify what you actually want to do? The Sunday in any given week may be ahead of or behind the other days depending on your locale. Do you want the day the user considers to be the start of the week (which will always be earlier than or the same day as the input date) or do you genuinely want Sunday? – Tommy Oct 20 '15 at 14:20

1 Answers1

5

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)!
    }
}
Martin R
  • 529,903
  • 94
  • 1,240
  • 1,382
  • What struck me as curious is that his device is set to a Danish locale, yet he's still trying to use Sunday as the first day of the week. Which made me consider that he possibly wants Sunday specifically, but then there's that bit where he doesn't seem to understand why he has to move a week backwards and add a day to get to the Monday. So I think that stuff about iOS not assuming Sunday — whether it has arbitrary ordinal value of 1 or not — necessarily to be the first day in a week is probably key to the problem. Positive votes, etc. – Tommy Oct 20 '15 at 19:56
  • @Tommy: Thanks! That dateAtWeekStart() method is actually from the SwiftDate project. The author of that project is from Rome, and (if I researched correctly) the Italian week begins with Monday. Which makes me wonder if that function was tested at all :) – Martin R Oct 20 '15 at 20:04
  • This works, i am somewhat new to working with dates, and they are very confusing sometimes, i think, and that is why i use swiftdate. - i actually don't want it to return sunday, i just thought that, that was the norm. Something seems weird to me though, if i use a breakpoint, to see the date the new dateAtWeekStart returns, it actually returns sunday 22.00, but when i use getWeekDates to convert them to monday, to friday, it works in my collectionview. And i just thought that was weird, maybe you have an explanation :) Just write if u need me to clarify. – Jonathan Oct 21 '15 at 06:59
  • @Jonathan: That is because 1) NSDate is an absolute point in time and has not knowledge of timezones, and 2) print(someDate) always uses the GMT time zone (as you can see from the trailing "+0000"). To print a date according to your local timezone you have to use a NSDateFormatter and convert the NSDate to a String. – Compare http://stackoverflow.com/questions/8466744/getting-date-from-nsdate-date-off-by-a-few-hours. – Martin R Oct 21 '15 at 07:02
  • @MartinR : Wow, that helps me so much, thanks!, I don't know how many times that i used a method called `.toUtc()` because it added +2 hours to the date, and that sometimes messed my code up. – Jonathan Oct 21 '15 at 07:11
  • @Jonathan: It is actually quite easy once you realize that there is NSDate (which is an absolute point in time), and NSDateComponents (the "interpretation" of a date in years, months, days, hours, ...). The first one is absolute, and the latter depends on your location, timezone, calendar, ... and conversion between those two is done with NSCalendar methods. – Martin R Oct 21 '15 at 07:28
  • @Jonathan (cont.) Something like "Return a new NSDate in UTC format from the current system timezone" (quote from the toUtc() method) normally makes no sense (because NSDate has no timezone). There may be applications of that method if you know precisely what you are doing. – Martin R Oct 21 '15 at 07:28