2

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")
AnderCover
  • 1,959
  • 2
  • 19
  • 34
  • `String(format: "%.0f", hours)` *rounds* the hours to the next integer. – Martin R Jun 14 '20 at 18:25
  • Excellent, thank you. Is there a quick fix for this? – Rob Wilkinson Jun 14 '20 at 18:29
  • Possibly helpful: https://stackoverflow.com/a/41207166/1187415. – Martin R Jun 14 '20 at 18:33
  • 1
    I would not recommend using formatted strings at all; NumberFormatter exists specifically to help you represent numbers as strings. First get the numbers right. Then think about how you want them represented to the user. – matt Jun 14 '20 at 18:34
  • ... and `DateComponentsFormatter` exists specifically to help you represent time intervals in hours and minutes. – Rob Jun 14 '20 at 23:18

4 Answers4

1

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.

AnderCover
  • 1,959
  • 2
  • 19
  • 34
1

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

Mojtaba Hosseini
  • 71,072
  • 19
  • 226
  • 225
  • This would have been by far the easiest option and it resolves the problem with the hours - rounding down on the hour figure will work all of the time. Since the reason for the problem has been highlighted I now see the same problem exists with the minutes. As I need this to be accurate I need to go for the DateComponentsFormatter - thank you for your help. – Rob Wilkinson Jun 15 '20 at 17:53
1

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"
Leo Dabus
  • 216,610
  • 56
  • 458
  • 536
  • You should _not_ use `en_US_POSIX` for showing results in the user interface. – Rob Jun 14 '20 at 21:53
  • @Rob It is intentional to avoid it getting localized – Leo Dabus Jun 14 '20 at 22:44
  • It only localizes it if you’ve added that localization to your app. As a general rule, it’s a mistake to add `en_US_POSIX` locale for interacting with the UI. It’s intended for those cases where you need an invariant format only, such as formatting for web service or persistent storage, in which case it is essential. But one should almost never use it when formatting for UI. – Rob Jun 14 '20 at 22:54
1

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.

Rob
  • 392,368
  • 70
  • 743
  • 952
  • Thank you for taking the time to explain this. It works and gives me the accuracy I need. – Rob Wilkinson Jun 15 '20 at 18:52
  • I must say I have my doubts about `DateComponentFormatter`'s accuracy. It's definitely the standard way to create a string representation of quantities of time but that's it, I would not trust it to round accurately. – AnderCover Jun 15 '20 at 21:35
  • That’s its whole _raison d’être._ It’s extremely reliable and accurate. The only time I’d personally contemplate doing my own when I need millisecond display (e.g. stopwatch like functionality), which `DateComponentsFormatter` doesn’t do. But for anything else, Apple engineers have provided this rich, consistent, and easily localized, solution. You’d need a reasonably compelling case to reinvent the wheel. – Rob Jun 15 '20 at 22:49
  • Well I say that because `DateComponentsFormatter` gives different values than `Measurement` whose _raison d'être_ is to provide "support for unit conversion and unit-aware calculations". So two options, either my usage of `Measurement` is flawed, or the rounding of `DateComponentsFormatter` is not that accurate – AnderCover Jun 16 '20 at 11:13
  • Also `DateComponentsFormatter` supports printing `DateComponents`, and `Measurement` is also part of `Foundation` ;) – AnderCover Jun 16 '20 at 11:19