datashift 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (108) hide show
  1. data/.document +5 -0
  2. data/Gemfile +25 -0
  3. data/Gemfile.lock +211 -0
  4. data/LICENSE.txt +27 -0
  5. data/README.markdown +286 -0
  6. data/README.rdoc +19 -0
  7. data/Rakefile +96 -0
  8. data/VERSION +5 -0
  9. data/bin/autospec +16 -0
  10. data/bin/convert_to_should_syntax +16 -0
  11. data/bin/erubis +16 -0
  12. data/bin/htmldiff +16 -0
  13. data/bin/jeweler +16 -0
  14. data/bin/ldiff +16 -0
  15. data/bin/nokogiri +16 -0
  16. data/bin/rackup +16 -0
  17. data/bin/rails +16 -0
  18. data/bin/rake +16 -0
  19. data/bin/rake2thor +16 -0
  20. data/bin/ri +16 -0
  21. data/bin/rspec +16 -0
  22. data/bin/spree +16 -0
  23. data/bin/thor +16 -0
  24. data/bin/tilt +16 -0
  25. data/bin/tt +16 -0
  26. data/datashift.gemspec +178 -0
  27. data/lib/applications/jruby/jexcel_file.rb +397 -0
  28. data/lib/applications/jruby/word.rb +79 -0
  29. data/lib/datashift.rb +114 -0
  30. data/lib/datashift/exceptions.rb +12 -0
  31. data/lib/datashift/file_definitions.rb +353 -0
  32. data/lib/datashift/mapping_file_definitions.rb +88 -0
  33. data/lib/datashift/method_detail.rb +237 -0
  34. data/lib/datashift/method_mapper.rb +257 -0
  35. data/lib/generators/csv_generator.rb +36 -0
  36. data/lib/generators/excel_generator.rb +122 -0
  37. data/lib/generators/generator_base.rb +14 -0
  38. data/lib/helpers/core_ext/to_b.rb +24 -0
  39. data/lib/helpers/spree_helper.rb +131 -0
  40. data/lib/java/poi-3.7/._poi-3.7-20101029.jar5645100390082102460.tmp +0 -0
  41. data/lib/java/poi-3.7/LICENSE +507 -0
  42. data/lib/java/poi-3.7/NOTICE +21 -0
  43. data/lib/java/poi-3.7/RELEASE_NOTES.txt +115 -0
  44. data/lib/java/poi-3.7/lib/commons-logging-1.1.jar +0 -0
  45. data/lib/java/poi-3.7/lib/junit-3.8.1.jar +0 -0
  46. data/lib/java/poi-3.7/lib/log4j-1.2.13.jar +0 -0
  47. data/lib/java/poi-3.7/ooxml-lib/dom4j-1.6.1.jar +0 -0
  48. data/lib/java/poi-3.7/ooxml-lib/geronimo-stax-api_1.0_spec-1.0.jar +0 -0
  49. data/lib/java/poi-3.7/ooxml-lib/xmlbeans-2.3.0.jar +0 -0
  50. data/lib/java/poi-3.7/poi-3.7-20101029.jar +0 -0
  51. data/lib/java/poi-3.7/poi-examples-3.7-20101029.jar +0 -0
  52. data/lib/java/poi-3.7/poi-ooxml-3.7-20101029.jar +0 -0
  53. data/lib/java/poi-3.7/poi-ooxml-schemas-3.7-20101029.jar +0 -0
  54. data/lib/java/poi-3.7/poi-scratchpad-3.7-20101029.jar +0 -0
  55. data/lib/loaders/csv_loader.rb +99 -0
  56. data/lib/loaders/excel_loader.rb +150 -0
  57. data/lib/loaders/loader_base.rb +332 -0
  58. data/lib/loaders/spreadsheet_loader.rb +137 -0
  59. data/lib/loaders/spree/image_loader.rb +46 -0
  60. data/lib/loaders/spree/product_loader.rb +225 -0
  61. data/spec/csv_loader_spec.rb +31 -0
  62. data/spec/datashift_spec.rb +27 -0
  63. data/spec/db/migrate/20110803201325_create_test_bed.rb +85 -0
  64. data/spec/excel_generator_spec.rb +79 -0
  65. data/spec/excel_loader_spec.rb +177 -0
  66. data/spec/file_definitions.rb +141 -0
  67. data/spec/fixtures/BadAssociationName.xls +0 -0
  68. data/spec/fixtures/DemoNegativeTesting.xls +0 -0
  69. data/spec/fixtures/ProjectsMultiCategories.xls +0 -0
  70. data/spec/fixtures/ProjectsSingleCategories.xls +0 -0
  71. data/spec/fixtures/SimpleProjects.xls +0 -0
  72. data/spec/fixtures/config/database.yml +25 -0
  73. data/spec/fixtures/interact_models_db.sqlite +0 -0
  74. data/spec/fixtures/interact_spree_db.sqlite +0 -0
  75. data/spec/fixtures/negative/SpreeProdMiss1Mandatory.csv +4 -0
  76. data/spec/fixtures/negative/SpreeProdMiss1Mandatory.xls +0 -0
  77. data/spec/fixtures/negative/SpreeProdMissManyMandatory.csv +4 -0
  78. data/spec/fixtures/negative/SpreeProdMissManyMandatory.xls +0 -0
  79. data/spec/fixtures/simple_export_spec.xls +0 -0
  80. data/spec/fixtures/simple_template_spec.xls +0 -0
  81. data/spec/fixtures/spree/SpreeProducts.csv +4 -0
  82. data/spec/fixtures/spree/SpreeProducts.xls +0 -0
  83. data/spec/fixtures/spree/SpreeProductsMultiColumn.csv +4 -0
  84. data/spec/fixtures/spree/SpreeProductsMultiColumn.xls +0 -0
  85. data/spec/fixtures/spree/SpreeProductsSimple.csv +4 -0
  86. data/spec/fixtures/spree/SpreeProductsSimple.xls +0 -0
  87. data/spec/fixtures/spree/SpreeZoneExample.csv +5 -0
  88. data/spec/fixtures/spree/SpreeZoneExample.xls +0 -0
  89. data/spec/fixtures/test_model_defs.rb +57 -0
  90. data/spec/loader_spec.rb +121 -0
  91. data/spec/method_mapper_spec.rb +238 -0
  92. data/spec/spec_helper.rb +116 -0
  93. data/spec/spree_generator_spec.rb +65 -0
  94. data/spec/spree_loader_spec.rb +311 -0
  95. data/spec/spree_method_mapping_spec.rb +215 -0
  96. data/tasks/config/seed_fu_product_template.erb +15 -0
  97. data/tasks/config/tidy_config.txt +13 -0
  98. data/tasks/db_tasks.rake +65 -0
  99. data/tasks/excel_generator.rake +79 -0
  100. data/tasks/file_tasks.rake +37 -0
  101. data/tasks/import/csv.rake +50 -0
  102. data/tasks/import/excel.rake +67 -0
  103. data/tasks/spree/image_load.rake +109 -0
  104. data/tasks/spree/product_loader.rake +44 -0
  105. data/tasks/word_to_seedfu.rake +167 -0
  106. data/test/helper.rb +18 -0
  107. data/test/test_interact.rb +7 -0
  108. metadata +301 -0
@@ -0,0 +1,397 @@
1
+ # Copyright:: (c) Autotelik Media Ltd 2011
2
+ # Author :: Tom Statter
3
+ # Date :: Aug 2010
4
+ # License:: MIT
5
+ #
6
+ # An Excel file helper. Create and populate XSL files
7
+ #
8
+ # The maximum number of columns and rows in an Excel file is fixed at 256 Columns and 65536 Rows
9
+ #
10
+ # POI jar location needs to be added to class path.
11
+ #
12
+ # TODO - Check out http://poi.apache.org/poi-ruby.html
13
+ #
14
+ if(DataShift::Guards::jruby?)
15
+
16
+ require "poi-3.7-20101029.jar"
17
+
18
+ class JExcelFile
19
+
20
+ java_import org.apache.poi.poifs.filesystem.POIFSFileSystem
21
+
22
+ include_class 'org.apache.poi.hssf.usermodel.HSSFCell'
23
+ include_class 'org.apache.poi.hssf.usermodel.HSSFWorkbook'
24
+ include_class 'org.apache.poi.hssf.usermodel.HSSFCellStyle'
25
+ include_class 'org.apache.poi.hssf.usermodel.HSSFDataFormat'
26
+ include_class 'org.apache.poi.hssf.usermodel.HSSFClientAnchor'
27
+ include_class 'org.apache.poi.hssf.usermodel.HSSFRichTextString'
28
+
29
+ include_class 'java.io.ByteArrayOutputStream'
30
+ include_class 'java.util.Date'
31
+ include_class 'java.io.FileInputStream'
32
+ include_class 'java.io.FileOutputStream'
33
+
34
+ attr_accessor :book, :row, :date_style
35
+ attr_reader :sheet
36
+
37
+ MAX_COLUMNS = 256.freeze
38
+
39
+ def self.date_format
40
+ HSSFDataFormat.getBuiltinFormat("m/d/yy h:mm")
41
+ end
42
+
43
+ # NOTE: this is the POI 3.7 HSSF maximum rows
44
+ def self.maxrows
45
+ return 65535
46
+ end
47
+
48
+ # The HSSFWorkbook uses 0 based indexes, whilst our companion jexcel_win32 class
49
+ # uses 1 based indexes. So they can be used interchangeably we bring indexes
50
+ # inline with JExcel usage in this class, as 1 based maps more intuitively for the user
51
+ #
52
+ # i.e Row 1 passed to this class, internally means Row 0
53
+
54
+ def initialize()
55
+ @book = nil
56
+ # The @patriarchs hash is a workaround because HSSFSheet.getDrawingPatriarch()
57
+ # causes a lot of issues (if it doesn't throw an exception!)
58
+ @patriarchs = Hash.new
59
+
60
+ @date_style = nil
61
+ end
62
+
63
+ def open(filename)
64
+ inp = FileInputStream.new(filename)
65
+
66
+ @book = HSSFWorkbook.new(inp)
67
+
68
+ @date_style = @book.createCellStyle
69
+ @date_style.setDataFormat( JExcelFile::date_format )
70
+
71
+ @current_sheet = 0
72
+ sheet(@current_sheet)
73
+ end
74
+
75
+ # EXCEL ITEMS
76
+
77
+ def create(sheet_name)
78
+ @book = HSSFWorkbook.new() if @book.nil?
79
+
80
+ # Double check sheet doesn't already exist
81
+ if(@book.getSheetIndex(sheet_name) < 0)
82
+ sheet = @book.createSheet(sheet_name.gsub(" ", ''))
83
+
84
+ @patriarchs.store(sheet_name, sheet.createDrawingPatriarch())
85
+ end
86
+ @current_sheet = @book.getSheetIndex(sheet_name)
87
+
88
+ @date_style = @book.createCellStyle
89
+ @date_style.setDataFormat( JExcelFile::date_format )
90
+
91
+ self.sheet()
92
+ end
93
+
94
+ alias_method(:create_sheet, :create)
95
+
96
+ # Return the current or specified HSSFSheet
97
+ def sheet(i = nil)
98
+ @current_sheet = i if i
99
+ @sheet = @book.getSheetAt(@current_sheet)
100
+ end
101
+
102
+ def activate_sheet(sheet)
103
+ active_sheet = @current_sheet
104
+ if(@book)
105
+ i = sheet if sheet.kind_of?(Integer)
106
+ i = @book.getSheetIndex(sheet) if sheet.kind_of?(String)
107
+
108
+ if( i >= 0 )
109
+ @book.setActiveSheet(i) unless @book.nil?
110
+ active_sheet = @book.getSheetAt(i)
111
+ active_sheet.setActive(true)
112
+ end unless i.nil?
113
+ end
114
+ return active_sheet
115
+ end
116
+
117
+ def num_rows
118
+ @sheet.getPhysicalNumberOfRows
119
+ end
120
+
121
+ # Process each row. (type is org.apache.poi.hssf.usermodel.HSSFRow)
122
+
123
+ def each_row
124
+ @sheet.rowIterator.each { |row| yield row }
125
+ end
126
+
127
+ # Create new row, bring index in line with POI usage (our 1 is their 0)
128
+ def create_row(index)
129
+ return if @sheet.nil?
130
+ raise "BAD INDEX: Row indexing starts at 1" if(index == 0)
131
+ @row = @sheet.createRow(index - 1)
132
+ @row
133
+ end
134
+
135
+ #############################
136
+ # INSERTING DATA INTO EXCEL #
137
+ #############################
138
+
139
+ # Populate a single cell with data
140
+ #
141
+ def set_cell(row, column, datum)
142
+ @row = @sheet.getRow(row - 1) || create_row(row)
143
+ @row.createCell(column - 1, excel_cell_type(datum)).setCellValue(datum)
144
+ end
145
+
146
+ # Convert array into a header row
147
+ def set_headers(headers)
148
+ create_row(1)
149
+ return if headers.empty?
150
+
151
+ set_row(1, 1, headers)
152
+ end
153
+
154
+ # Populate a row of cells with data in an array
155
+ # where the co-ordinates relate to row/column start position
156
+ #
157
+ def set_row( row, col, data, sheet_num = nil)
158
+
159
+ sheet(sheet_num)
160
+
161
+ create_row(row)
162
+
163
+ column = col
164
+ data.each do |datum|
165
+ set_cell(row, column, datum)
166
+ column += 1
167
+ end
168
+ end
169
+
170
+ # Return a mapping from Ruby type to type for HSSFCell
171
+ def excel_cell_type(data)
172
+
173
+ if(data.kind_of?(Numeric))
174
+ HSSFCell::CELL_TYPE_NUMERIC
175
+ elsif(data.nil?)
176
+ HSSFCell::CELL_TYPE_BLANK
177
+ elsif(data.is_a?(TrueClass) || data.is_a?(FalseClass))
178
+ HSSFCell::CELL_TYPE_BOOLEAN
179
+ else
180
+ HSSFCell::CELL_TYPE_STRING
181
+ end
182
+ # HSSFCell::CELL_TYPE_FORMULA
183
+ end
184
+
185
+ # TODO - Move into an ActiveRecord helper module of it's own
186
+ def ar_to_headers( records )
187
+ return if( !records.first.is_a?(ActiveRecord::Base) || records.empty?)
188
+
189
+ headers = records.first.class.columns.collect( &:name )
190
+ set_headers( headers )
191
+ end
192
+
193
+ # Pass a set of AR records
194
+ def ar_to_xls(records, options = {})
195
+ return if( ! records.first.is_a?(ActiveRecord::Base) || records.empty?)
196
+
197
+ row_index =
198
+ if(options[:no_headers])
199
+ 1
200
+ else
201
+ ar_to_headers( records )
202
+ 2
203
+ end
204
+
205
+ records.each do |record|
206
+ create_row(row_index)
207
+
208
+ ar_to_xls_row(1, record)
209
+
210
+ row_index += 1
211
+ end
212
+ end
213
+
214
+ # Save data from an AR record to the current row, based on the record's columns [c1,c2,c3]
215
+
216
+ def ar_to_xls_row(start_column, record)
217
+ return unless( record.is_a?(ActiveRecord::Base))
218
+
219
+ record.class.columns.each do |connection_column|
220
+ ar_to_xls_cell(start_column, record, connection_column)
221
+ start_column += 1
222
+ end
223
+ end
224
+
225
+ def ar_to_xls_cell(column, record, connection_column)
226
+ datum = record.send(connection_column.name)
227
+
228
+ if(connection_column.sql_type =~ /date/) then
229
+ @row.createCell(column - 1, HSSFCell::CELL_TYPE_STRING).setCellValue(datum.to_s)
230
+ elsif connection_column.sql_type =~ /int/ then
231
+ @row.createCell(column - 1, HSSFCell::CELL_TYPE_NUMERIC).setCellValue(datum.to_i)
232
+ else
233
+ @row.createCell(column - 1, HSSFCell::CELL_TYPE_STRING).setCellValue( datum.to_s )
234
+ end
235
+ end
236
+
237
+ ##############################
238
+ # RETRIEVING DATA FROM EXCEL #
239
+ ##############################
240
+
241
+ # Return the raw data of the requested cell by row/column
242
+ def get_cell_value(row, column)
243
+ raise TypeError, "Expect row argument of type HSSFRow" unless row.is_a?(Java::OrgApachePoiHssfUsermodel::HSSFRow)
244
+ cell_value( row.getCell(column) )
245
+ end
246
+
247
+ # Return the raw data of an HSSFCell
248
+ def cell_value(cell)
249
+ return unless cell
250
+ #puts "DEBUG CELL TYPE : #{cell} => #{cell.getCellType().inspect}"
251
+ case (cell.getCellType())
252
+ when HSSFCell::CELL_TYPE_FORMULA then return cell.getCellFormula()
253
+ when HSSFCell::CELL_TYPE_NUMERIC then return cell.getNumericCellValue()
254
+ when HSSFCell::CELL_TYPE_STRING then return cell.getStringCellValue()
255
+ when HSSFCell::CELL_TYPE_BOOLEAN then return cell.getBooleanCellValue()
256
+ when HSSFCell::CELL_TYPE_BLANK then return ""
257
+ end
258
+ end
259
+
260
+ def save( filename = nil )
261
+ filename.nil? ? file = @filepath : file = filename
262
+ out = FileOutputStream.new(file)
263
+ @book.write(out) unless @book.nil?
264
+ out.close
265
+ end
266
+
267
+ def save_to_text( filename )
268
+ File.open( filename, 'w') {|f| f.write(to_s) }
269
+ end
270
+
271
+
272
+ def add_comment( cell, text )
273
+ raise "Please supply valid HSSFCell" unless cell.respond_to?('setCellComment')
274
+ return if @sheet.nil?
275
+
276
+ patriarch = @patriarchs[@sheet.getSheetName()]
277
+
278
+ anchor = HSSFClientAnchor.new(100, 50, 100, 50, cell.getColumnIndex(), cell.getRowIndex(), cell.getColumnIndex()+3, cell.getRowIndex()+4)
279
+ comment = patriarch.createCellComment(anchor)
280
+
281
+ comment_text = HSSFRichTextString.new(text)
282
+ comment.setString(comment_text)
283
+ comment.setAuthor("Mapping")
284
+
285
+ cell.setCellComment(comment)
286
+ end
287
+
288
+ # The internal representation of a Excel File
289
+
290
+ # Get a percentage style
291
+ def getPercentStyle()
292
+ if (@percentCellStyle.nil? && @book)
293
+ @percentCellStyle = @book.createCellStyle();
294
+ @percentCellStyle.setDataFormat(HSSFDataFormat.getBuiltinFormat("0.00%"));
295
+ end
296
+ return @percentCellStyle
297
+ end
298
+
299
+ # Auto size either the given column index or all columns
300
+ def autosize(column = nil)
301
+ return if @sheet.nil?
302
+ if (column.kind_of? Integer)
303
+ @sheet.autoSizeColumn(column)
304
+ else
305
+ @sheet.getRow(0).cellIterator.each{|c| @sheet.autoSizeColumn(c.getColumnIndex)}
306
+ end
307
+ end
308
+
309
+ def to_s
310
+ return "" unless @book
311
+
312
+ outs = ByteArrayOutputStream.new
313
+ @book.write(outs);
314
+ outs.close();
315
+ String.from_java_bytes(outs.toByteArray)
316
+ end
317
+
318
+ def createFreezePane(row=1, column=0)
319
+ return if @sheet.nil?
320
+ @sheet.createFreezePane(row, column)
321
+ end
322
+
323
+ # Use execute to run sql query provided
324
+ # and write to a csv file (path required)
325
+ # header row is optional but default is on
326
+ # Auto mapping of specified columns is optional
327
+ # @mappings is a hash{column => map} of columns to a map{old_value => new_value}
328
+ def results_to_sheet( results, sheet, mappings=nil, header=true)
329
+ numrows = results.length
330
+ sheet_name = sheet
331
+
332
+ if (numrows == 0)
333
+ log :info, "WARNING - results are empty nothing written to sheet: #{sheet}"
334
+ return
335
+ end
336
+
337
+ #Check if we need to split the results into seperate sheets
338
+ if (numrows > @@maxrows )
339
+ startrow = 0
340
+ while (numrows > 0)
341
+ # Split the results and write to a new sheet
342
+ next_results = results.slice(startrow, @@maxrows > numrows ? numrows : @@maxrows)
343
+ self.results_to_sheet(next_results, "#{sheet_name}", mappings, header) if next_results
344
+
345
+ # Increase counters
346
+ numrows -= next_results.length
347
+ startrow += next_results.length
348
+ sheet_name += 'I'
349
+ end
350
+ else
351
+ # Create required sheet
352
+ self.create(sheet)
353
+
354
+ row_index = self.num_rows
355
+ # write header line
356
+ if (header && row_index==0 )
357
+ header_row = @sheet.createRow(row_index)
358
+ cell_index = 0
359
+ results[0].keys.each{ |h|
360
+ header_row.createCell(cell_index).setCellValue("#{h}")
361
+ @sheet.setDefaultColumnStyle(cell_index, self.getPercentStyle) if "#{h}".include? '%'
362
+ cell_index += 1
363
+ }
364
+ # Freeze the header row
365
+ @sheet.createFreezePane( 0, 1, 0, 1 )
366
+ row_index += 1
367
+ end
368
+
369
+ # write_results
370
+ results.each{ |row|
371
+ sheet_row = @sheet.createRow(row_index)
372
+ cell_index = 0
373
+ row.each{|k,v|
374
+ celltype = v.kind_of?(Numeric) ? HSSFCell::CELL_TYPE_NUMERIC : HSSFCell::CELL_TYPE_STRING
375
+ cell = sheet_row.createCell(cell_index, celltype)
376
+
377
+ v.nil? ? value = "<NIL>" : value = v
378
+
379
+ cell.setCellValue(value)
380
+
381
+ cell_index +=1
382
+ }
383
+ #puts "#{sheet}: written row #{row_index}"
384
+ row_index +=1
385
+ }
386
+ end
387
+
388
+ end
389
+
390
+ end # END JExcelFile
391
+ else
392
+ class JExcelFile
393
+ def initialize
394
+ raise DataShift::BadRuby, "Please install and use JRuby for working with .xls files"
395
+ end
396
+ end
397
+ end
@@ -0,0 +1,79 @@
1
+ # Author:: Tom Statter
2
+ # License:: MIT ?
3
+ #
4
+ # NOTES ON INVESTIGATING OLE METHODS in irb
5
+ #
6
+ # visible = @word_app.ole_method_help( 'Visible' ) # Get a Method Object
7
+
8
+ # log( visible.return_type_detail.to_s ) # => ["BOOL"]
9
+ # log( visible.invoke_kind.to_s ) # => "PROPERTYGET"
10
+ # log( visible.params.to_s ) # => []
11
+
12
+ # @fc.ole_method_help( 'Report' ).params[1].ole_type_detail
13
+ #
14
+ # prefs = @word_app.Preferences.Strings.ole_method_help( 'Set' ).params
15
+ # => [index, newVal]
16
+ #
17
+ # WORD_OLE_CONST.constants
18
+ #
19
+ # WORD_OLE_CONST.constants.sort.grep /CR/
20
+ # => ["ClHideCRLF", "LesCR", "LesCRLF"]
21
+ #
22
+ # WORD_OLE_CONST.const_get( 'LesCR' ) or WORD_OLE_CONST::LesCR
23
+ # => 1
24
+
25
+ if(Guards::windows?)
26
+
27
+ require 'win32ole'
28
+
29
+ # Module for constants to be loaded int
30
+
31
+ module WORD_OLE_CONST
32
+ end
33
+
34
+ class Word
35
+
36
+ attr_reader :wd, :doc
37
+
38
+ def initialize( visible )
39
+ @wd = WIN32OLE.new('Word.Application')
40
+
41
+ WIN32OLE.const_load(@wd, WORD_OLE_CONST) if WORD_OLE_CONST.constants.empty?
42
+
43
+ @wd.Visible = visible
44
+ end
45
+
46
+ def open(file)
47
+ @doc = @wd.Documents.Open(file)
48
+ @doc
49
+ end
50
+
51
+ def save()
52
+ @doc.Save()
53
+ @doc
54
+ end
55
+
56
+ # Format : From WORD_OLE_CONST e.g WORD_OLE_CONST::WdFormatHTML
57
+ #
58
+ def save_as(name, format)
59
+ @doc.SaveAs(name, format)
60
+ return @doc
61
+ end
62
+
63
+ # WdFormatFilteredHTML
64
+ # WdFormatHTML
65
+ def save_as_html(name)
66
+ @doc.SaveAs(name, WORD_OLE_CONST::WdFormatHTML)
67
+ return @doc
68
+ end
69
+
70
+ def quit
71
+ @wd.quit()
72
+ end
73
+ end
74
+
75
+ else
76
+
77
+ class Word
78
+ end
79
+ end