Bei jedem Simulator birgt die Schnittstelle zwischen dem Benutzer, der seine Problemstellung in Form von physikalischen Größen über die Eingabeparameter spezifiziert, und dem für die Auswertung verwendeten Rechenkern eine potenzielle Fehlerquelle. An die Schnittstelle werden folgende Anforderungen gestellt.
Um all diese Anforderungen zu erfüllen, wurde für den Simulator eigens ein Verwaltungssystem für die Einheiten geschaffen. Dazu bot sich das Konzept einer objektorientierten Sprache wie C++ an.
Um Rundungsfehler auszuschließen und eine praktische und genaue Darstellung zu erhalten, werden die Exponenten der Zerlegung in SI-Einheiten in Form von Brüchen in Instanzen der Klasse Bruch abgespeichert. Neben der Initialisierung über den Typ int und den arithmetischen Operatoren Addition, Subtraktion, Multiplikation und Division ermöglicht die Klasse Bruch auch die Vereinfachung auf den kleinsten gemeinsamen Nenner.
Unter Verwendung der Klasse Bruch wurde die Klasse Fnumber für die Speicherung reeller Zahlen in der Form
(B.1) |
Mit den bisher beschriebenen Klassen wird nun der neue Typ Basiseinheit - Basicunit - definiert, der sich aus einem Vorfaktor und einer Darstellung der SI-Einheiten zusammensetzt.
(B.2) |
Für den Entwickler des Simulators ist lediglich die von der Klasse Basicunit abgeleitete Klasse Unit von Interesse. Sie unterscheidet sich von Ihrer Basisklasse nur durch den Zuweisungsoperator, über den sie mittels einer Zeichenkette initialisiert werden kann. Die Trennung in Basisklasse Basicunit und Klasse Unit ist notwendig, um die Zerlegung zusammengesetzter Einheiten mit einem Parser vornehmen zu können. Beim Zerlegen der eingegebenen Einheit in die Basiseinheiten wird die Basisklasse für das Abspeichern der Zwischenresultate und die Umrechnungen benutzt.
Der Einstiegspunkt für die lexikalische Analyse
prüft, ob die Eingabe syntaktisch korrekt ist, und liefert als Ergebns
eine Instanz der Klasse Basicunit.
protected EXPR returns [Basicunit u] : u=UNITMULT { (LA(1) == EOF ) }? ;
Zur Erkennung von ganzen und reellen Zahlen werden die Funktionen atoi und atof der Standard-C-Bibliothek verwendet.
protected UINT returns [int i] : ('0'..'9')+ { i = atoi($getText.c_str()); } ; protected INT returns [int i] : ('-')? ('0'..'9')+ { i = atoi($getText.c_str()); } ; protected REAL returns [ double d] : ('-')? (UINT)? '.' (UINT)? (('e'|'E') INT)? { d = atof($getText.c_str()); } ;Hier werden die bekannten Präfixe und Basiseinheiten analysiert und in passender Form zwischengespeichert. Die angegebenen Beispiele sind nur ein kleiner Auszug aus den implementierten Präfixes und Basiseinheiten.
protected PREF returns [Bruch p] : | 'm' { p = -3; } | 'c' { p = -2; } | 'k' { p = 3; } ; protected UNIT returns [Basicunit u] : "m" { u=UNIT_METER; } | "g" { u=UNIT_GRAMM; } | "inch" { u= Basicunit(Fnumber(2.54,-2 ), 1 , 0, 0, 0, 0, 0, 0);} ;
Nun müssen arithmetische Ausdrücke richtig aufgelöst werden. Dabei ist die Wahrung der Prioritätsregeln von Bedeutung.
// lowest priority for addition and subtraction protected UNITMULT returns [Basicunit u] { Basicunit tmp;} : u=UNITADD ( '+' tmp=UNITADD { u += tmp; } | '-' tmp=UNITADD { u -= tmp; } )* ; // second lowest priority for multiplication and division protected UNITADD returns [Basicunit u] { Basicunit tmp;} : u=UNITEXP ( '*' tmp=UNITEXP { u *= tmp; } | '/' tmp=UNITEXP { u /= tmp; } )* ; // middle priority for exponent protected UNITEXP returns [Basicunit u] { Bruch b; } : u=SINGLEUNIT ( '^' b=BRUCH { u^=b; } )? ; // highest priority for explicit parenthesis protected SINGLEUNIT returns [Basicunit u] { Bruch p;double d;int i;} : (REAL) => d=REAL { u=Basicunit(d,0,0,0,0,0,0,0);} | (INT) => i=INT { u=Basicunit(i,0,0,0,0,0,0,0);} | (UNIT) => u=UNIT | (PREF UNIT) => p=PREF u=UNIT { u.adjust_pref(p);} | '(' u=UNITMULT ')' ;
Eine eigene Regel bewerkstelligt das Erkennen von ganzzahligen Brüchen.
protected BRUCH returns [Bruch b] { int i1,i2; } : b=INT | '(' i1=INT '/' i2=UINT ')' { b=Bruch(i1,i2); } ;
Die Flexibilität der Methode liegt in der Möglichkeit des Lexers eine aus Präfix und Basiseinheit zusammengesetzte Einheit zu erkennen. Dabei werden alle möglichen Kombinationen aus den definierten Basiseinheiten und Präfixen überprüft. In der Definition des Lexers wird dies durch eine einzige Zeile definiert, die als einzige Bedingung die Eindeutigkeit von Präfix und Basiseinheiten erfordert. Eine neue Basiseinheit mm ist also nicht zulässig, da sie dann sowohl als Präfix milli und Basiseinheit Meter als auch als die neue Basiseinheit mm interpretiert werden könnte.
Im Prinzip verfolgt die hier implementierte Methode genau jenem Vorgang den ein Benutzer bei der händischen Umwandlung der Einheit ausführen würde. Die Fülle der zusammengesetzten Einheiten, die in der aktuellen Version erkannt werden, ist recht beachtlich, und kann bei Bedarf leicht erweitert werden. Dies bezieht sich nicht nur auf die vorgegebenen Präfixe und Basiseinheiten, sondern auch auf die erkannten arithmetischen Verknüpfungen. Hier ein paar Beispiele aus dem Selbsttest des Einheitenpakets: , , , , , , , , , , , , , , ,
Eine weitere Klasse LabelUnit dient in erster Linie der Ausgabe und der Konsistenz. Die Klasse muss mit einer als Referenz verwendeten Einheit initialisiert werden. Bei weiteren Zuweisungen wird dann immer geprüft, ob es sich um eine Einheit mit gleicher Darstellung in SI-Einheiten handelt. Eine Ausgabe einer Instanz dieser Klasse erfolgt automatisch in der ursprünglichen Einheit. Hier ein Beispiel.
LabelUnit L("2*mm"); LabelUnit L1("km"); L = "1*m"; cout << "1*m" << " = " << L << endl; L1 = L; cout << "= " << L1 << endl;erzeugt die folgende Ausgabe:
1*m = 0.5 * 10^(3) * 2mm = 1 * 10^(-3) * km