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,26 @@
1
+ require_relative 'element'
2
+
3
+ module Calco
4
+
5
+ class If < Element
6
+
7
+ def initialize condition, _then, _else
8
+
9
+ super()
10
+
11
+ @condition, @_then, @_else = condition, Constant.wrap(_then), Constant.wrap(_else)
12
+
13
+ end
14
+
15
+ def generate row
16
+
17
+ _then = generate_operand(@_then, row)
18
+ _else = generate_operand(@_else, row)
19
+
20
+ "IF(#{@condition.generate(row)};#{_then};#{_else})"
21
+
22
+ end
23
+
24
+ end
25
+
26
+ end
@@ -0,0 +1,34 @@
1
+ require_relative 'element'
2
+
3
+ module Calco
4
+
5
+ class Operation < Element
6
+
7
+ include Operators
8
+
9
+ def initialize operator, operand1, operand2
10
+ @operator, @operand1, @operand2 = operator, Constant.wrap(operand1), Constant.wrap(operand2)
11
+ end
12
+
13
+ def generate row
14
+
15
+ return "#{@engine.operator(@operator)}#{generate_operand(@operand1, row)}" unless @operand2
16
+
17
+ operand1 = generate_operand(@operand1, row)
18
+ operand2 = generate_operand(@operand2, row)
19
+
20
+ "#{operand1}#{@engine.operator(@operator)}#{operand2}"
21
+
22
+ end
23
+
24
+ def as_operand row
25
+ "(#{generate(row)})"
26
+ end
27
+
28
+ def -@
29
+ Operation.new('-', self, nil)
30
+ end
31
+
32
+ end
33
+
34
+ end
@@ -0,0 +1,17 @@
1
+ module Calco
2
+
3
+ module Operators
4
+
5
+ %w[+ - / * == != < > >= <=].each do |op|
6
+
7
+ op_str = op == '==' ? '=' : op
8
+
9
+ define_method(op) do |arg|
10
+ Operation.new(op_str, self, arg)
11
+ end
12
+
13
+ end
14
+
15
+ end
16
+
17
+ end
@@ -0,0 +1,26 @@
1
+ require_relative 'element'
2
+
3
+ module Calco
4
+
5
+ class Or < Element
6
+
7
+ def initialize condition1, condition2
8
+
9
+ super()
10
+
11
+ @condition1, @condition2 = Constant.wrap(condition1), Constant.wrap(condition2)
12
+
13
+ end
14
+
15
+ def generate row
16
+
17
+ condition1 = generate_operand(@condition1, row)
18
+ condition2 = generate_operand(@condition2, row)
19
+
20
+ "OR(#{condition1};#{condition2})"
21
+
22
+ end
23
+
24
+ end
25
+
26
+ end
@@ -0,0 +1,42 @@
1
+ require_relative 'element'
2
+
3
+ module Calco
4
+
5
+ class ValueExtractor < Element
6
+
7
+ def initialize variable, reference_type
8
+ @variable, @reference_type = variable, reference_type
9
+ @variable.reference_type = reference_type
10
+ end
11
+
12
+ def column= column_name
13
+ @variable.column = column_name
14
+ end
15
+
16
+ def value_name
17
+ @variable.name
18
+ end
19
+
20
+ def value
21
+ @variable.value
22
+ end
23
+
24
+ def absolute_row
25
+ @variable.absolute_row
26
+ end
27
+
28
+ def reference_type
29
+ @variable.reference_type
30
+ end
31
+
32
+ def generate row
33
+
34
+ return nil if @variable.absolute_row and @variable.absolute_row != row
35
+
36
+ @engine.value(self)
37
+
38
+ end
39
+
40
+ end
41
+
42
+ end
@@ -0,0 +1,35 @@
1
+ require_relative 'element'
2
+ require_relative 'aggregator'
3
+
4
+ module Calco
5
+
6
+ class Variable < Element
7
+
8
+ include Operators
9
+
10
+ attr_accessor :value, :name
11
+
12
+ def initialize name, value
13
+
14
+ super()
15
+
16
+ @name, @value = name, value
17
+
18
+ end
19
+
20
+ def [] range
21
+
22
+ raise ArgumentError, "Expected Range got #{range.class}" unless range.is_a?(Range)
23
+ raise ArgumentError, "Invalid start of range (must be > 0, was #{range.first})" unless range.first > 0
24
+
25
+ Aggregator.new(self, range)
26
+
27
+ end
28
+
29
+ def generate row
30
+ @engine.column_reference(self, row)
31
+ end
32
+
33
+ end
34
+
35
+ end
@@ -0,0 +1,32 @@
1
+ require 'date'
2
+ require 'time'
3
+
4
+ require 'calco'
5
+
6
+ module Calco
7
+
8
+ module CalculatorBuiltinFunctions
9
+
10
+ def YEAR date
11
+ date.year
12
+ end
13
+
14
+ def TODAY
15
+ Date.today
16
+ end
17
+
18
+ def DATEVALUE text
19
+ Date.parse(text)
20
+ end
21
+
22
+ def TIMEVALUE text
23
+ Time.parse(text)
24
+ end
25
+
26
+ def LEFT text, n
27
+ text[0, n]
28
+ end
29
+
30
+ end
31
+
32
+ end
@@ -0,0 +1,80 @@
1
+ require 'csv'
2
+ require 'calco'
3
+
4
+ module Calco
5
+
6
+ class CSVEngine < Calco::DefaultEngine
7
+
8
+ def initialize col_sep = ',', quote_char = '"'
9
+ @col_sep, @quote_char = col_sep, quote_char
10
+ end
11
+
12
+ def empty_row
13
+ @out_stream.write CSV.generate_line([])
14
+ end
15
+
16
+ def write_row sheet, row_id
17
+
18
+ row_id += 1 if row_id > 0 and sheet.has_titles?
19
+
20
+ cells = sheet.row(row_id)
21
+
22
+ @out_stream.write CSV.generate_line(cells, :col_sep => @col_sep, :quote_char => @quote_char)
23
+
24
+ end
25
+
26
+ def generate_cell row_number, column, cell_style, column_style, column_type
27
+
28
+ if column.is_a?(Calco::Formula)
29
+
30
+ res = super
31
+
32
+ if column_type =~ /^[$]/
33
+ res = "=DOLLAR(#{res})"
34
+ elsif column_type == '%'
35
+ res = "=(#{res})%"
36
+ else
37
+ res = "=#{res}"
38
+ end
39
+
40
+ elsif column.respond_to?(:value)
41
+
42
+ value = column.value
43
+
44
+ style = cell_style ? "#{cell_style.generate(row_number)}" : ''
45
+
46
+ if value.is_a?(Time)
47
+ res = "=TIMEVALUE(\"#{value.strftime("%H:%M:%S")}\")#{style}"
48
+ elsif value.is_a?(Date)
49
+ res = "=DATEVALUE(\"#{value.strftime("%Y-%m-%d")}\")#{style}"
50
+ elsif value.is_a?(Numeric)
51
+
52
+ if column_type =~ /^[$]/
53
+ res = "=DOLLAR(#{value}#{style})"
54
+ elsif column_type == '%'
55
+ res = "=(#{value}*100)%"
56
+ else
57
+ res = "#{value}#{style}"
58
+ end
59
+
60
+ else
61
+ res = "#{value}#{style}"
62
+ end
63
+
64
+ end
65
+
66
+ res
67
+
68
+ end
69
+
70
+ def style statement, row
71
+ "+STYLE(#{statement.generate(row)})"
72
+ end
73
+
74
+ def current
75
+ 'CURRENT()'
76
+ end
77
+
78
+ end
79
+
80
+ end
@@ -0,0 +1,140 @@
1
+ require 'calco'
2
+
3
+ module Calco
4
+
5
+ class DefaultEngine
6
+
7
+ # output can be a String (as a file name) or an output stream
8
+ # examples:
9
+ # engine.save doc, "my_doc.txt" do |spreadsheet|
10
+ # ...
11
+ # end
12
+ #
13
+ # engine.save doc, $stdout do |spreadsheet|
14
+ # ...
15
+ # end
16
+ def save doc, output, &data_iterator
17
+
18
+ if output.respond_to?(:write)
19
+ @out_stream = output
20
+ else
21
+ @out_stream = open(output, "w")
22
+ end
23
+
24
+ data_iterator.call(doc)
25
+
26
+ end
27
+
28
+ def empty_row
29
+ end
30
+
31
+ def write_row sheet, row_id
32
+
33
+ cells = sheet.row(row_id)
34
+
35
+ cells.each_index do |i|
36
+
37
+ cell = cells[i]
38
+
39
+ @out_stream.write "#{COLUMNS[i]}#{row_id}: #{cell}\n"
40
+
41
+ end
42
+
43
+ @out_stream.write "\n"
44
+
45
+ end
46
+
47
+ def generate row_number, columns, cell_styles, column_styles, column_types
48
+
49
+ cells = []
50
+
51
+ columns.each_with_index do |column, i|
52
+ cells << generate_cell(row_number, column, cell_styles[i], column_styles[i], column_types[i])
53
+ end
54
+
55
+ cells
56
+
57
+ end
58
+
59
+ def generate_cell row_number, column, cell_style, column_style, column_type
60
+
61
+ cell = '' unless column
62
+ cell = column.generate(row_number) if column
63
+
64
+ if cell_style
65
+ cell = cell.to_s + cell_style.generate(row_number)
66
+ end
67
+
68
+ cell
69
+
70
+ end
71
+
72
+ def reference_types
73
+ {
74
+ :current => proc { |element, r, c| current },
75
+ :normal => proc { |element, r, c| "#{c}#{r}" },
76
+ :absolute => proc { |element, r, c| "$#{c}$#{element.absolute_row}" },
77
+ :absolute_row => proc { |element, r, c| "#{c}$#{element.absolute_row}" },
78
+ :absolute_column => proc { |element, r, c| "$#{c}#{r}" }
79
+ }
80
+ end
81
+
82
+ def column_reference element, row
83
+ reference_types[element.reference_type].call(element, row, element.column)
84
+ end
85
+
86
+ def range_reference element, row, range
87
+
88
+ from = reference_types[element.reference_type].call(element, range.first, element.column)
89
+
90
+ if range.grouping_range?
91
+ to = row - 1
92
+ elsif range.last == -1
93
+ to = DefaultEngine.row_infinity
94
+ else
95
+ to = range.last
96
+ end
97
+
98
+ to = reference_types[element.reference_type].call(element, to, element.column)
99
+
100
+ "#{from}:#{to}"
101
+
102
+ end
103
+
104
+ def value element
105
+ if ! element.respond_to?(:value)
106
+ element.inspect
107
+ elsif element.value.is_a?(Time)
108
+ res = element.value.strftime("%H:%M:%S")
109
+ elsif element.value.is_a?(Date)
110
+ res = element.value.strftime("%Y-%m-%d")
111
+ else
112
+ element.value.inspect
113
+ end
114
+ end
115
+
116
+ def style statement, row
117
+ "; apply_style(#{statement.generate(row)})"
118
+ end
119
+
120
+ def operator op
121
+ op
122
+ end
123
+
124
+ def current
125
+ 'self'
126
+ end
127
+
128
+ def self.row_infinity
129
+ @@row_infinity
130
+ end
131
+
132
+ def self.row_infinity= value
133
+ @@row_infinity = value
134
+ end
135
+
136
+ @@row_infinity = 1048576
137
+
138
+ end
139
+
140
+ end
@@ -0,0 +1,263 @@
1
+ require 'date'
2
+
3
+ require 'zip'
4
+ require 'tmpdir'
5
+ require 'pathname'
6
+ require 'tempfile'
7
+ require 'rexml/document'
8
+
9
+ require 'calco'
10
+
11
+ module Calco
12
+
13
+ class OfficeEngine < DefaultEngine
14
+
15
+ def initialize ods_template, first_row_is_header = true
16
+ @ods_template = ods_template
17
+ @first_row_is_header = first_row_is_header
18
+ end
19
+
20
+ # output is a String (as a file name)
21
+ def save doc, to_filename, &data_iterator
22
+
23
+ content_xml_file = Tempfile.new('office-gen')
24
+ result_xml_file = Tempfile.new('office-gen')
25
+
26
+ Zip::File.open(@ods_template) do |zipfile|
27
+ content = zipfile.read("content.xml")
28
+ open(content_xml_file, "w") {|out| out.write content}
29
+ end
30
+
31
+ write_result_content doc, content_xml_file, result_xml_file, @first_row_is_header, &data_iterator
32
+
33
+ FileUtils.cp(@ods_template, to_filename)
34
+
35
+ Zip::File.open(to_filename) do |zipfile|
36
+
37
+ zipfile.get_output_stream("content.xml") do |os|
38
+
39
+ File.open(result_xml_file).each_line do |line|
40
+ os.puts line
41
+ end
42
+
43
+ end
44
+
45
+ end
46
+
47
+ end
48
+
49
+ def empty_row
50
+ @out_stream.write '<table:table-row/>'
51
+ end
52
+
53
+ def write_row sheet, row_id
54
+
55
+ return if row_id == 0 and sheet.has_titles?
56
+
57
+ row_id += 1 # office sheet indexes start at 1
58
+
59
+ cells = sheet.row(row_id)
60
+
61
+ @out_stream.write '<table:table-row>'
62
+
63
+ cells.each_index do |i|
64
+
65
+ cell = cells[i]
66
+
67
+ @out_stream.write cell
68
+
69
+ end
70
+
71
+ @out_stream.write '</table:table-row>'
72
+
73
+ end
74
+
75
+ def generate_cell row_number, column, cell_style, column_style, column_type
76
+
77
+ return '<table:table-cell/>' unless column
78
+ return '<table:table-cell/>' if column.absolute_row and column.absolute_row != row_number
79
+
80
+ cell = column.generate(row_number)
81
+
82
+ if cell_style
83
+ cell = cell.to_s + cell_style.generate(row_number)
84
+ end
85
+
86
+ if column_style
87
+ column_style = %[table:style-name="#{column_style}"]
88
+ else
89
+ column_style = ''
90
+ end
91
+
92
+ if column_type
93
+
94
+ if column_type == '%'
95
+ column_type = %[office:value-type="percentage"]
96
+ elsif column_type =~ /\$([A-Z]{3})/
97
+ column_type = %[office:value-type="currency" office:currency="#{$1}"]
98
+ else
99
+ column_type = %[office:value-type="#{column_type}"]
100
+ end
101
+
102
+ end
103
+
104
+ if column.is_a?(Formula)
105
+
106
+ column_type = 'office:value-type="float"' unless column_type
107
+
108
+ %[<table:table-cell #{column_style} #{column_type} table:formula="of:=#{cell}" />]
109
+
110
+ elsif column.respond_to?(:value)
111
+
112
+ if column.value.nil?
113
+
114
+ "<table:table-cell/>"
115
+
116
+ elsif column.value.is_a?(Numeric)
117
+
118
+ column_type = 'office:value-type="float"' unless column_type
119
+
120
+ %[<table:table-cell #{column_style} #{column_type} office:value="#{cell}"/>]
121
+
122
+ elsif column.value.is_a?(Date)
123
+
124
+ column_type = 'office:value-type="date"' unless column_type
125
+
126
+ %[<table:table-cell #{column_style} #{column_type} office:date-value="#{cell.to_s}"/>]
127
+
128
+ else
129
+
130
+ column_type = 'office:value-type="string"' unless column_type
131
+
132
+ %[
133
+ <table:table-cell #{column_style} #{column_type}>
134
+ <text:p><![CDATA[#{cell}]]></text:p>
135
+ </table:table-cell>
136
+ ]
137
+
138
+ end
139
+
140
+ end
141
+
142
+ end
143
+
144
+ def column_reference element, row
145
+ element.is_a?(Current) ? "#{super}" : "[.#{super}]"
146
+ end
147
+
148
+ def value element
149
+
150
+ if ! element.respond_to?(:value)
151
+ s = element.to_s
152
+ else
153
+ s = element.value.to_s
154
+ end
155
+
156
+ s.gsub!('"', '&quot;') if s =~ /"/
157
+ s
158
+
159
+ end
160
+
161
+ def style statement, row
162
+ "+ORG.OPENOFFICE.STYLE(#{statement.generate(row)})"
163
+ end
164
+
165
+ def operator op
166
+
167
+ if op == '!='
168
+ '&lt;&gt;'
169
+ elsif op == '<'
170
+ '&lt;'
171
+ elsif op == '>'
172
+ '&gt;'
173
+ else
174
+ op
175
+ end
176
+
177
+ end
178
+
179
+ def current
180
+ 'ORG.OPENOFFICE.CURRENT()'
181
+ end
182
+
183
+ private
184
+
185
+ # returns the parent table and removes template/example rows, also returns
186
+ # the first template/example row (to find cell styles for instance)
187
+ def retrieve_template_row doc, first_row_is_header
188
+
189
+ root = doc.root
190
+
191
+ count = 0
192
+ template_row = nil
193
+
194
+ table = root.elements['//table:table']
195
+ table.each_element('table:table-row') do |row|
196
+
197
+ if first_row_is_header && count == 0
198
+ # keep the header row
199
+ else
200
+
201
+ table.delete_element(row)
202
+
203
+ template_row = row unless template_row
204
+
205
+ end
206
+
207
+ count += 1
208
+
209
+ end
210
+
211
+ raise "Cannot find template row in #{@ods_template}" unless template_row
212
+
213
+ return table, template_row
214
+
215
+ end
216
+
217
+ def create_temporary xml, to_filename
218
+
219
+ to = Pathname.new(to_filename)
220
+
221
+ temp_file = Tempfile.new('office-gen', to.dirname.to_s)
222
+
223
+ File.open(temp_file, 'w') { |stream| stream.puts xml }
224
+
225
+ temp_file
226
+
227
+ end
228
+
229
+ def write_result_content doc, content_xml_file, result_xml_file, first_row_is_header, &data_iterator
230
+
231
+ file = File.new(content_xml_file)
232
+
233
+ xml = REXML::Document.new(file)
234
+
235
+ table, template_row = retrieve_template_row(xml, first_row_is_header)
236
+
237
+ table.add_text "%%%Insert data here%%%\n"
238
+
239
+ temp_file = create_temporary(xml, result_xml_file)
240
+
241
+ File.open(result_xml_file, 'w') do |stream|
242
+
243
+ @out_stream = stream
244
+
245
+ File.open(temp_file, 'r').each do |line|
246
+
247
+ if line =~ /(.*)%%%Insert data here%%%(.*)/
248
+ @out_stream.write $1
249
+ data_iterator.call(doc)
250
+ @out_stream.write $2
251
+ else
252
+ @out_stream.write line
253
+ end
254
+
255
+ end
256
+
257
+ end
258
+
259
+ end
260
+
261
+ end
262
+
263
+ end