dentaku 0.2.12 → 0.2.13

Sign up to get free protection for your applications and to get access to all the features.
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.