tabledata 0.0.3
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/LICENSE.txt +8 -0
- data/README.markdown +55 -0
- data/Rakefile +10 -0
- data/lib/tabledata.rb +35 -0
- data/lib/tabledata/coercing_row.rb +15 -0
- data/lib/tabledata/coercion.rb +26 -0
- data/lib/tabledata/column.rb +47 -0
- data/lib/tabledata/detection.rb +76 -0
- data/lib/tabledata/exceptions.rb +17 -0
- data/lib/tabledata/parser.rb +64 -0
- data/lib/tabledata/patches/spreadsheet.rb +14 -0
- data/lib/tabledata/presenter.rb +35 -0
- data/lib/tabledata/presenters/csv.rb +39 -0
- data/lib/tabledata/presenters/excel.rb +36 -0
- data/lib/tabledata/presenters/html.rb +56 -0
- data/lib/tabledata/presenters/pdf.rb +27 -0
- data/lib/tabledata/row.rb +109 -0
- data/lib/tabledata/table.rb +250 -0
- data/lib/tabledata/version.rb +11 -0
- data/tabledata.gemspec +43 -0
- data/test/data/test1.xls +0 -0
- data/test/data/test1.xlsx +0 -0
- data/test/data/test_csv_utf8_comma_n.csv +4 -0
- data/test/data/test_csv_utf8_semicolon_n.csv +4 -0
- data/test/data/test_csv_win1252_comma_rn.csv +4 -0
- data/test/data/test_csv_win1252_semicolon_rn.csv +4 -0
- data/test/unit/all.rb +0 -0
- metadata +127 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 0542e111de916c971f41cdec6401c4a176225544
|
4
|
+
data.tar.gz: 7ead0e2dfc2147a68588285c2bb84d097bb91b32
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: a08b73d455e4f262dbf8de1641eee2a8187b7471a07aa9216e981b7529a42cb52aab711fe861c599e99ab12762a793f2236f808f5076608fb91db353867272ff
|
7
|
+
data.tar.gz: 24c0ebf7907290a4ea8267d80f6473554b72407b2be7a21930881d2db5b811eb8d973eb7256fcb670cc4d9227be6f10e360f548a6407f25c0c728196c2fbfb31
|
data/LICENSE.txt
ADDED
@@ -0,0 +1,8 @@
|
|
1
|
+
Copyright (c) 2013, Stefan Rusterholz <stefan.rusterholz@gmail.com>
|
2
|
+
All rights reserved.
|
3
|
+
|
4
|
+
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
|
5
|
+
|
6
|
+
Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
|
7
|
+
Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
|
8
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
data/README.markdown
ADDED
@@ -0,0 +1,55 @@
|
|
1
|
+
README
|
2
|
+
======
|
3
|
+
|
4
|
+
|
5
|
+
|
6
|
+
Summary
|
7
|
+
-------
|
8
|
+
|
9
|
+
Read tabular data from various formats, like Excel .xls, Excel .xlsx, CSV.
|
10
|
+
|
11
|
+
|
12
|
+
|
13
|
+
Installation
|
14
|
+
------------
|
15
|
+
|
16
|
+
`gem install tables`
|
17
|
+
|
18
|
+
|
19
|
+
|
20
|
+
Usage
|
21
|
+
-----
|
22
|
+
|
23
|
+
table = TableData.table_from_file(path)
|
24
|
+
|
25
|
+
|
26
|
+
|
27
|
+
Description
|
28
|
+
-----------
|
29
|
+
|
30
|
+
Read tabular data from various formats.
|
31
|
+
|
32
|
+
|
33
|
+
|
34
|
+
Known Issues
|
35
|
+
------------
|
36
|
+
|
37
|
+
* The 'spreadsheet' gem on which this gem depends does not yet correctly work with ruby 2.0.
|
38
|
+
|
39
|
+
|
40
|
+
|
41
|
+
Links
|
42
|
+
-----
|
43
|
+
|
44
|
+
* [Online API Documentation](http://rdoc.info/github/apeiros/tabledata/)
|
45
|
+
* [Public Repository](https://github.com/apeiros/tabledata)
|
46
|
+
* [Bug Reporting](https://github.com/apeiros/tabledata/issues)
|
47
|
+
* [RubyGems Site](https://rubygems.org/gems/tabledata)
|
48
|
+
|
49
|
+
|
50
|
+
|
51
|
+
License
|
52
|
+
-------
|
53
|
+
|
54
|
+
You can use this code under the {file:LICENSE.txt BSD-2-Clause License}, free of charge.
|
55
|
+
If you need a different license, please ask the author.
|
data/Rakefile
ADDED
@@ -0,0 +1,10 @@
|
|
1
|
+
$LOAD_PATH.unshift(File.expand_path('../rake/lib', __FILE__))
|
2
|
+
Dir.glob(File.expand_path('../rake/tasks/**/*.{rake,task,rb}', __FILE__)) do |task_file|
|
3
|
+
begin
|
4
|
+
import task_file
|
5
|
+
rescue LoadError => e
|
6
|
+
warn "Failed to load task file #{task_file}"
|
7
|
+
warn " #{e.class} #{e.message}"
|
8
|
+
warn " #{e.backtrace.first}"
|
9
|
+
end
|
10
|
+
end
|
data/lib/tabledata.rb
ADDED
@@ -0,0 +1,35 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
require 'tabledata/version'
|
4
|
+
require 'tabledata/table'
|
5
|
+
|
6
|
+
# Tables
|
7
|
+
# Read tabular data from various formats.
|
8
|
+
module TableData
|
9
|
+
module_function
|
10
|
+
|
11
|
+
# @see TableData::Table::from_file Full documentation
|
12
|
+
def table_from_file(path, options=nil)
|
13
|
+
Table.from_file(path, options)
|
14
|
+
end
|
15
|
+
|
16
|
+
# NOT IMPLEMENTED!
|
17
|
+
#
|
18
|
+
# @return [TableData::Tables]
|
19
|
+
def tables_from_file(path)
|
20
|
+
raise "Unimplemented"
|
21
|
+
end
|
22
|
+
|
23
|
+
def require_library(name, message)
|
24
|
+
$stdout, oldstdout = StringIO.new, $stdout
|
25
|
+
require name
|
26
|
+
rescue LoadError => error
|
27
|
+
if error.message =~ /cannot load such file -- #{Regexp.escape(name)}/ then
|
28
|
+
raise LibraryMissingError.new(name, message, error)
|
29
|
+
else
|
30
|
+
raise
|
31
|
+
end
|
32
|
+
ensure
|
33
|
+
$stdout = oldstdout
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
module TableData
|
4
|
+
class CoercingRow < Row
|
5
|
+
def initialize(table, index, data, coercions)
|
6
|
+
@coercions = coercions
|
7
|
+
super(table, index, data.map.with_index { |value, col| coerce(col, value) })
|
8
|
+
end
|
9
|
+
|
10
|
+
def coerce(column, value)
|
11
|
+
coercer = @coercions[column]
|
12
|
+
coercer ? coercer.call(value) : value
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
module TableData
|
4
|
+
module Coercion
|
5
|
+
@coerce = {}
|
6
|
+
|
7
|
+
def self.[](key)
|
8
|
+
@coerce[key]
|
9
|
+
end
|
10
|
+
|
11
|
+
def self.coercing(key, &block)
|
12
|
+
@coerce[key] = block
|
13
|
+
end
|
14
|
+
|
15
|
+
coercing String do |value, format|
|
16
|
+
value.to_s
|
17
|
+
end
|
18
|
+
|
19
|
+
coercing Integer do |value, format|
|
20
|
+
Integer(value, format.fetch(:base, 10))
|
21
|
+
end
|
22
|
+
|
23
|
+
coercing Date do |value, format|
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
module TableData
|
4
|
+
class Column
|
5
|
+
attr_reader :table
|
6
|
+
attr_reader :index
|
7
|
+
|
8
|
+
include Enumerable
|
9
|
+
|
10
|
+
def initialize(table, index)
|
11
|
+
@table = table
|
12
|
+
@index = index
|
13
|
+
end
|
14
|
+
|
15
|
+
def header
|
16
|
+
@table.column_header(@index)
|
17
|
+
end
|
18
|
+
|
19
|
+
def accessor
|
20
|
+
@table.column_accessor(@index)
|
21
|
+
end
|
22
|
+
|
23
|
+
def [](*args)
|
24
|
+
rows = @table.rows[*args]
|
25
|
+
|
26
|
+
if rows.is_a?(Array) # slice
|
27
|
+
rows.map { |row| row.at(@index) }
|
28
|
+
else # single row
|
29
|
+
rows.at(@index)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def each
|
34
|
+
return enum_for(__method__) unless block_given?
|
35
|
+
|
36
|
+
@table.each do |row|
|
37
|
+
yield row.at(@index)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def to_a(include_header=true)
|
42
|
+
data = @table.data.transpose[@index]
|
43
|
+
|
44
|
+
include_header || !@table.headers? ? data : data[1..-1]
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,76 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
# Encoding::Windows_1252
|
4
|
+
# Encoding::MacRoman
|
5
|
+
# Encoding::UTF_8
|
6
|
+
# Encoding::ISO8859_15
|
7
|
+
|
8
|
+
require 'tabledata/exceptions'
|
9
|
+
|
10
|
+
module TableData
|
11
|
+
module Detection
|
12
|
+
UnlikelyCharsWin1252 = "\xA0\xA1\xA2\xA3\xA4\xA5\xA6\xA7\xA8\xA9\xAA\xAB\xAC\xAD" \
|
13
|
+
"\xAE\xAF\xB0\xB1\xB2\xB3\xB4\xB5\xB6\xB7\xB8\xB9\xBA\xBB" \
|
14
|
+
"\xBC\xBD\xBE\xBF\xD7\xF7"
|
15
|
+
UnlikelyCharsIso8859_1 = ""
|
16
|
+
UnlikelyCharsMacRoman = ""
|
17
|
+
|
18
|
+
UmlautsMac = "äöü".encode(Encoding::MacRoman).force_encoding(Encoding::BINARY)
|
19
|
+
UmlautsWin = "äöü".encode(Encoding::Windows_1252).force_encoding(Encoding::BINARY)
|
20
|
+
|
21
|
+
DiacritsMac = "âàéèô".encode(Encoding::MacRoman).force_encoding(Encoding::BINARY)
|
22
|
+
DiacritsWin = "âàéèô".encode(Encoding::Windows_1252).force_encoding(Encoding::BINARY)
|
23
|
+
|
24
|
+
module_function
|
25
|
+
def force_guessed_encoding!(string)
|
26
|
+
return string if string.force_encoding(Encoding::UTF_8).valid_encoding?
|
27
|
+
string.force_encoding(Encoding::BINARY)
|
28
|
+
|
29
|
+
# check for non-mapped codepoints
|
30
|
+
possible_encodings = [Encoding::Windows_1252, Encoding::ISO8859_15, Encoding::MacRoman]
|
31
|
+
possible_encodings.delete(Encoding::ISO8859_15) if string =~ /[\x80-\x9f]/n
|
32
|
+
possible_encodings.delete(Encoding::Windows_1252) if string =~ /[\x81\x8D\x8F\x90\x9D]/n
|
33
|
+
return string.force_encoding(possible_encodings.first) if possible_encodings.size == 1
|
34
|
+
|
35
|
+
# # check for occurrences of characters with weighted expectancy
|
36
|
+
# # e.g. a "§" is quite unlikely
|
37
|
+
# win = string[0,10_000].count(UnlikelyCharsWin1252)
|
38
|
+
# iso = string[0,10_000].count(UnlikelyCharsIso8859_1)
|
39
|
+
# mac = string[0,10_000].count(UnlikelyCharsMacRoman)
|
40
|
+
|
41
|
+
# Check occurrences of äöü
|
42
|
+
case string[0,10_000].count(UmlautsMac) <=> string[0,10_000].count(UmlautsWin)
|
43
|
+
when -1 then return string.force_encoding(Encoding::Windows_1252)
|
44
|
+
when 1 then return string.force_encoding(Encoding::MacRoman)
|
45
|
+
end
|
46
|
+
|
47
|
+
# Check occurrences of âàéèô
|
48
|
+
case string[0,10_000].count(DiacritsMac) <=> string[0,10_000].count(DiacritsWin)
|
49
|
+
when -1 then return string.force_encoding(Encoding::Windows_1252)
|
50
|
+
when 1 then return string.force_encoding(Encoding::MacRoman)
|
51
|
+
end
|
52
|
+
|
53
|
+
# Bias for Windows_1252
|
54
|
+
string.force_encoding(Encoding::Windows_1252)
|
55
|
+
end
|
56
|
+
|
57
|
+
def guess_encoding(string)
|
58
|
+
force_guessed_encoding!(string.dup).encoding
|
59
|
+
end
|
60
|
+
|
61
|
+
def guess_csv_delimiter(csv, out_of=[',',';'])
|
62
|
+
out_of = out_of.map { |delimiter| delimiter.encode(csv.encoding) }
|
63
|
+
|
64
|
+
out_of.max_by { |delimiter| csv[0, 10_000].count(delimiter) }
|
65
|
+
end
|
66
|
+
|
67
|
+
def file_type_from_path(path)
|
68
|
+
case path
|
69
|
+
when /\.csv$/ then :csv
|
70
|
+
when /\.xls$/ then :xls
|
71
|
+
when /\.xlsx$/ then :xlsx
|
72
|
+
else raise InvalidFileType, "Unknown file format for path #{path.inspect}"
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
module TableData
|
4
|
+
module Exception
|
5
|
+
end
|
6
|
+
class InvalidFileType < ArgumentError
|
7
|
+
include Exception
|
8
|
+
end
|
9
|
+
class InvalidColumnCount < ArgumentError
|
10
|
+
include Exception
|
11
|
+
end
|
12
|
+
class InvalidColumnSpecifier < ArgumentError
|
13
|
+
include Exception
|
14
|
+
end
|
15
|
+
class InvalidColumnName < InvalidColumnSpecifier; end
|
16
|
+
class InvalidColumnAccessor < InvalidColumnSpecifier; end
|
17
|
+
end
|
@@ -0,0 +1,64 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
require 'tabledata/detection'
|
4
|
+
require 'tabledata/table'
|
5
|
+
require 'stringio'
|
6
|
+
|
7
|
+
module TableData
|
8
|
+
module Parser
|
9
|
+
module_function
|
10
|
+
|
11
|
+
def parse_csv(file, options=nil)
|
12
|
+
TableData.require_library 'csv', "To parse CSV files, the gem 'csv' must be installed." # Should not really happen, in 1.9, csv is part of stdlib and should be present
|
13
|
+
|
14
|
+
table_class = (options && options[:table_class]) || Table
|
15
|
+
table = table_class.new([], options)
|
16
|
+
data = read_file(file, options && options[:encoding])
|
17
|
+
seperator = (options && options[:separator]) || Detection.guess_csv_delimiter(data)
|
18
|
+
CSV.parse(data,col_sep: seperator) do |row|
|
19
|
+
table << row
|
20
|
+
end
|
21
|
+
|
22
|
+
table
|
23
|
+
end
|
24
|
+
|
25
|
+
def parse_xls(file, options=nil)
|
26
|
+
TableData.require_library 'roo', "To parse Excel .xls files, the gem 'roo' must be installed." # TODO: get rid of that dependency
|
27
|
+
TableData.require_library 'iconv', "To parse Excel .xls files, the gem 'iconv' must be installed." # TODO: get rid of that dependency
|
28
|
+
|
29
|
+
table_class = (options && options[:table_class]) || Table
|
30
|
+
table = table_class.new([], options)
|
31
|
+
parser = Roo::Excel.new(file)
|
32
|
+
parser.first_row.upto(parser.last_row) do |row|
|
33
|
+
table << parser.row(row)
|
34
|
+
end
|
35
|
+
|
36
|
+
table
|
37
|
+
end
|
38
|
+
|
39
|
+
def parse_xlsx(file, options=nil)
|
40
|
+
TableData.require_library 'roo', "To parse Excel .xlsx files, the gem 'roo' must be installed." # TODO: get rid of that dependency
|
41
|
+
|
42
|
+
table_class = (options && options[:table_class]) || Table
|
43
|
+
table = table_class.new([], options)
|
44
|
+
parser = Roo::Excelx.new(file)
|
45
|
+
parser.first_row.upto(parser.last_row) do |row|
|
46
|
+
table << parser.row(row)
|
47
|
+
end
|
48
|
+
|
49
|
+
table
|
50
|
+
end
|
51
|
+
|
52
|
+
def read_file(path, encoding)
|
53
|
+
if encoding then
|
54
|
+
File.read(path, encoding: encoding)
|
55
|
+
else
|
56
|
+
data = File.read(path, encoding: Encoding::BINARY)
|
57
|
+
Detection.force_guessed_encoding!(data)
|
58
|
+
data.encode!(Encoding.default_internal) if Encoding.default_internal
|
59
|
+
|
60
|
+
data
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
module TableData
|
4
|
+
class Presenter
|
5
|
+
@presenters = {
|
6
|
+
csv: ['tabledata/presenters/csv', [:TableData, :Presenters, :CSV], {}],
|
7
|
+
excel_csv: ['tabledata/presenters/csv', [:TableData, :Presenters, :CSV], {column_separator: ";", row_separator: "\r\n"}],
|
8
|
+
tab: ['tabledata/presenters/csv', [:TableData, :Presenters, :CSV], {column_separator: "\t"}],
|
9
|
+
xls: ['tabledata/presenters/excel', [:TableData, :Presenters, :Excel], {suffix: '.xls'}],
|
10
|
+
xlsx: ['tabledata/presenters/excel', [:TableData, :Presenters, :Excel], {suffix: '.xlsx'}],
|
11
|
+
html: ['tabledata/presenters/html', [:TableData, :Presenters, :HTML], {}],
|
12
|
+
pdf: ['tabledata/presenters/pdf', [:TableData, :Presenters, :PDF], {}],
|
13
|
+
}
|
14
|
+
|
15
|
+
def self.present(table, format, options)
|
16
|
+
code, constant, default_options = *@presenters[format]
|
17
|
+
raise ArgumentError, "Unknown format #{format.inspect}" unless code
|
18
|
+
require code
|
19
|
+
klass = constant.inject(Object) { |source, current| source.const_get(current) }
|
20
|
+
|
21
|
+
klass.new(table, options ? default_options.merge(options) : default_options.dup)
|
22
|
+
end
|
23
|
+
|
24
|
+
attr_reader :table
|
25
|
+
|
26
|
+
def initialize(table, options)
|
27
|
+
@table = table
|
28
|
+
@options = options
|
29
|
+
end
|
30
|
+
|
31
|
+
def write(path, options=nil)
|
32
|
+
File.write(path, string, encoding: 'utf-8')
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
require 'tabledata/presenter'
|
4
|
+
require 'csv'
|
5
|
+
|
6
|
+
module TableData
|
7
|
+
module Presenters
|
8
|
+
class CSV < TableData::Presenter
|
9
|
+
OptionMapping = {
|
10
|
+
column_separator: :col_sep,
|
11
|
+
row_separator: :row_sep,
|
12
|
+
quote_char: :quote_character,
|
13
|
+
}
|
14
|
+
|
15
|
+
def csv_options
|
16
|
+
options = ::CSV::DEFAULT_OPTIONS.dup
|
17
|
+
@options.each do |k,v| options[OptionMapping.fetch(k,k)] = v end
|
18
|
+
|
19
|
+
options
|
20
|
+
end
|
21
|
+
|
22
|
+
def string(options=nil)
|
23
|
+
::CSV.generate(csv_options) do |csv|
|
24
|
+
@table.each_row do |row|
|
25
|
+
csv << row.to_a
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def write(path, options=nil)
|
31
|
+
::CSV.open(path, 'wb', csv_options) do |csv|
|
32
|
+
@table.each_row do |row|
|
33
|
+
csv << row.to_a
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
require 'tabledata/presenter'
|
4
|
+
TableData.require_library 'spreadsheet', "To generate Excel files, the gem 'spreadsheet' must be installed."
|
5
|
+
require 'tabledata/patches/spreadsheet'
|
6
|
+
|
7
|
+
|
8
|
+
module TableData
|
9
|
+
module Presenters
|
10
|
+
class Excel < TableData::Presenter
|
11
|
+
Bold = Spreadsheet::Format.new weight: :bold
|
12
|
+
|
13
|
+
def document
|
14
|
+
document = Spreadsheet::Workbook.new
|
15
|
+
sheet = document.create_worksheet(name: @options[:worksheet_name])
|
16
|
+
sheet.row(0).default_format = Bold if @options[:bold_headers]
|
17
|
+
|
18
|
+
@table.data.each_with_index do |row, row_nr|
|
19
|
+
row.each_with_index do |col, col_nr|
|
20
|
+
sheet[row_nr, col_nr] = col
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
document
|
25
|
+
end
|
26
|
+
|
27
|
+
def string(options=nil)
|
28
|
+
document.to_string
|
29
|
+
end
|
30
|
+
|
31
|
+
def write(path, options=nil)
|
32
|
+
document.write(path)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
require 'tabledata/presenter'
|
4
|
+
require 'cgi'
|
5
|
+
|
6
|
+
module TableData
|
7
|
+
module Presenters
|
8
|
+
class HTML < TableData::Presenter
|
9
|
+
def html_head
|
10
|
+
<<-EOHTML
|
11
|
+
<!DOCTYPE html>
|
12
|
+
<html>
|
13
|
+
<head>
|
14
|
+
<meta charset='utf-8'>
|
15
|
+
</head>
|
16
|
+
<body>
|
17
|
+
<table>
|
18
|
+
EOHTML
|
19
|
+
end
|
20
|
+
|
21
|
+
def html_foot
|
22
|
+
<<-EOHTML
|
23
|
+
|
24
|
+
</tbody>
|
25
|
+
</table>
|
26
|
+
</body>
|
27
|
+
</html>
|
28
|
+
EOHTML
|
29
|
+
end
|
30
|
+
|
31
|
+
def html_table_header
|
32
|
+
if @table.headers?
|
33
|
+
" <thead>\n <tr>\n"+
|
34
|
+
@table.headers.map { |cell|" <th>#{CGI.escapeHTML(cell)}</th>" }.join("\n")+
|
35
|
+
"\n </tr>\n </thead>\n"
|
36
|
+
else
|
37
|
+
''
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def string(options=nil)
|
42
|
+
html_head+
|
43
|
+
html_table_header+
|
44
|
+
" </body>\n"+
|
45
|
+
@table.body.map { |row|
|
46
|
+
" <tr>\n"+row.map { |cell| " <td>#{CGI.escapeHTML(cell)}</td>" }.join("\n")+"\n </tr>"
|
47
|
+
}.join("\n")+
|
48
|
+
html_foot
|
49
|
+
end
|
50
|
+
|
51
|
+
def write(path, options=nil)
|
52
|
+
File.write(path, string, encoding: 'utf-8')
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
require 'tabledata/presenter'
|
4
|
+
TableData.require_library 'prawn', "To generate PDF files, the gem 'prawn' must be installed."
|
5
|
+
|
6
|
+
|
7
|
+
module TableData
|
8
|
+
module Presenters
|
9
|
+
class PDF < TableData::Presenter
|
10
|
+
|
11
|
+
def document
|
12
|
+
pdf = Prawn::Document.new
|
13
|
+
pdf.table @table.data
|
14
|
+
|
15
|
+
pdf
|
16
|
+
end
|
17
|
+
|
18
|
+
def string(options=nil)
|
19
|
+
document.render
|
20
|
+
end
|
21
|
+
|
22
|
+
def write(path, options=nil)
|
23
|
+
document.render_file(path)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,109 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
require 'tabledata/exceptions'
|
4
|
+
|
5
|
+
module TableData
|
6
|
+
class Row
|
7
|
+
attr_reader :table
|
8
|
+
attr_reader :index
|
9
|
+
attr_reader :data
|
10
|
+
|
11
|
+
include Enumerable
|
12
|
+
|
13
|
+
def initialize(table, index, data)
|
14
|
+
@table = table
|
15
|
+
@index = index
|
16
|
+
@data = data
|
17
|
+
end
|
18
|
+
|
19
|
+
# Iterate over each cell in this row
|
20
|
+
def each(&block)
|
21
|
+
@data.each(&block)
|
22
|
+
end
|
23
|
+
|
24
|
+
# @see #slice for a faster way to use ranges or offset+length
|
25
|
+
# @see #at_accessor for a faster way to access by name
|
26
|
+
# @see #at_index for a faster way to access by index
|
27
|
+
# @see #at_header for a faster way to access by header value
|
28
|
+
def [](a,b=nil)
|
29
|
+
if b || a.is_a?(Range) then
|
30
|
+
slice(a,b)
|
31
|
+
else
|
32
|
+
at(a)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def slice(*args)
|
37
|
+
@data[*args]
|
38
|
+
end
|
39
|
+
|
40
|
+
def at(column)
|
41
|
+
case column
|
42
|
+
when Symbol then at_accessor(column)
|
43
|
+
when String then at_header(column)
|
44
|
+
when Integer then at_index(column)
|
45
|
+
else raise InvalidColumnSpecifier, "Invalid index type, expected Symbol, String or Integer, but got #{column.class}"
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def at_header(name)
|
50
|
+
index = @table.index_for_header(name)
|
51
|
+
raise InvalidColumnName, "No column named #{name}" unless index
|
52
|
+
|
53
|
+
@data[index]
|
54
|
+
end
|
55
|
+
|
56
|
+
def at_accessor(name)
|
57
|
+
index = @table.index_for_accessor(name)
|
58
|
+
raise InvalidColumnAccessor, "No column named #{name}" unless index
|
59
|
+
|
60
|
+
@data[index]
|
61
|
+
end
|
62
|
+
|
63
|
+
def at_index(index)
|
64
|
+
@data.at(index)
|
65
|
+
end
|
66
|
+
|
67
|
+
def values_at(*columns)
|
68
|
+
columns.map { |column| at(column) }
|
69
|
+
end
|
70
|
+
|
71
|
+
# @return [Integer] The number of cells in this row
|
72
|
+
def size
|
73
|
+
@data.size
|
74
|
+
end
|
75
|
+
|
76
|
+
def to_hash
|
77
|
+
Hash[@table.accessor_columns.map { |accessor, index| [accessor, @data[index]] }]
|
78
|
+
end
|
79
|
+
|
80
|
+
alias to_a data
|
81
|
+
|
82
|
+
def respond_to_missing?(name, include_private)
|
83
|
+
@table.index_for_accessor(name) ? true : false
|
84
|
+
end
|
85
|
+
|
86
|
+
def method_missing(name, *args, &block)
|
87
|
+
return super unless @table.accessors?
|
88
|
+
|
89
|
+
name =~ /^(\w+)(=)?$/
|
90
|
+
name_mod, assign = $1, $2
|
91
|
+
index = @table.index_for_accessor(name_mod)
|
92
|
+
arg_count = assign ? 1 : 0
|
93
|
+
|
94
|
+
return super unless index
|
95
|
+
|
96
|
+
raise ArgumentError, "Wrong number of arguments (#{args.size} for #{arg_count})" if args.size > arg_count
|
97
|
+
|
98
|
+
if assign then
|
99
|
+
@data[index] = args.first
|
100
|
+
else
|
101
|
+
@data[index]
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
def inspect
|
106
|
+
sprintf "%s%p", self.class, to_a
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
@@ -0,0 +1,250 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
require 'tabledata/parser'
|
4
|
+
require 'tabledata/row'
|
5
|
+
require 'tabledata/column'
|
6
|
+
require 'tabledata/detection'
|
7
|
+
require 'tabledata/exceptions'
|
8
|
+
require 'tabledata/presenter'
|
9
|
+
|
10
|
+
module TableData
|
11
|
+
|
12
|
+
# This class represents the tabular data.
|
13
|
+
class Table
|
14
|
+
|
15
|
+
include Enumerable
|
16
|
+
|
17
|
+
# The default options for TableData::Table#initialize
|
18
|
+
DefaultOptions = {
|
19
|
+
has_header: true,
|
20
|
+
has_footer: false, # currently unused
|
21
|
+
accessors: [],
|
22
|
+
}
|
23
|
+
|
24
|
+
# @option options [Symbol] :file_type
|
25
|
+
# The file type. Nil for auto-detection (which uses the extension of the
|
26
|
+
# filename), or one of :csv, :xls or :xlsx
|
27
|
+
# @option options [Symbol] :table_class
|
28
|
+
# The class to use for this table. Defaults to self (TableData::Table)
|
29
|
+
#
|
30
|
+
# All other options are passed on to Parser.parse_csv, .parse_xls or parse_xlsx,
|
31
|
+
# which in turn passes remaining options on to Table#initialize
|
32
|
+
#
|
33
|
+
# @return [TableData::Table]
|
34
|
+
def self.from_file(path, options=nil)
|
35
|
+
options ||= {}
|
36
|
+
options[:table_class] ||= self
|
37
|
+
options[:file_type] ||= Detection.file_type_from_path(path)
|
38
|
+
|
39
|
+
case options[:file_type]
|
40
|
+
when :csv then Parser.parse_csv(path, options)
|
41
|
+
when :xls then Parser.parse_xls(path, options)
|
42
|
+
when :xlsx then Parser.parse_xlsx(path, options)
|
43
|
+
else raise InvalidFileType, "Unknown file format #{options[:file_type].inspect}"
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
# @return [Array<Symbol>] An array of all named accessors
|
48
|
+
attr_reader :accessors
|
49
|
+
|
50
|
+
# @return [Hash<Symbol => Integer>] A hash mapping column accessor names to the column index
|
51
|
+
attr_reader :accessor_columns
|
52
|
+
|
53
|
+
# @private
|
54
|
+
# The internal data structure. Do not modify.
|
55
|
+
attr_reader :data
|
56
|
+
|
57
|
+
def initialize(data=[], options=nil)
|
58
|
+
options = options ? self.class::DefaultOptions.merge(options) : self.class::DefaultOptions.dup
|
59
|
+
column_count = data.first ? data.first.size : 0
|
60
|
+
@has_header = options.delete(:has_header) ? true : false
|
61
|
+
@data = data
|
62
|
+
@rows = data.map.with_index { |row, index|
|
63
|
+
raise InvalidColumnCount, "Invalid column count in row #{index} (#{column_count} expected, but has #{row.size})" if index > 0 && row.size != column_count
|
64
|
+
raise ArgumentError, "Row must be provided as Array, but got #{row.class} in row #{index}" unless row.is_a?(Array)
|
65
|
+
|
66
|
+
Row.new(self, index, row)
|
67
|
+
}
|
68
|
+
@column_count = nil
|
69
|
+
@header_columns = nil
|
70
|
+
@accessor_columns = {}
|
71
|
+
@column_accessors = {}
|
72
|
+
@accessors = [].freeze
|
73
|
+
self.accessors = options.delete(:accessors)
|
74
|
+
end
|
75
|
+
|
76
|
+
# @param [Array<Symbol>] accessors
|
77
|
+
#
|
78
|
+
# Define the name of the accessors used in TableData::Row.
|
79
|
+
def accessors=(accessors)
|
80
|
+
if accessors
|
81
|
+
@accessors = accessors.map(&:to_sym).freeze
|
82
|
+
@accessors.each_with_index do |name, idx|
|
83
|
+
@accessor_columns[name] = idx
|
84
|
+
end
|
85
|
+
@column_accessors = @accessor_columns.invert
|
86
|
+
else
|
87
|
+
@accessors = [].freeze
|
88
|
+
@accessor_columns.clear
|
89
|
+
@column_accessors = @accessor_columns.clear
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
# The number of rows, excluding headers
|
94
|
+
def size
|
95
|
+
@data.size - (@has_header ? 1 : 0)
|
96
|
+
end
|
97
|
+
alias length size
|
98
|
+
|
99
|
+
# @return [Integer] The number of columns
|
100
|
+
def column_count
|
101
|
+
@data.first ? @data.first.size : 0
|
102
|
+
end
|
103
|
+
|
104
|
+
# Array#[] like access to the rows in the body of the table.
|
105
|
+
#
|
106
|
+
# @return [Array<TableData::Row>]
|
107
|
+
def [](*args)
|
108
|
+
body[*args]
|
109
|
+
end
|
110
|
+
|
111
|
+
def cell(row, column, default=nil)
|
112
|
+
row_data = row(row)
|
113
|
+
|
114
|
+
if row_data
|
115
|
+
row_data.at(column)
|
116
|
+
elsif block_given?
|
117
|
+
yield(self, row, column)
|
118
|
+
else
|
119
|
+
default
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
def row(row)
|
124
|
+
@rows[row]
|
125
|
+
end
|
126
|
+
|
127
|
+
def column_accessor(index)
|
128
|
+
@column_accessors[index]
|
129
|
+
end
|
130
|
+
|
131
|
+
def column_name(index)
|
132
|
+
h = headers
|
133
|
+
|
134
|
+
h && h.at(index)
|
135
|
+
end
|
136
|
+
|
137
|
+
def columns
|
138
|
+
Array.new(column_count) { |col| column(col) }
|
139
|
+
end
|
140
|
+
|
141
|
+
def column(index)
|
142
|
+
Column.new(self, index)
|
143
|
+
end
|
144
|
+
|
145
|
+
def index_for_accessor(name)
|
146
|
+
@accessor_columns[name.to_sym]
|
147
|
+
end
|
148
|
+
|
149
|
+
def index_for_header(name)
|
150
|
+
if @has_header && @data.first then
|
151
|
+
@header_columns ||= Hash[@data.first.each_with_index.to_a]
|
152
|
+
@header_columns[name]
|
153
|
+
else
|
154
|
+
nil
|
155
|
+
end
|
156
|
+
end
|
157
|
+
|
158
|
+
def accessors?
|
159
|
+
!@accessors.empty?
|
160
|
+
end
|
161
|
+
|
162
|
+
def headers?
|
163
|
+
@has_header
|
164
|
+
end
|
165
|
+
|
166
|
+
def headers
|
167
|
+
headers? ? @rows.first : nil
|
168
|
+
end
|
169
|
+
|
170
|
+
def body
|
171
|
+
headers? ? @rows[1..-1] : @rows
|
172
|
+
end
|
173
|
+
|
174
|
+
def <<(row)
|
175
|
+
index = @data.size
|
176
|
+
|
177
|
+
raise InvalidColumnCount, "Invalid column count in row #{index} (#{@data.first.size} expected, but has #{row.size})" if @data.first && row.size != @data.first.size
|
178
|
+
raise ArgumentError, "Row must be provided as Array, but got #{row.class} in row #{index}" unless row.is_a?(Array)
|
179
|
+
|
180
|
+
@data << row
|
181
|
+
@rows << Row.new(self, index, row)
|
182
|
+
|
183
|
+
self
|
184
|
+
end
|
185
|
+
|
186
|
+
# Iterate over all rows in the body
|
187
|
+
#
|
188
|
+
# @see TableData::Table#each_row A method which iterates over all rows, including headers
|
189
|
+
#
|
190
|
+
# @yield [row]
|
191
|
+
# @yieldparam [TableData::Row]
|
192
|
+
#
|
193
|
+
# @return [self]
|
194
|
+
def each(&block)
|
195
|
+
return enum_for(__method__) unless block
|
196
|
+
|
197
|
+
body.each(&block)
|
198
|
+
|
199
|
+
self
|
200
|
+
end
|
201
|
+
|
202
|
+
# Iterate over all rows, header and body
|
203
|
+
#
|
204
|
+
# @see TableData::Table#each A method which iterates only over body-rows
|
205
|
+
#
|
206
|
+
# @yield [row]
|
207
|
+
# @yieldparam [TableData::Row]
|
208
|
+
#
|
209
|
+
# @return [self]
|
210
|
+
def each_row(&block)
|
211
|
+
return enum_for(__method__) unless block
|
212
|
+
|
213
|
+
@data.each(&block)
|
214
|
+
|
215
|
+
self
|
216
|
+
end
|
217
|
+
|
218
|
+
# Iterate over all columns
|
219
|
+
#
|
220
|
+
# @yield [column]
|
221
|
+
# @yieldparam [TableData::Column]
|
222
|
+
#
|
223
|
+
# @return [self]
|
224
|
+
def each_column
|
225
|
+
return enum_for(__method__) unless block
|
226
|
+
|
227
|
+
column_count.times do |i|
|
228
|
+
yield column(i)
|
229
|
+
end
|
230
|
+
|
231
|
+
self
|
232
|
+
end
|
233
|
+
|
234
|
+
def to_nested_array
|
235
|
+
to_a.map(&:to_a)
|
236
|
+
end
|
237
|
+
|
238
|
+
def to_a
|
239
|
+
@data
|
240
|
+
end
|
241
|
+
|
242
|
+
def format(format_id, options=nil)
|
243
|
+
Presenter.present(self, format_id, options)
|
244
|
+
end
|
245
|
+
|
246
|
+
def inspect
|
247
|
+
sprintf "#<%s headers: %p, cols: %d, rows: %d>", self.class, headers?, column_count, size
|
248
|
+
end
|
249
|
+
end
|
250
|
+
end
|
data/tabledata.gemspec
ADDED
@@ -0,0 +1,43 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
Gem::Specification.new do |s|
|
4
|
+
s.name = "tabledata"
|
5
|
+
s.version = "0.0.3"
|
6
|
+
s.authors = "Stefan Rusterholz"
|
7
|
+
s.email = "stefan.rusterholz@gmail.com"
|
8
|
+
s.homepage = "https://github.com/apeiros/tabledata"
|
9
|
+
s.license = 'BSD 2-Clause'
|
10
|
+
|
11
|
+
s.description = <<-DESCRIPTION.gsub(/^ /, '').chomp
|
12
|
+
Read and write tabular data from and to various formats.
|
13
|
+
DESCRIPTION
|
14
|
+
s.summary = <<-SUMMARY.gsub(/^ /, '').chomp
|
15
|
+
Read and write tabular data from and to various formats.
|
16
|
+
SUMMARY
|
17
|
+
|
18
|
+
s.files =
|
19
|
+
Dir['bin/**/*'] +
|
20
|
+
Dir['lib/**/*'] +
|
21
|
+
Dir['rake/**/*'] +
|
22
|
+
Dir['test/**/*'] +
|
23
|
+
Dir['*.gemspec'] +
|
24
|
+
%w[
|
25
|
+
LICENSE.txt
|
26
|
+
Rakefile
|
27
|
+
README.markdown
|
28
|
+
]
|
29
|
+
|
30
|
+
if File.directory?('bin') then
|
31
|
+
s.executables = Dir.chdir('bin') { Dir.glob('**/*').select { |f| File.executable?(f) } }
|
32
|
+
end
|
33
|
+
|
34
|
+
s.add_dependency 'spreadsheet', '>= 0.8.5'
|
35
|
+
s.add_dependency 'prawn', '>= 0.12.0'
|
36
|
+
s.add_dependency 'roo', '>= 1.11.2'
|
37
|
+
s.add_dependency 'iconv', '>= 1.0.3'
|
38
|
+
|
39
|
+
s.required_ruby_version = ">= 1.9.2"
|
40
|
+
s.rubygems_version = "1.3.1"
|
41
|
+
s.specification_version = 3
|
42
|
+
s.required_rubygems_version = Gem::Requirement.new("> 1.3.1")
|
43
|
+
end
|
data/test/data/test1.xls
ADDED
Binary file
|
Binary file
|
data/test/unit/all.rb
ADDED
File without changes
|
metadata
ADDED
@@ -0,0 +1,127 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: tabledata
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.3
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Stefan Rusterholz
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2013-07-17 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: spreadsheet
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - '>='
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: 0.8.5
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - '>='
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: 0.8.5
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: prawn
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - '>='
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: 0.12.0
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - '>='
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: 0.12.0
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: roo
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - '>='
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: 1.11.2
|
48
|
+
type: :runtime
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - '>='
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: 1.11.2
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: iconv
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - '>='
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: 1.0.3
|
62
|
+
type: :runtime
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - '>='
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: 1.0.3
|
69
|
+
description: Read and write tabular data from and to various formats.
|
70
|
+
email: stefan.rusterholz@gmail.com
|
71
|
+
executables: []
|
72
|
+
extensions: []
|
73
|
+
extra_rdoc_files: []
|
74
|
+
files:
|
75
|
+
- lib/tabledata/coercing_row.rb
|
76
|
+
- lib/tabledata/coercion.rb
|
77
|
+
- lib/tabledata/column.rb
|
78
|
+
- lib/tabledata/detection.rb
|
79
|
+
- lib/tabledata/exceptions.rb
|
80
|
+
- lib/tabledata/parser.rb
|
81
|
+
- lib/tabledata/patches/spreadsheet.rb
|
82
|
+
- lib/tabledata/presenter.rb
|
83
|
+
- lib/tabledata/presenters/csv.rb
|
84
|
+
- lib/tabledata/presenters/excel.rb
|
85
|
+
- lib/tabledata/presenters/html.rb
|
86
|
+
- lib/tabledata/presenters/pdf.rb
|
87
|
+
- lib/tabledata/row.rb
|
88
|
+
- lib/tabledata/table.rb
|
89
|
+
- lib/tabledata/version.rb
|
90
|
+
- lib/tabledata.rb
|
91
|
+
- test/data/test1.xls
|
92
|
+
- test/data/test1.xlsx
|
93
|
+
- test/data/test_csv_utf8_comma_n.csv
|
94
|
+
- test/data/test_csv_utf8_semicolon_n.csv
|
95
|
+
- test/data/test_csv_win1252_comma_rn.csv
|
96
|
+
- test/data/test_csv_win1252_semicolon_rn.csv
|
97
|
+
- test/unit/all.rb
|
98
|
+
- tabledata.gemspec
|
99
|
+
- LICENSE.txt
|
100
|
+
- Rakefile
|
101
|
+
- README.markdown
|
102
|
+
homepage: https://github.com/apeiros/tabledata
|
103
|
+
licenses:
|
104
|
+
- BSD 2-Clause
|
105
|
+
metadata: {}
|
106
|
+
post_install_message:
|
107
|
+
rdoc_options: []
|
108
|
+
require_paths:
|
109
|
+
- lib
|
110
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
111
|
+
requirements:
|
112
|
+
- - '>='
|
113
|
+
- !ruby/object:Gem::Version
|
114
|
+
version: 1.9.2
|
115
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
116
|
+
requirements:
|
117
|
+
- - '>'
|
118
|
+
- !ruby/object:Gem::Version
|
119
|
+
version: 1.3.1
|
120
|
+
requirements: []
|
121
|
+
rubyforge_project:
|
122
|
+
rubygems_version: 2.0.3
|
123
|
+
signing_key:
|
124
|
+
specification_version: 3
|
125
|
+
summary: Read and write tabular data from and to various formats.
|
126
|
+
test_files: []
|
127
|
+
has_rdoc:
|