calco 0.1.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.
- 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
|