2

I have to present time trackings summary for specific payroll frequency periods: monthly, weekly, biweekly. I did find a way to get start date and end date for current week or current month ex.

var startOfMonth: Date? {
    let components = Calendar.current.dateComponents([.year, .month], from: startOfDay)
    return Calendar.current.date(from: components)
}

var endOfMonth: Date? {
    guard let startOfTheMonth = startOfMonth else { return nil }
    var components = DateComponents()
    components.month = 1
    components.second = -1
    return Calendar.current.date(byAdding: components, to: startOfTheMonth)
}

But how can I get those dates for current biweekly period?

I assume that biweekly periods (it is 26 paychecks a year) are every two weeks since start of current year: ex. 1st biweek is 1-2 week of the 2020 year is 1.01.20-12.01.20; 24th biweek is 47-48 week is 16.11.20-29.11.20 and last 26th biweek is 51-53 week is 14.12.20-31.12.20.

EDIT:

Based on @Leo Dabus answer I managed to get startOfBiweek and endOfBiweek. Thank you!

var startOfBiweek: Date? {
    let biweekIntervals = biweekIntervalsInSameYear(using: .iso8601)
    return biweekIntervals.first(where: { $0.contains(self)})?.start
}

var endOfBiweek: Date? {
    let biweekIntervals = biweekIntervalsInSameYear(using: .iso8601)
    return biweekIntervals.first(where: { $0.contains(self)})?.end
}
Leo Dabus
  • 229,809
  • 59
  • 489
  • 571
vandermesis
  • 63
  • 1
  • 8
  • Not related to your question but you don't need startOfDay to get startOfMonth. Regarding the endOfMonth if you use a date range there is no need to subtract a second. The month ends when the next month starts – Leo Dabus Nov 26 '20 at 01:41
  • @LeoDabus 1. Could you give me example with start/end of the month based on Date Range? 2. AFAIK Biweekly payment frequency is 26 payments in year. I believe it will be always problem with first and last week of year. – vandermesis Nov 26 '20 at 02:03
  • Do you want your weeks to always start on Monday? – Leo Dabus Nov 26 '20 at 02:18
  • Yes. Start - Monday, End - Sunday – vandermesis Nov 26 '20 at 02:21

1 Answers1

1

I don't know if there is an easier way to accomplish what you want but this is what I came up with. First I get all days in the same year. Then I create an array with 26 subarrays and group dates for every 2 weeks. Then I create an array of date intervals using the first and last date of each subarray.

extension Calendar {
    static let iso8601 = Calendar(identifier: .iso8601)
}
extension Date {
    func dayAfter(using calendar: Calendar = .current) -> Date {
        calendar.date(byAdding: .day, value: 1, to: noon(using: calendar))!
    }
    func noon(using calendar: Calendar = .current) -> Date  {
        calendar.date(bySettingHour: 12, minute: 0, second: 0, of: self)!
    }
    func startOfNextDay(using calendar: Calendar = .current) -> Date {
        calendar.startOfDay(for: dayAfter(using: calendar))
    }
    func lastSecondOfDay(using calendar: Calendar = .current) -> Date {
        calendar.date(byAdding: DateComponents(second: -1), to: calendar.startOfDay(for: dayAfter(using: calendar)))!
    }
    func weekOfYear(using calendar: Calendar = .current) -> Int { calendar.component(.weekOfYear, from: self) }
    func year(using calendar: Calendar = .current) -> Int { calendar.component(.year, from: self) }
    func month(using calendar: Calendar = .current) -> Int { calendar.component(.month, from: self) }
    func allDaysInSameYear(using calendar: Calendar = .current) -> [Date] {
        calendar.range(of: .day, in: .year, for: self)!.map {
            DateComponents(calendar: calendar,year: year(using: calendar), day: $0).date!
        }
    }
    func biweeksInSameYear(using calendar: Calendar = .current) -> [[Date]] {
        allDaysInSameYear(using: calendar).reduce(into: .init(repeating: [], count: 26)) {
            let weekOfYear = $1.weekOfYear(using: calendar)-1
            let index = weekOfYear > 51 ? 25 : weekOfYear / 2
            $0[index].append($1)
        }
    }
    func biweekIntervalsInSameYear(using calendar: Calendar = .current) -> [DateInterval] {
        biweeksInSameYear(using: calendar).map {
            DateInterval(start: $0.first!, end: $0.last!.lastSecondOfDay(using: calendar))
        }
    }
}

let biweekIntervals = Date().biweekIntervalsInSameYear(using: .iso8601)
for interval in biweekIntervals {
    print(interval.start.description(with: .current), interval.end.description(with: .current), terminator: "\n")
} 

This will print:

Wednesday, 1 January 2020 00:00:00 Sunday, 12 January 2020 23:59:59
Monday, 13 January 2020 00:00:00 Sunday, 26 January 2020 23:59:59
Monday, 27 January 2020 00:00:00 Sunday, 9 February 2020 23:59:59
Monday, 10 February 2020 00:00:00 Sunday, 23 February 2020 23:59:59
Monday, 24 February 2020 00:00:00 Sunday, 8 March 2020 23:59:59
Monday, 9 March 2020 00:00:00 Sunday, 22 March 2020 23:59:59
Monday, 23 March 2020 00:00:00 Sunday, 5 April 2020 23:59:59
Monday, 6 April 2020 00:00:00 Sunday, 19 April 2020 23:59:59
Monday, 20 April 2020 00:00:00 Sunday, 3 May 2020 23:59:59
Monday, 4 May 2020 00:00:00 Sunday, 17 May 2020 23:59:59
Monday, 18 May 2020 00:00:00 Sunday, 31 May 2020 23:59:59
Monday, 1 June 2020 00:00:00 Sunday, 14 June 2020 23:59:59
Monday, 15 June 2020 00:00:00 Sunday, 28 June 2020 23:59:59
Monday, 29 June 2020 00:00:00 Sunday, 12 July 2020 23:59:59
Monday, 13 July 2020 00:00:00 Sunday, 26 July 2020 23:59:59
Monday, 27 July 2020 00:00:00 Sunday, 9 August 2020 23:59:59
Monday, 10 August 2020 00:00:00 Sunday, 23 August 2020 23:59:59
Monday, 24 August 2020 00:00:00 Sunday, 6 September 2020 23:59:59
Monday, 7 September 2020 00:00:00 Sunday, 20 September 2020 23:59:59
Monday, 21 September 2020 00:00:00 Sunday, 4 October 2020 23:59:59
Monday, 5 October 2020 00:00:00 Sunday, 18 October 2020 23:59:59
Monday, 19 October 2020 00:00:00 Sunday, 1 November 2020 23:59:59
Monday, 2 November 2020 00:00:00 Sunday, 15 November 2020 23:59:59
Monday, 16 November 2020 00:00:00 Sunday, 29 November 2020 23:59:59
Monday, 30 November 2020 00:00:00 Sunday, 13 December 2020 23:59:59
Monday, 14 December 2020 00:00:00 Thursday, 31 December 2020 23:59:59



Another approach, this one seems shorter but I recommend testing it further:

extension Calendar {
    static let iso8601 = Calendar(identifier: .iso8601)
}
extension Date {
    func startOfYear(using calendar: Calendar = .current) -> Date {
        calendar.dateComponents([.calendar,.year], from: self).date!
    }
    func endOfYear(using calendar: Calendar = .current) -> Date {
        startOfYear(using: calendar).adding(.init(year: 1, second: -1))!
    }
    func adding(_ components: DateComponents, wrappingComponents: Bool = false, using calendar: Calendar = .current) -> Date? {
        calendar.date(byAdding: components, to: self)
    }
    func yearForWeekOfYear(using calendar: Calendar = .current) -> Int { 
        calendar.component(.yearForWeekOfYear, from: self)
    }
    func biweekIntervalsInSameYear(using calendar: Calendar = .current) -> [DateInterval] {
        let date = DateComponents(calendar: .iso8601, weekOfYear: 1, yearForWeekOfYear: yearForWeekOfYear(using: .iso8601)).date!
        var intervals: [DateInterval] = [.init(start: startOfYear(using: .iso8601), end: date.adding( .init(second: -1, weekOfYear: 2), using: calendar)!)]
        var weekOfYear = 3
        while let start = Calendar.iso8601.nextDate(after: date, matching: DateComponents(weekOfYear: weekOfYear), matchingPolicy: .strict) {
            if intervals.count < 25 {
                intervals.append(.init(start: start, end: start.adding( .init(second: -1, weekOfYear: 2), using: calendar)!)) } else if intervals.count == 25 {
                    intervals.append(.init(start: start, end: start.endOfYear(using: .iso8601)))
            }
            weekOfYear += 2
        }
        return  intervals
    }
}
Leo Dabus
  • 229,809
  • 59
  • 489
  • 571
  • Thank you very much for your answer. This is something which could solve my problem. But it doesn't seem to work for me if time zone is CET. First interval is 2019-12-31 23:00:00 +0000 to 2020-12-31 22:59:59 +0000 and all the rest are all messed up ex. second one is 2020-01-11 23:00:00 +0000 to 2020-01-25 22:59:59 +0000 – vandermesis Nov 27 '20 at 00:57
  • @vandermesis it does work for any timezone. Can you please elaborate? – Leo Dabus Nov 27 '20 at 00:58
  • Are you printing the date? It will always display UTC timezone (+0000) – Leo Dabus Nov 27 '20 at 01:10
  • You used `interval.start.description(with: .current)` to print all intervals for the year and its printing proper dates. But if I want to get current biweek DateInterval to get start and end dates it gives me wrong dates. This is what Im trying to do: `print(biweekIntervals.first(where: { $0.contains(Date())}))` returns `2020-11-15 23:00:00 +0000 to 2020-11-29 22:59:59 +0000` which is not correct. – vandermesis Nov 27 '20 at 01:13
  • I am pretty sure it is correct. +0000 means UTC. A date has no timezone it is just a point in time. You need to use `DateFormatter` to display your current timezone representation of the date. – Leo Dabus Nov 27 '20 at 01:15
  • `if let interval = biweekIntervals.first(where: { $0.contains(Date()) }) {` `print("start", interval.start.description(with: .current))` `print("end", interval.end.description(with: .current))` `}` – Leo Dabus Nov 27 '20 at 01:19
  • this will print **start Monday, 16 November 2020 00:00:00 Brasilia Standard Time** and **end Sunday, 29 November 2020 23:59:59 Brasilia Standard Time** – Leo Dabus Nov 27 '20 at 01:20
  • Btw your timezone is `GMT+1`. I bet your time now is `02:23:16 am` – Leo Dabus Nov 27 '20 at 01:23
  • Yes my time is 2:34 now :) As I said if I use `.description(with: .current)` it prints fine but I don't want print those. I have to access those dates and use it to api request. I need something like in my example startOfTheBiweek and endOfTheBiweek. And when I start processing return from your `biweekIntervalsInSameYear` it gives me wrong dates. – vandermesis Nov 27 '20 at 01:33
  • @vandermesis again believe me the date is correct. Whatever it prints doesn't matter. Don't mess with the timezone. Just send does dates to your server and you will be fine. If you need epoch time just get the start.timeIntervalSince1970 if you need ISO8601 check this [post](https://stackoverflow.com/questions/28016578/how-can-i-parse-create-a-date-time-stamp-formatted-with-fractional-seconds-utc/28016692#28016692) – Leo Dabus Nov 27 '20 at 01:38
  • If you need UTC time is a different story. All you need is to use a custom calendar and set its timezone to UTC – Leo Dabus Nov 27 '20 at 01:40
  • Ok. Will do as you said. Thank you very much for your help. Based on your answer I did add two computed properties for startOfBiweek and endOfBiweek. I'm adding those to my question and mark your answer. Regards! – vandermesis Nov 27 '20 at 01:51
  • @vandermesis just make sure to use ISO8601 calendar (first weekday is Monday). All other calendars the first weekday is Sunday. – Leo Dabus Nov 27 '20 at 01:58