expression_processor 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.
@@ -0,0 +1,4 @@
1
+ *.gem
2
+ .bundle
3
+ Gemfile.lock
4
+ pkg/*
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source "http://rubygems.org"
2
+
3
+ # Specify your gem's dependencies in expression_processor.gemspec
4
+ gemspec
@@ -0,0 +1,10 @@
1
+ # ExpressionProcessor
2
+
3
+ ```ruby
4
+ expression = ExpressionProcessor::Expression.new("MAX(10, SUM(A))")
5
+ expression.constants({:A => [1,3,5]})
6
+ expression.valid?([:A]) # specify constants that are allowed
7
+ expression.eval # => 10
8
+ ```
9
+
10
+
@@ -0,0 +1 @@
1
+ require "bundler/gem_tasks"
@@ -0,0 +1,24 @@
1
+ # -*- encoding: utf-8 -*-
2
+ $:.push File.expand_path("../lib", __FILE__)
3
+ require "expression_processor/version"
4
+
5
+ Gem::Specification.new do |s|
6
+ s.name = "expression_processor"
7
+ s.version = ExpressionProcessor::VERSION
8
+ s.authors = ["Stephen St. Martin"]
9
+ s.email = ["kuprishuz@gmail.com"]
10
+ s.homepage = ""
11
+ s.summary = %q{Simple excel like formula processor}
12
+ s.description = %q{Allows you to process simple excel like formulas.}
13
+
14
+ s.rubyforge_project = "expression_processor"
15
+
16
+ s.files = `git ls-files`.split("\n")
17
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
18
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
19
+ s.require_paths = ["lib"]
20
+
21
+ # specify any dependencies here; for example:
22
+ # s.add_development_dependency "rspec"
23
+ # s.add_runtime_dependency "rest-client"
24
+ end
@@ -0,0 +1,150 @@
1
+ require "expression_processor/version"
2
+
3
+ module ExpressionProcessor
4
+ class BlankSlate
5
+ instance_methods.each {|m| undef_method m unless m =~ /^(__|instance_eval|object_id)/}
6
+ end
7
+
8
+ class Expression
9
+ attr_accessor :constants, :errors, :expression
10
+
11
+ def initialize(expression)
12
+ @constants = {}
13
+ @errors = []
14
+ @expression = expression || ""
15
+ end
16
+
17
+ def constants=(constants)
18
+ constants.each {|constant, value| @constants[constant.to_s.downcase.to_sym] = value }
19
+ end
20
+
21
+ def eval
22
+ proxy = Proxy.new(@constants)
23
+ untainted = preprocess.untaint
24
+ result = valid? ? proc { $SAFE = 3; proxy.instance_eval(untainted) }.call : 0
25
+ result.to_f.round(2)
26
+ end
27
+
28
+ def preprocess
29
+ executable = @expression.downcase
30
+ tokenize.each do |token|
31
+ case token[0]
32
+ # ignore dollar signs
33
+ when :dollar
34
+ executable.gsub!(/\$/, '')
35
+ # convert percent to decimal 10% => 0.10
36
+ when :percent
37
+ executable.gsub!(/(#{token[1]})/) {|match| match.gsub(/(%)/, '').to_f / 100 }
38
+ end
39
+ end
40
+
41
+ # HACK: make sure operators have surrounding whitespace or calculations dont always
42
+ # have correct result (possibly an eval issue?)
43
+ executable.gsub!(/([%\/*+-])/, " \\1 ")
44
+ executable
45
+ end
46
+
47
+ def tokenize
48
+ @tokens ||= Lexer.new(@expression).tokenize
49
+ end
50
+
51
+ def valid?(constants = nil)
52
+ validate(constants)
53
+ @errors.empty?
54
+ end
55
+
56
+ private
57
+ def validate(constants = nil)
58
+ constants ||= @constants.keys
59
+
60
+ # check parentheses count
61
+ @errors << "has mismatched parenthesis" unless @expression.scan(/[()]/).length % 2 == 0
62
+
63
+ # check all tokens are valid
64
+ tokenize.each do |token|
65
+ case token[0]
66
+ when :call
67
+ @errors << "calls invalid method #{token[1]}" unless Proxy.instance_methods.include?(token[1].downcase.to_sym)
68
+ when :identifier
69
+ @errors << "uses invalid indentifier #{token[1]}" unless constants.include?(token[1].downcase) || constants.include?(token[1].downcase.to_sym)
70
+ when :operator
71
+ when :float
72
+ when :percent
73
+ when :dollar
74
+ when "("
75
+ when ")"
76
+ when ','
77
+ else
78
+ @errors << "has unrecognized token #{token[0]}"
79
+ end
80
+ end
81
+ end
82
+ end
83
+
84
+ class Lexer
85
+ def initialize(code)
86
+ @code = code.upcase.gsub(/\s/, '')
87
+ @tokens = []
88
+ end
89
+
90
+ def tokenize
91
+ position = 0
92
+
93
+ # scan one character at a time
94
+ while position < @code.size
95
+ chunk = @code[position..-1]
96
+
97
+ if token = chunk[/\A([A-Z]\w*)\(/, 1]
98
+ @tokens << [:call, token]
99
+ elsif token = chunk[/\A([A-Z]\w*)/, 1]
100
+ @tokens << [:identifier, token]
101
+ elsif token = chunk[/\A([0-9.]+%{1})/, 1]
102
+ @tokens << [:percent, token]
103
+ elsif token = chunk[/\A(\$)[\d.]+/, 1]
104
+ @tokens << [:dollar, token]
105
+ elsif token = chunk[/\A([0-9.]+)/, 1]
106
+ @tokens << [:float, token.to_f]
107
+ elsif token = chunk[/\A([%\/*+-])/, 1]
108
+ @tokens << [:operator, token]
109
+ else
110
+ token = chunk[0,1]
111
+ @tokens << [token, token]
112
+ end
113
+
114
+ position += token.size
115
+ end
116
+
117
+ @tokens
118
+ end
119
+ end
120
+
121
+ class Proxy < BlankSlate
122
+ def initialize(constants)
123
+ @constants = constants
124
+ end
125
+
126
+ def method_missing(sym, *args, &block)
127
+ @constants[sym] || 0.0
128
+ end
129
+
130
+ def max(*values)
131
+ values = values.flatten! || values
132
+ values.max
133
+ end
134
+
135
+ def min(*values)
136
+ values = values.flatten! || values
137
+ values.min
138
+ end
139
+
140
+ def round(value)
141
+ value.to_f.round
142
+ end
143
+
144
+ def sum(*values)
145
+ values = values.flatten! || values
146
+ values.inject(0.0) {|total, value| total += value if value.is_a?(Numeric) }
147
+ end
148
+ end
149
+ end
150
+
@@ -0,0 +1,3 @@
1
+ module ExpressionProcessor
2
+ VERSION = "0.0.1"
3
+ end
metadata ADDED
@@ -0,0 +1,52 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: expression_processor
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Stephen St. Martin
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2012-01-20 00:00:00.000000000 Z
13
+ dependencies: []
14
+ description: Allows you to process simple excel like formulas.
15
+ email:
16
+ - kuprishuz@gmail.com
17
+ executables: []
18
+ extensions: []
19
+ extra_rdoc_files: []
20
+ files:
21
+ - .gitignore
22
+ - Gemfile
23
+ - README.md
24
+ - Rakefile
25
+ - expression_processor.gemspec
26
+ - lib/expression_processor.rb
27
+ - lib/expression_processor/version.rb
28
+ homepage: ''
29
+ licenses: []
30
+ post_install_message:
31
+ rdoc_options: []
32
+ require_paths:
33
+ - lib
34
+ required_ruby_version: !ruby/object:Gem::Requirement
35
+ none: false
36
+ requirements:
37
+ - - ! '>='
38
+ - !ruby/object:Gem::Version
39
+ version: '0'
40
+ required_rubygems_version: !ruby/object:Gem::Requirement
41
+ none: false
42
+ requirements:
43
+ - - ! '>='
44
+ - !ruby/object:Gem::Version
45
+ version: '0'
46
+ requirements: []
47
+ rubyforge_project: expression_processor
48
+ rubygems_version: 1.8.15
49
+ signing_key:
50
+ specification_version: 3
51
+ summary: Simple excel like formula processor
52
+ test_files: []