calco 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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