require 'debug' # Generic FileHandler class FileHandler def initialize(file_path) @file_path = file_path end # Usage: # FileHandler.new('/path_to_file').read_lines do |line| # # process line # end def read_lines File.foreach(@file_path, chomp: true).map do |line| yield(line) end end # Usage: # file_content = FileHandler.new('/path_to_file').read_file( def read_file File.read(@file_path, chomp: true) end end class PuzzleSolver def initialize(file_handler, debug: false) @handler = file_handler @debug = debug end def debug(message) puts message if @debug end def print_debug(message) print message if @debug end def solve raise NotImplementedError, 'Please implement this method in subclasses.' end end class FreshnessDatabase # @param [] list of ranges of fresh IDs def initialize(fresh_ranges) @fresh_ranges = fresh_ranges end def fresh_id?(id) @fresh_ranges.any? {|range| range.include?(id)} end def parsed_ranges all_ranges = [] current_min = Float::INFINITY current_max = -Float::INFINITY @fresh_ranges.each_with_index do |range, index| if range.max < current_min all_ranges.insert(0, range) current_min = range.min current_max = range.max if range.max > current_max next end if range.min > current_max all_ranges << range current_max = range.max current_min = range.min if range.min < current_min next end overlapping_range_indices, closest_range_index = FreshnessDatabase.find_all_overlapping_range_indices(all_ranges, range) if overlapping_range_indices.empty? all_ranges.insert(closest_range_index + 1, range) elsif overlapping_range_indices.length == 1 overlap_index = overlapping_range_indices[0] new_range = FreshnessDatabase.max_range([all_ranges[overlap_index], range]) all_ranges[overlap_index] = new_range range = new_range else min_index = overlapping_range_indices[0] max_index = overlapping_range_indices[overlapping_range_indices.length - 1] min_range = all_ranges[min_index] max_range = all_ranges[max_index] new_range = FreshnessDatabase.max_range([min_range, range, max_range]) new_ranges = [] all_ranges.each_index do |i| if i < min_index new_ranges << all_ranges[i] elsif i == min_index new_ranges << new_range elsif min_index < i && i <= max_index # do nothing else new_ranges << all_ranges[i] end end all_ranges = new_ranges range = new_range end current_min = range.min if range.min < current_min current_max = range.max if range.max > current_max end all_ranges end def num_fresh_ids ranges = parsed_ranges puts ranges.inspect num_ids = ranges.map {|range| range.size}.sum num_ids end private def self.overlapping_ranges?(range1, range2) (range1.min <= range2.min && range2.min <= range1.max) || (range1.min <= range2.max && range2.min <= range1.max) end def self.max_range(ranges_array) (0...ranges_array.length - 1).each do |index| raise "Non overlapping ranges found on #{index}: #{ranges_array}" if !self.overlapping_ranges?(ranges_array[index], ranges_array[index+1]) end bounds = ranges_array.flat_map {|range| [range.min, range.max]} Range.new(bounds.min, bounds.max) end def self.find_all_overlapping_range_indices(all_ranges, range) # NOTE: we assume `range` is between smallest and largest range, as those cases are handles separately in main loop raise "Error unmet assumption: #{all_ranges}" if all_ranges.length < 2 indices = [] closest_range_index = 0 # used in case there is no overlap all_ranges.each_with_index do |possible_range, index| if self.overlapping_ranges?(possible_range, range) indices << index else closest_range_index = index if range.min > possible_range.max end end [indices, closest_range_index] end end class Solver < PuzzleSolver # @param [String] e.g., "11-22" # @return [Range] e.g., 11..22 (inclusive) def to_range(string) boundaries = string.split('-').map(&:to_i) raise "Invalid boundaries for #{string}, got #{boundaries.inspect}" if boundaries.length != 2 Range.new(boundaries[0], boundaries[1]) end # @param [String] full input e.g., "11-22\n33-604\n..." # @return [] e.g., [11..22, 33..604, ...] def parse_ranges(input) input.split("\n").map {|range| to_range(range)} end # @param [String] ids_str e.g., "1\n5\n8\n11\n17\n32" # @return [] e.g., [1, 5, 8, 11, 17, 32] def parse_ids(ids_str) ids_str.split("\n").map(&:to_i) end def solve two_parts = @handler.read_file.split("\n\n") raise "Invalid file format: #{two_parts.inpsect}" if two_parts.length != 2 db = FreshnessDatabase.new(parse_ranges(two_parts[0])) db.num_fresh_ids end end if ARGV[0].nil? || ARGV[0].empty? puts "Usage: ruby #{__FILE__} [debug]" exit 1 end file_path = ARGV[0] debug = (ARGV[1] == "debug") file_handler = FileHandler.new(file_path) ## Puzzle-specific cls = Solver ## puts "The answer is: #{cls.new(file_handler, debug:).solve}"