Nachdem wir die grundlegenden EGL-Programmstrukturen sowie die Datendefinitionen kennengelernt haben, stehen nun die mit den Daten arbeitenden Anweisungen im Inneren der Programmstrukturen im Mittelpunkt unseres Interesses. Durch klare Strukturen lassen sich dabei die zahlreichen Fakten so weit systematisieren, dass man auf dieser Grundlage mit Unterstützung des Editors bald schnell und zielgerichtet große Teile des Codes schreiben und auftretende Probleme erkennen und beheben kann.

Die Parts und ihre Funktionen sind nun bekannt, und beim Entwickeln unserer kleinen CRM-Anwendung haben wir mehr oder weniger intuitiv schon einige Zeilen Code geschrieben. Auch die Beispiele haben schon ein Stück Programmiergefühl für das Schreiben von Anweisungen zum Arbeiten mit Variablen vermittelt. Doch irgendwann muss man beginnen, das erworbene "lineare" Wissen zu systematisieren, damit man einen Überblick über die zur Verfügung stehende Funktionalität der Sprache erlangt und auch komplexere Probleme kreativ lösen kann. Dadurch braucht man nicht so viele Fakten und vermeintliche Besonderheiten zu lernen, und auftretende Probleme erklären sich meist auch von selbst.

Zu Beginn besprechen wir zunächst die Übung aus dem letzten Artikel, an die wir dann mit einigen Inhalten anknüpfen können.

Übung aus dem letzten Artikel:

  1. Erstellen Sie ein allgemeines EGL-Projekt EGLAllgemeinund in diesem das Paket math.
  2. Erstellen Sie im Paket mathdie EGL Library Zufall!
  3. Erstellen Sie in Zufalldas Gerüst der Funktion zufallsZahl():
    • empfängt einen Input-Parameter vom Typ intmit dem Namen max
    • gibt die Zufallszahl als intzurück
  4. Vervollständigen Sie die Funktion zufallszahl()mit folgendem Code und versuchen Sie, diesen zu verstehen:
    floatZahl float= JavaLib.invoke("java.lang.Math", "random");
    bigIntZahl bigInt=(floatZahl * max);
    intZahl int= bigIntZahl % max;
    return(intZahl + 1);
  5. Schreiben Sie für die Library Zufalleine weitere Funktion zufallsZahlen():
    • empfängt einen weiteren Parameter vom Typ intmit dem Namen anzahlfür die Anzahl auf einmal zu berechnender Zufallszahlen
    • gibt alle Zufallszahlen in einem passenden Array zurück
    • berechnet die Zufallszahlen mit der Funktion zufallsZahl()
  6. Schreiben Sie ein Programm math.ZufallTestzum Testen der Methoden der Library math. Zufall!

Nun zur Durchführung des Kodierens und zur Analyse des Codes:

  • Für das Erstellen des EGL-Projekts, des EGL-Pakets, des Library Part und des Program Part inklusive Main-Funktion benutzt man die dafür vorgesehenen Wizards, die am besten aus dem Kontextmenü des umgebenden Kontexts aufgerufen werden. Falls man bei der Programmerstellung die Checkbox für ein aufgerufenes Programm nicht abgewählt hat, muss man die Parameterliste auf Programmebene entfernen, sonst lässt sich das Programm nicht zum Testen aufrufen.
  • Die Funktionen der Library Zufallerstellt man am besten mit der Inhaltshilfe (Strg + leer), die auch sonst beim Vervollständigen des Codes nach ein paar eingegebenen Zeichen gute Dienste leistet, obwohl der Java-Programmierer vom Eclipse Java Editor noch mehr Komfort gewöhnt ist. Beachten Sie die Parameterlisten in den Zeilen 5 und 12 von Abbildung 1. Die zusätzlichen Parameterangaben inverbessern nur die Qualität der Software und sind optional.
  • Funktion zufallsZahl():
    • Abbildung 1, Zeile 6: Eine Zufallszahl zwischen 0 und (ausschließlich) 1 mit einer Mantisse doppelter Gleitkommagenauigkeit liefert die statische Methode random()der Java-Klasse java.lang.Math, die durch die EGL-Funktion invoke()der EGL Library JavaLibausgeführt wird. Da es sich um eine statische Methode ohne Parameter handelt, ist ihr Aufruf vergleichsweise einfach. Die wichtigste Aussage zu dieser Zeile ist jedoch nicht sichtbar: Durch die Möglichkeit des Aufrufs von Java-Funktionalität ist die Funktionalität von EGL quasi "unendlich". Was an Funktionalität durch EGL kodierbar ist, erhöht – etwas überspitzt ausgedrückt – nur den Programmierkomfort. Somit braucht EGL nur die wichtigste bzw. am häufigsten benutzte Funktionalität bereitzustellen und dieses Angebot im Zuge der Entwicklung der Sprache gemäß Nachfrage und Anforderungen zu erweitern.
    • Zeile 7: Für eine gleichverteilte ganze Zufallszahl zwischen 1 und einem vorgegeben Maximum muss erst einmal eine ganze Zahl gebildet werden, und diese sollte zur Verbesserung der Qualität – es handelt sich, genau genommen, nur um eine Pseudozufallszahl – am besten mit hoher Genauigkeit (63 Bit plus Vorzeichen) definiert werden, insbesondere auch deshalb, weil sie noch mit einem Integer multipliziert wird.
    • Zeile 8: Die Gleichverteilung im gewünschten Bereich erhält man durch den Divisionsrest bzgl. Division durch das Maximum.
    • Zeile 9: Die Funktionswertrückgabe als intgemäß returns-Klausel in Zeile 5 wird durch returnrealisiert, wobei der zurückzugebende Divisionsrest noch um 1 nach oben korrigiert werden muss.
  • Funktion zufallsZahlen():
    • Diese Funktion ist sinnvoll, da die Zufallszahlenbildung später einmal als lokaler Service und als Web Service bereitgestellt werden soll. Bei Letzterem wäre für sehr viele Zufallszahlen ein andauernder Aufruf, möglichst noch in einer engen Schleife, ungünstig.
    • Zeile 12: Bemerkenswert ist die Angabe des Array-Parameters ohne Größenangabe.
    • Zeile 13: Das zurückzugebende Array wird variabel angelegt und in Zeile 15 durch eine unbekannte Anzahl Elemente erweitert.
    • Zeile 15: Das Bilden der Zufallszahl erfolgt durch Aufruf der anderen Funktion der Library.
    • Zeile 17: Es wird ein Array-Objekt zurückgegeben.
  • Programm ZufallTest: Da alle essenziellen Aussagen zum Test sich am Aufruf der Funktion zufallsZahlen()festmachen lassen, verzichte ich auf den Test der Funktion zufallsZahl().
    • Abbildung 2, Zeile 7: Von der Array-Variablen wird nur der Typ deklariert, das Array wird weder angelegt noch initialisiert. Das reicht, denn von der aufgerufenen Funktion wird über die Funktionsrückgabe eine Referenz auf ein dort gebildetes Array geliefert.

Abbildung 1: Die Library Zufall

Abbildung 2: Test der Funktionen der Library Zufallmit dem Programm ZufallTest

Definition und Arbeitsweise von Anweisungen

Anweisungen bestehen aus Aktionen, die auf den Speicherstellen der Variablen oder im Environment irgendwelche Veränderungen durchführen oder solche Durchführungen organisieren bzw. steuern.

Es soll hier mehr um die durchzuführende Arbeit und weniger um die Rahmenbedingung dafür gehen. Somit konzentrieren wir uns jetzt auf

  • Anweisungen mit Befehlen,
  • parametergesteuerte Programm- und Funktionsaufrufe,
  • Zuweisungen von Werten zu Variablen,
  • Initialisierungen in Variablendefinitionen,
  • Aufruf von Programmen und Funktionen (behandelt im nächsten Artikel) sowie
  • Ablauflogik in Funktionen (behandelt im nächsten Artikel).

Bei den Anweisungstypen spielt die Zuweisung eine zentrale Rolle. Ein Teil der Anweisungen sind mit einem EGL-Befehl geschriebene Zuweisungen. In Ausdrücken für Argumente oder in Anweisungen zur Ablauflogik, aber auch bei der Initialisierung von Variablen kommen sie indirekt vor, meist aber in der nachfolgend beschriebenen direkten Form.

Zuweisungen und Ausdrücke

Der Grundtyp einer Zuweisung hat die Form

ziel = quelle;

Dabei ist einer Variablen ein Wert passenden Typs zugewiesen. Die Ziel-Variable kann auch von einem komplexen Typ sein, z. B. von einem Record- oder Array-Typ. Ist das Ziel eine Zeichenkette fester Länge, kann die Zuweisung auch auf einen Substring des Ziels erfolgen.

Dieser Quellwert kann eine Konstante, ein Literal, eine andere Variable oder ein Element eines Arrays sein oder aber wird durch den Aufruf einer Funktion mit Rückkehrtyp gebildet. Auch das Bilden eines Werts durch einen komplexen Ausdruck, der faktisch weitere Zuweisungen enthält, ist möglich (Abbildung 1, Zeilen 7 und 8). Nullwertfähigen Variablen kann auch ein Nullwert (null) zugewiesen werden.

So wie beim Übergang von RPG III zu RPG IV eine Folge von mehreren Rechenoperationen mit Hilfsvariablen für Zwischenergebnisse als komplexer Ausdruck einer eval-Operation bzw. Zuweisung im freien Format geschrieben werden kann, so wird in umgekehrter Richtung ein komplexer Ausdruck durch den Compiler wieder in eine Folge von Zuweisungen zu anonymen Hilfsvariablen bzw. zuletzt zum Ergebnis aufgedröselt, so dass in jeder dieser Zuweisungen immer nur eine Operation, ein Funktionsaufruf oder eine Typumwandlung (Cast) ausgeführt wird. Den Hilfsvariablen muss der Compiler einen passenden Typ zuweisen können. Kann er keinen passenden Typ finden, verleiht er seinem Unmut durch eine manchmal nicht sofort verständliche Fehlermeldung Ausdruck. Wählt er einen unpassenden Typ, kann dies – zur Laufzeit – zu heimtückischen oder zumindest merkwürdigen Effekten wie z. B. Genauigkeitsverlust oder "richtig falschen" Ergebnissen führen. Deshalb lohnt es sich, die Regeln bei der Bildung von Ausdrücken zu kennen, um sich ggf. sogar gegen einen komplexen Ausdruck entscheiden zu können. So habe ich im Beispiel von Abbildung 1, Zeilen 6 bis 8, anstatt alles in einen komplexen Ausdruck zu packen, die Anweisungen mit Hilfsvariablen einzeln geschrieben, um die Datentypen selbst bestimmen zu können.

Nicht nur in komplexen Ausdrücken stecken Zuweisungen, sondern auch in manchen Anweisungen, z. B. im return, insbesondere wenn dort noch ein komplexer Ausdruck verwendet wird wie im returnvon Abbildung 1, Zeile 9 oder bei der Übergabe der Argumente für Input-Parameter in Programm- oder Funktionsaufrufen. Bei der Argumentübergabe können Ausdrücke benutzt werden, deren Ergebnis dann einem anonymen Argument vom Typ des Parameters zugewiesen werden, wie in Abbildung 2, Zeile 9.

Neben dem oben beschriebenen Grundtyp der Zuweisung inklusive seiner "verkappten" Varianten gibt es noch weitere explizite Zuweisungen, und zwar mit den EGL-Anweisungen moveund set. Sie erlauben es, komplexere Zuweisungen mit Optionen zu steuern. Der Befehl moveist zuständig für die komfortable Übertragung von Record-Strukturen, wo per gleichem Namen, per gleicher Position oder, am performantesten, per Byte zugewiesen werden kann, und dies alles noch wiederholt in Verbindung mit Arrays.

Der Befehl set hebt das Setzen von Variablen auf definierte Zustände, wie den Initial- oder Nullwert, auf oder setzt den Cursor im Kontext von Satz-Ergebnismengen an den Anfang, das Ende, eine Position nach vorn oder nach hinten.

Typkompatibilität bei elementaren Zuweisungen

Grundsätzlich müssen die komplexen Datenstrukturen, wie Records und Arrays von Ziel und Quelle, aufeinander passen, wobei durch Vereinbarungen und Optionen im Groben gewisse Unterschiede überbrückt werden können. Schlimmstenfalls müssen eben die Strukturelemente einzeln übertragen werden. Dann müssen wie bei Zuweisungen von "einfachen" Variablen die elementaren Typen aufeinander passen.

Der elementare Typ einer "einfachen" Zielvariablen und der elementare Typ der Quelle müssen sinnvoll aufeinander passen:

  • Kaum nennenswerte Probleme gibt es, wenn Zeichenkettentypen wie string, unicode, char, mbchar, dbcharund m. E. hexeinander zugewiesen werden. Es ist höchstens zu beachten, dass, wenn das Ziel eine feste Länge hat, aufgefüllt oder abgeschnitten werden kann.
  • Bei Zahlentypen wie bin, smallInt, int, bigInt, decimal, num, number, float, smallFloat, money, numcund pacfuntereinander dürfen abweichende Vorkommastellen nicht zu einem Überlauf führen. Eigenartigerweise können decimal-Variablen mit geradzahliger Stelligkeit eine Stelle mehr aufnehmen, was man zwar durch die interne Darstellung erklären kann, aber nicht gut finden muss. Eine Überlauf-Exception tritt auf, wenn das Ziel die Ziffern gemäß Vor- und Nachkommastellen nicht aufnehmen kann. Ein eventueller Verlust von Nachkommastellen wird als normal angesehen, kann jedoch etwas abgemildert werden, indem durch die Build-Property truncateExtraDecimals=NOdas "kaufmännische" Runden indirekt angewiesen wird.
  • Ein Zahlentypziel und eine Zeichenkettenquelle sind nur in Verbindung mit Literalen möglich, wo der Compiler nur aus den allgemein gebräuchlichen Zahlendarstellungen wie 123, -123.4 oder 0.123E-3 die Zahl generiert. Solche Zuweisungen zur Ausführungszeit sind nur durch Parsen aus Textfeldern der Benutzeroberflächen mit Hilfe von Editiermasken möglich.
  • Ein Datumsdatenziel mit Zeichenkettenquelle wird ähnlich wie bei den Zahlentypen gehandhabt, nur dass hier beliebige Literale angegeben werden können, wenn bei der Definition eine entsprechende interne Maske angegeben wurde.
  • Ein Zeichenkettenziel zusammen mit numerischer Quelle funktioniert bei allen Zielen außer String ganz pragmatisch, indem die Zahl in die allgemein übliche Zeichenkettendarstellung konvertiert wird. Beim Zieltyp stringmuss die Funktion StrLib.formatNumber()benutzt werden.
  • Bei einem Ziel von einem Datumstyp und einer numerischen Quelle kann zur Programmierzeit wie bei einer Zeichenkettenquelle konvertiert werden, allerdings dürfen die internen Masken nur Zeichen haben, die von Zahlen bedient werden können.
  • Ein Ziel vom Typ booleankann zur Programmierzeit durch die beiden Schlüsselwortliterale trueund falseoder auch durch die Zahlen 0 und 1 belegt werden, zur Ausführungszeit nur durch ein booleanoder eine Zahl, wobei alle Zahlen außer 0 in truekonvertiert werden. Weiterhin kann der Inhalt aus Elementen von Benutzeroberflächen, z. B. einer Checkbox ermittelt werden, was nur am Rande erwähnt werden soll.
  • Eine boolean-Quelle kann nur in die Zeichenkettenvariablen "true" und "false" bzw. in Elemente von Benutzeroberflächen konvertiert werden.

Typen und Operationen

Es stehen folgende Operatoren zur Verfügung:

  • Numerische Operatoren +, -, *, /, % (Divisionsrest, ** (Potenzierung)
  • Text-Operatoren +, ::, ?:, Substring-Bildung
  • einstellige Operatoren: + und – (Vorzeichen) sowie as (Cast)
  • Funktionen, insbesondere zur Migration von Typen
  • Vergleichsoperatoren ==, !=, <, <=, >, >=, isa, is, not, like, in, matches
  • logische Operatoren &, &&, |, ||, !, and, or

Bei mehreren formal auf der gleichen Klammerebene vorkommenden Operatoren gelten die von anderen Programmiersprachen her bekannten Vorrangregeln (siehe EGL-Sprachreferenz), nach denen intern sinngemäß noch einmal zusätzlich geklammert wird, so dass der komplexe Ausdruck in Einzeloperationen zerlegt wird. Die Reihenfolge der Einzeloperationen ist im Allgemeinen entscheidend für das Ergebnis. So sollte eine maximale explizite Klammerung das gewünschte Ergebnis sichern, andererseits können an unkritischen Stellen weggelassene Klammern die Übersichtlichkeit eines Ausdrucks verbessern. Manchmal muss auch geklammert werden, weil die Vorrangregeln nicht passen. Irgendwo liegt da ein "individuelles" und von Fall zu Fall unterschiedliches Optimum, dem sich der Programmierer mit zunehmender Erfahrung immer weiter nähert.

Die Elementaroperationen sind im Allgemeinen überladen, d. h. verschiedene (allerdings sehr ähnliche) Operatoren werden durch das gleiche Zeichen dargestellt. So ist "/" nicht gleich "/", die Division von intarbeitet binär mit ca. neun Ziffern Genauigkeit und ohne Nachkommastellen und die von decimal(30,10)arbeitet dezimal mit 20 Vorkommastellen und zehn bzw. elf (je nach gewünschtem Runden) Nachkommastellen.

Welches "+" genommen wird – ganzzahlig, mit Gleitkommagenauigkeit oder als Verkettung von Texten – und in welchen Typen die Operation arbeitet, entscheiden der Kontext der Operation, bestehend aus dem Zieltyp, evtl. Typmigrationen durch asoder Funktionsaufrufe, sowie die Typen der Operanden

Grundsätzlich wird durch das Ergebnis oder bei Typmigration durch ein Teilergebnis bestimmt, welcher Grundtyp von Operation genommen werden soll, d. h. ob Zahl, Text, Datum/Zeit oder boolean. Innerhalb der Grundtypen erfolgt dann in Abhängigkeit von den Operanden die Auswahl des eigentlichen Operators im Sinne des Überladens, d.h. des passenden "/", "+" etc. Mit diesem Operator wird dann nach seinen Gesetzen die Operation durchgeführt.

Wird aufgrund eines numerischen Ziels und numerischer Operanden eine numerische Operation ausgeführt, muss der genaue Typ des Operators aus dem Typ des Ausdrucks ermittelt werden, der sich seinerseits aus dem Typ des  "ranghöchsten" Operanden der folgenden Rangfolge ermittelt:

  • number
  • float
  • smallFloat
  • num
  • numc
  • decimal
  • bin
  • bigInt
  • int
  • smallInt

Außerdem gelten objektiv begründbare Einschränkungen wie die, dass beim Potenzieren der erste Operand nicht negativ sein darf oder dass er beim Divisionsrest nicht floatund nicht smallFloatsein darf.

Der Wert des Ausdrucks ist dann kompatibel zum Typ des Ergebnisses oder der  anonymen Hilfsvariablen, zu der dieser zugewiesen wird, worauf im Falle einer Hilfsvariablen anschließend eine "logische" Klammerebene höher weiter-"operiert" wird.

Die Operatoren für Zeichenkettentypen "+", "::" und ?:" weisen geringe Unterschiede auf. Der "+"-Operator arbeitet nur dann als Verkettung, wenn der erste Operand von einem Zeichenkettentyp ist. Der Operator "::" konvertiert einen ersten Operanden, der nicht von einem Zeichenkettentyp ist in einen solchen und verkettet ihn mit dem zweiten Operanden, der ggf. auch noch konvertiert werden muss. Ist genau einer der beiden Operanden ein Nullwert, wird eine Leer- bzw. Leerzeichenkette angehängt. Bei zwei Nullwerten wird das Ergebnis auch zum Nullwert. Außerdem kann dieser Operator noch zum Anhängen von Elementen an ein Array benutzt werden. Der Operator "?:" arbeitet wie "::", nur dass das Ergebnis nullist, wenn einer der beiden Operanden nullist.

Der Cast-Operator as wird vor allem in Verbindung mit Java-Objekten gebildet, die den EGL-Typ anyentsprechend dem Java Typ Objecthaben. Aber es kann auch, wie oben schon erwähnt, ein Teilausdruck mit dem Cast-Typ gebildet werden, der dann die Zuweisungstypen in diesem Teilausdruck bestimmt. Ähnliches kann man mit EGL oder eigenen Funktionen erreichen, wo die Definitionstypen der Parameter den Ergebnistyp für Ausdrücke in den jeweiligen Argumenten bilden. Funktionen werden oft – wie insbesondere auch bei RPG IV oder Java – zur Migration eines für den Operanden nicht tauglichen in einen tauglichen Typ verwendet.

Vergleiche und die durch logische Ausdrücke gebildeten Bedingungen sowie die mit diesen eng zusammenhängenden Bedingungen zur Steuerung der Ablauflogik sind Gegenstand des nächsten Artikels.

Übung zum Artikel

Manche kommerzielle Software, z. B. das ERP System Movex, speichert Datumsdaten als decimal(8,0)in der Form yyyyMMddab. Es sollen jetzt von Kundenaufträgen die Zeitdifferenzen zwischen Zahlungsziel und Zahlungseingang statistisch untersucht werden, indem für bestimmte Gruppen von Aufträgen der Mittelwert und die Standardabweichung berechnet werden.

Das Problem soll in drei Teile aufgegliedert werden. Der erste Teil liest die Gruppe von Aufträgen, rechnet die Zeitdifferenzen aus und stellt sie in ein int-Array. Dieser Teil soll in der Aufgabe auf die Weise simuliert werden, dass die Datumsdaten in zwei mit Testdaten initialisierten "synchronen" Arrays vom Typ decimal(8,0)mit jeweils z. B. fünf Elementen bereitgestellt und aus diesen die Zeitdifferenzen berechnet werden, die anschließend in den int-Array mit gleicher Elementeanzahl gestellt werden.

Der zweite Teil berechnet die statistischen Werte. Die Berechnungen sollen in Funktionen einer Library für allgemein wiederverwendbare Funktionen stattfinden. Der dritte Teil ruft die beiden ersten Teile auf und gibt das Ergebnis auf die Konsole aus.

  1. Erstellen Sie im Paket mathdes Projekts EGLAllgemeindie Library Verteilung!
  2. Erstellen Sie in math.Verteilungdie Funktion mittelWert(), die aus einem übergebenen Array von float-Zahlen den Mittelwert nach der Formel
    m = (x1 + x2 +   + xn)/n
    als float-Zahl zurückgibt.
  3. Erstellen Sie in math.Verteilungdie Funktion standardAbweichung(), die aus einem übergebenen Array von float-Zahlen die Standardabweichung nach der Formel
    s = ((x1-m)2 + (x2-m)2 + … + (xn-m)2)/n)1/2
    als float-Zahl zurückgibt.
  4. Testen Sie beide Funktionen mit einem geeigneten Testprogramm TestVerteilungim gleichen Paket!
  5. Nehmen Sie das Projekt EGLAllgemeinin den EGL-Erstellungspfad des Projekts EGLKundenauf!
    • Kontextmenü von EGLKunden –>Eigenschaften
  6. Nehmen Sie das Projekt EGLAllgemeinanalog auch in den Java-Erstellungspfad auf!
  7. Erstellen Sie im Paket eglKunden.logikdes Projekts EGLKundendie Library Fristenmit der Funktion zahlungsDauer()! Diese
    • empfängt einen Input-Parameter gruppevom Typ string, mit dem (noch) nichts gemacht wird;
    • definiert die Variable zahlungsZielals Array mit z. B. fünf Elementen vom Typ decimal(8,0)und initialisiert ihn mit sinnvollen Dezimal-Datumswerten der Form yyyyMMdd;
    • definiert eine "gleiche" Variable zahlungmit zu zahlungsZielpassenden Datumswerten;
    • berechnet die Zeitintervalle der Differenzen (zahlung[i] – zahlungsZiel[i])und schreibt sie in ein genauso langes int-Array;
    • gibt das int-Array als Funktionswert zurück.
  8. Erstellen Sie im Paket eglKundendes Projekts EGLKundendas Programm ZahlungsAuswertung, das die Funktion zahlungsDauer() aufruft, das int-Array empfängt sowie anschließend die Funktionen mittelWert()und standardAbweichung()aufruft und deren Rückgabewerte auf Konsole ausgibt.

Ausblick auf den nächsten Artikel

Im nächsten Artikel werden Vergleichsoperatoren, die mit ihnen verbundenen Regeln und ihre Verwendung in logischen Ausdrücken behandelt, ergänzend dazu die Anweisungen zur Ablaufsteuerung in Funktionen und Anwendungen.