Soroban 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,68 @@
1
+ grammar Soroban
2
+ rule formula
3
+ '=' space? logical <Formula> / string / number / boolean
4
+ end
5
+ rule logical
6
+ and ( space? 'or' space? and )*
7
+ end
8
+ rule and
9
+ truthval ( space? 'and' space? truthval )*
10
+ end
11
+ rule truthval
12
+ comparison / '(' space? logical space? ')' / boolean
13
+ end
14
+ rule boolean
15
+ 'true' / 'false' / 'TRUE' / 'FALSE'
16
+ end
17
+ rule comparison
18
+ expression ( space? comparator space? expression )*
19
+ end
20
+ rule comparator
21
+ '=' <Equal> / '<>' <NotEqual> / '>=' / '<=' / '>' / '<'
22
+ end
23
+ rule expression
24
+ multiplicative ( space? additive_operator space? multiplicative )*
25
+ end
26
+ rule additive_operator
27
+ '+' / '-'
28
+ end
29
+ rule multiplicative
30
+ value ( space? multiplicative_operator space? value )*
31
+ end
32
+ rule multiplicative_operator
33
+ '^' <Pow> / '*' / '/'
34
+ end
35
+ rule value
36
+ ( function / '(' space? expression space? ')' / range / number / boolean / identifier / string / '-' value )
37
+ end
38
+ rule function
39
+ [a-zA-Z]+ '(' space? arguments? space? ')' <Function>
40
+ end
41
+ rule arguments
42
+ logical ( space? ',' space? logical )*
43
+ end
44
+ rule number
45
+ float / integer
46
+ end
47
+ rule float
48
+ [0-9]* '.' [0-9]+
49
+ end
50
+ rule integer
51
+ [0-9]+
52
+ end
53
+ rule identifier
54
+ [a-zA-Z] [a-zA-Z0-9]* <Identifier>
55
+ end
56
+ rule label
57
+ [A-Za-z]+ [1-9] [0-9]* <Label> / '$' [A-Za-z]+ '$' [1-9] [0-9]* <Label>
58
+ end
59
+ rule string
60
+ '"' ('\"' / !'"' .)* '"' / "'" [^']* "'"
61
+ end
62
+ rule range
63
+ label ':' label <Range>
64
+ end
65
+ rule space
66
+ [\s]+
67
+ end
68
+ end
@@ -0,0 +1,65 @@
1
+ module Soroban
2
+
3
+ class Formula < Treetop::Runtime::SyntaxNode
4
+ def rewrite(value)
5
+ value.gsub(/^= */, '')
6
+ end
7
+ end
8
+
9
+ class Identifier < Treetop::Runtime::SyntaxNode
10
+ def rewrite(value)
11
+ "@#{value}.get"
12
+ end
13
+ def extract(value)
14
+ value.to_sym
15
+ end
16
+ end
17
+
18
+ class Function < Treetop::Runtime::SyntaxNode
19
+ def rewrite(value)
20
+ match = /^([^(]*)(.*)$/.match(value)
21
+ "func_#{match[1].downcase}#{match[2]}"
22
+ end
23
+ end
24
+
25
+ class Pow < Treetop::Runtime::SyntaxNode
26
+ def rewrite(value)
27
+ "**"
28
+ end
29
+ end
30
+
31
+ class Equal < Treetop::Runtime::SyntaxNode
32
+ def rewrite(value)
33
+ "=="
34
+ end
35
+ end
36
+
37
+ class NotEqual < Treetop::Runtime::SyntaxNode
38
+ def rewrite(value)
39
+ "!="
40
+ end
41
+ end
42
+
43
+ class Label < Treetop::Runtime::SyntaxNode
44
+ def rewrite(value)
45
+ value.gsub('$', '')
46
+ end
47
+ end
48
+
49
+ class Range < Treetop::Runtime::SyntaxNode
50
+ def rewrite(value)
51
+ "'#{value}'"
52
+ end
53
+ def extract(value)
54
+ fc, fr, tc, tr = Soroban::getRange(value)
55
+ retval = []
56
+ (fc..tc).each do |cc|
57
+ (fr..tr).each do |cr|
58
+ retval << "#{cc}#{cr}".to_sym
59
+ end
60
+ end
61
+ retval
62
+ end
63
+ end
64
+
65
+ end
@@ -0,0 +1,36 @@
1
+ module Treetop
2
+ module Runtime
3
+ class SyntaxNode
4
+
5
+ def convert(dependencies)
6
+ if nonterminal?
7
+ value = ""
8
+ elements.each { |element| value << element.convert(dependencies) }
9
+ _add_dependency(dependencies, extract(value))
10
+ rewrite(value)
11
+ else
12
+ _add_dependency(dependencies, extract(text_value))
13
+ rewrite(text_value)
14
+ end
15
+ end
16
+
17
+ def rewrite(value)
18
+ value
19
+ end
20
+
21
+ def extract(value)
22
+ end
23
+
24
+ private
25
+
26
+ def _add_dependency(dependencies, value)
27
+ return if value.nil?
28
+ dependencies << value
29
+ dependencies.flatten!
30
+ dependencies.compact!
31
+ dependencies.uniq!
32
+ end
33
+
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,119 @@
1
+ require 'soroban/helpers'
2
+ require 'soroban/functions'
3
+ require 'soroban/walker'
4
+ require 'soroban/cell'
5
+
6
+ module Soroban
7
+
8
+ # A container for cells.
9
+ class Sheet
10
+ attr_reader :bindings
11
+
12
+ # Creates a new sheet.
13
+ def initialize
14
+ @cells = {}
15
+ @bindings = {}
16
+ end
17
+
18
+ # Used for calling dynamically defined functions, and for creating new
19
+ # cells (via `label=`).
20
+ def method_missing(method, *args, &block)
21
+ if match = /^func_(.*)$/i.match(method.to_s)
22
+ return Soroban::call(self, match[1], *args)
23
+ elsif match = /^([a-z][\w]*)=$/i.match(method.to_s)
24
+ return _add(match[1], args[0])
25
+ end
26
+ super
27
+ end
28
+
29
+ # Set the contents of one or more cells or ranges.
30
+ def set(label_or_range, contents)
31
+ unless range = Soroban::getRange(label_or_range)
32
+ return _add(label_or_range, contents)
33
+ end
34
+ fc, fr, tc, tr = range
35
+ if fc == tc || fr == tr
36
+ raise ArgumentError, "Expecting an array when setting #{label_or_range}" unless contents.kind_of? Array
37
+ cc, cr = fc, fr
38
+ contents.each do |item|
39
+ set("#{cc}#{cr}", item)
40
+ cc.next! if fr == tr
41
+ cr.next! if fc == tc
42
+ end
43
+ raise Soroban::RangeError, "Supplied array doesn't match range length" if cc != tc && cr != tr
44
+ else
45
+ raise ArgumentError, "Can only set cells or 1-dimensional ranges of cells"
46
+ end
47
+ end
48
+
49
+ # Retrieve the contents of a cell.
50
+ def get(label_or_name)
51
+ _get(label_or_name, eval("@#{label_or_name}", binding))
52
+ end
53
+
54
+ # Bind one or more named variables to a cell.
55
+ def bind(name, label)
56
+ unless @cells.keys.include?(label.to_sym)
57
+ raise Soroban::UndefinedError, "Cannot bind '#{name}' to non-existent cell '#{label}'"
58
+ end
59
+ _bind(name, label)
60
+ end
61
+
62
+ # Visit each cell in the supplied range, yielding its value.
63
+ def walk(range)
64
+ Walker.new(range, binding)
65
+ end
66
+
67
+ # Return a hash of `label => contents` for each cell in the sheet.
68
+ def cells
69
+ Hash[@cells.keys.map { |label| label.to_s }.zip( @cells.keys.map { |label| eval("@#{label}.excel") } )]
70
+ end
71
+
72
+ # Return a list of referenced but undefined cells.
73
+ def missing
74
+ @cells.values.map.flatten.uniq - @cells.keys
75
+ end
76
+
77
+ private
78
+
79
+ def _add(label, contents)
80
+ internal = "@#{label}"
81
+ _expose(internal, label)
82
+ cell = Cell.new(binding)
83
+ _set(label, cell, contents)
84
+ instance_variable_set(internal, cell)
85
+ end
86
+
87
+ def _set(label, cell, contents)
88
+ cell.set(contents)
89
+ @cells[label.to_sym] = cell.dependencies
90
+ end
91
+
92
+ def _get(label_or_name, cell)
93
+ label = label_or_name.to_sym
94
+ name = @cells[label] ? label : @bindings[label]
95
+ badref = @cells[name] & missing
96
+ raise Soroban::UndefinedError, "Unmet dependencies #{badref.join(', ')} for #{label}" if badref.length > 0
97
+ cell.get
98
+ end
99
+
100
+ def _bind(name, label)
101
+ @bindings[name.to_sym] = label.to_sym
102
+ internal = "@#{label}"
103
+ _expose(internal, name)
104
+ end
105
+
106
+ def _expose(internal, name)
107
+ instance_eval <<-EOV, __FILE__, __LINE__ + 1
108
+ def #{name}
109
+ _get("#{name}", #{internal})
110
+ end
111
+ def #{name}=(contents)
112
+ _set("#{name}", #{internal}, contents)
113
+ end
114
+ EOV
115
+ end
116
+
117
+ end
118
+
119
+ end
@@ -0,0 +1,26 @@
1
+ module Soroban
2
+
3
+ # An enumerable that allows cells in a range to be visited.
4
+ class Walker
5
+
6
+ include Enumerable
7
+
8
+ # Create a new walker from a supplied range and binding. The binding is
9
+ # required when calculating the value of each visited cell.
10
+ def initialize(range, binding)
11
+ @binding = binding
12
+ @fc, @fr, @tc, @tr = Soroban::getRange(range)
13
+ end
14
+
15
+ # Yield the value of each cell referenced by the supplied range.
16
+ def each
17
+ (@fc..@tc).each do |col|
18
+ (@fr..@tr).each do |row|
19
+ yield eval("@#{col}#{row}.get", @binding)
20
+ end
21
+ end
22
+ end
23
+
24
+ end
25
+
26
+ end
@@ -0,0 +1,97 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
2
+
3
+ describe "Soroban" do
4
+
5
+ let(:sheet) { Soroban::Sheet.new }
6
+
7
+ it "can add two numbers" do
8
+ sheet.x = 2
9
+ sheet.y = 3
10
+ sheet.f = "=x+y"
11
+ sheet.f.should eq(5)
12
+ sheet.x -= 1
13
+ sheet.f.should eq(4)
14
+ end
15
+
16
+ it "can iterate over a collection of cells" do
17
+ sheet.A1 = 'a'
18
+ sheet.B1 = 'b'
19
+ sheet.A2 = 'c'
20
+ sheet.B2 = 'd'
21
+ data = sheet.walk('A1:B2').to_a
22
+ data.sort.join.should eq('abcd')
23
+ end
24
+
25
+ it "can set a value" do
26
+ sheet.set(:foo, 'hello')
27
+ sheet.foo.should eq('hello')
28
+ end
29
+
30
+ it "can get a value" do
31
+ sheet.foo = 'hello'
32
+ sheet.get(:foo).should eq('hello')
33
+ end
34
+
35
+ it "can set an array" do
36
+ sheet.set("A1:A5", [ 1, 2, 3, 4, 5 ])
37
+ sheet.B1 = '=SUM(A1:A5)'
38
+ sheet.B1.should eq(15)
39
+ end
40
+
41
+ it "can set a hash" do
42
+ sheet.set("A1:A3", [ 'one', 'two', 'three' ])
43
+ sheet.set("B1:B3", [ 'mop', 'hai', 'bah' ])
44
+ sheet.C1 = '=VLOOKUP("two", A1:B3, 2, 0)'
45
+ sheet.C1.should eq('hai')
46
+ end
47
+
48
+ it "can iterate over all cells" do
49
+ sheet.set("A1:A3", [ 1, 2, 3 ])
50
+ sheet.set("B1:B3", [ 4, 5, 6 ])
51
+ sheet.set("C1:C3", [ 7, 8, 9 ])
52
+ sheet.cells.map { |label, contents| contents.to_i }.sort.should eq [1,2,3,4,5,6,7,8,9]
53
+ end
54
+
55
+ it "can bind variables to cells" do
56
+ sheet.A1 = 0
57
+ sheet.A2 = "=A1^2"
58
+ sheet.bind(:input, :A1)
59
+ sheet.bind(:output, :A2)
60
+ sheet.input = 5
61
+ sheet.output.should eq(25)
62
+ sheet.bindings.keys.should include :input
63
+ sheet.bindings.keys.should include :output
64
+ sheet.bindings.values.should include :A1
65
+ sheet.bindings.values.should include :A2
66
+ end
67
+
68
+ it "can define new functions" do
69
+ Soroban::define :FOO => lambda { |a, b| 2 * a + b / 2 }
70
+ sheet.A1 = 7
71
+ sheet.A2 = 8
72
+ sheet.A3 = "=foo(A1, A2)"
73
+ sheet.A3.should eq(18)
74
+ Soroban::functions.should include 'FOO'
75
+ end
76
+
77
+ it "can report on missing cells" do
78
+ sheet.A3 = "=A2+foo(A3:B4)"
79
+ expected = [:A2, :A4, :B3, :B4 ]
80
+ sheet.missing.should =~ expected
81
+ end
82
+
83
+ it "can detect loops when running formulas" do
84
+ lambda {
85
+ sheet.A1 = "=A2"
86
+ sheet.A2 = "=A1"
87
+ sheet.A2
88
+ }.should raise_error(Soroban::RecursionError)
89
+ end
90
+
91
+ it "can reject valid ruby code in formulas" do
92
+ lambda {
93
+ sheet.set(:A1, "=3**2")
94
+ }.should raise_error(Soroban::ParseError)
95
+ end
96
+
97
+ end
@@ -0,0 +1,12 @@
1
+ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
2
+ $LOAD_PATH.unshift(File.dirname(__FILE__))
3
+ require 'rspec'
4
+ require 'soroban'
5
+
6
+ # Requires supporting files with custom matchers and macros, etc,
7
+ # in ./support/ and its subdirectories.
8
+ Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].each {|f| require f}
9
+
10
+ RSpec.configure do |config|
11
+
12
+ end
metadata ADDED
@@ -0,0 +1,234 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: Soroban
3
+ version: !ruby/object:Gem::Version
4
+ hash: 27
5
+ prerelease:
6
+ segments:
7
+ - 0
8
+ - 1
9
+ - 0
10
+ version: 0.1.0
11
+ platform: ruby
12
+ authors:
13
+ - Jason Hutchens
14
+ autorequire:
15
+ bindir: bin
16
+ cert_chain: []
17
+
18
+ date: 2012-04-23 00:00:00 Z
19
+ dependencies:
20
+ - !ruby/object:Gem::Dependency
21
+ type: :runtime
22
+ prerelease: false
23
+ requirement: &id001 !ruby/object:Gem::Requirement
24
+ none: false
25
+ requirements:
26
+ - - ~>
27
+ - !ruby/object:Gem::Version
28
+ hash: 19
29
+ segments:
30
+ - 1
31
+ - 4
32
+ - 10
33
+ version: 1.4.10
34
+ version_requirements: *id001
35
+ name: treetop
36
+ - !ruby/object:Gem::Dependency
37
+ type: :development
38
+ prerelease: false
39
+ requirement: &id002 !ruby/object:Gem::Requirement
40
+ none: false
41
+ requirements:
42
+ - - ~>
43
+ - !ruby/object:Gem::Version
44
+ hash: 47
45
+ segments:
46
+ - 2
47
+ - 8
48
+ - 0
49
+ version: 2.8.0
50
+ version_requirements: *id002
51
+ name: rspec
52
+ - !ruby/object:Gem::Dependency
53
+ type: :development
54
+ prerelease: false
55
+ requirement: &id003 !ruby/object:Gem::Requirement
56
+ none: false
57
+ requirements:
58
+ - - ~>
59
+ - !ruby/object:Gem::Version
60
+ hash: 5
61
+ segments:
62
+ - 0
63
+ - 7
64
+ version: "0.7"
65
+ version_requirements: *id003
66
+ name: yard
67
+ - !ruby/object:Gem::Dependency
68
+ type: :development
69
+ prerelease: false
70
+ requirement: &id004 !ruby/object:Gem::Requirement
71
+ none: false
72
+ requirements:
73
+ - - ~>
74
+ - !ruby/object:Gem::Version
75
+ hash: 31
76
+ segments:
77
+ - 3
78
+ - 12
79
+ version: "3.12"
80
+ version_requirements: *id004
81
+ name: rdoc
82
+ - !ruby/object:Gem::Dependency
83
+ type: :development
84
+ prerelease: false
85
+ requirement: &id005 !ruby/object:Gem::Requirement
86
+ none: false
87
+ requirements:
88
+ - - ~>
89
+ - !ruby/object:Gem::Version
90
+ hash: 21
91
+ segments:
92
+ - 1
93
+ - 1
94
+ - 3
95
+ version: 1.1.3
96
+ version_requirements: *id005
97
+ name: bundler
98
+ - !ruby/object:Gem::Dependency
99
+ type: :development
100
+ prerelease: false
101
+ requirement: &id006 !ruby/object:Gem::Requirement
102
+ none: false
103
+ requirements:
104
+ - - ~>
105
+ - !ruby/object:Gem::Version
106
+ hash: 49
107
+ segments:
108
+ - 1
109
+ - 8
110
+ - 3
111
+ version: 1.8.3
112
+ version_requirements: *id006
113
+ name: jeweler
114
+ - !ruby/object:Gem::Dependency
115
+ type: :development
116
+ prerelease: false
117
+ requirement: &id007 !ruby/object:Gem::Requirement
118
+ none: false
119
+ requirements:
120
+ - - ">="
121
+ - !ruby/object:Gem::Version
122
+ hash: 3
123
+ segments:
124
+ - 0
125
+ version: "0"
126
+ version_requirements: *id007
127
+ name: rcov
128
+ - !ruby/object:Gem::Dependency
129
+ type: :development
130
+ prerelease: false
131
+ requirement: &id008 !ruby/object:Gem::Requirement
132
+ none: false
133
+ requirements:
134
+ - - ~>
135
+ - !ruby/object:Gem::Version
136
+ hash: 17
137
+ segments:
138
+ - 1
139
+ - 2
140
+ - 7
141
+ version: 1.2.7
142
+ version_requirements: *id008
143
+ name: rubyXL
144
+ - !ruby/object:Gem::Dependency
145
+ type: :development
146
+ prerelease: false
147
+ requirement: &id009 !ruby/object:Gem::Requirement
148
+ none: false
149
+ requirements:
150
+ - - ">="
151
+ - !ruby/object:Gem::Version
152
+ hash: 3
153
+ segments:
154
+ - 0
155
+ version: "0"
156
+ version_requirements: *id009
157
+ name: redcarpet
158
+ description: Soroban makes it easy to extract and execute formulas from Excel spreadsheets. It rewrites Excel formulas as Ruby expressions, and allows you to bind named variables to spreadsheet cells to easily manipulate inputs and capture outputs.
159
+ email: jason.hutchens@agworld.com.au
160
+ executables: []
161
+
162
+ extensions: []
163
+
164
+ extra_rdoc_files:
165
+ - LICENSE.txt
166
+ - README.md
167
+ files:
168
+ - .document
169
+ - .rspec
170
+ - .yardopts
171
+ - Gemfile
172
+ - Gemfile.lock
173
+ - LICENSE.txt
174
+ - README.md
175
+ - Rakefile
176
+ - VERSION
177
+ - lib/soroban.rb
178
+ - lib/soroban/cell.rb
179
+ - lib/soroban/error.rb
180
+ - lib/soroban/functions.rb
181
+ - lib/soroban/functions/and.rb
182
+ - lib/soroban/functions/average.rb
183
+ - lib/soroban/functions/if.rb
184
+ - lib/soroban/functions/max.rb
185
+ - lib/soroban/functions/min.rb
186
+ - lib/soroban/functions/not.rb
187
+ - lib/soroban/functions/or.rb
188
+ - lib/soroban/functions/sum.rb
189
+ - lib/soroban/functions/vlookup.rb
190
+ - lib/soroban/helpers.rb
191
+ - lib/soroban/parser.rb
192
+ - lib/soroban/parser/grammar.rb
193
+ - lib/soroban/parser/grammar.treetop
194
+ - lib/soroban/parser/nodes.rb
195
+ - lib/soroban/parser/rewrite.rb
196
+ - lib/soroban/sheet.rb
197
+ - lib/soroban/walker.rb
198
+ - spec/soroban_spec.rb
199
+ - spec/spec_helper.rb
200
+ homepage: http://github.com/jasonhutchens/soroban
201
+ licenses:
202
+ - MIT
203
+ post_install_message:
204
+ rdoc_options: []
205
+
206
+ require_paths:
207
+ - lib
208
+ required_ruby_version: !ruby/object:Gem::Requirement
209
+ none: false
210
+ requirements:
211
+ - - ">="
212
+ - !ruby/object:Gem::Version
213
+ hash: 3
214
+ segments:
215
+ - 0
216
+ version: "0"
217
+ required_rubygems_version: !ruby/object:Gem::Requirement
218
+ none: false
219
+ requirements:
220
+ - - ">="
221
+ - !ruby/object:Gem::Version
222
+ hash: 3
223
+ segments:
224
+ - 0
225
+ version: "0"
226
+ requirements: []
227
+
228
+ rubyforge_project:
229
+ rubygems_version: 1.8.10
230
+ signing_key:
231
+ specification_version: 3
232
+ summary: Soroban is a calculating engine that understands Excel formulas.
233
+ test_files: []
234
+