diff --git a/src/AoCHelper/AoCHelper.csproj b/src/AoCHelper/AoCHelper.csproj index 8f22152..77f54f9 100644 --- a/src/AoCHelper/AoCHelper.csproj +++ b/src/AoCHelper/AoCHelper.csproj @@ -26,6 +26,7 @@ + diff --git a/src/AoCHelper/BaseProblem.cs b/src/AoCHelper/BaseProblem.cs index b099bcc..bd68c33 100644 --- a/src/AoCHelper/BaseProblem.cs +++ b/src/AoCHelper/BaseProblem.cs @@ -28,7 +28,7 @@ public virtual uint CalculateIndex() { var typeName = GetType().Name; - return uint.TryParse(typeName.Substring(typeName.IndexOf(ClassPrefix) + ClassPrefix.Length).TrimStart('_'), out var index) + return uint.TryParse(typeName[(typeName.IndexOf(ClassPrefix) + ClassPrefix.Length)..].TrimStart('_'), out var index) ? index : default; } diff --git a/src/AoCHelper/IsExternalInit.cs b/src/AoCHelper/IsExternalInit.cs index 25732f0..47e2340 100644 --- a/src/AoCHelper/IsExternalInit.cs +++ b/src/AoCHelper/IsExternalInit.cs @@ -16,9 +16,7 @@ namespace System.Runtime.CompilerServices /// This class should not be used by developers in source code. /// [EditorBrowsable(EditorBrowsableState.Never)] - internal static class IsExternalInit - { - } + internal static class IsExternalInit; } #endif \ No newline at end of file diff --git a/src/AoCHelper/Range.cs b/src/AoCHelper/Range.cs new file mode 100644 index 0000000..3db71d4 --- /dev/null +++ b/src/AoCHelper/Range.cs @@ -0,0 +1,280 @@ +// Based on https://www.meziantou.net/how-to-use-csharp-8-indices-and-ranges-in-dotnet-standard-2-0-and-dotn.htm + +// https://github.com/dotnet/runtime/blob/419e949d258ecee4c40a460fb09c66d974229623/src/libraries/System.Private.CoreLib/src/System/Index.cs +// https://github.com/dotnet/runtime/blob/419e949d258ecee4c40a460fb09c66d974229623/src/libraries/System.Private.CoreLib/src/System/Range.cs + +using System.Runtime.CompilerServices; + +namespace System +{ + /// Represent a type can be used to index a collection either from the start or the end. + /// + /// Index is used by the C# compiler to support the new index syntax + /// + /// int[] someArray = new int[5] { 1, 2, 3, 4, 5 } ; + /// int lastElement = someArray[^1]; // lastElement = 5 + /// + /// + internal readonly struct Index : IEquatable + { + private readonly int _value; + + /// Construct an Index using a value and indicating if the index is from the start or from the end. + /// The index value. it has to be zero or positive number. + /// Indicating if the index is from the start or from the end. + /// + /// If the Index constructed from the end, index value 1 means pointing at the last element and index value 0 means pointing at beyond last element. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] +#pragma warning disable S3427 // Method overloads with default parameter values should not overlap + public Index(int value, bool fromEnd = false) +#pragma warning restore S3427 // Method overloads with default parameter values should not overlap + { + if (value < 0) + { + throw new ArgumentOutOfRangeException(nameof(value), "value must be non-negative"); + } + + if (fromEnd) + _value = ~value; + else + _value = value; + } + + // The following private constructors mainly created for perf reason to avoid the checks + private Index(int value) + { + _value = value; + } + + /// Create an Index pointing at first element. + public static Index Start => new(0); + + /// Create an Index pointing at beyond last element. + public static Index End => new(~0); + + /// Create an Index from the start at the position indicated by the value. + /// The index value from the start. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Index FromStart(int value) + { + if (value < 0) + { + throw new ArgumentOutOfRangeException(nameof(value), "value must be non-negative"); + } + + return new Index(value); + } + + /// Create an Index from the end at the position indicated by the value. + /// The index value from the end. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Index FromEnd(int value) + { + if (value < 0) + { + throw new ArgumentOutOfRangeException(nameof(value), "value must be non-negative"); + } + + return new Index(~value); + } + + /// Returns the index value. + public int Value + { + get + { + if (_value < 0) + { + return ~_value; + } + else + { + return _value; + } + } + } + + /// Indicates whether the index is from the start or the end. + public bool IsFromEnd => _value < 0; + + /// Calculate the offset from the start using the giving collection length. + /// The length of the collection that the Index will be used with. length has to be a positive value + /// + /// For performance reason, we don't validate the input length parameter and the returned offset value against negative values. + /// we don't validate either the returned offset is greater than the input length. + /// It is expected Index will be used with collections which always have non negative length/count. If the returned offset is negative and + /// then used to index a collection will get out of range exception which will be same affect as the validation. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public int GetOffset(int length) + { + var offset = _value; + if (IsFromEnd) + { + // offset = length - (~value) + // offset = length + (~(~value) + 1) + // offset = length + value + 1 + + offset += length + 1; + } + return offset; + } + + /// Indicates whether the current Index object is equal to another object of the same type. + /// An object to compare with this object + public override bool Equals(object? obj) => obj is Index index && _value == (index)._value; + + /// Indicates whether the current Index object is equal to another Index object. + /// An object to compare with this object + public bool Equals(Index other) => _value == other._value; + + /// Returns the hash code for this instance. + public override int GetHashCode() => _value; + + /// Converts integer number to an Index. + public static implicit operator Index(int value) => FromStart(value); + + /// Converts the value of the current Index object to its equivalent string representation. + public override string ToString() + { + if (IsFromEnd) + return "^" + ((uint)Value).ToString(); + + return ((uint)Value).ToString(); + } + } + + /// Represent a range has start and end indexes. + /// + /// Range is used by the C# compiler to support the range syntax. + /// + /// int[] someArray = new int[5] { 1, 2, 3, 4, 5 }; + /// int[] subArray1 = someArray[0..2]; // { 1, 2 } + /// int[] subArray2 = someArray[1..^0]; // { 2, 3, 4, 5 } + /// + /// + internal readonly struct Range : IEquatable + { + /// Represent the inclusive start index of the Range. + public Index Start { get; } + + /// Represent the exclusive end index of the Range. + public Index End { get; } + + /// Construct a Range object using the start and end indexes. + /// Represent the inclusive start index of the range. + /// Represent the exclusive end index of the range. + public Range(Index start, Index end) + { + Start = start; + End = end; + } + + /// Indicates whether the current Range object is equal to another object of the same type. + /// An object to compare with this object + public override bool Equals(object? obj) => + obj is Range r && + r.Start.Equals(Start) && + r.End.Equals(End); + + /// Indicates whether the current Range object is equal to another Range object. + /// An object to compare with this object + public bool Equals(Range other) => other.Start.Equals(Start) && other.End.Equals(End); + + /// Returns the hash code for this instance. + public override int GetHashCode() + { + return (Start.GetHashCode() * 31) + End.GetHashCode(); + } + + /// Converts the value of the current Range object to its equivalent string representation. + public override string ToString() + { + return Start + ".." + End; + } + + /// Create a Range object starting from start index to the end of the collection. + public static Range StartAt(Index start) => new Range(start, Index.End); + + /// Create a Range object starting from first element in the collection to the end Index. + public static Range EndAt(Index end) => new Range(Index.Start, end); + + /// Create a Range object starting from first element to the end. + public static Range All => new Range(Index.Start, Index.End); + + /// Calculate the start offset and length of range object using a collection length. + /// The length of the collection that the range will be used with. length has to be a positive value. + /// + /// For performance reason, we don't validate the input length parameter against negative values. + /// It is expected Range will be used with collections which always have non negative length/count. + /// We validate the range is inside the length scope though. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public (int Offset, int Length) GetOffsetAndLength(int length) + { + int start; + var startIndex = Start; + if (startIndex.IsFromEnd) + start = length - startIndex.Value; + else + start = startIndex.Value; + + int end; + var endIndex = End; + if (endIndex.IsFromEnd) + end = length - endIndex.Value; + else + end = endIndex.Value; + + if ((uint)end > (uint)length || (uint)start > (uint)end) + { + throw new ArgumentOutOfRangeException(nameof(length)); + } + + return (start, end - start); + } + } +} + +namespace System.Runtime.CompilerServices +{ + internal static class RuntimeHelpers + { + /// + /// Slices the specified array using the specified range. + /// + public static T[] GetSubArray(T[] array, Range range) + { + if (array == null) + { + throw new ArgumentNullException(nameof(array)); + } + + (int offset, int length) = range.GetOffsetAndLength(array.Length); + +#pragma warning disable S2955 // Generic parameters not constrained to reference types should not be compared to "null" + if (default(T) != null || typeof(T[]) == array.GetType()) + { + // We know the type of the array to be exactly T[]. + + if (length == 0) + { + return Array.Empty(); + } + + var dest = new T[length]; + Array.Copy(array, offset, dest, 0, length); + return dest; + } + else + { + // The array is actually a U[] where U:T. + var dest = (T[])Array.CreateInstance(array.GetType().GetElementType(), length); + Array.Copy(array, offset, dest, 0, length); + return dest; + } +#pragma warning restore S2955 // Generic parameters not constrained to reference types should not be compared to "null" + } + } +} diff --git a/src/AoCHelper/Solver.cs b/src/AoCHelper/Solver.cs index f5b70a6..c1333b7 100644 --- a/src/AoCHelper/Solver.cs +++ b/src/AoCHelper/Solver.cs @@ -1,6 +1,7 @@ using System.Diagnostics; using System.Reflection; using Spectre.Console; +using System.Linq; namespace AoCHelper; @@ -8,7 +9,7 @@ public static class Solver { private static readonly bool IsInteractiveEnvironment = Environment.UserInteractive && !Console.IsOutputRedirected; - private record ElapsedTime(double Constructor, double Part1, double Part2); + private sealed record ElapsedTime(double Constructor, double Part1, double Part2); /// /// Solves last problem. @@ -137,23 +138,19 @@ await AnsiConsole.Live(table) .StartAsync(async ctx => { var sw = new Stopwatch(); - foreach (Type problemType in LoadAllProblems(configuration.ProblemAssemblies)) + foreach (var problemType in LoadAllProblems(configuration.ProblemAssemblies).Where(problemType => problems.Contains(problemType))) { - if (problems.Contains(problemType)) + sw.Restart(); + var potentialProblem = InstantiateProblem(problemType); + sw.Stop(); + if (potentialProblem is BaseProblem problem) { - sw.Restart(); - var potentialProblem = InstantiateProblem(problemType); - sw.Stop(); - - if (potentialProblem is BaseProblem problem) - { - totalElapsedTime.Add(await SolveProblem(problem, table, CalculateElapsedMilliseconds(sw), configuration)); - ctx.Refresh(); - } - else - { - totalElapsedTime.Add(RenderEmptyProblem(problemType, potentialProblem as string, table, CalculateElapsedMilliseconds(sw), configuration)); - } + totalElapsedTime.Add(await SolveProblem(problem, table, CalculateElapsedMilliseconds(sw), configuration)); + ctx.Refresh(); + } + else + { + totalElapsedTime.Add(RenderEmptyProblem(problemType, potentialProblem as string, table, CalculateElapsedMilliseconds(sw), configuration)); } } }); @@ -442,9 +439,9 @@ private static void RenderOverallResultsPanel(List totalElapsedTime return; } - var totalConstructors = totalElapsedTime.Select(t => t.Constructor).Sum(); - var totalPart1 = totalElapsedTime.Select(t => t.Part1).Sum(); - var totalPart2 = totalElapsedTime.Select(t => t.Part2).Sum(); + var totalConstructors = totalElapsedTime.Sum(t => t.Constructor); + var totalPart1 = totalElapsedTime.Sum(t => t.Part1); + var totalPart2 = totalElapsedTime.Sum(t => t.Part2); var total = totalPart1 + totalPart2 + (configuration.ShowConstructorElapsedTime ? totalConstructors : 0); var grid = new Grid() @@ -466,12 +463,12 @@ private static void RenderOverallResultsPanel(List totalElapsedTime if (configuration.ShowConstructorElapsedTime) { - grid.AddRow("Mean constructors", FormatTime(totalElapsedTime.Select(t => t.Constructor).Average(), configuration)); + grid.AddRow("Mean constructors", FormatTime(totalElapsedTime.Average(t => t.Constructor), configuration)); } grid - .AddRow("Mean parts 1", FormatTime(totalElapsedTime.Select(t => t.Part1).Average(), configuration)) - .AddRow("Mean parts 2", FormatTime(totalElapsedTime.Select(t => t.Part2).Average(), configuration)); + .AddRow("Mean parts 1", FormatTime(totalElapsedTime.Average(t => t.Part1), configuration)) + .AddRow("Mean parts 2", FormatTime(totalElapsedTime.Average(t => t.Part2), configuration)); AnsiConsole.Write( new Panel(grid) diff --git a/tests/AoCHelper.Test/SolverTest.cs b/tests/AoCHelper.Test/SolverTest.cs index 6bbde53..96c6c17 100644 --- a/tests/AoCHelper.Test/SolverTest.cs +++ b/tests/AoCHelper.Test/SolverTest.cs @@ -67,8 +67,8 @@ public async Task SolveIntParams() [Fact] public async Task SolveIntEnumerable() { - await Solver.Solve(new List { 1, 2 }); - await Solver.Solve(new List { 1, 2 }, _ => { }); + await Solver.Solve([1, 2]); + await Solver.Solve([1, 2], _ => { }); } [Fact] @@ -81,8 +81,8 @@ public async Task SolveTypeParams() [Fact] public async Task SolveTypeEnumerable() { - await Solver.Solve(new List { typeof(Problem66) }); - await Solver.Solve(new List { typeof(Problem66) }, _ => { }); + await Solver.Solve([typeof(Problem66)]); + await Solver.Solve([typeof(Problem66)], _ => { }); } [Fact]