Initialising DateComponents with weekOfMonth returning wrong date - swift

Here is the code:
let components = DateComponents(year: 2022,month: 9,weekOfMonth: 3)
let date = Calendar.autoupdatingCurrent.date(from: components)!
let formatter = DateFormatter()
formatter.dateStyle = .long
formatter.timeStyle = .none
print(formatter.string(from: date))
Here is output:
September 1, 2022
But the correct answer should be September 11, 2022. Why? Is this a bug or I just missed something?

This method will create a date for start of the given year, month and week in month. This will return nil if the start of the week is not in the month specified:
func startOfWeek(in year: Int, month: Int, week: Int, calendar: Calendar = Calendar(identifier: .iso8601)) -> Date?{
//You would need to set this calendar according when your week starts.
//For me it´s monday so I need .iso8601
//For sunday set it to gregorian
var components = DateComponents(calendar: calendar, year: year, month: month, weekOfMonth: week )
//Get the first day of the month. This will probably never fail but....
guard let firstOfMonth = components.date else{
return nil
}
// Handling edge case first of month is start of week
// if the first is a monday and we look for the first return it
let startOfWeek = calendar.dateComponents([.calendar, .yearForWeekOfYear, .weekOfYear], from: firstOfMonth).date
if startOfWeek == firstOfMonth, week == 1{
return firstOfMonth
}
// else just ommit the additional week and move on
else if startOfWeek == firstOfMonth {
components = DateComponents(calendar: calendar, year: year, month: month, weekOfMonth: week)
}
// search for the next date starting at first of month according to components given
return calendar.nextDate(after: firstOfMonth, matching: components, matchingPolicy: .nextTime)
}
Most of the code should be self explanatory.
Testing:
let dates = [(2022, 1, 2), (2022, 2, 1), (2022, 3, 4), (2022, 8, 1), (2022, 9, 1), (2022, 9, 2), (2022, 9, 3), (2022, 9, 4), (2022, 12, 4) , (2021, 11, 5)]
let formatter = DateFormatter()
formatter.dateFormat = "dd MM yyyy"
dates.forEach { year, month, week in
let date = startOfWeek(in: year, month: month, week: week, calendar: Calendar(identifier: .gregorian))
print(date != nil ? formatter.string(from: date!) : "no date found")
}
Output:
02 01 2022
no date found
20 03 2022
no date found
no date found
04 09 2022
11 09 2022 <-- thrid week in september 2022
18 09 2022
18 12 2022
28 11 2021

Related

Swift date components incorrect return of date day number

I need to obtain a date from some variable values
So I specify year, month and day and I need a Date as return
Doing the following works except for the day because it return the day input - 1
let todayDate: Date = Calendar.current.startOfDay(for: Date.from(year: 2022, month: 09, day: 05)!)
print("today date = \(todayDate)")
extension Date {
static func from(year: Int, month: Int, day: Int) -> Date? {
let calendar = Calendar(identifier: .gregorian)
var dateComponents = DateComponents()
dateComponents.year = year
dateComponents.month = month
dateComponents.day = day
return calendar.date(from: dateComponents) ?? nil
}
}
And the output is
today date = 2022-09-04 22:00:00 +0000
Date and time can be a bit tricky. The Date struct stores a point in time relative to GMT. If you print it it will show exactly that.
Solution:
Don´t use print, use a proper Dateformatter. To illustrate what I mean use this in a playground:
let date = Calendar.current.startOfDay(for: Date())
print(date)
//2022-09-03 22:00:00 +0000
// when it is 4.th of september 00:00 in my timezone (+- Daylight saving) it is this time in GMT
let formatter = DateFormatter()
formatter.dateFormat = "dd MM yyyy HH:mm:ss"
print(formatter.string(from: date))
//04 09 2022 00:00:00
// this is the time in my timezone
So the issue here is not that it has the wrong time, it is just not presented in the correct time zone.

Problem with adding month value to date in Swift

I've got a problem. I'm getting start date of current year with function below:
func startOfYear() -> Date {
var calendar = Calendar(identifier: .iso8601)
let timezone = TimeZone(secondsFromGMT: 0)!
calendar.timeZone = timezone
return calendar.dateComponents([.calendar, .year], from: self).date!
}
My goal is to iterate through this date to extract data for every single month. I use this function to add value to .month to get start date of every month of year:
func getNextMonths(using calendar: Calendar = .iso8601, monthAmount: Int) -> Date {
calendar.date(byAdding: .month, value: monthAmount, to: self.startOfYear())!
}
When I iterate through this fuction and print dates I receive this:
func getMonth() {
for number in 0...12 {
print("month \(number): \(Date().getNextMonths(monthAmount: number))")
}
}
output:
month 0: 2022-01-01 00:00:00 +0000
month 1: 2022-02-01 00:00:00 +0000
month 2: 2022-03-01 00:00:00 +0000
month 3: 2022-03-31 23:00:00 +0000
month 4: 2022-04-30 23:00:00 +0000
month 5: 2022-05-31 23:00:00 +0000
month 6: 2022-06-30 23:00:00 +0000
month 7: 2022-07-31 23:00:00 +0000
month 8: 2022-08-31 23:00:00 +0000
month 9: 2022-09-30 23:00:00 +0000
month 10: 2022-11-01 00:00:00 +0000
month 11: 2022-12-01 00:00:00 +0000
month 12: 2023-01-01 00:00:00 +0000
As you can see there is a problem starting with 2022-03-31 - it supposed to return 2022-04-01 but I don't really know what I'm doing wrong and why I can't achieve this.
EDIT
I've changed fuction getNextMonths to check if it's daylight saving time and it's working now, thanks to [Joakim Danielson][1] for help.
It's silly solution but it works:
func getNextMonths(using calendar: Calendar = .iso8601, monthAmount: Int) -> Date {
if monthAmount == 0 || monthAmount == 1 || monthAmount == 2 || monthAmount == 10 || monthAmount == 11 || monthAmount == 12 {
return calendar.date(byAdding: .month, value: monthAmount, to: self.startOfYear())!
} else {
var dateComponents = DateComponents()
dateComponents.month = monthAmount
dateComponents.hour = 1
return calendar.date(byAdding: dateComponents, to: self.startOfYear())!
}
}
Hey I copied your code as an extension to Date and I got some errors, after some search I finally can run it and it gives me the beginning of each month, here's my changes hope it helps!
extension Date{
func startOfYear() -> Date {
var calendar = Calendar(identifier: .iso8601)
let timezone = TimeZone(secondsFromGMT: 0)!
calendar.timeZone = timezone
return calendar.dateComponents([.calendar, .year], from: self).date!
}
func getNextMonths(using calendar: Calendar, monthAmount: Int) -> Date {
calendar.date(byAdding: .month, value: monthAmount, to: self.startOfYear())!
}
func getMonth() {
for number in 0...12 {
print("month \(number): \(Date().getNextMonths(using: Calendar.current, monthAmount: number))")
}
}
}
Then I made a Date object like this
let d = Date()
print(d.getMonth())
Output (In Indian numbers, but it should be the same)

week range in swift with weekYear

Could you help me, I don't know what configuration I'm missing
I want to obtain the week range for the year 2021, week 37 in Mexico that starts from Monday to Sunday but it seems that my code brings another week number and also starts from Sunday to Monday
The week in Mexico starts from
Week 37 September 13, 2021 September 19, 2021
var weekNumber: Int = 0
var calendar = Calendar(identifier: .gregorian)
calendar.firstWeekday = 2
calendar.minimumDaysInFirstWeek = 4
let year = calendar.component(.year, from: Date())
weekNumber = calendar.component(.weekOfYear, from: Date())
print("weekNumber",weekNumber)
print("Date",Date())
let startComponents = DateComponents(weekOfYear: weekNumber, yearForWeekOfYear: year)
let startDate = calendar.date(from: startComponents)!
let endComponents = DateComponents(day:7, second: 0)
let endDate = calendar.date(byAdding: endComponents, to: startDate)!
print("startDate",startDate)
print("endDate",endDate)
You are using the wrong calendar. The first weekday of Gregorian calendar is Sunday, what you need is to use ISO8601 calendar. Also when using weekOfYear you would use yearForWeekOfYear:
let now = Date()
var calendar = Calendar(identifier: .iso8601)
let yearForWeekOfYear = calendar.component(.yearForWeekOfYear, from: now)
let weekNumber = calendar.component(.weekOfYear, from: now)
print("weekNumber", weekNumber)
print("Date", now.description(with: .current))
let startDate = DateComponents(calendar: calendar, weekOfYear: weekNumber, yearForWeekOfYear: yearForWeekOfYear).date!
let endDate = calendar.date(byAdding: .weekOfYear, value: 1, to: startDate)!
print("startDate", startDate.description(with: .current))
print("endDate", endDate.description(with: .current))
This will print:
weekNumber 37
Date Sunday, September 19, 2021 at 9:07:02 PM Brasilia Standard Time
startDate Monday, September 13, 2021 at 12:00:00 AM Brasilia Standard Time
endDate Monday, September 20, 2021 at 12:00:00 AM Brasilia Standard Time

Get the current week number in month from Date

I am currently facing a weird issue. I am trying to find out in which week of a month a given Date instance lies.
My code is the following:
var calendar : Calendar {
var calendar = Calendar(identifier: .iso8601)
calendar.timeZone = .UTC
return calendar
}
func generateDate(year: Int, month: Int, day: Int) -> Date {
let dateComponents = DateComponents(year: year, month: month, day: day)
return calendar.date(from: dateComponents)!
}
print(calendar.component(.weekOfMonth, from: Date.generateDate(year: 2019, month: 12, day: 1))) // prints "0"
print(calendar.component(.weekOfMonth, from: Date.generateDate(year: 2020, month: 1, day: 1))) // prints "1"
generateDate simply generates a Date with the help of calendar. When I print the both statements I get 0 and 1 as the result. In my opinion this is wrong. I would assume I get the same value for both since both dates should be in the first week of their respective month value.
Another example would be the 2. Dec 2019, this should give the second week as well as the 6. January 2020 should also give me the second week.
Does anyone know what could be wrong here or where my mistake could be ?
This is due to the 1st day of the month falling on a sunday, try march 01, 2020 if you want confirmation.
The first week of the month is chosen by Swift according to their own standards as ISO standards do not suggest any specific implementations so the Swift team went with this. You can ask them in their forums what their reasoning behind this is Wikipedia link
The logic for .weekOfMonth seems to be that if the "first" week is less than half a week, that is 3 days, then it is considered to be week 0 and otherwise week 1. But note that this is dependent on what locale is being used, for a country like Canada that has Sunday as first day of week there is never a week 0 when running the below code. So when the first day of week is Monday .weekOfMonth will return a value between 0 and 5 but when it is Sunday the range is 1 to 6.
This can be seen running the following code in a playground
let calendar = Calendar.current
let year = 2019
print("First day of week: \(calendar.weekdaySymbols[calendar.firstWeekday - 1])")
for month in 1...12 {
print(calendar.monthSymbols[month - 1])
let first = calendar.date(from: DateComponents(year: year, month: month, day: 1))!
if let range = calendar.range(of: .day, in: .month, for: first) {
var currentWeek = -1
for day in range {
let date = calendar.date(from: DateComponents(year: year, month: month, day: day))!
let week = calendar.component(.weekOfMonth, from: date)
if week > currentWeek {
currentWeek = week
let dayOfWeek = calendar.component(.weekday, from: date)
print("Week# \(week), weekday \(calendar.weekdaySymbols[dayOfWeek - 1])")
}
}
}
}
It seems to me if you want whatever day it is on the 1st to be the first day of weekOfMonth = 1 then you need to write your own code for this

Subtracting 2 Dates using DateComponents gives undefined behavior

Given the following method I added to the Date object in an extension:
extension Date {
static func -(left: Date, right: Date) -> DateComponents {
let components: Set<Calendar.Component> = [
.year,
.month,
.day,
.hour,
.minute,
.second,
//.weekOfYear, // issue!
]
let gregorian = Calendar(identifier: .gregorian)
let dateComponents = gregorian.dateComponents(components, from: right, to: left)
return dateComponents
}
}
And given the following dates (format is Month Day, Year, hh:mm:ss) you get 1:
let day1 = Date(timeIntervalSince1970: 1516003200) // January 15, 2018, 00:00:00
let day2 = Date(timeIntervalSince1970: 1516089600) // January 16, 2018, 00:00:00
let diff = day2 - day1
print(diff.day!) // 1
But given the following dates, you also get 1:
let day3 = Date(timeIntervalSince1970: 1515916800) // January 14, 2018, 00:00:00
let day4 = Date(timeIntervalSince1970: 1516089600) // January 16, 2018, 00:00:00
let diff2 = day4 - day3
print(diff.day!) // 1
So my first question is wondering why the day difference is the same even though one of the pair is 1 day apart and the other is 2 days apart.
Finally, given the following dates, you get the correct number of 14:
let day5 = Date(timeIntervalSince1970: 1516089600) // January 16, 2018, 00:00:00
let day6 = Date(timeIntervalSince1970: 1517299200) // January 30, 2018, 00:00:00
let diff3 = day6 - day5
print(diff3.day!)
But if you were to go back to static func -(left:right:) and uncomment .weekOfYear and re run the above block of code with day5 and day6 you get 0 days. So my second question is why I'm getting 0 days if I add the .weekOfYear component to the Set.
I included two screenshots of a playground below, first one with .weekOfYear not included and the second one including .weekOfYear:
In the following code
let day3 = Date(timeIntervalSince1970: 1515916800) // January 14, 2018, 00:00:00
let day4 = Date(timeIntervalSince1970: 1516089600) // January 16, 2018, 00:00:00
let diff2 = day4 - day3
print(diff.day!) // 1
you are using diff.day when in fact you should be using diff2.day
As for your second issue, the calendar will return date components by assigning from the maximum to minimum. That is to say that when you add weekOfYear, it assigns a value of 2 to weekOfYear. If you were to print out diff3.weekOfYear, you will get 2 (because the dates come up to be 2 weeks). Since there are no days left behind after the date is converted into weeks, you get 0 days for diff3.day. If you were to use the dates Jan 16th and Jan 31st, you would get a value of 2 for week and 1 for day