How to create a formatter for TimeInterval to print minutes, seconds and milliseconds - swift

Is there a way to create a DateComponentFormatter for TimeInterval that outputs minutes, seconds and milliseconds (bonus, if I could specify how many fractional places after seconds).
let t: TimeInterval = 124.344657 // 124 seconds, 345 milliseconds
// output as 2m 4s 345ms
I tried the following:
let formatter = DateComponentsFormatter()
formatter.unitsStyle = .abbreviated
formatter.allowedUnits = [.minute, .second]
formatter.allowsFractionalUnits = true
print("\(formatter.string(from: t)!)") // outputs 2m 4s
I tried playing with more parameters, e.g. like adding .nanosecond, but to no effect.
What's the right approach here?

I think the way to look at this is that it's a misuse of a date components formatter. This isn't a date of any kind. It's a string consisting of a certain number of minutes, seconds, and milliseconds. Unlike date math, that's a calculation you can perform, and then you are free to present the string however you like.
If you want to use a formatter to help you with user locales and so forth, then you are looking for a measurement formatter (for each of the substrings).
Example (using the new Swift 5.5 formatter notation):
let t1 = Measurement<UnitDuration>(value: 2, unit: .minutes)
let t2 = Measurement<UnitDuration>(value: 4, unit: .seconds)
let t3 = Measurement<UnitDuration>(value: 345, unit: .milliseconds)
let s1 = t1.formatted(.measurement(width: .narrow))
let s2 = t2.formatted(.measurement(width: .narrow))
let s3 = t3.formatted(.measurement(width: .narrow))
let result = "\(s1) \(s2) \(s3)" // "2m 4s 345ms"
Addendum: You say in a comment that you're having trouble deriving the number milliseconds. Here's a possible way. Start with seconds and let the Measurement do the conversion. Then format the resulting value in the formatter. Like this:
let t3 = Measurement<UnitDuration>(value: 0.344657, unit: .seconds)
.converted(to: .milliseconds)
// getting the `0.xxx` from `n.xxx` is easy and not shown here
let s3 = t3.formatted(.measurement(
width: .narrow,
numberFormatStyle: .number.precision(.significantDigits(3))))
You might have to play around a little with the number-formatter part of that, but the point is that a measurement formatter lets you dictate the number format and thus get the truncation / rounding behavior you're after.

Related

Converting total minutes to "HH:mm" format

I'm currently saving some timestamped data in the minutes format, eg 550 is 9:10AM.
Is there a way to convert this into the string "09:10"? I'll be using 24hr format.
I'm using swift for an iOS app, but if there is logic that's non-language specific, that would be helpful too.
Cheers,
Josh
The question is what the 550 really represents:
If it represents an abstract time interval, measured in minutes, you would likely convert it to a TimeInterval and then use DateComponentsFormatter to prepare a string representation of hours and minutes:
let timeInterval = TimeInterval(minutes * 60)
let formatter = DateComponentsFormatter()
formatter.allowedUnits = [.hour, .minute]
formatter.unitsStyle = .positional
formatter.zeroFormattingBehavior = .pad
let string = formatter.string(from: timeInterval)
This is a simple “hours and minutes” representation. This pattern is especially useful if the number of hours could exceed 24 (e.g. 1,550 minutes is 25 hours and 50 minutes, or 1 day, 1 hour, and 50 minutes, depending upon whether you add .day to allowedUnits or not).
If, however, you really mean that the 550 minutes is intended to literally represent the time of the day, then you might use calendrical date calculations and use DateFormatter for a string representation of the time:
let timeInterval = TimeInterval(minutes * 60)
let date = Calendar.current.startOfDay(for: Date()).addingTimeInterval(timeInterval)
let formatter = DateFormatter()
formatter.locale = Locale(identifier: "en_US_POSIX")
formatter.dateFormat = "HH:mm"
let string = formatter.string(from: date)
But, that having been said, if this really represented 9:10am in the morning (not an abstract time interval nine hours and ten minutes) and you wanted to show it in the UI, you would generally honor the device’s preferred time format (am/pm or 24-hour clock):
let timeInterval = TimeInterval(minutes * 60)
let date = Calendar.current.startOfDay(for: Date()).addingTimeInterval(timeInterval)
let formatter = DateFormatter()
formatter.timeStyle = .short
formatter.dateStyle = .none
let string = formatter.string(from: date)
Either way (forcing 24 hour clock or honor the user’s preferences), though, you really are displaying a time of day, which means that this while it will be 9:10am most days of the year, if you do this on the day that we spring forward to daylight savings, it will say 10:10am, but if on the day we fall backwards back to standard time on that day, it would be 8:10am.
Clearly, the syntax is different in Objective-C than it is in Swift, but the basic API is the same (though you obviously would be using the NS prefixes, e.g. NSDateComponentsFormatter and/or NSDateFormatter).

DateComponentsFormatter and .nanosecond

So, I'm using a DateComponentsFormatter in a situation where I need to format only the nanoseconds of a given TimeInterval. The problem is that it just does not work as intended, or I'm confusing something here that I'm not aware of.
Here are 3 quick tests to illustrate what I'm saying.
1st Scenario
let fooFormatter = DateComponentsFormatter()
fooFormatter.allowedUnits = [.second, .nanosecond]
fooFormatter.unitsStyle = .full
fooFormatter.collapsesLargestUnit = true
fooFormatter.allowsFractionalUnits = true
print(fooFormatter.string(from: 42.42424242424242424242424242424242))
Output: Optional("42 seconds").
Expected: Because this collapses the "largest unit" - seconds in this case -, it's expected to be something like (Optional("42.42424242424242424242424242424242 * 1e^9")).
2nd Scenario
let fooFormatter = DateComponentsFormatter()
fooFormatter.allowedUnits = [.second, .nanosecond]
fooFormatter.unitsStyle = .full
fooFormatter.allowsFractionalUnits = true
print(fooFormatter.string(from: 42.42))
Output: Optional("42 seconds").
Expected: Even without opting to not collapse the .second, I expected anything close to (Optional("42 seconds 0.42 * 1e^9 nanoseconds")).
3rd Scenario
let fooFormatter = DateComponentsFormatter()
fooFormatter.allowedUnits = [.nanosecond]
fooFormatter.unitsStyle = .full
fooFormatter.allowsFractionalUnits = true
print(fooFormatter.string(from: 42.424242))
Output: nil.
Expected: Now it won't even recognize the TimeInterval as valid - when it's defined as the smallest fraction possible - and obviously expected everything in nanoseconds.
It's important to notice that I've used allowsFractionalUnits = true, which is even more alarming, because this behavior is also not working in the outputs given above (see the intended behavior here).
Thanks in advance.
DateComponentsFormatter does not support nanoseconds.
See the documentation for allowedUnits, which does not include nanoseconds. It says:
The allowed calendar units are:
year
month
weekOfMonth
day
hour
minute
second
Assigning any other calendar units to this property results in an exception.

Result of adding second to date is one minute off; workaround

I'm adding a second to an instance of Foundation's date, but the result is off by an entire minute.
var calendar = Calendar(identifier: .iso8601)
calendar.locale = Locale(identifier: "en")
calendar.timeZone = TimeZone(identifier: "GMT")!
let date1 = Date(timeIntervalSinceReferenceDate: -62544967141.9)
let date2 = calendar.date(byAdding: DateComponents(second: 1),
to: date1,
wrappingComponents: true)!
ISO8601DateFormatter().string(from: date1) // => 0019-01-11T22:00:58Z
ISO8601DateFormatter().string(from: date2) // => 0019-01-11T21:59:59Z
Interestingly, one of the following makes the error go away:
round time interval since reference date
don't add time zone to calendar
set wrappingComponents to false (even though it shouldn't wrap in this case)
I don't really need sub-second precision in my code, so I created this extension that allows me to discard it.
extension Date {
func roundedToSeconds() -> Date {
return Date(timeIntervalSinceReferenceDate: round(timeIntervalSinceReferenceDate))
}
}
I want to know this:
Why does this error happen?
Am I doing something wrong?
Is there any issue with my workaround?
Why does this error happen?
I would say this is a bug in Core Foundation (CF).
Calendar.date(byAdding:to:wrappingComponents:) calls down to the internal Core Foundation function _CFCalendarAddComponentsV, which in turn uses the ICU Calendar C API. ICU represents a time as an floating-point number of milliseconds since the Unix epoch, while CF uses a floating-point number of seconds since the NeXT reference date. So CF has to convert its representation to ICU's representation before calling into ICU, and convert back to return the result to you.
Here's how it converts from a CF timestamp to an ICU timestamp:
double startingInt;
double startingFrac = modf(*atp, &startingInt);
UDate udate = (startingInt + kCFAbsoluteTimeIntervalSince1970) * 1000.0;
The modf function splits a floating-point number into its integer and fractional parts. Let's plug in your example date:
var startingInt: Double = 0
var startingFrac: Double = modf(date1.timeIntervalSinceReferenceDate, &startingInt)
print(startingInt, startingFrac)
// Output:
-62544967141.0 -0.9000015258789062
Next, CF calls __CFCalendarAdd to add one second to -62544967141. Note that -62544967141 lies in the round one-minute interval -62544967200 ..< -62544967140.0. So when CF adds one second to -62544967141, it gets -62544967140, which would be in the next round one-minute interval. Since you specified wrapping components, CF isn't allowed to change the minute part of the date, so it wraps back to the beginning of the original round one-minute interval, -62544967200.
Finally, CF converts the ICU time back to a CF time, adding in the fractional part of the original time:
*atp = (udate / 1000.0) - kCFAbsoluteTimeIntervalSince1970 + startingFrac + (nanosecond * 1.0e-9);
So it returns -62544967200 + -0.9000015258789062 = -62544967200.9, exactly 59 seconds earlier than the input time.
Am I doing something wrong?
No, the bug is in CF, not in your code.
Is there any issue with my workaround?
If you don't need sub-second precision, your workaround should be fine.
I can reproduce it with more recent dates but so far only with negative reference dates, e.g. Date(timeIntervalSinceReferenceDate: -1008899941.9), which is 1969-01-11T22:00:58Z.
Any negative timeIntervalSinceReferenceDate in the last second of a minute interval should cause the problem. The bug effectively makes the first round whole minute prior to time 0 span from -60.99999999999999 through -1.0, but it should span from -60.0 through -5e324. All more-negative round minute intervals are similarly offset.

How can I get current date and time in swift with out Foundation

I am trying to do the equivalent of NSDate() but with out importing Foundation.
Does the Darwin module have a way to do this?
I was looking at this answer but no dice
How can I get a precise time, for example in milliseconds in Objective-C?
I am not sure if this is what you are looking for, but the BSD library
function
let t = time(nil)
gives the number of seconds since the Unix epoch as an integer, so this is almost the same as
let t = NSDate().timeIntervalSince1970
only that the latter returns the time as a Double with higher
precision. If you need this higher precision then you could use
gettimeofday():
var tv = timeval()
gettimeofday(&tv, nil)
let t = Double(tv.tv_sec) + Double(tv.tv_usec) / Double(USEC_PER_SEC)
If you are looking for the time broken down to years, month, days, hours etc according to your local time zone, then use
var t = time(nil)
var tmValue = tm()
localtime_r(&t, &tmValue)
let year = tmValue.tm_year + 1900
let month = tmValue.tm_mon + 1
let day = tmValue.tm_mday
let hour = tmValue.tm_hour
// ...
tmValue is a struct tm, and the fields are described in
https://developer.apple.com/library/mac/documentation/Darwin/Reference/ManPages/man3/localtime.3.html.

How to display seconds in year, day format in swift

All i need to do is to change the way that xcode displaying my values in. My code currently displaying the age of something in minutes e.g 4503mint . I want to be able to display these values in the following format 3 days 3 hours 3 mint rather than 4503 mint. Really appreciate your help. Regards
You said:
My code currently displaying the age of something in minutes e.g 4503mint . I want to be able to display these values in the following format 3 days 3 hours 3 mint rather than 4503 mint.
You can format this using NSDateComponentsFormatter. Just multiply the number of minutes by 60 to get the NSTimeInterval, and then you can supply that to the formatter:
let formatter = NSDateComponentsFormatter()
formatter.unitsStyle = .Full
let minutes = 4503
let timeInterval = NSTimeInterval(minutes * 60)
print(formatter.stringFromTimeInterval(timeInterval))
That will produce "3 days, 3 hours, 3 minutes" (or in whatever format appropriate for the locale for that device).
See NSDateComponentsFormatter Reference for more information.
You can obviously calculate it yourself (i.e. days = seconds/(3600*24)etc.), you can also look at NSDateComponentsFormatter, which may be exactly the functionality you are looking for with almost no coding effort.
You could calculate it yourself or do something like this:
let mySeconds = 700000
let date = NSDate(timeIntervalSinceNow: mySeconds) // difference to now
var dateFormatter = NSDateFormatter()
dateFormatter.dateFormat = NSDateFormatter.dateFormatFromTemplate("yyyy.MM.dd", options: 0, locale: NSLocale.currentLocale())
dateFormatter.stringFromDate(date)