dentaku 0.2.14 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|
[](https://travis-ci.org/rubysolo/dentaku)
|
6
6
|
[](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
|