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.
- data/.gitignore +4 -0
- data/Gemfile +4 -0
- data/README.md +10 -0
- data/Rakefile +1 -0
- data/expression_processor.gemspec +24 -0
- data/lib/expression_processor.rb +150 -0
- data/lib/expression_processor/version.rb +3 -0
- metadata +52 -0
data/.gitignore
ADDED
data/Gemfile
ADDED
data/README.md
ADDED
data/Rakefile
ADDED
@@ -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
|
+
|
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: []
|