diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..f9e337f --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +*.txt text eol=lf diff --git a/AdventOfCode2023.FSharp/AdventOfCode2023.FSharp.Lib/AdventOfCode2023.FSharp.Lib.fsproj b/AdventOfCode2023.FSharp/AdventOfCode2023.FSharp.Lib/AdventOfCode2023.FSharp.Lib.fsproj index e2186c7..d0848ee 100644 --- a/AdventOfCode2023.FSharp/AdventOfCode2023.FSharp.Lib/AdventOfCode2023.FSharp.Lib.fsproj +++ b/AdventOfCode2023.FSharp/AdventOfCode2023.FSharp.Lib/AdventOfCode2023.FSharp.Lib.fsproj @@ -7,6 +7,8 @@ + + diff --git a/AdventOfCode2023.FSharp/AdventOfCode2023.FSharp.Lib/Day1.fs b/AdventOfCode2023.FSharp/AdventOfCode2023.FSharp.Lib/Day1.fs new file mode 100644 index 0000000..3f43e4e --- /dev/null +++ b/AdventOfCode2023.FSharp/AdventOfCode2023.FSharp.Lib/Day1.fs @@ -0,0 +1,97 @@ +namespace AdventOfCode2023 + +open System + +[] +module Day1 = + + let firstDigit (s : ReadOnlySpan) = + let mutable pos = 0 + + while '0' > s.[pos] || s.[pos] > '9' do + pos <- pos + 1 + + 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 + + byte s.[pos] - byte '0' + + let part1 (s : string) = + use enum = StringSplitEnumerator.make '\n' s + let mutable total = 0 + + for line in enum do + if not line.IsEmpty then + let firstDigit = firstDigit line + let lastDigit = lastDigit line + + total <- total + int (lastDigit + 10uy * firstDigit) + + total + + let table = + [| + "one", 1uy + "two", 2uy + "three", 3uy + "four", 4uy + "five", 5uy + "six", 6uy + "seven", 7uy + "eight", 8uy + "nine", 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 + + answer + + let lastDigitIncSpelled (s : ReadOnlySpan) = + let mutable pos = s.Length - 1 + 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 + + answer + + let part2 (s : string) = + use enum = StringSplitEnumerator.make '\n' s + let mutable total = 0 + + for line in enum do + if not line.IsEmpty then + total <- total + int (10uy * firstDigitIncSpelled line + lastDigitIncSpelled line) + + total diff --git a/AdventOfCode2023.FSharp/AdventOfCode2023.FSharp.Lib/EfficientString.fs b/AdventOfCode2023.FSharp/AdventOfCode2023.FSharp.Lib/EfficientString.fs new file mode 100644 index 0000000..16eb2fb --- /dev/null +++ b/AdventOfCode2023.FSharp/AdventOfCode2023.FSharp.Lib/EfficientString.fs @@ -0,0 +1,97 @@ +namespace AdventOfCode2023 + +open System +open System.Globalization +open System.Runtime.CompilerServices + +type EfficientString = System.ReadOnlySpan + +[] +module EfficientString = + + let inline isEmpty (s : EfficientString) : bool = s.IsEmpty + + + let inline ofString (s : string) : EfficientString = s.AsSpan () + + let inline toString (s : EfficientString) : string = s.ToString () + + let inline trimStart (s : EfficientString) : EfficientString = s.TrimStart () + + let inline slice (start : int) (length : int) (s : EfficientString) : EfficientString = s.Slice (start, length) + + let inline equals (a : string) (other : EfficientString) : bool = + MemoryExtensions.Equals (other, a.AsSpan (), StringComparison.Ordinal) + + /// Mutates the input to drop up to the first instance of the input char, + /// and returns what was dropped. + /// If the char is not present, deletes the input. + let takeUntil<'a> (c : char) (s : EfficientString byref) : EfficientString = + let first = s.IndexOf c + + if first < 0 then + let toRet = s + s <- EfficientString.Empty + toRet + else + let toRet = slice 0 first s + s <- slice (first + 1) (s.Length - first - 1) s + toRet + +[] +[] +type StringSplitEnumerator = + internal + { + Original : EfficientString + mutable Remaining : EfficientString + mutable InternalCurrent : EfficientString + SplitOn : char + } + + interface IDisposable with + member this.Dispose () = () + + member this.Current : EfficientString = this.InternalCurrent + + member this.MoveNext () = + if this.Remaining.Length = 0 then + false + else + this.InternalCurrent <- EfficientString.takeUntil this.SplitOn &this.Remaining + true + + member this.GetEnumerator () = this + +[] +module StringSplitEnumerator = + + let make (splitChar : char) (s : string) : StringSplitEnumerator = + { + Original = EfficientString.ofString s + Remaining = EfficientString.ofString s + InternalCurrent = EfficientString.Empty + SplitOn = splitChar + } + + let make' (splitChar : char) (s : ReadOnlySpan) : StringSplitEnumerator = + { + Original = s + Remaining = s + InternalCurrent = EfficientString.Empty + SplitOn = splitChar + } + + let chomp (s : string) (e : byref) : unit = +#if DEBUG + if not (e.MoveNext ()) || not (EfficientString.equals s e.Current) then + failwithf "expected '%s', got '%s'" s (e.Current.ToString ()) +#else + e.MoveNext () |> ignore +#endif + + let consumeInt (e : byref) : int = + if not (e.MoveNext ()) then + failwith "expected an int, got nothing" + + Int32.Parse e.Current diff --git a/AdventOfCode2023.FSharp/Test/Test.fsproj b/AdventOfCode2023.FSharp/Test/Test.fsproj index 95406ce..c095584 100644 --- a/AdventOfCode2023.FSharp/Test/Test.fsproj +++ b/AdventOfCode2023.FSharp/Test/Test.fsproj @@ -8,6 +8,7 @@ + diff --git a/AdventOfCode2023.FSharp/Test/TestDay1.fs b/AdventOfCode2023.FSharp/Test/TestDay1.fs new file mode 100644 index 0000000..6fe4416 --- /dev/null +++ b/AdventOfCode2023.FSharp/Test/TestDay1.fs @@ -0,0 +1,56 @@ +namespace AdventOfCode2023.Test + +open AdventOfCode2023 +open NUnit.Framework +open FsUnitTyped +open System.IO + +[] +module TestDay1 = + + let sample1 = + """1abc2 +pqr3stu8vwx +a1b2c3d4e5f +treb7uchet +""" + + [] + let part1Sample () = + sample1 |> Day1.part1 |> shouldEqual 142 + + let sample2 = + """two1nine +eightwothree +abcone2threexyz +xtwone3four +4nineeightseven2 +zoneight234 +7pqrstsixteen +""" + + [] + let part2Sample () = + sample2 |> Day1.part2 |> shouldEqual 281 + + [] + let part1Actual () = + let s = + try + File.ReadAllText (Path.Combine (__SOURCE_DIRECTORY__, "../../inputs/day1.txt")) + with :? FileNotFoundException -> + Assert.Inconclusive () + failwith "unreachable" + + Day1.part1 s |> shouldEqual 54304 + + [] + let part2Actual () = + let s = + try + File.ReadAllText (Path.Combine (__SOURCE_DIRECTORY__, "../../inputs/day1.txt")) + with :? FileNotFoundException -> + Assert.Inconclusive () + failwith "unreachable" + + Day1.part2 s |> shouldEqual 54418