diff options
| author | DJ O'Leary <dijitol@proton.me> | 2026-06-27 00:11:43 +0200 |
|---|---|---|
| committer | DJ O'Leary <dijitol@proton.me> | 2026-06-27 00:11:43 +0200 |
| commit | 976048a85403fbb0751c34fa42703a3c47f6e918 (patch) | |
| tree | 46e321d8120c45e318ead66f60165b0b854c85a1 /ocaml/lib/year2025 | |
| parent | 6d8fb621d1e93b83786890d6d9cb6f0d8defa8ae (diff) | |
refactor!: change repo directory structure for ocaml
Diffstat (limited to 'ocaml/lib/year2025')
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))) |
