diff --git a/.git-blame-ignore-revs b/.git-blame-ignore-revs new file mode 100644 index 0000000..07d7ee7 --- /dev/null +++ b/.git-blame-ignore-revs @@ -0,0 +1,20 @@ +# ref: Reorganize Day 1 files +4618c80de3fc4cbe60dc339de058ffdbea397946 + +# ref: Reorganize Day 2 files +5ab3e6e4510d9cd5e37786746da46e1fe7f8e440 + +# ref: Reorganize Day 3 files +66690ddb52c70f887f90d32830ff04faa6283589 + +# ref: Reorganize Day 4 files +c313bcc707b50839d2e61a36ad94284d48f743d5 + +# ref: Reorganize Day 5 files +04dd034d9efc23d3cdc27c91c08af418fe635c53 + +# ref: Reorganize Day 6 files +6ea2bb3121a127830eb5f5b393765a61a3f07278 + +# doc: Update README.md files +3d0d817c3198d81c2c9330adb67cd49edcfbc446 diff --git a/README.md b/README.md index 8ea4488..a735881 100644 --- a/README.md +++ b/README.md @@ -10,24 +10,58 @@ The project is compose of two main parts: - A library, located in the [`lib/`](lib/) directory, containing the code for the solutions. - A set of tests, located in the [`test/`](test/) directory, containing the tests for the solutions. -Tests are written using the [:octocat: minitest](https://github.com/minitest/minitest) framework. +Tests are written using the [minitest](https://github.com/minitest/minitest) framework. To run the tests, simply run the following command: ```bash bundle install -bundle exec rake test +SKIP_SLOW_TESTS=1 bundle exec rake test ``` ## Puzzles -### 2023 + -| Day | Solution | Rank | -|-------------------------------------------:|:--------------------------------------------------------:|:-----:| -| [1️⃣](https://adventofcode.com/2023/day/1) | [`lib/puzzles/2023/day01.rb`](lib/puzzles/2023/day01.rb) | ⭐🌟✴️ | -| [2️⃣](https://adventofcode.com/2023/day/2) | [`lib/puzzles/2023/day02.rb`](lib/puzzles/2023/day02.rb) | ⭐🌟✴️ | -| [3️⃣](https://adventofcode.com/2023/day/3) | [`lib/puzzles/2023/day03.rb`](lib/puzzles/2023/day03.rb) | ⭐🌟✴️ | -| [4️⃣](https://adventofcode.com/2023/day/4) | [`lib/puzzles/2023/day04.rb`](lib/puzzles/2023/day04.rb) | ⭐🌟✴️ | -| [5️⃣](https://adventofcode.com/2023/day/5) | [`lib/puzzles/2023/day05.rb`](lib/puzzles/2023/day05.rb) | ⭐🌟✴️ | -| [6️⃣](https://adventofcode.com/2023/day/6) | [`lib/puzzles/2023/day06.rb`](lib/puzzles/2023/day06.rb) | ⭐🌟✴️ | +
+ Advent of Code - 2023 +

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
DaySolutionRank
1️⃣ Trebuchet?!lib/puzzles/2023/day01🌟🌟
2️⃣ Cube Conundrumlib/puzzles/2023/day02🌟🌟
3️⃣ Gear Ratioslib/puzzles/2023/day03🌟🌟
4️⃣ Scratchcardslib/puzzles/2023/day04🌟🌟
5️⃣ If You Give A Seed A Fertilizerlib/puzzles/2023/day05🌟🌟
6️⃣ Wait For Itlib/puzzles/2023/day06🌟🌟
+

+
diff --git a/lib/puzzles/2023/day01/README.md b/lib/puzzles/2023/day01/README.md new file mode 100644 index 0000000..35e1723 --- /dev/null +++ b/lib/puzzles/2023/day01/README.md @@ -0,0 +1,52 @@ +# [Day 1: Trebuchet?!](https://adventofcode.com/2023/day/1) + +## Part One + +Something is wrong with global snow production, and you've been selected to take a look. The Elves have even given you a map; on it, they've used stars to mark the top fifty locations that are likely to be having problems. + +You've been doing this long enough to know that to restore snow operations, you need to check all **fifty stars** by December 25th. + +Collect stars by solving puzzles. Two puzzles will be made available on each day in the Advent calendar; the second puzzle is unlocked when you complete the first. Each puzzle grants **one star**. Good luck! + +You try to ask why they can't just use a [weather machine](https://adventofcode.com/2015/day/1) ("not powerful enough") and where they're even sending you ("the sky") and why your map looks mostly blank ("you sure ask a lot of questions") and hang on did you just say the sky ("of course, where do you think snow comes from") when you realize that the Elves are already loading you into a [trebuchet](https://en.wikipedia.org/wiki/Trebuchet) ("please hold still, we need to strap you in"). + +As they're making the final adjustments, they discover that their calibration document (your puzzle input) has been **amended** by a very young Elf who was apparently just excited to show off her art skills. Consequently, the Elves are having trouble reading the values on the document. + +The newly-improved calibration document consists of lines of text; each line originally contained a specific **calibration value** that the Elves now need to recover. On each line, the calibration value can be found by combining the **first digit** and the **last digit** (in that order) to form a single **two-digit number**. + +For example: + +``` +1abc2 +pqr3stu8vwx +a1b2c3d4e5f +treb7uchet +``` + +In this example, the calibration values of these four lines are `12`, `38`, `15`, and `77`. Adding these together produces `142`. + +Consider your entire calibration document. **What is the sum of all of the calibration values?** + +Your puzzle answer was `54450`. + +## Part Two + +Your calculation isn't quite right. It looks like some of the digits are actually **spelled out with letters**: `one`, `two`, `three`, `four`, `five`, `six`, `seven`, `eight`, and `nine` **also** count as valid "digits". + +Equipped with this new information, you now need to find the real first and last digit on each line. For example: + +``` +two1nine +eightwothree +abcone2threexyz +xtwone3four +4nineeightseven2 +zoneight234 +7pqrstsixteen +``` + +In this example, the calibration values are `29`, `83`, `13`, `24`, `42`, `14`, and `76`. Adding these together produces `281`. + +**What is the sum of all of the calibration values?** + +Your puzzle answer was `54265`. diff --git a/lib/puzzles/2023/day01/day01.rb b/lib/puzzles/2023/day01/day01.rb new file mode 100644 index 0000000..5255b89 --- /dev/null +++ b/lib/puzzles/2023/day01/day01.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +require_relative "part1" +require_relative "part2" + +module AdventOfCode + module Puzzles2023 + ## + # {include:file:lib/puzzles/2023/day01/README.md} + module Day01 + end + end +end diff --git a/test/puzzles/2023/day01/input.txt b/lib/puzzles/2023/day01/input.txt similarity index 100% rename from test/puzzles/2023/day01/input.txt rename to lib/puzzles/2023/day01/input.txt diff --git a/lib/puzzles/2023/day01/part1.rb b/lib/puzzles/2023/day01/part1.rb new file mode 100644 index 0000000..5188053 --- /dev/null +++ b/lib/puzzles/2023/day01/part1.rb @@ -0,0 +1,73 @@ +# frozen_string_literal: true + +module AdventOfCode + module Puzzles2023 + module Day01 + ## + # Class for solving Day 1 - Part 1 puzzle + class Part1 + ## + # @param file [String] file with puzzle input + def initialize(file: nil) + file ||= "#{File.dirname(__FILE__)}/input.txt" + @file_contents = File.readlines(file, chomp: true) + end + + ## + # Compute the answer for the puzzle. + # The answer is the sum of all calibration values. + # + # @return [Integer] answer for the puzzle + def answer + calibration_values.sum + end + + ## + # Compute calibration values based on file contents. + # + # @return [Array] calibration values + def calibration_values + @calibration_values ||= file_contents.map { |line| select_number(line) } + end + + protected + + ## + # @return [Array] file contents + attr_reader :file_contents + + ## + # Select first and last number from line. + # + # @param line [String] line to select numbers from + # + # @return [Integer] first and last number concatenated + def select_number(line) + first_number = find_first_number(line) + last_number = find_last_number(line) + "#{first_number}#{last_number}".to_i + end + + ## + # Find and return the first number in a line. + # + # @param line [String] line to search + # + # @return [Integer, nil] first number found + def find_first_number(line) + line.match(%r{^\D*(\d).*})[1]&.to_i + end + + ## + # Find and return the last number in a line. + # + # @param line [String] line to search + # + # @return [Integer, nil] last number found + def find_last_number(line) + line.match(%r{.*?(\d)\D*$})[1]&.to_i + end + end + end + end +end diff --git a/lib/puzzles/2023/day01.rb b/lib/puzzles/2023/day01/part2.rb similarity index 53% rename from lib/puzzles/2023/day01.rb rename to lib/puzzles/2023/day01/part2.rb index 1e96bb1..8ca6631 100644 --- a/lib/puzzles/2023/day01.rb +++ b/lib/puzzles/2023/day01/part2.rb @@ -1,79 +1,18 @@ # frozen_string_literal: true +require_relative "part1" + module AdventOfCode module Puzzles2023 - ## - # Advent of Code 2023 - Day 1 - # https://adventofcode.com/2023/day/1 module Day01 ## - # Class for solving Day 01 - Part 1 puzzle - class Part1 - attr_reader :file_contents - - ## - # @param file [String] file with puzzle input - def initialize(file:) - @file_contents = File.readlines(file, chomp: true) - end - - ## - # Compute the answer for the puzzle. - # The answer is the sum of all calibration values. - # - # @return [Integer] answer for the puzzle - def answer - calibration_values.sum - end - - ## - # Compute calibration values based on file contents. - # - # @return [Array] calibration values - def calibration_values - @calibration_values ||= file_contents.map { |line| select_number(line) } - end - - protected - - ## - # Select first and last number from line. - # - # @param line [String] line to select numbers from - # - # @return [Integer] first and last number concatenated - def select_number(line) - first_number = find_first_number(line) - last_number = find_last_number(line) - "#{first_number}#{last_number}".to_i - end - - ## - # Find and return the first number in a line. - # - # @param line [String] line to search - # - # @return [Integer, nil] first number found - def find_first_number(line) - line.match(%r{^\D*(\d).*})[1]&.to_i - end - - ## - # Find and return the last number in a line. - # - # @param line [String] line to search - # - # @return [Integer, nil] last number found - def find_last_number(line) - line.match(%r{.*?(\d)\D*$})[1]&.to_i - end - end - - ## - # Class for solving Day 01 - Part 2 puzzle + # Class for solving Day 1 - Part 2 puzzle class Part2 < Part1 protected + ## + # Helper constant for translating word numbers to digits. + # @return [Hash] word numbers to digits TRANSLATIONS = %i[one two three four five six seven eight nine].zip(1..9).to_h.freeze ## diff --git a/lib/puzzles/2023/day02.rb b/lib/puzzles/2023/day02.rb deleted file mode 100644 index 390126f..0000000 --- a/lib/puzzles/2023/day02.rb +++ /dev/null @@ -1,194 +0,0 @@ -# frozen_string_literal: true - -module AdventOfCode - module Puzzles2023 - ## - # Advent of Code 2023 - Day 2 - # https://adventofcode.com/2023/day/2 - module Day02 - ## - # Set class to store the number of blue, green, and red cubes. - class Set - attr_reader :blue, :green, :red - - ## - # @param blue [Integer] number of blue cubes - # @param green [Integer] number of green cubes - # @param red [Integer] number of red cubes - def initialize(blue: 0, green: 0, red: 0) - @blue = blue - @green = green - @red = red - end - - ## - # Compute the power of the set by multiplying the number of - # blue, green, and red cubes. - # - # @return [Integer] power of the set - def power - return @power if @power - - @power = (blue || 1) * (green || 1) * (red || 1) - end - end - - ## - # Game class to store the game id and its sets. - class Game - attr_reader :id, :sets - - ## - # @param id [Integer] game id - # @param sets [Array] sets of cubes - def initialize(id:, sets:) - @id = id - @sets = sets - end - - ## - # Check if the game is valid given the number of blue, green, - # and red cubes. - # - # @param blue [Integer] number of blue cubes - # @param green [Integer] number of green cubes - # @param red [Integer] number of red cubes - # - # @return [Boolean] true if the game is valid, false otherwise - def valid?(blue:, green:, red:) - # A game is valid only if every set has the same or less - # number of blue, green, and red cubes as the set it is - sets.each do |set| - return false if set.blue > blue - return false if set.green > green - return false if set.red > red - end - true - end - end - - ## - # Class for solving Day 02 - Part 1 puzzle - class Part1 - attr_reader :games - - ## - # @param file [String] path to the input file - def initialize(file:) - file_contents = File.readlines(file, chomp: true) - load_games(file_contents) - end - - ## - # Find games that are valid given the number of blue, green, - # and red cubes. - # - # @param blue [Integer] number of blue cubes - # @param green [Integer] number of green cubes - # @param red [Integer] number of red cubes - # - # @return [Array] valid games - def valid_games(blue:, green:, red:) - games.select { |game| game.valid?(blue:, green:, red:) } - end - - ## - # Find the sum of the game ids that are valid given the number - # of blue, green, and red cubes. - # - # @param blue [Integer] number of blue cubes - # @param green [Integer] number of green cubes - # @param red [Integer] number of red cubes - # - # @return [Integer] sum of the game ids - def answer(blue:, green:, red:) - valid_games(blue:, green:, red:).sum(&:id) - end - - protected - - ## - # Load games from the file contents. - # - # @param file_contents [Array] file contents - # - # @return [Array] games - def load_games(file_contents) - @games = file_contents.map do |line| - id, sets = line.split(": ") - sets = sets.split(%r{;\s+}).map { |set| parse_set(set) } - game_id = parse_game_id(id) - Game.new(id: game_id, sets:) - end - end - - ## - # Parse the game id from a string. - # The string is in the format "Game X". - # - # @param game [String] game id - # - # @return [Integer] game id - def parse_game_id(game) - game.gsub("Game ", "").to_i - end - - ## - # Parse a set of cubes from a string. - # The string is in the format "X blue, Y green, Z red". - # - # @param set [String] set of cubes - # - # @return [Set] set of cubes - def parse_set(set) - colors = set.split(%r{,\s+}).map(&:split) - colors = colors.to_h { |number, color| [color.downcase.to_sym, number.to_i] } - Set.new(**colors) - end - end - - ## - # Class for solving Day 02 - Part 2 puzzle - class Part2 < Part1 - ## - # Find the sum of the powers for the minimum valid sets. - # - # @return [Integer] sum of the powers - def answer - minimum_valid_sets.values.sum(&:power) - end - - ## - # Find the minimum valid set for each game. - # - # @return [Hash] minimum valid sets - def minimum_valid_sets - @minimum_valid_sets ||= games.to_h { |game| [game, minimum_valid_set_for(game:)] } - end - - protected - - COLORS = %i[blue green red].freeze - - ## - # Find the minimum valid set for a game. - # The minimum valid set is the set with the maximum number of - # blue, green, and red cubes for all sets. - # - # @param game [Game] game to find the minimum valid set for - # - # @return [Set] minimum valid set - def minimum_valid_set_for(game:) - # Find the maximum number of blue, green, and red cubes in each set - minimum_set = COLORS.to_h { |color| [color, 0] } - game.sets.each do |set| - COLORS.each do |color| - minimum_set[color] = [set.send(color), minimum_set[color]].max - end - end - Set.new(**minimum_set) - end - end - end - end -end diff --git a/lib/puzzles/2023/day02/README.md b/lib/puzzles/2023/day02/README.md new file mode 100644 index 0000000..58b36a6 --- /dev/null +++ b/lib/puzzles/2023/day02/README.md @@ -0,0 +1,61 @@ +# [Day 2: Cube Conundrum](https://adventofcode.com/2023/day/2) + +## Part One + +You're launched high into the atmosphere! The apex of your trajectory just barely reaches the surface of a large island floating in the sky. You gently land in a fluffy pile of leaves. It's quite cold, but you don't see much snow. An Elf runs over to greet you. + +The Elf explains that you've arrived at **Snow Island** and apologizes for the lack of snow. He'll be happy to explain the situation, but it's a bit of a walk, so you have some time. They don't get many visitors up here; would you like to play a game in the meantime? + +As you walk, the Elf shows you a small bag and some cubes which are either red, green, or blue. Each time you play this game, he will hide a secret number of cubes of each color in the bag, and your goal is to figure out information about the number of cubes. + +To get information, once a bag has been loaded with cubes, the Elf will reach into the bag, grab a handful of random cubes, show them to you, and then put them back in the bag. He'll do this a few times per game. + +You play several games and record the information from each game (your puzzle input). Each game is listed with its ID number (like the `11` in `Game 11: ...`) followed by a semicolon-separated list of subsets of cubes that were revealed from the bag (like `3 red, 5 green, 4 blue`). + +For example, the record of a few games might look like this: + +``` +Game 1: 3 blue, 4 red; 1 red, 2 green, 6 blue; 2 green +Game 2: 1 blue, 2 green; 3 green, 4 blue, 1 red; 1 green, 1 blue +Game 3: 8 green, 6 blue, 20 red; 5 blue, 4 red, 13 green; 5 green, 1 red +Game 4: 1 green, 3 red, 6 blue; 3 green, 6 red; 3 green, 15 blue, 14 red +Game 5: 6 red, 1 blue, 3 green; 2 blue, 1 red, 2 green +``` + +In game 1, three sets of cubes are revealed from the bag (and then put back again). The first set is 3 blue cubes and 4 red cubes; the second set is 1 red cube, 2 green cubes, and 6 blue cubes; the third set is only 2 green cubes. + +The Elf would first like to know which games would have been possible if the bag contained **only 12 red cubes, 13 green cubes, and 14 blue cubes**? + +In the example above, games 1, 2, and 5 would have been **possible** if the bag had been loaded with that configuration. However, game 3 would have been **impossible** because at one point the Elf showed you 20 red cubes at once; similarly, game 4 would also have been **impossible** because the Elf showed you 15 blue cubes at once. If you add up the IDs of the games that would have been possible, you get **`8`**. + +Determine which games would have been possible if the bag had been loaded with only 12 red cubes, 13 green cubes, and 14 blue cubes. **What is the sum of the IDs of those games?** + +Your puzzle answer was `2278`. + +## Part Two + +The Elf says they've stopped producing snow because they aren't getting any **water**! He isn't sure why the water stopped; however, he can show you how to get to the water source to check it out for yourself. It's just up ahead! + +As you continue your walk, the Elf poses a second question: in each game you played, what is the **fewest number of cubes of each color** that could have been in the bag to make the game possible? + +Again consider the example games from earlier: + +``` +Game 1: 3 blue, 4 red; 1 red, 2 green, 6 blue; 2 green +Game 2: 1 blue, 2 green; 3 green, 4 blue, 1 red; 1 green, 1 blue +Game 3: 8 green, 6 blue, 20 red; 5 blue, 4 red, 13 green; 5 green, 1 red +Game 4: 1 green, 3 red, 6 blue; 3 green, 6 red; 3 green, 15 blue, 14 red +Game 5: 6 red, 1 blue, 3 green; 2 blue, 1 red, 2 green +``` + +- In game 1, the game could have been played with as few as 4 red, 2 green, and 6 blue cubes. If any color had even one fewer cube, the game would have been impossible. +- Game 2 could have been played with a minimum of 1 red, 3 green, and 4 blue cubes. +- Game 3 must have been played with at least 20 red, 13 green, and 6 blue cubes. +- Game 4 required at least 14 red, 3 green, and 15 blue cubes. +- Game 5 needed no fewer than 6 red, 3 green, and 2 blue cubes in the bag. + +The **power** of a set of cubes is equal to the numbers of red, green, and blue cubes multiplied together. The power of the minimum set of cubes in game 1 is `48`. In games 2-5 it was `12`, `1560`, `630`, and `36`, respectively. Adding up these five powers produces the sum **`2286`**. + +For each game, find the minimum set of cubes that must have been present. **What is the sum of the power of these sets?** + +Your puzzle answer was `67953`. diff --git a/lib/puzzles/2023/day02/day02.rb b/lib/puzzles/2023/day02/day02.rb new file mode 100644 index 0000000..394b106 --- /dev/null +++ b/lib/puzzles/2023/day02/day02.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +require_relative "part1" +require_relative "part2" + +module AdventOfCode + module Puzzles2023 + ## + # {include:file:lib/puzzles/2023/day02/README.md} + module Day02 + end + end +end diff --git a/lib/puzzles/2023/day02/game.rb b/lib/puzzles/2023/day02/game.rb new file mode 100644 index 0000000..9e9da6a --- /dev/null +++ b/lib/puzzles/2023/day02/game.rb @@ -0,0 +1,47 @@ +# frozen_string_literal: true + +module AdventOfCode + module Puzzles2023 + module Day02 + ## + # Game class to store the game id and its sets. + class Game + ## + # @return [Integer] game id + attr_reader :id + + ## + # @return [Array] sets of cubes + attr_reader :sets + + ## + # @param id [Integer] game id + # @param sets [Array] sets of cubes + def initialize(id:, sets:) + @id = id + @sets = sets + end + + ## + # Check if the game is valid given the number of blue, green, + # and red cubes. + # + # @param blue [Integer] number of blue cubes + # @param green [Integer] number of green cubes + # @param red [Integer] number of red cubes + # + # @return [Boolean] true if the game is valid, false otherwise + def valid?(blue:, green:, red:) + # A game is valid only if every set has the same or less + # number of blue, green, and red cubes as the set it is + sets.each do |set| + return false if set.blue > blue + return false if set.green > green + return false if set.red > red + end + true + end + end + end + end +end diff --git a/test/puzzles/2023/day02/input.txt b/lib/puzzles/2023/day02/input.txt similarity index 100% rename from test/puzzles/2023/day02/input.txt rename to lib/puzzles/2023/day02/input.txt diff --git a/lib/puzzles/2023/day02/part1.rb b/lib/puzzles/2023/day02/part1.rb new file mode 100644 index 0000000..53fe36e --- /dev/null +++ b/lib/puzzles/2023/day02/part1.rb @@ -0,0 +1,94 @@ +# frozen_string_literal: true + +require_relative "game" +require_relative "set" + +module AdventOfCode + module Puzzles2023 + module Day02 + ## + # Class for solving Day 2 - Part 1 puzzle + class Part1 + ## + # Array of games + # @return [Array] the games of the puzzle + attr_reader :games + + ## + # @param file [String] path to the input file + def initialize(file: nil) + file ||= "#{File.dirname(__FILE__)}/input.txt" + file_contents = File.readlines(file, chomp: true) + load_games(file_contents) + end + + ## + # Find games that are valid given the number of blue, green, + # and red cubes. + # + # @param blue [Integer] number of blue cubes + # @param green [Integer] number of green cubes + # @param red [Integer] number of red cubes + # + # @return [Array] valid games + def valid_games(blue:, green:, red:) + games.select { |game| game.valid?(blue:, green:, red:) } + end + + ## + # Find the sum of the game ids that are valid given the number + # of blue, green, and red cubes. + # + # @param blue [Integer] number of blue cubes + # @param green [Integer] number of green cubes + # @param red [Integer] number of red cubes + # + # @return [Integer] sum of the game ids + def answer(blue:, green:, red:) + valid_games(blue:, green:, red:).sum(&:id) + end + + protected + + ## + # Load games from the file contents. + # + # @param file_contents [Array] file contents + # + # @return [Array] games + def load_games(file_contents) + @games = file_contents.map do |line| + id, sets = line.split(": ") + sets = sets.split(%r{;\s+}).map { |set| parse_set(set) } + game_id = parse_game_id(id) + Game.new(id: game_id, sets:) + end + end + + ## + # Parse the game id from a string. + # The string is in the format "Game X". + # + # @param game [String] game id + # + # @return [Integer] game id + def parse_game_id(game) + game.gsub("Game ", "").to_i + end + + ## + # Parse a set of cubes from a string. + # The string is in the format "X blue, Y green, Z red". + # + # @param set [String] set of cubes + # + # @return [Set] set of cubes + def parse_set(set) + colors = set.split(%r{,\s+}).map(&:split) + colors = colors.to_h { |number, color| [color.downcase.to_sym, number.to_i] } + Set.new(**colors) + end + end + end + end +end diff --git a/lib/puzzles/2023/day02/part2.rb b/lib/puzzles/2023/day02/part2.rb new file mode 100644 index 0000000..53361c6 --- /dev/null +++ b/lib/puzzles/2023/day02/part2.rb @@ -0,0 +1,56 @@ +# frozen_string_literal: true + +require_relative "part1" +require_relative "set" + +module AdventOfCode + module Puzzles2023 + module Day02 + ## + # Class for solving Day 02 - Part 2 puzzle + class Part2 < Part1 + ## + # Find the sum of the powers for the minimum valid sets. + # + # @return [Integer] sum of the powers + def answer + minimum_valid_sets.values.sum(&:power) + end + + ## + # Find the minimum valid set for each game. + # + # @return [Hash] minimum valid sets + def minimum_valid_sets + @minimum_valid_sets ||= games.to_h { |game| [game, minimum_valid_set_for(game:)] } + end + + protected + + ## + # Array of colors + # @return [Array] the colors of the cubes + COLORS = %i[blue green red].freeze + + ## + # Find the minimum valid set for a game. + # The minimum valid set is the set with the maximum number of + # blue, green, and red cubes for all sets. + # + # @param game [Game] game to find the minimum valid set for + # + # @return [Set] minimum valid set + def minimum_valid_set_for(game:) + # Find the maximum number of blue, green, and red cubes in each set + minimum_set = COLORS.to_h { |color| [color, 0] } + game.sets.each do |set| + COLORS.each do |color| + minimum_set[color] = [set.send(color), minimum_set[color]].max + end + end + Set.new(**minimum_set) + end + end + end + end +end diff --git a/lib/puzzles/2023/day02/set.rb b/lib/puzzles/2023/day02/set.rb new file mode 100644 index 0000000..93b718c --- /dev/null +++ b/lib/puzzles/2023/day02/set.rb @@ -0,0 +1,44 @@ +# frozen_string_literal: true + +module AdventOfCode + module Puzzles2023 + module Day02 + ## + # Set class to store the number of blue, green, and red cubes. + class Set + ## + # @return [Integer] number of blue cubes + attr_reader :blue + + ## + # @return [Integer] number of green cubes + attr_reader :green + + ## + # @return [Integer] number of red cubes + attr_reader :red + + ## + # @param blue [Integer] number of blue cubes + # @param green [Integer] number of green cubes + # @param red [Integer] number of red cubes + def initialize(blue: 0, green: 0, red: 0) + @blue = blue + @green = green + @red = red + end + + ## + # Compute the power of the set by multiplying the number of + # blue, green, and red cubes. + # + # @return [Integer] power of the set + def power + return @power if @power + + @power = (blue || 1) * (green || 1) * (red || 1) + end + end + end + end +end diff --git a/lib/puzzles/2023/day03/README.md b/lib/puzzles/2023/day03/README.md new file mode 100644 index 0000000..cd15a89 --- /dev/null +++ b/lib/puzzles/2023/day03/README.md @@ -0,0 +1,69 @@ +# [Day 3: Gear Ratios](https://adventofcode.com/2023/day/3) + +## Part One + +You and the Elf eventually reach a [gondola lift](https://en.wikipedia.org/wiki/Gondola_lift) station; he says the gondola lift will take you up to the water source, but this is as far as he can bring you. You go inside. + +It doesn't take long to find the gondolas, but there seems to be a problem: they're not moving. + +"Aaah!" + +You turn around to see a slightly-greasy Elf with a wrench and a look of surprise. "Sorry, I wasn't expecting anyone! The gondola lift isn't working right now; it'll still be a while before I can fix it." You offer to help. + +The engineer explains that an engine part seems to be missing from the engine, but nobody can figure out which one. If you can **add up all the part numbers** in the engine schematic, it should be easy to work out which part is missing. + +The engine schematic (your puzzle input) consists of a visual representation of the engine. There are lots of numbers and symbols you don't really understand, but apparently **any number adjacent to a symbol**, even diagonally, is a "part number" and should be included in your sum. (Periods (`.`) do not count as a symbol.) + +Here is an example engine schematic: + +``` +467..114.. +...*...... +..35..633. +......#... +617*...... +.....+.58. +..592..... +......755. +...$.*.... +.664.598.. +``` + +In this schematic, two numbers are **not** part numbers because they are not adjacent to a symbol: `114` (top right) and `58` (middle right). Every other number is adjacent to a symbol and so **is** a part number; their sum is **`4361`**. + +Of course, the actual engine schematic is much larger. **What is the sum of all of the part numbers in the engine schematic?** + +Your puzzle answer was `532445`. + +## Part Two + +The engineer finds the missing part and installs it in the engine! As the engine springs to life, you jump in the closest gondola, finally ready to ascend to the water source. + +You don't seem to be going very fast, though. Maybe something is still wrong? Fortunately, the gondola has a phone labeled "help", so you pick it up and the engineer answers. + +Before you can explain the situation, she suggests that you look out the window. There stands the engineer, holding a phone in one hand and waving with the other. You're going so slowly that you haven't even left the station. You exit the gondola. + +The missing part wasn't the only issue - one of the gears in the engine is wrong. A **gear** is any `*` symbol that is adjacent to **exactly two part numbers**. Its **gear ratio** is the result of multiplying those two numbers together. + +This time, you need to find the gear ratio of every gear and add them all up so that the engineer can figure out which gear needs to be replaced. + +Consider the same engine schematic again: + +``` +467..114.. +...*...... +..35..633. +......#... +617*...... +.....+.58. +..592..... +......755. +...$.*.... +.664.598.. +``` + +In this schematic, there are **two** gears. The first is in the top left; it has part numbers `467` and `35`, so its gear ratio is `16345`. The second gear is in the lower right; its gear ratio is `451490`. (The `*` adjacent to `617` is **not** a gear because it is only adjacent to one part number.) Adding up all of the gear ratios produces **`467835`**. + +**What is the sum of all of the gear ratios in your engine schematic?** + +Your puzzle answer was `79842967`. diff --git a/lib/puzzles/2023/day03/day03.rb b/lib/puzzles/2023/day03/day03.rb new file mode 100644 index 0000000..00ec0d8 --- /dev/null +++ b/lib/puzzles/2023/day03/day03.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +require_relative "part1" +require_relative "part2" + +module AdventOfCode + module Puzzles2023 + ## + # {include:file:lib/puzzles/2023/day03/README.md} + module Day03 + end + end +end diff --git a/lib/puzzles/2023/day03/gear.rb b/lib/puzzles/2023/day03/gear.rb new file mode 100644 index 0000000..ae092a7 --- /dev/null +++ b/lib/puzzles/2023/day03/gear.rb @@ -0,0 +1,41 @@ +# frozen_string_literal: true + +module AdventOfCode + module Puzzles2023 + module Day03 + ## + # Class for representing a gear + class Gear + ## + # @return [String] symbol of the gear + attr_reader :symbol + + ## + # @return [String] index of the gear + attr_reader :index + + ## + # @return [Array] array of numbers of the gear + attr_reader :numbers + + ## + # @param symbol [String] symbol of the gear + # @param index [String] index of the gear + # @param numbers [Array] array of numbers of the gear + def initialize(symbol:, index:, numbers:) + @symbol = symbol + @index = index + @numbers = numbers + end + + ## + # Get the ratio of the gear. + # + # @return [Integer] ratio of the gear + def ratio + @ratio ||= numbers.reduce(:*) + end + end + end + end +end diff --git a/test/puzzles/2023/day03/input.txt b/lib/puzzles/2023/day03/input.txt similarity index 100% rename from test/puzzles/2023/day03/input.txt rename to lib/puzzles/2023/day03/input.txt diff --git a/lib/puzzles/2023/day03.rb b/lib/puzzles/2023/day03/part1.rb similarity index 65% rename from lib/puzzles/2023/day03.rb rename to lib/puzzles/2023/day03/part1.rb index d370e39..bf747d2 100644 --- a/lib/puzzles/2023/day03.rb +++ b/lib/puzzles/2023/day03/part1.rb @@ -2,50 +2,26 @@ module AdventOfCode module Puzzles2023 - ## - # Advent of Code 2023 - Day 3 - # https://adventofcode.com/2023/day/3 module Day03 ## - # Class for representing a gear - class Gear - attr_reader :symbol, :index, :numbers - - ## - # @param symbol [String] symbol of the gear - # @param index [String] index of the gear - # @param numbers [Array] array of numbers of the gear - def initialize(symbol:, index:, numbers:) - @symbol = symbol - @index = index - @numbers = numbers - end - - ## - # Get the ratio of the gear. - # - # @return [Integer] ratio of the gear - def ratio - @ratio ||= numbers.reduce(:*) - end - end - - ## - # Class for solving Day 03 - Part 1 puzzle + # Class for solving Day 3 - Part 1 puzzle class Part1 ## # An array of hashes with the indexes and numbers of the # numbers in each line. + # @return [Array>] matrix of number indexes and numbers attr_reader :numbers ## # An array of hashes with the indexes of the symbols and # the current symbol in each line. + # @return [Array>] matrix of symbol indexes and symbols attr_reader :symbols ## # @param file [String] path to input file - def initialize(file:) + def initialize(file: nil) + file ||= "#{File.dirname(__FILE__)}/input.txt" file_contents = File.readlines(file, chomp: true) @symbols = extract_symbols(file_contents) @numbers = extract_numbers(file_contents) @@ -53,6 +29,8 @@ def initialize(file:) ## # Get the sum of the engine part numbers. + # + # @return [Integer] sum of the engine part numbers def answer part_numbers.sum end @@ -161,66 +139,6 @@ def adjacent_rows_symbols(symbols, row_index) symbols.values_at(*adjacent_symbol_rows).map(&:keys).flatten.uniq end end - - ## - # Class for solving Day 03 - Part 2 puzzle - class Part2 < Part1 - ## - # Array of Gear objects - attr_reader :gears - - ## - # @param file [String] path to input file - def initialize(file:) - super - @gears = find_gears - end - - ## - # Get the sum of the ratios of the gears. - def answer - gears.sum(&:ratio) - end - - protected - - def find_gears - gears = [] - symbols.each_with_index do |row_symbols, row_index| - row_symbols.each do |symbol_index, symbol| - adjacent_numbers = find_adjacent_numbers(row_index, symbol_index) - if adjacent_numbers.length == 2 - gears << Gear.new(symbol:, index: "#{row_index}:#{symbol_index}", numbers: adjacent_numbers) - end - end - end - gears - end - - ## - # Find the numbers next to the given symbol. - # - # @param row_index [Integer] index of the row to check - # @param symbol_index [Integer] index of the symbol to check - # - # @return [Array] array of numbers next to the symbol - def find_adjacent_numbers(row_index, symbol_index) - # Adjacent rows - adjacent_number_rows = adjacent_rows_range(row_index, numbers.length - 1) - - # Check numbers next to the current symbol - adjacent_numbers = [] - adjacent_number_rows.each do |number_row_idx| - numbers[number_row_idx].each do |number_idx, number| - # Indexes occupied by the current number - number_limits = number_idx - 1..number_idx + number.length - adjacent_numbers << number.to_i if number_limits.include?(symbol_index) - end - end - - adjacent_numbers - end - end end end end diff --git a/lib/puzzles/2023/day03/part2.rb b/lib/puzzles/2023/day03/part2.rb new file mode 100644 index 0000000..298071c --- /dev/null +++ b/lib/puzzles/2023/day03/part2.rb @@ -0,0 +1,77 @@ +# frozen_string_literal: true + +require_relative "part1" +require_relative "gear" + +module AdventOfCode + module Puzzles2023 + module Day03 + ## + # Class for solving Day 3 - Part 2 puzzle + class Part2 < Part1 + ## + # Array of Gear objects + # @return [Array] array of gears + attr_reader :gears + + ## + # @param file [String] path to input file + def initialize(file: nil) + super(file:) + @gears = find_gears + end + + ## + # Get the sum of the ratios of the gears. + # + # @return [Integer] sum of the ratios of the gears + def answer + gears.sum(&:ratio) + end + + protected + + ## + # Find the gears in the puzzle. + # + # @return [Array] array of gears + def find_gears + gears = [] + symbols.each_with_index do |row_symbols, row_index| + row_symbols.each do |symbol_index, symbol| + adjacent_numbers = find_adjacent_numbers(row_index, symbol_index) + if adjacent_numbers.length == 2 + gears << Gear.new(symbol:, index: "#{row_index}:#{symbol_index}", numbers: adjacent_numbers) + end + end + end + gears + end + + ## + # Find the numbers next to the given symbol. + # + # @param row_index [Integer] index of the row to check + # @param symbol_index [Integer] index of the symbol to check + # + # @return [Array] array of numbers next to the symbol + def find_adjacent_numbers(row_index, symbol_index) + # Adjacent rows + adjacent_number_rows = adjacent_rows_range(row_index, numbers.length - 1) + + # Check numbers next to the current symbol + adjacent_numbers = [] + adjacent_number_rows.each do |number_row_idx| + numbers[number_row_idx].each do |number_idx, number| + # Indexes occupied by the current number + number_limits = number_idx - 1..number_idx + number.length + adjacent_numbers << number.to_i if number_limits.include?(symbol_index) + end + end + + adjacent_numbers + end + end + end + end +end diff --git a/lib/puzzles/2023/day04.rb b/lib/puzzles/2023/day04.rb deleted file mode 100644 index da6c206..0000000 --- a/lib/puzzles/2023/day04.rb +++ /dev/null @@ -1,139 +0,0 @@ -# frozen_string_literal: true - -module AdventOfCode - module Puzzles2023 - ## - # Advent of Code 2023 - Day 4 - # https://adventofcode.com/2023/day/4 - module Day04 - ## - # Class for representing a card - class Card - attr_reader :id, :owned, :winning - - ## - # @param id [Integer] id of the card - # @param owned [Array] array of owned numbers - # @param winning [Array] array of winning numbers - def initialize(id:, owned:, winning:) - @id = id - @owned = owned - @winning = winning - end - - ## - # Get the matches of the card. - # That is, the numbers that are both owned and winning. - # - # @return [Array] array of matches - def matches - return @matches unless @matches.nil? - - @matches = winning.intersection(owned) - end - end - - ## - # Class for solving Day 04 - Part 1 puzzle - class Part1 - attr_reader :cards - - ## - # @param file [String] path to input file - def initialize(file:) - file_contents = File.readlines(file, chomp: true) - @cards = extract_cards(file_contents) - end - - ## - # Get the sum of the points of the cards. - def answer - points.sum - end - - ## - # Get the points of the cards. - # - # The points are calculated as 2^(n-1), where n is the number - # of matches of the card. - # If the card has no matches, it gets 0 points. - # - # @return [Array] array of points - def points - cards.map do |card| - matches = card.matches.length - matches.zero? ? 0 : (2**(matches - 1)).to_i - end - end - - protected - - ## - # Method to extract cards from file contents. - # - # @param file_contents [Array] array of file lines - # - # @return [Array] array of cards - def extract_cards(file_contents) - cards = [] - file_contents.each do |line| - match = %r{Card\s+(?\d+): (?[0-9 ]+) \| (?[0-9 ]+)}.match(line) - id = match[:id].to_i - owned = match[:owned].split.map(&:to_i) - winning = match[:winning].split.map(&:to_i) - cards << Card.new(id:, owned:, winning:) - end - cards - end - end - - ## - # Class for solving Day 04 - Part 2 puzzle - class Part2 < Part1 - ## - # Get the total number of cards. - # - # @return [Integer] total number of cards - def answer - total_cards.sum { |c| c[:copies] } - end - - ## - # Array of cards, with each card having a copies attribute. - # - # @return [Array>] array of cards with copies counter - def total_cards - # Initialize every element of the array with one copy of each card - cards_h = cards.map { |card| { card:, copies: 1 } } - - # Count cards - cards_h.each do |item| - # This is the number of copies of the card that we already have - current_copies = item[:copies] - - # New cards that are won by the card - won_cards = get_matching_cards(card: item[:card]) - won_cards.each do |won_card| - cards_h[won_card.id - 1][:copies] += current_copies - end - end - - cards_h - end - - protected - - ## - # Get the cards that are won by the card. - # That is, one copy of the following `n` cards, where `n` is the - # number of matches of the card. - # - # @return [Array] array of card ids - def get_matching_cards(card:) - number_of_copies = card.matches.length - cards[card.id, number_of_copies] || [] - end - end - end - end -end diff --git a/lib/puzzles/2023/day04/README.md b/lib/puzzles/2023/day04/README.md new file mode 100644 index 0000000..bfcd4f2 --- /dev/null +++ b/lib/puzzles/2023/day04/README.md @@ -0,0 +1,77 @@ +# [Day 4: Scratchcards](https://adventofcode.com/2023/day/4) + +## Part One + +The gondola takes you up. Strangely, though, the ground doesn't seem to be coming with you; you're not climbing a mountain. As the circle of Snow Island recedes below you, an entire new landmass suddenly appears above you! The gondola carries you to the surface of the new island and lurches into the station. + +As you exit the gondola, the first thing you notice is that the air here is much **warmer** than it was on Snow Island. It's also quite **humid**. Is this where the water source is? + +The next thing you notice is an Elf sitting on the floor across the station in what seems to be a pile of colorful square cards. + +"Oh! Hello!" The Elf excitedly runs over to you. "How may I be of service?" You ask about water sources. + +"I'm not sure; I just operate the gondola lift. That does sound like something we'd have, though - this is **Island Island**, after all! I bet the **gardener** would know. He's on a different island, though - er, the small kind surrounded by water, not the floating kind. We really need to come up with a better naming scheme. Tell you what: if you can help me with something quick, I'll let you **borrow my boat** and you can go visit the gardener. I got all these [scratchcards](https://en.wikipedia.org/wiki/Scratchcard) as a gift, but I can't figure out what I've won." + +The Elf leads you over to the pile of colorful cards. There, you discover dozens of scratchcards, all with their opaque covering already scratched off. Picking one up, it looks like each card has two lists of numbers separated by a vertical bar (|): a list of **winning numbers** and then a list of **numbers you have**. You organize the information into a table (your puzzle input). + +As far as the Elf has been able to figure out, you have to figure out which of the **numbers you have** appear in the list of **winning numbers**. The first match makes the card worth **one point** and each match after the first **doubles** the point value of that card. + +For example: + +``` +Card 1: 41 48 83 86 17 | 83 86 6 31 17 9 48 53 +Card 2: 13 32 20 16 61 | 61 30 68 82 17 32 24 19 +Card 3: 1 21 53 59 44 | 69 82 63 72 16 21 14 1 +Card 4: 41 92 73 84 69 | 59 84 76 51 58 5 54 83 +Card 5: 87 83 26 28 32 | 88 30 70 12 93 22 82 36 +Card 6: 31 18 13 56 72 | 74 77 10 23 35 67 36 11 +``` + +In the above example, card 1 has five winning numbers (`41`, `48`, `83`, `86`, and `17`) and eight numbers you have (`83`, `86`, `6`, `31`, `17`, `9`, `48`, and `53`). Of the numbers you have, four of them (`48`, `83`, `17`, and `86`) are winning numbers! That means card 1 is worth **`8`** points (1 for the first match, then doubled three times for each of the three matches after the first). + +- Card 2 has two winning numbers (`32` and `61`), so it is worth **`2`** points. +- Card 3 has two winning numbers (`1` and `21`), so it is worth **`2`** points. +- Card 4 has one winning number (`84`), so it is worth 1 point. +- Card 5 has no winning numbers, so it is worth no points. +- Card 6 has no winning numbers, so it is worth no points. + +So, in this example, the Elf's pile of scratchcards is worth **`13`** points. + +Take a seat in the large pile of colorful cards. **How many points are they worth in total?** + +Your puzzle answer was `22897`. + +## Part Two + +Just as you're about to report your findings to the Elf, one of you realizes that the rules have actually been printed on the back of every card this whole time. + +There's no such thing as "points". Instead, scratchcards only cause you to **win more scratchcards** equal to the number of winning numbers you have. + +Specifically, you win **copies** of the scratchcards below the winning card equal to the number of matches. So, if card 10 were to have 5 matching numbers, you would win one copy each of cards 11, 12, 13, 14, and 15. + +Copies of scratchcards are scored like normal scratchcards and have the **same card number** as the card they copied. So, if you win a copy of card 10 and it has 5 matching numbers, it would then win a copy of the same cards that the original card 10 won: cards 11, 12, 13, 14, and 15. This process repeats until none of the copies cause you to win any more cards. (Cards will never make you copy a card past the end of the table.) + +This time, the above example goes differently: + +``` +Card 1: 41 48 83 86 17 | 83 86 6 31 17 9 48 53 +Card 2: 13 32 20 16 61 | 61 30 68 82 17 32 24 19 +Card 3: 1 21 53 59 44 | 69 82 63 72 16 21 14 1 +Card 4: 41 92 73 84 69 | 59 84 76 51 58 5 54 83 +Card 5: 87 83 26 28 32 | 88 30 70 12 93 22 82 36 +Card 6: 31 18 13 56 72 | 74 77 10 23 35 67 36 11 +``` + +- Card 1 has four matching numbers, so you win one copy each of the next four cards: cards 2, 3, 4, and 5. +- Your original card 2 has two matching numbers, so you win one copy each of cards 3 and 4. +- Your copy of card 2 also wins one copy each of cards 3 and 4. +- Your four instances of card 3 (one original and three copies) have two matching numbers, so you win **four** copies each of cards 4 and 5. +- Your eight instances of card 4 (one original and seven copies) have one matching number, so you win **eight** copies of card 5. +- Your fourteen instances of card 5 (one original and thirteen copies) have no matching numbers and win no more cards. +- Your one instance of card 6 (one original) has no matching numbers and wins no more cards. + +Once all of the originals and copies have been processed, you end up with **`1`** instance of card 1, **`2`** instances of card 2, **`4`** instances of card 3, **`8`** instances of card 4, **`14`** instances of card 5, and **`1`** instance of card 6. In total, this example pile of scratchcards causes you to ultimately have **`30`** scratchcards! + +Process all of the original and copied scratchcards until no more scratchcards are won. Including the original set of scratchcards, **how many total scratchcards do you end up with?** + +Your puzzle answer was `5095824`. diff --git a/lib/puzzles/2023/day04/card.rb b/lib/puzzles/2023/day04/card.rb new file mode 100644 index 0000000..27f0e33 --- /dev/null +++ b/lib/puzzles/2023/day04/card.rb @@ -0,0 +1,44 @@ +# frozen_string_literal: true + +module AdventOfCode + module Puzzles2023 + module Day04 + ## + # Class for representing a card + class Card + ## + # @return [Integer] id of the card + attr_reader :id + + ## + # @return [Array] array of owned numbers + attr_reader :owned + + ## + # @return [Array] array of winning numbers + attr_reader :winning + + ## + # @param id [Integer] id of the card + # @param owned [Array] array of owned numbers + # @param winning [Array] array of winning numbers + def initialize(id:, owned:, winning:) + @id = id + @owned = owned + @winning = winning + end + + ## + # Get the matches of the card. + # That is, the numbers that are both owned and winning. + # + # @return [Array] array of matches + def matches + return @matches unless @matches.nil? + + @matches = winning.intersection(owned) + end + end + end + end +end diff --git a/lib/puzzles/2023/day04/day04.rb b/lib/puzzles/2023/day04/day04.rb new file mode 100644 index 0000000..270c4f5 --- /dev/null +++ b/lib/puzzles/2023/day04/day04.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +require_relative "part1" +require_relative "part2" + +module AdventOfCode + module Puzzles2023 + ## + # {include:file:lib/puzzles/2023/day04/README.md} + module Day04 + end + end +end diff --git a/test/puzzles/2023/day04/input.txt b/lib/puzzles/2023/day04/input.txt similarity index 100% rename from test/puzzles/2023/day04/input.txt rename to lib/puzzles/2023/day04/input.txt diff --git a/lib/puzzles/2023/day04/part1.rb b/lib/puzzles/2023/day04/part1.rb new file mode 100644 index 0000000..b83c40e --- /dev/null +++ b/lib/puzzles/2023/day04/part1.rb @@ -0,0 +1,64 @@ +# frozen_string_literal: true + +require_relative "card" + +module AdventOfCode + module Puzzles2023 + module Day04 + ## + # Class for solving Day 04 - Part 1 puzzle + class Part1 + ## + # @return [Array] array of cards + attr_reader :cards + + ## + # @param file [String] path to input file + def initialize(file: nil) + file ||= "#{File.dirname(__FILE__)}/input.txt" + file_contents = File.readlines(file, chomp: true) + parse_cards(file_contents) + end + + ## + # Get the sum of the points of the cards. + def answer + points.sum + end + + ## + # Get the points of the cards. + # + # The points are calculated as 2^(n-1), where n is the number + # of matches of the card. + # If the card has no matches, it gets 0 points. + # + # @return [Array] array of points + def points + cards.map do |card| + matches = card.matches.length + matches.zero? ? 0 : (2**(matches - 1)).to_i + end + end + + protected + + ## + # Method to parse cards from file contents. + # + # @param file_contents [Array] array of file lines + # + # @return [Array] array of cards + def parse_cards(file_contents) + @cards = file_contents.map do |line| + match = %r{Card\s+(?\d+): (?[0-9 ]+) \| (?[0-9 ]+)}.match(line) + id = match[:id].to_i + owned = match[:owned].split.map(&:to_i) + winning = match[:winning].split.map(&:to_i) + Card.new(id:, owned:, winning:) + end + end + end + end + end +end diff --git a/lib/puzzles/2023/day04/part2.rb b/lib/puzzles/2023/day04/part2.rb new file mode 100644 index 0000000..5acf7ad --- /dev/null +++ b/lib/puzzles/2023/day04/part2.rb @@ -0,0 +1,57 @@ +# frozen_string_literal: true + +require_relative "part1" + +module AdventOfCode + module Puzzles2023 + module Day04 + ## + # Class for solving Day 04 - Part 2 puzzle + class Part2 < Part1 + ## + # Get the total number of cards. + # + # @return [Integer] total number of cards + def answer + total_cards.sum { |c| c[:copies] } + end + + ## + # Array of cards, with each card having a copies attribute. + # + # @return [Array>] array of cards with copies counter + def total_cards + # Initialize every element of the array with one copy of each card + cards_h = cards.map { |card| { card:, copies: 1 } } + + # Count cards + cards_h.each do |item| + # This is the number of copies of the card that we already have + current_copies = item[:copies] + + # New cards that are won by the card + won_cards = get_matching_cards(card: item[:card]) + won_cards.each do |won_card| + cards_h[won_card.id - 1][:copies] += current_copies + end + end + + cards_h + end + + protected + + ## + # Get the cards that are won by the card. + # That is, one copy of the following `n` cards, where `n` is the + # number of matches of the card. + # + # @return [Array] array of card ids + def get_matching_cards(card:) + number_of_copies = card.matches.length + cards[card.id, number_of_copies] || [] + end + end + end + end +end diff --git a/lib/puzzles/2023/day05.rb b/lib/puzzles/2023/day05.rb deleted file mode 100644 index b31dda7..0000000 --- a/lib/puzzles/2023/day05.rb +++ /dev/null @@ -1,299 +0,0 @@ -# frozen_string_literal: true - -module AdventOfCode - module Puzzles2023 - ## - # Advent of Code 2023 - Day 5 - # https://adventofcode.com/2023/day/5 - module Day05 - ## - # Class for representing a relation. - class Relation - ## - # @param source [Range] range of source values - # @param destination [Range] range of destination values - def initialize(source:, destination:) - @source = source - @destination = destination - end - - ## - # Convert a source value to destination value. - # If value is not in source range, return nil. - # - # @param value [Integer] source value to convert - # - # @return [Integer, nil] destination value or nil if value is not in source range - def convert(value:) - # Is value in source range? - return unless @source.include?(value) - - # Then, convert to destination range - @destination.begin + (value - @source.begin) - end - end - - ## - # Class for converting values from source to destination. - class Converter - attr_reader :source, :destination - - ## - # @param source [Symbol] source of the conversion - # @param destination [Symbol] destination of the conversion - # @param relations [Array] array of relations - def initialize(source:, destination:, relations:) - @source = source - @destination = destination - @relations = relations - end - - ## - # Convert a value from source to destination. - # - # @param value [Integer] value to convert - # - # @return [Integer] converted value - def convert(value:) - # Find value in maps - @relations.each do |relation| - if (result = relation.convert(value:)) - return result - end - end - value - end - end - - ## - # Class for representing an almanac. - # An almanac is a collection of converters. - class Almanac - ## - # @param file [String] path to input file - # @param reverse [Boolean] if true, the conversion is done from destination to source - def initialize(file:, reverse: false) - @targets = reverse ? %i[destination source] : %i[source destination] - parse_file(file) - end - - ## - # Convert a source value to a destination. - # - # @param value [Integer] value to convert - # @param from [Symbol] source of the conversion - # @param to [Symbol] destination of the conversion - # - # @return [Integer] converted value - def convert(value:, from:, to:) - converter = converters[from] - value = converter.convert(value:) - return value if converter.destination == to - - convert(value:, from: converter.destination, to:) - end - - protected - - ## - # Hash of converters. - # Keys are source symbols, values are Converter objects. - # - # @return [Hash] hash of converters - attr_reader :converters - - ## - # Contains symbols: :source, :destination, - # ordered by the direction of the conversion. - # - # @return [Array] array of symbols - attr_reader :targets - - ## - # Build the converters from the file. - # - # @param file [String] path to input file - def parse_file(file) - file_contents = File.readlines(file, chomp: true) - - # Remove seeds line - file_contents.shift - - parse_maps(file_contents) - end - - ## - # Parse a header line. - # A header line is a line that contains the source and destination of the conversion. - # It is in the form: source-to-destination map: - # If line is not a header line, return nil. - # - # @param line [String] line to parse - # - # @return [Array, nil] array of symbols or nil if line is not a header line - def parse_header(line) - match = line.match(%r{(?\w+)-to-(?\w+) map:$}) - return if match.nil? - - targets.map { |target| match[target] }.map(&:to_sym) - end - - ## - # Parse a map line. - # - # @param line [String] line to parse - # - # @return [Relation] relation - def parse_map(line) - destination_start, source_start, length = line.split.map(&:to_i) - relation = { - targets.first => source_start...source_start + length, - targets.last => destination_start...destination_start + length - } - Relation.new(**relation) - end - - ## - # Parse the maps from the file contents. - # A map is a set of relations. - # - # @param lines [Array] array of lines - def parse_maps(lines) - source = nil - destination = nil - relations = [] - @converters = {} - - # Append empty line to force save last conversion - lines << "" - - lines.each do |line| - if line.empty? - # Save conversion if source - @converters[source] = Converter.new(source:, destination:, relations:) unless source.nil? - - # Reset variables - source = nil - destination = nil - relations = [] - elsif (header = parse_header(line)) - # Is header - source, destination = header - else - # Is a map - relations << parse_map(line) - end - end - end - end - - ## - # Class for solving Day 5 - Part 1 puzzle - class Part1 - attr_reader :seeds - - ## - # @param file [String] path to input file - def initialize(file:) - seeds_line = File.readlines(file, chomp: true).first || "" - parse_seeds(seeds_line) - build_almanac(file:) - end - - ## - # Get the minimum location among the seeds. - # - # @return [Integer, nil] minimum location - def answer - locations = seeds.map do |seed| - almanac.convert(value: seed, from: :seed, to: :location) - end - locations.min - end - - protected - - attr_reader :almanac - - ## - # Build the almanac from the file. - # - # @param file [String] path to input file - def build_almanac(file:) - @almanac = Almanac.new(file:) - end - - ## - # Parse the seeds from the seeds line. - # Seeds are in the form: seeds: ... - # - # @param line [String] seeds line - # - # @raise [RuntimeError] if seeds line is invalid - def parse_seeds(line) - match = line.match(%r{^seeds: (?[0-9 ]+)$}) - raise "Invalid seeds line: #{line}" if match.nil? - - @seeds = match[:seeds].split.map(&:to_i) || [] - end - end - - ## - # Class for solving Day 5 - Part 2 puzzle - class Part2 < Part1 - ## - # Get the minimum location that is associated with a valid seed. - # - # @return [Integer, nil] minimum location - def answer - location = 0 - loop do - seed = almanac.convert(value: location, from: :location, to: :seed) - return location if valid_seed?(value: seed) - - location += 1 - end - end - - protected - - ## - # Build the almanac from the file. - # - # @param file [String] path to input file - def build_almanac(file:) - @almanac = Almanac.new(file:, reverse: true) - end - - ## - # Parse the seeds from the seeds line. - # Seeds are in the form: seeds: ... - # is the first seed, is the number of seeds. - # - # @param line [String] seeds line - def parse_seeds(line) - match = line.match(%r{^seeds: \b\d+\s+\d+\b}) - raise "Invalid seeds line: #{line}" if match.nil? - - @seeds = [] - line.scan(%r{\b(?\d+)\s+(?\d+)\b}).each do |start, length| - from = start.to_i - to = from + length.to_i - @seeds << (from...to) - end - end - - ## - # Check if a value is a valid seed. - # A valid seed is a seed that is in the seeds ranges. - # - # @param value [Integer] value to check - # - # @return [Boolean] true if value is a valid seed, false otherwise - def valid_seed?(value:) - seeds.any? { |seed_range| seed_range.include?(value) } - end - end - end - end -end diff --git a/lib/puzzles/2023/day05/README.md b/lib/puzzles/2023/day05/README.md new file mode 100644 index 0000000..1013443 --- /dev/null +++ b/lib/puzzles/2023/day05/README.md @@ -0,0 +1,130 @@ +# [Day 5: If You Give A Seed A Fertilizer](https://adventofcode.com/2023/day/5) + +## Part One + +You take the boat and find the gardener right where you were told he would be: managing a giant "garden" that looks more to you like a farm. + +"A water source? Island Island **is** the water source!" You point out that Snow Island isn't receiving any water. + +"Oh, we had to stop the water because we **ran out of sand** to [filter](https://en.wikipedia.org/wiki/Sand_filter) it with! Can't make snow with dirty water. Don't worry, I'm sure we'll get more sand soon; we only turned off the water a few days... weeks... oh no." His face sinks into a look of horrified realization. + +"I've been so busy making sure everyone here has food that I completely forgot to check why we stopped getting more sand! There's a ferry leaving soon that is headed over in that direction - it's much faster than your boat. Could you please go check it out?" + +You barely have time to agree to this request when he brings up another. "While you wait for the ferry, maybe you can help us with our **food production problem**. The latest Island Island [Almanac](https://en.wikipedia.org/wiki/Almanac) just arrived and we're having trouble making sense of it." + +The almanac (your puzzle input) lists all of the seeds that need to be planted. It also lists what type of soil to use with each kind of seed, what type of fertilizer to use with each kind of soil, what type of water to use with each kind of fertilizer, and so on. Every type of seed, soil, fertilizer and so on is identified with a number, but numbers are reused by each category - that is, soil `123` and fertilizer `123` aren't necessarily related to each other. + +For example: + +``` +seeds: 79 14 55 13 + +seed-to-soil map: +50 98 2 +52 50 48 + +soil-to-fertilizer map: +0 15 37 +37 52 2 +39 0 15 + +fertilizer-to-water map: +49 53 8 +0 11 42 +42 0 7 +57 7 4 + +water-to-light map: +88 18 7 +18 25 70 + +light-to-temperature map: +45 77 23 +81 45 19 +68 64 13 + +temperature-to-humidity map: +0 69 1 +1 0 69 + +humidity-to-location map: +60 56 37 +56 93 4 +``` + +The almanac starts by listing which seeds need to be planted: seeds `79`, `14`, `55`, and `13`. + +The rest of the almanac contains a list of **maps** which describe how to convert numbers from a **source category** into numbers in a **destination category**. That is, the section that starts with `seed-to-soil map:` describes how to convert a **seed number** (the source) to a **soil number** (the destination). This lets the gardener and his team know which soil to use with which seeds, which water to use with which fertilizer, and so on. + +Rather than list every source number and its corresponding destination number one by one, the maps describe entire **ranges** of numbers that can be converted. Each line within a map contains three numbers: the **destination range start**, the **source range start**, and the **range length**. + +Consider again the example `seed-to-soil map:` + +``` +50 98 2 +52 50 48 +``` + +The first line has a **destination range start** of `50`, a **source range start** of `98`, and a **range length** of `2`. This line means that the source range starts at `98` and contains two values: `98` and `99`. The destination range is the same length, but it starts at `50`, so its two values are `50` and `51`. With this information, you know that seed number `98` corresponds to soil number `50` and that seed number `99` corresponds to soil number `51`. + +The second line means that the source range starts at `50` and contains `48` values: `50`, `51`, ..., `96`, `97`. This corresponds to a destination range starting at `52` and also containing `48` values: `52`, `53`, ..., `98`, `99`. So, seed number `53` corresponds to soil number `55`. + +Any source numbers that **aren't mapped** correspond to the **same** destination number. So, seed number `10` corresponds to soil number `10`. + +So, the entire list of seed numbers and their corresponding soil numbers looks like this: + +``` +seed soil +0 0 +1 1 +... ... +48 48 +49 49 +50 52 +51 53 +... ... +96 98 +97 99 +98 50 +99 51 +``` + +With this map, you can look up the soil number required for each initial seed number: + +- Seed number `79` corresponds to soil number `81`. +- Seed number `14` corresponds to soil number `14`. +- Seed number `55` corresponds to soil number `57`. +- Seed number `13` corresponds to soil number `13`. + +The gardener and his team want to get started as soon as possible, so they'd like to know the closest location that needs a seed. Using these maps, find **the lowest location number that corresponds to any of the initial seeds**. To do this, you'll need to convert each seed number through other categories until you can find its corresponding **location number**. In this example, the corresponding types are: + +- Seed `79`, soil `81`, fertilizer `81`, water `81`, light `74`, temperature `78`, humidity `78`, **location `82`**. +- Seed `14`, soil `14`, fertilizer `53`, water `49`, light `42`, temperature `42`, humidity `43`, **location `43`**. +- Seed `55`, soil `57`, fertilizer `57`, water `53`, light `46`, temperature `82`, humidity `82`, **location `86`**. +- Seed `13`, soil `13`, fertilizer `52`, water `41`, light `34`, temperature `34`, humidity `35`, **location `35`**. + +So, the lowest location number in this example is **`35`**. + +**What is the lowest location number that corresponds to any of the initial seed numbers?** + +Your puzzle answer was `318728750`. + +## Part Two + +Everyone will starve if you only plant such a small number of seeds. Re-reading the almanac, it looks like the `seeds:` line actually describes **ranges of seed numbers**. + +The values on the initial `seeds:` line come in pairs. Within each pair, the first value is the **start** of the range and the second value is the **length** of the range. So, in the first line of the example above: + +``` +seeds: 79 14 55 13 +``` + +This line describes two ranges of seed numbers to be planted in the garden. The first range starts with seed number `79` and contains `14` values: `79`, `80`, ..., `91`, `92`. The second range starts with seed number `55` and contains `13` values: `55`, `56`, ..., `66`, `67`. + +Now, rather than considering four seed numbers, you need to consider a total of **`27`** seed numbers. + +In the above example, the lowest location number can be obtained from seed number `82`, which corresponds to soil `84`, fertilizer `84`, water `84`, light `77`, temperature `45`, humidity `46`, and **location `46`**. So, the lowest location number is **`46`**. + +Consider all of the initial seed numbers listed in the ranges on the first line of the almanac. **What is the lowest location number that corresponds to any of the initial seed numbers?** + +Your puzzle answer was `37384986`. diff --git a/lib/puzzles/2023/day05/almanac.rb b/lib/puzzles/2023/day05/almanac.rb new file mode 100644 index 0000000..2fedc04 --- /dev/null +++ b/lib/puzzles/2023/day05/almanac.rb @@ -0,0 +1,132 @@ +# frozen_string_literal: true + +require_relative "converter" +require_relative "relation" + +module AdventOfCode + module Puzzles2023 + module Day05 + ## + # Class for representing an almanac. + # An almanac is a collection of converters. + class Almanac + ## + # @param file [String] path to input file + # @param reverse [Boolean] if true, the conversion is done from destination to source + def initialize(file:, reverse: false) + @targets = reverse ? %i[destination source] : %i[source destination] + parse_file(file) + end + + ## + # Convert a source value to a destination. + # + # @param value [Integer] value to convert + # @param from [Symbol] source of the conversion + # @param to [Symbol] destination of the conversion + # + # @return [Integer] converted value + def convert(value:, from:, to:) + converter = converters[from] + value = converter.convert(value:) + return value if converter.destination == to + + convert(value:, from: converter.destination, to:) + end + + protected + + ## + # Hash of converters. + # Keys are source symbols, values are Converter objects. + # + # @return [Hash] hash of converters + attr_reader :converters + + ## + # Contains symbols: :source, :destination, + # ordered by the direction of the conversion. + # + # @return [Array] array of symbols + attr_reader :targets + + ## + # Build the converters from the file. + # + # @param file [String] path to input file + def parse_file(file) + file_contents = File.readlines(file, chomp: true) + + # Remove seeds line + file_contents.shift + + parse_maps(file_contents) + end + + ## + # Parse a header line. + # A header line is a line that contains the source and destination of the conversion. + # It is in the form: source-to-destination map: + # If line is not a header line, return nil. + # + # @param line [String] line to parse + # + # @return [Array, nil] array of symbols or nil if line is not a header line + def parse_header(line) + match = line.match(%r{(?\w+)-to-(?\w+) map:$}) + return if match.nil? + + targets.map { |target| match[target] }.map(&:to_sym) + end + + ## + # Parse a map line. + # + # @param line [String] line to parse + # + # @return [Relation] relation + def parse_map(line) + destination_start, source_start, length = line.split.map(&:to_i) + relation = { + targets.first => source_start...source_start + length, + targets.last => destination_start...destination_start + length + } + Relation.new(**relation) + end + + ## + # Parse the maps from the file contents. + # A map is a set of relations. + # + # @param lines [Array] array of lines + def parse_maps(lines) + source = nil + destination = nil + relations = [] + @converters = {} + + # Append empty line to force save last conversion + lines << "" + + lines.each do |line| + if line.empty? + # Save conversion if source + @converters[source] = Converter.new(source:, destination:, relations:) unless source.nil? + + # Reset variables + source = nil + destination = nil + relations = [] + elsif (header = parse_header(line)) + # Is header + source, destination = header + else + # Is a map + relations << parse_map(line) + end + end + end + end + end + end +end diff --git a/lib/puzzles/2023/day05/converter.rb b/lib/puzzles/2023/day05/converter.rb new file mode 100644 index 0000000..ab4bf10 --- /dev/null +++ b/lib/puzzles/2023/day05/converter.rb @@ -0,0 +1,45 @@ +# frozen_string_literal: true + +module AdventOfCode + module Puzzles2023 + module Day05 + ## + # Class for converting values from source to destination. + class Converter + ## + # @return [Symbol] source of the conversion + attr_reader :source + + ## + # @return [Symbol] destination of the conversion + attr_reader :destination + + ## + # @param source [Symbol] source of the conversion + # @param destination [Symbol] destination of the conversion + # @param relations [Array] array of relations + def initialize(source:, destination:, relations:) + @source = source + @destination = destination + @relations = relations + end + + ## + # Convert a value from source to destination. + # + # @param value [Integer] value to convert + # + # @return [Integer] converted value + def convert(value:) + # Find value in maps + @relations.each do |relation| + if (result = relation.convert(value:)) + return result + end + end + value + end + end + end + end +end diff --git a/lib/puzzles/2023/day05/day05.rb b/lib/puzzles/2023/day05/day05.rb new file mode 100644 index 0000000..2eaa0b5 --- /dev/null +++ b/lib/puzzles/2023/day05/day05.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +require_relative "part1" +require_relative "part2" + +module AdventOfCode + module Puzzles2023 + ## + # {include:file:lib/puzzles/2023/day05/README.md} + module Day05 + end + end +end diff --git a/test/puzzles/2023/day05/input.txt b/lib/puzzles/2023/day05/input.txt similarity index 100% rename from test/puzzles/2023/day05/input.txt rename to lib/puzzles/2023/day05/input.txt diff --git a/lib/puzzles/2023/day05/part1.rb b/lib/puzzles/2023/day05/part1.rb new file mode 100644 index 0000000..cb7c59e --- /dev/null +++ b/lib/puzzles/2023/day05/part1.rb @@ -0,0 +1,62 @@ +# frozen_string_literal: true + +require_relative "almanac" + +module AdventOfCode + module Puzzles2023 + module Day05 + ## + # Class for solving Day 5 - Part 1 puzzle + class Part1 + attr_reader :seeds + + ## + # @param file [String] path to input file + def initialize(file: nil) + file ||= "#{File.dirname(__FILE__)}/input.txt" + seeds_line = File.readlines(file, chomp: true).first || "" + parse_seeds(seeds_line) + build_almanac(file:) + end + + ## + # Get the minimum location among the seeds. + # + # @return [Integer, nil] minimum location + def answer + locations = seeds.map do |seed| + almanac.convert(value: seed, from: :seed, to: :location) + end + locations.min + end + + protected + + attr_reader :almanac + + ## + # Build the almanac from the file. + # + # @param file [String] path to input file + def build_almanac(file:) + @almanac = Almanac.new(file:) + end + + ## + # Parse the seeds from the seeds line. + # Seeds are in the form: seeds: ... + # + # @param line [String] seeds line + # + # @raise [RuntimeError] if seeds line is invalid + def parse_seeds(line) + match = line.match(%r{^seeds: (?[0-9 ]+)$}) + raise "Invalid seeds line: #{line}" if match.nil? + + matched_seeds = match[:seeds] || "" + @seeds = matched_seeds.split.map(&:to_i) + end + end + end + end +end diff --git a/lib/puzzles/2023/day05/part2.rb b/lib/puzzles/2023/day05/part2.rb new file mode 100644 index 0000000..de30781 --- /dev/null +++ b/lib/puzzles/2023/day05/part2.rb @@ -0,0 +1,67 @@ +# frozen_string_literal: true + +require_relative "part1" +require_relative "almanac" + +module AdventOfCode + module Puzzles2023 + module Day05 + ## + # Class for solving Day 5 - Part 2 puzzle + class Part2 < Part1 + ## + # Get the minimum location that is associated with a valid seed. + # + # @return [Integer, nil] minimum location + def answer + location = 0 + loop do + seed = almanac.convert(value: location, from: :location, to: :seed) + return location if valid_seed?(value: seed) + + location += 1 + end + end + + protected + + ## + # Build the almanac from the file. + # + # @param file [String] path to input file + def build_almanac(file:) + @almanac = Almanac.new(file:, reverse: true) + end + + ## + # Parse the seeds from the seeds line. + # Seeds are in the form: seeds: ... + # is the first seed, is the number of seeds. + # + # @param line [String] seeds line + def parse_seeds(line) + match = line.match(%r{^seeds: \b\d+\s+\d+\b}) + raise "Invalid seeds line: #{line}" if match.nil? + + @seeds = [] + line.scan(%r{\b(?\d+)\s+(?\d+)\b}).each do |start, length| + from = start.to_i + to = from + length.to_i + @seeds << (from...to) + end + end + + ## + # Check if a value is a valid seed. + # A valid seed is a seed that is in the seeds ranges. + # + # @param value [Integer] value to check + # + # @return [Boolean] true if value is a valid seed, false otherwise + def valid_seed?(value:) + seeds.any? { |seed_range| seed_range.include?(value) } + end + end + end + end +end diff --git a/lib/puzzles/2023/day05/relation.rb b/lib/puzzles/2023/day05/relation.rb new file mode 100644 index 0000000..76a7d60 --- /dev/null +++ b/lib/puzzles/2023/day05/relation.rb @@ -0,0 +1,34 @@ +# frozen_string_literal: true + +module AdventOfCode + module Puzzles2023 + module Day05 + ## + # Class for representing a relation. + class Relation + ## + # @param source [Range] range of source values + # @param destination [Range] range of destination values + def initialize(source:, destination:) + @source = source + @destination = destination + end + + ## + # Convert a source value to destination value. + # If value is not in source range, return nil. + # + # @param value [Integer] source value to convert + # + # @return [Integer, nil] destination value or nil if value is not in source range + def convert(value:) + # Is value in source range? + return unless @source.include?(value) + + # Then, convert to destination range + @destination.begin + (value - @source.begin) + end + end + end + end +end diff --git a/lib/puzzles/2023/day06/README.md b/lib/puzzles/2023/day06/README.md new file mode 100644 index 0000000..fee59b8 --- /dev/null +++ b/lib/puzzles/2023/day06/README.md @@ -0,0 +1,75 @@ +# [Day 6: Wait For It](https://adventofcode.com/2023/day/6) + +## Part One + +The ferry quickly brings you across Island Island. After asking around, you discover that there is indeed normally a large pile of sand somewhere near here, but you don't see anything besides lots of water and the small island where the ferry has docked. + +As you try to figure out what to do next, you notice a poster on a wall near the ferry dock. "Boat races! Open to the public! Grand prize is an all-expenses-paid trip to **Desert Island**!" That must be where the sand comes from! Best of all, the boat races are starting in just a few minutes. + +You manage to sign up as a competitor in the boat races just in time. The organizer explains that it's not really a traditional race - instead, you will get a fixed amount of time during which your boat has to travel as far as it can, and you win if your boat goes the farthest. + +As part of signing up, you get a sheet of paper (your puzzle input) that lists the **time** allowed for each race and also the best **distance** ever recorded in that race. To guarantee you win the grand prize, you need to make sure you **go farther in each race** than the current record holder. + +The organizer brings you over to the area where the boat races are held. The boats are much smaller than you expected - they're actually **toy boats**, each with a big button on top. Holding down the button **charges the boat**, and releasing the button **allows the boat to move**. Boats move faster if their button was held longer, but time spent holding the button counts against the total race time. You can only hold the button at the start of the race, and boats don't move until the button is released. + +For example: + +``` +Time: 7 15 30 +Distance: 9 40 200 +``` + +This document describes three races: + +- The first race lasts 7 milliseconds. The record distance in this race is 9 millimeters. +- The second race lasts 15 milliseconds. The record distance in this race is 40 millimeters. +- The third race lasts 30 milliseconds. The record distance in this race is 200 millimeters. + +Your toy boat has a starting speed of **zero millimeters per millisecond**. For each whole millisecond you spend at the beginning of the race holding down the button, the boat's speed increases by **one millimeter per millisecond**. + +So, because the first race lasts 7 milliseconds, you only have a few options: + +- Don't hold the button at all (that is, hold it for **`0` milliseconds**) at the start of the race. The boat won't move; it will have traveled **`0` millimeters** by the end of the race. +- Hold the button for **`1` millisecond** at the start of the race. Then, the boat will travel at a speed of `1` millimeter per millisecond for 6 milliseconds, reaching a total distance traveled of **`6` millimeters**. +- Hold the button for **`2` milliseconds**, giving the boat a speed of `2` millimeters per millisecond. It will then get 5 milliseconds to move, reaching a total distance of **`10` millimeters**. +- Hold the button for **`3` milliseconds**. After its remaining 4 milliseconds of travel time, the boat will have gone **`12` millimeters**. +- Hold the button for **`4` milliseconds**. After its remaining 3 milliseconds of travel time, the boat will have gone **`12` millimeters**. +- Hold the button for **`5` milliseconds**, causing the boat to travel a total of **`10` millimeters**. +- Hold the button for **`6` milliseconds**, causing the boat to travel a total of **`6` millimeters**. +- Hold the button for **`7` milliseconds**. That's the entire duration of the race. You never let go of the button. The boat can't move until you let go of the button. Please make sure you let go of the button so the boat gets to move. **`0` millimeters**. + +Since the current record for this race is 9 millimeters, there are actually **`4`** different ways you could win: you could hold the button for `2`, `3`, `4`, or `5` milliseconds at the start of the race. + +In the second race, you could hold the button for at least `4` milliseconds and at most `11` milliseconds and beat the record, a total of **`8`** different ways to win. + +In the third race, you could hold the button for at least `11` milliseconds and no more than `19` milliseconds and still beat the record, a total of **`9`** ways you could win. + +To see how much margin of error you have, determine the **number of ways you can beat the record** in each race; in this example, if you multiply these values together, you get **`288`** (`4` * `8` * `9`). + +Determine the number of ways you could beat the record in each race. **What do you get if you multiply these numbers together?** + +Your puzzle answer was `2756160`. + +## Part Two + +As the race is about to start, you realize the piece of paper with race times and record distances you got earlier actually just has very bad [kerning](https://en.wikipedia.org/wiki/Kerning). There's really **only one race** - ignore the spaces between the numbers on each line. + +So, the example from before: + +``` +Time: 7 15 30 +Distance: 9 40 200 +``` + +...now instead means this: + +``` +Time: 71530 +Distance: 940200 +``` + +Now, you have to figure out how many ways there are to win this single race. In this example, the race lasts for **`71530 milliseconds`** and the record distance you need to beat is **`940200 millimeters`**. You could hold the button anywhere from `14` to `71516` milliseconds and beat the record, a total of **`71503`** ways! + +**How many ways can you beat the record in this one much longer race?** + +Your puzzle answer was `34788142`. diff --git a/lib/puzzles/2023/day06/day06.rb b/lib/puzzles/2023/day06/day06.rb new file mode 100644 index 0000000..d78f3c1 --- /dev/null +++ b/lib/puzzles/2023/day06/day06.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +require_relative "part1" +require_relative "part2" + +module AdventOfCode + module Puzzles2023 + ## + # {include:file:lib/puzzles/2023/day06/README.md} + module Day06 + end + end +end diff --git a/test/puzzles/2023/day06/input.txt b/lib/puzzles/2023/day06/input.txt similarity index 100% rename from test/puzzles/2023/day06/input.txt rename to lib/puzzles/2023/day06/input.txt diff --git a/lib/puzzles/2023/day06.rb b/lib/puzzles/2023/day06/part1.rb similarity index 80% rename from lib/puzzles/2023/day06.rb rename to lib/puzzles/2023/day06/part1.rb index 92a26b6..809872d 100644 --- a/lib/puzzles/2023/day06.rb +++ b/lib/puzzles/2023/day06/part1.rb @@ -1,31 +1,10 @@ # frozen_string_literal: true +require_relative "race" + module AdventOfCode module Puzzles2023 - ## - # Advent of Code 2023 - Day 6 - # https://adventofcode.com/2023/day/6 module Day06 - ## - # Class for representing a race - class Race - ## - # @return [Integer] total time in milliseconds - attr_reader :time - - ## - # @return [Integer] distance to beat in millimeters - attr_reader :distance - - ## - # @param time [Integer] total time in milliseconds - # @param distance [Integer] distance to beat in millimeters - def initialize(time:, distance:) - @time = time - @distance = distance - end - end - ## # Class for solving Day 6 - Part 1 puzzle class Part1 @@ -35,7 +14,8 @@ class Part1 ## # @param file [String] file name of the input file - def initialize(file:) + def initialize(file: nil) + file ||= "#{File.dirname(__FILE__)}/input.txt" parse_file(file:) end @@ -158,24 +138,6 @@ def valid_time?(race:, charging_time:) charging_time * (race.time - charging_time) > race.distance end end - - ## - # Class for solving Day 6 - Part 2 puzzle - class Part2 < Part1 - protected - - ## - # Parse the values from the file contents for a given key. - # - # @param file_contents [Array] array of lines - # @param key [String] key to find - # - # @return [Array] array of values - def parse_values(file_contents, key:) - values = super - [values.join.to_i] - end - end end end end diff --git a/lib/puzzles/2023/day06/part2.rb b/lib/puzzles/2023/day06/part2.rb new file mode 100644 index 0000000..4b2e7c1 --- /dev/null +++ b/lib/puzzles/2023/day06/part2.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +require_relative "part1" + +module AdventOfCode + module Puzzles2023 + module Day06 + ## + # Class for solving Day 6 - Part 2 puzzle + class Part2 < Part1 + protected + + ## + # Parse the values from the file contents for a given key. + # + # @param file_contents [Array] array of lines + # @param key [String] key to find + # + # @return [Array] array of values + def parse_values(file_contents, key:) + values = super + [values.join.to_i] + end + end + end + end +end diff --git a/lib/puzzles/2023/day06/race.rb b/lib/puzzles/2023/day06/race.rb new file mode 100644 index 0000000..a6670e7 --- /dev/null +++ b/lib/puzzles/2023/day06/race.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +module AdventOfCode + module Puzzles2023 + module Day06 + ## + # Class for representing a race + class Race + ## + # @return [Integer] total time in milliseconds + attr_reader :time + + ## + # @return [Integer] distance to beat in millimeters + attr_reader :distance + + ## + # @param time [Integer] total time in milliseconds + # @param distance [Integer] distance to beat in millimeters + def initialize(time:, distance:) + @time = time + @distance = distance + end + end + end + end +end diff --git a/sig/puzzles/2023/day01.rbs b/sig/puzzles/2023/day01.rbs index fde1094..acddeda 100644 --- a/sig/puzzles/2023/day01.rbs +++ b/sig/puzzles/2023/day01.rbs @@ -2,14 +2,11 @@ module AdventOfCode module Puzzles2023 module Day01 class Part1 - @calibration_values: [Integer] - attr_reader file_contents: [String] + attr_reader calibration_values: [Integer] def answer: () -> Integer - def calibration_values: () -> [Integer] - def find_first_number: () -> Integer? def find_last_number: () -> Integer? diff --git a/sig/puzzles/2023/day02.rbs b/sig/puzzles/2023/day02.rbs index bfa9f19..9d1de4c 100644 --- a/sig/puzzles/2023/day02.rbs +++ b/sig/puzzles/2023/day02.rbs @@ -2,13 +2,10 @@ module AdventOfCode module Puzzles2023 module Day02 class Set - @power: Integer - attr_reader blue: Integer attr_reader green: Integer attr_reader red: Integer - - def power: () -> Integer + attr_reader power: Integer end class Game diff --git a/sig/puzzles/2023/day03.rbs b/sig/puzzles/2023/day03.rbs index 4ec74d6..6f40a2f 100644 --- a/sig/puzzles/2023/day03.rbs +++ b/sig/puzzles/2023/day03.rbs @@ -2,20 +2,16 @@ module AdventOfCode module Puzzles2023 module Day03 class Gear - @ratio: Integer - attr_reader symbol: String attr_reader index: String attr_reader numbers: [Integer] - - def ratio: () -> Integer + attr_reader ratio: Integer end class Part1 - @part_numbers: [Integer] - attr_reader numbers: [{ Integer: String }] attr_reader symbols: [{ Integer: Integer }] + attr_reader part_numbers: [Integer] def adjacent_rows_range: (Integer, Integer) -> Range[Integer] @@ -29,8 +25,6 @@ module AdventOfCode def extract_symbols: ([String]) -> [{ Integer: String }] - def part_numbers: () -> [Integer] - def select_numbers: ([{ Integer: String }]) -> [Integer] end diff --git a/sig/puzzles/2023/day04.rbs b/sig/puzzles/2023/day04.rbs index de200cd..63ac8f4 100644 --- a/sig/puzzles/2023/day04.rbs +++ b/sig/puzzles/2023/day04.rbs @@ -2,13 +2,10 @@ module AdventOfCode module Puzzles2023 module Day04 class Card - @matches: [Integer] - attr_reader id: Integer attr_reader owned: [Integer] attr_reader winning: [Integer] - - def matches: () -> [Integer] + attr_reader matches: [Integer] end class Part1 @@ -16,7 +13,7 @@ module AdventOfCode def answer: () -> Integer - def extract_cards: ([String]) -> [Card] + def parse_cards: ([String]) -> void def points: () -> [Integer] end diff --git a/test/puzzles/2023/day01/day01_test.rb b/test/puzzles/2023/day01/day01_test.rb index 34e1b63..a5b1eca 100644 --- a/test/puzzles/2023/day01/day01_test.rb +++ b/test/puzzles/2023/day01/day01_test.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true require "test_helper" -require "puzzles/2023/day01" +require "puzzles/2023/day01/day01" module AdventOfCode module Test @@ -33,8 +33,7 @@ def test_answer_test_set end def test_answer_real_set - input_file = "#{File.dirname(__FILE__)}/input.txt" - puzzle = AdventOfCode::Puzzles2023::Day01::Part1.new(file: input_file) + puzzle = AdventOfCode::Puzzles2023::Day01::Part1.new assert_equal 54_450, puzzle.answer end @@ -66,8 +65,7 @@ def test_answer_test_set end def test_answer_real_set - input_file = "#{File.dirname(__FILE__)}/input.txt" - puzzle = AdventOfCode::Puzzles2023::Day01::Part2.new(file: input_file) + puzzle = AdventOfCode::Puzzles2023::Day01::Part2.new assert_equal 54_265, puzzle.answer end diff --git a/test/puzzles/2023/day02/day02_test.rb b/test/puzzles/2023/day02/day02_test.rb index 64a7ef4..d8f15ca 100644 --- a/test/puzzles/2023/day02/day02_test.rb +++ b/test/puzzles/2023/day02/day02_test.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true require "test_helper" -require "puzzles/2023/day02" +require "puzzles/2023/day02/day02" module AdventOfCode module Test @@ -33,8 +33,7 @@ def test_answer_test_data_set end def test_answer_input_set - input_file = "#{File.dirname(__FILE__)}/input.txt" - puzzle = AdventOfCode::Puzzles2023::Day02::Part1.new(file: input_file) + puzzle = AdventOfCode::Puzzles2023::Day02::Part1.new assert_equal 2278, puzzle.answer(blue: 14, green: 13, red: 12) end @@ -66,8 +65,7 @@ def test_answer_test_data_set end def test_answer_input_set - input_file = "#{File.dirname(__FILE__)}/input.txt" - puzzle = AdventOfCode::Puzzles2023::Day02::Part2.new(file: input_file) + puzzle = AdventOfCode::Puzzles2023::Day02::Part2.new assert_equal 67_953, puzzle.answer end diff --git a/test/puzzles/2023/day03/day03_test.rb b/test/puzzles/2023/day03/day03_test.rb index 98e779a..14d9d99 100644 --- a/test/puzzles/2023/day03/day03_test.rb +++ b/test/puzzles/2023/day03/day03_test.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true require "test_helper" -require "puzzles/2023/day03" +require "puzzles/2023/day03/day03" module AdventOfCode module Test @@ -33,8 +33,7 @@ def test_answer_test_data_set end def test_answer_input_set - input_file = "#{File.dirname(__FILE__)}/input.txt" - puzzle = AdventOfCode::Puzzles2023::Day03::Part1.new(file: input_file) + puzzle = AdventOfCode::Puzzles2023::Day03::Part1.new assert_equal 532_445, puzzle.answer end @@ -66,8 +65,7 @@ def test_answer_test_data_set end def test_answer_input_set - input_file = "#{File.dirname(__FILE__)}/input.txt" - puzzle = AdventOfCode::Puzzles2023::Day03::Part2.new(file: input_file) + puzzle = AdventOfCode::Puzzles2023::Day03::Part2.new assert_equal 79_842_967, puzzle.answer end diff --git a/test/puzzles/2023/day04/day04_test.rb b/test/puzzles/2023/day04/day04_test.rb index e568cf9..4843de2 100644 --- a/test/puzzles/2023/day04/day04_test.rb +++ b/test/puzzles/2023/day04/day04_test.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true require "test_helper" -require "puzzles/2023/day04" +require "puzzles/2023/day04/day04" module AdventOfCode module Test @@ -33,8 +33,7 @@ def test_answer_test_data_set end def test_answer_input_set - input_file = "#{File.dirname(__FILE__)}/input.txt" - puzzle = AdventOfCode::Puzzles2023::Day04::Part1.new(file: input_file) + puzzle = AdventOfCode::Puzzles2023::Day04::Part1.new assert_equal 22_897, puzzle.answer end @@ -66,8 +65,7 @@ def test_answer_test_data_set end def test_answer_input_set - input_file = "#{File.dirname(__FILE__)}/input.txt" - puzzle = AdventOfCode::Puzzles2023::Day04::Part2.new(file: input_file) + puzzle = AdventOfCode::Puzzles2023::Day04::Part2.new assert_equal 5_095_824, puzzle.answer end diff --git a/test/puzzles/2023/day05/day05_test.rb b/test/puzzles/2023/day05/day05_test.rb index 2b6e1bc..6ff5ff5 100644 --- a/test/puzzles/2023/day05/day05_test.rb +++ b/test/puzzles/2023/day05/day05_test.rb @@ -1,7 +1,9 @@ # frozen_string_literal: true require "test_helper" -require "puzzles/2023/day05" +require "puzzles/2023/day05/almanac" +require "puzzles/2023/day05/converter" +require "puzzles/2023/day05/day05" module AdventOfCode module Test @@ -79,8 +81,7 @@ def test_answer_test_data_set end def test_answer_input_set - input_file = "#{File.dirname(__FILE__)}/input.txt" - puzzle = AdventOfCode::Puzzles2023::Day05::Part1.new(file: input_file) + puzzle = AdventOfCode::Puzzles2023::Day05::Part1.new assert_equal 318_728_750, puzzle.answer end @@ -107,8 +108,7 @@ def test_answer_test_data_set def test_answer_input_set skip "Takes too long to run" if ENV["SKIP_SLOW_TESTS"] - input_file = "#{File.dirname(__FILE__)}/input.txt" - puzzle = AdventOfCode::Puzzles2023::Day05::Part2.new(file: input_file) + puzzle = AdventOfCode::Puzzles2023::Day05::Part2.new assert_equal 37_384_986, puzzle.answer end diff --git a/test/puzzles/2023/day06/day06_test.rb b/test/puzzles/2023/day06/day06_test.rb index a26210c..43f56ce 100644 --- a/test/puzzles/2023/day06/day06_test.rb +++ b/test/puzzles/2023/day06/day06_test.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true require "test_helper" -require "puzzles/2023/day06" +require "puzzles/2023/day06/day06" module AdventOfCode module Test @@ -33,8 +33,7 @@ def test_answer_test_data_set end def test_answer_input_set - input_file = "#{File.dirname(__FILE__)}/input.txt" - puzzle = AdventOfCode::Puzzles2023::Day06::Part1.new(file: input_file) + puzzle = AdventOfCode::Puzzles2023::Day06::Part1.new assert_equal 2_756_160, puzzle.answer end @@ -59,8 +58,7 @@ def test_answer_test_data_set end def test_answer_input_set - input_file = "#{File.dirname(__FILE__)}/input.txt" - puzzle = AdventOfCode::Puzzles2023::Day06::Part2.new(file: input_file) + puzzle = AdventOfCode::Puzzles2023::Day06::Part2.new assert_equal 34_788_142, puzzle.answer end