Convert decimal time to hours and minutes in Swift - swift

I am new to programming and this is my first program and question. I'm trying to write a function which will simply convert decimal time to Hours & Minutes. I'm removing the hours and multiplying the decimal minutes by 60 and adding the two back together as a string. I need to use this facility a couple of times in my program hence the function. The calculation which uses this function is straightforward but I'm getting odd results. If I maintain 'plannedStartFuel' as 450 and adjust 'minLandAllowance' I get the following results,
185 returns 1:28
182 returns 1:29
181 returns 1:30
180 returns 2:30
179 returns 2:30
175 returns 2:32
The correct answers are the 1:00 figures. I don't understand why the program seems to add an hour to the results at the 180 point. I'm sure there are are far better ways of completing this calculation than I've used, but if you can help I'd be grateful to know which part is causing the error and why. What have I tried?...everything! If you pitch your answer at a 7 year old I may have a chance of understanding. Thank you.
import UIKit
import Foundation
func decimalHoursConv (hours : Double) -> (_hrs:String, mins:String) {
let remainder = hours.truncatingRemainder(dividingBy: 1) * 60
let mins = (String(format: "%.0f", remainder))
let hrs = (String(format: "%.0f", hours))
return (hrs, mins)
}
var plannedStartFuel = Double (0)
var minLandAllowance = Double (0)
var flyingTimeToMLA = Double(0)
plannedStartFuel = 450
minLandAllowance = 180
flyingTimeToMLA = ((plannedStartFuel - minLandAllowance) / 3)/60
let MLAtime = (decimalHoursConv(hours: flyingTimeToMLA))
print ("Flight Time To MLA =", MLAtime.0,"hrs",MLAtime.1,"mins")

I might advise not bothering to calculate hours and minutes at all, but rather let DateComponentsFormatter do this, creating the final string for you.
For example:
let formatter: DateComponentsFormatter = {
let formatter = DateComponentsFormatter()
formatter.unitsStyle = .full
formatter.allowedUnits = [.hour, .minute]
return formatter
}()
Then supply this formatter the elapsed time measured in seconds (a TimeInterval, which is just an alias for Double):
let remaining: TimeInterval = 90 * 60 // e.g. 90 minutes represented in seconds
if let result = formatter.string(from: remaining) {
print(result)
}
On a English speaking device, that will produce:
1 hour, 30 minutes
The virtue of this approach is that not only does it get you out of the business of manually calculating hours and minutes yourself, but also that the result is easily localized. So, if and when you get around to localizing your app, this string will be localized automatically for you, too, with no further work on your part. For example, if you add German to your app localizations, then the US user will still see the above, but on a German device, it will produce:
1 Stunde und 30 Minuten
If you want it to say how much time is remaining, set includesTimeRemainingPhrase:
let formatter: DateComponentsFormatter = {
let formatter = DateComponentsFormatter()
formatter.unitsStyle = .full
formatter.includesTimeRemainingPhrase = true
formatter.allowedUnits = [.hour, .minute]
return formatter
}()
That will produce:
1 hour, 30 minutes remaining
If you want a “hh:mm” sort of representation:
let formatter: DateComponentsFormatter = {
let formatter = DateComponentsFormatter()
formatter.unitsStyle = .positional
formatter.zeroFormattingBehavior = .pad
formatter.allowedUnits = [.hour, .minute]
return formatter
}()
Will produce:
01:30
Bottom line, if you really want to calculate minutes and seconds, feel free, but if it’s solely to create a string representation, let the DateComponentFormatter do this for you.

EDIT
I realize you wanted to know what did not work with your method.
It's a matter of rounding, try roundind hours before passing it to String(format:) :
func decimalHoursConv (hours : Double) -> (_hrs:String, mins:String) {
let remainder = hours.truncatingRemainder(dividingBy: 1) * 60
let mins = (String(format: "%.0f", remainder))
let hours = hours.rounded(.towardZero)
let hrs = (String(format: "%.0f", hours))
return (hrs, mins)
}
it gives :
var value = (450.0-185.0)/3
decimalHoursConv(hours: value/60) // (_hrs "1", mins "28")
value = (450.0-182.0)/3
decimalHoursConv(hours: value/60) // (_hrs "1", mins "29")
value = (450.0-181.0)/3
decimalHoursConv(hours: value/60) // (_hrs "1", mins "30")
value = (450.0-180.0)/3
decimalHoursConv(hours: value/60) // (_hrs "1", mins "30")
value = (450.0-179.0)/3
decimalHoursConv(hours: value/60) // (_hrs "1", mins "30")
value = (450.0-175.0)/3
decimalHoursConv(hours: value/60) // (_hrs "1", mins "32")
BUT Still
If you're using Swift you should use Measurement
func convertToHoursAndMinutes(_ value: Double) -> DateComponents {
let unitMeasurement = Measurement(value: value, unit: UnitDuration.minutes)
let hours = unitMeasurement.converted(to: .hours).value
let decimalPart = hours.truncatingRemainder(dividingBy: 1)
let decimalPartMeasurement = Measurement(value: decimalPart, unit: UnitDuration.hours)
let decimalPartMeasurementInMinutes = decimalPartMeasurement.converted(to: .minutes)
let minutes = decimalPartMeasurementInMinutes.value.rounded(.toNearestOrEven)
return DateComponents(hour: Int(hours), minute: Int(minutes))
}
usage :
var value = (450.0-185.0)/3 // 88.33333333333333
convertToHoursAndMinutes(value) // hour: 1 minute: 28 isLeapMonth: false
value = (450.0-182.0)/3 // 89.33333333333333
convertToHoursAndMinutes(value) // hour: 1 minute: 29 isLeapMonth: false
value = (450.0-181.0)/3 // 89.66666666666667
convertToHoursAndMinutes(value) // hour: 1 minute: 30 isLeapMonth: false
value = (450.0-180.0)/3 // 90
convertToHoursAndMinutes(value) // hour: 1 minute: 30 isLeapMonth: false
value = (450.0-179.0)/3 // 90.33333333333333
convertToHoursAndMinutes(value) // hour: 1 minute: 30 isLeapMonth: false
value = (450.0-175.0)/3 // 91.66666666666667
convertToHoursAndMinutes(value) // hour: 1 minute: 32 isLeapMonth: false
Note that you can always use a tuple instead of DateComponents if you prefer.

String formatter rounds up.
You can use .rounded(.down) on Doubles to round them down. (or with other rules you need)
let number = (179.0/60.0) // 2.983333333333333
String(format: "%.0f", number) // returns 3
number.rounded(.up) // returns 3
number.rounded(.down) // returns 2

First you should structure your data. Next you don't need to format your value as a Double if you are not gonna display fractions. So you can simply convert your double to integer.
struct FlightPlan {
let plannedStartFuel: Double
let minimumLandAllowance: Double
}
extension FlightPlan {
var mlaTime: (hours: Int, minutes: Int) {
let hours = (plannedStartFuel - minimumLandAllowance) / 180
return (Int(hours), Int(modf(hours).1 * 60))
}
}
And you should use DateComponentsFormatter when displaying time to the user:
extension Formatter {
static let time: DateComponentsFormatter = {
let formatter = DateComponentsFormatter()
formatter.calendar?.locale = .init(identifier: "en_US_POSIX")
formatter.unitsStyle = .brief
formatter.allowedUnits = [.hour,.minute]
return formatter
}()
}
extension FlightPlan {
var mlaTimeDescrition: String {
return "Flight Time To MLA = " + Formatter.time.string(from: .init(hour: mlaTime.hours, minute: mlaTime.minutes))!
}
}
let flightPlan = FlightPlan(plannedStartFuel: 450,
minimumLandAllowance: 180)
flightPlan.mlaTime // (hours 1, minutes 30)
flightPlan.mlaTime.hours // 1
flightPlan.mlaTime.minutes // 30
flightPlan.mlaTimeDescrition // "Flight Time To MLA = 1hr 30min"

Related

Swift Correct calculation of time as a proportion of day

I am trying to calculate the proportion of a day that a specific time equates to. For example, 06:00 is 0.25, 18:00 is 0.75 etc.
I am evaluating a series of dates of Date type, which were created in timeZone = "GMT". The routine below works fine. However when I evaluate a time after 23:00 for dates in DST, then the calculation goes wrong, as the time is evaluated as the next day (e.g. 23:08 is evaluated as 00:08)
Is there any way that I can recognise when the move from GMT to DST takes the date into the next day? I can then adjust the calculation accordingly.
My function for determining the proportion that the input time represents is:
func getTimeAsProportionOfDay(time: Date) -> Double {
//calculates the amount of day, between 0 and 1 given the input date
let calendar = Calendar.current
let hours = calendar.component(.hour, from: time)
let minutes = calendar.component(.minute, from: time)
let seconds = calendar.component(.second, from: time)
let totalSeconds = Double(hours * 60 * 60 + minutes * 60 + seconds)
return Double(totalSeconds) / Double(secondsInDay)
}
Also, I'm aware that my constant of secondsInDay (= 24*60*60) may not be technically correct but I'm not sure what system constant to replace it with.
Thanks.
You just need get the day after the original date and subtract a second. Then calculate the number of seconds in that date using calendar method
func ordinality(of smaller: Calendar.Component, in larger: Calendar.Component, for date: Date) -> Int?
You can make your life easier with some helpers
extension Date {
var dayAfter: Date { Calendar.current.date(byAdding: .day, value: 1, to: noon)!}
var noon: Date { Calendar.current.date(bySettingHour: 12, minute: 0, second: 0, of: self)! }
var startOfDay: Date { Calendar.current.startOfDay(for: self) }
var endOfDay: Date { Calendar.current.date(byAdding: .init(second: -1), to: dayAfter.startOfDay)! }
}
Testing the endOfDay
Date().endOfDay // "Feb 7, 2020 at 11:59 PM"
And your method:
func getTimeAsProportionOfDay(time: Date) -> Double {
// discarding the fractional seconds
let time = Calendar.current.date(bySetting: .nanosecond, value: 0, of: time)!
return Double(Calendar.current.ordinality(of: .second, in: .day, for: time)!-1) /
Double(Calendar.current.ordinality(of: .second, in: .day, for: time.endOfDay)!-1)
}
Playground testing:
let date = DateComponents(calendar: .current, year: 2020, month: 2, day: 7, hour: 23, minute: 08).date!
date.endOfDay
let result = getTimeAsProportionOfDay(time: date) // 0.9639000451394113
Generally speaking, I would do something like this:
let date = Date()
var dayStart = Date()
var dayDuration: TimeInterval = 0
Calendar.current.dateInterval(of: .day, start: &dayStart, interval: &dayDuration, for: date)
let timeInterval = date.timeIntervalSince(dayStart)
let percentage = timeInterval / dayDuration
print(percentage)
All, thanks for your help. I think I have found a solution to this by using a fixed start of day to compare against.
func getTimeAsProportionOfDay(time: Date) -> Double {
//calculates the amount of day, between 0 and 1 given the input date
if Int(time.timeIntervalSince(tides.tideDate)) > secondsInDay { //time has been moved to next day by BST change) so return 1 for gradient
return 1.0
} else {/// this was my original code
let calendar = Calendar.current
let hours = calendar.component(.hour, from: time)
let minutes = calendar.component(.minute, from: time)
let seconds = calendar.component(.second, from: time)
let totalSeconds = Double(hours * 60 * 60 + minutes * 60 + seconds)
return Double(totalSeconds) / Double(secondsInDay)
}
}

Convert Minutes to HH:MM:SS - Swift

I have some text fields in an array. HH:MM:SS. I'm having two issues, one is when one of the fields is left blank, the app crashes and I want it to just read as "0" if blank. Second, I use below to convert HH:MM:SS to Minutes. I do 0:18:30 and the math below to decimal comes out to 18.5
Now how would I convert this back to HH:MM:SS? array[0] is hours, array[1] is minutes, array[2] is seconds.
stepOne = (Float(array[0])! * 60) + Float(array[1])! + (Float(array[2])! / 60)
What you need is to calculate the number of seconds instead of number of minutes. Using the reduce method just multiply the first element by 3600 and then divide the multiplier by 60 after each iteration. Next you can use DateComponentsFormatter to display the resulting seconds time interval using .positional units style to the user:
let array = [0,18,30]
var n = 3600
let seconds = array.reduce(0) {
defer { n /= 60 }
return $0 + $1 * n
}
let dcf = DateComponentsFormatter()
dcf.allowedUnits = [.hour, .minute, .second]
dcf.unitsStyle = .positional
dcf.zeroFormattingBehavior = .pad
let string = dcf.string(from: TimeInterval(seconds)) // "00:18:30"

Calculate average pace

I need to convert a decimal hour in to hh:mm:ss to display as average pace for a walking app.
I have converted the time to decimal and I have calculated the pace in to decimal however I am unsure how to convert this to time.
My timeDecimal is:
let timeDecimal:Double = (hourSource + (minuteSource / 60) + (secondSource / 3600)) / distanceSource
which gives me something like 0.4375
0 = 0
.4375 * 60 = 26.25
.25 = * 60 = 15
I know the time should be 00:26:15 but not sure the formula to achieve this without splitting up the result and performing the multiplication multiple times.
Any help is appreciated.
let formatter = NSDateComponentsFormatter()
formatter.allowedUnits = [ .Hour, .Minute, .Second ]
formatter.unitsStyle = .Positional
formatter.zeroFormattingBehavior = .Pad
let string = formatter.stringFromTimeInterval(0.4375 * 3600)!
Result: 0:26:15.
Try
var mytime = timeDecimal * 60
var minutes = floor(mytime)
var seconds = (mytime - minutes) * 60
func timeStringFrom(time time: Int) -> String {
let HoursLeft = time/3600
let MinutesLeft = (time%3600)/60
let SecondsLeft = (((time%3600)%60)%60)
if HoursLeft == 0 {
return String(format:"%.2d:%.2d", MinutesLeft, SecondsLeft)
} else {
return String(format:"%2d:%.2d:%.2d", HoursLeft, MinutesLeft, SecondsLeft)
}
}
Note: I'll probably turn it into a generic function soon

How can I format numbers in order to have always two digits?

I am creating a countdown timer that shows hours:minutes:seconds.
The function I have works however, it returns the values as 1 digit if the number is less than 10. (ex. 2:3:15)
How can I format my function in order to have always two digits?
I am looking for a result like: 02:03:15
func secondsToHoursMinutesSeconds (seconds : Int) -> (Int, Int, Int) {
return (seconds / 3600, (seconds % 3600) / 60, (seconds % 3600) % 60)
}
Alternatively using DateComponentsFormatter
lazy var dateComponentsFormatter: DateComponentsFormatter = {
let formatter = DateComponentsFormatter()
formatter.zeroFormattingBehavior = .pad
formatter.allowedUnits = [.hour, .minute, .second]
return formatter
}()
func secondsToHoursMinutesSeconds(seconds: Int) -> String {
let hourPad = seconds < 36000 ? "0" : "" // add 0 in front if less than 10 hours
return hourPad + dateComponentsFormatter.string(from: seconds)!
}
secondsToHoursMinutesSeconds(3602) // 01:00:02
Since DateComponentsFormatter does not pad the hours automatically with zeroFormattingBehavior = .pad, you have to add that "manually".
The formatter itself is declared as a lazy computed property to avoid repetitive re-instantiation.

How to find the time span between two times?

I want to calculate a remaining time. I have a finish time. How can I calculate the time remaining between now and finish time?
I need something like that
let date = NSDate()
let formatter = NSDateFormatter()
formatter.timeStyle = .ShortStyle
var now = formatter.stringFromDate(date) //prints 12.21 AM
var finishTime = "13:30 PM"
var remaining = now - finishTime //I want it to print 01:09 as remaining time
func timeRemainingString(finishDate date:NSDate) -> String {
let secondsFromNowToFinish = date.timeIntervalSinceNow
let hours = Int(secondsFromNowToFinish / 3600)
let minutes = Int((secondsFromNowToFinish - Double(hours) * 3600) / 60)
let seconds = Int(secondsFromNowToFinish - Double(hours) * 3600 - Double(minutes) * 60 + 0.5)
return String(format: "%02d:%02d:%02d", hours, minutes, seconds)
}
The correct and ultimately much simpler solution, and handles localization and things like leap seconds / days properly, is to use NSDateComponentsFormatter.
let formatter = DateComponentsFormatter()
formatter.zeroFormattingBehavior = .pad
formatter.allowedUnits = [.hour, .minute, .second]
print(formatter.string(from: 200.0)!)
Outputs "0:03:20"
This is a Swift version of this Objective C answer.
There are lots of different options that you can see in the documentation in case you want something more like "About 3 seconds remaining", abbreviations, more units like years/months/days etc.