Using NSDate calculations to determine previous and next pay period for bi-weekly pay frequency - ios

I'm trying to calculate the next and previous pay days for a user, when given a specified date.
I'm storing two properties for the user like so:
public var firstPayDay: NSDate {
get { return (NSDate(dateNumber: self["firstPayDay"] as! NSNumber)) }
set(date) {
self["firstPayDay"] = date.dateNumber()
self.payDayOfWeek = self.firstPayDay.dayOfWeek(zeroBased: true)!
// day of week from 0 - 6
}
}
When first introduced to the app, the user is asked to provide their next pay day. This is stored as an integer, e.g. 20160126 on the user object, and the following convenience is used to convert it to NSDate:
convenience init(dateNumber: NSNumber) {
let dateFormatter = NSDateFormatter()
dateFormatter.dateFormat = "yyyyMMdd"
self.init(timeInterval:0, sinceDate: dateFormatter.dateFromString(dateNumber.stringValue)!)
}
public var payDayOfWeek: Int {
get { return (self["payDayOfWeek"] as! Int) }
set(dayOfWeek) { self["payDayOfWeek"] = dayOfWeek }
}
When firstPayDay is set, payDayOfWeek is updated using the 0-based (Sunday ⇢ Saturday) index of the weekday.
I can use this method just fine to get the next and previous pay days for weekly pay periods like this, but I'm struggling how to do this for bi-weekly pay periods?
To determine the previous and next bi-weekly pay days, I would need to calculate in increments of two weeks from the first pay day, then determine where the specified day fits in between the two. How would I do that in code with the two known properties firstPayDay and payDayOfWeek?
Expected output:
user.firstPayDay = NSDate(fromNumber: 20160101) // January 1st, 2016
print(user.payDayOfWeek) // 5 (Friday, inferred from firstPayDay setter)
// For Today, 20160126
print(user.nextPayDayForDate(20160126)) // should return 20160129, the closest Friday an even two weeks from 20160101 and in the future
print(user.prevPayDayForDate(20160126)) // should return 20160115, the closest Friday an even two weeks from 20160101 and in the past
// For Saturday, 20160130
print(user.nextPayDayForDate(20160130)) // should return 20160212, the closest Friday an even two weeks from 20160101 and in the future
print(user.prevPayDayFordate(20160130)) // should return 20160129, the closest Friday an even two weeks from 20160101 and in the past
Work in Progress
internal func nextWeekBasedPayDate(payFrequency: PayFrequency, firstPayDay: NSDate) -> NSDate? {
let interval: Int = payFrequency == .Weekly ? 7 : 14
let calendar: NSCalendar = NSCalendar.autoupdatingCurrentCalendar()
guard
let date: NSDate = calendar.dateByAddingUnit(.Day, value: interval, toDate: self.previousWeekBasedPayDate(payFrequency, firstPayDay), options: [])! else { return nil }
return date.at12pm()
}
internal func previousWeekBasedPayDate(payFrequency: PayFrequency, firstPayDay: NSDate) -> NSDate? {
let interval: Int = payFrequency == .Weekly ? 7 : 14
let calendar: NSCalendar = NSCalendar.autoupdatingCurrentCalendar()
guard
let daysSinceFirstPayDate: Int = calendar.components([ .Day ], fromDate: firstPayDay, toDate: self, options: []).day,
let daysSincePreviousPayDate: Int = daysSinceFirstPayDate % interval + (daysSinceFirstPayDate < 0 ? interval : 0),
let date: NSDate = calendar.dateByAddingUnit(.Day, value: -daysSincePreviousPayDate, toDate: self, options: [])! else { return nil }
return date.at12pm()
}
internal func at12pm() -> NSDate? {
let cal = NSCalendar.currentCalendar()
return cal.dateBySettingHour(12, minute: 0, second: 0, ofDate: self, options: [])
}
Then to calculate a pay period:
func currentPayPeriod(payFrequency: PayFrequency, firstPayDay: NSDate) -> [NSDate]? {
var startDate: NSDate
var endDate: NSDate
switch payFrequency {
case .Monthly:
startDate = self.startOfMonth()!
endDate = self.endOfMonth()!
case .Weekly, .BiWeekly:
startDate = (self.previousPayDay(payFrequency, firstPayDay: firstPayDay))!
endDate = (self.nextPayDay(payFrequency, firstPayDay: firstPayDay)?.addDays(-1))!
case .SemiMonthly:
startDate = self.startOfMonth()!
endDate = self.middleOfMonth()!
}
return [startDate, endDate]
}
This seems to work perfectly:
/*
payPeriod [NSDate] 2 values
[0] __NSTaggedDate * 2016-01-24 20:00:00 UTC 0xe41bc5564c000000
[1] __NSTaggedDate * 2016-01-30 20:00:00 UTC 0xe41bc5d4dc000000
*/

I don't understand why you'd store the date as a number instead of a string, since you can't do math on it and you just have to convert it to a string to parse it anyway. So I'm just going to use string dates in this answer.
Also, converting a date to an NSDate without considering the time within the day is dangerous, because it tends to produce wrong answers for some days in some time zones. So let's tell the parser to use noon as the time within the day:
extension NSDate {
class func withYMDString(string: String) -> NSDate {
let dateFormatter = NSDateFormatter()
let defaultComponents = NSDateComponents()
defaultComponents.hour = 12
defaultComponents.minute = 0
defaultComponents.second = 0
dateFormatter.defaultDate = dateFormatter.calendar.dateFromComponents(defaultComponents)
dateFormatter.dateFormat = "yyyyMMdd"
return dateFormatter.dateFromString(string)!
}
}
Now let's consider how to find the prior and next pay dates. We have a reference pay date:
let referencePayDate = NSDate.withYMDString("20160101")
And we have some date of interest:
let someDate = NSDate.withYMDString("20151231")
For the purpose of finding the nearest pay dates to someDate, it doesn't really matter what day of week the pay dates fall on. A pay date comes every two weeks, which is every fourteen days. So we want to find those dates which are a multiple of fourteen days from referencePayDate, and which are nearest to someDate. We start by computing the number of days from referencePayDate to someDate:
let calendar = NSCalendar.autoupdatingCurrentCalendar()
let daysSinceReferencePayDate = calendar.components([ .Day ],
fromDate: referencePayDate, toDate: someDate, options: []).day
Then we round it down to the nearest multiple of 14, toward -∞:
let daysSincePriorPayDate = daysSinceReferencePayDate % 14
+ (daysSinceReferencePayDate < 0 ? 14 : 0)
(Note that we need to adjust for a negative numerator because of the way Swift computes the remainder.)
From that we can compute the prior pay date:
let priorPayDate = calendar.dateByAddingUnit(.Day, value: -daysSincePriorPayDate,
toDate: someDate, options: [])!
// Result: Dec 18, 2015, 12:00 PM
The next pay date is 14 days later:
let nextPayDate = calendar.dateByAddingUnit(.Day, value: 14,
toDate: priorPayDate, options: [])!
// Result: Jan 1, 2016, 12:00 PM

Related

Date from string is not coming properly and two dates difference also using Swift 3?

I have a date in string format, example:- "2017-07-31" or can be multiple dates (any) in string format. My requirement is to check this date to current date and if it is greater than 0 and less than 15, then that time I have to do another operation.
So first I am converting that date string to in date format. But it is giving one day ago date. Here is my code:
//Date from string
func dateFromString(date : String) -> Date {
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "yyyy-MM-dd"
let currentDate = (dateFormatter.date(from: date))//(from: date))
return currentDate!
}
Ex. my date is "2017-08-30" and this function is returning 2017-08-29 18:30:00 +0000 in date format. It means 1 day ago. I am little bit confuse about dates operation. I read so many blogs also.
After that I have to check this date to current date if it is in between 0 < 15 than I will do other operation.
Comparing two dates:
extension Date {
func daysBetweenDate(toDate: Date) -> Int {
let components = Calendar.current.dateComponents([.day], from: self, to: toDate)
return components.day ?? 0
}
}
If my date is today date and comparing to tomorrow date then also it is giving 0 days difference. Why?
If – for example – the current date is 2017-07-31 at 11AM then the
difference to 2017-08-01 (midnight) is 0 days and 13 hours, and that's
why you get "0 days difference" as result.
What you probably want is to compare the difference between the start
of the current day and the other date in days:
extension Date {
func daysBetween(toDate: Date) -> Int {
let cal = Calendar.current
let startOfToday = cal.startOfDay(for: self)
let startOfOtherDay = cal.startOfDay(for: toDate)
return cal.dateComponents([.day], from: startOfToday, to: startOfOtherDay).day!
}
}
Try this method for convert string to date:
func dateFromString(date : String) -> Date {
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "yyyy-MM-dd"
dateFormatter.timeZone = TimeZone.init(abbreviation: "UTC")
let currentDate = (dateFormatter.date(from: date))//(from: date))
return currentDate!
}
Try this to compare the time between two dates in seconds :
var seconds = Calendar.current.dateComponents([.second], from: date1!, to: date2!).second ?? 0
seconds = abs(seconds)
let min = seconds/60 // this gives you the number of minutes between two dates
let hours = seconds/3600 // this gives you the number of hours between two dates
let days = seconds/3600*24 // this gives you the number of days between two dates

How to calculate days difference between two NSDate objects in different time zones?

I have two NSDate which are initiated from UTC dates.
Lets call them (A & B)
I know that the A represents a day in China and B represents a day in USA. (I know the time zones.) How can I calculate the difference in days between the two...?
I have been using the following method which is obviously incorrect.
class func daysDifferenceIn(firstDate: NSDate, firstTimeZone: String, secondDate: NSDate, secondTimeZone: String) -> Int {
objc_sync_enter(self)
let firstDateComponents = NSCalendar.CommonCalendar.componentsInTimeZone(NSTimeZone(name: firstTimeZone)!, fromDate: firstDate)
let secondDateComponents = NSCalendar.CommonCalendar.componentsInTimeZone(NSTimeZone(name: secondTimeZone)!, fromDate: secondDate)
NSCalendar.CommonCalendar.timeZone = NSTimeZone(name: firstTimeZone)!
let firstCalDate = NSCalendar.CommonCalendar.dateFromComponents(firstDateComponents)
NSCalendar.CommonCalendar.timeZone = NSTimeZone(name: secondTimeZone)!
let secondCalDate = NSCalendar.CommonCalendar.dateFromComponents(secondDateComponents)
objc_sync_exit(self)
return firstCalDate!.numberOfDaysUntilDateTime(secondCalDate!)
}
func numberOfDaysUntilDateTime(toDateTime: NSDate, inTimeZone timeZone: NSTimeZone? = nil) -> Int {
let calendar = NSCalendar.CommonCalendar
if let timeZone = timeZone {
calendar.timeZone = timeZone
}
var fromDate: NSDate?, toDate: NSDate?
calendar.rangeOfUnit(.Day, startDate: &fromDate, interval: nil, forDate: self)
calendar.rangeOfUnit(.Day, startDate: &toDate, interval: nil, forDate: toDateTime)
let difference = calendar.components(.Day, fromDate: fromDate!, toDate: toDate!, options: [])
return difference.day
}
I can manually subtract day components from firstDateComponents and secondDateComponents which I don't want to do as I have to look for edge cases of 31 and 28 and so on.
Any help is appreciated. Thanks.
UPDATE
First date is 2017-02-10 16:15:00 +0000
Second date is 2017-02-11 03:20:00 +0000 Both are UTC.
firstTimeZone String "Asia/Shanghai"
secondTimeZone String "America/Los_Angeles"
So the day difference is -1 Day. Basically I am implementing flight status and you can see the following link as the flight lands 1 day prior to take day. As it flies from West to East.
https://www.google.co.in/webhp?sourceid=chrome-instant&ion=1&espv=2&ie=UTF-8#q=ua+890
Also a description of Date components.
Printing description of firstDateComponents:
<NSDateComponents: 0x600000142310>
Calendar: <CFCalendar 0x60000088b6d0 [0x10c5d3df0]>{identifier = 'gregorian'}
TimeZone: Asia/Shanghai (GMT+8) offset 28800
Era: 1
Calendar Year: 2017
Month: 2
Leap month: no
Day: 11
Hour: 0
Minute: 15
Second: 0
Nanosecond: 0
Quarter: 0
Year for Week of Year: 2017
Week of Year: 6
Week of Month: 2
Weekday: 7
Weekday Ordinal: 2
Printing description of secondDateComponents:
<NSDateComponents: 0x60000014b8f0>
Calendar: <CFCalendar 0x60000049b620 [0x10c5d3df0]>{identifier = 'gregorian'}
TimeZone: America/Los_Angeles (PST) offset -28800
Era: 1
Calendar Year: 2017
Month: 2
Leap month: no
Day: 10
Hour: 19
Minute: 20
Second: 0
Nanosecond: 0
Quarter: 0
Year for Week of Year: 2017
Week of Year: 6
Week of Month: 2
Weekday: 6
Weekday Ordinal: 2
This is an odd case. You're looking for the difference in calendar dates between two Dates when those dates are evaluated in a specific time zone.
I did some playing, and came up with code that works for dates that fall in the same year:
let date = Date()
guard let nycTimeZone = TimeZone(abbreviation: "EST"),
let nzTimeZone = TimeZone(abbreviation: "NZDT") else {
fatalError()
}
var nycCalendar = Calendar(identifier: .gregorian)
nycCalendar.timeZone = nycTimeZone
var nzCalendar = Calendar(identifier: .gregorian)
nzCalendar.timeZone = nzTimeZone
let now = Date()
let nycDayOfYear = nycCalendar.ordinality(of: .day, in: .year, for: now)
var nzDayOfYear = nzCalendar.ordinality(of: .day, in: .year, for: now)
I'm using New York and Aukland, NZ as my time zones because as of the time of this writing, those zones are on different dates.
As of now (~12:00 PM on Feb 11, 2017 in US Eastern Standard Time (UTC - 5) the code above gives
nycDayOfYear = 42
and
nzDayOfYear = 43
It would take some work to make that calculation work across year boundaries.
Curiously, the following code:
var nzDayOfEra = nzCalendar.ordinality(of: .day, in: .era, for: now)
let nycDayOfEra = nycCalendar.ordinality(of: .day, in: .era, for: now)
Gives the same value for both NZ and NYC. I'm not sure why.
EDIT:
Ok, I did some experimenting and got code that works. What I do is to convert both dates to month/day/year date components using a calendar set to the local time zone for each time. Then I use a method dateComponents(_:from:to:) to calculate the difference between those 2 DateComponents, in days:
import UIKit
let date = Date()
guard let nycTimeZone = TimeZone(abbreviation: "EST"),
let nzTimeZone = TimeZone(abbreviation: "NZDT") else {
fatalError()
}
var nycCalendar = Calendar(identifier: .gregorian)
nycCalendar.timeZone = nycTimeZone
var nzCalendar = Calendar(identifier: .gregorian)
nzCalendar.timeZone = nzTimeZone
let now = Date()
let nycDateComponents = nycCalendar.dateComponents([.month, .day, .year], from: now)
let nzDateComponents = nzCalendar.dateComponents([.month, .day, .year], from: now)
let difference = Calendar.current.dateComponents([.day],
from: nycDateComponents,
to: nzDateComponents)
let daysDifference = difference.days
As of this writing that gives a daysDifference of 1. Since we're using the dateComponents(_:from:to:) function, it takes care of the math to calculate the number of days difference between the 2 month/day/year DateComponents.
A NSDate represents a moment in time. It has no time zone. Only string representations of dates have time zone information.
If you have the dates, just take the number of days between them. Don't worry about time zones.
Usually:
let difference = calendar.components(.Day, fromDate: self, toDate: toDate!, options: [])
return difference.day
should be enough.
Have you tried using let interval = laterDate.timeIntervalSinceDate(earlierDate)? This will return the difference between the two dates in seconds.

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)

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

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

Resources