diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..fd34831 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,11 @@ +# To get started with Dependabot version updates, you'll need to specify which +# package ecosystems to update and where the package manifests are located. +# Please see the documentation for all configuration options: +# https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file + +version: 2 +updates: + - package-ecosystem: "dotnet-sdk" # See documentation for possible values + directory: "/" # Location of package manifests + schedule: + interval: "weekly" diff --git a/.github/workflows/dotnet.yml b/.github/workflows/dotnet.yml index 03db47d..2abe74b 100644 --- a/.github/workflows/dotnet.yml +++ b/.github/workflows/dotnet.yml @@ -48,7 +48,7 @@ jobs: uses: actions/cache/save@v4 if: always() with: - key: session-${{ hashFiles('.adventofcode.session') }}-${{ hashFiles('**/inputs/**') }} + key: session-${{ hashFiles('.adventofcode.session') }}-test path: '**/inputs/**' diff --git a/2025/fsharp/Day06.fs b/2025/fsharp/Day06.fs new file mode 100644 index 0000000..79e5437 --- /dev/null +++ b/2025/fsharp/Day06.fs @@ -0,0 +1,69 @@ +namespace AdventOfCode.FSharp.Y2025 + +// Day 6: Trash Compactor +module Day06 = + open AdventOfCode.FSharp.Util + + let parseOps (opLine: byte[]) = + let mutable ops = [] + + for i = 0 to opLine.Length - 1 do + if opLine[i] <> ' 'B then + ops <- opLine[i] :: ops + + ops |> List.rev |> Array.ofList + + let parseInputPart1 (lines: byte array array) = + let operands = + lines |> Array.take (lines.Length - 1) |> Array.map parseInts |> Array.transpose + + let ops = lines |> Array.last |> parseOps + + Array.zip operands ops + + let inline arraySplit<'T when 'T: equality> (splitValue: 'T) (a: 'T[]) = + let mutable chunk = [] + let mutable result = [] + + for x in a do + if x = splitValue then + if chunk <> [] then + result <- (chunk |> Array.ofList) :: result + + chunk <- [] + else + chunk <- x :: chunk + + if chunk <> [] then + result <- (chunk |> Array.ofList) :: result + + result |> Array.ofList + + + let parseInputPart2 (lines: byte array array) = + let operands = + lines + |> Array.take (lines.Length - 1) + |> Array.transpose + |> Array.map (fun input -> parseIntToAny input 0 |> snd) + |> arraySplit 0 + + let ops = lines |> Array.last |> parseOps |> Array.rev + + Array.zip operands ops + + let doMathHomework homework = + homework + |> Array.map (fun (values, op) -> + match op with + | '+'B -> values |> Array.sum |> int64 + | '*'B -> values |> Array.map int64 |> Array.reduce (fun a b -> a * b) + | _ -> failwith "invalid operation") + |> Array.sum + + let run (input: byte array) (output: int -> string -> unit) = + let lines = input |> bsplit '\n'B + + lines |> parseInputPart1 |> doMathHomework |> string |> output 1 + + lines |> parseInputPart2 |> doMathHomework |> string |> output 2 diff --git a/2025/fsharp/Day07.fs b/2025/fsharp/Day07.fs new file mode 100644 index 0000000..2d1cab0 --- /dev/null +++ b/2025/fsharp/Day07.fs @@ -0,0 +1,36 @@ +namespace AdventOfCode.FSharp.Y2025 + +// Day 7: Laboratories +module Day07 = + open AdventOfCode.FSharp.Util + open Checked + + let analyzeManifold (lines: byte[][]) = + let mutable beams = Array.zeroCreate lines[0].Length + let mutable next = beams |> Array.copy + let mutable zero = beams |> Array.copy + + let mutable splitCount = 0 + + for line in lines do + for index = 0 to line.Length - 1 do + match line[index] with + | 'S'B -> next[index] <- 1L + | '^'B when beams[index] > 0 -> + splitCount <- splitCount + 1 + next[index - 1] <- next[index - 1] + beams[index] + next[index + 1] <- next[index + 1] + beams[index] + next[index] <- 0L + | _ -> next[index] <- next[index] + beams[index] + + Array.blit next 0 beams 0 beams.Length + Array.blit zero 0 next 0 next.Length + + splitCount, beams |> Array.sum + + let run (input: byte array) (output: int -> string -> unit) = + let manifold = input |> bsplit '\n'B + + let part1, part2 = manifold |> analyzeManifold + part1 |> string |> output 1 + part2 |> string |> output 2 diff --git a/Common/AdventOfCode.Cli.SourceGenerator/CalendarGenerator.cs b/Common/AdventOfCode.Cli.SourceGenerator/CalendarGenerator.cs index 8ee1810..b882f39 100644 --- a/Common/AdventOfCode.Cli.SourceGenerator/CalendarGenerator.cs +++ b/Common/AdventOfCode.Cli.SourceGenerator/CalendarGenerator.cs @@ -209,7 +209,12 @@ private static (string dayDefinition, string runMethod) GenerateDayDefinition(So throw new NotSupportedException(); } - var dayDefinition = $"new Solution(Year: {year}, Day: {day}, Name: \"{name}\", Run: {runMethodName}),"; + var dayDefinition = $$""" + new Solution + { + Year = {{year}}, Day = {{day}}, Name = "{{name}}", Run = {{runMethodName}}, + }, + """; return (dayDefinition, runMethod); } @@ -241,10 +246,10 @@ internal static class Calendar { public static readonly Solution[] Days = [ - {{dayDefinitions}} + {{dayDefinitions}} ]; - {{runMethods}} + {{runMethods}} } """; } diff --git a/Common/AdventOfCode.Cli/App.cs b/Common/AdventOfCode.Cli/App.cs index 178a867..7e34a38 100644 --- a/Common/AdventOfCode.Cli/App.cs +++ b/Common/AdventOfCode.Cli/App.cs @@ -1,6 +1,8 @@ using System.CommandLine; using System.Reflection; using System.Security.Cryptography.X509Certificates; +using System.Text.Json; +using System.Text.Json.Serialization; using BenchmarkDotNet.Running; @@ -35,13 +37,15 @@ internal static async Task RunCommand(IEnumerable solutions, Inpu var output = await NorthPole.RunAsync(solution, options); - var (result, actual) = output.PartOutputs[0]; + var result = output.PartOutputs[0].Result; + var actual = output.PartOutputs[0].ResultText; AnsiConsole.MarkupLineInterpolated($" {output.ElapsedMs,8:0.000} {1,4} {result,7} {actual}"); for (var part = 1; part < output.PartOutputs.Length; ++part) { - (result, actual) = output.PartOutputs[part]; + result = output.PartOutputs[part].Result; + actual = output.PartOutputs[part].ResultText; AnsiConsole.MarkupLineInterpolated($"{string.Empty,29} {part + 1,4} {result,7} {actual}"); } @@ -104,12 +108,24 @@ bool IsAllOk(SolutionOutputs output) return output.PartOutputs.All(p => p.Result == ResultType.Ok); } + var runTimestamp = DateTime.UtcNow; + + foreach (var day in selected.GroupBy(s => (s.Year, s.Day)).OrderBy(g => g.Key)) { var fastestMs = double.PositiveInfinity; var slowestMs = double.NegativeInfinity; var success = false; + var latestResult = new LatestSolutionFile(); + var (InformationVersion, BuildConfiguration) = NorthPole.GetBuildInformation(); + var fullResult = new SolutionFile + { + InformationVersion = InformationVersion, + BuildConfiguration = BuildConfiguration, + Timestamp = runTimestamp + }; + foreach (var solution in day) { var output = await NorthPole.RunAsync(solution, options); @@ -134,6 +150,17 @@ bool IsAllOk(SolutionOutputs output) fastestMs = Math.Min(fastestMs, output.ElapsedMs); slowestMs = Math.Max(slowestMs, output.ElapsedMs); } + + fullResult.Solutions.Add(output); + latestResult.Solutions.Add(new LatestSolutionOutputs + { + Year = output.Year, + Day = output.Day, + Name = output.Name, + PartOutputs = output.PartOutputs + .Select((value, index) => (value, index)) + .ToDictionary(k => k.index + 1, v => v.value) + }); } if (success) @@ -141,6 +168,16 @@ bool IsAllOk(SolutionOutputs output) fastestTotalMs += fastestMs; slowestTotalMs += slowestMs; } + + var latestResultJson = JsonSerializer.Serialize( + latestResult, SourceGenerationContext.Default.LatestSolutionFile); + var latestJsonPath = NorthPole.GetCachePath(FileType.LatestResultJson, day.Key.Year, day.Key.Day); + File.WriteAllText(latestJsonPath, latestResultJson); + + var timestampResultJson = JsonSerializer.Serialize( + fullResult, SourceGenerationContext.Default.SolutionFile); + var timestampJsonPath = NorthPole.GetCachePath(FileType.TimestampResultJson, day.Key.Year, day.Key.Day, null, runTimestamp); + File.WriteAllText(timestampJsonPath, timestampResultJson); } var byFastestTime = outputs diff --git a/Common/AdventOfCode.Cli/FileType.cs b/Common/AdventOfCode.Cli/FileType.cs index 32e5a99..a7d9cff 100644 --- a/Common/AdventOfCode.Cli/FileType.cs +++ b/Common/AdventOfCode.Cli/FileType.cs @@ -4,19 +4,16 @@ namespace AdventOfCode.Cli; public enum FileType { HtmlPage = 0, - Input = 1, - Output = 2, - Official = 4, - Example = 8, - Other = 16, - OfficialInput = Official | Input, - ExampleInput = Example | Input, - OtherInput = Other | Input, + OfficialInput = 1, + ExampleInput = 3, + OtherInput = 5, - OfficialOutput = Official | Output, - ExampleOutput = Example | Output, - OtherOutput = Other | Output + OfficialOutput = 2, + ExampleOutput = 4, + OtherOutput = 6, + LatestResultJson = 100, + TimestampResultJson = 101, } public enum InputType diff --git a/Common/AdventOfCode.Cli/Model.cs b/Common/AdventOfCode.Cli/Model.cs index bfc308f..d094df7 100644 --- a/Common/AdventOfCode.Cli/Model.cs +++ b/Common/AdventOfCode.Cli/Model.cs @@ -1,3 +1,6 @@ +using System.ComponentModel.DataAnnotations; +using System.Text.Json.Serialization; + namespace AdventOfCode.Cli; public class InvalidSessionFileException : Exception @@ -9,11 +12,52 @@ public InvalidSessionFileException(string message) : base(message) { } public delegate void SolveThunk(byte[] input, Action output); -public record Solution(int Year, int Day, string Name, SolveThunk Run) +public record Solution { + public required int Year { get; init; } + public required int Day { get; init; } + public required string Name { get; init; } + public required SolveThunk Run { get; init; } public override string ToString() => $"{Year}:{Day}"; } -public record PartOutput(ResultType Result, string? ResultText); +public record PartOutput +{ + public required ResultType Result { get; init; } + public required string? ResultText { get; init; } +} + +public record SolutionOutputs +{ + public required int Year { get; init; } + public required int Day { get; init; } + public required string Name { get; init; } + public required PartOutput[] PartOutputs { get; init; } + public required double ElapsedMs { get; init; } +} + +public record LatestSolutionOutputs +{ + public required int Year { get; init; } + public required int Day { get; init; } + public required string Name { get; init; } + public required Dictionary PartOutputs { get; init; } +} + +public record LatestSolutionFile +{ + public List Solutions { get; init; } = []; +} + +public record SolutionFile +{ + public string? InformationVersion { get; init; } + public string? BuildConfiguration { get; init; } + public DateTime Timestamp { get; init; } = DateTime.UtcNow; + public List Solutions { get; init; } = []; +} -public record SolutionOutputs(int Year, int Day, string Name, PartOutput[] PartOutputs, double ElapsedMs); +[JsonSourceGenerationOptions(WriteIndented = true, Converters = [typeof(JsonStringEnumConverter)])] +[JsonSerializable(typeof(SolutionFile))] +[JsonSerializable(typeof(LatestSolutionFile))] +internal partial class SourceGenerationContext : JsonSerializerContext { } diff --git a/Common/AdventOfCode.Cli/NorthPole.cs b/Common/AdventOfCode.Cli/NorthPole.cs index fd1fd9b..6a85e38 100644 --- a/Common/AdventOfCode.Cli/NorthPole.cs +++ b/Common/AdventOfCode.Cli/NorthPole.cs @@ -1,4 +1,5 @@ using System.Diagnostics; +using System.Reflection; using System.Text; using System.Text.RegularExpressions; @@ -32,9 +33,16 @@ public IEnumerable AllPossibleDates() while (current < now) { - yield return new Solution(current.Year, current.Day, "*", (input, output) => { }); + yield return new Solution + { + Year = current.Year, + Day = current.Day, + Name = "*", + Run = (input, output) => { } + }; current = current.AddDays(1); + if (current > new DateTime(current.Year, current.Month, 25)) { current = new DateTime(current.Year + 1, 12, 1); @@ -126,12 +134,12 @@ public string GetFolder(int year, int day) return sessionDir != null ? Path.Combine(sessionDir, defaultPath) : defaultPath; } - public string GetCachePath(FileType inputType, int year, int day, int? part = null) + public string GetCachePath(FileType inputType, int year, int day, int? part = null, DateTime? timestamp = null) { string inputFolder = GetFolder(year, day); var paths = from pattern in _options.FileNamePatterns[inputType] - let filename = string.Format(pattern, year, day, part) + let filename = string.Format(pattern, year, day, part, timestamp) select Path.Combine(inputFolder, filename); var path = paths.FirstOrDefault(p => File.Exists(p)); @@ -263,7 +271,7 @@ void HandleOutput(int part, string result) var results = new PartOutput[maxActualOutput + 1]; for (var part = 0; part < results.Length; ++part) { - var expectedType = (OutputType)(((FileType)options.InputType & ~FileType.Input) | FileType.Output); + var expectedType = (OutputType)((int)options.InputType + 1); var expected = await GetExpected(expectedType, solution.Year, solution.Day, part + 1); var actual = actualOutputs[part]; @@ -274,10 +282,21 @@ void HandleOutput(int part, string result) actual != expected ? ResultType.Error : ResultType.Ok; - results[part] = new PartOutput(result, actual); + results[part] = new PartOutput + { + Result = result, + ResultText = actual, + }; } - return new SolutionOutputs(solution.Year, solution.Day, solution.Name, results, w.Elapsed.TotalMilliseconds / options.Repeats); + return new SolutionOutputs + { + Year = solution.Year, + Day = solution.Day, + Name = solution.Name, + PartOutputs = results, + ElapsedMs = w.Elapsed.TotalMilliseconds / options.Repeats + }; } /// @@ -308,4 +327,19 @@ internal async Task ResetAsync(int year, int day) await GetExpected(OutputType.Official, year, day, 1); await GetExpected(OutputType.Official, year, day, 2); } + + public (string InformationVersion, string BuildConfiguration) GetBuildInformation() + { + var infoVersion = Assembly.GetExecutingAssembly() + .GetCustomAttributes(typeof(System.Reflection.AssemblyInformationalVersionAttribute), false) + .OfType() + .FirstOrDefault()?.InformationalVersion ?? "unknown"; + + var buildConfiguration = Assembly.GetExecutingAssembly() + .GetCustomAttributes(typeof(AssemblyConfigurationAttribute), false) + .OfType() + .FirstOrDefault()?.Configuration ?? "unknown"; + + return (infoVersion, buildConfiguration); + } } diff --git a/Common/AdventOfCode.Cli/NorthPoleOptions.cs b/Common/AdventOfCode.Cli/NorthPoleOptions.cs index 200558b..72f1dc3 100644 --- a/Common/AdventOfCode.Cli/NorthPoleOptions.cs +++ b/Common/AdventOfCode.Cli/NorthPoleOptions.cs @@ -32,6 +32,8 @@ public static string GetDefaultApiUserAgent() [FileType.OfficialOutput] = ["input.s{2}.txt"], [FileType.ExampleOutput] = ["example.s{2}.txt"], [FileType.OtherOutput] = ["other.s{2}.txt"], + [FileType.LatestResultJson] = ["latest.json"], + [FileType.TimestampResultJson] = ["result-{3:yyyyMMddTHHmmss}.json"], }; public string AdventOfCodeUrl { get; set; } = "https://adventofcode.com"; diff --git a/Directory.Packages.props b/Directory.Packages.props index 3b89bf5..f284138 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -4,19 +4,19 @@ false - + - + - - - - + + + + - + \ No newline at end of file diff --git a/global.json b/global.json new file mode 100644 index 0000000..936a420 --- /dev/null +++ b/global.json @@ -0,0 +1,5 @@ +{ + "sdk": { + "version": "10.0.101" + } +}