Eine Bundesligatabelle und LINQ

1. Aufgabe

Erstellen Sie ein Programm, das für eine populäre Sportart (Fußball, Hallenhandball, etc.) die Daten der Spiele verwaltet und eine aktuelle Tabelle berechnet. Die beteiligten Mannschaften sind durch eine Klasse Team zu beschreiben. Informationen wie der Vereinsname und die Stadt des Vereins gehören zu den Daten dieser Klasse. In einer weiteren Klasse Game wird ein Spiel beschrieben. Dazu benötigt man die Namen der beiden beteiligten Mannschaften, den Endstand des Spiels und den Spieltag, an dem das Spiel ausgetragen wurde.

Bei der Erstellung der Tabelle sollte man sich an bekannten Ligasystemen orientieren. Die Mannschaften werden hier sortiert nach ihrem Punktestand aufgelistet. Zur besseren Lesbarkeit enthält eine Tabelle auch statistische Informationen wie zum Beispiel die Anzahl der absolvierten Spiele. Beachten Sie dabei: Nicht alle Mannschaften eines Ligabetriebs müssen zu einem bestimmten Zeitpunkt dieselbe Anzahl von Spielen absolviert haben. Weitere statistische Informationen in der Tabelle könnten pro Mannschaft sein: Die Anzahl der Siege, Unentschieden und Niederlagen, der erzielten Tore sowie der Gegentore und das Torverhältnis (Differenz von Toren und Gegentoren).

Interessant wird das Erstellen einer Tabelle für Mannschaften, die dieselbe Anzahl von Punkten aufweisen. In diesem Fall sollte man das offizielle Regelwerk anwenden, das in den meisten Ligasystemen Anwendung findet. Abgesehen von der Anzahl der Punkte, die bei einem siegreichem Spiel für eine Mannschaft vergeben werden (zum Beispiel Fußball: 3 Punkte, Hallenhandball: 2 Punkte) lautet dieses: Nach jeder Partie erhält die siegreiche Mannschaft drei (zwei) Punkte und die besiegte keinen Punkt, bei einem Unentschieden jede Mannschaft einen Punkt. Die erreichten Punkte einer Spielzeit werden addiert und ergeben so für jeden Spieltag eine aktuelle Rangliste der Vereine. Bei Punktgleichheit entscheidet die bessere Tordifferenz über die Reihenfolge der Platzierung, bei gleicher Differenz die Anzahl der erzielten Tore. Sollten danach zwei Mannschaften immer noch gleich platziert sein, entscheidet das Gesamtergebnis aus den Partien gegeneinander, wobei die auswärts erzielten Tore stärker zählen. Falls auch die erzielten Auswärtstore in allen Spielen gleich sind, wird auf neutralem Platz ein Entscheidungsspiel ausgetragen (dies war bisher in der Fußballbundesliga jedoch noch nie der Fall).

Als Konsolenanwendung sollte das Programm mit einem so genannten Console.WriteLine-Menü mit dem Anwender kommunizieren. Folgender Dialog diene als Richtschnur:

[1]      Mannschaften ausgeben
[2]      Ergebnis eingeben
[3]      Ergebnisse ausgeben
[4]      Ergebnisse eines Spieltags ausgeben
[5]      Tabelle ausgeben
[6]      Tabelle bis zu einem Spieltag ausgeben
[0]      Programm verlassen

Eingabe:

Wurden in der Liga schon Spiele absolviert, könnte die Ausgabe einer berechneten Tabelle so aussehen:

Mannschaft                    Spiele   S U N  Tore  Diff. Punkte
================================================================
 1: Borussia Dortmund              3   3 0 0   7: 1    6       9
 2: Bayer Leverkusen               3   3 0 0   8: 3    5       9
 3: FC Bayern MÜnchen              3   3 0 0   6: 1    5       9
 4: 1. FSV Mainz                   3   3 0 0   7: 3    4       9
 5: Hertha BSC                     3   2 1 0   9: 3    6       7
 6: SV Werder Bremen               3   2 0 1   2: 1    1       6
 7: Hannover 96                    3   2 0 1   4: 4    0       6
 8: 1899 Hoffenheim                3   1 2 0  10: 6    4       5
 9: VfL Wolfsburg                  3   1 0 2   4: 4    0       3
10: Borussia Mönchengladbach       3   1 0 2   6: 7   -1       3
11: Eintracht Frankfurt            3   1 0 2   3: 7   -4       3
12: FC Augsburg                    3   1 0 2   2: 6   -4       3
13: 1. FC Nürnberg                 3   0 2 1   4: 6   -2       2
14: SC Freiburg                    3   0 1 2   5: 8   -3       1
15: Hamburger SV                   3   0 1 2   4: 9   -5       1
16: FC Schalke 04                  3   0 1 2   4: 9   -5       1
17: VfB Stuttgart                  3   0 0 3   3: 6   -3       0
18: Eintracht Braunschweig         3   0 0 3   1: 5   -4       0

Um ein absolviertes Spiel in den Datenbestand der Anwendung einzugeben, ist das Console.WriteLine-Menü etwas zu ergänzen. Die beteiligten Mannschaften sollten dabei nicht mit ihrem Namen eingegeben werden (das wäre zu umständlich und vor allem auch viel zu fehleranfällig), sondern zum Beispiel mit ihrem Index, mit dem sie intern in einem Feld aller Team-Objekte abgelegt sind. In meinem Programm läuft die Eingabe eines Spielergebnisses wie folgt ab:

[1]      Mannschaften ausgeben
[2]      Ergebnis eingeben
[3]      Ergebnisse ausgeben
[4]      Ergebnisse eines Spieltags ausgeben
[5]      Tabelle ausgeben
[6]      Tabelle bis zu einem Spieltag ausgeben
[0]      Programm verlassen

Eingabe: 2

 0: Hertha BSC (Berlin)
 1: Borussia Dortmund (Dortmund)
 2: Bayer Leverkusen (Leverkusen)
 3: FC Bayern München (München)
 4: Hannover 96 (Hannover)
 5: 1. FSV Mainz (Mainz)
 6: SV Werder Bremen (Bremen)
 7: Hamburger SV (Hamburg)
 8: FC Schalke 04 (Gelsenkirchen)
 9: 1. FC Nürnberg (Nürnberg)
10: 1899 Hoffenheim (Hoffenheim)
11: VfB Stuttgart (Stuttgart)
12: Eintracht Braunschweig (Braunschweig)
13: SC Freiburg (Freiburg)
14: Borussia Mönchengladbach (Mönchengladbach)
15: VfL Wolfsburg (Wolfsburg)
16: FC Augsburg (Augsburg)
17: Eintracht Frankfurt (Frankfurt)

Wählen Sie die Heimmannschaft aus: 7

 0: Hertha BSC (Berlin)
 1: Borussia Dortmund (Dortmund)
 2: Bayer Leverkusen (Leverkusen)
 3: FC Bayern München (München)
 4: Hannover 96 (Hannover)
 5: 1. FSV Mainz (Mainz)
 6: SV Werder Bremen (Bremen)
 7: Hamburger SV (Hamburg)
 8: FC Schalke 04 (Gelsenkirchen)
 9: 1. FC Nürnberg (Nürnberg)
10: 1899 Hoffenheim (Hoffenheim)
11: VfB Stuttgart (Stuttgart)
12: Eintracht Braunschweig (Braunschweig)
13: SC Freiburg (Freiburg)
14: Borussia Mönchengladbach (Mönchengladbach)
15: VfL Wolfsburg (Wolfsburg)
16: FC Augsburg (Augsburg)
17: Eintracht Frankfurt (Frankfurt)

Wählen Sie die Auswärtsmannschaft aus: 9

Wählen Sie den Spieltag aus: 4
Anzahl geschossene Tore Heimmannschaft: 1
Anzahl geschossene Tore Auswärtsmannschaft: 4

[1]      Mannschaften ausgeben
[2]      Ergebnis eingeben
[3]      Ergebnisse ausgeben
[4]      Ergebnisse eines Spieltags ausgeben
[5]      Tabelle ausgeben
[6]      Tabelle bis zu einem Spieltag ausgeben
[0]      Programm verlassen

Eingabe: 4
Wählen Sie einen Spieltag aus: 4

4. Spieltag:
1: Hamburger SV              : 1. FC Nürnberg              1: 4

Die vorgestellten Dialoge dienen nur dazu, einen Eindruck zu vermitteln, wie interaktiv eine Sportliga-Anwendung mit Leben erfüllt werden kann. Beim Entwurf Ihrer Anwendung sollte Ihre eigene Kreativität im Vordergrund stehen.

2. Lösung

Die Überschrift der Aufgabe hat es bereits angedeutet: Die programmiersprachlichen Konstrukte des LINQ-Umfelds (Language Integrated Query) sollen bei einer Implementierung zum Einsatz kommen. Aus diesem Grund konzipieren wir im Folgenden nur die beiden bereits angesprochenen Klassen Team und Game. Für die Berechnung der Tabelle kommen anonyme Klassen als Resultat einer LINQ-Abfrage ins Spiel. All dies ordnen wir einer Klasse League zu, die den Rahmen für die Anwendung bereitstellt.

Mit Hilfe automatisch implementierter Eigenschaften lassen sich die beiden Klassen Team und Game recht kompakt implementierten (Listing 1 und Listing 2):

01: class Team
02: {
03:     // auto-implemented properties
04:     public int Id { get; set; } 
05:     public String Name { get; set; } 
06:     public String City { get; set; }
07: 
08:     // overrides
09:     public override string ToString()
10:     {
11:         return String.Format("{0} ({1})", this.Name, this.City);
12:     }
13: }

Beispiel 1. Klasse Team.


01: class Game
02: {
03:     public int MatchDay  { get; set; }  // Spieltag
04:     public int TeamHome  { get; set; }  // Heimmannschaft
05:     public int TeamAway  { get; set; }  // Auswärtsmannschaft
06:     public int GoalsHome { get; set; }  // Tore Heimmannschaft
07:     public int GoalsAway { get; set; }  // Tore Auswärtsmannschaft
08: 
09:     public Game(int matchDay, int teamHome, int teamAway,
10:         int goalsHome, int goalsAway)
11:     {
12:         this.MatchDay = matchDay;
13:         this.TeamHome = teamHome;
14:         this.TeamAway = teamAway;
15:         this.GoalsHome = goalsHome;
16:         this.GoalsAway = goalsAway;
17:     }
18: }

Beispiel 2. Klasse Game.


Um nicht mühselig nach jedem Start des Programms die Mannschaften und Spiele von der Konsole eintippen zu müssen, werden die ersten drei Spieltage der Fußball-Bundesliga Saison 2013/14 – die wir exemplarisch hier betrachten – durch zwei Hilfsmethoden InitTeams_2013_14 und InitGames_2013_14 in den Datenstrukturen der Anwendung vorbelegt. An dieser Stelle wäre natürlich eine andere Softwarearchitektur gefragt, zum Beispiel eine Persistenz der Daten mit Hilfe eines Datenbanksystems. Bei der vorgestellten Lösung soll dieser Ansatz nicht im Vordergrund stehen. Wir legen deswegen zur Vereinfachung der Programmieraufgabe die notwendigen Daten in zwei Feldern bzw. Containerklassen des Basistyps Team bzw. Game ab:

private Team[] teams;
private List<Game> games;

Das Befüllen dieser Felder kann nun auf diese Weise erfolgen:

private void InitTeams_2013_14()
{
    this.teams = new Team[MaxTeams];

    this.teams[0] = new Team() { Id = 0, Name = "Hertha BSC", City = "Berlin" };
    this.teams[1] = new Team() { Id = 1, Name = "Borussia Dortmund", City = "Dortmund" };
    this.teams[2] = new Team() { Id = 2, Name = "Bayer Leverkusen", City = "Leverkusen" };
    this.teams[3] = new Team() { Id = 3, Name = "FC Bayern München", City = "München" };
    this.teams[4] = new Team() { Id = 4, Name = "Hannover 96", City = "Hannover" };
    this.teams[5] = new Team() { Id = 5, Name = "1. FSV Mainz", City = "Mainz" };
    this.teams[6] = new Team() { Id = 6, Name = "SV Werder Bremen", City = "Bremen" };
    this.teams[7] = new Team() { Id = 7, Name = "Hamburger SV", City = "Hamburg" };
    this.teams[8] = new Team() { Id = 8, Name = "FC Schalke 04", City = "Gelsenkirchen" };
    this.teams[9] = new Team() { Id = 9, Name = "1. FC Nürnberg", City = "Nürnberg" };
    this.teams[10] = new Team() { Id = 10, Name = "1899 Hoffenheim", City = "Hoffenheim" };
    this.teams[11] = new Team() { Id = 11, Name = "VfB Stuttgart", City = "Stuttgart" };
    this.teams[12] = new Team() { Id = 12, Name = "Eintracht Braunschweig", City =  "Braunschweig" };
    this.teams[13] = new Team() { Id = 13, Name = "SC Freiburg", City = "Freiburg" };
    this.teams[14] = new Team() { Id = 14, Name = "Borussia Mönchengladbach", City =  "Mönchengladbach" };
    this.teams[15] = new Team() { Id = 15, Name = "VfL Wolfsburg", City = "Wolfsburg" };
    this.teams[16] = new Team() { Id = 16, Name = "FC Augsburg", City = "Augsburg" };
    this.teams[17] = new Team() { Id = 17, Name = "Eintracht Frankfurt", City = "Frankfurt" };
}

private void InitGames_2013_14()
{
    this.InitGamesMatchDay_01_2013_14();
    this.InitGamesMatchDay_02_2013_14();
    this.InitGamesMatchDay_03_2013_14();
}

private void InitGamesMatchDay_01_2013_14()
{
    // 1. matchday
    this.games.Add(new Game(1, 3, 14, 3, 1));  // "Bayern München : Borussia M'gladbach 3:1"
    this.games.Add(new Game(1, 10, 9, 2, 2));  // "1899 Hoffenheim : 1. FC Nürnberg 2:2"
    this.games.Add(new Game(1, 2, 13, 3, 1));  // "Bayer Leverkusen : SC Freiburg 3:1"
    this.games.Add(new Game(1, 4, 15, 2, 0));  // "Hannover 96 : VfL Wolfsburg 2:0"
    this.games.Add(new Game(1, 16, 1, 0, 4));  // "FC Augsburg : Borussia Dortmund 0:4"
    this.games.Add(new Game(1, 0, 17, 6, 1));  // "Hertha BSC : Eintracht Frankfurt 6:1"
    this.games.Add(new Game(1, 12, 6, 0, 1));  // "Eintr. Braunschweig : Werder Bremen 0:1"
    this.games.Add(new Game(1, 5, 11, 3, 2));  // "Mainz 05 : VfB Stuttgart 3:2"
    this.games.Add(new Game(1, 8, 7, 3, 3));   // "Schalke 04 : Hamburger SV 3:3"
}

private void InitGamesMatchDay_02_2013_14()
{
    // 2. matchday
    this.games.Add(new Game(2, 11, 2, 0, 1));  // "VfB Stuttgart : Bayer Leverkusen 0:1"
    this.games.Add(new Game(2, 15, 8, 4, 0));  // "VfL Wolfsburg : Schalke 04 4:0 "
    this.games.Add(new Game(2, 6, 16, 1, 0));  // "Werder Bremen : FC Augsburg 1:0"
    this.games.Add(new Game(2, 13, 5, 1, 2));  // "SC Freiburg : Mainz 05 1:2"
    this.games.Add(new Game(2, 7, 10, 1, 5));  // "Hamburger SV : 1899 Hoffenheim 1:5"
    this.games.Add(new Game(2, 17, 3, 0, 1));  // "Eintracht Frankfurt : Bayern München 0:1"
    this.games.Add(new Game(2, 14, 4, 3, 0));  // "Borussia M'gladbach : Hannover 96 3:0"
    this.games.Add(new Game(2, 9, 0, 2, 2));   // "1. FC Nürnberg : Hertha BSC 2:2"
    this.games.Add(new Game(2, 1, 12, 2, 1));  // "Borussia Dortmund : Eintr. Braunschweig 2:1"
}

private void InitGamesMatchDay_03_2013_14()
{
    // 3. matchday
    this.games.Add(new Game(3, 1, 6, 1, 0));   // "Borussia Dortmund : Werder Bremen 1:0"
    this.games.Add(new Game(3, 3, 9, 2, 0));   // "Bayern München : 1. FC Nürnberg 2:0"
    this.games.Add(new Game(3, 2, 14, 4, 2));  // "Bayer Leverkusen : Borussia M'gladbach 4:2"
    this.games.Add(new Game(3, 4, 8, 2, 1));   // "Hannover 96 : Schalke 04 2:1"
    this.games.Add(new Game(3, 10, 13, 3, 3)); // "1899 Hoffenheim : SC Freiburg 3:3"
    this.games.Add(new Game(3, 5, 15, 2, 0));  // "Mainz 05 : VfL Wolfsburg 2:0"
    this.games.Add(new Game(3, 0, 7, 1, 0));   // "Hertha BSC : Hamburger SV 1:0"
    this.games.Add(new Game(3, 12, 17, 0, 2)); // "Eintr. Braunschweig : Eintracht Frankfurt 0:2"
    this.games.Add(new Game(3, 16, 11, 2, 1)); // "FC Augsburg : VfB Stuttgart 2:1"
}

Kommen wir auf die Berechnung der Tabelle zu sprechen. LINQ bietet hierzu die Möglichkeit an, mit Hilfe von Abfrageausdrücken aus der Menge aller Spielergebnisse und der beteiligten Mannschaften eine Anordnung der Mannschaften zu generieren, die dem Tabellenstand entspricht. Charakteristisch für LINQ-Abfragen ist, dass das Ergebnis durch einen neuen Objekttyp dargestellt werden kann.

In der konkret vorliegenden Aufgabe transformieren wir die Menge aller Mannschaften in eine neue Ergebnismenge, die die Mannschaften um zusätzliche Daten wie Anzahl der Siege, Anzahl der Niederlagen etc. ergänzt und die Ergebnismenge an Hand einiger Sortierkriterien in einer bestimmten Reihenfolge auflistet. Dazu müssen wir ein Resultat eines Spiels, also ein Game-Objekt, noch einmal gesondert betrachten. Wenn wir beispielsweise das Ergebnis Borussia Dortmund : Werder Bremen 1:0 ansehen, finden wir dort zweierlei Informationen vor: Zum einen hat die Mannschaft Borussia Dortmund mit 1:0 ein Spiel gewonnen, zum anderen hat die Mannschaft Werder Bremen ein Spiel mit 0:1 verloren. Wir zerlegen quasi ein Spielergebnis in zwei Datensätze für jede der beteiligten Mannschaften und können auf diese Weise anschließend recht einfach aus allen resultierenden Datensätzen eine Ergebnismenge (Tabelle) zum aktuellen Spieltag oder zu allen Spieltagen mit einem Abfrageausdruck formulieren. In Listing 3 finden Sie in Klasse League die Details dazu vor:

001: class League
002: {
003:     private const int MaxTeams = 18;
004:     private const int MaxMatchDays = ((MaxTeams - 1) * 2);
005: 
006:     private const int GamesPerMatchDay = (MaxTeams / 2);
007:     private const int GamesPerSeason = (GamesPerMatchDay * (MaxTeams - 1) * 2); 
008: 
009:     private Team[] teams;
010:     private List<Game> games;
011: 
012:     public League () 
013:     {
014:         this.games = new List<Game>();
015:         this.InitLeague_2013_14();
016:     }
017: 
018:     private void InitLeague_2013_14()
019:     {
020:         this.InitTeams_2013_14();
021:         this.InitGames_2013_14();
022:     }
023: 
024:     private void InitTeams_2013_14()
025:     {
026:         // see code fragment above
027:     }
028: 
029:     private void InitGames_2013_14()
030:     {
031:         // see code fragment above
032:     }
033: 
034:     private void InitGamesMatchDay_01_2013_14()
035:     {
036:         // see code fragment above
037:     }
038: 
039:     private void InitGamesMatchDay_02_2013_14()
040:     {
041:         // see code fragment above
042:     }
043: 
044:     private void InitGamesMatchDay_03_2013_14()
045:     {
046:         // see code fragment above
047:     }
048: 
049:     private void PrintTeams ()
050:     {
051:         for (int i = 0; i < this.teams.Length; i++)
052:         {
053:             Console.Write ("{0,2}: ", i);
054:             Console.WriteLine (this.teams[i].ToString());
055:         }
056:         Console.WriteLine (Environment.NewLine);
057:     }
058: 
059:     private void EnterGame()
060:     {
061:         int teamHome;               // Heimmannschaft
062:         int teamAway;               // Auswärtsmannschaft
063:         int numberGoalsHomeTeam;    // Tore Heimmannschaft
064:         int numberGoalsAwayTeam ;   // Tore Auswärtsmannschaft
065:         int matchDay;               // Spieltag
066: 
067:         Console.WriteLine();
068:         this.PrintTeams();
069:         Console.Write("Wählen Sie die Heimmannschaft aus: ");
070:         String input = Console.ReadLine();
071:         Int32.TryParse(input, out teamHome);
072: 
073:         Console.WriteLine(); 
074:         this.PrintTeams();
075:         Console.Write("Wählen Sie die Auswärtsmannschaft aus: ");
076:         input = Console.ReadLine();
077:         Int32.TryParse(input, out teamAway);
078: 
079:         Console.WriteLine(); 
080:         Console.Write("Wählen Sie den Spieltag aus: ");
081:         input = Console.ReadLine();
082:         Int32.TryParse(input, out matchDay);
083: 
084:         Console.Write("Anzahl geschossene Tore Heimmannschaft: ");
085:         input = Console.ReadLine();
086:         Int32.TryParse(input, out numberGoalsHomeTeam);
087: 
088:         Console.Write("Anzahl geschossene Tore Auswärtsmannschaft: ");
089:         input = Console.ReadLine();
090:         Int32.TryParse(input, out numberGoalsAwayTeam);
091: 
092:         this.games.Add(new Game(matchDay, teamHome, teamAway,
093:             numberGoalsHomeTeam, numberGoalsAwayTeam));
094:         Console.WriteLine(); 
095:     }
096: 
097:     private void PrintGames()
098:     {
099:         var query =
100:             from g in this.games
101:             join t1 in this.teams on g.TeamHome equals t1.Id
102:             join t2 in this.teams on g.TeamHome equals t2.Id
103:             select new
104:             {
105:                 HomeTeam = t1.Name,
106:                 AwayTeam = t2.Name,
107:                 NumberGoalsHome = g.GoalsHome,
108:                 NumberGoalsAway = g.GoalsAway,
109:                 g.MatchDay
110:             } into entry
111:             orderby entry.MatchDay
112:             select entry;
113: 
114:         int index = 0;
115:         foreach (var q in query)
116:         {
117:             index++;
118:             PrintGame(index, q.HomeTeam, q.AwayTeam, q.NumberGoalsHome, q.NumberGoalsAway);
119:         }
120:     }
121: 
122:     private void PrintGamesOfMatchDay()
123:     {
124:         int matchDay;
125: 
126:         Console.Write("Wählen Sie einen Spieltag aus: ");
127:         String input = Console.ReadLine();
128:         Int32.TryParse(input, out matchDay);
129:         Console.WriteLine(Environment.NewLine);
130: 
131:         Console.WriteLine("{0}. Spieltag: ", matchDay);
132:         Console.WriteLine();
133: 
134:         var selectedGames =
135:             from g in this.games
136:             where g.MatchDay == matchDay
137:             select new
138:             {
139:                 TeamHomeId = g.TeamHome,
140:                 TeamAwayId = g.TeamAway,
141:                 GoalsHome = g.GoalsHome,
142:                 GoalsAway = g.GoalsAway
143:             };
144: 
145:         int index = 0;
146:         foreach (var g in selectedGames)
147:         {
148:             index ++;
149:             String teamHome = this.teams[g.TeamHomeId].Name;
150:             String teamAway = this.teams[g.TeamAwayId].Name;
151:             this.PrintGame(index, teamHome, teamAway, g.GoalsHome, g.GoalsAway);
152:         }
153:         Console.WriteLine();
154:     }
155: 
156:     private void PrintTableUntilMatchday()
157:     {
158:         int matchDay = 0;
159: 
160:         Console.Write("Eingabe Spieltag: ");
161:         String input = Console.ReadLine();
162:         Int32.TryParse(input, out matchDay);
163:         Console.WriteLine();
164:         this.PrintTable(matchDay);
165:     }
166: 
167:     private void PrintGame (
168:         int index, String teamHome, String teamAway, int goalsHome, int goalsAway)
169:     {
170:         Console.WriteLine("{0,2}: {1,-26}: {2,-26} {3,2}:{4,2}",
171:             index, teamHome, teamAway, goalsHome, goalsAway);
172:     }
173: 
174:     private class TableEntry
175:     {
176:         public int TeamId { get; set; }
177:         public int Points { get; set; }
178:         public int NumberInStock { get; set; }
179:         public int GamesPlayed { get; set; }
180:         public int Wins { get; set; }
181:         public int Draws { get; set; }
182:         public int Losses { get; set; }
183:         public int GoalsFor { get; set; }
184:         public int GoalsAgainst { get; set; }
185:         public int GoalsDifference { get; set; }
186:     }
187: 
188:     private void PrintTable()
189:     {
190:         this.PrintTable (MaxMatchDays);
191:     }
192: 
193:     private void PrintTable(int matchDay)
194:     {
195:         var playedGames =
196:         (
197:             from g in this.games
198:             where g.MatchDay <= matchDay
199:             select new
200:             {
201:                 Team = g.TeamHome,
202:                 GoalsSelf = g.GoalsHome,
203:                 GoalsOther = g.GoalsAway
204:             }
205:         )
206:         .Concat
207:         (
208:             from g in this.games
209:             where g.MatchDay <= matchDay
210:             select new
211:             {
212:                 Team = g.TeamAway,
213:                 GoalsSelf = g.GoalsAway,
214:                 GoalsOther = g.GoalsHome
215:             }
216:         );
217: 
218:         var table =
219:             from result in playedGames
220:             group result by result.Team into grouping
221:             select new TableEntry()
222:             {
223:                 TeamId = grouping.Key,
224:                 Points = grouping.Sum(game => (game.GoalsSelf > game.GoalsOther) ? 3
225:                     : (game.GoalsSelf == game.GoalsOther) ? 1 : 0),
226:                 GamesPlayed = grouping.Count(),
227:                 Wins = grouping.Sum(game => (game.GoalsSelf > game.GoalsOther) ? 1 : 0),
228:                 Draws = grouping.Sum(game => (game.GoalsSelf < game.GoalsOther) ? 1 : 0),
229:                 Losses = grouping.Sum(game => (game.GoalsSelf == game.GoalsOther) ? 1 : 0),
230:                 GoalsFor = grouping.Sum(game => game.GoalsSelf),
231:                 GoalsAgainst = grouping.Sum(game => game.GoalsOther),
232:                 GoalsDifference =
233:                     grouping.Sum(game => game.GoalsSelf) - grouping.Sum(game => game.GoalsOther)
234:             }
235:             into entry
236:             orderby
237:                 entry.Points descending,
238:                 entry.GoalsDifference descending,
239:                 entry.GoalsFor descending
240:             select entry;
241: 
242:         // output
243:         Console.WriteLine();
244:         this.PrintTableToConsole(table);
245:         Console.WriteLine(Environment.NewLine);
246:     }
247: 
248:     private void PrintTableToConsole (IOrderedEnumerable<TableEntry> table)
249:     {
250:         Console.WriteLine("Mannschaft                Spiele    S U N  Tore Diff. Punkte");
251:         Console.WriteLine("============================================================");
252: 
253:         int index = 0;
254:         foreach (var q in table)
255:         {
256:             index++;
257:             String team = this.teams[q.TeamId].Name;
258:             Console.WriteLine("{0,2}: {1,-26} {2}    {3} {4} {5} {6,2}:{7,2}  {8,3}       {9}",
259:                 index, team, q.GamesPlayed, q.Wins, q.Draws, q.Losses,
260:                 q.GoalsFor, q.GoalsAgainst, q.GoalsDifference, q.Points);
261:         }
262:     }
263: 
264:     public void Menu()
265:     {
266:         int choice = 1;
267: 
268:         while (choice != 0)
269:         {
270:             // print menue
271:             Console.WriteLine("[1]      Mannschaften ausgeben");
272:             Console.WriteLine("[2]      Ergebnis eingeben");
273:             Console.WriteLine("[3]      Ergebnisse ausgeben");
274:             Console.WriteLine("[4]      Ergebnisse eines Spieltags ausgeben");
275:             Console.WriteLine("[5]      Tabelle ausgeben");
276:             Console.WriteLine("[6]      Tabelle bis zu einem Spieltag ausgeben");
277:             Console.WriteLine("[0]      Programm verlassen");
278:             Console.WriteLine(Environment.NewLine);
279: 
280:             Console.Write("Eingabe: ");
281:             String input = Console.ReadLine();
282:             Int32.TryParse(input, out choice);
283: 
284:             switch (choice)
285:             {
286:                 case 1:
287:                     this.PrintTeams();
288:                     break;
289: 
290:                 case 2:
291:                     this.EnterGame();
292:                     break;
293: 
294:                 case 3:
295:                     this.PrintGames();
296:                     break;
297: 
298:                 case 4:
299:                     this.PrintGamesOfMatchDay();
300:                     break;
301: 
302:                 case 5:
303:                     this.PrintTable();
304:                     break;
305: 
306:                 case 6:
307:                     this.PrintTableUntilMatchday();
308:                     break;
309: 
310:                 case 0:
311:                     Console.WriteLine("Auf Wiedersehen!");
312:                     break;
313:             }
314:         }
315:     }
316: }

Beispiel 3. Klasse League.


Kernstück der Klasse League aus Listing 3 ist die Methode PrintTable in den Zeilen 193 bis 246. Zu allen vorliegenden Spielergebnissen wird mit Hilfe der LINQ-Concat-Methode eine temporäre Datenmenge generiert, die die Spielergebnisse aus Sicht der jeweils beteiligten Mannschaften bildet, also mehr oder minder die Anzahl der Game-Objekte in einer neuen Ergebnismenge verdoppelt. Anschließend wird diese Datenmenge mit der group-Operation zunächst bezüglich der Mannschaften gruppiert, dann mit der orderby-Operation an Hand einiger Aggregatoperatoren (wie Sum und Count) sortiert, um die Anzahl der Punkte oder die Zahl der Spiele darstellen zu können. Gibt man die auf diese Weise erzeugte Ergebnismenge mittels einer foreach-Schleife auf der Konsole aus, kann man sich von der Erreichung des gesteckten Ziels überzeugen.

Noch ein Hinweis zu Zeile 221: Das Resultat einer group-Operation ließe sich ohne weiteres in einer Ergebnismenge anonymen Typs ablegen. Dies schließt aber aus, dass man im selben LINQ-Abfrageausdruck kaskadierte Abfragen wie beispielweise eine orderby-Operation unmittelbar anschließen. Zu diesem Zweck gibt es in den Zeilen 174 bis 186 einen Klassentyp TableEntry, mit dessen Hilfe die Abfrageergebnisse einem konkreten Datentyp zugewiesen werden können.

Die PrintTable-Methode gibt es in zwei Überladungen. Zum einen, um eine Tabelle zu allen Spielergebnissen generieren zu können oder zum anderen eben nur bis zu einem bestimmten Spieltag. Sollten Sie zu den LINQ-Puristen zählen und darauf bestehen, die beiden LINQ-Abfragen in der PrintTable-Methode zu einer zusammenzufassen, dann wäre dies prinzipiell auch möglich. Meines Erachtens ist dieser Abfrageausdruck eher zu lang und damit auch unübersichtlich, aber ich will Ihnen diese Möglichkeit nicht vorenthalten:

var table =
    from result in
    (
        (
            from g in this.games
            select new
            {
                Team = g.TeamHome,
                GoalsSelf = g.GoalsHome,
                GoalsOther = g.GoalsAway
            }
        )
        .Concat
        (
            from g in this.games
            select new
            {
                Team = g.TeamAway,
                GoalsSelf = g.GoalsAway,
                GoalsOther = g.GoalsHome
            }
        )
    )
    group result by result.Team into grouping
    select new TableEntry()
    {
        TeamId = grouping.Key,
        Points = grouping.Sum(game => (game.GoalsSelf > game.GoalsOther) ? 3
            : (game.GoalsSelf == game.GoalsOther) ? 1 : 0),
        GamesPlayed = grouping.Count(),
        Wins = grouping.Sum(game => (game.GoalsSelf > game.GoalsOther) ? 1 : 0),
        Draws = grouping.Sum(game => (game.GoalsSelf < game.GoalsOther) ? 1 : 0),
        Losses = grouping.Sum(game => (game.GoalsSelf == game.GoalsOther) ? 1 : 0),
        GoalsFor = grouping.Sum(game => game.GoalsSelf),
        GoalsAgainst = grouping.Sum(game => game.GoalsOther),
        GoalsDifference =
            grouping.Sum(game => game.GoalsSelf) - grouping.Sum(game => game.GoalsOther)
    }
    into entry
    orderby
        entry.Points descending, 
        entry.GoalsDifference descending,
        entry.GoalsFor descending
    select entry;