diff --git a/AdventOfCode2023.FSharp/AdventOfCode2023.FSharp.Lib/AdventOfCode2023.FSharp.Lib.fsproj b/AdventOfCode2023.FSharp/AdventOfCode2023.FSharp.Lib/AdventOfCode2023.FSharp.Lib.fsproj
index d0848ee..270bd11 100644
--- a/AdventOfCode2023.FSharp/AdventOfCode2023.FSharp.Lib/AdventOfCode2023.FSharp.Lib.fsproj
+++ b/AdventOfCode2023.FSharp/AdventOfCode2023.FSharp.Lib/AdventOfCode2023.FSharp.Lib.fsproj
@@ -9,6 +9,7 @@
+
diff --git a/AdventOfCode2023.FSharp/AdventOfCode2023.FSharp.Lib/Day2.fs b/AdventOfCode2023.FSharp/AdventOfCode2023.FSharp.Lib/Day2.fs
new file mode 100644
index 0000000..4ff87e8
--- /dev/null
+++ b/AdventOfCode2023.FSharp/AdventOfCode2023.FSharp.Lib/Day2.fs
@@ -0,0 +1,61 @@
+namespace AdventOfCode2023
+
+open System
+
+[]
+module Day2 =
+
+ let part1 (s : string) =
+ use lines = StringSplitEnumerator.make '\n' s
+ let mutable answer = 0
+
+ for line in lines do
+ if not line.IsEmpty then
+ use mutable words = StringSplitEnumerator.make' ' ' line
+ let mutable prevWord = ReadOnlySpan.Empty
+ let mutable isOk = true
+
+ while isOk && words.MoveNext () do
+ match words.Current.[0] with
+ | 'b' ->
+ if Int32.Parse prevWord > 14 then
+ isOk <- false
+ | 'r' ->
+ if Int32.Parse prevWord > 12 then
+ isOk <- false
+ | 'g' ->
+ if Int32.Parse prevWord > 13 then
+ isOk <- false
+ | _ -> ()
+
+ prevWord <- words.Current
+
+ if isOk then
+ answer <- answer + Int32.Parse (line.Slice (5, line.IndexOf ':' - 5))
+
+ answer
+
+ let part2 (s : string) =
+ use lines = StringSplitEnumerator.make '\n' s
+ let mutable answer = 0
+
+ for line in lines do
+ if not line.IsEmpty then
+ let mutable reds = 0
+ let mutable blues = 0
+ let mutable greens = 0
+ use mutable words = StringSplitEnumerator.make' ' ' line
+ let mutable prevWord = ReadOnlySpan.Empty
+
+ while words.MoveNext () do
+ match words.Current.[0] with
+ | 'b' -> blues <- max blues (Int32.Parse prevWord)
+ | 'r' -> reds <- max reds (Int32.Parse prevWord)
+ | 'g' -> greens <- max greens (Int32.Parse prevWord)
+ | _ -> ()
+
+ prevWord <- words.Current
+
+ answer <- answer + (reds * greens * blues)
+
+ answer
diff --git a/AdventOfCode2023.FSharp/Test/Test.fsproj b/AdventOfCode2023.FSharp/Test/Test.fsproj
index c095584..4c64424 100644
--- a/AdventOfCode2023.FSharp/Test/Test.fsproj
+++ b/AdventOfCode2023.FSharp/Test/Test.fsproj
@@ -9,6 +9,7 @@
+
diff --git a/AdventOfCode2023.FSharp/Test/TestDay2.fs b/AdventOfCode2023.FSharp/Test/TestDay2.fs
new file mode 100644
index 0000000..e484cd3
--- /dev/null
+++ b/AdventOfCode2023.FSharp/Test/TestDay2.fs
@@ -0,0 +1,46 @@
+namespace AdventOfCode2023.Test
+
+open AdventOfCode2023
+open NUnit.Framework
+open FsUnitTyped
+open System.IO
+
+[]
+module TestDay2 =
+
+ let sample =
+ """Game 1: 3 blue, 4 red; 1 red, 2 green, 6 blue; 2 green
+Game 2: 1 blue, 2 green; 3 green, 4 blue, 1 red; 1 green, 1 blue
+Game 3: 8 green, 6 blue, 20 red; 5 blue, 4 red, 13 green; 5 green, 1 red
+Game 4: 1 green, 3 red, 6 blue; 3 green, 6 red; 3 green, 15 blue, 14 red
+Game 5: 6 red, 1 blue, 3 green; 2 blue, 1 red, 2 green
+"""
+
+ []
+ let part1Sample () = sample |> Day2.part1 |> shouldEqual 8
+
+ []
+ let part2Sample () =
+ sample |> Day2.part2 |> shouldEqual 2286
+
+ []
+ let part1Actual () =
+ let s =
+ try
+ File.ReadAllText (Path.Combine (__SOURCE_DIRECTORY__, "../../inputs/day2.txt"))
+ with :? FileNotFoundException ->
+ Assert.Inconclusive ()
+ failwith "unreachable"
+
+ Day2.part1 s |> shouldEqual 2727
+
+ []
+ let part2Actual () =
+ let s =
+ try
+ File.ReadAllText (Path.Combine (__SOURCE_DIRECTORY__, "../../inputs/day2.txt"))
+ with :? FileNotFoundException ->
+ Assert.Inconclusive ()
+ failwith "unreachable"
+
+ Day2.part2 s |> shouldEqual 56580