Skip to content

Commit

Permalink
feat: Add doc and signatures for day 5
Browse files Browse the repository at this point in the history
  • Loading branch information
cdalvaro committed Dec 28, 2023
1 parent 8607d0f commit b9ec0fb
Show file tree
Hide file tree
Showing 4 changed files with 241 additions and 29 deletions.
166 changes: 145 additions & 21 deletions lib/puzzles/2023/day05.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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<Relation>] 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:)
Expand All @@ -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<Symbol, Converter>] hash of converters
attr_reader :converters

##
# Contains symbols: :source, :destination,
# ordered by the direction of the conversion.
#
# @return [Array<Symbol>] 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)

Expand All @@ -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<Symbol>, nil] array of symbols or nil if line is not a header line
def parse_header(line)
match = line.match(%r{(?<source>\w+)-to-(?<destination>\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<String>] array of lines
def parse_maps(lines)
source = nil
destination = nil
Expand All @@ -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
Expand All @@ -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)
Expand All @@ -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: <seed1> <seed2> ...
#
# @param line [String] seeds line
#
# @raise [RuntimeError] if seeds line is invalid
def parse_seeds(line)
match = line.match(%r{^seeds: (?<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
Expand All @@ -148,24 +257,39 @@ 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: <start> <length> <start> <length> ...
# <start> is the first seed, <length> 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(?<start>\d+)\s+(?<length>\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
Expand Down
42 changes: 40 additions & 2 deletions sig/puzzles/2023/day05.rbs
Original file line number Diff line number Diff line change
@@ -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
Expand Down
50 changes: 50 additions & 0 deletions sig/test/puzzles/2023/day05_test.rbs
Original file line number Diff line number Diff line change
@@ -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
Loading

0 comments on commit b9ec0fb

Please sign in to comment.