Reputation: 73034
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
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
*/
Upvotes: 0
Views: 320
Reputation: 386018
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
Upvotes: 1