summaryrefslogtreecommitdiff
path: root/2024/ocaml
diff options
context:
space:
mode:
authorDJ O'Leary <dijitol@proton.me>2025-11-29 20:23:02 +0100
committerDJ O'Leary <dijitol@proton.me>2025-11-29 20:23:02 +0100
commit4c3a50166bf0f78ce89459c352cc1c6536a8b96d (patch)
tree02e4b66a1435680ef1e92bf82576747b27f16447 /2024/ocaml
parentbe2f8528b3829f8263507da301812d852297c168 (diff)
chore: make language folders snake_case
Diffstat (limited to '2024/ocaml')
-rw-r--r--2024/ocaml/.envrc1
-rw-r--r--2024/ocaml/.gitignore29
-rw-r--r--2024/ocaml/.ocamlformat2
-rw-r--r--2024/ocaml/AoC_2024.opam32
-rw-r--r--2024/ocaml/README.md21
-rw-r--r--2024/ocaml/Taskfile.yml56
-rw-r--r--2024/ocaml/bin/dune5
-rw-r--r--2024/ocaml/bin/main.ml46
-rw-r--r--2024/ocaml/dune-project28
-rw-r--r--2024/ocaml/flake.lock27
-rw-r--r--2024/ocaml/flake.nix44
-rw-r--r--2024/ocaml/lib/day_01/part_01.ml40
-rw-r--r--2024/ocaml/lib/day_01/part_02.ml44
-rw-r--r--2024/ocaml/lib/day_02/part_01.ml42
-rw-r--r--2024/ocaml/lib/day_03/part_01.ml33
-rw-r--r--2024/ocaml/lib/day_04/part_01.ml85
-rw-r--r--2024/ocaml/lib/day_05/part_01.ml62
-rw-r--r--2024/ocaml/lib/dune5
-rw-r--r--2024/ocaml/lib/file/read_input.ml11
-rw-r--r--2024/ocaml/lib/grid/cell.ml18
-rw-r--r--2024/ocaml/lib/grid/cell.mli18
-rw-r--r--2024/ocaml/lib/grid/grid.ml14
-rw-r--r--2024/ocaml/lib/grid/grid.mli6
-rw-r--r--2024/ocaml/test/day_01_part_01.ml27
-rw-r--r--2024/ocaml/test/day_01_part_02.ml17
-rw-r--r--2024/ocaml/test/day_02_part_01.ml38
-rw-r--r--2024/ocaml/test/day_03_part_01.ml29
-rw-r--r--2024/ocaml/test/day_04_part_01.ml151
-rw-r--r--2024/ocaml/test/day_05_part_01.ml4
-rw-r--r--2024/ocaml/test/dune10
-rw-r--r--2024/ocaml/test/grid.ml5
31 files changed, 950 insertions, 0 deletions
diff --git a/2024/ocaml/.envrc b/2024/ocaml/.envrc
new file mode 100644
index 0000000..3550a30
--- /dev/null
+++ b/2024/ocaml/.envrc
@@ -0,0 +1 @@
+use flake
diff --git a/2024/ocaml/.gitignore b/2024/ocaml/.gitignore
new file mode 100644
index 0000000..a18e084
--- /dev/null
+++ b/2024/ocaml/.gitignore
@@ -0,0 +1,29 @@
+*.annot
+*.cmo
+*.cma
+*.cmi
+*.a
+*.o
+*.cmx
+*.cmxs
+*.cmxa
+
+# ocamlbuild working directory
+_build/
+
+# ocamlbuild targets
+*.byte
+*.native
+
+# oasis generated files
+setup.data
+setup.log
+
+# Merlin configuring file for Vim and Emacs
+.merlin
+
+# Dune generated files
+*.install
+
+# Local OPAM switch
+_opam/
diff --git a/2024/ocaml/.ocamlformat b/2024/ocaml/.ocamlformat
new file mode 100644
index 0000000..912bea4
--- /dev/null
+++ b/2024/ocaml/.ocamlformat
@@ -0,0 +1,2 @@
+profile = default
+version = 0.27.0
diff --git a/2024/ocaml/AoC_2024.opam b/2024/ocaml/AoC_2024.opam
new file mode 100644
index 0000000..6be14e1
--- /dev/null
+++ b/2024/ocaml/AoC_2024.opam
@@ -0,0 +1,32 @@
+# This file is generated by dune, edit dune-project instead
+opam-version: "2.0"
+synopsis: "Advent of Code 2024"
+description: "Solutions to the Advent of Code puzzles for 2024"
+maintainer: ["D.J. O'Leary"]
+authors: ["D.J. O'Leary"]
+license: "Unlicense"
+tags: ["aoc2024" "Advent of Code 2024"]
+homepage: "https://github.com/DJOLEARY/Advent_of_Code"
+doc:
+ "https://github.com/DJOLEARY/Advent_of_Code/blob/main/2024/OCaml/README.md"
+bug-reports: "https://github.com/DJOLEARY/Advent_of_Code/issues"
+depends: [
+ "ocaml"
+ "dune" {>= "3.16"}
+ "odoc" {with-doc}
+]
+build: [
+ ["dune" "subst"] {dev}
+ [
+ "dune"
+ "build"
+ "-p"
+ name
+ "-j"
+ jobs
+ "@install"
+ "@runtest" {with-test}
+ "@doc" {with-doc}
+ ]
+]
+dev-repo: "git+https://github.com/DJOLEARY/Advent_of_Code.git"
diff --git a/2024/ocaml/README.md b/2024/ocaml/README.md
new file mode 100644
index 0000000..caa88fe
--- /dev/null
+++ b/2024/ocaml/README.md
@@ -0,0 +1,21 @@
+# OCaml 2024
+
+## Getting Started
+
+### How to Build
+
+```sh
+task build
+```
+
+### How to Run
+
+```sh
+task run
+```
+
+### How to Test
+
+```sh
+task test
+```
diff --git a/2024/ocaml/Taskfile.yml b/2024/ocaml/Taskfile.yml
new file mode 100644
index 0000000..1d8a89e
--- /dev/null
+++ b/2024/ocaml/Taskfile.yml
@@ -0,0 +1,56 @@
+# https://taskfile.dev
+
+version: "3"
+vars:
+ PROJECT_NAME: AoC_2024
+tasks:
+ default:
+ desc: Prints this message
+ cmds:
+ - cmd: task --list-all
+ build:doc:
+ desc: Builds documentation using ocamldoc
+ preconditions:
+ - which ocamldoc
+ cmds:
+ - cmd: ocamldoc -html -d docs bin/main.ml
+ silent: true
+ build:
+ desc: Builds the project
+ preconditions:
+ - which dune
+ cmds:
+ - cmd: dune build
+ test:
+ desc: Runs the tests in the 'tests' directory
+ ignore_error: true
+ env:
+ OUNIT_CI: true
+ preconditions:
+ - which dune
+ deps:
+ - task: build
+ cmds:
+ - cmd: dune test {{.CLI_ARGS}}
+ test:watch:
+ desc: Continuously runs the tests in the 'tests' directory
+ ignore_error: true
+ sources:
+ - lib/**/*.ml
+ - test/**/*.ml
+ env:
+ OUNIT_CI: true
+ preconditions:
+ - which dune
+ deps:
+ - task: build
+ cmds:
+ - cmd: dune test --watch {{.CLI_ARGS}}
+ run:
+ desc: Runs the project
+ preconditions:
+ - which dune
+ deps:
+ - task: build
+ cmds:
+ - cmd: dune exec {{.PROJECT_NAME}}
diff --git a/2024/ocaml/bin/dune b/2024/ocaml/bin/dune
new file mode 100644
index 0000000..ee36b46
--- /dev/null
+++ b/2024/ocaml/bin/dune
@@ -0,0 +1,5 @@
+(executable
+ (public_name AoC_2024)
+ (name main)
+ (modes byte exe)
+ (libraries AoC_2024))
diff --git a/2024/ocaml/bin/main.ml b/2024/ocaml/bin/main.ml
new file mode 100644
index 0000000..a76e046
--- /dev/null
+++ b/2024/ocaml/bin/main.ml
@@ -0,0 +1,46 @@
+open AoC_2024
+
+let day_to_path day =
+ let string_day = string_of_int day in
+ let formatted_day = if day < 10 then "0" ^ string_day else string_day in
+ "../_input/day_" ^ formatted_day ^ ".txt"
+
+let print_solution day_num part_num answer =
+ print_string "D";
+ print_int day_num;
+ print_string ":P";
+ print_int part_num;
+ print_string " -> ";
+ print_int answer;
+ print_newline ()
+
+let day_1 =
+ let lines = File.Read_input.lines_from_file (day_to_path 1) in
+ let left, right = Day_01.Part_01.lines_to_lists lines in
+ let part_01 = Day_01.Part_01.solve left right in
+ print_solution 1 1 part_01;
+ let part_02 = Day_01.Part_02.solve left right in
+ print_solution 1 2 part_02
+
+let day_2 =
+ let lines = File.Read_input.lines_from_file (day_to_path 2) in
+ let reports = Day_02.Part_01.lines_to_reports lines in
+ let part_01 = Day_02.Part_01.solve reports in
+ print_solution 2 1 part_01
+
+let day_3 =
+ let lines = File.Read_input.lines_from_file (day_to_path 3) in
+ let line = List.fold_left (fun acc line -> acc ^ line) "" lines in
+ let part_01 = Day_03.Part_01.solve line in
+ print_solution 3 1 part_01
+
+let day_4 =
+ let lines = File.Read_input.lines_from_file (day_to_path 4) in
+ let part_01 = Day_04.Part_01.solve lines in
+ print_solution 4 1 part_01
+
+let _ =
+ day_1;
+ day_2;
+ day_3;
+ day_4
diff --git a/2024/ocaml/dune-project b/2024/ocaml/dune-project
new file mode 100644
index 0000000..3f3a7bc
--- /dev/null
+++ b/2024/ocaml/dune-project
@@ -0,0 +1,28 @@
+(lang dune 3.16)
+
+(name AoC_2024)
+
+(generate_opam_files true)
+
+(source
+ (github DJOLEARY/Advent_of_Code))
+
+(authors "D.J. O'Leary")
+
+(maintainers "D.J. O'Leary")
+
+(license Unlicense)
+
+(documentation https://github.com/DJOLEARY/Advent_of_Code/blob/main/2024/OCaml/README.md)
+
+(map_workspace_root false)
+
+(package
+ (name AoC_2024)
+ (synopsis "Advent of Code 2024")
+ (description "Solutions to the Advent of Code puzzles for 2024")
+ (depends ocaml dune)
+ (tags
+ (aoc2024 "Advent of Code 2024")))
+
+; See the complete stanza docs at https://dune.readthedocs.io/en/stable/reference/dune-project/index.html
diff --git a/2024/ocaml/flake.lock b/2024/ocaml/flake.lock
new file mode 100644
index 0000000..b7695d9
--- /dev/null
+++ b/2024/ocaml/flake.lock
@@ -0,0 +1,27 @@
+{
+ "nodes": {
+ "nixpkgs": {
+ "locked": {
+ "lastModified": 1741010256,
+ "narHash": "sha256-WZNlK/KX7Sni0RyqLSqLPbK8k08Kq7H7RijPJbq9KHM=",
+ "owner": "nixos",
+ "repo": "nixpkgs",
+ "rev": "ba487dbc9d04e0634c64e3b1f0d25839a0a68246",
+ "type": "github"
+ },
+ "original": {
+ "owner": "nixos",
+ "ref": "nixos-unstable",
+ "repo": "nixpkgs",
+ "type": "github"
+ }
+ },
+ "root": {
+ "inputs": {
+ "nixpkgs": "nixpkgs"
+ }
+ }
+ },
+ "root": "root",
+ "version": 7
+}
diff --git a/2024/ocaml/flake.nix b/2024/ocaml/flake.nix
new file mode 100644
index 0000000..74a5dc6
--- /dev/null
+++ b/2024/ocaml/flake.nix
@@ -0,0 +1,44 @@
+{
+ description = "Advent of Code 2024: OCaml";
+
+ inputs = {
+ nixpkgs.url = "github:nixos/nixpkgs?ref=nixos-unstable";
+ };
+
+ outputs =
+ { nixpkgs, ... }:
+ let
+ pkgs = nixpkgs.legacyPackages.x86_64-linux;
+ in
+ {
+ devShells.x86_64-linux.default =
+ let
+ ocamlPackages = pkgs.ocaml-ng.ocamlPackages_5_2;
+ in
+ pkgs.mkShell {
+ # build tools
+ nativeBuildInputs = with ocamlPackages; [
+ pkgs.opam
+ ocaml
+ findlib
+ dune_3
+ ];
+ # dependencies
+ buildInputs = with ocamlPackages; [
+ bisect_ppx
+ findlib
+ menhir
+ ocaml-lsp
+ earlybird
+ ocamlformat
+ ocamlgraph
+ odoc
+ ounit2
+ re2
+ utop
+
+ pkgs.gh # GitHub CLI
+ ];
+ };
+ };
+}
diff --git a/2024/ocaml/lib/day_01/part_01.ml b/2024/ocaml/lib/day_01/part_01.ml
new file mode 100644
index 0000000..100b12a
--- /dev/null
+++ b/2024/ocaml/lib/day_01/part_01.ml
@@ -0,0 +1,40 @@
+(* Day 1: Part 1 *)
+
+let calculate_distance x y = Stdlib.abs (x - y)
+
+(** Require: left and right to be the same length *)
+let solve left right =
+ let sorted_left = List.sort Stdlib.compare left in
+ let sorted_right = List.sort Stdlib.compare right in
+ let distances = List.map2 calculate_distance sorted_left sorted_right in
+ List.fold_left ( + ) 0 distances
+
+let left line =
+ let pos = 0 in
+ let len = String.index line ' ' in
+ String.sub line pos len
+
+let right line =
+ let pos = String.rindex line ' ' + 1 in
+ let len = String.length line - pos in
+ String.sub line pos len
+
+let line_to_tuple line = (left line, right line)
+
+let lines_to_lists lines =
+ let tuples = List.map line_to_tuple lines in
+ let left =
+ List.map
+ (fun t ->
+ let l, _ = t in
+ int_of_string l)
+ tuples
+ in
+ let right =
+ List.map
+ (fun t ->
+ let _, r = t in
+ int_of_string r)
+ tuples
+ in
+ (left, right)
diff --git a/2024/ocaml/lib/day_01/part_02.ml b/2024/ocaml/lib/day_01/part_02.ml
new file mode 100644
index 0000000..25c8b7d
--- /dev/null
+++ b/2024/ocaml/lib/day_01/part_02.ml
@@ -0,0 +1,44 @@
+(* Day 1: Part 2 *)
+
+let rec count_occurrances_help element acc lst =
+ match lst with
+ | [] -> acc
+ | h :: t ->
+ if h = element then count_occurrances_help element (acc + 1) t
+ else count_occurrances_help element acc t
+
+let count_occurrances element lst = count_occurrances_help element 0 lst
+
+let solve left right =
+ List.map (fun x -> x * count_occurrances x right) left
+ |> List.fold_left ( + ) 0
+
+let left line =
+ let pos = 0 in
+ let len = String.index line ' ' in
+ String.sub line pos len
+
+let right line =
+ let pos = String.rindex line ' ' + 1 in
+ let len = String.length line - pos in
+ String.sub line pos len
+
+let line_to_tuple line = (left line, right line)
+
+let lines_to_lists lines =
+ let tuples = List.map line_to_tuple lines in
+ let left =
+ List.map
+ (fun t ->
+ let l, _ = t in
+ int_of_string l)
+ tuples
+ in
+ let right =
+ List.map
+ (fun t ->
+ let _, r = t in
+ int_of_string r)
+ tuples
+ in
+ (left, right)
diff --git a/2024/ocaml/lib/day_02/part_01.ml b/2024/ocaml/lib/day_02/part_01.ml
new file mode 100644
index 0000000..32d7ab8
--- /dev/null
+++ b/2024/ocaml/lib/day_02/part_01.ml
@@ -0,0 +1,42 @@
+(* Day 2: Part 1 *)
+
+type level = int
+type reports = level list list
+
+let rec compare_report_levels_helper comparison prev report =
+ match report with
+ | [] -> true
+ | h :: t -> comparison prev h && compare_report_levels_helper comparison h t
+
+let compare_report_levels comparison report =
+ match report with
+ | [] -> true
+ | h :: t -> compare_report_levels_helper comparison h t
+
+let all_increasing report = compare_report_levels ( < ) report
+let all_decreasing report = compare_report_levels ( > ) report
+
+let at_least_one report =
+ compare_report_levels (fun prev curr -> prev <> curr) report
+
+let at_most_three report =
+ compare_report_levels (fun prev curr -> abs (prev - curr) <= 3) report
+
+let check_report_safety report =
+ if not (all_increasing report || all_decreasing report) then false
+ else if not (at_least_one report && at_most_three report) then false
+ else true
+
+let rec count_safe_reports acc lst =
+ match lst with
+ | [] -> acc
+ | h :: t ->
+ if h then count_safe_reports (acc + 1) t else count_safe_reports acc t
+
+let solve reports =
+ reports |> List.map check_report_safety |> count_safe_reports 0
+
+let line_to_report line =
+ line |> String.split_on_char ' ' |> List.map int_of_string
+
+let lines_to_reports lines = List.map line_to_report lines
diff --git a/2024/ocaml/lib/day_03/part_01.ml b/2024/ocaml/lib/day_03/part_01.ml
new file mode 100644
index 0000000..c12b68a
--- /dev/null
+++ b/2024/ocaml/lib/day_03/part_01.ml
@@ -0,0 +1,33 @@
+(* Day 3: Part 1 *)
+
+let find_mul text =
+ let regex_or_err = Re2.create "mul\\(([0-9]{1,3}),([0-9]{1,3})\\)" in
+ match regex_or_err with
+ | Ok regex -> (
+ let matches = Re2.get_matches regex text in
+ match matches with Ok matches -> matches | Error _ -> [])
+ | Error _ -> []
+
+let extract_pairs (matches : Re2.Match.t list) : (int * int) list =
+ List.map
+ (fun x ->
+ let left = Re2.Match.get x ~sub:(`Index 1) in
+ match left with
+ | None -> (0, 0)
+ | Some left -> (
+ let left_num = int_of_string left in
+ let right = Re2.Match.get x ~sub:(`Index 2) in
+ match right with
+ | None -> (0, 0)
+ | Some right ->
+ let right_num = int_of_string right in
+ (left_num, right_num)))
+ matches
+
+let solve input =
+ input |> find_mul |> extract_pairs
+ |> List.fold_left
+ (fun acc pair ->
+ let left, right = pair in
+ acc + (left * right))
+ 0
diff --git a/2024/ocaml/lib/day_04/part_01.ml b/2024/ocaml/lib/day_04/part_01.ml
new file mode 100644
index 0000000..acb3e3b
--- /dev/null
+++ b/2024/ocaml/lib/day_04/part_01.ml
@@ -0,0 +1,85 @@
+(* Day 4: Part 1 *)
+
+type character = { index : int; character : char }
+type direction = N | NE | E | SE | S | SW | W | NW
+
+let all_directions = [ N; NE; E; SE; S; SW; W; NW ]
+let needle = "XMAS"
+
+(** [calculate_row_offset lines] is the offset used to move between the rows of
+ the concatenated variant of [lines] as though it were a grid. Requires:
+ values of [lines] all have the same length *)
+let calculate_row_offset lines =
+ match lines with [] -> 0 | h :: _ -> String.length h
+
+let join_lines lines =
+ let concat_trim acc line = acc ^ String.trim line in
+ List.fold_left concat_trim "" lines
+
+let explode s = List.init (String.length s) (String.get s)
+let is_in_bounds length index = index >= 0 && index < length
+
+let get_at_index line index =
+ let length = String.length line in
+ if not (is_in_bounds length index) then "."
+ else
+ let character = String.get line index in
+ Char.escaped character
+
+let get_direction_index row_offset direction index =
+ match direction with
+ | N -> index - row_offset
+ | NE -> index - row_offset + 1
+ | E -> index + 1
+ | SE -> index + row_offset + 1
+ | S -> index + row_offset
+ | SW -> index + row_offset - 1
+ | W -> index - 1
+ | NW -> index - row_offset - 1
+
+let get_direction_string get_char get_offset_index index =
+ let first = get_char index in
+ let second_index = get_offset_index index in
+ let second = get_char second_index in
+ let third_index = get_offset_index second_index in
+ let third = get_char third_index in
+ let fourth_index = get_offset_index third_index in
+ let fourth = get_char fourth_index in
+ first ^ second ^ third ^ fourth
+
+let check_direction get_from_line get_offset_index index direction =
+ get_direction_string get_from_line (get_offset_index direction) index = needle
+
+let calculate_num_of_matches line row_offset character =
+ let get_from_line_at_index = get_at_index line in
+ let get_offset_direction_index = get_direction_index row_offset in
+ let check_direction_from_index =
+ check_direction get_from_line_at_index get_offset_direction_index
+ character.index
+ in
+ all_directions
+ |> List.map check_direction_from_index
+ |> List.map (fun x -> match x with true -> 1 | false -> 0)
+ |> List.fold_left ( + ) 0
+
+(* Tried:
+ 2544 -> too high
+ 1272 (previous halved) -> too low *)
+let solve lines =
+ let row_offset = calculate_row_offset lines in
+ let line = join_lines lines in
+ let chars = explode line in
+ let characters = List.mapi (fun i c -> { index = i; character = c }) chars in
+ let xs =
+ List.filter
+ (fun c -> match c.character with 'X' -> true | _ -> false)
+ characters
+ in
+ let calculate_num_of_surrounding_matches_in_line =
+ calculate_num_of_matches line row_offset
+ in
+ let num_of_matches =
+ List.map calculate_num_of_surrounding_matches_in_line xs
+ |> List.fold_left ( + ) 0
+ in
+ num_of_matches
diff --git a/2024/ocaml/lib/day_05/part_01.ml b/2024/ocaml/lib/day_05/part_01.ml
new file mode 100644
index 0000000..f32bef1
--- /dev/null
+++ b/2024/ocaml/lib/day_05/part_01.ml
@@ -0,0 +1,62 @@
+(* Day 5: Part 1 *)
+
+module StringMap = Map.Make (String)
+
+let inverse_page_rule page_rule =
+ String.split_on_char '|' page_rule |> fun lst ->
+ match lst with [] -> "" | _ :: _ -> String.concat "|" (List.rev lst)
+
+(** [parse_aux acc page_rules] is a map of all invalid transitions as defined by
+ [page_rules] *)
+let rec get_invalid_transitions_aux acc page_rules : unit StringMap.t =
+ match page_rules with
+ | [] -> acc
+ | h :: t ->
+ get_invalid_transitions_aux
+ (acc |> StringMap.add (inverse_page_rule h) ())
+ t
+
+let get_invalid_transitions page_rules =
+ get_invalid_transitions_aux StringMap.empty page_rules
+
+(** [solve page_rules updates] is the solution for this puzzle, given the
+ [page_rules] and a list of [updates].
+
+ BELOW DOESN'T WORK: no guarantee that second page comes immediately after
+ the first
+ - pass in updates to solve
+ - make sliding window of 2 elements
+ - invert element order and concat
+ - check if key is in map from [parse page_rules],
+ - if yes, then it is an illegal transition
+ - if no, then add the list to a list for further processing *)
+let solve page_rules _ =
+ let invalid_transitions = get_invalid_transitions page_rules in
+ ()
+
+let _ =
+ solve
+ [
+ "47|53";
+ "97|13";
+ "97|61";
+ "97|47";
+ "75|29";
+ "61|13";
+ "75|53";
+ "29|13";
+ "97|29";
+ "53|29";
+ "61|53";
+ "97|53";
+ "61|29";
+ "47|13";
+ "75|47";
+ "97|75";
+ "47|61";
+ "75|61";
+ "47|29";
+ "75|13";
+ "53|13";
+ ]
+ []
diff --git a/2024/ocaml/lib/dune b/2024/ocaml/lib/dune
new file mode 100644
index 0000000..84e62ca
--- /dev/null
+++ b/2024/ocaml/lib/dune
@@ -0,0 +1,5 @@
+(library
+ (name AoC_2024)
+ (libraries re2))
+
+(include_subdirs qualified)
diff --git a/2024/ocaml/lib/file/read_input.ml b/2024/ocaml/lib/file/read_input.ml
new file mode 100644
index 0000000..0f7470b
--- /dev/null
+++ b/2024/ocaml/lib/file/read_input.ml
@@ -0,0 +1,11 @@
+let rec lines_from_channel ic =
+ try
+ let line = input_line ic in
+ line :: lines_from_channel ic
+ with End_of_file -> []
+
+let lines_from_file filename =
+ let channel = open_in filename in
+ let lines = lines_from_channel channel in
+ close_in channel;
+ lines
diff --git a/2024/ocaml/lib/grid/cell.ml b/2024/ocaml/lib/grid/cell.ml
new file mode 100644
index 0000000..186e451
--- /dev/null
+++ b/2024/ocaml/lib/grid/cell.ml
@@ -0,0 +1,18 @@
+type t = {
+ value : string;
+ up : t option;
+ down : t option;
+ left : t option;
+ right : t option;
+}
+
+let create value = { value; up = None; down = None; left = None; right = None }
+let with_up up c = { c with up = Some up }
+let with_down down c = { c with down = Some down }
+let with_left left c = { c with left = Some left }
+let with_right right c = { c with right = Some right }
+let move_up co = match co with Some c -> c.up | None -> None
+let move_down co = match co with Some c -> c.down | None -> None
+let move_left co = match co with Some c -> c.left | None -> None
+let move_right co = match co with Some c -> c.right | None -> None
+let ( = ) c1 c2 = c1.value = c2.value
diff --git a/2024/ocaml/lib/grid/cell.mli b/2024/ocaml/lib/grid/cell.mli
new file mode 100644
index 0000000..e61e34a
--- /dev/null
+++ b/2024/ocaml/lib/grid/cell.mli
@@ -0,0 +1,18 @@
+type t = {
+ value : string;
+ up : t option;
+ down : t option;
+ left : t option;
+ right : t option;
+}
+
+val create : string -> t
+val with_up : t -> t -> t
+val with_down : t -> t -> t
+val with_left : t -> t -> t
+val with_right : t -> t -> t
+val move_up : t option -> t option
+val move_down : t option -> t option
+val move_left : t option -> t option
+val move_right : t option -> t option
+val ( = ) : t -> t -> bool
diff --git a/2024/ocaml/lib/grid/grid.ml b/2024/ocaml/lib/grid/grid.ml
new file mode 100644
index 0000000..0d6bc4c
--- /dev/null
+++ b/2024/ocaml/lib/grid/grid.ml
@@ -0,0 +1,14 @@
+let rec to_grid_aux tokens max_width acc =
+ match tokens with
+ | [] -> acc
+ | h :: t ->
+ let new_cell = Cell.create h |> Cell.with_left acc in
+ let new_acc = acc |> Cell.with_right new_cell in
+ to_grid_aux t max_width new_acc
+
+let to_grid tokens max_width : Cell.t option =
+ match tokens with
+ | [] -> None
+ | h :: t ->
+ let first_cell = Cell.create h in
+ Some (to_grid_aux t max_width first_cell)
diff --git a/2024/ocaml/lib/grid/grid.mli b/2024/ocaml/lib/grid/grid.mli
new file mode 100644
index 0000000..a69fcfe
--- /dev/null
+++ b/2024/ocaml/lib/grid/grid.mli
@@ -0,0 +1,6 @@
+val to_grid : string list -> int -> Cell.t option
+(** The cell in the top left corner of the grid is returned where each element
+ of [tokens] is converted into rows with [width]. An empty list of [tokens]
+ returns None. Requires: [width] is greater than 0 **)
+
+(* val print : Cell.t -> () *)
diff --git a/2024/ocaml/test/day_01_part_01.ml b/2024/ocaml/test/day_01_part_01.ml
new file mode 100644
index 0000000..824c95b
--- /dev/null
+++ b/2024/ocaml/test/day_01_part_01.ml
@@ -0,0 +1,27 @@
+open OUnit2
+open AoC_2024.Day_01.Part_01
+
+let tests =
+ "test suite for day 01 part 01"
+ >::: [
+ ("empty list is 0" >:: fun _ -> assert_equal 0 (solve [] []));
+ ( "distance between 0 and 1 is 1" >:: fun _ ->
+ assert_equal 1 (solve [ 0 ] [ 1 ]) );
+ ( "distance between 1 and 0 is 1" >:: fun _ ->
+ assert_equal 1 (solve [ 1 ] [ 0 ]) );
+ ( "smallest are paired, then next smallest, ..." >:: fun _ ->
+ assert_equal 0 (solve [ 1; 2; 3 ] [ 3; 2; 1 ]) );
+ (* See https://adventofcode.com/2024/day/1 *)
+ ( "solves example correctly" >:: fun _ ->
+ assert_equal 11 (solve [ 3; 4; 2; 1; 3; 3 ] [ 4; 3; 5; 3; 9; 3 ]) );
+ ( "same number is 0 distance apart" >:: fun _ ->
+ assert_equal 0 (calculate_distance 0 0) );
+ ( "same number is 0 distance apart" >:: fun _ ->
+ assert_equal 0 (calculate_distance 1 1) );
+ ( "distance is equal to abs(x - y)" >:: fun _ ->
+ assert_equal 1 (calculate_distance 0 1) );
+ ( "order of inputs doesn't matter" >:: fun _ ->
+ assert_equal 1 (calculate_distance 1 0) );
+ ]
+
+let _ = run_test_tt_main tests
diff --git a/2024/ocaml/test/day_01_part_02.ml b/2024/ocaml/test/day_01_part_02.ml
new file mode 100644
index 0000000..f7340df
--- /dev/null
+++ b/2024/ocaml/test/day_01_part_02.ml
@@ -0,0 +1,17 @@
+open OUnit2
+open AoC_2024.Day_01.Part_02
+
+let test_solve =
+ "test suite for day 01 part 02"
+ >::: [
+ ("empty list is 0" >:: fun _ -> assert_equal 0 (solve [] []));
+ ( "ten x zero occurrances of ten" >:: fun _ ->
+ assert_equal 0 (solve [ 10 ] [ 1; 2; 3 ]) );
+ ( "two x three occurrances of two" >:: fun _ ->
+ assert_equal 6 (solve [ 2 ] [ 2; 2; 2 ]) );
+ (* See https://adventofcode.com/2024/day/1#part2 *)
+ ( "solves example correctly" >:: fun _ ->
+ assert_equal 31 (solve [ 3; 4; 2; 1; 3; 3 ] [ 4; 3; 5; 3; 9; 3 ]) );
+ ]
+
+let _ = run_test_tt_main test_solve
diff --git a/2024/ocaml/test/day_02_part_01.ml b/2024/ocaml/test/day_02_part_01.ml
new file mode 100644
index 0000000..36af248
--- /dev/null
+++ b/2024/ocaml/test/day_02_part_01.ml
@@ -0,0 +1,38 @@
+open OUnit2
+open AoC_2024.Day_02.Part_01
+
+let tests =
+ "test suite for day 2 part 1"
+ >::: [
+ ( "empty list should have no valid reports" >:: fun _ ->
+ assert_equal 0 (solve []) );
+ ( "valid report should count as one" >:: fun _ ->
+ assert_equal 1 (solve [ [ 7; 6; 4; 2; 1 ] ]) );
+ ( "example correct" >:: fun _ ->
+ assert_equal 2
+ (solve
+ [
+ [ 7; 6; 4; 2; 1 ];
+ [ 1; 2; 7; 8; 9 ];
+ [ 9; 7; 6; 2; 1 ];
+ [ 1; 3; 2; 4; 5 ];
+ [ 8; 6; 4; 4; 1 ];
+ [ 1; 3; 6; 7; 9 ];
+ ]) );
+ ( "all increasing" >:: fun _ ->
+ assert_equal true (check_report_safety [ 1; 2; 3 ]) );
+ ( "all decreasing" >:: fun _ ->
+ assert_equal true (check_report_safety [ 3; 2; 1 ]) );
+ ( "increasing and decreasing" >:: fun _ ->
+ assert_equal false (check_report_safety [ 1; 3; 2 ]) );
+ ( "change of at least one" >:: fun _ ->
+ assert_equal true (check_report_safety [ 1; 2; 3 ]) );
+ ( "no change" >:: fun _ ->
+ assert_equal false (check_report_safety [ 1; 1; 2 ]) );
+ ( "change of at most three" >:: fun _ ->
+ assert_equal true (check_report_safety [ 1; 2; 4; 7 ]) );
+ ( "change of at more than three" >:: fun _ ->
+ assert_equal false (check_report_safety [ 1; 5 ]) );
+ ]
+
+let _ = run_test_tt_main tests
diff --git a/2024/ocaml/test/day_03_part_01.ml b/2024/ocaml/test/day_03_part_01.ml
new file mode 100644
index 0000000..0db039a
--- /dev/null
+++ b/2024/ocaml/test/day_03_part_01.ml
@@ -0,0 +1,29 @@
+open OUnit2
+open AoC_2024.Day_03.Part_01
+
+let example_input =
+ "xmul(2,4)%&mul[3,7]!@^do_not_mul(5,5)+mul(32,64]then(mul(11,8)mul(8,5))"
+
+let test_find_mul _ =
+ assert_equal
+ [ "mul(2,4)"; "mul(5,5)"; "mul(11,8)"; "mul(8,5)" ]
+ (let result = find_mul example_input in
+ List.map (fun x -> Re2.Match.get_exn x ~sub:(`Index 0)) result)
+
+let test_extract_pairs _ =
+ assert_equal
+ [ (2, 4); (5, 5); (11, 8); (8, 5) ]
+ (let matches = find_mul example_input in
+ extract_pairs matches)
+
+let test_solve _ = assert_equal ~printer:string_of_int 161 (solve example_input)
+
+let tests =
+ "test suite for day 3 part 1"
+ >::: [
+ "muls extracted from example correctly" >:: test_find_mul;
+ "pairs extracted from matches" >:: test_extract_pairs;
+ "example correctly solved" >:: test_solve;
+ ]
+
+let _ = run_test_tt_main tests
diff --git a/2024/ocaml/test/day_04_part_01.ml b/2024/ocaml/test/day_04_part_01.ml
new file mode 100644
index 0000000..9550e5b
--- /dev/null
+++ b/2024/ocaml/test/day_04_part_01.ml
@@ -0,0 +1,151 @@
+open OUnit2
+open AoC_2024.Day_04.Part_01
+
+let example_input =
+ [
+ "MMMSXXMASM";
+ "MSAMXMSMSA";
+ "AMXSXMAAMM";
+ "MSAMASMSMX";
+ "XMASAMXAMM";
+ "XXAMMXXAMA";
+ "SMSMSASXSS";
+ "SAXAMASAAA";
+ "MAMMMXMMMM";
+ "MXMXAXMASX";
+ ]
+
+let is_in_bounds_tests =
+ let test_is_in_bounds expected length index _ =
+ let actual = is_in_bounds length index in
+ assert_equal ~printer:string_of_bool expected actual
+ in
+ [
+ "not in bounds if below 0" >:: test_is_in_bounds false 1 (-1);
+ "in bounds if gte 0 and lt length" >:: test_is_in_bounds true 1 0;
+ "not in bounds if eq length" >:: test_is_in_bounds false 1 1;
+ "not in bounds if gt length" >:: test_is_in_bounds false 1 2;
+ ]
+
+let calculate_num_of_matches_tests =
+ let test_calculate_num_of_matches expected line row_offset character _ =
+ let actual = calculate_num_of_matches line row_offset character in
+ assert_equal ~printer:string_of_int expected actual
+ in
+ [
+ "is 0 when no matches present"
+ >:: test_calculate_num_of_matches 0 "SMMX" 1 { index = 3; character = 'X' };
+ "is 1 when only one match present - top to bottom"
+ >::
+ (*
+ |.|X|.|
+ |.|M|.|
+ |.|A|.|
+ |.|S|.|
+ *)
+ test_calculate_num_of_matches 1 ".X..M..A..S." 3
+ { index = 1; character = 'X' };
+ "is 1 when only one match present - top to bottom edge"
+ >::
+ (*
+ |X|.|.|
+ |M|.|.|
+ |A|.|.|
+ |S|.|.|
+ *)
+ test_calculate_num_of_matches 1 "X..M..A..S.." 3
+ { index = 0; character = 'X' };
+ "is 1 when only one match present - bottom to top"
+ >::
+ (*
+ |.|S|.|
+ |.|A|.|
+ |.|M|.|
+ |.|X|.|
+ *)
+ test_calculate_num_of_matches 1 ".S..A..M..X." 3
+ { index = 10; character = 'X' };
+ "is 1 when only one match present - left to right"
+ >::
+ (*
+ |X|M|A|S|
+ *)
+ test_calculate_num_of_matches 1 "XMAS" 4 { index = 0; character = 'X' };
+ "is 1 when only one match present - right to left"
+ >::
+ (*
+ |S|A|M|X|
+ *)
+ test_calculate_num_of_matches 1 "SAMX" 4 { index = 3; character = 'X' };
+ "is 1 when only one match present - bottom right to top left"
+ >::
+ (*
+ |S|.|.|.|
+ |.|A|.|.|
+ |.|.|M|.|
+ |.|.|.|X|
+ *)
+ test_calculate_num_of_matches 1 "S....A....M....X" 4
+ { index = 15; character = 'X' };
+ "is 1 when only one match present - top left to bottom right"
+ >::
+ (*
+ |X|.|.|.|
+ |.|M|.|.|
+ |.|.|A|.|
+ |.|.|.|S|
+ *)
+ test_calculate_num_of_matches 1 "X....M....A....S" 4
+ { index = 0; character = 'X' };
+ "is 1 when only one match present - bottom left to top right"
+ >::
+ (*
+ |.|.|.|S|
+ |.|.|A|.|
+ |.|M|.|.|
+ |X|.|.|.|
+ *)
+ test_calculate_num_of_matches 1 "...S..A..M..X..." 4
+ { index = 12; character = 'X' };
+ "is 1 when only one match present - top right to bottom left"
+ >::
+ (*
+ |.|.|.|X|
+ |.|.|M|.|
+ |.|A|.|.|
+ |S|.|.|.|
+ *)
+ test_calculate_num_of_matches 1 "...X..M..A..S..." 4
+ { index = 3; character = 'X' };
+ "is 8 when matches present in all possible directions"
+ >::
+ (*
+ |S|.|.|S|.|.|S|
+ |.|A|.|A|.|A|.|
+ |.|.|M|M|M|.|.|
+ |S|A|M|X|M|A|S|
+ |.|.|M|M|M|.|.|
+ |.|A|.|A|.|A|.|
+ |S|.|.|S|.|.|S|
+ *)
+ test_calculate_num_of_matches 8
+ "S..S..S.A.A.A...MMM..SAMXMAS..MMM...A.A.A.S..S..S" 7
+ { index = 24; character = 'X' };
+ ]
+
+let solve_tests =
+ let test_solve expected lines _ =
+ let actual = solve lines in
+ assert_equal ~printer:string_of_int expected actual
+ in
+ [
+ "example is solved correctly" >:: test_solve 18 example_input;
+ "minimal example is solved correctly"
+ >:: test_solve 4 [ "..X..."; ".SAMX."; ".A..A."; "XMAS.S"; ".X...." ];
+ ]
+
+let tests =
+ "test suite for day 4 part 1"
+ >::: is_in_bounds_tests @ calculate_num_of_matches_tests @ solve_tests
+
+let _ = run_test_tt_main tests
diff --git a/2024/ocaml/test/day_05_part_01.ml b/2024/ocaml/test/day_05_part_01.ml
new file mode 100644
index 0000000..0f50476
--- /dev/null
+++ b/2024/ocaml/test/day_05_part_01.ml
@@ -0,0 +1,4 @@
+open OUnit2
+
+let tests = "test suite for day n part 1" >::: []
+let _ = run_test_tt_main tests
diff --git a/2024/ocaml/test/dune b/2024/ocaml/test/dune
new file mode 100644
index 0000000..e19cbe9
--- /dev/null
+++ b/2024/ocaml/test/dune
@@ -0,0 +1,10 @@
+(tests
+ (names
+ day_01_part_01
+ day_01_part_02
+ day_02_part_01
+ day_03_part_01
+ day_04_part_01
+ grid)
+ (modes byte exe)
+ (libraries AoC_2024 ounit2))
diff --git a/2024/ocaml/test/grid.ml b/2024/ocaml/test/grid.ml
new file mode 100644
index 0000000..bd384cd
--- /dev/null
+++ b/2024/ocaml/test/grid.ml
@@ -0,0 +1,5 @@
+open OUnit2
+open AoC_2024.Grid
+
+let tests = "" >::: []
+let _ = run_test_tt_main tests