soroban 0.7.3 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|
|