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.
Files changed (70) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +34 -0
  3. data/Gemfile +4 -0
  4. data/Gemfile.lock +26 -0
  5. data/LICENSE +21 -0
  6. data/LICENSE.txt +22 -0
  7. data/README.md +360 -0
  8. data/Rakefile +1 -0
  9. data/calco.gemspec +23 -0
  10. data/examples/ages.ods +0 -0
  11. data/examples/compute_cells.rb +61 -0
  12. data/examples/data.csv +8 -0
  13. data/examples/example.rb +97 -0
  14. data/examples/multiplication_tables.ods +0 -0
  15. data/examples/multiplication_tables.rb +73 -0
  16. data/examples/register_function.rb +44 -0
  17. data/examples/using_date_functions.rb +42 -0
  18. data/examples/write_csv.rb +68 -0
  19. data/examples/write_ods.rb +69 -0
  20. data/lib/calco.rb +17 -0
  21. data/lib/calco/core_ext/fixnum.rb +22 -0
  22. data/lib/calco/core_ext/float.rb +22 -0
  23. data/lib/calco/core_ext/range.rb +15 -0
  24. data/lib/calco/core_ext/string.rb +20 -0
  25. data/lib/calco/date_functions.rb +13 -0
  26. data/lib/calco/definition_dsl.rb +127 -0
  27. data/lib/calco/elements/aggregator.rb +17 -0
  28. data/lib/calco/elements/builtin_function.rb +84 -0
  29. data/lib/calco/elements/constant.rb +31 -0
  30. data/lib/calco/elements/current.rb +19 -0
  31. data/lib/calco/elements/element.rb +31 -0
  32. data/lib/calco/elements/empty.rb +9 -0
  33. data/lib/calco/elements/formula.rb +42 -0
  34. data/lib/calco/elements/if.rb +26 -0
  35. data/lib/calco/elements/operation.rb +34 -0
  36. data/lib/calco/elements/operator.rb +17 -0
  37. data/lib/calco/elements/or.rb +26 -0
  38. data/lib/calco/elements/value_extractor.rb +42 -0
  39. data/lib/calco/elements/variable.rb +35 -0
  40. data/lib/calco/engines/calculator_builtin_functions.rb +32 -0
  41. data/lib/calco/engines/csv_engine.rb +80 -0
  42. data/lib/calco/engines/default_engine.rb +140 -0
  43. data/lib/calco/engines/office_engine.rb +263 -0
  44. data/lib/calco/engines/simple_calculator_engine.rb +151 -0
  45. data/lib/calco/math_functions.rb +9 -0
  46. data/lib/calco/sheet.rb +363 -0
  47. data/lib/calco/spreadsheet.rb +172 -0
  48. data/lib/calco/string_functions.rb +9 -0
  49. data/lib/calco/style.rb +15 -0
  50. data/lib/calco/time_functions.rb +12 -0
  51. data/lib/calco/version.rb +3 -0
  52. data/spec/absolute_references_spec.rb +86 -0
  53. data/spec/builtin_functions_spec.rb +161 -0
  54. data/spec/calculator_engine_spec.rb +251 -0
  55. data/spec/conditions_spec.rb +118 -0
  56. data/spec/content_change_spec.rb +190 -0
  57. data/spec/csv_engine_spec.rb +324 -0
  58. data/spec/default_engine_spec.rb +135 -0
  59. data/spec/definitions_spec.rb +65 -0
  60. data/spec/errors_spec.rb +189 -0
  61. data/spec/functions_spec.rb +251 -0
  62. data/spec/header_row_spec.rb +63 -0
  63. data/spec/range_spec.rb +189 -0
  64. data/spec/sheet_selections_spec.rb +49 -0
  65. data/spec/sheet_spec.rb +229 -0
  66. data/spec/smart_types_spec.rb +43 -0
  67. data/spec/spreadsheet_spec.rb +80 -0
  68. data/spec/styles_spec.rb +29 -0
  69. data/spec/variables_spec.rb +41 -0
  70. 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
@@ -0,0 +1,9 @@
1
+ module Calco
2
+
3
+ class BuiltinFunction
4
+
5
+ declare :sum, :n, Object
6
+
7
+ end
8
+
9
+ end
@@ -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