Skip to content

Commit cb0c0e5

Browse files
Fabian Segurarubysolo
Fabian Segura
authored andcommitted
feat: add lambda function to process external function arguments
Closes #290
1 parent 0b841e9 commit cb0c0e5

File tree

4 files changed

+90
-18
lines changed

4 files changed

+90
-18
lines changed

lib/dentaku/ast/function_registry.rb

+10-1
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ def get(name)
88
nil
99
end
1010

11-
def register(name, type, implementation)
11+
def register(name, type, implementation, callback = nil)
1212
function = Class.new(Function) do
1313
def self.name=(name)
1414
@name = name
@@ -34,6 +34,14 @@ def self.type
3434
@type
3535
end
3636

37+
def self.callback=(callback)
38+
@callback = callback
39+
end
40+
41+
def self.callback
42+
@callback
43+
end
44+
3745
def self.arity
3846
@implementation.arity < 0 ? nil : @implementation.arity
3947
end
@@ -61,6 +69,7 @@ def type
6169
function.name = name
6270
function.type = type
6371
function.implementation = implementation
72+
function.callback = callback
6473

6574
self[function_name(name)] = function
6675
end

lib/dentaku/calculator.rb

+5-5
Original file line numberDiff line numberDiff line change
@@ -24,17 +24,17 @@ def initialize(options = {})
2424
@function_registry = Dentaku::AST::FunctionRegistry.new
2525
end
2626

27-
def self.add_function(name, type, body)
28-
Dentaku::AST::FunctionRegistry.default.register(name, type, body)
27+
def self.add_function(name, type, body, callback = nil)
28+
Dentaku::AST::FunctionRegistry.default.register(name, type, body, callback)
2929
end
3030

31-
def add_function(name, type, body)
32-
@function_registry.register(name, type, body)
31+
def add_function(name, type, body, callback = nil)
32+
@function_registry.register(name, type, body, callback)
3333
self
3434
end
3535

3636
def add_functions(fns)
37-
fns.each { |(name, type, body)| add_function(name, type, body) }
37+
fns.each { |(name, type, body, callback)| add_function(name, type, body, callback) }
3838
self
3939
end
4040

lib/dentaku/parser.rb

+4
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,10 @@ def consume(count = 2)
6161
args = Array.new(args_size) { output.pop }.reverse
6262

6363
output.push operator.new(*args)
64+
65+
if operator.respond_to?(:callback) && !operator.callback.nil?
66+
operator.callback.call(args)
67+
end
6468
rescue ::ArgumentError => e
6569
raise Dentaku::ArgumentError, e.message
6670
rescue NodeError => e

spec/external_function_spec.rb

+71-12
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,7 @@
55
describe Dentaku::Calculator do
66
describe 'functions' do
77
describe 'external functions' do
8-
9-
let(:with_external_funcs) do
8+
let(:custom_calculator) do
109
c = described_class.new
1110

1211
c.add_function(:now, :string, -> { Time.now.to_s })
@@ -22,30 +21,30 @@
2221
end
2322

2423
it 'includes NOW' do
25-
now = with_external_funcs.evaluate('NOW()')
24+
now = custom_calculator.evaluate('NOW()')
2625
expect(now).not_to be_nil
2726
expect(now).not_to be_empty
2827
end
2928

3029
it 'includes POW' do
31-
expect(with_external_funcs.evaluate('POW(2,3)')).to eq(8)
32-
expect(with_external_funcs.evaluate('POW(3,2)')).to eq(9)
33-
expect(with_external_funcs.evaluate('POW(mantissa,exponent)', mantissa: 2, exponent: 4)).to eq(16)
30+
expect(custom_calculator.evaluate('POW(2,3)')).to eq(8)
31+
expect(custom_calculator.evaluate('POW(3,2)')).to eq(9)
32+
expect(custom_calculator.evaluate('POW(mantissa,exponent)', mantissa: 2, exponent: 4)).to eq(16)
3433
end
3534

3635
it 'includes BIGGEST' do
37-
expect(with_external_funcs.evaluate('BIGGEST(8,6,7,5,3,0,9)')).to eq(9)
36+
expect(custom_calculator.evaluate('BIGGEST(8,6,7,5,3,0,9)')).to eq(9)
3837
end
3938

4039
it 'includes SMALLEST' do
41-
expect(with_external_funcs.evaluate('SMALLEST(8,6,7,5,3,0,9)')).to eq(0)
40+
expect(custom_calculator.evaluate('SMALLEST(8,6,7,5,3,0,9)')).to eq(0)
4241
end
4342

4443
it 'includes OPTIONAL' do
45-
expect(with_external_funcs.evaluate('OPTIONAL(1,2)')).to eq(3)
46-
expect(with_external_funcs.evaluate('OPTIONAL(1,2,3)')).to eq(6)
47-
expect { with_external_funcs.dependencies('OPTIONAL()') }.to raise_error(Dentaku::ParseError)
48-
expect { with_external_funcs.dependencies('OPTIONAL(1,2,3,4)') }.to raise_error(Dentaku::ParseError)
44+
expect(custom_calculator.evaluate('OPTIONAL(1,2)')).to eq(3)
45+
expect(custom_calculator.evaluate('OPTIONAL(1,2,3)')).to eq(6)
46+
expect { custom_calculator.dependencies('OPTIONAL()') }.to raise_error(Dentaku::ParseError)
47+
expect { custom_calculator.dependencies('OPTIONAL(1,2,3,4)') }.to raise_error(Dentaku::ParseError)
4948
end
5049

5150
it 'supports array parameters' do
@@ -62,6 +61,66 @@
6261
end
6362
end
6463

64+
describe 'with callbacks' do
65+
let(:custom_calculator) do
66+
c = described_class.new
67+
68+
@counts = Hash.new(0)
69+
70+
@initial_time = "2023-02-03"
71+
@last_time = @initial_time
72+
73+
c.add_function(
74+
:reverse,
75+
:stringl,
76+
->(a) { a.reverse },
77+
lambda do |args|
78+
args.each do |arg|
79+
@counts[arg.value] += 1 if arg.type == :string
80+
end
81+
end
82+
)
83+
84+
fns = [
85+
[:biggest_callback, :numeric, ->(*args) { args.max }, ->(args) { args.each { |arg| raise Dentaku::ArgumentError unless arg.type == :numeric } }],
86+
[:pythagoras, :numeric, ->(l1, l2) { Math.sqrt(l1**2 + l2**2) }, ->(e) { @last_time = Time.now.to_s }],
87+
[:callback_lambda, :string, ->() { " " }, ->() { "lambda executed" }],
88+
[:no_lambda_function, :numeric, ->(a) { a**a }],
89+
]
90+
91+
c.add_functions(fns)
92+
end
93+
94+
it 'includes BIGGEST_CALLBACK' do
95+
expect(custom_calculator.evaluate('BIGGEST_CALLBACK(1, 2, 5, 4)')).to eq(5)
96+
expect { custom_calculator.dependencies('BIGGEST_CALLBACK(1, 3, 6, "hi", 10)') }.to raise_error(Dentaku::ArgumentError)
97+
end
98+
99+
it 'includes REVERSE' do
100+
expect(custom_calculator.evaluate('REVERSE(\'Dentaku\')')).to eq('ukatneD')
101+
expect { custom_calculator.evaluate('REVERSE(22)') }.to raise_error(NoMethodError)
102+
expect(@counts["Dentaku"]).to eq(1)
103+
end
104+
105+
it 'includes PYTHAGORAS' do
106+
expect(custom_calculator.evaluate('PYTHAGORAS(8, 7)')).to eq(10.63014581273465)
107+
expect(custom_calculator.evaluate('PYTHAGORAS(3, 4)')).to eq(5)
108+
expect(@last_time).not_to eq(@initial_time)
109+
end
110+
111+
it 'exposes the `callback` method of a function' do
112+
expect(Dentaku::AST::Function::Callback_lambda.callback.call()).to eq("lambda executed")
113+
end
114+
115+
it 'does not add a `callback` method to built-in functions' do
116+
expect { Dentaku::AST::If.callback.call }.to raise_error(NoMethodError)
117+
end
118+
119+
it 'defaults `callback` method to nil if not specified' do
120+
expect(Dentaku::AST::Function::No_lambda_function.callback).to eq(nil)
121+
end
122+
end
123+
65124
it 'allows registering "bang" functions' do
66125
calculator = described_class.new
67126
calculator.add_function(:hey!, :string, -> { "hey!" })

0 commit comments

Comments
 (0)