0

This doesn't compile because Initializer 'init(_:)' requires that 'Number' conform to 'BinaryInteger'

struct Percentage<Number: Numeric> {
    let value: Number
    let total: Number
    var percentage: Double {
        Double(value) / Double(total)
    }
}

Does anyone have a nice solution?

To give some context to the problem from real life: I'm coding a SwiftUI app, that has a CircularProgress-view. I would like to use the same CircularProgress-view with different number types and to be able to show the current value in proportion to min and max. To do that, I need to solve the problem above.

Melodius
  • 2,505
  • 3
  • 22
  • 37

2 Answers2

3

You can create extensions on Percentage where you restrict Number to BinaryInteger and FloatingPoint to be able to use the / operator.

struct Percentage<Number: Numeric> {
    let value: Number
    let total: Number
}

extension Percentage where Number: BinaryInteger {
    var percentage: Double {
        Double(value) / Double(total)
    }
}

extension Percentage where Number: FloatingPoint {
    var percentage: Number {
        value / total
    }
}

Percentage(value: 15, total: 60).percentage // 25.0
Percentage(value: 1.5, total: 3.0).percentage // 50.0
Dávid Pásztor
  • 51,403
  • 9
  • 85
  • 116
  • I was about to post a similar solution using generic methods instead of computed properties. Note that you still need to provide a solution for Decimal type. The main draw back with this solution is that for binary integer it will only return a Double. I will post my approach anyway – Leo Dabus Jan 15 '21 at 14:31
  • Slightly unrelated, but you might want to check if `total` is not zero if you want to be safe. Like `total != .zero ? value / total : .nan` or something. – rraphael Jan 15 '21 at 14:34
  • 1
    @rraphael that's also a good point and Apple does cover it on their FloatingPoint documentation. But this is probably not important when dealing with prices – Leo Dabus Jan 15 '21 at 14:36
  • Regarding the semantics of percentage: my understanding is that 50 % means 0.5. If 50 % means 50, then 50 % * 100 = 500 – Melodius Jan 15 '21 at 15:20
  • @Melodius What do you mean? 50% of 100 is 50. `0.5 * 100` – Leo Dabus Jan 15 '21 at 16:07
  • @DávidPásztor no need to multiply by 100. The result is correct IMO. You can verify/display it with NumberFormatter using percent style `let nf = NumberFormatter()` `nf.numberStyle = .percent` `nf.string(for: 0.5)` // "50%". Ratio is 16:9, 1:2, 1:5 and so on. – Leo Dabus Jan 15 '21 at 16:10
  • @Leo Dabus: I just mean that 50 % = 50/100 = 0.5. So if a function should return percentage as a number, then it by definition should return 0.5 for 50 % and not the number 50. But this is of course just nit picking semantics ;o) – Melodius Jan 15 '21 at 16:27
  • @Melodius I still dont get what is the point. We are talking about the percentage of a value compared to total. Percentage is not the input, is the output here. But I definitely agree that 1.5 of 3.0 should return 0.5. As I've commented above as well percentage shoudn't be multiplied by 100. – Leo Dabus Jan 15 '21 at 16:30
3

The main issue is that Numeric doesn't support generic divisions. One possible solution is to provide multiple generic methods to support different protocols (Integer/FloatingPoint) and Decimal as well:

extension Decimal {
    var number: NSDecimalNumber { self as NSDecimalNumber }
    var double: Double { number.doubleValue }
}

struct Percentage<T: Numeric> {
    let value: T
    let total: T
    func percentage<F: BinaryFloatingPoint>() -> F where T: BinaryFloatingPoint {
        F(value) / F(total)
    }
    func percentage<F: BinaryFloatingPoint>() -> F where T: BinaryInteger {
        F(value) / F(total)
    }
    func percentage<F: BinaryFloatingPoint>() -> F where T == Decimal {
        F(value.double) / F(total.double)
    }
    func percentage() -> Decimal where T == Decimal {
        value / total
    }
}

let percentageInt = Percentage<Int>(value: 10, total: 100)
let result1: Double = percentageInt.percentage()   // 0.1

let percentageDouble = Percentage<Double>(value: 10, total: 100)
let result2: Double = percentageDouble.percentage()   // 0.1
let result3: CGFloat = percentageDouble.percentage()   // 0.1

let percentageDecimal = Percentage<Decimal>(value: 10, total: 100)
let result4 = percentageDecimal.percentage()     // 0.1 decimal
let result5: Double = percentageDecimal.percentage()   // 0.1
Leo Dabus
  • 229,809
  • 59
  • 489
  • 571