Pretty Date Intervals in Swift. NSCalendar components - ios

I'm trying to print a prettier version of a timeIntervalSinceDate e.g '10 days, 6 hours...'. But i'm failing to get NSCalendar components to work correctly.
I get an "Extra argument 'toDate' in call" error with the following:
let dateFormatter = NSDateFormatter()
dateFormatter.dateFormat = "EE, dd MMM y hh:mm a zzz"
var dateString = "Wed, 08 Jun 2011 11:58 pm EDT"
var date: NSDate = dateFormatter.dateFromString(dateString)!
var now = NSDate()
let calendar: NSCalendar = NSCalendar()
let components = calendar.components(unitFlags: .CalendarUnitDay,
fromDate: date,
toDate: now,
options: 0)
components.day
The 'toDate' feature is documented, so I'm not sure why I get the error?
Any help appreciated.

There are two errors:
The first parameter in a method does not have an external parameter name,
so you must not specify unitFlags:.
For "no options", specify NSCalendarOptions(0) or simply nil.
Together:
let components = calendar.components(.CalendarUnitDay,
fromDate: date, toDate: now, options: nil)
Update for Swift 2: NS_OPTIONS are now imported as conforming
to OptionSetType, and "no options" can be specified as the empty set:
let components = calendar.components(.Day,
fromDate: date, toDate: now, options: [])

Related

How would you set a date to a specific value in swift? [duplicate]

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:
Day
Month
Year
Hours
Minutes
Seconds
Is there a way to set hours, minutes, and seconds at the same time and retain the date (day/month/year)?
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)
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);
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;
;-)
Swift 5+
let date = Calendar.current.date(bySettingHour: 0, minute: 0, second: 0, of: Date())
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!
Just in case someone is looking for this:
Using SwiftDate you could just do this:
Date().atTime(hour: 0, minute: 0, second: 0)
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)
}
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!
}
Use the current calendar to get the start of the day for the current time.
let today = Calendar.current.startOfDay(for: Date())

Get number of days between two NSDates when crossing new year

I need to get the number of days between two dates which I get but if I input for example: 2016-12-10 and 2017-01-10 there will be a negative number of days. This is just occur when it´s new year between those two dates.
//Get the number of days between two NSDates
func daysBetween(date: NSDate) -> Int {
let calendar: NSCalendar = NSCalendar.currentCalendar()
let date1 = calendar.startOfDayForDate(self)
let date2 = calendar.startOfDayForDate(date)
let components = calendar.components(.Day, fromDate: date1, toDate: date2, options: [])
return components.day
}
//Using the function
let daysBetween = fromDate.daysBetween(toDate)
//Printing -334 if new year is between fromDate and toDate
print(daysBetween)
How can I modify my daysBetween(date: NSDate) function to prevent this from happening?
Edit:
Please don´t pay attention to why it´s printing exacltly -334. That´s just because fromDate and toDate are different days in month. The problem a wanna solve is why the response is negative and not 31 as it should be.
Solved:
It turned out to be as many of you thought. The fromDate is greater then toDate and causing a negative number. Stupid mistake. Thx guys!
You not need to worry about the days going negative. It's better to know if the first date (for example selected from an input UIDatePicker) is bigger than another. That is handled automatically when you converted back to an NSDate.
If the problem is how to print the days , you can use abs(days).
First compare both dates and then you can get correct positive value
let dateFormatter = NSDateFormatter()
dateFormatter.dateFormat = "yyyy-MM-dd"
let startDate:NSDate = dateFormatter.dateFromString("2016-12-10")!
let endDate:NSDate = dateFormatter.dateFromString("2017-01-10")!
let cal = NSCalendar.currentCalendar()
let dayUnit: NSCalendarUnit = .Day
if startDate.compare(endDate) == NSComparisonResult.OrderedDescending{
let components = cal.components(dayUnit, fromDate: endDate, toDate: startDate, options: [])
print(components)
} else {
let components = cal.components(unit, fromDate: startDate, toDate: endDate, options: [])
print(components)
}
As Describe by Leo Dabus
You can do it without comparing the dates also:
let com = cal.components(dayUnit, fromDate: startDate.earlierDate(endDate), toDate: startDate.laterDate(endDate), options: [])
print(com)

how to get NSDate of a specific next day and time

I have some events for which I need to calculate NSDates.
For example I'm trying to get the next Monday at 8:00 AM.
So I tried some stuff but nothing works:
1.
let nextMonday = NSCalendar.currentCalendar().dateBySettingUnit(NSCalendarUnit.Weekday, value: 2, ofDate: startDate, options: NSCalendarOptions.MatchNextTime)
let nextMondayEight = NSCalendar.currentCalendar().dateBySettingUnit(NSCalendarUnit.Hour, value: 8, ofDate: nextMonday!, options: NSCalendarOptions.MatchNextTime)
I get:
2016-04-12 05:00:00 +0000
That's Tuesday at 8:00 (the time difference is my local time GMT -3).
2.
let unitFlags: NSCalendarUnit = [.Day, .Month, .Year]
let comp = NSCalendar.currentCalendar().components(unitFlags, fromDate: NSDate())
comp.timeZone = NSTimeZone.localTimeZone()
comp.weekday = 1
comp.hour = 8
comp.minute = 0
comp.second = 0
let compDate = NSCalendar.currentCalendar().dateFromComponents(comp)
print("time: \(compDate!)")
I get:
2016-04-11 05:00:00 +0000
That's today at 8:00 and not next Monday at 8:00.
Any suggestions?
Thanks
NSCalendar has a method nextDateAfterDate:matchingComponents:options for this kind of date math.
let calendar = NSCalendar.currentCalendar()
let components = NSDateComponents()
components.hour = 8 // 8:00
components.weekday = 2 // Monday in Gregorian Calendar
let nextMondayEightOClock = calendar.nextDateAfterDate(NSDate(), matchingComponents: components, options: .MatchStrictly)

Swift Get previous years datetime at GMT+08:00 timezone

I need to get previous 5 years of GMT +08:00 timezone but I'm having trouble of getting the correct timezone.
let today = NSDate()
let gregorian = NSCalendar(calendarIdentifier: NSGregorianCalendar)
gregorian?.timeZone = NSTimeZone(forSecondsFromGMT: 60*60*8)
let offsetComponents = NSDateComponents()
offsetComponents.year = years
let nYearsDate: NSDate = gregorian!.dateByAddingComponents(offsetComponents, toDate: today, options: NSCalendarOptions(0))!
println("getNYearsDate: \(nYearsDate)")
I am getting 2010-07-23 11:44:47 +0000
instead of 2010-07-23 00:00:00 +0800
I need to get
2010-07-23 00:00:00 +0800 and 2010-07-23 23:59:59 +0800
is there anyway to achieve this in Swift and iOS 7.1 above?
You need to reset the time as well. Date periods are also best modeled with NSDateComponents.
// a little trick to get all calendar unit masks
let kAllCalendarUnits = NSCalendarUnit(rawValue: UInt.max)
// normalize the date
let components = NSCalendar.currentCalendar().components(
kAllCalendarUnits, fromDate: NSDate())
components.hour = 0
components.minute = 0
components.second = 0
let dateAtStartOfDay = NSCalendar.currentCalendar().dateFromComponents(components)!
// subtract 5 years
var period = NSDateComponents()
period.year = -5
let finalDate = NSCalendar.currentCalendar().dateByAddingComponents(period,
toDate: dateAtStartOfDay, options: [])!
Note that the method with date components will give you the correct day, regardless if the period contains one or two leap years.
The rest is just a question of how to display this date. This is done with NSDateFormatter. Eg.:
let formatter = NSDateFormatter()
formatter.timeZone = NSTimeZone(forSecondsFromGMT: 60 * 60 * 8)
formatter.dateFormat = "yyyy-MM-dd hh:mm:ss Z"
formatter.stringFromDate(finalDate)
// "2010-07-23 6:00:00 +0800"
In case you are wondering why it says "6:00" instead of "8:00": I ran this code with my machine set to GMT+1, plus summer time +1. Obviously, to get "0:00" you have to subtract the desired time difference again, but that would be 8 hours earlier in the GMT time zone.

Date string to NSDate swift

i'm having an array fill with date string, which i would like to go through and check whether the date is today or yesterday. The date string could look like following:
2015-04-10 22:07:00
So far i've tried just to convert it using dateFormatter, but it keeps returning nil
var dateString = arrayNews[0][0].date as NSString
var dateFormatter = NSDateFormatter()
dateFormatter.dateFormat = "yyyy-MM-dd hh:mm:ss"
var date = dateFormatter.dateFromString(dateString as String)
println(date)
the sudo code i would like to look something like this
if dateString == currentDate
date = "Today, hh:mm"
else if dateString == currentDate(-1)
date = "Yesterday, hh:mm
else
date = dd. MM, hh:mm
in the else statement the date could be 1. April, 12:00
How can i achieve such a logic?
Without the logic
func getDate(dateStr:String, format:String = "yyyy-MM-dd HH:mm:ss") -> NSString {
var dateFmt = NSDateFormatter()
dateFmt.timeZone = NSTimeZone.defaultTimeZone()
dateFmt.dateFormat = format
let newsDate = dateFmt.dateFromString(dateStr)!
let date = NSDate();
let dateFormatter = NSDateFormatter()
dateFormatter.timeZone = NSTimeZone()
let localDate = dateFormatter.stringFromDate(date)
return ""
}
You have to use yyyy-MM-dd HH:mm:ss instead of yyyy-MM-dd hh:mm:ss
HH: 24h format
hh: 12h format (with AM/PM)
Pay attention with dateFormatter, by default, it use localtime zone.
println() shows the value for GMT time, which is hold by NSDate.
It means, without specifying timezone, when you convert your string 2015-04-10 22:07:00 to NSDatetime, it return a data which is the time at your local time zone is 2015-04-10 22:07:00. As NSDate holds date time in GMT, you will see a different value when you show the value of that NSDate with println().
If your timezone is GMT+2 (2h earlier than GMT), when it's 22h:07 in your place, the GMT time is 20h:07. When you call println() on that NSDate, you see 2015-04-10 20:07:00
To compare 2 NSDate:
let calendar = NSCalendar(identifier: NSCalendarIdentifierGregorian)
let compareResult = calendar?.compareDate(date, toDate: date2, toUnitGranularity: NSCalendarUnit.CalendarUnitDay)
if (compareResult == NSComparisonResult.OrderedSame) {
println("yes")
} else {
println("no")
}
//to get yesterday, using NSCalendar:
let yesterday = calendar?.dateByAddingUnit(NSCalendarUnit.CalendarUnitDay, value: -1, toDate: date, options: NSCalendarOptions.MatchStrictly)

Resources