diff --git a/Axuno.Tools/GermanFederalStates.cs b/Axuno.Tools/GermanFederalStates.cs index 751e5cb3..bf65beb1 100644 --- a/Axuno.Tools/GermanFederalStates.cs +++ b/Axuno.Tools/GermanFederalStates.cs @@ -1,5 +1,4 @@ using System; -using System.Collections.Generic; using System.Collections.ObjectModel; using System.Linq; @@ -68,11 +67,11 @@ public GermanFederalStates() { try { - Contains(this[stateId]); + _ = Contains(this[stateId]); } catch { - throw new Exception(string.Format("GermanFederalStateId \"{0}\" wird in \"{1}\" nicht abgebildet.", stateId, GetType())); + throw new Exception($"GermanFederalStateId \"{stateId}\" is not included in \"{GetType()}\"."); } } #endif @@ -88,8 +87,8 @@ public GermanFederalStates() /// /// Get a GermanFederalState object. /// - /// State abbriviation or state name. - /// Returns the GermanFederalState for the state abbriviation or state name. + /// State abbreviation or state name. + /// Returns the GermanFederalState for the state abbreviation or state name. public GermanFederalState this[string nameOrAbbreviation] { get @@ -100,4 +99,4 @@ public GermanFederalState this[string nameOrAbbreviation] return this.First(fs => fs.Name == nameOrAbbreviation); } } -} \ No newline at end of file +} diff --git a/Axuno.Tools/GermanHolidays.cs b/Axuno.Tools/GermanHolidays.cs index e79c5c72..2157c07a 100644 --- a/Axuno.Tools/GermanHolidays.cs +++ b/Axuno.Tools/GermanHolidays.cs @@ -100,7 +100,7 @@ public bool IsPublicHoliday(GermanFederalStates.Id stateId) public static bool operator ==(GermanHoliday? h1, GermanHoliday? h2) { - if (h1 == null || h2 == null) return false; + if (h1 is null || h2 is null) return false; return h1.Equals(h2); } @@ -162,7 +162,7 @@ public GermanHolidays(int year) { // Plausibility check if (year < 1583 || year > 4099) - throw new Exception("Year must be between 1583 and 4099."); + throw new ArgumentException("Year must be between 1583 and 4099.", nameof(year)); Year = year; _easterSunday = GetEasterSunday(); diff --git a/TournamentManager/TournamentManager/Assets/Holidays.A-Indoor.config b/TournamentManager/TournamentManager/Assets/Holidays.A-Indoor.config deleted file mode 100644 index c63c7414..00000000 --- a/TournamentManager/TournamentManager/Assets/Holidays.A-Indoor.config +++ /dev/null @@ -1,159 +0,0 @@ - - - - - - - - - Public - Augsburger Friedensfest´ - - Bayern - - - - Custom - Heiliger Abend - - - Custom - Rosenmontag - - - Custom - Faschingsdienstag - - - Custom - Silvester - - - - School - 2019-10-28 - 2019-10-31 - Herbstferien - - Bayern - - - - School - 2019-11-20 - 2019-11-20 - Herbstferien - - Bayern - - - - School - 2019-12-23 - 2020-01-04 - Weihnachtsferien - - Bayern - - - - School - 2020-02-24 - 2020-02-28 - Winterferien - - Bayern - - - - School - 2020-04-06 - 2020-04-18 - Osterferien - - Bayern - - - - School - 2020-06-02 - 2020-06-13 - Pfingstferien - - Bayern - - - - School - 2020-07-27 - 2020-09-07 - Sommerferien - - Bayern - - - - - School - 2020-10-31 - 2020-11-06 - Herbstferien - - Bayern - - - - School - 2020-11-18 - 2020-11-18 - Herbstferien - - Bayern - - - - School - 2020-12-23 - 2021-01-09 - Weihnachtsferien - - Bayern - - - - School - 2021-02-15 - 2021-02-19 - Winterferien - - Bayern - - - - School - 2021-03-29 - 2021-04-10 - Osterferien - - Bayern - - - - School - 2021-05-25 - 2021-06-04 - Pfingstferien - - Bayern - - - - School - 2021-07-30 - 2021-09-13 - Sommerferien - - Bayern - - - diff --git a/TournamentManager/TournamentManager/Assets/PublicHolidaysGermany.xlsx b/TournamentManager/TournamentManager/Assets/PublicHolidaysGermany.xlsx deleted file mode 100644 index 330b10b5..00000000 Binary files a/TournamentManager/TournamentManager/Assets/PublicHolidaysGermany.xlsx and /dev/null differ diff --git a/TournamentManager/TournamentManager/Data/AvailableMatchDateRepository.cs b/TournamentManager/TournamentManager/Data/AvailableMatchDateRepository.cs index f2cd0d7e..33f1a94d 100644 --- a/TournamentManager/TournamentManager/Data/AvailableMatchDateRepository.cs +++ b/TournamentManager/TournamentManager/Data/AvailableMatchDateRepository.cs @@ -18,7 +18,7 @@ namespace TournamentManager.Data; /// -/// Class for ExcludedMatchDate related data selections +/// Class for database operations for available match dates. /// public class AvailableMatchDateRepository { @@ -47,8 +47,6 @@ public async Task> GetAvailableMatchD await da.FetchEntityCollectionAsync(qp, cancellationToken); da.CloseConnection(); - _logger.LogDebug("{count} available match dates found", available.Count); - return available; } -} \ No newline at end of file +} diff --git a/TournamentManager/TournamentManager/Data/ExcludedMatchDateRepository.cs b/TournamentManager/TournamentManager/Data/ExcludedMatchDateRepository.cs index 645ed6b8..b25b29d3 100644 --- a/TournamentManager/TournamentManager/Data/ExcludedMatchDateRepository.cs +++ b/TournamentManager/TournamentManager/Data/ExcludedMatchDateRepository.cs @@ -27,6 +27,7 @@ public class ExcludedMatchDateRepository public ExcludedMatchDateRepository(MultiTenancy.IDbContext dbContext) { _dbContext = dbContext; + _logger.LogDebug("Repository created. {Repository} {Identifier}", nameof(TournamentRepository), dbContext.Tenant?.Identifier); } /// @@ -47,8 +48,6 @@ public async Task> GetExcludedMatchDate await da.FetchEntityCollectionAsync(qp, cancellationToken); da.CloseConnection(); - _logger.LogDebug("{count} excluded match dates found", excluded.Count); - return excluded; } @@ -98,4 +97,4 @@ public async Task> GetExcludedMatchDate .AddWithOr(roundFilter).AddWithOr(teamFilter)).Limit(1), cancellationToken)).Cast().FirstOrDefault(); } -} \ No newline at end of file +} diff --git a/TournamentManager/TournamentManager/Data/TournamentRepository.cs b/TournamentManager/TournamentManager/Data/TournamentRepository.cs index 2613cda2..17205868 100644 --- a/TournamentManager/TournamentManager/Data/TournamentRepository.cs +++ b/TournamentManager/TournamentManager/Data/TournamentRepository.cs @@ -28,7 +28,7 @@ public class TournamentRepository public TournamentRepository(MultiTenancy.IDbContext dbContext) { _dbContext = dbContext; - _logger.LogDebug("{repository} created.", nameof(TournamentRepository)); + _logger.LogDebug("Repository created. {Repository} {Identifier}", nameof(TournamentRepository), dbContext.Tenant?.Identifier); } [Obsolete("Use GetTournamentAsync instead", false)] @@ -95,4 +95,4 @@ public virtual EntityCollection GetTournamentRounds(long tournament await da.FetchEntityCollectionAsync(qp, cancellationToken); return t.FirstOrDefault(); } -} \ No newline at end of file +} diff --git a/TournamentManager/TournamentManager/Plan/AvailableMatchDates.cs b/TournamentManager/TournamentManager/Plan/AvailableMatchDates.cs index f4bf8843..dcd33e0b 100644 --- a/TournamentManager/TournamentManager/Plan/AvailableMatchDates.cs +++ b/TournamentManager/TournamentManager/Plan/AvailableMatchDates.cs @@ -38,18 +38,27 @@ internal AvailableMatchDates(ITenantContext tenantContext, _logger = logger; } + /// + /// Clears and loads excluded match dates and available match dates + /// from storage. + /// private async Task Initialize(CancellationToken cancellationToken) { + _logger.LogDebug($"Initializing {nameof(AvailableMatchDates)}"); _excludedMatchDateEntities.Clear(); _excludedMatchDateEntities.AddRange( await _appDb.ExcludedMatchDateRepository.GetExcludedMatchDatesAsync( _tenantContext.TournamentContext.MatchPlanTournamentId, cancellationToken)); + _logger.LogDebug("{count} excluded match dates loaded from storage", _excludedMatchDateEntities.Count); + _availableMatchDateEntities.Clear(); _availableMatchDateEntities.AddRange( await _appDb.AvailableMatchDateRepository.GetAvailableMatchDatesAsync( _tenantContext.TournamentContext.MatchPlanTournamentId, cancellationToken)); + _logger.LogDebug("{count} available match dates loaded from storage", _availableMatchDateEntities.Count); + _generatedAvailableMatchDateEntities.Clear(); } @@ -92,6 +101,32 @@ internal async Task ClearAsync(ClearMatchDates clear, CancellationToken can return deleted; } + /// + /// Checks the for , + /// and for not . + /// + private bool IsVenueAndDateDefined(TeamEntity team) + { + return team is { MatchDayOfWeek: not null, MatchTime: not null, VenueId: not null }; + } + + /// + /// Verifies, that the given is within the date + /// bounderies, and it is not excluded, and the venue is not occupied by another match. + /// + private async Task IsDateUsable(DateTime matchDateTimeUtc, RoundLegEntity roundLeg, TeamEntity team, CancellationToken cancellationToken) + { + var plannedDuration = _tenantContext.TournamentContext.FixtureRuleSet.PlannedDurationOfMatch; + + // Todo: This code creates heavy load on the database + return IsDateWithinRoundLegDateTime(roundLeg, matchDateTimeUtc) + && !IsExcludedDate(matchDateTimeUtc, roundLeg.RoundId, team.Id) + && !await IsVenueOccupiedByMatchAsync( + new DateTimePeriod(matchDateTimeUtc, matchDateTimeUtc.Add(plannedDuration)), + team.VenueId!.Value, cancellationToken); + } + + /// /// Generate available match dates for teams where /// , , @@ -103,23 +138,10 @@ internal async Task ClearAsync(ClearMatchDates clear, CancellationToken can internal async Task GenerateNewAsync(RoundEntity round, CancellationToken cancellationToken) { await Initialize(cancellationToken); - var teamIdProcessed = new List(); - var listTeamsWithSameVenue = new List>(); - - // Make a list of teams of the same round and with the same venue AND weekday AND match time - // Venues will later be assigned to these teams alternately - foreach (var team in round.TeamCollectionViaTeamInRound) - { - // the collection will contain at least one team - var teams = GetTeamsWithSameVenueAndMatchTime(team, round); - if (teamIdProcessed.Contains(teams[0].Id)) continue; - - listTeamsWithSameVenue.Add(teams); - foreach (var t in teams) - if (!teamIdProcessed.Contains(t.Id)) - teamIdProcessed.Add(t.Id); - } + // Venues will later be assigned to these teams on a rotating basis + var listTeamsWithSameVenue = GetListOfTeamsWithSameVenue(round); + foreach (var roundLeg in round.RoundLegs) { var startDate = DateTime.SpecifyKind(roundLeg.StartDateTime, DateTimeKind.Utc); @@ -127,54 +149,44 @@ internal async Task GenerateNewAsync(RoundEntity round, CancellationToken cancel foreach (var teamsWithSameVenue in listTeamsWithSameVenue) { - var teamIndex = 0; + var team = teamsWithSameVenue[0]; - // Make sure these values are not null - if (!teamsWithSameVenue[teamIndex].MatchDayOfWeek.HasValue || - !teamsWithSameVenue[teamIndex].MatchTime.HasValue || - !teamsWithSameVenue[teamIndex].VenueId.HasValue) - continue; - - // Create Tuple for non-nullable context -#pragma warning disable IDE0042 // Deconstruct variable declaration - var team = (teamsWithSameVenue[teamIndex].Id, - MatchDayOfWeek: (DayOfWeek) teamsWithSameVenue[teamIndex].MatchDayOfWeek!.Value, - MatchTime: teamsWithSameVenue[teamIndex].MatchTime!.Value, - VenueId: teamsWithSameVenue[teamIndex].VenueId!.Value); -#pragma warning restore IDE0042 // Deconstruct variable declaration - // get the first possible match date equal or after the leg's starting date - var matchDate = IncrementDateUntilDayOfWeek(startDate, team.MatchDayOfWeek); + var matchDate = IncrementDateUntilDayOfWeek(startDate, (DayOfWeek) team.MatchDayOfWeek!); // process the period of a leg + var teamIndex = 0; while (matchDate <= endDate) { + team = teamsWithSameVenue[teamIndex]; + // if there is more than one team per venue with same weekday and match time, // match dates will be assigned alternately - var matchDateAndTimeUtc = _timeZoneConverter.ToUtc(matchDate.Date.Add(team.MatchTime)); + var matchDateTimeUtc = _timeZoneConverter.ToUtc(matchDate.Date.Add(team.MatchTime!.Value)); // check whether the calculated date // is within the borders of round legs (if any) and is not marked as excluded - if (IsDateWithinRoundLegDateTime(roundLeg, matchDateAndTimeUtc) - && !IsExcludedDate(matchDateAndTimeUtc, round.Id, team.Id) - && !await IsVenueOccupiedByMatchAsync( - new DateTimePeriod(matchDateAndTimeUtc, - matchDateAndTimeUtc.Add(_tenantContext.TournamentContext.FixtureRuleSet - .PlannedDurationOfMatch)), team.VenueId, cancellationToken)) + + if (await IsDateUsable(matchDateTimeUtc, roundLeg, team, cancellationToken)) { var av = new AvailableMatchDateEntity { TournamentId = _tenantContext.TournamentContext.MatchPlanTournamentId, HomeTeamId = team.Id, - VenueId = team.VenueId, - MatchStartTime = matchDateAndTimeUtc, + VenueId = team.VenueId!.Value, + MatchStartTime = matchDateTimeUtc, MatchEndTime = - matchDateAndTimeUtc.Add(_tenantContext.TournamentContext.FixtureRuleSet.PlannedDurationOfMatch), + matchDateTimeUtc.Add(_tenantContext.TournamentContext.FixtureRuleSet.PlannedDurationOfMatch), IsGenerated = true }; _generatedAvailableMatchDateEntities.Add(av); - teamIndex = ++teamIndex >= teamsWithSameVenue.Count ? 0 : teamIndex; + } + + if (teamsWithSameVenue.Count > 1) + { + teamIndex++; + if (teamIndex >= teamsWithSameVenue.Count) teamIndex = 0; } matchDate = matchDate.Date.AddDays(7); @@ -189,6 +201,29 @@ internal async Task GenerateNewAsync(RoundEntity round, CancellationToken cancel // await _appDb.GenericRepository.SaveEntitiesAsync(_generatedAvailableMatchDateEntities, true, false, cancellationToken); } + /// + /// Make a list of teams of the same round and with the same venue AND weekday AND overlapping match time + /// + private List> GetListOfTeamsWithSameVenue(RoundEntity round) + { + var listTeamsWithSameVenue = new List>(); + + var teamIdProcessed = new List(); + foreach (var team in round.TeamCollectionViaTeamInRound) + { + // the collection will contain at least one team + var teams = GetTeamsWithSameVenueAndMatchTime(team, round); + if (!IsVenueAndDateDefined(teams[0]) || teamIdProcessed.Contains(teams[0].Id)) continue; + + listTeamsWithSameVenue.Add(teams); + foreach (var t in teams) + if (!teamIdProcessed.Contains(t.Id)) + teamIdProcessed.Add(t.Id); + } + + return listTeamsWithSameVenue; + } + private async Task IsVenueOccupiedByMatchAsync(DateTimePeriod matchTime, long venueId, CancellationToken cancellationToken) { @@ -277,6 +312,10 @@ internal List GetGeneratedAndManualAvailableMatchDates return result.ToList(); } + /// + /// Gets the teams in a round with the same , + /// and overlapping . + /// private EntityCollection GetTeamsWithSameVenueAndMatchTime(TeamEntity team, RoundEntity round) { var resultTeams = new EntityCollection(); @@ -305,4 +344,4 @@ private EntityCollection GetTeamsWithSameVenueAndMatchTime(TeamEntit return resultTeams; } -} \ No newline at end of file +} diff --git a/TournamentManager/TournamentManager/Plan/MatchPlanner.cs b/TournamentManager/TournamentManager/Plan/MatchPlanner.cs index 3a9d1e01..49af46f3 100644 --- a/TournamentManager/TournamentManager/Plan/MatchPlanner.cs +++ b/TournamentManager/TournamentManager/Plan/MatchPlanner.cs @@ -190,7 +190,7 @@ await _appDb.GenericRepository.DeleteEntitiesDirectlyAsync(typeof(MatchEntity), // matchDates contains calculated dates in the same order as combinations, // so the index can be used for both. var availableDates = GetMatchDates(roundLeg, teamCombinationGroup, roundMatches); - _logger.LogDebug("Selected dates: {dates}", string.Join(", ", availableDates.OrderBy(bd => bd?.MatchStartTime).Select(bd => bd?.MatchStartTime.ToShortDateString())).TrimEnd(',', ' ')); + _logger.LogDebug("Available dates for combination: {dates}", string.Join(", ", availableDates.OrderBy(bd => bd?.MatchStartTime).Select(bd => bd?.MatchStartTime.ToShortDateString())).TrimEnd(',', ' ')); for (var index = 0; index < teamCombinationGroup.Count; index++) { @@ -222,6 +222,9 @@ await _appDb.GenericRepository.DeleteEntitiesDirectlyAsync(typeof(MatchEntity), ChangeSerial = 0, Remarks = string.Empty }; + + _logger.LogDebug("Fixture: {HomeTeam} - {GuestTeam}: {PlannedStart}", match.HomeTeamId, match.GuestTeamId, match.PlannedStart); + roundMatches.Add(match); } } @@ -268,7 +271,7 @@ private static List GetOccupiedMatchDates(TeamCombination combin new DateTimePeriod(roundLeg.StartDateTime, roundLeg.EndDateTime), GetOccupiedMatchDates(combination, groupMatches)); } - + matchDates.Add(availableDates); #if DEBUG @@ -286,7 +289,7 @@ private static List GetOccupiedMatchDates(TeamCombination combin #endif } - // we can't proceed without and match dates found + // we can't proceed without any match dates found if (matchDates.Count == 0) return matchDatePerCombination; // only 1 match date found, so optimization is not possible