Rob Wilkinson
Rob Wilkinson

Reputation: 140

Convert decimal time to hours and minutes in 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")

Upvotes: 3

Views: 2658

Answers (4)

Rob
Rob

Reputation: 438277

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


By the way, if you are wondering how to get the quotient and remainder (notably, avoiding the undesired rounding in your example), refer to the truncatingRemainder(dividingBy:) documentation, which says:

Returns the remainder of this value divided by the given value using truncating division.

Performing truncating division with floating-point values results in a truncated integer quotient and a remainder. For values x and y and their truncated integer quotient q, the remainder r satisfies x == y * q + r.

The following example calculates the truncating remainder of dividing 8.625 by 0.75:

let x = 8.625
print(x / 0.75)
// Prints "11.5"

let q = (x / 0.75).rounded(.towardZero)
// q == 11.0
let r = x.truncatingRemainder(dividingBy: 0.75)
// r == 0.375

let x1 = 0.75 * q + r
// x1 == 8.625

If this value and other are both finite numbers, the truncating remainder has the same sign as this value and is strictly smaller in magnitude than other. The truncatingRemainder(dividingBy:) method is always exact.

I might be inclined to write a floating point rendition of quotientAndRemainder:

extension FloatingPoint {
    func quotientAndRemainder(dividingBy divisor: Self) -> (Self, Self) {
        let q = (self / divisor).rounded(.towardZero)
        let r = truncatingRemainder(dividingBy: divisor)
        return (q, r)
    }
}

Then, in your example, use that method:

func decimalHoursConv(hours input: Double) -> (_hrs: String, mins: String) {
    let (q, r) = input.quotientAndRemainder(dividingBy: 1)

    let minutes = abs((r * 60).rounded(.towardZero))

    let mins = String(format: "%.0f", minutes)
    let hrs = String(format: "%.0f", q)

    return (hrs, mins)
}

Note, if the value of the input is negative, e.g., -1.75 hours, the quotient is -1 hours, and the remainder is -0.75 hours. That is mathematically correct, but clearly if you are going to display these in the UI, you probably would want to show hours with the negative value, but not the minutes, too. So I have performed the abs of the remainder.

Also, you probably want to round minutes down (so that 1.9999 hours does not report 1:60, but rather 1:59). So, I have rounded down (to zero) the product of the remainder times 60.

Bottom line, if you really want to calculate minutes and seconds, feel free, but if it is solely to create a string representation, you might consider letting the DateComponentFormatter do this for you. It handles positive and negative values correctly, localized strings, etc.

Upvotes: 5

Leo Dabus
Leo Dabus

Reputation: 236538

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"

Upvotes: 1

Mojtaba Hosseini
Mojtaba Hosseini

Reputation: 120042

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

Upvotes: 1

AnderCover
AnderCover

Reputation: 2661

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.

Upvotes: 1

Related Questions