soroban 0.7.3 → 0.8.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.travis.yml +4 -3
- data/README.md +4 -4
- data/Soroban.gemspec +3 -3
- data/VERSION +1 -1
- data/lib/soroban.rb +1 -2
- data/lib/soroban/cell.rb +47 -18
- data/lib/soroban/errors.rb +20 -0
- data/lib/soroban/functions.rb +31 -21
- data/lib/soroban/functions/and.rb +4 -2
- data/lib/soroban/functions/average.rb +2 -2
- data/lib/soroban/functions/exp.rb +1 -3
- data/lib/soroban/functions/if.rb +1 -1
- data/lib/soroban/functions/ln.rb +1 -3
- data/lib/soroban/functions/max.rb +2 -2
- data/lib/soroban/functions/min.rb +2 -2
- data/lib/soroban/functions/not.rb +1 -3
- data/lib/soroban/functions/or.rb +4 -2
- data/lib/soroban/functions/sum.rb +2 -2
- data/lib/soroban/functions/vlookup.rb +2 -2
- data/lib/soroban/helpers.rb +62 -38
- data/lib/soroban/import.rb +4 -1
- data/lib/soroban/import/ruby_xl_importer.rb +21 -15
- data/lib/soroban/import/ruby_xl_patch.rb +2 -0
- data/lib/soroban/label_walker.rb +10 -9
- data/lib/soroban/parser.rb +6 -4
- data/lib/soroban/parser/grammar.rb +1474 -1472
- data/lib/soroban/parser/grammar.treetop +71 -67
- data/lib/soroban/parser/nodes.rb +49 -47
- data/lib/soroban/parser/rewrite.rb +20 -13
- data/lib/soroban/sheet.rb +33 -29
- data/lib/soroban/value_walker.rb +19 -19
- data/spec/documentation_spec.rb +31 -43
- data/spec/import_spec.rb +5 -13
- data/spec/soroban_spec.rb +6 -2
- data/spec/spec_helper.rb +8 -0
- metadata +6 -6
- data/lib/soroban/error.rb +0 -20
@@ -1,68 +1,72 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
1
|
+
module Soroban
|
2
|
+
|
3
|
+
grammar Excel
|
4
|
+
rule formula
|
5
|
+
'=' space? logical <Formula> / string / number / boolean
|
6
|
+
end
|
7
|
+
rule logical
|
8
|
+
and ( space? 'or' space? and )*
|
9
|
+
end
|
10
|
+
rule and
|
11
|
+
truthval ( space? 'and' space? truthval )*
|
12
|
+
end
|
13
|
+
rule truthval
|
14
|
+
comparison / '(' space? logical space? ')' / boolean
|
15
|
+
end
|
16
|
+
rule boolean
|
17
|
+
'true' / 'false' / 'TRUE' / 'FALSE'
|
18
|
+
end
|
19
|
+
rule comparison
|
20
|
+
expression ( space? comparator space? expression )*
|
21
|
+
end
|
22
|
+
rule comparator
|
23
|
+
'=' <Equal> / '<>' <NotEqual> / '>=' / '<=' / '>' / '<'
|
24
|
+
end
|
25
|
+
rule expression
|
26
|
+
multiplicative ( space? additive_operator space? multiplicative )*
|
27
|
+
end
|
28
|
+
rule additive_operator
|
29
|
+
'+' / '-'
|
30
|
+
end
|
31
|
+
rule multiplicative
|
32
|
+
value ( space? multiplicative_operator space? value )*
|
33
|
+
end
|
34
|
+
rule multiplicative_operator
|
35
|
+
'^' <Pow> / '*' / '/'
|
36
|
+
end
|
37
|
+
rule value
|
38
|
+
( function / '(' space? expression space? ')' / range / number / boolean / identifier / string / '-' value )
|
39
|
+
end
|
40
|
+
rule function
|
41
|
+
[a-zA-Z]+ '(' space? arguments? space? ')' <Function>
|
42
|
+
end
|
43
|
+
rule arguments
|
44
|
+
logical ( space? ',' space? logical )*
|
45
|
+
end
|
46
|
+
rule number
|
47
|
+
( float / integer / '-' float / '-' integer )
|
48
|
+
end
|
49
|
+
rule float
|
50
|
+
[0-9]* '.' [0-9]+ <FloatValue>
|
51
|
+
end
|
52
|
+
rule integer
|
53
|
+
[0-9]+ <IntegerValue>
|
54
|
+
end
|
55
|
+
rule identifier
|
56
|
+
[a-zA-Z] [a-zA-Z0-9]* <Identifier>
|
57
|
+
end
|
58
|
+
rule label
|
59
|
+
[A-Za-z]+ [1-9] [0-9]* <Label> / '$' [A-Za-z]+ '$' [1-9] [0-9]* <Label>
|
60
|
+
end
|
61
|
+
rule string
|
62
|
+
'"' ('\"' / !'"' .)* '"' / "'" [^']* "'"
|
63
|
+
end
|
64
|
+
rule range
|
65
|
+
label ':' label <Range>
|
66
|
+
end
|
67
|
+
rule space
|
68
|
+
[\s]+
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
68
72
|
end
|
data/lib/soroban/parser/nodes.rb
CHANGED
@@ -1,70 +1,72 @@
|
|
1
1
|
module Soroban
|
2
|
+
module Excel
|
2
3
|
|
3
|
-
|
4
|
-
|
5
|
-
|
4
|
+
class Formula < Treetop::Runtime::SyntaxNode
|
5
|
+
def rewrite_ruby(value)
|
6
|
+
value.gsub(/^= */, '')
|
7
|
+
end
|
6
8
|
end
|
7
|
-
end
|
8
9
|
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
10
|
+
class Identifier < Treetop::Runtime::SyntaxNode
|
11
|
+
def rewrite_ruby(value)
|
12
|
+
"@#{value}.get"
|
13
|
+
end
|
14
|
+
def extract_labels(value)
|
15
|
+
value.to_sym
|
16
|
+
end
|
15
17
|
end
|
16
|
-
end
|
17
18
|
|
18
|
-
|
19
|
-
|
20
|
-
|
19
|
+
class IntegerValue < Treetop::Runtime::SyntaxNode
|
20
|
+
def rewrite_ruby(value)
|
21
|
+
"#{value.to_f}"
|
22
|
+
end
|
21
23
|
end
|
22
|
-
end
|
23
24
|
|
24
|
-
|
25
|
-
|
26
|
-
|
25
|
+
class FloatValue < Treetop::Runtime::SyntaxNode
|
26
|
+
def rewrite_ruby(value)
|
27
|
+
"#{value.to_f}"
|
28
|
+
end
|
27
29
|
end
|
28
|
-
end
|
29
30
|
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
31
|
+
class Function < Treetop::Runtime::SyntaxNode
|
32
|
+
def rewrite_ruby(value)
|
33
|
+
match = /^([^(]*)(.*)$/.match(value)
|
34
|
+
"func_#{match[1].downcase}#{match[2]}"
|
35
|
+
end
|
34
36
|
end
|
35
|
-
end
|
36
37
|
|
37
|
-
|
38
|
-
|
39
|
-
|
38
|
+
class Pow < Treetop::Runtime::SyntaxNode
|
39
|
+
def rewrite_ruby(value)
|
40
|
+
"**"
|
41
|
+
end
|
40
42
|
end
|
41
|
-
end
|
42
43
|
|
43
|
-
|
44
|
-
|
45
|
-
|
44
|
+
class Equal < Treetop::Runtime::SyntaxNode
|
45
|
+
def rewrite_ruby(value)
|
46
|
+
"=="
|
47
|
+
end
|
46
48
|
end
|
47
|
-
end
|
48
49
|
|
49
|
-
|
50
|
-
|
51
|
-
|
50
|
+
class NotEqual < Treetop::Runtime::SyntaxNode
|
51
|
+
def rewrite_ruby(value)
|
52
|
+
"!="
|
53
|
+
end
|
52
54
|
end
|
53
|
-
end
|
54
55
|
|
55
|
-
|
56
|
-
|
57
|
-
|
56
|
+
class Label < Treetop::Runtime::SyntaxNode
|
57
|
+
def rewrite_ruby(value)
|
58
|
+
value.gsub('$', '')
|
59
|
+
end
|
58
60
|
end
|
59
|
-
end
|
60
61
|
|
61
|
-
|
62
|
-
|
63
|
-
|
62
|
+
class Range < Treetop::Runtime::SyntaxNode
|
63
|
+
def rewrite_ruby(value)
|
64
|
+
"'#{value}'"
|
65
|
+
end
|
66
|
+
def extract_labels(value)
|
67
|
+
Soroban::LabelWalker.new(value).map { |label| "#{label}".to_sym }
|
68
|
+
end
|
64
69
|
end
|
65
|
-
def extract(value)
|
66
|
-
LabelWalker.new(value).map { |label| "#{label}".to_sym }
|
67
|
-
end
|
68
|
-
end
|
69
70
|
|
71
|
+
end
|
70
72
|
end
|
@@ -1,36 +1,43 @@
|
|
1
1
|
module Treetop
|
2
2
|
module Runtime
|
3
|
-
class SyntaxNode
|
4
3
|
|
5
|
-
|
4
|
+
# Each node in the AST produced by the treetop parser implements to_ruby,
|
5
|
+
# which allows the Soroban sheet to store the ruby version of the Excel
|
6
|
+
# contents of each cell in the sheet (and which also gathers and stores the
|
7
|
+
# dependencies that call has on other cells). Each concrete syntax node may
|
8
|
+
# override rewrite_ruby and extract_labels.
|
9
|
+
class SyntaxNode
|
10
|
+
def to_ruby(cell)
|
6
11
|
if nonterminal?
|
7
12
|
value = ""
|
8
|
-
elements.each { |element| value << element.to_ruby(
|
9
|
-
_add_dependency(
|
13
|
+
elements.each { |element| value << element.to_ruby(cell) }
|
14
|
+
_add_dependency(cell, value)
|
10
15
|
rewrite_ruby(value)
|
11
16
|
else
|
12
|
-
_add_dependency(
|
17
|
+
_add_dependency(cell, text_value)
|
13
18
|
rewrite_ruby(text_value)
|
14
19
|
end
|
15
20
|
end
|
16
21
|
|
22
|
+
# Return the ruby version of the Excel value. By default this does
|
23
|
+
# nothing; see nodes.rb for concrete implementations.
|
17
24
|
def rewrite_ruby(value)
|
18
25
|
value
|
19
26
|
end
|
20
27
|
|
21
|
-
|
28
|
+
# Return either a single label of the form :A1, or an array of labels of
|
29
|
+
# the form [:B1, :B2, ...]. This is used to keep track of the dependencies
|
30
|
+
# of this particular cell.
|
31
|
+
def extract_labels(value)
|
32
|
+
nil
|
22
33
|
end
|
23
34
|
|
24
35
|
private
|
25
36
|
|
26
|
-
def _add_dependency(
|
27
|
-
|
28
|
-
dependencies << value
|
29
|
-
dependencies.flatten!
|
30
|
-
dependencies.compact!
|
31
|
-
dependencies.uniq!
|
37
|
+
def _add_dependency(cell, value)
|
38
|
+
cell.add_dependencies(extract_labels(value))
|
32
39
|
end
|
33
|
-
|
34
40
|
end
|
41
|
+
|
35
42
|
end
|
36
43
|
end
|
data/lib/soroban/sheet.rb
CHANGED
@@ -1,22 +1,26 @@
|
|
1
|
+
unless defined?(Set)
|
2
|
+
require 'set'
|
3
|
+
end
|
4
|
+
|
5
|
+
require 'soroban/errors'
|
1
6
|
require 'soroban/helpers'
|
2
7
|
require 'soroban/functions'
|
8
|
+
require 'soroban/cell'
|
3
9
|
require 'soroban/label_walker'
|
4
10
|
require 'soroban/value_walker'
|
5
|
-
require 'soroban/cell'
|
6
|
-
|
7
|
-
require 'set'
|
8
11
|
|
9
12
|
module Soroban
|
10
13
|
|
11
|
-
# A container for cells.
|
14
|
+
# A container for cells. This is what the end user of Soroban will manipulate,
|
15
|
+
# either directly or via an importer that returns a Sheet instance.
|
12
16
|
class Sheet
|
13
17
|
attr_reader :bindings
|
14
18
|
|
15
19
|
# Creates a new sheet.
|
16
20
|
def initialize(logger=nil)
|
17
|
-
@
|
18
|
-
@
|
19
|
-
@
|
21
|
+
@_logger = logger
|
22
|
+
@_cells = {}
|
23
|
+
@_changes = Hash.new { |h, k| h[k] = Set.new }
|
20
24
|
@bindings = {}
|
21
25
|
end
|
22
26
|
|
@@ -24,7 +28,7 @@ module Soroban
|
|
24
28
|
# cells (via `label=`).
|
25
29
|
def method_missing(method, *args, &block)
|
26
30
|
if match = /^func_(.*)$/i.match(method.to_s)
|
27
|
-
return Soroban::call(self, match[1], *args)
|
31
|
+
return Soroban::Functions.call(self, match[1], *args)
|
28
32
|
elsif match = /^([a-z][\w]*)=$/i.match(method.to_s)
|
29
33
|
return _add(match[1], args[0])
|
30
34
|
end
|
@@ -35,11 +39,11 @@ module Soroban
|
|
35
39
|
def set(options_hash)
|
36
40
|
options_hash.each do |label_or_range, contents|
|
37
41
|
_debug("setting '#{label_or_range}' to '#{contents}'")
|
38
|
-
unless
|
42
|
+
unless Soroban::Helpers.range?(label_or_range)
|
39
43
|
_add(label_or_range, contents)
|
40
44
|
next
|
41
45
|
end
|
42
|
-
fc, fr, tc, tr =
|
46
|
+
fc, fr, tc, tr = Soroban::Helpers.getRange(label_or_range)
|
43
47
|
if fc == tc || fr == tr
|
44
48
|
raise ArgumentError, "Expecting an array when setting #{label_or_range}" unless contents.kind_of? Array
|
45
49
|
cc, cr = fc, fr
|
@@ -59,7 +63,7 @@ module Soroban
|
|
59
63
|
def get(label_or_name)
|
60
64
|
label = @bindings[label_or_name.to_sym] || label_or_name
|
61
65
|
_debug("retrieving '#{label_or_name}' from '#{label}'}")
|
62
|
-
if Soroban::range?(label)
|
66
|
+
if Soroban::Helpers.range?(label)
|
63
67
|
walk(label)
|
64
68
|
else
|
65
69
|
_get(label_or_name, eval("@#{label}", binding))
|
@@ -70,14 +74,14 @@ module Soroban
|
|
70
74
|
def bind(options_hash)
|
71
75
|
options_hash.each do |name, label_or_range|
|
72
76
|
_debug("binding '#{name}' to '#{label_or_range}'}")
|
73
|
-
if Soroban::range?(label_or_range)
|
74
|
-
LabelWalker.new(label_or_range).each do |label|
|
75
|
-
next if @
|
77
|
+
if Soroban::Helpers.range?(label_or_range)
|
78
|
+
Soroban::LabelWalker.new(label_or_range).each do |label|
|
79
|
+
next if @_cells.has_key?(label.to_sym)
|
76
80
|
raise Soroban::UndefinedError, "Cannot bind '#{name}' to range '#{label_or_range}'; cell #{label} is not defined"
|
77
81
|
end
|
78
82
|
_bind_range(name, label_or_range)
|
79
83
|
else
|
80
|
-
unless @
|
84
|
+
unless @_cells.has_key?(label_or_range.to_sym)
|
81
85
|
raise Soroban::UndefinedError, "Cannot bind '#{name}' to non-existent cell '#{label_or_range}'"
|
82
86
|
end
|
83
87
|
_bind(name, label_or_range)
|
@@ -87,34 +91,34 @@ module Soroban
|
|
87
91
|
|
88
92
|
# Visit each cell in the supplied range, yielding its value.
|
89
93
|
def walk(range)
|
90
|
-
ValueWalker.new(range, binding)
|
94
|
+
Soroban::ValueWalker.new(range, binding)
|
91
95
|
end
|
92
96
|
|
93
97
|
# Return a hash of `label => contents` for each cell in the sheet.
|
94
98
|
def cells
|
95
|
-
labels = @
|
99
|
+
labels = @_cells.keys.map(&:to_sym)
|
96
100
|
contents = labels.map { |label| eval("@#{label}.excel") }
|
97
101
|
Hash[labels.zip(contents)]
|
98
102
|
end
|
99
103
|
|
100
|
-
# Return
|
104
|
+
# Return an array of referenced but undefined cells.
|
101
105
|
def missing
|
102
|
-
@
|
106
|
+
(@_cells.values.reduce(:|) - @_cells.keys).to_a
|
103
107
|
end
|
104
108
|
|
105
109
|
private
|
106
110
|
|
107
111
|
def _debug(message)
|
108
|
-
return if @
|
109
|
-
@
|
112
|
+
return if @_logger.nil?
|
113
|
+
@_logger.debug "SOROBAN: #{message}"
|
110
114
|
end
|
111
115
|
|
112
116
|
def _link(name, dependencies)
|
113
|
-
dependencies.each { |target| @
|
117
|
+
dependencies.each { |target| @_changes[target] << name if name != target }
|
114
118
|
end
|
115
119
|
|
116
120
|
def _unlink(name, dependencies)
|
117
|
-
dependencies.each { |target| @
|
121
|
+
dependencies.each { |target| @_changes[target].delete(name) }
|
118
122
|
end
|
119
123
|
|
120
124
|
def _add(label, contents)
|
@@ -126,7 +130,7 @@ module Soroban
|
|
126
130
|
end
|
127
131
|
internal = "@#{label}"
|
128
132
|
_expose(internal, label)
|
129
|
-
cell = Cell.new(binding)
|
133
|
+
cell = Soroban::Cell.new(binding)
|
130
134
|
_set(label, cell, contents)
|
131
135
|
instance_variable_set(internal, cell)
|
132
136
|
end
|
@@ -136,14 +140,14 @@ module Soroban
|
|
136
140
|
name = @bindings[label] || label
|
137
141
|
_unlink(name, cell.dependencies)
|
138
142
|
cell.set(contents)
|
139
|
-
@
|
143
|
+
@_cells[name] = cell.dependencies
|
140
144
|
_link(name, cell.dependencies)
|
141
145
|
_clear(name)
|
142
146
|
end
|
143
147
|
|
144
148
|
def _clear(name)
|
145
|
-
@
|
146
|
-
next unless @
|
149
|
+
@_changes[name].each do |target|
|
150
|
+
next unless @_cells.has_key?(target)
|
147
151
|
begin
|
148
152
|
eval("@#{target.to_s}.clear")
|
149
153
|
_clear(target)
|
@@ -155,8 +159,8 @@ module Soroban
|
|
155
159
|
def _get(label_or_name, cell)
|
156
160
|
label = label_or_name.to_sym
|
157
161
|
name = @bindings[label] || label
|
158
|
-
badref = @
|
159
|
-
raise Soroban::UndefinedError, "Unmet dependencies #{badref.join(', ')} for #{label}" if badref.length > 0
|
162
|
+
badref = @_cells[name] & missing
|
163
|
+
raise Soroban::UndefinedError, "Unmet dependencies #{badref.to_a.join(', ')} for #{label}" if badref.length > 0
|
160
164
|
cell.get
|
161
165
|
end
|
162
166
|
|