Problems with massive decimal

I have strange behavior on this function. At some point (when share is 39.0 / 100) cake became more than one on a console, but the assert does not invoke. I attach the log from the console. May be I am absolutely stupid in an implementing of that algorithm, but where am I?

Code:

struct IntrestingTask {
    func calculate() {
        var cake = 1.0
        var pupils = 100
        var array = [(pipulNumber: Int, amount: Double)]()
        var index = 1
        var currentpercent = 1.0
        while pupils > 0 {
            let piece = cake * Double((currentpercent/100))
            cake -= piece
            print("piece: \(piece), share: \(currentpercent) / 100, decimal: \(Double(currentpercent/100)) cake: \(cake)")
            assert(cake < 1)
            array.append((index, piece))
            index += 1
            currentpercent = Double(index)
            pupils -= 1
        }
        let sorted = array.sorted { $0.amount < $1.amount }
 //       array.forEach { print($0) }
    }
}

I try to describe the task. It is a school task about pupils and cake. There are 100 pupils and one cake.
First takes 1 percent from the cake. Second takes 2 percent from the reminder and so one. Which pupil takes a biggest piece?

But when I execute same expression in REPL it returns fine.

The results here are all less than one; they're written in what's called scientific notation. The e-05 on the end of the number means the number is multiplied by pow(10, -5), or can be thought of as shifting the decimal point five places to the right. Your code is doing the right thing.

As a side note, you're not actually dealing with decimals here, but rather floating point arithmetic, since many decimals can't be exactly represented by a computer without taking up a lot of space. For what you're doing, floating point arithmetic should be completely fine, but it's a good thing to be aware of – just remember those numbers you're getting in the console may not be exact.

6 Likes

To add on a bit, one should also be aware that the printed representation may not reflect the precise value of the floating point value. That's on top of the value itself not precisely representing the result of the operation which created it.

String representations often truncate and round, and not all transforms will round the same way, so how you print can change the apparent result.

5 Likes

Yep I see. The floating point arithmetic is a minefield.

let one = tan(5 * Double.pi / 8)
let two = -tan(3 * Double.pi / 8)
let result = one == two
print("one: \(one), two: \(two), result: \(result)")
// one: -2.4142135623730954, two: -2.4142135623730945, result: false

It's because called Rounding Error.

tl;dr, many values can't be stored in Double properly (e.g. pi, 1/7), so they use nearest one that is representable.
So Double.pi is actually 3.141592653589793 which is slightly smaller than actual mathematical pi.

In most cases this works just fine with small error, but that also mean == may not work as expected.

A fun one is also

0.1 + 0.2 == 0.3 // false

Since Swift and many other languages use standard floating points (as per IEEE 754), you'd see consistent behaviour across other languages as well.

This is not true specifically for Swift as Float and Double conforms to LosslessStringConvertible.
But yes, for other languages (and formatted Swift string), it's something to be vary of.

2 Likes

Yes, I wrong, trying to represent an irrational number pi by decimal.

Even for rational numbers (like 0.3), most are not representable via Double (or Float, for that matter).
One need not be concerned too much about this error, but it can become noticeable if you do a lot of arithmetic (like, A LOT) and the error starts to accumulate, and that == is not very great when using these types.

On an unrelated note, your code can be cleaned up somewhat

func calculate() -> (percent: Int, amount: Double) {
    var cake = 1.0, array: [(percent: Int, amount: Double)] = []
    for percent in 1...100 {
        let piece = cake * Double(percent) / 100
        cake -= piece
        print("share: \(percent)%, piece: \(piece), cake: \(cake)")
        array.append((percent, piece))
    }
    return array.max { $0.amount < $1.amount }!
}

let (percent, piece) = calculate()
print("Largest piece is at percent \(percent), amount \(piece)")
1 Like

Actually, @Avi is correct, LosslessStringConvertable, and the Float and Double implementations, only guarantee that the description parses back to the original value when parsed to the source type, not that the description parses to a mathematically equivalent value in a wider type.

1 Like

Ah, right. I keep forgetting that it only requires lossless round-trip, not that the String representation needs to have precise bearing to the original value. String(0.3) is 0.3 to note.

Sounds more like a stackoverflow type of question :wink: