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 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