calco 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +34 -0
- data/Gemfile +4 -0
- data/Gemfile.lock +26 -0
- data/LICENSE +21 -0
- data/LICENSE.txt +22 -0
- data/README.md +360 -0
- data/Rakefile +1 -0
- data/calco.gemspec +23 -0
- data/examples/ages.ods +0 -0
- data/examples/compute_cells.rb +61 -0
- data/examples/data.csv +8 -0
- data/examples/example.rb +97 -0
- data/examples/multiplication_tables.ods +0 -0
- data/examples/multiplication_tables.rb +73 -0
- data/examples/register_function.rb +44 -0
- data/examples/using_date_functions.rb +42 -0
- data/examples/write_csv.rb +68 -0
- data/examples/write_ods.rb +69 -0
- data/lib/calco.rb +17 -0
- data/lib/calco/core_ext/fixnum.rb +22 -0
- data/lib/calco/core_ext/float.rb +22 -0
- data/lib/calco/core_ext/range.rb +15 -0
- data/lib/calco/core_ext/string.rb +20 -0
- data/lib/calco/date_functions.rb +13 -0
- data/lib/calco/definition_dsl.rb +127 -0
- data/lib/calco/elements/aggregator.rb +17 -0
- data/lib/calco/elements/builtin_function.rb +84 -0
- data/lib/calco/elements/constant.rb +31 -0
- data/lib/calco/elements/current.rb +19 -0
- data/lib/calco/elements/element.rb +31 -0
- data/lib/calco/elements/empty.rb +9 -0
- data/lib/calco/elements/formula.rb +42 -0
- data/lib/calco/elements/if.rb +26 -0
- data/lib/calco/elements/operation.rb +34 -0
- data/lib/calco/elements/operator.rb +17 -0
- data/lib/calco/elements/or.rb +26 -0
- data/lib/calco/elements/value_extractor.rb +42 -0
- data/lib/calco/elements/variable.rb +35 -0
- data/lib/calco/engines/calculator_builtin_functions.rb +32 -0
- data/lib/calco/engines/csv_engine.rb +80 -0
- data/lib/calco/engines/default_engine.rb +140 -0
- data/lib/calco/engines/office_engine.rb +263 -0
- data/lib/calco/engines/simple_calculator_engine.rb +151 -0
- data/lib/calco/math_functions.rb +9 -0
- data/lib/calco/sheet.rb +363 -0
- data/lib/calco/spreadsheet.rb +172 -0
- data/lib/calco/string_functions.rb +9 -0
- data/lib/calco/style.rb +15 -0
- data/lib/calco/time_functions.rb +12 -0
- data/lib/calco/version.rb +3 -0
- data/spec/absolute_references_spec.rb +86 -0
- data/spec/builtin_functions_spec.rb +161 -0
- data/spec/calculator_engine_spec.rb +251 -0
- data/spec/conditions_spec.rb +118 -0
- data/spec/content_change_spec.rb +190 -0
- data/spec/csv_engine_spec.rb +324 -0
- data/spec/default_engine_spec.rb +135 -0
- data/spec/definitions_spec.rb +65 -0
- data/spec/errors_spec.rb +189 -0
- data/spec/functions_spec.rb +251 -0
- data/spec/header_row_spec.rb +63 -0
- data/spec/range_spec.rb +189 -0
- data/spec/sheet_selections_spec.rb +49 -0
- data/spec/sheet_spec.rb +229 -0
- data/spec/smart_types_spec.rb +43 -0
- data/spec/spreadsheet_spec.rb +80 -0
- data/spec/styles_spec.rb +29 -0
- data/spec/variables_spec.rb +41 -0
- metadata +158 -0
@@ -0,0 +1,151 @@
|
|
1
|
+
require 'calco'
|
2
|
+
|
3
|
+
require_relative 'calculator_builtin_functions'
|
4
|
+
|
5
|
+
module Calco
|
6
|
+
|
7
|
+
#
|
8
|
+
# A simple engine computing the formulas.
|
9
|
+
#
|
10
|
+
# It does not cover some cases like
|
11
|
+
# * ahead formulas (formula using higher cells)
|
12
|
+
#
|
13
|
+
# Has no optimization, could make blow variable space...
|
14
|
+
#
|
15
|
+
# Is not thread safe if the same engine is used by several threads.
|
16
|
+
# When 'save' is called an internal context is used and is the
|
17
|
+
# weakness for thread safety...
|
18
|
+
#
|
19
|
+
class SimpleCalculatorEngine < Calco::DefaultEngine
|
20
|
+
|
21
|
+
include CalculatorBuiltinFunctions
|
22
|
+
|
23
|
+
def save doc, output, &data_iterator
|
24
|
+
|
25
|
+
@context = Object.new.send(:binding)
|
26
|
+
|
27
|
+
super
|
28
|
+
|
29
|
+
end
|
30
|
+
|
31
|
+
def write_row sheet, row_id
|
32
|
+
|
33
|
+
return if sheet.has_titles? and row_id == 0
|
34
|
+
|
35
|
+
names = column_names(sheet)
|
36
|
+
|
37
|
+
cells = sheet.row(row_id)
|
38
|
+
|
39
|
+
values, longest_name = compute_cell_values(row_id, cells, names)
|
40
|
+
|
41
|
+
values.each_with_index do |value, i|
|
42
|
+
|
43
|
+
next unless value
|
44
|
+
|
45
|
+
value = value.to_s if value.is_a?(Date) or value.is_a?(Time)
|
46
|
+
|
47
|
+
@out_stream.write "%#{longest_name}s = #{value.inspect}\n" % names[i]
|
48
|
+
|
49
|
+
end
|
50
|
+
|
51
|
+
@out_stream.write "\n"
|
52
|
+
|
53
|
+
end
|
54
|
+
|
55
|
+
def generate_cell row_number, column, cell_style, column_style, column_type
|
56
|
+
|
57
|
+
return nil unless column
|
58
|
+
|
59
|
+
if column.absolute_row
|
60
|
+
return nil if column.absolute_row != row_number
|
61
|
+
end
|
62
|
+
|
63
|
+
if column.respond_to?(:value)
|
64
|
+
|
65
|
+
if column.value.is_a?(Date)
|
66
|
+
%{Date.parse("#{column.value}")}
|
67
|
+
else
|
68
|
+
column.value.inspect
|
69
|
+
end
|
70
|
+
|
71
|
+
else
|
72
|
+
column.generate(row_number)
|
73
|
+
end
|
74
|
+
|
75
|
+
end
|
76
|
+
|
77
|
+
def column_reference element, row
|
78
|
+
|
79
|
+
name = super.downcase
|
80
|
+
|
81
|
+
name.gsub!(%r{[$]([a-z]+)[$](\d+)}, '\1\2')
|
82
|
+
|
83
|
+
name
|
84
|
+
|
85
|
+
end
|
86
|
+
|
87
|
+
private
|
88
|
+
|
89
|
+
# returns an array of column title names, variable names or nil(s)
|
90
|
+
def column_names sheet
|
91
|
+
|
92
|
+
names = []
|
93
|
+
|
94
|
+
titles = sheet.column_titles
|
95
|
+
|
96
|
+
sheet.each_cell_definition do |column, i|
|
97
|
+
|
98
|
+
if titles[i]
|
99
|
+
|
100
|
+
names << titles[i]
|
101
|
+
|
102
|
+
elsif column.respond_to?(:value_name)
|
103
|
+
|
104
|
+
names << column.value_name
|
105
|
+
|
106
|
+
else
|
107
|
+
|
108
|
+
names << nil
|
109
|
+
|
110
|
+
end
|
111
|
+
|
112
|
+
end
|
113
|
+
|
114
|
+
names
|
115
|
+
|
116
|
+
end
|
117
|
+
|
118
|
+
def compute_cell_values row_id, cells, names
|
119
|
+
|
120
|
+
values = []
|
121
|
+
longest_name = 0
|
122
|
+
|
123
|
+
cells.each_with_index do |cell, i|
|
124
|
+
|
125
|
+
name = "#{Calco::COLUMNS[i]}#{row_id}"
|
126
|
+
|
127
|
+
if cell
|
128
|
+
|
129
|
+
res = eval("#{name.downcase} = #{cell}", @context)
|
130
|
+
|
131
|
+
else
|
132
|
+
|
133
|
+
res = nil
|
134
|
+
|
135
|
+
end
|
136
|
+
|
137
|
+
values << res
|
138
|
+
|
139
|
+
names[i] = name unless names[i]
|
140
|
+
|
141
|
+
longest_name = [longest_name, names[i].length].max
|
142
|
+
|
143
|
+
end
|
144
|
+
|
145
|
+
return values, longest_name
|
146
|
+
|
147
|
+
end
|
148
|
+
|
149
|
+
end
|
150
|
+
|
151
|
+
end
|
data/lib/calco/sheet.rb
ADDED
@@ -0,0 +1,363 @@
|
|
1
|
+
require 'set'
|
2
|
+
|
3
|
+
require_relative 'style'
|
4
|
+
require_relative 'definition_dsl'
|
5
|
+
|
6
|
+
module Calco
|
7
|
+
|
8
|
+
letters = (65...65 + 26).collect { |i| i.chr }
|
9
|
+
COLUMNS = letters + letters.collect { |letter| (65...65 + 26).collect { |i| letter + i.chr } }.flatten
|
10
|
+
|
11
|
+
# don't use method names, of the Sheet class, as variable or formula or even 'sheet_name'
|
12
|
+
class Sheet
|
13
|
+
|
14
|
+
include BuilderDSL
|
15
|
+
|
16
|
+
attr_accessor :definitions
|
17
|
+
attr_accessor :sheet_name, :column_titles
|
18
|
+
|
19
|
+
def initialize sheet_name = 'Sheet', engine = DefaultEngine.new
|
20
|
+
|
21
|
+
@sheet_name = sheet_name
|
22
|
+
|
23
|
+
@columns = []
|
24
|
+
@cell_styles = []
|
25
|
+
@column_types = []
|
26
|
+
@title_styles = []
|
27
|
+
@column_styles = []
|
28
|
+
@assigned_column_index = 0
|
29
|
+
|
30
|
+
@columns_by_id = {}
|
31
|
+
|
32
|
+
@save_contents = []
|
33
|
+
|
34
|
+
@has_titles = false
|
35
|
+
@column_titles = []
|
36
|
+
|
37
|
+
@engine = engine
|
38
|
+
|
39
|
+
end
|
40
|
+
|
41
|
+
def owner= owner
|
42
|
+
@owner = owner
|
43
|
+
end
|
44
|
+
|
45
|
+
def current
|
46
|
+
Current.new
|
47
|
+
end
|
48
|
+
|
49
|
+
def column_style index
|
50
|
+
@column_styles[index]
|
51
|
+
end
|
52
|
+
|
53
|
+
def title_style index
|
54
|
+
@title_styles[index]
|
55
|
+
end
|
56
|
+
|
57
|
+
# name is a numeric or string constant, a variable name, a ValueExtractor, a formula name, empty (skip)
|
58
|
+
# options is a has than can contain next keys:
|
59
|
+
# style => a dynamic (or computed) style for the cell
|
60
|
+
# column_style => style name for the column
|
61
|
+
# title_style => style name for the column title (if row 0 contains the title)
|
62
|
+
# type => type for the column values
|
63
|
+
# title => the title of the column, supposed to appear at first row
|
64
|
+
# id => id associated to the column
|
65
|
+
def column name, options = {}
|
66
|
+
|
67
|
+
if name.is_a?(Numeric) or name.is_a?(String)
|
68
|
+
@columns << Constant.wrap(name)
|
69
|
+
elsif name.is_a?(ValueExtractor)
|
70
|
+
@columns << name
|
71
|
+
name.column = COLUMNS[@assigned_column_index]
|
72
|
+
elsif name == Empty.instance
|
73
|
+
@columns << nil
|
74
|
+
elsif @definitions.variable?(name)
|
75
|
+
@definitions.variable(name).column = COLUMNS[@assigned_column_index]
|
76
|
+
@columns << @definitions.variable(name)
|
77
|
+
elsif @definitions.formula?(name)
|
78
|
+
@definitions.formula(name).column = COLUMNS[@assigned_column_index]
|
79
|
+
@columns << @definitions.formula(name)
|
80
|
+
else
|
81
|
+
raise "Unknown function or variable '#{name}'"
|
82
|
+
end
|
83
|
+
|
84
|
+
raise ArgumentError, "Options should be a Hash" unless options.is_a?(Hash)
|
85
|
+
|
86
|
+
@columns_by_id[options[:id]] = @assigned_column_index if options[:id]
|
87
|
+
|
88
|
+
title = nil
|
89
|
+
title = options[:title] if options[:title]
|
90
|
+
|
91
|
+
style = nil
|
92
|
+
style = Style.new(options[:style]) if options[:style]
|
93
|
+
|
94
|
+
type = nil
|
95
|
+
type = options[:type] if options[:type]
|
96
|
+
|
97
|
+
column_style = nil
|
98
|
+
column_style = options[:column_style] if options[:column_style]
|
99
|
+
|
100
|
+
title_style = nil
|
101
|
+
title_style = options[:title_style] if options[:title_style]
|
102
|
+
|
103
|
+
@column_titles << title
|
104
|
+
@has_titles = @has_titles || !title.nil?
|
105
|
+
|
106
|
+
@cell_styles << style
|
107
|
+
@column_types << type
|
108
|
+
@title_styles << title_style
|
109
|
+
@column_styles << column_style
|
110
|
+
|
111
|
+
@assigned_column_index += 1
|
112
|
+
|
113
|
+
end
|
114
|
+
|
115
|
+
def replace_and_clear replacements
|
116
|
+
|
117
|
+
changed = Set.new
|
118
|
+
|
119
|
+
replacements.each do |id, new_content|
|
120
|
+
|
121
|
+
replace_content id, new_content
|
122
|
+
|
123
|
+
changed << @columns_by_id[id]
|
124
|
+
|
125
|
+
end
|
126
|
+
|
127
|
+
@columns.each_with_index do |current_content, index|
|
128
|
+
|
129
|
+
next if changed.include?(index)
|
130
|
+
|
131
|
+
@save_contents[index] = current_content
|
132
|
+
@columns[index] = nil
|
133
|
+
|
134
|
+
end
|
135
|
+
|
136
|
+
end
|
137
|
+
|
138
|
+
def replace_content column_id, new_content
|
139
|
+
|
140
|
+
raise "Column id '#{column_id}' not found" unless @columns_by_id.include?(column_id)
|
141
|
+
|
142
|
+
index = @columns_by_id[column_id]
|
143
|
+
|
144
|
+
current_content = @columns[index]
|
145
|
+
|
146
|
+
@save_contents[index] = current_content
|
147
|
+
|
148
|
+
if new_content.nil?
|
149
|
+
@columns[index] = nil
|
150
|
+
return
|
151
|
+
end
|
152
|
+
|
153
|
+
if new_content.is_a?(Numeric) or new_content.is_a?(String)
|
154
|
+
new_content = Constant.new(new_content)
|
155
|
+
assign_engine new_content, @engine
|
156
|
+
elsif @definitions.variable?(new_content)
|
157
|
+
new_content = ValueExtractor.new(@definitions.variable(new_content), :normal)
|
158
|
+
assign_engine new_content, @engine
|
159
|
+
elsif @definitions.formula?(new_content)
|
160
|
+
new_content = @definitions.formula(new_content)
|
161
|
+
else
|
162
|
+
raise "Unknown function or variable '#{new_content}' for replacement"
|
163
|
+
end
|
164
|
+
|
165
|
+
new_content.column = current_content.column
|
166
|
+
|
167
|
+
@columns[index] = new_content
|
168
|
+
|
169
|
+
end
|
170
|
+
|
171
|
+
def restore_content
|
172
|
+
|
173
|
+
@save_contents.each_with_index do |saved_content, index|
|
174
|
+
@columns[index] = saved_content if saved_content
|
175
|
+
end
|
176
|
+
|
177
|
+
@save_contents.clear
|
178
|
+
|
179
|
+
end
|
180
|
+
|
181
|
+
def skip
|
182
|
+
Empty.instance
|
183
|
+
end
|
184
|
+
|
185
|
+
# type can be :normal, :absolute, :absolute_row, :absolute_column
|
186
|
+
def value_of name, type = :normal
|
187
|
+
|
188
|
+
if @definitions.variable?(name)
|
189
|
+
ValueExtractor.new(@definitions.variable(name), type)
|
190
|
+
else
|
191
|
+
raise "Unknown variable #{name}"
|
192
|
+
end
|
193
|
+
|
194
|
+
end
|
195
|
+
|
196
|
+
def [] name
|
197
|
+
|
198
|
+
return @definitions.variable(name).value if @definitions.variable?(name)
|
199
|
+
|
200
|
+
nil
|
201
|
+
|
202
|
+
end
|
203
|
+
|
204
|
+
def []= variable_name, new_value
|
205
|
+
|
206
|
+
raise "Unknown variable '#{variable_name}'" unless @definitions.variable?(variable_name)
|
207
|
+
|
208
|
+
@definitions.variable(variable_name).value = new_value
|
209
|
+
|
210
|
+
end
|
211
|
+
|
212
|
+
# record is a Hash or any object implementing #each
|
213
|
+
# with a block expecting two parameters:
|
214
|
+
# * the key used (the variable name)
|
215
|
+
# * the value to assign
|
216
|
+
def record_assign record
|
217
|
+
|
218
|
+
record.each do |name, value|
|
219
|
+
self[name] = value
|
220
|
+
end
|
221
|
+
|
222
|
+
end
|
223
|
+
|
224
|
+
def has_titles?
|
225
|
+
@has_titles
|
226
|
+
end
|
227
|
+
|
228
|
+
def has_titles flag
|
229
|
+
@has_titles = @has_titles || flag
|
230
|
+
end
|
231
|
+
|
232
|
+
# Returns an array of String objects generates by the engine
|
233
|
+
# attached to this Sheet's Spreadsheet.
|
234
|
+
def row row_number
|
235
|
+
|
236
|
+
if row_number == 0
|
237
|
+
|
238
|
+
cells = []
|
239
|
+
|
240
|
+
if @has_titles
|
241
|
+
|
242
|
+
@column_titles.each do |title|
|
243
|
+
cells << title ? title : ''
|
244
|
+
end
|
245
|
+
|
246
|
+
end
|
247
|
+
|
248
|
+
cells
|
249
|
+
|
250
|
+
else
|
251
|
+
|
252
|
+
@engine.generate(row_number, @columns, @cell_styles, @column_styles, @column_types)
|
253
|
+
|
254
|
+
end
|
255
|
+
|
256
|
+
end
|
257
|
+
|
258
|
+
# set "next row" as empty, delegated to current engine
|
259
|
+
def empty_row
|
260
|
+
@engine.empty_row
|
261
|
+
end
|
262
|
+
|
263
|
+
# Calls the passed block for every Element, the cell definition,
|
264
|
+
# attached to columns for this Sheet.
|
265
|
+
#
|
266
|
+
# Depending an the block's arity, passes the Element or the Element
|
267
|
+
# and the index to the given block.
|
268
|
+
#
|
269
|
+
# Examples:
|
270
|
+
# each_cell_definition {|column_def| ... }
|
271
|
+
# each_cell_definition {|column_def, index| ... }
|
272
|
+
def each_cell_definition &block
|
273
|
+
|
274
|
+
if block.arity == 1
|
275
|
+
|
276
|
+
@columns.each do |column|
|
277
|
+
yield column
|
278
|
+
end
|
279
|
+
|
280
|
+
else
|
281
|
+
|
282
|
+
@columns.each_with_index do |column, i|
|
283
|
+
yield column, i
|
284
|
+
end
|
285
|
+
|
286
|
+
end
|
287
|
+
|
288
|
+
end
|
289
|
+
|
290
|
+
# data is a hash passed to #record_assign
|
291
|
+
def write_row row_id, data = nil
|
292
|
+
|
293
|
+
record_assign data if data
|
294
|
+
|
295
|
+
@engine.write_row self, row_id
|
296
|
+
|
297
|
+
end
|
298
|
+
|
299
|
+
def compile engine = @engine
|
300
|
+
|
301
|
+
@engine = engine
|
302
|
+
|
303
|
+
@definitions.formulas.each_value do |formula|
|
304
|
+
assign_engine formula, engine
|
305
|
+
end
|
306
|
+
@definitions.variables.each_value do |variable|
|
307
|
+
assign_engine variable, engine
|
308
|
+
end
|
309
|
+
|
310
|
+
@columns.each do |column|
|
311
|
+
assign_engine column, engine
|
312
|
+
end
|
313
|
+
|
314
|
+
@cell_styles.each do |style|
|
315
|
+
assign_engine style, engine
|
316
|
+
end
|
317
|
+
|
318
|
+
end
|
319
|
+
|
320
|
+
def method_missing sym, *args
|
321
|
+
# to avoid some strange behavior when some spec fails (ending with a
|
322
|
+
# call to #to_ary) enable next line to help fixing the problem
|
323
|
+
#super.respond_to?(sym)
|
324
|
+
@definitions.resolve! sym, *args
|
325
|
+
end
|
326
|
+
|
327
|
+
private
|
328
|
+
|
329
|
+
def assign_engine el, engine
|
330
|
+
|
331
|
+
return unless el.is_a?(Calco::Element) or el.is_a?(Calco::Style)
|
332
|
+
|
333
|
+
current_engine = el.instance_variable_get(:@engine)
|
334
|
+
|
335
|
+
unless current_engine == engine
|
336
|
+
|
337
|
+
el.instance_variable_set(:@engine, engine)
|
338
|
+
|
339
|
+
el.instance_variables.each do |var|
|
340
|
+
|
341
|
+
val = el.instance_variable_get(var)
|
342
|
+
|
343
|
+
if val.respond_to?(:each)
|
344
|
+
|
345
|
+
val.each do |item|
|
346
|
+
assign_engine item, engine
|
347
|
+
end
|
348
|
+
|
349
|
+
else
|
350
|
+
|
351
|
+
assign_engine val, engine
|
352
|
+
|
353
|
+
end
|
354
|
+
|
355
|
+
end
|
356
|
+
|
357
|
+
end
|
358
|
+
|
359
|
+
end
|
360
|
+
|
361
|
+
end
|
362
|
+
|
363
|
+
end
|