diff --git a/League.Demo/Configuration/Tenant.Default.Development.config b/League.Demo/Configuration/Tenant.Default.Development.config
index ecd1e38a..e3813c5f 100644
--- a/League.Demo/Configuration/Tenant.Default.Development.config
+++ b/League.Demo/Configuration/Tenant.Default.Development.config
@@ -169,5 +169,10 @@
True
+
+
+
+ None
+
\ No newline at end of file
diff --git a/League.Demo/Configuration/Tenant.Default.Production.config b/League.Demo/Configuration/Tenant.Default.Production.config
index d55ae5a7..94236eae 100644
--- a/League.Demo/Configuration/Tenant.Default.Production.config
+++ b/League.Demo/Configuration/Tenant.Default.Production.config
@@ -168,5 +168,10 @@
True
+
+
+
+ None
+
\ No newline at end of file
diff --git a/League.Demo/Configuration/Tenant.OtherOrg.Development.config b/League.Demo/Configuration/Tenant.OtherOrg.Development.config
index e9b3b1d5..25b639f4 100644
--- a/League.Demo/Configuration/Tenant.OtherOrg.Development.config
+++ b/League.Demo/Configuration/Tenant.OtherOrg.Development.config
@@ -169,5 +169,10 @@
True
+
+
+
+ Home
+
\ No newline at end of file
diff --git a/League.Demo/Configuration/Tenant.OtherOrg.Production.config b/League.Demo/Configuration/Tenant.OtherOrg.Production.config
index 923bf351..6218a9a4 100644
--- a/League.Demo/Configuration/Tenant.OtherOrg.Production.config
+++ b/League.Demo/Configuration/Tenant.OtherOrg.Production.config
@@ -168,5 +168,10 @@
True
+
+
+
+ Home
+
\ No newline at end of file
diff --git a/League.Demo/Configuration/Tenant.TestOrg.Development.config b/League.Demo/Configuration/Tenant.TestOrg.Development.config
index fc4e95ce..d0997c12 100644
--- a/League.Demo/Configuration/Tenant.TestOrg.Development.config
+++ b/League.Demo/Configuration/Tenant.TestOrg.Development.config
@@ -169,5 +169,10 @@
True
+
+
+
+ Home
+
\ No newline at end of file
diff --git a/League.Demo/Configuration/Tenant.TestOrg.Production.config b/League.Demo/Configuration/Tenant.TestOrg.Production.config
index 0a499251..f962800b 100644
--- a/League.Demo/Configuration/Tenant.TestOrg.Production.config
+++ b/League.Demo/Configuration/Tenant.TestOrg.Production.config
@@ -169,5 +169,10 @@
True
+
+
+
+ Home
+
\ No newline at end of file
diff --git a/TournamentManager/TournamentManager/Data/AvailableMatchDateRepository.cs b/TournamentManager/TournamentManager/Data/AvailableMatchDateRepository.cs
index e2697e69..a8c2826c 100644
--- a/TournamentManager/TournamentManager/Data/AvailableMatchDateRepository.cs
+++ b/TournamentManager/TournamentManager/Data/AvailableMatchDateRepository.cs
@@ -2,6 +2,7 @@
using SD.LLBLGen.Pro.ORMSupportClasses;
using TournamentManager.DAL.EntityClasses;
using TournamentManager.DAL.HelperClasses;
+using TournamentManager.Plan;
namespace TournamentManager.Data;
@@ -35,6 +36,46 @@ public async Task> GetAvailableMatchD
await da.FetchEntityCollectionAsync(qp, cancellationToken);
da.CloseConnection();
+ _logger.LogDebug("Fetched {count} available match dates for tournament {tournamentId}.", available.Count, tournamentId);
+
return available;
}
+
+ ///
+ /// Removes entries in AvailableMatchDates database table.
+ ///
+ /// The tournament ID.
+ /// Which entries to delete for the tournament.
+ ///
+ /// Returns the number of deleted records.
+ public async Task ClearAsync(long tournamentId, MatchDateClearOption clear, CancellationToken cancellationToken)
+ {
+ var deleted = 0;
+
+ // tournament is always in the filter
+ var filterAvailable = new RelationPredicateBucket();
+ filterAvailable.PredicateExpression.Add(AvailableMatchDateFields.TournamentId == tournamentId);
+
+ if ((clear & MatchDateClearOption.All) == MatchDateClearOption.All)
+ {
+ deleted = await _dbContext.AppDb.GenericRepository.DeleteEntitiesDirectlyAsync(typeof(AvailableMatchDateEntity),
+ null, cancellationToken);
+ }
+ else if ((clear & MatchDateClearOption.OnlyAutoGenerated) == MatchDateClearOption.OnlyAutoGenerated)
+ {
+ filterAvailable.PredicateExpression.AddWithAnd(AvailableMatchDateFields.IsGenerated == true);
+ deleted = await _dbContext.AppDb.GenericRepository.DeleteEntitiesDirectlyAsync(typeof(AvailableMatchDateEntity),
+ filterAvailable, cancellationToken);
+ }
+ else if ((clear & MatchDateClearOption.OnlyManual) == MatchDateClearOption.OnlyManual)
+ {
+ filterAvailable.PredicateExpression.AddWithAnd(AvailableMatchDateFields.IsGenerated == false);
+ deleted = await _dbContext.AppDb.GenericRepository.DeleteEntitiesDirectlyAsync(typeof(AvailableMatchDateEntity),
+ filterAvailable, cancellationToken);
+ }
+
+ _logger.LogDebug("Deleted {deleted} available match dates for tournament {tournamentId}.", deleted, tournamentId);
+
+ return deleted;
+ }
}
diff --git a/TournamentManager/TournamentManager/Data/ExcludedMatchDateRepository.cs b/TournamentManager/TournamentManager/Data/ExcludedMatchDateRepository.cs
index 8aa8170d..9f7f6e8e 100644
--- a/TournamentManager/TournamentManager/Data/ExcludedMatchDateRepository.cs
+++ b/TournamentManager/TournamentManager/Data/ExcludedMatchDateRepository.cs
@@ -49,7 +49,8 @@ public async Task> GetExcludedMatchDate
/// but only for the team or round.
///
///
- /// Same behavior as with .
+ /// Same conditions as with
+ /// which uses a cached list of s.
///
/// The where RoundId and TeamId are taken.
/// The TournamentId to filter the result.
@@ -58,7 +59,7 @@ public async Task> GetExcludedMatchDate
public virtual async Task GetExcludedMatchDateAsync(MatchEntity match,
long tournamentId, CancellationToken cancellationToken)
{
- if (!(match.PlannedStart.HasValue && match.PlannedEnd.HasValue)) return null;
+ if (match is not { PlannedStart: not null, PlannedEnd: not null }) return null;
using var da = _dbContext.GetNewAdapter();
var tournamentFilter = new PredicateExpression(
diff --git a/TournamentManager/TournamentManager/Data/GenericRepository.cs b/TournamentManager/TournamentManager/Data/GenericRepository.cs
index c83521e8..5bdfbdf3 100644
--- a/TournamentManager/TournamentManager/Data/GenericRepository.cs
+++ b/TournamentManager/TournamentManager/Data/GenericRepository.cs
@@ -77,7 +77,7 @@ public virtual async Task DeleteEntitiesAsync(T entitiesToDelete, Cancellatio
var count = await da.DeleteEntityCollectionAsync(entitiesToDelete, cancellationToken);
}
- public virtual async Task DeleteEntitiesDirectlyAsync(Type entityType, IRelationPredicateBucket filterBucket, CancellationToken cancellationToken)
+ public virtual async Task DeleteEntitiesDirectlyAsync(Type entityType, IRelationPredicateBucket? filterBucket, CancellationToken cancellationToken)
{
using var da = _dbContext.GetNewAdapter();
return await da.DeleteEntitiesDirectlyAsync(entityType, filterBucket, cancellationToken);
@@ -97,4 +97,4 @@ public virtual async Task DeleteEntitiesUsingConstraintAsync(IPredicateE
var bucket = new RelationPredicateBucket(uniqueConstraintFilter);
return await da.DeleteEntitiesDirectlyAsync(typeof(T), bucket, cancellationToken);
}
-}
\ No newline at end of file
+}
diff --git a/TournamentManager/TournamentManager/MultiTenancy/ITournamentContext.cs b/TournamentManager/TournamentManager/MultiTenancy/ITournamentContext.cs
index edb83358..8035a238 100644
--- a/TournamentManager/TournamentManager/MultiTenancy/ITournamentContext.cs
+++ b/TournamentManager/TournamentManager/MultiTenancy/ITournamentContext.cs
@@ -59,4 +59,9 @@ public interface ITournamentContext
/// Rules for team master data
///
TeamRules TeamRuleSet { get; set; }
-}
\ No newline at end of file
+
+ ///
+ /// Rules for referee master data
+ ///
+ RefereeRules RefereeRuleSet { get; set; }
+}
diff --git a/TournamentManager/TournamentManager/MultiTenancy/TournamentContext.cs b/TournamentManager/TournamentManager/MultiTenancy/TournamentContext.cs
index b59cac09..3bc5ecc0 100644
--- a/TournamentManager/TournamentManager/MultiTenancy/TournamentContext.cs
+++ b/TournamentManager/TournamentManager/MultiTenancy/TournamentContext.cs
@@ -75,9 +75,9 @@ public class TournamentContext : ITournamentContext
public TeamRules TeamRuleSet { get; set; } = new();
///
- /// Rules for organizing referees.
+ /// Rules for referee master data.
///
- [YAXLib.Attributes.YAXComment("Rules for organizing referees")]
+ [YAXLib.Attributes.YAXComment("Rules for referee master data")]
public RefereeRules RefereeRuleSet { get; set; } = new();
}
@@ -166,14 +166,14 @@ public class TeamRules
}
///
-/// Rules for organizing referees.
+/// Rules for referee master data.
///
public class RefereeRules
{
///
/// Rules for teams' home match time
///
- [YAXLib.Attributes.YAXComment("Rules for organizing referees")]
+ [YAXLib.Attributes.YAXComment("Rule for organizing referees")]
public Plan.RefereeType RefereeType { get; set; } = Plan.RefereeType.None;
}
diff --git a/TournamentManager/TournamentManager/Plan/AvailableMatchDates.cs b/TournamentManager/TournamentManager/Plan/AvailableMatchDates.cs
index 47de06f4..071283a2 100644
--- a/TournamentManager/TournamentManager/Plan/AvailableMatchDates.cs
+++ b/TournamentManager/TournamentManager/Plan/AvailableMatchDates.cs
@@ -1,6 +1,5 @@
using TournamentManager.DAL.EntityClasses;
using TournamentManager.DAL.HelperClasses;
-using SD.LLBLGen.Pro.ORMSupportClasses;
using Microsoft.Extensions.Logging;
using TournamentManager.Data;
using TournamentManager.MultiTenancy;
@@ -62,39 +61,22 @@ await _appDb.AvailableMatchDateRepository.GetAvailableMatchDatesAsync(
///
- /// Removes entries in AvailableMatchDates database table.
+ /// Removes entries in database table and internal cache.
///
/// Which entries to delete for the tournament.
///
/// Returns the number of deleted records.
public async Task ClearAsync(MatchDateClearOption clear, CancellationToken cancellationToken)
{
- var deleted = 0;
+ var deleted =
+ await _appDb.AvailableMatchDateRepository.ClearAsync(_tenantContext.TournamentContext.MatchPlanTournamentId,
+ clear, cancellationToken);
- // tournament is always in the filter
- var filterAvailable = new RelationPredicateBucket();
- filterAvailable.PredicateExpression.Add(AvailableMatchDateFields.TournamentId ==
- _tenantContext.TournamentContext.MatchPlanTournamentId);
-
- if (clear == MatchDateClearOption.All)
- {
- deleted = await _appDb.GenericRepository.DeleteEntitiesDirectlyAsync(typeof(AvailableMatchDateEntity),
- null!, cancellationToken);
- _generatedAvailableMatchDateEntities.Clear();
- }
- else if (clear == MatchDateClearOption.OnlyAutoGenerated)
- {
- filterAvailable.PredicateExpression.AddWithAnd(AvailableMatchDateFields.IsGenerated == true);
- deleted = await _appDb.GenericRepository.DeleteEntitiesDirectlyAsync(typeof(AvailableMatchDateEntity),
- filterAvailable, cancellationToken);
+ if((clear & MatchDateClearOption.OnlyAutoGenerated) == MatchDateClearOption.OnlyAutoGenerated)
_generatedAvailableMatchDateEntities.Clear();
- }
- else if (clear == MatchDateClearOption.OnlyManual)
- {
- filterAvailable.PredicateExpression.AddWithAnd(AvailableMatchDateFields.IsGenerated == false);
- deleted = await _appDb.GenericRepository.DeleteEntitiesDirectlyAsync(typeof(AvailableMatchDateEntity),
- filterAvailable, cancellationToken);
- }
+
+ if ((clear & MatchDateClearOption.OnlyManual) == MatchDateClearOption.OnlyManual)
+ _availableMatchDateEntities.Clear();
return deleted;
}
@@ -112,16 +94,19 @@ private static bool IsVenueAndDateDefined(TeamEntity team)
/// Verifies, that the given is within the date
/// boundaries, 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)
+ private bool IsDateUsable(DateTime matchDateTimeUtc, RoundLegEntity roundLeg, IEnumerable tournamentMatches, TeamEntity team)
{
var plannedDuration = _tenantContext.TournamentContext.FixtureRuleSet.PlannedDurationOfMatch;
- // Todo: This code creates heavy load on the database
- return IsDateWithinRoundLegDateTime(roundLeg, matchDateTimeUtc)
+ var isAvailable = IsDateWithinRoundLegDateTime(roundLeg, matchDateTimeUtc)
&& !IsExcludedDate(matchDateTimeUtc, roundLeg.RoundId, team.Id)
- && !await IsVenueOccupiedByMatchAsync(
+ && !IsVenueOccupiedByMatch(
new DateTimePeriod(matchDateTimeUtc, matchDateTimeUtc.Add(plannedDuration)),
- team.VenueId!.Value, cancellationToken);
+ team.VenueId!.Value, tournamentMatches);
+
+ _logger.LogDebug("Venue '{venueId}' is available for '{matchDateTimeUtc}': {isAvailable}", team.VenueId, matchDateTimeUtc, isAvailable);
+
+ return isAvailable;
}
@@ -131,9 +116,10 @@ private async Task IsDateUsable(DateTime matchDateTimeUtc, RoundLegEntity
/// are not .
///
///
+ ///
///
///
- public async Task GenerateNewAsync(RoundEntity round, CancellationToken cancellationToken)
+ public async Task GenerateNewAsync(RoundEntity round, EntityCollection tournamentMatches, CancellationToken cancellationToken)
{
await Initialize(cancellationToken);
@@ -165,7 +151,7 @@ public async Task GenerateNewAsync(RoundEntity round, CancellationToken cancella
// check whether the calculated date
// is within the borders of round legs (if any) and is not marked as excluded
- if (await IsDateUsable(matchDateTimeUtc, roundLeg, team, cancellationToken))
+ if (IsDateUsable(matchDateTimeUtc, roundLeg, tournamentMatches, team))
{
var av = new AvailableMatchDateEntity
{
@@ -195,8 +181,7 @@ public async Task GenerateNewAsync(RoundEntity round, CancellationToken cancella
_logger.LogDebug("Generated {Count} UTC dates for HomeTeams:", _generatedAvailableMatchDateEntities.Count);
_logger.LogDebug("{Generated}\n", _generatedAvailableMatchDateEntities.Select(gen => (gen.HomeTeamId, gen.MatchStartTime)));
- // save to the persistent storage
- // await _appDb.GenericRepository.SaveEntitiesAsync(_generatedAvailableMatchDateEntities, true, false, cancellationToken);
+ // Note: Generated dates are not saved to the database, but only used for the current run.
}
///
@@ -222,11 +207,13 @@ private List> GetListOfTeamsWithSameVenue(RoundEnti
return listTeamsWithSameVenue;
}
- private async Task IsVenueOccupiedByMatchAsync(DateTimePeriod matchTime, long venueId,
- CancellationToken cancellationToken)
+ private static bool IsVenueOccupiedByMatch(DateTimePeriod matchTime, long venueId,
+ IEnumerable tournamentMatches)
{
- return (await _appDb.VenueRepository.GetOccupyingMatchesAsync(venueId, matchTime,
- _tenantContext.TournamentContext.MatchPlanTournamentId, cancellationToken)).Count == 0;
+ return tournamentMatches.Any(m => m.VenueId == venueId &&
+ m is { IsComplete: false, PlannedStart: not null, PlannedEnd: not null }
+ && (m.PlannedStart <= matchTime.End &&
+ matchTime.Start <= m.PlannedEnd)); // overlapping periods
}
private static bool IsDateWithinRoundLegDateTime(RoundLegEntity leg, DateTime queryDate)
@@ -249,7 +236,8 @@ private static DateTime IncrementDateUntilDayOfWeek(DateTime date, DayOfWeek day
/// but only for the team or round.
///
///
- /// Same behavior as with .
+ /// Same conditions as with
+ /// which gets a s from the database, if one exists, or .
///
/// Date to test, whether it is excluded.
/// OR excluded on the round level. If , there is no round restriction.
@@ -278,7 +266,7 @@ private bool IsExcludedDate(DateTime queryDate, long? roundId, long? teamId)
;
}
- public List GetGeneratedAndManualAvailableMatchDateDays(RoundLegEntity leg)
+ internal List GetGeneratedAndManualAvailableMatchDateDays(RoundLegEntity leg)
{
var result = _generatedAvailableMatchDateEntities.Union(_availableMatchDateEntities)
.Where(gen => gen.TournamentId == _tenantContext.TournamentContext.MatchPlanTournamentId
@@ -291,7 +279,7 @@ public List GetGeneratedAndManualAvailableMatchDateDays(RoundLegEntity
return result;
}
- public List GetGeneratedAndManualAvailableMatchDates(long homeTeamId,
+ internal List GetGeneratedAndManualAvailableMatchDates(long homeTeamId,
DateTimePeriod datePeriod, List? excludedDates)
{
if (datePeriod is not { Start: not null, End: not null }) throw new ArgumentNullException(nameof(datePeriod));
diff --git a/TournamentManager/TournamentManager/Plan/MatchCreator.cs b/TournamentManager/TournamentManager/Plan/MatchCreator.cs
index 6c0c19a6..2cb8a56d 100644
--- a/TournamentManager/TournamentManager/Plan/MatchCreator.cs
+++ b/TournamentManager/TournamentManager/Plan/MatchCreator.cs
@@ -7,7 +7,7 @@ namespace TournamentManager.Plan;
///
/// Class to create matches for a group of participants.
-/// The round robin system is applied, i.e. all participants in the group play each other.
+/// The round-robin system is applied, i.e. all participants in the group play each other.
///
/// The participant type.
/// The referee type.
@@ -118,7 +118,7 @@ public ParticipantCombinations GetCombinations(LegType legType)
{
CreateCombinations(_tenantContext.TournamentContext.RefereeRuleSet.RefereeType);
- return ((legType == LegType.First) ? _participantCombinationsFirstLeg : _participantCombinationsReturnLeg);
+ return (legType == LegType.First) ? _participantCombinationsFirstLeg : _participantCombinationsReturnLeg;
}
///
diff --git a/TournamentManager/TournamentManager/Plan/MatchScheduler.cs b/TournamentManager/TournamentManager/Plan/MatchScheduler.cs
index 449ffd78..42fa5a49 100644
--- a/TournamentManager/TournamentManager/Plan/MatchScheduler.cs
+++ b/TournamentManager/TournamentManager/Plan/MatchScheduler.cs
@@ -46,11 +46,11 @@ private async Task LoadEntitiesAsync(CancellationToken cancellationToken)
}
internal async Task GenerateAvailableMatchDatesAsync(MatchDateClearOption clearMatchDates, RoundEntity round,
- CancellationToken cancellationToken)
+ EntityCollection tournamentMatches, CancellationToken cancellationToken)
{
await LoadEntitiesAsync(cancellationToken);
_ = await _availableMatchDates.ClearAsync(clearMatchDates, cancellationToken);
- await _availableMatchDates.GenerateNewAsync(round, cancellationToken);
+ await _availableMatchDates.GenerateNewAsync(round, tournamentMatches, cancellationToken);
}
///
@@ -70,7 +70,7 @@ public async Task ScheduleFixturesForTournament(bool keepExisting, CancellationT
}
///
- /// Generates round match combinations for the Round Robin system,
+ /// Generates round match combinations for the round-robin system,
/// assigns optimized match dates and stores the matches to
/// the persistent storage.
///
@@ -82,9 +82,10 @@ public async Task ScheduleFixturesForRound(RoundEntity round, bool keepExisting,
if (_appDb.MatchRepository.AnyCompleteMatchesExist(round))
throw new InvalidOperationException($"Completed matches exist for round '{round.Id}'. Generating fixtures aborted.");
- var roundMatches = await GetOrCreateRoundMatches(round, keepExisting, cancellationToken);
+ // We load all tournament matches, so that we can check for venues occupied by existing matches in memory.
+ var tournamentMatches = await GetOrCreateTournamentMatches(round, keepExisting, cancellationToken);
- await GenerateAvailableMatchDatesAsync(MatchDateClearOption.OnlyAutoGenerated, round, cancellationToken);
+ await GenerateAvailableMatchDatesAsync(MatchDateClearOption.OnlyAutoGenerated, round, tournamentMatches, cancellationToken);
var teams = new Collection(round.TeamCollectionViaTeamInRound.Select(t => t.Id).ToList());
@@ -105,12 +106,12 @@ public async Task ScheduleFixturesForRound(RoundEntity round, bool keepExisting,
// We assign the match dates based on the turn combinations' TurnDateTimePeriods
foreach (var turn in combinations.GetTurns())
{
- SetMatchDates(round, combinations, turn, roundLeg, roundMatches);
+ SetMatchDates(round, roundLeg, turn, combinations, tournamentMatches);
}
}
// save the matches for the group
- await _appDb.GenericRepository.SaveEntitiesAsync(roundMatches, true, false, cancellationToken);
+ await _appDb.GenericRepository.SaveEntitiesAsync(tournamentMatches, true, false, cancellationToken);
await _availableMatchDates.ClearAsync(MatchDateClearOption.OnlyAutoGenerated, cancellationToken);
}
@@ -119,24 +120,27 @@ public async Task ScheduleFixturesForRound(RoundEntity round, bool keepExisting,
/// Sets the match dates for the given in and .
///
///
- ///
- ///
///
- ///
- private void SetMatchDates(RoundEntity round, ParticipantCombinations combinations, int turn, RoundLegEntity roundLeg,
- EntityCollection roundMatches)
+ ///
+ ///
+ ///
+ private void SetMatchDates(RoundEntity round, RoundLegEntity roundLeg,
+ int turn, ParticipantCombinations combinations,
+ EntityCollection tournamentMatches)
{
+ // Get the selected turn combinations for the given turn.
var selectedTurnCombinations = combinations.GetCombinations(turn).ToList();
- // Get match dates for every combination of a group.
- // Matches in the same turnCombinations can even take at the same time.
- var datesFound = GetMatchDatesForTurn(turn, combinations, roundMatches);
+ // Get match dates for every combination of a turn.
+ // Matches in the same turnCombinations can even take place at the same time.
+ var datesFound = GetMatchDatesForTurn(turn, combinations, tournamentMatches);
_logger.LogDebug("Found dates for combination: {dates}",
string.Join(", ",
- datesFound.OrderBy(bd => bd?.MatchStartTime).Select(bd => bd?.MatchStartTime.ToShortDateString()))
- .TrimEnd(',', ' '));
+ datesFound.OrderBy(bd => bd?.MatchStartTime)
+ .Select(bd => bd?.MatchStartTime.ToShortDateString() ?? "(null)"))
+ .Trim(',', ' '));
- // MatchDates contains calculated dates in the same order as turn combinations,
+ // datesFound contains calculated dates in the same sequence as turn combinations,
// so the index can be used for both.
for (var index = 0; index < selectedTurnCombinations.Count; index++)
{
@@ -144,7 +148,7 @@ private void SetMatchDates(RoundEntity round, ParticipantCombinations
+ if (tournamentMatches.Any(rm =>
(rm.HomeTeamId == combination.Home && rm.GuestTeamId == combination.Guest &&
rm.LegSequenceNo == roundLeg.SequenceNo) ||
(rm.GuestTeamId == combination.Home && rm.HomeTeamId == combination.Guest &&
@@ -173,7 +177,7 @@ private void SetMatchDates(RoundEntity round, ParticipantCombinations CreateCombinations(Collection
{
// build up match combinations for the teams of round
var matchCreator = new MatchCreator(_tenantContext, _loggerFactory.CreateLogger>());
- // TODO: RefereeType should be configurable via ITenantContext
var combinations =
matchCreator.SetParticipants(teams).GetCombinations(
roundLeg.SequenceNo % 2 == 1 ? LegType.First : LegType.Return);
@@ -197,34 +200,28 @@ private ParticipantCombinations CreateCombinations(Collection
}
///
- /// Gets the existing matches for the given .
+ /// Gets the existing matches for the given of a .
/// If is true, the existing matches are returned.
- /// If is false, the existing matches are deleted and an empty collection is returned.
+ /// If is false, the existing matches of the round are deleted before returning tournament matches.
///
///
///
///
- /// The existing matches for the given , depending on .
- private async Task> GetOrCreateRoundMatches(RoundEntity round, bool keepExistingMatches, CancellationToken cancellationToken)
+ /// The existing matches for the given , depending on .
+ private async Task> GetOrCreateTournamentMatches(RoundEntity round, bool keepExistingMatches, CancellationToken cancellationToken)
{
- EntityCollection roundMatches;
-
- if (keepExistingMatches)
+ if (!keepExistingMatches)
{
- // load existing matches from storage
- roundMatches = _appDb.MatchRepository.GetMatches(round);
- }
- else
- {
- roundMatches = new EntityCollection();
- // delete existing matches from storage
+ // delete existing matches of the round from storage
+ // before load tournament matches
var bucket = new RelationPredicateBucket(new PredicateExpression(
new FieldCompareRangePredicate(MatchFields.RoundId, null, false, new[] { round.Id })));
await _appDb.GenericRepository.DeleteEntitiesDirectlyAsync(typeof(MatchEntity), bucket,
cancellationToken);
}
- return roundMatches;
+ var tournamentMatches = _appDb.MatchRepository.GetMatches(round.TournamentId!.Value);
+ return tournamentMatches;
}
///
@@ -280,6 +277,8 @@ private static List GetOccupiedMatchDates(ParticipantCombination>();
for (var index = 0; index < turnCombinations.Count; index++)
@@ -291,6 +290,7 @@ private static List GetOccupiedMatchDates(ParticipantCombination amd.MinTimeDiff = TimeSpan.MaxValue);
+#if DEBUG
// Get the last match for at least one of the teams, if any
var lastMatchOfCombination = roundMatches.OrderBy(gm => gm.PlannedStart).LastOrDefault(gm =>
gm.HomeTeamId == combination.Home || gm.GuestTeamId == combination.Guest);
@@ -302,44 +302,44 @@ private static List GetOccupiedMatchDates(ParticipantCombination();
-
- // we can't proceed without any match dates found
- if (matchDates.Count == 0) return matchDatesPerCombination;
- // only 1 match date found, so optimization is not possible
- // and the following "i-loop" will be skipped
- if (matchDates.Count == 1)
- {
- matchDatesPerCombination.Add(matchDates[0][0]);
- return matchDatesPerCombination;
+ // We have to add the list of available dates even if it is empty.
+ matchDates.Add(availableDatesForCombination);
}
- FindBestDate(matchDates, matchDatesPerCombination);
-
- return matchDatesPerCombination;
+ return matchDates.Count switch {
+ // We can't proceed without any match dates found
+ 0 => new List(),
+ // Only 1 match date found, so optimization is not possible
+ // and FindBestDate() would throw an exception
+ 1 => new List { matchDates[0][0] },
+ _ => FindBestDates(matchDates)
+ };
}
///
/// Finds the best match dates from a list of available match dates for each combination.
+ /// The best match is the date that has the smallest distance to the smallest date in the other turn(s).
+ /// If no match dates could be determined for a team, bestDate will be set to null.
+ ///
+ /// This method is optimizing across all combinations of all turns.
///
///
- ///
- private static void FindBestDate(List> availableMatchDates, List bestMatchDatesPerCombination)
+ private static List FindBestDates(List> availableMatchDates)
{
- // Cross-compute the number of dates between between matches of turn.
- // Goal: found match dates should be as close together as possible
+ var bestMatchDatesPerCombination = new List();
+
+ // Cross-compute the number of dates between matches of a turn.
+ // Goal: Found match dates are as close to each other as possible
// start with 1st dates, end with last but one dates
for (var i = 0; i < availableMatchDates.Count - 1; i++)
@@ -365,10 +365,12 @@ private static void FindBestDate(List> availableM
} // end dates1
} // end j
- // Get the date that has least distance to smallest date in other turn(s)
+ // Get the date that has the smallest distance to the smallest date in the other turn(s).
// Note: If no match dates could be determined for a team, bestDate will be null.
- var bestDate = availableMatchDates[i].Where(md => md.MinTimeDiff == availableMatchDates[i].Min(d => d.MinTimeDiff))
- .OrderBy(md => md.MinTimeDiff).FirstOrDefault();
+ var bestDate = availableMatchDates[i]
+ .Where(md => md.MinTimeDiff == availableMatchDates[i]
+ .Min(d => d.MinTimeDiff))
+ .MinBy(md => md.MinTimeDiff);
bestMatchDatesPerCombination.Add(bestDate);
// process the last combination
@@ -377,12 +379,18 @@ private static void FindBestDate(List> availableM
// now the "j-loop" group is not processed yet:
if (i + 1 >= availableMatchDates.Count - 1)
{
- bestDate = availableMatchDates[^1].Where(md => md.MinTimeDiff == availableMatchDates[^1].Min(d => d.MinTimeDiff))
+ bestDate = availableMatchDates[^1]
+ .Where(md => md.MinTimeDiff == availableMatchDates[^1].
+ Min(d => d.MinTimeDiff))
.MinBy(md => md.MinTimeDiff);
// the last "j-increment" is always the same as "matchDates[^1]" (loop condition)
bestMatchDatesPerCombination.Add(bestDate);
}
} // end i
+
+ // returns the best match date found per combination,
+ // so the number of elements is the same as the number of combinations
+ return bestMatchDatesPerCombination;
}
///
diff --git a/TournamentManager/TournamentManager/TournamentCreator.cs b/TournamentManager/TournamentManager/TournamentCreator.cs
index 35ee7eeb..8da3ecc6 100644
--- a/TournamentManager/TournamentManager/TournamentCreator.cs
+++ b/TournamentManager/TournamentManager/TournamentCreator.cs
@@ -45,9 +45,11 @@ public static TournamentCreator Instance(IAppDb appDb)
public async Task CopyTournament (long fromTournamentId)
{
var now = DateTime.UtcNow;
- var tournament = await _appDb.TournamentRepository.GetTournamentAsync(new PredicateExpression(TournamentFields.Id == fromTournamentId), CancellationToken.None);
- if (tournament is null) throw new NullReferenceException($"'{fromTournamentId}' not found.");
-
+ var tournament =
+ await _appDb.TournamentRepository.GetTournamentAsync(
+ new PredicateExpression(TournamentFields.Id == fromTournamentId), CancellationToken.None)
+ ?? throw new InvalidOperationException($"'{fromTournamentId}' not found.");
+
var newTournament = new TournamentEntity
{
IsPlanningMode = true,
@@ -77,13 +79,13 @@ public async Task CopyTournament (long fromTournamentId)
///
/// Existing source tournament id.
/// Existing target tournament id.
- /// List of round id's to be excluded (may be null for none)
+ /// List of round id's to be excluded (empty list for 'none')
/// True, if creation was successful, false otherwise.
- public bool CopyRound(long fromTournamentId, long toTournamentId, IEnumerable excludeRoundId)
+ public bool CopyRound(long fromTournamentId, long toTournamentId, IList excludeRoundId)
{
const string transactionName = "CloneRounds";
var now = DateTime.UtcNow;
-
+
// get the rounds of SOURCE tournament
var roundIds = _appDb.TournamentRepository.GetTournamentRounds(fromTournamentId).Select(r => r.Id).ToList();
@@ -134,7 +136,7 @@ public bool CopyRound(long fromTournamentId, long toTournamentId, IEnumerable rounds , int sequenceNo, DateTi
var roundEntities = (rounds as RoundEntity[] ?? rounds.ToArray()).ToList();
- if (!roundEntities.Any())
+ if (roundEntities.Count == 0)
return false;
var roundIds = roundEntities.Select(r => r.Id).ToList();
@@ -204,9 +206,7 @@ public async Task SetTournamentCompleted(long tournamentId)
throw new ArgumentException($"Tournament {tournamentId} contains incomplete matches.");
}
- var tournament = await new TournamentRepository(_appDb.DbContext).GetTournamentWithRoundsAsync(tournamentId, CancellationToken.None);
- if (tournament == null) throw new InvalidOperationException($"Tournament with Id '{tournamentId}' not found.");
-
+ var tournament = await new TournamentRepository(_appDb.DbContext).GetTournamentWithRoundsAsync(tournamentId, CancellationToken.None) ?? throw new InvalidOperationException($"Tournament with Id '{tournamentId}' not found.");
var now = DateTime.UtcNow;
foreach (var round in tournament.Rounds)