summaryrefslogtreecommitdiff
path: root/ocaml/lib/year2025
diff options
context:
space:
mode:
Diffstat (limited to 'ocaml/lib/year2025')
-rw-r--r--ocaml/lib/year2025/day01/part01/dial.ml76
-rw-r--r--ocaml/lib/year2025/day01/part01/solver.ml53
-rw-r--r--ocaml/lib/year2025/day01/part01/solver.mli4
-rw-r--r--ocaml/lib/year2025/day01/part02/dial.ml68
-rw-r--r--ocaml/lib/year2025/day01/part02/solver.ml56
-rw-r--r--ocaml/lib/year2025/day01/part02/solver.mli4
-rw-r--r--ocaml/lib/year2025/day01/part02/turner.ml28
-rw-r--r--ocaml/lib/year2025/day02/part01/id.ml16
-rw-r--r--ocaml/lib/year2025/day02/part01/id.mli22
-rw-r--r--ocaml/lib/year2025/day02/part01/range.ml18
-rw-r--r--ocaml/lib/year2025/day02/part01/range.mli15
-rw-r--r--ocaml/lib/year2025/day02/part01/solver.ml55
-rw-r--r--ocaml/lib/year2025/day02/part01/solver.mli4
-rw-r--r--ocaml/lib/year2025/day02/part02/id.ml12
-rw-r--r--ocaml/lib/year2025/day02/part02/id.mli16
-rw-r--r--ocaml/lib/year2025/day02/part02/range.ml18
-rw-r--r--ocaml/lib/year2025/day02/part02/range.mli15
-rw-r--r--ocaml/lib/year2025/day02/part02/solver.ml87
-rw-r--r--ocaml/lib/year2025/day02/part02/solver.mli4
-rw-r--r--ocaml/lib/year2025/day03/part01/battery.ml13
-rw-r--r--ocaml/lib/year2025/day03/part01/battery.mli13
-rw-r--r--ocaml/lib/year2025/day03/part01/battery_bank.ml75
-rw-r--r--ocaml/lib/year2025/day03/part01/battery_bank.mli18
-rw-r--r--ocaml/lib/year2025/day03/part01/solver.ml23
-rw-r--r--ocaml/lib/year2025/day03/part01/solver.mli4
-rw-r--r--ocaml/lib/year2025/day03/part02/battery.ml25
-rw-r--r--ocaml/lib/year2025/day03/part02/battery.mli13
-rw-r--r--ocaml/lib/year2025/day03/part02/battery_bank.ml77
-rw-r--r--ocaml/lib/year2025/day03/part02/battery_bank.mli18
-rw-r--r--ocaml/lib/year2025/day03/part02/solver.ml23
-rw-r--r--ocaml/lib/year2025/day03/part02/solver.mli4
-rw-r--r--ocaml/lib/year2025/day04/part01/solver.ml48
-rw-r--r--ocaml/lib/year2025/day04/part01/solver.mli4
-rw-r--r--ocaml/lib/year2025/day04/part02/solver.ml66
-rw-r--r--ocaml/lib/year2025/day04/part02/solver.mli4
-rw-r--r--ocaml/lib/year2025/day06/part01/solver.ml50
-rw-r--r--ocaml/lib/year2025/day06/part01/solver.mli4
-rw-r--r--ocaml/lib/year2025/day06/part02/solver.ml50
-rw-r--r--ocaml/lib/year2025/day06/part02/solver.mli4
-rw-r--r--ocaml/lib/year2025/dune16
40 files changed, 1123 insertions, 0 deletions
diff --git a/ocaml/lib/year2025/day01/part01/dial.ml b/ocaml/lib/year2025/day01/part01/dial.ml
new file mode 100644
index 0000000..6b531f4
--- /dev/null
+++ b/ocaml/lib/year2025/day01/part01/dial.ml
@@ -0,0 +1,76 @@
+module Dial : sig
+ (** [t] is a dial between 0 and 99 inclusive *)
+ type t
+
+ (** [direction] is a direction the dial can be turned *)
+ type direction = Left of int | Right of int
+
+ val start : t
+ (** [start] is the initial state of the dial *)
+
+ val dial_out_of_range : string
+ (** [dial_out_of_range] is the error message when the dial is out of range *)
+
+ val turn : t -> direction -> t
+ (** [turn current direction] is the new state of the dial after turning [direction] from [current] *)
+
+ val to_string : t -> string
+ (** [to_string t] is the string representation of [t] *)
+
+ val to_int : t -> int
+ (** [to_int t] is the integer representation of [t] *)
+
+ val pp_direction : direction -> string
+end = struct
+ (** [t] is an int between 0 and 99 inclusive *)
+ type t = int
+
+ type direction = Left of int | Right of int
+
+ let start = 50
+
+ let dial_out_of_range = "dial out of range"
+
+ (** [next n] is the new state of the dial after turning [Right].
+ Raises: [dial_out_of_range] if [n] is greater than 99. *)
+ let next = function
+ | n when n < 99 ->
+ n + 1
+ | 99 ->
+ 0
+ | _ ->
+ failwith dial_out_of_range
+
+ (** [prev n] is the new state of the dial after turning [Left].
+ Raises: [dial_out_of_range] if [n] is less than 0. *)
+ let prev = function
+ | n when n > 0 ->
+ n - 1
+ | 0 ->
+ 99
+ | _ ->
+ failwith dial_out_of_range
+
+ (** [turn current direction] is the new state of the dial after turning [direction] from [current].
+ Raises: [dial_out_of_range] if the dial is moved to an invalid state. *)
+ let rec turn current direction =
+ match direction with
+ | Left 0 ->
+ current
+ | Left n ->
+ turn (prev current) (Left (n - 1))
+ | Right 0 ->
+ current
+ | Right n ->
+ turn (next current) (Right (n - 1))
+
+ let to_string = string_of_int
+
+ let to_int t = t
+
+ let pp_direction = function
+ | Left n ->
+ "Left " ^ string_of_int n
+ | Right n ->
+ "Right " ^ string_of_int n
+end
diff --git a/ocaml/lib/year2025/day01/part01/solver.ml b/ocaml/lib/year2025/day01/part01/solver.ml
new file mode 100644
index 0000000..f4f0dde
--- /dev/null
+++ b/ocaml/lib/year2025/day01/part01/solver.ml
@@ -0,0 +1,53 @@
+open Dial
+
+let err_empty_input = "input is empty"
+
+let rec input_to_lines input =
+ let lines =
+ String.split_on_char '\n' input |> List.filter (fun l -> l <> "")
+ in
+ pp_lines lines ; lines
+
+and pp_lines = function
+ | [] ->
+ ()
+ | h :: t ->
+ Logs.debug (fun m -> m "Line=%s" h) ;
+ pp_lines t
+
+let parse_line line =
+ Scanf.sscanf line "%c%d" (fun d n ->
+ match d with
+ | 'L' ->
+ Dial.Left n
+ | 'R' ->
+ Dial.Right n
+ | _ ->
+ failwith "invalid direction" )
+
+let lines_to_turns =
+ List.map (fun (l : string) ->
+ Logs.debug (fun m -> m "parsing %s into turn" l) ;
+ let turn = parse_line l in
+ Logs.debug (fun m -> m "%s" (Dial.pp_direction turn)) ;
+ turn )
+
+let rec turn_aux count current = function
+ | [] ->
+ count
+ | h :: t ->
+ let current' = Dial.turn current h in
+ Logs.debug (fun m -> m "dial at %d" (Dial.to_int current')) ;
+ if Dial.to_int current' = 0 then turn_aux (count + 1) current' t
+ else turn_aux count current' t
+
+let turn current = turn_aux 0 current
+
+let solve input () =
+ if input = "" then Error err_empty_input
+ else
+ let lines = input_to_lines input in
+ let turns = lines_to_turns lines in
+ let start = Dial.start in
+ let answer = turn start turns in
+ Ok answer
diff --git a/ocaml/lib/year2025/day01/part01/solver.mli b/ocaml/lib/year2025/day01/part01/solver.mli
new file mode 100644
index 0000000..2eecf19
--- /dev/null
+++ b/ocaml/lib/year2025/day01/part01/solver.mli
@@ -0,0 +1,4 @@
+val solve : string -> unit -> (int, string) result
+(** [solve input ()] is the solution for the first part of the first day of advent of code 2025 for [input].
+ The solution for an empty input is [None].
+ Requires: [input] to be a multiline string using `\n` as it's line separator. *)
diff --git a/ocaml/lib/year2025/day01/part02/dial.ml b/ocaml/lib/year2025/day01/part02/dial.ml
new file mode 100644
index 0000000..e061d6d
--- /dev/null
+++ b/ocaml/lib/year2025/day01/part02/dial.ml
@@ -0,0 +1,68 @@
+module Dial : sig
+ (** [t] is a dial between 0 and 99 inclusive *)
+ type t
+
+ (** [direction] is a direction the dial can be turned *)
+ type direction = Left of int | Right of int
+
+ val start : t
+ (** [start] is the initial state of the dial *)
+
+ val dial_out_of_range : string
+ (** [dial_out_of_range] is the error message when the dial is out of range *)
+
+ val prev : t -> t
+
+ val next : t -> t
+
+ val is_zero : t -> bool
+
+ val to_string : t -> string
+ (** [to_string t] is the string representation of [t] *)
+
+ val to_int : t -> int
+ (** [to_int t] is the integer representation of [t] *)
+
+ val pp_direction : direction -> string
+end = struct
+ (** [t] is an int between 0 and 99 inclusive *)
+ type t = int
+
+ type direction = Left of int | Right of int
+
+ let start = 50
+
+ let dial_out_of_range = "dial out of range"
+
+ (** [next n] is the new state of the dial after turning [Right].
+ Raises: [dial_out_of_range] if [n] is greater than 99. *)
+ let next = function
+ | n when n < 99 ->
+ n + 1
+ | 99 ->
+ 0
+ | _ ->
+ failwith dial_out_of_range
+
+ (** [prev n] is the new state of the dial after turning [Left].
+ Raises: [dial_out_of_range] if [n] is less than 0. *)
+ let prev = function
+ | n when n > 0 ->
+ n - 1
+ | 0 ->
+ 99
+ | _ ->
+ failwith dial_out_of_range
+
+ let is_zero = function 0 -> true | _ -> false
+
+ let to_string = string_of_int
+
+ let to_int t = t
+
+ let pp_direction = function
+ | Left n ->
+ "Left " ^ string_of_int n
+ | Right n ->
+ "Right " ^ string_of_int n
+end
diff --git a/ocaml/lib/year2025/day01/part02/solver.ml b/ocaml/lib/year2025/day01/part02/solver.ml
new file mode 100644
index 0000000..452209f
--- /dev/null
+++ b/ocaml/lib/year2025/day01/part02/solver.ml
@@ -0,0 +1,56 @@
+open Dial
+open Turner
+
+let err_empty_input = "input is empty"
+
+let rec input_to_lines input =
+ let lines =
+ String.split_on_char '\n' input |> List.filter (fun l -> l <> "")
+ in
+ pp_lines lines ; lines
+
+and pp_lines = function
+ | [] ->
+ ()
+ | h :: t ->
+ Logs.debug (fun m -> m "Line=%s" h) ;
+ pp_lines t
+
+let parse_line line =
+ Scanf.sscanf line "%c%d" (fun d n ->
+ match d with
+ | 'L' ->
+ Dial.Left n
+ | 'R' ->
+ Dial.Right n
+ | _ ->
+ failwith "invalid direction" )
+
+let lines_to_turns =
+ List.map (fun (l : string) ->
+ Logs.debug (fun m -> m "parsing %s into turn" l) ;
+ let turn = parse_line l in
+ Logs.debug (fun m -> m "%s" (Dial.pp_direction turn)) ;
+ turn )
+
+let rec turn_aux count dial = function
+ | [] ->
+ count
+ | h :: t -> (
+ let dial_and_count = Turner.turn dial count h in
+ match dial_and_count with
+ | dial', count' ->
+ Logs.debug (fun m ->
+ m "(dial=%d, count=%d)" (Dial.to_int dial') count' ) ;
+ turn_aux count' dial' t )
+
+let turn current = turn_aux 0 current
+
+let solve input () =
+ if input = "" then Error err_empty_input
+ else
+ let lines = input_to_lines input in
+ let turns = lines_to_turns lines in
+ let start = Dial.start in
+ let answer = turn start turns in
+ Ok answer
diff --git a/ocaml/lib/year2025/day01/part02/solver.mli b/ocaml/lib/year2025/day01/part02/solver.mli
new file mode 100644
index 0000000..9e9a2f2
--- /dev/null
+++ b/ocaml/lib/year2025/day01/part02/solver.mli
@@ -0,0 +1,4 @@
+val solve : string -> unit -> (int, string) result
+(** [solve input ()] is the solution for the second part of the first day of advent of code 2025 for [input].
+ The solution for an empty input is [Error msg].
+ Requires: [input] to be a multiline string using `\n` as it's line separator. *)
diff --git a/ocaml/lib/year2025/day01/part02/turner.ml b/ocaml/lib/year2025/day01/part02/turner.ml
new file mode 100644
index 0000000..7477093
--- /dev/null
+++ b/ocaml/lib/year2025/day01/part02/turner.ml
@@ -0,0 +1,28 @@
+open Dial
+
+module Turner : sig
+ val turn : Dial.t -> int -> Dial.direction -> Dial.t * int
+ (** [turn dial direction] is the new state of the dial after turning [direction] on [dial] *)
+end = struct
+ (** [turn dial direction] is the new state of the dial after turning [direction] on [dial].
+ Raises: [dial_out_of_range] if the dial is moved to an invalid state. *)
+ let rec turn dial zero_count direction =
+ let open Dial in
+ match direction with
+ | Left 0 ->
+ (dial, zero_count)
+ | Left n ->
+ let dial' = prev dial in
+ let zero_count' =
+ if Dial.is_zero dial' then zero_count + 1 else zero_count
+ in
+ turn dial' zero_count' (Left (n - 1))
+ | Right 0 ->
+ (dial, zero_count)
+ | Right n ->
+ let dial' = next dial in
+ let zero_count' =
+ if Dial.is_zero dial' then zero_count + 1 else zero_count
+ in
+ turn dial' zero_count' (Right (n - 1))
+end
diff --git a/ocaml/lib/year2025/day02/part01/id.ml b/ocaml/lib/year2025/day02/part01/id.ml
new file mode 100644
index 0000000..b7a3bd2
--- /dev/null
+++ b/ocaml/lib/year2025/day02/part01/id.ml
@@ -0,0 +1,16 @@
+module ID = struct
+ (** *)
+ type t = int
+
+ let create num = num
+
+ let validate rules id = List.fold_left (fun acc r -> acc && r id) true rules
+
+ let incr = ( + ) 1
+
+ let to_int id = id
+
+ let to_string = string_of_int
+
+ let pp id = "ID=" ^ string_of_int id
+end
diff --git a/ocaml/lib/year2025/day02/part01/id.mli b/ocaml/lib/year2025/day02/part01/id.mli
new file mode 100644
index 0000000..63d5942
--- /dev/null
+++ b/ocaml/lib/year2025/day02/part01/id.mli
@@ -0,0 +1,22 @@
+module ID : sig
+ (** [t] is a ID within a range *)
+ type t
+
+ val create : int -> t
+ (** [create num] is an ID [t] *)
+
+ val validate : (t -> bool) list -> t -> bool
+ (** [validate rules id] is [true] if the [id] passes all rule checks *)
+
+ val incr : t -> t
+ (** [incr id] is the next [id] in the sequence *)
+
+ val to_int : t -> int
+ (** [to_int id] is an integer representation of [id] *)
+
+ val to_string : t -> string
+ (** [to_string id] is an string representation of [id] *)
+
+ val pp : t -> string
+ (** [pp id] is a pretty string representing [id] *)
+end
diff --git a/ocaml/lib/year2025/day02/part01/range.ml b/ocaml/lib/year2025/day02/part01/range.ml
new file mode 100644
index 0000000..ba27568
--- /dev/null
+++ b/ocaml/lib/year2025/day02/part01/range.ml
@@ -0,0 +1,18 @@
+open Id
+
+module Range = struct
+ type t = ID.t * ID.t
+
+ let create (id1, id2) = (ID.create id1, ID.create id2)
+
+ let rec collect range = collect_range_aux [] range
+
+ and collect_range_aux lst (id1, id2) =
+ match id1 <= id2 with
+ | false ->
+ lst
+ | true ->
+ collect_range_aux (id1 :: lst) (ID.incr id1, id2)
+
+ let pp (id1, id2) = "(L" ^ ID.pp id1 ^ ", R" ^ ID.pp id2 ^ ")"
+end
diff --git a/ocaml/lib/year2025/day02/part01/range.mli b/ocaml/lib/year2025/day02/part01/range.mli
new file mode 100644
index 0000000..e525e43
--- /dev/null
+++ b/ocaml/lib/year2025/day02/part01/range.mli
@@ -0,0 +1,15 @@
+open Id
+
+module Range : sig
+ (** [t] is a range of values *)
+ type t
+
+ val create : int * int -> t
+ (** [create (s, e)] is a range starting at [s] and ending at [e]. *)
+
+ val collect : t -> ID.t list
+ (** [collect r] is a list containing all elements within the range (inclusive). *)
+
+ val pp : t -> string
+ (** [pp r] is a pretty string representing [r] *)
+end
diff --git a/ocaml/lib/year2025/day02/part01/solver.ml b/ocaml/lib/year2025/day02/part01/solver.ml
new file mode 100644
index 0000000..417aa0d
--- /dev/null
+++ b/ocaml/lib/year2025/day02/part01/solver.ml
@@ -0,0 +1,55 @@
+open Id
+open Range
+
+let err_empty_input = "input is empty"
+
+(** [input_to_lines input] is a list of strings that have been split on ','.
+ Example: [input_to_lines "12-34,56-78,90-100"] becomes the list \[ "12-34"; "56-78"; "90-100" \].
+ Requires: [input] to be a string in the format "12-34,56-78,90-100". *)
+let input_to_lines input = input |> String.trim |> String.split_on_char ','
+
+let pp_lines = Seq.iter (fun l -> Logs.debug (fun m -> m "line=%s" l))
+
+(** [lines_to_ranges line] is a list of [Range]s found on [line]. *)
+let lines_to_ranges =
+ Seq.map (fun line ->
+ Scanf.sscanf line "%d-%d" (fun id1 id2 -> Range.create (id1, id2)) )
+
+let pp_ranges = Seq.iter (fun r -> Logs.debug (fun m -> m "%s" (Range.pp r)))
+
+(** [collect_all ranges] is an unsorted list of [ID]s within the [ranges] (inclusive). *)
+let collect_all =
+ Seq.concat_map (fun range -> List.to_seq (Range.collect range))
+
+let pp_collection = Seq.iter (fun c -> Logs.debug (fun m -> m "%s" (ID.pp c)))
+
+(** [rules] is a list of rules that [ID]s must pass to be considered invalid. *)
+let rules =
+ [ (fun id -> String.length (ID.to_string id) mod 2 = 0)
+ ; (fun id ->
+ let id' = ID.to_string id in
+ let id_len = String.length id' in
+ let left = String.sub id' 0 (id_len / 2) in
+ let right = String.sub id' (id_len / 2) (id_len / 2) in
+ String.equal left right ) ]
+
+(** [is_invalid id] is [true] for an [id] that passes all of the rules and is considered invalid. *)
+let is_invalid = ID.validate rules
+
+(** [passthrough_with_logs logger lst] is [lst] with the side-effects caused by calling [logger] on [lst]. *)
+let passthrough_with_logs logger lst = logger lst ; lst
+
+let solve input () =
+ if input = "" then Error err_empty_input
+ else
+ let answer =
+ input |> input_to_lines |> List.to_seq
+ |> passthrough_with_logs pp_lines
+ |> lines_to_ranges
+ |> passthrough_with_logs pp_ranges
+ |> collect_all
+ |> passthrough_with_logs pp_collection
+ |> Seq.filter is_invalid
+ |> Seq.fold_left (fun acc id -> acc + ID.to_int id) 0
+ in
+ Ok answer
diff --git a/ocaml/lib/year2025/day02/part01/solver.mli b/ocaml/lib/year2025/day02/part01/solver.mli
new file mode 100644
index 0000000..8a8eb32
--- /dev/null
+++ b/ocaml/lib/year2025/day02/part01/solver.mli
@@ -0,0 +1,4 @@
+val solve : string -> unit -> (int, string) result
+(** [solve input ()] is the solution for the first part of the second day of advent of code 2025 for [input].
+ The solution for an empty input is [Error msg].
+ Requires: [input] to be a string. *)
diff --git a/ocaml/lib/year2025/day02/part02/id.ml b/ocaml/lib/year2025/day02/part02/id.ml
new file mode 100644
index 0000000..f26c395
--- /dev/null
+++ b/ocaml/lib/year2025/day02/part02/id.ml
@@ -0,0 +1,12 @@
+module ID = struct
+ (** *)
+ type t = int
+
+ let create num = num
+
+ let incr = ( + ) 1
+
+ let to_string = string_of_int
+
+ let pp id = "ID=" ^ string_of_int id
+end
diff --git a/ocaml/lib/year2025/day02/part02/id.mli b/ocaml/lib/year2025/day02/part02/id.mli
new file mode 100644
index 0000000..a512593
--- /dev/null
+++ b/ocaml/lib/year2025/day02/part02/id.mli
@@ -0,0 +1,16 @@
+module ID : sig
+ (** [t] is a ID within a range *)
+ type t
+
+ val create : int -> t
+ (** [create num] is an ID [t] *)
+
+ val incr : t -> t
+ (** [incr id] is the next [id] in the sequence *)
+
+ val to_string : t -> string
+ (** [to_string id] is an string representation of [id] *)
+
+ val pp : t -> string
+ (** [pp id] is a pretty string representing [id] *)
+end
diff --git a/ocaml/lib/year2025/day02/part02/range.ml b/ocaml/lib/year2025/day02/part02/range.ml
new file mode 100644
index 0000000..ba27568
--- /dev/null
+++ b/ocaml/lib/year2025/day02/part02/range.ml
@@ -0,0 +1,18 @@
+open Id
+
+module Range = struct
+ type t = ID.t * ID.t
+
+ let create (id1, id2) = (ID.create id1, ID.create id2)
+
+ let rec collect range = collect_range_aux [] range
+
+ and collect_range_aux lst (id1, id2) =
+ match id1 <= id2 with
+ | false ->
+ lst
+ | true ->
+ collect_range_aux (id1 :: lst) (ID.incr id1, id2)
+
+ let pp (id1, id2) = "(L" ^ ID.pp id1 ^ ", R" ^ ID.pp id2 ^ ")"
+end
diff --git a/ocaml/lib/year2025/day02/part02/range.mli b/ocaml/lib/year2025/day02/part02/range.mli
new file mode 100644
index 0000000..e525e43
--- /dev/null
+++ b/ocaml/lib/year2025/day02/part02/range.mli
@@ -0,0 +1,15 @@
+open Id
+
+module Range : sig
+ (** [t] is a range of values *)
+ type t
+
+ val create : int * int -> t
+ (** [create (s, e)] is a range starting at [s] and ending at [e]. *)
+
+ val collect : t -> ID.t list
+ (** [collect r] is a list containing all elements within the range (inclusive). *)
+
+ val pp : t -> string
+ (** [pp r] is a pretty string representing [r] *)
+end
diff --git a/ocaml/lib/year2025/day02/part02/solver.ml b/ocaml/lib/year2025/day02/part02/solver.ml
new file mode 100644
index 0000000..f5fdf0c
--- /dev/null
+++ b/ocaml/lib/year2025/day02/part02/solver.ml
@@ -0,0 +1,87 @@
+open Id
+open Range
+
+(* Errors *)
+
+let err_empty_input = "input is empty"
+
+(* Logging *)
+
+(** [passthrough_with_logs logger lst] is [lst] with the side-effects caused by calling [logger] on [lst]. *)
+let passthrough_with_logs logger lst = logger lst ; lst
+
+(** [input_to_lines input] is a list of strings that have been split on ','.
+ Example: [input_to_lines "12-34,56-78,90-100"] becomes the list \[ "12-34"; "56-78"; "90-100" \].
+ Requires: [input] to be a string in the format "12-34,56-78,90-100". *)
+let input_to_lines input = input |> String.trim |> String.split_on_char ','
+
+let pp_lines = Seq.iter (fun l -> Logs.debug (fun m -> m "line=%s" l))
+
+let pp_ranges = Seq.iter (fun r -> Logs.debug (fun m -> m "%s" (Range.pp r)))
+
+let pp_collection = Seq.iter (fun c -> Logs.debug (fun m -> m "%s" (ID.pp c)))
+
+(* Core Logic *)
+
+(** [lines_to_ranges line] is a list of [Range]s found on [line]. *)
+let lines_to_ranges =
+ Seq.map (fun line ->
+ Scanf.sscanf line "%d-%d" (fun id1 id2 -> Range.create (id1, id2)) )
+
+(** [collect_all ranges] is an unsorted list of [ID]s within the [ranges] (inclusive). *)
+let collect_all =
+ Seq.concat_map (fun range -> List.to_seq (Range.collect range))
+
+(** [string_to_segments count string] is a list containing [count] string segments with of equal length.
+ Requires: length of [string] to be divisible by [count]
+ and [count] to be greater than or equal to zero *)
+let rec string_to_segments s =
+ let segment_lengths = possible_segment_lengths s in
+ List.map (fun c -> string_to_segments_aux [] c s |> List.rev) segment_lengths
+
+(** [possible_segment_lengths s] is a list of segment sizes that fit within [s]
+ that must be checked to rule out a repeating pattern. *)
+and possible_segment_lengths s =
+ List.init (String.length s) (fun x -> x + 1)
+ |> List.filter (fun x -> x != 1 && String.length s mod x = 0)
+
+and string_to_segments_aux segments count string =
+ match List.length segments = count with
+ | true ->
+ segments
+ | false ->
+ let segment_start = String.length string / count * List.length segments in
+ let segment_len = String.length string / count in
+ string_to_segments_aux
+ (String.sub string segment_start segment_len :: segments)
+ count string
+
+(** [cmp_segments segments] is true is all segments are equal *)
+let cmp_segments segments =
+ List.length (List.sort_uniq String.compare segments) = 1
+
+(** [is_invalid id] is [true] for an [id] that is considered invalid. *)
+let is_invalid id =
+ Logs.debug (fun m -> m "CHECKING ID=%s" id) ;
+ let repeating_patterns =
+ id |> string_to_segments |> List.filter cmp_segments
+ in
+ List.iter
+ (fun l -> Logs.debug (fun m -> m "HAS SUBPATTERN=%s" (List.hd l)))
+ repeating_patterns ;
+ List.length repeating_patterns > 0
+
+let solve input () =
+ if input = "" then Error err_empty_input
+ else
+ let answer =
+ input |> input_to_lines |> List.to_seq
+ |> passthrough_with_logs pp_lines
+ |> lines_to_ranges
+ |> passthrough_with_logs pp_ranges
+ |> collect_all
+ |> passthrough_with_logs pp_collection
+ |> Seq.map ID.to_string |> Seq.filter is_invalid
+ |> Seq.fold_left (fun acc id -> acc + int_of_string id) 0
+ in
+ Ok answer
diff --git a/ocaml/lib/year2025/day02/part02/solver.mli b/ocaml/lib/year2025/day02/part02/solver.mli
new file mode 100644
index 0000000..8a8eb32
--- /dev/null
+++ b/ocaml/lib/year2025/day02/part02/solver.mli
@@ -0,0 +1,4 @@
+val solve : string -> unit -> (int, string) result
+(** [solve input ()] is the solution for the first part of the second day of advent of code 2025 for [input].
+ The solution for an empty input is [Error msg].
+ Requires: [input] to be a string. *)
diff --git a/ocaml/lib/year2025/day03/part01/battery.ml b/ocaml/lib/year2025/day03/part01/battery.ml
new file mode 100644
index 0000000..cb2d9e5
--- /dev/null
+++ b/ocaml/lib/year2025/day03/part01/battery.ml
@@ -0,0 +1,13 @@
+module Battery = struct
+ type t = int
+
+ let of_int i = i
+
+ let to_int b = b
+
+ let to_string = string_of_int
+
+ let compare = Int.compare
+
+ let pp = string_of_int
+end
diff --git a/ocaml/lib/year2025/day03/part01/battery.mli b/ocaml/lib/year2025/day03/part01/battery.mli
new file mode 100644
index 0000000..e00940b
--- /dev/null
+++ b/ocaml/lib/year2025/day03/part01/battery.mli
@@ -0,0 +1,13 @@
+module Battery : sig
+ type t
+
+ val of_int : int -> t
+
+ val to_int : t -> int
+
+ val to_string : t -> string
+
+ val compare : t -> t -> int
+
+ val pp : t -> string
+end
diff --git a/ocaml/lib/year2025/day03/part01/battery_bank.ml b/ocaml/lib/year2025/day03/part01/battery_bank.ml
new file mode 100644
index 0000000..7ebce75
--- /dev/null
+++ b/ocaml/lib/year2025/day03/part01/battery_bank.ml
@@ -0,0 +1,75 @@
+open Battery
+
+module BatteryBank = struct
+ type t = Battery.t list
+
+ let rec highest_joltage bb =
+ assert (List.length bb > 0) ;
+ match highest_joltage_aux (None, None) bb with
+ | None, None ->
+ 0
+ | Some x, Some y ->
+ int_of_string (Battery.to_string x ^ Battery.to_string y)
+ | _ ->
+ failwith "highest_joltage must be two digits"
+
+ and highest_joltage_aux acc = function
+ | [] ->
+ acc
+ | [elt] -> (
+ match acc with
+ | None, None ->
+ (Some (Battery.of_int 0), Some elt)
+ | Some first, None ->
+ (Some first, Some elt)
+ | None, Some second ->
+ (Some second, Some elt)
+ | Some first, Some second ->
+ if elt > second then (Some first, Some elt)
+ else (Some first, Some second) )
+ | h :: t ->
+ highest_joltage_aux
+ ( match acc with
+ | None, None ->
+ (Some h, None)
+ | Some first, None ->
+ let second = Battery.of_int 0 in
+ swap_if_higher first second h
+ | Some first, Some second ->
+ swap_if_higher first second h
+ | _ ->
+ failwith "this branch should be impossible" )
+ t
+
+ and swap_if_higher first second considering =
+ assert (first >= second) ;
+ let c_gt_f = Battery.compare considering first = 1 in
+ let c_gt_s = Battery.compare considering second = 1 in
+ match (c_gt_f, c_gt_s) with
+ | true, true ->
+ (Some considering, None)
+ | false, true ->
+ (Some first, Some considering)
+ | true, false ->
+ failwith "[second] should never be greater than [first]"
+ | false, false ->
+ (Some first, Some second)
+
+ let of_string s =
+ s |> String.to_seq
+ |> Seq.map (fun c -> Char.escaped c |> int_of_string |> Battery.of_int)
+ |> List.of_seq
+
+ let of_list lst = lst
+
+ let to_list bb = bb
+
+ let pp bb =
+ "[ "
+ ^ ( bb |> List.map Battery.pp
+ |> List.fold_left
+ (fun acc b ->
+ if acc = "" then b ^ ";" else Printf.sprintf "%s %s;" acc b )
+ "" )
+ ^ " ]"
+end
diff --git a/ocaml/lib/year2025/day03/part01/battery_bank.mli b/ocaml/lib/year2025/day03/part01/battery_bank.mli
new file mode 100644
index 0000000..bffc4cb
--- /dev/null
+++ b/ocaml/lib/year2025/day03/part01/battery_bank.mli
@@ -0,0 +1,18 @@
+open Battery
+
+module BatteryBank : sig
+ type t
+
+ val highest_joltage : t -> int
+ (** [highest_joltage bb] is an integer created from the two highest
+ digits in [bb].
+ Raises: [Assert_failure] if [bb] is empty. *)
+
+ val of_string : string -> t
+
+ val of_list : Battery.t list -> t
+
+ val to_list : t -> Battery.t list
+
+ val pp : t -> string
+end
diff --git a/ocaml/lib/year2025/day03/part01/solver.ml b/ocaml/lib/year2025/day03/part01/solver.ml
new file mode 100644
index 0000000..4a06e83
--- /dev/null
+++ b/ocaml/lib/year2025/day03/part01/solver.ml
@@ -0,0 +1,23 @@
+open Battery_bank
+
+let err_empty_input = "input is empty"
+
+let to_lines s =
+ s |> String.split_on_char '\n' |> List.map String.trim
+ |> List.filter (fun l -> l <> "")
+
+let solve input () =
+ if input = "" then Error err_empty_input
+ else
+ let answer =
+ input |> to_lines |> List.to_seq
+ |> Seq.map BatteryBank.of_string
+ |> Seq.mapi (fun idx bb ->
+ let joltage = BatteryBank.highest_joltage bb in
+ Logs.debug (fun m ->
+ m "%d line=%s highest_joltage=%d" (idx + 1) (BatteryBank.pp bb)
+ joltage ) ;
+ joltage )
+ |> Seq.fold_left ( + ) 0
+ in
+ Ok answer
diff --git a/ocaml/lib/year2025/day03/part01/solver.mli b/ocaml/lib/year2025/day03/part01/solver.mli
new file mode 100644
index 0000000..31dd0cd
--- /dev/null
+++ b/ocaml/lib/year2025/day03/part01/solver.mli
@@ -0,0 +1,4 @@
+val solve : string -> unit -> (int, string) result
+(** [solve input ()] is the solution for the first part of the third day of advent of code 2025 for [input].
+ The solution for an empty input is [Error msg].
+ Requires: [input] to be a string. *)
diff --git a/ocaml/lib/year2025/day03/part02/battery.ml b/ocaml/lib/year2025/day03/part02/battery.ml
new file mode 100644
index 0000000..e8f0443
--- /dev/null
+++ b/ocaml/lib/year2025/day03/part02/battery.ml
@@ -0,0 +1,25 @@
+module Battery : sig
+ type t
+
+ val of_int : int -> t
+
+ val to_int : t -> int
+
+ val to_string : t -> string
+
+ val compare : t -> t -> int
+
+ val pp : t -> string
+end = struct
+ type t = int
+
+ let of_int i = i
+
+ let to_int b = b
+
+ let to_string = string_of_int
+
+ let compare = Int.compare
+
+ let pp = string_of_int
+end
diff --git a/ocaml/lib/year2025/day03/part02/battery.mli b/ocaml/lib/year2025/day03/part02/battery.mli
new file mode 100644
index 0000000..e00940b
--- /dev/null
+++ b/ocaml/lib/year2025/day03/part02/battery.mli
@@ -0,0 +1,13 @@
+module Battery : sig
+ type t
+
+ val of_int : int -> t
+
+ val to_int : t -> int
+
+ val to_string : t -> string
+
+ val compare : t -> t -> int
+
+ val pp : t -> string
+end
diff --git a/ocaml/lib/year2025/day03/part02/battery_bank.ml b/ocaml/lib/year2025/day03/part02/battery_bank.ml
new file mode 100644
index 0000000..d7dc213
--- /dev/null
+++ b/ocaml/lib/year2025/day03/part02/battery_bank.ml
@@ -0,0 +1,77 @@
+open Battery
+
+module BatteryBank = struct
+ type t = Battery.t list
+
+ type highest = {one: Battery.t option; two: Battery.t option}
+
+ let rec highest_joltage bb =
+ assert (List.length bb > 0) ;
+ match highest_joltage_aux {one= None; two= None} bb with
+ | {one= None; two= None} ->
+ 0
+ | {one= Some x; two= Some y} ->
+ int_of_string (Battery.to_string x ^ Battery.to_string y)
+ | _ ->
+ failwith "highest_joltage must be two digits"
+
+ and highest_joltage_aux acc = function
+ | [] ->
+ acc
+ | [elt] -> (
+ match acc with
+ | {one= None; two= None} ->
+ {one= Some (Battery.of_int 0); two= Some elt}
+ | {one= Some first; two= None} ->
+ {one= Some first; two= Some elt}
+ | {one= None; two= Some second} ->
+ {one= Some second; two= Some elt}
+ | {one= Some first; two= Some second} ->
+ if elt > second then {one= Some first; two= Some elt}
+ else {one= Some first; two= Some second} )
+ | h :: t ->
+ highest_joltage_aux
+ ( match acc with
+ | {one= None; two= None} ->
+ {one= Some h; two= None}
+ | {one= Some first; two= None} ->
+ let second = Battery.of_int 0 in
+ swap_if_higher first second h
+ | {one= Some first; two= Some second} ->
+ swap_if_higher first second h
+ | _ ->
+ failwith "this branch should be impossible" )
+ t
+
+ and swap_if_higher first second considering =
+ assert (first >= second) ;
+ let c_gt_f = Battery.compare considering first = 1 in
+ let c_gt_s = Battery.compare considering second = 1 in
+ match (c_gt_f, c_gt_s) with
+ | true, true ->
+ {one= Some considering; two= None}
+ | false, true ->
+ {one= Some first; two= Some considering}
+ | true, false ->
+ failwith "[second] should never be greater than [first]"
+ | false, false ->
+ {one= Some first; two= Some second}
+
+ let of_string s =
+ s |> String.to_seq
+ |> Seq.map (fun c -> Char.escaped c |> int_of_string |> Battery.of_int)
+ |> List.of_seq
+
+ let of_list lst = lst
+
+ let to_list bb = bb
+
+ let pp bb =
+ "[ "
+ ^ ( bb |> List.map Battery.pp
+ |> List.fold_left
+ (fun acc b ->
+ if acc = "" then b ^ ";" else Printf.sprintf "%s %s;" acc b )
+ "" )
+ ^ " ]"
+end
diff --git a/ocaml/lib/year2025/day03/part02/battery_bank.mli b/ocaml/lib/year2025/day03/part02/battery_bank.mli
new file mode 100644
index 0000000..bffc4cb
--- /dev/null
+++ b/ocaml/lib/year2025/day03/part02/battery_bank.mli
@@ -0,0 +1,18 @@
+open Battery
+
+module BatteryBank : sig
+ type t
+
+ val highest_joltage : t -> int
+ (** [highest_joltage bb] is an integer created from the two highest
+ digits in [bb].
+ Raises: [Assert_failure] if [bb] is empty. *)
+
+ val of_string : string -> t
+
+ val of_list : Battery.t list -> t
+
+ val to_list : t -> Battery.t list
+
+ val pp : t -> string
+end
diff --git a/ocaml/lib/year2025/day03/part02/solver.ml b/ocaml/lib/year2025/day03/part02/solver.ml
new file mode 100644
index 0000000..4a06e83
--- /dev/null
+++ b/ocaml/lib/year2025/day03/part02/solver.ml
@@ -0,0 +1,23 @@
+open Battery_bank
+
+let err_empty_input = "input is empty"
+
+let to_lines s =
+ s |> String.split_on_char '\n' |> List.map String.trim
+ |> List.filter (fun l -> l <> "")
+
+let solve input () =
+ if input = "" then Error err_empty_input
+ else
+ let answer =
+ input |> to_lines |> List.to_seq
+ |> Seq.map BatteryBank.of_string
+ |> Seq.mapi (fun idx bb ->
+ let joltage = BatteryBank.highest_joltage bb in
+ Logs.debug (fun m ->
+ m "%d line=%s highest_joltage=%d" (idx + 1) (BatteryBank.pp bb)
+ joltage ) ;
+ joltage )
+ |> Seq.fold_left ( + ) 0
+ in
+ Ok answer
diff --git a/ocaml/lib/year2025/day03/part02/solver.mli b/ocaml/lib/year2025/day03/part02/solver.mli
new file mode 100644
index 0000000..93d3b98
--- /dev/null
+++ b/ocaml/lib/year2025/day03/part02/solver.mli
@@ -0,0 +1,4 @@
+val solve : string -> unit -> (int, string) result
+(** [solve input ()] is the solution for the second part of the third day of advent of code 2025 for [input].
+ The solution for an empty input is [Error msg].
+ Requires: [input] to be a string. *)
diff --git a/ocaml/lib/year2025/day04/part01/solver.ml b/ocaml/lib/year2025/day04/part01/solver.ml
new file mode 100644
index 0000000..7cc54f6
--- /dev/null
+++ b/ocaml/lib/year2025/day04/part01/solver.ml
@@ -0,0 +1,48 @@
+let err_empty_input = "input is empty"
+
+let rec solve input () =
+ if input = "" then Error err_empty_input
+ else
+ let matrix = Shared.Matrix.M.of_string input in
+ let count_paper lst = List.length @@ List.filter (fun c -> c <> ".") lst in
+ let accessible_rolls_of_paper = ref 0 in
+ for y = 0 to Array.length matrix - 1 do
+ for x = 0 to Array.length matrix.(y) - 1 do
+ if matrix.(y).(x) = "." then ()
+ else
+ let neighbour_coords =
+ Shared.Matrix.M.get_valid_neighbours matrix x y
+ in
+ let neighbours =
+ List.map
+ (fun (x', y') -> Shared.Matrix.M.get matrix x' y')
+ neighbour_coords
+ in
+ let paper_count = count_paper neighbours in
+ let is_accessible = paper_count < 4 in
+ if is_accessible then
+ accessible_rolls_of_paper := !accessible_rolls_of_paper + 1 ;
+ print_iteration_debug_msg x y neighbour_coords neighbours paper_count
+ is_accessible
+ done
+ done ;
+ Ok !accessible_rolls_of_paper
+
+and print_iteration_debug_msg x y neighbour_coords neighbours paper_count
+ is_accessible =
+ Logs.debug (fun m ->
+ m
+ "coords=(%d,%d) neighbour_coords=[%s] neighbours=[%s] paper=%d \
+ is_accessible=%B"
+ x y
+ (List.fold_left
+ (fun acc (x, y) ->
+ if acc = "" then
+ " (" ^ string_of_int x ^ "," ^ string_of_int y ^ "); "
+ else acc ^ "(" ^ string_of_int x ^ "," ^ string_of_int y ^ "); " )
+ "" neighbour_coords )
+ (List.fold_left
+ (fun acc neighbour ->
+ if acc = "" then " " ^ neighbour ^ "; " else acc ^ neighbour ^ "; " )
+ "" neighbours )
+ paper_count is_accessible )
diff --git a/ocaml/lib/year2025/day04/part01/solver.mli b/ocaml/lib/year2025/day04/part01/solver.mli
new file mode 100644
index 0000000..873acff
--- /dev/null
+++ b/ocaml/lib/year2025/day04/part01/solver.mli
@@ -0,0 +1,4 @@
+val solve : string -> unit -> (int, string) result
+(** [solve input ()] is the solution for the first part of the fourth day of advent of code 2025 for [input].
+ The solution for an empty input is [Error msg].
+ Requires: [input] to be a string. *)
diff --git a/ocaml/lib/year2025/day04/part02/solver.ml b/ocaml/lib/year2025/day04/part02/solver.ml
new file mode 100644
index 0000000..e92c982
--- /dev/null
+++ b/ocaml/lib/year2025/day04/part02/solver.ml
@@ -0,0 +1,66 @@
+let err_empty_input = "input is empty"
+
+let rec count_accessible_rolls matrix =
+ let count_paper lst = List.length @@ List.filter (fun c -> c = "@") lst in
+ let accessible_rolls_of_paper = ref [] in
+ let no_change = ref false in
+ while not !no_change do
+ let accessible_rolls_of_paper' = !accessible_rolls_of_paper in
+ count_accessible_rolls_aux matrix count_paper accessible_rolls_of_paper ;
+ if
+ List.length accessible_rolls_of_paper'
+ = List.length !accessible_rolls_of_paper
+ then no_change := true
+ done ;
+ List.length !accessible_rolls_of_paper
+
+and count_accessible_rolls_aux matrix count_paper accessible_rolls_of_paper =
+ for y = 0 to Array.length matrix - 1 do
+ for x = 0 to Array.length matrix.(y) - 1 do
+ if matrix.(y).(x) = "." then ()
+ else
+ let neighbour_coords =
+ Shared.Matrix.M.get_valid_neighbours matrix x y
+ in
+ let neighbours =
+ List.map
+ (fun (nx, ny) -> Shared.Matrix.M.get matrix nx ny)
+ neighbour_coords
+ in
+ let paper_count = count_paper neighbours in
+ let is_accessible = paper_count < 4 in
+ if is_accessible then
+ accessible_rolls_of_paper := (x, y) :: !accessible_rolls_of_paper ;
+ print_iteration_debug_msg x y neighbour_coords neighbours paper_count
+ is_accessible
+ done
+ done ;
+ List.iter
+ (fun (x', y') -> Shared.Matrix.M.set matrix x' y' ".")
+ !accessible_rolls_of_paper
+
+and print_iteration_debug_msg x y neighbour_coords neighbours paper_count
+ is_accessible =
+ Logs.debug (fun m ->
+ m
+ "coords=(%d,%d) neighbour_coords=[%s] neighbours=[%s] paper=%d \
+ is_accessible=%B"
+ x y
+ (List.fold_left
+ (fun acc (x, y) ->
+ if acc = "" then
+ " (" ^ string_of_int x ^ "," ^ string_of_int y ^ "); "
+ else acc ^ "(" ^ string_of_int x ^ "," ^ string_of_int y ^ "); " )
+ "" neighbour_coords )
+ (List.fold_left
+ (fun acc neighbour ->
+ if acc = "" then " " ^ neighbour ^ "; " else acc ^ neighbour ^ "; " )
+ "" neighbours )
+ paper_count is_accessible )
+
+let solve input () =
+ if input = "" then Error err_empty_input
+ else
+ let matrix = Shared.Matrix.M.of_string input in
+ let accessible_rolls_of_paper = count_accessible_rolls matrix in
+ Ok accessible_rolls_of_paper
diff --git a/ocaml/lib/year2025/day04/part02/solver.mli b/ocaml/lib/year2025/day04/part02/solver.mli
new file mode 100644
index 0000000..388ec56
--- /dev/null
+++ b/ocaml/lib/year2025/day04/part02/solver.mli
@@ -0,0 +1,4 @@
+val solve : string -> unit -> (int, string) result
+(** [solve input ()] is the solution for the second part of the fourth day of advent of code 2025 for [input].
+ The solution for an empty input is [Error msg].
+ Requires: [input] to be a string. *)
diff --git a/ocaml/lib/year2025/day06/part01/solver.ml b/ocaml/lib/year2025/day06/part01/solver.ml
new file mode 100644
index 0000000..d992ac2
--- /dev/null
+++ b/ocaml/lib/year2025/day06/part01/solver.ml
@@ -0,0 +1,50 @@
+let err_empty_input = "input is empty"
+
+let log_matrix matrix =
+ let open Shared.Matrix in
+ Logs.debug (fun m -> m "%s" (M.of_list matrix |> M.pp (fun elt -> elt)))
+
+let log_list lst =
+ Logs.debug (fun m ->
+ m "[%s]" (List.fold_left (fun acc i -> acc ^ i ^ "; ") " " lst) )
+
+type operation = Mult | Add
+
+let solve input () =
+ if input = "" then Error err_empty_input
+ else
+ let input' = Shared.Parser.columns_of_string input in
+ assert (input' <> []) ;
+ log_matrix input' ;
+ let nums =
+ List.map (List.filter (fun i -> i <> "*" && i <> "+")) input'
+ |> List.map
+ @@ List.map (fun n ->
+ Logs.debug (fun m -> m "peeking: %s" n) ;
+ int_of_string n )
+ and ops =
+ List.map (List.filter (fun i -> i = "*" || i = "+")) input'
+ |> List.concat
+ |> List.map (fun op ->
+ match op with
+ | "*" ->
+ Mult
+ | "+" ->
+ Add
+ | _ ->
+ failwith "case not covered" )
+ in
+ let answer =
+ List.map2
+ (fun op num_lst ->
+ match op with
+ | Add ->
+ List.fold_left (fun acc i -> acc + i) 0 num_lst
+ | Mult ->
+ List.fold_left (fun acc i -> acc * i) 1 num_lst )
+ ops nums
+ |> fun lst ->
+ log_list @@ List.map string_of_int lst ;
+ lst |> List.fold_left ( + ) 0
+ in
+ Ok answer
diff --git a/ocaml/lib/year2025/day06/part01/solver.mli b/ocaml/lib/year2025/day06/part01/solver.mli
new file mode 100644
index 0000000..b3abc1e
--- /dev/null
+++ b/ocaml/lib/year2025/day06/part01/solver.mli
@@ -0,0 +1,4 @@
+val solve : string -> unit -> (int, string) result
+(** [solve input ()] is the solution for the first part of the sixth day of advent of code 2025 for [input].
+ The solution for an empty input is [Error msg].
+ Requires: [input] to be a string. *)
diff --git a/ocaml/lib/year2025/day06/part02/solver.ml b/ocaml/lib/year2025/day06/part02/solver.ml
new file mode 100644
index 0000000..d992ac2
--- /dev/null
+++ b/ocaml/lib/year2025/day06/part02/solver.ml
@@ -0,0 +1,50 @@
+let err_empty_input = "input is empty"
+
+let log_matrix matrix =
+ let open Shared.Matrix in
+ Logs.debug (fun m -> m "%s" (M.of_list matrix |> M.pp (fun elt -> elt)))
+
+let log_list lst =
+ Logs.debug (fun m ->
+ m "[%s]" (List.fold_left (fun acc i -> acc ^ i ^ "; ") " " lst) )
+
+type operation = Mult | Add
+
+let solve input () =
+ if input = "" then Error err_empty_input
+ else
+ let input' = Shared.Parser.columns_of_string input in
+ assert (input' <> []) ;
+ log_matrix input' ;
+ let nums =
+ List.map (List.filter (fun i -> i <> "*" && i <> "+")) input'
+ |> List.map
+ @@ List.map (fun n ->
+ Logs.debug (fun m -> m "peeking: %s" n) ;
+ int_of_string n )
+ and ops =
+ List.map (List.filter (fun i -> i = "*" || i = "+")) input'
+ |> List.concat
+ |> List.map (fun op ->
+ match op with
+ | "*" ->
+ Mult
+ | "+" ->
+ Add
+ | _ ->
+ failwith "case not covered" )
+ in
+ let answer =
+ List.map2
+ (fun op num_lst ->
+ match op with
+ | Add ->
+ List.fold_left (fun acc i -> acc + i) 0 num_lst
+ | Mult ->
+ List.fold_left (fun acc i -> acc * i) 1 num_lst )
+ ops nums
+ |> fun lst ->
+ log_list @@ List.map string_of_int lst ;
+ lst |> List.fold_left ( + ) 0
+ in
+ Ok answer
diff --git a/ocaml/lib/year2025/day06/part02/solver.mli b/ocaml/lib/year2025/day06/part02/solver.mli
new file mode 100644
index 0000000..49444de
--- /dev/null
+++ b/ocaml/lib/year2025/day06/part02/solver.mli
@@ -0,0 +1,4 @@
+val solve : string -> unit -> (int, string) result
+(** [solve input ()] is the solution for the second part of the sixth day of advent of code 2025 for [input].
+ The solution for an empty input is [Error msg].
+ Requires: [input] to be a string. *)
diff --git a/ocaml/lib/year2025/dune b/ocaml/lib/year2025/dune
new file mode 100644
index 0000000..99d5c0a
--- /dev/null
+++ b/ocaml/lib/year2025/dune
@@ -0,0 +1,16 @@
+(include_subdirs qualified)
+
+(library
+ (name year2025)
+ (libraries
+ shared
+ fmt
+ logs)
+ (inline_tests)
+ (preprocess
+ (pps
+ ppx_deriving.show
+ ppx_deriving.eq
+ ppx_deriving.ord
+ ppx_expect
+ ppx_inline_test)))