From c8e6f9803ccec659b8db73effd8d726e0e55d199 Mon Sep 17 00:00:00 2001 From: Andy Meneely Date: Tue, 13 Oct 2015 10:37:49 -0400 Subject: [PATCH] xlsx,csv: trim whitespace, yield to optional block Closes #108 and #79 --- CHANGELOG.md | 8 ++ lib/squib/api/data.rb | 23 ++++- lib/squib/args/import.rb | 34 +++++++ samples/excel.rb | 33 +++++- samples/sample.xlsx | Bin 11770 -> 10748 bytes spec/api/api_data_spec.rb | 51 ++++++++-- spec/data/csv/with_spaces.csv | 2 +- spec/data/samples/excel.rb.txt | 181 +++++++++++++++++++++++++++++++++ spec/data/xlsx/whitespace.xlsx | Bin 0 -> 8134 bytes 9 files changed, 317 insertions(+), 15 deletions(-) create mode 100644 lib/squib/args/import.rb create mode 100644 spec/data/xlsx/whitespace.xlsx diff --git a/CHANGELOG.md b/CHANGELOG.md index 7856095..a695126 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,14 @@ # Squib CHANGELOG Squib follows [semantic versioning](http://semver.org). +## v0.8.0 / Unreleased +Features +* The `xlsx` and `csv` methods will now strip leading and trailing whitespace by default where applicable. This is now turned on by default, but can be turned off with `strip: false`. +* The `xlsx` and `csv` methods will now yield to a block (if given) for each cell so you can do some extra processing if you like. See samples/excel.rb for an example. + +Compatibility change: +* Stripping leading and trailing whitespace of xlsx and csv values by default might change how your data gets parsed. + ## v0.7.0 / 2015-09-11 Features diff --git a/lib/squib/api/data.rb b/lib/squib/api/data.rb index b6574a4..e9872ec 100644 --- a/lib/squib/api/data.rb +++ b/lib/squib/api/data.rb @@ -1,6 +1,7 @@ require 'roo' require 'csv' require 'squib/args/input_file' +require 'squib/args/import' module Squib @@ -16,24 +17,30 @@ module Squib # # | 1 | 2 | # # | 3 | 4 | # data = xlsx file: 'data.xlsx', sheet: 0 - # {'h1' => [1,3], 'h2' => [2,4]} + # => {'h1' => [1,3], 'h2' => [2,4]} # # @option opts file [String] the file to open. Must end in `.xlsx`. Opens relative to the current directory. # @option opts sheet [Integer] (0) The zero-based index of the sheet from which to read. + # @option opts strip [Boolean] (true) When true, strips leading and trailing whitespace on values and headers + # @option opts qty_header [String] ('qty']) Quantity explosion will be applied to the column this name # @return [Hash] a hash of arrays based on columns in the spreadsheet # @api public def xlsx(opts = {}) input = Args::InputFile.new(file: 'deck.xlsx').load!(opts) + import = Args::Import.new.load!(opts) s = Roo::Excelx.new(input.file[0]) s.default_sheet = s.sheets[input.sheet[0]] data = {} s.first_column.upto(s.last_column) do |col| header = s.cell(s.first_row,col).to_s + header.strip! if import.strip? data[header] = [] (s.first_row + 1).upto(s.last_row) do |row| cell = s.cell(row,col) # Roo hack for avoiding unnecessary .0's on whole integers (https://github.com/roo-rb/roo/issues/139) cell = s.excelx_value(row,col) if s.excelx_type(row,col) == [:numeric_or_formula, 'General'] + cell.strip! if cell.respond_to?(:strip) && import.strip? + cell = yield(header, cell) if block_given? data[header] << cell end#row end#col @@ -58,17 +65,25 @@ module Squib # http://www.ruby-doc.org/stdlib-2.0/libdoc/csv/rdoc/CSV.html # # @option opts file [String] the CSV-formatted file to open. Opens relative to the current directory. + # @option opts strip [Boolean] (true) When true, strips leading and trailing whitespace on values and headers + # @option opts qty_header [String] ('qty']) Quantity explosion will be applied to the column this name # @return [Hash] a hash of arrays based on columns in the table # @api public def csv(opts = {}) file = Args::InputFile.new(file: 'deck.csv').load!(opts).file[0] - opts = Squib::SYSTEM_DEFAULTS.merge(opts) - # opts = Squib::InputHelpers.fileify(opts) + import = Args::Import.new.load!(opts) table = CSV.read(file, headers: true, converters: :numeric) check_duplicate_csv_headers(table) hash = Hash.new table.headers.each do |header| - hash[header.to_s] ||= table[header] + new_header = header.to_s + new_header.strip! if import.strip? + hash[new_header] ||= table[header] + end + if import.strip? + hash.each do |header, col| + col.map! { |str| str.strip! if str.respond_to?(:strip); str } + end end return hash end diff --git a/lib/squib/args/import.rb b/lib/squib/args/import.rb new file mode 100644 index 0000000..512b0ac --- /dev/null +++ b/lib/squib/args/import.rb @@ -0,0 +1,34 @@ +require 'squib/args/arg_loader' + +module Squib + # @api private + module Args + + class Import + include ArgLoader + + def self.parameters + { strip: true } + end + + def self.expanding_parameters + [] # none of them + end + + def self.params_with_units + [] # none of them + end + + def validate_strip(arg) + raise 'Strip must be true or false' unless arg == true || arg == false + arg + end + + def strip? + strip + end + + end + + end +end \ No newline at end of file diff --git a/samples/excel.rb b/samples/excel.rb index 1ac8710..936ab20 100644 --- a/samples/excel.rb +++ b/samples/excel.rb @@ -3,7 +3,7 @@ require 'squib' Squib::Deck.new(cards: 3) do background color: :white - # Takes the first sheet by default + # Reads the first sheet by default (sheet 0) # Outputs a hash of arrays with the header names as keys data = xlsx file: 'sample.xlsx' @@ -11,8 +11,33 @@ Squib::Deck.new(cards: 3) do text str: data['Level'], x: 65, y: 65, font: 'Arial 72' text str: data['Description'], x: 65, y: 600, font: 'Arial 36' - # You can also specify the sheet, starting at 0 - data = xlsx file: 'sample.xlsx', sheet: 2 + save format: :png, prefix: 'sample_excel_' #save to individual pngs +end + +# Here's another example, a bit more realistic. Here's what's going on: +# * We call xlsx from Squib directly - BEFORE Squib::Deck creation. This +# allows us to infer the number of cards based on the size of the "Name" +# field +# * We make use of quantity explosion. Fields named "Qty" or "Quantity" +# (any capitalization), or any other in the "qty_header" get expanded by the +# number given +# * We also make sure that trailing and leading whitespace is stripped +# from each value. This is the default behavior in Squib, but the options +# are here just to make sure. - save format: :png, prefix: 'sample_excel_' +resource_data = Squib.xlsx(file: 'sample.xlsx', sheet: 2, strip: true) do |header, value| + case header + when 'Cost' + "$#{value}k" # e.g. "3" becomes "$3k" + else + value # always return the original value if you didn't do anything to it + end +end + +Squib::Deck.new(cards: resource_data['Name'].size) do + background color: :white + rect width: :deck, height: :deck + text str: resource_data['Name'], align: :center, width: :deck, hint: 'red' + text str: resource_data['Cost'], align: :right, width: :deck, hint: 'red' + save_sheet prefix: 'sample_excel_resources_' #save to a whole sheet end diff --git a/samples/sample.xlsx b/samples/sample.xlsx index 9b48ae292181286e63c74fc7f8227237104863f8..eabcf9b9f5ba16b0da724a2cdcd812dedb14b6a6 100644 GIT binary patch delta 7071 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 Z8=kBL1W>i#H$x ['a b c ', 3], + 'With Spaces' => ['a b c', 3], 'h2' => [2, 4], 'h3' => [3, nil] }) end + + it 'skips space stripping if told to' do + expect(Squib.csv(strip: false, file: csv_file('with_spaces.csv'))).to eq({ + ' With Spaces ' => ['a b c ', 3], + 'h2' => [2, 4], + 'h3' => [3, nil] + }) + end + end context '#xlsx' do it 'loads basic xlsx data' do expect(Squib.xlsx(file: xlsx_file('basic.xlsx'))).to eq({ - 'Name' => %w(Larry Curly Mo), - 'General Number' => %w(1 2 3), #general types always get loaded as strings with no conversion - 'Actual Number' => [4.0, 5.0, 6.0], #numbers get auto-converted to integers - }) + 'Name' => %w(Larry Curly Mo), + 'General Number' => %w(1 2 3), #general types always get loaded as strings with no conversion + 'Actual Number' => [4.0, 5.0, 6.0], #numbers get auto-converted to integers + }) end it 'loads xlsx with formulas' do @@ -53,5 +62,35 @@ describe Squib::Deck do }) end + it 'strips whitespace by default' do + expect(Squib.xlsx(file: xlsx_file('whitespace.xlsx'))).to eq({ + 'With Whitespace' => ['foo', 'bar', 'baz'], + }) + end + + it 'does not strip whitespace when specified' do + expect(Squib.xlsx(file: xlsx_file('whitespace.xlsx'), strip: false)).to eq({ + ' With Whitespace ' => ['foo ', ' bar', ' baz '], + }) + end + + it 'yields to block when given' do + data = Squib.xlsx(file: xlsx_file('basic.xlsx')) do |header, value| + case header + when 'Name' + 'he' + when 'Actual Number' + value * 2 + else + 'ha' + end + end + expect(data).to eq({ + 'Name' => %w(he he he), + 'General Number' => %w(ha ha ha), + 'Actual Number' => [8.0, 10.0, 12.0], + }) + end + end end diff --git a/spec/data/csv/with_spaces.csv b/spec/data/csv/with_spaces.csv index 624932a..e1b56db 100644 --- a/spec/data/csv/with_spaces.csv +++ b/spec/data/csv/with_spaces.csv @@ -1,3 +1,3 @@ -With Spaces,h2,h3 + With Spaces ,h2,h3 a b c , 2,3 3 ,4 \ No newline at end of file diff --git a/spec/data/samples/excel.rb.txt b/spec/data/samples/excel.rb.txt index 4a35e15..7469999 100644 --- a/spec/data/samples/excel.rb.txt +++ b/spec/data/samples/excel.rb.txt @@ -160,3 +160,184 @@ cairo: restore([]) surface: write_to_png(["_output/sample_excel_00.png"]) surface: write_to_png(["_output/sample_excel_01.png"]) surface: write_to_png(["_output/sample_excel_02.png"]) +cairo: antialias=(["subpixel"]) +cairo: antialias=(["subpixel"]) +cairo: antialias=(["subpixel"]) +cairo: save([]) +cairo: set_source_color(["white"]) +cairo: paint([]) +cairo: restore([]) +cairo: save([]) +cairo: set_source_color(["white"]) +cairo: paint([]) +cairo: restore([]) +cairo: save([]) +cairo: set_source_color(["white"]) +cairo: paint([]) +cairo: restore([]) +cairo: save([]) +cairo: rounded_rectangle([0, 0, 825, 1125, 0, 0]) +cairo: set_source_color(["#0000"]) +cairo: fill_preserve([]) +cairo: set_source_color(["black"]) +cairo: set_line_width([2.0]) +cairo: set_line_join([0]) +cairo: set_line_cap([0]) +cairo: set_dash([[]]) +cairo: stroke([]) +cairo: restore([]) +cairo: save([]) +cairo: rounded_rectangle([0, 0, 825, 1125, 0, 0]) +cairo: set_source_color(["#0000"]) +cairo: fill_preserve([]) +cairo: set_source_color(["black"]) +cairo: set_line_width([2.0]) +cairo: set_line_join([0]) +cairo: set_line_cap([0]) +cairo: set_dash([[]]) +cairo: stroke([]) +cairo: restore([]) +cairo: save([]) +cairo: rounded_rectangle([0, 0, 825, 1125, 0, 0]) +cairo: set_source_color(["#0000"]) +cairo: fill_preserve([]) +cairo: set_source_color(["black"]) +cairo: set_line_width([2.0]) +cairo: set_line_join([0]) +cairo: set_line_cap([0]) +cairo: set_dash([[]]) +cairo: stroke([]) +cairo: restore([]) +cairo: save([]) +cairo: set_source_color(["black"]) +cairo: translate([0, 0]) +cairo: rotate([0]) +cairo: move_to([0, 0]) +pango: font_description=([MockDouble]) +pango: text=(["Wood"]) +pango: width=([844800]) +pango: wrap=([#]) +pango: ellipsize=([#]) +pango: alignment=([#]) +pango: justify=([false]) +cairo: move_to([0, 0]) +cairo: move_to([0, 0]) +cairo: show_pango_layout([MockDouble]) +cairo: rounded_rectangle([0, 0, 0, 0, 0, 0]) +cairo: set_source_color(["red"]) +cairo: set_line_width([2.0]) +cairo: stroke([]) +pango: ellipsized?([]) +cairo: restore([]) +cairo: save([]) +cairo: set_source_color(["black"]) +cairo: translate([0, 0]) +cairo: rotate([0]) +cairo: move_to([0, 0]) +pango: font_description=([MockDouble]) +pango: text=(["Metal"]) +pango: width=([844800]) +pango: wrap=([#]) +pango: ellipsize=([#]) +pango: alignment=([#]) +pango: justify=([false]) +cairo: move_to([0, 0]) +cairo: move_to([0, 0]) +cairo: show_pango_layout([MockDouble]) +cairo: rounded_rectangle([0, 0, 0, 0, 0, 0]) +cairo: set_source_color(["red"]) +cairo: set_line_width([2.0]) +cairo: stroke([]) +pango: ellipsized?([]) +cairo: restore([]) +cairo: save([]) +cairo: set_source_color(["black"]) +cairo: translate([0, 0]) +cairo: rotate([0]) +cairo: move_to([0, 0]) +pango: font_description=([MockDouble]) +pango: text=(["Stone"]) +pango: width=([844800]) +pango: wrap=([#]) +pango: ellipsize=([#]) +pango: alignment=([#]) +pango: justify=([false]) +cairo: move_to([0, 0]) +cairo: move_to([0, 0]) +cairo: show_pango_layout([MockDouble]) +cairo: rounded_rectangle([0, 0, 0, 0, 0, 0]) +cairo: set_source_color(["red"]) +cairo: set_line_width([2.0]) +cairo: stroke([]) +pango: ellipsized?([]) +cairo: restore([]) +cairo: save([]) +cairo: set_source_color(["black"]) +cairo: translate([0, 0]) +cairo: rotate([0]) +cairo: move_to([0, 0]) +pango: font_description=([MockDouble]) +pango: text=(["$2k"]) +pango: width=([844800]) +pango: wrap=([#]) +pango: ellipsize=([#]) +pango: alignment=([#]) +pango: justify=([false]) +cairo: move_to([0, 0]) +cairo: move_to([0, 0]) +cairo: show_pango_layout([MockDouble]) +cairo: rounded_rectangle([0, 0, 0, 0, 0, 0]) +cairo: set_source_color(["red"]) +cairo: set_line_width([2.0]) +cairo: stroke([]) +pango: ellipsized?([]) +cairo: restore([]) +cairo: save([]) +cairo: set_source_color(["black"]) +cairo: translate([0, 0]) +cairo: rotate([0]) +cairo: move_to([0, 0]) +pango: font_description=([MockDouble]) +pango: text=(["$3k"]) +pango: width=([844800]) +pango: wrap=([#]) +pango: ellipsize=([#]) +pango: alignment=([#]) +pango: justify=([false]) +cairo: move_to([0, 0]) +cairo: move_to([0, 0]) +cairo: show_pango_layout([MockDouble]) +cairo: rounded_rectangle([0, 0, 0, 0, 0, 0]) +cairo: set_source_color(["red"]) +cairo: set_line_width([2.0]) +cairo: stroke([]) +pango: ellipsized?([]) +cairo: restore([]) +cairo: save([]) +cairo: set_source_color(["black"]) +cairo: translate([0, 0]) +cairo: rotate([0]) +cairo: move_to([0, 0]) +pango: font_description=([MockDouble]) +pango: text=(["$5k"]) +pango: width=([844800]) +pango: wrap=([#]) +pango: ellipsize=([#]) +pango: alignment=([#]) +pango: justify=([false]) +cairo: move_to([0, 0]) +cairo: move_to([0, 0]) +cairo: show_pango_layout([MockDouble]) +cairo: rounded_rectangle([0, 0, 0, 0, 0, 0]) +cairo: set_source_color(["red"]) +cairo: set_line_width([2.0]) +cairo: stroke([]) +pango: ellipsized?([]) +cairo: restore([]) +cairo: set_source([MockDouble, 0, 0]) +cairo: paint([]) +cairo: set_source([MockDouble, 100, 0]) +cairo: paint([]) +cairo: set_source([MockDouble, 200, 0]) +cairo: paint([]) +surface: write_to_png(["_output/sample_excel_resources_00.png"]) diff --git a/spec/data/xlsx/whitespace.xlsx b/spec/data/xlsx/whitespace.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..14ef08008811b2831cad53858c79025888a535a8 GIT binary patch literal 8134 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