active_tsv 0.1.0 → 0.2.0
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.
- checksums.yaml +4 -4
- data/README.md +27 -7
- data/data/benchmark.png +0 -0
- data/data/benchmark.rb +104 -0
- data/data/users.tsv +1 -1
- data/lib/active_tsv/base.rb +17 -40
- data/lib/active_tsv/ordering.rb +15 -0
- data/lib/active_tsv/querying.rb +12 -0
- data/lib/active_tsv/relation.rb +156 -16
- data/lib/active_tsv/version.rb +1 -1
- data/lib/active_tsv.rb +3 -1
- metadata +7 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 7aace3a8490ac510480e630683114815b46ce0a8
|
4
|
+
data.tar.gz: 5e3c803aa0ce4e2df7b7cadc95e4bb9d07b57270
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 5ed85bc2c763103d1f036910a1fe8c6a7effc47838b8666d0ce06c1d8b93b931cebd918b735627206714a0cdcf7294f54e24ab93953f615ab1cfa600b2399185
|
7
|
+
data.tar.gz: e6b13eff65621c3ce3f995e80c94ca00ad0c5280667f82daaa0285821d06d564a7aa180b822fed07e2107bcf42fcaa321202442981c61edadf1f59c7a38c753c
|
data/README.md
CHANGED
@@ -17,25 +17,45 @@ id name age
|
|
17
17
|
|
18
18
|
```ruby
|
19
19
|
require 'active_tsv'
|
20
|
+
|
20
21
|
class User < ActiveTsv::Base
|
21
22
|
self.table_path = "data/users.tsv"
|
22
23
|
end
|
23
24
|
|
25
|
+
User.all
|
26
|
+
=> #<ActiveTsv::Relation [#<User id: "1", name: "ksss", age: "30">, #<User id: "2", name: "foo", age: "29">, #<User id: "3", name: "bar", age: "30">]>
|
27
|
+
User.all.to_a
|
28
|
+
=> [#<User id: "1", name: "ksss", age: "30">, #<User id: "2", name: "foo", age: "29">, #<User id: "3", name: "bar", age: "30">]
|
29
|
+
|
24
30
|
User.first
|
25
|
-
#=> #<User
|
31
|
+
#=> #<User id: "1", name: "ksss", age: "30">
|
26
32
|
User.last
|
27
|
-
#=> #<User
|
33
|
+
#=> #<User id: "3", name: "bar", age: "30">
|
34
|
+
|
28
35
|
User.where(age: 30).each do |user|
|
29
36
|
user.name #=> "ksss", "bar"
|
30
37
|
end
|
38
|
+
|
31
39
|
User.where(age: 30).to_a
|
32
|
-
#=> [#<User
|
40
|
+
#=> [#<User id: "1", name: "ksss", age: "30">, #<User id: "3", name: "bar", age: "30">]
|
41
|
+
|
33
42
|
User.where(age: 30).last
|
34
|
-
#=> #<User
|
43
|
+
#=> #<User id: "3", name: "bar", age: "30">
|
44
|
+
|
35
45
|
User.where(age: 30).where(name: "ksss").first
|
36
|
-
#=> #<User
|
46
|
+
#=> #<User id: "1", name: "ksss", age: "30">
|
47
|
+
|
37
48
|
User.where.not(name: "ksss").first
|
38
|
-
#=> #<User
|
49
|
+
#=> #<User id: "2", name: "foo", age: "29">
|
50
|
+
|
51
|
+
User.group(:age).count
|
52
|
+
#=> {"30"=>2, "29"=>1}
|
53
|
+
|
54
|
+
User.order(:name).to_a
|
55
|
+
#=> [#<User id: "3", name: "bar", age: "30">, #<User id: "2", name: "foo", age: "29">, #<User id: "1", name: "ksss", age: "30">]
|
56
|
+
|
57
|
+
User.order(name: :desc).to_a
|
58
|
+
=> [#<User id: "1", name: "ksss", age: "30">, #<User id: "2", name: "foo", age: "29">, #<User id: "3", name: "bar", age: "30">]
|
39
59
|
```
|
40
60
|
|
41
61
|
Also Supported **CSV**.
|
@@ -49,7 +69,7 @@ end
|
|
49
69
|
|
50
70
|
## Goal
|
51
71
|
|
52
|
-
Support all methods
|
72
|
+
Support all methods of ActiveRecord
|
53
73
|
|
54
74
|
## Installation
|
55
75
|
|
data/data/benchmark.png
ADDED
Binary file
|
data/data/benchmark.rb
ADDED
@@ -0,0 +1,104 @@
|
|
1
|
+
#! /usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'active_tsv'
|
4
|
+
require 'tempfile'
|
5
|
+
require 'active_hash'
|
6
|
+
require 'csv'
|
7
|
+
require 'objspace'
|
8
|
+
require 'stringio'
|
9
|
+
|
10
|
+
module ActiveHashTsv
|
11
|
+
class Base < ActiveFile::Base
|
12
|
+
SEPARATER = "\t"
|
13
|
+
extend ActiveFile::HashAndArrayFiles
|
14
|
+
class << self
|
15
|
+
def load_file
|
16
|
+
raw_data
|
17
|
+
end
|
18
|
+
|
19
|
+
def extension
|
20
|
+
"tsv"
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
|
25
|
+
def load_path(path)
|
26
|
+
data = []
|
27
|
+
CSV.open(path, col_sep: self::SEPARATER) do |csv|
|
28
|
+
keys = csv.gets.map(&:to_sym)
|
29
|
+
while line = csv.gets
|
30
|
+
data << keys.zip(line).to_h
|
31
|
+
end
|
32
|
+
end
|
33
|
+
data
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def open_csv_with_temp_table(n)
|
40
|
+
headers = [*'a'..'j']
|
41
|
+
Tempfile.create(["", ".tsv"]) do |f|
|
42
|
+
f.puts headers.join("\t")
|
43
|
+
n.times do |i|
|
44
|
+
f.puts [*1..(headers.length)].map{ |j| i * j }.join("\t")
|
45
|
+
end
|
46
|
+
f.close
|
47
|
+
yield f.path
|
48
|
+
end
|
49
|
+
end
|
50
|
+
io = StringIO.new
|
51
|
+
$stdout = io
|
52
|
+
def b
|
53
|
+
GC.start
|
54
|
+
before = ObjectSpace.memsize_of_all
|
55
|
+
realtime = Benchmark.realtime {
|
56
|
+
yield
|
57
|
+
}
|
58
|
+
GC.start
|
59
|
+
mem = (ObjectSpace.memsize_of_all - before).to_f
|
60
|
+
[realtime, mem]
|
61
|
+
end
|
62
|
+
puts "title\tActiveHash\tActiveTsv\ttActiveHash\ttActiveTsv"
|
63
|
+
ns = [100, 200, 300, 400, 500]
|
64
|
+
ns.each do |n|
|
65
|
+
open_csv_with_temp_table(n) do |path|
|
66
|
+
hr, hm = b {
|
67
|
+
h = Class.new(ActiveHashTsv::Base) do
|
68
|
+
set_root_path File.dirname(path)
|
69
|
+
set_filename File.basename(path).sub(/\..*/, '')
|
70
|
+
end
|
71
|
+
h.all.each {}
|
72
|
+
}
|
73
|
+
tr, tm = b {
|
74
|
+
t = Class.new(ActiveTsv::Base) do
|
75
|
+
self.table_path = path
|
76
|
+
end
|
77
|
+
t.all.each {}
|
78
|
+
}
|
79
|
+
puts sprintf("%d\t%0.5f\t%0.5f\t%d\t%d", n, hr, tr, hm, tm)
|
80
|
+
|
81
|
+
hr, hm = b {
|
82
|
+
h = Class.new(ActiveHashTsv::Base) do
|
83
|
+
set_root_path File.dirname(path)
|
84
|
+
set_filename File.basename(path).sub(/\..*/, '')
|
85
|
+
end
|
86
|
+
h.where(a: '10').first
|
87
|
+
}
|
88
|
+
|
89
|
+
tr, tm = b {
|
90
|
+
t = Class.new(ActiveTsv::Base) do
|
91
|
+
self.table_path = path
|
92
|
+
end
|
93
|
+
t.where(a: '10').first
|
94
|
+
}
|
95
|
+
puts sprintf("%d\t%0.5f\t%0.5f\t%d\t%d", n, hr, tr, hm, tm)
|
96
|
+
end
|
97
|
+
end
|
98
|
+
$stdout = STDOUT
|
99
|
+
io.rewind
|
100
|
+
puts io.gets
|
101
|
+
lines = io.each_line.to_a
|
102
|
+
2.times do |i|
|
103
|
+
puts lines.values_at(i, i+2, i+4, i+6, i+8)
|
104
|
+
end
|
data/data/users.tsv
CHANGED
data/lib/active_tsv/base.rb
CHANGED
@@ -7,9 +7,10 @@ module ActiveTsv
|
|
7
7
|
# end
|
8
8
|
class Base
|
9
9
|
SEPARATER = "\t"
|
10
|
-
BUF_SIZE = 1024
|
11
10
|
|
12
11
|
class << self
|
12
|
+
include Querying
|
13
|
+
|
13
14
|
attr_reader :table_path
|
14
15
|
|
15
16
|
def table_path=(path)
|
@@ -33,31 +34,11 @@ module ActiveTsv
|
|
33
34
|
end
|
34
35
|
|
35
36
|
def all
|
36
|
-
Relation.new(self
|
37
|
+
Relation.new(self)
|
37
38
|
end
|
38
39
|
|
39
|
-
def
|
40
|
-
|
41
|
-
new(keys.zip(first_value).to_h)
|
42
|
-
end
|
43
|
-
|
44
|
-
def last
|
45
|
-
last_value = File.open(table_path) do |f|
|
46
|
-
f.seek(0, IO::SEEK_END)
|
47
|
-
size = f.size
|
48
|
-
buf_size = [size, self::BUF_SIZE].min
|
49
|
-
while true
|
50
|
-
f.seek(-buf_size, IO::SEEK_CUR)
|
51
|
-
buf = f.read(buf_size)
|
52
|
-
if index = buf.rindex($INPUT_RECORD_SEPARATOR, -2)
|
53
|
-
f.seek(-buf_size + index + 1, IO::SEEK_CUR)
|
54
|
-
break f.read.chomp
|
55
|
-
else
|
56
|
-
f.seek(-buf_size, IO::SEEK_CUR)
|
57
|
-
end
|
58
|
-
end
|
59
|
-
end
|
60
|
-
new(keys.zip(CSV.new(last_value, col_sep: self::SEPARATER).shift).to_h)
|
40
|
+
def scope(name, proc)
|
41
|
+
define_singleton_method(name, &proc)
|
61
42
|
end
|
62
43
|
|
63
44
|
def open(&block)
|
@@ -67,30 +48,21 @@ module ActiveTsv
|
|
67
48
|
def keys
|
68
49
|
@keys ||= open { |csv| csv.gets }.map(&:to_sym)
|
69
50
|
end
|
70
|
-
|
71
|
-
def where(condition = nil)
|
72
|
-
all.where(condition)
|
73
|
-
end
|
74
|
-
|
75
|
-
def count
|
76
|
-
all.count
|
77
|
-
end
|
78
|
-
|
79
|
-
def order(*columns)
|
80
|
-
all.order(*columns)
|
81
|
-
end
|
82
51
|
end
|
83
52
|
|
84
53
|
def initialize(attrs = {})
|
85
|
-
|
54
|
+
case attrs
|
55
|
+
when Hash
|
56
|
+
@attrs = attrs
|
57
|
+
when Array
|
58
|
+
@attrs = self.class.keys.zip(attrs).to_h
|
59
|
+
else
|
86
60
|
raise ArgumentError, "#{attrs.class} is not supported value"
|
87
61
|
end
|
88
|
-
|
89
|
-
@attrs = attrs
|
90
62
|
end
|
91
63
|
|
92
64
|
def inspect
|
93
|
-
"#<#{self.class} #{
|
65
|
+
"#<#{self.class} #{@attrs.map { |k, v| "#{k}: #{v.inspect}" }.join(', ')}>"
|
94
66
|
end
|
95
67
|
|
96
68
|
def [](key)
|
@@ -104,5 +76,10 @@ module ActiveTsv
|
|
104
76
|
def to_h
|
105
77
|
@attrs.dup
|
106
78
|
end
|
79
|
+
|
80
|
+
def ==(other)
|
81
|
+
super || other.instance_of?(self.class) && to_h == other.to_h
|
82
|
+
end
|
83
|
+
alias eql? ==
|
107
84
|
end
|
108
85
|
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
module ActiveTsv
|
2
|
+
module Querying
|
3
|
+
METHODS = %i(first last take where count order group pluck)
|
4
|
+
METHODS.each do |m|
|
5
|
+
module_eval <<-DEFINE_METHOD, __FILE__, __LINE__
|
6
|
+
def #{m}(*args, &block)
|
7
|
+
all.#{m}(*args, &block)
|
8
|
+
end
|
9
|
+
DEFINE_METHOD
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
data/lib/active_tsv/relation.rb
CHANGED
@@ -4,12 +4,31 @@ module ActiveTsv
|
|
4
4
|
class Relation
|
5
5
|
include Enumerable
|
6
6
|
|
7
|
+
BUF_SIZE = 1024
|
8
|
+
VALID_DIRECTIONS = [:asc, :desc, :ASC, :DESC, "asc", "desc", "ASC", "DESC"]
|
9
|
+
|
7
10
|
attr_reader :model
|
8
|
-
|
9
|
-
|
11
|
+
attr_accessor :where_values
|
12
|
+
attr_accessor :order_values
|
13
|
+
attr_accessor :group_values
|
14
|
+
|
15
|
+
def initialize(model)
|
10
16
|
@model = model
|
11
|
-
@where_values =
|
12
|
-
@order_values =
|
17
|
+
@where_values = []
|
18
|
+
@order_values = []
|
19
|
+
@group_values = []
|
20
|
+
end
|
21
|
+
|
22
|
+
def initialize_copy(copy)
|
23
|
+
copy.where_values = where_values.dup
|
24
|
+
copy.order_values = order_values.dup
|
25
|
+
copy.group_values = group_values.dup
|
26
|
+
end
|
27
|
+
|
28
|
+
def ==(other)
|
29
|
+
where_values == other.where_values &&
|
30
|
+
order_values == other.order_values &&
|
31
|
+
group_values == other.group_values
|
13
32
|
end
|
14
33
|
|
15
34
|
def where(where_value = nil)
|
@@ -22,16 +41,89 @@ module ActiveTsv
|
|
22
41
|
end
|
23
42
|
end
|
24
43
|
|
44
|
+
def pluck(*fields)
|
45
|
+
key_to_value_index = @model.keys.each_with_index.to_h
|
46
|
+
if fields.empty?
|
47
|
+
to_value_a
|
48
|
+
elsif fields.one?
|
49
|
+
field = fields.first
|
50
|
+
to_value_a.map { |v| v[key_to_value_index[field]] }
|
51
|
+
else
|
52
|
+
to_value_a.map { |v| fields.map { |field| v[key_to_value_index[field]] } }
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
25
56
|
def exists?
|
26
57
|
!first.nil?
|
27
58
|
end
|
28
59
|
|
60
|
+
def first
|
61
|
+
if @order_values.empty?
|
62
|
+
each_model.first
|
63
|
+
else
|
64
|
+
to_a.first
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
29
68
|
def last
|
30
|
-
|
69
|
+
if @where_values.empty? && @order_values.empty?
|
70
|
+
last_value = File.open(@model.table_path) do |f|
|
71
|
+
f.seek(0, IO::SEEK_END)
|
72
|
+
buf_size = [f.size, self.class::BUF_SIZE].min
|
73
|
+
while true
|
74
|
+
f.seek(-buf_size, IO::SEEK_CUR)
|
75
|
+
buf = f.read(buf_size)
|
76
|
+
if index = buf.rindex($INPUT_RECORD_SEPARATOR, -2)
|
77
|
+
f.seek(-buf_size + index + 1, IO::SEEK_CUR)
|
78
|
+
break f.read.chomp
|
79
|
+
else
|
80
|
+
f.seek(-buf_size, IO::SEEK_CUR)
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
@model.new(CSV.new(last_value, col_sep: @model::SEPARATER).shift)
|
85
|
+
else
|
86
|
+
to_a.last
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
def take(n = nil)
|
91
|
+
if n
|
92
|
+
if @order_values.empty?
|
93
|
+
each_model.take(n)
|
94
|
+
else
|
95
|
+
to_a.take(n)
|
96
|
+
end
|
97
|
+
else
|
98
|
+
first
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
def count
|
103
|
+
if @group_values.empty?
|
104
|
+
super
|
105
|
+
else
|
106
|
+
h = if @group_values.one?
|
107
|
+
group_by { |i| i[@group_values.first] }
|
108
|
+
else
|
109
|
+
group_by { |i| @group_values.map { |c| i[c] } }
|
110
|
+
end
|
111
|
+
h.each do |k, v|
|
112
|
+
h[k] = v.count
|
113
|
+
end
|
114
|
+
h
|
115
|
+
end
|
31
116
|
end
|
32
117
|
|
33
118
|
def order(*columns)
|
34
|
-
@order_values += columns
|
119
|
+
@order_values += order_conditions(columns)
|
120
|
+
@order_values.uniq!
|
121
|
+
self
|
122
|
+
end
|
123
|
+
|
124
|
+
def group(*columns)
|
125
|
+
@group_values += columns
|
126
|
+
@group_values.uniq!
|
35
127
|
self
|
36
128
|
end
|
37
129
|
|
@@ -40,26 +132,74 @@ module ActiveTsv
|
|
40
132
|
end
|
41
133
|
|
42
134
|
def to_a
|
43
|
-
|
44
|
-
|
45
|
-
|
135
|
+
to_value_a.map { |v| @model.new(v) }
|
136
|
+
end
|
137
|
+
|
138
|
+
def inspect
|
139
|
+
a = to_a.take(11).map(&:inspect)
|
140
|
+
a[10] = '...' if a.length == 11
|
141
|
+
|
142
|
+
"#<#{self.class.name} [#{a.join(', ')}]>"
|
143
|
+
end
|
144
|
+
|
145
|
+
private
|
146
|
+
|
147
|
+
def to_value_a
|
148
|
+
ret = each_value.to_a
|
149
|
+
key_to_value_index = @model.keys.each_with_index.to_h
|
150
|
+
if @order_values.empty?.!
|
151
|
+
ret.sort! do |a, b|
|
152
|
+
@order_values.each.with_index(1) do |order_condition, index|
|
153
|
+
comp = a[key_to_value_index[order_condition.column]] <=> b[key_to_value_index[order_condition.column]]
|
154
|
+
break 0 if comp == 0 && index == @order_values.length
|
155
|
+
break comp * order_condition.to_i if comp != 0
|
156
|
+
end
|
157
|
+
end
|
158
|
+
end
|
159
|
+
ret
|
160
|
+
end
|
161
|
+
|
162
|
+
def each_value
|
163
|
+
return to_enum(__method__) unless block_given?
|
164
|
+
|
165
|
+
key_to_value_index = @model.keys.each_with_index.to_h
|
46
166
|
@model.open do |csv|
|
47
167
|
csv.gets
|
48
168
|
csv.each do |value|
|
49
|
-
|
169
|
+
yield value if @where_values.all? { |cond|
|
50
170
|
cond.values.all? do |k, v|
|
51
171
|
value[key_to_value_index[k]].__send__(cond.method_name, v.to_s)
|
52
172
|
end
|
53
173
|
}
|
54
174
|
end
|
55
175
|
end
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
176
|
+
end
|
177
|
+
|
178
|
+
def each_model
|
179
|
+
return to_enum(__method__) unless block_given?
|
180
|
+
|
181
|
+
each_value { |v| yield @model.new(v) }
|
182
|
+
end
|
183
|
+
|
184
|
+
def order_conditions(columns)
|
185
|
+
columns.map { |column|
|
186
|
+
case column
|
187
|
+
when Symbol
|
188
|
+
Ascending.new(column)
|
189
|
+
when Hash
|
190
|
+
column.map do |col, direction|
|
191
|
+
unless VALID_DIRECTIONS.include?(direction)
|
192
|
+
raise ArgumentError, %(Direction "#{direction}" is invalid. Valid directions are: #{VALID_DIRECTIONS})
|
193
|
+
end
|
194
|
+
case direction.downcase.to_sym
|
195
|
+
when :asc
|
196
|
+
Ascending.new(col)
|
197
|
+
when :desc
|
198
|
+
Descending.new(col)
|
199
|
+
end
|
200
|
+
end
|
61
201
|
end
|
62
|
-
|
202
|
+
}.flatten
|
63
203
|
end
|
64
204
|
end
|
65
205
|
end
|
data/lib/active_tsv/version.rb
CHANGED
data/lib/active_tsv.rb
CHANGED
@@ -2,8 +2,10 @@
|
|
2
2
|
|
3
3
|
require 'csv'
|
4
4
|
|
5
|
-
require "active_tsv/
|
5
|
+
require "active_tsv/querying"
|
6
6
|
require "active_tsv/relation"
|
7
7
|
require "active_tsv/where_chain"
|
8
8
|
require "active_tsv/condition"
|
9
|
+
require "active_tsv/ordering"
|
10
|
+
require "active_tsv/base"
|
9
11
|
require "active_tsv/version"
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: active_tsv
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.2.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- ksss
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2016-05-
|
11
|
+
date: 2016-05-17 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|
@@ -68,6 +68,8 @@ files:
|
|
68
68
|
- active_tsv.gemspec
|
69
69
|
- bin/console
|
70
70
|
- bin/setup
|
71
|
+
- data/benchmark.png
|
72
|
+
- data/benchmark.rb
|
71
73
|
- data/names.tsv
|
72
74
|
- data/users.csv
|
73
75
|
- data/users.tsv
|
@@ -75,6 +77,8 @@ files:
|
|
75
77
|
- lib/active_tsv.rb
|
76
78
|
- lib/active_tsv/base.rb
|
77
79
|
- lib/active_tsv/condition.rb
|
80
|
+
- lib/active_tsv/ordering.rb
|
81
|
+
- lib/active_tsv/querying.rb
|
78
82
|
- lib/active_tsv/relation.rb
|
79
83
|
- lib/active_tsv/version.rb
|
80
84
|
- lib/active_tsv/where_chain.rb
|
@@ -98,7 +102,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
98
102
|
version: '0'
|
99
103
|
requirements: []
|
100
104
|
rubyforge_project:
|
101
|
-
rubygems_version: 2.6.
|
105
|
+
rubygems_version: 2.6.4
|
102
106
|
signing_key:
|
103
107
|
specification_version: 4
|
104
108
|
summary: A Class of Active record pattern for TSV/CSV
|