10 minute read

med tall

Next Article
Sammendrag

Sammendrag

Dersom vi bruker streng.charAt(0) i stedet for streng[0] til å hente ut første bokstav, bør det med andre ord være mulig å kutte ut lengdesjekken. Vi endrer koden slik:

1 [Kodeboks, start] 2 // _bibliotek/hjelpemidler-strenger.js 3 export let gjoerForbokstavStor = (streng) => { 4 let foersteBokstav = streng.charAt(0) 5 let resten = streng.slice(1) 6 return foersteBokstav.toUpperCase() + resten

7 }

Når vi har lagret filen, ser vi en hake i en grønn firkant foran alle testene i listen. Det betyr at de fortsatt lykkes. Nå ser vi ikke noe mer vi kan forbedre, og vi sier oss fornøyd med funksjonen.

Enhetstester

En enhetstest er en test som sjekker om koden oppfører seg som forventet. Når vi skriver enhetstester, lager vi en samling testtilfeller. Målet er at disse skal være dekkende, altså at de representerer den typen data som vi forventer å få fra brukere.

Vi kan skrive enhetstester for en funksjon etter at vi har laget den, men det beste er å skrive testene før vi skriver funksjonen. Dette kalles testdrevet utvikling og omfatter disse stegene: 1. Skriv en funksjon som ikke returnerer noe. 2. Lag en testsamling med dekkende testtilfeller. 3. Gjennomfør ett og ett testtilfelle og sørg for at alle testtilfellene lykkes. Ofte lykkes flere testtilfeller samtidig. 4. Når alle testtilfellene passerer, refaktorerer du.

2.7 Vanlige problemer når du tester med tall

På grunn av måten datamaskinen lagrer tall på, blir ikke resultatet alltid slik vi forventer. Her er en testsamling vi har laget for funksjonen leggSammen fra filen matematikk.js.

1 // _bibliotek/matematikk.js 2 export let leggSammen = (x, y) => { 3 return x + y

4 }

1 <!-- matematikk-testsamling.svelte --> 2 <script> 3 import ErLikTest from "./_komponenter/ErLikTest.svelte" 4 import { leggSammen } from "./_bibliotek/matematikk.js"

5 6 let leggSammenTilfeller = [ 7 // Noen enkle heltall 8 [[0, 0], 0], 9 [[0, 1], 1], 10 [[1, 1], 2], 11 // Desimaltall 12 [[0.5, 0.5], 1], 13 [[0.1, 0.2], 0.3],

14 /* 15 * Store tall

16 *

17 * I Javascript kan man bruke understrekingstegn

18 * for å gjøre lange tall lettere å lese.

19 */ 20 [[9_007_199_254_740_991, 1], 9_007_199_254_740_992], 21 [[9_007_199_254_740_991, 2], 9_007_199_254_740_993], 22 [[9_007_199_254_740_991, 3], 9_007_199_254_740_994], 23 [[9_007_199_254_740_991, 4], 9_007_199_254_740_995],

24 ] 25 </script>

26 27 <h1>Enhetstester for matematikk.js</h1>

28 29 <h2>leggSammen</h2> 30 <ul> 31 {#each leggSammenTilfeller as data} 32 <li>

33 <ErLikTest

34 35 36 37 /> 38 </li> 39 {/each} 40 </ul> inndata={data[0]} forventetUtdata={data[1]} funksjon={leggSammen}

Utfallet av det femte testtilfellet er uventet: I stedet for 0,3 får vi et langt tall. Og selv om de påfølgende testtilfellene passerer, har vi fått et annet resultatet enn vi oppga, i to av dem, nærmere bestemt i de tilfellene der resultatet skulle ha blitt et oddetall.

DATAMASKINEN LAGRER IKKE ALLE TALL SLIK VI SKRIVER DEM

Årsaken til begge disse feilene er at datamaskinen bruker totallssystemet, der tall bare består av tallene 0 og 1. Derfor er det mange desimaltall datamaskinen ikke klarer å representere nøyaktig. Store heltall er også et problem fordi datamaskinen ikke har plass nok til å lagre dem.

Hvis du vil se den verdien som datamaskinen egentlig lagrer når du skriver inn et tall, kan du skrive (tallet).toPrecision(100) i konsollen i nettleseren. Da får du tilbake en streng med den verdien som faktisk er lagret (med mange overflødige nuller på slutten). Hvis du bruker toPrecision med tallene 0,1, 0,2 og 0,3, får du se at datamaskinen egentlig lagrer følgende verdier (her har vi kuttet bort de bakerste nullene):

• • • 0,1000000000000000055511151231257827021181583404541015625 0,200000000000000011102230246251565404236316680908203125 0,299999999999999988897769753748434595763683319091796875

Disse verdiene returneres som strenger, men hvis vi kopierer tallverdien som står mellom anførselstegnene, og limer den inn igjen i konsollen, får vi verdien vi startet

EKSTRASTOFF

Totallssystemet og flyttall

I totallssystemet er det slik at den plassen som står til venstre for desimaltegnet, er verdt 1, og derfra dobler verdien seg for hver plass man går mot venstre: 2, 4, 8, 16, 32 og så videre. Titallssystemet følger et lignende mønster der verdien tidobler seg for hver plass man går mot venstre: 1, 10, 100, 1000 og så videre. Så lenge vi jobber med heltall, kan vi oversette nøyaktig mellom alle tall i begge systemer. Når vi passerer kommaet, blir ting vanskeligere. Siden vi går mot høyre i titallssystemet, får plassene verdiene 1 10 1 100 1 1000

og så videre, mens plassene i totallsystemet får verdiene

1 2 1 4 1 8

og så videre. Det går dessverre ikke an å summere brøkene som står til høyre for kommaet i totallsystemet, og ende opp med brøker som kan forkortes til 1 10 2 10

eller 3 10

,

altså 0,1, 0,2 og 0,3. Når vi skriver desimaltall, lagrer datamaskinen disse tallene med så mange sifre som det er plass til. Datamaskinen lagrer store og små tall på en plassbesparende normalform som kalles for flyttall (floating point numbers på engelsk). Hvis vi skal skrive veldig store eller veldig små tall i titallssystemet, kan vi for eksempel skrive én million som 1 · 106 eller én milliontedel som 1 · 10–6. Systemet datamaskinen bruker for å lagre tall, er basert på samme mønster, bortsett fra at den ikke bruker en tierpotens for å forstørre eller forminske verdien, men en toerpotens. For eksempel vil verdien 1 2

bli lagret som 1 · 2–1 .

Fordi datamaskinen har en bestemt mengde bits, altså plasser til 1- og 0-tegn, som den kan bruke på eksponenten og på tallet som står foran gangetegnet, klarer den ikke å lagre oddetall nøyaktig hvis tallet er større enn 253 eller mindre enn –253 .

med: 0,1, 0,2 og 0,3. Det er fordi datamaskinen bruker avrundingsregler når den viser tallverdier. Disse reglene er bestemt slik at overraskende mange tall vises som den verdien vi skrev inn.

Disse avrundingsreglene er imidlertid ikke ufeilbarlige. Selv om vi legger sammen to tall som avrundes riktig, blir ikke resultatet alltid som vi forventer. Regnestykket 0,1 + 0,2 har for eksempel resultatet 0,300000000000000044408920985006261616945266 72363, som datamaskinen runder av til 0.30000000000000004 når den viser tallet. Denne verdien er ikke lik verdien som 0,3 blir konvertert til, og derfor gir 0.1 + 0.2 === 0.3 resultatet false. Som en parallell kan vi forestille oss at vi ble bedt om å vurdere om 1

3 1 3

2 3 , men måtte regne med desimaltall i stedet for brøker. Da ville vi ha skrevet om til 0,333 + 0,333 = 0,666 ≈ 0,67 og funnet at 0,66 ≠ 0,67. Utfallet er feil, men vi kunne ikke ha gjort det noe bedre med de begrensningene vi hadde.

LAGE EN TESTKOMPONENT SOM SAMMENLIGNER ET VISST ANTALL DESIMALER

For å løse problemet med desimaler som rundes av, i testene våre, kan vi lage en ny komponent, ErLikTestMedPresisjon, som sjekker om resultatet er likt innenfor et visst antall desimaler. Den er stort sett identisk med ErLikTest, men har egenskapen antallDesimaler, som vi bruker for å bestemme hvor mange desimaler tallene skal ha når vi sammenligner dem. Vi velger at denne egenskapen skal ha standardverdien 2. For å runde av tallene bruker vi toPrecision.

1 <!-- _komponenter/ErLikTestMedPresisjon.svelte --> 2 <script> 3 export let funksjon 4 export let inndata 5 export let forventetUtdata 6 // Ny egenskap 7 export let antallDesimaler = 2

8 9 let feil 10 let erLike 11 let resultat

12 13 try { 14 resultat = funksjon(...inndata) 15 // Runder av før sammenligning 16 erLike =

17

resultat.toPrecision(antallDesimaler) === 18 forventetUtdata.toPrecision(antallDesimaler) 19 } catch (error) { 20 feil = error 21 console.log(error)

22 } 23 </script>

24

25 {#if feil}��{:else if erLike}✅{:else}❌{/if}

26 <!-- Lik visning, bortsett fra to detaljer: --> 27 <span class="data">{JSON.stringify(inndata)}</span>

28 -> 29 <span class="data">{JSON.stringify(forventetUtdata)}</span> 30 <!-- Opplys om antall desimaler etter testen … --> 31 (innenfor {antallDesimaler} desimaler)

32 {#if feil || !erLike} 33 <ul> 34 <li>

35 36 {#if feil}

KRASJET: <span class="data">"{feil.message}"</span>

37 38 39 {:else}

Fikk -> <span class="data">{JSON.stringify(resultat)}</span> <!-- … og hva det avrundede resultatet var i feilvisningen. -->

40 , som avrundes til

41

<span class="data">{resultat.toPrecision(antallDesimaler)}</span> 42 {/if} 43 </li> 44 </ul> 45 {/if}

46 47 <style> 48 .data { 49 font-family: monospace; /* Kodeskrift */ 50 white-space: pre; /* La alle mellomrom stå. */

51 } 52 </style>

I matematikk-testsamling.svelte bytter vi til ErLikTestMedPresisjon.

1 <!-- matematikk-testsamling.svelte --> 2 <script> 3 import ErLikTestMedPresisjon from "./_komponenter/ErLikTestMedPresisjon.svelte" 4 // Ellers lik script-del 5 </script>

6 7 <h1>Enhetstester for matematikk.js</h1>

8 9 <h2>leggSammen</h2> 10 <ul> 11 {#each leggSammenTilfeller as data} 12 <li>

13 14 15 16 <!-- ... og her bruker vi den nye komponenten. --> <ErLikTestMedPresisjon inndata={data[0]} forventetUtdata={data[1]}

18 19 20 /> 21 </li> 22 {/each} 23 </ul> funksjon={leggSammen} antallDesimaler={2}

Da blir resultatet:

Hvis vi sender inn en høyere verdi til egenskapen antallDesimaler, for eksempel 20 med antallDesimaler={20}, vil testen feile igjen, for da er ikke lenger de avrundede verdiene like.

HVA VI SKAL GJØRE MED STORE TALL, AVHENGER AV SITUASJONEN

Du kan se det høyeste heltallet som datamaskinen klarer å lagre nøyaktig, ved å skrive Number.MAX_SAFE_INTEGER i konsollen. Da får du ut verdien 9007199254740991. Når et heltall er over denne grensen, er det ikke sikkert at datamaskinen klarer å lagre nøyaktig. Hvis datamaskinen ikke klarer å lagre den nøyaktige verdien, runder den det av til den nærmeste verdien den klarer å representere. Burde vi gjøre noe med leggSammen for å hanskes med tall som er større enn dette? Hvis vi kan akseptere litt unøyaktighet, kan vi godt la være å gjøre noe. Det er ikke sikkert at funksjonen leggSammen noensinne skal brukes til å legge sammen tall som er så store som dem vi brukte i denne testen. Number.MAX_SAFE_INTEGER er et veldig stort tall, over 9 billiarder. Når vi jobber med så store tall, blir et avvik på 1 forsvinnende lite – under én billiarddel. I slike situasjoner klarer vi oss som regel fint med et avrundet, litt unøyaktig svar.

I visse sammenhenger er det ikke akseptabelt med unøyaktige tall. Hvis vi for eksempel leter etter store primtall, nytter det ikke å jobbe med en avrundet verdi – da må vi jobbe med det nøyaktige tallet. Hvis leggSammen skal brukes i en slik sammenheng, er det bedre at funksjonen sier fra til brukeren. Nedenfor ser du hvordan vi kan få funksjonen leggSammen til å gi en feilmelding dersom resultatet er for høyt til å være nøyaktig. Da vil testtilfellene si fra når tallet er for høyt.

1 // matematikk.js 2 export let leggSammen = (x, y) => { 3 let resultat = x + y 4 if (resultat > Number.MAX_SAFE_INTEGER) { 5 throw new Error(

6 7 ) 8 } "Resultatet var høyere enn Number.MAX_SAFE_INTEGER"

9 return resultat

10 }

EKSTRASTOFF

Noen tall bør lagres som strenger

En av bokas forfattere hadde en gang et oppdrag for et firma som laget et salgssystem. Dette salgssystemet hadde bestillings-ID-er som besto av tallrekker på 20 sifre. Den dagen firmaet lanserte hjemmesiden sin, fikk de feilmeldinger fra kunder som fikk opp andre bestillinger enn den de hadde søkt på. Feilen viste seg å ligge på nettsiden der kundene skulle taste inn bestillings-ID-en. Etter at kunden hadde tastet bestillings-ID-en inn i et input-felt for tekst, ble den gjort om til et tall. Fordi tall på 20 sifre ligger langt over Number.MAX_SAFE_INTEGER, ble mange tall rundet av til et nærliggende tall som tilsvarte en annen bestilling. De klarte heldigvis å fikse feilen samme dag. I hverdagen bruker vi ofte tallrekker av ulik lengde for å identifisere personer, steder, bestillinger og lignende. Noen eksempler er fødselsnumre, telefonnumre og postnumre. Disse tallene er godt under Number.MAX_SAFE_INTEGER, men de starter ofte med 0-tegn. JavaScript fjerner overflødige 0-tegn fra starten av et tall, så slike 0-tegn vil forsvinne dersom disse numrene lagres som tall. Skatteetaten.no oppgir «011299 55131» som et eksempel på et fødselsnummer. Postnumrene i Oslo starter på 0, for eksempel 0010 (postnummeret til Det kongelige slott). Prøver vi å lagre disse verdiene som tall, kan det skape små og store problemer. Når vi bruker en tallrekke til å identifisere noe, er det altså best å lagre den som en streng. Da slipper vi å få problemer hvis tallet starter på 0 eller blir veldig langt.

Viktige grenser for tall

Number.MAX_SAFE_INTEGER og Number.MIN_SAFE_INTEGER er det høyeste heltallet og det laveste heltallet datamaskinen klarer å representere nøyaktig. Når et heltall befinner seg innenfor dette området, klarer datamaskinen alltid å lagre tallverdien nøyaktig. Når et heltall befinner seg utenfor dette området, runder datamaskinen av til den nærmeste verdien den klarer å lagre. Jo lenger man beveger seg utenfor grensene, desto større blir unøyaktigheten i absoluttverdi, men den er som regel bitte liten i forhold til hvor stort tallet er. Number.MAX_VALUE er det største tallet datamaskinen i det hele tatt klarer å lagre. Denne verdien er større enn 10300, altså et titall med 300 nuller bak. Hvis du skriver inn et tall som er større enn dette, klarer ikke datamaskinen engang å runde det av.

This article is from: