Last active
January 3, 2016 04:39
-
-
Save bbuck/8410291 to your computer and use it in GitHub Desktop.
Small Ruby Algorithm DSL
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# 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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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