soroban 0.1.1 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.travis.yml +13 -0
- data/.yardopts +1 -0
- data/Gemfile +16 -5
- data/Gemfile.lock +14 -12
- data/README.md +56 -16
- data/Rakefile +1 -1
- data/Soroban.gemspec +15 -24
- data/VERSION +1 -1
- data/lib/soroban.rb +1 -0
- data/lib/soroban/cell.rb +2 -2
- data/lib/soroban/helpers.rb +9 -2
- data/lib/soroban/import.rb +1 -0
- data/lib/soroban/import/ruby_xl_importer.rb +56 -0
- data/lib/soroban/import/ruby_xl_patch.rb +7 -0
- data/lib/soroban/label_walker.rb +24 -0
- data/lib/soroban/sheet.rb +58 -27
- data/lib/soroban/value_walker.rb +50 -0
- data/spec/documentation_spec.rb +90 -0
- data/spec/soroban_spec.rb +22 -12
- metadata +30 -98
- data/lib/soroban/walker.rb +0 -26
data/.travis.yml
ADDED
data/.yardopts
CHANGED
data/Gemfile
CHANGED
@@ -3,12 +3,23 @@ source "http://rubygems.org"
|
|
3
3
|
gem "treetop", "~> 1.4.10"
|
4
4
|
|
5
5
|
group :development do
|
6
|
-
gem "
|
6
|
+
gem "rubyXL", "~> 1.2.7"
|
7
|
+
gem "nokogiri", ">= 1.4.4"
|
8
|
+
gem "rubyzip", ">= 0.9.4"
|
9
|
+
# gem "rcov", ">= 0"
|
10
|
+
end
|
11
|
+
|
12
|
+
group :test do
|
13
|
+
gem "rake"
|
14
|
+
gem "rspec", "~> 2.9.0"
|
7
15
|
gem "yard", "~> 0.7"
|
16
|
+
end
|
17
|
+
|
18
|
+
group :document do
|
8
19
|
gem "rdoc", "~> 3.12"
|
9
|
-
gem "bundler", "~> 1.1.3"
|
10
|
-
gem "jeweler", "~> 1.8.3"
|
11
|
-
gem "rcov", ">= 0"
|
12
|
-
gem "rubyXL", "~> 1.2.7"
|
13
20
|
gem "redcarpet"
|
14
21
|
end
|
22
|
+
|
23
|
+
group :gemify do
|
24
|
+
gem "jeweler", "~> 1.8.3"
|
25
|
+
end
|
data/Gemfile.lock
CHANGED
@@ -9,21 +9,22 @@ GEM
|
|
9
9
|
rake
|
10
10
|
rdoc
|
11
11
|
json (1.6.6)
|
12
|
+
nokogiri (1.5.2)
|
12
13
|
polyglot (0.3.3)
|
13
14
|
rake (0.9.2.2)
|
14
|
-
rcov (1.0.0)
|
15
15
|
rdoc (3.12)
|
16
16
|
json (~> 1.4)
|
17
17
|
redcarpet (2.1.1)
|
18
|
-
rspec (2.
|
19
|
-
rspec-core (~> 2.
|
20
|
-
rspec-expectations (~> 2.
|
21
|
-
rspec-mocks (~> 2.
|
22
|
-
rspec-core (2.
|
23
|
-
rspec-expectations (2.
|
24
|
-
diff-lcs (~> 1.1.
|
25
|
-
rspec-mocks (2.
|
18
|
+
rspec (2.9.0)
|
19
|
+
rspec-core (~> 2.9.0)
|
20
|
+
rspec-expectations (~> 2.9.0)
|
21
|
+
rspec-mocks (~> 2.9.0)
|
22
|
+
rspec-core (2.9.0)
|
23
|
+
rspec-expectations (2.9.1)
|
24
|
+
diff-lcs (~> 1.1.3)
|
25
|
+
rspec-mocks (2.9.0)
|
26
26
|
rubyXL (1.2.7)
|
27
|
+
rubyzip (0.9.7)
|
27
28
|
treetop (1.4.10)
|
28
29
|
polyglot
|
29
30
|
polyglot (>= 0.3.1)
|
@@ -33,12 +34,13 @@ PLATFORMS
|
|
33
34
|
ruby
|
34
35
|
|
35
36
|
DEPENDENCIES
|
36
|
-
bundler (~> 1.1.3)
|
37
37
|
jeweler (~> 1.8.3)
|
38
|
-
|
38
|
+
nokogiri (>= 1.4.4)
|
39
|
+
rake
|
39
40
|
rdoc (~> 3.12)
|
40
41
|
redcarpet
|
41
|
-
rspec (~> 2.
|
42
|
+
rspec (~> 2.9.0)
|
42
43
|
rubyXL (~> 1.2.7)
|
44
|
+
rubyzip (>= 0.9.4)
|
43
45
|
treetop (~> 1.4.10)
|
44
46
|
yard (~> 0.7)
|
data/README.md
CHANGED
@@ -3,37 +3,56 @@ Soroban
|
|
3
3
|
|
4
4
|
Soroban is a calculating engine that understands Excel formulas.
|
5
5
|
|
6
|
+
[![Build Status](https://secure.travis-ci.org/agworld/soroban.png)](http://travis-ci.org/#!/agworld/soroban)
|
7
|
+
[![Dependency Status](https://gemnasium.com/agworld/soroban.png)](https://gemnasium.com/agworld/soroban)
|
8
|
+
|
9
|
+
|
6
10
|
Getting Started
|
7
11
|
---------------
|
8
12
|
|
9
|
-
|
10
|
-
|
11
|
-
|
13
|
+
Simply `sudo gem install soroban` and then `require 'soroban'` in your code.
|
14
|
+
|
15
|
+
Look at the examples on this page, the [tests](https://github.com/agworld/soroban/blob/master/spec/soroban_spec.rb) and the [API docs](http://rubydoc.info/github/agworld/soroban/master/frames) to get up to speed.
|
12
16
|
|
13
17
|
Example Usage
|
14
18
|
-------------
|
15
19
|
|
16
20
|
```ruby
|
17
|
-
require 'soroban'
|
18
|
-
|
19
21
|
s = Soroban::Sheet.new()
|
20
22
|
|
21
23
|
s.A1 = 2
|
22
|
-
s.set('B1:B5'
|
23
|
-
s.C1 = "=SUM(B1:B5) + A1 ^ 3"
|
24
|
-
s.C2 = "=IF(C1>
|
24
|
+
s.set('B1:B5' => [1,2,3,4,5])
|
25
|
+
s.C1 = "=SUM(A1, B1:B5, 5) + A1 ^ 3"
|
26
|
+
s.C2 = "=IF(C1>30,'Large','Tiny')"
|
25
27
|
|
26
|
-
puts s.C1 # =>
|
28
|
+
puts s.C1 # => 30
|
27
29
|
|
28
|
-
s.bind(:input, :
|
29
|
-
s.bind(:output, :C2)
|
30
|
+
s.bind(:input => :A1, :output => :C2)
|
30
31
|
|
31
32
|
puts s.output # => "Tiny"
|
32
33
|
|
33
34
|
s.input = 3
|
34
35
|
|
35
36
|
puts s.output # => "Large"
|
36
|
-
puts s.C1 # =>
|
37
|
+
puts s.C1 # => 50
|
38
|
+
```
|
39
|
+
|
40
|
+
Bindings
|
41
|
+
--------
|
42
|
+
|
43
|
+
Soroban allows you to bind meaningful variable names to individual cells and to ranges of cells. When bound to a range, variables act as an array,.
|
44
|
+
|
45
|
+
```ruby
|
46
|
+
s.set(:A1 => 'hello', 'B1:B5' => [1,2,3,4,5])
|
47
|
+
|
48
|
+
s.bind(:foo => :A1, :bar => 'B1:B5')
|
49
|
+
|
50
|
+
puts s.foo # => 'hello'
|
51
|
+
puts s.bar[0] # => 1
|
52
|
+
|
53
|
+
s.bar[0] = 'howdy'
|
54
|
+
|
55
|
+
puts s.B1 # => 'howdy'
|
37
56
|
```
|
38
57
|
|
39
58
|
Persistence
|
@@ -54,16 +73,37 @@ s.F1 = "= E1 + SUM(D1:D5)"
|
|
54
73
|
s.missing # => [:E1, :D1, :D2, :D3, :D4, :D5]
|
55
74
|
|
56
75
|
s.E1 = "= D1 ^ D2"
|
57
|
-
s.set("D1:D5"
|
76
|
+
s.set("D1:D5" => [1,2,3,4,5])
|
58
77
|
|
59
78
|
s.missing # => []
|
60
79
|
|
61
80
|
s.cells # => {"F1"=>"= E1 + SUM(D1:D5)", "E1"=>"= D1 ^ D2", "D1"=>"1", "D2"=>"2", "D3"=>"3", "D4"=>"4", "D5"=>"5"}
|
62
81
|
```
|
63
82
|
|
64
|
-
|
83
|
+
Importers
|
84
|
+
---------
|
85
|
+
|
86
|
+
Soroban has a built-in importer for xlsx files. It requires the [RubyXL](https://github.com/gilt/rubyXL) gem. Use it as follows:
|
87
|
+
|
88
|
+
```ruby
|
89
|
+
require 'rubyXL'
|
90
|
+
|
91
|
+
BINDINGS = {
|
92
|
+
:gravity => :B1,
|
93
|
+
:mass => :B2,
|
94
|
+
:force => :B10
|
95
|
+
}
|
96
|
+
|
97
|
+
s = Soroban::Import::rubyXL("/Users/kranzky/Desktop/Physics.xlsx", 0, BINDINGS )
|
98
|
+
```
|
99
|
+
|
100
|
+
This import process returns a new Soroban::Sheet object that contains all the
|
101
|
+
cells required to calculate the values of the bound variables, and which has the
|
102
|
+
bindings set up correctly.
|
103
|
+
|
104
|
+
You can import other kinds of file using the following pattern:
|
65
105
|
|
66
|
-
* Add the cells that correspond to inputs and outputs
|
106
|
+
* Add the cells that correspond to bound inputs and outputs
|
67
107
|
* Add the cells reported by `missing` (and continue to do so until it's empty)
|
68
108
|
* Persist the hash returned by `cells`
|
69
109
|
|
@@ -75,7 +115,7 @@ you want to iterate over cell values (including computed values of formulas),
|
|
75
115
|
then use `walk`.
|
76
116
|
|
77
117
|
```ruby
|
78
|
-
s.set('D1:D5'
|
118
|
+
s.set('D1:D5' => [1,2,3,4,5])
|
79
119
|
s.walk('D1:D5').reduce(:+) # => 15
|
80
120
|
```
|
81
121
|
|
data/Rakefile
CHANGED
data/Soroban.gemspec
CHANGED
@@ -5,11 +5,11 @@
|
|
5
5
|
|
6
6
|
Gem::Specification.new do |s|
|
7
7
|
s.name = "soroban"
|
8
|
-
s.version = "0.
|
8
|
+
s.version = "0.2.0"
|
9
9
|
|
10
10
|
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
|
11
11
|
s.authors = ["Jason Hutchens"]
|
12
|
-
s.date = "2012-04-
|
12
|
+
s.date = "2012-04-24"
|
13
13
|
s.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."
|
14
14
|
s.email = "jason.hutchens@agworld.com.au"
|
15
15
|
s.extra_rdoc_files = [
|
@@ -19,6 +19,7 @@ Gem::Specification.new do |s|
|
|
19
19
|
s.files = [
|
20
20
|
".document",
|
21
21
|
".rspec",
|
22
|
+
".travis.yml",
|
22
23
|
".yardopts",
|
23
24
|
"Gemfile",
|
24
25
|
"Gemfile.lock",
|
@@ -41,13 +42,18 @@ Gem::Specification.new do |s|
|
|
41
42
|
"lib/soroban/functions/sum.rb",
|
42
43
|
"lib/soroban/functions/vlookup.rb",
|
43
44
|
"lib/soroban/helpers.rb",
|
45
|
+
"lib/soroban/import.rb",
|
46
|
+
"lib/soroban/import/ruby_xl_importer.rb",
|
47
|
+
"lib/soroban/import/ruby_xl_patch.rb",
|
48
|
+
"lib/soroban/label_walker.rb",
|
44
49
|
"lib/soroban/parser.rb",
|
45
50
|
"lib/soroban/parser/grammar.rb",
|
46
51
|
"lib/soroban/parser/grammar.treetop",
|
47
52
|
"lib/soroban/parser/nodes.rb",
|
48
53
|
"lib/soroban/parser/rewrite.rb",
|
49
54
|
"lib/soroban/sheet.rb",
|
50
|
-
"lib/soroban/
|
55
|
+
"lib/soroban/value_walker.rb",
|
56
|
+
"spec/documentation_spec.rb",
|
51
57
|
"spec/soroban_spec.rb",
|
52
58
|
"spec/spec_helper.rb"
|
53
59
|
]
|
@@ -62,35 +68,20 @@ Gem::Specification.new do |s|
|
|
62
68
|
|
63
69
|
if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
|
64
70
|
s.add_runtime_dependency(%q<treetop>, ["~> 1.4.10"])
|
65
|
-
s.add_development_dependency(%q<rspec>, ["~> 2.8.0"])
|
66
|
-
s.add_development_dependency(%q<yard>, ["~> 0.7"])
|
67
|
-
s.add_development_dependency(%q<rdoc>, ["~> 3.12"])
|
68
|
-
s.add_development_dependency(%q<bundler>, ["~> 1.1.3"])
|
69
|
-
s.add_development_dependency(%q<jeweler>, ["~> 1.8.3"])
|
70
|
-
s.add_development_dependency(%q<rcov>, [">= 0"])
|
71
71
|
s.add_development_dependency(%q<rubyXL>, ["~> 1.2.7"])
|
72
|
-
s.add_development_dependency(%q<
|
72
|
+
s.add_development_dependency(%q<nokogiri>, [">= 1.4.4"])
|
73
|
+
s.add_development_dependency(%q<rubyzip>, [">= 0.9.4"])
|
73
74
|
else
|
74
75
|
s.add_dependency(%q<treetop>, ["~> 1.4.10"])
|
75
|
-
s.add_dependency(%q<rspec>, ["~> 2.8.0"])
|
76
|
-
s.add_dependency(%q<yard>, ["~> 0.7"])
|
77
|
-
s.add_dependency(%q<rdoc>, ["~> 3.12"])
|
78
|
-
s.add_dependency(%q<bundler>, ["~> 1.1.3"])
|
79
|
-
s.add_dependency(%q<jeweler>, ["~> 1.8.3"])
|
80
|
-
s.add_dependency(%q<rcov>, [">= 0"])
|
81
76
|
s.add_dependency(%q<rubyXL>, ["~> 1.2.7"])
|
82
|
-
s.add_dependency(%q<
|
77
|
+
s.add_dependency(%q<nokogiri>, [">= 1.4.4"])
|
78
|
+
s.add_dependency(%q<rubyzip>, [">= 0.9.4"])
|
83
79
|
end
|
84
80
|
else
|
85
81
|
s.add_dependency(%q<treetop>, ["~> 1.4.10"])
|
86
|
-
s.add_dependency(%q<rspec>, ["~> 2.8.0"])
|
87
|
-
s.add_dependency(%q<yard>, ["~> 0.7"])
|
88
|
-
s.add_dependency(%q<rdoc>, ["~> 3.12"])
|
89
|
-
s.add_dependency(%q<bundler>, ["~> 1.1.3"])
|
90
|
-
s.add_dependency(%q<jeweler>, ["~> 1.8.3"])
|
91
|
-
s.add_dependency(%q<rcov>, [">= 0"])
|
92
82
|
s.add_dependency(%q<rubyXL>, ["~> 1.2.7"])
|
93
|
-
s.add_dependency(%q<
|
83
|
+
s.add_dependency(%q<nokogiri>, [">= 1.4.4"])
|
84
|
+
s.add_dependency(%q<rubyzip>, [">= 0.9.4"])
|
94
85
|
end
|
95
86
|
end
|
96
87
|
|
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
0.
|
1
|
+
0.2.0
|
data/lib/soroban.rb
CHANGED
data/lib/soroban/cell.rb
CHANGED
@@ -11,9 +11,9 @@ module Soroban
|
|
11
11
|
|
12
12
|
# Cells are initialised with a binding to allow formulas to be executed
|
13
13
|
# within the context of the sheet which ownes the cell.
|
14
|
-
def initialize(
|
14
|
+
def initialize(context)
|
15
15
|
@dependencies = []
|
16
|
-
@binding =
|
16
|
+
@binding = context
|
17
17
|
@touched = false
|
18
18
|
end
|
19
19
|
|
data/lib/soroban/helpers.rb
CHANGED
@@ -37,9 +37,16 @@ module Soroban
|
|
37
37
|
/^([a-zA-Z]+)([\d]+):([a-zA-Z]+)([\d]+)$/.match(range.to_s).to_a[1..-1]
|
38
38
|
end
|
39
39
|
|
40
|
+
# Return the row and column index of the given label.
|
41
|
+
def self.getPos(label)
|
42
|
+
# TODO: fix for labels such as "BC42"
|
43
|
+
match = /^([a-zA-Z]+)([\d]+)$/.match(label.to_s)
|
44
|
+
return match[2].to_i - 1, match[1].upcase[0]-"A"[0]
|
45
|
+
end
|
46
|
+
|
40
47
|
# Return an array of values for the supplied arguments (which may be numbers, labels and ranges).
|
41
|
-
def self.getValues(
|
42
|
-
args.map { |arg| Soroban::range?(arg) ?
|
48
|
+
def self.getValues(context, *args)
|
49
|
+
args.map { |arg| Soroban::range?(arg) ? ValueWalker.new(arg, context).to_a : arg }.to_a.flatten
|
43
50
|
end
|
44
51
|
|
45
52
|
end
|
@@ -0,0 +1 @@
|
|
1
|
+
require 'soroban/import/ruby_xl_importer'
|
@@ -0,0 +1,56 @@
|
|
1
|
+
module Soroban
|
2
|
+
module Import
|
3
|
+
|
4
|
+
# Use the RubyXL gem to load an xlsx file, returning a new Soroban::Sheet
|
5
|
+
# object. Specify the path to the xlsx file, the index of the sheet to be
|
6
|
+
# imported, and a hash of name => label bindings.
|
7
|
+
def self.rubyXL(path, sheet, bindings)
|
8
|
+
require 'rubyXL'
|
9
|
+
require 'soroban/import/ruby_xl_patch'
|
10
|
+
RubyXLImporter.new(path, sheet, bindings).import
|
11
|
+
end
|
12
|
+
|
13
|
+
private
|
14
|
+
|
15
|
+
class RubyXLImporter
|
16
|
+
|
17
|
+
def initialize(path, index, bindings)
|
18
|
+
@path, @index, @bindings = path, index, bindings
|
19
|
+
end
|
20
|
+
|
21
|
+
def import
|
22
|
+
workbook = RubyXL::Parser.parse(@path)
|
23
|
+
@sheet = workbook.worksheets[@index]
|
24
|
+
@model = Soroban::Sheet.new
|
25
|
+
@bindings.values.each do |label_or_range|
|
26
|
+
if Soroban::range?(label_or_range)
|
27
|
+
LabelWalker.new(label_or_range).each do |label|
|
28
|
+
_addCell(label)
|
29
|
+
end
|
30
|
+
else
|
31
|
+
_addCell(label_or_range)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
while label = @model.missing.first
|
35
|
+
_addCell(label)
|
36
|
+
end
|
37
|
+
@model.bind(@bindings)
|
38
|
+
return @model
|
39
|
+
end
|
40
|
+
|
41
|
+
private
|
42
|
+
|
43
|
+
def _addCell(label)
|
44
|
+
row, col = Soroban::getPos(label)
|
45
|
+
cell = @sheet[row][col]
|
46
|
+
data = cell.formula rescue nil
|
47
|
+
data = "=#{data}" unless data.nil?
|
48
|
+
data ||= cell.value.to_s rescue nil
|
49
|
+
puts "#{label} => #{row},#{col} = #{data}"
|
50
|
+
@model.set(label.to_sym => data)
|
51
|
+
end
|
52
|
+
|
53
|
+
end
|
54
|
+
|
55
|
+
end
|
56
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
module Soroban
|
2
|
+
|
3
|
+
# An enumerable that allows cells in a range to be visited.
|
4
|
+
class LabelWalker
|
5
|
+
|
6
|
+
include Enumerable
|
7
|
+
|
8
|
+
# Create a new walker from a supplied range.
|
9
|
+
def initialize(range)
|
10
|
+
@fc, @fr, @tc, @tr = Soroban::getRange(range)
|
11
|
+
end
|
12
|
+
|
13
|
+
# Yield the label of each cell referenced by the supplied range.
|
14
|
+
def each
|
15
|
+
(@fc..@tc).each do |col|
|
16
|
+
(@fr..@tr).each do |row|
|
17
|
+
yield "#{col}#{row}"
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
end
|
23
|
+
|
24
|
+
end
|
data/lib/soroban/sheet.rb
CHANGED
@@ -1,6 +1,7 @@
|
|
1
1
|
require 'soroban/helpers'
|
2
2
|
require 'soroban/functions'
|
3
|
-
require 'soroban/
|
3
|
+
require 'soroban/label_walker'
|
4
|
+
require 'soroban/value_walker'
|
4
5
|
require 'soroban/cell'
|
5
6
|
|
6
7
|
module Soroban
|
@@ -27,51 +28,70 @@ module Soroban
|
|
27
28
|
end
|
28
29
|
|
29
30
|
# Set the contents of one or more cells or ranges.
|
30
|
-
def set(
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
31
|
+
def set(options_hash)
|
32
|
+
options_hash.each do |label_or_range, contents|
|
33
|
+
unless range = Soroban::getRange(label_or_range)
|
34
|
+
return _add(label_or_range, contents)
|
35
|
+
end
|
36
|
+
fc, fr, tc, tr = range
|
37
|
+
if fc == tc || fr == tr
|
38
|
+
raise ArgumentError, "Expecting an array when setting #{label_or_range}" unless contents.kind_of? Array
|
39
|
+
cc, cr = fc, fr
|
40
|
+
contents.each do |item|
|
41
|
+
set("#{cc}#{cr}" => item)
|
42
|
+
cc.next! if fr == tr
|
43
|
+
cr.next! if fc == tc
|
44
|
+
end
|
45
|
+
raise Soroban::RangeError, "Supplied array doesn't match range length" if cc != tc && cr != tr
|
46
|
+
else
|
47
|
+
raise ArgumentError, "Can only set cells or 1-dimensional ranges of cells"
|
42
48
|
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
49
|
end
|
47
50
|
end
|
48
51
|
|
49
52
|
# Retrieve the contents of a cell.
|
50
53
|
def get(label_or_name)
|
51
|
-
|
54
|
+
label = @bindings[label_or_name.to_sym] || label_or_name
|
55
|
+
if Soroban::range?(label)
|
56
|
+
walk(label)
|
57
|
+
else
|
58
|
+
_get(label_or_name, eval("@#{label}", binding))
|
59
|
+
end
|
52
60
|
end
|
53
61
|
|
54
62
|
# Bind one or more named variables to a cell.
|
55
|
-
def bind(
|
56
|
-
|
57
|
-
|
63
|
+
def bind(options_hash)
|
64
|
+
options_hash.each do |name, label_or_range|
|
65
|
+
if Soroban::range?(label_or_range)
|
66
|
+
LabelWalker.new(label_or_range).each do |label|
|
67
|
+
next if @cells.keys.include?(label.to_sym)
|
68
|
+
raise Soroban::UndefinedError, "Cannot bind '#{name}' to range '#{label_or_range}'; cell #{label} is not defined"
|
69
|
+
end
|
70
|
+
_bind_range(name, label_or_range)
|
71
|
+
else
|
72
|
+
unless @cells.keys.include?(label_or_range.to_sym)
|
73
|
+
raise Soroban::UndefinedError, "Cannot bind '#{name}' to non-existent cell '#{label_or_range}'"
|
74
|
+
end
|
75
|
+
_bind(name, label_or_range)
|
76
|
+
end
|
58
77
|
end
|
59
|
-
_bind(name, label)
|
60
78
|
end
|
61
79
|
|
62
80
|
# Visit each cell in the supplied range, yielding its value.
|
63
81
|
def walk(range)
|
64
|
-
|
82
|
+
ValueWalker.new(range, binding)
|
65
83
|
end
|
66
84
|
|
67
85
|
# Return a hash of `label => contents` for each cell in the sheet.
|
68
86
|
def cells
|
69
|
-
|
87
|
+
labels = @cells.keys.map { |label| label.to_sym }
|
88
|
+
contents = labels.map { |label| eval("@#{label}.excel") }
|
89
|
+
Hash[labels.zip(contents)]
|
70
90
|
end
|
71
91
|
|
72
92
|
# Return a list of referenced but undefined cells.
|
73
93
|
def missing
|
74
|
-
@cells.values.
|
94
|
+
@cells.values.flatten.uniq - @cells.keys
|
75
95
|
end
|
76
96
|
|
77
97
|
private
|
@@ -84,14 +104,16 @@ module Soroban
|
|
84
104
|
instance_variable_set(internal, cell)
|
85
105
|
end
|
86
106
|
|
87
|
-
def _set(
|
107
|
+
def _set(label_or_name, cell, contents)
|
108
|
+
label = label_or_name.to_sym
|
109
|
+
name = @bindings[label] || label
|
88
110
|
cell.set(contents)
|
89
|
-
@cells[
|
111
|
+
@cells[name] = cell.dependencies
|
90
112
|
end
|
91
113
|
|
92
114
|
def _get(label_or_name, cell)
|
93
115
|
label = label_or_name.to_sym
|
94
|
-
name = @
|
116
|
+
name = @bindings[label] || label
|
95
117
|
badref = @cells[name] & missing
|
96
118
|
raise Soroban::UndefinedError, "Unmet dependencies #{badref.join(', ')} for #{label}" if badref.length > 0
|
97
119
|
cell.get
|
@@ -103,6 +125,15 @@ module Soroban
|
|
103
125
|
_expose(internal, name)
|
104
126
|
end
|
105
127
|
|
128
|
+
def _bind_range(name, range)
|
129
|
+
@bindings[name.to_sym] = range.to_s
|
130
|
+
instance_eval <<-EOV, __FILE__, __LINE__ + 1
|
131
|
+
def #{name}
|
132
|
+
walk("#{range}")
|
133
|
+
end
|
134
|
+
EOV
|
135
|
+
end
|
136
|
+
|
106
137
|
def _expose(internal, name)
|
107
138
|
instance_eval <<-EOV, __FILE__, __LINE__ + 1
|
108
139
|
def #{name}
|
@@ -0,0 +1,50 @@
|
|
1
|
+
module Soroban
|
2
|
+
|
3
|
+
# An enumerable that allows cells in a range to be visited.
|
4
|
+
class ValueWalker
|
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, context)
|
11
|
+
@range, @binding = range, context
|
12
|
+
@walker = LabelWalker.new(range)
|
13
|
+
end
|
14
|
+
|
15
|
+
# Yield the value of each cell referenced by the supplied range.
|
16
|
+
def each
|
17
|
+
@walker.each { |label| yield eval("get('#{label}')", @binding) }
|
18
|
+
end
|
19
|
+
|
20
|
+
# Retrieve the value of a cell within the range by index
|
21
|
+
def [](index)
|
22
|
+
labels = @walker.to_a
|
23
|
+
if index < 0 || index >= labels.length
|
24
|
+
raise Soroban::RangeError, "Index #{index} falls outside of '#{@range}'"
|
25
|
+
end
|
26
|
+
eval("get('#{labels[index]}')", @binding)
|
27
|
+
end
|
28
|
+
|
29
|
+
# Set the value of a cell within the range by index
|
30
|
+
def []=(index, value)
|
31
|
+
count = 0
|
32
|
+
@walker.each do |label|
|
33
|
+
if index == count
|
34
|
+
eval("@#{label}.set('#{value}')", @binding)
|
35
|
+
return value
|
36
|
+
end
|
37
|
+
count += 1
|
38
|
+
end
|
39
|
+
raise Soroban::RangeError, "Index #{index} falls outside of '#{@range}'"
|
40
|
+
end
|
41
|
+
|
42
|
+
# Display the range if the user outputs the binding directly
|
43
|
+
def to_s
|
44
|
+
@range
|
45
|
+
end
|
46
|
+
alias inspect to_s
|
47
|
+
|
48
|
+
end
|
49
|
+
|
50
|
+
end
|
@@ -0,0 +1,90 @@
|
|
1
|
+
require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
|
2
|
+
|
3
|
+
describe "Documentation" do
|
4
|
+
|
5
|
+
it "has documentation that works as advertised" do
|
6
|
+
|
7
|
+
# Example Usage
|
8
|
+
|
9
|
+
s = Soroban::Sheet.new()
|
10
|
+
|
11
|
+
s.A1 = 2
|
12
|
+
s.set('B1:B5' => [1,2,3,4,5])
|
13
|
+
s.C1 = "=SUM(A1, B1:B5, 5) + A1 ^ 3"
|
14
|
+
s.C2 = "=IF(C1>30,'Large','Tiny')"
|
15
|
+
|
16
|
+
puts s.C1 # => 30
|
17
|
+
s.C1.should eq(30)
|
18
|
+
|
19
|
+
s.bind(:input => :A1, :output => :C2)
|
20
|
+
|
21
|
+
puts s.output # => "Tiny"
|
22
|
+
s.output.should eq('Tiny')
|
23
|
+
|
24
|
+
s.input = 3
|
25
|
+
|
26
|
+
puts s.output # => "Large"
|
27
|
+
s.output.should eq('Large')
|
28
|
+
puts s.C1 # => 50
|
29
|
+
s.C1.should eq(50)
|
30
|
+
|
31
|
+
# Bindings
|
32
|
+
|
33
|
+
s.set(:A1 => 'hello', 'B1:B5' => [1,2,3,4,5])
|
34
|
+
|
35
|
+
s.bind(:foo => :A1, :bar => 'B1:B5')
|
36
|
+
|
37
|
+
puts s.foo # => 'hello'
|
38
|
+
s.foo.should eq('hello')
|
39
|
+
puts s.bar[0] # => 1
|
40
|
+
s.bar[0].should eq(1)
|
41
|
+
|
42
|
+
s.bar[0] = 'howdy'
|
43
|
+
s.bar[0].should eq('howdy')
|
44
|
+
|
45
|
+
puts s.B1 # => 'howdy'
|
46
|
+
s.B1.should eq('howdy')
|
47
|
+
|
48
|
+
# Persistence
|
49
|
+
|
50
|
+
s.F1 = "= E1 + SUM(D1:D5)"
|
51
|
+
|
52
|
+
s.missing # => [:E1, :D1, :D2, :D3, :D4, :D5]
|
53
|
+
expected = [:E1, :D1, :D2, :D3, :D4, :D5]
|
54
|
+
s.missing.should =~ expected
|
55
|
+
|
56
|
+
s.E1 = "= D1 ^ D2"
|
57
|
+
s.set("D1:D5" => [1,2,3,4,5])
|
58
|
+
|
59
|
+
s.missing # => []
|
60
|
+
expected = []
|
61
|
+
s.missing.should =~ expected
|
62
|
+
|
63
|
+
s.cells # => {"F1"=>"= E1 + SUM(D1:D5)", "E1"=>"= D1 ^ D2", "D1"=>"1", "D2"=>"2", "D3"=>"3", "D4"=>"4", "D5"=>"5"}
|
64
|
+
|
65
|
+
# Importers
|
66
|
+
|
67
|
+
# (TBD)
|
68
|
+
|
69
|
+
# Iteration
|
70
|
+
|
71
|
+
s.set('D1:D5' => [1,2,3,4,5])
|
72
|
+
s.walk('D1:D5').reduce(:+) # => 15
|
73
|
+
s.walk('D1:D5').reduce(:+).should eq(15)
|
74
|
+
|
75
|
+
# Functions
|
76
|
+
|
77
|
+
Soroban::functions # => ["MIN", "VLOOKUP", "AND", "MAX", "OR", "NOT", "IF", "AVERAGE", "SUM"]
|
78
|
+
|
79
|
+
Soroban::define :FOO => lambda { |lo, hi|
|
80
|
+
raise ArgumentError if lo > hi
|
81
|
+
rand(hi-lo) + lo
|
82
|
+
}
|
83
|
+
|
84
|
+
s.g = "=FOO(10, 20)"
|
85
|
+
|
86
|
+
puts s.g # => 17
|
87
|
+
|
88
|
+
end
|
89
|
+
|
90
|
+
end
|
data/spec/soroban_spec.rb
CHANGED
@@ -23,7 +23,7 @@ describe "Soroban" do
|
|
23
23
|
end
|
24
24
|
|
25
25
|
it "can set a value" do
|
26
|
-
sheet.set(:foo
|
26
|
+
sheet.set(:foo => 'hello')
|
27
27
|
sheet.foo.should eq('hello')
|
28
28
|
end
|
29
29
|
|
@@ -33,38 +33,48 @@ describe "Soroban" do
|
|
33
33
|
end
|
34
34
|
|
35
35
|
it "can set an array" do
|
36
|
-
sheet.set("A1:A5"
|
37
|
-
sheet.
|
38
|
-
sheet.B1
|
36
|
+
sheet.set("A1:A5" => [ 1, 2, 3, 4, 5 ])
|
37
|
+
sheet.set("B2" => 5)
|
38
|
+
sheet.B1 = '=SUM(10, A1:A5, B2)'
|
39
|
+
sheet.B1.should eq(30)
|
39
40
|
end
|
40
41
|
|
41
42
|
it "can set a hash" do
|
42
|
-
sheet.set("A1:A3"
|
43
|
-
sheet.set("B1:B3", [ 'mop', 'hai', 'bah' ])
|
43
|
+
sheet.set("A1:A3" => [ 'one', 'two', 'three' ], "B1:B3" => [ 'mop', 'hai', 'bah' ])
|
44
44
|
sheet.C1 = '=VLOOKUP("two", A1:B3, 2, 0)'
|
45
45
|
sheet.C1.should eq('hai')
|
46
46
|
end
|
47
47
|
|
48
48
|
it "can iterate over all cells" do
|
49
|
-
sheet.set("A1:A3"
|
50
|
-
sheet.set("B1:B3", [ 4, 5, 6 ])
|
51
|
-
sheet.set("C1:C3", [ 7, 8, 9 ])
|
49
|
+
sheet.set("A1:A3" => [ 1, 2, 3 ], "B1:B3" => [ 4, 5, 6 ], "C1:C3" => [ 7, 8, 9 ])
|
52
50
|
sheet.cells.map { |label, contents| contents.to_i }.sort.should eq [1,2,3,4,5,6,7,8,9]
|
53
51
|
end
|
54
52
|
|
55
53
|
it "can bind variables to cells" do
|
56
54
|
sheet.A1 = 0
|
57
55
|
sheet.A2 = "=A1^2"
|
58
|
-
sheet.bind(:input, :
|
59
|
-
sheet.bind(:output, :A2)
|
56
|
+
sheet.bind(:input => :A1, :output => :A2)
|
60
57
|
sheet.input = 5
|
61
58
|
sheet.output.should eq(25)
|
59
|
+
sheet.get(:input).should eq(5)
|
62
60
|
sheet.bindings.keys.should include :input
|
63
61
|
sheet.bindings.keys.should include :output
|
64
62
|
sheet.bindings.values.should include :A1
|
65
63
|
sheet.bindings.values.should include :A2
|
66
64
|
end
|
67
65
|
|
66
|
+
it "can bind variables to ranges" do
|
67
|
+
sheet.set("X1:X5" => [1,2,3,4,5], "Z1:Z5" => [6,7,8,9,0])
|
68
|
+
sheet.bind(:foo => "X1:X5", :bar => "Z1:Z5")
|
69
|
+
sheet.foo[0].should eq(1)
|
70
|
+
sheet.foo[4].should eq(5)
|
71
|
+
sheet.bar[0].should eq(6)
|
72
|
+
sheet.bar[3].should eq(9)
|
73
|
+
sheet.bar[4].should eq(0)
|
74
|
+
sheet.bar[2] = 'foo'
|
75
|
+
sheet.Z3.should eq('foo')
|
76
|
+
end
|
77
|
+
|
68
78
|
it "can define new functions" do
|
69
79
|
Soroban::define :FOO => lambda { |a, b| 2 * a + b / 2 }
|
70
80
|
sheet.A1 = 7
|
@@ -90,7 +100,7 @@ describe "Soroban" do
|
|
90
100
|
|
91
101
|
it "can reject valid ruby code in formulas" do
|
92
102
|
lambda {
|
93
|
-
sheet.set(:A1
|
103
|
+
sheet.set(:A1 => "=3**2")
|
94
104
|
}.should raise_error(Soroban::ParseError)
|
95
105
|
end
|
96
106
|
|
metadata
CHANGED
@@ -1,13 +1,13 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: soroban
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
hash:
|
4
|
+
hash: 23
|
5
5
|
prerelease:
|
6
6
|
segments:
|
7
7
|
- 0
|
8
|
-
-
|
9
|
-
-
|
10
|
-
version: 0.
|
8
|
+
- 2
|
9
|
+
- 0
|
10
|
+
version: 0.2.0
|
11
11
|
platform: ruby
|
12
12
|
authors:
|
13
13
|
- Jason Hutchens
|
@@ -15,7 +15,7 @@ autorequire:
|
|
15
15
|
bindir: bin
|
16
16
|
cert_chain: []
|
17
17
|
|
18
|
-
date: 2012-04-
|
18
|
+
date: 2012-04-24 00:00:00 Z
|
19
19
|
dependencies:
|
20
20
|
- !ruby/object:Gem::Dependency
|
21
21
|
type: :runtime
|
@@ -41,120 +41,46 @@ dependencies:
|
|
41
41
|
requirements:
|
42
42
|
- - ~>
|
43
43
|
- !ruby/object:Gem::Version
|
44
|
-
hash:
|
44
|
+
hash: 17
|
45
45
|
segments:
|
46
|
+
- 1
|
46
47
|
- 2
|
47
|
-
-
|
48
|
-
|
49
|
-
version: 2.8.0
|
48
|
+
- 7
|
49
|
+
version: 1.2.7
|
50
50
|
version_requirements: *id002
|
51
|
-
name:
|
51
|
+
name: rubyXL
|
52
52
|
- !ruby/object:Gem::Dependency
|
53
53
|
type: :development
|
54
54
|
prerelease: false
|
55
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
56
|
none: false
|
119
57
|
requirements:
|
120
58
|
- - ">="
|
121
59
|
- !ruby/object:Gem::Version
|
122
|
-
hash:
|
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
|
60
|
+
hash: 15
|
137
61
|
segments:
|
138
62
|
- 1
|
139
|
-
-
|
140
|
-
-
|
141
|
-
version: 1.
|
142
|
-
version_requirements: *
|
143
|
-
name:
|
63
|
+
- 4
|
64
|
+
- 4
|
65
|
+
version: 1.4.4
|
66
|
+
version_requirements: *id003
|
67
|
+
name: nokogiri
|
144
68
|
- !ruby/object:Gem::Dependency
|
145
69
|
type: :development
|
146
70
|
prerelease: false
|
147
|
-
requirement: &
|
71
|
+
requirement: &id004 !ruby/object:Gem::Requirement
|
148
72
|
none: false
|
149
73
|
requirements:
|
150
74
|
- - ">="
|
151
75
|
- !ruby/object:Gem::Version
|
152
|
-
hash:
|
76
|
+
hash: 51
|
153
77
|
segments:
|
154
78
|
- 0
|
155
|
-
|
156
|
-
|
157
|
-
|
79
|
+
- 9
|
80
|
+
- 4
|
81
|
+
version: 0.9.4
|
82
|
+
version_requirements: *id004
|
83
|
+
name: rubyzip
|
158
84
|
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
85
|
email: jason.hutchens@agworld.com.au
|
160
86
|
executables: []
|
@@ -167,6 +93,7 @@ extra_rdoc_files:
|
|
167
93
|
files:
|
168
94
|
- .document
|
169
95
|
- .rspec
|
96
|
+
- .travis.yml
|
170
97
|
- .yardopts
|
171
98
|
- Gemfile
|
172
99
|
- Gemfile.lock
|
@@ -189,13 +116,18 @@ files:
|
|
189
116
|
- lib/soroban/functions/sum.rb
|
190
117
|
- lib/soroban/functions/vlookup.rb
|
191
118
|
- lib/soroban/helpers.rb
|
119
|
+
- lib/soroban/import.rb
|
120
|
+
- lib/soroban/import/ruby_xl_importer.rb
|
121
|
+
- lib/soroban/import/ruby_xl_patch.rb
|
122
|
+
- lib/soroban/label_walker.rb
|
192
123
|
- lib/soroban/parser.rb
|
193
124
|
- lib/soroban/parser/grammar.rb
|
194
125
|
- lib/soroban/parser/grammar.treetop
|
195
126
|
- lib/soroban/parser/nodes.rb
|
196
127
|
- lib/soroban/parser/rewrite.rb
|
197
128
|
- lib/soroban/sheet.rb
|
198
|
-
- lib/soroban/
|
129
|
+
- lib/soroban/value_walker.rb
|
130
|
+
- spec/documentation_spec.rb
|
199
131
|
- spec/soroban_spec.rb
|
200
132
|
- spec/spec_helper.rb
|
201
133
|
homepage: https://github.com/agworld/soroban
|
data/lib/soroban/walker.rb
DELETED
@@ -1,26 +0,0 @@
|
|
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
|