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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 2786a55545e9a2472089d941fa6611aea6124991
4
- data.tar.gz: 312eacef50b85dac484b9166c378a7ca359d305c
3
+ metadata.gz: 93d875177f2fb6231220227687ce2fa8cfb01a83
4
+ data.tar.gz: 2bfd4229e677bacb4838f101180574def2d1941b
5
5
  SHA512:
6
- metadata.gz: fdd35ea4a57f82c6f3c1f9e65ae28fa73d2862e7729225ce714b976466650a2e09473ea3a25c06a18fbaa76e8665294526fed8a3451649140855b79f38337a31
7
- data.tar.gz: ea460a2b63b8318bb03a51ecb6c1370794da332395241357f2b486d2495e4079489894a7271b96d4464fac734c4af96caced5cdd3248505726e0841604a6f05c
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
- calculator = Dentaku::Calculator.new
23
- calculator.evaluate('10 * 2')
24
- => 20
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
- calculator.evaluate('kiwi + 5', :kiwi => 2)
30
- => 7
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
- calculator.store(:peaches => 15)
36
- calculator.evaluate('peaches - 5')
37
- => 10
38
- calculator.evaluate('peaches >= 15')
39
- => true
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
- calculator.evaluate('5 + 3 * 2')
47
- => 11
48
- calculator.evaluate('(5 + 3) * 2')
49
- => 16
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
- calculator.evaluate('if (pears < 10, 10, 20)', :pears => 5)
56
- => 10
57
- calculator.evaluate('if (pears < 10, 10, 20)', :pears => 15)
58
- => 20
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
- calculator.evaluate('round(8.2)')
63
- => 8
64
- calculator.evaluate('round(8.2759, 2)')
65
- => 8.28
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
- Dentaku('plums * 1.5', {:plums => 2})
73
- => 3.0
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
- See `spec/external_function_spec.rb` for examples of how to add your own functions.
88
-
89
- The short, dense version:
90
-
91
- Each rule for an external function consists of three parts: the function's name,
92
- a list of tokens describing its signature (parameters), and a lambda representing the
93
- function's body.
94
-
95
- The function name should be passed as a symbol (for example, `:func`).
96
-
97
- The token list should consist of `:numeric` or `:string` if a single value of the named
98
- type should be passed; `:non_group` or `:non_group_star` for grouped expressions.
99
-
100
- > (what's the difference? when would you use one instead of the other?)
101
-
102
- The function body should accept a list of parameters. Each function body will be passed
103
- a sequence of tokens, in order:
104
-
105
- 1. The function's name
106
- 2. A token representing the opening parenthesis
107
- 3. Tokens representing the parameter values, separated by tokens representing the commas between parameters
108
- 4. A token representing the closing parenthesis
109
-
110
- It should return a token (either `:numeric` or `:string`) representing the return value.
111
-
112
- Rules can be set individually using Calculator#add_rule, or en masse using Calculator#add_rules.
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 :default => :spec
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
@@ -11,13 +11,13 @@ module Dentaku
11
11
  clear
12
12
  end
13
13
 
14
- def add_rule(new_rule)
15
- Rules.add_rule new_rule
14
+ def add_function(fn)
15
+ Rules.add_function(fn)
16
16
  self
17
17
  end
18
18
 
19
- def add_rules(new_rules)
20
- new_rules.each { | r | Rules.add_rule r }
19
+ def add_functions(fns)
20
+ fns.each { |fn| Rules.add_function(fn) }
21
21
  self
22
22
  end
23
23
 
@@ -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
- token_stream.insert start, *(func.call(*expr))
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
@@ -0,0 +1,10 @@
1
+ class ExternalFunction < Struct.new(:name, :type, :signature, :body)
2
+ def initialize(*)
3
+ super
4
+ self.name = self.name.to_sym
5
+ end
6
+
7
+ def tokens
8
+ signature.flat_map { |t| [t, :comma] }[0...-1]
9
+ end
10
+ end
@@ -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(:round_one), :round ],
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.add_rule(new_rule)
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; for precedence?
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(*new_rule[:tokens]),
46
- t(:close),
46
+ *pattern(*ext.tokens),
47
+ t(:close)
47
48
  ],
48
- name
49
+ ext.name
49
50
  ]
50
- @funcs[name] = new_rule[:body]
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, :non_group_star, :close),
79
- math_add: pattern(:numeric, :addsub, :numeric),
80
- math_mul: pattern(:numeric, :muldiv, :numeric),
81
- math_pow: pattern(:numeric, :pow, :numeric),
82
- math_mod: pattern(:numeric, :mod, :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, :mod),
85
- range_asc: pattern(:numeric, :comp_lt, :numeric, :comp_lt, :numeric),
86
- range_desc: pattern(:numeric, :comp_gt, :numeric, :comp_gt, :numeric),
87
- num_comp: pattern(:numeric, :comparator, :numeric),
88
- str_comp: pattern(:string, :comparator, :string),
89
- combine: pattern(:logical, :combinator, :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
- round_one: func_pattern(:round, :non_group_star),
93
- round_two: func_pattern(:round, :non_group_star, :comma, :numeric),
94
- roundup: func_pattern(:roundup, :non_group_star),
95
- rounddown: func_pattern(:rounddown, :non_group_star),
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 = 1.0/0
42
+ @max = Float::INFINITY
43
43
  self
44
44
  end
45
45
 
46
46
  def plus
47
- @max = 1.0/0
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
 
@@ -1,3 +1,3 @@
1
1
  module Dentaku
2
- VERSION = "0.2.14"
2
+ VERSION = "1.0.0"
3
3
  end
@@ -7,25 +7,31 @@ describe Dentaku::Calculator do
7
7
  let(:with_external_funcs) do
8
8
  c = described_class.new
9
9
 
10
- rule = { :name => :now, :tokens => [], :body => ->(*args) { Dentaku::Token.new(:string, Time.now.to_s) } }
11
- c.add_rule rule
10
+ now = { name: :now, type: :string, signature: [], body: -> { Time.now.to_s } }
11
+ c.add_function(now)
12
12
 
13
- new_rules = [
13
+ fns = [
14
14
  {
15
- :name => :exp,
16
- :tokens => [ :non_group_star, :comma, :non_group_star ],
17
- :body => ->(*args) do
18
- ## first one is function name
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.add_rules new_rules
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)', :mantissa => 2, :exponent => 4).should eq(16)
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.2.14
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-01-24 00:00:00.000000000 Z
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