Skip to content

Commit f4d9424

Browse files
committed
import: data frames are here!
Implemented a whole new class to represent the data that comes in from CSV and XLSX. See docs for more info. Closes #153
1 parent 935040b commit f4d9424

File tree

9 files changed

+484
-30
lines changed

9 files changed

+484
-30
lines changed

CHANGELOG.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@ Squib follows [semantic versioning](http://semver.org).
66
Features:
77
* `save_pdf` now supports crop marks! These are lines drawn in the margins of a PDF file to help you cut. These can be enabled by setting `crop_marks: true` in your `save_pdf` call. Can be further customized with `crop_margin_bottom`, `crop_margin_left`, `crop_margin_right`, `crop_margin_top`, `crop_marks`, `crop_stroke_color`, `crop_stroke_dash`, and `crop_stroke_width` (#123)
88
* `Squib.configure` allows you to set options programmatically, overriding your config.yml. This is useful for Rakefiles, and will be documented in my upcoming tutorial on workflows.
9-
* `Squib.enable_build_globally` and `Squib.disable_build_globally` are new convenience methods for working with the `SQUIB_BUILD` environment variable. Handy for Rakefiles and Guard sessions for turning certain builds on an off. Also will be in upcoming workflow tutorial.
9+
* `Squib.enable_build_globally` and `Squib.disable_build_globally` are new convenience methods for working with the `SQUIB_BUILD` environment variable. Handy for Rakefiles and Guard sessions for turning certain builds on an off. Also will be documented in upcoming workflow tutorial.
10+
* The import methods `csv` and `xlsx` now return `Squib::DataFrame`, which behaves exactly as before - but has more cool features like being able to do `data.name` instead of `data['name']`. Also: check out `data.to_pretty_text`. Check out the docs. (#156)
1011

1112
Bugs:
1213
* `showcase` works as expected when using `backend: svg` (#179)

docs/build_groups.rst

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,9 @@ One adaptation of this is to do the environment setting in a ``Rakefile``. `Rake
3434
:language: ruby
3535
:linenos:
3636

37-
Thus, you can just run this code on the command line like these::
37+
Thus, you can just run this code on the command line like these:
38+
39+
.. code-block:: none
3840
3941
$ rake
4042
$ rake pnp

docs/data.rst

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,18 +3,22 @@ Be Data-Driven with XLSX and CSV
33

44
Squib supports importing data from ExcelX (.xlsx) files and Comma-Separated Values (.csv) files. Because :doc:`/arrays`, 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 name of the column.
55

6-
Hash of Arrays
7-
--------------
6+
Squib::DataFrame, or a Hash of Arrays
7+
-------------------------------------
88

9-
In both DSL methods, Squib will return a ``Hash`` of ``Arrays`` correspoding to each row. Thus, be sure to structure your data like this:
9+
In both DSL methods, Squib will return a "data frame" (literally of type ``Squib::DataFrame``). The best way to think of this is a ``Hash`` of ``Arrays``, where each column is a key in the hash, and every element of each Array represents a data point on a card.
10+
11+
The data import methods expect you to structure your Excel sheet or CSV like this:
1012

1113
* First row should be a header - preferably with concise naming since you'll reference it in Ruby code
1214
* Rows should represent cards in the deck
1315
* Columns represent data about cards (e.g. "Type", "Cost", or "Name")
1416

1517
Of course, you can always import your game data other ways using just Ruby (e.g. from a REST API, a JSON file, or your own custom format). There's nothing special about Squib's methods in how they relate to ``Squib::Deck`` other than their convenience.
1618

17-
See :doc:`/dsl/xlsx` and :doc:`/dsl/csv` for more details and examples.
19+
See :doc:`/dsl/xlsx` and :doc:`/dsl/csv` for more details and examples on how the data can be imported.
20+
21+
The ``Squib::DataFrame`` class provides much more than what a ``Hash`` provides, however. The :doc:`/dsl/data_frame`
1822

1923
Quantity Explosion
2024
------------------

docs/dsl/data_frame.rst

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
Squib::DataFrame
2+
================
3+
4+
As described in :doc:`/data`, the ``Squib::DataFrame`` is what is returned by Squib's data import methods (:doc:`/dsl/csv` and :doc:`/dsl/xlsx`).
5+
6+
It behaves like a ``Hash`` of ``Arrays``, so acessing an individual column can be done via the square brackets, e.g. ``data['title']``.
7+
8+
Here are some other convenience methods in ``Squib::DataFrame``
9+
10+
columns become methods
11+
----------------------
12+
13+
Through magic of Ruby metaprogramming, every column also becomes a method on the data frame. So these two are equivalent:
14+
15+
.. code-block:: irb
16+
17+
irb(main):002:0> data = Squib.csv file: 'basic.csv'
18+
=> #<Squib::DataFrame:0x00000003764550 @hash={"h1"=>[1, 3], "h2"=>[2, 4]}>
19+
irb(main):003:0> data.h1
20+
=> [1, 3]
21+
irb(main):004:0> data['h1']
22+
=> [1, 3]
23+
24+
#columns
25+
--------
26+
27+
Returns an array of the column names in the data frame
28+
29+
#ncolumns
30+
---------
31+
32+
Returns the number of columns in the data frame
33+
34+
#col?(name)
35+
-----------
36+
37+
Returns ``true`` if there is column ``name``.
38+
39+
#row(i)
40+
-------
41+
42+
Returns a hash of values across all columns in the ``i``th row of the dataframe. Represents a single card.
43+
44+
#nrows
45+
------
46+
47+
Returns the number of rows the data frame has, computed by the maximum length of any column array.
48+
49+
#to_json
50+
--------
51+
52+
Returns a ``json`` representation of the entire data frame.
53+
54+
#to_pretty_json
55+
---------------
56+
57+
Returns a ``json`` representation of the entire data frame, formatted with indentation for human viewing.
58+
59+
#to_pretty_text
60+
---------------
61+
62+
Returns a textual representation of the dataframe that emulates what the information looks like on an individual card. Here's an example:
63+
64+
.. code-block:: text
65+
66+
╭------------------------------------╮
67+
Name | Mage |
68+
Cost | 1 |
69+
Description | You may cast 1 spell per turn |
70+
Snark | Magic, dude. |
71+
╰------------------------------------╯
72+
╭------------------------------------╮
73+
Name | Rogue |
74+
Cost | 2 |
75+
Description | You always take the first turn. |
76+
Snark | I like to be sneaky |
77+
╰------------------------------------╯
78+
╭------------------------------------╮
79+
Name | Warrior |
80+
Cost | 3 |
81+
Description |
82+
Snark | I have a long story to tell to tes |
83+
| t the word-wrapping ability of pre |
84+
| tty text formatting. |
85+
╰------------------------------------╯

docs/install.rst

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,9 @@ Squib works with both x86 and x86_64 versions of Ruby.
1313
Typical Install
1414
---------------
1515

16-
Regardless of your OS, installation is::
16+
Regardless of your OS, installation is
17+
18+
.. code-block:: none
1719
1820
$ gem install squib
1921

lib/squib/api/data.rb

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
require_relative '../args/input_file'
44
require_relative '../args/import'
55
require_relative '../args/csv_opts'
6+
require_relative '../import/data_frame'
67

78
module Squib
89

@@ -12,7 +13,7 @@ def xlsx(opts = {})
1213
import = Args::Import.new.load!(opts)
1314
s = Roo::Excelx.new(input.file[0])
1415
s.default_sheet = s.sheets[input.sheet[0]]
15-
data = {}
16+
data = Squib::DataFrame.new
1617
s.first_column.upto(s.last_column) do |col|
1718
header = s.cell(s.first_row, col).to_s
1819
header.strip! if import.strip?
@@ -39,14 +40,14 @@ def csv(opts = {})
3940
csv_opts = Args::CSV_Opts.new(opts)
4041
table = CSV.parse(data, csv_opts.to_hash)
4142
check_duplicate_csv_headers(table)
42-
hash = Hash.new
43+
hash = Squib::DataFrame.new
4344
table.headers.each do |header|
4445
new_header = header.to_s
4546
new_header = new_header.strip if import.strip?
4647
hash[new_header] ||= table[header]
4748
end
4849
if import.strip?
49-
new_hash = Hash.new
50+
new_hash = Squib::DataFrame.new
5051
hash.each do |header, col|
5152
new_hash[header] = col.map do |str|
5253
str = str.strip if str.respond_to?(:strip)
@@ -78,9 +79,9 @@ def check_duplicate_csv_headers(table)
7879

7980
# @api private
8081
def explode_quantities(data, qty)
81-
return data unless data.key? qty.to_s.strip
82+
return data unless data.col? qty.to_s.strip
8283
qtys = data[qty]
83-
new_data = {}
84+
new_data = Squib::DataFrame.new
8485
data.each do |col, arr|
8586
new_data[col] = []
8687
qtys.each_with_index do |qty, index|

lib/squib/import/data_frame.rb

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
# encoding: UTF-8
2+
3+
require 'json'
4+
require 'forwardable'
5+
6+
module Squib
7+
class DataFrame
8+
include Enumerable
9+
10+
def initialize(hash = {}, def_columns = true)
11+
@hash = hash
12+
columns.each { |col| def_column(col) } if def_columns
13+
end
14+
15+
def each(&block)
16+
@hash.each(&block)
17+
end
18+
19+
def [](i)
20+
@hash[i]
21+
end
22+
23+
def []=(col, v)
24+
@hash[col] = v
25+
def_column(col)
26+
return v
27+
end
28+
29+
def columns
30+
@hash.keys
31+
end
32+
33+
def ncolumns
34+
@hash.keys.size
35+
end
36+
37+
def col?(col)
38+
@hash.key? col
39+
end
40+
41+
def row(i)
42+
@hash.inject(Hash.new) { |ret, (name, arr)| ret[name] = arr[i]; ret }
43+
end
44+
45+
def nrows
46+
@hash.inject(0) { |max, (_n, col)| col.size > max ? col.size : max }
47+
end
48+
49+
def to_json
50+
@hash.to_json
51+
end
52+
53+
def to_pretty_json
54+
JSON.pretty_generate(@hash)
55+
end
56+
57+
def to_h
58+
@hash
59+
end
60+
61+
def to_pretty_text
62+
max_col = columns.inject(0) { |max, c | c.length > max ? c.length : max }
63+
top = " ╭#{'-' * 36}\n"
64+
bottom = " ╰#{'-' * 36}\n"
65+
str = ''
66+
0.upto(nrows - 1) do | i |
67+
str += (' ' * max_col) + top
68+
row(i).each do |col, data|
69+
str += "#{col.rjust(max_col)} #{wrap_n_pad(data, max_col)}"
70+
end
71+
str += (' ' * max_col) + bottom
72+
end
73+
return str
74+
end
75+
76+
private
77+
78+
def snake_case(str)
79+
str.to_s.
80+
strip.
81+
gsub(/\s+/,'_').
82+
gsub(/([A-Z]+)([A-Z][a-z])/,'\1_\2').
83+
gsub(/([a-z]+)([A-Z])/,'\1_\2').
84+
downcase.
85+
to_sym
86+
end
87+
88+
def wrap_n_pad(str, max_col)
89+
str.to_s.
90+
concat(' '). # handle nil & empty strings
91+
scan(/.{1,34}/).
92+
map { |s| (' ' * max_col) + " | " + s.ljust(34) }.
93+
join(" |\n").
94+
lstrip. # initially no whitespace next to key
95+
concat(" |\n")
96+
end
97+
98+
def def_column(col)
99+
raise "Column #{col} - does not exist" unless @hash.key? col
100+
method_name = snake_case(col)
101+
return if self.class.method_defined?(method_name) #warn people? or skip?
102+
define_singleton_method method_name do
103+
@hash[col]
104+
end
105+
end
106+
107+
end
108+
end

0 commit comments

Comments
 (0)