diff options
| author | DJ O'Leary <dijitol@proton.me> | 2025-03-03 00:56:33 +0100 |
|---|---|---|
| committer | DJ O'Leary <dijitol@proton.me> | 2025-03-03 00:56:33 +0100 |
| commit | e24effa8fd3574e9d396be0e1a47f101c27c0a60 (patch) | |
| tree | 793896d20384facd10d925e09f8f0dd89c930cce /2024/OCaml | |
perf: Merge Advent of Code year specific repos
Diffstat (limited to '2024/OCaml')
34 files changed, 1165 insertions, 0 deletions
diff --git a/2024/OCaml/.editorconfig b/2024/OCaml/.editorconfig new file mode 100644 index 0000000..24b0621 --- /dev/null +++ b/2024/OCaml/.editorconfig @@ -0,0 +1,21 @@ +root = true + +[*] +charset = utf-8 +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true + +[*.ml] +indent_style = space +indent_size = 2 + +[*.php] +indent_style = space +indent_size = 4 + +[*.go] +indent_style = tab + +[Makefile] +indent_style = tab diff --git a/2024/OCaml/.env.template b/2024/OCaml/.env.template new file mode 100644 index 0000000..637094f --- /dev/null +++ b/2024/OCaml/.env.template @@ -0,0 +1 @@ +SESSION=REPLACE_ME diff --git a/2024/OCaml/.envrc b/2024/OCaml/.envrc new file mode 100644 index 0000000..1d953f4 --- /dev/null +++ b/2024/OCaml/.envrc @@ -0,0 +1 @@ +use nix diff --git a/2024/OCaml/.gitignore b/2024/OCaml/.gitignore new file mode 100644 index 0000000..a251688 --- /dev/null +++ b/2024/OCaml/.gitignore @@ -0,0 +1,32 @@ +*.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/ + +.env +.direnv diff --git a/2024/OCaml/.ocamlformat b/2024/OCaml/.ocamlformat new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/2024/OCaml/.ocamlformat diff --git a/2024/OCaml/AoC_2024.opam b/2024/OCaml/AoC_2024.opam new file mode 100644 index 0000000..41c0f97 --- /dev/null +++ b/2024/OCaml/AoC_2024.opam @@ -0,0 +1,31 @@ +# 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: "MIT" +tags: ["aoc2024" "Advent of Code 2024"] +homepage: "https://github.com/DJOLEARY/AoC_2024" +doc: "https://github.com/DJOLEARY/AoC_2024/blob/main/README.md" +bug-reports: "https://github.com/DJOLEARY/AoC_2024/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/AoC_2024.git" diff --git a/2024/OCaml/LICENSE b/2024/OCaml/LICENSE new file mode 100644 index 0000000..714c645 --- /dev/null +++ b/2024/OCaml/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 D.J. O'Leary + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/2024/OCaml/README.md b/2024/OCaml/README.md new file mode 100644 index 0000000..96db988 --- /dev/null +++ b/2024/OCaml/README.md @@ -0,0 +1,22 @@ +# Advent of Code 2024 + +## Getting Started + +### How to Build + +```sh +dune build +``` + +### How to Run + +```sh +dune exec AoC_2024 +``` + +### How to Test + +```sh +dune runtest +``` + diff --git a/2024/OCaml/Taskfile.yml b/2024/OCaml/Taskfile.yml new file mode 100644 index 0000000..067ecec --- /dev/null +++ b/2024/OCaml/Taskfile.yml @@ -0,0 +1,100 @@ +# https://taskfile.dev + +version: '3' + +vars: + PROJECT_NAME: AoC_2024 + YEAR: 2024 + ASSET_DIR: input + SRC_DIR: lib + SRC_TEMPLATE: part.ml + TEST_DIR: test + TEST_TEMPLATE: test.ml + +dotenv: ['.env'] + +tasks: + default: + desc: "Prints this message" + cmds: + - cmd: task --list-all + + day:setup: + desc: "Creates the files needed to solve a day of Advent of Code" + requires: + vars: + - DAY + vars: + FORMATTED_DAY: "{{if (lt (atoi .DAY) 10)}}0{{.DAY}}{{else}}{{.DAY}}{{end}}" + cmds: + - cmd: mkdir --parents {{ .SRC_DIR }}/day_{{ .FORMATTED_DAY }} + - cmd: cp templates/{{.SRC_TEMPLATE}} {{.SRC_DIR}}/day_{{.FORMATTED_DAY}}/part_01.ml + - cmd: mkdir --parents {{ .TEST_DIR }} + - cmd: cp templates/{{.TEST_TEMPLATE}} {{.TEST_DIR}}/day_{{.FORMATTED_DAY}}_part_01.ml + - task: fetch-input + - task: build + silent: true + + fetch-input: + desc: "Fetches your input from adventofcode.com for the specified day" + internal: true + requires: + vars: + - DAY + vars: + URI: https://adventofcode.com/{{ .YEAR }}/day/{{.DAY}}/input + FORMATTED_DAY: "{{if (lt (atoi .DAY) 10)}}0{{.DAY}}{{else}}{{.DAY}}{{end}}" + cmds: + - cmd: curl --cookie 'session={{.SESSION}}' {{.URI}} > {{.ASSET_DIR}}/day_{{.FORMATTED_DAY}}.txt + silent: true + + 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..ee86a69 --- /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/AoC_2024)) + +(authors "D.J. O'Leary") + +(maintainers "D.J. O'Leary") + +(license MIT) + +(documentation https://github.com/DJOLEARY/AoC_2024/blob/main/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/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..532b344 --- /dev/null +++ b/2024/OCaml/lib/day_01/part_02.ml @@ -0,0 +1,43 @@ +(* 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..63c1cfd --- /dev/null +++ b/2024/OCaml/lib/day_04/part_01.ml @@ -0,0 +1,84 @@ +(* 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_04/php/Grid.php b/2024/OCaml/lib/day_04/php/Grid.php new file mode 100644 index 0000000..c0382df --- /dev/null +++ b/2024/OCaml/lib/day_04/php/Grid.php @@ -0,0 +1,67 @@ +<?php + +declare(strict_types=1); + +class Grid implements Stringable +{ + /** + * @param string[][] $data + */ + public function __construct(public array $data) {} + + public function __toString(): string + { + $rowToLine = fn(array $row): string => array_reduce( + array: $row, + callback: fn(string $acc, string $character): string => $acc . $character, + initial: "", + ); + + return array_reduce( + array: $this->data, + callback: fn(string $acc, array $row): string => $acc . $rowToLine($row) . PHP_EOL, + initial: "", + ); + } + + public function getCharacterAtPosition(Position $position): string + { + if ($this->isInBounds($position)) { + return $this->data[$position->y][$position->x]; + } + + return "."; + } + + /** + * @param Position[] $positions + */ + public function getStringFromPositions(array $positions): string + { + $characters = array_map( + callback: fn(Position $position): string => $this->getCharacterAtPosition($position), + array: $positions, + ); + + $string = array_reduce( + array: $characters, + callback: fn(string $acc, string $character): string => $acc . $character, + initial: "", + ); + + return $string; + } + + public function isInBounds(Position $position): bool + { + if (!array_key_exists($position->y, $this->data)) { + return false; + } + + if (!array_key_exists($position->x, $this->data[$position->y])) { + return false; + } + + return true; + } +} diff --git a/2024/OCaml/lib/day_04/php/Input.php b/2024/OCaml/lib/day_04/php/Input.php new file mode 100644 index 0000000..f7e03c1 --- /dev/null +++ b/2024/OCaml/lib/day_04/php/Input.php @@ -0,0 +1,25 @@ +<?php + +declare(strict_types=1); + +class Input +{ + public string $contents; + + public function __construct(private string $fileName) + { + $contents = file_get_contents($this->fileName, use_include_path: true); + $this->contents = $contents === false ? "" : $contents; + } + + public function toGrid(): Grid + { + $lines = explode("\n", $this->contents); + $gridArray = array_map( + callback: fn(string $line): array => str_split(trim($line)), + array: $lines + ); + + return new Grid($gridArray); + } +} diff --git a/2024/OCaml/lib/day_04/php/Position.php b/2024/OCaml/lib/day_04/php/Position.php new file mode 100644 index 0000000..182da7e --- /dev/null +++ b/2024/OCaml/lib/day_04/php/Position.php @@ -0,0 +1,77 @@ +<?php + +declare(strict_types=1); + +class Position implements Stringable +{ + public function __construct(public $x, public $y) {} + + public function __toString(): string + { + return "({$this->x}, {$this->y})"; + } + + /** + * Not validated + */ + public function getNorth(): self + { + return new self($this->x, $this->y - 1); + } + + /** + * Not validated + */ + public function getNorthEast(): self + { + return new self($this->x + 1, $this->y - 1); + } + + /** + * Not validated + */ + public function getEast(): self + { + return new self($this->x + 1, $this->y); + } + + /** + * Not validated + */ + public function getSouthEast(): self + { + return new self($this->x + 1, $this->y + 1); + } + + /** + * Not validated + */ + public function getSouth(): self + { + return new self($this->x, $this->y + 1); + } + + /** + * Not validated + */ + public function getSouthWest(): self + { + return new self($this->x - 1, $this->y + 1); + } + + /** + * Not validated + */ + public function getWest(): self + { + return new self($this->x - 1, $this->y); + } + + /** + * Not validated + */ + public function getNorthWest(): self + { + return new self($this->x - 1, $this->y - 1); + } +} diff --git a/2024/OCaml/lib/day_04/php/part_01.php b/2024/OCaml/lib/day_04/php/part_01.php new file mode 100644 index 0000000..250da0c --- /dev/null +++ b/2024/OCaml/lib/day_04/php/part_01.php @@ -0,0 +1,94 @@ +<?php + +declare(strict_types=1); + +spl_autoload_register(fn(string $className) => require "{$className}.php"); + +// Assumes executed from workspace root +$input = new Input('input/day_04.txt'); +if ($input->contents === "") { + throw new RuntimeException("File not found"); +} + +$grid = $input->toGrid(); + +$matchCount = 0; +foreach ($grid->data as $y => $row) { + foreach ($row as $x => $char) { + if ($char !== 'X') { + continue; + } + + $start = new Position($x, $y); + + $possibleMatches = []; + + $possibleMatches["north"] = $grid->getStringFromPositions([ + $start, + $start->getNorth(), + $start->getNorth()->getNorth(), + $start->getNorth()->getNorth()->getNorth(), + ]); + + $possibleMatches["east"] = $grid->getStringFromPositions([ + $start, + $start->getEast(), + $start->getEast()->getEast(), + $start->getEast()->getEast()->getEast(), + ]); + + $possibleMatches["south"] = $grid->getStringFromPositions([ + $start, + $start->getSouth(), + $start->getSouth()->getSouth(), + $start->getSouth()->getSouth()->getSouth(), + ]); + + $possibleMatches["west"] = $grid->getStringFromPositions([ + $start, + $start->getWest(), + $start->getWest()->getWest(), + $start->getWest()->getWest()->getWest(), + ]); + + $possibleMatches["north-east"] = $grid->getStringFromPositions([ + $start, + $start->getNorthEast(), + $start->getNorthEast()->getNorthEast(), + $start->getNorthEast()->getNorthEast()->getNorthEast(), + ]); + + $possibleMatches["north-west"] = $grid->getStringFromPositions([ + $start, + $start->getNorthWest(), + $start->getNorthWest()->getNorthWest(), + $start->getNorthWest()->getNorthWest()->getNorthWest(), + ]); + + $possibleMatches["south-east"] = $grid->getStringFromPositions([ + $start, + $start->getSouthEast(), + $start->getSouthEast()->getSouthEast(), + $start->getSouthEast()->getSouthEast()->getSouthEast(), + ]); + + $possibleMatches["south-west"] = $grid->getStringFromPositions([ + $start, + $start->getSouthWest(), + $start->getSouthWest()->getSouthWest(), + $start->getSouthWest()->getSouthWest()->getSouthWest(), + ]); + + $matchCount += array_reduce( + array: $possibleMatches, + callback: fn(int $acc, string $possibleMatch): int => match ($possibleMatch) { + "XMAS" => $acc + 1, + default => $acc, + }, + initial: 0, + ); + } +} + +// ANSWER: 2517 +echo $matchCount; 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..92bb105 --- /dev/null +++ b/2024/OCaml/lib/day_05/part_01.ml @@ -0,0 +1,23 @@ +(* Day 5: Part 1 *) + +(* + 01. Parse pages that need updating into list, e.g. "75,47,61,53,29" -> [ 75; 47; 61; 53; 29; ] + 02. Parse page ordering rules into DTOs, e.g. "47|53" -> {"before": 47, "after": 53} + 03. Find all rules that have pages from the parsed page list on both sides, e.g. "47|53" is valid but "97|47" and "47|13" are not + - filter down to "before" values that are in page list + - filter down to "after" values that are in page list + 04. Map across the page list and ensure that all rules for each page are upheld + 05. Map across the rules for the current page and ensure that all rules are upheld + - filter rules down to rules with "before" equal to current page + - walk the page list and ensure that all "after" values do no appear before the "before" value + - return true if all rules are valid for the current page + 06. Reduce result down to single boolean + 07. IF FALSE, move on to next page in page list + 08. IF TRUE, continue until all rules are validated + 09. IF ALL RULES ARE NOT VALID FOR PAGE LIST, move on to next page list + 10. IF ALL RULES ARE VALID FOR PAGE LIST, save middle number in page list + 11. ONCE ALL PAGE LISTS HAVE BEEN PROCESSED, add all saved middle numbers + 12. Return sum of middle numbers +*) + +let solve _ = () 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/shell.nix b/2024/OCaml/shell.nix new file mode 100644 index 0000000..aa955b6 --- /dev/null +++ b/2024/OCaml/shell.nix @@ -0,0 +1,42 @@ +{ }: +let + config = { + allowUnfree = true; + }; + pkgs = import <nixpkgs> { inherit config; }; + + # choose the ocaml version you want to use + 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 + + # Fallback language + pkgs.php84 + pkgs.intelephense + pkgs.php84Packages.php-cs-fixer + pkgs.php84Packages.phpstan + pkgs.nodejs_18 + ]; +} diff --git a/2024/OCaml/templates/part.ml b/2024/OCaml/templates/part.ml new file mode 100644 index 0000000..062c917 --- /dev/null +++ b/2024/OCaml/templates/part.ml @@ -0,0 +1,3 @@ +(* Day N: Part 1 *) + +let solve _ = () diff --git a/2024/OCaml/templates/test.ml b/2024/OCaml/templates/test.ml new file mode 100644 index 0000000..0f50476 --- /dev/null +++ b/2024/OCaml/templates/test.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/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..9f43df0 --- /dev/null +++ b/2024/OCaml/test/day_04_part_01.ml @@ -0,0 +1,141 @@ +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..8d01938 --- /dev/null +++ b/2024/OCaml/test/dune @@ -0,0 +1,8 @@ +(tests + (names day_01_part_01 + day_01_part_02 + day_02_part_01 + day_03_part_01 + day_04_part_01) + (modes byte exe) + (libraries AoC_2024 ounit2)) |
