From b9ec0fbf059aa6217539f6a85ccc94f199b2fc9f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Carlos=20=C3=81lvaro?= Date: Thu, 28 Dec 2023 12:55:17 +0100 Subject: [PATCH] feat: Add doc and signatures for day 5 --- lib/puzzles/2023/day05.rb | 166 ++++++++++++++++++++++---- sig/puzzles/2023/day05.rbs | 42 ++++++- sig/test/puzzles/2023/day05_test.rbs | 50 ++++++++ test/puzzles/2023/day05/day05_test.rb | 12 +- 4 files changed, 241 insertions(+), 29 deletions(-) create mode 100644 sig/test/puzzles/2023/day05_test.rbs diff --git a/lib/puzzles/2023/day05.rb b/lib/puzzles/2023/day05.rb index f6d6692..b31dda7 100644 --- a/lib/puzzles/2023/day05.rb +++ b/lib/puzzles/2023/day05.rb @@ -4,38 +4,87 @@ module AdventOfCode module Puzzles2023 ## # Advent of Code 2023 - Day 5 - # https://adventofcode.com/2023/day/4 + # 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, :relations + 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| - # Is value in source range? - if relation[:source].include?(value) - # Then, convert to destination range - return relation[:destination].begin + (value - relation[:source].begin) + @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 - attr_reader :converters - + ## + # @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:) @@ -46,8 +95,24 @@ def convert(value:, from:, to:) protected - attr_accessor :targets + ## + # 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) @@ -57,21 +122,42 @@ def parse_file(file) 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 nil if match.nil? + return if match.nil? - targets.map { |target| match[target].to_sym } + 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 @@ -83,7 +169,7 @@ def parse_maps(lines) lines.each do |line| if line.empty? - # Save conversion if relations + # Save conversion if source @converters[source] = Converter.new(source:, destination:, relations:) unless source.nil? # Reset variables @@ -104,14 +190,20 @@ def parse_maps(lines) ## # Class for solving Day 5 - Part 1 puzzle class Part1 - attr_reader :almanac, :seeds + 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) - parse_almanac(file:) + 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) @@ -121,21 +213,38 @@ def answer protected - def parse_almanac(file:) + 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) + @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 @@ -148,17 +257,25 @@ def answer protected - def parse_almanac(file:) + ## + # 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 = line.gsub(%r{seeds:\s+}, "") line.scan(%r{\b(?\d+)\s+(?\d+)\b}).each do |start, length| from = start.to_i to = from + length.to_i @@ -166,6 +283,13 @@ def parse_seeds(line) 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 diff --git a/sig/puzzles/2023/day05.rbs b/sig/puzzles/2023/day05.rbs index ce2bef0..60cfd99 100644 --- a/sig/puzzles/2023/day05.rbs +++ b/sig/puzzles/2023/day05.rbs @@ -1,12 +1,50 @@ module AdventOfCode module Puzzles2023 module Day05 - class Part1 + class Relation + @source: Range[Integer] + @destination: Range[Integer] + + def convert: (Integer) -> Integer? + end + + class Converter + @relations: [Relation] + + attr_reader source: Symbol + attr_reader destination: Symbol + def convert: (Integer) -> Integer end - class Part2 + class Almanac + attr_reader converters: { Symbol: Converter } + attr_reader targets: [Symbol] + + def convert: (Integer, Symbol, Symbol) -> Integer + + def parse_file: (String) -> void + + def parse_header: (String) -> [Symbol]? + + def parse_map: (String) -> Relation + + def parse_maps: ([String]) -> void + end + + class Part1 + attr_reader almanac: Almanac + attr_reader seeds: [Integer] + + def answer: () -> Integer? + + def build_almanac: (String) -> void + + def parse_seeds: (String) -> void + end + class Part2 < Part1 + def valid_seed?: (Integer) -> bool end end end diff --git a/sig/test/puzzles/2023/day05_test.rbs b/sig/test/puzzles/2023/day05_test.rbs new file mode 100644 index 0000000..e51a757 --- /dev/null +++ b/sig/test/puzzles/2023/day05_test.rbs @@ -0,0 +1,50 @@ +module AdventOfCode + module Test + module Puzzles2023 + module Day05 + class ConverterTest < MiniTest::Test + attr_reader converter: Puzzles2023::Day05::Converter + + def setup: -> untyped + + def teardown: -> untyped + + def test_conversion: -> untyped + end + + class AlmanacTest < MiniTest::Test + attr_reader almanac: Puzzles2023::Day05::Almanac + attr_reader file: String + + def setup: -> untyped + + def teardown: -> untyped + + def test_seeds_conversion_no_reverse: -> untyped + + def test_seeds_conversion_reverse: -> untyped + end + + class Part1Test < MiniTest::Test + def setup: -> untyped + + def teardown: -> untyped + + def test_answer_test_data_set: -> untyped + + def test_answer_input_set: -> untyped + end + + class Part2Test < MiniTest::Test + def setup: -> untyped + + def teardown: -> untyped + + def test_answer_test_data_set: -> untyped + + def test_answer_input_set: -> untyped + end + end + end + end +end diff --git a/test/puzzles/2023/day05/day05_test.rb b/test/puzzles/2023/day05/day05_test.rb index b789c86..2b6e1bc 100644 --- a/test/puzzles/2023/day05/day05_test.rb +++ b/test/puzzles/2023/day05/day05_test.rb @@ -7,17 +7,17 @@ module AdventOfCode module Test module Puzzles2023 module Day05 - class ConversionTest < Minitest::Test - attr_reader :conversion + class ConverterTest < Minitest::Test + attr_reader :converter def setup source = :seed destination = :soil relations = [ - { source: 98...100, destination: 50...52 }, - { source: 50...98, destination: 52...100 } + AdventOfCode::Puzzles2023::Day05::Relation.new(source: 98...100, destination: 50...52), + AdventOfCode::Puzzles2023::Day05::Relation.new(source: 50...98, destination: 52...100) ] - @conversion = AdventOfCode::Puzzles2023::Day05::Converter.new(source:, destination:, relations:) + @converter = AdventOfCode::Puzzles2023::Day05::Converter.new(source:, destination:, relations:) end def teardown @@ -25,7 +25,7 @@ def teardown end def test_conversion - assert_equal([81, 14, 57, 13], [79, 14, 55, 13].map { |value| conversion.convert(value:) }) + assert_equal([81, 14, 57, 13], [79, 14, 55, 13].map { |value| converter.convert(value:) }) end end