module ConditionalSample::MixMe

This module is suitable as a mixin, using the results of self#to_a

It is automatically included in Array, so each of these methods are added to Array when you require 'conditional_sample'

For both methods, the 'conditions' array must contain boolean procs using args |arr, elem|

arr

a reference to the current array that has been built up through the recursion chain.

elem

a reference to the current element being considered.

Public Instance Methods

conditional_permutation(conditions, seconds = nil) click to toggle source

Return a permutation of 'array' where each element validates to the same index in a 'conditions' array of procs that return Boolean.

The output is an array that is a complete permutation of the input array. i.e. output.length == array.length

Any elements in the array that are extra to the number of conditions will be assumed valid.

array = [1,2,3,4,5].shuffle
conditions = [
  proc { |arr, elem| elem < 2},
  proc { |arr, elem| elem > 2},
  proc { |arr, elem| elem > 1}
]
array.conditional_permutation(conditions)

possible output => [1, 3, 4, 5, 2]
# File lib/conditional_sample/public.rb, line 40
def conditional_permutation conditions, seconds = nil
  ConditionalSample::method_assert(self, 'to_a')

  # Convert the conditions to an array.
  conditions =
    ConditionalSample::to_conditions_array(conditions, proc { true })

  # Run the recursion, and rescue after a number of seconds.
  timeout_rescue(seconds, []) do
    conditional_permutation_recurse(self.to_a, conditions).tap{ |i| i.pop }
  end
end
conditional_sample(conditions, seconds = nil) click to toggle source

Return values from 'array' where each element validates to the same index in a 'conditions' array of procs that return Boolean.

The output is an array of conditions.length that is a partial permutation of the input array, where satisfies only the conditions.

Any elements in the array that are extra to the number of conditions will not be output.

array = [1,2,3,4,5].shuffle
conditions = [
  proc { |arr, elem| elem < 2},
  proc { |arr, elem| elem > 2},
  proc { |arr, elem| elem > 1}
]
array.conditional_sample(conditions)

possible output => [1, 5, 3]
# File lib/conditional_sample/public.rb, line 73
def conditional_sample conditions, seconds = nil
  ConditionalSample::method_assert(self, 'to_a')

  # Would always return [] anyway if there are more conditions than
  # inputs, this just avoids running the complex recursion code.
  return [] if conditions.length > self.to_a.length

  # Convert the conditions to an array.
  conditions =
    ConditionalSample::to_conditions_array(conditions, proc { true })

  # Run the recursion, and rescue after a number of seconds.
  timeout_rescue(seconds, []) do
    conditional_sample_recurse(self.to_a, conditions).tap{ |i| i.pop }
  end
end

Private Instance Methods

conditional_permutation_recurse( array, conditions, current_iter = 0, current_array = []) click to toggle source

Private recursive method. conditional_permutation is the public interface.

# File lib/conditional_sample/private.rb, line 36
def conditional_permutation_recurse (
    array,
    conditions,
    current_iter = 0,
    current_array = [])

  output = []

  # Get the current conditional.
  cond = conditions[current_iter]

  # Loop through and return the first element that validates.
  valid = false
  array.each do |elem|

    # Test the condition. If we've run out of elements
    #   in the condition array, then allow any value.
    valid = cond ? cond.call(current_array, elem) : true
    if valid

      # Remove this element from the array, and recurse.
      remain = array.dup
      delete_first!(remain, elem)

      # If the remaining array is empty, no need to recurse.
      new_val = nil
      if !remain.empty?
        new_val = conditional_permutation_recurse(
                  remain,
                  conditions,
                  current_iter + 1,
                  current_array + [elem])
      end

      # If we cannot use this value, because it breaks future conditions.
      if !remain.empty? && new_val.empty?
        valid = false
      else
        output << elem << new_val
      end
    end

    break if valid
  end

  output.flatten
end
conditional_sample_recurse( array, conditions, current_iter = 0, current_array = []) click to toggle source

Private recursive method. conditional_sample is the public interface.

# File lib/conditional_sample/private.rb, line 88
def conditional_sample_recurse (
    array,
    conditions,
    current_iter = 0,
    current_array = [])

  output = []

  # Get the current conditional.
  cond = conditions[current_iter]

  # Return nil if we have reached the end of the conditionals.
  return nil if cond.nil?

  # Loop through and return the first element that validates.
  valid = false
  array.each do |elem|

    # Test the condition. If we've run out of elements
    #   in the condition array, then allow any value.
    valid = cond.call(current_array, elem)
    if valid

      # Remove this element from the array, and recurse.
      remain = array.dup
      delete_first!(remain, elem)

      # If the remaining array is empty, no need to recurse.
      new_val = conditional_sample_recurse(
                remain,
                conditions,
                current_iter + 1,
                current_array + [elem])

      # If we cannot use this value, because it breaks future conditions.
      if new_val and new_val.empty?
        valid = false
      else
        output << elem << new_val
      end
    end

    break if valid
  end

  output.flatten
end
delete_first!(array, value) click to toggle source

Delete the first matching value in an array. Destructive to the first argument.

# File lib/conditional_sample/private.rb, line 28
def delete_first! array, value
  array.delete_at(array.index(value) || array.length)
end
timeout_rescue(seconds, rescue_value = nil) { || ... } click to toggle source

Run a bloc in a given number of 'seconds'. If the bloc completes in time, then return the result of the bloc, else return 'rescue_value'.

# File lib/conditional_sample/private.rb, line 14
def timeout_rescue seconds, rescue_value = nil
  begin
    Timeout::timeout(seconds.to_f) do
      yield
    end
  rescue Timeout::Error
    rescue_value
  end
end