Menu

22 juli 2016

Decimale getallen gebruiken in Swift

Swift icoon Apple

Om getallen met één of meer cijfers achter de komma te kunnen gebruiken, kun je in Swift werken met het datatype Double. Een Double-waarde zet zo’n waarde om in een binaire representatie: een combinatie van nullen en enen.

De manier waarop zulke getallen worden bewaard, wordt ook wel de floating-point methode genoemd. Daarbij wordt door Swift (en alle andere programmeertalen) echter afgerond, waardoor je voor verrassingen kunt komen te staan.

Double in Swift: dit kan er misgaan

In de volgende Playground zie je een demonstratie. We tellen telkens met ééntiende terug vanaf de waarde 1,8.

let start = 1.8
let m = 0.1
 
var teller = start
 
while teller > 0 {
  print("Teller: \(teller)")
  teller -= m
}

Het resultaat is echter anders dan je zou verwachten:

Swift double decimale getallen

Zowel start als m zijn Double-waarden. De variabele teller wordt dus ook een Double-waarde. (Dit wordt type-afleiding genoemd: Swift probeert waar het maar kan om het datatype van een variabele automatisch te bepalen.)

Je ziet dat het niet overal goed gaat. Zodra Swift probeert om 0.1 af te trekken van 1.1, is het resultaat niet 1.0, maar 0.99999.

Dergelijke afrondingsfouten horen bij floating-point, maar zijn op z’n zachtst gezegd niet prettig. Zodra je bijvoorbeeld met bedragen gaat werken, kan dit enorm vervelende gevolgen hebben. Niet voor niets is één van de eerste dingen die je als ontwikkelaar leert: gebruik NOOIT floating-point voor geldbedragen!

Het alternatief: NSDecimalnumber

Als het absoluut cruciaal is dat decimale getallen juist worden afgerond, bijvoorbeeld bij geldbedragen, kun je werken met een speciaal datatype uit het Foundation-framework: NSDecimalNumber. Dit is geen waardetype (zoals Double), maar een verwijzingstype (dus een class). NSDecimalNumber is daarom wat omslachtiger in het gebruik, maar je voorkomt er een hoop problemen mee. Kijk maar naar de volgende Playground:

import Foundation
 
let start = 1.8
let m = 0.1
 
var betereTeller = NSDecimalNumber(double: start)
 
while betereTeller.doubleValue > 0 {
  betereTeller = betereTeller.decimalNumberBySubtracting(NSDecimalNumber(double: m))
  print("betereTeller: \(betereTeller)")
}

Hier krijgen we wél het verwachte resultaat:

Nog meer swift decimale getallen

Berekeningen met NSDecimalNumber

Omdat NSDecimalNumber een class is en betereTeller dus een object van die class is, kunnen we niet zomaar optellen of aftrekken. In plaats daarvan maken we gebruik van een methode: .decimalNumberBySubstracting(_: NSDecimalNumber). En als we de inhoud van ons betereTeller-object in Double-formaat willen hebben, gebruiken we de property .doubleValue.

Ook voor andere berekeningen bestaan methodes, zoals:

  • .decimalNumberByAdding()
  • .decimalNumberByMultiplyingBy()
  • .decimalNumberByDividingBy()
  • .decimalNumberByRaisingToPower()
  • .decimalNumberByMultiplyingByPowerOf10()

Al deze methodes verwachten slechts één argument: een NSDecimalNumber-object. Om dus bijvoorbeeld twee getallen bij elkaar op te tellen, heb je twee NSDecimalNumber-objecten nodig!

Het resultaat van deze berekeningen is, zoals je waarschijnlijk al had verwacht, ook weer een NSDecimalNumber-object. Daar kun je vervolgens, met properties zoals .floatValue, .doubleValue en .integerValue weer ‘gewone’ Swift-datatypes van maken.

Wil je meer weten over Swift, en over het maken van je eigen apps voor de iPhone, iPad en Apple Watch? Als appletips-lezer krijg je tien euro korting op het populaire eBook van de iOS Academie: Apps bouwen met Swift. Klik hier voor meer informatie.

Wil je zelf apps ontwikkelen met Swift? Dat kan, op appletips maken we je wegwijs in de wereld van Swift, Klik hier voor alle posts, plus lesmateriaal.







Reacties


  • David 22 juli 2016 om 14:15

    We zijn opgegroeid met het decimale stelsel en daardoor laten we ons verrassen door ander gedrag in andere getalstelsels, maar ook het decimale stelsel heeft dit soort problemen. Decimaal is niet per se beter, voor wetenschappelijke berekeningen kun je beter floating point datatypes blijven gebruiken.
    Voor valuta geldt dat er nog meer regeltjes zijn om rekening mee te houden en NSDecimalNumber is ook daar niet altijd de juiste oplossing. (Denk aan specifieke regels hoe je om moet gaan met afrondingen wanneer een bedrag wordt geconverteerd van de ene valuta naar de andere).

  • ruurd 22 juli 2016 om 16:31

    De conditie voor de loop in betereTeller vind ik nogal lomp. Ook met het vergelijken van floats en doubles moet je erg uitkijken. Ik zou liever:

    while ticker.compare(NSDecimalNumber.zero()) == NSComparisonResult.OrderedDescending {

    }

    doen. Daarmee bewerstellig je een nauwkeuriger vergelijking met 0.

  • Paul 23 juli 2016 om 17:33

    Het is zeer aan te raden om een betere lusconditie te gebruiken om te voorkomen dat de lus nooit stopt, dus een test op <= 0 ipv == 0 (neem start = 1.85 als voorbeeld)

Een reactie toevoegen: