What's wrong with my usage of `enumerateDates`? - swift

I'm trying to use enumerateDates(startingAfter:matching:matchingPolicy:repeatedTimePolicy:direction:using:) to list all first days of months between two dates. However, I find it didn't return the last date as what I expected.
Here's the code snippet. I set the endDate as a random time on 2023/1/1. I thought it should also include 2023/1/1 in its output, but seems it ignored it. Is it an time zone issue because I'm in UTC+8?
let calendar = Calendar.current
let startDate = calendar.date(from: DateComponents(year: 2022, month: 10, day: 2))!
let endDate = calendar.date(from: DateComponents(year: 2023, month: 1, day: 1, hour: 12))!
let firstDayOfMonth = DateComponents(day: 1)
calendar.enumerateDates(startingAfter: endDate, matching: firstDayOfMonth, matchingPolicy: .strict, direction: .backward) { result, exactMatch, stop in
if let date = result {
if date < startDate {
stop = true
} else {
print(date)
}
}
}
The output:
// I expect a `2022-12-31 16:00:00 +0000` here!
2022-11-30 16:00:00 +0000
2022-10-31 16:00:00 +0000

Related

Find dates in current month corresponding to a given weekday

I am currently making a scheduling feature for my application. I have a view where a user is able to select what days of the week they will be available for. Now, I am looking for a way to generate an array of dates for the given weekdays for the upcoming month.
In other words, if the user selects that they will be available on Monday, the function needs to return all dates that satisfy the given predicament for the month ahead (4 date objects).
Here's what I have tried but it doesn't seem to work:
var masterSchedule = [WorkDay(weekDay: 1, startTime: 8, endTime: 16, busyHours: []), WorkDay(weekDay: 4, startTime: 8, endTime: 16, busyHours: [])]
func getDates() {
for workDay in masterSchedule {
var components = DateComponents()
components.weekday = workDay.weekDay
components.year = 2021
components.month = 8
let date = Calendar.current.date(from: components) ?? Date()
availableDays.append(date)
}
}
For some reason, the output returns 2 same dates of 2021-07-31 23:00:00 +0000.
Can someone help me?
Here's an example that prints all the Mondays in October
import UIKit
import SwiftUI
import PlaygroundSupport
let calendar = Calendar.current
let formatter = DateFormatter()
formatter.dateStyle = .short
let firstOfOctober = formatter.date(from: "10/1/2021")!
if let interval = calendar.dateInterval(of: .month, for: firstOfOctober) {
let mondays = DateComponents(weekday: 2)
calendar.enumerateDates(startingAfter: firstOfOctober,
matching: mondays, matchingPolicy: .previousTimePreservingSmallerComponents) {
date, exactMatch, stopLooking in
if let date = date {
if date < interval.end {
print(formatter.string(from: date))
} else {
stopLooking = true
}
}
}
}

Swift compare between time

I have the current time, I need to check if the current time is between two times.
But I'm having trouble, as you can see startDate and endDate print past dates.
Can you give me a hand?
func getDate() -> Bool {
let start = "07:00"
let end = "19:00"
let dateFormat = "HH:mm"
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = dateFormat
let startDate = dateFormatter.date(from: start)
let endDate = dateFormatter.date(from: end)
let currentDate = Date()
guard let startDate = startDate, let endDate = endDate else {
fatalError("Date Format does not match ⚠️")
}
print(startDate < currentDate && currentDate < endDate)
print(startDate) //2000-01-01 06:00:00 +0000
print(endDate) //2000-01-01 22:59:00 +0000
print(currentDate) //2021-07-13 22:11:05 +0000
return startDate < currentDate && currentDate < endDate
}
You just need to set your DateFormatter defaultDate to the start of the current date. If you would like to allow it to work with midnight (24:00) time as well you just need to set the date formatter isLenient to true. Note that if you create your date formatter inside your method it will create a new date formatter every time you call this method:
extension Formatter {
static let time: DateFormatter = {
let formatter = DateFormatter()
formatter.locale = .init(identifier: "en_US_POSIX")
formatter.dateFormat = "HH:mm"
formatter.defaultDate = Calendar.current.startOfDay(for: Date())
formatter.isLenient = true
return formatter
}()
}
func isTimeBetween(start: String, end: String) -> Bool {
Formatter.time.defaultDate = Calendar.current.startOfDay(for: Date())
guard
let start = Formatter.time.date(from: start),
let end = Formatter.time.date(from: end) else {
print("invalid time input")
return false
}
print(start.description(with: .current)) // Tuesday, July 13, 2021 at 11:00:00 PM
print(end.description(with: .current)) // Wednesday, July 14, 2021 at 12:00:00 AM
print(Date().description(with: .current)) // Tuesday, July 13, 2021 at 11:42:02 PM
return start...end ~= Date()
}
isTimeBetween(start: "23:00", end: "24:00") // true
This will print:
Tuesday, July 13, 2021 at 11:00:00 PM Brasilia Standard Time
Wednesday, July 14, 2021 at 12:00:00 AM Brasilia Standard Time
Tuesday, July 13, 2021 at 11:42:02 PM Brasilia Standard Time
You can use Calendar.current.date(bySetting...) to set the hour/second/minute of an existing date. Then, compare those results.
func getDate() -> Bool {
let currentDate = Date()
let startDate = Calendar.current.date(bySettingHour: 7, minute: 0, second: 0, of: currentDate)
let endDate = Calendar.current.date(bySettingHour: 19, minute: 0, second: 0, of: currentDate)
guard let startDate = startDate, let endDate = endDate else {
fatalError("Date creation failed ⚠️")
}
print(startDate < currentDate && currentDate < endDate)
print(startDate)
print(endDate)
print(currentDate)
return startDate < currentDate && currentDate < endDate
}

How to get dates for every Friday between two dates?

I currently use the following code to return an array of dates for every single day between two dates, including today's date and the last date itself. This works great.
However, how would I go about modifying what I'm already doing in
order to do the same exact thing otherwise, but instead return an
array of the date of every Friday between the dates? For example, if the function was called on Wed Oct 23rd 2019 to return every Friday until November 10th, the first date would be Fri the 25th, Nov 1st, and then Nov 8th.
How would I do the same thing as above but for the 1st of every month? If I called the function on Wed Oct 23rd 2019 to return the first of every month until December 16th. The array should have Nov 1st and Dec 1st in it.
func dates(for date: String) -> [String] {
// first get the endDate
guard var endDate = Formatter.date.date(from: date) else { return [] }
// for calendrical calculations you should use noon time
endDate = Calendar.current.date(bySettingHour: 12, minute: 0, second: 0, of: endDate)!
// lets get todays noon time to start
var date = Calendar.current.date(bySettingHour: 12, minute: 0, second: 0, of: Date())!
var dates: [String] = []
// while date less than or equal to end date
while date <= endDate {
// add the formatted date to the array
dates.append( Formatter.date.string(from: date))
// increment the date by one day
date = Calendar.current.date(byAdding: .day, value: 1, to: date)!
}
return dates
}
You just need to add a weekday parameter to your method and check if the weekday of the date inside the loop before adding it to your array:
extension Formatter {
static let date = DateFormatter()
}
func dates(for date: String, weekday: Int? = nil) -> [String] {
Formatter.date.locale = Locale(identifier: "en_US_POSIX")
Formatter.date.dateFormat = "yyyy-MM-dd"
// first get the endDate
guard var endDate = Formatter.date.date(from: date) else { return [] }
// for calendrical calculations you should use noon time
endDate = Calendar.current.date(bySettingHour: 12, minute: 0, second: 0, of: endDate)!
// lets get todays noon time to start
var date = Calendar.current.date(bySettingHour: 12, minute: 0, second: 0, of: Date())!
var dates: [String] = []
// while date less than or equal to end date
while date <= endDate {
if weekday == nil {
dates.append(Formatter.date.string(from: date))
date = Calendar.current.date(byAdding: .day, value: 1, to: date)!
} else if let weekday = weekday, Calendar.current.component(.weekday, from: date) == weekday {
// add the formatted date to the array
dates.append(Formatter.date.string(from: date))
date = Calendar.current.date(byAdding: .weekOfYear, value: 1, to: date)!
} else {
date = Calendar.current.date(byAdding: .day, value: 1, to: date)!
}
}
return dates
}
dates(for: "2019-12-25") // ["2019-10-23", "2019-10-24", "2019-10-25", "2019-10-26", "2019-10-27", "2019-10-28", "2019-10-29", "2019-10-30", "2019-10-31", "2019-11-01", "2019-11-02", "2019-11-03", "2019-11-04", "2019-11-05", "2019-11-06", "2019-11-07", "2019-11-08", "2019-11-09", "2019-11-10", "2019-11-11", "2019-11-12", "2019-11-13", "2019-11-14", "2019-11-15", "2019-11-16", "2019-11-17", "2019-11-18", "2019-11-19", "2019-11-20", "2019-11-21", "2019-11-22", "2019-11-23", "2019-11-24", "2019-11-25", "2019-11-26", "2019-11-27", "2019-11-28", "2019-11-29", "2019-11-30", "2019-12-01", "2019-12-02", "2019-12-03", "2019-12-04", "2019-12-05", "2019-12-06", "2019-12-07", "2019-12-08", "2019-12-09", "2019-12-10", "2019-12-11", "2019-12-12", "2019-12-13", "2019-12-14", "2019-12-15", "2019-12-16", "2019-12-17", "2019-12-18", "2019-12-19", "2019-12-20", "2019-12-21", "2019-12-22", "2019-12-23", "2019-12-24", "2019-12-25"]
dates(for: "2019-12-25", weekday: 6) // ["2019-10-25", "2019-11-01", "2019-11-08", "2019-11-15", "2019-11-22", "2019-11-29", "2019-12-06", "2019-12-13", "2019-12-20"]
func firstDayOfTheMonth(until date: String) -> [String] {
Formatter.date.locale = Locale(identifier: "en_US_POSIX")
Formatter.date.dateFormat = "yyyy-MM-dd"
guard let endDate = Formatter.date.date(from: date) else { return [] }
var date = Date()
var dates: [String] = []
// while date less than or equal to end date
while let firstDayOfTheMonth = Calendar.current.nextDate(after: date, matching: .init(day: 1), matchingPolicy: .nextTime), firstDayOfTheMonth <= endDate {
dates.append(Formatter.date.string(from: firstDayOfTheMonth))
date = firstDayOfTheMonth
}
return dates
}
firstDayOfTheMonth(until: "2019-12-25") // ["2019-11-01", "2019-12-01"]
You can get the weekday for any day of the week with:
let weekDayIndex = Calendar.current.component(.weekday, from: Date())
Friday happens to be day 5. You can get the name of any day with:
print(Calendar.current.weekdaySymbols[weekDayIndex])
So just loop over all of your dates and filter out anything where the weekday is not 5 and you have your answer:
func fridays(in dates: [Date]) {
dates.filter { Calendar.current.component(.weekday, from: $0) == 5 }
}

Setting calendar unit month in swift increase the year

The following code was supposed to set the current year to its first day:
import UIKit
private func getYearStartDate() -> NSDate {
let calendar = NSCalendar.currentCalendar()
var date = calendar.dateBySettingHour(0, minute: 0, second: 0, ofDate: NSDate(), options: [])
date = calendar.dateBySettingUnit(.Month, value: 1, ofDate: date!, options: [])
date = calendar.dateBySettingUnit(.Day, value: 1, ofDate: date!, options: [])
return date!
}
print("startdate=\(getYearStartDate())")
print("enddate=\(NSDate())")
But when I inspect it, the value is set to the first day of the NEXT year.
My result:
startdate=2017-01-01 02:00:00 +0000
enddate=2016-09-06 13:44:06 +0000
Result I expect:
startdate=2016-01-01 02:00:00 +0000
enddate=2016-09-06 13:44:06 +0000
The method nextDateAfterDate:matchingComponents:options: of NSCalendar is more suitable for that purpose, it can also search backwards for the past January 1st:
let calendar = NSCalendar.currentCalendar()
let components = NSDateComponents()
components.month = 1
components.day = 1
let yearStartDate = calendar.nextDateAfterDate(NSDate(),
matchingComponents: components,
options: [.MatchNextTime, .SearchBackwards])

Generating the start and end of the month. Swift 2

I have the following code:
func rangeOfPeriod(period: NSCalendarUnit, date: NSDate) -> (NSDate, NSDate) {
let calendar = NSCalendar.currentCalendar()
var startDate: NSDate? = nil
var duration: NSTimeInterval = 0
calendar.rangeOfUnit(period, startDate: &startDate, interval: &duration, forDate: date)
let endDate = startDate!.dateByAddingTimeInterval(duration - 1)
return (startDate!, endDate)
}
When i print the following lines:
print("Day test = \(rangeOfPeriod(.Day, date: NSDate()))")
print("Week test = \(rangeOfPeriod(.WeekOfYear, date: NSDate()))")
print("month test = \(rangeOfPeriod(.Month, date: NSDate()))")
print("Year test = \(rangeOfPeriod(.Year, date: NSDate()))")
All of them work as expected apart from month.
I get 'month test = (2016-03-01 00:00:00 +0000, 2016-03-31 22:59:59 +0000)' as a result and it seems to be missing a hour. For the time for all the others i get '2016-03-26 23:59:59 +0000'.
Any help would be appreciated
Maybe there is a confusion about the interval parameter in the rangeOfUnit method.
From the documentation
func rangeOfUnit(_ unit: NSCalendarUnit,
startDate datep: AutoreleasingUnsafeMutablePointer<NSDate?>,
interval tip: UnsafeMutablePointer<NSTimeInterval>,
forDate date: NSDate) -> Bool
.....
tip : Upon return, contains the duration (as NSTimeInterval) of the calendar unit unit that
contains the date date
For this month (March 2016) due to Daylight Saving Time change the duration is
31 * 86400.0 - 3600.0 = 2674800.0
That means there is an hour missing, but it's not specified when.
To get the end of the month this is more accurate
func endOfThisMonth() -> NSDate
{
let calendar = NSCalendar.currentCalendar()
let components = NSDateComponents()
components.day = 1
let startOfNextMonth = calendar.nextDateAfterDate(NSDate(), matchingComponents: components, options: .MatchNextTime)!
return calendar.dateByAddingUnit(.Second, value: -1, toDate: startOfNextMonth, options: [])!
}