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