r_calc 0.0.1

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.
data/.autotest ADDED
@@ -0,0 +1,10 @@
1
+ # ./.autotest
2
+ Autotest.add_hook(:initialize) {|at|
3
+ at.add_exception %r{^\.git} # ignore Version Control System
4
+ at.add_exception %r{^./tmp} # ignore temp files, lest autotest will run again, and again...
5
+ # at.clear_mappings # take out the default (test/test*rb)
6
+ at.add_mapping(%r{^lib/.*\.rb$}) {|f, _|
7
+ Dir['spec/**/*_spec.rb']
8
+ }
9
+ nil
10
+ }
data/.gitignore ADDED
@@ -0,0 +1,21 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
18
+
19
+ .rvmrc
20
+
21
+ .idea
data/.rspec ADDED
File without changes
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in r_calc.gemspec
4
+ gemspec
data/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2013 Milan Jaric
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,74 @@
1
+ # RCalc
2
+
3
+ RCalc is gem which can help you adding calculator like behaviour to your code. It is sandboxed and there is no posibility that end user can execute some code against your system.
4
+
5
+ ## Installation
6
+
7
+ Add this line to your application's Gemfile:
8
+
9
+ gem 'r_calc'
10
+
11
+ And then execute:
12
+
13
+ $ bundle
14
+
15
+ Or install it yourself as:
16
+
17
+ $ gem install r_calc
18
+
19
+ ## Usage
20
+
21
+ To use this gem, after including this could be minimum of required code to run calculation:
22
+
23
+ `
24
+ require 'r_calc'
25
+ #require 'supermodel'
26
+ #require 'minitest/spec'
27
+ #require 'minitest/autorun'
28
+
29
+ module RCalcHelpers
30
+ class Calculator < RCalc::FormulaProcessor
31
+ # Let's just add all of the usual operators
32
+
33
+ include ::RCalc::ParentheticalGrouping # ()
34
+ include ::RCalc::AssignmentOperator # =
35
+ include ::RCalc::LastResultOperator # $
36
+ include ::RCalc::ArithmeticOperators # +-*/%^
37
+ include ::RCalc::BooleanOperators # true false and or eq ne gt lt ge le not
38
+ include ::RCalc::FunctionOperators # sum(), max(), min()
39
+ # example of adding custom operators
40
+
41
+ def AddOperators_custom
42
+
43
+ calc = self
44
+
45
+
46
+ # Now let's show how you can use a function to access a constant, like PI
47
+
48
+ @operators << ::RCalc::BinaryOperator.new("[", 3) {|x, y| x.value + y.value}
49
+ @operators << ::RCalc::UnaryOperator.new("]", 4) { |x| x.value }
50
+
51
+ end
52
+ end
53
+
54
+ def calculator
55
+
56
+
57
+ values = Hash.new
58
+ calc = Calculator.new do |key, val|
59
+ values[key] = val if val
60
+ values[key]
61
+ end
62
+ [calc, values]
63
+ end
64
+
65
+ end
66
+ `
67
+
68
+ ## Contributing
69
+
70
+ 1. Fork it
71
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
72
+ 3. Commit your changes (`git commit -am 'Added some feature'`)
73
+ 4. Push to the branch (`git push origin my-new-feature`)
74
+ 5. Create new Pull Request
data/Rakefile ADDED
@@ -0,0 +1,17 @@
1
+ #!/usr/bin/env rake
2
+ require "bundler/gem_tasks"
3
+ require 'bundler'
4
+ require 'rspec/core/rake_task'
5
+
6
+ Bundler::GemHelper.install_tasks
7
+ RSpec::Core::RakeTask.new(:spec)
8
+ task default: :spec
9
+
10
+ #require 'rake/testtask'
11
+ #
12
+ #Rake::TestTask.new do |t|
13
+ # t.libs.push "lib"
14
+ # t.test_files = FileList['specs/**/*_spec.rb']
15
+ # t.verbose = true
16
+ #end
17
+
@@ -0,0 +1,34 @@
1
+ require 'r_calc/binary_operator'
2
+ require 'r_calc/binary_or_unary_operator'
3
+
4
+ # Arithmetic operators + - * / % ^
5
+ module RCalc
6
+ module ArithmeticOperators
7
+ def AddOperators_Arithmetic
8
+
9
+ # Plus and minus are binary operators if they are preceeded by an operand.
10
+ # Otherwise they are unary, and have higher priority.
11
+
12
+ @operators << (plus = (BinOrUnOperator.new("+", nil) { |x, y| y ? x.value + y.value : x.value }))
13
+ @operators << (minus = (BinOrUnOperator.new("-", nil) { |x, y| y ? x.value - y.value : -x.value }))
14
+
15
+ # If a + or - has an operand preceding, the priority is lower than * and /,
16
+ # but higher than =. If no operand precedes, it is higher than * or /.
17
+ # So, we create a code block to make this determination.
18
+
19
+ pmPriority = Proc.new { |operandPreceding| operandPreceding ? 2 : 4 }
20
+
21
+ # and assign it as the priorityProc for the + and - operators
22
+
23
+ plus.priorityProc = pmPriority
24
+ minus.priorityProc = pmPriority
25
+
26
+ # Now we add some easy binary operators...
27
+
28
+ @operators << (BinaryOperator.new("*", 3) { |x, y| x.value * y.value })
29
+ @operators << (BinaryOperator.new("/", 3) { |x, y| x.value / y.value })
30
+ @operators << (BinaryOperator.new("%", 3) { |x, y| x.value % y.value })
31
+ @operators << (BinaryOperator.new("^", 3) { |x, y| x.value ** y.value })
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,11 @@
1
+ require 'r_calc/binary_operator'
2
+
3
+
4
+ # = for assignment
5
+ module RCalc
6
+ module AssignmentOperator
7
+ def AddOperators_Assignment
8
+ @operators << (::RCalc::BinaryOperator.new("=", 1) { |x, y| x.value = y.value })
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,10 @@
1
+ require 'r_calc/operator'
2
+
3
+ # An operator that takes two operands
4
+ class RCalc::BinaryOperator < ::RCalc::Operator
5
+
6
+ private
7
+ def initialize(symbol, priority)
8
+ super(symbol, 2, 2, priority)
9
+ end
10
+ end
@@ -0,0 +1,8 @@
1
+ require 'r_calc/operator'
2
+
3
+ class RCalc::BinOrUnOperator < ::RCalc::Operator
4
+ private
5
+ def initialize(symbol, priority)
6
+ super(symbol, 1, 2, priority)
7
+ end
8
+ end
@@ -0,0 +1,36 @@
1
+ require 'r_calc/function'
2
+ require 'r_calc/binary_operator'
3
+ require 'r_calc/unary_operator'
4
+
5
+ module RCalc
6
+ module BooleanOperators
7
+ def AddOperators_Boolean
8
+
9
+ # Functions for true and false
10
+
11
+ @operators << ::RCalc::Function.new("true", 0, 0) { true }
12
+ @operators << ::RCalc::Function.new("false", 0, 0) { false }
13
+
14
+ # Equality and its clan (note we cannot use '==' or other two-character
15
+ # non-word operators, because of the way we parse the string. Non-word
16
+ # characters come in one at a time.
17
+
18
+ @operators << ::RCalc::BinaryOperator.new("eq", 3) { |x, y| x.value == y.value }
19
+ @operators << ::RCalc::BinaryOperator.new("ne", 3) { |x, y| x.value != y.value }
20
+ @operators << ::RCalc::BinaryOperator.new("gt", 3) { |x, y| x.value > y.value }
21
+ @operators << ::RCalc::BinaryOperator.new("lt", 3) { |x, y| x.value < y.value }
22
+ @operators << ::RCalc::BinaryOperator.new("ge", 3) { |x, y| x.value >= y.value }
23
+ @operators << ::RCalc::BinaryOperator.new("le", 3) { |x, y| x.value <= y.value }
24
+
25
+ # Boolean and/or
26
+
27
+ @operators << ::RCalc::BinaryOperator.new("and", 2) { |x, y| x.value && y.value }
28
+ @operators << ::RCalc::BinaryOperator.new("or", 2) { |x, y| x.value || y.value }
29
+
30
+ # Logical not
31
+
32
+ @operators << ::RCalc::UnaryOperator.new("not", 4) { |x| !x.value }
33
+
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,5 @@
1
+ =begin
2
+ A unique class for exceptions so we can rescue them separately if we want to
3
+ =end
4
+ class RCalc::FormulaExpressionException < RuntimeError
5
+ end
@@ -0,0 +1,195 @@
1
+ require 'r_calc/parse_token'
2
+ require 'r_calc/operator'
3
+ require 'r_calc/unary_operator'
4
+ require 'r_calc/binary_operator'
5
+ require 'r_calc/binary_or_unary_operator'
6
+ require 'r_calc/function'
7
+ require 'r_calc/pusher'
8
+ require 'r_calc/popper'
9
+ require 'r_calc/separator'
10
+ require 'r_calc/identifier'
11
+ require 'r_calc/literal'
12
+ require 'r_calc/operator_list'
13
+ require 'r_calc/parse_token'
14
+
15
+
16
+ module RCalc
17
+ # An expression processor. This class can be derived to process many types of
18
+ # expressions. The modules defined below can be included in a derived class to
19
+ # support popular sets of operations, or you can define your own.
20
+ # The code block passed to the new method for this class is used for evaluating
21
+ # identifiers in the expression. This allows the client code to supply/store
22
+ # variables in any way it wishes.
23
+ class FormulaProcessor
24
+ attr_reader :operators, :last_result, :receipt_components
25
+ private
26
+ def initialize(&proc)
27
+ @proc = proc # code block for accessing values for identifiers
28
+ @operators = OperatorList.new # hash for operators
29
+ @operators << Separator.new(',') # comma separates by default
30
+ @curr = nil # current operand stack
31
+ @currop = nil # current operator
32
+ @opstack = nil # stack of operators
33
+ @last_result = nil # result of last expression
34
+ @receipt_components =[] # the list of local identifiers which was used to store
35
+ # values carried to next calculation
36
+
37
+ # Here's a nifty little trick. We'll inspect our methods and call any method
38
+ # whose name starts with "AddOperator_". That allows a derived class to include
39
+ # any of the operator modules defined below, and automatically get support
40
+ # for the operators included in each module, without having to invoke the
41
+ # methods explicitly.
42
+
43
+ methods.each do |name|
44
+ self.send(name) if (name =~ /^AddOperators_/)
45
+ end
46
+ end
47
+
48
+ # Resolve the expression composed of the current operator (@currop) and the
49
+ # current operand stack (@curr)
50
+
51
+ def resolve
52
+ if (@currop) then # Do we have an operator?
53
+ args = @curr[1..-1] # omit the "pusher" (curr[0] is always a Pusher)
54
+
55
+ # check argument count requirements
56
+
57
+ if (@currop.minarg && (args.length < @currop.minarg))
58
+ raise(FormulaExpressionException, "Not enough arguments for operation '#{@currop.symbol}'\nMinimum: #{@currop.minarg}, encountered #{args.length}")
59
+ end
60
+ if (@currop.maxarg && (args.length > @currop.maxarg))
61
+ raise(FormulaExpressionException, "Too many arguments for operation '#{@currop.symbol}'\nMaximum: #{@currop.maxarg}, encountered #{args.length}")
62
+ end
63
+
64
+ # Call the operator's code block and store the result as element 1, trimming the other operands
65
+
66
+ @curr[1..-1] = Literal.new(@currop.proc.call(*args))
67
+ @currop = nil # No more current operator
68
+ end
69
+ if (@curr[0].implied) # pop off any implied Pusher
70
+ val = @curr[1] # grab the value
71
+ @curr = @curr[0].parent # pop back up to the parent operand stack
72
+ @curr[-1] = val # store the value over the reference to the stack we just popped
73
+ @currop = @opstack.pop # pop off the operator from the previous level
74
+ end
75
+ return @curr[-1] # In either case, return the last value on the current operand stack
76
+ end
77
+
78
+
79
+ public
80
+ # Evaluate an expression
81
+ def eval(input=$_) # default to current string if not passed
82
+ stack = [Pusher.new(nil)] # operand array (a stack of operands and/or operand stacks)
83
+ @opstack = [] # operator stack
84
+ @curr = stack # current operand stack
85
+ @currop = nil # operator at current stack level
86
+
87
+ # Tokenize as numeric or identifier or non-space/comma (presumably an operator or parentheses)
88
+ input.scan(/[0-9.]+|\w+|,|[^\s\t\r\n\f]/) do |token|
89
+ t = nil
90
+ if (token =~ /^[0-9]+$/) then # integer literal
91
+ t = Literal.new(token.to_i)
92
+ elsif (token =~ /^[0-9.]+$/) then # floating point literal
93
+ t = Literal.new(token.to_f)
94
+ elsif (t = @operators[token]) then # a defined operator
95
+ elsif (token =~ /^\w+$/) # an identifier
96
+ t = Identifier.new(token, @proc)
97
+ receipt_components << token unless receipt_components.include? token
98
+ else
99
+ raise(FormulaExpressionException, "Unrecognized character in expression: #{token}")
100
+ end
101
+
102
+ if (t) # Did we create something?
103
+ case t.parseAction # What are we to do with it?
104
+ when ParseToken::PARSE_VALUE # Literals and identifiers
105
+ @curr.push(t) # get pushed as operands
106
+
107
+ when ParseToken::PARSE_OP # Operators and functions
108
+ t = t.clone # Must copy to establish priority independently
109
+
110
+ # Get the priority (note our only context-based syntax element, a leading operand)
111
+ prior = t.priority(opPreceding = @currop ? (@curr.length > 2) : (@curr.length > 1))
112
+
113
+ # If we already have an operator with at least equal priority, resolve it
114
+ while (@currop && (prior <= @currop.priority))
115
+ resolve
116
+ end
117
+ if (@currop) then # We found an operator of lower priority
118
+ pusher = Pusher.new(nil) # Group our new operation (implied parenthesis)
119
+ pusher.implied = true
120
+ pusher.parent = @curr # back-link the stack
121
+ if (opPreceding) then # operand preceding?
122
+ new = [pusher, @curr[-1]] # move last operand to new operation
123
+ @curr[-1] = new # link new stack to end of old one
124
+ else
125
+ new = [pusher]
126
+ @curr.push(new) # just push and go
127
+ end
128
+ @curr = new # save reference to new operand stack
129
+ @opstack.push(@currop) # push the previous operator
130
+ end
131
+ @currop = t # now we have a new operator
132
+
133
+ # Resolve the operation if we already reached the maximum argument count
134
+ resolve if (@currop.maxarg && (@curr.length > @currop.maxarg))
135
+
136
+ when ParseToken::PARSE_PUSH # Grouping (parenthesis equivalent)
137
+ t = t.clone # Clone so we have a unique parent property
138
+ t.parent = @curr # Back-link to previous operand stack
139
+ @curr.push(new = [t]) # Forward-link to new operand stack
140
+ @curr = new # Save reference to new stack
141
+ @opstack.push(@currop) # Push the current operator, if any
142
+ @currop = nil # No current operator yet within this grouping
143
+
144
+ when ParseToken::PARSE_POP # End grouping (close parenthesis)
145
+ resolve while (@curr[0].implied) # Resolve all implied pushes first
146
+ if (@currop) then # Do we have an operator?
147
+ val = resolve # Resolve the contained operation
148
+
149
+ # If we're already unwound, this is an extra one
150
+ raise(FormulaExpressionException, "Unmatched #{t.symbol}") if (@curr == stack)
151
+ @curr = @curr[0].parent # Pop back to parent stack
152
+ @curr[-1] = val # Store the value over the forward reference (losing the reference)
153
+ else # No current operator, just move the operands back
154
+ val = @curr[1..-1] # Gather the operands
155
+ @curr = @curr[0].parent # Pop back to parent stack
156
+ @curr[-1, val.length] = val # Extend the operand stack with the new operands
157
+ end
158
+ @currop = @opstack.pop # Pop back to previous operator
159
+
160
+ when ParseToken::PARSE_SEPARATE # Separator, like comma
161
+ resolve if (@currop || @curr[0].implied) # Resolve any pending operation
162
+ pusher = Pusher.new(nil) # Implied push to prevent folding operators across separator
163
+ pusher.implied = true
164
+ pusher.parent = @curr
165
+ new = [pusher]
166
+ @curr.push(new)
167
+ @curr = new
168
+ @opstack.push(@currop)
169
+ @currop = nil
170
+ end # case t.parseAction
171
+ end # if (t)
172
+ end # input.scan
173
+
174
+ # Pop off any implied pushes, resolving the values
175
+ resolve while (@curr[0].implied)
176
+
177
+ # Make sure we unwound completely
178
+ raise(FormulaExpressionException, "Unmatched #{@curr[0].symbol}") if (@curr != stack)
179
+
180
+ # Resolve any final operator, and save the value
181
+ @last_result = resolve.value
182
+
183
+ # Make sure we ended up with just one value
184
+ raise(FormulaExpressionException, "Missing operator") if (@curr.length > 2)
185
+
186
+ # Return that value
187
+ @last_result
188
+ end
189
+
190
+ end
191
+ end
192
+
193
+
194
+
195
+
@@ -0,0 +1,11 @@
1
+ require 'r_calc/operator'
2
+
3
+ # A function has the lowest priority, because usually the arguments are
4
+ # enclosed in a push/pop sequence (e.g., parentheses)
5
+
6
+ class RCalc::Function < ::RCalc::Operator
7
+ private
8
+ def initialize(name, minarg, maxarg)
9
+ super(name, minarg, maxarg, 5)
10
+ end
11
+ end
@@ -0,0 +1,33 @@
1
+ require 'r_calc/function'
2
+ require 'r_calc/binary_operator'
3
+ require 'r_calc/unary_operator'
4
+
5
+ module RCalc
6
+ module FunctionOperators
7
+ def AddOperators_ArithmeticFunctions
8
+ @operators << ::RCalc::Function.new("sum", 1, nil) do |*args|
9
+ args.inject(0) { |sum, arg| sum + arg.value }
10
+ end
11
+
12
+ @operators << ::RCalc::Function.new("max", 1, nil) do |*args|
13
+ max = nil
14
+ args.each { |arg| max = arg.value if !max || arg.value > max }
15
+ max
16
+ end
17
+
18
+ @operators << ::RCalc::Function.new("min", 1, nil) do |*args|
19
+ min = nil
20
+ args.each { |arg| min = arg.value if !min || arg.value < min }
21
+ min
22
+ end
23
+
24
+ @operators << ::RCalc::Function.new("PI", 0, 0) { Math::PI }
25
+ end
26
+
27
+ def AddOperators_BooleanFunctions
28
+ @operators << ::RCalc::Function.new("if", 3, 3) do |cond, x, y|
29
+ cond.value ? x.value : (y.value || 0)
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,21 @@
1
+ require 'r_calc/parse_token'
2
+
3
+ # A token that presumably references a variable of some sort
4
+ # This class demonstrates calling a code block from a saved reference to it.
5
+
6
+ class RCalc::Identifier < RCalc::ParseToken
7
+ private
8
+ def initialize(name, proc)
9
+ super(name, ::RCalc::ParseToken::PARSE_VALUE) # Note we use the name as the symbol
10
+ @proc = proc # proc is a code block for evaluating the symbol
11
+ end
12
+
13
+ public
14
+ def value=(newvalue) # Assign to value
15
+ @proc.call(self.symbol, newvalue) # We pass newvalue to denote assignment
16
+ end
17
+
18
+ def value # Get value
19
+ @proc.call(self.symbol) # No newvalue passed here
20
+ end
21
+ end
@@ -0,0 +1,9 @@
1
+ require 'r_calc/function'
2
+ # $ for last result
3
+ module RCalc
4
+ module LastResultOperator
5
+ def AddOperators_LastResult
6
+ @operators << ::RCalc::Function.new("$", 0, 0) { self.last_result }
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,25 @@
1
+ require 'r_calc/formula_expression_exception'
2
+ require 'r_calc/parse_token'
3
+ # A literal value
4
+
5
+ class RCalc::Literal < RCalc::ParseToken
6
+ attr_reader :value # Just like an Identifier, we can get the
7
+ # value using the 'value' property, even
8
+ # though the implementation has nothing in common.
9
+ # The nice thing about Ruby is that these
10
+ # two classes do not need to be derived
11
+ # from an abstract parent class (less typing,
12
+ # in every sense of the word).
13
+ private
14
+ def initialize(value)
15
+ super(value, ::RCalc::ParseToken::PARSE_VALUE) # Note we use the value itself as the symbol
16
+ @value = value
17
+ end
18
+
19
+ public
20
+ # We could have left out the following method, but our version gives a better error
21
+ # messsage than the default "method missing value=".
22
+ def value=(newvalue) # Assign a value
23
+ raise(::RCalc::FormulaExpressionException, "Writing into a literal") # Not!
24
+ end
25
+ end
@@ -0,0 +1,32 @@
1
+ require 'r_calc/parse_token'
2
+
3
+
4
+ =begin
5
+ An operator (or function identifier)
6
+ =end
7
+ class RCalc::Operator < RCalc::ParseToken
8
+ attr_reader :proc, :minarg, :maxarg, :priorityProc
9
+ attr_writer :priorityProc
10
+
11
+ private
12
+ def initialize(symbol, minarg, maxarg, priority, &proc)
13
+ super(symbol, RCalc::ParseToken::PARSE_OP)
14
+ @minarg = minarg # minimum number of operands
15
+ @maxarg = maxarg # maximum number of operands
16
+ @priority = priority # operator precedence (0 = low .. 5 = high)
17
+ @proc = proc # code block for performing the operation
18
+ @priorityProc = nil # procedure for determining priority
19
+ # (we need this for operators that determine
20
+ # priority based on context, like + and -
21
+ end
22
+
23
+ public
24
+ # Get priority of the operator
25
+ def priority(operandPreceding = nil)
26
+ # only determine priority once, possibly based on context
27
+ if (!(@priority) && @priorityProc) # do we have a proc for processing priority?
28
+ @priority = @priorityProc.call(operandPreceding) # call it
29
+ end
30
+ @priority # return it
31
+ end
32
+ end
@@ -0,0 +1,8 @@
1
+ # A special Hash for the list of supported operators
2
+ # The user can append new operators, without worrying about how we need to handle them.
3
+
4
+ class RCalc::OperatorList < Hash
5
+ def <<(oper) # No need for the user to know about the keys
6
+ self[oper.symbol] = oper # We use the operator's symbol for the key, BTW
7
+ end
8
+ end
@@ -0,0 +1,20 @@
1
+ require 'r_calc/pusher'
2
+ require 'r_calc/popper'
3
+
4
+ module RCalc
5
+
6
+ # Mix-ins for the usual operations. See ExpressionProcessor#initialize for how
7
+ # these "AddOperator_" methods get invoked.
8
+
9
+ # First, parenthetical grouping
10
+ module ParentheticalGrouping
11
+ def AddOperators_ParentheticalGrouping
12
+
13
+ # Define "(" and ")" as our grouping operators
14
+
15
+ @operators << ::RCalc::Pusher.new("(")
16
+ @operators << ::RCalc::Popper.new(")")
17
+ end
18
+ end
19
+
20
+ end
@@ -0,0 +1,18 @@
1
+ =begin
2
+ Any token we might encounter in the expression
3
+ =end
4
+ class RCalc::ParseToken
5
+ public
6
+ # values for parseAction (what to do when we encounter this token):
7
+ PARSE_VALUE = 0 # A literal or an identifier (stack it)
8
+ PARSE_OP = 1 # An operator
9
+ PARSE_PUSH = 2 # Open parenthesis, for example
10
+ PARSE_POP = 3 # Close parenthesis, for example
11
+ PARSE_SEPARATE = 4 # Comma, for example
12
+ attr_reader :symbol, :parseAction
13
+ private
14
+ def initialize(symbol, parseAction)
15
+ @symbol = symbol # symbol used for parsing
16
+ @parseAction = parseAction # action when parsing
17
+ end
18
+ end
@@ -0,0 +1,10 @@
1
+ require 'r_calc/parse_token'
2
+
3
+ # The complementary close parenthesis (or equivalent)
4
+
5
+ class RCalc::Popper < RCalc::ParseToken
6
+ private
7
+ def initialize(symbol)
8
+ super(symbol, RCalc::ParseToken::PARSE_POP)
9
+ end
10
+ end
@@ -0,0 +1,17 @@
1
+ require 'r_calc/formula_expression_exception'
2
+ require 'r_calc/parse_token'
3
+
4
+ class RCalc::Pusher < RCalc::ParseToken
5
+ attr_reader :parent, :implied # parent operand array
6
+ attr_writer :parent, :implied # implied means we created it via operator precedence
7
+ private
8
+ def initialize(symbol)
9
+ super(symbol, RCalc::ParseToken::PARSE_PUSH)
10
+ @implied = false # by default it is a literal token
11
+ end
12
+
13
+ public
14
+ def value # Tried to evaluate one of these
15
+ raise(RCalc::FormulaExpressionException, "Unrecognized expression")
16
+ end
17
+ end
@@ -0,0 +1,10 @@
1
+ require 'r_calc/parse_token'
2
+
3
+ # A separator, like comma
4
+
5
+ class RCalc::Separator < RCalc::ParseToken
6
+ private
7
+ def initialize(symbol)
8
+ super(symbol, RCalc::ParseToken::PARSE_SEPARATE)
9
+ end
10
+ end
@@ -0,0 +1,11 @@
1
+ require 'r_calc/operator'
2
+
3
+
4
+ # An operator that takes only one operand
5
+
6
+ class RCalc::UnaryOperator < RCalc::Operator
7
+ private
8
+ def initialize(symbol, priority)
9
+ super(symbol, 1, 1, priority)
10
+ end
11
+ end
@@ -0,0 +1,3 @@
1
+ module RCalc
2
+ VERSION = "0.0.1"
3
+ end
data/lib/r_calc.rb ADDED
@@ -0,0 +1,12 @@
1
+ require "r_calc/version"
2
+ require 'r_calc/formula_expression_exception'
3
+ require 'r_calc/formula_processor'
4
+ require 'r_calc/parenthetical_grouping'
5
+ require 'r_calc/assignment_operator'
6
+ require 'r_calc/last_result_operator'
7
+ require 'r_calc/arithmetic_operations'
8
+ require 'r_calc/boolean_operators'
9
+ require 'r_calc/function_operators'
10
+ module RCalc
11
+ # Your code goes here...
12
+ end
data/r_calc.gemspec ADDED
@@ -0,0 +1,26 @@
1
+ # -*- encoding: utf-8 -*-
2
+ require File.expand_path('../lib/r_calc/version', __FILE__)
3
+
4
+ Gem::Specification.new do |gem|
5
+ gem.authors = ["Milan Jaric"]
6
+ gem.email = ["milan.jaric@gmail.com"]
7
+ gem.description = %q{Gem defines basic grammar for formula builder and ability to execute formulas to get it results or sum them.}
8
+ gem.summary = %q{Sandboxed formula builder and evaluator}
9
+ gem.homepage = "https://github.com/mjaric/formulator"
10
+
11
+ gem.files = `git ls-files`.split($\)
12
+ gem.executables = gem.files.grep(%r{^bin/}).map { |f| File.basename(f) }
13
+ gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
14
+ gem.name = "r_calc"
15
+ gem.require_paths = ["lib"]
16
+ gem.version = RCalc::VERSION
17
+
18
+ gem.add_development_dependency "rake"
19
+ gem.add_development_dependency "rspec"
20
+ gem.add_development_dependency "ZenTest"
21
+ gem.add_development_dependency "autotest-fsevent"
22
+ gem.add_development_dependency "autotest-growl"
23
+ gem.add_development_dependency "minitest"
24
+ gem.add_development_dependency "minitest-spec"
25
+ #gem.add_development_dependency "supermodel"
26
+ end
@@ -0,0 +1,91 @@
1
+ # encoding: utf-8
2
+ require 'spec/spec_helper'
3
+
4
+ describe RCalc::FormulaProcessor do
5
+
6
+ RSpec.configure do |c|
7
+ c.include RCalcHelpers
8
+ end
9
+
10
+ let :c do
11
+ a, b= calculator
12
+ a
13
+ end
14
+
15
+ describe "#eval" do
16
+ it "can add two numbers" do
17
+ c.eval("2+2").should eq(4)
18
+ end
19
+ it "can multiple two numbers" do
20
+ c.eval("2*2").should eq(4)
21
+ end
22
+ it "can divide two numbers" do
23
+ c.eval("2/2").should eq(1)
24
+ end
25
+ it "can mod (%) two numbers" do
26
+ c.eval("3%2").should eq(1)
27
+ end
28
+ it "can pow (^) two numbers" do
29
+ c.eval("3^2").should eq(9)
30
+ end
31
+ it "can sum more than 2 numbers" do
32
+ c.eval("sum(1,2,3,4,5)").should eq(15)
33
+ end
34
+ it "can get minimum of number sequence" do
35
+ c.eval("min(1,2,3,4,5)").should eq(1)
36
+ end
37
+ it "can get maximum of number sequence" do
38
+ c.eval("max(1,2,3,4,5)").should eq(5)
39
+ end
40
+ it "can compare numbers" do
41
+ c.eval("4 lt 2").should eq(false)
42
+ c.eval("4 lt 4").should eq(false)
43
+ c.eval("4 lt 5").should eq(true)
44
+
45
+ c.eval("4 le 2").should eq(false)
46
+ c.eval("4 le 4").should eq(true)
47
+ c.eval("4 le 5").should eq(true)
48
+
49
+ c.eval("4 gt 2").should eq(true)
50
+ c.eval("4 gt 4").should eq(false)
51
+ c.eval("4 gt 5").should eq(false)
52
+
53
+ c.eval("4 ge 2").should eq(true)
54
+ c.eval("4 ge 4").should eq(true)
55
+ c.eval("4 ge 5").should eq(false)
56
+
57
+ c.eval("4 eq 2").should eq(false)
58
+ c.eval("4 eq 4").should eq(true)
59
+ c.eval("4 eq 5").should eq(false)
60
+
61
+ c.eval("4 ne 2").should eq(true)
62
+ c.eval("4 ne 4").should eq(false)
63
+ c.eval("4 ne 5").should eq(true)
64
+
65
+ end
66
+
67
+ it "can compare numbers using if statement with two branches" do
68
+ c.eval("if(4 lt 2, 1, -1 )").should eq(-1)
69
+ end
70
+
71
+ end
72
+
73
+ describe "memoizable #eval" do
74
+ it "can memoize variables into the hash" do
75
+ calc, memo = calculator
76
+ calc.eval("number=12*3").should eq(36)
77
+ memo["number"].should eq(36)
78
+ end
79
+ it "can memoize variables and it can be reused later in next step formula" do
80
+ calc, memo = calculator
81
+ calc.eval("number=12*3")
82
+ calc.eval("number * 10").should eq(360)
83
+ end
84
+
85
+ it "can memoize last value sing $" do
86
+ calc, memo = calculator
87
+ val = calc.eval("12*3")
88
+ calc.eval("$").should eq(36)
89
+ end
90
+ end
91
+ end
@@ -0,0 +1,42 @@
1
+ require 'r_calc'
2
+ #require 'supermodel'
3
+ #require 'minitest/spec'
4
+ #require 'minitest/autorun'
5
+
6
+ module RCalcHelpers
7
+ class Calculator < RCalc::FormulaProcessor
8
+ # Let's just add all of the usual operators
9
+
10
+ include ::RCalc::ParentheticalGrouping # ()
11
+ include ::RCalc::AssignmentOperator # =
12
+ include ::RCalc::LastResultOperator # $
13
+ include ::RCalc::ArithmeticOperators # +-*/%^
14
+ include ::RCalc::BooleanOperators # true false and or eq ne gt lt ge le not
15
+ include ::RCalc::FunctionOperators # sum(), max(), min()
16
+ # example of adding custom operators
17
+
18
+ def AddOperators_custom
19
+
20
+ calc = self
21
+
22
+
23
+ # Now let's show how you can use a function to access a constant, like PI
24
+
25
+ @operators << ::RCalc::BinaryOperator.new("[", 3) {|x, y| x.value + y.value}
26
+ @operators << ::RCalc::UnaryOperator.new("]", 4) { |x| x.value }
27
+
28
+ end
29
+ end
30
+
31
+ def calculator
32
+
33
+
34
+ values = Hash.new
35
+ calc = Calculator.new do |key, val|
36
+ values[key] = val if val
37
+ values[key]
38
+ end
39
+ [calc, values]
40
+ end
41
+
42
+ end
metadata ADDED
@@ -0,0 +1,213 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: r_calc
3
+ version: !ruby/object:Gem::Version
4
+ prerelease:
5
+ version: 0.0.1
6
+ platform: ruby
7
+ authors:
8
+ - Milan Jaric
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2013-03-15 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: rake
16
+ version_requirements: !ruby/object:Gem::Requirement
17
+ requirements:
18
+ - - ">="
19
+ - !ruby/object:Gem::Version
20
+ version: !binary |-
21
+ MA==
22
+ none: false
23
+ requirement: !ruby/object:Gem::Requirement
24
+ requirements:
25
+ - - ">="
26
+ - !ruby/object:Gem::Version
27
+ version: !binary |-
28
+ MA==
29
+ none: false
30
+ prerelease: false
31
+ type: :development
32
+ - !ruby/object:Gem::Dependency
33
+ name: rspec
34
+ version_requirements: !ruby/object:Gem::Requirement
35
+ requirements:
36
+ - - ">="
37
+ - !ruby/object:Gem::Version
38
+ version: !binary |-
39
+ MA==
40
+ none: false
41
+ requirement: !ruby/object:Gem::Requirement
42
+ requirements:
43
+ - - ">="
44
+ - !ruby/object:Gem::Version
45
+ version: !binary |-
46
+ MA==
47
+ none: false
48
+ prerelease: false
49
+ type: :development
50
+ - !ruby/object:Gem::Dependency
51
+ name: ZenTest
52
+ version_requirements: !ruby/object:Gem::Requirement
53
+ requirements:
54
+ - - ">="
55
+ - !ruby/object:Gem::Version
56
+ version: !binary |-
57
+ MA==
58
+ none: false
59
+ requirement: !ruby/object:Gem::Requirement
60
+ requirements:
61
+ - - ">="
62
+ - !ruby/object:Gem::Version
63
+ version: !binary |-
64
+ MA==
65
+ none: false
66
+ prerelease: false
67
+ type: :development
68
+ - !ruby/object:Gem::Dependency
69
+ name: autotest-fsevent
70
+ version_requirements: !ruby/object:Gem::Requirement
71
+ requirements:
72
+ - - ">="
73
+ - !ruby/object:Gem::Version
74
+ version: !binary |-
75
+ MA==
76
+ none: false
77
+ requirement: !ruby/object:Gem::Requirement
78
+ requirements:
79
+ - - ">="
80
+ - !ruby/object:Gem::Version
81
+ version: !binary |-
82
+ MA==
83
+ none: false
84
+ prerelease: false
85
+ type: :development
86
+ - !ruby/object:Gem::Dependency
87
+ name: autotest-growl
88
+ version_requirements: !ruby/object:Gem::Requirement
89
+ requirements:
90
+ - - ">="
91
+ - !ruby/object:Gem::Version
92
+ version: !binary |-
93
+ MA==
94
+ none: false
95
+ requirement: !ruby/object:Gem::Requirement
96
+ requirements:
97
+ - - ">="
98
+ - !ruby/object:Gem::Version
99
+ version: !binary |-
100
+ MA==
101
+ none: false
102
+ prerelease: false
103
+ type: :development
104
+ - !ruby/object:Gem::Dependency
105
+ name: minitest
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - ">="
109
+ - !ruby/object:Gem::Version
110
+ version: !binary |-
111
+ MA==
112
+ none: false
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - ">="
116
+ - !ruby/object:Gem::Version
117
+ version: !binary |-
118
+ MA==
119
+ none: false
120
+ prerelease: false
121
+ type: :development
122
+ - !ruby/object:Gem::Dependency
123
+ name: minitest-spec
124
+ version_requirements: !ruby/object:Gem::Requirement
125
+ requirements:
126
+ - - ">="
127
+ - !ruby/object:Gem::Version
128
+ version: !binary |-
129
+ MA==
130
+ none: false
131
+ requirement: !ruby/object:Gem::Requirement
132
+ requirements:
133
+ - - ">="
134
+ - !ruby/object:Gem::Version
135
+ version: !binary |-
136
+ MA==
137
+ none: false
138
+ prerelease: false
139
+ type: :development
140
+ description: Gem defines basic grammar for formula builder and ability to execute formulas to get it results or sum them.
141
+ email:
142
+ - milan.jaric@gmail.com
143
+ executables: []
144
+ extensions: []
145
+ extra_rdoc_files: []
146
+ files:
147
+ - ".autotest"
148
+ - ".gitignore"
149
+ - ".rspec"
150
+ - Gemfile
151
+ - LICENSE
152
+ - README.md
153
+ - Rakefile
154
+ - lib/r_calc.rb
155
+ - lib/r_calc/arithmetic_operations.rb
156
+ - lib/r_calc/assignment_operator.rb
157
+ - lib/r_calc/binary_operator.rb
158
+ - lib/r_calc/binary_or_unary_operator.rb
159
+ - lib/r_calc/boolean_operators.rb
160
+ - lib/r_calc/formula_expression_exception.rb
161
+ - lib/r_calc/formula_processor.rb
162
+ - lib/r_calc/function.rb
163
+ - lib/r_calc/function_operators.rb
164
+ - lib/r_calc/identifier.rb
165
+ - lib/r_calc/last_result_operator.rb
166
+ - lib/r_calc/literal.rb
167
+ - lib/r_calc/operator.rb
168
+ - lib/r_calc/operator_list.rb
169
+ - lib/r_calc/parenthetical_grouping.rb
170
+ - lib/r_calc/parse_token.rb
171
+ - lib/r_calc/popper.rb
172
+ - lib/r_calc/pusher.rb
173
+ - lib/r_calc/separator.rb
174
+ - lib/r_calc/unary_operator.rb
175
+ - lib/r_calc/version.rb
176
+ - r_calc.gemspec
177
+ - spec/r_calc_spec.rb
178
+ - spec/spec_helper.rb
179
+ homepage: https://github.com/mjaric/formulator
180
+ licenses: []
181
+ post_install_message:
182
+ rdoc_options: []
183
+ require_paths:
184
+ - lib
185
+ required_ruby_version: !ruby/object:Gem::Requirement
186
+ requirements:
187
+ - - ">="
188
+ - !ruby/object:Gem::Version
189
+ segments:
190
+ - 0
191
+ hash: 2
192
+ version: !binary |-
193
+ MA==
194
+ none: false
195
+ required_rubygems_version: !ruby/object:Gem::Requirement
196
+ requirements:
197
+ - - ">="
198
+ - !ruby/object:Gem::Version
199
+ segments:
200
+ - 0
201
+ hash: 2
202
+ version: !binary |-
203
+ MA==
204
+ none: false
205
+ requirements: []
206
+ rubyforge_project:
207
+ rubygems_version: 1.8.25
208
+ signing_key:
209
+ specification_version: 3
210
+ summary: Sandboxed formula builder and evaluator
211
+ test_files:
212
+ - spec/r_calc_spec.rb
213
+ - spec/spec_helper.rb