excel_templating 0.3.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (59) hide show
  1. checksums.yaml +7 -0
  2. data/.document +3 -0
  3. data/.gitignore +4 -0
  4. data/.rspec +1 -0
  5. data/.rubocop.hound.yml +261 -0
  6. data/.rubocop.ph.yml +44 -0
  7. data/.rubocop.yml +3 -0
  8. data/.yardopts +1 -0
  9. data/ChangeLog.md +8 -0
  10. data/Gemfile +10 -0
  11. data/LICENSE.txt +3 -0
  12. data/README.md +133 -0
  13. data/Rakefile +43 -0
  14. data/excel_templating.gemspec +32 -0
  15. data/lib/excel_templating/document/data_source_registry/registry_list.rb +48 -0
  16. data/lib/excel_templating/document/data_source_registry/registry_renderer.rb +74 -0
  17. data/lib/excel_templating/document/data_source_registry.rb +64 -0
  18. data/lib/excel_templating/document/sheet/repeated_row.rb +39 -0
  19. data/lib/excel_templating/document/sheet.rb +133 -0
  20. data/lib/excel_templating/document.rb +71 -0
  21. data/lib/excel_templating/document_dsl.rb +85 -0
  22. data/lib/excel_templating/excel_abstraction/active_cell_reference.rb +59 -0
  23. data/lib/excel_templating/excel_abstraction/cell.rb +23 -0
  24. data/lib/excel_templating/excel_abstraction/cell_range.rb +26 -0
  25. data/lib/excel_templating/excel_abstraction/cell_reference.rb +39 -0
  26. data/lib/excel_templating/excel_abstraction/date.rb +36 -0
  27. data/lib/excel_templating/excel_abstraction/row.rb +29 -0
  28. data/lib/excel_templating/excel_abstraction/sheet.rb +102 -0
  29. data/lib/excel_templating/excel_abstraction/spread_sheet.rb +28 -0
  30. data/lib/excel_templating/excel_abstraction/time.rb +42 -0
  31. data/lib/excel_templating/excel_abstraction/work_book.rb +47 -0
  32. data/lib/excel_templating/excel_abstraction.rb +16 -0
  33. data/lib/excel_templating/render_helper.rb +14 -0
  34. data/lib/excel_templating/renderer.rb +251 -0
  35. data/lib/excel_templating/rspec_excel_matcher.rb +129 -0
  36. data/lib/excel_templating/version.rb +4 -0
  37. data/lib/excel_templating.rb +4 -0
  38. data/spec/assets/alphalist_7_4.mustache.xlsx +0 -0
  39. data/spec/assets/alphalist_seven_four_expected.xlsx +0 -0
  40. data/spec/assets/valid_cell.mustache.xlsx +0 -0
  41. data/spec/assets/valid_cell_expected.xlsx +0 -0
  42. data/spec/assets/valid_cell_expected_inline.xlsx +0 -0
  43. data/spec/assets/valid_column_expected.xlsx +0 -0
  44. data/spec/cell_validation_spec.rb +114 -0
  45. data/spec/column_validation_spec.rb +47 -0
  46. data/spec/excel_abstraction/active_cell_reference_spec.rb +73 -0
  47. data/spec/excel_abstraction/cell_range_spec.rb +36 -0
  48. data/spec/excel_abstraction/cell_reference_spec.rb +69 -0
  49. data/spec/excel_abstraction/cell_spec.rb +54 -0
  50. data/spec/excel_abstraction/date_spec.rb +27 -0
  51. data/spec/excel_abstraction/row_spec.rb +42 -0
  52. data/spec/excel_abstraction/sheet_spec.rb +83 -0
  53. data/spec/excel_abstraction/spread_sheet_spec.rb +35 -0
  54. data/spec/excel_abstraction/time_spec.rb +27 -0
  55. data/spec/excel_abstraction/work_book_spec.rb +22 -0
  56. data/spec/excel_helper.rb +16 -0
  57. data/spec/excel_templating_spec.rb +141 -0
  58. data/spec/spec_helper.rb +13 -0
  59. metadata +281 -0
@@ -0,0 +1,102 @@
1
+ module ExcelAbstraction
2
+ class Sheet < SimpleDelegator
3
+ attr_reader :active_cell_reference
4
+
5
+ def initialize(sheet, workbook)
6
+ super(sheet)
7
+ @workbook = workbook
8
+ @active_cell_reference = ExcelAbstraction::ActiveCellReference.new
9
+ end
10
+
11
+ def header(value, options = {})
12
+ cell(value, {bold: 1}.merge(options))
13
+ self
14
+ end
15
+
16
+ def headers(array, options = {})
17
+ Array(array).each { |element| header(element, options) }
18
+ self
19
+ end
20
+
21
+ # Fills the cell at the current pointer with the value and format or options specified
22
+ # @param [Object] value Value to place in the cell.
23
+ # @param [Object] options (optional) options to create a format object
24
+ # @param [Object] format (optional) The format to use when creating this cell
25
+ def cell(value, format: nil, type: :auto, **options)
26
+ format = format || _format(options)
27
+ value = Float(value) if value.is_a?(ExcelAbstraction::Time) || value.is_a?(ExcelAbstraction::Date)
28
+
29
+ writer = case type
30
+ when :string then method(:write_string)
31
+ else method(:write)
32
+ end
33
+
34
+ writer.call(active_cell_reference.row, active_cell_reference.col, value, format)
35
+
36
+ if options[:new_row]
37
+ next_row
38
+ else
39
+ active_cell_reference.right
40
+ end
41
+ self
42
+ end
43
+
44
+ # Advance the pointer to the next row.
45
+ # @return nil
46
+ def next_row
47
+ active_cell_reference.newline
48
+ end
49
+
50
+ def cells(array, options = {})
51
+ Array(array).each { |value| cell(value, options) }
52
+ self
53
+ end
54
+
55
+ def merge(length, value, options = {})
56
+ format = _format(options)
57
+ merge_range(active_cell_reference.row, active_cell_reference.col, active_cell_reference.row, active_cell_reference.col + length, value, format)
58
+ active_cell_reference.right(length + 1)
59
+ self
60
+ end
61
+
62
+ def style_row(row, properties = {})
63
+ height = properties[:height]
64
+ format = _format(properties[:format] || {})
65
+ hidden = properties[:hidden] || 0
66
+ outline_level = properties[:level] || 0
67
+ if outline_level
68
+ raise "Outline level can only be between 0 and 7" unless (0..7) === outline_level
69
+ end
70
+ collapse = properties[:collapse] || 0
71
+ set_row(row, height, format, hidden, outline_level, collapse)
72
+ self
73
+ end
74
+
75
+ # Style the numbered column
76
+ # @param [Integer] col 0 based index of the column.
77
+ # @param [Integer] width numeric width of the column (30 = 2.6 inches)
78
+ # @param [Hash] format Properties to set for the format of the column
79
+ # @param [Integer] level Outline level
80
+ # @param [Integer] collapse 1 = collapse, 0 = do not collapse
81
+ def style_col(col, width: nil, format: {}, level: 0, collapse: 0, hidden: nil)
82
+ format = _format(format || {})
83
+ if level
84
+ raise "Outline level can only be between 0 and 7" unless (0..7) === level
85
+ end
86
+ set_column(col, col, width, format, hidden, level, collapse)
87
+ self
88
+ end
89
+
90
+ private
91
+
92
+ def _format(options)
93
+ format = @workbook.add_format
94
+ format.set_align('valign')
95
+ options.each do |key, value|
96
+ method = "set_#{key.to_s}".to_sym
97
+ format.send(method, value) if format.respond_to?(method)
98
+ end
99
+ format
100
+ end
101
+ end
102
+ end
@@ -0,0 +1,28 @@
1
+ require 'tempfile'
2
+
3
+ module ExcelAbstraction
4
+ class SpreadSheet < DelegateClass(Tempfile)
5
+ attr_reader :workbook
6
+
7
+ def initialize(format: :xls, skip_default_sheet: false)
8
+ extension = format == :xls ? ".xls" : ".xlsx"
9
+ tmp_file = Tempfile.new(["temp_spreadsheet_#{::Time.now.to_i}", extension])
10
+ super(tmp_file)
11
+ @workbook = ExcelAbstraction::WorkBook.new(tmp_file.path, format: format, skip_default_sheet: skip_default_sheet)
12
+ end
13
+
14
+ def close
15
+ workbook.close
16
+ yield if block_given?
17
+ super
18
+ end
19
+
20
+ def to_s
21
+ data = nil
22
+ close do
23
+ data = read
24
+ end
25
+ data
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,42 @@
1
+ module ExcelAbstraction
2
+ class Time < SimpleDelegator
3
+ ADJUSTMENT = "1900-03-01 00:00 +00:00"
4
+ REFERENCE = "1900-01-01 00:00 +00:00"
5
+
6
+ attr_reader :value
7
+
8
+ def initialize(raw_value)
9
+ super(convert(raw_value))
10
+ end
11
+
12
+ def to_excel_time
13
+ self
14
+ end
15
+
16
+ private
17
+
18
+ def adjustment
19
+ ::Time.parse(ADJUSTMENT)
20
+ end
21
+
22
+ def reference
23
+ ::Time.parse(REFERENCE)
24
+ end
25
+
26
+ def adjust(raw_value)
27
+ adjustment < raw_value ? two_days : one_day
28
+ end
29
+
30
+ def two_days
31
+ one_day * 2.0
32
+ end
33
+
34
+ def one_day
35
+ 60 * 60 * 24.to_f
36
+ end
37
+
38
+ def convert(raw_value)
39
+ (raw_value - reference + adjust(raw_value)).to_f / one_day
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,47 @@
1
+ module ExcelAbstraction
2
+ class WorkBook < SimpleDelegator
3
+ attr_accessor :active_sheet
4
+
5
+ def initialize(file, format: :xls, skip_default_sheet: false)
6
+ @format = format
7
+ @file = file
8
+ super(workbook)
9
+ unless skip_default_sheet
10
+ @active_sheet = ExcelAbstraction::Sheet.new(workbook.add_worksheet, workbook)
11
+ end
12
+ end
13
+
14
+
15
+ def title(text)
16
+ set_properties(title: text)
17
+ self
18
+ end
19
+
20
+ def organization(name)
21
+ set_properties(company: name)
22
+ self
23
+ end
24
+
25
+ private
26
+
27
+ attr_reader :format, :file
28
+
29
+ def default_options
30
+ {
31
+ :font => 'Calibri',
32
+ :size => 12,
33
+ :align => 'center',
34
+ :text_wrap => 1
35
+ }
36
+ end
37
+
38
+ def workbook
39
+ @workbook ||= if format == :xlsx
40
+ WriteXLSX.new(file, default_options)
41
+ else
42
+ WriteExcel.new(file, default_options)
43
+ end
44
+ end
45
+ end
46
+
47
+ end
@@ -0,0 +1,16 @@
1
+ require 'write_xlsx'
2
+ require 'writeexcel'
3
+
4
+ require_relative 'excel_abstraction/active_cell_reference'
5
+ require_relative 'excel_abstraction/cell'
6
+ require_relative 'excel_abstraction/cell_range'
7
+ require_relative 'excel_abstraction/cell_reference'
8
+ require_relative 'excel_abstraction/date'
9
+ require_relative 'excel_abstraction/row'
10
+ require_relative 'excel_abstraction/sheet'
11
+ require_relative 'excel_abstraction/spread_sheet'
12
+ require_relative 'excel_abstraction/time'
13
+ require_relative 'excel_abstraction/work_book'
14
+
15
+ module ExcelAbstraction
16
+ end
@@ -0,0 +1,14 @@
1
+ module ExcelTemplating
2
+ module RenderHelper
3
+ class << self
4
+ def letter_for(one_based_index)
5
+ letters[one_based_index-1]
6
+ end
7
+
8
+ private
9
+ def letters
10
+ @row_letters ||= ('A' .. 'ZZ').to_a
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,251 @@
1
+ require_relative 'excel_abstraction'
2
+ require 'mustache'
3
+
4
+ module ExcelTemplating
5
+ # Render class for ExcelTemplating Documents. Used by the Document to render the defined document
6
+ # with the data to a new file. Responsible for reading the template and applying the data to it
7
+ class Renderer
8
+ extend Forwardable
9
+
10
+ # @param [ExcelTemplating::Document] document Document to render with
11
+ def initialize(document)
12
+ @template_document = document
13
+ @data_source_registry = document.class.data_source_registry
14
+ end
15
+
16
+ # Render the document provided. Yields the path to the tempfile created.
17
+ def render
18
+ @spreadsheet = ExcelAbstraction::SpreadSheet.new(format: :xlsx)
19
+ @template = Roo::Spreadsheet.open(template_path)
20
+ @registry_renderer = data_source_registry.renderer(data: data[:all_sheets])
21
+ apply_document_level_items
22
+ apply_data_to_sheets
23
+ protect_spreadsheet
24
+ registry_renderer.write_sheet(@spreadsheet.workbook)
25
+
26
+ @spreadsheet.close
27
+ yield(spreadsheet.path)
28
+ end
29
+
30
+ private
31
+
32
+ attr_reader :template_document, :spreadsheet, :template, :registry_renderer, :data_source_registry, :current_sheet
33
+ delegate [:workbook] => :spreadsheet
34
+ delegate [:data] => :template_document
35
+ delegate [:active_sheet] => :workbook
36
+ delegate [:active_cell_reference] => :active_sheet
37
+
38
+ def current_row
39
+ active_cell_reference.row
40
+ end
41
+
42
+ def current_col
43
+ active_cell_reference.col
44
+ end
45
+
46
+ def template_path
47
+ template_document.class.template_path
48
+ end
49
+
50
+ def default_format_styling
51
+ template_document.class.document_default_styling
52
+ end
53
+
54
+ def protected?
55
+ template_document.class.protected?
56
+ end
57
+
58
+ def sheets
59
+ template_document.class.sheets
60
+ end
61
+
62
+ def apply_document_level_items
63
+ workbook.title mustachify(template_document.class.document_title, locals: common_data_variables)
64
+ workbook.organization mustachify(template_document.class.document_organization, locals: common_data_variables)
65
+ end
66
+
67
+ def common_data_variables
68
+ stringify_keys(data[:all_sheets])
69
+ end
70
+
71
+ def stringify_keys(hash)
72
+ Hash[hash.map { |k, v| [k.to_s, v] } ]
73
+ end
74
+
75
+ def style_columns(sheet, template_sheet)
76
+ default_style = sheet.default_column_style
77
+ column_styles = sheet.column_styles
78
+ if column_styles || default_style
79
+ roo_columns(template_sheet).each do |column_number|
80
+ style = column_styles[column_number] || default_style
81
+ active_sheet.style_col(column_number - 1, style) # Note: Styling columns is zero indexed
82
+ end
83
+ end
84
+ end
85
+
86
+ def style_rows(sheet, template_sheet)
87
+ default_style = sheet.default_row_style
88
+ row_styles = sheet.row_styles
89
+ if row_styles || default_style
90
+ roo_rows(template_sheet).each do |row_number|
91
+ style = row_styles[row_number] || default_style
92
+ active_sheet.style_row(row_number - 1, style) # Note: Styling rows is zero indexed
93
+ end
94
+ end
95
+ end
96
+
97
+ def roo_columns(roo_sheet)
98
+ (roo_sheet.first_column .. roo_sheet.last_column)
99
+ end
100
+
101
+ def roo_rows(roo_sheet)
102
+ (roo_sheet.first_row .. roo_sheet.last_row)
103
+ end
104
+
105
+ def protect_spreadsheet
106
+ active_sheet.protect if protected?
107
+ end
108
+
109
+ def apply_data_to_sheets
110
+ sheets.each_with_index do |sheet, sheet_number|
111
+ @current_sheet = sheet
112
+ sheet_data = sheet.sheet_data(data)
113
+ template_sheet = template.sheet(sheet_number)
114
+
115
+ # column and row styles should be applied before writing any data to the sheet
116
+ style_columns(sheet, template_sheet)
117
+ # row styles have priority over column styles
118
+ style_rows(sheet, template_sheet)
119
+
120
+ roo_rows(template_sheet).each do |row_number|
121
+ sheet.each_row_at(row_number, sheet_data) do |row_data|
122
+ local_data = stringify_keys(data[:all_sheets]).merge(stringify_keys(row_data))
123
+ roo_columns(template_sheet).each do |column_number|
124
+ apply_data_to_cell(local_data, template_sheet, row_number, column_number)
125
+ if sheet.validated_cell?(row_number, column_number)
126
+ add_validation(sheet, row_number, column_number)
127
+ end
128
+ end
129
+ active_sheet.next_row
130
+ end
131
+ end
132
+ end
133
+ end
134
+
135
+ def apply_data_to_cell(local_data, template_sheet, row_number, column_number)
136
+ template_cell = template_sheet.cell(row_number, column_number)
137
+ font = template_sheet.font(row_number, column_number)
138
+ format = format_for(font, row_number, column_number)
139
+ value = mustachify(template_cell, locals: local_data)
140
+
141
+ active_sheet.cell(
142
+ value,
143
+ type: type_for_value(value),
144
+ format: format
145
+ )
146
+ end
147
+
148
+ def type_for_value(value)
149
+ looks_like_a_separator?(value) ? :string : :auto
150
+ end
151
+
152
+ def looks_like_a_separator?(value)
153
+ value.is_a?(String) && value =~ /^==/
154
+ end
155
+
156
+ def format_for(font, row_number, column_number)
157
+ format = font_formats(font)
158
+
159
+ set_column_lock(format, column_number) if column_is_locked?(column_number)
160
+ set_row_lock(format, row_number) if row_is_locked?(row_number)
161
+
162
+ format
163
+ end
164
+
165
+ def column_is_locked?(column_number)
166
+ row_or_column_is_locked? current_sheet.column_styles[column_number]
167
+ end
168
+
169
+ def row_is_locked?(row_number)
170
+ row_or_column_is_locked? current_sheet.row_styles[row_number]
171
+ end
172
+
173
+ def row_or_column_is_locked?(row_or_column)
174
+ row_or_column && row_or_column[:format] && row_or_column[:format].has_key?(:locked)
175
+ end
176
+
177
+ def row_or_column_lock_attribute(row_or_column)
178
+ row_or_column && row_or_column[:format] && row_or_column[:format][:locked]
179
+ end
180
+
181
+ def set_row_lock(format, row_number)
182
+ format.set_locked row_or_column_lock_attribute(current_sheet.row_styles[row_number])
183
+ end
184
+
185
+ def set_column_lock(format, column_number)
186
+ format.set_locked row_or_column_lock_attribute(current_sheet.column_styles[column_number])
187
+ end
188
+
189
+ def font_formats(font=nil)
190
+ template_font = font || Roo::Font.new
191
+
192
+ format_details = {
193
+ bold: template_font.bold? ? 1 : 0,
194
+ italic: template_font.italic? ? 1 : 0,
195
+ underline: template_font.underline? ? 1 : 0
196
+ }.merge(default_format_styling)
197
+
198
+ workbook.add_format format_details
199
+ end
200
+
201
+ def mustachify(inline_template, locals: {})
202
+ if whole_cell_template?(inline_template)
203
+ locals.fetch(extract_key_from_template(inline_template))
204
+ elsif no_mustache_found?(inline_template)
205
+ inline_template
206
+ else
207
+ MustacheRenderer.render(inline_template, locals)
208
+ end
209
+ end
210
+
211
+ def no_mustache_found?(value)
212
+ !value.is_a?(String) || !(value.match(/{{.+}}/))
213
+ end
214
+
215
+ def extract_key_from_template(template)
216
+ template[whole_cell_template_matcher, 1]
217
+ end
218
+
219
+ def whole_cell_template_matcher
220
+ /^{{([^{}]+)}}$/
221
+ end
222
+
223
+ def whole_cell_template?(template)
224
+ template =~ whole_cell_template_matcher
225
+ end
226
+
227
+ def add_validation(sheet, row_number, column_number)
228
+ raise ArgumentError, "No :data_sources defined for validation!" unless data_source_registry
229
+ source = sheet.validation_source_name(row_number, column_number)
230
+ #Use current_row and current_col here because row_number and column_number refer to the template
231
+ #sheet and we want to write a reference to the cell we just wrote
232
+ active_sheet.data_validation absolute_reference(current_row + 1, current_col),
233
+ registry_renderer.absolute_reference_for(source)
234
+ end
235
+
236
+ def absolute_reference(row_number, column_number)
237
+ "$#{RenderHelper.letter_for(column_number)}$#{row_number}"
238
+ end
239
+
240
+ class MustacheRenderer < Mustache
241
+
242
+ def self.render(template, locals)
243
+ renderer = new
244
+ renderer.template = template
245
+ renderer.raise_on_context_miss = true
246
+ renderer.render(locals)
247
+ end
248
+
249
+ end
250
+ end
251
+ end
@@ -0,0 +1,129 @@
1
+ require 'roo'
2
+ require "roo-xls"
3
+
4
+ # Matcher for rspec 'match_excel_content'
5
+ # @example
6
+ # expect do
7
+ # subject.render do |path|
8
+ # expect(path).to match_excel_content('spec/assets/spreadsheets/seven_three_expected.xlsx')
9
+ # end
10
+ # end
11
+ RSpec::Matchers.define :match_excel_content do |expected_excel_path|
12
+ match do |excel_path|
13
+ @excel_matcher = RSpec::Matchers::ExcelMatcher.new
14
+ @excel_matcher.expected_roo = Roo::Spreadsheet.open(expected_excel_path)
15
+ @excel_matcher.actual_roo = Roo::Spreadsheet.open(excel_path)
16
+ @excel_matcher.match?
17
+ end
18
+
19
+ failure_message do |excel_path|
20
+ messages = ["expected excel:#{expected_excel_path} to exactly match the content of #{excel_path} but did not."]
21
+ @excel_matcher.errors.group_by(&:first).each do |sheet_name, errors|
22
+ messages << "#{sheet_name}:"
23
+ errors.each do |_, error|
24
+ messages << " #{error}:"
25
+ end
26
+ end
27
+ messages.join("\n")
28
+ end
29
+
30
+ failure_message_when_negated do |excel_path|
31
+ "expected excel:#{expected_excel_path} to not exactly match the content of #{excel_path} but it did."
32
+ end
33
+ end
34
+
35
+ # Specific Matcher class helper for comparing two excel documents.
36
+ class RSpec::Matchers::ExcelMatcher
37
+
38
+ def initialize
39
+ @errors = []
40
+ end
41
+
42
+ def match?
43
+ sheet_count_equal? && all_sheets_equal?
44
+ end
45
+
46
+ attr_accessor :errors, :expected_roo, :actual_roo
47
+
48
+ private
49
+
50
+ def check(sheet_name, check_result, fail_msg)
51
+ if check_result
52
+ true
53
+ else
54
+ errors << [sheet_name, fail_msg]
55
+ false
56
+ end
57
+ end
58
+
59
+ def sheet_count_equal?
60
+ check(:base, expected_roo.sheets.count == actual_roo.sheets.count, "Number of sheets do not match.")
61
+ end
62
+
63
+ def all_sheets_equal?
64
+ sheets(expected_roo).zip(sheets(actual_roo)).all? do |expected_sheet, actual_sheet|
65
+ sheets_equal?(expected_sheet, actual_sheet)
66
+ end
67
+ end
68
+
69
+ def sheets(roo)
70
+ (0 .. (roo.sheets.count - 1)).map do |i|
71
+ roo.sheet(i)
72
+ end
73
+ end
74
+
75
+ def sheets_equal?(expected_workbook, actual_workbook)
76
+ expected_workbook.sheets.all? do |sheet_name|
77
+ expected = expected_workbook.sheet(sheet_name)
78
+ actual = begin
79
+ actual_workbook.sheet(sheet_name)
80
+ rescue
81
+ nil
82
+ end
83
+
84
+ check(sheet_name, actual, "Sheet names do not match.") if actual != nil
85
+ # check(sheet_name, expected.first_row == actual.first_row, "Number of rows do not match.")
86
+ # check(sheet_name, expected.last_row == actual.last_row, "Number of rows do not match")
87
+ # check(sheet_name, expected.first_column == actual.first_column, "Number of columns do not match")
88
+ # check(sheet_name, expected.last_column == actual.last_column, "Number of columns do not match")
89
+ check_cells(sheet_name, expected, actual)
90
+
91
+ errors.empty?
92
+ end
93
+ end
94
+
95
+ def check_cells(sheet_name, sheet1, sheet2)
96
+ (sheet1.first_row .. sheet1.last_row).each do |row|
97
+ (sheet1.first_column .. sheet1.last_column).each do |col|
98
+ check(sheet_name, compare_value(col, row, sheet1, sheet2), discrepancy_message(col, row, sheet1, sheet2))
99
+ end
100
+ end
101
+ end
102
+
103
+ def compare_value(col, row, sheet1, sheet2)
104
+ value2 = sheet2.cell(row, col)
105
+ value1 = sheet1.cell(row, col)
106
+ if value2.class == Float
107
+ value2 == value1.to_f
108
+ elsif value1.class == Float
109
+ value1 == value2.to_f
110
+ else
111
+ value2 == value1
112
+ end
113
+ end
114
+
115
+ def discrepancy_message(col, row, sheet1, sheet2)
116
+ cell_id = "#{column_letter(col)}#{row}"
117
+ actual_value = sheet2.cell(row, col).inspect
118
+ expected_value = sheet1.cell(row, col).inspect
119
+ "Cell #{cell_id} actual:#{actual_value} expected:#{expected_value}"
120
+ end
121
+
122
+ def column_letter(col_number)
123
+ column_letters[col_number - 1]
124
+ end
125
+
126
+ def column_letters
127
+ @column_letters ||= ('A' .. 'ZZ').to_a
128
+ end
129
+ end
@@ -0,0 +1,4 @@
1
+ module ExcelTemplating
2
+ # excel_templating version
3
+ VERSION = "0.3.2"
4
+ end
@@ -0,0 +1,4 @@
1
+ require 'excel_templating/version'
2
+ require 'excel_templating/renderer'
3
+ require 'excel_templating/document'
4
+ require 'excel_templating/render_helper'