Skip to content

Instantly share code, notes, and snippets.

@bbuck
Last active January 3, 2016 04:39
Show Gist options
  • Save bbuck/8410291 to your computer and use it in GitHub Desktop.
Save bbuck/8410291 to your computer and use it in GitHub Desktop.
Small Ruby Algorithm DSL
# Defines a simple DSL means for building an algorithm that follows a list of
# steps. Despite all algorithms being a simple set of steps that doesn't mean
# that this would be a prime solution for implementing said algorithm. This
# class is designed for implementing algorithms that follow a very defined set
# of steps with easy to comprehend mutations.
#
# The purpose of step definition is to force the person designed the algorithm
# to break each step down, ideally so that each step performs a single mutation
# and returns a new value. There is a means to managing a "state" for the
# algorithm for more complex "moving parts" algorithms or just simply caching
# data for future steps.
#
# Author:: Brandon Buck (mailto:lordizuriel@gmail.com)
# License:: MIT
class Algorithm
#--
# Begin Class Values
#++
class << self
# Create a new algorithm, the given block
def create(&block)
Algorithm.new(&block)
end
alias_method :define, :create
end
#++
# Begin Class Values
#--
# Initialize the algorithm with default values and then execute the block
# if a block is given.
# @param block [Proc] a block that defines the steps for the given algorithm
def initialize(&block)
@steps = []
@base_value = nil
@base_value_set = false
instance_eval(&block) if block_given?
end
# Adds a step block that will be performed during the calculation of this
# algorithm. The steps are performed in the order in which they are defined.
#
# The block given to add_step can take either one or two values. The first
# argument is the current value computed before the step was reached (or the
# base_value if the step is the first). The second argument is the state
# object of the algorithm. It is okay to alternate between taking both arguments
# or a single argument throughout the algorithm depending on the needs of the
# current step.
#
# The return value from this block is the most important, anything returned
# from a block becomes the new current value. This allows for shorter and more
# concise step definitions. Using this return value is up to you, and you can
# look over the easter_date algorithm which depends on state more than value
# but the final step needs to return the result of the algorithm, that return
# value is the one passed back to the calling line.
# @param block [Proc] a proc to execute in place of this step. If a block is
# given the symbol name will be ignored.
# @return [void]
# @example Adding steps
# # Take the current value and current state
# add_step { |value, state| state[:some_value] + value }
# # Take just the current value
# add_step { |value| value + 10 }
def add_step(&block)
@steps << block
end
# Enters the given step into the step list the number of times specified.
# This allows for simple, but repeated steps to be defined one time.
# @param count [Integer] the number of times to repeat this step
# @param block [Proc] a proc to call to perform the actions of this step.
# @return [void]
# @example Repeating steps
# repeat_step 5, { |v| v + 10 } # adds 5 steps that perform the block
def repeat_step(count, &block)
count.times do
add_step(&block)
end
end
# Sets the default base value for the algorithm. This base value can be
# anything and will be the value sent to your first step. If your algorithm
# uses an object value (references) then you may want to define this as
# a block.
# @return [void]
# @example Using a block to define base_value
# base_value [] # Defines the base value as an instance of an array which
# # all calls to calculate will share.
# base_value { [] } # Returns a new instance of an array so each call to
# # calculate will use a fresh instance
def base_value(value = nil, &block)
@base_value_set = true
@base_value = if block_given?
block
else
value
end
end
# Calculates the results of this algorithm by starting with the base_value and
# performing each step in order before returning the final value.
#
# You can seed the state of the algorithm by passing in a hash with desired
# initial state to the calculate function, otherwise the state will initially
# be an empty hash.
#
# The base_value can be overridden or provided individually by this function.
# If you wish to alter the base value then you simply pass the option
# :override_base_value and have it's value set to the new base_value. If you
# did not specify a base value then you will need to set one with :base_value
# otherwise the base_value is 0.
#
# == Options
# * :base_value - Used when no base_value was set but a base_value is needed
# to begin the algorithm (for algorithms that require input).
# * :override_base_value - Override the set base_value with a new one. For
# instances where a base_value was set but needs to be a certain value for
# a single execution.
# @param initial_state [Object] the value you with to be used as an initial
# state.
# @param opts [Hash{Symbol=>Object}] options for the calculate function
# @return [Object] the final result of an execution of the algorithm, the type
# of this value is ultimately returned to what your algorithm
# returns
def calculate(initial_state = {}, opts = {})
value = get_base_value(opts)
state = initial_state || {}
@steps.each do |step|
step_method = if step.respond_to?(:call)
step
else
method(step.to_sym)
end
value = call_step(step_method, value, state)
end
value
end
alias_method :process, :calculate
alias_method :compute, :calculate
private
# fetch the base value
# @return [Object] the base value defined for this class, a base value must be
# defined in order for this method to work
def _base_value
if @base_value.respond_to?(:call)
@base_value.call
else
@base_value
end
end
# Determine if a base value has been set for this class. It is safe to use
# nil as a base value becuase this looks to see if any value was defined for
# the class, not checking any specific values.
# @return [Boolean] whether or not a base_value was ever set for this class
def base_value?
@base_value_set
end
# Call the step Proc/Method with the given value and state. This is a helper
# that determines how to call the step based on the arity (does it expect just
# a value or a value and state?).
# @param step [Proc,Method] a proc or method representing the current set
# @param value [Object] the current value of the algorithm's calculation
# @param state [Object] the current state of the algorithm
# @return [Object] the result of calling the step block
def call_step(step, value, state)
if step.arity == 1
step.call(value)
else
step.call(value, state)
end
end
# Fetches the base value of this class given an options hash. This is a helper
# that checks to see if base_value was defined and is being overridden; if
# base_value was not defined and is expecting one; or defaulting the base value
# to 0
# @param opts [Hash] the hash of options sent to calculate
# @return [Object] the base value for this class, defaults to 0
def get_base_value(opts)
value = if base_value? && opts.has_key?(:override_base_value)
opts[:override_base_value]
elsif base_value?
_base_value
elsif opts.has_key?(:base_value)
opts[:base_value]
else
0
end
if value.respond_to?(:call)
value.call
else
value
end
end
end
# Helper globals, you can decide to include them in whichever you scope you wish
# for them to appear in. The globals are not forced on you.
module AlgorithmHelpers
# See also {Algorithm#initialize}
def define_algorithm(&block)
Algorithm.new(&block)
end
end
require "./algorithm"
MONTHS = {2 => "February", 3 => "March", 4 => "April", 5 => "May"}
easter_date = Algorithm.define do
add_step do |value, state|
state[:year] = value
end
add_step do |value, state|
state[:a] = state[:year] % 19
end
add_step do |value, state|
state[:b] = (state[:year] / 100.0).floor
end
add_step do |value, state|
state[:c] = state[:year] % 100
end
add_step do |value, state|
state[:d] = (state[:b] / 4.0).floor
end
add_step do |value, state|
state[:e] = state[:b] % 4
end
add_step do |value, state|
state[:f] = ((state[:b] + 8) / 25.0).floor
end
add_step do |value, state|
state[:g] = ((state[:b] - state[:f] + 1) / 3.0).floor
end
add_step do |value, state|
state[:h] = (19 * state[:a] + state[:b] - state[:d] - state[:g] + 15) % 30
end
add_step do |value, state|
state[:i] = (state[:c] / 4.0).floor
end
add_step do |value, state|
state[:k] = state[:c] % 4
end
add_step do |value, state|
state[:L] = (32 + 2 * state[:e] + 2 * state[:i] - state[:h] - state[:k]) % 7
end
add_step do |value, state|
state[:m] = ((state[:a] + 11 * state[:h] + 22 * state[:L]) / 451).floor
end
add_step do |value, state|
state[:day_month_calc] = (state[:h] + state[:L] - 7 * state[:m] + 114)
month = (state[:day_month_calc] / 31).floor
state[:month] = if MONTHS.has_key?(month)
MONTHS[month]
else
"Unknown"
end
end
add_step do |value, state|
state[:day] = (state[:day_month_calc] % 31) + 1
end
add_step do |value, state|
"#{state[:month]} #{state[:day]}, #{state[:year]}"
end
end
print "Enter a year >> "
year = gets.chomp.to_i
puts easter_date.calculate(nil, {base_value: year})
require "./algorithm"
include AlgorithmHelpers
# This is just a simple mathmatical "trick" that takes any number
# and computs a 3, it's only here for a simple example.
math_trick = define_algorithm do
base_value { (rand * 99 + 1).round }
add_step do |value, state|
state[:original] = value
end
add_step do |value|
value + value ** 2
end
add_step do |value, state|
value / state[:original]
end
add_step do |value|
value + 17
end
add_step do |value, state|
value - state[:original]
end
add_step do |value|
value / 6
end
end
puts math_trick.compute
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment