Entwicklung eines einfachen Taschenrechners

1. Aufgabe

In dieser Aufgabe betrachten wir einen Taschenrechner zum Ausführen einfacher Berechnungen in den vier Grundrechenarten. Der Rechner kann nur die vier Grundrechenarten und führt diese mit Gleitkommaarithmetik durch. Weitere erweiterte Funktionen, wie sie handelsübliche wissenschaftliche und statistische Rechner besitzen, werden nicht unterstützt. Ein Blick auf Abbildung 1 lässt den gesamten Funktionsumfang des Rechners erahnen:

Oberfläche eines einfachen Taschenrechners.

Abbildung 1. Oberfläche eines einfachen Taschenrechners.


Neben den Schaltflächen („Tasten“) für die Ziffern 0 bis 9 und die vier Grundrechenarten gibt es noch eine Taste „=“ zum Abrufen des aktuellen Ergebnisses, die Backspace-Taste zum Löschen des letzten eingegebenen Zeichens (Ziffer oder Komma) sowie die „C“-Taste (Clear), um den Rechner in den Anfangszustand zu versetzen. Die Berechnungen des Taschenrechners bestehen aus einfachen, arithmetischen Ausdrücken, die fortlaufend über die Tastatur des Taschenrechners eingegeben werden. Da runde Klammern fehlen, ist die Reihenfolge in der Berechnung von Zwischenergebnissen nicht beeinflussbar. Dafür kann der Rechner beliebige Kettenrechnungen in den vier Grundrechenarten (ohne Beachtung eines Operatorenvorrangs) ausrechnen.

Jede eingegebene Zahl wird im so genannten Eingabe-Textfeld angezeigt. Daneben (genauer: darüber) gibt es noch ein zweites Textfeld. Dieses zeigt den gesamten Verlauf einer Berechung an, die so genannte Berechnungshistorie. Die Berechnungshistorie kann durch Drücken der „=“-Taste wieder von Neuem gestartet werden.

2. Lösung

Quellcode: Siehe auch github.com/peterloos/Wpf_SimpleCalculator.git.

Um möglichst einfach und unkompliziert einen Einstieg in den Themenkomplex „Windows Presentation Framework“-Programmierung zu erlangen, lassen sich erste Prototypen eines Taschenrechners vergleichsweise schnell erstellen. Wendet man sich der Verfeinerung derartiger Prototypen zu – zum Beispiel Verarbeitung des Komma-Zeichens und damit Umstellung von ganzzahliger auf Gleitkommaarithmetik – stößt man schnell an die Grenzen eines hemdsärmligen Softwaredesigns. Die vergleichsweise einfache Aufgabenstellung erfordert gerade im Umgang mit der interaktiven Eingabe des Rechenausdrucks (konsekutives Eingeben oder auch Löschen von Ziffern und Sonderzeichen, Ändern des zuletzt eingegebenen Operators, Vorzeichenwechsel, sowie Beachtung der Ausnahmeregeln für die Backspace-Taste und das Komma) einen wohlüberlegten Softwareentwurf.

In der „Windows Presentation Framework“ bietet sich das MVVM-Entwurfsmuster an. Die Gestaltung der Taschenrechneroberfläche nehmen wir ausschließlich deklarativ in XAML vor. Die Weiterleitung aller Eingaben kann einfach auf Kommandos eines unterlagerten ViewModels umgesetzt werden. Die Aktualisierung des Eingabe- und des Berechnungshistorie-Textfelds wiederum ist Angelegenheit für ein Databinding. Das Kernstück des Taschenrechners – die interaktive Verarbeitung aller Eingaben von der Taschenrechnertastatur sowie die Berechnung der arithmetischen Operationen – wird in eine Modellklasse ausgelagert. Damit haben zwar die Komplexität der Eingabeverarbeitung nicht wirklich spürbar reduzieren können, aber:

  • Die gesamte Taschenrechnerfunktionalität ist in einer Klasse konzentriert zusammengefasst – und damit einfacher realisierbar.

  • Ein systematischer Test des Taschenrechners ist ohne Benutzeroberfläche möglich. Sowohl die Modelklasse wie auch die überlagerte ViewModel-Klasse lassen sich automatisiert testen.

Es bieten sich für den nachfolgenden Lösungsvorschlag folgende Klassen an:

  • Klasse CalculatorWindow – View der Anwendung: nur XAML-Anweisungen, kein Codebehind-Anteil.

  • Klasse CalculatorViewModel – ViewModel der Anwendung: C#-Quellcode, der im wesentlichen nur Kommandos (Schnittstelle ICommand) und eine Implementierung der INotifyPropertyChanged-Schnittstelle für das Databinding bereitstellt.

  • Klasse RelayCommand – Hilfsklasse für das ViewModel.

  • Klasse TokenScanner – Modell der Anwendung: C#-Quellcode, der alle Eingaben des Taschenrechners verarbeitet und Rechenergebnisse zur Verfügung stellt.

Entsprechende Realisierungsvorschläge schließen sich nun in Listing 1, Listing 2, Listing 3, und Listing 4 an:

001: <Window x:Class="SimpleCalculator.CalculatorWindow"
002:         xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
003:         xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
004:         xmlns:local="clr-namespace:SimpleCalculator.ViewModel"
005:         Title="Another Simple Calculator" Width="525"
006:         Height="350">
007:     
008:     <Window.DataContext>
009:         <local:CalculatorViewModel />
010:     </Window.DataContext>
011: 
012:     <Grid>
013:         <Grid.ColumnDefinitions>
014:             <ColumnDefinition />
015:             <ColumnDefinition />
016:             <ColumnDefinition />
017:             <ColumnDefinition />
018:             <ColumnDefinition />
019:         </Grid.ColumnDefinitions>
020:         <Grid.RowDefinitions>
021:             <RowDefinition />
022:             <RowDefinition />
023:             <RowDefinition />
024:             <RowDefinition />
025:             <RowDefinition />
026:             <RowDefinition />
027:         </Grid.RowDefinitions>
028: 
029:         <TextBox Grid.Row="0" Grid.Column="0" Grid.ColumnSpan="5"
030:                  Height="30" Margin="3" IsEnabled="False"
031:                  Text="{Binding DisplayHistory}"
032:                  TextAlignment="Right" />
033: 
034:         <TextBox Grid.Row="1" Grid.Column="0" Grid.ColumnSpan="5"
035:                  Height="30" Margin="3" IsEnabled="False"
036:                  Text="{Binding DisplayInput}"
037:                  TextAlignment="Right" />
038: 
039:         <Button Grid.Row="2" Grid.Column="0" Margin="3"
040:                 Command="{Binding DigitCommand}"
041:                 CommandParameter="7" Content="7" />
042:         <Button Grid.Row="2" Grid.Column="1" Margin="3"
043:                 Command="{Binding DigitCommand}"
044:                 CommandParameter="8" Content="8" />
045:         <Button Grid.Row="2" Grid.Column="2" Margin="3"
046:                 Command="{Binding DigitCommand}"
047:                 CommandParameter="9" Content="9" />
048:         <Button Grid.Row="2" Grid.Column="3" Margin="3"
049:                 Command="{Binding OperatorCommand}"
050:                 CommandParameter="/" Content="/" />
051:         <Button Grid.Row="2" Grid.Column="4" Margin="3"
052:                 Command="{Binding BackCommand}"
053:                 Content="&#x232B;" />
054: 
055:         <Button Grid.Row="3" Grid.Column="0" Margin="3"
056:                 Command="{Binding DigitCommand}"
057:                 CommandParameter="4" Content="4" />
058:         <Button Grid.Row="3" Grid.Column="1" Margin="3"
059:                 Command="{Binding DigitCommand}"
060:                 CommandParameter="5" Content="5" />
061:         <Button Grid.Row="3" Grid.Column="2" Margin="3"
062:                 Command="{Binding DigitCommand}"
063:                 CommandParameter="6" Content="6" />
064:         <Button Grid.Row="3" Grid.Column="3" Margin="3"
065:                 Command="{Binding OperatorCommand}"
066:                 CommandParameter="*" Content="*" />
067:         <Button Grid.Row="3" Grid.Column="4" Margin="3"
068:                 Command="{Binding ClearCommand}"
069:                 Content="C" />
070: 
071:         <Button Grid.Row="4" Grid.Column="0" Margin="3"
072:                 Command="{Binding DigitCommand}"
073:                 CommandParameter="1" Content="1" />
074:         <Button Grid.Row="4" Grid.Column="1" Margin="3"
075:                 Command="{Binding DigitCommand}"
076:                 CommandParameter="2" Content="2" />
077:         <Button Grid.Row="4" Grid.Column="2" Margin="3"
078:                 Command="{Binding DigitCommand}"
079:                 CommandParameter="3" Content="3" />
080:         <Button Grid.Row="4" Grid.Column="3" Margin="3"
081:                 Command="{Binding OperatorCommand}"
082:                 CommandParameter="-" Content="-" />
083:         <Button Grid.Row="4" Grid.RowSpan="2" Grid.Column="4"
084:                 Margin="3"
085:                 Command="{Binding EqualCommand}"
086:                 Content="=" />
087: 
088:         <Button Grid.Row="5" Grid.Column="0" Margin="3"
089:                 Command="{Binding NegateCommand}"
090:                 Content="+-" />
091:         <Button Grid.Row="5" Grid.Column="1" Margin="3"
092:                 Command="{Binding DigitCommand}"
093:                 CommandParameter="0" Content="0" />
094:         <Button Grid.Row="5" Grid.Column="2" Margin="3"
095:                 Command="{Binding CommaCommand}"
096:                 Content="," />
097:         <Button Grid.Row="5" Grid.Column="3" Margin="3"
098:                 Command="{Binding OperatorCommand}"
099:                 CommandParameter="+" Content="+" />
100:     </Grid>
101: </Window>

Beispiel 1. Klasse CalculatorWindow: Oberfläche des Taschenrechners (XAML).


001: namespace SimpleCalculator.ViewModel
002: {
003:     using System;
004:     using System.ComponentModel;
005:     using System.Diagnostics;
006:     using System.Windows.Input;
007:     using WpfApplication.Common;
008: 
009:     public class CalculatorViewModel : INotifyPropertyChanged
010:     {
011:         public event PropertyChangedEventHandler PropertyChanged;
012: 
013:         private TokenScanner scanner;
014:         private String displayInput;
015:         private String displayHistory;
016: 
017:         private bool debugViewModel;
018: 
019:         public CalculatorViewModel ()
020:         {
021:             this.scanner = new TokenScanner();
022: 
023:             this.displayInput = "";
024:             this.displayHistory = "";
025: 
026:             this.debugViewModel = true;
027:         }
028: 
029:         // properties
030:         public String DisplayInput
031:         {
032:             get
033:             {
034:                 return this.displayInput;
035:             }
036: 
037:             set
038:             {
039:                 if (this.displayInput != value)
040:                 {
041:                     this.displayInput = value;
042:                     this.OnPropertyChanged("DisplayInput");
043:                 }
044:             }
045:         }
046: 
047:         public String DisplayHistory
048:         {
049:             get
050:             {
051:                 return this.displayHistory;
052:             }
053: 
054:             set
055:             {
056:                 if (this.displayHistory != value)
057:                 {
058:                     this.displayHistory = value;
059:                     this.OnPropertyChanged("DisplayHistory");
060:                 }
061:             }
062:         }
063: 
064:         // commands
065:         public ICommand DigitCommand
066:         {
067:             get
068:             {
069:                 return new RelayCommand (
070:                     param =>
071:                     {
072:                         char digit = ((String) param)[0];
073:                         String msg = String.Format("Digit Command: {0}", digit);
074:                         Debug.WriteLineIf(this.debugViewModel, msg);
075:                         this.scanner.PushChar(digit);
076:                         this.DisplayInput = this.scanner.CurrentInput;
077:                     }
078:                 );
079:             }
080:         }
081: 
082:         public ICommand OperatorCommand
083:         {
084:             get
085:             {
086:                 return new RelayCommand (
087:                     param =>
088:                     {
089:                         String msg = String.Format("Operator Command: {0}", param);
090:                         Debug.WriteLineIf(this.debugViewModel, msg);
091:                         Operator op = Operator.NoOp;
092: 
093:                         switch (((String)param)[0])
094:                         {
095:                             case '+':
096:                                 op = Operator.AddOp;
097:                                 break;
098:                             case '-':
099:                                 op = Operator.SubOp;
100:                                 break;
101:                             case '*':
102:                                 op = Operator.MulOp;
103:                                 break;
104:                             case '/':
105:                                 op = Operator.DivOp;
106:                                 break;
107:                         }
108: 
109:                         this.scanner.PushOp(op);
110:                         this.DisplayInput = this.scanner.CurrentInput;
111:                         this.DisplayHistory = this.scanner.CurrentHistory;
112:                     }
113:                 );
114:             }
115:         }
116: 
117:         public ICommand ClearCommand
118:         {
119:             get
120:             {
121:                 return new RelayCommand(
122:                     param =>
123:                     {
124:                         Debug.WriteLineIf(this.debugViewModel, "Clear Command");
125:                         this.scanner.Reset();
126:                         this.DisplayInput = this.scanner.CurrentInput;
127:                         this.DisplayHistory = this.scanner.CurrentHistory;
128:                     }
129:                 );
130:             }
131:         }
132: 
133:         public ICommand BackCommand
134:         {
135:             get
136:             {
137:                 return new RelayCommand(
138:                     param =>
139:                     {
140:                         Debug.WriteLineIf(this.debugViewModel, "Back Command");
141:                         this.scanner.Back();
142:                         this.DisplayInput = this.scanner.CurrentInput;
143:                     }
144:                 );
145:             }
146:         }
147: 
148:         public ICommand EqualCommand
149:         {
150:             get
151:             {
152:                 return new RelayCommand(
153:                     param =>
154:                     {
155:                         Debug.WriteLineIf(this.debugViewModel, "Equal Command");
156:                         this.scanner.Equal();
157:                         this.DisplayInput = this.scanner.CurrentInput;
158:                         this.DisplayHistory = this.scanner.CurrentHistory;
159:                     }
160:                 );
161:             }
162:         }
163: 
164:         public ICommand CommaCommand
165:         {
166:             get
167:             {
168:                 return new RelayCommand(
169:                     param =>
170:                     {
171:                         Debug.WriteLineIf(this.debugViewModel, "Comma Command");
172:                         this.scanner.Comma();
173:                         this.DisplayInput = this.scanner.CurrentInput;
174:                     }
175:                 );
176:             }
177:         }
178: 
179:         public ICommand NegateCommand
180:         {
181:             get
182:             {
183:                 return new RelayCommand(
184:                     param =>
185:                     {
186:                         Debug.WriteLineIf(this.debugViewModel, "Negate Command");
187:                         this.scanner.Negate();
188:                         this.DisplayInput = this.scanner.CurrentInput;
189:                     }
190:                 );
191:             }
192:         }
193: 
194:         // private helper methods
195:         private void OnPropertyChanged (String propertyName)
196:         {
197:             PropertyChangedEventHandler handler = this.PropertyChanged;
198:             if (handler != null)
199:             {
200:                 PropertyChangedEventArgs args = new PropertyChangedEventArgs(propertyName);
201:                 handler.Invoke(this, args);
202:             }
203:         }
204:     }
205: }

Beispiel 2. Klasse CalculatorViewModel: ViewModel der Anwendung.


Das ViewModel verwendet einen Aufzählungstyp Operator mit folgender Definition:

namespace SimpleCalculator
{
    public enum Operator { NoOp, AddOp, SubOp, MulOp, DivOp };
}

Eine Klasse RelayCommand wird häufig im Umfeld des MVVM-Entwurfsmusters verwendet, ich verweise auf die einschlägigen Hinweise im Netz:

01: namespace WpfApplication.Common
02: {
03:     using System;
04:     using System.Windows.Input;
05: 
06:     class RelayCommand : ICommand
07:     {
08:         private Action<Object> execute;
09:         private Predicate<Object> canExecute;
10: 
11:         public RelayCommand(Action<Object> execute, Predicate<Object> canExecute)
12:         {
13:             if (execute == null)
14:             {
15:                 throw new ArgumentNullException("execute");
16:             }
17: 
18:             if (canExecute == null)
19:             {
20:                 throw new ArgumentNullException("canExecute");
21:             }
22: 
23:             this.execute = execute;
24:             this.canExecute = canExecute;
25:         }
26: 
27:         public RelayCommand(Action<Object> execute)
28:             : this(execute, DefaultCanExecute)
29:         {
30:         }
31: 
32:         public event EventHandler CanExecuteChanged
33:         {
34:             add
35:             {
36:                 if (this.canExecute != null)
37:                     CommandManager.RequerySuggested += value;
38:             }
39:             remove
40:             {
41:                 if (this.canExecute != null)
42:                     CommandManager.RequerySuggested -= value;
43:             }
44:         }
45: 
46:         public bool CanExecute(Object parameter)
47:         {
48:             return this.canExecute != null && this.canExecute(parameter);
49:         }
50: 
51:         public void Execute(Object parameter)
52:         {
53:             this.execute(parameter);
54:         }
55: 
56:         public void Destroy()
57:         {
58:             this.execute = param => { return; };
59:             this.canExecute = param => false;
60:         }
61: 
62:         private static bool DefaultCanExecute(Object parameter)
63:         {
64:             return true;
65:         }
66:     }
67: }

Beispiel 3. Hilfsklasse RelayCommand.


001: namespace SimpleCalculator
002: {
003:     using System;
004:     using System.Diagnostics;
005:     using System.Text;
006: 
007:     public class TokenScanner
008:     {
009:         // input and history
010:         private StringBuilder currentInput;
011:         private StringBuilder currentHistory;
012: 
013:         // last recognized operand and operator
014:         private double lastOperand;
015:         private Operator lastOperator;
016: 
017:         // flags controlling interactive input
018:         private bool twoOperandsExisting;
019:         private bool replaceNextOperatorIfAny;
020:         private bool resetInput;
021:         private bool isBackspaceAllowed;
022:         private bool isConsecutiveEqual;
023: 
024:         private bool debugScanner;
025: 
026:         // c'tor
027:         public TokenScanner ()
028:         {
029:             this.debugScanner = true;
030: 
031:             this.currentInput = new StringBuilder(32);
032:             this.currentHistory = new StringBuilder(32);
033: 
034:             this.Reset();
035:         }
036: 
037:         // properties
038:         public String CurrentInput
039:         {
040:             get
041:             {
042:                 StringBuilder copy = new StringBuilder (this.currentInput.ToString());
043:                 return this.RawToDisplay(copy);
044:             }
045:         }
046: 
047:         public String CurrentHistory
048:         {
049:             get
050:             {
051:                 return this.currentHistory.ToString();
052:             }
053:         }
054: 
055:         // public interface
056:         public void PushChar(char ch)
057:         {
058:             this.isBackspaceAllowed = true;
059: 
060:             if (this.resetInput)
061:             {
062:                 this.currentInput.Clear();
063:                 this.currentInput.Append(ch);
064: 
065:                 this.resetInput = false; 
066:                 this.replaceNextOperatorIfAny = false;
067:             }
068:             else if (this.currentInput.Length == 1)
069:             {
070:                 if (this.currentInput[0] == '0')
071:                 {
072:                     this.currentInput[0] = ch;
073:                 }
074:                 else
075:                 {
076:                     this.currentInput.Append(ch);
077:                 }
078:             }
079:             else
080:             {
081:                 this.currentInput.Append(ch);
082:             }
083: 
084:             this.DumpInputLine();
085:         }
086: 
087:         public void Back()
088:         {
089:             if (!this.isBackspaceAllowed)
090:                 return;
091: 
092:             bool isNegative = false;
093:             if (this.currentInput.Length >= 2 && this.currentInput[0] == '-')
094:             {
095:                 isNegative = true;
096:             }
097: 
098:             if (this.currentInput.Length == 1 && !isNegative)
099:             {
100:                 this.currentInput.Clear();
101:                 this.currentInput.Append('0');
102:             }
103:             else if (this.currentInput.Length == 2 && isNegative)
104:             {
105:                 this.currentInput.Clear();
106:                 this.currentInput.Append('0');
107:             }
108:             else
109:             {
110:                 this.currentInput.Remove(this.currentInput.Length - 1, 1);
111:             }
112: 
113:             this.DumpInputLine();
114:         }
115: 
116:         public void Negate()
117:         {
118:             // should never occur
119:             if (this.currentInput.Length == 0)
120:                 return;
121: 
122:             // don't negate zero
123:             if (this.currentInput.Length == 1 && this.currentInput[0] == '0')
124:                 return;
125: 
126:             char sign = this.currentInput[0];
127:             if (sign != '-')
128:             {
129:                 this.currentInput.Insert(0, '-');
130:             }
131:             else
132:             {
133:                 this.currentInput.Remove(0, 1);
134:             }
135: 
136:             this.DumpInputLine();
137:         }
138: 
139:         public void Comma()
140:         {
141:             if (this.resetInput)
142:             {
143:                 this.resetInput = false;
144: 
145:                 this.currentInput.Clear();
146:                 this.currentInput.Append("0,");
147:             }
148:             else if (this.currentInput.ToString().IndexOf(',') == -1)
149:             {
150:                 this.currentInput.Append(',');  // check for several commas
151:             }
152: 
153:             this.DumpInputLine();
154:         }
155: 
156:         public void PushOp(Operator op)
157:         {
158:             // input needs to be reset upon next input
159:             this.resetInput = true;
160: 
161:             // prevent backspace key destroying current result
162:             this.isBackspaceAllowed = false;
163: 
164:             // current operator isn't equal
165:             this.isConsecutiveEqual = false;
166: 
167:             if (! this.replaceNextOperatorIfAny)
168:             {
169:                 // convert input into numerical value
170:                 double operand = this.ParseInputAsDouble(this.currentInput.ToString());
171:                 
172:                 // build new history
173:                 this.currentHistory.Append(operand.ToString());
174:                 this.currentHistory.Append(this.OperatorToString(op));
175: 
176:                 if (this.twoOperandsExisting)
177:                 {
178:                     // evaluate operation
179:                     this.lastOperand =
180:                         this.CalculateValue(this.lastOperand, this.lastOperator, operand);
181:                 }
182:                 else
183:                 {
184:                     // no first operand: assign input to first operand
185:                     this.lastOperand = operand;
186:                     this.twoOperandsExisting = true;
187:                 }
188: 
189:                 // replace input with last operand or result of calculation
190:                 this.currentInput.Clear();
191:                 this.currentInput.Append(this.lastOperand.ToString());
192: 
193:                 this.replaceNextOperatorIfAny = true;
194:             }
195:             else
196:             {
197:                 this.currentHistory.Remove(this.currentHistory.Length - 3, 3);
198:                 this.currentHistory.Append(this.OperatorToString(op));
199:             }
200: 
201:             // store current operator
202:             this.lastOperator = op;
203: 
204:             this.DumpInputLine();
205:         }
206: 
207:         public void Equal()
208:         {
209:             if (!this.isConsecutiveEqual)
210:             {
211:                 // clear history buffer
212:                 this.currentHistory.Clear();
213: 
214:                 // calculate current calculation result
215:                 double operand = this.ParseInputAsDouble(this.currentInput.ToString());
216:                 double result = this.CalculateValue(this.lastOperand, this.lastOperator, operand);
217: 
218:                 // replace input buffer with result of operation
219:                 this.currentInput.Clear();
220:                 this.currentInput.Append(result.ToString());
221: 
222:                 // clear last operand
223:                 this.twoOperandsExisting = false;
224: 
225:                 // prevent backspace key destroying current result
226:                 this.isBackspaceAllowed = false;
227: 
228:                 // handle upcoming equal key, if any
229:                 this.isConsecutiveEqual = true;
230: 
231:                 // and store second operator, if necessary
232:                 this.lastOperand = operand;
233: 
234:                 // in case of equal next operator should not be replaced
235:                 this.replaceNextOperatorIfAny = false;
236:             }
237:             else
238:             {
239:                 // calculate current calculation result
240:                 double operand = this.ParseInputAsDouble(this.currentInput.ToString());
241:                 double result = this.CalculateValue(operand, this.lastOperator, this.lastOperand);
242: 
243:                 // replace input buffer with result of operation
244:                 this.currentInput.Clear();
245:                 this.currentInput.Append(result.ToString());
246:             }
247: 
248:             this.DumpInputLine();
249:         }
250: 
251:         public void Reset()
252:         {
253:             this.currentInput.Clear();
254:             this.currentInput.Append('0');
255:             this.currentHistory.Clear();
256: 
257:             this.twoOperandsExisting = false;
258:             this.replaceNextOperatorIfAny = false;
259:             this.resetInput = false;
260:             this.isBackspaceAllowed = false;
261:             this.isConsecutiveEqual = false;
262: 
263:             this.lastOperand = 0.0;
264:             this.lastOperator = Operator.NoOp;
265: 
266:             this.DumpInputLine();
267:         }
268: 
269:         // private helper methods
270:         private String OperatorToString (Operator op)
271:         {
272:             String result = "";
273: 
274:             switch (op)
275:             {
276:                 case Operator.AddOp:
277:                     result = " + ";
278:                     break;
279:                 case Operator.SubOp:
280:                     result = " - ";
281:                     break;
282:                 case Operator.MulOp:
283:                     result = " * ";
284:                     break;
285:                 case Operator.DivOp:
286:                     result = " / ";
287:                     break;
288:             }
289: 
290:             return result;
291:         }
292: 
293:         private double CalculateValue(double firstOperand, Operator op, double secondOperand)
294:         {
295:             double result = 0.0;
296: 
297:             try
298:             {
299:                 switch (op)
300:                 {
301:                     case Operator.AddOp:
302:                         result = checked(firstOperand + secondOperand);
303:                         break;
304:                     case Operator.SubOp:
305:                         result = checked(firstOperand - secondOperand);
306:                         break;
307:                     case Operator.MulOp:
308:                         result = checked(firstOperand * secondOperand);
309:                         break;
310:                     case Operator.DivOp:
311:                         if (secondOperand != 0.0)
312:                             result = checked(firstOperand / secondOperand);
313:                         break;
314:                 }
315:             }
316:             catch (OverflowException)
317:             {
318:                 String msg = String.Format("OverflowException: {0} {1} {2}",
319:                     firstOperand.ToString(), op.ToString(), secondOperand.ToString());
320:                 Debug.WriteLineIf(this.debugScanner, msg);
321:             }
322: 
323:             return result;
324:         }
325: 
326:         private String RawToDisplay(StringBuilder sb)
327:         {
328:             if (sb.Length <= 3)
329:                 return sb.ToString();
330: 
331:             int exponentIndex = sb.ToString().IndexOfAny(new char[] { 'e', 'E' });
332:             if (exponentIndex >= 0)
333:                 return sb.ToString();
334: 
335:             int commaIndex = sb.ToString().IndexOf (',');
336:             int negativeSignIndex = sb.ToString().IndexOf('-');
337: 
338:             String result = "";
339: 
340:             // retrieve part of number to extend with decimal points
341:             if (commaIndex == -1)
342:             {
343:                 if (negativeSignIndex == -1)
344:                 {
345:                     result = this.AddDecimalSeparators(sb);
346:                 }
347:                 else
348:                 {
349:                     result = '-' + this.AddDecimalSeparators(sb.Remove(0, 1));
350:                 }
351:             }
352:             else
353:             {
354:                 if (negativeSignIndex == -1)
355:                 {
356:                     char[] triple = new char[commaIndex];
357:                     sb.CopyTo(0, triple, 0, commaIndex);
358:                     sb.Remove(0, commaIndex);
359: 
360:                     String integralPart = new String(triple);
361:                     result = this.AddDecimalSeparators(new StringBuilder(integralPart)) + sb.ToString();
362:                 }
363:                 else
364:                 {
365:                     sb.Remove(0, 1);  // remove '-' sign
366:                     commaIndex--;     // comma index includes '-' sign
367: 
368:                     char[] triple = new char[commaIndex];
369:                     sb.CopyTo(0, triple, 0, commaIndex);
370:                     sb.Remove(0, commaIndex);
371: 
372:                     String integralPart = new String(triple);
373:                     result = '-' + this.AddDecimalSeparators(new StringBuilder(integralPart)) + sb.ToString();
374:                 }
375:             }
376: 
377:             return result;
378:         }
379: 
380:         private String AddDecimalSeparators(StringBuilder sb)
381:         {
382:             String result = "";
383:             while (sb.Length > 3)
384:             {
385:                 char[] triple = new char[3];
386:                 sb.CopyTo(sb.Length - 3, triple, 0, 3);
387:                 sb.Remove(sb.Length - 3, 3);
388:                 result = "." + new String(triple) + result;
389:             }
390: 
391:             result = sb.ToString() + result;
392:             return result;
393:         }
394: 
395:         private double ParseInputAsDouble (String s)
396:         {
397:             double d = 0.0;
398:             try
399:             {
400:                 d = Double.Parse(s);
401:             }
402:             catch (FormatException)
403:             {
404:                 String msg = String.Format("FormatException: {0}", s);
405:                 Debug.WriteLineIf(this.debugScanner, msg);
406:             }
407:             catch (OverflowException)
408:             {
409:                 String msg = String.Format("OverflowException: {0}", s);
410:                 Debug.WriteLineIf(this.debugScanner, msg);
411:             }
412: 
413:             return d;
414:         }
415: 
416:         // private trace helper method
417:         private void DumpInputLine()
418:         {
419:             String msg = String.Format("INPUT: {0}", this.currentInput.ToString());
420:             Debug.WriteLineIf(this.debugScanner, msg);
421:         }
422:     }
423: }

Beispiel 4. Klasse TokenScanner: Modell der Anwendung.


Bei der Verarbeitung von Kommazahlen stellt sich die Frage nach der Globalisierung bzw. Lokalisierung der Anwendung. Um die Realisierung möglichst einfach zu halten, habe ich folgende Randbedingungen an den Entwurf gestellt:

  • Die Anwendung ist nicht lokalisierbar.

  • Unabhängig von den Lokalisierungs-Einstellungen des zu Grunde liegenden Rechners sind der Vor- und Nachkommaanteil einer Gleitpunktzahl immer durch ein Komma ',' voneinander zu trennen.

  • Um große Zahlen leichter lesen zu können, kommt eine Zifferngruppierung mit Tausendertrennzeichen Punkt '.'zum Einsatz.

Das Beispiel der Taschenrechneranwendung lässt sich leicht automatisiert testen. Nachfolgend folgt exemplarisch eine Routine, die einen automatisierten Test des ViewModels demonstriert:

01: public static void Test_PowerByTwo()
02: {
03:     CalculatorViewModel vm = new CalculatorViewModel();
04: 
05:     // calculation with calculator
06:     vm.DigitCommand.Execute("2");
07:     vm.OperatorCommand.Execute("*");
08:     vm.DigitCommand.Execute("2");
09: 
10:     // calculation with program
11:     long n1 = 2;
12: 
13:     for (int i = 0; i < 48; i++)
14:     {
15:         vm.EqualCommand.Execute(null);
16:         String result = vm.DisplayInput;
17:         Console.WriteLine("RESULT ==> {0}", result);
18: 
19:         // verify result
20:         String stripped = result.Replace(".", "");
21:         long n2 = Int64.Parse(stripped);
22: 
23:         n1 = 2 * n1;
24:         if (n1 != n2)
25:             Console.WriteLine("ERROR: {0} differs from {1} !!!!", n1, n2);
26:     }
27: }

Es wird in dem Testbeispiel eine Instanz der Klasse CalculatorViewModel erzeugt. Die unterschiedlichen Kommandos eines ViewModels lassen sich auch imperativ mit Hilfe der Execute-Methode aufrufen. Diese Methode erwartet einen Parameter des Typs Object, der entsprechend des Kommandos bereit zu stellen ist. Ein Vergleich der arithmetischen Routinen des Taschenrechners mit denen der C#-Laufzeitumgebung verifiziert die Resultate des Taschenrechners, hier exemplarisch am Beispiel von Zweier-Potenzen demonstriert:

RESULT ==> 4
RESULT ==> 8
RESULT ==> 16
RESULT ==> 32
RESULT ==> 64
RESULT ==> 128
RESULT ==> 256
RESULT ==> 512
RESULT ==> 1.024
RESULT ==> 2.048
RESULT ==> 4.096
RESULT ==> 8.192
RESULT ==> 16.384
RESULT ==> 32.768
RESULT ==> 65.536
RESULT ==> 131.072
RESULT ==> 262.144
RESULT ==> 524.288
RESULT ==> 1.048.576
RESULT ==> 2.097.152
RESULT ==> 4.194.304
RESULT ==> 8.388.608
RESULT ==> 16.777.216
RESULT ==> 33.554.432
RESULT ==> 67.108.864
RESULT ==> 134.217.728
RESULT ==> 268.435.456
RESULT ==> 536.870.912
RESULT ==> 1.073.741.824
RESULT ==> 2.147.483.648
RESULT ==> 4.294.967.296
RESULT ==> 8.589.934.592
RESULT ==> 17.179.869.184
RESULT ==> 34.359.738.368
RESULT ==> 68.719.476.736
RESULT ==> 137.438.953.472
RESULT ==> 274.877.906.944
RESULT ==> 549.755.813.888
RESULT ==> 1.099.511.627.776
RESULT ==> 2.199.023.255.552
RESULT ==> 4.398.046.511.104
RESULT ==> 8.796.093.022.208
RESULT ==> 17.592.186.044.416
RESULT ==> 35.184.372.088.832
RESULT ==> 70.368.744.177.664
RESULT ==> 140.737.488.355.328
RESULT ==> 281.474.976.710.656
RESULT ==> 562.949.953.421.312