diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 00000000..3580d23c --- /dev/null +++ b/.editorconfig @@ -0,0 +1,4 @@ +[*.{cs,vb}] + +# IDE0305: Initialisierung der Sammlung vereinfachen +dotnet_diagnostic.IDE0305.severity = none diff --git a/iRLeagueApiCore.sln b/iRLeagueApiCore.sln index c6dfef91..926619f2 100644 --- a/iRLeagueApiCore.sln +++ b/iRLeagueApiCore.sln @@ -9,6 +9,7 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "iRLeagueApiCore.UnitTests", EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{7F90E099-56DE-4FEF-8CA2-4F4610CDEF19}" ProjectSection(SolutionItems) = preProject + .editorconfig = .editorconfig .github\workflows\dotnet_develop.yml = .github\workflows\dotnet_develop.yml .github\workflows\dotnet_main_pr.yml = .github\workflows\dotnet_main_pr.yml .github\workflows\dotnet_main_push.yml = .github\workflows\dotnet_main_push.yml diff --git a/src/iRLeagueApiCore.Client/iRLeagueApiCore.Client.csproj b/src/iRLeagueApiCore.Client/iRLeagueApiCore.Client.csproj index 4fd59d84..d0cd4a42 100644 --- a/src/iRLeagueApiCore.Client/iRLeagueApiCore.Client.csproj +++ b/src/iRLeagueApiCore.Client/iRLeagueApiCore.Client.csproj @@ -12,7 +12,7 @@ Library net6.0 iRLeagueApiCore.Client - 0.12.1 + 0.12.2 Simon Schulze Simon Schulze This package contains shared objects for all members of the iRLeagueDatabase-iRLeagueApi stack diff --git a/src/iRLeagueApiCore.Common/Enums/ComparatorType.cs b/src/iRLeagueApiCore.Common/Enums/ComparatorType.cs index c9d59652..030c13be 100644 --- a/src/iRLeagueApiCore.Common/Enums/ComparatorType.cs +++ b/src/iRLeagueApiCore.Common/Enums/ComparatorType.cs @@ -9,5 +9,7 @@ public enum ComparatorType IsBigger, NotEqual, InList, - ForEach, // special comparator that multiplies the configured bonus/penalty for each multiple of the provided value + ForEach, // special comparator that multiplies the configured bonus/penalty for each multiple of the provided value + Min, + Max, } diff --git a/src/iRLeagueApiCore.Common/Enums/FilterType.cs b/src/iRLeagueApiCore.Common/Enums/FilterType.cs index 0347533a..840ee26c 100644 --- a/src/iRLeagueApiCore.Common/Enums/FilterType.cs +++ b/src/iRLeagueApiCore.Common/Enums/FilterType.cs @@ -5,4 +5,5 @@ public enum FilterType ColumnProperty, Member, Team, + Count, } diff --git a/src/iRLeagueApiCore.Common/Models/ResultConfigurations/BonusPointModel.cs b/src/iRLeagueApiCore.Common/Models/ResultConfigurations/BonusPointModel.cs index 9784f6f4..d6648fc4 100644 --- a/src/iRLeagueApiCore.Common/Models/ResultConfigurations/BonusPointModel.cs +++ b/src/iRLeagueApiCore.Common/Models/ResultConfigurations/BonusPointModel.cs @@ -3,6 +3,8 @@ namespace iRLeagueApiCore.Common.Models; [DataContract] public class BonusPointModel { + [DataMember] + public string Name { get; set; } = string.Empty; [DataMember] public BonusPointType Type { get; set; } [DataMember] diff --git a/src/iRLeagueApiCore.Common/iRLeagueApiCore.Common.csproj b/src/iRLeagueApiCore.Common/iRLeagueApiCore.Common.csproj index bfee5b11..04e17124 100644 --- a/src/iRLeagueApiCore.Common/iRLeagueApiCore.Common.csproj +++ b/src/iRLeagueApiCore.Common/iRLeagueApiCore.Common.csproj @@ -18,7 +18,7 @@ Library net6.0 iRLeagueApiCore.Common - 0.12.1 + 0.12.2 Simon Schulze Simon Schulze enable diff --git a/src/iRLeagueApiCore.Server/iRLeagueApiCore.Server.csproj b/src/iRLeagueApiCore.Server/iRLeagueApiCore.Server.csproj index afcc27c1..85593521 100644 --- a/src/iRLeagueApiCore.Server/iRLeagueApiCore.Server.csproj +++ b/src/iRLeagueApiCore.Server/iRLeagueApiCore.Server.csproj @@ -71,7 +71,7 @@ true - 0.12.1 + 0.12.2 enable diff --git a/src/iRLeagueApiCore.Services/ResultService/Calculation/CalculationPointRuleBase.cs b/src/iRLeagueApiCore.Services/ResultService/Calculation/CalculationPointRuleBase.cs index 8ba51883..74a635be 100644 --- a/src/iRLeagueApiCore.Services/ResultService/Calculation/CalculationPointRuleBase.cs +++ b/src/iRLeagueApiCore.Services/ResultService/Calculation/CalculationPointRuleBase.cs @@ -1,5 +1,6 @@ using iRLeagueApiCore.Common.Enums; using iRLeagueApiCore.Common.Models; +using iRLeagueApiCore.Services.ResultService.Calculation.Filters; using iRLeagueApiCore.Services.ResultService.Extensions; using iRLeagueApiCore.Services.ResultService.Models; using MySqlX.XDevAPI.Relational; diff --git a/src/iRLeagueApiCore.Services/ResultService/Calculation/CalculationServiceBase.cs b/src/iRLeagueApiCore.Services/ResultService/Calculation/CalculationServiceBase.cs index cd9750e2..8c642b2e 100644 --- a/src/iRLeagueApiCore.Services/ResultService/Calculation/CalculationServiceBase.cs +++ b/src/iRLeagueApiCore.Services/ResultService/Calculation/CalculationServiceBase.cs @@ -30,9 +30,10 @@ protected static IEnumerable ApplyPoints(IEnumerable // Calculation pointRule.ApplyPoints(data, pointRows.ToList()); // remove points from filtered rows and set points eligible - pointRows.ForEach(x => x.PointsEligible = true); + pointRows.ForEach(x => x.PointsEligible = true); rows.Except(pointRows) - .ForEach(x => { + .ForEach(x => + { x.RacePoints = 0; x.PointsEligible = false; }); @@ -74,7 +75,7 @@ protected static (TId? id, TimeSpan lap) GetBestLapValue(IEnumerable } catch (Exception ex) when (ex is InvalidOperationException) { - return Array.Empty<(TId? id, TValue value)>(); + return []; } } @@ -144,7 +145,7 @@ protected static IEnumerable CombineResults(IEnum return combined.ToList(); } - private static AddPenaltyCalculationData CreateAddPenaltyFromAutoPenalty(ResultRowCalculationResult row, AutoPenaltyConfigurationData autoPenalty, + private static AddPenaltyCalculationData CreateAddPenaltyFromAutoPenalty(ResultRowCalculationResult row, AutoPenaltyConfigurationData autoPenalty, int penaltyMultiplikator) { var penalty = new AddPenaltyCalculationData() @@ -158,17 +159,17 @@ private static AddPenaltyCalculationData CreateAddPenaltyFromAutoPenalty(ResultR return penalty; } - private static IEnumerable CalculateAutoPenalties(IEnumerable rows, + private static IEnumerable CalculateAutoPenalties(IEnumerable rows, IEnumerable autoPenalties) { - foreach(var autoPenalty in autoPenalties) + foreach (var autoPenalty in autoPenalties) { var penaltyRows = autoPenalty.Conditions .FilterRows(rows); var grouped = penaltyRows.GroupBy(x => x); - foreach(var row in grouped.Where(x => x.Any())) + foreach (var row in grouped.Where(x => x.Any())) { - row.Key.AddPenalties = row.Key.AddPenalties.Concat(new[] { CreateAddPenaltyFromAutoPenalty(row.Key, autoPenalty, row.Count()) }); + row.Key.AddPenalties = [.. row.Key.AddPenalties, CreateAddPenaltyFromAutoPenalty(row.Key, autoPenalty, row.Count())]; } } return rows; @@ -287,13 +288,12 @@ private static IEnumerable ApplyBonusPoints(IEnumera } var minIncs = rows.Any(x => x.PenaltyPoints == 0) ? rows.Where(x => x.PenaltyPoints == 0).Min(x => x.Incidents) : -1; - var fastestLapRow = GetBestLapValue(rows, x => x, x => x.FastestLapTime); foreach (var bonus in BonusPoints) { var bonusType = bonus.Type; - var bonusKeyValue = (int)bonus.Value; - var bonusPoints = (int)bonus.Points; + var bonusKeyValue = bonus.Value; + var bonusPoints = bonus.Points; rows = bonusType switch { BonusPointType.Position => ApplyPositionBonusPoints(rows, bonusKeyValue, bonusPoints), @@ -306,7 +306,7 @@ private static IEnumerable ApplyBonusPoints(IEnumera BonusPointType.LeadMostLaps => ApplyLeadMostLapsBonusPoints(rows, bonusPoints), BonusPointType.NoIncidents => ApplyNoIncidentsBonusPoints(rows, bonusPoints), BonusPointType.FastestAverageLap => ApplyFastestAverageLapBonusPoints(rows, bonusPoints), - BonusPointType.Custom => ApplyCustomBonusPoints(rows, bonus.Conditions, bonusPoints), + BonusPointType.Custom => ApplyCustomBonusPoints(rows, bonus, bonusPoints), _ => rows, }; } @@ -314,10 +314,11 @@ private static IEnumerable ApplyBonusPoints(IEnumera return rows; } - private static IEnumerable ApplyCustomBonusPoints(IEnumerable rows, FilterGroupRowFilter conditions, + private static IEnumerable ApplyCustomBonusPoints(IEnumerable rows, BonusPointConfiguration bonus, int bonusPoints) { - var bonusRows = conditions.FilterRows(rows); + var bonusRows = bonus.Conditions.FilterRows(rows); + foreach (var row in bonusRows) { row.BonusPoints += bonusPoints; @@ -337,12 +338,11 @@ private static IEnumerable ApplyFastestAverageLapBon private static IEnumerable ApplyNoIncidentsBonusPoints(IEnumerable rows, int points) { - foreach(var row in rows) + var bonusRows = rows.Where(x => x.Incidents == 0); + + foreach (var row in bonusRows) { - if (row.Incidents == 0) - { - row.BonusPoints += points; - } + row.BonusPoints += points; } return rows; } @@ -350,24 +350,22 @@ private static IEnumerable ApplyNoIncidentsBonusPoin private static IEnumerable ApplyLeadMostLapsBonusPoints(IEnumerable rows, int points) { var mostLapsLead = rows.Max(x => x.LeadLaps); - foreach(var row in rows) + var bonusRows = rows.Where(x => x.LeadLaps == mostLapsLead); + + foreach (var row in bonusRows) { - if (row.LeadLaps == mostLapsLead) - { - row.BonusPoints += points; - } + row.BonusPoints += points; } return rows; } private static IEnumerable ApplyLeadOneLapBonusPoints(IEnumerable rows, int points) { - foreach(var row in rows) + var bonusRows = rows.Where(x => x.LeadLaps > 0); + + foreach (var row in bonusRows) { - if (row.LeadLaps > 0) - { - row.BonusPoints += points; - } + row.BonusPoints += points; } return rows; } @@ -375,12 +373,11 @@ private static IEnumerable ApplyLeadOneLapBonusPoint private static IEnumerable ApplyMostPositionsLostBonusPoints(IEnumerable rows, int points) { var mostPositionsLost = rows.Max(x => x.PositionChange); - foreach (var row in rows) + var bonusRows = rows.Where(x => x.PositionChange == mostPositionsLost); + + foreach (var row in bonusRows) { - if (row.PositionChange == mostPositionsLost) - { - row.BonusPoints += points; - } + row.BonusPoints += points; } return rows; } @@ -388,12 +385,11 @@ private static IEnumerable ApplyMostPositionsLostBon private static IEnumerable ApplyMostPositionsGainedBonusPoints(IEnumerable rows, int points) { var mostPositionsGained = rows.Min(x => x.PositionChange); - foreach(var row in rows) + var bonusRows = rows.Where(x => x.PositionChange == mostPositionsGained); + + foreach (var row in bonusRows) { - if (row.PositionChange == mostPositionsGained) - { - row.BonusPoints += points; - } + row.BonusPoints += points; } return rows; } @@ -416,6 +412,7 @@ private static IEnumerable ApplyCleanestDriverBonusP var minIncRows = GetBestValues(rows.Where(condition), x => x.Incidents, x => x, x => x.Min()) .Select(x => x.id) .NotNull(); + foreach (var row in minIncRows) { row.BonusPoints += points; diff --git a/src/iRLeagueApiCore.Services/ResultService/Calculation/ColumnValueRowFilter.cs b/src/iRLeagueApiCore.Services/ResultService/Calculation/ColumnValueRowFilter.cs deleted file mode 100644 index 9e3c89df..00000000 --- a/src/iRLeagueApiCore.Services/ResultService/Calculation/ColumnValueRowFilter.cs +++ /dev/null @@ -1,132 +0,0 @@ -using iRLeagueApiCore.Common.Enums; -using iRLeagueApiCore.Services.ResultService.Models; -using System.Globalization; -using System.Reflection; - -namespace iRLeagueApiCore.Services.ResultService.Calculation; - -internal sealed class ColumnValueRowFilter : RowFilter -{ - public bool AllowForEach { get; private set; } - - /// - /// Create an instance of a - /// - /// Name of the property matching the desired column - /// Type of comparison to execute - /// Values corresponding to comparator - /// Action to perform: Keep / Remove - /// Enable multiplication of rows using "ForEach" comparator - /// - public ColumnValueRowFilter(string propertyName, ComparatorType comparator, IEnumerable values, MatchedValueAction action, bool allowForEach = false) - { - Action = action; - AllowForEach = allowForEach; - try - { - ColumnProperty = GetColumnPropertyInfo(propertyName); - } - catch (InvalidOperationException ex) - { - throw new ArgumentException($"Parameter value {propertyName} did not target a valid column property on type {typeof(ResultRowCalculationResult)}", - nameof(propertyName), ex); - } - Comparator = comparator; - CompareFunc = GetCompareFunction(comparator); - try - { - FilterValues = GetFilterValuesOfType(ColumnProperty.PropertyType, values).ToList(); - } - catch (Exception ex) when (ex is InvalidCastException || - ex is FormatException || - ex is OverflowException || - ex is ArgumentNullException) - { - throw new ArgumentException($"Parameter was not of type {ColumnProperty.PropertyType} of column property {ColumnProperty.Name}", nameof(values), ex); - } - } - - public PropertyInfo ColumnProperty { get; } - public ComparatorType Comparator { get; } - private Func, bool> CompareFunc { get; } - public IEnumerable FilterValues { get; } - public MatchedValueAction Action { get; } - - public override IEnumerable FilterRows(IEnumerable rows) - { - var match = rows.Where(x => MatchFilterValues(x, ColumnProperty, FilterValues, CompareFunc)); - if (Comparator == ComparatorType.ForEach && AllowForEach) - { - // special handling for ForEach --> duplicate rows by multiple of values - match = MultiplyRows(match, ColumnProperty, FilterValues); - } - return Action switch - { - MatchedValueAction.Keep => match, - MatchedValueAction.Remove => rows.Except(match), - _ => rows, - }; - } - - private static bool MatchFilterValues(ResultRowCalculationResult row, PropertyInfo property, IEnumerable filterValues, Func, bool> compare) - { - var value = (IComparable?)property.GetValue(row); - return compare(value, filterValues); - } - - private static PropertyInfo GetColumnPropertyInfo(string propertyName) - { - var sourceType = typeof(ResultRowCalculationResult); - var propertyInfo = sourceType.GetProperty(propertyName) - ?? throw new InvalidOperationException($"{typeof(ResultRowCalculationResult)} does not have a property with name {propertyName}"); - if (typeof(IComparable).IsAssignableFrom(propertyInfo.PropertyType) == false) - { - throw new InvalidOperationException($"Column {propertyName} of type {typeof(ResultRowCalculationResult)} does not implement IComparable"); - } - return propertyInfo; - } - - private static IEnumerable GetFilterValuesOfType(Type type, IEnumerable values) - { - if (type.Equals(typeof(TimeSpan))) - { - return values.Select(x => TimeSpan.Parse(x)).Cast(); - } - return values.Select(x => Convert.ChangeType(x, type, CultureInfo.InvariantCulture)).Cast(); - } - - private static IEnumerable MultiplyRows(IEnumerable rows, PropertyInfo property, - IEnumerable filterValues) - { - if (filterValues.Any() == false) - { - return rows; - } - var compareValue = Convert.ToDouble(filterValues.First()); - List multipliedRows = new(); - foreach (var row in rows) - { - var value = Convert.ToDouble(property.GetValue(row)); - var count = (int)(value / compareValue); - for (int i=0; i, bool> GetCompareFunction(ComparatorType comparatorType) - => comparatorType switch - { - ComparatorType.IsBigger => (x, y) => { var c = x?.CompareTo(y.FirstOrDefault()); return c == 1; }, - ComparatorType.IsBiggerOrEqual => (x, y) => { var c = x?.CompareTo(y.FirstOrDefault()); return c == 1 || c == 0; }, - ComparatorType.IsEqual => (x, y) => { var c = x?.CompareTo(y.FirstOrDefault()); return c == 0; }, - ComparatorType.IsSmallerOrEqual => (x, y) => { var c = x?.CompareTo(y.FirstOrDefault()); return c == -1 || c == 0; }, - ComparatorType.IsSmaller => (x, y) => { var c = x?.CompareTo(y.FirstOrDefault()); return c == -1; }, - ComparatorType.NotEqual => (x, y) => { var c = x?.CompareTo(y.FirstOrDefault()); return c == 1 || c == -1; }, - ComparatorType.InList => (x, y) => { var c = y.Any(z => x?.CompareTo(z) == 0); return c; }, - ComparatorType.ForEach => (x, y) => { var c = x?.CompareTo(y.FirstOrDefault()); return c == 1 || c == 0; }, - _ => (x, y) => true, - }; -} diff --git a/src/iRLeagueApiCore.Services/ResultService/Calculation/DefaultPointRule.cs b/src/iRLeagueApiCore.Services/ResultService/Calculation/DefaultPointRule.cs index 786807de..d8f417ba 100644 --- a/src/iRLeagueApiCore.Services/ResultService/Calculation/DefaultPointRule.cs +++ b/src/iRLeagueApiCore.Services/ResultService/Calculation/DefaultPointRule.cs @@ -1,5 +1,6 @@ using iRLeagueApiCore.Common.Enums; using iRLeagueApiCore.Common.Models; +using iRLeagueApiCore.Services.ResultService.Calculation.Filters; using iRLeagueApiCore.Services.ResultService.Models; namespace iRLeagueApiCore.Services.ResultService.Calculation; diff --git a/src/iRLeagueApiCore.Services/ResultService/Calculation/Filters/ColumnValueRowFilter.cs b/src/iRLeagueApiCore.Services/ResultService/Calculation/Filters/ColumnValueRowFilter.cs new file mode 100644 index 00000000..66abad7d --- /dev/null +++ b/src/iRLeagueApiCore.Services/ResultService/Calculation/Filters/ColumnValueRowFilter.cs @@ -0,0 +1,75 @@ +using iRLeagueApiCore.Common.Enums; +using iRLeagueApiCore.Services.ResultService.Extensions; +using iRLeagueApiCore.Services.ResultService.Models; +using MySqlX.XDevAPI.Relational; +using System.Globalization; +using System.Reflection; + +namespace iRLeagueApiCore.Services.ResultService.Calculation.Filters; + +internal sealed class ColumnValueRowFilter : RowFilter +{ + /// + /// Create an instance of a + /// + /// Name of the property matching the desired column + /// Type of comparison to execute + /// Values corresponding to comparator + /// Action to perform: Keep / Remove + /// Enable multiplication of rows using "ForEach" comparator + /// + public ColumnValueRowFilter(string propertyName, ComparatorType comparatorType, IEnumerable values, MatchedValueAction action, bool allowForEach = false) + { + try + { + ColumnProperty = GetColumnPropertyInfo(propertyName); + } + catch (InvalidOperationException ex) + { + throw new ArgumentException($"Parameter value {propertyName} did not target a valid column property on type {typeof(ResultRowCalculationResult)}", + nameof(propertyName), ex); + } + try + { + FilterValues = GetFilterValuesOfType(ColumnProperty.PropertyType, values).ToList(); + } + catch (Exception ex) when (ex is InvalidCastException || + ex is FormatException || + ex is OverflowException || + ex is ArgumentNullException) + { + throw new ArgumentException($"Parameter was not of type {ColumnProperty.PropertyType} of column property {ColumnProperty.Name}", nameof(values), ex); + } + Comparator = new(comparatorType, action, allowForEach); + } + + public PropertyInfo ColumnProperty { get; } + public ComparatorFilter Comparator { get; } + public IEnumerable FilterValues { get; } + + public override IEnumerable FilterRows(IEnumerable rows) + { + return Comparator.GetFilteredRows(rows, (row) => (IComparable?)ColumnProperty.GetValue(row), FilterValues); + } + + private static PropertyInfo GetColumnPropertyInfo(string propertyName) + { + var sourceType = typeof(ResultRowCalculationResult); + var propertyInfo = sourceType.GetProperty(propertyName) + ?? throw new InvalidOperationException($"{typeof(ResultRowCalculationResult)} does not have a property with name {propertyName}"); + if (typeof(IComparable).IsAssignableFrom(propertyInfo.PropertyType) == false) + { + throw new InvalidOperationException($"Column {propertyName} of type {typeof(ResultRowCalculationResult)} does not implement IComparable"); + } + return propertyInfo; + } + + private static IEnumerable GetFilterValuesOfType(Type type, IEnumerable values) + { + if (type.Equals(typeof(TimeSpan))) + { + return values.Select(x => TimeSpan.Parse(x)).Cast(); + } + return values.Select(x => Convert.ChangeType(x, type, CultureInfo.InvariantCulture)).Cast(); + } +} diff --git a/src/iRLeagueApiCore.Services/ResultService/Calculation/Filters/ComparatorFilter.cs b/src/iRLeagueApiCore.Services/ResultService/Calculation/Filters/ComparatorFilter.cs new file mode 100644 index 00000000..4fb1c713 --- /dev/null +++ b/src/iRLeagueApiCore.Services/ResultService/Calculation/Filters/ComparatorFilter.cs @@ -0,0 +1,99 @@ +using iRLeagueApiCore.Common.Enums; +using iRLeagueApiCore.Services.ResultService.Models; +using System.Reflection; + +namespace iRLeagueApiCore.Services.ResultService.Calculation.Filters; +internal sealed class ComparatorFilter +{ + public bool AllowForEach { get; private set; } + public ComparatorType ComparatorType { get; } + private Func, bool> CompareFunc { get; } + public MatchedValueAction Action { get; } + + public ComparatorFilter(ComparatorType type, MatchedValueAction action, bool allowForEach = false) + { + ComparatorType = type; + CompareFunc = GetCompareFunction(ComparatorType); + Action = action; + AllowForEach = allowForEach; + } + + public IEnumerable GetFilteredRows(IEnumerable rows, Func getCompareValue, IEnumerable filterValues) + { + if (rows.Any()) { + filterValues = ComparatorType switch + { + // Min and Max comaparators do not have fixed compare values - fetch value from row column + ComparatorType.Min => [rows.Min(x => getCompareValue(x))], + ComparatorType.Max => [rows.Max(x => getCompareValue(x))], + // Use user provided values otherwise + _ => filterValues, + }; + } + + var match = rows.Where(x => MatchFilterValues(x, getCompareValue, filterValues, CompareFunc)); + if (ComparatorType == ComparatorType.ForEach && AllowForEach) + { + // special handling for ForEach --> duplicate rows by multiple of values + match = MultiplyRows(match, getCompareValue, filterValues); + } + return Action switch + { + MatchedValueAction.Keep => match, + MatchedValueAction.Remove => rows.Except(match), + _ => rows, + }; + } + + private static bool MatchFilterValues(T row, Func getCompareValue, IEnumerable filterValues, + Func, bool> compare) + { + var value = getCompareValue(row); + return compare(value, filterValues); + } + + private static IEnumerable MultiplyRows(IEnumerable rows, Func getCompareValue, + IEnumerable filterValues) + { + if (filterValues.Any() == false) + { + return rows; + } + var compareValue = Convert.ToDouble(filterValues.First()); + List multipliedRows = []; + foreach (var row in rows) + { + var value = Convert.ToDouble(getCompareValue(row)); + var count = (int)(value / compareValue); + for (int i = 0; i < count; i++) + { + multipliedRows.Add(row); + } + } + return multipliedRows; + } + + private static Func, bool> GetCompareFunction(ComparatorType comparatorType) + => comparatorType switch + { + ComparatorType.IsBigger => (x, y) => { var c = x?.CompareTo(y.FirstOrDefault()); return c == 1; } + , + ComparatorType.IsBiggerOrEqual => (x, y) => { var c = x?.CompareTo(y.FirstOrDefault()); return c == 1 || c == 0; } + , + ComparatorType.IsEqual => (x, y) => { var c = x?.CompareTo(y.FirstOrDefault()); return c == 0; } + , + ComparatorType.IsSmallerOrEqual => (x, y) => { var c = x?.CompareTo(y.FirstOrDefault()); return c == -1 || c == 0; } + , + ComparatorType.IsSmaller => (x, y) => { var c = x?.CompareTo(y.FirstOrDefault()); return c == -1; } + , + ComparatorType.NotEqual => (x, y) => { var c = x?.CompareTo(y.FirstOrDefault()); return c == 1 || c == -1; } + , + ComparatorType.InList => (x, y) => { var c = y.Any(z => x?.CompareTo(z) == 0); return c; } + , + ComparatorType.ForEach => (x, y) => { var c = x?.CompareTo(y.FirstOrDefault()); return c == 1 || c == 0; } + , + ComparatorType.Max or ComparatorType.Min => (x, y) => { var c = x?.CompareTo(y.FirstOrDefault()); return c == 0; } + , + _ => (x, y) => true, + }; +} diff --git a/src/iRLeagueApiCore.Services/ResultService/Calculation/Filters/CountRowFilter.cs b/src/iRLeagueApiCore.Services/ResultService/Calculation/Filters/CountRowFilter.cs new file mode 100644 index 00000000..40d72099 --- /dev/null +++ b/src/iRLeagueApiCore.Services/ResultService/Calculation/Filters/CountRowFilter.cs @@ -0,0 +1,36 @@ + +using Google.Protobuf.WellKnownTypes; +using iRLeagueApiCore.Common.Enums; +using iRLeagueApiCore.Services.ResultService.Models; + +namespace iRLeagueApiCore.Services.ResultService.Calculation.Filters; + +/// +/// Filter that counts the number of rows in the filter input and either matches all rows if the conditions match or none +/// +internal sealed class CountRowFilter : RowFilter +{ + public ComparatorFilter Comparator { get; } + public IEnumerable FilterValues { get; } + + public CountRowFilter(ComparatorType comparator, IEnumerable filterValues, MatchedValueAction action, bool allowForEach = false) + { + Comparator = new(comparator, action, allowForEach); + try + { + FilterValues = filterValues.Select(x => Convert.ToInt32(x)).Cast().ToList(); + } + catch (Exception ex) when (ex is InvalidCastException || + ex is FormatException || + ex is OverflowException) + { + throw new ArgumentException($"Parameter was not of type {typeof(int)}", nameof(filterValues), ex); + } + } + + public override IEnumerable FilterRows(IEnumerable rows) + { + int count = rows.Count(); + return Comparator.GetFilteredRows(rows, _ => count, FilterValues); + } +} diff --git a/src/iRLeagueApiCore.Services/ResultService/Calculation/DefaultRowFilter.cs b/src/iRLeagueApiCore.Services/ResultService/Calculation/Filters/DefaultRowFilter.cs similarity index 70% rename from src/iRLeagueApiCore.Services/ResultService/Calculation/DefaultRowFilter.cs rename to src/iRLeagueApiCore.Services/ResultService/Calculation/Filters/DefaultRowFilter.cs index d1efc03f..245caf95 100644 --- a/src/iRLeagueApiCore.Services/ResultService/Calculation/DefaultRowFilter.cs +++ b/src/iRLeagueApiCore.Services/ResultService/Calculation/Filters/DefaultRowFilter.cs @@ -1,4 +1,4 @@ -namespace iRLeagueApiCore.Services.ResultService.Calculation; +namespace iRLeagueApiCore.Services.ResultService.Calculation.Filters; internal sealed class DefaultRowFilter : RowFilter { diff --git a/src/iRLeagueApiCore.Services/ResultService/Calculation/FilterGroupRowFilter.cs b/src/iRLeagueApiCore.Services/ResultService/Calculation/Filters/FilterGroupRowFilter.cs similarity index 88% rename from src/iRLeagueApiCore.Services/ResultService/Calculation/FilterGroupRowFilter.cs rename to src/iRLeagueApiCore.Services/ResultService/Calculation/Filters/FilterGroupRowFilter.cs index 83a24f16..b7869b84 100644 --- a/src/iRLeagueApiCore.Services/ResultService/Calculation/FilterGroupRowFilter.cs +++ b/src/iRLeagueApiCore.Services/ResultService/Calculation/Filters/FilterGroupRowFilter.cs @@ -1,4 +1,4 @@ -namespace iRLeagueApiCore.Services.ResultService.Calculation; +namespace iRLeagueApiCore.Services.ResultService.Calculation.Filters; internal sealed class FilterGroupRowFilter : RowFilter { private readonly IList<(FilterCombination combination, RowFilter rowFilter)> filters; @@ -18,7 +18,7 @@ public FilterGroupRowFilter(IEnumerable<(FilterCombination, RowFilter)> filte public override IEnumerable FilterRows(IEnumerable rows) { var originalRows = rows.ToList(); - foreach(var (combination, filter) in filters) + foreach (var (combination, filter) in filters) { rows = combination switch { diff --git a/src/iRLeagueApiCore.Services/ResultService/Calculation/IdRowFilter.cs b/src/iRLeagueApiCore.Services/ResultService/Calculation/Filters/IdRowFilter.cs similarity index 92% rename from src/iRLeagueApiCore.Services/ResultService/Calculation/IdRowFilter.cs rename to src/iRLeagueApiCore.Services/ResultService/Calculation/Filters/IdRowFilter.cs index 72316bed..6529186d 100644 --- a/src/iRLeagueApiCore.Services/ResultService/Calculation/IdRowFilter.cs +++ b/src/iRLeagueApiCore.Services/ResultService/Calculation/Filters/IdRowFilter.cs @@ -3,7 +3,7 @@ using System.Diagnostics.Eventing.Reader; using System.Globalization; -namespace iRLeagueApiCore.Services.ResultService.Calculation; +namespace iRLeagueApiCore.Services.ResultService.Calculation.Filters; internal sealed class IdRowFilter : RowFilter { public IReadOnlyCollection MatchIds; @@ -40,7 +40,7 @@ public override IEnumerable FilterRows(IEnumerable rows) { return (TId?)Convert.ChangeType(idString, typeof(TId?), CultureInfo.InvariantCulture); } - catch (Exception ex) when (ex is InvalidCastException || ex is FormatException || ex is ArgumentException) + catch (Exception ex) when (ex is InvalidCastException || ex is FormatException || ex is ArgumentException) { throw new ArgumentException($"Argument \"{idString}\" is not a valid id"); } diff --git a/src/iRLeagueApiCore.Services/ResultService/Calculation/RowFilter.cs b/src/iRLeagueApiCore.Services/ResultService/Calculation/Filters/RowFilter.cs similarity index 74% rename from src/iRLeagueApiCore.Services/ResultService/Calculation/RowFilter.cs rename to src/iRLeagueApiCore.Services/ResultService/Calculation/Filters/RowFilter.cs index d39f1257..a8bd917b 100644 --- a/src/iRLeagueApiCore.Services/ResultService/Calculation/RowFilter.cs +++ b/src/iRLeagueApiCore.Services/ResultService/Calculation/Filters/RowFilter.cs @@ -1,4 +1,4 @@ -namespace iRLeagueApiCore.Services.ResultService.Calculation; +namespace iRLeagueApiCore.Services.ResultService.Calculation.Filters; internal abstract class RowFilter { diff --git a/src/iRLeagueApiCore.Services/ResultService/Calculation/PointRule.cs b/src/iRLeagueApiCore.Services/ResultService/Calculation/PointRule.cs index 881c8ba8..664f9519 100644 --- a/src/iRLeagueApiCore.Services/ResultService/Calculation/PointRule.cs +++ b/src/iRLeagueApiCore.Services/ResultService/Calculation/PointRule.cs @@ -1,4 +1,5 @@ using iRLeagueApiCore.Common.Models; +using iRLeagueApiCore.Services.ResultService.Calculation.Filters; using iRLeagueApiCore.Services.ResultService.Models; namespace iRLeagueApiCore.Services.ResultService.Calculation; diff --git a/src/iRLeagueApiCore.Services/ResultService/DataAccess/SessionCalculationConfigurationProvider.cs b/src/iRLeagueApiCore.Services/ResultService/DataAccess/SessionCalculationConfigurationProvider.cs index 8b10797b..84e09d14 100644 --- a/src/iRLeagueApiCore.Services/ResultService/DataAccess/SessionCalculationConfigurationProvider.cs +++ b/src/iRLeagueApiCore.Services/ResultService/DataAccess/SessionCalculationConfigurationProvider.cs @@ -1,6 +1,7 @@ using iRLeagueApiCore.Common.Enums; using iRLeagueApiCore.Common.Models; using iRLeagueApiCore.Services.ResultService.Calculation; +using iRLeagueApiCore.Services.ResultService.Calculation.Filters; using iRLeagueApiCore.Services.ResultService.Extensions; using iRLeagueApiCore.Services.ResultService.Models; using iRLeagueDatabaseCore.Models; @@ -247,6 +248,7 @@ private static FilterGroupRowFilter MapToFilterGroup condition.FilterValues, condition.Action, allowForEach: allowForEach), FilterType.Member => new IdRowFilter(condition.FilterValues, x => x.MemberId.GetValueOrDefault(), condition.Action), FilterType.Team => new IdRowFilter(condition.FilterValues, x => x.TeamId.GetValueOrDefault(), condition.Action), + FilterType.Count => new CountRowFilter(condition.Comparator, condition.FilterValues, condition.Action), _ => null, }; } diff --git a/src/iRLeagueApiCore.Services/ResultService/Models/AutoPenaltyConfigurationData.cs b/src/iRLeagueApiCore.Services/ResultService/Models/AutoPenaltyConfigurationData.cs index e2f6529b..30427e76 100644 --- a/src/iRLeagueApiCore.Services/ResultService/Models/AutoPenaltyConfigurationData.cs +++ b/src/iRLeagueApiCore.Services/ResultService/Models/AutoPenaltyConfigurationData.cs @@ -1,5 +1,5 @@ using iRLeagueApiCore.Common.Enums; -using iRLeagueApiCore.Services.ResultService.Calculation; +using iRLeagueApiCore.Services.ResultService.Calculation.Filters; namespace iRLeagueApiCore.Services.ResultService.Models; internal sealed class AutoPenaltyConfigurationData diff --git a/src/iRLeagueApiCore.Services/ResultService/Models/BonusPointConfiguration.cs b/src/iRLeagueApiCore.Services/ResultService/Models/BonusPointConfiguration.cs index 82614a25..258feef6 100644 --- a/src/iRLeagueApiCore.Services/ResultService/Models/BonusPointConfiguration.cs +++ b/src/iRLeagueApiCore.Services/ResultService/Models/BonusPointConfiguration.cs @@ -1,4 +1,4 @@ -using iRLeagueApiCore.Services.ResultService.Calculation; +using iRLeagueApiCore.Services.ResultService.Calculation.Filters; namespace iRLeagueApiCore.Services.ResultService.Models; internal sealed class BonusPointConfiguration diff --git a/src/iRLeagueDatabaseCore/iRLeagueDatabaseCore.csproj b/src/iRLeagueDatabaseCore/iRLeagueDatabaseCore.csproj index 3fd1684d..e3fee16a 100644 --- a/src/iRLeagueDatabaseCore/iRLeagueDatabaseCore.csproj +++ b/src/iRLeagueDatabaseCore/iRLeagueDatabaseCore.csproj @@ -28,7 +28,7 @@ net6.0 11 iRLeagueDatabaseCore - 0.12.1 + 0.12.2 enable diff --git a/test/iRLeagueApiCore.Services.Tests/ResultService/Calculation/CalculationMockHelper.cs b/test/iRLeagueApiCore.Services.Tests/ResultService/Calculation/CalculationMockHelper.cs index 14b43680..f8214fbd 100644 --- a/test/iRLeagueApiCore.Services.Tests/ResultService/Calculation/CalculationMockHelper.cs +++ b/test/iRLeagueApiCore.Services.Tests/ResultService/Calculation/CalculationMockHelper.cs @@ -1,5 +1,6 @@ using iRLeagueApiCore.Common.Models; using iRLeagueApiCore.Services.ResultService.Calculation; +using iRLeagueApiCore.Services.ResultService.Calculation.Filters; using iRLeagueApiCore.Services.ResultService.Models; namespace iRLeagueApiCore.Services.Tests.ResultService.Calculation; diff --git a/test/iRLeagueApiCore.Services.Tests/ResultService/Calculation/ColumnValueRowFilterTests.cs b/test/iRLeagueApiCore.Services.Tests/ResultService/Calculation/ColumnValueRowFilterTests.cs index 7dc396a4..d425a89f 100644 --- a/test/iRLeagueApiCore.Services.Tests/ResultService/Calculation/ColumnValueRowFilterTests.cs +++ b/test/iRLeagueApiCore.Services.Tests/ResultService/Calculation/ColumnValueRowFilterTests.cs @@ -1,6 +1,6 @@ using iRLeagueApiCore.Common.Enums; using iRLeagueApiCore.Mocking.Extensions; -using iRLeagueApiCore.Services.ResultService.Calculation; +using iRLeagueApiCore.Services.ResultService.Calculation.Filters; using iRLeagueApiCore.Services.ResultService.Extensions; using iRLeagueApiCore.Services.ResultService.Models; using System.Globalization; @@ -11,32 +11,32 @@ public sealed class ColumnValueRowFilterTests { private static readonly Fixture fixture = new(); - private static IEnumerable TestValidConstructorData() => new[] - { - new object[] { nameof(ResultRowCalculationResult.FinalPosition), new[] { fixture.Create().ToString() } }, - new object[] { nameof(ResultRowCalculationResult.FinishPosition), new[] { fixture.Create().ToString(CultureInfo.InvariantCulture) }}, - new object[] { nameof(ResultRowCalculationResult.Firstname), new[] { fixture.Create() }}, - new object[] { nameof(ResultRowCalculationResult.QualifyingTime), new[] { fixture.Create().ToString() }}, - }; - - private static IEnumerable TestInValidConstructorData() => new[] - { - new object[] { nameof(ResultRowCalculationResult.FinalPosition), new[] { fixture.Create().ToString() } }, - new object[] { nameof(ResultRowCalculationResult.FinishPosition), new[] { fixture.Create().ToString(CultureInfo.InvariantCulture) }}, - new object[] { nameof(ResultRowCalculationResult.QualifyingTime), new[] { fixture.Create().ToString() }}, - new object[] { fixture.Create(), new[] { fixture.Create().ToString() }}, - }; + private static IEnumerable TestValidConstructorData() => + [ + [nameof(ResultRowCalculationResult.FinalPosition), new[] { fixture.Create().ToString() }], + [nameof(ResultRowCalculationResult.FinishPosition), new[] { fixture.Create().ToString(CultureInfo.InvariantCulture) }], + [nameof(ResultRowCalculationResult.Firstname), new[] { fixture.Create() }], + [nameof(ResultRowCalculationResult.QualifyingTime), new[] { fixture.Create().ToString() }], + ]; + + private static IEnumerable TestInValidConstructorData() => + [ + [nameof(ResultRowCalculationResult.FinalPosition), new[] { fixture.Create().ToString() }], + [nameof(ResultRowCalculationResult.FinishPosition), new[] { fixture.Create().ToString(CultureInfo.InvariantCulture) }], + [nameof(ResultRowCalculationResult.QualifyingTime), new[] { fixture.Create().ToString() }], + [fixture.Create(), new[] { fixture.Create().ToString() }], + ]; private static IEnumerable TestFilterRowsData() { var matchRow = fixture.Create(); - return new[] - { - new object[] { nameof(ResultRowCalculationResult.CompletedLaps), matchRow, new[] { matchRow.CompletedLaps.ToString() } }, - new object[] { nameof(ResultRowCalculationResult.FastestLapTime), matchRow, new[] { matchRow.FastestLapTime.ToString() } }, - new object[] { nameof(ResultRowCalculationResult.Firstname), matchRow, new[] { matchRow.Firstname.ToString() } }, - new object[] { nameof(ResultRowCalculationResult.CompletedPct), matchRow, new[] { matchRow.CompletedPct.ToString() } }, - }; + return + [ + [nameof(ResultRowCalculationResult.CompletedLaps), matchRow, new[] { matchRow.CompletedLaps.ToString() }], + [nameof(ResultRowCalculationResult.FastestLapTime), matchRow, new[] { matchRow.FastestLapTime.ToString() }], + [nameof(ResultRowCalculationResult.Firstname), matchRow, new[] { matchRow.Firstname.ToString() }], + [nameof(ResultRowCalculationResult.CompletedPct), matchRow, new[] { matchRow.CompletedPct.ToString() }], + ]; } [Theory] @@ -94,7 +94,7 @@ public void FilterRows_ShouldFilterValues_WhenComparaterIsSmaller() var rows = fixture.CreateMany(5) .OrderBy(x => x.TotalPoints); var cutoffRow = rows.ElementAt(1); - var sut = CreateSut(propertyName, new[] { cutoffRow.TotalPoints.ToString() }, ComparatorType.IsSmaller, MatchedValueAction.Keep); + var sut = CreateSut(propertyName, [cutoffRow.TotalPoints.ToString()], ComparatorType.IsSmaller, MatchedValueAction.Keep); var test = sut.FilterRows(rows); @@ -109,7 +109,7 @@ public void FilterRows_ShouldFilterValues_WhenComparaterIsSmallerOrEqual() var rows = fixture.CreateMany(5) .OrderBy(x => x.TotalPoints); var cutoffRow = rows.ElementAt(1); - var sut = CreateSut(propertyName, new[] { cutoffRow.TotalPoints.ToString() }, ComparatorType.IsSmallerOrEqual, MatchedValueAction.Keep); + var sut = CreateSut(propertyName, [cutoffRow.TotalPoints.ToString()], ComparatorType.IsSmallerOrEqual, MatchedValueAction.Keep); var test = sut.FilterRows(rows); @@ -125,7 +125,7 @@ public void FilterRows_ShouldFilterValues_WhenComparaterIsEqual() var rows = fixture.CreateMany(5) .OrderBy(x => x.TotalPoints); var cutoffRow = rows.ElementAt(1); - var sut = CreateSut(propertyName, new[] { cutoffRow.TotalPoints.ToString() }, ComparatorType.IsEqual, MatchedValueAction.Keep); + var sut = CreateSut(propertyName, [cutoffRow.TotalPoints.ToString()], ComparatorType.IsEqual, MatchedValueAction.Keep); var test = sut.FilterRows(rows); @@ -140,7 +140,7 @@ public void FilterRows_ShouldFilterValues_WhenComparaterIsBiggerOrEqual() var rows = fixture.CreateMany(5) .OrderBy(x => x.TotalPoints); var cutoffRow = rows.ElementAt(1); - var sut = CreateSut(propertyName, new[] { cutoffRow.TotalPoints.ToString() }, ComparatorType.IsBiggerOrEqual, MatchedValueAction.Keep); + var sut = CreateSut(propertyName, [cutoffRow.TotalPoints.ToString()], ComparatorType.IsBiggerOrEqual, MatchedValueAction.Keep); var test = sut.FilterRows(rows); @@ -155,7 +155,7 @@ public void FilterRows_ShouldFilterValues_WhenComparaterIsBigger() var rows = fixture.CreateMany(5) .OrderBy(x => x.TotalPoints); var cutoffRow = rows.ElementAt(1); - var sut = CreateSut(propertyName, new[] { cutoffRow.TotalPoints.ToString() }, ComparatorType.IsBigger, MatchedValueAction.Keep); + var sut = CreateSut(propertyName, [cutoffRow.TotalPoints.ToString()], ComparatorType.IsBigger, MatchedValueAction.Keep); var test = sut.FilterRows(rows); @@ -171,7 +171,7 @@ public void FilterRows_ShouldFilterValues_WhenComparaterIsNotEqual() var rows = fixture.CreateMany(5) .OrderBy(x => x.TotalPoints); var cutoffRow = rows.ElementAt(1); - var sut = CreateSut(propertyName, new[] { cutoffRow.TotalPoints.ToString() }, ComparatorType.NotEqual, MatchedValueAction.Keep); + var sut = CreateSut(propertyName, [cutoffRow.TotalPoints.ToString()], ComparatorType.NotEqual, MatchedValueAction.Keep); var test = sut.FilterRows(rows); @@ -187,7 +187,7 @@ public void FilterRows_ShouldFilterValues_WhenComparaterIsInList() .OrderBy(x => x.Firstname); var testRow1 = rows.ElementAt(1); var testRow2 = rows.ElementAt(3); - var sut = CreateSut(propertyName, new[] { testRow1.Firstname, testRow2.Firstname }, ComparatorType.InList, MatchedValueAction.Keep); + var sut = CreateSut(propertyName, [testRow1.Firstname, testRow2.Firstname], ComparatorType.InList, MatchedValueAction.Keep); var test = sut.FilterRows(rows); @@ -207,7 +207,7 @@ public void FilterRows_ShouldMultiplyRows_WhenComparatorIsForEach() var testRow1 = rows.ElementAt(0); var testRow2 = rows.ElementAt(1); var testRow3 = rows.ElementAt(2); - var sut = CreateSut(propertyName, new[] { "4.0" }, ComparatorType.ForEach, MatchedValueAction.Keep, allowForEach: true); + var sut = CreateSut(propertyName, ["4.0"], ComparatorType.ForEach, MatchedValueAction.Keep, allowForEach: true); var test = sut.FilterRows(rows); @@ -216,7 +216,39 @@ public void FilterRows_ShouldMultiplyRows_WhenComparatorIsForEach() test.Count(x => x.ScoredResultRowId == testRow3.ScoredResultRowId).Should().Be(2); } - private ColumnValueRowFilter CreateSut(string propertyName, + [Fact] + public void FilterRows_ShouldFilterValues_WhenComparatorIsMax() + { + var propertyName = nameof(ResultRowCalculationResult.TotalPoints); + var rows = fixture.CreateMany(5) + .OrderBy(x => x.TotalPoints); + rows.ElementAt(3).TotalPoints = rows.ElementAt(4).TotalPoints; + var sut = CreateSut(propertyName, [], ComparatorType.Max, MatchedValueAction.Keep); + + var test = sut.FilterRows(rows); + + test.Should().Contain(rows.ElementAt(3)); + test.Should().Contain(rows.ElementAt(4)); + test.Should().HaveCount(2); + } + + [Fact] + public void FilterRows_ShouldFilterValues_WhenComparatorIsMin() + { + var propertyName = nameof(ResultRowCalculationResult.TotalPoints); + var rows = fixture.CreateMany(5) + .OrderBy(x => x.TotalPoints); + rows.ElementAt(1).TotalPoints = rows.ElementAt(0).TotalPoints; + var sut = CreateSut(propertyName, [], ComparatorType.Min, MatchedValueAction.Keep); + + var test = sut.FilterRows(rows); + + test.Should().Contain(rows.ElementAt(0)); + test.Should().Contain(rows.ElementAt(1)); + test.Should().HaveCount(2); + } + + private static ColumnValueRowFilter CreateSut(string propertyName, IEnumerable filterValues, ComparatorType comparator = ComparatorType.IsSmallerOrEqual, MatchedValueAction action = MatchedValueAction.Keep, diff --git a/test/iRLeagueApiCore.Services.Tests/ResultService/Calculation/CountRowFilterTests.cs b/test/iRLeagueApiCore.Services.Tests/ResultService/Calculation/CountRowFilterTests.cs new file mode 100644 index 00000000..d4ffd110 --- /dev/null +++ b/test/iRLeagueApiCore.Services.Tests/ResultService/Calculation/CountRowFilterTests.cs @@ -0,0 +1,84 @@ +using iRLeagueApiCore.Common.Enums; +using iRLeagueApiCore.Services.ResultService.Calculation.Filters; +using iRLeagueApiCore.Services.ResultService.Extensions; +using iRLeagueApiCore.Services.ResultService.Models; +using Org.BouncyCastle.Asn1.Cmp; + +namespace iRLeagueApiCore.Services.Tests.ResultService.Calculation; +public sealed class CountRowFilterTests +{ + private static readonly Fixture fixture = new(); + + [Theory] + [InlineData("-1")] + [InlineData("2")] + [InlineData("42")] + public void Constructor_ShouldNotThrow_WithValidValue(string value) + { + var test = () => CreateSut([value]); + test.Should().NotThrow(); + } + + [Theory] + [InlineData("1.0")] + [InlineData("1e-2")] + [InlineData("test")] + public void Constructor_ShouldThrow_WithInvalidValue(string value) + { + var test = () => CreateSut([value]); + test.Should().Throw(); + } + + [Theory] + [InlineData(ComparatorType.IsSmaller, 2)] + [InlineData(ComparatorType.IsSmallerOrEqual, 3)] + [InlineData(ComparatorType.IsSmallerOrEqual, 2)] + [InlineData(ComparatorType.IsEqual, 3)] + [InlineData(ComparatorType.IsBiggerOrEqual, 3)] + [InlineData(ComparatorType.IsBiggerOrEqual, 4)] + [InlineData(ComparatorType.IsBigger, 4)] + [InlineData(ComparatorType.NotEqual, 2)] + public void FilterRows_ShouldKeepAllRows_WhenConditionMatches(ComparatorType comparatorType, int rowCount) + { + var rows = fixture.CreateMany(rowCount) + .ToList(); + rows = rows.Shuffle().ToList(); + List filterValues = ["3"]; + var sut = CreateSut(filterValues, comparatorType, MatchedValueAction.Keep); + + var test = sut.FilterRows(rows).ToList(); + + test.Should().HaveCount(rowCount); + } + + [Theory] + [InlineData(ComparatorType.IsSmaller, 3)] + [InlineData(ComparatorType.IsSmaller, 4)] + [InlineData(ComparatorType.IsSmallerOrEqual, 4)] + [InlineData(ComparatorType.IsEqual, 2)] + [InlineData(ComparatorType.IsEqual, 4)] + [InlineData(ComparatorType.IsBiggerOrEqual, 2)] + [InlineData(ComparatorType.IsBigger, 3)] + [InlineData(ComparatorType.IsBigger, 2)] + [InlineData(ComparatorType.NotEqual, 3)] + public void FilterRows_ShouldRemoveAllRows_WhenConditionMismatches(ComparatorType comparatorType, int rowCount) + { + var rows = fixture.CreateMany(rowCount) + .ToList(); + rows = rows.Shuffle().ToList(); + List filterValues = ["3"]; + var sut = CreateSut(filterValues, comparatorType, MatchedValueAction.Keep); + + var test = sut.FilterRows(rows).ToList(); + + test.Should().HaveCount(0); + } + + private static CountRowFilter CreateSut(IEnumerable filterValues, + ComparatorType comparator = ComparatorType.IsSmallerOrEqual, + MatchedValueAction action = MatchedValueAction.Keep, + bool allowForEach = false) + { + return new CountRowFilter(comparator, filterValues, action, allowForEach); + } +} diff --git a/test/iRLeagueApiCore.Services.Tests/ResultService/Calculation/FilterGroupRowFilterTests.cs b/test/iRLeagueApiCore.Services.Tests/ResultService/Calculation/FilterGroupRowFilterTests.cs index 622fc6a7..e53a5691 100644 --- a/test/iRLeagueApiCore.Services.Tests/ResultService/Calculation/FilterGroupRowFilterTests.cs +++ b/test/iRLeagueApiCore.Services.Tests/ResultService/Calculation/FilterGroupRowFilterTests.cs @@ -1,6 +1,6 @@ using iRLeagueApiCore.Common.Enums; using iRLeagueApiCore.Mocking.Extensions; -using iRLeagueApiCore.Services.ResultService.Calculation; +using iRLeagueApiCore.Services.ResultService.Calculation.Filters; using System.Diagnostics; namespace iRLeagueApiCore.Services.Tests.ResultService.Calculation; diff --git a/test/iRLeagueApiCore.Services.Tests/ResultService/Calculation/IdRowFilterTests.cs b/test/iRLeagueApiCore.Services.Tests/ResultService/Calculation/IdRowFilterTests.cs index 166d5b34..2b796653 100644 --- a/test/iRLeagueApiCore.Services.Tests/ResultService/Calculation/IdRowFilterTests.cs +++ b/test/iRLeagueApiCore.Services.Tests/ResultService/Calculation/IdRowFilterTests.cs @@ -1,9 +1,7 @@ using iRLeagueApiCore.Common.Enums; -using iRLeagueApiCore.Services.ResultService.Calculation; +using iRLeagueApiCore.Services.ResultService.Calculation.Filters; using iRLeagueApiCore.Services.ResultService.Extensions; using iRLeagueApiCore.Services.ResultService.Models; -using Microsoft.EntityFrameworkCore.Metadata.Internal; -using System.Globalization; namespace iRLeagueApiCore.Services.Tests.ResultService.Calculation; public sealed class IdRowFilterTests diff --git a/test/iRLeagueApiCore.Services.Tests/ResultService/Calculation/MemberSessionCalculationServiceTests.cs b/test/iRLeagueApiCore.Services.Tests/ResultService/Calculation/MemberSessionCalculationServiceTests.cs index 27937498..156f6dce 100644 --- a/test/iRLeagueApiCore.Services.Tests/ResultService/Calculation/MemberSessionCalculationServiceTests.cs +++ b/test/iRLeagueApiCore.Services.Tests/ResultService/Calculation/MemberSessionCalculationServiceTests.cs @@ -5,6 +5,7 @@ using iRLeagueApiCore.Common.Enums; using iRLeagueApiCore.Services.ResultService.Extensions; using iRLeagueApiCore.Common.Models; +using iRLeagueApiCore.Services.ResultService.Calculation.Filters; namespace iRLeagueApiCore.Services.Tests.ResultService.Calculation; @@ -232,7 +233,7 @@ public async Task Calculate_ShouldApplyTimePenaltyToInterval() .With(x => x.Time, TimeSpan.FromSeconds(spread + 1)) .Create(); var penaltyRow = data.ResultRows.ElementAt(0); - penaltyRow.AddPenalties = new[] { addPenalty }; + penaltyRow.AddPenalties = [addPenalty]; var config = GetCalculationConfiguration(data.LeagueId, data.SessionId); fixture.Register(() => config); var sut = CreateSut(); @@ -264,7 +265,7 @@ public async Task Calculate_ShouldSortAfterApplyingTimePenalty() .With(x => x.Time, TimeSpan.FromSeconds(spread + 1)) .Create(); var penaltyRow = data.ResultRows.ElementAt(penaltyIndex); - penaltyRow.AddPenalties = new[] { addPenalty }; + penaltyRow.AddPenalties = [addPenalty]; var config = GetCalculationConfiguration(data.LeagueId, data.SessionId); config.PointRule = CalculationMockHelper.MockPointRule( sortForPoints: x => x.OrderBy(x => x.Interval).ToList()); @@ -296,7 +297,7 @@ public async Task Calculate_ShouldApplyPositioningPenalty(int penaltyIndex, int .With(x => x.Positions, positionPenalty) .Create(); var penaltyRow = data.ResultRows.ElementAt(penaltyIndex); - penaltyRow.AddPenalties = new[] { addPenalty }; + penaltyRow.AddPenalties = [addPenalty]; var sut = CreateSut(); var test = await sut.Calculate(data); @@ -332,9 +333,9 @@ public async Task Calculate_ShouldApplyPositioningPenalty_WithMultiple( .With(x => x.Positions, positionPenalty2) .Create(); var penaltyRow1 = data.ResultRows.ElementAt(penaltyIndex1); - penaltyRow1.AddPenalties = new[] { addPenalty1 }; + penaltyRow1.AddPenalties = [addPenalty1]; var penaltyRow2 = data.ResultRows.ElementAt(penaltyIndex2); - penaltyRow2.AddPenalties = new[] { addPenalty2 }; + penaltyRow2.AddPenalties = [addPenalty2]; var sut = CreateSut(); var test = await sut.Calculate(data); @@ -366,7 +367,7 @@ public async Task Calculate_ShouldApplyPointsPenalty() .With(x => x.Points, spread + 1) .Create(); var penaltyRow = data.ResultRows.ElementAt(penaltyIndex); - penaltyRow.AddPenalties = new[] { addPenalty }; + penaltyRow.AddPenalties = [addPenalty]; var config = GetCalculationConfiguration(data.LeagueId, data.SessionId); config.PointRule = CalculationMockHelper.MockPointRule( sortFinal: x => x.OrderByDescending(x => x.TotalPoints).ToList()); @@ -401,7 +402,7 @@ public async Task Calculate_ShouldApplyNegativePointsPenaltyAsBonus() .With(x => x.Points, -(spread + 1)) .Create(); var penaltyRow = data.ResultRows.ElementAt(penaltyIndex); - penaltyRow.AddPenalties = new[] { addPenalty }; + penaltyRow.AddPenalties = [addPenalty]; var config = GetCalculationConfiguration(data.LeagueId, data.SessionId); config.PointRule = CalculationMockHelper.MockPointRule( sortFinal: x => x.OrderByDescending(x => x.TotalPoints).ToList()); @@ -427,11 +428,10 @@ public async Task Calculate_ShouldApplyPositionBonusPoints() .CreateMany(3); var config = GetCalculationConfiguration(data.LeagueId, data.SessionId); config.PointRule = CalculationMockHelper.MockPointRule( - bonusPoints: new BonusPointConfiguration[] - { + bonusPoints: [ new() { Type = BonusPointType.QualyPosition, Value = 1, Points = 2 }, new() { Type = BonusPointType.QualyPosition, Value = 2, Points = 1 }, - }); + ]); fixture.Register(() => config); var sut = CreateSut(); @@ -456,21 +456,21 @@ public async Task Calculate_ShouldApplyCustomBonusPoints() data.ResultRows.First().LeadLaps = 16; var config = GetCalculationConfiguration(data.LeagueId, data.SessionId); RowFilter condition1 = new ColumnValueRowFilter( - nameof(ResultRowCalculationResult.StartPosition), ComparatorType.IsEqual, new[] { "1" }, + nameof(ResultRowCalculationResult.StartPosition), ComparatorType.IsEqual, ["1"], MatchedValueAction.Keep, allowForEach: true); RowFilter condition2 = new ColumnValueRowFilter( - nameof(ResultRowCalculationResult.StartPosition), ComparatorType.IsEqual, new[] { "2" }, + nameof(ResultRowCalculationResult.StartPosition), ComparatorType.IsEqual, ["2"], MatchedValueAction.Keep, allowForEach: true); RowFilter condition3 = new ColumnValueRowFilter( - nameof(ResultRowCalculationResult.LeadLaps), ComparatorType.ForEach, new[] { "5" }, + nameof(ResultRowCalculationResult.LeadLaps), ComparatorType.ForEach, ["5"], MatchedValueAction.Keep, allowForEach: true); config.PointRule = CalculationMockHelper.MockPointRule( - bonusPoints: new BonusPointConfiguration[] - { - new() { Type = BonusPointType.Custom, Points = 2, Conditions = new(new[] { (FilterCombination.And, condition1) }) }, - new() { Type = BonusPointType.Custom, Points = 1, Conditions = new(new[] { (FilterCombination.And, condition2) }) }, - new() { Type = BonusPointType.Custom, Points = 2, Conditions = new(new[] { (FilterCombination.And, condition3) }) }, - }); + bonusPoints: + [ + new() { Type = BonusPointType.Custom, Points = 2, Conditions = new([(FilterCombination.And, condition1)]) }, + new() { Type = BonusPointType.Custom, Points = 1, Conditions = new([(FilterCombination.And, condition2)]) }, + new() { Type = BonusPointType.Custom, Points = 2, Conditions = new([(FilterCombination.And, condition3)]) }, + ]); fixture.Register(() => config); var sut = CreateSut(); @@ -488,7 +488,7 @@ public async Task Calculate_ShouldApplyCustomBonusPoints() [Fact] public async Task Calculate_ShouldAddAutoPenalties() { - var incidents = new[] { 2.0, 4.0, 8.0 }; + var incidents = new[] { 2.0, 4.0, 8.0 }; var data = GetCalculationData(); data.ResultRows = TestRowBuilder() .With(x => x.Incidents, incidents.CreateSequence()) @@ -497,18 +497,18 @@ public async Task Calculate_ShouldAddAutoPenalties() RowFilter filter = new ColumnValueRowFilter( nameof(ResultRowCalculationResult.Incidents), ComparatorType.ForEach, - new[] { "4.0" }, + ["4.0"], MatchedValueAction.Keep, allowForEach: true); var autoPenalty = new AutoPenaltyConfigurationData() { - Conditions = new FilterGroupRowFilter(new[] { (FilterCombination.And, filter) }), + Conditions = new FilterGroupRowFilter([(FilterCombination.And, filter)]), Description = "Test Autopenalty", Points = 10, Type = PenaltyType.Points, }; config.PointRule = CalculationMockHelper.MockPointRule( - autoPenalties: new[] { autoPenalty }); + autoPenalties: [autoPenalty]); fixture.Register(() => config); var sut = CreateSut(); @@ -520,8 +520,8 @@ public async Task Calculate_ShouldAddAutoPenalties() testRow1.PenaltyPoints.Should().Be(autoPenalty.Points); var testRow2 = test.ResultRows.ElementAt(2); testRow2.AddPenalties.Should().HaveCount(1); - testRow2.AddPenalties.First().Points.Should().Be(autoPenalty.Points*2); - testRow2.PenaltyPoints.Should().Be(autoPenalty.Points*2); + testRow2.AddPenalties.First().Points.Should().Be(autoPenalty.Points * 2); + testRow2.PenaltyPoints.Should().Be(autoPenalty.Points * 2); } [Fact] @@ -536,10 +536,10 @@ public async Task Calculate_ShouldApplyResultFilters() RowFilter filter = new ColumnValueRowFilter( nameof(ResultRowCalculationResult.FinishPosition), ComparatorType.IsSmallerOrEqual, - new[] { "3" }, + ["3"], MatchedValueAction.Keep); config.PointRule = CalculationMockHelper.MockPointRule( - resultFilters: new(new[] { (FilterCombination.And, filter) })); + resultFilters: new([(FilterCombination.And, filter)])); fixture.Register(() => config); var sut = CreateSut(); @@ -560,10 +560,10 @@ public async Task Calculate_ShouldApplyChampSeasonFilters() RowFilter filter = new ColumnValueRowFilter( nameof(ResultRowCalculationResult.FinishPosition), ComparatorType.IsSmallerOrEqual, - new[] { "3" }, + ["3"], MatchedValueAction.Keep); config.PointRule = CalculationMockHelper.MockPointRule( - champSeasonFilters: new(new[] { (FilterCombination.And, filter) })); + champSeasonFilters: new([(FilterCombination.And, filter)])); fixture.Register(() => config); var sut = CreateSut(); @@ -584,10 +584,10 @@ public async Task Calculate_ShouldApplyPointFilters() RowFilter filter = new ColumnValueRowFilter( nameof(ResultRowCalculationResult.FinishPosition), ComparatorType.IsSmallerOrEqual, - new[] { "3" }, + ["3"], MatchedValueAction.Keep); config.PointRule = CalculationMockHelper.MockPointRule( - pointFilters: new(new[] { (FilterCombination.And, filter) }), + pointFilters: new([(FilterCombination.And, filter)]), getRacePoints: (x, p) => 1); fixture.Register(() => config); var sut = CreateSut(); @@ -611,10 +611,10 @@ public async Task Calculate_ShouldSetPointsZero_WhenFiltered() RowFilter filter = new ColumnValueRowFilter( nameof(ResultRowCalculationResult.FinishPosition), ComparatorType.IsSmallerOrEqual, - new[] { "3" }, + ["3"], MatchedValueAction.Keep); config.PointRule = CalculationMockHelper.MockPointRule( - pointFilters: new(new[] { (FilterCombination.And, filter) }), + pointFilters: new([(FilterCombination.And, filter)]), getRacePoints: (x, p) => 1); fixture.Register(() => config); var sut = CreateSut(); @@ -638,10 +638,10 @@ public async Task Calculate_ShouldSetPointsEligible() RowFilter filter = new ColumnValueRowFilter( nameof(ResultRowCalculationResult.FinishPosition), ComparatorType.IsSmallerOrEqual, - new[] { "3" }, + ["3"], MatchedValueAction.Keep); config.PointRule = CalculationMockHelper.MockPointRule( - pointFilters: new(new[] { (FilterCombination.And, filter) }), + pointFilters: new([(FilterCombination.And, filter)]), getRacePoints: (x, p) => 1); fixture.Register(() => config); var sut = CreateSut(); @@ -664,7 +664,7 @@ public async Task Calculate_ShouldApplyDsqPenalty() .With(x => x.Type, PenaltyType.Disqualification) .Create(); var penaltyRow = data.ResultRows.ElementAt(0); - penaltyRow.AddPenalties = new[] { addPenalty }; + penaltyRow.AddPenalties = [addPenalty]; var config = GetCalculationConfiguration(data.LeagueId, data.SessionId); fixture.Register(() => config); var sut = CreateSut(); diff --git a/test/iRLeagueApiCore.Services.Tests/ResultService/DataAccess/SessionCalculationConfigurationProviderTests.cs b/test/iRLeagueApiCore.Services.Tests/ResultService/DataAccess/SessionCalculationConfigurationProviderTests.cs index e37b44ff..774829ca 100644 --- a/test/iRLeagueApiCore.Services.Tests/ResultService/DataAccess/SessionCalculationConfigurationProviderTests.cs +++ b/test/iRLeagueApiCore.Services.Tests/ResultService/DataAccess/SessionCalculationConfigurationProviderTests.cs @@ -2,6 +2,7 @@ using iRLeagueApiCore.Common.Models; using iRLeagueApiCore.Mocking.DataAccess; using iRLeagueApiCore.Services.ResultService.Calculation; +using iRLeagueApiCore.Services.ResultService.Calculation.Filters; using iRLeagueApiCore.Services.ResultService.DataAccess; using iRLeagueApiCore.Services.ResultService.Extensions; using iRLeagueApiCore.Services.ResultService.Models; @@ -358,9 +359,9 @@ public async Task GetConfigurations_ShouldProvidePointRuleWithPointFilters_WhenR var testFilter = filterGroup!.GetFilters().First().rowFilter as ColumnValueRowFilter; testFilter.Should().NotBeNull(); testFilter!.ColumnProperty.Name.Should().Be(condition.ColumnPropertyName); - testFilter.Comparator.Should().Be(condition.Comparator); + testFilter.Comparator.ComparatorType.Should().Be(condition.Comparator); testFilter.FilterValues.Should().BeEquivalentTo(condition.FilterValues); - testFilter.Action.Should().Be(condition.Action); + testFilter.Comparator.Action.Should().Be(condition.Action); } } @@ -396,9 +397,9 @@ public async Task GetConfigurations_ShouldProvidePointRuleWithResultFilters_When var testFilter = filterGroup!.GetFilters().First().rowFilter as ColumnValueRowFilter; testFilter.Should().NotBeNull(); testFilter!.ColumnProperty.Name.Should().Be(condition.ColumnPropertyName); - testFilter.Comparator.Should().Be(condition.Comparator); + testFilter.Comparator.ComparatorType.Should().Be(condition.Comparator); testFilter.FilterValues.Should().BeEquivalentTo(condition.FilterValues); - testFilter.Action.Should().Be(condition.Action); + testFilter.Comparator.Action.Should().Be(condition.Action); } } @@ -432,9 +433,9 @@ public async Task GetConfigurations_ShouldProvideColumnValueRowFilter_WhenConfig var testFilter = filterGroup!.GetFilters().First().rowFilter as ColumnValueRowFilter; testFilter.Should().NotBeNull(); testFilter!.ColumnProperty.Name.Should().Be(condition.ColumnPropertyName); - testFilter.Comparator.Should().Be(condition.Comparator); + testFilter.Comparator.ComparatorType.Should().Be(condition.Comparator); testFilter.FilterValues.Should().BeEquivalentTo(condition.FilterValues); - testFilter.Action.Should().Be(condition.Action); + testFilter.Comparator.Action.Should().Be(condition.Action); } } @@ -525,8 +526,8 @@ public async Task GetConfigurations_ShouldProvideDsqFilter_WhenNoStatusFilterCon .OfType() .SingleOrDefault(x => x.ColumnProperty.Name == nameof(ResultRowCalculationResult.Status)); testFilter.Should().NotBeNull(); - testFilter!.Comparator.Should().Be(ComparatorType.IsEqual); - testFilter.Action.Should().Be(MatchedValueAction.Remove); + testFilter!.Comparator.ComparatorType.Should().Be(ComparatorType.IsEqual); + testFilter.Comparator.Action.Should().Be(MatchedValueAction.Remove); testFilter.FilterValues.Should().BeEquivalentTo(new[] { (int)RaceStatus.Disqualified }); } } @@ -567,8 +568,8 @@ public async Task GetConfigurations_ShouldNotProvideDsqFilter_WhenStatusFilterCo .OfType() .SingleOrDefault(x => x.ColumnProperty.Name == nameof(ResultRowCalculationResult.Status)); testFilter.Should().NotBeNull(); - testFilter!.Comparator.Should().Be(ComparatorType.NotEqual); - testFilter.Action.Should().Be(MatchedValueAction.Keep); + testFilter!.Comparator.ComparatorType.Should().Be(ComparatorType.NotEqual); + testFilter.Comparator.Action.Should().Be(MatchedValueAction.Keep); testFilter.FilterValues.Should().BeEquivalentTo(new[] { (int)RaceStatus.Disconnected }); } }