dentaku 0.2.14 → 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +105 -52
- data/Rakefile +16 -1
- data/lib/dentaku/calculator.rb +4 -4
- data/lib/dentaku/evaluator.rb +11 -1
- data/lib/dentaku/external_function.rb +10 -0
- data/lib/dentaku/rules.rb +27 -27
- data/lib/dentaku/token_matcher.rb +3 -2
- data/lib/dentaku/version.rb +1 -1
- data/spec/external_function_spec.rb +29 -15
- metadata +3 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 93d875177f2fb6231220227687ce2fa8cfb01a83
|
4
|
+
data.tar.gz: 2bfd4229e677bacb4838f101180574def2d1941b
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 95daa0fa7ff7bc708492f0747ef33601c555b32a441f5b50b13c8201edbc3f76d0d606cdd8057ee408e271a9869f1ff1a34ca8219d19e2bd00fc5119c52ea311
|
7
|
+
data.tar.gz: 25e9184f6f077d7af863e434dbdbd87e505cf44d4fd3baf9c1a69400de77912f31b39ff48ad94f412ac3e759acd3a5ead9943837484b8555cc83d9e333a3a6e4
|
data/README.md
CHANGED
@@ -5,8 +5,6 @@ Dentaku
|
|
5
5
|
[![Build Status](https://travis-ci.org/rubysolo/dentaku.png?branch=master)](https://travis-ci.org/rubysolo/dentaku)
|
6
6
|
[![Code Climate](https://codeclimate.com/github/rubysolo/dentaku.png)](https://codeclimate.com/github/rubysolo/dentaku)
|
7
7
|
|
8
|
-
http://github.com/rubysolo/dentaku
|
9
|
-
|
10
8
|
DESCRIPTION
|
11
9
|
-----------
|
12
10
|
|
@@ -19,97 +17,152 @@ EXAMPLE
|
|
19
17
|
|
20
18
|
This is probably simplest to illustrate in code:
|
21
19
|
|
22
|
-
|
23
|
-
|
24
|
-
|
20
|
+
```ruby
|
21
|
+
calculator = Dentaku::Calculator.new
|
22
|
+
calculator.evaluate('10 * 2')
|
23
|
+
=> 20
|
24
|
+
```
|
25
25
|
|
26
26
|
Okay, not terribly exciting. But what if you want to have a reference to a
|
27
27
|
variable, and evaluate it at run-time? Here's how that would look:
|
28
28
|
|
29
|
-
|
30
|
-
|
29
|
+
```ruby
|
30
|
+
calculator.evaluate('kiwi + 5', kiwi: 2)
|
31
|
+
=> 7
|
32
|
+
```
|
31
33
|
|
32
34
|
You can also store the variable values in the calculator's memory and then
|
33
35
|
evaluate expressions against those stored values:
|
34
36
|
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
37
|
+
```ruby
|
38
|
+
calculator.store(peaches: 15)
|
39
|
+
calculator.evaluate('peaches - 5')
|
40
|
+
=> 10
|
41
|
+
calculator.evaluate('peaches >= 15')
|
42
|
+
=> true
|
43
|
+
```
|
40
44
|
|
41
45
|
For maximum CS geekery, `bind` is an alias of `store`.
|
42
46
|
|
43
47
|
Dentaku understands precedence order and using parentheses to group expressions
|
44
48
|
to ensure proper evaluation:
|
45
49
|
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
+
```ruby
|
51
|
+
calculator.evaluate('5 + 3 * 2')
|
52
|
+
=> 11
|
53
|
+
calculator.evaluate('(5 + 3) * 2')
|
54
|
+
=> 16
|
55
|
+
```
|
50
56
|
|
51
57
|
A number of functions are also supported. Okay, the number is currently five,
|
52
58
|
but more will be added soon. The current functions are
|
53
59
|
`if`, `not`, `round`, `rounddown`, and `roundup`, and they work like their counterparts in Excel:
|
54
60
|
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
61
|
+
```ruby
|
62
|
+
calculator.evaluate('if (pears < 10, 10, 20)', pears: 5)
|
63
|
+
=> 10
|
64
|
+
calculator.evaluate('if (pears < 10, 10, 20)', pears: 15)
|
65
|
+
=> 20
|
66
|
+
```
|
59
67
|
|
60
68
|
`round`, `rounddown`, and `roundup` can be called with or without the number of decimal places:
|
61
69
|
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
70
|
+
```ruby
|
71
|
+
calculator.evaluate('round(8.2)')
|
72
|
+
=> 8
|
73
|
+
calculator.evaluate('round(8.2759, 2)')
|
74
|
+
=> 8.28
|
75
|
+
```
|
66
76
|
|
67
77
|
`round` and `rounddown` round down, while `roundup` rounds up.
|
68
78
|
|
69
79
|
If you're too lazy to be building calculator objects, there's a shortcut just
|
70
80
|
for you:
|
71
81
|
|
72
|
-
|
73
|
-
|
82
|
+
```ruby
|
83
|
+
Dentaku('plums * 1.5', plums: 2)
|
84
|
+
=> 3.0
|
85
|
+
```
|
74
86
|
|
75
87
|
|
76
88
|
BUILT-IN OPERATORS AND FUNCTIONS
|
77
89
|
---------------------------------
|
78
90
|
|
79
91
|
Math: `+ - * / %`
|
92
|
+
|
80
93
|
Logic: `< > <= >= <> != = AND OR`
|
94
|
+
|
81
95
|
Functions: `IF NOT ROUND ROUNDDOWN ROUNDUP`
|
82
96
|
|
83
97
|
|
84
98
|
EXTERNAL FUNCTIONS
|
85
99
|
------------------
|
86
100
|
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
101
|
+
I don't know everything, so I might not have implemented all the functions you
|
102
|
+
need. Please implement your favorites and send a pull request! Okay, so maybe
|
103
|
+
that's not feasible because:
|
104
|
+
|
105
|
+
1. You can't be bothered to share
|
106
|
+
2. You can't wait for me to respond to a pull request, you need it `NOW()`
|
107
|
+
3. The formula is the secret sauce for your startup
|
108
|
+
|
109
|
+
Whatever your reasons, Dentaku supports adding functions at runtime. To add a
|
110
|
+
function, you'll need to specify:
|
111
|
+
|
112
|
+
* Name
|
113
|
+
* Return type
|
114
|
+
* Signature
|
115
|
+
* Body
|
116
|
+
|
117
|
+
Naming can be the hardest part, so you're on your own for that.
|
118
|
+
|
119
|
+
`:type` specifies the type of value that will be returned, most likely
|
120
|
+
`:numeric`, `:string`, or `:logical`.
|
121
|
+
|
122
|
+
`:signature` specifies the types and order of the parameters for your function.
|
123
|
+
|
124
|
+
`:body` is a lambda that implements your function. It is passed the arguments
|
125
|
+
and should return the calculated value.
|
126
|
+
|
127
|
+
As an example, the exponentiation function takes two parameters, the mantissa
|
128
|
+
and the exponent, so the token list could be defined as: `[:numeric,
|
129
|
+
:numeric]`. Other functions might be variadic -- consider `max`, a function
|
130
|
+
that takes any number of numeric inputs and returns the largest one. Its token
|
131
|
+
list could be defined as: `[:non_close_plus]` (one or more tokens that are not
|
132
|
+
closing parentheses.
|
133
|
+
|
134
|
+
Functions can be added individually using Calculator#add_function, or en masse using
|
135
|
+
Calculator#add_functions.
|
136
|
+
|
137
|
+
Here's an example of adding the `exp` function:
|
138
|
+
|
139
|
+
```ruby
|
140
|
+
> c = Dentaku::Calculator.new
|
141
|
+
> c.add_function(
|
142
|
+
name: :exp,
|
143
|
+
type: :numeric,
|
144
|
+
signature: [:numeric, :numeric],
|
145
|
+
body: ->(mantissa, exponent) { mantissa ** exponent }
|
146
|
+
)
|
147
|
+
> c.evaluate('EXP(3,2)')
|
148
|
+
=> 9
|
149
|
+
> c.evaluate('EXP(2,3)')
|
150
|
+
=> 8
|
151
|
+
```
|
152
|
+
|
153
|
+
Here's an example of adding the `max` function:
|
154
|
+
|
155
|
+
```ruby
|
156
|
+
> c = Dentaku::Calculator.new
|
157
|
+
> c.add_function(
|
158
|
+
name: :max,
|
159
|
+
type: :numeric,
|
160
|
+
signature: [:non_close_plus],
|
161
|
+
body: ->(*args) { args.max }
|
162
|
+
)
|
163
|
+
> c.evaluate 'MAX(5,3,9,6,2)'
|
164
|
+
=> 9
|
165
|
+
```
|
113
166
|
|
114
167
|
|
115
168
|
THANKS
|
data/Rakefile
CHANGED
@@ -10,4 +10,19 @@ task :spec do
|
|
10
10
|
end
|
11
11
|
|
12
12
|
desc "Default: run specs."
|
13
|
-
task :
|
13
|
+
task default: :spec
|
14
|
+
|
15
|
+
task :console do
|
16
|
+
begin
|
17
|
+
require 'pry'
|
18
|
+
console = Pry
|
19
|
+
rescue LoadError
|
20
|
+
require 'irb'
|
21
|
+
require 'irb/completion'
|
22
|
+
console = IRB
|
23
|
+
end
|
24
|
+
|
25
|
+
require 'dentaku'
|
26
|
+
ARGV.clear
|
27
|
+
console.start
|
28
|
+
end
|
data/lib/dentaku/calculator.rb
CHANGED
@@ -11,13 +11,13 @@ module Dentaku
|
|
11
11
|
clear
|
12
12
|
end
|
13
13
|
|
14
|
-
def
|
15
|
-
Rules.
|
14
|
+
def add_function(fn)
|
15
|
+
Rules.add_function(fn)
|
16
16
|
self
|
17
17
|
end
|
18
18
|
|
19
|
-
def
|
20
|
-
|
19
|
+
def add_functions(fns)
|
20
|
+
fns.each { |fn| Rules.add_function(fn) }
|
21
21
|
self
|
22
22
|
end
|
23
23
|
|
data/lib/dentaku/evaluator.rb
CHANGED
@@ -55,15 +55,25 @@ module Dentaku
|
|
55
55
|
|
56
56
|
def evaluate_step(token_stream, start, length, evaluator)
|
57
57
|
expr = token_stream.slice!(start, length)
|
58
|
+
|
58
59
|
if self.respond_to?(evaluator)
|
59
60
|
token_stream.insert start, *self.send(evaluator, *expr)
|
60
61
|
else
|
61
62
|
func = Rules.func(evaluator)
|
62
63
|
raise "unknown evaluator '#{evaluator.to_s}'" if func.nil?
|
63
|
-
|
64
|
+
|
65
|
+
arguments = extract_arguments_from_function_call(expr).map { |t| t.value }
|
66
|
+
return_value = func.body.call(*arguments)
|
67
|
+
|
68
|
+
token_stream.insert start, Token.new(func.type, return_value)
|
64
69
|
end
|
65
70
|
end
|
66
71
|
|
72
|
+
def extract_arguments_from_function_call(tokens)
|
73
|
+
_function_name, _open, *args_and_commas, _close = tokens
|
74
|
+
args_and_commas.reject { |token| token.is?(:grouping) }
|
75
|
+
end
|
76
|
+
|
67
77
|
def evaluate_group(*args)
|
68
78
|
evaluate_token_stream(args[1..-2])
|
69
79
|
end
|
data/lib/dentaku/rules.rb
CHANGED
@@ -1,3 +1,4 @@
|
|
1
|
+
require 'dentaku/external_function'
|
1
2
|
require 'dentaku/token'
|
2
3
|
require 'dentaku/token_matcher'
|
3
4
|
|
@@ -6,8 +7,7 @@ module Dentaku
|
|
6
7
|
def self.core_rules
|
7
8
|
[
|
8
9
|
[ p(:if), :if ],
|
9
|
-
[ p(:
|
10
|
-
[ p(:round_two), :round ],
|
10
|
+
[ p(:round), :round ],
|
11
11
|
[ p(:roundup), :round_int ],
|
12
12
|
[ p(:rounddown), :round_int ],
|
13
13
|
[ p(:not), :not ],
|
@@ -32,22 +32,23 @@ module Dentaku
|
|
32
32
|
@rules.each { |r| yield r }
|
33
33
|
end
|
34
34
|
|
35
|
-
def self.
|
35
|
+
def self.add_function(f)
|
36
|
+
ext = ExternalFunction.new(f[:name], f[:type], f[:signature], f[:body])
|
37
|
+
|
36
38
|
@rules ||= core_rules
|
37
39
|
@funcs ||= {}
|
38
|
-
name = new_rule[:name].to_sym
|
39
40
|
|
40
|
-
## rules need to be added to the beginning of @rules
|
41
|
+
## rules need to be added to the beginning of @rules for precedence
|
41
42
|
@rules.unshift [
|
42
43
|
[
|
43
|
-
TokenMatcher.send(name),
|
44
|
+
TokenMatcher.send(ext.name),
|
44
45
|
t(:open),
|
45
|
-
*pattern(*
|
46
|
-
t(:close)
|
46
|
+
*pattern(*ext.tokens),
|
47
|
+
t(:close)
|
47
48
|
],
|
48
|
-
name
|
49
|
+
ext.name
|
49
50
|
]
|
50
|
-
@funcs[name] =
|
51
|
+
@funcs[ext.name] = ext
|
51
52
|
end
|
52
53
|
|
53
54
|
def self.func(name)
|
@@ -65,7 +66,7 @@ module Dentaku
|
|
65
66
|
:numeric, :string, :addsub, :subtract, :muldiv, :pow, :mod,
|
66
67
|
:comparator, :comp_gt, :comp_lt,
|
67
68
|
:open, :close, :comma,
|
68
|
-
:non_group, :non_group_star,
|
69
|
+
:non_close_plus, :non_group, :non_group_star,
|
69
70
|
:logical, :combinator,
|
70
71
|
:if, :round, :roundup, :rounddown, :not
|
71
72
|
].each_with_object({}) do |name, matchers|
|
@@ -75,25 +76,24 @@ module Dentaku
|
|
75
76
|
|
76
77
|
def self.p(name)
|
77
78
|
@patterns ||= {
|
78
|
-
group: pattern(:open,
|
79
|
-
math_add: pattern(:numeric,
|
80
|
-
math_mul: pattern(:numeric,
|
81
|
-
math_pow: pattern(:numeric,
|
82
|
-
math_mod: pattern(:numeric,
|
79
|
+
group: pattern(:open, :non_group_star, :close),
|
80
|
+
math_add: pattern(:numeric, :addsub, :numeric),
|
81
|
+
math_mul: pattern(:numeric, :muldiv, :numeric),
|
82
|
+
math_pow: pattern(:numeric, :pow, :numeric),
|
83
|
+
math_mod: pattern(:numeric, :mod, :numeric),
|
83
84
|
negation: pattern(:subtract, :numeric),
|
84
|
-
percentage: pattern(:numeric,
|
85
|
-
range_asc: pattern(:numeric,
|
86
|
-
range_desc: pattern(:numeric,
|
87
|
-
num_comp: pattern(:numeric,
|
88
|
-
str_comp: pattern(:string,
|
89
|
-
combine: pattern(:logical,
|
85
|
+
percentage: pattern(:numeric, :mod),
|
86
|
+
range_asc: pattern(:numeric, :comp_lt, :numeric, :comp_lt, :numeric),
|
87
|
+
range_desc: pattern(:numeric, :comp_gt, :numeric, :comp_gt, :numeric),
|
88
|
+
num_comp: pattern(:numeric, :comparator, :numeric),
|
89
|
+
str_comp: pattern(:string, :comparator, :string),
|
90
|
+
combine: pattern(:logical, :combinator, :logical),
|
90
91
|
|
91
92
|
if: func_pattern(:if, :non_group, :comma, :non_group, :comma, :non_group),
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
not: func_pattern(:not, :non_group_star)
|
93
|
+
round: func_pattern(:round, :non_close_plus),
|
94
|
+
roundup: func_pattern(:roundup, :non_close_plus),
|
95
|
+
rounddown: func_pattern(:rounddown, :non_close_plus),
|
96
|
+
not: func_pattern(:not, :non_close_plus)
|
97
97
|
}
|
98
98
|
|
99
99
|
@patterns[name]
|
@@ -39,12 +39,12 @@ module Dentaku
|
|
39
39
|
|
40
40
|
def star
|
41
41
|
@min = 0
|
42
|
-
@max =
|
42
|
+
@max = Float::INFINITY
|
43
43
|
self
|
44
44
|
end
|
45
45
|
|
46
46
|
def plus
|
47
|
-
@max =
|
47
|
+
@max = Float::INFINITY
|
48
48
|
self
|
49
49
|
end
|
50
50
|
|
@@ -78,6 +78,7 @@ module Dentaku
|
|
78
78
|
def self.roundup; new(:function, :roundup); end
|
79
79
|
def self.rounddown; new(:function, :rounddown); end
|
80
80
|
def self.not; new(:function, :not); end
|
81
|
+
def self.non_close_plus; new(:grouping, :close).invert.plus; end
|
81
82
|
def self.non_group; new(:grouping).invert; end
|
82
83
|
def self.non_group_star; new(:grouping).invert.star; end
|
83
84
|
|
data/lib/dentaku/version.rb
CHANGED
@@ -7,25 +7,31 @@ describe Dentaku::Calculator do
|
|
7
7
|
let(:with_external_funcs) do
|
8
8
|
c = described_class.new
|
9
9
|
|
10
|
-
|
11
|
-
c.
|
10
|
+
now = { name: :now, type: :string, signature: [], body: -> { Time.now.to_s } }
|
11
|
+
c.add_function(now)
|
12
12
|
|
13
|
-
|
13
|
+
fns = [
|
14
14
|
{
|
15
|
-
|
16
|
-
:
|
17
|
-
:
|
18
|
-
|
19
|
-
## second one is open parenthesis
|
20
|
-
## last one is close parenthesis
|
21
|
-
## all others are commas
|
22
|
-
_, _, mantissa, _, exponent, _ = args
|
23
|
-
Dentaku::Token.new(:numeric, (mantissa.value ** exponent.value))
|
24
|
-
end
|
15
|
+
name: :exp,
|
16
|
+
type: :numeric,
|
17
|
+
signature: [ :numeric, :numeric ],
|
18
|
+
body: ->(mantissa, exponent) { mantissa ** exponent }
|
25
19
|
},
|
20
|
+
{
|
21
|
+
name: :max,
|
22
|
+
type: :numeric,
|
23
|
+
signature: [ :non_close_plus ],
|
24
|
+
body: ->(*args) { args.max }
|
25
|
+
},
|
26
|
+
{
|
27
|
+
name: :min,
|
28
|
+
type: :numeric,
|
29
|
+
signature: [ :non_close_plus ],
|
30
|
+
body: ->(*args) { args.min }
|
31
|
+
}
|
26
32
|
]
|
27
33
|
|
28
|
-
c.
|
34
|
+
c.add_functions(fns)
|
29
35
|
end
|
30
36
|
|
31
37
|
it 'should include NOW' do
|
@@ -37,7 +43,15 @@ describe Dentaku::Calculator do
|
|
37
43
|
it 'should include EXP' do
|
38
44
|
with_external_funcs.evaluate('EXP(2,3)').should eq(8)
|
39
45
|
with_external_funcs.evaluate('EXP(3,2)').should eq(9)
|
40
|
-
with_external_funcs.evaluate('EXP(mantissa,exponent)', :
|
46
|
+
with_external_funcs.evaluate('EXP(mantissa,exponent)', mantissa: 2, exponent: 4).should eq(16)
|
47
|
+
end
|
48
|
+
|
49
|
+
it 'should include MAX' do
|
50
|
+
with_external_funcs.evaluate('MAX(8,6,7,5,3,0,9)').should eq(9)
|
51
|
+
end
|
52
|
+
|
53
|
+
it 'should include MIN' do
|
54
|
+
with_external_funcs.evaluate('MIN(8,6,7,5,3,0,9)').should eq(0)
|
41
55
|
end
|
42
56
|
end
|
43
57
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: dentaku
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 1.0.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Solomon White
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2014-
|
11
|
+
date: 2014-03-06 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rake
|
@@ -56,6 +56,7 @@ files:
|
|
56
56
|
- lib/dentaku/binary_operation.rb
|
57
57
|
- lib/dentaku/calculator.rb
|
58
58
|
- lib/dentaku/evaluator.rb
|
59
|
+
- lib/dentaku/external_function.rb
|
59
60
|
- lib/dentaku/rules.rb
|
60
61
|
- lib/dentaku/token.rb
|
61
62
|
- lib/dentaku/token_matcher.rb
|