From 7dee9b7346d7935024da40def9baf77700601cb3 Mon Sep 17 00:00:00 2001 From: axunonb Date: Fri, 15 Sep 2023 14:39:04 +0200 Subject: [PATCH 1/4] Merge comparers into one class RankComparer * Change RanComparerEnum to RankComparison enum * Remove double initialization of Rank properties --- League/BackgroundTasks/RankingUpdateTask.cs | 2 +- .../HroThreeWinningSetsRankComparisonTests.cs | 485 ++++++++++++++++++ .../Ranking/AlternateRankComparer1.cs | 122 ----- .../Ranking/AlternateRankComparer2.cs | 132 ----- .../Ranking/IRankComparer.cs | 19 +- .../TournamentManager/Ranking/Rank.cs | 31 +- .../TournamentManager/Ranking/RankComparer.cs | 318 ++++++++++++ .../Ranking/RankComparerEnum.cs | 8 - .../Ranking/RankComparer_Definitions.cs | 150 ++++++ .../Ranking/RankComparison.cs | 9 + .../TournamentManager/Ranking/Ranking.cs | 63 +-- .../Ranking/StandardRankComparer.cs | 76 --- 12 files changed, 1019 insertions(+), 396 deletions(-) create mode 100644 TournamentManager/TournamentManager.Tests/Ranking/HroThreeWinningSetsRankComparisonTests.cs delete mode 100644 TournamentManager/TournamentManager/Ranking/AlternateRankComparer1.cs delete mode 100644 TournamentManager/TournamentManager/Ranking/AlternateRankComparer2.cs create mode 100644 TournamentManager/TournamentManager/Ranking/RankComparer.cs delete mode 100644 TournamentManager/TournamentManager/Ranking/RankComparerEnum.cs create mode 100644 TournamentManager/TournamentManager/Ranking/RankComparer_Definitions.cs create mode 100644 TournamentManager/TournamentManager/Ranking/RankComparison.cs delete mode 100644 TournamentManager/TournamentManager/Ranking/StandardRankComparer.cs diff --git a/League/BackgroundTasks/RankingUpdateTask.cs b/League/BackgroundTasks/RankingUpdateTask.cs index a422c19d..cc85815d 100644 --- a/League/BackgroundTasks/RankingUpdateTask.cs +++ b/League/BackgroundTasks/RankingUpdateTask.cs @@ -144,7 +144,7 @@ await TenantContext.DbContext.AppDb.TeamInRoundRepository.GetTeamInRoundAsync( await TenantContext.DbContext.AppDb.RoundRepository.GetMatchRuleAsync(roundId, cancellationToken); // filter matches to only contain a single round var newRanking = new Ranking(matchesPlayed.Where(mp => mp.RoundId == roundId), - matchesToPlay.Where(mtp => mtp.RoundId == roundId), (RankComparerEnum) matchRule.RankComparer); + matchesToPlay.Where(mtp => mtp.RoundId == roundId), (RankComparison) matchRule.RankComparer); // Save the current last update var currentLastUpdated = currentRanking.Any() ? currentRanking.Where(l => l.RoundId == roundId).Max(l => l.ModifiedOn) : DateTime.MinValue; var newRankingList = newRanking.GetList(out var newLastUpdated); diff --git a/TournamentManager/TournamentManager.Tests/Ranking/HroThreeWinningSetsRankComparisonTests.cs b/TournamentManager/TournamentManager.Tests/Ranking/HroThreeWinningSetsRankComparisonTests.cs new file mode 100644 index 00000000..a536c4b6 --- /dev/null +++ b/TournamentManager/TournamentManager.Tests/Ranking/HroThreeWinningSetsRankComparisonTests.cs @@ -0,0 +1,485 @@ +using NUnit.Framework; +using TournamentManager.DAL.EntityClasses; +using TournamentManager.ExtensionMethods; +using TournamentManager.DAL.TypedViewClasses; +using TournamentManager.Ranking; + +namespace TournamentManager.Tests.Ranking; + +[TestFixture] +public class HroThreeWinningSetsRankComparisonTests +{ + [Test] + public void Two_Teams_Are_Fully_Equal_With_Match_Results() + { + var matchId = 1; + var matches = new List { + GetMatch(matchId++, 1, 2, "25:0 25:0 25:0"), + GetMatch(matchId, 2, 1, "25:0 25:0 25:0") + }; + + var ranking = new TournamentManager.Ranking.Ranking(CreateMatchCompleteRows(matches), + new List(), RankComparison.HroThreeWinningSetsRankComparison); + + var rl= ranking.GetList(out var updatedOn); + + Assert.That(rl.Count, Is.EqualTo(2)); + Assert.That(rl[0].Number, Is.EqualTo(1)); + Assert.That(rl[1].Number, Is.EqualTo(2)); + // Note: Even the direct comparison between teams is the same, so the team with the higher team ID is returned. + Assert.That(rl[0].TeamId, Is.EqualTo(2)); + } + + //[TestCase("25:0 25:0 25:0", "0:25 0:25 0:25", 1, 2)] + [TestCase("0:25 0:25 0:25", "25:0 25:0 25:0", 2, 1)] + public void Ranking_Based_On_MatchPoints_Won(string xResult, string yResult, long expected1,long expected2) + { + var matchId = 1; + var matches = new List { + GetMatch(matchId++, 1, 2, xResult), + GetMatch(matchId, 2, 1, yResult) + }; + + var ranking = new TournamentManager.Ranking.Ranking(CreateMatchCompleteRows(matches), + new List(), RankComparison.HroThreeWinningSetsRankComparison); + + var rl= ranking.GetList(out var updatedOn); + + Assert.That(rl.Count, Is.EqualTo(2)); + Assert.That(rl[0].TeamId, Is.EqualTo(expected1)); + Assert.That(rl[1].TeamId, Is.EqualTo(expected2)); + } + + + private IList CreateMatchCompleteRows(List matches) + { + var completeMatches = new List(); + foreach (var match in matches) + { + completeMatches.Add(new MatchCompleteRawRow { + Id = match.Id, + HomeTeamId = match.HomeTeamId, + GuestTeamId = match.GuestTeamId, + HomeMatchPoints = match.HomePoints, + GuestMatchPoints = match.GuestPoints, + HomeSetPoints = match.Sets.Sum(s => s.HomeSetPoints), + GuestSetPoints = match.Sets.Sum(s => s.GuestSetPoints), + HomeBallPoints = match.Sets.Sum(s => s.HomeBallPoints), + GuestBallPoints= match.Sets.Sum(s => s.GuestBallPoints), + MatchDate = match.RealStart + }); + } + + return completeMatches; + } + + private MatchEntity GetMatch(long matchId, long home, long guest, string setResults) + { + var matchRule = GetMatchRule_TieBreakRule(); + var setRule = GetSetRule(); + var dateTime = DateTime.UtcNow.Date.AddHours(-4); + var match = new MatchEntity(matchId) { + HomeTeamId = home, + GuestTeamId = guest, + RealStart = dateTime, + RealEnd = dateTime.AddHours(2), + CreatedOn = dateTime, + ModifiedOn = dateTime + }; + match.Sets.Add(match.Id, setResults); + _ = match.Sets.CalculateSetPoints(setRule, matchRule); + _ = match.CalculateMatchPoints(matchRule); + + return match; + } + + /* + [Test] + public void Calc_With_No_Sets_Should_Not_Throw() + { + var matchRule = GetMatchRule_NoTieBreakRule(); + var setRule = GetSetRule(); + var match = new MatchEntity(1234); + + Assert.That(del: () => _ = match.Sets.CalculateSetPoints(setRule, matchRule), Throws.Nothing); + } + + [TestCase("25:1 25:2 25:3", 3, 0)] + [TestCase("25:1 25:2 1:25 1:25 15:1", 3, 0)] + [TestCase("1:25 2:25 2:25", 0, 3)] + [TestCase("1:25 2:25 25:13 25:1 1:15", 0, 3)] + [TestCase("1:25 25:1", 1, 1)] + public void Calc_MatchPoints_No_TieBreakRule(string setResults, int expectedHome, int expectedGuest) + { + // Note: Test cases are not validated against the rules here, but they are valid. + // Validation is tested in ModelValidator tests + + var matchRule = GetMatchRule_NoTieBreakRule(); + var setRule = GetSetRule(); + var match = new MatchEntity(1234); + match.Sets.Add(match.Id, setResults); + _ = match.Sets.CalculateSetPoints(setRule, matchRule); + _ = match.CalculateMatchPoints(matchRule); + + var pointResult = new PointResult(match.HomePoints, match.GuestPoints); + var expectedResult = new PointResult(expectedHome, expectedGuest); + + pointResult.Should().BeEquivalentTo(expectedResult); + } + + [TestCase("25:1 25:2 25:3", 3, 0)] + [TestCase("25:1 25:2 1:25 1:25 15:1", 2, 1)] + [TestCase("1:25 2:25 2:25", 0, 3)] + [TestCase("1:25 2:25 25:13 25:1 1:15", 1, 2)] + [TestCase("1:25 25:1", 1, 1)] + public void Calc_MatchPoints_With_TieBreakRule(string setResults, int expectedHome, int expectedGuest) + { + // Note: Test cases are not validated against the rules here, but they are valid. + // Validation is tested in ModelValidator tests + + var matchRule = GetMatchRule_TieBreakRule(); + var setRule = GetSetRule(); + var match = new MatchEntity(1234); + match.Sets.Add(match.Id, setResults); + _ = match.Sets.CalculateSetPoints(setRule, matchRule); + _ = match.CalculateMatchPoints(matchRule); + + var pointResult = new PointResult(match.HomePoints, match.GuestPoints); + var expectedResult = new PointResult(expectedHome, expectedGuest); + + pointResult.Should().BeEquivalentTo(expectedResult); + } + + [Test] + public void Calc_MatchPoints_With_TieBreakRule_Throws() + { + var matchRule = GetMatchRule_TieBreakRule(); + var setRule = GetSetRule(); + var match = new MatchEntity(1234); + match.Sets.Add(match.Id, "25:1 25:2 1:25 1:25 15:1"); + _ = match.Sets.CalculateSetPoints(setRule, matchRule); + + // This will trigger an exception, because the set points are tie + var lastSet = match.Sets.Last(); + lastSet.HomeSetPoints = lastSet.GuestSetPoints = 0; + + Assert.That(del: () => _ = match.CalculateMatchPoints(matchRule), Throws.InvalidOperationException); + } + + [Test] + public void CreateMatches() + { + // SELECT * FROM [Match] WHERE RoundId=1 and LegSequenceNo=1 FOR JSON AUTO + // Round-robin: + // Each team must match 5 opponents, thus there are 5 rounds. + // There are 3 matches per round (6 teams divided by 2). + // This makes 15 matches (5 rounds x 3 matches per round). + var matchesJson = + """ + [ + { + "Id": 1, + "HomeTeamId": 1, + "GuestTeamId": 12, + "RefereeId": 1, + "RoundId": 1, + "VenueId": 1, + "LegSequenceNo": 1, + "IsComplete": false, + "IsOverruled": false, + "PlannedStart": "2023-09-27T18:00:00", + "PlannedEnd": "2023-09-27T20:00:00", + "OrigPlannedStart": "2023-09-20T18:00:00", + "OrigPlannedEnd": "2023-09-20T20:00:00", + "Remarks": "", + "ChangeSerial": 1, + "CreatedOn": "2023-09-01T22:03:26.830", + "ModifiedOn": "2023-09-02T05:22:21.063" + }, + { + "Id": 2, + "HomeTeamId": 14, + "GuestTeamId": 17, + "RefereeId": 14, + "RoundId": 1, + "VenueId": 5, + "LegSequenceNo": 1, + "IsComplete": false, + "IsOverruled": false, + "PlannedStart": "2023-09-19T18:00:00", + "PlannedEnd": "2023-09-19T20:00:00", + "Remarks": "", + "ChangeSerial": 0, + "CreatedOn": "2023-09-01T22:03:26.913", + "ModifiedOn": "2023-09-01T14:30:00" + }, + { + "Id": 3, + "HomeTeamId": 22, + "GuestTeamId": 25, + "RefereeId": 22, + "RoundId": 1, + "VenueId": 18, + "LegSequenceNo": 1, + "IsComplete": false, + "IsOverruled": false, + "PlannedStart": "2023-09-19T18:00:00", + "PlannedEnd": "2023-09-19T20:00:00", + "Remarks": "", + "ChangeSerial": 0, + "CreatedOn": "2023-09-01T22:03:26.990", + "ModifiedOn": "2023-09-01T14:30:00" + }, + { + "Id": 4, + "HomeTeamId": 1, + "GuestTeamId": 14, + "RefereeId": 1, + "RoundId": 1, + "VenueId": 1, + "LegSequenceNo": 1, + "IsComplete": false, + "IsOverruled": false, + "PlannedStart": "2023-10-18T18:00:00", + "PlannedEnd": "2023-10-18T20:00:00", + "Remarks": "", + "ChangeSerial": 0, + "CreatedOn": "2023-09-01T22:03:27.053", + "ModifiedOn": "2023-09-01T14:30:00" + }, + { + "Id": 5, + "HomeTeamId": 12, + "GuestTeamId": 17, + "RefereeId": 12, + "RoundId": 1, + "VenueId": 11, + "LegSequenceNo": 1, + "IsComplete": false, + "IsOverruled": false, + "PlannedStart": "2023-10-17T18:00:00", + "PlannedEnd": "2023-10-17T20:00:00", + "Remarks": "", + "ChangeSerial": 0, + "CreatedOn": "2023-09-01T22:03:27.147", + "ModifiedOn": "2023-09-01T14:30:00" + }, + { + "Id": 6, + "HomeTeamId": 22, + "GuestTeamId": 1, + "RefereeId": 22, + "RoundId": 1, + "VenueId": 18, + "LegSequenceNo": 1, + "IsComplete": false, + "IsOverruled": false, + "PlannedStart": "2023-11-07T19:00:00", + "PlannedEnd": "2023-11-07T21:00:00", + "Remarks": "", + "ChangeSerial": 0, + "CreatedOn": "2023-09-01T22:03:27.240", + "ModifiedOn": "2023-09-01T14:30:00" + }, + { + "Id": 7, + "HomeTeamId": 25, + "GuestTeamId": 12, + "RefereeId": 25, + "RoundId": 1, + "VenueId": 2, + "LegSequenceNo": 1, + "IsComplete": false, + "IsOverruled": false, + "PlannedStart": "2023-11-10T19:00:00", + "PlannedEnd": "2023-11-10T21:00:00", + "Remarks": "", + "ChangeSerial": 0, + "CreatedOn": "2023-09-01T22:03:27.333", + "ModifiedOn": "2023-09-01T14:30:00" + }, + { + "Id": 8, + "HomeTeamId": 14, + "GuestTeamId": 22, + "RefereeId": 14, + "RoundId": 1, + "VenueId": 5, + "LegSequenceNo": 1, + "IsComplete": false, + "IsOverruled": false, + "PlannedStart": "2023-11-14T19:00:00", + "PlannedEnd": "2023-11-14T21:00:00", + "Remarks": "", + "ChangeSerial": 0, + "CreatedOn": "2023-09-01T22:03:27.397", + "ModifiedOn": "2023-09-01T14:30:00" + }, + { + "Id": 9, + "HomeTeamId": 17, + "GuestTeamId": 25, + "RefereeId": 17, + "RoundId": 1, + "VenueId": 14, + "LegSequenceNo": 1, + "IsComplete": false, + "IsOverruled": false, + "PlannedStart": "2023-11-15T18:30:00", + "PlannedEnd": "2023-11-15T20:30:00", + "Remarks": "", + "ChangeSerial": 0, + "CreatedOn": "2023-09-01T22:03:27.477", + "ModifiedOn": "2023-09-01T14:30:00" + }, + { + "Id": 10, + "HomeTeamId": 1, + "GuestTeamId": 17, + "RefereeId": 1, + "RoundId": 1, + "VenueId": 1, + "LegSequenceNo": 1, + "IsComplete": false, + "IsOverruled": false, + "PlannedStart": "2023-12-06T19:00:00", + "PlannedEnd": "2023-12-06T21:00:00", + "Remarks": "", + "ChangeSerial": 0, + "CreatedOn": "2023-09-01T22:03:28.577", + "ModifiedOn": "2023-09-01T14:30:00" + }, + { + "Id": 11, + "HomeTeamId": 12, + "GuestTeamId": 14, + "RefereeId": 12, + "RoundId": 1, + "VenueId": 11, + "LegSequenceNo": 1, + "IsComplete": false, + "IsOverruled": false, + "PlannedStart": "2023-12-05T19:00:00", + "PlannedEnd": "2023-12-05T21:00:00", + "Remarks": "", + "ChangeSerial": 0, + "CreatedOn": "2023-09-01T22:03:27.617", + "ModifiedOn": "2023-09-01T14:30:00" + }, + { + "Id": 12, + "HomeTeamId": 22, + "GuestTeamId": 12, + "RefereeId": 22, + "RoundId": 1, + "VenueId": 18, + "LegSequenceNo": 1, + "IsComplete": false, + "IsOverruled": false, + "PlannedStart": "2023-12-19T19:00:00", + "PlannedEnd": "2023-12-19T21:00:00", + "Remarks": "", + "ChangeSerial": 0, + "CreatedOn": "2023-09-01T22:03:27.680", + "ModifiedOn": "2023-09-01T14:30:00" + }, + { + "Id": 13, + "HomeTeamId": 25, + "GuestTeamId": 1, + "RefereeId": 25, + "RoundId": 1, + "VenueId": 2, + "LegSequenceNo": 1, + "IsComplete": false, + "IsOverruled": false, + "PlannedStart": "2023-12-15T19:00:00", + "PlannedEnd": "2023-12-15T21:00:00", + "Remarks": "", + "ChangeSerial": 0, + "CreatedOn": "2023-09-01T22:03:27.743", + "ModifiedOn": "2023-09-01T14:30:00" + }, + { + "Id": 14, + "HomeTeamId": 14, + "GuestTeamId": 25, + "RefereeId": 14, + "RoundId": 1, + "VenueId": 5, + "LegSequenceNo": 1, + "IsComplete": false, + "IsOverruled": false, + "PlannedStart": "2024-01-16T19:00:00", + "PlannedEnd": "2024-01-16T21:00:00", + "Remarks": "", + "ChangeSerial": 0, + "CreatedOn": "2023-09-01T22:03:27.820", + "ModifiedOn": "2023-09-01T14:30:00" + }, + { + "Id": 15, + "HomeTeamId": 17, + "GuestTeamId": 22, + "RefereeId": 17, + "RoundId": 1, + "VenueId": 14, + "LegSequenceNo": 1, + "IsComplete": false, + "IsOverruled": false, + "PlannedStart": "2024-01-17T18:30:00", + "PlannedEnd": "2024-01-17T20:30:00", + "Remarks": "", + "ChangeSerial": 0, + "CreatedOn": "2023-09-01T22:03:27.887", + "ModifiedOn": "2023-09-01T14:30:00" + } + ] + + """; + + var matches = JsonSerializer.Deserialize>(matchesJson)!; + matches[0].Sets.Add(matches[0].Id, "25:1 25:2 1:25 1:25 15:1"); + //return new List(); + } + */ + private static MatchRuleEntity GetMatchRule_NoTieBreakRule() + { + return new MatchRuleEntity { + BestOf = true, + NumOfSets = 3, + PointsMatchWon = 3, + PointsMatchLost = 0, + PointsMatchTie = 1, + RankComparer = (int) RankComparison.HroThreeWinningSetsRankComparison + }; + } + + private static MatchRuleEntity GetMatchRule_TieBreakRule() + { + return new MatchRuleEntity { + BestOf = true, + NumOfSets = 3, + PointsMatchWon = 3, + PointsMatchLost = 0, + PointsMatchWonAfterTieBreak = 2, + PointsMatchLostAfterTieBreak = 1, + PointsMatchTie = 1, + RankComparer = (int) RankComparison.HroThreeWinningSetsRankComparison + }; + } + + private static SetRuleEntity GetSetRule() + { + return new SetRuleEntity { + NumOfPointsToWinRegular = 25, + NumOfPointsToWinTiebreak = 15, + PointsDiffToWinRegular = 2, + PointsDiffToWinTiebreak = 2, + PointsSetWon = 1, + PointsSetLost = 0, + PointsSetTie = 0 + }; + } + +} diff --git a/TournamentManager/TournamentManager/Ranking/AlternateRankComparer1.cs b/TournamentManager/TournamentManager/Ranking/AlternateRankComparer1.cs deleted file mode 100644 index a13facbd..00000000 --- a/TournamentManager/TournamentManager/Ranking/AlternateRankComparer1.cs +++ /dev/null @@ -1,122 +0,0 @@ -namespace TournamentManager.Ranking; - -/// -/// The for the rules of Bayerischer Volleyballverband as of November 2005 -/// -internal class AlternateRankComparer1 : IRankComparer -{ - private static bool _directCompareRankingInProgress = false; - - /// - /// Required CTOR for Activator.CreateInstance(...) - /// - internal AlternateRankComparer1() - { } - - internal AlternateRankComparer1(Ranking ranking, DateTime upperDateLimit) - { - Ranking = ranking; - UpperDateLimit = upperDateLimit; - } - - public DateTime UpperDateLimit { get; set; } - - public Ranking? Ranking { get; set; } - - public int Compare(Rank? x, Rank? y) - { - /* - BVV-Spielordnung - Stand: 25.11.05 - http://bvv.volley.de/n/fileadmin/user_all/Download/Ordnungen/spielordnung25112005.pdf - - 11. Spielwertung, Nichtantreten von Mannschaften - 11.1 a) Alle Pflichtspiele sind über 3 Gewinnsätze auszutragen. - b) Meisterschaftsspiele der Altersklassen und Pokalspiele bis Bezirksebene können - über 2 Gewinnsätze ausgetragen werden. Eine bindende Festlegung trifft - der jeweils zuständige Spielausschuss. - 11.2 a) Zur Ermittlung der Rangfolge in Spielrunden und bei Turnieren erhalten gewinnende - Mannschaften zwei Pluspunkte (2:0), verlierende Mannschaften zwei - Minuspunkte (0:2). Die Mannschaft, die mehr Pluspunkte aufweist, erhält den - besseren Platz. Bei gleicher Punktzahl ist besser platziert, wer weniger Minuspunkte - aufweist. - b) Bei Punktgleichheit von zwei oder mehr Mannschaften entscheidet über die - Platzierung zunächst die Satzdifferenz (Subtraktionsverfahren). Bei gleicher - Satzdifferenz zählt die Anzahl der gewonnenen Sätze. - c) Bei Punktgleichheit, gleicher Satzdifferenz und gleicher Anzahl der gewonnenen - Sätze von zwei oder mehr Mannschaften entscheidet über die Platzierung - die Balldifferenz (Subtraktionsverfahren). Bei gleicher Balldifferenz zählt die - Anzahl der gewonnenen Bälle. - d) Er gibt sich nach Anwendung der Absätze a) bis c) ein Gleichstand für zwei - oder mehr Mannschaften, müssen diese Mannschaften nochmals gegeneinander - spielen; die Entscheidungsspiele sind dann maßgebend für die Platzierung. - Solche Entscheidungsspiele sollen auf neutralen Plätzen stattfinden. - */ - if (Ranking is null) throw new NullReferenceException("Ranking property must not be null"); - - if (x == null || y == null) throw new NullReferenceException($"{nameof(Rank)} arguments must not be null"); - - // sort down teams with no matches played - if (x.MatchesPlayed == 0) return 1; - if (y.MatchesPlayed == 0) return -1; - - // sort down overruled results with no points at all - if (x.MatchPoints.Home + x.MatchPoints.Guest + x.SetPoints.Home + x.SetPoints.Guest + x.BallPoints.Home + x.BallPoints.Guest == 0) - return 1; - if (y.MatchPoints.Home + y.MatchPoints.Guest + y.SetPoints.Home + y.SetPoints.Guest + y.BallPoints.Home + y.BallPoints.Guest == 0) - return -1; - - // a) - if (x.MatchPoints.Home < y.MatchPoints.Home) - return 1; - else if (x.MatchPoints.Home > y.MatchPoints.Home) - return -1; - - if (x.MatchPoints.Guest > y.MatchPoints.Guest) - return 1; - else if (x.MatchPoints.Guest < y.MatchPoints.Guest) - return -1; - - // b) - if ((x.SetPoints.Home - x.SetPoints.Guest) < (y.SetPoints.Home - y.SetPoints.Guest)) - return 1; - else if ((x.SetPoints.Home - x.SetPoints.Guest) > (y.SetPoints.Home - y.SetPoints.Guest)) - return -1; - - if (x.SetPoints.Home < y.SetPoints.Home) - return 1; - else if (x.SetPoints.Home > y.SetPoints.Home) - return -1; - - // c) - if ((x.BallPoints.Home - x.BallPoints.Guest) < (y.BallPoints.Home - y.BallPoints.Guest)) - return 1; - else if ((x.BallPoints.Home - x.BallPoints.Guest) > (y.BallPoints.Home - y.BallPoints.Guest)) - return -1; - - if (x.BallPoints.Home < y.BallPoints.Home) - return 1; - else if (x.BallPoints.Home > y.BallPoints.Home) - return -1; - - // d) taking already played matches instead of new match to play - // avoid an infinite loop - if (!_directCompareRankingInProgress && x.MatchesPlayed > 0 && y.MatchesPlayed > 0) - { - _directCompareRankingInProgress = true; - var directCompareRanking = Ranking.GetList(new[] { x.TeamId, y.TeamId }, UpperDateLimit); - _directCompareRankingInProgress = false; - - if (directCompareRanking[0].TeamId == x.TeamId) - return -1; - else - return 1; - } - else - { - // if directCompareRanking is reached twice, both teams must have the same score, - // so we return a random winner by comparing Team Ids - return (x.TeamId < y.TeamId ? 1 : -1); - } - } -} \ No newline at end of file diff --git a/TournamentManager/TournamentManager/Ranking/AlternateRankComparer2.cs b/TournamentManager/TournamentManager/Ranking/AlternateRankComparer2.cs deleted file mode 100644 index b5987dff..00000000 --- a/TournamentManager/TournamentManager/Ranking/AlternateRankComparer2.cs +++ /dev/null @@ -1,132 +0,0 @@ -namespace TournamentManager.Ranking; - -/// -/// The for the rules of Bayerischer Volleyballverband as of June 2019 -/// -internal class AlternateRankComparer2 : IRankComparer -{ - private static bool _directCompareRankingInProgress = false; - - /// - /// Required CTOR for Activator.CreateInstance(...) - /// - internal AlternateRankComparer2() - { - throw new NotImplementedException("This comparer is not implemented yet according to the given rules."); - } - - internal AlternateRankComparer2(Ranking ranking, DateTime upperDateLimit) : this() - { - Ranking = ranking; - UpperDateLimit = upperDateLimit; - } - - public DateTime UpperDateLimit { get; set; } = DateTime.MinValue; - - public Ranking? Ranking { get; set; } - - public int Compare(Rank? x, Rank? y) - { - if (Ranking == null) throw new NullReferenceException($"{nameof(Ranking)} cannot be null."); - - /* - BVV-Spielordnung - Stand: 28.06.2019 - http://www.volleyball-bayern.de/fileadmin/user_upload/Bezirke/Niederbayern/Spielwesen/VSPO_2019.pdf - - 11.2 - - a) Zur Ermittlung der Rangfolge in Spielrunden und bei Turnieren erhalten - - bei Spielen über 3 Gewinnsätze - der Gewinner bei einem 3:0 oder 3:1 drei Punkte, - der Gewinner bei einem 3:2 zwei Punkte und der Verlierer einen Punkt. - Der Verlierer erhält bei einem 1:3 oder 0:3 keinen Punkt. - Es werden nur Pluspunkte vergeben. - Über die Rangfolge von zwei oder mehr Mannschaften entscheidet in absteigender Priorität - a1) die Anzahl der Punkte, - a2) die Anzahl gewonnener Spiele, - a3) der Satzquotient, indem die Anzahl der gewonnenen Sätze durch die Anzahl der verlorenen Sätze dividiert wird, - a4) der Ballpunktquotient, indem die Anzahl der gewonnenen Ballpunkte durch die Anzahl der verlorenen Ballpunkte dividiert wird, - a5) der direkte Vergleich zwischen den Mannschaften, wobei die Kriterien nach a1) bis a4) zur Berechnung der Rangfolge herangezogen werden. - Ergibt sich nach Anwendung der Ziffern a1) bis a5) ein Gleichstand für zwei oder mehr Mannschaften, müssen diese Mannschaften nochmals gegeneinander spielen; die Entscheidungsspiele sind dann maßgebend für die Platzierung. Solche Entscheidungsspiele sollen auf neutralen Plätzen stattfinden. Bei Turnieren kann in der Ausschreibung eine hiervon abweichende Regelung getroffen werden. - - b) Zur Ermittlung der Rangfolge in Spielrunden und bei Turnieren erhalten - - bei Spielen über 2 Gewinnsätze - der Gewinner bei einem 2:0 oder 2:1 zwei Punkte, der Verlierer keinen Punkt. - Es werden nur Pluspunkte vergeben. - Über die Rangfolge von zwei oder mehr Mannschaften entscheidet in absteigender Priorität - b1) die Anzahl der Punkte, - b2) die Satzdifferenz, indem die Anzahl der verlorenen Sätze von der Anzahl der gewonnenen Sätze subtrahiert wird, - b3) bei gleicher Satzdifferenz die Anzahl der gewonnenen Sätze, - b4) die Ballpunktdifferenz, indem die Anzahl der verlorenen Bälle von der Anzahl der gewonnenen Bälle subtrahiert wird, - b5) bei gleicher Balldifferenz die Anzahl der gewonnenen Bälle. - Ergibt sich nach Anwendung der Ziffern b1) bis b5) ein Gleichstand für zwei oder mehr Mannschaften, müssen diese Mannschaften nochmals gegeneinander spielen; die Entscheidungsspiele sind dann maßgebend für die Platzierung. Solche Entscheidungsspiele sollen auf neutralen Plätzen stattfinden. Bei Turnieren kann in der Ausschreibung eine hiervon abweichende Regelung getroffen werden. - */ - - if (x == null || y == null) throw new NullReferenceException($"{nameof(Rank)} arguments must not be null"); - - // sort down teams with no matches played - if (x.MatchesPlayed == 0) return 1; - if (y.MatchesPlayed == 0) return -1; - - // sort down overruled results with no points at all - if (x.MatchPoints.Home + x.MatchPoints.Guest + x.SetPoints.Home + x.SetPoints.Guest + x.BallPoints.Home + x.BallPoints.Guest == 0) - return 1; - if (y.MatchPoints.Home + y.MatchPoints.Guest + y.SetPoints.Home + y.SetPoints.Guest + y.BallPoints.Home + y.BallPoints.Guest == 0) - return -1; - - - // a) - if (x.MatchPoints.Home < y.MatchPoints.Home) - return 1; - else if (x.MatchPoints.Home > y.MatchPoints.Home) - return -1; - - if (x.MatchPoints.Guest > y.MatchPoints.Guest) - return 1; - else if (x.MatchPoints.Guest < y.MatchPoints.Guest) - return -1; - - // b) - if ((x.SetPoints.Home - x.SetPoints.Guest) < (y.SetPoints.Home - y.SetPoints.Guest)) - return 1; - else if ((x.SetPoints.Home - x.SetPoints.Guest) > (y.SetPoints.Home - y.SetPoints.Guest)) - return -1; - - if (x.SetPoints.Home < y.SetPoints.Home) - return 1; - else if (x.SetPoints.Home > y.SetPoints.Home) - return -1; - - // c) - if ((x.BallPoints.Home - x.BallPoints.Guest) < (y.BallPoints.Home - y.BallPoints.Guest)) - return 1; - else if ((x.BallPoints.Home - x.BallPoints.Guest) > (y.BallPoints.Home - y.BallPoints.Guest)) - return -1; - - if (x.BallPoints.Home < y.BallPoints.Home) - return 1; - else if (x.BallPoints.Home > y.BallPoints.Home) - return -1; - - // d) taking already played matches instead of new match to play - // avoid an infinite loop - if (!_directCompareRankingInProgress && x.MatchesPlayed > 0 && y.MatchesPlayed > 0) - { - _directCompareRankingInProgress = true; - var directCompareRanking = Ranking.GetList(new[] { x.TeamId, y.TeamId }, UpperDateLimit); - _directCompareRankingInProgress = false; - - if (directCompareRanking[0].TeamId == x.TeamId) - return -1; - else - return 1; - } - else - { - // if directCompareRanking is reached twice, both teams must have the same score, - // so we return a random winner by comparing Team Ids - return (x.TeamId < y.TeamId ? 1 : -1); - } - } -} \ No newline at end of file diff --git a/TournamentManager/TournamentManager/Ranking/IRankComparer.cs b/TournamentManager/TournamentManager/Ranking/IRankComparer.cs index c020d1de..e49a8b5b 100644 --- a/TournamentManager/TournamentManager/Ranking/IRankComparer.cs +++ b/TournamentManager/TournamentManager/Ranking/IRankComparer.cs @@ -1,7 +1,24 @@ namespace TournamentManager.Ranking; +/// +/// The interface for rank comparers. +/// internal interface IRankComparer : IComparer { + /// + /// The description how the comparer works. + /// + string Description { get; } + + /// + /// A reference to the class to calculate the ranking table of matches. + /// Used by s to compare the match results of 2 neighboring teams. + /// Ranking? Ranking { get; set; } + + /// + /// The maximum of match dates that are included in the ranking table. + /// Used by s to compare the match results of 2 neighboring teams. + /// DateTime UpperDateLimit { get; set; } -} \ No newline at end of file +} diff --git a/TournamentManager/TournamentManager/Ranking/Rank.cs b/TournamentManager/TournamentManager/Ranking/Rank.cs index 47bf289a..f193c03e 100644 --- a/TournamentManager/TournamentManager/Ranking/Rank.cs +++ b/TournamentManager/TournamentManager/Ranking/Rank.cs @@ -5,25 +5,15 @@ namespace TournamentManager.Ranking; /// public class Rank { - public Rank() - { - Number = -1; - TeamId = -1; - MatchesWonAndLost = new PointResult(0,0); - MatchPoints = new PointResult(0, 0); - SetPoints = new PointResult(0, 0); - BallPoints = new PointResult(0, 0); - } - /// /// The number of the rank. /// - public int Number { get; internal set; } + public int Number { get; internal set; } = -1; /// /// The team ID. /// - public long TeamId { get; internal set; } + public long TeamId { get; internal set; } = -1; /// /// The number of matches played. @@ -38,29 +28,34 @@ public Rank() /// /// The number of matches the team won and lost. /// - public PointResult MatchesWonAndLost { get; internal set; } + public PointResult MatchesWon { get; internal set; } = new(0,0); /// /// The number of match points won and lost. /// - public PointResult MatchPoints { get; internal set; } + public PointResult MatchPoints { get; internal set; } = new(0, 0); + + /// + /// The number of sets won and lost. + /// + public PointResult SetsWon { get; internal set; } = new(0, 0); /// /// The number of set points won and lost. /// - public PointResult SetPoints { get; internal set; } + public PointResult SetPoints { get; internal set; } = new(0, 0); /// /// The number of ball points won and lost. /// - public PointResult BallPoints { get; internal set; } + public PointResult BallPoints { get; internal set; } = new(0, 0); /// - /// Gets the rank a string. + /// Gets the rank as a string. /// /// Returns the rank as a string. public override string ToString() { return Number.ToString(); } -} \ No newline at end of file +} diff --git a/TournamentManager/TournamentManager/Ranking/RankComparer.cs b/TournamentManager/TournamentManager/Ranking/RankComparer.cs new file mode 100644 index 00000000..e3f4be7c --- /dev/null +++ b/TournamentManager/TournamentManager/Ranking/RankComparer.cs @@ -0,0 +1,318 @@ +namespace TournamentManager.Ranking; +internal partial class RankComparer : IRankComparer +{ + // if true, a recursive call has started + private bool _directCompareRankingInProgress = false; + + /// + /// Required CTOR for used by . + /// + public RankComparer(RankComparison rankComparison) + { + SetDefinitions(rankComparison); + } + + public delegate bool ComparisonDelegate(Rank x, Rank y, out int result); + + private List> Comparisons { get; set; } = null!; + + /// + /// + public int Compare(Rank? x, Rank? y) + { + CheckForNull(); + System.Diagnostics.Debug.Assert(x != null && y != null, @"Rank arguments must not be null"); + + foreach (var comparisonDelegate in Comparisons) + { + if (comparisonDelegate.Invoke(x, y, out var result)) return result; + } + + throw new InvalidOperationException(@"List of comparisons is missing the final comparison"); + } + + /// + public string Description { get; private set; } = string.Empty; + + /// + public DateTime UpperDateLimit { get; set; } = DateTime.MaxValue; + + /// + public Ranking? Ranking { get; set; } + + private void CheckForNull() + { + if (Ranking is null) throw new InvalidOperationException("Ranking property must not be null"); + } + + /// + /// Teams which have no matches played (yet) go to the end of the ranking table. + /// + private static bool SortDownTeamsWithNoMatchesPlayed(Rank x, Rank y, out int result) + { + result = 0; + + if (x.MatchesPlayed == 0) + { + result = 1; + return true; + } + + if (y.MatchesPlayed == 0) + { + result = -1; + return true; + } + + return false; + } + + /// + /// Matches which have no points at all (i.e. that have been overruled this way) + /// go to the end of the ranking table. + /// + private static bool SortDownMatchesWithNoPoints(Rank x, Rank y, out int result) + { + result = 0; + + if (x.MatchPoints.Home + x.MatchPoints.Guest + x.SetPoints.Home + x.SetPoints.Guest + x.BallPoints.Home + x.BallPoints.Guest == 0) + { + result = 1; + return true; + } + + if (y.MatchPoints.Home + y.MatchPoints.Guest + y.SetPoints.Home + y.SetPoints.Guest + y.BallPoints.Home + y.BallPoints.Guest == 0) + { + result = -1; + return true; + } + + return false; + } + + /// + /// The match points won decide. + /// + private static bool MatchPointsWonDecide(Rank x, Rank y, out int result) + { + result = 0; + + if (x.MatchPoints.Home < y.MatchPoints.Home) + { + result = 1; + return true; + } + + if (x.MatchPoints.Home > y.MatchPoints.Home) + { + result = -1; + return true; + } + + return false; + } + + /// + /// The match point difference (i.e. home points minus guest points) decides. + /// + private static bool MatchPointsDifferenceDecides(Rank x, Rank y, out int result) + { + result = 0; + + if ((x.MatchPoints.Home - x.MatchPoints.Guest) < (y.MatchPoints.Home - y.MatchPoints.Guest)) + { + result = 1; + return true; + } + + if ((x.MatchPoints.Home - x.MatchPoints.Guest) > (y.MatchPoints.Home - y.MatchPoints.Guest)) + { + result = -1; + return true; + } + + return false; + } + + /// + /// The number of matches won decides. + /// + private static bool MatchesWonDecide(Rank x, Rank y, out int result) + { + result = 0; + + if (x.MatchesWon.Home < y.MatchesWon.Home) + { + result = 1; + return true; + } + + if (x.MatchesWon.Home > y.MatchesWon.Home) + { + result = -1; + return true; + } + + return false; + } + + /// + /// The set points ratio (i.e. home set points divided by guest set points) decides + /// + private static bool SetPointsRatioDecides(Rank x, Rank y, out int result) + { + result = 0; + + if (((float?) x.SetPoints.Home / x.SetPoints.Guest) < ((float?) y.SetPoints.Home / y.SetPoints.Guest)) + { + result = 1; + return true; + } + + if (((float?) x.SetPoints.Home / x.SetPoints.Guest) > ((float?) y.SetPoints.Home / y.SetPoints.Guest)) + { + result = -1; + return true; + } + + return false; + } + + /// + /// The set point difference (i.e. home points minus guest points) decides. + /// + private static bool SetPointsDifferenceDecides(Rank x, Rank y, out int result) + { + result = 0; + + if ((x.SetPoints.Home - x.SetPoints.Guest) < (y.SetPoints.Home - y.SetPoints.Guest)) + { + result = 1; + return true; + } + + if ((x.SetPoints.Home - x.SetPoints.Guest) > (y.SetPoints.Home - y.SetPoints.Guest)) + { + result = -1; + return true; + } + + return false; + } + + /// + /// The number of sets won decides. + /// + private static bool SetsWonDecide(Rank x, Rank y, out int result) + { + result = 0; + + if (x.SetsWon.Home < y.SetsWon.Home) + { + result = 1; + return true; + } + + if (x.SetsWon.Home > y.SetsWon.Home) + { + result = -1; + return true; + } + + return false; + } + + /// + /// The ball points ratio (i.e. home ball points divided by guest ball points) decides + /// + private static bool BallPointsRatioDecides(Rank x, Rank y, out int result) + { + result = 0; + + if (((float?) x.BallPoints.Home / x.BallPoints.Guest) < ((float?) y.BallPoints.Home / y.BallPoints.Guest)) + { + result = 1; + return true; + } + + if (((float?) x.BallPoints.Home / x.BallPoints.Guest) > ((float?) y.BallPoints.Home / y.BallPoints.Guest)) + { + result = -1; + return true; + } + + return false; + } + + /// + /// The ball point difference (i.e. home points minus guest points) decides. + /// + private static bool BallPointsDifferenceDecides(Rank x, Rank y, out int result) + { + result = 0; + + if ((x.BallPoints.Home - x.BallPoints.Guest) < (y.BallPoints.Home - y.BallPoints.Guest)) + { + result = 1; + return true; + } + + if ((x.BallPoints.Home - x.BallPoints.Guest) > (y.BallPoints.Home - y.BallPoints.Guest)) + { + result = -1; + return true; + } + + return false; + } + + /// + /// The number of ball points won decides. + /// + private static bool BallPointsWonDecide(Rank x, Rank y, out int result) + { + result = 0; + + if (x.BallPoints.Home < y.BallPoints.Home) + { + result = 1; + return true; + } + + if (x.BallPoints.Home > y.BallPoints.Home) + { + result = -1; + return true; + } + + return false; + } + + /// + /// The matches that two neighboring have played against each other decide. + /// This must be the last method of the decision list, because it always return . + /// + private bool DirectComparisonOfTeamsDecides(Rank x, Rank y, out int result) + { + // avoid an infinite loop + if (!_directCompareRankingInProgress && x.MatchesPlayed > 0 && y.MatchesPlayed > 0) + { + _directCompareRankingInProgress = true; + var directCompareRanking = Ranking!.GetList(new[] { x.TeamId, y.TeamId }, UpperDateLimit); + _directCompareRankingInProgress = false; + + if (directCompareRanking[0].TeamId == x.TeamId) + { + result = -1; + return true; + } + result = 1; + return true; + } + + // if directCompareRanking is reached twice, both teams must have the same score, + // so we return the team with the higher TeamId (a random alternative) + result = x.TeamId < y.TeamId ? 1 : -1; + return true; + } +} diff --git a/TournamentManager/TournamentManager/Ranking/RankComparerEnum.cs b/TournamentManager/TournamentManager/Ranking/RankComparerEnum.cs deleted file mode 100644 index 6c560892..00000000 --- a/TournamentManager/TournamentManager/Ranking/RankComparerEnum.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace TournamentManager.Ranking; - -public enum RankComparerEnum -{ - StandardRankComparer, - AlternateRankComparer1, - AlternateRankComparer2 -} \ No newline at end of file diff --git a/TournamentManager/TournamentManager/Ranking/RankComparer_Definitions.cs b/TournamentManager/TournamentManager/Ranking/RankComparer_Definitions.cs new file mode 100644 index 00000000..a3bb0f6c --- /dev/null +++ b/TournamentManager/TournamentManager/Ranking/RankComparer_Definitions.cs @@ -0,0 +1,150 @@ +namespace TournamentManager.Ranking; +internal partial class RankComparer +{ + public void SetDefinitions(RankComparison comparerType) + { + switch (comparerType) + { + case RankComparison.LegacyRankComparison: + SetLegacyRankComparer(); + break; + case RankComparison.TwoWinningSetsRankComparison: + SetTwoWinningSetsRankComparer(); + break; + case RankComparison.ThreeWinningSetsRankComparison: + SetThreeWinningSetsRankComparer(); + break; + case RankComparison.HroThreeWinningSetsRankComparison: + SetHroThreeWinningSetsRankComparer(); + break; + default: + throw new ArgumentOutOfRangeException(nameof(comparerType), comparerType, null); + } + } + + private void SetLegacyRankComparer() + { + Description = + """ + To determine the ranking in rounds with matches over 2 or 3 winning sets, the following will be compared in sequence: + a) The difference of match points (won minus lost) + b) The difference of set points (won minus lost) + c) The difference of ball points (won minus lost) + d) The direct comparison of the matches of teams which are equal in the difference of ball points won and lost. + """; + Comparisons = new List> { + SortDownTeamsWithNoMatchesPlayed, + SortDownMatchesWithNoPoints, + MatchPointsDifferenceDecides, // a) + SetPointsDifferenceDecides, // b) + BallPointsDifferenceDecides, // c) + DirectComparisonOfTeamsDecides // d) + }; + } + + /// + /// The rules of Bavarian Volleyball Association (BVV) as of 2020-06-19 + /// for 2 winning set matches. + /// + private void SetTwoWinningSetsRankComparer() + { + Description = + """ + To determine the ranking in rounds with matches over 2 winning sets, the following will be compared in sequence. + The winner in a 2:0 or 2:1 scores two match points, the loser no match point. Only plus points are awarded. + a) The match points won + b) The difference of set points (won minus lost) + c) The number of sets won + d) The difference of ball points (won minus lost) + e) The number of ball points won + f) The direct comparison of the matches of teams which are equal in "ball points won". + """; + Comparisons = new List> { + SortDownTeamsWithNoMatchesPlayed, + SortDownMatchesWithNoPoints, + MatchPointsWonDecide, // a) + SetPointsDifferenceDecides, // b) + SetsWonDecide, // c) + BallPointsDifferenceDecides, // d) + BallPointsWonDecide, // e) + DirectComparisonOfTeamsDecides // f) + }; + } + + /// + /// The rules of German Volleyball National League (DVV), in use since season 2013/14 + /// for 3 winning set matches, and applying the 3-point rule. + /// + private void SetThreeWinningSetsRankComparer() + { + Description = + """ + To determine the ranking in rounds with matches over 3 winning sets, the following will be compared in sequence. + The winner in a 3:0 or 3:1 scores three match points, the loser scores no match point. + The winner in a 3:2 scores 2 match points, the loser scores one match point. + Only plus points are awarded. + a) The match points won + b) The number of matches won + c) The ratio of sets won divided by sets lost + d) The ratio of ball points won divided by ballpoints lost + e) The direct comparison of the matches of teams which are equal in "ball points ratio". + """; + + Comparisons = new List> { + SortDownTeamsWithNoMatchesPlayed, + SortDownMatchesWithNoPoints, + MatchPointsWonDecide, // a) + MatchesWonDecide, // b) + SetPointsRatioDecides, // c) + BallPointsRatioDecides, // d) + DirectComparisonOfTeamsDecides // e) + }; + } + + /// + /// The for the rules of the Volleyball-Stadtliga Rostock, starting with season 2023/24 + /// for 3 winning set matches, and applying the 3-point rule. + /// + private void SetHroThreeWinningSetsRankComparer() + { + /* + Regeln für die Tabellenermittlung der Stadtliga Rostock: + + Zur Ermittlung der Rangfolge von Spielen werden 3 Gewinnsätze gespielt. Dabei erhält + * der Gewinner bei einem 3:0 oder 3:1 drei Punkte, der Verlierer keinen Punkt. + * der Gewinner bei einem 3:2 zwei Punkte und der Verlierer einen Punkt. + Es werden nur Pluspunkte vergeben. + Ãœber die Rangfolge von zwei oder mehr Mannschaften entscheidet in absteigender Priorität + a) die Anzahl der Punkte, + b) die Satzdifferenz, indem die Anzahl der verlorenen Sätze von der Anzahl der gewonnenen Sätze subtrahiert wird, + c) bei gleicher Satzdifferenz die Anzahl der gewonnenen Sätze, + d) die Ballpunktdifferenz, indem die Anzahl der verlorenen Bälle von der Anzahl der gewonnenen Bälle subtrahiert wird, + e) bei gleicher Balldifferenz die Anzahl der gewonnenen Bälle. + f) der direkte Vergleich zwischen den Mannschaften + */ + Description = + """ + To determine the ranking in rounds with matches over 3 winning sets, the following will be compared in sequence. + The winner in a 3:0 or 3:1 scores three match points, the loser scores no match point. + The winner in a 3:2 scores 2 match points, the loser scores one match point. + Only plus points are awarded. + a) The match points won + b) The difference of set points (won minus lost) + c) The number of sets won + d) The difference of ball points (won minus lost) + e) The number of ball points won + f) The direct comparison of the matches of teams which are equal in "ball points won". + """; + + Comparisons = new List> { + SortDownTeamsWithNoMatchesPlayed, + SortDownMatchesWithNoPoints, + MatchPointsWonDecide, // a) + SetPointsDifferenceDecides, // b) + SetsWonDecide, // c) + BallPointsDifferenceDecides, // d) + BallPointsWonDecide, // e) + DirectComparisonOfTeamsDecides // f) + }; + } +} diff --git a/TournamentManager/TournamentManager/Ranking/RankComparison.cs b/TournamentManager/TournamentManager/Ranking/RankComparison.cs new file mode 100644 index 00000000..eda799ae --- /dev/null +++ b/TournamentManager/TournamentManager/Ranking/RankComparison.cs @@ -0,0 +1,9 @@ +namespace TournamentManager.Ranking; + +public enum RankComparison +{ + LegacyRankComparison = 1, + TwoWinningSetsRankComparison, + ThreeWinningSetsRankComparison, + HroThreeWinningSetsRankComparison +} diff --git a/TournamentManager/TournamentManager/Ranking/Ranking.cs b/TournamentManager/TournamentManager/Ranking/Ranking.cs index 97d372e6..12c3e50d 100644 --- a/TournamentManager/TournamentManager/Ranking/Ranking.cs +++ b/TournamentManager/TournamentManager/Ranking/Ranking.cs @@ -1,4 +1,3 @@ -using System.Collections.ObjectModel; using TournamentManager.DAL.TypedViewClasses; namespace TournamentManager.Ranking; @@ -8,19 +7,6 @@ namespace TournamentManager.Ranking; /// public class Ranking { - /// - /// The list of available rank comparers - /// - private readonly Dictionary _rankComparers = new() - { - {RankComparerEnum.StandardRankComparer, typeof(StandardRankComparer)}, - {RankComparerEnum.AlternateRankComparer1, typeof(AlternateRankComparer1) }, - {RankComparerEnum.AlternateRankComparer2, typeof(AlternateRankComparer2) } - }; - - /// - /// The rank comparer currently selected - /// private readonly IRankComparer _rankComparer; /// @@ -28,33 +14,26 @@ public class Ranking /// /// The list of completed matches. /// The list of matches still to be played. - /// The key for the comparer to calculate the ranking table. - public Ranking(IEnumerable matchesComplete, IEnumerable matchesToPlay, RankComparerEnum rankComparerKey) + /// The key for the comparer to calculate the ranking table. + public Ranking(IEnumerable matchesComplete, IEnumerable matchesToPlay, RankComparison rankComparison) { MatchesPlayed = matchesComplete.ToList().AsReadOnly(); MatchesToPlay = matchesToPlay.ToList().AsReadOnly(); - _rankComparer = (IRankComparer?) Activator.CreateInstance(_rankComparers[rankComparerKey], true) ?? throw new InvalidOperationException(); - _rankComparer.Ranking = this; - } - - /// - /// Gets the list of available s. - /// - /// Gets the list of available s with their keys. - public ReadOnlyDictionary GetRankComparers() - { - return new ReadOnlyDictionary(_rankComparers); + _rankComparer = new RankComparer(rankComparison) { + UpperDateLimit = DateTime.MaxValue, + Ranking = this + }; } /// /// The read-only list of completed matches. /// - public IList MatchesPlayed { get; private set; } + public IList MatchesPlayed { get; } /// /// The read-only list of matches still to be played. /// - public IList MatchesToPlay { get; private set; } + public IList MatchesToPlay { get; } /// /// Get the for the default upper date limit, which @@ -66,7 +45,7 @@ public RankingList GetList(out DateTime lastUpdatedOn) { var upperDateLimit = MatchesPlayed.Count > 0 ? MatchesPlayed.Max(m => m.MatchDate ?? DateTime.MinValue) - : MatchesToPlay.Max(m => m.MatchDate ?? DateTime.UtcNow); + : MatchesToPlay.Max(m => m.MatchDate ?? DateTime.MaxValue); return GetList(upperDateLimit, out lastUpdatedOn); } @@ -118,19 +97,20 @@ internal RankingList GetList(IEnumerable teamIds, DateTime upperDateLimit) private RankingList GetUnsortedList(IEnumerable teamIds, DateTime upperDateLimit, out DateTime lastUpdatedOn) { + var teamIdList = teamIds.ToList(); // no multiple enumerations lastUpdatedOn = DateTime.UtcNow; if (MatchesPlayed.Any()) lastUpdatedOn = MatchesPlayed - .Where(m => teamIds.Contains(m.HomeTeamId) || teamIds.Contains(m.GuestTeamId)) + .Where(m => teamIdList.Contains(m.HomeTeamId) || teamIdList.Contains(m.GuestTeamId)) .Max(m => m.ModifiedOn); else if (MatchesToPlay.Any()) lastUpdatedOn = MatchesToPlay - .Where(m => teamIds.Contains(m.HomeTeamId) || teamIds.Contains(m.GuestTeamId)) + .Where(m => teamIdList.Contains(m.HomeTeamId) || teamIdList.Contains(m.GuestTeamId)) .Max(m => m.ModifiedOn); var rankingList = new RankingList {UpperDateLimit = upperDateLimit, LastUpdatedOn = lastUpdatedOn}; - foreach (var teamId in teamIds) + foreach (var teamId in teamIdList) { var rank = new Rank {Number = -1, TeamId = teamId, MatchesPlayed = MatchesPlayed.Count(m => m.HomeTeamId == teamId || m.GuestTeamId == teamId), MatchesToPlay = MatchesToPlay.Count(m => m.HomeTeamId == teamId || m.GuestTeamId == teamId)}; @@ -148,8 +128,11 @@ private RankingList GetUnsortedList(IEnumerable teamIds, DateTime upperDat rank.BallPoints.Home += match.HomeBallPoints ?? 0; rank.BallPoints.Guest += match.GuestBallPoints ?? 0; - rank.MatchesWonAndLost.Home += match.HomeMatchPoints > match.GuestMatchPoints ? 1 : 0; - rank.MatchesWonAndLost.Guest += match.HomeMatchPoints < match.GuestMatchPoints ? 1 : 0; + rank.MatchesWon.Home += match.HomeMatchPoints > match.GuestMatchPoints ? 1 : 0; + rank.MatchesWon.Guest += match.HomeMatchPoints < match.GuestMatchPoints ? 1 : 0; + + rank.SetsWon.Home += match.HomeSetPoints; + rank.SetsWon.Guest += match.GuestSetPoints; } else { @@ -162,8 +145,12 @@ private RankingList GetUnsortedList(IEnumerable teamIds, DateTime upperDat rank.BallPoints.Home += match.GuestBallPoints ?? 0; rank.BallPoints.Guest += match.HomeBallPoints ?? 0; - rank.MatchesWonAndLost.Home += match.HomeMatchPoints < match.GuestMatchPoints ? 1 : 0; - rank.MatchesWonAndLost.Guest += match.HomeMatchPoints > match.GuestMatchPoints ? 1 : 0; + rank.MatchesWon.Home += match.HomeMatchPoints < match.GuestMatchPoints ? 1 : 0; + rank.MatchesWon.Guest += match.HomeMatchPoints > match.GuestMatchPoints ? 1 : 0; + + // Todo: This only correct if SetRule.PointsSetWon = 1 and SetRule.PointsSetLost = 0 and SetRule.PointSetTie = 0 + rank.SetsWon.Home += match.GuestSetPoints; + rank.SetsWon.Guest += match.HomeSetPoints; } } rankingList.Add(rank); @@ -191,4 +178,4 @@ public List GetMatchDays() matchDates.Sort(); return matchDates; } -} \ No newline at end of file +} diff --git a/TournamentManager/TournamentManager/Ranking/StandardRankComparer.cs b/TournamentManager/TournamentManager/Ranking/StandardRankComparer.cs deleted file mode 100644 index 6b3bd9f9..00000000 --- a/TournamentManager/TournamentManager/Ranking/StandardRankComparer.cs +++ /dev/null @@ -1,76 +0,0 @@ -namespace TournamentManager.Ranking; - -/// -/// The standard used with Mixedliga Augsburg. -/// -internal class StandardRankComparer : IRankComparer -{ - private static bool _directCompareRankingInProgress = false; - - /// - /// Required CTOR for Activator.CreateInstance(...) - /// - internal StandardRankComparer() - { } - - internal StandardRankComparer(Ranking ranking, DateTime upperDateLimit) - { - Ranking = ranking; - UpperDateLimit = upperDateLimit; - } - - public DateTime UpperDateLimit { get; set; } - - public Ranking? Ranking { get; set; } - - public int Compare(Rank? x, Rank? y) - { - if (Ranking is null) throw new NullReferenceException("Ranking property must not be null"); - - if (x == null || y == null) throw new NullReferenceException($"{nameof(Rank)} arguments must not be null"); - - // sort down teams with no matches played - if (x.MatchesPlayed == 0) return 1; - if (y.MatchesPlayed == 0) return -1; - - // sort down overruled results with no points at all - if (x.MatchPoints.Home + x.MatchPoints.Guest + x.SetPoints.Home + x.SetPoints.Guest + x.BallPoints.Home + x.BallPoints.Guest == 0) - return 1; - if (y.MatchPoints.Home + y.MatchPoints.Guest + y.SetPoints.Home + y.SetPoints.Guest + y.BallPoints.Home + y.BallPoints.Guest == 0) - return -1; - - if ((x.MatchPoints.Home - x.MatchPoints.Guest) < (y.MatchPoints.Home - y.MatchPoints.Guest)) - return 1; - else if ((x.MatchPoints.Home - x.MatchPoints.Guest) > (y.MatchPoints.Home - y.MatchPoints.Guest)) - return -1; - - if ((x.SetPoints.Home - x.SetPoints.Guest) < (y.SetPoints.Home - y.SetPoints.Guest)) - return 1; - else if ((x.SetPoints.Home - x.SetPoints.Guest) > (y.SetPoints.Home - y.SetPoints.Guest)) - return -1; - - if ((x.BallPoints.Home - x.BallPoints.Guest) < (y.BallPoints.Home - y.BallPoints.Guest)) - return 1; - else if ((x.BallPoints.Home - x.BallPoints.Guest) > (y.BallPoints.Home - y.BallPoints.Guest)) - return -1; - - // avoid an infinite loop - if (!_directCompareRankingInProgress && x.MatchesPlayed > 0 && y.MatchesPlayed > 0) - { - _directCompareRankingInProgress = true; - var directCompareRanking = Ranking.GetList(new[] { x.TeamId, y.TeamId }, UpperDateLimit); - _directCompareRankingInProgress = false; - - if (directCompareRanking[0].TeamId == x.TeamId) - return -1; - else - return 1; - } - else - { - // if directCompareRanking is reached twice, both teams must have the same score, - // so we return a random winner by comparing Team Ids - return (x.TeamId < y.TeamId ? 1 : -1); - } - } -} \ No newline at end of file From bedd0b4238c7bd29f71ee47b035d66214f1f0b9f Mon Sep 17 00:00:00 2001 From: axunonb Date: Sun, 17 Sep 2023 23:17:32 +0200 Subject: [PATCH 2/4] Complete refactoring of RankComparer and Ranking * Add property RankComparison to IRankComparer * Remove setters from all PointResult properties * RankComparer.Ranking is not nullable * Fix initialization of RankingChart.GraphBackgroundColorArgb and RankingChart.PlotAreaBackgroundColorArgb * RankingTeamHistory: Remove parameterless constructor Add unit tests. TournamentManager.Ranking now has 93% coverage Bump version to v6.6.0 --- Directory.Build.props | 4 +- .../Ranking/GeneralRankingTests.cs | 118 +++++++ .../HroThreeWinningSetsRankComparisonTests.cs | 291 ++++++++++++------ .../Ranking/LegacyRankComparisonTests.cs | 175 +++++++++++ .../Ranking/RankingTestUtilities.cs | 94 ++++++ .../ThreeWinningSetsRankComparisonTests.cs | 175 +++++++++++ .../TwoWinningSetsRankComparisonTests.cs | 211 +++++++++++++ .../Ranking/IRankComparer.cs | 7 +- .../TournamentManager/Ranking/Rank.cs | 10 +- .../TournamentManager/Ranking/RankComparer.cs | 23 +- .../TournamentManager/Ranking/Ranking.cs | 8 +- .../TournamentManager/Ranking/RankingChart.cs | 14 +- .../Ranking/RankingTeamHistory.cs | 7 +- 13 files changed, 1013 insertions(+), 124 deletions(-) create mode 100644 TournamentManager/TournamentManager.Tests/Ranking/GeneralRankingTests.cs create mode 100644 TournamentManager/TournamentManager.Tests/Ranking/LegacyRankComparisonTests.cs create mode 100644 TournamentManager/TournamentManager.Tests/Ranking/RankingTestUtilities.cs create mode 100644 TournamentManager/TournamentManager.Tests/Ranking/ThreeWinningSetsRankComparisonTests.cs create mode 100644 TournamentManager/TournamentManager.Tests/Ranking/TwoWinningSetsRankComparisonTests.cs diff --git a/Directory.Build.props b/Directory.Build.props index 219861e4..8fc9107c 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -7,8 +7,8 @@ Copyright 2011-$(CurrentYear) axuno gGmbH https://github.com/axuno/Volleyball-League true - 6.5.0 - 6.5.0 + 6.6.0 + 6.6.0 6.0.0.0 latest enable diff --git a/TournamentManager/TournamentManager.Tests/Ranking/GeneralRankingTests.cs b/TournamentManager/TournamentManager.Tests/Ranking/GeneralRankingTests.cs new file mode 100644 index 00000000..a0245ecb --- /dev/null +++ b/TournamentManager/TournamentManager.Tests/Ranking/GeneralRankingTests.cs @@ -0,0 +1,118 @@ +using FluentAssertions; +using NUnit.Framework; +using TournamentManager.DAL.EntityClasses; +using TournamentManager.DAL.TypedViewClasses; +using TournamentManager.Ranking; + +namespace TournamentManager.Tests.Ranking; + +[TestFixture] +internal class GeneralRankingTests +{ + [TestCase(RankComparison.LegacyRankComparison)] + [TestCase(RankComparison.TwoWinningSetsRankComparison)] + [TestCase(RankComparison.ThreeWinningSetsRankComparison)] + [TestCase(RankComparison.HroThreeWinningSetsRankComparison)] + public void CreateRankingWithRankComparisonTests(RankComparison comparison) + { + var ranking = new TournamentManager.Ranking.Ranking(new List(), + new List(), comparison); + + Assert.That(ranking.RankComparer.RankComparison, Is.EqualTo(comparison)); + Assert.That(ranking.RankComparer.Description.Length, Is.AtLeast(1)); + } + + [Test] + public void CreateRankingWithUnknownRankComparisonShouldThrow() + { + Assert.That(() => + { + var ranking = new TournamentManager.Ranking.Ranking(new List(), + new List(), (RankComparison) int.MaxValue); + + }, Throws.Exception.TypeOf()); + + + } + + [Test] + public void RankPointsCalculationTests() + { + var matchId = 1; + var matches = new List { + RankingTestUtilities.GetMatch(matchId++, 2, 1, "25:1 25:2 25:3"), + RankingTestUtilities.GetMatch(matchId, 1, 2, "1:25 2:25 25:3 25:4 15:14") + }; + + var ranking = new TournamentManager.Ranking.Ranking(RankingTestUtilities.CreateMatchCompleteRows(matches), + new List { new() { HomeTeamId = 3, GuestTeamId = 4 } }, + RankComparison.ThreeWinningSetsRankComparison); + + var ranks = ranking.GetList(out var lastUpdateOn); + var r1 = ranks[0]; + + Assert.Multiple(() => + { + Assert.That(lastUpdateOn, Is.EqualTo(DateTime.MinValue)); + Assert.That(ranks.LastUpdatedOn, Is.EqualTo(lastUpdateOn)); + Assert.That(r1.MatchesPlayed, Is.EqualTo(2)); + Assert.That(r1.MatchesToPlay, Is.EqualTo(0)); + Assert.That(ranks[3].MatchesToPlay, Is.EqualTo(1)); + + Assert.That(ranks.Count, Is.EqualTo(4)); // 2 played for Team Ids 1 and 2, 2 to play for Team Ids 3 and 4 + Assert.That(r1.TeamId, Is.EqualTo(2)); + Assert.That(r1.ToString(), Is.EqualTo("1")); + + r1.MatchesWon.Should().BeEquivalentTo(new PointResult(1, 1)); + r1.MatchPoints.Should().BeEquivalentTo(new PointResult(4, 2)); + r1.SetsWon.Should().BeEquivalentTo(new PointResult(5, 3)); + r1.SetPoints.Should().BeEquivalentTo(new PointResult(5, 3)); + r1.BallPoints.Should().BeEquivalentTo(new PointResult(146, 74)); + }); + } + + [Test] + public void GetMatchDaysTest() + { + var matchId = 1; + var completedMatches = RankingTestUtilities.CreateMatchCompleteRows(new List { + RankingTestUtilities.GetMatch(matchId++, 2, 1, "25:1 25:2 25:3"), + RankingTestUtilities.GetMatch(matchId, 1, 2, "25:1 25:2 25:3") + }); + completedMatches[0].MatchDate = new DateTime(2024, 7, 1); + completedMatches[1].MatchDate = new DateTime(2024, 7, 2); + + var ranking = new TournamentManager.Ranking.Ranking(completedMatches, + new List(), RankComparison.ThreeWinningSetsRankComparison); + + var matchDays = ranking.GetMatchDays(); + + Assert.That(matchDays.Count, Is.EqualTo(2)); + } + + [Test] + public void GetRankingHistoryTest() + { + var matchId = 1; + var completedMatches = RankingTestUtilities.CreateMatchCompleteRows(new List { + RankingTestUtilities.GetMatch(matchId++, 2, 1, "25:1 25:2 25:3"), + RankingTestUtilities.GetMatch(matchId, 1, 2, "25:3 25:4 25:5") + }); + completedMatches[0].MatchDate = new DateTime(2024, 7, 1); + completedMatches[1].MatchDate = new DateTime(2024, 7, 2); + + var ranking = new TournamentManager.Ranking.Ranking(completedMatches, + new List(), RankComparison.ThreeWinningSetsRankComparison); + + var history = ranking.GetRankingHistory(); + history.ReCalculate(); + history = ranking.GetRankingHistory(); + var chart = new RankingChart(ranking, new List<(long TeamId, string TeamName)> { (1, "1"), (2, "2") }, + new RankingChart.ChartSettings()) { UseMatchDayMarker = true, ShowUpperDateLimit = true }; + + Assert.That(history.GetMatchDays().Count, Is.EqualTo(2)); + Assert.That(history.GetByTeam(1).Count, Is.EqualTo(2)); + Assert.That(history.GetByMatchDay().Count, Is.EqualTo(2)); + Assert.That(() => { chart.GetSvg(); chart.GetPng(); }, Throws.Nothing); + } +} diff --git a/TournamentManager/TournamentManager.Tests/Ranking/HroThreeWinningSetsRankComparisonTests.cs b/TournamentManager/TournamentManager.Tests/Ranking/HroThreeWinningSetsRankComparisonTests.cs index a536c4b6..4cbd656e 100644 --- a/TournamentManager/TournamentManager.Tests/Ranking/HroThreeWinningSetsRankComparisonTests.cs +++ b/TournamentManager/TournamentManager.Tests/Ranking/HroThreeWinningSetsRankComparisonTests.cs @@ -1,6 +1,5 @@ using NUnit.Framework; using TournamentManager.DAL.EntityClasses; -using TournamentManager.ExtensionMethods; using TournamentManager.DAL.TypedViewClasses; using TournamentManager.Ranking; @@ -14,11 +13,11 @@ public void Two_Teams_Are_Fully_Equal_With_Match_Results() { var matchId = 1; var matches = new List { - GetMatch(matchId++, 1, 2, "25:0 25:0 25:0"), - GetMatch(matchId, 2, 1, "25:0 25:0 25:0") + RankingTestUtilities.GetMatch(matchId++, 1, 2, "25:0 25:0 25:0"), + RankingTestUtilities.GetMatch(matchId, 2, 1, "25:0 25:0 25:0") }; - var ranking = new TournamentManager.Ranking.Ranking(CreateMatchCompleteRows(matches), + var ranking = new TournamentManager.Ranking.Ranking(RankingTestUtilities.CreateMatchCompleteRows(matches), new List(), RankComparison.HroThreeWinningSetsRankComparison); var rl= ranking.GetList(out var updatedOn); @@ -30,67 +29,220 @@ public void Two_Teams_Are_Fully_Equal_With_Match_Results() Assert.That(rl[0].TeamId, Is.EqualTo(2)); } - //[TestCase("25:0 25:0 25:0", "0:25 0:25 0:25", 1, 2)] - [TestCase("0:25 0:25 0:25", "25:0 25:0 25:0", 2, 1)] - public void Ranking_Based_On_MatchPoints_Won(string xResult, string yResult, long expected1,long expected2) + [Test] + public void MatchPointsWonDecideTest() { - var matchId = 1; - var matches = new List { - GetMatch(matchId++, 1, 2, xResult), - GetMatch(matchId, 2, 1, yResult) + var comparer = new RankComparer(RankComparison.HroThreeWinningSetsRankComparison); + var ranks = new List { + new() { + TeamId = 1, + MatchPoints = { Home = 3, Guest = 0 }, // one match won + MatchesWon = { Home = 1, Guest = 0 }, + SetPoints = { Home = 3, Guest = 1 }, + SetsWon = { Home = 3, Guest = 1 }, + BallPoints = { Home = 75, Guest = 0 }, + MatchesPlayed = 1 + }, + new() { + TeamId = 2, + MatchPoints = { Home = 0, Guest = 3 }, // no match won + MatchesWon = { Home = 0, Guest = 1 }, + SetPoints = { Home = 0, Guest = 3 }, + SetsWon = { Home = 0, Guest = 3 }, + BallPoints = { Home = 0, Guest = 25 }, + MatchesPlayed = 1 + } }; - var ranking = new TournamentManager.Ranking.Ranking(CreateMatchCompleteRows(matches), - new List(), RankComparison.HroThreeWinningSetsRankComparison); + ranks.Sort(comparer); + var teamId1 = ranks[0].TeamId; + // swap x and y of sorted list for the comparer + ranks = new List { ranks[1], ranks[0] }; + ranks.Sort(comparer); + var teamId2 = ranks[0].TeamId; - var rl= ranking.GetList(out var updatedOn); + Assert.That(teamId1, Is.EqualTo(1)); + Assert.That(teamId2, Is.EqualTo(1)); + } - Assert.That(rl.Count, Is.EqualTo(2)); - Assert.That(rl[0].TeamId, Is.EqualTo(expected1)); - Assert.That(rl[1].TeamId, Is.EqualTo(expected2)); + [Test] + public void MatchesWonDecideTest() + { + var comparer = new RankComparer(RankComparison.HroThreeWinningSetsRankComparison); + var ranks = new List { + new() { + TeamId = 1, + MatchPoints = { Home = 0, Guest = 0 }, + MatchesWon = { Home = 1, Guest = 0 }, // one match won + SetPoints = { Home = 3, Guest = 1 }, + SetsWon = { Home = 3, Guest = 1 }, + BallPoints = { Home = 75, Guest = 0 }, + MatchesPlayed = 1 + }, + new() { + TeamId = 2, + MatchPoints = { Home = 0, Guest = 0 }, + MatchesWon = { Home = 0, Guest = 0 },// no match won + SetPoints = { Home = 0, Guest = 3 }, + SetsWon = { Home = 0, Guest = 3 }, + BallPoints = { Home = 0, Guest = 25 }, + MatchesPlayed = 1 + } + }; + + ranks.Sort(comparer); + var teamId1 = ranks[0].TeamId; + // swap x and y of sorted list for the comparer + ranks = new List { ranks[0], ranks[1] }; + ranks.Sort(comparer); + var teamId2 = ranks[0].TeamId; + + Assert.That(teamId1, Is.EqualTo(1)); + Assert.That(teamId2, Is.EqualTo(1)); } + [Test] + public void SetPointsDifferenceDecidesTest() + { + var comparer = new RankComparer(RankComparison.HroThreeWinningSetsRankComparison); + var ranks = new List { + new() { + TeamId = 1, + MatchPoints = { Home = 3, Guest = 0 }, + MatchesWon = { Home = 1, Guest = 0 }, + SetPoints = { Home = 3, Guest = 1 }, // one set lost + SetsWon = { Home = 3, Guest = 1 }, + BallPoints = { Home = 50, Guest = 25 }, + MatchesPlayed = 1 + }, + new() { + TeamId = 2, + MatchPoints = { Home = 3, Guest = 0 }, + MatchesWon = { Home = 1, Guest = 0 }, + SetPoints = { Home = 3, Guest = 0 }, // no set lost + SetsWon = { Home = 3, Guest = 0 }, + BallPoints = { Home = 75, Guest = 0 }, + MatchesPlayed = 1 + } + }; + + ranks.Sort(comparer); + var teamId1 = ranks[0].TeamId; + // swap x and y of sorted list for the comparer + ranks = new List { ranks[0], ranks[1] }; + ranks.Sort(comparer); + var teamId2 = ranks[0].TeamId; + + Assert.That(teamId1, Is.EqualTo(2)); + Assert.That(teamId2, Is.EqualTo(2)); + } + + [Test] + public void SetsWonDecideTest() + { + var comparer = new RankComparer(RankComparison.HroThreeWinningSetsRankComparison); + var ranks = new List { + new() { + TeamId = 1, + MatchPoints = { Home = 3, Guest = 0 }, + MatchesWon = { Home = 1, Guest = 0 }, + SetPoints = { Home = 3, Guest = 0 }, + SetsWon = { Home = 4, Guest = 0 }, // 4 sets won + BallPoints = { Home = 75, Guest = 0 }, + MatchesPlayed = 1 + }, + new() { + TeamId = 2, + MatchPoints = { Home = 3, Guest = 0 }, + MatchesWon = { Home = 1, Guest = 0 }, + SetPoints = { Home = 3, Guest = 0 }, + SetsWon = { Home = 3, Guest = 0 }, // 3 sets won + BallPoints = { Home = 75, Guest = 0 }, + MatchesPlayed = 1 + } + }; + + ranks.Sort(comparer); + var teamId1 = ranks[0].TeamId; + // swap x and y of sorted list for the comparer + ranks = new List { ranks[1], ranks[0] }; + ranks.Sort(comparer); + var teamId2 = ranks[0].TeamId; - private IList CreateMatchCompleteRows(List matches) + Assert.That(teamId1, Is.EqualTo(1)); + Assert.That(teamId2, Is.EqualTo(1)); + } + + [Test] + public void BallPointsDifferenceDecidesTest() { - var completeMatches = new List(); - foreach (var match in matches) - { - completeMatches.Add(new MatchCompleteRawRow { - Id = match.Id, - HomeTeamId = match.HomeTeamId, - GuestTeamId = match.GuestTeamId, - HomeMatchPoints = match.HomePoints, - GuestMatchPoints = match.GuestPoints, - HomeSetPoints = match.Sets.Sum(s => s.HomeSetPoints), - GuestSetPoints = match.Sets.Sum(s => s.GuestSetPoints), - HomeBallPoints = match.Sets.Sum(s => s.HomeBallPoints), - GuestBallPoints= match.Sets.Sum(s => s.GuestBallPoints), - MatchDate = match.RealStart - }); - } - - return completeMatches; + var comparer = new RankComparer(RankComparison.HroThreeWinningSetsRankComparison); + var ranks = new List { + new() { + TeamId = 1, + MatchPoints = { Home = 3, Guest = 0 }, + MatchesWon = { Home = 1, Guest = 0 }, + SetPoints = { Home = 3, Guest = 0 }, + SetsWon = { Home = 3, Guest = 0 }, + BallPoints = { Home = 75, Guest = 1 }, // one ball point lost + MatchesPlayed = 1 + }, + new() { + TeamId = 2, + MatchPoints = { Home = 3, Guest = 0 }, + MatchesWon = { Home = 1, Guest = 0 }, + SetPoints = { Home = 3, Guest = 0 }, + SetsWon = { Home = 3, Guest = 0 }, + BallPoints = { Home = 75, Guest = 0 }, // no ball point lost + MatchesPlayed = 1 + } + }; + + ranks.Sort(comparer); + var teamId1 = ranks[0].TeamId; + // swap x and y of sorted list for the comparer + ranks = new List { ranks[0], ranks[1] }; + ranks.Sort(comparer); + var teamId2 = ranks[0].TeamId; + + Assert.That(teamId1, Is.EqualTo(2)); + Assert.That(teamId2, Is.EqualTo(2)); } - private MatchEntity GetMatch(long matchId, long home, long guest, string setResults) + [Test] + public void BallPointsWonDecideTest() { - var matchRule = GetMatchRule_TieBreakRule(); - var setRule = GetSetRule(); - var dateTime = DateTime.UtcNow.Date.AddHours(-4); - var match = new MatchEntity(matchId) { - HomeTeamId = home, - GuestTeamId = guest, - RealStart = dateTime, - RealEnd = dateTime.AddHours(2), - CreatedOn = dateTime, - ModifiedOn = dateTime + var comparer = new RankComparer(RankComparison.HroThreeWinningSetsRankComparison); + var ranks = new List { + new() { + TeamId = 1, + MatchPoints = { Home = 3, Guest = 0 }, + MatchesWon = { Home = 1, Guest = 0 }, + SetPoints = { Home = 3, Guest = 0 }, + SetsWon = { Home = 3, Guest = 0 }, + BallPoints = { Home = 76, Guest = 1 }, // same ball point difference, but 1 more won + MatchesPlayed = 1 + }, + new() { + TeamId = 2, + MatchPoints = { Home = 3, Guest = 0 }, + MatchesWon = { Home = 1, Guest = 0 }, + SetPoints = { Home = 3, Guest = 0 }, + SetsWon = { Home = 3, Guest = 0 }, + BallPoints = { Home = 75, Guest = 0 }, + MatchesPlayed = 1 + } }; - match.Sets.Add(match.Id, setResults); - _ = match.Sets.CalculateSetPoints(setRule, matchRule); - _ = match.CalculateMatchPoints(matchRule); - return match; + ranks.Sort(comparer); + var teamId1 = ranks[0].TeamId; + // swap x and y of sorted list for the comparer + ranks = new List { ranks[1], ranks[0] }; + ranks.Sort(comparer); + var teamId2 = ranks[0].TeamId; + + Assert.That(teamId1, Is.EqualTo(1)); + Assert.That(teamId2, Is.EqualTo(1)); } /* @@ -443,43 +595,6 @@ public void CreateMatches() //return new List(); } */ - private static MatchRuleEntity GetMatchRule_NoTieBreakRule() - { - return new MatchRuleEntity { - BestOf = true, - NumOfSets = 3, - PointsMatchWon = 3, - PointsMatchLost = 0, - PointsMatchTie = 1, - RankComparer = (int) RankComparison.HroThreeWinningSetsRankComparison - }; - } - private static MatchRuleEntity GetMatchRule_TieBreakRule() - { - return new MatchRuleEntity { - BestOf = true, - NumOfSets = 3, - PointsMatchWon = 3, - PointsMatchLost = 0, - PointsMatchWonAfterTieBreak = 2, - PointsMatchLostAfterTieBreak = 1, - PointsMatchTie = 1, - RankComparer = (int) RankComparison.HroThreeWinningSetsRankComparison - }; - } - - private static SetRuleEntity GetSetRule() - { - return new SetRuleEntity { - NumOfPointsToWinRegular = 25, - NumOfPointsToWinTiebreak = 15, - PointsDiffToWinRegular = 2, - PointsDiffToWinTiebreak = 2, - PointsSetWon = 1, - PointsSetLost = 0, - PointsSetTie = 0 - }; - } } diff --git a/TournamentManager/TournamentManager.Tests/Ranking/LegacyRankComparisonTests.cs b/TournamentManager/TournamentManager.Tests/Ranking/LegacyRankComparisonTests.cs new file mode 100644 index 00000000..1110f7cf --- /dev/null +++ b/TournamentManager/TournamentManager.Tests/Ranking/LegacyRankComparisonTests.cs @@ -0,0 +1,175 @@ +using NUnit.Framework; +using TournamentManager.DAL.EntityClasses; +using TournamentManager.DAL.TypedViewClasses; +using TournamentManager.Ranking; + +namespace TournamentManager.Tests.Ranking; + +[TestFixture] +public class LegacyRankComparisonTests +{ + [Test] + public void Two_Teams_Are_Fully_Equal_With_Match_Results() + { + var matchId = 1; + var matches = new List { + RankingTestUtilities.GetMatch(matchId++, 1, 2, "25:0 25:0 25:0"), + RankingTestUtilities.GetMatch(matchId, 2, 1, "25:0 25:0 25:0") + }; + + var ranking = new TournamentManager.Ranking.Ranking(RankingTestUtilities.CreateMatchCompleteRows(matches), + new List(), RankComparison.LegacyRankComparison); + + var rl= ranking.GetList(out var updatedOn); + + Assert.That(rl.Count, Is.EqualTo(2)); + Assert.That(rl[0].Number, Is.EqualTo(1)); + Assert.That(rl[1].Number, Is.EqualTo(2)); + // Note: Even the direct comparison between teams is the same, so the team with the higher team ID is returned. + Assert.That(rl[0].TeamId, Is.EqualTo(2)); + } + + [Test] + public void MatchPointsDifferenceDecidesTest() + { + var comparer = new RankComparer(RankComparison.LegacyRankComparison); + var ranks = new List { + new() { + TeamId = 1, + MatchPoints = { Home = 3, Guest = 0 }, // one match won + MatchesWon = { Home = 1, Guest = 0 }, + SetPoints = { Home = 3, Guest = 1 }, + SetsWon = { Home = 3, Guest = 1 }, + BallPoints = { Home = 75, Guest = 0 }, + MatchesPlayed = 1 + }, + new() { + TeamId = 2, + MatchPoints = { Home = 0, Guest = 3 }, // no match won + MatchesWon = { Home = 0, Guest = 1 }, + SetPoints = { Home = 0, Guest = 3 }, + SetsWon = { Home = 0, Guest = 3 }, + BallPoints = { Home = 0, Guest = 25 }, + MatchesPlayed = 1 + } + }; + + ranks.Sort(comparer); + var teamId1 = ranks[0].TeamId; + // swap x and y of sorted list for the comparer + ranks = new List { ranks[1], ranks[0] }; + ranks.Sort(comparer); + var teamId2 = ranks[0].TeamId; + + Assert.That(teamId1, Is.EqualTo(1)); + Assert.That(teamId2, Is.EqualTo(1)); + } + + [Test] + public void SetPointsDifferenceDecidesTest() + { + var comparer = new RankComparer(RankComparison.LegacyRankComparison); + var ranks = new List { + new() { + TeamId = 1, + MatchPoints = { Home = 3, Guest = 0 }, + MatchesWon = { Home = 1, Guest = 0 }, + SetPoints = { Home = 3, Guest = 1 }, // one set lost + SetsWon = { Home = 3, Guest = 1 }, + BallPoints = { Home = 50, Guest = 25 }, + MatchesPlayed = 1 + }, + new() { + TeamId = 2, + MatchPoints = { Home = 3, Guest = 0 }, + MatchesWon = { Home = 1, Guest = 0 }, + SetPoints = { Home = 3, Guest = 0 }, // no set lost + SetsWon = { Home = 3, Guest = 0 }, + BallPoints = { Home = 75, Guest = 0 }, + MatchesPlayed = 1 + } + }; + + ranks.Sort(comparer); + var teamId1 = ranks[0].TeamId; + // swap x and y of sorted list for the comparer + ranks = new List { ranks[0], ranks[1] }; + ranks.Sort(comparer); + var teamId2 = ranks[0].TeamId; + + Assert.That(teamId1, Is.EqualTo(2)); + Assert.That(teamId2, Is.EqualTo(2)); + } + + [Test] + public void BallPointsDifferenceDecidesTest() + { + var comparer = new RankComparer(RankComparison.LegacyRankComparison); + var ranks = new List { + new() { + TeamId = 1, + MatchPoints = { Home = 3, Guest = 0 }, + MatchesWon = { Home = 1, Guest = 0 }, + SetPoints = { Home = 3, Guest = 0 }, + SetsWon = { Home = 3, Guest = 0 }, + BallPoints = { Home = 75, Guest = 1 }, // one ball point lost + MatchesPlayed = 1 + }, + new() { + TeamId = 2, + MatchPoints = { Home = 3, Guest = 0 }, + MatchesWon = { Home = 1, Guest = 0 }, + SetPoints = { Home = 3, Guest = 0 }, + SetsWon = { Home = 3, Guest = 0 }, + BallPoints = { Home = 75, Guest = 0 }, // no ball point lost + MatchesPlayed = 1 + } + }; + + ranks.Sort(comparer); + var teamId1 = ranks[0].TeamId; + // swap x and y of sorted list for the comparer + ranks = new List { ranks[0], ranks[1] }; + ranks.Sort(comparer); + var teamId2 = ranks[0].TeamId; + + Assert.That(teamId1, Is.EqualTo(2)); + Assert.That(teamId2, Is.EqualTo(2)); + } + + [Test] + public void BallPointsWonDecideTest() + { + var comparer = new RankComparer(RankComparison.LegacyRankComparison); + var ranks = new List { + new() { + TeamId = 1, + MatchPoints = { Home = 3, Guest = 0 }, + MatchesWon = { Home = 1, Guest = 0 }, + SetPoints = { Home = 3, Guest = 0 }, + SetsWon = { Home = 3, Guest = 0 }, + BallPoints = { Home = 76, Guest = 0 }, // one more ball point + MatchesPlayed = 1 + }, + new() { + TeamId = 2, + MatchPoints = { Home = 3, Guest = 0 }, + MatchesWon = { Home = 1, Guest = 0 }, + SetPoints = { Home = 3, Guest = 0 }, + SetsWon = { Home = 3, Guest = 0 }, + BallPoints = { Home = 75, Guest = 0 }, + MatchesPlayed = 1 + } + }; + + ranks.Sort(comparer); + var teamId1 = ranks[0].TeamId; + // swap x and y of sorted list for the comparer + ranks = new List { ranks[1], ranks[0] }; + ranks.Sort(comparer); + var teamId2 = ranks[0].TeamId; + + Assert.That(teamId1, Is.EqualTo(1)); + Assert.That(teamId2, Is.EqualTo(1)); + } +} diff --git a/TournamentManager/TournamentManager.Tests/Ranking/RankingTestUtilities.cs b/TournamentManager/TournamentManager.Tests/Ranking/RankingTestUtilities.cs new file mode 100644 index 00000000..104ecf2a --- /dev/null +++ b/TournamentManager/TournamentManager.Tests/Ranking/RankingTestUtilities.cs @@ -0,0 +1,94 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using TournamentManager.DAL.EntityClasses; +using TournamentManager.DAL.TypedViewClasses; +using TournamentManager.ExtensionMethods; +using TournamentManager.Ranking; + +namespace TournamentManager.Tests.Ranking; +internal class RankingTestUtilities +{ + public static MatchEntity GetMatch(long matchId, long home, long guest, string setResults) + { + var matchRule = GetMatchRule_TieBreakRule(); + var setRule = GetSetRule(); + var dateTime = DateTime.UtcNow.Date.AddHours(-4); + var match = new MatchEntity(matchId) { + HomeTeamId = home, + GuestTeamId = guest, + RealStart = dateTime, + RealEnd = dateTime.AddHours(2), + CreatedOn = dateTime, + ModifiedOn = dateTime + }; + match.Sets.Add(match.Id, setResults); + _ = match.Sets.CalculateSetPoints(setRule, matchRule); + _ = match.CalculateMatchPoints(matchRule); + + return match; + } + + public static IList CreateMatchCompleteRows(List matches) + { + var completeMatches = new List(); + foreach (var match in matches) + { + completeMatches.Add(new MatchCompleteRawRow { + Id = match.Id, + HomeTeamId = match.HomeTeamId, + GuestTeamId = match.GuestTeamId, + HomeMatchPoints = match.HomePoints, + GuestMatchPoints = match.GuestPoints, + HomeSetPoints = match.Sets.Sum(s => s.HomeSetPoints), + GuestSetPoints = match.Sets.Sum(s => s.GuestSetPoints), + HomeBallPoints = match.Sets.Sum(s => s.HomeBallPoints), + GuestBallPoints= match.Sets.Sum(s => s.GuestBallPoints), + MatchDate = match.RealStart + }); + } + + return completeMatches; + } + + public static MatchRuleEntity GetMatchRule_NoTieBreakRule() + { + return new MatchRuleEntity { + BestOf = true, + NumOfSets = 3, + PointsMatchWon = 3, + PointsMatchLost = 0, + PointsMatchTie = 1, + RankComparer = (int) RankComparison.HroThreeWinningSetsRankComparison + }; + } + + public static MatchRuleEntity GetMatchRule_TieBreakRule() + { + return new MatchRuleEntity { + BestOf = true, + NumOfSets = 3, + PointsMatchWon = 3, + PointsMatchLost = 0, + PointsMatchWonAfterTieBreak = 2, + PointsMatchLostAfterTieBreak = 1, + PointsMatchTie = 1, + RankComparer = (int) RankComparison.HroThreeWinningSetsRankComparison + }; + } + + public static SetRuleEntity GetSetRule() + { + return new SetRuleEntity { + NumOfPointsToWinRegular = 25, + NumOfPointsToWinTiebreak = 15, + PointsDiffToWinRegular = 2, + PointsDiffToWinTiebreak = 2, + PointsSetWon = 1, + PointsSetLost = 0, + PointsSetTie = 0 + }; + } +} diff --git a/TournamentManager/TournamentManager.Tests/Ranking/ThreeWinningSetsRankComparisonTests.cs b/TournamentManager/TournamentManager.Tests/Ranking/ThreeWinningSetsRankComparisonTests.cs new file mode 100644 index 00000000..a1f5a0c3 --- /dev/null +++ b/TournamentManager/TournamentManager.Tests/Ranking/ThreeWinningSetsRankComparisonTests.cs @@ -0,0 +1,175 @@ +using NUnit.Framework; +using TournamentManager.DAL.EntityClasses; +using TournamentManager.DAL.TypedViewClasses; +using TournamentManager.Ranking; + +namespace TournamentManager.Tests.Ranking; + +[TestFixture] +public class ThreeWinningSetsRankComparisonTests +{ + [Test] + public void Two_Teams_Are_Fully_Equal_With_Match_Results() + { + var matchId = 1; + var matches = new List { + RankingTestUtilities.GetMatch(matchId++, 1, 2, "25:0 25:0 25:0"), + RankingTestUtilities.GetMatch(matchId, 2, 1, "25:0 25:0 25:0") + }; + + var ranking = new TournamentManager.Ranking.Ranking(RankingTestUtilities.CreateMatchCompleteRows(matches), + new List(), RankComparison.ThreeWinningSetsRankComparison); + + var rl= ranking.GetList(out var updatedOn); + + Assert.That(rl.Count, Is.EqualTo(2)); + Assert.That(rl[0].Number, Is.EqualTo(1)); + Assert.That(rl[1].Number, Is.EqualTo(2)); + // Note: Even the direct comparison between teams is the same, so the team with the higher team ID is returned. + Assert.That(rl[0].TeamId, Is.EqualTo(2)); + } + + [Test] + public void MatchPointsWonDecideTest() + { + var comparer = new RankComparer(RankComparison.ThreeWinningSetsRankComparison); + var ranks = new List { + new() { + TeamId = 1, + MatchPoints = { Home = 3, Guest = 0 }, // one match won + MatchesWon = { Home = 1, Guest = 0 }, + SetPoints = { Home = 3, Guest = 1 }, + SetsWon = { Home = 3, Guest = 1 }, + BallPoints = { Home = 75, Guest = 0 }, + MatchesPlayed = 1 + }, + new() { + TeamId = 2, + MatchPoints = { Home = 0, Guest = 3 }, // no match won + MatchesWon = { Home = 0, Guest = 1 }, + SetPoints = { Home = 0, Guest = 3 }, + SetsWon = { Home = 0, Guest = 3 }, + BallPoints = { Home = 0, Guest = 25 }, + MatchesPlayed = 1 + } + }; + + ranks.Sort(comparer); + var teamId1 = ranks[0].TeamId; + // swap x and y of sorted list for the comparer + ranks = new List { ranks[1], ranks[0] }; + ranks.Sort(comparer); + var teamId2 = ranks[0].TeamId; + + Assert.That(teamId1, Is.EqualTo(1)); + Assert.That(teamId2, Is.EqualTo(1)); + } + + [Test] + public void MatchesWonDecideTest() + { + var comparer = new RankComparer(RankComparison.ThreeWinningSetsRankComparison); + var ranks = new List { + new() { + TeamId = 1, + MatchPoints = { Home = 0, Guest = 0 }, + MatchesWon = { Home = 1, Guest = 0 }, // one match won + SetPoints = { Home = 3, Guest = 1 }, + SetsWon = { Home = 3, Guest = 1 }, + BallPoints = { Home = 75, Guest = 0 }, + MatchesPlayed = 1 + }, + new() { + TeamId = 2, + MatchPoints = { Home = 0, Guest = 0 }, + MatchesWon = { Home = 0, Guest = 0 },// no match won + SetPoints = { Home = 0, Guest = 3 }, + SetsWon = { Home = 0, Guest = 3 }, + BallPoints = { Home = 0, Guest = 25 }, + MatchesPlayed = 1 + } + }; + + ranks.Sort(comparer); + var teamId1 = ranks[0].TeamId; + // swap x and y of sorted list for the comparer + ranks = new List { ranks[1], ranks[0] }; + ranks.Sort(comparer); + var teamId2 = ranks[0].TeamId; + + Assert.That(teamId1, Is.EqualTo(1)); + Assert.That(teamId2, Is.EqualTo(1)); + } + + [Test] + public void SetPointsRatioDecidesTest() + { + var comparer = new RankComparer(RankComparison.ThreeWinningSetsRankComparison); + var ranks = new List { + new() { + TeamId = 1, + MatchPoints = { Home = 3, Guest = 0 }, + MatchesWon = { Home = 1, Guest = 0 }, + SetPoints = { Home = 3, Guest = 1 }, // one set lost + SetsWon = { Home = 3, Guest = 1 }, + BallPoints = { Home = 50, Guest = 25 }, + MatchesPlayed = 1 + }, + new() { + TeamId = 2, + MatchPoints = { Home = 3, Guest = 0 }, + MatchesWon = { Home = 1, Guest = 0 }, + SetPoints = { Home = 3, Guest = 0 }, // no set lost + SetsWon = { Home = 3, Guest = 0 }, + BallPoints = { Home = 75, Guest = 0 }, + MatchesPlayed = 1 + } + }; + + ranks.Sort(comparer); + var teamId1 = ranks[0].TeamId; + // swap x and y of sorted list for the comparer + ranks = new List { ranks[0], ranks[1] }; + ranks.Sort(comparer); + var teamId2 = ranks[0].TeamId; + + Assert.That(teamId1, Is.EqualTo(2)); + Assert.That(teamId2, Is.EqualTo(2)); + } + + [Test] + public void BallPointsRatioDecidesTest() + { + var comparer = new RankComparer(RankComparison.ThreeWinningSetsRankComparison); + var ranks = new List { + new() { + TeamId = 1, + MatchPoints = { Home = 3, Guest = 0 }, + MatchesWon = { Home = 1, Guest = 0 }, + SetPoints = { Home = 3, Guest = 0 }, + SetsWon = { Home = 3, Guest = 0 }, + BallPoints = { Home = 75, Guest = 1 }, // one ball point lost + MatchesPlayed = 1 + }, + new() { + TeamId = 2, + MatchPoints = { Home = 3, Guest = 0 }, + MatchesWon = { Home = 1, Guest = 0 }, + SetPoints = { Home = 3, Guest = 0 }, + SetsWon = { Home = 3, Guest = 0 }, + BallPoints = { Home = 75, Guest = 0 }, // no ball point lost + MatchesPlayed = 1 + } + }; + + ranks.Sort(comparer); + var teamId1 = ranks[0].TeamId; + // swap x and y of sorted list for the comparer + ranks = new List { ranks[0], ranks[1] }; + ranks.Sort(comparer); + var teamId2 = ranks[0].TeamId; + + Assert.That(teamId1, Is.EqualTo(2)); + Assert.That(teamId2, Is.EqualTo(2)); + } +} diff --git a/TournamentManager/TournamentManager.Tests/Ranking/TwoWinningSetsRankComparisonTests.cs b/TournamentManager/TournamentManager.Tests/Ranking/TwoWinningSetsRankComparisonTests.cs new file mode 100644 index 00000000..7367dcab --- /dev/null +++ b/TournamentManager/TournamentManager.Tests/Ranking/TwoWinningSetsRankComparisonTests.cs @@ -0,0 +1,211 @@ +using NUnit.Framework; +using TournamentManager.DAL.EntityClasses; +using TournamentManager.DAL.TypedViewClasses; +using TournamentManager.Ranking; + +namespace TournamentManager.Tests.Ranking; + +[TestFixture] +public class TwoWinningSetsRankComparisonTests +{ + [Test] + public void Two_Teams_Are_Fully_Equal_With_Match_Results() + { + var matchId = 1; + var matches = new List { + RankingTestUtilities.GetMatch(matchId++, 1, 2, "25:0 25:0"), + RankingTestUtilities.GetMatch(matchId, 2, 1, "25:0 25:0") + }; + + var ranking = new TournamentManager.Ranking.Ranking(RankingTestUtilities.CreateMatchCompleteRows(matches), + new List(), RankComparison.TwoWinningSetsRankComparison); + + var rl= ranking.GetList(out var updatedOn); + + Assert.That(rl.Count, Is.EqualTo(2)); + Assert.That(rl[0].Number, Is.EqualTo(1)); + Assert.That(rl[1].Number, Is.EqualTo(2)); + // Note: Even the direct comparison between teams is the same, so the team with the higher team ID is returned. + Assert.That(rl[0].TeamId, Is.EqualTo(2)); + } + + [Test] + public void MatchPointsWonDecideTest() + { + var comparer = new RankComparer(RankComparison.TwoWinningSetsRankComparison); + var ranks = new List { + new() { + TeamId = 1, + MatchPoints = { Home = 2, Guest = 0 }, // one match won + MatchesWon = { Home = 1, Guest = 0 }, + SetPoints = { Home = 2, Guest = 1 }, + SetsWon = { Home = 2, Guest = 1 }, + BallPoints = { Home = 75, Guest = 0 }, + MatchesPlayed = 1 + }, + new() { + TeamId = 2, + MatchPoints = { Home = 0, Guest = 2 }, // no match won + MatchesWon = { Home = 0, Guest = 1 }, + SetPoints = { Home = 0, Guest = 2 }, + SetsWon = { Home = 0, Guest = 2 }, + BallPoints = { Home = 0, Guest = 25 }, + MatchesPlayed = 1 + } + }; + + ranks.Sort(comparer); + var teamId1 = ranks[0].TeamId; + // swap x and y of sorted list for the comparer + ranks = new List { ranks[1], ranks[0] }; + ranks.Sort(comparer); + var teamId2 = ranks[0].TeamId; + + Assert.That(teamId1, Is.EqualTo(1)); + Assert.That(teamId2, Is.EqualTo(1)); + } + + [Test] + public void SetPointsDifferenceDecidesTest() + { + var comparer = new RankComparer(RankComparison.HroThreeWinningSetsRankComparison); + var ranks = new List { + new() { + TeamId = 1, + MatchPoints = { Home = 2, Guest = 0 }, + MatchesWon = { Home = 1, Guest = 0 }, + SetPoints = { Home = 2, Guest = 1 }, // one set lost + SetsWon = { Home = 2, Guest = 1 }, + BallPoints = { Home = 50, Guest = 25 }, + MatchesPlayed = 1 + }, + new() { + TeamId = 2, + MatchPoints = { Home = 2, Guest = 0 }, + MatchesWon = { Home = 1, Guest = 0 }, + SetPoints = { Home = 2, Guest = 0 }, // no set lost + SetsWon = { Home = 2, Guest = 0 }, + BallPoints = { Home = 50, Guest = 0 }, + MatchesPlayed = 1 + } + }; + + ranks.Sort(comparer); + var teamId1 = ranks[0].TeamId; + // swap x and y of sorted list for the comparer + ranks = new List { ranks[0], ranks[1] }; + ranks.Sort(comparer); + var teamId2 = ranks[0].TeamId; + + Assert.That(teamId1, Is.EqualTo(2)); + Assert.That(teamId2, Is.EqualTo(2)); + } + + [Test] + public void SetsWonDecideTest() + { + var comparer = new RankComparer(RankComparison.HroThreeWinningSetsRankComparison); + var ranks = new List { + new() { + TeamId = 1, + MatchPoints = { Home = 2, Guest = 0 }, + MatchesWon = { Home = 1, Guest = 0 }, + SetPoints = { Home = 2, Guest = 0 }, + SetsWon = { Home = 3, Guest = 0 }, // 3 sets won + BallPoints = { Home = 50, Guest = 0 }, + MatchesPlayed = 1 + }, + new() { + TeamId = 2, + MatchPoints = { Home = 2, Guest = 0 }, + MatchesWon = { Home = 1, Guest = 0 }, + SetPoints = { Home = 2, Guest = 0 }, + SetsWon = { Home = 2, Guest = 0 }, // 3 sets won + BallPoints = { Home = 50, Guest = 0 }, + MatchesPlayed = 1 + } + }; + + ranks.Sort(comparer); + var teamId1 = ranks[0].TeamId; + // swap x and y of sorted list for the comparer + ranks = new List { ranks[1], ranks[0] }; + ranks.Sort(comparer); + var teamId2 = ranks[0].TeamId; + + Assert.That(teamId1, Is.EqualTo(1)); + Assert.That(teamId2, Is.EqualTo(1)); + } + + [Test] + public void BallPointsDifferenceDecidesTest() + { + var comparer = new RankComparer(RankComparison.HroThreeWinningSetsRankComparison); + var ranks = new List { + new() { + TeamId = 1, + MatchPoints = { Home = 2, Guest = 0 }, + MatchesWon = { Home = 1, Guest = 0 }, + SetPoints = { Home = 2, Guest = 0 }, + SetsWon = { Home = 2, Guest = 0 }, + BallPoints = { Home = 50, Guest = 1 }, // one ball point lost + MatchesPlayed = 1 + }, + new() { + TeamId = 2, + MatchPoints = { Home = 2, Guest = 0 }, + MatchesWon = { Home = 1, Guest = 0 }, + SetPoints = { Home = 2, Guest = 0 }, + SetsWon = { Home = 2, Guest = 0 }, + BallPoints = { Home = 50, Guest = 0 }, // no ball point lost + MatchesPlayed = 1 + } + }; + + ranks.Sort(comparer); + var teamId1 = ranks[0].TeamId; + // swap x and y of sorted list for the comparer + ranks = new List { ranks[0], ranks[1] }; + ranks.Sort(comparer); + var teamId2 = ranks[0].TeamId; + + Assert.That(teamId1, Is.EqualTo(2)); + Assert.That(teamId2, Is.EqualTo(2)); + } + + [Test] + public void BallPointsWonDecideTest() + { + var comparer = new RankComparer(RankComparison.HroThreeWinningSetsRankComparison); + var ranks = new List { + new() { + TeamId = 1, + MatchPoints = { Home = 2, Guest = 0 }, + MatchesWon = { Home = 1, Guest = 0 }, + SetPoints = { Home = 2, Guest = 0 }, + SetsWon = { Home = 2, Guest = 0 }, + BallPoints = { Home = 51, Guest = 1 }, // same ball point difference, but 1 more won + MatchesPlayed = 1 + }, + new() { + TeamId = 2, + MatchPoints = { Home = 2, Guest = 0 }, + MatchesWon = { Home = 1, Guest = 0 }, + SetPoints = { Home = 2, Guest = 0 }, + SetsWon = { Home = 2, Guest = 0 }, + BallPoints = { Home = 50, Guest = 0 }, + MatchesPlayed = 1 + } + }; + + ranks.Sort(comparer); + var teamId1 = ranks[0].TeamId; + // swap x and y of sorted list for the comparer + ranks = new List { ranks[1], ranks[0] }; + ranks.Sort(comparer); + var teamId2 = ranks[0].TeamId; + + Assert.That(teamId1, Is.EqualTo(1)); + Assert.That(teamId2, Is.EqualTo(1)); + } +} diff --git a/TournamentManager/TournamentManager/Ranking/IRankComparer.cs b/TournamentManager/TournamentManager/Ranking/IRankComparer.cs index e49a8b5b..091cf82e 100644 --- a/TournamentManager/TournamentManager/Ranking/IRankComparer.cs +++ b/TournamentManager/TournamentManager/Ranking/IRankComparer.cs @@ -5,6 +5,11 @@ /// internal interface IRankComparer : IComparer { + /// + /// The used by the comparer. + /// + public RankComparison RankComparison { get; } + /// /// The description how the comparer works. /// @@ -14,7 +19,7 @@ internal interface IRankComparer : IComparer /// A reference to the class to calculate the ranking table of matches. /// Used by s to compare the match results of 2 neighboring teams. /// - Ranking? Ranking { get; set; } + Ranking Ranking { get; set; } /// /// The maximum of match dates that are included in the ranking table. diff --git a/TournamentManager/TournamentManager/Ranking/Rank.cs b/TournamentManager/TournamentManager/Ranking/Rank.cs index f193c03e..6ddc93ed 100644 --- a/TournamentManager/TournamentManager/Ranking/Rank.cs +++ b/TournamentManager/TournamentManager/Ranking/Rank.cs @@ -28,27 +28,27 @@ public class Rank /// /// The number of matches the team won and lost. /// - public PointResult MatchesWon { get; internal set; } = new(0,0); + public PointResult MatchesWon { get; } = new(0,0); /// /// The number of match points won and lost. /// - public PointResult MatchPoints { get; internal set; } = new(0, 0); + public PointResult MatchPoints { get; } = new(0, 0); /// /// The number of sets won and lost. /// - public PointResult SetsWon { get; internal set; } = new(0, 0); + public PointResult SetsWon { get; } = new(0, 0); /// /// The number of set points won and lost. /// - public PointResult SetPoints { get; internal set; } = new(0, 0); + public PointResult SetPoints { get; } = new(0, 0); /// /// The number of ball points won and lost. /// - public PointResult BallPoints { get; internal set; } = new(0, 0); + public PointResult BallPoints { get; } = new(0, 0); /// /// Gets the rank as a string. diff --git a/TournamentManager/TournamentManager/Ranking/RankComparer.cs b/TournamentManager/TournamentManager/Ranking/RankComparer.cs index e3f4be7c..8c38100a 100644 --- a/TournamentManager/TournamentManager/Ranking/RankComparer.cs +++ b/TournamentManager/TournamentManager/Ranking/RankComparer.cs @@ -1,17 +1,28 @@ namespace TournamentManager.Ranking; + +/// +/// The used to sort a list of s. +/// internal partial class RankComparer : IRankComparer { // if true, a recursive call has started private bool _directCompareRankingInProgress = false; /// - /// Required CTOR for used by . + /// CTOR. /// + /// public RankComparer(RankComparison rankComparison) { SetDefinitions(rankComparison); + RankComparison = rankComparison; } + /// + /// Gets the used by the comparer. + /// + public RankComparison RankComparison { get; } + public delegate bool ComparisonDelegate(Rank x, Rank y, out int result); private List> Comparisons { get; set; } = null!; @@ -20,7 +31,6 @@ public RankComparer(RankComparison rankComparison) /// public int Compare(Rank? x, Rank? y) { - CheckForNull(); System.Diagnostics.Debug.Assert(x != null && y != null, @"Rank arguments must not be null"); foreach (var comparisonDelegate in Comparisons) @@ -38,12 +48,7 @@ public int Compare(Rank? x, Rank? y) public DateTime UpperDateLimit { get; set; } = DateTime.MaxValue; /// - public Ranking? Ranking { get; set; } - - private void CheckForNull() - { - if (Ranking is null) throw new InvalidOperationException("Ranking property must not be null"); - } + public Ranking Ranking { get; set; } = null!; /// /// Teams which have no matches played (yet) go to the end of the ranking table. @@ -298,7 +303,7 @@ private bool DirectComparisonOfTeamsDecides(Rank x, Rank y, out int result) if (!_directCompareRankingInProgress && x.MatchesPlayed > 0 && y.MatchesPlayed > 0) { _directCompareRankingInProgress = true; - var directCompareRanking = Ranking!.GetList(new[] { x.TeamId, y.TeamId }, UpperDateLimit); + var directCompareRanking = Ranking.GetList(new[] { x.TeamId, y.TeamId }, UpperDateLimit); _directCompareRankingInProgress = false; if (directCompareRanking[0].TeamId == x.TeamId) diff --git a/TournamentManager/TournamentManager/Ranking/Ranking.cs b/TournamentManager/TournamentManager/Ranking/Ranking.cs index 12c3e50d..0d1c5ed8 100644 --- a/TournamentManager/TournamentManager/Ranking/Ranking.cs +++ b/TournamentManager/TournamentManager/Ranking/Ranking.cs @@ -7,7 +7,7 @@ namespace TournamentManager.Ranking; /// public class Ranking { - private readonly IRankComparer _rankComparer; + internal readonly IRankComparer RankComparer; /// /// Ctor. @@ -19,7 +19,7 @@ public Ranking(IEnumerable matchesComplete, IEnumerableReturns the for the given upper date limit. public RankingList GetList(DateTime upperDateLimit, out DateTime lastUpdatedOn) { - _rankComparer.UpperDateLimit = upperDateLimit; + RankComparer.UpperDateLimit = upperDateLimit; var rankingList = GetSortedList(GetUnsortedList(upperDateLimit, out lastUpdatedOn)); rankingList.LastUpdatedOn = lastUpdatedOn; rankingList.UpperDateLimit = upperDateLimit; @@ -66,7 +66,7 @@ public RankingList GetList(DateTime upperDateLimit, out DateTime lastUpdatedOn) private RankingList GetSortedList(RankingList rankingList) { - rankingList.Sort(_rankComparer); + rankingList.Sort(RankComparer); for (var i = 0; i < rankingList.Count; i++) { diff --git a/TournamentManager/TournamentManager/Ranking/RankingChart.cs b/TournamentManager/TournamentManager/Ranking/RankingChart.cs index abdde8ef..3b5bcc0e 100644 --- a/TournamentManager/TournamentManager/Ranking/RankingChart.cs +++ b/TournamentManager/TournamentManager/Ranking/RankingChart.cs @@ -7,7 +7,7 @@ namespace TournamentManager.Ranking; /// -/// Class for creating a image from a instance. +/// Class for creating a image from a instance. /// public class RankingChart { @@ -16,10 +16,6 @@ public class RankingChart public class ChartSettings { - public ChartSettings() - { - GraphBackgroundColorArgb = OxyColor.FromRgb(0xEF, 0xFF, 0xEF).ToByteString(); - } /// /// Gets or sets the width of the graph. /// @@ -36,12 +32,12 @@ public ChartSettings() /// /// Gets or sets the graph background color as a decimal byte string with decimal format "{A},{R},{G},{B}" or "#AARRGGBB" in hex format. /// - public string GraphBackgroundColorArgb { get; set; } + public string GraphBackgroundColorArgb { get; set; } = OxyColor.FromRgb(0xEF, 0xFF, 0xEF).ToByteString(); /// /// Gets or sets the plot area background color as a decimal byte string with decimal format "{A},{R},{G},{B}" or "#AARRGGBB" in hex format. /// - public string PlotAreaBackgroundColorArgb { get; set; } = string.Empty; + public string PlotAreaBackgroundColorArgb { get; set; } = OxyColor.FromRgb(0xFF, 0xFF, 0xFF).ToByteString(); /// /// Gets or sets whether the legend will be rendered. @@ -75,7 +71,7 @@ public RankingChart(Ranking ranking, List<(long TeamId, string TeamName)> teams, Ranking = ranking; Settings = settings; _rankingHistory = ranking.GetRankingHistory(); - _teams = teams ?? throw new ArgumentNullException(nameof(teams)); + _teams = teams; } /// @@ -263,4 +259,4 @@ public Stream GetSvg() /// Gets the instance used by the . /// public Ranking Ranking { get; } -} \ No newline at end of file +} diff --git a/TournamentManager/TournamentManager/Ranking/RankingTeamHistory.cs b/TournamentManager/TournamentManager/Ranking/RankingTeamHistory.cs index 6b6c8b3a..f6de0649 100644 --- a/TournamentManager/TournamentManager/Ranking/RankingTeamHistory.cs +++ b/TournamentManager/TournamentManager/Ranking/RankingTeamHistory.cs @@ -2,13 +2,8 @@ namespace TournamentManager.Ranking; public class RankingTeamHistory : Dictionary { - public RankingTeamHistory() - : base() - { - } - public RankingTeamHistory(int capacity) : base(capacity) { } -} \ No newline at end of file +} From 6c172e7826c16a3eb1e0358363ab2df5c0d4a2ea Mon Sep 17 00:00:00 2001 From: axunonb Date: Sun, 17 Sep 2023 23:32:09 +0200 Subject: [PATCH 3/4] Add description for LegacyRankComparer --- .../TournamentManager/Ranking/RankComparer_Definitions.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/TournamentManager/TournamentManager/Ranking/RankComparer_Definitions.cs b/TournamentManager/TournamentManager/Ranking/RankComparer_Definitions.cs index a3bb0f6c..9ba1521a 100644 --- a/TournamentManager/TournamentManager/Ranking/RankComparer_Definitions.cs +++ b/TournamentManager/TournamentManager/Ranking/RankComparer_Definitions.cs @@ -22,6 +22,10 @@ public void SetDefinitions(RankComparison comparerType) } } + /// + /// The rules Augsburg volleyball leagues until August 2023 + /// for 2 or 3 winning set matches (depending on season) + /// private void SetLegacyRankComparer() { Description = From 33c5a48d097a36d6938d6c66de71155157b8c816 Mon Sep 17 00:00:00 2001 From: axunonb Date: Sun, 17 Sep 2023 23:36:24 +0200 Subject: [PATCH 4/4] Correct description for HroThreeWinningSetsRankComparer --- .../TournamentManager/Ranking/RankComparer_Definitions.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/TournamentManager/TournamentManager/Ranking/RankComparer_Definitions.cs b/TournamentManager/TournamentManager/Ranking/RankComparer_Definitions.cs index 9ba1521a..694dade4 100644 --- a/TournamentManager/TournamentManager/Ranking/RankComparer_Definitions.cs +++ b/TournamentManager/TournamentManager/Ranking/RankComparer_Definitions.cs @@ -106,7 +106,7 @@ Only plus points are awarded. } /// - /// The for the rules of the Volleyball-Stadtliga Rostock, starting with season 2023/24 + /// The rules of the Volleyball-Stadtliga Rostock, starting with season 2023/24 /// for 3 winning set matches, and applying the 3-point rule. /// private void SetHroThreeWinningSetsRankComparer()