soroban 0.5.4 → 0.7.2

Sign up to get free protection for your applications and to get access to all the features.
data/Gemfile CHANGED
@@ -6,6 +6,7 @@ group :development do
6
6
  gem "rubyXL", "~> 1.2.7"
7
7
  gem "nokogiri", ">= 1.4.4"
8
8
  gem "rubyzip", ">= 0.9.4"
9
+ gem "awesome_print"
9
10
  end
10
11
 
11
12
  group :test do
data/Gemfile.lock CHANGED
@@ -1,6 +1,7 @@
1
1
  GEM
2
2
  remote: http://rubygems.org/
3
3
  specs:
4
+ awesome_print (1.1.0)
4
5
  diff-lcs (1.1.3)
5
6
  git (1.2.5)
6
7
  jeweler (1.8.4)
@@ -8,11 +9,11 @@ GEM
8
9
  git (>= 1.2.5)
9
10
  rake
10
11
  rdoc
11
- json (1.7.6)
12
+ json (1.7.7)
12
13
  nokogiri (1.5.6)
13
14
  polyglot (0.3.3)
14
15
  rake (10.0.3)
15
- rdoc (3.12)
16
+ rdoc (3.12.2)
16
17
  json (~> 1.4)
17
18
  redcarpet (2.2.2)
18
19
  rspec (2.12.0)
@@ -22,18 +23,19 @@ GEM
22
23
  rspec-core (2.12.2)
23
24
  rspec-expectations (2.12.1)
24
25
  diff-lcs (~> 1.1.3)
25
- rspec-mocks (2.12.1)
26
+ rspec-mocks (2.12.2)
26
27
  rubyXL (1.2.10)
27
28
  rubyzip (0.9.9)
28
29
  treetop (1.4.12)
29
30
  polyglot
30
31
  polyglot (>= 0.3.1)
31
- yard (0.8.3)
32
+ yard (0.8.5.2)
32
33
 
33
34
  PLATFORMS
34
35
  ruby
35
36
 
36
37
  DEPENDENCIES
38
+ awesome_print
37
39
  jeweler (~> 1.8.3)
38
40
  nokogiri (>= 1.4.4)
39
41
  rake
data/README.md CHANGED
@@ -3,8 +3,9 @@ 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)
6
+ [![Code Climate](https://codeclimate.com/github/agworld/soroban.png)](https://codeclimate.com/github/agworld/soroban)
7
7
  [![Dependency Status](https://gemnasium.com/agworld/soroban.png)](https://gemnasium.com/agworld/soroban)
8
+ [![Build Status](https://secure.travis-ci.org/agworld/soroban.png)](http://travis-ci.org/#!/agworld/soroban)
8
9
 
9
10
 
10
11
  Getting Started
@@ -146,6 +147,13 @@ s.g = "=FOO(10, 20)"
146
147
  puts s.g # => 17
147
148
  ```
148
149
 
150
+ Compilation
151
+ -----------
152
+
153
+ Rather than interact with a `Soroban::Sheet` object at runtime, you can compile
154
+ the sheet into a Ruby or JavaScript class which you can then either save out to
155
+ a file or evaluate directly. This is slightly less flexible, but more efficient.
156
+
149
157
  Contributing to Soroban
150
158
  -----------------------
151
159
 
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.5.4"
8
+ s.version = "0.7.2"
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 = "2013-01-18"
12
+ s.date = "2013-09-03"
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 = [
@@ -55,6 +55,7 @@ Gem::Specification.new do |s|
55
55
  "lib/soroban/parser/nodes.rb",
56
56
  "lib/soroban/parser/rewrite.rb",
57
57
  "lib/soroban/sheet.rb",
58
+ "lib/soroban/tabulator.rb",
58
59
  "lib/soroban/value_walker.rb",
59
60
  "spec/documentation_spec.rb",
60
61
  "spec/import_spec.rb",
@@ -64,7 +65,7 @@ Gem::Specification.new do |s|
64
65
  s.homepage = "https://github.com/agworld/soroban"
65
66
  s.licenses = ["MIT"]
66
67
  s.require_paths = ["lib"]
67
- s.rubygems_version = "1.8.24"
68
+ s.rubygems_version = "1.8.25"
68
69
  s.summary = "Soroban is a calculating engine that understands Excel formulas."
69
70
 
70
71
  if s.respond_to? :specification_version then
@@ -75,17 +76,20 @@ Gem::Specification.new do |s|
75
76
  s.add_development_dependency(%q<rubyXL>, ["~> 1.2.7"])
76
77
  s.add_development_dependency(%q<nokogiri>, [">= 1.4.4"])
77
78
  s.add_development_dependency(%q<rubyzip>, [">= 0.9.4"])
79
+ s.add_development_dependency(%q<awesome_print>, [">= 0"])
78
80
  else
79
81
  s.add_dependency(%q<treetop>, ["~> 1.4.10"])
80
82
  s.add_dependency(%q<rubyXL>, ["~> 1.2.7"])
81
83
  s.add_dependency(%q<nokogiri>, [">= 1.4.4"])
82
84
  s.add_dependency(%q<rubyzip>, [">= 0.9.4"])
85
+ s.add_dependency(%q<awesome_print>, [">= 0"])
83
86
  end
84
87
  else
85
88
  s.add_dependency(%q<treetop>, ["~> 1.4.10"])
86
89
  s.add_dependency(%q<rubyXL>, ["~> 1.2.7"])
87
90
  s.add_dependency(%q<nokogiri>, [">= 1.4.4"])
88
91
  s.add_dependency(%q<rubyzip>, [">= 0.9.4"])
92
+ s.add_dependency(%q<awesome_print>, [">= 0"])
89
93
  end
90
94
  end
91
95
 
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.5.4
1
+ 0.7.2
data/lib/soroban/cell.rb CHANGED
@@ -7,7 +7,7 @@ module Soroban
7
7
  # representation of its contents, and the executable Ruby version of same, as
8
8
  # generated via a rewrite grammar. Cells also store their dependencies.
9
9
  class Cell
10
- attr_reader :excel, :ruby, :dependencies
10
+ attr_reader :excel, :javascript, :dependencies
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 owns the cell.
@@ -18,12 +18,19 @@ module Soroban
18
18
  @value = nil
19
19
  end
20
20
 
21
+ def to_compiled_ruby
22
+ @tree.to_compiled_ruby
23
+ end
24
+
21
25
  # Set the contents of a cell, and store the executable Ruby version.
22
26
  def set(contents)
23
27
  contents = contents.to_s
24
28
  contents = "'#{contents}'" if Soroban::unknown?(contents)
25
29
  clear
26
- @excel, @ruby = contents, _convert(contents)
30
+ @excel = contents
31
+ @tree = Soroban::parser.parse(@excel)
32
+ raise Soroban::ParseError, Soroban::parser.failure_reason if @tree.nil?
33
+ @ruby = _to_ruby
27
34
  end
28
35
 
29
36
  # Clear the cached value of a cell to force it to be recalculated
@@ -45,10 +52,12 @@ module Soroban
45
52
 
46
53
  private
47
54
 
48
- def _convert(contents)
49
- tree = Soroban::parser.parse(contents)
50
- raise Soroban::ParseError, Soroban::parser.failure_reason if tree.nil?
51
- tree.convert(@dependencies.clear)
55
+ def _to_ruby
56
+ @tree.to_ruby(@dependencies.clear)
57
+ end
58
+
59
+ def _to_javascript
60
+ @tree.to_javascript(@dependencies.clear)
52
61
  end
53
62
 
54
63
  end
@@ -1,67 +1,103 @@
1
1
  module Soroban
2
2
 
3
3
  class Formula < Treetop::Runtime::SyntaxNode
4
- def rewrite(value)
4
+ def rewrite_ruby(value)
5
5
  value.gsub(/^= */, '')
6
6
  end
7
+ alias :compile_ruby :rewrite_ruby
7
8
  end
8
9
 
9
10
  class Identifier < Treetop::Runtime::SyntaxNode
10
- def rewrite(value)
11
+ def rewrite_ruby(value)
11
12
  "@#{value}.get"
12
13
  end
14
+ def compile_ruby(value)
15
+ "@cells[:#{value}].call"
16
+ end
13
17
  def extract(value)
14
18
  value.to_sym
15
19
  end
16
20
  end
17
21
 
18
22
  class IntegerValue < Treetop::Runtime::SyntaxNode
19
- def rewrite(value)
23
+ def rewrite_ruby(value)
20
24
  "#{value.to_f}"
21
25
  end
26
+ alias :compile_ruby :rewrite_ruby
22
27
  end
23
28
 
24
29
  class FloatValue < Treetop::Runtime::SyntaxNode
25
- def rewrite(value)
30
+ def rewrite_ruby(value)
26
31
  "#{value.to_f}"
27
32
  end
33
+ alias :compile_ruby :rewrite_ruby
28
34
  end
29
35
 
30
36
  class Function < Treetop::Runtime::SyntaxNode
31
- def rewrite(value)
37
+ def rewrite_ruby(value)
32
38
  match = /^([^(]*)(.*)$/.match(value)
33
39
  "func_#{match[1].downcase}#{match[2]}"
34
40
  end
41
+ def compile_ruby(value)
42
+ match = /^([A-Z]+)\((.*)\)$/.match(value)
43
+ name, args = match[1], match[2].split(',')
44
+ case name
45
+ when 'VLOOKUP'
46
+ find, table, column, _ = args
47
+ table = table[1...-1]
48
+ column = column.to_i
49
+ table_key = "'#{table}_#{column}'"
50
+ code = []
51
+ code << "begin"
52
+ code << " @cache[#{table_key}] ||= {"
53
+ cols = Tabulator.new(table).get
54
+ lookup = Hash[cols[0].zip(cols[column-1])]
55
+ code << lookup.map do |key, val|
56
+ " @cells[:#{key}].call => @cells[:#{val}].call"
57
+ end.join(",\n")
58
+ code << " }"
59
+ code << " @cache[#{table_key}][#{find}] || 0.0"
60
+ code << " end"
61
+ code.join("\n")
62
+ else
63
+ value
64
+ end
65
+ end
35
66
  end
36
67
 
37
68
  class Pow < Treetop::Runtime::SyntaxNode
38
- def rewrite(value)
69
+ def rewrite_ruby(value)
39
70
  "**"
40
71
  end
72
+ alias :compile_ruby :rewrite_ruby
41
73
  end
42
74
 
43
75
  class Equal < Treetop::Runtime::SyntaxNode
44
- def rewrite(value)
76
+ def rewrite_ruby(value)
45
77
  "=="
46
78
  end
79
+ alias :compile_ruby :rewrite_ruby
47
80
  end
48
81
 
49
82
  class NotEqual < Treetop::Runtime::SyntaxNode
50
- def rewrite(value)
83
+ def rewrite_ruby(value)
51
84
  "!="
52
85
  end
86
+ alias :compile_ruby :rewrite_ruby
53
87
  end
54
88
 
55
89
  class Label < Treetop::Runtime::SyntaxNode
56
- def rewrite(value)
90
+ def rewrite_ruby(value)
57
91
  value.gsub('$', '')
58
92
  end
93
+ alias :compile_ruby :rewrite_ruby
59
94
  end
60
95
 
61
96
  class Range < Treetop::Runtime::SyntaxNode
62
- def rewrite(value)
97
+ def rewrite_ruby(value)
63
98
  "'#{value}'"
64
99
  end
100
+ alias :compile_ruby :rewrite_ruby
65
101
  def extract(value)
66
102
  LabelWalker.new(value).map { |label| "#{label}".to_sym }
67
103
  end
@@ -2,19 +2,49 @@ module Treetop
2
2
  module Runtime
3
3
  class SyntaxNode
4
4
 
5
- def convert(dependencies)
5
+ def to_ruby(dependencies)
6
6
  if nonterminal?
7
7
  value = ""
8
- elements.each { |element| value << element.convert(dependencies) }
8
+ elements.each { |element| value << element.to_ruby(dependencies) }
9
9
  _add_dependency(dependencies, extract(value))
10
- rewrite(value)
10
+ rewrite_ruby(value)
11
11
  else
12
12
  _add_dependency(dependencies, extract(text_value))
13
- rewrite(text_value)
13
+ rewrite_ruby(text_value)
14
14
  end
15
15
  end
16
16
 
17
- def rewrite(value)
17
+ def to_compiled_ruby
18
+ if nonterminal?
19
+ value = ""
20
+ elements.each { |element| value << element.to_compiled_ruby }
21
+ compile_ruby(value)
22
+ else
23
+ compile_ruby(text_value)
24
+ end
25
+ end
26
+
27
+ def to_javascript(dependencies)
28
+ if nonterminal?
29
+ value = ""
30
+ elements.each { |element| value << element.to_javascript(dependencies) }
31
+ _add_dependency(dependencies, extract(value))
32
+ rewrite_javascript(value)
33
+ else
34
+ _add_dependency(dependencies, extract(text_value))
35
+ rewrite_javascript(text_value)
36
+ end
37
+ end
38
+
39
+ def compile_ruby(value)
40
+ value
41
+ end
42
+
43
+ def rewrite_ruby(value)
44
+ value
45
+ end
46
+
47
+ def rewrite_javascript(value)
18
48
  value
19
49
  end
20
50
 
data/lib/soroban/sheet.rb CHANGED
@@ -2,8 +2,11 @@ require 'soroban/helpers'
2
2
  require 'soroban/functions'
3
3
  require 'soroban/label_walker'
4
4
  require 'soroban/value_walker'
5
+ require 'soroban/tabulator'
5
6
  require 'soroban/cell'
6
7
 
8
+ require 'set'
9
+
7
10
  module Soroban
8
11
 
9
12
  # A container for cells.
@@ -14,10 +17,62 @@ module Soroban
14
17
  def initialize(logger=nil)
15
18
  @logger = logger
16
19
  @cells = {}
20
+ @compiled = {}
17
21
  @changes = Hash.new{ |h, k| h[k] = Set.new }
18
22
  @bindings = {}
19
23
  end
20
24
 
25
+ def factory(name)
26
+ eval(self.to_ruby(name), TOPLEVEL_BINDING)
27
+ Object::const_get('Soroban').const_get('Model').const_get(name).new
28
+ end
29
+
30
+ # Return a string containing a ruby class that implements the sheet. You can
31
+ # call eval() on this string to create the class, which you can then
32
+ # instantiate. Set inputs on the instance and read outputs off.
33
+ def to_ruby(class_name)
34
+ data = []
35
+ data << "module Soroban"
36
+ data << "module Model"
37
+ data << "class #{class_name}"
38
+ data << " def initialize"
39
+ data << " @binds = {"
40
+ data << bindings.map do |name, cell|
41
+ " '#{name}' => :#{cell}"
42
+ end.join(",\n")
43
+ data << " }"
44
+ data << " @cache = {}"
45
+ data << " @cells = {"
46
+ data << @compiled.map do |label, cell|
47
+ " :#{label} => lambda { @cache[:#{label}] ||= #{cell.to_compiled_ruby} }"
48
+ end.join(",\n")
49
+ data << " }"
50
+ data << " end"
51
+ data << " def clear"
52
+ data << " @cache.clear"
53
+ data << " end"
54
+ data << " def get(name)"
55
+ data << " @cells[@binds[name]].call"
56
+ data << " end"
57
+ data << " def set(name, value)"
58
+ data << " self.clear"
59
+ data << " @cells[@binds[name]] = lambda { @cache[@binds[name]] ||= value }"
60
+ data << " end"
61
+ bindings.each do |name, cell|
62
+ data << " def #{name}"
63
+ data << " get('#{name}')"
64
+ data << " end"
65
+ data << " def #{name}=(value)"
66
+ data << " set('#{name}', value)"
67
+ data << " end"
68
+ end
69
+ data << "end"
70
+ data << "end"
71
+ data << "end"
72
+ puts data.join("\n")
73
+ data.join("\n")
74
+ end
75
+
21
76
  # Used for calling dynamically defined functions, and for creating new
22
77
  # cells (via `label=`).
23
78
  def method_missing(method, *args, &block)
@@ -125,6 +180,7 @@ module Soroban
125
180
  internal = "@#{label}"
126
181
  _expose(internal, label)
127
182
  cell = Cell.new(binding)
183
+ @compiled[label] = cell
128
184
  _set(label, cell, contents)
129
185
  instance_variable_set(internal, cell)
130
186
  end
@@ -0,0 +1,34 @@
1
+ module Soroban
2
+
3
+ # An enumerable that splits a range of cells into an nxm array
4
+ class Tabulator
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
+ def get
14
+ row = []
15
+ cols = [row]
16
+ col_ref, row_ref = @fc, @fr
17
+ while true do
18
+ row << "#{col_ref}#{row_ref}".to_sym
19
+ break if row_ref == @tr && col_ref == @tc
20
+ if row_ref == @tr
21
+ row = []
22
+ cols << row
23
+ row_ref = @fr
24
+ col_ref = col_ref.next
25
+ else
26
+ row_ref = row_ref.next
27
+ end
28
+ end
29
+ cols
30
+ end
31
+
32
+ end
33
+
34
+ end
data/spec/import_spec.rb CHANGED
@@ -28,6 +28,47 @@ describe "Documentation", :if => has_rubyxl do
28
28
  puts s.force # => 710.044826106394
29
29
  s.force.should be_within(0.01).of(710.04)
30
30
 
31
+ require 'benchmark'
32
+
33
+ i_time = Benchmark.realtime do
34
+ 1000.times do
35
+ s.planet = 'Earth'
36
+ s.mass = 80
37
+ s.force
38
+ s.planet = 'Venus'
39
+ s.mass = 80
40
+ s.force
41
+ end
42
+ end
43
+
44
+ puts "Interpreted Time: #{i_time}"
45
+
46
+ eval(s.to_ruby("Test"))
47
+ model = Soroban::Model::Test.new
48
+
49
+ model.planet = 'Earth'
50
+ model.mass = 80
51
+ model.force.should be_within(0.01).of(783.46)
52
+
53
+ model.planet = 'Venus'
54
+ model.mass = 80
55
+ model.force.should be_within(0.01).of(710.04)
56
+
57
+ c_time = Benchmark.realtime do
58
+ 1000.times do
59
+ model.planet = 'Earth'
60
+ model.mass = 80
61
+ model.force
62
+ model.planet = 'Venus'
63
+ model.mass = 80
64
+ model.force
65
+ end
66
+ end
67
+
68
+ puts "Compiled Time: #{c_time}"
69
+
70
+ (10.0 * c_time).should be < i_time
71
+
31
72
  end
32
73
 
33
74
  end
data/spec/soroban_spec.rb CHANGED
@@ -61,6 +61,12 @@ describe "Soroban" do
61
61
  sheet.bindings.keys.should include :output
62
62
  sheet.bindings.values.should include :A1
63
63
  sheet.bindings.values.should include :A2
64
+
65
+ model = sheet.factory('Test')
66
+ model.input = 5
67
+ model.output.should eq(25)
68
+ model.input = 4
69
+ model.output.should eq(16)
64
70
  end
65
71
 
66
72
  it "can bind variables to ranges" do
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: 3
4
+ hash: 7
5
5
  prerelease:
6
6
  segments:
7
7
  - 0
8
- - 5
9
- - 4
10
- version: 0.5.4
8
+ - 7
9
+ - 2
10
+ version: 0.7.2
11
11
  platform: ruby
12
12
  authors:
13
13
  - Jason Hutchens
@@ -15,11 +15,10 @@ autorequire:
15
15
  bindir: bin
16
16
  cert_chain: []
17
17
 
18
- date: 2013-01-18 00:00:00 Z
18
+ date: 2013-09-03 00:00:00 Z
19
19
  dependencies:
20
20
  - !ruby/object:Gem::Dependency
21
- name: treetop
22
- version_requirements: &id001 !ruby/object:Gem::Requirement
21
+ requirement: &id001 !ruby/object:Gem::Requirement
23
22
  none: false
24
23
  requirements:
25
24
  - - ~>
@@ -31,11 +30,11 @@ dependencies:
31
30
  - 10
32
31
  version: 1.4.10
33
32
  type: :runtime
34
- requirement: *id001
33
+ version_requirements: *id001
34
+ name: treetop
35
35
  prerelease: false
36
36
  - !ruby/object:Gem::Dependency
37
- name: rubyXL
38
- version_requirements: &id002 !ruby/object:Gem::Requirement
37
+ requirement: &id002 !ruby/object:Gem::Requirement
39
38
  none: false
40
39
  requirements:
41
40
  - - ~>
@@ -47,11 +46,11 @@ dependencies:
47
46
  - 7
48
47
  version: 1.2.7
49
48
  type: :development
50
- requirement: *id002
49
+ version_requirements: *id002
50
+ name: rubyXL
51
51
  prerelease: false
52
52
  - !ruby/object:Gem::Dependency
53
- name: nokogiri
54
- version_requirements: &id003 !ruby/object:Gem::Requirement
53
+ requirement: &id003 !ruby/object:Gem::Requirement
55
54
  none: false
56
55
  requirements:
57
56
  - - ">="
@@ -63,11 +62,11 @@ dependencies:
63
62
  - 4
64
63
  version: 1.4.4
65
64
  type: :development
66
- requirement: *id003
65
+ version_requirements: *id003
66
+ name: nokogiri
67
67
  prerelease: false
68
68
  - !ruby/object:Gem::Dependency
69
- name: rubyzip
70
- version_requirements: &id004 !ruby/object:Gem::Requirement
69
+ requirement: &id004 !ruby/object:Gem::Requirement
71
70
  none: false
72
71
  requirements:
73
72
  - - ">="
@@ -79,7 +78,22 @@ dependencies:
79
78
  - 4
80
79
  version: 0.9.4
81
80
  type: :development
82
- requirement: *id004
81
+ version_requirements: *id004
82
+ name: rubyzip
83
+ prerelease: false
84
+ - !ruby/object:Gem::Dependency
85
+ requirement: &id005 !ruby/object:Gem::Requirement
86
+ none: false
87
+ requirements:
88
+ - - ">="
89
+ - !ruby/object:Gem::Version
90
+ hash: 3
91
+ segments:
92
+ - 0
93
+ version: "0"
94
+ type: :development
95
+ version_requirements: *id005
96
+ name: awesome_print
83
97
  prerelease: false
84
98
  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.
85
99
  email: jason.hutchens@agworld.com.au
@@ -129,6 +143,7 @@ files:
129
143
  - lib/soroban/parser/nodes.rb
130
144
  - lib/soroban/parser/rewrite.rb
131
145
  - lib/soroban/sheet.rb
146
+ - lib/soroban/tabulator.rb
132
147
  - lib/soroban/value_walker.rb
133
148
  - spec/documentation_spec.rb
134
149
  - spec/import_spec.rb
@@ -163,7 +178,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
163
178
  requirements: []
164
179
 
165
180
  rubyforge_project:
166
- rubygems_version: 1.8.24
181
+ rubygems_version: 1.8.25
167
182
  signing_key:
168
183
  specification_version: 3
169
184
  summary: Soroban is a calculating engine that understands Excel formulas.