2
\$\begingroup\$

I haven't been programming for a while, I wanted to do a simple project to warm myself up and get back into the game; I made this simple tool that finds the probabilities of coin toss combinations. Enter a number to get all combinations with that many flips, or enter a string to search for that combination, i.e. H**H would find all combinations starting and ending with heads.

module Every
  def every(&block)
    arys = map do |ary|
      base_ary = eval ary.inspect.split(':')[1] rescue nil
      base_ary.nil? ? ary : base_ary
    end
    longest = arys.max_by(&:count).count

    arys.map!.with_index do |ary, i|
      cyc = ary * ( longest / ary.count + 1 )
      self[i].is_a?(Enumerator) ? cyc[0...longest] : ary
    end
    arys.transpose.each &block
  end
end

class Array
  # I didn't have to use this module, but I had written it in the past 
  # and this seemed like a good place to use it
  include Every
end

class CoinToss
  attr_accessor :outcomes, :reference, :iterations, :combins, :occrs

  def initialize(outcomes = ["H", "T"], reference = gets.chomp.upcase)
    @outcomes = outcomes.each &:to_s
    if (Integer reference rescue false)
      @iterations = reference.to_i
      @reference = "*" * @iterations
    else
      set = @outcomes.join("*")
      raise ArgumentError, "Invalid Characters" unless reference =~ /\A[#{set}]+\z/
      @reference = reference
      @iterations = @reference.length
    end

    @combins = @outcomes.repeated_permutation(@iterations).to_a
    @occrs = 0

    ref = [@reference.chars]
    # every loop ref is repeated but combins is iterated
    [ref.cycle, @combins].every do |r, c|
      # replaces `*` in ref with regex string for characters from `outcomes`
      @template = r.join.gsub "*", "[#{@outcomes.join}]"
      @occrs += 1 unless !(c.join =~ /#{@template}/) # +1 if it matches the template
    end
  end

  def get_combins(match_marker = "~")
    @combins.each do |c|
      # m is blank if combin doesn't match the template
      m = !(c.join =~ /#{@template}/) ? "" : match_marker
      line = Array c.join
      # skips marker when reference contains only `*`
      line << m unless @reference.chars.all? { |r| r == "*" }
      puts line.join(" ")
    end
  end

  def get_probability(rationalize = true, match_word = "out of")
    puts Rational(occrs, combins.length) if rationalize
    puts [@occrs, match_word, @combins.length].join(" ") unless rationalize
  end
end


tosses = CoinToss.new
tosses.get_combins
tosses.get_probability(rationalize = false)
tosses.get_probability(rationalize = true)

Thanks for any advice.

\$\endgroup\$

1 Answer 1

1
\$\begingroup\$

Here are some suggestions to your code

Every module

I had quite a hard time to understand what it does.

Using eval can be quite dangerous so I would try to avoid it.

eval ary.inspect.split(':')[1] rescue nil

You used a module to monkey patch the Array class which is sensible. Another, even more safe way to monkey patch could be to use refinements.

Refinements are designed to reduce the impact of monkey patching on other users of the monkey-patched class. Refinements provide a way to extend a class locally.

https://docs.ruby-lang.org/en/2.4.0/syntax/refinements_rdoc.html

The safest way of course is to not use monkey patching at all. Maybe something like array.flatten.each could do the trick too?

CoinToss

Initialize reference

Initializing the reference in your constructor looks quite complicated. Maybe it could get extracted to dedicated classes.

class Reference
  def initialize(input, allowed_characters = [])
    @input = input
    @allowed_characters = allowed_characters
  end

  def to_s
    if integer?
      IntegerReference.new(input.to_i)
    else
      StringReference.new(input, allowed_characters)
    end
  end

  private

  attr_accessor :input, :allowed_characters

  def integer?
    Integer(reference)
  rescue
    false
  end
end

class IntegerReference
  def initialize(size)
    @size = size
  end

  def to_s
    build_string
  end

  def valid?
    true
  end

  private

  attr_accessor :size

  def build_string
    "*" * size
  end
end

class StringReference
  def initialize(string, allowed_characters = [])
    @string = string
    @allowed_characters = allowed_characters
  end

  def to_s
    return string if valid?
  end

  def valid?
    string =~ /\A[#{validation_regex}]+\z/
  end

  private

  attr_accessor :string

  def validation_regex
    allowed_characters.join("*")
  end
end

Use getter

You already setup attribute accessors but then fail to use them. Instead of @combins use combins. This has the advantage that you can change the way combins is returned by changing the getter method.

Return data instead of puts

Return data from your methods instead of using a puts. This has several advantages likes

  • Use the classes outside a terminal (e.g. website)
  • Easier to test

Naming

There are only two hard things in computer science ... Try to use speaking names and avoid abbreviations like combins, occrs, r, c. This will help to make your program easier to read and understand.

Lazy loading

Instead of setting up everything in the controller, use lazy loading and memoization.

def combinations
  @combinations ||= outcomes.repeated_permutation(@iterations).to_a
end

def iterations
  @iterations ||= reference.length
end
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.