Découverte du langage COBOL avec GnuCOBOL.
Adaptation de Praktische Softwareentwicklung mit COBOL de Frank Rehfeldt et Leon Westhof.
Autres sources :
* ex01.cob
IDENTIFICATION DIVISION.
PROGRAM-ID. Exemple. *> Nom du programme
PROCEDURE DIVISION.
DISPLAY "Salut la compagnie !".
STOP RUN.Le programme peut être compilé et exécuté au moyen de la commande suivante :
cobc -x -j ex01.cob L’option -x sert à générer un exécutable, et l’option
-j à l’exécuter automatiquement.
$ cobc -x -j ex01.cob
Salut la compagnie !
La colonne sur laquelle se trouve une commande ou un caractère a son importance.
Une ligne de code COBOL classique compte 80 caractères.
*>
------*A---B---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+-------
* Eine Kommentarzeile *>
IDENTIFICATION DIVISION. *>
PROGRAM-ID. halloWelt. DOKU
PROCEDURE DIVISION.
DISPLAY "Hallo Welt!". *>
STOP RUN.Die Spalten 1 bis 6 sowie 73 bis 80 werden vom Compiler ignoriert und können zu Dokumentationszwecken genutzt werden.
Eine Kommentarzeile wird durch einen * in Spalte 7, der Indikatorspalte, erkannt.
Eine DIVISION beginnt in Area A, dem Bereich von Spalte 8 bis Spalte 11. Ebenso SECTION Namen und PARAGRAPH Namen.
Normale Befehle stehen in Area B von Spalte 12 bis 71.
Der GnuCOBOL-Compiler unterstützt zusätzlich noch so einen modernen Modus, den Free-Format Mode. Dabei sind die Einrückungen dann egal und man kann Hallo Welt! noch prägnanter schreiben:
program-id.hello.procedure division.display “Hallo Welt!”.
Wem das noch nicht kurz genug ist, nimmt die definitiv kürzeste Variante:
display”Hallo Welt!“.
Das soll ein gültiges COBOL-Programm sein? Schauen wir uns an, was cobc macht, wenn wir den Schalter frelax-syntax setzen:
$ cobc -x -frelax-syntax -free hallo.cob hallo.cob: 1: Warning: PROGRAM-ID header missing - assumed hallo.cob: 1: Warning: PROCEDURE DIVISION header missing - assumed
Da praktisch jede Legacy-COBOL Anwendung im fixed format geschrieben ist, werden wir das free format sofort wieder vergessen.
2.3. Klarheit der Sprache
COBOL steht für Common Business Oriented Language und hat in diesem Sinne den Anspruch, möglichst lesbar zu sein.
Die Lesbarkeit und Verständlichkeit ist COBOLs große Stärke. Es gibt wenige Programmiersprachen, in denen die tatsächliche Geschäftslogik derart sprechend ablesbar ist.
In einer Übungsaufgabe haben wir einen Mehrwertsteuer-Rechner implementiert. Zur Erinnerung hier nochmal der Code:
IDENTIFICATION DIVISION.
PROGRAM-ID. MWST.
ENVIRONMENT DIVISION.
DATA DIVISION.
WORKING-STORAGE SECTION.
01 NETTO-BETRAG PIC 999V99.
01 BRUTTO-BETRAG PIC 999.99.
01 MWST PIC V999 VALUE .19.
PROCEDURE DIVISION.
PERFORM CALCULATE-MWST
GOBACK.
CALCULATE-MWST SECTION.
COMPUTE BRUTTO-BETRAG ROUNDED
= NETTO-BETRAG * (1 + MWST)
END-COMPUTE
EXIT.
END PROGRAM MWST.Nachdem wir jetzt ein einfaches COBOL-Programm gesehen haben, wollen wir uns mal die Details genauer anschauen. Nehmen wir den Programmrumpf aus der Mehrwertsteuer-Übung:
IDENTIFICATION DIVISION. *>
PROGRAM-ID. MWST.
ENVIRONMENT DIVISION. *>
DATA DIVISION. *>
WORKING-STORAGE SECTION.
01 NETTO-BETRAG PIC 999V99.
01 BRUTTO-BETRAG PIC 999.99.
01 MWST PIC V999 VALUE .19.
PROCEDURE DIVISION. *>
PERFORM CALCULATE-MWST
GOBACK.
CALCULATE-MWST SECTION.
* Noch nicht implementiert
.
EXIT.
END PROGRAM MWST.Ein COBOL-Programm besteht aus genau 4 DIVISIONs.[2]
Schauen wir uns nun jede DIVISION mal im Schnelldurchlauf an.
Die IDENTIFICATION DIVISION enthält nützliche Informationen über das Programm, z.Bsp. für den Compiler oder für die Entwickler.
Verpflichtend ist der Eintrag
PROGRAMM-ID. program-name..
Für ein Programm wie unseren Mehrwertsteuer-Rechner ist der Programm-Name unwichtig, aber für Unterprogramme gibt der Programm-Name an, wie dieses Programm per CALL-Befehl aufrufbar ist.
Weitere Attribute in der IDENTIFICATION DIVISION sind der Author-Name und das Datum, zu dem das Programm geschrieben wurde.
IDENTIFICATION DIVISION.
PROGRAM-ID. meinErstesProgramm.
AUTHOR. Bob der Dinosaurier.
DATE-WRITTEN. 20th June 2018.Die Idee der ENVIRONMENT DIVISION ist, dass es genau eine Stelle im Programm gibt, an der die Konfiguration an die Laufzeit-Umgebung geschieht.
Teil der Laufzeit-Umgebung sind z.Bsp. die Pfade von Dateien, die das Programm einlesen oder ausgeben soll. Das kann man dann in der INPUT-OUTPUT SECTION angeben:
ENVIRONMENT DIVISION.
CONFIGURATION SECTION.
INPUT-OUTPUT SECTION.
FILE-CONTROL.
SELECT Students ASSIGN TO "C:\Daten\Students.txt"
ORGANIZATION IS SEQUENTIAL.In der DATA DIVISION werden alle Datenfelder[3] definiert.
Schauen wir uns einige Beispiele an. Soll das Programm den Namen des Benutzers einlesen, braucht man ein Datenfeld um den Namen zu speichern. In COBOL gibt es keinen Datentyp String für beliebig lange Zeichenketten, sondern man muss die Länge vorher fest definieren:
01 BENUTZER-NAME PIC A(30) VALUE SPACES.In diesem Beispiel ist das Datenfeld BENUTZER-NAME 30 Zeichen lang und darf nur Buchstaben enthalten, da das A für Alphabet steht.
Würden wir im BENUTZER-NAMEN auch Zahlen erlauben wollen, müssten wir die Datenfelddefinition minimal anpassen: Wir ersetzen das A durch ein X, welches für alphanumerischen Inhalt steht:
01 BENUTZER-NAME PIC X(30) VALUE SPACES.Würde man das Alter des Benutzers einlesen wollen, bräuchte man ein Datenfeld, in dem nur Zahlen erlaubt sind. Das könnte man so erreichen:
01 BENUTZER-ALTER PIC 9(02) VALUE SPACES.Hier wäre das Datenfeld für das BENUTZER-ALTER nun 2-stellig numerisch.
Folgende PICTURE-Bedingungen sind äquivalent:
PIC 99
PIC 9(2)
PIC 9(02)
Diese Datendefinition für BENUTZER-ALTER wäre äußerst ungünstig. Würde ein 100-jähriger unsere Software benutzen, hätten wir ein Problem.
Was wäre die Ausgabe dieses Programms?
IDENTIFICATION DIVISION.
PROGRAM-ID. COMMAND.
ENVIRONMENT DIVISION.
DATA DIVISION.
WORKING-STORAGE SECTION.
01 BENUTZER-ALTER PIC 99.
PROCEDURE DIVISION.
DISPLAY "Hallo. Bitte Alter eingeben:"
ACCEPT BENUTZER-ALTER
DISPLAY "Sie sind " BENUTZER-ALTER
GOBACK.
END PROGRAM COMMAND.Gibt ein Benutzer hier 100 ein, so würde das Programm antworten:
Sie sind 10.
Nicht so gut. Wir werden aber später noch Möglichkeiten kennenlernen, auf diese Situationen angemessen zu reagieren.
COBOL bietet eine ganze Reihe Möglichkeiten, seine Datenfelder möglichst genau zu spezifizieren. Im ersten Schritt ist es aber ausreichend, wenn wir uns die eben genannten Beispiele merken und noch ein bißchen Verständnis der WORKING-STORAGE SECTION haben.
Eine weitere wichtige Möglichkeit, Variablen in COBOL zu definieren, sind Gruppenstrukturen. Wenn wir zu einem Benutzer seinen Vornamen, Nachnamen und das Alter speichern wollen, können wir uns eine Struktur BENUTZER wie folgt anlegen:
01 BENUTZER. *>
05 VORNAME PIC X(30).
05 NACHNAME PIC X(30).
05 B-ALTER PIC 999. *> Hier wird die Struktur definiert. Beachte, dass hier keine PICTURE-Bedingung steht.
Da ALTER ein Befehl in COBOL ist, ist die Bezeichnung reserviert und wir können keine Variable ALTER nennen.
Angenommen, wir haben noch einen Admin und müssen auch seinen Vornamen, Nachnamen und sein Alter merken. Wir würden uns also eine ähnliche Struktur anlegen:
01 ADMIN.
05 VORNAME PIC X(30).
05 NACHNAME PIC X(30).
05 A-ALTER PIC 999. *>
```cobol
Das Feld heißt nun nicht B-ALTER, sondern A-ALTER.
Gut. Nun wollen wir dem Admin den Vornamen BOFH [4] zuweisen. Dazu nutzen wir den MOVE-Befehl:
```cobol
IDENTIFICATION DIVISION.
PROGRAM-ID. DATADEFS.
ENVIRONMENT DIVISION.
DATA DIVISION.
WORKING-STORAGE SECTION.
01 BENUTZER.
05 VORNAME PIC X(30).
05 NACHNAME PIC X(30).
05 B-ALTER PIC 999.
01 ADMIN.
05 VORNAME PIC X(30).
05 NACHNAME PIC X(30).
05 A-ALTER PIC 999.
PROCEDURE DIVISION.
MOVE "BOFH" *>
TO VORNAME
DISPLAY VORNAME
GOBACK.
END PROGRAM DATADEFS.Der Variable VORNAME wird der Wert BOFH zugewiesen.
Funktioniert das so?
cobc -x DATADEFS.cbl DATADEFS.cbl:18: error: ‘VORNAME’ is ambiguous; needs qualification DATADEFS.cbl:8: error: ‘VORNAME IN BENUTZER’ defined here DATADEFS.cbl:12: error: ‘VORNAME IN ADMIN’ defined here
Der Compiler beschwert sich: er weiß nicht, welcher VORNAME gemeint ist: Der VORNAME in der Struktur BENUTZER oder in der Struktur ADMIN?
Wir müssen ihm auf die Sprünge helfen und unsere Variable qualifizieren:
MOVE “BOFH” TO VORNAME IN ADMIN *>
Das IN ADMIN sagt dem Compiler eindeutig, welcher VORNAME gemeint ist.
Mit Hilfe dieser abgestuften Definitionen lassen sich strukturierte Daten in COBOL wunderbar abbilden. Dafür werden wir in den späteren Kapiteln noch diverse Beispiele sehen.
Um mehrdimensionale Strukturen abbilden zu können, also beispielsweise Spielfelder oder - ganz allgemein - Matrizen, gibt es Arrays.
Wollen wir ein 3x3 Spielfeld für TicTacToe modellieren, können wir Folgendes schreiben:
01 SPIELFELD. 05 Y-ACHSE OCCURS 3. 07 ZELLE PIC X OCCURS 3.
Auf die Zellen des Spielfelds können wir jetzt zugreifen:
MOVE “X” TO ZELLE(2,1)
Dieser Befehl setzt ein X in die erste Spalte der zweiten Zeile.
Interessant wird es, wenn wir das ganze Spielfeld auf einmal mit Werten befüllen wollen. Falls wir die Zellen des Spielfelds der Reihe nach durchnummerieren wollen, reicht dafür dieser Befehl:
MOVE “123456789” TO SPIELFELD
Warum funktioniert das? Wie im Abschnitt über Gruppenstrukturen ist die Erklärung der Compiler: Für den Compiler ist das Feld Spielfeld ein ganz normales Datenfeld, das zwar nochmals in einzelne Zellen unterteilt ist, aber trotzdem wie ein zusammenhängender Speicherbereich behandelt werden kann. Der Wert 123456789 wird also in den Speicher geschrieben und erst beim Zugriff auf dieses Feld über die Schablonen Y-ACHSE oder ZELLE interpretiert.
Mit diesem Wissen können wir das Spielfeld auch sehr leicht auf einen komplett leeren Zustand zurücksetzen:
MOVE SPACES TO SPIELFELD
Die WORKING-STORAGE SECTION ist ein zusammenhängender Speicherbereich, in dem alle Variablen des Programms gespeichert werden. Die Datenfeld-Definitionen haben keinen Einfluss auf den tatsächlichen Aufbau des reservierten Speicherbereichs.
Die Datenfelddefinitionen in der WORKING-STORAGE SECTION dienen als Schablone, die das Programm auf den Speicherbereich legt, um einfacher damit arbeiten zu können.
Schauen wir uns ein kurzes Beispiel an, um das Schablonen-Prinzip zu verdeutlichen. Angenommen, wir haben ein Programm mit folgender WORKING-STORAGE SECTION:
WORKING-STORAGE SECTION. 01 BENUTZER-ALTER PIC 99. 01 BENUTZER-NAME PIC X(30).
Der Compiler berechnet nun das OFFSET aller Datenfelder, also den relativen Startpunkt des Datenfeldes im Speicher:
BENUTZER-ALTER: +0
BENUTZER-NAME: +2
Das Feld BENUTZER-ALTER beginnt direkt am Anfang des Speicherbereichs.
Der Compiler berechnet die Länge aller vorher definierten Felder und kennt somit den Startpunkt von BENUTZER-NAME. Die Länge von BENUTZER-ALTER ist 2, also belegt BENUTZER-ALTER die ersten beiden Bytes und BENUTZER-NAME beginnt direkt im Anschluss bei Byte 3.
Insgesamt wäre die WORKING-STORAGE SECTION dieses Programms 32 Bytes groß.
Das soll an dieser Stelle für einen groben Überblick reichen. Wir werden später ausführlich die Datendefinitionen besprechen. manager summary
Die PROCEDURE DIVISION enthält die Anweisungen, um die in der DATA DIVISION definierten Daten zu manipulieren.
Sie ist hierarchisch aufgebaut und besteht aus Paragraphen, Sections und Statements. Paragraphen und Sections dienen zur Strukturierung des Programms und können mit PERFORM oder GO TO aufgerufen werden.
Da das Zitieren von Fachartikeln aus den 1970iger Jahren einen Text seriös wirken lässt[5], soll hier angemerkt werden, dass Sprungbefehle wie GO TO spätestens seit Edgar Dijkstra’s Artikel Go To Statement Considered Harmful verpöhnt sind. Sie führen zu undurchschaubarem Spaghetti-Code und sind somit nicht mehr verständlich und fehleranfällig. [ed]
In der PROCEDURE DIVISION codiert man die Logik des Programms. Schauen wir uns nun der Reihe nach die Bausteine an, die wir brauchen, um unsere Geschäftslogik zu implementieren. 3.4.1. Datenmanipulationen
Angenommen, wir haben eine Variable TEMP-NAME die folgendermaßen definiert ist:
01 TEMP-NAME PIC X(30).
Wie können wir dieser Variable einen Wert zuweisen? Dafür gibt es den MOVE-Befehl: 3.4.1.1. Elementarer MOVE
MOVE “Bob” TO TEMP-NAME
Für strukturierte Felder funktioniert das ebenso. Nehmen wir das Beispiel aus dem Abschnitt über strukturierte Daten:
01 BENUTZER. 05 VORNAME PIC X(30). 05 NACHNAME PIC X(30). 05 B-ALTER PIC 999. 01 ADMIN. 05 VORNAME PIC X(30). 05 NACHNAME PIC X(30). 05 A-ALTER PIC 999.
Wenn wir jetzt einen BENUTZER mit den Werten Thomas|Müller|29 haben und möchten ihn zum ADMIN machen, können wir das mit einem einzigen MOVE erreichen:
MOVE BENUTZER TO ADMIN
Dieser Gruppen-Move funktioniert nur, wenn BENUTZER und ADMIN auch tatsächlich die gleichen Felder mit der gleichen Länge enthalten.
Wären bei ADMIN z.Bsp. zwei Felder vertauscht:
01 ADMIN. 05 VORNAME PIC X(30). 05 A-ALTER PIC 999. *> 05 NACHNAME PIC X(30).
Das Alter kommt jetzt vor dem Nachnamen
Dann kommt nicht das gewünschte Ergebnis raus. Der Compiler berechnet die Länge, die BENUTZER im Speicher belegt, nimmt den gesamten Inhalt und schiebt ihn in die Gruppe ADMIN. Insbesondere interessieren ihn die Feldnamen überhaupt nicht!
Möchte man einen Gruppenmove, wo nur Felder mit dem gleichen Namen aufeinander abgebildet werden, kann man den Befehl MOVE CORRESPONDING verwenden.
Der Befehl MOVE CORRESPONDING ruft bei etlichen Entwicklern eine bestimmte Reaktion hervor:
Was könnte der Grund dafür sein?
Schauen wir uns im Detail an, wie der MOVE Befehl funktioniert.
Angenommen, wir haben ein numerisches Feld:
01 TEMP-FELD PIC 9.
und wir versuchen, diesem Feld einen Buchstaben zuzuweisen:
MOVE “A” TO TEMP-FELD
Was passiert? Der GnuCOBOL-Compiler beanstandet das nicht, aber was passiert zur Laufzeit? Probieren wir es aus:
IDENTIFICATION DIVISION.
PROGRAM-ID. DATADEFS.
ENVIRONMENT DIVISION.
DATA DIVISION.
WORKING-STORAGE SECTION.
01 TEMP-FELD PIC 9.
PROCEDURE DIVISION.
MOVE "A"
TO TEMP-FELD
DISPLAY TEMP-FELD
GOBACK.
END PROGRAM DATADEFS.Wandeln wir das Programm nun um und führen es aus:
cobc -x DATADEFS.cbl ./DATADEFS A
Wir sehen also: nichts passiert! Am Ende steht in TEMP-FELD ein ungültiger Wert.
Das ist ungünstig, aber GnuCOBOL bietet eine Compiler-Option, die einen auf solche fragwürdigen MOVE-Befehle hinweist:
cobc -x –Wall DATADEFS.cbl DATADEFS.cbl:11: warning: numeric value is expected DATADEFS.cbl:7: warning: ‘TEMP-FELD’ defined here as PIC 9
Es empfiehlt sich generell mit der Compiler-Option –Wall zu arbeiten. Diese Option aktiviert alle in GnuCOBOL verfügbaren Warnungen.
Alles, was der Compiler für uns sicherstellt, müssen wir nicht mehr testen!
Auch an Stellen, wo der Compiler uns nicht weiterhelfen kann, z.Bsp. bei externen Datenquellen, sind wir nicht auf verlorenem Posten. Wir können mit IS NUMERIC überprüfen, ob in TEMP-FELD tatsächlich ein gültiger numerischer Wert steht:
IDENTIFICATION DIVISION.
PROGRAM-ID. DATADEFS.
ENVIRONMENT DIVISION.
DATA DIVISION.
WORKING-STORAGE SECTION.
01 TEMP-FELD PIC 9.
PROCEDURE DIVISION.
MOVE "A"
TO TEMP-FELD
IF TEMP-FELD IS NUMERIC THEN
DISPLAY "Numerisch"
ELSE
DISPLAY "Nicht numerisch"
END-IF
GOBACK.
END PROGRAM DATADEFS.Und dieses Programm liefert - wenig überraschend - die Ausgabe
Nicht numerisch
Schauen wir uns noch 2 weitere Möglichkeiten an, Variablen Werte zuzuweisen:
Mit dem INITIALIZE-Befehl können Variablen zurückgesetzt werden, d.h. im Wesentlichen im werden numerischen Felder der Wert ZERO zugewiesen, und alphanumerischen Felder der Wert SPACE.
Das ist insbesondere für Gruppenstrukturen nützlich. Möchte man einen neuen BENUTZER anlegen, so sollte man alle Felder der Struktur zurücksetzen, damit keine Daten des alten BENUTZER noch irgendwo stehen bleiben.
Eine Möglichkeit wäre:
MOVE SPACES TO VORNAME IN BENUTZER NACHNAME IN BENUTZER
MOVE ZERO TO B-ALTER IN BENUTZER
Da das insbesondere für größere Strukturen sehr umständlich wäre, kann man das kürzer schreiben:
INITIALIZE BENUTZER
Variablen in der WORKING-STORAGE SECTION können mit einem Wert vorbelegt werden:
01 PLZ-MUENSTER-X4TEL PIC X(5) VALUE “48147”.
Das Feld PLZ-MUENSTER-X4TEL ist nun mit dem Wert 48147 vorbelegt.
Welchen Inhalt hat PLZ-MUENSTER-X4TEL nach INITIALIZE PLZ-MUENSTER-X4TEL
Es gibt eine weitere Möglichkeit um Variablen Werte zuzuweisen, welche insbesondere im Abschnitt über Bedingungen nützlich ist.
Nehmen wir an, wir möchten zu einem BENUTZER auch noch den STATUS speichern. Der STATUS kann dabei die Werte
NORMAL
PREMIUM
GESPERRT
annehmen. Diesen beschränkten Wertebereich können wir nun z.Bsp. so modellieren:
01 BENUTZER. … 05 BSTATUS PIC X VALUE SPACE. > 88 NORMAL VALUE SPACE. > 88 PREMIUM VALUE ‘P’. 88 GESPERRT VALUE ‘G’.
Da STATUS ein reserviertes Wort ist, nennen wir die Gruppe BSTATUS.
Der Wert SPACE für NORMAL entspricht der Standardbelegung von BSTATUS. NORMAL ist also der Standardwert, welcher bei einer frischen BENUTZER-Gruppe aktiviert ist.
Um nun einem BENUTZER den Status PREMIUM zu vergeben, können wir sowohl
MOVE ‘P’ TO BSTATUS
als auch
SET PREMIUM TO TRUE
schreiben. Dabei ist die letztere Variante natürlich deutlich lesbarer, denn nicht jeder Leser des Codes weiß sofort, dass P für PREMIUM steht.
Die 88-iger Stufen sind zusammen mit dem SET-Befehl ein gutes Mittel gegen magische Zahlen [6].
Natürlich gibt es auch in COBOL die aus anderen Sprachen bekannten IF-Abfragen.
Nehmen wir unseren BENUTZER aus dem vorherigen Kapitel:
01 BENUTZER. 05 VORNAME PIC X(30). 05 B-ALTER PIC 999. 05 BSTATUS PIC X VALUE SPACE. 88 NORMAL VALUE SPACE. 88 PREMIUM VALUE ‘P’. 88 GESPERRT VALUE ‘G’.
Dann können wir in unserem Programm z.Bsp. diese Bedingungen formulieren:
IF VORNAME = “Michael” THEN DISPLAY “Hallo Michael!” END-IF
Oder auch mit einem ELSE-Zweig:
IF VORNAME = “Michael” THEN DISPLAY “Hallo Michael!” ELSE DISPLAY “Hallo, jemand anderes als Michael!” END-IF
In COBOL gibt es kein Zeichen für ungleich, sondern man muss das Schlüsselwort NOT benutzen:
IF VORNAME NOT = “Michael” THEN DISPLAY “Hallo, jemand anderes als Michael!” ELSE DISPLAY “Hallo Michael!” END-IF
Schauen wir uns an, wie nützlich die 88-iger Stufen sind. Wollen wir überprüfen, ob ein BENUTZER den Status GESPERRT hat, so schreiben wir:
IF GESPERRT THEN DISPLAY “Benutzer ist gesperrt” END-IF
Oder analog:
IF NOT GESPERRT THEN DISPLAY “Benutzer ist nicht gesperrt” END-IF
Man kann Bedingungen auch mittels OR verknüpfen:
IF NORMAL OR PREMIUM THEN DISPLAY “Hallo aktiver Benutzer!” END-IF
Oder mittels AND:
IF PREMIUM AND B-ALTER >= 16 THEN DISPLAY “Du darfst ein Bier bestellen.” END-IF
Da sich COBOL als Ziel gesetzt hat, möglichst in flüssigem Englisch programmieren zu können, ist auch sowas erlaubt:
IF PREMIUM AND B-ALTER GREATER THAN OR EQUAL TO 16 THEN DISPLAY “Du darfst ein Bier bestellen.” END-IF
Wenn wir z.Bsp. das Alter eines BENUTZER von der Kommandozeile einlesen wollen, sollten wir auch überprüfen, ob tatsächlich ein gültiger Wert eingegeben wurde.
Um zu prüfen, ob in einer Variable ein numerischer Wert steht, können wir den Klassentest NUMERIC benutzen:
IF TEMP-FELD IS NUMERIC THEN DISPLAY “Das Feld enthält nur Ziffern.” END-IF
Ebenso können wir prüfen, ob der Name eines BENUTZER nur Buchstaben besteht:
IF VORNAME IS ALPHABETIC THEN DISPLAY “Das Feld enthält nur Buchstaben.” END-IF
Der GnuCOBOL-Compiler unterstützt folgende Klassentests:
NUMERIC
ALPHABETIC
ALPHABETIC-LOWER
ALPHABETIC-UPPER
3.4.2.4. EVALUATE
Nehmen wir an, wir wollen je nach Status des BENUTZER eine bestimmte Aktion ausführen. Dann könnte man das mittels IF-Abfragen so lösen:
IF PREMIUM THEN DISPLAY “Nutzer ist Premium-Kunde.” ELSE IF GESPERRT THEN DISPLAY “Nutzer ist gesperrt.” ELSE IF NORMAL THEN DISPLAY “Nutzer ist normal.” ELSE DISPLAY “Ungültiger Status.” END-IF END-IF END-IF
Dieser Code ist sowohl unverständlich, als auch schlecht erweiterbar, falls es eine neue Status - Ausprägung gibt.
Wesentlich schöner geht es mit dem EVALUATE-Befehl:
EVALUATE TRUE WHEN NORMAL DISPLAY “User ist normal.” WHEN PREMIUM
DISPLAY “Nutzer ist Premium-Kunde.” WHEN GESPERRT DISPLAY “Nutzer ist
gesperrt.” WHEN OTHER
DISPLAY “Status ist ungültig.” END-EVALUATE
Der WHEN OTHER-Fall wird immer dann durchlaufen, wenn kein vorheriges Kriterium zutraf.
Noch nützlicher wird das EVALUATE-Statement, wenn man es für den Vergleich mit mehreren Variablen benutzt.
Wir wollen herausfinden, ob ein BENUTZER ein Bier bestellen darf. Das soll er genau dann dürfen, wenn er
Den Status PREMIUM hat und mindestens 16 Jahre alt ist.
Den Status NORMAL hat und mindestens 18 Jahre alt ist.
Das modellieren wir mit EVALUATE z.Bsp. so:
EVALUATE TRUE ALSO B-ALTER
WHEN PREMIUM ALSO GREATER THAN OR EQUAL TO 16
WHEN NORMAL ALSO GREATER THAN OR EQUAL TO 18 DISPLAY “Du darfst ein Bier
bestellen.” WHEN OTHER DISPLAY “Du darfst leider kein Bier bestellen.”
END-EVALUATE
Jeder WHEN-Zweig muss nun zwei Bedingungen prüfen, welche mit ALSO getrennt werden.
Die erste Bedingung, die zu TRUE evaluieren muss, ist die Condition PREMIUM. Die zweite, dass das B-ALTER größer oder gleich 16 ist.
Es ist Geschmackssache, ob man folgende Variante für lesbarer hält:
EVALUATE TRUE ALSO TRUE
WHEN PREMIUM ALSO B-ALTER >= 16 WHEN NORMAL ALSO B-ALTER >= 18
DISPLAY “Du darfst ein Bier bestellen.” WHEN OTHER DISPLAY “Du darfst
leider kein Bier bestellen.” END-EVALUATE
Die zweite Bedingung ist nun nicht auf einen Vergleich von B-ALTER festgelegt, sondern ist beliebig.
Semantisch äquivalent wäre auch noch diese Variante:
EVALUATE TRUE ALSO B-ALTER WHEN PREMIUM ALSO >= 16 WHEN NORMAL ALSO >= 18 DISPLAY “Du darfst ein Bier bestellen.” WHEN OTHER DISPLAY “Du darfst leider kein Bier bestellen.” END-EVALUATE
Welche Variante man bevorzugt, kann jeder selber entscheiden. 3.4.3. Schleifen
Bei fast jeder Programmieraufgabe gibt es Dinge, die immer wieder getan werden müssen. Auch in COBOL gibt es für solche Fälle die bekannten Schleifen. 3.4.3.1. PERFORM n TIMES
Die einfachste Schleife mit fest definierter Durchlaufanzahl ist PERFORM n TIMES:
PERFORM 3 TIMES DISPLAY “Hallo” END-PERFORM
Die Ausgabe dieses Programms ist wenig überraschend:
./NTIMES Hallo Hallo Hallo
Die Anzahl der Schleifendurchläufe muss nicht hart codiert sein, sondern kann durchaus variabel sein:
MOVE 4 TO TEMP-ZAEHLER PERFORM TEMP-ZAEHLER TIMES DISPLAY “Hallo” END-PERFORM
Frage 1
Wie oft wird diese Schleife durchlaufen?
MOVE 5 TO TEMP-ZAEHLER PERFORM TEMP-ZAEHLER TIMES ADD 1 TO TEMP-ZAEHLER END-PERFORM
Antwort:
Genau 5 mal.
Unendlich oft.
Es gibt einen Compilefehler.
3.4.3.2. PERFORM UNTIL
In den meisten Fällen will man einen bestimmten Code-Block nicht nur n mal durchlaufen, sondern man möchte auch wissen, das wievielte Mal die Schleife schon durchlaufen wurde und mit diesem Index dann etwas machen, z.Bsp. auf eine Tabelle zugreifen oder für die Ausgabe benutzen.
Dafür gibt es in COBOL das PERFORM VARYING … FROM … BY … UNTIL … -Konstrukt. Beispiel 1. PERFORM UNTIL
IDENTIFICATION DIVISION. PROGRAM-ID. DATADEFS. ENVIRONMENT DIVISION. DATA DIVISION.
WORKING-STORAGE SECTION. 01 TEMP-ZAEHLER PIC 99. *>
PROCEDURE DIVISION. PERFORM VARYING TEMP-ZAEHLER FROM 1 BY 1 > UNTIL TEMP-ZAEHLER > 10 >
DISPLAY TEMP-ZAEHLER
END-PERFORM
GOBACK. END PROGRAM DATADEFS.
Hier wird der Schleifenindex definiert.
Setzen des Startwerts von TEMP-ZAEHLER und Definition der Schrittweite.
Die Abbruchbedingung.
Dieses Programm gibt die Zahlen von 1 bis 10 auf der Konsole aus.
Frage 2
Was passiert, wenn man den Index im Beispiel 1 so definiert:
01 TEMP-ZAEHLER PIC 9.
Antwort
Eine Endlosschleife.
Das Programm macht nichts.
Ein Compilefehler.
Die Schrittweise ist standardmäßig schon 1, es ist also nicht nötig jedes Mal BY 1 explizit hinzuschreiben.
Mit EXIT PERFORM CYCLE kann man vorzeitig aus einer Schleife ausbrechen.
Auch die VARYING-Anweisung ist optional, man kann den Schleifenindex auch selber verwalten:
MOVE 1 TO TEMP-ZAEHLER PERFORM UNTIL TEMP-ZAEHLER > 10 DISPLAY TEMP-ZAEHLER
ADD 1 TO TEMP-ZAEHLER END-PERFORM
Der Startwert wird gesetzt.
Der Zähler muss manuell hochgezählt werden.
3.4.3.2.1. WITH TEST AFTER
Wann wird die Abbruchbedingung der Schleife geprüft? Passiert das vor dem Durchlauf, oder danach? Probieren wir es aus:
MOVE 10 TO TEMP-ZAEHLER PERFORM UNTIL TEMP-ZAEHLER = 10 DISPLAY TEMP-ZAEHLER END-PERFORM
Wenn die Abbruchbedingung vor dem Durchlauf geprüft würde, würden wir keine Ausgabe erwarten:
./DATADEFS >
Und so ist es auch. Wir können den Compiler aber auch dazu bringen, die Abbruchbedingung erst am Ende zu prüfen, durch Zusatz von WITH TEST AFTER:
MOVE 10 TO TEMP-ZAEHLER PERFORM WITH TEST AFTER UNTIL TEMP-ZAEHLER = 10 DISPLAY TEMP-ZAEHLER END-PERFORM
Nun bekommen wir:
./DATADEFS 10
Diese Unterscheidung wird beim Lesen von Dateien später noch wichtig werden. 3.4.4. Strukturierung
Um die Programmlogik strukturisieren und modularisieren zu können, gibt es in COBOL mehrere Möglichkeiten. Wir werden uns auf zwei Alternativen beschränken, die aber für die allermeisten Fälle genau passend sind. 3.4.4.1. SECTIONs
Programmlogik innerhalb der PROCEDURE DIVISION kann mittels SECTIONs strukturiert werden.
Schauen wir uns das Beispiel aus dem Kapitel über Schleifen nochmal an.
Wir könnten die Schleife in eine eigene SECTION auslagern:
AUSGABE-ZAHLEN-1-BIS-10 SECTION. *>
PERFORM WITH TEST AFTER VARYING TEMP-ZAEHLER FROM 1 BY 1
UNTIL TEMP-ZAEHLER = 10
DISPLAY TEMP-ZAEHLER
END-PERFORM
EXIT. *>
Wir definieren eine SECTION mit dem Namen AUSGABE-ZAHLEN-1-BIS-10
Am Ende einer SECTION sollte ein EXIT-Befehl stehen, damit der Programmfluss an die Stelle zurückkehrt, wo die SECTION aufgerufen wurde.
Und diese dann in der PROCEDURE DIVISION aufrufen:
PROCEDURE DIVISION. PERFORM AUSGABE-ZAHLEN-1-BIS-10
GOBACK.
Indem man Logik in eine eigene SECTION auslagert, kann man der Logik einen aussagekräftigen Namen geben und somit seinen Nachfolgern einen großen Gefallen tun.
Das hat den großen Vorteil, dass jemand direkt beim Blick in die PROCEDURE DIVISION erkennen kann, was das Programm macht. Es ist nicht nötig, erst Sinn und Zweck der Schleife zu entschlüsseln.
Man könnte ja auch Kommentare verwenden, um die Logik zu beschreiben, z.Bsp.:
PROCEDURE DIVISION.
*Ausgabe der Zahlen von 1 bis 10
PERFORM WITH TEST AFTER VARYING TEMP-ZAEHLER FROM 1 BY 1
UNTIL TEMP-ZAEHLER = 10
DISPLAY TEMP-ZAEHLER
END-PERFORM
GOBACK.
Was ist der Nachteil an Kommentaren im Code? 3.4.4.2. Paragraphen
Eine weitere Möglichkeit, COBOL-Code zu strukturieren, sind Paragraphen. Diese verhalten sich so ähnlich wie eine SECTION, werden aber ohne ein bestimmtes Schlüsselwort definiert:
DAS-IST-EIN-PARAGRAPH. *>
DISPLAY "Herzlich Willkommen."
. *>
Die Definition eines Paragraphen besteht nur aus seinem Namen und einem Punkt.
Ein Paragraph endet immer beim nächsten Punkt.
Dieser Paragraph kann jetzt auch mittels PERFORM aufgerufen werden:
PERFORM DAS-IST-EIN-PARAGRAPH
Der Unterschied zwischen Paragraphen und SECTIONs ist, dass eine SECTION mehrere Paragraphen enthalten kann. Beim PERFORM einer SECTION werden dann alle enthaltenen Paragraphen durchlaufen.
Schauen wir uns einige Beispiele an.
Nehmen wir ein kleines Programm und strukturieren es mittels Paragraphen:
IDENTIFICATION DIVISION. PROGRAM-ID. DATADEFS. ENVIRONMENT DIVISION. DATA DIVISION. PROCEDURE DIVISION. PERFORM ICH-BIN-EIN-PARAGRAPH PERFORM ICH-BIN-AUCH-EIN-PARAGRAPH GOBACK.
ICH-BIN-EIN-PARAGRAPH. DISPLAY “Hallo,” . ICH-BIN-AUCH-EIN-PARAGRAPH. DISPLAY “Welt!” . END PROGRAM DATADEFS.
Führen wir dieses Programm aus, erhalten wir:
./DATADEFS Hallo, Welt!
Genau das Gleiche gilt, wenn wir aus den Paragraphen jeweils eine SECTION machen:
IDENTIFICATION DIVISION. PROGRAM-ID. DATADEFS. ENVIRONMENT DIVISION. DATA DIVISION. PROCEDURE DIVISION. PERFORM ICH-BIN-EINE-SECTION PERFORM ICH-BIN-AUCH-EINE-SECTION GOBACK.
ICH-BIN-EINE-SECTION SECTION. > DISPLAY “Hallo,” . ICH-BIN-AUCH-EINE-SECTION SECTION. > DISPLAY “Welt!” . END PROGRAM DATADEFS.
Die Paragraphen wurden nun SECTIONs.
An der Ausgabe des Programms hat sich nichts geändert, das können wir leicht nachprüfen. 3.4.4.3. Bitte nicht mischen!
Es ist keine gute Idee, sein Programm mit einer Mischung aus Paragraphen und SECTIONs zu strukturieren.
Warum ist das so? Schauen wir uns folgendes Beispiel an:
IDENTIFICATION DIVISION. PROGRAM-ID. DATADEFS. ENVIRONMENT DIVISION. DATA DIVISION. PROCEDURE DIVISION. PERFORM ICH-BIN-EINE-SECTION *> PERFORM ICH-BIN-EIN-PARAGRAPH PERFORM ICH-BIN-AUCH-EIN-PARAGRAPH GOBACK.
ICH-BIN-EINE-SECTION SECTION. DISPLAY “Herzlich Willkommen!” .
ICH-BIN-EIN-PARAGRAPH. DISPLAY “Hallo,” . ICH-BIN-AUCH-EIN-PARAGRAPH. DISPLAY “Welt!” . END PROGRAM DATADEFS.
Wir haben jetzt eine SECTION und zwei Paragraphen
Frage 3
Welche Ausgabe erwarten wir bei dem obigen Programm?
Probieren wir auch dieses Programm einmal aus:
./DATADEFS Herzlich Willkommen! Hallo, Welt! Hallo, Welt!
Überrascht? Warum ist das so? Warum haben wir zweimal als Ausgabe?
Hallo, Welt! Hallo, Welt!
Schauen wir uns die SECTION ICH-BIN-EINE-SECTION mal genauer an:
ICH-BIN-EINE-SECTION SECTION. DISPLAY “Herzlich Willkommen!” .
Wo ist diese SECTION zu Ende? Beim ersten Punkt? Nein, eben gerade nicht! Ein Punkt beendet einen PARAGRAPH, aber eine SECTION kann mehrere Paragraphen enthalten und bei einem PERFORM auf die SECTION werden dann alle Paragraphen aufgerufen.
Gut, also haben wir schonmal verstanden, warum die Ausgabe so ist, wie sie ist. Aber können wir die Mischung aus Paragraphen und SECTIONs irgendwie heilen?
In vielen Legacy-COBOL-Anwendungen findet man am Ende jeder SECTION einen EXIT-Befehl, also z.Bsp:
ICH-BIN-EINE-SECTION SECTION. DISPLAY “Herzlich Willkommen!” EXIT. *>
Hat das EXIT-Keyword den gewünschten Effekt?
Wir können es ausprobieren… aber, um die Enttäuschung vorweg zu nehmen… Nein, das EXIT hilft nicht weiter.
Ein Blick in die Dokumentation erklärt auch, warum EXIT hier nicht hilft. Einfach aus dem Grund, dass EXIT nicht als SECTION-Ende spezifiert ist!
Wie können wir denn dann unsere SECTION ICH-BIN-EINE-SECTION beenden?
Die Dokumentation ist eindeutig. Eine SECTION endet dort, wo eine neue SECTION anfängt.
Daraus folgt direkt, dass eine Mischung aus Paragraphen und SECTIONs nur unnötige Probleme macht. Man sollte sich für eine Strukturierung entscheiden!
Wir werden unsere Beispiele mittels SECTIONs strukturieren.
Mit COBOL 2002 gäbe es aber tatsächlich doch eine Möglichkeit, eine SECTION mit einem Keyword zu beenden: durch das neue EXIT SECTION-Konstrukt, welches eigentlich dazu gedacht ist, eine SECTION vorzeitig zu verlassen. Dieses Keyword ist auch bei IBM-Compilern ab Version 5.2 verfügbar.
Frage 4
Ist der Einsatz von EXIT SECTION der Verständlichkeit eines Programms eher förderlich oder abträglich? 4. Dateien Lesen und Schreiben
Mit COBOL ist es sehr einfach, Dateien einzulesen und auch wieder zu schreiben. Tatsächlich ist das auch eines der Haupteinsatzgebiete von COBOL.
Wir wollen uns anschauen, wie wir eine einfache Datei, in der Datensätze über Personen stehen, einlesen können.
IDENTIFICATION DIVISION.
PROGRAM-ID. IO.
ENVIRONMENT DIVISION.
INPUT-OUTPUT SECTION.
FILE-CONTROL.
SELECT STUDENT ASSIGN TO 'students.txt' *>
ORGANIZATION IS LINE SEQUENTIAL.
DATA DIVISION.
FILE SECTION.
FD STUDENT.
01 STUDENT-FILE. *>
05 STUDENT-ID PIC 9(5).
05 STUDENT-NAME PIC X(20).
WORKING-STORAGE SECTION.
01 WS-STUDENT.
05 WS-STUDENT-ID PIC 9(05).
05 WS-STUDENT-NAME PIC X(20).
01 WS-EOF PIC X(1).
PROCEDURE DIVISION.
OPEN INPUT STUDENT.
PERFORM UNTIL WS-EOF="Y" *>
READ STUDENT INTO WS-STUDENT *>
AT END MOVE "Y" TO WS-EOF *>
NOT AT END DISPLAY WS-STUDENT *>
END-READ
END-PERFORM
CLOSE STUDENT.
GOBACK.
END PROGRAM IO.
Hier wird der Dateiname fixiert.
Wir beschreiben den Aufbau der Datei. Die Datei muss an den ersten 5 Stellen Ziffern haben, die der ID einer Person entsprechen. Ab dem 6. Zeichen kommt dann in einer Länge von 20 Zeichen der Name des Studenten.
Es wird so lange ein neuer Satz, also die nächste Zeile, gelesen, bis der Schalter WS-EOF auf Y steht.
Der Inhalt der Datei wird in die WORKING-STORAGE SECTION übertragen. Ab hier können wir dann mit den gelesenen Daten arbeiten.
Wird erkannt, dass die Datei bis zum Ende gelesen wurde, wird der Schalter WS-EOF`auf `Y gesetzt.
Der Inhalt von WS-STUDENT wird auf der Konsole ausgegeben.
Das Programm liest die Datei students.txt ein, schreibt die Sätze in die WORKING-STORAGE SECTION und gibt diese anschließend auf der Konsole aus. 4.1. Schreiben von Dateien
Nehmen wir an, wir sollen die Datei aus dem vorherigen Beispiel einlesen und nur die Namen der Studenten ausgeben - ohne die ID. Wir müssen nun also wie vorhin die Eingabedatei Satz für Satz einlesen und dann jeweils den Namen in eine neue Datei schreiben.
IDENTIFICATION DIVISION.
PROGRAM-ID. IO.
ENVIRONMENT DIVISION.
INPUT-OUTPUT SECTION.
FILE-CONTROL.
SELECT STUDENT-IN ASSIGN TO 'students.txt'
ORGANIZATION IS LINE SEQUENTIAL.
SELECT STUDENT-OUT ASSIGN TO 'output.txt' *>
ORGANIZATION IS LINE SEQUENTIAL.
DATA DIVISION.
FILE SECTION.
FD STUDENT-IN.
01 STUDENT-IN-FILE.
05 STUDENT-ID PIC 9(5).
05 STUDENT-NAME PIC X(20).
FD STUDENT-OUT.
01 STUDENT-OUT-FILE.
05 STUDENT-NAME PIC X(20). *>
WORKING-STORAGE SECTION.
01 WS-STUDENT.
05 WS-STUDENT-ID PIC 9(05).
05 WS-STUDENT-NAME PIC X(20).
01 WS-EOF PIC X(1).
PROCEDURE DIVISION.
OPEN OUTPUT STUDENT-OUT *>
OPEN INPUT STUDENT-IN
PERFORM UNTIL WS-EOF="Y"
READ STUDENT-IN INTO WS-STUDENT
AT END MOVE "Y" TO WS-EOF
NOT AT END
MOVE WS-STUDENT-NAME
TO STUDENT-NAME IN STUDENT-OUT-FILE *>
WRITE STUDENT-OUT-FILE FROM STUDENT-OUT-FILE *>
END-READ
END-PERFORM
CLOSE STUDENT-IN
CLOSE STUDENT-OUT *>
GOBACK.
END PROGRAM IO.
Wir definieren eine weitere Datei für die Ausgabe.
Der Record für die Ausgabedatei besteht jetzt nur aus dem Namen des Studenten.
Die Ausgabedatei wird geöffnet, diesmal für OUTPUT. Logisch.
Der Name wird in den Record der Ausgabedatei geschrieben …
… und anschließend direkt rausgeschrieben.
Zu guter Letzt wird die Datei geschlossen.
FizzBuzz
Draw a Diamond
Tic Tac Toe
ohce
Römische Zahlen
Taschenrechner
Elfen-Memory
Krabbenpoker
Addieren zu 2020
References
[mf] https://www.microfocus.com/de-de/about/press-room/article/2013/academia-needs-more-support-to-tackle-the-it-skills-gap/
[ed] https://homepages.cwi.nl/~storm/teaching/reader/Dijkstra68.pdf
[dp] David Parnas, irgendwann 1970. Mündlich überliefert.