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 ADDED
@@ -0,0 +1,13 @@
1
+ language: ruby
2
+ bundler_args: --without development document
3
+ notifications:
4
+ email: false
5
+ rvm:
6
+ - ree
7
+ - 1.8.7
8
+ - 1.9.2
9
+ - 1.9.3
10
+ - jruby-18mode
11
+ - jruby-19mode
12
+ - rbx-18mode
13
+ - rbx-19mode
data/.yardopts CHANGED
@@ -1,6 +1,7 @@
1
1
  --protected
2
2
  --no-private
3
3
  --exclude lib/soroban/parser/
4
+ --exclude lib/soroban/import/
4
5
  -
5
6
  README.md
6
7
  LICENSE.txt
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 "rspec", "~> 2.8.0"
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.8.0)
19
- rspec-core (~> 2.8.0)
20
- rspec-expectations (~> 2.8.0)
21
- rspec-mocks (~> 2.8.0)
22
- rspec-core (2.8.0)
23
- rspec-expectations (2.8.0)
24
- diff-lcs (~> 1.1.2)
25
- rspec-mocks (2.8.0)
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
- rcov
38
+ nokogiri (>= 1.4.4)
39
+ rake
39
40
  rdoc (~> 3.12)
40
41
  redcarpet
41
- rspec (~> 2.8.0)
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
- > sudo gem install soroban
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', [1,2,3,4,5])
23
- s.C1 = "=SUM(B1:B5) + A1 ^ 3"
24
- s.C2 = "=IF(C1>25,'Large','Tiny')"
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 # => 23
28
+ puts s.C1 # => 30
27
29
 
28
- s.bind(:input, :A1)
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 # => 42
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", [1,2,3,4,5])
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
- This means parsing a file can be done as follows.
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', [1,2,3,4,5])
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
@@ -33,7 +33,7 @@ end
33
33
 
34
34
  RSpec::Core::RakeTask.new(:rcov) do |spec|
35
35
  spec.pattern = 'spec/**/*_spec.rb'
36
- spec.rcov = true
36
+ spec.rcov = false
37
37
  end
38
38
 
39
39
  task :default => :spec
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.1.1"
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-23"
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/walker.rb",
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<redcarpet>, [">= 0"])
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<redcarpet>, [">= 0"])
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<redcarpet>, [">= 0"])
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.1
1
+ 0.2.0
data/lib/soroban.rb CHANGED
@@ -1,3 +1,4 @@
1
1
  require 'soroban/sheet'
2
2
  require 'soroban/cell'
3
3
  require 'soroban/error'
4
+ require 'soroban/import'
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(binding)
14
+ def initialize(context)
15
15
  @dependencies = []
16
- @binding = binding
16
+ @binding = context
17
17
  @touched = false
18
18
  end
19
19
 
@@ -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(binding, *args)
42
- args.map { |arg| Soroban::range?(arg) ? Walker.new(arg, binding).map : arg }.flatten
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,7 @@
1
+ module RubyXL
2
+ class Cell < PrivateClass
3
+ def is_date?
4
+ return false
5
+ end
6
+ end
7
+ 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/walker'
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(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
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
- _get(label_or_name, eval("@#{label_or_name}", binding))
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(name, label)
56
- unless @cells.keys.include?(label.to_sym)
57
- raise Soroban::UndefinedError, "Cannot bind '#{name}' to non-existent cell '#{label}'"
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
- Walker.new(range, binding)
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
- Hash[@cells.keys.map { |label| label.to_s }.zip( @cells.keys.map { |label| eval("@#{label}.excel") } )]
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.map.flatten.uniq - @cells.keys
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(label, cell, contents)
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[label.to_sym] = cell.dependencies
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 = @cells[label] ? label : @bindings[label]
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, 'hello')
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", [ 1, 2, 3, 4, 5 ])
37
- sheet.B1 = '=SUM(A1:A5)'
38
- sheet.B1.should eq(15)
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", [ 'one', 'two', 'three' ])
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", [ 1, 2, 3 ])
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, :A1)
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, "=3**2")
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: 25
4
+ hash: 23
5
5
  prerelease:
6
6
  segments:
7
7
  - 0
8
- - 1
9
- - 1
10
- version: 0.1.1
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-23 00:00:00 Z
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: 47
44
+ hash: 17
45
45
  segments:
46
+ - 1
46
47
  - 2
47
- - 8
48
- - 0
49
- version: 2.8.0
48
+ - 7
49
+ version: 1.2.7
50
50
  version_requirements: *id002
51
- name: rspec
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: 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
60
+ hash: 15
137
61
  segments:
138
62
  - 1
139
- - 2
140
- - 7
141
- version: 1.2.7
142
- version_requirements: *id008
143
- name: rubyXL
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: &id009 !ruby/object:Gem::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: 3
76
+ hash: 51
153
77
  segments:
154
78
  - 0
155
- version: "0"
156
- version_requirements: *id009
157
- name: redcarpet
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/walker.rb
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
@@ -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