diff --git a/CHANGELOG.md b/CHANGELOG.md index 7c84865..1f924d2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,6 @@ # Squib CHANGELOG +# Added a `csv` command that works just like `xslx`. Uses Ruby's CSV inside, with some extra checking and warnings. # Custom layouts now support loading & merging multiple Yaml files! Updated README, docs, and sample to document it. # Built-in layouts! Currently we support `hand.yml` and `playing-card.yml`. Documented in the `layouts.rb` sample. # `text` now returns the ink extent rectangle of the rendered text. Updated docs and sample to document it. diff --git a/README.md b/README.md index ccdeec9..a74ea83 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ Squib is a Ruby [DSL](http://en.wikipedia.org/wiki/Domain-specific_language) for * A concise set of rules for laying out your cards * Loading PNGs and SVGs using [Cairo](http://cairographics.org/) * Complex text rendering using [Pango](http://www.pango.org/) -* Reading `.xlsx` files +* Reading `xlsx` and `csv` files * Basic shape drawing * Rendering decks to PNGs and PDFs * Data-driven layouts @@ -255,6 +255,14 @@ See the `custom_config` sample found [here](https://github.com/andymeneely/squib {include:file:samples/custom_config.rb} +## Importing from Excel and CSV + +Squib supports importing data from `xlsx` files and `csv` files. These methods are column-based, which means that they assume you have a header row in your table, and that header row will define the column. Squib will return a `Hash` of `Arrays` correspoding to each row. Warnings are thrown on things like duplicate columns. See the `excel.rb` and the `csv_import.rb` sample found [here](https://github.com/andymeneely/squib/tree/master/samples/). + +{include:file:samples/excel.rb} + +Of course, you can always import your game data other ways using just Ruby. There's nothing special about Squib's methods other than their convenience. + ## Making Squib Verbose By default, Squib's logger is set to WARN, but more fine-grained logging is embedded in the code. To set the logger, just put this at the top of your script: diff --git a/lib/squib/api/data.rb b/lib/squib/api/data.rb index e22f1d8..91f3be5 100644 --- a/lib/squib/api/data.rb +++ b/lib/squib/api/data.rb @@ -63,13 +63,26 @@ module Squib def csv(opts = {}) opts = Squib::SYSTEM_DEFAULTS.merge(opts) opts = Squib::InputHelpers.fileify(opts) - hash = {} - csv = CSV.open(opts[:file], headers: true, converters: :numeric).read - + table = CSV.read(opts[:file], headers: true, converters: :numeric) + check_duplicate_csv_headers(table) + hash = Hash.new + table.headers.each do |header| + hash[header.to_s] ||= table[header] + end return hash end module_function :csv + # Check if the given CSV table has duplicate columns, and throw a warning + # @api private + def check_duplicate_csv_headers(table) + if table.headers.size != table.headers.uniq.size + dups = table.headers.select{|e| table.headers.count(e) > 1 } + Squib.logger.warn "CSV duplicated the following column keys: #{dups.join(',')}" + end + end + module_function :check_duplicate_csv_headers + class Deck # Convenience call on deck goes to the module function diff --git a/samples/csv.rb b/samples/csv_import.rb similarity index 75% rename from samples/csv.rb rename to samples/csv_import.rb index 6491e9f..e2d5785 100644 --- a/samples/csv.rb +++ b/samples/csv_import.rb @@ -13,5 +13,8 @@ Squib::Deck.new(cards: 2) do # You can also specify the sheet, starting at 0 data = xlsx file: 'sample.xlsx', sheet: 2 - save format: :png, prefix: 'sample_excel_' + save format: :png, prefix: 'sample_csv_' end + +# CSV is also a Squib-module-level function, so this also works: +data = Squib.csv file: 'sample.csv' diff --git a/spec/api/api_data_spec.rb b/spec/api/api_data_spec.rb index c029630..b90bd44 100644 --- a/spec/api/api_data_spec.rb +++ b/spec/api/api_data_spec.rb @@ -2,11 +2,30 @@ require 'spec_helper' describe Squib::Deck do context '#csv' do - # it 'loads basic csv data' do - # expect(Squib.csv(file: csv_file('basic.csv'))).to eq({ - # 'h1' => [1, 3], - # 'h2' => [2, 4] - # }) - # end + it 'loads basic csv data' do + expect(Squib.csv(file: csv_file('basic.csv'))).to eq({ + 'h1' => [1, 3], + 'h2' => [2, 4] + }) + end + + it 'collapses duplicate columns and warns' do + expect(Squib.logger).to receive(:warn) + .with('CSV duplicated the following column keys: h1,h1') + expect(Squib.csv(file: csv_file('dup_cols.csv'))).to eq({ + 'h1' => [1, 3], + 'h2' => [5, 7], + 'H2' => [6, 8], + 'h3' => [9, 10], + }) + end + + it 'handles spaces properly' do + expect(Squib.csv(file: csv_file('with_spaces.csv'))).to eq({ + 'With Spaces' => ['a b c ', 3], + 'h2' => [2, 4], + 'h3' => [3, nil] + }) + end end end \ No newline at end of file diff --git a/spec/data/csv/dup_cols.csv b/spec/data/csv/dup_cols.csv new file mode 100644 index 0000000..722079b --- /dev/null +++ b/spec/data/csv/dup_cols.csv @@ -0,0 +1,3 @@ +h1,h1,h2,H2,h3 +1,2,5,6,9 +3,4,7,8,10 \ No newline at end of file diff --git a/spec/data/csv/with_spaces.csv b/spec/data/csv/with_spaces.csv new file mode 100644 index 0000000..624932a --- /dev/null +++ b/spec/data/csv/with_spaces.csv @@ -0,0 +1,3 @@ +With Spaces,h2,h3 +a b c , 2,3 +3 ,4 \ No newline at end of file diff --git a/spec/data/samples/csv_import.rb.txt b/spec/data/samples/csv_import.rb.txt new file mode 100644 index 0000000..c00e4ef --- /dev/null +++ b/spec/data/samples/csv_import.rb.txt @@ -0,0 +1,112 @@ +cairo: save([]) +cairo: set_source_color([#]) +cairo: paint([]) +cairo: restore([]) +cairo: save([]) +cairo: set_source_color([#]) +cairo: paint([]) +cairo: restore([]) +cairo: save([]) +cairo: set_source_color([#]) +cairo: translate([250, 55]) +cairo: rotate([0]) +cairo: translate([-250, -55]) +cairo: move_to([250, 55]) +pango: font_description=([]) +pango: text=([""]) +pango: wrap=([#]) +pango: ellipsize=([#]) +pango: alignment=([#]) +pango: justify=([false]) +pango: spacing=([0]) +cairo: update_pango_layout([MockDouble]) +cairo: update_pango_layout([MockDouble]) +cairo: show_pango_layout([MockDouble]) +cairo: restore([]) +cairo: save([]) +cairo: set_source_color([#]) +cairo: translate([250, 55]) +cairo: rotate([0]) +cairo: translate([-250, -55]) +cairo: move_to([250, 55]) +pango: font_description=([]) +pango: text=([""]) +pango: wrap=([#]) +pango: ellipsize=([#]) +pango: alignment=([#]) +pango: justify=([false]) +pango: spacing=([0]) +cairo: update_pango_layout([MockDouble]) +cairo: update_pango_layout([MockDouble]) +cairo: show_pango_layout([MockDouble]) +cairo: restore([]) +cairo: save([]) +cairo: set_source_color([#]) +cairo: translate([65, 65]) +cairo: rotate([0]) +cairo: translate([-65, -65]) +cairo: move_to([65, 65]) +pango: font_description=([]) +pango: text=([""]) +pango: wrap=([#]) +pango: ellipsize=([#]) +pango: alignment=([#]) +pango: justify=([false]) +pango: spacing=([0]) +cairo: update_pango_layout([MockDouble]) +cairo: update_pango_layout([MockDouble]) +cairo: show_pango_layout([MockDouble]) +cairo: restore([]) +cairo: save([]) +cairo: set_source_color([#]) +cairo: translate([65, 65]) +cairo: rotate([0]) +cairo: translate([-65, -65]) +cairo: move_to([65, 65]) +pango: font_description=([]) +pango: text=([""]) +pango: wrap=([#]) +pango: ellipsize=([#]) +pango: alignment=([#]) +pango: justify=([false]) +pango: spacing=([0]) +cairo: update_pango_layout([MockDouble]) +cairo: update_pango_layout([MockDouble]) +cairo: show_pango_layout([MockDouble]) +cairo: restore([]) +cairo: save([]) +cairo: set_source_color([#]) +cairo: translate([65, 600]) +cairo: rotate([0]) +cairo: translate([-65, -600]) +cairo: move_to([65, 600]) +pango: font_description=([]) +pango: text=([""]) +pango: wrap=([#]) +pango: ellipsize=([#]) +pango: alignment=([#]) +pango: justify=([false]) +pango: spacing=([0]) +cairo: update_pango_layout([MockDouble]) +cairo: update_pango_layout([MockDouble]) +cairo: show_pango_layout([MockDouble]) +cairo: restore([]) +cairo: save([]) +cairo: set_source_color([#]) +cairo: translate([65, 600]) +cairo: rotate([0]) +cairo: translate([-65, -600]) +cairo: move_to([65, 600]) +pango: font_description=([]) +pango: text=([""]) +pango: wrap=([#]) +pango: ellipsize=([#]) +pango: alignment=([#]) +pango: justify=([false]) +pango: spacing=([0]) +cairo: update_pango_layout([MockDouble]) +cairo: update_pango_layout([MockDouble]) +cairo: show_pango_layout([MockDouble]) +cairo: restore([]) +surface: write_to_png(["_output/sample_csv_0.png"]) +surface: write_to_png(["_output/sample_csv_1.png"]) diff --git a/spec/samples/samples_regression_spec.rb b/spec/samples/samples_regression_spec.rb index 5647970..94dd5eb 100644 --- a/spec/samples/samples_regression_spec.rb +++ b/spec/samples/samples_regression_spec.rb @@ -54,6 +54,7 @@ describe "Squib samples" do draw_shapes.rb colors.rb excel.rb + csv_import.rb portrait-landscape.rb tgc_proofs.rb ranges.rb