Funktionsanalyse in GFA-BASIC

Bei FUANAL handelt es sich um ein Programm mit einer Länge von ca. 2,5 kB (exkl. Kommentare). Es handelt sich um einen Algorithmus ZUR Analyse einer vom Benutzer innerhalb eines Programms eingegebenen Formel bzw. Funktion und sinnigerweise deren Lösung.

Sicherlich kann diese Problemlösung von vielen Programmierern benötigt werden, da das problem der direkten Eingabe von Funktionen während der Programmlaufzeit recht häufig auftritt, denn die BASIC-Funktion DEF FN ist ja nur vom Programmierer, nicht aber vom Benutzer modifizierbar.

Das Programm beinhaltet ausschließlich acht Subroutines, die ich der Reihe nach erklären möchte.

Init: Ein einmaliger Aufruf am Anfang eines Programms genügt. Es werden drei Felder dimensioniert: Res() enthält die von der Funktion definierten Variablen und Konstanten sowie die aus den diversen Rechenoperationen erhaltenen Zwischenergebnisse. Var!() und Con!() sind Flags, die bei der Eingabe einer Variablen bzw. Konstanten gesetzt werden, um eine Doppelabfrage zu vermeiden. Op1$ enthält alle Rechenoperationen mit zwei Operanden, Op2$ diejenigen mit einem Operanden.

Input: Eine vom Programmierer beliebig gestaltbare Routine, Hauptsache, die Funktion steht in Func$. Diese Variable bleibt übrigens erhalten, um sie zum Beispiel der Eingaberoutine zum Edieren übergeben zu können. Zu beachten ist das Eingabeformat: Akzeptiert werden beliebig viele Klammerebenen und alle Rechenoperationen, die in Op1$ und Op2$ enthalten sind, Zahlen sind jedoch unzulässig. Es sind je zehn Variablen und Konstanten erlaubt im Format V0 bis V9 bzw. C0 bis C9, das sollte wohl genügen. Eine gemischtquadratische Gleichung 2. Grades wird also z.B. so eingegeben: c1v1^c2+c3v1+c4. Das hat durchaus einen Sinn: Man kann die Funktion nicht nur benutzer-, sondern auch programmgesteuert mit beliebigen Werten belegen, ohne die Funktion edieren zu müssen.

Trim: Hier erfolgt die Umwandlung in Großbuchstaben, die Entfernung von Leerzeichen und die Umwandlung von V1, V2,... in 01,02,... bzw. C1, C2,... in 11, 12,..., deren Wert auf diese Weise direkt den Index im Feld Res() darstellt. V1 steht also in Res(1), V2 is Res(2), C1 in Res(11), C2 in Res(12) usw. Die restlichen Feldelemente werden die Zwischenergebnisse enthalten. Unser Beispiel sieht also so aus: 1101^12+1301+14.

Check: Es wird kontrolliert, ob eine Eingabe gemacht wurde, bzw. ob alle Klammerebenen richtig geöffnet bzw. geschlossen wurden. Die Routine kann beliebig erweitert werden, z.B. mit einer Kontrolle auf unbekannte Funktionen und Operationen.

Const: Hier werden die Konstanten eingegeben und in Res(10) bis Res(19) gespeichert. Im obigen Beispiel würde C2 also gleich 2 sein und in Res(12) stehen.

Vars: Das gleiche geschieht hier mit den Variablen. Eine Trennung der beiden Eingaberoutinen ist deswegen sinnvoll, weil man die Konstanten meistens nur einmal eingibt, die Variable(n) jedoch mehrmals. Es muß dafür also nur Vars aufgerufen werden.

Main: Hier steht der Lösungsalgorithmus, der folgendermaßen funktioniert: Gesucht wird nach der ersten geschlossenen und der dazugehörigen offenen Klammer; man erhält so eine von mehreren möglichen innersten Klammerebenen. Die For-Schleife bestimmt die höchstwertige Rechenoperation innerhalb dieser Ebene. Falls eine solche Operation gefunden wird, wird deren Position bestimmt und die beiden Operanden rechts und links des Rechensymbols aus Res() in O1 und O2 geholt. Anschließend werden diese abhängig von der Operation miteinander in R verknüpft. R wird nun in Res() ab dem Index 20 abgelegt, der String dementsprechend manipuliert (gekürzt) und der Indexpointer für Res() inkrementiert. Wird keine Operation gefunden, werden die nun unnötigen Klammern aus dem String entfernt und mit der “kleinen” Repeat-Schleife nach einer evtl. vorhandenen davorstehenden Funktion gesucht (Bsp.: SIN). Findet sich eine, wird der Operand aus Res() in O1 übertragen und das Ergebnis der Funktion in R abgelegt. Die nächsten drei Schritte sind mit den obigen drei gleich. Nun wird wieder nach einer geschlossenen Klammer gesucht, und zwar so lange, bis eben keine mehr vorhanden ist. Was übrigbleibt, ist das Ergebnis, welches in Res steht.

Folgende Funktion mit zwei Variablen soll definiert werden:

Herkömmliche Schreibweise: f(x,y)=sin(x+5)((4+3)/y^2)+x mit x=3, y=7
Eingabeformat: sin(v1+c1)
((c2+c3)/v2^c4)+v1
Eingabe der Konstanten: c1=5, c2=4, c3=3, c4=2
Eingabe der Variablen: v1=3, v2=7
Res() sieht so aus: Res(1)=3, Res(2)=7, Res(11)=5, Res(12)=4, Res(13)=3, Res(14)=2, Rest 0
Die getrimmte Funktion lautet: (SIN(01+11)*(12+13)/02^14)+01)
Die Reduktion geht folgendermaßen vor sich:

   
(SIN(20)*((12+13)/02A14)+01) In Res(20) steht res(1 )+Res(11)
(21*((12+13)/02^14)+01) Res(21)=SIN(Res(20))
(21*(22/02^14)+01) Res(22)=Res(12)+Res(13)
(21*(22/23)+01) Res(23)=Res(2)^Res(14)
(21*24+01) Res(24)=Res(22)/Res(23)
(25+01) Res(25)=Res(21)*Res(24)
(26) Res(26)=Res(25)+Res(1)

Das Ergebnis steht in Res(26)

Tabelle 1: Beispiel eines Rechenablaufs

Output: Das Ergebnis wird, hier nur als Beispiel, ausgedruckt. Ich denke, eine Beispiel (siehe Tabelle 1) wäre nun angebracht, stimmt’s?

Man kann den Ablauf gut verfolgen, wenn man in der Routine Main nach dem Print At-Befehl Void Inp(2) o.ä. einfügt.

Die Anzahl der Rechenoperationen sowie der Funktionen können beliebig erweitert werden. Dabei ist nur ein wenig zu beachten: Eine Rechenoperation muß in Op1$ vermerkt sein und darf nur ein Zeichen lang sein. Die Position des Zeichens in Op1$ bestimmt gleichzeitig die Priorität der Operation (von links nach rechts aufsteigend). Außerdem muß sie natürlich in der ersten Select-Case-Abfrage der Routine Main enthalten sein. Die Zahl hinter dem Case ist die Position des Zeichens in Op1$. Eine Funktion muß in Op2$ vermerkt sein, der Name darf beliebig lang sein und es gibt keine Priorität. In der zweiten Select-Case-Anweisung (nach Else) muß sie ebenfalls abgearbeitet werden. Die Zahl nach Case ist die Position, ab der der Funktionsname in Op2$ beginnt.

Beispiel: Aufnahme der Modulberechnung, dargestellt durch “&” und der Signum-Funktion Sgn():

Noch einen Hinweis: Will man eine Wertetabelle erstellen, muß man normalerweise die Routinen Vars (oder eine eigene Routine zur Variablenbelegung) und Main mehrmals durchlaufen. Dadurch wird die Formel jedesmal erneut reduziert, was jedoch Zeit kostet. Abhilfe kann man schaffen, indem man Res() um eine Dimension erweitert. Will man z.B. das Intervall von 1 bis 2,5 in Schritten von 0,5 durchlaufen und VI damit belegen, belegt man Res() vor dem Aufruf von Main: Res(1,0)=1, Res(1,1 )= 1.5, Res(1,2)=2, Res(1,3)=2.5. Eine Konstante ist für alle VI-Werte gleich, dementsprechend wird Res(1x,0) bis Res(1x,3)=Cx. Genauso oft muß man dann die beiden Select-Case-Anweisungen durchlaufen, genauer gesagt den Teil von “O1=Res(... bis Res(F1)=R”. Dies gilt in beiden Fällen, natürlich müssen alle Res()-Aufrufe angepaßt werden. Weitere Erläuterungen und die Erklärung der Variablen kann man dem Listing entnehmen.


    Op1$="-+\&*/^"
    Op2$="SINCOSTANLOGLNSGN"
    Procedure Main, 1.Select-Case: (Case 4,5,6 um 1 erhöhen) 
                      Case 4 
                      R=01 Mod 02 
                    2.select-Case: Case 15 
                      R=Sgn(01)
Init            !(c) MAXON Computer GmbH
Input
Trim
Check
Const
Vars
Main
Output
'
Procedure Init              !Initialisierung
    Dim Res(99),Var!(9),Con!(9) !(Zw.-)Ergebnisse
    Op1$="-+\*/^"           !Oper. m. 2 Operanden
    Op2$="SINCOSTANLOGLN"   !Oper. m. 1 Operand
Return
'
Procedure Input             !Eingabe
    Box 120,100,520,150v.
    Text 128,120,"Bitte geben Sie die Funktion ein: "
    Print At(17,9);"F=";
    Form input 45 As Func$  !Func$ bleibt erhalten 
Return
'
Procedure Trim              !Formatieren
    Fn$="("+Upper$(Func$)+")" !Nur Gr|oßbuchstaben
    For I=1 To Len(Fn$)     !Fn$ durchsuchen
        J!=Mid$(Fn$,I|,1)=” " !hier nach Leerzeichen 
        Fn$=Left$(Fn$,Pred(I|))+Mid$(Fn$,I|-J!) !weg damit, falls gefunden 
        Add I|,J!               !Zählerkorrektur
        J|=Asc(Mid$(Fn$,I|))    !hier nach "C" und "V" 
        If J|>47 And J|<58      !"C" wird "1", "V" wird "0"
            Mid$(Fn$,Pred(I|))=Chr$(48-Mid$(Fn$,Pred(I|),1)="C")
        Endif 
    Next I|
    Ln|=Len(Fn$)                !Endgültige Länge
Return
'
Procedure Check                 !Eingabekontrolle
    If Fn$<>"()"                !Falls Eingabe
        Clr A|
        For I|=1 To Ln|         !Klammern zählen
            Sub A|,Mid$(Fn$,I|,1)="("-Mid$(Fn$,I|,1)=")" 
        Next I|                 !A|=0, wenn ok
        If A|<>0
            Alert 0," Klammerebenen|falsch gesetzt.|",1,"  OK  ",Z|
        Endif
    Else                        !Falls keine Eingabe
        Alert 0,"Keine Funktion |  eingegeben.|",1,"  OK  ",Z|
    Endif
Return
'
Procedure Const                 !Konstanteneingabe
    Locate 1,2
    Arrayfill Con!(),False      !Indikator zurücksetzen 
    For I|=1 To Ln|             !Fn$ durchsuchen
        J|=Asc(Mid$(Fn$,I|))    !Aktuelles Zeichen
        K|=Asc(Mid$(Fn$,Pred(I|)))   !Zeichen links
        If J|>47 And J|<58 And K|=49 !Falls J|=Zahl und K|="1"
            If Not Con!(J|-48)  !und nicht eingegeben 
                Print " C";Chr$(J|);"=";
                Input Res(J|-38)     !Eingabe (in Res(10)-Res(19))
                Con!(J|-48)=True     !Indikator setzen 
            Endif 
        Endif 
    Next I|
Return
'
Procedure Vars                  !Variableneingabe
    Print
    Arrayfill Var!(), False     !Indikator rücksetzen 
    For I|=1 To Ln|             !Fn$ durchsuchen
        J|=Asc(Mid$(Fn$,I|))    !Aktuelles Zeichen
        K|=Asc(Mid$(Fn$,Pred(I|)))      !Zeichen links
        If J|>47 And J|<58 And K|=48    !Falls J|=Zahl und K|="0"
            If Not Var!(J|-48)  !und nicht eingegeben
                Print " V";Chr$(J|);"=";
                Input Res(J|-48)        !Eingabe (in Res(0)-Res(9))
                Var!(J|-48)=True        !Indikator setzen 
            Endif 
        Endif 
    Next I|
Return
'
Procedure Main                  !Hauptroutine
    F$=Fn$                      !Fn$ bleibt erhalten
    F|=20                       !Index n. Zw.ergebnis
    Repeat
        Print At(17,11);F$'''''
        J|=Pred(Instr(F$,")"))  !Erste geschl. Klammer 
        K|=Succ(Rinstr(F$,"(",J|)) !Dazugehörige offene Klammer
        L|=0
        For I|=K| To J|         !Suche höchste Oper.
            L|=Max(L|,Instr(Op1$,Mid$(F$,I|,1))) !L|=Position in Op1$
        Next I|
        If L|                   !Falls vorhanden
            M|=K|+Pred(Instr(Mid$(F$,K|,J|-Pred(K|)),Mid$(Op1$,L|,1))) ! hier ist sie
            O1=Res(Val(Mid$(F$,M|-2,2)))    !Operand links von ihr 
            O2=Res(Val(Mid$(F$,Succ(M|),2))) !Operand rechts 
            Select L|           !Welche denn nu?
            Case 1 
                R=O1-O2 
            Case 2 
                R=O1+O2
            Case 3              !Zahl nach "Case" gibt
                R=O1\O2         !Pos. d. Rechensymbols
            Case 4              !in Op1$ wieder
                R=O1*O2 
            Case 5 
                R=O1/C2 
            Case 6 
                R=O1^O2 
            Endselect
            Res(F|)=R           !Ergebnis merken
            F$=Left$(F$,M|-3)+Str$(F|)+Mid$(F$,M|+3) !String kürzen 
            Inc F|              !Index erhöhen
        Else                    !Falls keine gefunden
            F$=Left$(F$,K|-2)+Mid$(F$,K|,2)+Mid$(F$,J|+2) !Klammern eliminieren
            M|=Pred(K|)         !Nach vorhandener Fkt
            Repeat              !davor suchen
                Dec M|
                N|=Asc(Mid$(F$,M|))
            Until N|<65 Or N|>90 !b. Zeichen<>Buchstabe 
            If M|<K|-2          !Falls gefunden
                O1=Res(Val(Mid$(F$,Pred(K|),2)))    !Operand der Funktion 
                Select Instr(Op2$,Mid$(F$,Succ(M|),K|-M|-2)) ! Funktion herauspicken
                Case 1
                    R=Sin(O1)
                Case 4          !Zahl nach "Case" gibt
                    R=Cos(O1)   !Position in Op2$
                Case 7          !ab der Fktname steht
                    R=Tan(O1)
                Case 10 
                    R=Log10(O1)
                Case 13
                    R=Log(O1)
                Endselect
                Res(F|)=R       !Ergebnis merken
                F$=Left$(F$,M|)+Str$(F|)+Mid$(F$,Succ(K|))  !Funktion canceln
                Inc F|          !Index erhöhen
            Endif 
        Endif
    Until Len(F$)=2             !Bis noch einer übrig
    Res=Res(Val(F$))            !Übertrag in "Res"
Return
'
Procedure Output                !Ausgabe (als Bsp.)
    Box 120,186,520,216
    Text 128,206,"Das Ergebnis lautet: F="+Str$(Res)
    ~Inp(2)
Return

Michael Kraus
Aus: ST-Computer 02 / 1990, Seite 88

Links

Copyright-Bestimmungen: siehe Über diese Seite