Technical Article

Technische functies in Delphi: Talstelselconversie, Complex

De technische functiefamilie (engineering family) in Excel leest als het makkelijkste deel van de functiereferentie. DEC2BIN zet een getal om in een binaire string. HEX2DEC zet het weer terug. IMSUM telt twee complexe getallen bij elkaar op. Elk daarvan lijkt op een opmaakoefening. Dat zijn ze niet. Achter deze namen schuilt een 10-bits 'two's complement'-codering die de meeste ontwikkelaars sinds hun lessen computerarchitectuur niet meer hebben aangeraakt, een complex-getalformaat dat volledig binnen strings leeft, en bitwise operators die geruisloos een 64-bits integer kunnen laten overlopen als u verschuift (shiften) voordat u controleert. Een spreadsheet-engine die Excel exact reproduceert, kan daar niets van weglaten.

De functies zijn onderverdeeld in drie groepen, en elke groep verbergt een andere valkuil. Talstelselconversie (base conversion) gaat over negatieve getallen en drempels per talstelsel. Complexe rekenkunde gaat over het parseren en opmaken van een string. Bitwise bewerkingen gaan over het binnen de grenzen van Int64 blijven. Dit artikel bespreekt elke groep zoals HotXLS deze implementeert, met de werkbladaanroepen die u daadwerkelijk zou schrijven.

Talstelselconversie en het 10-bits 'two's complement'

De heenweg is wat iedereen verwacht. DEC2BIN(9) geeft "1001", en een optioneel tweede argument vult het resultaat aan de linkerkant aan tot een vaste breedte. De valkuil is negatieve invoer. Excel schrijft geen minteken. Het codeert de waarde als een 10-cijferige 'two's complement'-string in het doeltalstelsel, en dat is waarom DEC2BIN(-5,10) de string "1111111011" retourneert in plaats van iets met een plusteken of minteken. Het aantal-posities-argument (places) wordt genegeerd zodra de waarde negatief is, omdat de codering al is vastgezet op tien cijfers.

Tien cijfers is een vast budget, en dat budget bepaalt het bereik dat per talstelsel kan worden weergegeven. In het binaire stelsel is de grootte die omslaat naar de negatieve helft is 512, en de wikkelmodulus 1024, dus een binaire string is alleen getekend (signed) wanneer deze exact tien tekens lang is en de waarde ten minste 512 is. Hetzelfde idee schaalt met het talstelsel. Octaal gebruikt een halve drempel van 2^29 en een volledige modulus van 2^30. Hexadecimaal gebruikt 2^39 en 2^40. De HotXLS-lezer past exact deze regel toe: hij verzamelt de cijfers, en alleen wanneer the string tien tekens breed is en de verzamelde waarde op of boven de halve drempel ligt, trekt hij de volledige modulus eraf om de getekende waarde te herstellen. Een string van negen tekens is altijd niet-negatief, hoe groot ook.

De encoder is het spiegelbeeld. Een niet-negatieve waarde wordt cijfer voor cijfer geconverteerd en optioneel met voorloopnullen aangevuld tot de gewenste breedte, en wordt geweigerd als deze het positieve maximum van het talstelsel overschrijdt of als de gewenste breedte te smal is om de waarde te bevatten. Een negatieve waarde wordt eerst binnen het bereik gebracht door de volledige modulus op te tellen, wat het verandert in een waarde waarvan de weergave in het talstelsel altijd tien cijfers is, en dan de cijfers worden verzonden met voorloopnullen om de breedte te vullen. De enkele gedeelde bereikcontrole, de symmetrische onder- en bovengrenzen per talstelsel, is wat DEC2BIN, DEC2OCT en DEC2HEX consistent met elkaar houdt aan hun grenzen.

Dat laat de conversies tussen verschillende talstelsels over, zoals HEX2BIN en OCT2HEX, die van stelsel wisselen zonder decimaal te passeren in de functienaam. De implementatie bevat geen afzonderlijke routine voor elk geordend paar. Het parseert de invoerstring naar een getekende decimale waarde met behulp van het brontalstelsel, en maakt die decimale waarde vervolgens op in het doeltalstelsel. Decimaal is het draaipunt. Eén parseerroutine en één opmaakroutine, gecombineerd, dekken elke combinatie, en omdat beide helften dezelfde 10-cijferige getekende conventie delen, overleeft een negatieve waarde de reis met behoud van het teken.

Complexe getallen zijn strings, dus het werk is parseren

Excel heeft geen complex datatype. Een complexe waarde is de string "a+bi", en elke functie in de IM-familie accepteert die strings en geeft er een terug. COMPLEX bouwt de string op uit een reëel en een imaginair deel. IMSUM, IMSUB, IMPRODUCT en IMDIV parseren hun argumenten, voeren de berekening uit op de numerieke delen en maken het resultaat weer op als een string. Het numerieke werk is basisalgebra. De moeilijkheid zit hem volledig in het betrouwbaar omzetten van de tekst in twee zwevende-kommagetallen (floating-point), en dat is waar de interne parser zijn nut bewijst.

Twee details in die parser zijn gemakkelijk verkeerd te doen. Het eerste is de kale imaginaire eenheid. De string "i" betekent één keer i, niet nul en geen fout. Dus wanneer de coëfficiënt voor het achtervoegsel (suffix) leeg is of een los plus-teken is, de parser moet dit lezen als de waarde 1, en een losse min als -1. Sla dat over en IMSUM("i","i") is niet langer 2i. Het tweede is dat wetenschappelijke notatie botst met het teken dat het reële en imaginaire deel scheidt. De parser vindt dat scheidingsteken door te zoeken naar een plus of min, maar een getal geschreven als "1.5E-3" bevat een min die bij de exponent hoort. De zoekopdracht weigert daarom een plus of min als scheidingsteken te behandelen wanneer het teken er direct voor e of E is. Zonder die beveiliging zou het reële deel bij het exponentteken in tweeën worden gesplitst en zou de parsering mislukken op volkomen geldige invoer.

Het achtervoegsel zelf blijft behouden in plaats van te worden genormaliseerd. Excel accepteert zowel i als j, en HotXLS onthoudt welke van de twee de invoer gebruikte, zodat het opgemaakte resultaat dezelfde letter draagt. De opmaak past vervolgens de gebruikelijke verkortingen toe: een imaginair deel van één wordt afgedrukt als alleen het achtervoegsel, min één als -i, een imaginair deel van nul valt samen tot een gewoon reëel deel, en een reëel deel van nul laat de voorafgaande 0+ weg.

var
  Book: TXLSXWorkbook;
  Sheet: TXLSXWorksheet;
begin
  Book := TXLSXWorkbook.Create;
  try
    Sheet := Book.Sheets.Add('Engineering');
    // Negative input: a ten-bit two's complement, places argument ignored.
    Sheet.Cells[1, 1].Value := Sheet.Calculate('=DEC2BIN(-5,10)'); // 1111111011
    // Complex multiply on two "a+bi" strings.
    Sheet.Cells[2, 1].Value := Sheet.Calculate('=IMPRODUCT("3+4i","1+2i")'); // -5+10i
  finally
    Book.Free;
  end;
end;

De transcendente complexe functies, waaronder IMSQRT, IMEXP, IMLN en IMPOWER, werken niet in rechthoekige coördinaten. Ze converteren de geparseerde waarde naar de poolvorm (polar form), passen de bewerking toe op de modulus en het argument, en converteren weer terug. Een vierkantswortel halveert het argument en neemt de wortel van de modulus. Een macht vermenigvuldigt het argument en verheft de modulus. Dit op een andere manier doen zou betekenen dat elke identiteit in rechthoekige vorm opnieuw moet worden afgeleid, wat leidt tot zowel meer code als minder numerieke stabiliteit nabij de vertakkingssneden (branch cuts).

Bitwise operators en de overloop die u eerst moet controleren

Excel 2013 heeft BITAND, BITOR, BITXOR, BITLSHIFT en BITRSHIFT toegevoegd. De operanden zijn beperkt: elk moet een niet-negatief geheel getal zijn dat niet groter is dan 2^48 min 1, en elk fractioneel of negatief argument is een numerieke fout. Dat maximum is genereus genoeg om elke realistische set vlaggen te dekken, terwijl het ruim binnen het exact representeerbare bereik van een double blijft. Dit is belangrijk omdat Excel elk numeriek argument als een zwevende-kommagetal doorgeeft.

De verschuivingsfuncties (shift functions) dragen de enige ordeningsregel met zich mee die echt gevaarlijk is. Een verschuiving naar links (left shift) kan een waarde opleveren die veel groter is dan de invoer. Als u de shl eerst uitvoert en het resultaat achteraf controleert, is Int64 al afgelopen (overflow) en is de test betekenisloos. De controle moet vóór de verschuiving plaatsvinden. HotXLS vergelijkt de operand met het maximum dat naar rechts is verschoven met de verschuivingsgrootte, en voert de daadwerkelijke verschuiving naar links alleen uit als de operand past. Een verschuivingsgrootte van meer dan 53 bits wordt direct geweigerd, en een negatieve verschuiving keert simpelweg de richting om, zodat BITLSHIFT met een negatief aantal zich gedraagt als een verschuiving naar rechts. Het principe reikt veel verder dan deze ene functie: wanneer er een controle bestaat om overloop te voorkomen, moet deze worden uitgevoerd op de invoer, en nooit op het resultaat dat deze moest beschermen.

// Bitwise calls evaluate the same way through Calculate.
Sheet.Cells[3, 1].Value := Sheet.Calculate('=BITAND(13,11)');    // 9
Sheet.Cells[4, 1].Value := Sheet.Calculate('=BITLSHIFT(5,2)');   // 20
Sheet.Cells[5, 1].Value := Sheet.Calculate('=BITRSHIFT(40,3)');  // 5

Toekomstige functies en het _xlfn-naamprefix

De bitwise operators en een lange lijst van andere toevoegingen van na 2007 hebben te maken met een naamgevingsschema dat niets te maken heeft met wat ze berekenen, maar alles met hoe Excel ze opslaat. Het oorspronkelijke binaire werkbladformaat wees aan elke ingebouwde functie een numerieke positie toe in een vaste tabel. Functies die zijn bedacht nadat die tabel was bevroren, hebben geen positie. Om zo'n functie in een bestand op te slaan en door een modernere Excel te laten herkennen, de naam is geschreven met een _xlfn.-prefix, zodat BITAND op schijf wordt opgeslagen als _xlfn.BITAND, ook al typt de gebruiker alleen BITAND.

De addert onder het gras is dat de regel niet uniform is. Sommige nieuwere functies kregen tabelposities en worden kaal geschreven, terwijl een paar oudere verborgen functies ook zonder prefix worden geschreven, ondanks hun leeftijd. HotXLS houdt een expliciete whitelist van welke namen de prefix nodig hebben, voegt deze toe bij het schrijven en verwijdert deze bij het lezen, zodat de formuletekst die u instelt en terugleest altijd de schone, voor Excel bestemde naam is. U stelt =BITLSHIFT(5,2), het bestand bevat _xlfn.BITLSHIFT, en de waarde komt hoe dan ook terug als 20. De prefix is een opslagdetail dat nooit mag lekken naar de formules waarmee u in code werkt.

Alles samenbrengen op een werkblad

De openbare interface hiervoor is klein. Maak een TXLSXWorkbook aan, voeg een werkblad toe en schrijf ofwel een formule in een cel via Cells[Row, Col].Formula en herbereken deze, of evalueer een expressie rechtstreeks met de Calculate-methode van het werkblad, die de formule compileert ten opzichte van dat blad en een Variant retourneert. De bovenstaande voorbeelden gebruiken Calculate omdat dit het resultaat van een enkele technische aanroep toont zonder de omringende bladstatus, maar dezelfde functies evalueren identiek binnen echte celformules wanneer het werkboek herberkent.

De coderingen zijn het belangrijkste om te onthouden, niet de plaatsen waar de functies worden aangeroepen. Een binaire string is alleen getekend bij tien cijfers en alleen voorbij de halve drempel van zijn talstelsel. Een complex getal is tekst, een lege imaginaire coëfficiënt is één, en de parser stapt over de e van een exponent heen. Een verschuiving naar links wordt gecontroleerd voordat deze verschuift. Onthoud deze vier feiten en de technische functiefamilie is niet langer een bron van verrassingen over verkeerde tekens.

Als u uw eigen domeinberekeningen aan dezelfde engine koppelt, de mechanismen voor het registreren van een handler en het retourneren van waarden worden behandeld in ons artikel over het uitbreiden van de formule-engine met aangepaste functies, en wanneer die formules via een naam naar andere bladen moeten verwijzen in plaats van via een celadres, de handleiding over gedefinieerde namen en formules over meerdere bladen laat zien hoe de referenties worden opgelost. De technische functies die hier worden beschreven, worden geleverd als onderdeel van de HotXLS-spreadsheetcomponent voor Delphi en C++Builder, naast de API's voor lezen, schrijven en berekenen die elders op deze blog worden behandeld.