Day 1 in F#
This commit is contained in:
1
.gitattributes
vendored
Normal file
1
.gitattributes
vendored
Normal file
@@ -0,0 +1 @@
|
||||
*.txt text eol=lf
|
@@ -7,6 +7,8 @@
|
||||
|
||||
<ItemGroup>
|
||||
<Compile Include="Arr2D.fs" />
|
||||
<Compile Include="EfficientString.fs" />
|
||||
<Compile Include="Day1.fs" />
|
||||
<Compile Include="Day3.fs" />
|
||||
</ItemGroup>
|
||||
|
||||
|
97
AdventOfCode2023.FSharp/AdventOfCode2023.FSharp.Lib/Day1.fs
Normal file
97
AdventOfCode2023.FSharp/AdventOfCode2023.FSharp.Lib/Day1.fs
Normal file
@@ -0,0 +1,97 @@
|
||||
namespace AdventOfCode2023
|
||||
|
||||
open System
|
||||
|
||||
[<RequireQualifiedAccess>]
|
||||
module Day1 =
|
||||
|
||||
let firstDigit (s : ReadOnlySpan<char>) =
|
||||
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<char>) =
|
||||
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<char>) =
|
||||
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<char>) =
|
||||
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
|
@@ -0,0 +1,97 @@
|
||||
namespace AdventOfCode2023
|
||||
|
||||
open System
|
||||
open System.Globalization
|
||||
open System.Runtime.CompilerServices
|
||||
|
||||
type EfficientString = System.ReadOnlySpan<char>
|
||||
|
||||
[<RequireQualifiedAccess>]
|
||||
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
|
||||
|
||||
[<Struct>]
|
||||
[<IsByRefLike>]
|
||||
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
|
||||
|
||||
[<RequireQualifiedAccess>]
|
||||
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<char>) : StringSplitEnumerator =
|
||||
{
|
||||
Original = s
|
||||
Remaining = s
|
||||
InternalCurrent = EfficientString.Empty
|
||||
SplitOn = splitChar
|
||||
}
|
||||
|
||||
let chomp (s : string) (e : byref<StringSplitEnumerator>) : 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<StringSplitEnumerator>) : int =
|
||||
if not (e.MoveNext ()) then
|
||||
failwith "expected an int, got nothing"
|
||||
|
||||
Int32.Parse e.Current
|
@@ -8,6 +8,7 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Compile Include="TestDay1.fs" />
|
||||
<Compile Include="TestDay3.fs" />
|
||||
</ItemGroup>
|
||||
|
||||
|
56
AdventOfCode2023.FSharp/Test/TestDay1.fs
Normal file
56
AdventOfCode2023.FSharp/Test/TestDay1.fs
Normal file
@@ -0,0 +1,56 @@
|
||||
namespace AdventOfCode2023.Test
|
||||
|
||||
open AdventOfCode2023
|
||||
open NUnit.Framework
|
||||
open FsUnitTyped
|
||||
open System.IO
|
||||
|
||||
[<TestFixture>]
|
||||
module TestDay1 =
|
||||
|
||||
let sample1 =
|
||||
"""1abc2
|
||||
pqr3stu8vwx
|
||||
a1b2c3d4e5f
|
||||
treb7uchet
|
||||
"""
|
||||
|
||||
[<Test>]
|
||||
let part1Sample () =
|
||||
sample1 |> Day1.part1 |> shouldEqual 142
|
||||
|
||||
let sample2 =
|
||||
"""two1nine
|
||||
eightwothree
|
||||
abcone2threexyz
|
||||
xtwone3four
|
||||
4nineeightseven2
|
||||
zoneight234
|
||||
7pqrstsixteen
|
||||
"""
|
||||
|
||||
[<Test>]
|
||||
let part2Sample () =
|
||||
sample2 |> Day1.part2 |> shouldEqual 281
|
||||
|
||||
[<Test>]
|
||||
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
|
||||
|
||||
[<Test>]
|
||||
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
|
Reference in New Issue
Block a user