DateIntervalFormatter: string of format "m:ss" - swift

I'd like to get a string of format "m:ss" out of two dates.
E.g.: "0:27" for 27 seconds difference and "1:30" for 90 seconds difference between dates.
Here's the code I'm using:
import Foundation
let formatter = DateIntervalFormatter()
formatter.dateStyle = .none
formatter.timeStyle = .none
formatter.dateTemplate = "m:ss"
let startDate = Date()
let endDate = Date(timeInterval: 1, since: startDate)
let outputString = formatter.string(from: startDate, to: endDate)
print(outputString) //16:12 – 16:13 ???
// This is correct, but it doesn't actually calculate the interval.
But I'm getting just two dates printed out with a dash.
How can I actually make the DateIntervalFormatter to calculate the difference as I want?
The code is almost 1:1 sample from the Apple documentation but with the custom dateTemplate: https://developer.apple.com/documentation/foundation/dateintervalformatter

It seems that you actually want DateComponentsFormatter
let formatter = DateComponentsFormatter()
formatter.allowedUnits = [.minute, .second]
formatter.zeroFormattingBehavior = .pad
let startDate = Date()
let endDate = Date(timeInterval: 129, since: startDate)
let outputString = formatter.string(from: startDate, to: endDate)!
print(outputString)
to remove the leading zero if the minutes are < 10 you could use Regular Expression
print(outputString.replacingOccurrences(of: "^0(\\d)", with: "$1", options: .regularExpression))

I created this solution which doesn't involve the DateIntervalFormatter:
import Foundation
let minutes = 2
let seconds = 9
let formatted = String(format: "%01d:%02d", minutes, seconds)
print(formatted) // 2:09
Looks like what DateIntervalFormatter does is just applying a standard Date->String conversion to both of the dates and adds a dash between them.

Related

Is there a more efficient way to combine a date and time in swift 5? [duplicate]

This question already has an answer here:
How to combine two strings (date & time) into a new date in Swift 3
(1 answer)
Closed 2 years ago.
I have function which receives 2 strings the first is a date "y-M-d" and the second a time "HH:mm". I then combine them using the following code.
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "y-M-d"
let date = dateFormatter.date(from: dateStr)!
dateFormatter.dateFormat = "HH:mm"
let time = dateFormatter.date(from: timeStr)!
let calendar = Calendar.init(identifier: .iso8601)
let components = NSDateComponents()
components.day = calendar.component(.day, from: date) //split from date above
components.month = calendar.component(.month, from: date)
components.year = calendar.component(.year, from: date)
components.hour = calendar.component(.hour, from: time) //split from time above
components.minute = calendar.component(.minute, from: time)
let newDate = calendar.date(from: components as DateComponents)
The code all works fine and is doing what I want it to. However, I was wondering if anyone can suggest a slicker way of doing it, using less lines of code?
You can join the date & time strings and parse them in one go:
let dateStr = "2020-03-12"
let timeStr = "15:35"
let df = DateFormatter()
df.dateFormat = "y-M-d HH:mm"
let date = df.date(from: dateStr + " " + timeStr)
// prints: 2020-03-12 13:35:00 +0000 (my machine is GMT+2)
Edit: As Leo Dabus said in the comments, a more appropriate format for the provided strings should be yyyy-MM-dd HH:mm (just kept the provided format from the question). The spirit of the answer was not to propose a format but to provide a way to avoid parsing date/time separately.
Simply get a combined String using date and time. Then use that String to get the Date instance from it.
func getDate(d: String, t: String) -> Date? {
let str = d + t
let formatter = DateFormatter()
formatter.dateFormat = "y-M-dHH:mm"
let date = formatter.date(from: str)
return date
}

Swift DateIntervalFormatter: keep dates out of the string even if interval spans multiple days

I am trying to format a time interval. I want a result that looks like this:
10:45 - 12:00 AM
I can get very close to this using DateInvervalFormatter:
let cal = Calendar.current
let formatter = DateIntervalFormatter()
formatter.dateStyle = .none
formatter.timeStyle = .short
let start = Date()
let end = cal.date(byAdding: .hour, value: 1, to: start)
formatter.string(from: DateInterval(start: start, end: end!))
The above (in the en_US locale) will produce an output such as:
5:27 – 6:27 PM
Looks good right? However, this does not work if the two dates in the interval are on different days. For example:
let formatter = DateIntervalFormatter()
formatter.dateStyle = .none
formatter.timeStyle = .short
let startComponents = DateComponents(year: 2020, month: 1, day: 1, hour: 23, minute: 45)
let start = cal.date(from: startComponents)
let end = cal.date(byAdding: .hour, value: 1, to: start!)
formatter.string(from: DateInterval(start: start!, end: end!))
Despite setting dateStyle to .none, the string produced in the above example (in the en_US locale) is:
1/1/2020, 11:45 PM – 1/2/2020, 12:45 AM
What I want is:
11:45 PM – 12:45 AM
How can I get this? I know I could use a DateFormatter to format each date (start and end) into just a time, and then append the two strings together with a hyphen (-) in the middle, but this is not necessarily localization-friendly.
What I ended up with is:
extension Date {
func formatTimeInterval(
to: Date,
timeZone: TimeZone = .autoupdatingCurrent
) -> String {
let formatter = DateIntervalFormatter()
formatter.timeZone = timeZone
formatter.timeStyle = .short
formatter.dateStyle = .none
// we need to manually strip any "date" metadata, because for some locales,
// even if we set `formatter.dateStyle = .none`, if the dates are in two different days
// we will still get the date information in the end result (e.g. for the US locale)
let calendar = Calendar.current
let fromComponents = calendar.dateComponents([.hour, .minute, .second], from: self)
let toComponents = calendar.dateComponents([.hour, .minute, .second], from: to)
let fromDate = calendar.date(from: fromComponents)!
let toDate = calendar.date(from: toComponents)!
return formatter.string(from: fromDate, to: toDate)
}
}
However, if the interval dates are in different months (say 31st August - 1 September), it adds a date starting at year 1, something like
1/1/1, 11:45 PM – 1/2/1, 12:45 AM
No clean solution to this still...

Creating a date with DateComponents

I want to get the first day and the last day of the week. But my results do not match the documentation from apple:
https://developer.apple.com/documentation/foundation/nsdatecomponents/1410442-weekday
This is my function:
func startAndEndDateOfWeek(weekOfYearWithYear: (week: Int,year: Int)) -> (start: Date, end: Date) {
var output = (start: Date.init(), end: Date.init())
let calendar = Calendar(identifier: .gregorian)
var firstDayComponents = DateComponents()
firstDayComponents.weekOfYear = weekOfYearWithYear.week
firstDayComponents.yearForWeekOfYear = weekOfYearWithYear.year
firstDayComponents.weekday = 1
let firstDay = calendar.date(from: firstDayComponents)
var lastDayComponents = DateComponents()
lastDayComponents.weekOfYear = weekOfYearWithYear.week
lastDayComponents.yearForWeekOfYear = weekOfYearWithYear.year
lastDayComponents.weekday = 2
let lastDay = calendar.date(from: lastDayComponents)
output = (start: firstDay!, end: lastDay!)
return output
}
.weekday = 2 -> leads to the sunday and not 0.
I also want to have the entire day and not 16:00.
A couple of observations:
In the Gregorian calendar, weekday = 1 means Sunday; weekday = 2 means Monday; etc. You can look at calendar.maximumRange(of: .weekday) to get the range of valid values, and you can look at calendar.weekdaySymbols to see what these weekDay values mean (e.g. “Sun”, “Mon”, “Tue”, “Wed”, “Thu”, “Fri”, and “Sat”).
You said:
I also want to have the entire day and not 16:00.
A Date object references a moment in time. So it can’t represent an “entire day”. But it can represent midnight (and midnight in your time zone is likely 4pm in GMT/UTC/Zulu).
You can, alternatively, return a DateInterval, which does represent a range of time.
func interval(ofWeek week: Int, in year: Int) -> DateInterval {
let calendar = Calendar.current
let date = DateComponents(calendar: calendar, weekOfYear: week, yearForWeekOfYear: year).date!
return calendar.dateInterval(of: .weekOfYear, for: date)!
}
And then
let formatter = DateIntervalFormatter()
formatter.dateStyle = .short
formatter.timeStyle = .short
let year = Calendar.current.component(.year, from: Date())
let dateInterval = interval(ofWeek: 2, in: year)
print(formatter.string(from: dateInterval))
In a US locale, the interval starts on January 6th:
1/6/19, 12:00 AM – 1/13/19, 12:00 AM
Whereas in a German locale, the interval starts on the 7th:
07.01.19, 00:00 – 14.01.19, 00:00
If you want the start of the first day of the week and the last day of the week, you can do:
func startAndEndDate(ofWeek week: Int, in year: Int) -> (Date, Date) {
let date = DateComponents(calendar: calendar, weekOfYear: week, yearForWeekOfYear: year).date!
let lastDate = calendar.date(byAdding: .day, value: 6, to: date)!
return (date, lastDate)
}
And then
let formatter = DateFormatter()
formatter.dateStyle = .short
let year = Calendar.current.component(.year, from: Date())
let (start, end) = startAndEndDate(ofWeek: 2, in: year)
print(formatter.string(from: start), "-", formatter.string(from: end))

Get Date from week number in Swift 3

I have myYear and myWeek strings comes from apis , I want to convert them to date string like YYYY-mm-dd how can I do it in swift 3 ? my strings under below.
let myYear = "2017"
let myWeek = "8"
You just have to convert the strings into Ints, then generate a DateComponents object from them, get a Date from the DateComponents through a Calendar and finally use a DateFormatter to get the expected String representation of the date.
Bear in mind that the Date object will represent the first second of the week of that year and hence the String representation will correspond to the first day of that week.
let yearString = "2017"
let weekOfYearString = "8"
guard let year = Int(yearString), let weekOfYear = Int(weekOfYearString) else {return}
let components = DateComponents(weekOfYear: weekOfYear, yearForWeekOfYear: year)
guard let date = Calendar.current.date(from: components) else {return}
let df = DateFormatter()
df.dateFormat = "yyyy-MM-dd"
let outputDate = df.string(from: date) //2017-02-19

Get percentage between two dates as a float in Swift

So I'm currently trying to add a feature that given two dates, will tell me how far through I am.
TLDR:
So, let's say I have a date that is July 1st, 2017 20:00:00 and another that is July 2nd, 2017 22:00:00 and today is July 2nd, 08:00:00, then I will get that I am 46.17% of the way through.
The way I tried to do this is using a simple formula:
progress = (current time - start time) / (end time - start time)
but when put into code, I can subtract two dates, and get the DateComponent difference, but I cannot divide two DateComponent's. Here is my code set up, with an extension t
let formatter = DateFormatter()
formatter.dateFormat = "yyyy-MM-dd HH:mm:ss"
let startDate = formatter.date(from: "2017-07-01 20:00:00")
let endDate = formatter.date(from: "2017-07-02 22:00:00")
let currentDate = formatter.date(from: "2017-07-01 08:00:00")
let progress = (currentDate - startDate) / (endDate - startDate)
extension Date {
static func - (date1: Date, date2: Date) -> DateComponents {
let calender:Calendar = Calendar.current
return calender.dateComponents([.year, .month, .day, .hour, .minute, .second], from: date1, to: date2)
}
}
There has to be a way around dividing two dates. I can't think of it though, everything I find online (using different languages) has required division between two dates.
I tried to convert everything to seconds and just divide that, but I didn't know what to do with the seconds to convert them back to a DateComponent because there might be a 5000 second difference. Any help appreciated!
You just need to turn the dates into numbers, because then you can add and subtract them.
You can use the timeIntervalSince1970 to turn the date into numbers, then you can use your formula:
let formatter = DateFormatter()
formatter.dateFormat = "yyyy-MM-dd HH:mm:ss"
let startDate = formatter.date(from: "2017-07-01 20:00:00")!.timeIntervalSince1970
let endDate = formatter.date(from: "2017-07-02 22:00:00")!.timeIntervalSince1970
let currentDate = formatter.date(from: "2017-07-02 08:00:00")!.timeIntervalSince1970
let percentage = (currentDate - startDate) / (endDate - startDate)
Alternatively, use timeIntervalSince(_:):
let formatter = DateFormatter()
formatter.dateFormat = "yyyy-MM-dd HH:mm:ss"
let startDate = formatter.date(from: "2017-07-01 20:00:00")!
let duration = formatter.date(from: "2017-07-02 22:00:00")!.timeIntervalSince(startDate)
let elapsed = formatter.date(from: "2017-07-02 08:00:00")!.timeIntervalSince(startDate)
let percentage = elapsed / duration
I think this way is better because you get less maths :).
What you are looking for is Date().timeIntervalSinceReferenceDate:
let currentInterval = currentDate.timeIntervalSinceReferenceDate
let startInterval = startDate.timeIntervalSinceReferenceDate
let endInterval = endDate.timeIntervalSinceReferenceDate
let progress = ((currentInterval - startInterval) / (endInterval - startInterval)) * 100
Here is apples documentation on it. Basically it returns the amount of seconds that have passed since the reference date of January 1, 2001 UTC.
Do the exact same calculation, just convert startdate, enddate, and currentdate into milliseconds using yourDate.timeIntervalSince1970
That should give you a decimal between 0 and 1, and just multiply by 100 for the percentage