Det' regner – så programmer en regnemaskine i JS

[03.08.2015] [Nørderier/Projekt]

Det aller første jeg overhovede programmerede i sin tid, var en regnemaskine. Og det er jeg nok ikke den eneste der har startet sin programmeringskarriere med. Det er ret lige til, alle programmeringssprog indeholder de gængse matematiske operationer (+ , -, *, /, hhv. plus, minus, gange og dividere), derudover har alle programmeringssprog de fleste matematiske funktioner indbygget, feks. cos, sin, kvadratrod osv. Alt der skal bruges til at regne, følger altså med det at programmere uden man skal gøre andet end at finde det.

Min første regner kan ses her. Den har to felter, det ene til et tal, det andet til et andet tal. Når man har skrevet et tal i hvert felt, kan man vælge hvad der skal gøres ved de to tal, om de skal ganges med hinanden, divideres eller noget helt tredje. Problemet med regneren er nok først og fremmest at den ikke er særligt praktisk – nogle vil muligvis kalde den ubrugelig. Den kan ikke arbejde med mere komplekse stykker, feks. 2*2+2*2+2*2, den kan ikke regne med parenteser, den kan ikke huske hvad den tidligere har regnet, og feks. en konstant som pi kan den kun oplyse, den kan som udgangspunkt ikke regne med pi medmindre man orker at kopiere pi fra resultat og over i inputtet. Selv de der lommeregner med solceller der for 20 år siden lå rundt omkring i hjemmene, er mere praktiske. Derfor har jeg lavet sådan en her:

Problemet med dem var at de ikke fattede regnehierarki, de forstod ikke at 2+2*2 = 6 og ikke 8. De fleste af dem havde også meget sparsomt med funktionalitet hvilket var ok da de var beregnet til at bruge i hjemmet eller i brugsen hvis man er pedantisk med husholdningen.

Da jeg lavede min første regnemaskine, havde jeg allerede en ti-89, jeg har den faktisk endnu, den holdt op mod min egen kreation fik min regnemaskine til at falme noget i lakken. Det jeg allerhelst ville have ud af en hjemmelavet regnemaskine, var den samme form for kommandolinjeinput som i en ti-89 – jeg var blevet mageligt indstillet. I stedet for at trykke på knapper på skærmen eller markere bokse for at vælge regneoperation ville jeg lave en lommeregner hvor alt input kom fra tastaturet, og hvor piltasterne så kunne navigere i allerede regnede stykker. Alt skulle foregå med linjer. Dengang kendte jeg kun sproget PHP, det er ikke så praktisk til den slags da alle nye input kun kan blive til variable ved at hele siden opdateres. Jeg har siden lært JavaScript (hvor er det et smart sprog) der kan ændre ting på skærmen ved at brugeren trykker på et eller andet, og det uden at siden skal opdatere.

En dag tidligere på sommeren hvor vejret var regn, gik jeg altså i gang med at lave en regnemaskine. Problemet er ikke at lave en kommandolinje, det er nogenlunde lige ud ad mælkevejen, det samme gælder for de regnede linjer der skal gemmes – de kan samles i et array der forholdsvis let kan navigeres i. Problemet er derimod de ellers let tilgængelige regneoperationer i JS. For når du skriver et brugerinput, så læser programmeringssproget det som en streng. Hvis inputtet er enten et decimaltal eller et heltal, kan det parses, dvs. det kan laves om til et tal i stedet for en streng. Men computeren fatter ikke at parse plusser, minusser og den slags. I programmeringssproget er der jo allerede en mulighed for at regne med operationerne, men computer skal stadig forstå at det er operationer der står i strengen. Det kaldes at evaluere en streng, altså at gøre den feks. til et matematikstykke. I JS er der funktionen .eval() der netop evaluerer en streng, men den gør det med strengen uanset hvad der står i den, det den gør er at lave et brugerinput eller en variabel om til kode hvilket ikke er så heldigt. Tænk på en ondsindet person, ikke dig selvfølgelig, der kan skrive lige den kode på ens side han/hun har lyst til. Det kan hurtigt gå galt. Alternativt kan brugerinputtet saniteres, inden det bliver evalueret, kan man fjerne alle uønskede bogstaver og tegn og den slags fra det. Det tænkte jeg bare ikke på da jeg begyndte at skrive selve regneprocessoren. I stedet valgte jeg at regne brugerinputtet ved at bruge algoritmer – det blev til omkring 400 linjer kode og nok det mest komplekse program jeg endnu har lavet. Det fungerer ved at det sortere inputtet i grupper først af plusser, så af minusser, så af gange osv. Alle grupper regnes så igennem, på den måde kan jeg selv bestemme hierarkiet (rækkefølgen) i de udregninger programmet laver. Parentesregningerne er blevet en hel samling af funktioner for sig selv. Jeg tror måske det går for vidt at forklare min regnemaskine i detaljer, men nu er den lavet, og den dur. Den hedder, med inspiration fra programmer halvfemserne, ComCalc, og den kan fungere udelukkende på kommandoer. Se selv:

Det er ret sjovt at lave algoritmer. Det er altid noget med at sortere et eller andet, og hvem bliver ikke højt stemt af det. Men det er egentligt ikke nødvendigt at lave dem selv for at skrive en lommeregner. Jeg søgte lidt på Google og fandt biblioteket math.js – jeg ved ikke hvordan det fungerer, men det kan evaluere strenge med matematik. Og jeg går ud fra at det ikke er farligt at bruge som JS's egen .eval() funktion kan være. Med biblioteket er det noget nemmere at lave en regnemaskine: først sættes en variabel med det samlede data der skal regnes, og så skal den evalueres når personen der regner, trykker på ligmed, og det er det. Her er et eksempel på et stykke kode hvor et par strenge evalueres – for at biblioteket math.js skal kunne bruges skal det først linkes til:

<script src="http://brkmnd.dk/https://cdnjs.cloudflare.com/ajax/libs/mathjs/2.0.1/math.min.js"> <script> alert(math.eval('2+2*2')); alert(math.eval('(25+25)*2')); alert(math.eval('sqrt(24+24+log(e^2))*2'); </script>

Med det nye bibliotek har jeg opgraderet min køkkenregner til en 4000 i stedet for en 2000. Det er alligevel blevet til nogle linjer kode, men den er faktisk en del mere simpel end forgængeren der tilmed er mindre avanceret. Den kan udvides som man lyster, såfremt udvidelserne indgår i math.js biblioteket - og det ser ud til at være ret fleksibelt. SuperRegn 4000 kan tjekkes her:

Koden til 4000 versionen er her:

HTML:

<div class="skal"> <div id="display" class="disp">regner</div> <span id="solcelle1-lag"></span> <div class="solcelle" id="solcelle1" onmouseover="flimmerOn(this.id)" onmouseout="flimmerOff(this.id)"> <span></span> <span></span> <span></span> <span></span> </div> <span class="navn">SuperRegn4000</span><br> <button id="knap-parentesl" onclick="klik(this.id, 'operation')">(</button> <button onclick="clearTal('clear')">C</button> <button onclick="clearTal('all')">aC</button> <button id="knap-procent" onclick="klik(this.id, 'operation')">%</button> <button id="knap-plus" onclick="klik(this.id, 'operation')">+</button><br> <button id="knap-parentesr" onclick="klik(this.id, 'operation')">)</button> <button id="knap-1" onclick="klik(this.id, 'tal')">1</button> <button id="knap-2" onclick="klik(this.id, 'tal')">2</button> <button id="knap-3" onclick="klik(this.id, 'tal')">3</button> <button id="knap-minus" onclick="klik(this.id, 'operation')">-</button><br> <button id="knap-oploeft" onclick="klik(this.id, 'operation')">x<sup>y</sup></button> <button id="knap-4" onclick="klik(this.id, 'tal')">4</button> <button id="knap-5" onclick="klik(this.id, 'tal')">5</button> <button id="knap-6" onclick="klik(this.id, 'tal')">6</button> <button id="knap-gange" onclick="klik(this.id, 'operation')">*</button><br> <button id="knap-sqrt" onclick="klik(this.id, 'operation')">√</button> <button id="knap-7" onclick="klik(this.id, 'tal')">7</button> <button id="knap-8" onclick="klik(this.id, 'tal')">8</button> <button id="knap-9" onclick="klik(this.id, 'tal')">9</button> <button id="knap-divider" onclick="klik(this.id, 'operation')">/</button><br> <button id="knap-pi" onclick="klik(this.id, 'operation')">π</button> <button id="knap.0" onclick="klik(this.id, 'tal')" style="width:95px;">0</button> <button id="knap-komma" onclick="klik(this.id, 'operation')">.</button> <button id="knap-ligmed" onclick="ligMed()">=</button> </div>

CSS:

@font-face { // Finder skrifttypen der jo er digital. font-family: 'digital'; src: url('font/digital-7.eot'); src: url('font/digital-7.eot?#iefix') format('embedded-opentype'), url('font/digital-7.woff') format('woff'), url('font/digital-7.ttf') format('truetype'), url('font/digital-7#radioland_slimregular') format('svg'); font-weight: normal; font-style: normal; } div.skal { display:inline-block; border:1px solid black; padding:10px; border-radius:5px; background-color:#8181F7; } button { font-size:16pt; margin:5px; width:40px; } button:hover { color:#B43104; } div.disp border:1px solid black; font-family:digital; font-size:28pt; padding-right:10px; width:350px; height:40px; text-align:right; background-color:#AEB404; } div.solcelle { position:relative; background-color:#1B0A2A; border:1px solid black; width:115px; height:25px; display:inline-block; margin-top:10px; margin-left:10px; margin-right:50px; overflow:hidden; z-index: 1; } div.solcelle span { border-right:2px solid gray; width:20px; height:25px; display:inline-block; } .navn { background: linear-gradient(to left, white, gray); font-size:14pt; display:inline-block; border:1px solid gray; padding:2px; } sup { font-size:8pt; }

JS:

var tal = ""; function flimmerOn(objId) // Flimmerfunktion til solcellerne - har ikke betydning for regneren. { var obj = document.getElementById(objId); var lag = document.getElementById(objId + "-lag"); var objRect = obj.getBoundingClientRect(); var objWidth = (objRect.right - objRect.left); var objHeight = (objRect.bottom - objRect.top); lag.style.width = objWidth + "px"; lag.style.height = objHeight + "px"; lag.style.position = "fixed"; lag.style.top = objRect.top + "px"; lag.style.left = objRect.left + "px"; lag.style.zIndex = 4; lag.onmouseover = function(){this.style.zIndex = 0;} display.style.opacity = 0.8; } function flimmerOff(objId) { display.style.opacity = ""; } function clearTal(type) { if(type === "clear") { tal = tal.slice(0,-1); display.innerHTML = tal; } if(type === "all") { tal = ""; display.innerHTML = "0"; } } function filterDisp(input) { var output; output = input.replace(/pi/g, "π").replace(/sqrt/g, "√"); return output; } function klik(input, type) { var inputSlice = input.slice(5); if(type === "tal") { tal += inputSlice; display.innerHTML = filterDisp(tal); } else if(type === "operation") { if(inputSlice === "plus") { tal += "+"; display.innerHTML = filterDisp(tal); } else if(inputSlice === "minus") { tal += "-"; display.innerHTML = filterDisp(tal); } else if(inputSlice === "gange") { tal += "*"; display.innerHTML = filterDisp(tal); } else if(inputSlice === "divider") { tal += "/"; display.innerHTML = filterDisp(tal); } else if(inputSlice === "komma") { tal += "."; display.innerHTML = filterDisp(tal); } else if(inputSlice === "parentesl") { tal += "("; display.innerHTML = filterDisp(tal); } else if(inputSlice === "parentesr") { tal += ")"; display.innerHTML = filterDisp(tal); } else if(inputSlice === "sqrt") { tal += "sqrt("; display.innerHTML = filterDisp(tal); } else if(inputSlice === "oploeft") { tal += "^"; display.innerHTML = filterDisp(tal); } else if(inputSlice === "pi") { tal += "pi"; display.innerHTML = filterDisp(tal); } } } function ligMed() { tal = math.eval(tal); display.innerHTML = tal; }

Det skal ikke være nogen hemmelighed at jeg selv klart er gladest for min ComCalc. Den virker ret dynamisk. Løbende vil jeg opdatere den, og snarest vil jeg lave en samling med appz hvori ComCalc er den første. Så hvis du skal regne noget ud: stay calm and use ComCalc!

Index