Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -7,36 +7,26 @@ namespace Turnierplan.Core.Test.Unit.Tournament.Definitions;

public sealed class AbstractTeamSelectorParserTest
{
public static readonly TheoryData<string, string, object> ParseAbstractTeamSelectorTestData = new()
public static readonly TheoryData<string, AbstractTeamSelector> ParseAbstractTeamSelectorTestData = new()
{
{ "1.0", "A1", new AbstractTeamSelector(false, 0, 1, null) },
{ "2.0", "A2", new AbstractTeamSelector(false, 0, 2, null) },
{ "1.1", "B1", new AbstractTeamSelector(false, 1, 1, null) },
{ "3.2", "C3", new AbstractTeamSelector(false, 2, 3, null) },
{ "4.25", "Z4", new AbstractTeamSelector(false, 25, 4, null) },

{ "0B1", "1B1", new AbstractTeamSelector(true, null, 1, 0) },
{ "2B1", "3B1", new AbstractTeamSelector(true, null, 1, 2) },
{ "1B3", "2B3", new AbstractTeamSelector(true, null, 3, 1) },
{ "4B4", "5B4", new AbstractTeamSelector(true, null, 4, 4) }
{ "1.0", new AbstractTeamSelector(false, 0, 1, null) },
{ "2.0", new AbstractTeamSelector(false, 0, 2, null) },
{ "1.1", new AbstractTeamSelector(false, 1, 1, null) },
{ "3.2", new AbstractTeamSelector(false, 2, 3, null) },
{ "4.25", new AbstractTeamSelector(false, 25, 4, null) },

{ "0B1", new AbstractTeamSelector(true, null, 1, 0) },
{ "2B1", new AbstractTeamSelector(true, null, 1, 2) },
{ "1B3", new AbstractTeamSelector(true, null, 3, 1) },
{ "4B4", new AbstractTeamSelector(true, null, 4, 4) }
};

[Theory]
[MemberData(nameof(ParseAbstractTeamSelectorTestData))]
public void AbstractTeamSelectorParser___Parse_Valid_Abstract_Team_Selector___Works_As_Expected(string input,
#pragma warning disable xUnit1026 // Theory methods should use all of their parameters
string _,
#pragma warning restore xUnit1026
object expected)
public void AbstractTeamSelectorParser___Parse_Valid_Abstract_Team_Selector___Works_As_Expected(string input, AbstractTeamSelector expected)
{
// Arrange
var expectedTeamSelector = (AbstractTeamSelector)expected;

// Act
var parsed = AbstractTeamSelectorParser.ParseAbstractTeamSelector(input);

// Assert
parsed.Should().BeEquivalentTo(expectedTeamSelector);
parsed.Should().BeEquivalentTo(expected);
}

[Theory]
Expand All @@ -49,47 +39,7 @@ public void AbstractTeamSelectorParser___Parse_Valid_Abstract_Team_Selector___Wo
[InlineData("0C2")]
public void AbstractTeamSelectorParser___Parse_Invalid_Abstract_Team_Selector___Throws_Exception(string input)
{
// Act
var func = void () => AbstractTeamSelectorParser.ParseAbstractTeamSelector(input);

// Assert
func.Should().ThrowExactly<TurnierplanException>().WithMessage($"The abstract team selector '{input}' is not valid.");
}

[Theory]
[MemberData(nameof(ParseAbstractTeamSelectorTestData))]
public void AbstractTeamSelectorParser___Parse_Valid_Abstract_Team_Selector_With_Definitions_Format___Works_As_Expected(
#pragma warning disable xUnit1026 // Theory methods should use all of their parameters
string _,
#pragma warning restore xUnit1026
string input,
object expected)
{
// Arrange
var expectedTeamSelector = (AbstractTeamSelector)expected;

// Act
var parsed = AbstractTeamSelectorParser.ParseAbstractTeamSelectorFromDefinitionFormat(input);

// Assert
parsed.Should().BeEquivalentTo(expectedTeamSelector);
}

[Theory]
[InlineData("A")]
[InlineData("0")]
[InlineData("A0")]
[InlineData("00")]
[InlineData("_0")]
[InlineData("0B2")]
[InlineData("2B0")]
[InlineData("0C2")]
public void AbstractTeamSelectorParser___Parse_Invalid_Abstract_Team_Selector_With_Definitions_Format___Throws_Exception(string input)
{
// Act
var func = void () => AbstractTeamSelectorParser.ParseAbstractTeamSelectorFromDefinitionFormat(input);

// Assert
func.Should().ThrowExactly<InvalidOperationException>().WithMessage($"Invalid abstract team selector: '{input}'");
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using Turnierplan.Core.Tournament.TeamSelectors;
using System.Diagnostics.CodeAnalysis;
using Turnierplan.Core.Tournament.TeamSelectors;

namespace Turnierplan.Core.Tournament.Definitions;

Expand All @@ -8,11 +9,51 @@ namespace Turnierplan.Core.Tournament.Definitions;
/// created yet and that therefore do not have a specific id. Abstract team selectors are used to configure the
/// generation of tournaments because the group ids are only generated during the generation process.
/// </summary>
/// <remarks>
/// If <paramref name="IsNthRanked"/> is <c>true</c>, <paramref name="PlacementRank"/> and <paramref name="OrdinalNumber"/>
/// must be specified and denote the team selector parameters as used in <see cref="GroupResultsNthRankedSelector"/>.
/// If <paramref name="IsNthRanked"/> is <c>false</c>, <paramref name="GroupIndex"/> and <paramref name="PlacementRank"/>
/// must be specified and will be used to generate a team selector to select the team with that position in a specific
/// group in the tournament.
/// </remarks>
public sealed record AbstractTeamSelector(bool IsNthRanked, int? GroupIndex, int PlacementRank, int? OrdinalNumber);
public sealed record AbstractTeamSelector
{
/// <remarks>
/// If <paramref name="isNthRanked"/> is <c>true</c>, <paramref name="placementRank"/> and <paramref name="ordinalNumber"/>
/// must be specified and denote the team selector parameters as used in <see cref="GroupResultsNthRankedSelector"/>.
/// If <paramref name="isNthRanked"/> is <c>false</c>, <paramref name="groupIndex"/> and <paramref name="placementRank"/>
/// must be specified and will be used to generate a team selector to select the team with that position in a specific
/// group in the tournament.
/// </remarks>
internal AbstractTeamSelector(bool isNthRanked, int? groupIndex, int placementRank, int? ordinalNumber)
{
if (isNthRanked)
{
if (groupIndex is not null || ordinalNumber is null)
{
throw new ArgumentException($"If '{nameof(isNthRanked)}' is true, '{nameof(groupIndex)}' must be null and '{nameof(ordinalNumber)}' must be non-null.");
}

ArgumentOutOfRangeException.ThrowIfLessThan(placementRank, 1);
ArgumentOutOfRangeException.ThrowIfLessThan(ordinalNumber.Value, 0);
}
else
{
if (groupIndex is null || ordinalNumber is not null)
{
throw new ArgumentException($"If '{nameof(isNthRanked)}' is false, '{nameof(groupIndex)}' must be non-null and '{nameof(ordinalNumber)}' must be null.");
}

ArgumentOutOfRangeException.ThrowIfLessThan(placementRank, 1);
ArgumentOutOfRangeException.ThrowIfLessThan(groupIndex.Value, 0);
}

IsNthRanked = isNthRanked;
GroupIndex = groupIndex;
PlacementRank = placementRank;
OrdinalNumber = ordinalNumber;
}

[MemberNotNullWhen(true, nameof(OrdinalNumber))]
[MemberNotNullWhen(false, nameof(GroupIndex))]
public bool IsNthRanked { get; }

public int? GroupIndex { get; }

public int PlacementRank { get; }

public int? OrdinalNumber { get; }
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,6 @@

namespace Turnierplan.Core.Tournament.Definitions;

/// <remarks>
/// The FinalsMatchDefinitions.json uses a modified string representation for the abstract team selectors because the
/// definitions file should be easily readable and the "standard" string representation format is not particularly intuitive.
/// </remarks>
public static partial class AbstractTeamSelectorParser
{
public static AbstractTeamSelector ParseAbstractTeamSelector(string? source)
Expand All @@ -25,7 +21,7 @@ public static bool TryParseAbstractTeamSelector(string? source, [NotNullWhen(tru
return false;
}

var regex = AbstractTeamSelectorExternalFormatRegex();
var regex = AbstractTeamSelectorRegex();
var match = regex.Match(source);

if (!match.Success)
Expand Down Expand Up @@ -58,46 +54,6 @@ public static bool TryParseAbstractTeamSelector(string? source, [NotNullWhen(tru
return false;
}

internal static AbstractTeamSelector ParseAbstractTeamSelectorFromDefinitionFormat(string source)
{
var regex = AbstractTeamSelectorDefinitionFormatRegex();
var match = regex.Match(source);

if (!match.Success)
{
throw new InvalidOperationException($"Invalid abstract team selector: '{source}'");
}

if (match.Groups["GroupRef"].Success)
{
if (int.TryParse(match.Groups["PlacementRank"].Value, out var placementRank))
{
var groupRef = match.Groups["GroupRef"].Value.Single();

if (char.IsLetter(groupRef) && placementRank >= 1)
{
var upper = char.ToUpper(groupRef);
var groupIndex = upper - 'A';

return new AbstractTeamSelector(false, groupIndex, placementRank, null);
}
}
}
else if (int.TryParse(match.Groups["OrdinalNumber"].Value, out var ordinalNumber)
&& int.TryParse(match.Groups["PlacementRank"].Value, out var placementRank)
&& ordinalNumber >= 1
&& placementRank >= 1)
{
// Subtract 1 from ordinal number because in definitions json the referenced rankings are provided on a 1.. range.
return new AbstractTeamSelector(true, null, placementRank, ordinalNumber - 1);
}

throw new InvalidOperationException($"Invalid abstract team selector: '{source}'");
}

[GeneratedRegex(@"^(?:(?<PlacementRank>\d+)\.(?<GroupIndex>\d+)|(?<OrdinalNumber>\d)B(?<PlacementRank>\d))$")]
private static partial Regex AbstractTeamSelectorExternalFormatRegex();

[GeneratedRegex(@"^(?:(?<GroupRef>[A-Z])(?<PlacementRank>\d)|(?<OrdinalNumber>\d)B(?<PlacementRank>\d))$")]
private static partial Regex AbstractTeamSelectorDefinitionFormatRegex();
private static partial Regex AbstractTeamSelectorRegex();
}
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
namespace Turnierplan.Core.Tournament.Definitions;
using System.Collections.Immutable;

namespace Turnierplan.Core.Tournament.Definitions;

public sealed record FinalsMatchDefinition
{
public FinalsMatchDefinition(IEnumerable<MatchDefinition> matches)
public FinalsMatchDefinition(ImmutableArray<MatchDefinition> matches)
{
Matches = matches.ToList();
Matches = matches;
RequiredTeamsPerGroup = Matches.SelectMany(x => new[] { x.TeamA.PlacementRank, x.TeamB.PlacementRank }).Max();
}

public IReadOnlyList<MatchDefinition> Matches { get; }
public ImmutableArray<MatchDefinition> Matches { get; }

public int RequiredTeamsPerGroup { get; }

Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,14 @@ namespace Turnierplan.Core.Tournament.Definitions;

public sealed record GroupMatchDefinition
{
public GroupMatchDefinition(IEnumerable<MatchBlock> matchBlocks)
public GroupMatchDefinition(ImmutableArray<MatchBlock> matchBlocks)
{
MatchBlocks = matchBlocks.ToList();
MatchBlocks = matchBlocks;
}

public IReadOnlyList<MatchBlock> MatchBlocks { get; }
public ImmutableArray<MatchBlock> MatchBlocks { get; }

public int BlockCount => MatchBlocks.Count;
public int BlockCount => MatchBlocks.Length;

public sealed record MatchBlock(ImmutableArray<MatchDefinition> Matches);

Expand Down
Loading