Swift - NSDate and last week of year - ios

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

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)!
}
}

Related

Number of days in month returns wrong value after 10:00 PM

I am having a small issue with getting the total days in a month using Swift.
I have extended the Date class and created this function:
func daysInMonth() -> Int {
print(self.day) ##30
print(self.month) ##12
print(self) ## 2021-11-30 23:46:29 +0000
print(Calendar.current.range(of: .day, in: .month, for: self)?.count) ##31
return Calendar.current.range(of: .day, in: .month, for: self)?.count ?? 0
}
I have set the Date&Time to the 30th of November, at 11:45 PM in the settings of my Mac, in Preferences.
I called the above function at 11:46 PM and obtained the above results (inline, next to the print statements).
The date output is correct as well as the day. The month output is wrong and the result is 31 days in the month of November.
If I run this exact same code before 10:00 PM, I get the right result which is 30 days.
Does anyone know why this is happening?
Thank you,
Paprika
It's a GMT offset issue combined with the current day in a month.
When you create a date without set a day, it will be set to the first day of the month.
So, if your timezone offset is for example -4 means your are 4 hours behind the GMT 0 and by default the timezone defined at Calendar.current is equal the system timezone. So what it means? Means you'll obtain the previous month if you test it in a boundary of 23 + (-4) or the next month if your offset is positive.
You can test this behaviour copying'n paste the following code in the Playground.
func getDaysInMonth(month: Int, year: Int, offset: Int = 0) -> Int? {
let someDate = DateComponents(year: year, month: month, hour: 3)
var current = Calendar.current
let timezone = TimeZone(secondsFromGMT: 60 * 60 * offset)!
current.timeZone = timezone
guard let someDay = current.date(from: someDate) else { return nil }
print("date: \(someDay)") // this will always
return someDay.daysInCurrentMonth
}
for hour in -12...12 {
print("hour: \(hour)\ndays: \(getDaysInMonth(month: 10, year: 2021, offset: hour) ?? -1)")
print("---\n")
}
extension Date {
var daysInCurrentMonth: Int? {
Calendar.current.range(of: .day, in: .month, for: self)?.count
}
}
Notice the days will change starting by your current system time zone (notice only the month will change).
How to fix this?
In your case, I guess you just want to show how many days a month have, so you can just set the to zero like this:
TimeZone(secondsFromGMT: 0)
Do this change at a instance of Calendar.current and check if it works for you.
It appears there something wrong with your Date extension methods for .day and .month.
Without seeing code it's hard to determine what the problem is though. Below is some code for returning the current month (Int) and current numbered day of month (Int)
extension Date
{
var month: Int
{
let date = Date()
let calendar = Calendar.current
let components = calendar.dateComponents([.month], from: date)
return components.month
}
var day: Int
{
let date = Date()
let calendar = Calendar.current
let components = calendar.dateComponents([.day], from: self)
return components.day
}
}
Please also ensure your time/date settings are correct on your mac/simulator/device. If these are wrong - it could have been jumping to a different month if you were in a timezone that was ahead a few hours.

How to check the amount of time between two NSDates?

I'm making a kind of challenge based app that requires that the user comes back every day. If he misses one day, he has to start all over again.
My problem is my dateChanged()-function; the first thing is, that it doesn't work very reliable, the second is that I just check if the date changed, I accordingly don't know if there were one or two days between using the app.
Here's my current function:
public func changeDays()
{
myFormatter.dateStyle = .short
myFormatter.locale = Locale(identifier: "de_DE")
oldDate = defaults.string(forKey: "oldDate")!
let newDate = Date()
let newDateString = myFormatter.string(from: newDate)
if newDateString == oldDate
{
NumberOfDaysInARow.text = "\(days) / 30"
}
else if newDateString != oldDate
{
days += 1
NumberOfDaysInARow.text = "\(days) / 30"
defaults.set(days, forKey: "days")
}
oldDate = newDateString
defaults.set(oldDate, forKey: "oldDate")
}
Just today it started giving me a fatal error when starting the app on my iPhone, did not happen in the simulator though... weird.
How do I have to change my function to make it a 100% reliable (and working) while also being able to check the amount of time between the two dates?
Thank you for having a look! Have a great day
You could extend Date with the function below that returns the amount of days from another date.
extension Date {
// Returns the amount of days from another date
func days(from date: Date) -> Int {
return Calendar.current.dateComponents([.day], from: date, to: self).day ?? 0
}
}
Instead of saving oldDate as a string you can set it to defaults as a date:
defaults.set(Date(), forKey: "oldDate")
You can get the old date from defaults using:
let oldDate = defaults.object(forKey:"oldDate") as! Date
Once you have your old date
let dateNow = Date()
let timeDifference = dateNow.days(from: oldDate!)
If timeDifference > 1 {
// older than 1 day
} else {
// Streak still alive
}
}
If you look in the documentation you will see that Date has a method whose sole purpose is too determine the interval between two dates time​Interval​Since(_:​).
If you set the old date to always be 11:59PM on the day it was last used you only have to see if the interval is greater than 24 hours worth of seconds (60 seconds * 60 minutes * 24 hours).
You may want to look at the docs for DateComponents for help creating a date that uses the current date but with a specific time.

check value existence by NSDate as key in dictionary

I have a dictionary like this:
var dic = [NSDate: Int]()
it is used in my iOS to-do app to get the number of finished tasks of a particular date. I only care about the year, month and day sections in NSDate and also want to be able to get the number of tasks in a particular date using this dictionary, how can I do that? thanks.
Instead of storing your date as NSDate in your dictionary you can save it as String so that comparison will be easier. Use following code to store it as a string
func dateFromString(date : NSDate) -> String {
var dateFormatter = NSDateFormatter()
dateFormatter.dateFormat = "yyyy-MM-dd"
return dateFormatter.stringFromDate(date)
}
You can pass NSDate() to above function and it will give you string containing only year, month and date. For retrieving your data from dictionary use following.
func dateFrom(year:Int, month:Int, day:Int) -> String {
let components = NSDateComponents()
components.year = year
components.month = month
components.day = day
let gregorian = NSCalendar(identifier:NSCalendarIdentifierGregorian)
let date = gregorian!.dateFromComponents(components)
return dateFromString(date!)
}
You can pass year, month and date to above function and it will return corresponding date in string format. So your dictionary operations will look like
dict[dateFromString(NSDate())] = 1 //for insertion or updation
let numOfTasks = dict[dateFrom(2016, month: 1, day: 15)] //to get task for any particular day
EDIT
If you want to proceed with NSDate as key for your dictionary then you'll have to modify above code as follows. dateFrom will return date with year,month and date of your choice, and time will be some constant value. Time will be set to midnight in your current time zone if you don't set it.
func dateFrom(year:Int, month:Int, day:Int) -> NSDate {
let components = NSDateComponents()
components.year = year
components.month = month
components.day = day
let gregorian = NSCalendar(identifier:NSCalendarIdentifierGregorian)
let date = gregorian!.dateFromComponents(components)
return date!
}
And for getting current date use following so that you store date object with current year, date, month and time to some constant value.
func getCurrentDate()->NSDate {
let date = NSDate()
let calendar = NSCalendar.currentCalendar()
let components = calendar.components([.Day , .Month , .Year], fromDate: date)
return dateFrom(components.year, month: components.month, day: components.day)
}
Usage will be as follows
dict[getCurrentDate()] = i //for insertion or updation
let numOfTasks = dict[dateFrom(2016, month: 1, day: 15)] //to get task for any particular day

Improper date conversion?

I'm trying to create a function that will get the Gregorian start and end dates for a month passed in of any arbitrary calendar.
So if I pass Hebrew calendar, month 3, year 5775, I would expect back something like this (2015-05-19T00:00:00.000-05:00, 2015-06-17T00:00:00.000-05:00)
let (start,end) = gregorianMonthForCalendarMonth(NSCalendar(calendarIdentifier: NSCalendarIdentifierHebrew)!, 3, 5775)
println(start)
println(end)
What I'm getting is (2014-11-23T00:00:00.000-05:00, 2014-12-22T00:00:00.000-05:00)
Not sure what I'm doing wrong.
My functions:
let fullDateFormatter = getFullDateFormatter()
func getFullDateFormatter() -> NSDateFormatter {
let _dateFormatter = NSDateFormatter()
_dateFormatter.calendar = NSCalendar(calendarIdentifier: NSCalendarIdentifierGregorian)
_dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss'.'SSSZZZZZ"
return _dateFormatter
}
func gregorianMonthForCalendarMonth(calendar:NSCalendar, month:Int, year:Int) -> (start:String, end:String) {
// get 1st day of month
let components = NSDateComponents()
components.year = year
components.month = month
components.day = 1
let start = calendar.dateFromComponents(components)
// get number of days in the month
let days = calendar.rangeOfUnit(NSCalendarUnit.CalendarUnitDay, inUnit: NSCalendarUnit.CalendarUnitMonth, forDate: start!)
components.day = days.length
let end = calendar.dateFromComponents(components)
return (start!.stringValue(),end!.stringValue())
}
extension NSDate {
func stringValue() -> String {
let strDate = fullDateFormatter.stringFromDate(self)
return strDate
}
}
The problem would appear to stem from the interpretation of what the "third" month of 5775 is. Kislev is the 3rd month of the civil year. And 1 Kislev 5775 is 23 November 2014.
However, Silvan is the 3rd month of the ecclesiastical year and 1 Silvan 5775 is 19 May 2015.
Apple's choice of the civil year month numbering is somewhat understandable as the Hebrew numeric year value increments on Tishrei (the first month of the civil year, the seventh month of the ecclesiastical year).
Admittedly, many Hebrew date conversion sites list the months in the order they occur in the ecclesiastical year, but if you dive into those sites, you'll see that they accurately reflect the year number changing on the first of Tishrei. I've noticed that many of these sites don't ask for numeric month number, but rather let you select from a list of month names, eliminating the sort of confusion that simply referring to the "third" month might otherwise entail.

Neat way to get end of day for date?

I am querying a database for values between a startDate and an endDate. I am therefore looking for a neat way to get the NSDate for the end of the day for a date. Just like you can get startOfDayForDate().
I guess you could do :
let cal = NSCalendar.currentCalendar()
let startOfDay = cal.startOfDayForDate(NSDate())
let aDay:NSTimeInterval = 60*60*23 + 60*59 + 59
let endofDay = startOfDay.dateByAddingTimeInterval(aDay)
Is adding component.day + 1 to startOfDayForDate the correct method to get the "end of the day" for a date, or is there a better method?
A better way would be to get the start of the next day and subtract 0.0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001 second.
Do you get what I want to say? It's very hard to define the end of the day. 23:59 is certainly not the end of the day, there is almost a whole minute left until the next day. And even 23:59:59 is not the end of a day. Because there is an infinite amount of fraction seconds between this time and the start of the next day. As far as I know NSDate supports nanoseconds out of the box, so in the current implementation there are at least 1 000 000 000 possible NSDates between 23:59:59 and the next day.
That's why you should see if you can find a way to use the start of the next day.
For example, instead of if (startOfDay <= date && date <= endOfDay) you could use if (startOfDay <= date && date < startOfNextDay).
To calculate the start of the next day you have to use NSCalendar. In iOS8 Apple added a couple of nice methods to make these calculations short:
let calendar = NSCalendar.currentCalendar()
let startOfDay = calendar.startOfDayForDate(NSDate())
let startOfNextDay = calendar.dateByAddingUnit(.CalendarUnitDay, value: 1, toDate: startOfDay, options: nil)!
EDIT: Since you now state that you want to query a database you don't need to find the end of the day. Check if the date is on or after the start of the day, and before the start of the next day.
Try this:
var endOfDay: Date? {
var components = DateComponents()
components.day = 1
components.second = -1
var calendar = Calendar.current
calendar.timeZone = TimeZone(abbreviation: "GMT")!
return calendar.date(bySettingHour: 11, minute: 59, second: 59, of: Date())
}

Resources