Basically I wanted to make a generic class for parsing time codes and dealing with small units of time.I have tried to keep it very simple (small API) and 0 dependency (apart from RSpec).Some points I am wondering about:Should I define HOURS_MINUTES_SECONDS_REGEXP as a class variable for performance?Should I raise errors from the initializer?Am I correct to only the value as seconds internally and discard any intermediates? Is there a good way to check if the val is a Hash or MatchData that are usable in .valid?Code is also available on Githubmodule Countdown  # Used to parse time code strings  class Counter    # @param val [Interger|String|Hash] representing a length of time in HH:MM:SS or HH:SS    # Sets the internal time store in seconds from input    def initialize val      self.seconds = val    end    # @return [Integer] the total number of seconds    attr_reader :seconds    # @param val [Integer|String|Hash]    #   - seconds    #   - a string representing a length of time in HH:MM:SS or HH:SS    #   - a Hash or Hash like object with the keys :seconds and :minutes    # @raise TypeError    #   if val is not an acceptable parameter    def seconds= val      if val.is_a?(Integer)        @seconds = val      elsif val.is_a?(String)        @seconds = from_string val      elsif self.class.quacks_like_a_hash?(val) # quacks like a hash        @seconds = from_hash(val)      else        raise TypeError, input must be a String, Integer or Hash like object      end    end    # Checks the format of a string or hash like object    # @param input [String|Hash]    # @return [Boolean]    def self.valid? input      hash = input.is_a?(String) ?  HOURS_MINUTES_SECONDS_REGEXP.match(input) : input      if hash && hash[:minutes] && hash[:seconds]        if hash[:hours].to_i < 24 && hash[:minutes].to_i < 60 && hash[:seconds].to_i < 60          return true        end      end      false    end    # @param short_format [Boolean] prefer mm:ss output    # @return [String]    #   formated as a hh:mm:ss countdown    def to_s(short_format: false)      mm, ss = @seconds.divmod(60)      hh, mm = mm.divmod(60)      parts = short_format && hh.zero? ? [ mm, ss] : [hh, mm, ss]      parts.map { |d| d.to_s.rjust(2, '0') }.join(':')    end    alias_method :to_i, :seconds    # @return [Float] the total amount of time in minutes decimal    def minutes      to_i / 60.0    end    # @return [Float] the total amount of time in hours decimal    def hours      to_i / 3600.0    end    # @return [Hash]    def to_h      mm, ss = @seconds.divmod(60)      hh, mm = mm.divmod(60)      {          hours: hh,          minutes: mm,          seconds: ss      }    end    def self.quacks_like_a_hash? val      val.respond_to?(:[]) && val[:seconds] || val[:minutes] || val[:hours]    end    private    HOURS_MINUTES_SECONDS_REGEXP = /^((?<hours>([0-5]?[0-9]|60))?:)?(?<minutes>([0-5]?[0-9]|60)):(?<seconds>([0-5]?[0-9]|60))$/.freeze    def from_string tcs      matches = HOURS_MINUTES_SECONDS_REGEXP.match(tcs)      unless Counter.valid?(matches)        raise TypeError, #{tcs} is not a valid time code string.      end      from_hash(matches)    end    def from_hash hash      unless Counter.valid?(matches)        raise TypeError, #{hash} is not a valid time input      end      (hash[:hours].to_i * 3600) + (hash[:minutes].to_i * 60) + hash[:seconds].to_i    end  endendSpec:require 'spec_helper'require 'countdown/counter'RSpec.describe Countdown::Counter do  describe '.valid?' do    it allows mm:ss do      expect(Countdown::Counter.valid?('12:56')).to eq(true)    end    it allows hh:mm:ss do      expect(Countdown::Counter.valid?('13:12:56')).to eq(true)    end    it ensures there are not more than 24 hours do      expect(Countdown::Counter.valid?('55:12:56')).to eq(false)    end    it ensures there are not more than 60 minutes do      expect(Countdown::Counter.valid?('12:99:56')).to eq(false)    end    it ensures there are not more than 60 seconds do      expect(Countdown::Counter.valid?('12:23:67')).to eq(false)    end  end  describe 'initialization' do    it 'will raise ArgumentError if input is invalid' do      expect {        expect( Countdown::Counter.new('asdasd') )      }.to raise_error asdasd is not a valid time code string.    end    it allows creation from an integer do      expect( Countdown::Counter.new(10).seconds ).to eq 10    end    it allows creation from an hash do      expect( Countdown::Counter.new(minutes: 1, seconds: 2).seconds ).to eq 62      expect( Countdown::Counter.new(hours: 1, seconds: 2).seconds ).to eq 3602    end  end  describe '#to_s' do    it formats a countdown do      expect( Countdown::Counter.new('12:56').to_s ).to eq('00:12:56')    end    it can output a short MM:SS format do      expect( Countdown::Counter.new('12:56').to_s(short_format: true) ).to eq('12:56')    end  end  describe #to_i do    it outputs the total seconds do      expect( Countdown::Counter.new('1:20').to_i ).to eq(80)      expect( Countdown::Counter.new('01:01:20').to_i ).to eq(3680)    end  end  describe #seconds do    it outputs the total seconds do      expect( Countdown::Counter.new('1:22').seconds ).to eq(82)    end  end  describe #minutes do    it outputs the time expressed in minutes decimal do      expect( Countdown::Counter.new('1:30').minutes ).to eq(1.5)    end  end  describe #hours do    it outputs the time expressed in hours decimal do      expect( Countdown::Counter.new('01:30:00').hours ).to eq(1.5)    end  end  describe #to_h do    it outputs the time as a hash do      input = {        seconds: 1,        minutes: 2,        hours: 1      }      expect( Countdown::Counter.new(input).to_h ).to eq input    end  endend
Handling hours, minutes, seconds
ruby;parsing;datetime