I noticed that when comparing two instances of a Swift Date with ==, they qualify as the same date when the difference in DateComponents.nanoseconds is less than 30. For example:
let calendar = Calendar(identifier: .gregorian)
let startComps = DateComponents(year: 2017, month: 1, day: 1, hour: 0, minute: 0, second: 0, nanosecond: 0)
let endComps = DateComponents(year: 2017, month: 1, day: 1, hour: 0, minute: 0, second: 0, nanosecond: 29)
let startDate = calendar.date(from: startComps)!
let endDate = calendar.date(from: endComps)!
print(startDate == endDate)
//prints true, changing 29 to 30 prints false
The behavior is the same if comparing with startDate.compare(endDate) == .orderedSame. I couldn't find any mention of this in the docs or headers. Is there a logical reason for 30 nanoseconds to be the cutoff for equality?
Dates are represented internally as a Double. Doubles have a limited precision. Your first date there has an internal value of 504950400.000000000000000 (when printed at 15 decimal places). The next higher representable value is 504950400.000000059604645. It just so happens that your end date, if you set the nanoseconds to 30, is 504950400.000000059604645 (which actually corresponds to a nanoseconds value of 59).
Which is to say, in January of 2017, Date can only distinguish between 59-nanosecond intervals. Meanwhile back in January of 2001, Date can distinguish every single nanosecond. And in 2027 it will distinguish between 119-nanosecond intervals.
Related
Usually, during end of day light saving, we will be gaining extra 1 hours.
Take Tehran timezone as an example.
During 22 September 2021, Tehran will backward by 1 hour from 00:00
AM, to 11:00 PM.
I wrote the following code to demonstrate such.
import UIKit
func date(year: Int, month: Int, day: Int, hour: Int, minute: Int, second: Int) -> Date {
var dateComponents = DateComponents()
dateComponents.year = year
dateComponents.month = month
dateComponents.day = day
dateComponents.hour = hour
dateComponents.minute = minute
dateComponents.second = second
let date = Calendar.current.date(from: dateComponents)!
return date
}
// During 22 September 2021, Tehran will backward by 1 hour from 00:00 AM, to 11:00 PM.
let tehranTimeZone = TimeZone(identifier: "Asia/Tehran")!
let oldDefault = NSTimeZone.default
NSTimeZone.default = tehranTimeZone
defer {
NSTimeZone.default = oldDefault
}
let date1 = date(year: 2021, month: 09, day: 21, hour: 23, minute: 59, second: 59)
let date2 = date(year: 2021, month: 09, day: 22, hour: 00, minute: 00, second: 00)
let date3 = date(year: 2021, month: 09, day: 22, hour: 00, minute: 00, second: 01)
// STEP 1: 2021 Sep 21 23:59:59 => 1632252599.0, Tuesday, September 21, 2021 at 11:59:59 PM Iran Daylight Time
print("STEP 1: 2021 Sep 21 23:59:59 => \(date1.timeIntervalSince1970), \(date1.description(with: .current))")
// STEP 2: 2021 Sep 22 00:00:00 => 1632256200.0, Wednesday, September 22, 2021 at 12:00:00 AM Iran Standard Time
print("STEP 2: 2021 Sep 22 00:00:00 => \(date2.timeIntervalSince1970), \(date2.description(with: .current))")
// STEP 3: 2021 Sep 22 00:00:01 => 1632256201.0, Wednesday, September 22, 2021 at 12:00:01 AM Iran Standard Time
print("STEP 3: 2021 Sep 22 00:00:01 => \(date3.timeIntervalSince1970), \(date3.description(with: .current))")
From STEP 1 transits to STEP 2, instead for their timeIntervalSince1970 different by +1 seconds, their difference are +3601 seconds, due to the extra 1 hour gain.
Now, my question is, how can we use DateComponents to represent the extra 1 hour period at the end of day light saving?
In another, how can I use DateComponents to generate a Date which is capable to print the following?
2021 Sep 21 23:00:00 => 1632252600.0, Tuesday, September 21, 2021 at 11:00:00 PM Iran Standard Time
Now, we understand that, in Tehran, during 2021 Sept 21, there are 2 type of 23:00:00 time
23:00:00 Iran Daylight time (Epoch is 1632249000)
23:00:00 Iran Standard time (Epoch is 1632252600)
23:00:00 Iran Daylight time (Epoch is 1632249000)
I can represent the above using
let date = date(year: 2021, month: 09, day: 21, hour: 23, minute: 00, second: 00)
23:00:00 Iran Standard time (Epoch is 1632252600)
I have no idea how to represent the above. As, I do not find a way in DateComponents, to enable us to specific whether the local time is belong to standard time, or daylight time.
DateComponents do not have a time zone. The time zone comes into it when you convert DateComponents to a Date, using the call Calendar.date(from:). It's the calendar's time zone that determines how those DateComponents are converted to a Date.
Instead of using Calendar.current, create a custom calendar and set it to the IRST time zone. (I couldn't figure out the time zone for Iran Daylight time. I would have expected it to have the abbreviation "IRDT", but that doesn't work.)
Let's say we have a calendar irstCalendar that's set to Iran Standard Time ("IRST").
If you use irstCalendar.date(from: dateComponents) you'll always get the Date based on standard time.
Consider this code:
func date(year: Int, month: Int, day: Int, hour: Int, minute: Int, second: Int, calendar: Calendar = Calendar.current) -> Date {
var dateComponents = DateComponents()
dateComponents.year = year
dateComponents.month = month
dateComponents.day = day
dateComponents.hour = hour
dateComponents.minute = minute
dateComponents.second = second
let date = calendar.date(from: dateComponents)!
return date
}
guard let tehranStandardTimeZone = TimeZone(abbreviation: "IRST") else {
fatalError("Can't create time zones")
}
var tehranSTCalendar = Calendar(identifier: .gregorian)
tehranSTCalendar.timeZone = tehranStandardTimeZone
let tehranDateFormatter = DateFormatter()
tehranDateFormatter.dateStyle = .medium
tehranDateFormatter.timeStyle = .medium
tehranDateFormatter.timeZone = tehranStandardTimeZone
let date1 = date(year: 2021, month: 09, day: 21, hour: 23, minute: 59, second: 59, calendar: tehranSTCalendar)
let date2 = date(year: 2021, month: 09, day: 22, hour: 00, minute: 00, second: 00, calendar: tehranSTCalendar)
let date3 = date(year: 2021, month: 09, day: 22, hour: 00, minute: 00, second: 01, calendar: tehranSTCalendar)
print("STEP 1: 2021 Sep 21 23:59:59 => \(date1.timeIntervalSince1970), \(tehranDateFormatter.string(from:date1))")
print("STEP 2: 2021 Sep 22 00:00:00 => \(date2.timeIntervalSince1970), \(tehranDateFormatter.string(from:date2))")
print("STEP 3: 2021 Sep 22 00:00:01 => \(date3.timeIntervalSince1970), \(tehranDateFormatter.string(from:date3))")
That outputs:
STEP 1: 2021 Sep 21 23:59:59 => 1632252599.0, Sep 21, 2021 at 11:59:59 PM
STEP 2: 2021 Sep 22 00:00:00 => 1632256200.0, Sep 22, 2021 at 12:00:00 AM
STEP 3: 2021 Sep 22 00:00:01 => 1632256201.0, Sep 22, 2021 at 12:00:01 AM
So this is interesting:
po Calendar.default.date(from: DateComponents(year: 2022, month: 1, hour: 16, minute: 1, second: 1, weekday: 1, weekOfMonth: 1))
▿ Optional<Date>
▿ some : 2021-12-26 23:01:01 +0000
- timeIntervalSinceReferenceDate : 662252461.0
I'm expecting January 1st 2022, but I'm getting December 26th 2021? Why is it doing this? Am I doing something wrong?
It's because the date components you are providing are contradictory. You have weekDay 1 (which will probably be a Sunday or Monday, depending on your locale) but the 1 jan 2022 is a Saturday.
(you also used Calendar.default when I think you meant Calendar.current?)
If you take out the weekDay term you will get the correct answer:
Calendar.current.date(from: DateComponents(year: 2022, month: 1, hour: 16, minute: 1, second: 1, weekOfMonth: 1))
// "Jan 1, 2022 at 4:01 PM"
You could also remove the weekOfMonth term as it is superfluous when you are specifying the actual date.
I am trying to get every 10 minutes of the last hour.
For example, now is 15:46:41
I want [15:40:00, 15:30:00, 15:20:00, 15:10:00, 15:00:00, 14:50:00, 14:40:00, 14:30:00, 14:20:00, 14:10:00, 14:00:00, 13:50:00, 13:40:00]
let calendar = Calendar.current
let now = Date()
var components = DateComponents()
components.hour = -2
if let early = calendar.date(byAdding: components, to: now) {
let nowMin = calendar.component(.minute, from: early)
let diff = 10 - (nowMin % 10)
components.minute = diff
var minutes: [Int] = []
for _ in 0...13 {
// I cant figure out what should I do next.
}
print(minutes)
}
You can get now's minute, get the remainder of this value divided by ten and subtract it from that value. This way you get the last tenth hour minute, then you just need to set it with the same hour component to now to find out the first element of your array. Next you can fill the rest of dates subtracting 10 minutes times the element position from the start date. Try like this:
Xcode 11 • Swift 5.1 (for older versions just add the return statement as usual)
extension Date {
var hour: Int { Calendar.current.component(.hour, from: self) }
var minute: Int { Calendar.current.component(.minute, from: self) }
var previousHourTenth: Date { Calendar.current.date(bySettingHour: hour, minute: minute - minute % 10, second: 0, of: self)! }
func lastNthHourTenth(n: Int) -> [Date] { (0..<n).map { Calendar.current.date(byAdding: .minute, value: -10*$0, to: previousHourTenth)! } }
}
Playground testing
Date() // "Sep 25, 2019 at 10:19 AM"
Date().previousHourTenth // "Sep 25, 2019 at 10:10 AM"
Date().lastNthHourTenth(n: 13) // "Sep 25, 2019 at 10:10 AM", "Sep 25, 2019 at 10:00 AM", "Sep 25, 2019 at 9:50 AM", "Sep 25, 2019 at 9:40 AM", "Sep 25, 2019 at 9:30 AM", "Sep 25, 2019 at 9:20 AM", "Sep 25, 2019 at 9:10 AM", "Sep 25, 2019 at 9:00 AM", "Sep 25, 2019 at 8:50 AM", "Sep 25, 2019 at 8:40 AM", "Sep 25, 2019 at 8:30 AM", "Sep 25, 2019 at 8:20 AM", "Sep 25, 2019 at 8:10 AM"]
Now you just need to use DateFormatter to display those dates as needed to the user.
I'm trying to display a string in this format
x day, y hr, z min
where x, y, z are Int values from user and user is required to send in at least 5 min if day and hr are 0.
This is my function where I handle displaying user input
func pickerTimeShow(day: Int, hour: Int, minutes: Int) -> String {
if day > 0 {
return "\(day) day, \(hour) hr, \(minutes) min"
}
if hour > 0 {
return "\(hour) hr, \(minutes) min"
}
return "\(minutes) min"
}
However, if user sends pickerTimeShow(day: 0, hour: 2, minutes: 0), my string will show
2 hr, 0 min
which is not idea because it supposes to just show
2 hr
In addition, if user sends pickerTimeShow(day: 5, hour: 0, minutes: 5), my string will show
5 day, 0 hr, 5 min
but I just want
5 day, 5 min
Do you have a simple solution to handle this case? I really can't think of anything else besides adding more if statement to check which makes my function really long. Thank you so much.
Here is one way to do it. Put the pieces into an array and then join them at the end:
func pickerTimeShow(day: Int, hour: Int, minutes: Int) -> String {
var result = [String]()
if day > 0 {
result.append("\(day) day")
}
if hour > 0 {
result.append("\(hour) hr")
}
if minutes > 0 {
result.append("\(minutes) min")
}
if result.isEmpty {
// decide what to return in the case that all are zero
return "0 min"
} else {
return result.joined(separator: ", ")
}
}
Tests:
pickerTimeShow(day: 0, hour: 0, minutes: 0) // "0 min"
pickerTimeShow(day: 5, hour: 0, minutes: 0) // "5 day"
pickerTimeShow(day: 0, hour: 6, minutes: 0) // "6 hr"
pickerTimeShow(day: 0, hour: 0, minutes: 7) // "7 min"
pickerTimeShow(day: 5, hour: 6, minutes: 0) // "5 day, 6 hr"
pickerTimeShow(day: 0, hour: 6, minutes: 7) // "6 hr, 7 min"
pickerTimeShow(day: 5, hour: 0, minutes: 7) // "5 day, 7 min"
pickerTimeShow(day: 5, hour: 6, minutes: 7) // "5 day, 6 hr, 7 min"
Alternatively – if you can live with 5 days rather than 5 day – this is a solution using DateComponentsFormatter
func pickerTimeShow(day: Int, hour: Int, minutes: Int) -> String? {
let dateComponents = DateComponents(day: day, hour: hour, minute: minutes)
let formatter = DateComponentsFormatter()
formatter.unitsStyle = .short
formatter.allowedUnits = [.day, .hour, .minute]
return formatter.string(from: dateComponents)
}
The function returns nil if the input is out-of-order.
Given a date-range, how can I generate a sequential list of
weeks
months
that are included in the date range? For example, if the date-range is Jan 15 - Apr 29, then for the weeks it should be
15 Jan - 17, 18 - 24, 25 - 31 ... 19 Apr - 25, 26 - 29 Apr
where Jan 18th is Sunday, so in this case the week starts on Sunday, but it can also be Monday, doesn't matter. And for the months:
15 Jan - 31, 1 Feb - 28, 1 - 31 March, 1 - 29 Apr
What's the easiest way to do it?
Not sure if this is the easiest way, but it is a way. (Lazy way?)
Months: You start with a range like:
range = (Date.today..6.months.from_now)
Then you can get each month like:
months = range.to_a.map(&:beginning_of_month).uniq
#lookup docs on strftime to get exactly what you want here
months.map { |date| date.strftime('%Y %b') }
For weeks, you can start with months:
weeks = months.flat_map { |date|
m_weeks = [date]
until (week = date + 1.week) && week.month > date.month
m_weeks << week
end
m_weeks
}
#enter some string in strftime (im too lazy to look it up now)
weeks.map { |date| date.strftime('') }
I didn't test this, but I think it should work. Anyway, there are many ways to do this.
Ok so to get all weeks in the month by weeks Monday - Sunday, you can do something like this:
m_days = (Date.today.beginning_of_month..Date.today.end_of_month).to_a
day_offset = m_days.first.wday - 1
day_offset.times { m_days.unshift(nil) }
Then if you want to get an array of strings that have the first day of the week to the last day of the week you could do this.
weeks = []
m.days.each_slice(7) do |w_days|
w_days.compact!
weeks << "#{w_days.first} - #{w_days.last}"
end
You have two problems:
determining the beginning and end of each week between the start date and end date.
formatting the dates for display.
I have addressed the first problem only, but have provided all the information required to format the result in any way desired.
We are given:
start_date = "Jan 15, 2015"
end_date = "Apr 29, 2015"
We then compute the array weeks as follows:
require 'date'
days = (Date.parse(start_date)..Date.parse(end_date)).to_a
weeks = ([days.shift(7-days.first.wday)].concat(
days.each_slice(7).to_a)).map { |w|
[w.first, w.last].map { |d| [d.year, d.month, d.day, d.wday] } }
# => [[[2015, 1, 15, 4], [2015, 1, 17, 6]],
# [[2015, 1, 18, 0], [2015, 1, 24, 6]],
# [[2015, 1, 25, 0], [2015, 1, 31, 6]],
# [[2015, 2, 1, 0], [2015, 2, 7, 6]],
# ...
# [[2015, 4, 19, 0], [2015, 4, 25, 6]],
# [[2015, 4, 26, 0], [2015, 4, 29, 3]]]
Each element of weeks corresponds to a week and contains two arrays, one for the first day of the week (a Sunday, for all weeks after the first); the second for the last day of the week (a Saturday for all weeks other than the last). The arrays for individual days contain the year, month (1-12), day of month and day of week (Sunday: 0, Monday: 1,...Saturday: 6).
You can then use the Date class constants (search for "constants" at Date) to format the results.