# frozen_string_literal: true require 'date' module AceOfBase # A single row of data, including casting and validations. class Record include Comparable STRING_TOO_LONG_MESSAGE = 'Field `%s` is over the maximum length of %d characters (%d in total)' VALUE_OUTSIDE_RANGE = 'Field `%s`: %p is outside the range of %p' INVALID_DATE_MESSAGE = 'Field `%s`: unable to parse date with format %p' INCORRECT_COLUMN_NUMBER_MESSAGE = 'Row contains an incorrect number of columns. Expected 7, received %d' ATTRIBUTES = %i[project shot version status finish_date internal_bid created_date].freeze attr_reader :errors, *ATTRIBUTES def initialize(row) @errors = [] cast_and_validate(row) end def valid? errors.empty? end def <=>(other) ATTRIBUTES .lazy .map { |attr| public_send(attr) <=> other.public_send(attr) } .find(-> { 0 }, &:nonzero?) end private def cast_and_validate(row) # rubocop:disable Metrics/AbcSize return add_incorrect_column_number_error(row.size) unless row.size == 7 @project = cast_string_with_limit(:project, row[0], 64) @shot = cast_string_with_limit(:shot, row[1], 64) @version = cast_integer_within_range(:version, row[2], 0..0xffff) @status = cast_string_with_limit(:status, row[3], 32) @finish_date = cast_date_with_format(:finish_date, row[4], '%Y-%m-%d') @internal_bid = cast_float_within_range(:internal_bid, row[5], 0..0xffff) @created_date = cast_date_with_format(:created_date, row[6], '%Y-%m-%d %H:%M') end def add_incorrect_column_number_error(column_count) add_error(format(INCORRECT_COLUMN_NUMBER_MESSAGE, column_count)) end def add_error(message) @errors << message nil end def cast_string_with_limit(field, incoming, limit) casted = incoming.to_s length = casted.length return add_error(format(STRING_TOO_LONG_MESSAGE, field, limit, length)) if length > limit casted end def cast_integer_within_range(field, incoming, range) casted = incoming.to_i return add_error(format(VALUE_OUTSIDE_RANGE, field, incoming, range)) unless range.cover?(casted) casted end def cast_date_with_format(field, incoming, format) DateTime.strptime(incoming.to_s, format) rescue ArgumentError add_error(format(INVALID_DATE_MESSAGE, field, format)) end def cast_float_within_range(field, incoming, range) casted = incoming.to_f return add_error(format(VALUE_OUTSIDE_RANGE, field, incoming, range)) unless range.cover?(casted) casted end end end