Get the current week number in month from Date - swift

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

Related

ISO8601dateformatter 1970-01-01 00:00:00 issue

How to deal with the new year and ISO8601 returning last year as year component.
To my horror, I realized ISO8601DateFormatter was returning 1977 as a year to the 1978-01-01 00:00:00
It took a while to realize this. That turned out is not wrong. Nonetheless, given the specific year of 1978, for the formatted to return 1977 is shocking.
I don't even need the timestamp. How can I reliably retrieve the specified year without having to add a second to every calendar date?
import Foundation
let datestring = "1978/1/1"
var formatter = ISO8601DateFormatter()
formatter.timeZone = TimeZone(abbreviation: "UTC")
formatter.formatOptions = [.withFullDate]
let date2 = formatter.date(from: datestring) ?? Date()
print(date2)
var calendar = Calendar(identifier: .iso8601)
var year = calendar.component(.year, from: date2)
var month = calendar.component(.month, from: date2)
var day = calendar.component(.day, from: date2)
var era = calendar.component(.era, from: date2)
print("year \(year) month \(month) day \(day) era: \(era)")
===
1978-01-01 00:00:00 +0000
year 1977 month 12 day 31 era: 1
By default the Calendar instance will have your local timeZone. You can see this by printing print(calendar.timeZone.abbreviation() ?? "UNKNOWN"). In my case (in Seattle, WA, USA) it prints "PDT". If you simply set your calendar timezone to UTC it prints exactly what you expect:
year 1978 month 1 day 1 era: 1

Changing visible months in a Calendar Swift

I am working with Calendar and I am able to get some things in that for this current year, I have Jan, feb, march, April, may, June, July ... up to December. But now since we are in May, I do not want to see June, July, Aug .... up to December. Rather I want to see Jan, feb, march, April, may and be able to scroll backwards to probably ... Nov, 2020, Dec, 2020 With the current implementation of Calendar I do not understand how to pick this out.
This is what I use to get my Months currently private let months = Calendar.current.shortMonthSymbols.map { $0.uppercased() }
Which results into Jan, feb, march, April, may, June, July ... up to December how can I change this behaviour to reflect what I want?
Use an enumerator on the array and then use prefix(while:) to get all months up to the current one.
let thisMonth = Calendar.current component(.month, from: Date())
let currentYearMonths = Calendar.current.shortMonthSymbols.enumerated()
.prefix(while: { $0.offset < thisMonth })
.filter { $0.offset < thisMonth }
.map(\.element.localizedUppercase)
For the previous year part, calculate last year and then simply join the year to the elements of .shortMonthSymbols using map
let aYearAgo = Calendar.current.date(byAdding: .year, value: -1, to: Date())!
let lastYear = Calendar.current.component(.year, from: aYearAgo)
let lastYearMonths = Calendar.current.shortMonthSymbols.map { "\($0.localizedUppercase), \(lastYear)"}

Swift/SwiftUI: week/date management in swift

I'm working on fitness app where users selects the days he wants to exercise on.
When he opens the app I wanna shown him the current week where he can observe the days his training sessions are scheduled for.
If he is from the US i wanna show him a week starting from Sunday. For EU users it should start with Monday.
Is there any way to get the "current" week dates depending on user's location/geo? Taking into account what day does the week start with in appropriate location.
I tried to find a solution for your question. I think this should work:
// Define a function that returns the following seven dates, given a start date
func getWeekDates(of startDate: Date, with calender: Calendar) -> [Date] {
var weekDates: [Date] = []
for i in 0..<7 {
weekDates.append(calendar.date(byAdding: .day, value: i, to: startDate)!)
}
return weekDates
}
// This should automatically take the right calendar for the user's locale
// If you want to specify the day weeks start with manually, choose .gregorian or .iso8601:
// .gregorian starts on Sunday, .iso8601 starts on Monday
let calendar = Calendar.current
let startOfCurrentWeek = calendar.date(from: calendar.dateComponents([.yearForWeekOfYear, .weekOfYear], from: Date()))
let currentWeekDates = getWeekDates(of: startOfCurrentWeek!, with: calendar)
Hope this helps.
Use
Calendar.current.firstWeekday
If it returns 1, then Sunday is the first day of week
If it returns 2, then Monday is the first day of week.
You can test this by setting locale manually
var calendar = Calendar.current
calendar.locale = Locale(identifier: "en_GB")
print("\(calendar.locale!) starts on day \(calendar.firstWeekday)")
// en_GB starts on day 2
calendar.locale = Locale(identifier: "en_US")
print("\(calendar.locale!) starts on day \(calendar.firstWeekday)")

How to ask a user for an accurate date in a date format

So I have a program which I need to ask a user for a date and time. But that date has to either be in the future or the the very far past. And I have tried a UIDatePicker but have found out that a date picker isn't the best way to tend to this sort of problem because it requires a lot of scrolling, and the it doesn't load past a certain date for some reason. I was thinking of a UIPickerView that asks for Year, Month, Day, and Time. But I don't know how to do it because lets say that the month is September there is only 30 days so then how can I stop the user from selecting 31. So if anyone can help me that would be amazing. And if you have any questions or need for code just ask. Oh and by the way I would like it in a date format where I can compare it to other date.
Given the year and month, you can calculate the array of possible day values to be supplied to your picker like so:
let calendar = Calendar.current
let components = DateComponents(year: year, month: month, day: 1)
guard let date = calendar.date(from: components),
let range = calendar.range(of: .day, in: .month, for: date) else { return }
let days = Array(range)
I would like it in a date format where I can compare it to other date.
Once you have the year, month, and day, hour (converted to 24 hour value, from 0 to 23), you can build the Date object like so:
let calendar = Calendar.current
let components = DateComponents(year: year, month: month, day: day, hour: hour, minute: minute)
guard let date = calendar.date(from: components) else { return }
That date can now be compared to any other Date object.

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