Skip to content

Commit

Permalink
Added support for Math functions.
Browse files Browse the repository at this point in the history
Closes #51
  • Loading branch information
dblock authored and rubysolo committed Jun 20, 2015
1 parent 4673ea5 commit 634cf74
Show file tree
Hide file tree
Showing 10 changed files with 142 additions and 60 deletions.
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,8 @@ Logic: `< > <= >= <> != = AND OR`

Functions: `IF NOT ROUND ROUNDDOWN ROUNDUP`

Math: all functions from Ruby's Math module, including `SIN, COS, TAN, ...`

RESOLVING DEPENDENCIES
----------------------

Expand Down
2 changes: 2 additions & 0 deletions lib/dentaku/evaluator.rb
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
require 'dentaku/rule_set'
require 'dentaku/binary_operation'
require 'dentaku/math'

module Dentaku
class Evaluator
include Dentaku::Math
attr_reader :rule_set

def initialize(rule_set)
Expand Down
40 changes: 40 additions & 0 deletions lib/dentaku/math.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
require 'dentaku/token_matchers'

module Dentaku
module Math
def self.exported_methods
::Math.methods(false)
end

def self.rules
Math.exported_methods.map do |method|
[pattern(method), :delegate_to_math]
end
end

def self.patterns
@patterns ||= Hash[Math.exported_methods.map { |method|
[method, TokenMatchers.function_token_matchers(method, :arguments)]
}]
end

def self.pattern(name)
patterns[name]
end

def delegate_to_math(func_token, *args)
function = func_token.value
_, *tokens, _ = *args
values = tokens.reject(&:grouping?).map(&:value)

arity = ::Math.method(function).arity
unless values.length == arity || arity == -1
raise "Wrong number of arguments (#{ values.length } for #{ arity })"
end

Token.new(:numeric, ::Math.send(function, *values))
end

extend self
end
end
59 changes: 25 additions & 34 deletions lib/dentaku/rule_set.rb
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
require 'dentaku/token_matchers'
require 'dentaku/external_function'

module Dentaku
Expand All @@ -19,7 +20,7 @@ def add_function(function)
fn = ExternalFunction.new(function[:name], function[:type], function[:signature], function[:body])

custom_rules.push [
function_token_matchers(fn.name, *fn.tokens),
TokenMatchers.function_token_matchers(fn.name, *fn.tokens),
fn.name
]

Expand Down Expand Up @@ -93,47 +94,37 @@ def core_rules
[ pattern(:num_comp), :apply ],
[ pattern(:str_comp), :apply ],
[ pattern(:combine), :apply ]
]
].concat(Math.rules)
end

def pattern(name)
@patterns ||= {
group: token_matchers(:open, :non_group_star, :close),
math_add: token_matchers(:numeric, :addsub, :numeric),
math_mul: token_matchers(:numeric, :muldiv, :numeric),
math_neg_mul: token_matchers(:numeric, :muldiv, :subtract, :numeric),
math_pow: token_matchers(:numeric, :pow, :numeric),
math_neg_pow: token_matchers(:numeric, :pow, :subtract, :numeric),
math_mod: token_matchers(:numeric, :mod, :numeric),
negation: token_matchers(:subtract, :numeric),
start_neg: token_matchers(:anchored_minus, :numeric),
percentage: token_matchers(:numeric, :mod),
range_asc: token_matchers(:numeric, :comp_lt, :numeric, :comp_lt, :numeric),
range_desc: token_matchers(:numeric, :comp_gt, :numeric, :comp_gt, :numeric),
num_comp: token_matchers(:numeric, :comparator, :numeric),
str_comp: token_matchers(:string, :comparator, :string),
combine: token_matchers(:logical, :combinator, :logical),

if: function_token_matchers(:if, :non_group, :comma, :non_group, :comma, :non_group),
round: function_token_matchers(:round, :arguments),
roundup: function_token_matchers(:roundup, :arguments),
rounddown: function_token_matchers(:rounddown, :arguments),
not: function_token_matchers(:not, :arguments)
}
group: TokenMatchers.token_matchers(:open, :non_group_star, :close),
math_add: TokenMatchers.token_matchers(:numeric, :addsub, :numeric),
math_mul: TokenMatchers.token_matchers(:numeric, :muldiv, :numeric),
math_neg_mul: TokenMatchers.token_matchers(:numeric, :muldiv, :subtract, :numeric),
math_pow: TokenMatchers.token_matchers(:numeric, :pow, :numeric),
math_neg_pow: TokenMatchers.token_matchers(:numeric, :pow, :subtract, :numeric),
math_mod: TokenMatchers.token_matchers(:numeric, :mod, :numeric),
negation: TokenMatchers.token_matchers(:subtract, :numeric),
start_neg: TokenMatchers.token_matchers(:anchored_minus, :numeric),
percentage: TokenMatchers.token_matchers(:numeric, :mod),
range_asc: TokenMatchers.token_matchers(:numeric, :comp_lt, :numeric, :comp_lt, :numeric),
range_desc: TokenMatchers.token_matchers(:numeric, :comp_gt, :numeric, :comp_gt, :numeric),
num_comp: TokenMatchers.token_matchers(:numeric, :comparator, :numeric),
str_comp: TokenMatchers.token_matchers(:string, :comparator, :string),
combine: TokenMatchers.token_matchers(:logical, :combinator, :logical),

if: TokenMatchers.function_token_matchers(:if, :non_group, :comma, :non_group, :comma, :non_group),
round: TokenMatchers.function_token_matchers(:round, :arguments),
roundup: TokenMatchers.function_token_matchers(:roundup, :arguments),
rounddown: TokenMatchers.function_token_matchers(:rounddown, :arguments),
not: TokenMatchers.function_token_matchers(:not, :arguments)
}.merge(Math.patterns)

@patterns[name]
end

def token_matchers(*symbols)
symbols.map { |s| matcher(s) }
end

def function_token_matchers(function_name, *symbols)
token_matchers(:fopen, *symbols, :close).unshift(
TokenMatcher.send(function_name)
)
end

def matcher(symbol)
@matchers ||= [
:numeric, :string, :addsub, :subtract, :muldiv, :pow, :mod,
Expand Down
4 changes: 4 additions & 0 deletions lib/dentaku/token.rb
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@ def length
raw_value.to_s.length
end

def grouping?
is?(:grouping)
end

def is?(c)
category == c
end
Expand Down
29 changes: 29 additions & 0 deletions lib/dentaku/token_matchers.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
module Dentaku
module TokenMatchers
def self.token_matchers(*symbols)
symbols.map { |s| matcher(s) }
end

def self.function_token_matchers(function_name, *symbols)
token_matchers(:fopen, *symbols, :close).unshift(
TokenMatcher.send(function_name)
)
end

def self.matcher(symbol)
@matchers ||= [
:numeric, :string, :addsub, :subtract, :muldiv, :pow, :mod,
:comparator, :comp_gt, :comp_lt, :fopen, :open, :close, :comma,
:non_close_plus, :non_group, :non_group_star, :arguments,
:logical, :combinator, :if, :round, :roundup, :rounddown, :not,
:anchored_minus, :math_neg_pow, :math_neg_mul
].each_with_object({}) do |name, matchers|
matchers[name] = TokenMatcher.send(name)
end

@matchers.fetch(symbol) do
raise "Unknown token symbol #{ symbol }"
end
end
end
end
12 changes: 12 additions & 0 deletions spec/calculator_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -198,4 +198,16 @@
expect(calculator.evaluate('NOT(some_boolean) AND -1 > 3', some_boolean: true)).to be_falsey
end
end

describe 'math functions' do
Math.methods(false).each do |method|
it method do
if Math.method(method).arity == 2
expect(calculator.evaluate("#{method}(1,2)")).to eq Math.send(method, 1, 2)
else
expect(calculator.evaluate("#{method}(1)")).to eq Math.send(method, 1)
end
end
end
end
end
1 change: 1 addition & 0 deletions spec/evaluator_spec.rb
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
require 'spec_helper'
require 'dentaku/token_matcher'
require 'dentaku/evaluator'

describe Dentaku::Evaluator do
Expand Down
10 changes: 5 additions & 5 deletions spec/external_function_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@

fns = [
{
name: :exp,
name: :cexp,
type: :numeric,
signature: [ :numeric, :numeric ],
body: ->(mantissa, exponent) { mantissa ** exponent }
Expand Down Expand Up @@ -41,10 +41,10 @@
expect(now).not_to be_empty
end

it 'includes EXP' do
expect(with_external_funcs.evaluate('EXP(2,3)')).to eq(8)
expect(with_external_funcs.evaluate('EXP(3,2)')).to eq(9)
expect(with_external_funcs.evaluate('EXP(mantissa,exponent)', mantissa: 2, exponent: 4)).to eq(16)
it 'includes CEXP' do
expect(with_external_funcs.evaluate('CEXP(2,3)')).to eq(8)
expect(with_external_funcs.evaluate('CEXP(3,2)')).to eq(9)
expect(with_external_funcs.evaluate('CEXP(mantissa,exponent)', mantissa: 2, exponent: 4)).to eq(16)
end

it 'includes MAX' do
Expand Down
43 changes: 22 additions & 21 deletions spec/rule_set_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,27 +6,28 @@
it 'yields core rules' do
functions = []
subject.each { |pattern, function| functions << function }
expect(functions).to eq [:if,
:round,
:round_int,
:round_int,
:not,
:evaluate_group,
:negate,
:apply,
:pow_negate,
:apply,
:apply,
:mul_negate,
:apply,
:percentage,
:negate,
:expand_range,
:expand_range,
:apply,
:apply,
:apply,
]
expect(functions).to eq [
:if,
:round,
:round_int,
:round_int,
:not,
:evaluate_group,
:negate,
:apply,
:pow_negate,
:apply,
:apply,
:mul_negate,
:apply,
:percentage,
:negate,
:expand_range,
:expand_range,
:apply,
:apply,
:apply,
].concat(Dentaku::Math.exported_methods.map { :delegate_to_math })
end

it 'adds custom function patterns' do
Expand Down

0 comments on commit 634cf74

Please sign in to comment.