diff --git a/.woodpecker/.build.yml b/.woodpecker/.build.yml index 80ee333..473a9ab 100644 --- a/.woodpecker/.build.yml +++ b/.woodpecker/.build.yml @@ -11,8 +11,9 @@ steps: - nix develop --command alejandra --check . - nix develop --command dotnet tool restore - nix develop --command dotnet fantomas --check . - # - nix develop --command dotnet publish AdventOfCode2023.FSharp/AdventOfCode2023.FSharp/AdventOfCode2023.FSharp.fsproj --configuration Release - # - nix develop --command sh -c "$(find . -type f -name AdventOfCode2023.FSharp | grep Release | grep publish) AdventOfCode2023.FSharp/Test/samples" + # TODO: if https://github.com/dotnet/sdk/issues/37295 ever gets fixed, remove the PublishAot=false + - "nix develop --command dotnet publish AdventOfCode2023.FSharp/AdventOfCode2023.FSharp/AdventOfCode2023.FSharp.fsproj --configuration Release -p:PublishAot=false -p:SelfContained=true" + - '$(find . -type f -name AdventOfCode2023.FSharp | grep Release | grep publish) "$(pwd)/AdventOfCode2023.FSharp/Test/samples"' when: - event: "push" diff --git a/AdventOfCode2023.FSharp/AdventOfCode2023.FSharp.Lib/AdventOfCode2023.FSharp.Lib.fsproj b/AdventOfCode2023.FSharp/AdventOfCode2023.FSharp.Lib/AdventOfCode2023.FSharp.Lib.fsproj index a760c72..0c23d7a 100644 --- a/AdventOfCode2023.FSharp/AdventOfCode2023.FSharp.Lib/AdventOfCode2023.FSharp.Lib.fsproj +++ b/AdventOfCode2023.FSharp/AdventOfCode2023.FSharp.Lib/AdventOfCode2023.FSharp.Lib.fsproj @@ -14,6 +14,7 @@ + diff --git a/AdventOfCode2023.FSharp/AdventOfCode2023.FSharp.Lib/Day1.fs b/AdventOfCode2023.FSharp/AdventOfCode2023.FSharp.Lib/Day1.fs index 3f43e4e..654a236 100644 --- a/AdventOfCode2023.FSharp/AdventOfCode2023.FSharp.Lib/Day1.fs +++ b/AdventOfCode2023.FSharp/AdventOfCode2023.FSharp.Lib/Day1.fs @@ -5,21 +5,13 @@ open System [] module Day1 = - let firstDigit (s : ReadOnlySpan) = - let mutable pos = 0 - - while '0' > s.[pos] || s.[pos] > '9' do - pos <- pos + 1 - + let inline firstDigit (s : ReadOnlySpan) = + let pos = s.IndexOfAnyInRange ('0', '9') byte s.[pos] - byte '0' // No surrogate pairs please! - let lastDigit (s : ReadOnlySpan) = - let mutable pos = s.Length - 1 - - while '0' > s.[pos] || s.[pos] > '9' do - pos <- pos - 1 - + let inline lastDigit (s : ReadOnlySpan) = + let pos = s.LastIndexOfAnyInRange ('0', '9') byte s.[pos] - byte '0' let part1 (s : string) = @@ -35,35 +27,66 @@ module Day1 = total - let table = - [| - "one", 1uy - "two", 2uy - "three", 3uy - "four", 4uy - "five", 5uy - "six", 6uy - "seven", 7uy - "eight", 8uy - "nine", 9uy - |] + let isDigitSpelled (s : ReadOnlySpan) (pos : int) (answer : byref) = + // Can't be bothered to write a jump-table compiler + if s.[pos] >= '0' && s.[pos] <= '9' then + answer <- byte s.[pos] - byte '0' + else if s.[pos] = 'o' then + if pos + 2 < s.Length && s.[pos + 1] = 'n' && s.[pos + 2] = 'e' then + answer <- 1uy + elif s.[pos] = 't' then + if pos + 2 < s.Length && s.[pos + 1] = 'w' && s.[pos + 2] = 'o' then + answer <- 2uy + elif + pos + 4 < s.Length + && s.[pos + 1] = 'h' + && s.[pos + 2] = 'r' + && s.[pos + 3] = 'e' + && s.[pos + 4] = 'e' + then + answer <- 3uy + elif s.[pos] = 'f' then + if pos + 3 < s.Length then + if s.[pos + 1] = 'o' && s.[pos + 2] = 'u' && s.[pos + 3] = 'r' then + answer <- 4uy + elif s.[pos + 1] = 'i' && s.[pos + 2] = 'v' && s.[pos + 3] = 'e' then + answer <- 5uy + elif s.[pos] = 's' then + if pos + 2 < s.Length && s.[pos + 1] = 'i' && s.[pos + 2] = 'x' then + answer <- 6uy + elif + pos + 4 < s.Length + && s.[pos + 1] = 'e' + && s.[pos + 2] = 'v' + && s.[pos + 3] = 'e' + && s.[pos + 4] = 'n' + then + answer <- 7uy + elif s.[pos] = 'e' then + if + pos + 4 < s.Length + && s.[pos + 1] = 'i' + && s.[pos + 2] = 'g' + && s.[pos + 3] = 'h' + && s.[pos + 4] = 't' + then + answer <- 8uy + elif s.[pos] = 'n' then + if + pos + 3 < s.Length + && s.[pos + 1] = 'i' + && s.[pos + 2] = 'n' + && s.[pos + 3] = 'e' + then + answer <- 9uy let firstDigitIncSpelled (s : ReadOnlySpan) = let mutable pos = 0 let mutable answer = 255uy while answer = 255uy do - if s.[pos] >= '0' && s.[pos] <= '9' then - answer <- byte s.[pos] - byte '0' - else - for i, value in table do - if - pos + i.Length < s.Length - && MemoryExtensions.SequenceEqual (s.Slice (pos, i.Length), i) - then - answer <- value - - pos <- pos + 1 + isDigitSpelled s pos &answer + pos <- pos + 1 answer @@ -72,17 +95,8 @@ module Day1 = let mutable answer = 255uy while answer = 255uy do - if s.[pos] >= '0' && s.[pos] <= '9' then - answer <- byte s.[pos] - byte '0' - else - for i, value in table do - if - pos - i.Length + 1 >= 0 - && MemoryExtensions.SequenceEqual (s.Slice (pos - i.Length + 1, i.Length), i) - then - answer <- value - - pos <- pos - 1 + isDigitSpelled s pos &answer + pos <- pos - 1 answer diff --git a/AdventOfCode2023.FSharp/AdventOfCode2023.FSharp.Lib/Day3.fs b/AdventOfCode2023.FSharp/AdventOfCode2023.FSharp.Lib/Day3.fs index 9b8cda2..7fef57f 100644 --- a/AdventOfCode2023.FSharp/AdventOfCode2023.FSharp.Lib/Day3.fs +++ b/AdventOfCode2023.FSharp/AdventOfCode2023.FSharp.Lib/Day3.fs @@ -146,7 +146,7 @@ module Day3 = let mutable answer = 0 - for KeyValue (_gearPos, gears) in gears do + for gears in gears.Values do if gears.Count = 2 then answer <- answer + gears.[0] * gears.[1] diff --git a/AdventOfCode2023.FSharp/AdventOfCode2023.FSharp.Lib/Day4.fs b/AdventOfCode2023.FSharp/AdventOfCode2023.FSharp.Lib/Day4.fs index 69ea3d3..5930f29 100644 --- a/AdventOfCode2023.FSharp/AdventOfCode2023.FSharp.Lib/Day4.fs +++ b/AdventOfCode2023.FSharp/AdventOfCode2023.FSharp.Lib/Day4.fs @@ -54,8 +54,6 @@ module Day4 = total - - let part2 (s : string) = use lines = StringSplitEnumerator.make '\n' s let winningNumbers = ResizeArray () diff --git a/AdventOfCode2023.FSharp/AdventOfCode2023.FSharp.Lib/Day7.fs b/AdventOfCode2023.FSharp/AdventOfCode2023.FSharp.Lib/Day7.fs new file mode 100644 index 0000000..ef8a5bd --- /dev/null +++ b/AdventOfCode2023.FSharp/AdventOfCode2023.FSharp.Lib/Day7.fs @@ -0,0 +1,204 @@ +namespace AdventOfCode2023 + +open System +open System.Collections.Generic + +type Hand = + | Five = 10 + | Four = 9 + | FullHouse = 8 + | Three = 7 + | TwoPairs = 6 + | Pair = 5 + | High = 4 + +type HandContents = + { + First : byte + Second : byte + Third : byte + Fourth : byte + Fifth : byte + } + +[] +module Day7 = + + let inline toByte (adjustJoker : bool) (c : char) : byte = + if c <= '9' then byte c - byte '0' + elif c = 'T' then 10uy + elif c = 'J' then (if adjustJoker then 1uy else 11uy) + elif c = 'Q' then 12uy + elif c = 'K' then 13uy + elif c = 'A' then 14uy + else failwithf "could not parse: %c" c + + let inline private updateState (tallies : ResizeArray<_>) newNum = + let mutable isAdded = false + + for i = 0 to tallies.Count - 1 do + if fst tallies.[i] = newNum then + tallies.[i] <- (fst tallies.[i], snd tallies.[i] + 1) + isAdded <- true + + if not isAdded then + tallies.Add (newNum, 1) + + let inline parseHand + (tallyBuffer : ResizeArray<_>) + (adjustJoker : bool) + (s : ReadOnlySpan) + : Hand * HandContents + = + let contents = + { + First = toByte adjustJoker s.[0] + Second = toByte adjustJoker s.[1] + Third = toByte adjustJoker s.[2] + Fourth = toByte adjustJoker s.[3] + Fifth = toByte adjustJoker s.[4] + } + + tallyBuffer.Clear () + tallyBuffer.Add (contents.First, 1) + updateState tallyBuffer contents.Second + updateState tallyBuffer contents.Third + updateState tallyBuffer contents.Fourth + updateState tallyBuffer contents.Fifth + + let jokerCount, jokerPos = + if not adjustJoker then + 0, -1 + else + let mutable count = 0 + let mutable jokerPos = -1 + + for i = 0 to tallyBuffer.Count - 1 do + let card, tally = tallyBuffer.[i] + + if card = 1uy then + count <- tally + jokerPos <- i + + count, jokerPos + + let hand = + if jokerCount > 0 then + match tallyBuffer.Count with + | 1 -> + // Five jokers + Hand.Five + | 2 -> + // Jokers plus one other card type + Hand.Five + | 3 -> + // Jokers plus two other card types. Either full house, or four of a kind + if jokerCount >= 2 then + // JJABB or JJJAB + Hand.Four + else if + // JAABB or JAAAB + jokerPos <> 0 + then + if snd tallyBuffer.[0] = 2 then + Hand.FullHouse + else + Hand.Four + else if snd tallyBuffer.[1] = 2 then + Hand.FullHouse + else + Hand.Four + | 4 -> + // Jokers plus three other card types, exactly one of which therefore is a two-of. + Hand.Three + | 5 -> + // Five different cards, one of which is a joker. + Hand.Pair + | _ -> failwith "bad tallyBuffer" + elif tallyBuffer.Count = 1 then + Hand.Five + elif tallyBuffer.Count = 2 then + // AAAAB or AAABB + if snd tallyBuffer.[0] = 3 || snd tallyBuffer.[0] = 2 then + Hand.FullHouse + else + Hand.Four + elif tallyBuffer.Count = 3 then + // AAABC or AABBC + if snd tallyBuffer.[0] = 3 then Hand.Three + elif snd tallyBuffer.[0] = 2 then Hand.TwoPairs + elif snd tallyBuffer.[1] = 3 then Hand.Three + elif snd tallyBuffer.[1] = 2 then Hand.TwoPairs + else Hand.Three + elif tallyBuffer.Count = 4 then + Hand.Pair + else + Hand.High + + hand, contents + + let parse (adjustJoker : bool) (s : string) : ResizeArray = + use mutable lines = StringSplitEnumerator.make '\n' s + let result = ResizeArray () + let tallies = ResizeArray 5 + + while lines.MoveNext () do + if not lines.Current.IsEmpty then + use mutable line = StringSplitEnumerator.make' ' ' lines.Current + line.MoveNext () |> ignore + let hand, contents = parseHand tallies adjustJoker line.Current + line.MoveNext () |> ignore + let bid = Int32.Parse line.Current + + result.Add (hand, contents, bid) + + result + + let compArrBasic (a : HandContents) (b : HandContents) = + if a.First > b.First then 1 + elif a.First < b.First then -1 + elif a.Second > b.Second then 1 + elif a.Second < b.Second then -1 + elif a.Third > b.Third then 1 + elif a.Third < b.Third then -1 + elif a.Fourth > b.Fourth then 1 + elif a.Fourth < b.Fourth then -1 + elif a.Fifth > b.Fifth then 1 + elif a.Fifth < b.Fifth then -1 + else 0 + + let compBasic : IComparer = + { new IComparer<_> with + member _.Compare ((aHand, aContents, _), (bHand, bContents, _)) = + match compare aHand bHand with + | 0 -> compArrBasic aContents bContents + | x -> x + } + + let part1 (s : string) : int = + let parsed = parse false s + + parsed.Sort compBasic + + let mutable answer = 0 + let mutable pos = 1 + + for _, _, bid in parsed do + answer <- answer + bid * pos + pos <- pos + 1 + + answer + + let part2 (s : string) = + let parsed = parse true s + + parsed.Sort compBasic + + let mutable answer = 0 + let mutable pos = 1 + + for _, _, bid in parsed do + answer <- answer + bid * pos + pos <- pos + 1 + + answer diff --git a/AdventOfCode2023.FSharp/AdventOfCode2023.FSharp/Program.fs b/AdventOfCode2023.FSharp/AdventOfCode2023.FSharp/Program.fs index debee23..254e137 100644 --- a/AdventOfCode2023.FSharp/AdventOfCode2023.FSharp/Program.fs +++ b/AdventOfCode2023.FSharp/AdventOfCode2023.FSharp/Program.fs @@ -27,12 +27,13 @@ module Program = do sw.Restart () - let input = Path.Combine (dir.FullName, "day1.txt") |> File.ReadAllText + let input = Path.Combine (dir.FullName, "day1part1.txt") |> File.ReadAllText let part1 = Day1.part1 input sw.Stop () Console.WriteLine (part1.ToString ()) Console.Error.WriteLine ((1_000.0 * float sw.ElapsedTicks / float Stopwatch.Frequency).ToString () + "ms") sw.Restart () + let input = Path.Combine (dir.FullName, "day1.txt") |> File.ReadAllText let part2 = Day1.part2 input sw.Stop () Console.WriteLine (part2.ToString ()) @@ -136,6 +137,21 @@ module Program = Console.WriteLine (part2.ToString ()) Console.Error.WriteLine ((1_000.0 * float sw.ElapsedTicks / float Stopwatch.Frequency).ToString () + "ms") + printfn "=====Day 7=====" + + do + let input = Path.Combine (dir.FullName, "day7.txt") |> File.ReadAllText + sw.Restart () + let part1 = Day7.part1 input + sw.Stop () + Console.WriteLine (part1.ToString ()) + Console.Error.WriteLine ((1_000.0 * float sw.ElapsedTicks / float Stopwatch.Frequency).ToString () + "ms") + sw.Restart () + let part2 = Day7.part2 input + sw.Stop () + Console.WriteLine (part2.ToString ()) + Console.Error.WriteLine ((1_000.0 * float sw.ElapsedTicks / float Stopwatch.Frequency).ToString () + "ms") + endToEnd.Stop () Console.Error.WriteLine ( diff --git a/AdventOfCode2023.FSharp/Test/Test.fsproj b/AdventOfCode2023.FSharp/Test/Test.fsproj index e7a0f34..d7232f8 100644 --- a/AdventOfCode2023.FSharp/Test/Test.fsproj +++ b/AdventOfCode2023.FSharp/Test/Test.fsproj @@ -15,6 +15,7 @@ + @@ -22,6 +23,7 @@ + diff --git a/AdventOfCode2023.FSharp/Test/TestDay7.fs b/AdventOfCode2023.FSharp/Test/TestDay7.fs new file mode 100644 index 0000000..ebade5c --- /dev/null +++ b/AdventOfCode2023.FSharp/Test/TestDay7.fs @@ -0,0 +1,46 @@ +namespace AdventOfCode2023.Test + +open System +open AdventOfCode2023 +open NUnit.Framework +open FsUnitTyped +open System.IO + +[] +module TestDay7 = + + let sample = Assembly.getEmbeddedResource typeof.Assembly "day7.txt" + + [] + let part1Sample () = + sample |> Day7.part1 |> shouldEqual 6440 + + [] + let part2Sample () = + sample |> Day7.part2 |> shouldEqual 5905 + + [] + let part1Actual () = + let s = + try + File.ReadAllText (Path.Combine (__SOURCE_DIRECTORY__, "../../inputs/day7.txt")) + with + | :? DirectoryNotFoundException + | :? FileNotFoundException -> + Assert.Inconclusive () + failwith "unreachable" + + Day7.part1 s |> shouldEqual 250058342 + + [] + let part2Actual () = + let s = + try + File.ReadAllText (Path.Combine (__SOURCE_DIRECTORY__, "../../inputs/day7.txt")) + with + | :? DirectoryNotFoundException + | :? FileNotFoundException -> + Assert.Inconclusive () + failwith "unreachable" + + Day7.part2 s |> shouldEqual 250506580 diff --git a/AdventOfCode2023.FSharp/Test/samples/day7.txt b/AdventOfCode2023.FSharp/Test/samples/day7.txt new file mode 100644 index 0000000..e3500c3 --- /dev/null +++ b/AdventOfCode2023.FSharp/Test/samples/day7.txt @@ -0,0 +1,5 @@ +32T3K 765 +T55J5 684 +KK677 28 +KTJJT 220 +QQQJA 483