dentaku 0.2.12 → 0.2.13

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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: bc04ff45c34b77ede3ebb41f6ed34a94413925ac
4
+ data.tar.gz: 19906fc893a540448be14ed1374cc1cb89b7c999
5
+ SHA512:
6
+ metadata.gz: b8ef9498e64f6050bc7fd587b5c3cae3c0489bc050810601c70820d233a03bd586ed37567b186a423a397c1571e0cb02828ed17126627b1764e7c089abe36dfd
7
+ data.tar.gz: a5d0ee82db757582c697bfb721b27a9670cf46b790b17839967fd40fc52bdaf0e91a9839be73504da6bd026fbcb750edd7235c53d23f7930aec89da77b4f839a
data/.gitignore CHANGED
@@ -5,3 +5,6 @@ Gemfile.lock
5
5
  bin/*
6
6
  pkg/*
7
7
  vendor/*
8
+
9
+ /.ruby-gemset
10
+ /.ruby-version
data/README.md CHANGED
@@ -48,22 +48,23 @@ to ensure proper evaluation:
48
48
  calculator.evaluate('(5 + 3) * 2')
49
49
  => 16
50
50
 
51
- A number of functions are also supported. Okay, the number is currently two,
52
- but more will be added soon. The current functions are `round` and `if`, and
53
- they work like their counterparts in Excel:
51
+ A number of functions are also supported. Okay, the number is currently five,
52
+ but more will be added soon. The current functions are
53
+ `if`, `not`, `round`, `rounddown`, and `roundup`, and they work like their counterparts in Excel:
54
54
 
55
55
  calculator.evaluate('if (pears < 10, 10, 20)', :pears => 5)
56
56
  => 10
57
57
  calculator.evaluate('if (pears < 10, 10, 20)', :pears => 15)
58
58
  => 20
59
59
 
60
- `Round` can be called with or without the number of decimal places:
60
+ `round`, `rounddown`, and `roundup` can be called with or without the number of decimal places:
61
61
 
62
62
  calculator.evaluate('round(8.2)')
63
63
  => 8
64
64
  calculator.evaluate('round(8.2759, 2)')
65
65
  => 8.28
66
66
 
67
+ `round` and `rounddown` round down, while `roundup` rounds up.
67
68
 
68
69
  If you're too lazy to be building calculator objects, there's a shortcut just
69
70
  for you:
@@ -72,12 +73,44 @@ for you:
72
73
  => 3.0
73
74
 
74
75
 
75
- SUPPORTED OPERATORS AND FUNCTIONS
76
+ BUILT-IN OPERATORS AND FUNCTIONS
76
77
  ---------------------------------
77
78
 
78
- Math: `+ - * /`
79
- Logic: `< > <= >= <> != = AND OR NOT`
80
- Functions: `IF ROUND`
79
+ Math: `+ - * / %`
80
+ Logic: `< > <= >= <> != = AND OR`
81
+ Functions: `IF NOT ROUND ROUNDDOWN ROUNDUP`
82
+
83
+
84
+ EXTERNAL FUNCTIONS
85
+ ------------------
86
+
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.
113
+
81
114
 
82
115
  THANKS
83
116
  ------
data/dentaku.gemspec CHANGED
@@ -12,7 +12,6 @@ Gem::Specification.new do |s|
12
12
  s.description = <<-DESC
13
13
  Dentaku is a parser and evaluator for mathematical formulas
14
14
  DESC
15
- s.license = 'MIT'
16
15
 
17
16
  s.rubyforge_project = "dentaku"
18
17
 
@@ -18,6 +18,8 @@ module Dentaku
18
18
  [:numeric, left.to_f / right.to_f]
19
19
  end
20
20
 
21
+ def mod; [:numeric, left % right]; end
22
+
21
23
  def le; [:logical, left <= right]; end
22
24
  def ge; [:logical, left >= right]; end
23
25
  def lt; [:logical, left < right]; end
@@ -1,4 +1,5 @@
1
1
  require 'dentaku/evaluator'
2
+ require 'dentaku/rules'
2
3
  require 'dentaku/token'
3
4
  require 'dentaku/tokenizer'
4
5
 
@@ -10,6 +11,16 @@ module Dentaku
10
11
  clear
11
12
  end
12
13
 
14
+ def add_rule(new_rule)
15
+ Rules.add_rule new_rule
16
+ self
17
+ end
18
+
19
+ def add_rules(new_rules)
20
+ new_rules.each { | r | Rules.add_rule r }
21
+ self
22
+ end
23
+
13
24
  def evaluate(expression, data={})
14
25
  @tokenizer ||= Tokenizer.new
15
26
  @tokens = @tokenizer.tokenize(expression)
@@ -55,7 +55,13 @@ module Dentaku
55
55
 
56
56
  def evaluate_step(token_stream, start, length, evaluator)
57
57
  expr = token_stream.slice!(start, length)
58
- token_stream.insert start, *self.send(evaluator, *expr)
58
+ if self.respond_to?(evaluator)
59
+ token_stream.insert start, *self.send(evaluator, *expr)
60
+ else
61
+ func = Rules.func(evaluator)
62
+ raise "unknown evaluator '#{evaluator.to_s}'" if func.nil?
63
+ token_stream.insert start, *(func.call(*expr))
64
+ end
59
65
  end
60
66
 
61
67
  def evaluate_group(*args)
@@ -68,6 +74,10 @@ module Dentaku
68
74
  Token.new(*operation.send(operator.value))
69
75
  end
70
76
 
77
+ def negate(_, token)
78
+ Token.new(token.category,token.value * -1)
79
+ end
80
+
71
81
  def expand_range(left, oper1, middle, oper2, right)
72
82
  [left, oper1, middle, Token.new(:combinator, :and), middle, oper2, right]
73
83
  end
data/lib/dentaku/rules.rb CHANGED
@@ -3,8 +3,8 @@ require 'dentaku/token_matcher'
3
3
 
4
4
  module Dentaku
5
5
  class Rules
6
- def self.each
7
- @rules ||= [
6
+ def self.core_rules
7
+ [
8
8
  [ p(:if), :if ],
9
9
  [ p(:round_one), :round ],
10
10
  [ p(:round_two), :round ],
@@ -14,21 +14,49 @@ module Dentaku
14
14
 
15
15
  [ p(:group), :evaluate_group ],
16
16
  [ p(:math_pow), :apply ],
17
+ [ p(:math_mod), :apply ],
17
18
  [ p(:math_mul), :apply ],
18
19
  [ p(:math_add), :apply ],
20
+ [ p(:negation), :negate ],
19
21
  [ p(:range_asc), :expand_range ],
20
22
  [ p(:range_desc), :expand_range ],
21
23
  [ p(:num_comp), :apply ],
22
24
  [ p(:str_comp), :apply ],
23
25
  [ p(:combine), :apply ]
24
26
  ]
27
+ end
25
28
 
29
+ def self.each
30
+ @rules ||= core_rules
26
31
  @rules.each { |r| yield r }
27
32
  end
28
33
 
34
+ def self.add_rule(new_rule)
35
+ @rules ||= core_rules
36
+ @funcs ||= {}
37
+ name = new_rule[:name].to_sym
38
+
39
+ ## rules need to be added to the beginning of @rules; for precedence?
40
+ @rules.unshift [
41
+ [
42
+ TokenMatcher.send(name),
43
+ t(:open),
44
+ *pattern(*new_rule[:tokens]),
45
+ t(:close),
46
+ ],
47
+ name
48
+ ]
49
+ @funcs[name] = new_rule[:body]
50
+ end
51
+
52
+ def self.func(name)
53
+ @funcs ||= {}
54
+ @funcs[name]
55
+ end
56
+
29
57
  def self.t(name)
30
58
  @matchers ||= [
31
- :numeric, :string, :addsub, :muldiv, :pow,
59
+ :numeric, :string, :addsub, :subtract, :muldiv, :pow, :mod,
32
60
  :comparator, :comp_gt, :comp_lt,
33
61
  :open, :close, :comma,
34
62
  :non_group, :non_group_star,
@@ -47,6 +75,8 @@ module Dentaku
47
75
  math_add: pattern(:numeric, :addsub, :numeric),
48
76
  math_mul: pattern(:numeric, :muldiv, :numeric),
49
77
  math_pow: pattern(:numeric, :pow, :numeric),
78
+ math_mod: pattern(:numeric, :mod, :numeric),
79
+ negation: pattern(:subtract, :numeric),
50
80
  range_asc: pattern(:numeric, :comp_lt, :numeric, :comp_lt, :numeric),
51
81
  range_desc: pattern(:numeric, :comp_gt, :numeric, :comp_gt, :numeric),
52
82
  num_comp: pattern(:numeric, :comparator, :numeric),
@@ -61,8 +61,10 @@ module Dentaku
61
61
  def self.numeric; new(:numeric); end
62
62
  def self.string; new(:string); end
63
63
  def self.addsub; new(:operator, [:add, :subtract]); end
64
+ def self.subtract; new(:operator, :subtract); end
64
65
  def self.muldiv; new(:operator, [:multiply, :divide]); end
65
66
  def self.pow; new(:operator, :pow); end
67
+ def self.mod; new(:operator, :mod); end
66
68
  def self.comparator; new(:comparator); end
67
69
  def self.comp_gt; new(:comparator, [:gt, :ge]); end
68
70
  def self.comp_lt; new(:comparator, [:lt, :le]); end
@@ -78,6 +80,16 @@ module Dentaku
78
80
  def self.not; new(:function, :not); end
79
81
  def self.non_group; new(:grouping).invert; end
80
82
  def self.non_group_star; new(:grouping).invert.star; end
83
+
84
+
85
+ def self.method_missing(name, *args, &block)
86
+ new(:function, name)
87
+ end
88
+
89
+ def self.respond_to_missing?(name, include_priv)
90
+ true
91
+ end
92
+
81
93
  end
82
94
  end
83
95
 
@@ -52,8 +52,8 @@ module Dentaku
52
52
  end
53
53
 
54
54
  def operator
55
- names = { pow: '^', add: '+', subtract: '-', multiply: '*', divide: '/' }.invert
56
- new(:operator, '\^|\+|-|\*|\/', lambda { |raw| names[raw] })
55
+ names = { pow: '^', add: '+', subtract: '-', multiply: '*', divide: '/', mod: '%' }.invert
56
+ new(:operator, '\^|\+|-|\*|\/|%', lambda { |raw| names[raw] })
57
57
  end
58
58
 
59
59
  def grouping
@@ -72,7 +72,7 @@ module Dentaku
72
72
  end
73
73
 
74
74
  def function
75
- new(:function, '(if|round(up|down)?|not)\b', lambda {|raw| raw.strip.downcase.to_sym })
75
+ new(:function, '(\w+\s*(?=\())', lambda {|raw| raw.strip.downcase.to_sym })
76
76
  end
77
77
 
78
78
  def identifier
@@ -1,3 +1,3 @@
1
1
  module Dentaku
2
- VERSION = "0.2.12"
2
+ VERSION = "0.2.13"
3
3
  end
@@ -38,4 +38,8 @@ describe Dentaku::BinaryOperation do
38
38
  logical.and.should eq [:logical, false]
39
39
  logical.or.should eq [:logical, true]
40
40
  end
41
+
42
+ it 'mods two numbers' do
43
+ operation.mod.should eq [:numeric, 2%3]
44
+ end
41
45
  end
@@ -72,10 +72,17 @@ describe Dentaku::Calculator do
72
72
  calculator.evaluate('some_boolean AND 7 > 5', :some_boolean => true).should be_true
73
73
  calculator.evaluate('some_boolean AND 7 < 5', :some_boolean => true).should be_false
74
74
  calculator.evaluate('some_boolean AND 7 > 5', :some_boolean => false).should be_false
75
+
76
+ calculator.evaluate('some_boolean OR 7 > 5', :some_boolean => true).should be_true
77
+ calculator.evaluate('some_boolean OR 7 < 5', :some_boolean => true).should be_true
78
+ calculator.evaluate('some_boolean OR 7 < 5', :some_boolean => false).should be_false
79
+
75
80
  end
76
81
 
77
82
  describe 'functions' do
78
83
  it 'should include IF' do
84
+ calculator.evaluate('if(foo < 8, 10, 20)', :foo => 2).should eq(10)
85
+ calculator.evaluate('if(foo < 8, 10, 20)', :foo => 9).should eq(20)
79
86
  calculator.evaluate('if (foo < 8, 10, 20)', :foo => 2).should eq(10)
80
87
  calculator.evaluate('if (foo < 8, 10, 20)', :foo => 9).should eq(20)
81
88
  end
@@ -87,5 +94,13 @@ describe Dentaku::Calculator do
87
94
 
88
95
  calculator.evaluate('ROUND(apples * 0.93)', { :apples => 10 }).should eq(9)
89
96
  end
97
+
98
+ it 'should include NOT' do
99
+ calculator.evaluate('NOT(some_boolean)', :some_boolean => true).should be_false
100
+ calculator.evaluate('NOT(some_boolean)', :some_boolean => false).should be_true
101
+
102
+ calculator.evaluate('NOT(some_boolean) AND 7 > 5', :some_boolean => true).should be_false
103
+ calculator.evaluate('NOT(some_boolean) OR 7 < 5', :some_boolean => false).should be_true
104
+ end
90
105
  end
91
106
  end
@@ -41,6 +41,11 @@ describe Dentaku::Evaluator do
41
41
  evaluator.evaluate_step(stream, 0, 5, :evaluate_group).should eq(expected)
42
42
  end
43
43
 
44
+ it 'supports unary minus' do
45
+ evaluator.evaluate(token_stream(:subtract, 1)).should eq(-1)
46
+ evaluator.evaluate(token_stream(1, :subtract, :subtract, 1)).should eq(2)
47
+ end
48
+
44
49
  describe 'maths' do
45
50
  it 'should perform addition' do
46
51
  evaluator.evaluate(token_stream(1, :add, 1)).should eq(2)
@@ -48,6 +53,7 @@ describe Dentaku::Evaluator do
48
53
 
49
54
  it 'should respect order of precedence' do
50
55
  evaluator.evaluate(token_stream(1, :add, 1, :multiply, 5)).should eq(6)
56
+ evaluator.evaluate(token_stream(2, :add, 10, :mod, 2)).should eq(2)
51
57
  end
52
58
 
53
59
  it 'should respect explicit grouping' do
@@ -0,0 +1,44 @@
1
+ require 'dentaku/calculator'
2
+
3
+ describe Dentaku::Calculator do
4
+ describe 'functions' do
5
+ describe 'external functions' do
6
+
7
+ let(:with_external_funcs) do
8
+ c = described_class.new
9
+
10
+ rule = { :name => :now, :tokens => [], :body => ->(*args) { Dentaku::Token.new(:string, Time.now.to_s) } }
11
+ c.add_rule rule
12
+
13
+ new_rules = [
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
25
+ },
26
+ ]
27
+
28
+ c.add_rules new_rules
29
+ end
30
+
31
+ it 'should include NOW' do
32
+ now = with_external_funcs.evaluate('NOW()')
33
+ now.should_not be_nil
34
+ now.should_not be_empty
35
+ end
36
+
37
+ it 'should include EXP' do
38
+ with_external_funcs.evaluate('EXP(2,3)').should eq(8)
39
+ 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)
41
+ end
42
+ end
43
+ end
44
+ end
data/spec/spec_helper.rb CHANGED
@@ -12,7 +12,7 @@ def type_for(value)
12
12
  case value
13
13
  when Numeric
14
14
  :numeric
15
- when :add, :subtract, :multiply, :divide
15
+ when :add, :subtract, :multiply, :divide, :mod
16
16
  :operator
17
17
  when :open, :close, :comma
18
18
  :grouping
metadata CHANGED
@@ -1,51 +1,45 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: dentaku
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.12
5
- prerelease:
4
+ version: 0.2.13
6
5
  platform: ruby
7
6
  authors:
8
7
  - Solomon White
9
8
  autorequire:
10
9
  bindir: bin
11
10
  cert_chain: []
12
- date: 2013-08-25 00:00:00.000000000 Z
11
+ date: 2014-01-17 00:00:00.000000000 Z
13
12
  dependencies:
14
13
  - !ruby/object:Gem::Dependency
15
14
  name: rake
16
15
  requirement: !ruby/object:Gem::Requirement
17
- none: false
18
16
  requirements:
19
- - - ! '>='
17
+ - - '>='
20
18
  - !ruby/object:Gem::Version
21
19
  version: '0'
22
20
  type: :development
23
21
  prerelease: false
24
22
  version_requirements: !ruby/object:Gem::Requirement
25
- none: false
26
23
  requirements:
27
- - - ! '>='
24
+ - - '>='
28
25
  - !ruby/object:Gem::Version
29
26
  version: '0'
30
27
  - !ruby/object:Gem::Dependency
31
28
  name: rspec
32
29
  requirement: !ruby/object:Gem::Requirement
33
- none: false
34
30
  requirements:
35
- - - ! '>='
31
+ - - '>='
36
32
  - !ruby/object:Gem::Version
37
33
  version: '0'
38
34
  type: :development
39
35
  prerelease: false
40
36
  version_requirements: !ruby/object:Gem::Requirement
41
- none: false
42
37
  requirements:
43
- - - ! '>='
38
+ - - '>='
44
39
  - !ruby/object:Gem::Version
45
40
  version: '0'
46
- description: ! ' Dentaku is a parser and evaluator for mathematical formulas
47
-
48
- '
41
+ description: |2
42
+ Dentaku is a parser and evaluator for mathematical formulas
49
43
  email:
50
44
  - rubysolo@gmail.com
51
45
  executables: []
@@ -55,7 +49,6 @@ files:
55
49
  - .gitignore
56
50
  - .travis.yml
57
51
  - Gemfile
58
- - MIT-LICENSE.txt
59
52
  - README.md
60
53
  - Rakefile
61
54
  - dentaku.gemspec
@@ -73,41 +66,41 @@ files:
73
66
  - spec/calculator_spec.rb
74
67
  - spec/dentaku_spec.rb
75
68
  - spec/evaluator_spec.rb
69
+ - spec/external_function_spec.rb
76
70
  - spec/spec_helper.rb
77
71
  - spec/token_matcher_spec.rb
78
72
  - spec/token_scanner_spec.rb
79
73
  - spec/token_spec.rb
80
74
  - spec/tokenizer_spec.rb
81
75
  homepage: http://github.com/rubysolo/dentaku
82
- licenses:
83
- - MIT
76
+ licenses: []
77
+ metadata: {}
84
78
  post_install_message:
85
79
  rdoc_options: []
86
80
  require_paths:
87
81
  - lib
88
82
  required_ruby_version: !ruby/object:Gem::Requirement
89
- none: false
90
83
  requirements:
91
- - - ! '>='
84
+ - - '>='
92
85
  - !ruby/object:Gem::Version
93
86
  version: '0'
94
87
  required_rubygems_version: !ruby/object:Gem::Requirement
95
- none: false
96
88
  requirements:
97
- - - ! '>='
89
+ - - '>='
98
90
  - !ruby/object:Gem::Version
99
91
  version: '0'
100
92
  requirements: []
101
93
  rubyforge_project: dentaku
102
- rubygems_version: 1.8.23
94
+ rubygems_version: 2.0.3
103
95
  signing_key:
104
- specification_version: 3
96
+ specification_version: 4
105
97
  summary: A formula language parser and evaluator
106
98
  test_files:
107
99
  - spec/binary_operation_spec.rb
108
100
  - spec/calculator_spec.rb
109
101
  - spec/dentaku_spec.rb
110
102
  - spec/evaluator_spec.rb
103
+ - spec/external_function_spec.rb
111
104
  - spec/spec_helper.rb
112
105
  - spec/token_matcher_spec.rb
113
106
  - spec/token_scanner_spec.rb
data/MIT-LICENSE.txt DELETED
@@ -1,21 +0,0 @@
1
- The MIT License (MIT)
2
-
3
- Copyright (c) 2012 Solomon White
4
-
5
- Permission is hereby granted, free of charge, to any person obtaining a copy
6
- of this software and associated documentation files (the "Software"), to deal
7
- in the Software without restriction, including without limitation the rights
8
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
- copies of the Software, and to permit persons to whom the Software is
10
- furnished to do so, subject to the following conditions:
11
-
12
- The above copyright notice and this permission notice shall be included in
13
- all copies or substantial portions of the Software.
14
-
15
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
- THE SOFTWARE.