table_fu 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. data/.gitignore +4 -0
  2. data/LICENSE +20 -0
  3. data/README +37 -0
  4. data/Rakefile +58 -0
  5. data/VERSION.yml +5 -0
  6. data/doc/TableFu/Datum.html +743 -0
  7. data/doc/TableFu/Formatting.html +469 -0
  8. data/doc/TableFu/Header.html +198 -0
  9. data/doc/TableFu/Row.html +508 -0
  10. data/doc/TableFu.html +1806 -0
  11. data/doc/_index.html +154 -0
  12. data/doc/class_list.html +36 -0
  13. data/doc/css/common.css +1 -0
  14. data/doc/css/full_list.css +50 -0
  15. data/doc/css/style.css +268 -0
  16. data/doc/file.README.html +89 -0
  17. data/doc/file_list.html +38 -0
  18. data/doc/frames.html +13 -0
  19. data/doc/index.html +89 -0
  20. data/doc/js/app.js +99 -0
  21. data/doc/js/full_list.js +106 -0
  22. data/doc/js/jquery.js +19 -0
  23. data/doc/method_list.html +363 -0
  24. data/doc/top-level-namespace.html +85 -0
  25. data/documentation/css/dawn.css +121 -0
  26. data/documentation/css/styles.css +63 -0
  27. data/documentation/images/proplogo.png +0 -0
  28. data/documentation/index.html.erb +148 -0
  29. data/examples/columns.rb +7 -0
  30. data/examples/columns_hidden.rb +5 -0
  31. data/examples/faceting.rb +2 -0
  32. data/examples/formatting_options.rb +6 -0
  33. data/examples/last_name.rb +4 -0
  34. data/examples/link.rb +11 -0
  35. data/examples/only.rb +6 -0
  36. data/examples/rails_helpers.rb +3 -0
  37. data/examples/sort_by_column.rb +11 -0
  38. data/examples/sort_by_number.rb +0 -0
  39. data/examples/totals.rb +2 -0
  40. data/examples/zap_joyce.rb +3 -0
  41. data/index.html +210 -0
  42. data/lib/table_fu/formatting.rb +52 -0
  43. data/lib/table_fu.rb +386 -0
  44. data/spec/assets/sample.csv +476 -0
  45. data/spec/assets/test.csv +8 -0
  46. data/spec/assets/test_macro.csv +8 -0
  47. data/spec/rcov.opts +2 -0
  48. data/spec/readme_example_spec.rb +39 -0
  49. data/spec/spec.opts +4 -0
  50. data/spec/spec_helper.rb +4 -0
  51. data/spec/table_fu_spec.rb +221 -0
  52. data/table_fu.gemspec +112 -0
  53. metadata +141 -0
data/lib/table_fu.rb ADDED
@@ -0,0 +1,386 @@
1
+ require 'rubygems'
2
+ require 'fastercsv'
3
+
4
+
5
+ # TableFu turns a matric array(from a csv file for example) into a spreadsheet.
6
+ #
7
+ # Allows formatting, macros, sorting, and faceting.
8
+ #
9
+ # Documentation:
10
+ # http://propublica.github.com/table-fu
11
+ class TableFu
12
+
13
+ attr_reader :deleted_rows, :table, :totals, :column_headers
14
+ attr_accessor :faceted_on, :col_opts
15
+
16
+ # Should be initialized with a matrix array or a string containing a csv, and expects the first
17
+ # array in the matrix to be column headers.
18
+ def initialize(table, column_opts = {})
19
+ # Assume if we're past a string or filehandle we need to parse a csv
20
+ if table.is_a?(String) || table.is_a?(File)
21
+ table = FasterCSV.parse(table)
22
+ end
23
+ @column_headers = table.slice!(0)
24
+ @totals = {}
25
+ @table = table
26
+ @col_opts = column_opts
27
+ yield self if block_given?
28
+ end
29
+
30
+
31
+
32
+ # Pass it an array and it will delete it from the table, but save the data in
33
+ # @deleted_rows@ for later perusal.
34
+ #
35
+ # Returns:
36
+ # nothing
37
+ def delete_rows!(arr)
38
+ @deleted_rows ||= []
39
+ arr.map do |item|
40
+ @deleted_rows << @table[item] #account for header and 0 index
41
+ @table[item] = nil
42
+ end
43
+ @table.compact!
44
+ end
45
+
46
+
47
+ # Inverse slice: Only keep the rows in the range after sorting
48
+ def only!(range)
49
+ rows_to_exclude = rows.map do |row|
50
+ range.include?(row.row_num) ? nil : row.row_num
51
+ end
52
+ delete_rows!(rows_to_exclude.compact)
53
+ end
54
+
55
+ # Returns a Row object for the row at a certain index
56
+ def row_at(row_num)
57
+ TableFu::Row.new(@table[row_num], row_num, self)
58
+ end
59
+
60
+ # Returns all the Row objects for this object as a collection
61
+ def rows
62
+ all_rows = []
63
+ @table.each_with_index do |row, index|
64
+ all_rows << TableFu::Row.new(row, index, self)
65
+ end
66
+ all_rows.sort
67
+ end
68
+
69
+ # Return the headers defined in column headers or cherry picked from @col_opts
70
+ def columns
71
+ @col_opts[:columns] || column_headers
72
+ end
73
+
74
+ # Return the headers of the array
75
+ def headers
76
+ all_columns = []
77
+ columns.each do |header|
78
+ all_columns << TableFu::Header.new(header, header, nil, self)
79
+ end
80
+ all_columns
81
+ end
82
+
83
+ # Sum the values of a particular column
84
+ def sum_totals_for(column)
85
+ @totals[column.to_s] = rows.inject(0) { |sum, r| to_numeric(r.datum_for(column).value) + sum }
86
+ end
87
+
88
+ # Sum the values of a particular column and return a Datum
89
+ def total_for(column)
90
+ sum_totals_for(column)
91
+ Datum.new(@totals[column.to_s], column, nil, self)
92
+ end
93
+
94
+ # Return an array of TableFu instances grouped by a column.
95
+ def faceted_by(column, opts = {})
96
+ faceted_spreadsheets = {}
97
+ rows.each do |row|
98
+ unless row.column_for(column).value.nil?
99
+ faceted_spreadsheets[row.column_for(column).value] ||= []
100
+ faceted_spreadsheets[row.column_for(column).value] << row
101
+ end
102
+ end
103
+
104
+ # Create new table_fu instances for each facet
105
+ tables = []
106
+ faceted_spreadsheets.each do |key,value|
107
+ new_table = [@column_headers] + value
108
+ table = TableFu.new(new_table)
109
+ table.faceted_on = key
110
+ table.col_opts = @col_opts #formatting should be carried through
111
+ tables << table
112
+ end
113
+
114
+ tables.sort! do |a,b|
115
+ a.faceted_on <=> b.faceted_on
116
+ end
117
+
118
+ if opts[:total]
119
+ opts[:total].each do |c|
120
+ tables.each do |f|
121
+ f.sum_totals_for(c)
122
+ end
123
+ end
124
+ end
125
+
126
+ tables
127
+ end
128
+
129
+ # Return a numeric instance for a string number, or if it's a string we
130
+ # return 1, this way if we total up a series of strings it's a count
131
+ def to_numeric(num)
132
+ if num.nil?
133
+ 0
134
+ elsif num.kind_of? Integer
135
+ num
136
+ else
137
+ 1 # We count each instance of a string this way
138
+ end
139
+ end
140
+
141
+ # Return true if this table is faceted
142
+ def faceted?
143
+ not faceted_on.nil?
144
+ end
145
+
146
+ # Return the sorted_by column
147
+ def sorted_by
148
+ @col_opts[:sorted_by]
149
+ end
150
+
151
+ # Set the sorted_by column
152
+ def sorted_by=(header)
153
+ @col_opts[:sorted_by] = header
154
+ end
155
+
156
+ # Return the formatting hash
157
+ def formatting
158
+ @col_opts[:formatting]
159
+ end
160
+
161
+ # Set the formatting hash
162
+ def formatting=(headers)
163
+ @col_opts[:formatting] = headers
164
+ end
165
+
166
+ # Set up the cherry picked columns
167
+ def columns=(array)
168
+ @col_opts[:columns] = array
169
+ end
170
+
171
+
172
+
173
+ end
174
+
175
+ class TableFu
176
+ # TableFu::Row adds functionality to an row array in a TableFu instance
177
+ class Row < Array
178
+
179
+ attr_reader :row_num
180
+
181
+ def initialize(row, row_num, spreadsheet)
182
+ self.replace row
183
+ @row_num = row_num
184
+ @spreadsheet = spreadsheet
185
+ end
186
+
187
+ def columns
188
+ all_cols = []
189
+ @spreadsheet.columns.each do |column|
190
+ all_cols << datum_for(column)
191
+ end
192
+ all_cols
193
+ end
194
+
195
+ # This returns a Datum object for a header name. Will return a nil Datum object
196
+ # for nonexistant column names
197
+ #
198
+ # Parameters:
199
+ # header name
200
+ #
201
+ # Returns:
202
+ # Datum object
203
+ #
204
+ def datum_for(col_name)
205
+ if col_num = @spreadsheet.column_headers.index(col_name)
206
+ TableFu::Datum.new(self[col_num], col_name, @row_num, @spreadsheet)
207
+ else # Return a nil Datum object for non existant column names
208
+ TableFu::Datum.new(nil, col_name, @row_num, @spreadsheet)
209
+ end
210
+ end
211
+ alias_method :column_for, :datum_for
212
+
213
+ # Comparator for sorting a spreadsheet row.
214
+ #
215
+ def <=>(b)
216
+ if @spreadsheet.sorted_by
217
+ column = @spreadsheet.sorted_by.keys.first
218
+ order = @spreadsheet.sorted_by[@spreadsheet.sorted_by.keys.first]["order"]
219
+ format = @spreadsheet.sorted_by[@spreadsheet.sorted_by.keys.first]["format"]
220
+ a = column_for(column).value || ''
221
+ b = b.column_for(column).value || ''
222
+ if format
223
+ a = TableFu::Formatting.send(format, a) || ''
224
+ b = TableFu::Formatting.send(format, b) || ''
225
+ end
226
+ result = a <=> b
227
+ result = -1 if result.nil?
228
+ result = result * -1 if order == 'descending'
229
+ result
230
+ else
231
+ -1
232
+ end
233
+ end
234
+
235
+ end
236
+ # A Datum is an individual cell in the TableFu::Row
237
+ class Datum
238
+
239
+ attr_reader :options, :column_name
240
+
241
+ # Each piece of datum should know where it is by column and row number, along
242
+ # with the spreadsheet it's apart of. There's probably a better way to go
243
+ # about doing this. Subclass?
244
+ def initialize(datum, col_name, row_num, spreadsheet)
245
+ @datum = datum
246
+ @column_name = col_name
247
+ @row_num = row_num
248
+ @spreadsheet = spreadsheet
249
+ end
250
+
251
+ # Our standard formatter for the datum
252
+ #
253
+ # Returns:
254
+ # the formatted value, macro value, or a empty string
255
+ #
256
+ # First we test to see if this Datum has a macro attached to it. If so
257
+ # we let the macro method do it's magic
258
+ #
259
+ # Then we test for a simple formatter method.
260
+ #
261
+ # And finally we return a empty string object or the value.
262
+ #
263
+ def to_s
264
+ if macro_value
265
+ macro_value
266
+ elsif @spreadsheet.formatting && format_method = @spreadsheet.formatting[column_name]
267
+ TableFu::Formatting.send(format_method, @datum) || ''
268
+ else
269
+ @datum || ''
270
+ end
271
+ end
272
+
273
+ # Returns the macro'd format if there is one
274
+ #
275
+ # Returns:
276
+ # The macro value if it exists, otherwise nil
277
+ def macro_value
278
+ # Grab the macro method first
279
+ # Then get a array of the values in the columns listed as arguments
280
+ # Splat the arguments to the macro method.
281
+ # Example:
282
+ # @spreadsheet.col_opts[:formatting] =
283
+ # {'Total Appropriation' => :currency,
284
+ # 'AppendedColumn' => {'method' => 'append', 'arguments' => ['Projects','State']}}
285
+ #
286
+ # in the above case we handle the AppendedColumn in this method
287
+ if @row_num && @spreadsheet.formatting && @spreadsheet.formatting[@column_name].is_a?(Hash)
288
+ method = @spreadsheet.formatting[@column_name]['method']
289
+ arguments = @spreadsheet.formatting[@column_name]['arguments'].inject([]){|arr,arg| arr << @spreadsheet.rows[@row_num].column_for(arg); arr}
290
+ TableFu::Formatting.send(method, *arguments)
291
+ end
292
+ end
293
+
294
+ # Returns the raw value of a datum
295
+ #
296
+ # Returns:
297
+ # raw value of the datum, could be nil
298
+ def value
299
+ if @datum =~ /[0-9]+/
300
+ @datum.to_i
301
+ else
302
+ @datum
303
+ end
304
+ end
305
+
306
+ # This method missing looks for 4 matches
307
+ #
308
+ # First Option
309
+ # We have a column option by that method name and it applies to this column
310
+ # Example -
311
+ # >> @data.column_name
312
+ # => 'Total'
313
+ # >> @datum.style
314
+ # Finds col_opt[:style] = {'Total' => 'text-align:left;'}
315
+ # => 'text-align:left;'
316
+ #
317
+ # Second Option
318
+ # We have a column option by that method name, but no attribute
319
+ # >> @data.column_name
320
+ # => 'Total'
321
+ # >> @datum.style
322
+ # Finds col_opt[:style] = {'State' => 'text-align:left;'}
323
+ # => ''
324
+ #
325
+ # Third Option
326
+ # The boolean
327
+ # >> @data.invisible?
328
+ # And we've set it col_opts[:invisible] = ['Total']
329
+ # => true
330
+ #
331
+ # Fourth Option
332
+ # The boolean that's false
333
+ # >> @data.invisible?
334
+ # And it's not in the list col_opts[:invisible] = ['State']
335
+ # => false
336
+ #
337
+ def method_missing(method)
338
+ opts = indifferent_access @spreadsheet.col_opts
339
+ if val = opts[method] && opts[method][column_name]
340
+ val
341
+ elsif val = opts[method] && !opts[method][column_name]
342
+ ''
343
+ elsif method.to_s =~ /\?$/ && col_opts = opts[method.to_s.chop.to_sym]
344
+ col_opts.index(column_name) || false
345
+ elsif method.to_s =~ /\?$/ && !opts[method.to_s.chop.to_sym]
346
+ nil
347
+ else
348
+ super
349
+ end
350
+ end
351
+
352
+ private
353
+
354
+ # Enable string or symbol key access to col_opts
355
+ # from sinatra
356
+ def indifferent_access(params)
357
+ params = indifferent_hash.merge(params)
358
+ params.each do |key, value|
359
+ next unless value.is_a?(Hash)
360
+ params[key] = indifferent_access(value)
361
+ end
362
+ end
363
+
364
+ def indifferent_hash
365
+ Hash.new {|hash,key| hash[key.to_s] if Symbol === key }
366
+ end
367
+ end
368
+
369
+ # A header object needs to be a special kind of Datum, and
370
+ # we may want to extend this further, but currently we just
371
+ # need to ensure that when to_s is called on a @Header@ object
372
+ # that we don't run it through a macro, or a formatter.
373
+ class Header < Datum
374
+
375
+ def to_s
376
+ @datum
377
+ end
378
+
379
+ end
380
+
381
+ end
382
+
383
+ $:.unshift(File.dirname(__FILE__)) unless
384
+ $:.include?(File.dirname(__FILE__)) || $:.include?(File.expand_path(File.dirname(__FILE__)))
385
+
386
+ require 'table_fu/formatting'