/
builder.cr
177 lines (158 loc) · 3.42 KB
/
builder.cr
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
require "csv"
# A CSV Builder writes CSV to an `IO`.
#
# ```
# require "csv"
#
# result = CSV.build do |csv|
# # A row can be written by specifying several values
# csv.row "Hello", 1, 'a', "String with \"quotes\"", '"', :sym
#
# # Or an enumerable
# csv.row [1, 2, 3]
#
# # Or using a block, and appending to the row
# csv.row do |row|
# # Appending a single value
# row << 4
#
# # Or multiple values
# row.concat 5, 6
#
# # Or an enumerable
# row.concat [7, 8]
# end
# end
# puts result
# ```
#
# Output:
#
# ```text
# Hello,1,a,"String with ""quotes""","""",sym
# 1,2,3
# 4,5,6,7,8
# ```
class CSV::Builder
enum Quoting
# No quotes
NONE
# Quotes according to RFC 4180 (default)
RFC
# Always quote
ALL
end
# Creates a builder that will write to the given `IO`.
def initialize(@io : IO, @separator : Char = DEFAULT_SEPARATOR, @quote_char : Char = DEFAULT_QUOTE_CHAR, @quoting : Quoting = Quoting::RFC)
@first_cell_in_row = true
end
# Yields a `CSV::Row` to append a row. A newline is appended
# to `IO` after the block exits.
def row
yield Row.new(self, @separator, @quote_char, @quoting)
@io << '\n'
@first_cell_in_row = true
end
# Appends the given values as a single row, and then a newline.
def row(values : Enumerable) : Nil
row do |row|
values.each do |value|
row << value
end
end
end
# :ditto:
def row(*values) : Nil
row values
end
# :nodoc:
def cell
append_cell do
yield @io
end
end
# :nodoc:
def quote_cell(value : String)
append_cell do
@io << @quote_char
value.each_char do |char|
case char
when @quote_char
@io << @quote_char << @quote_char
else
@io << char
end
end
@io << @quote_char
end
end
private def append_cell
@io << @separator unless @first_cell_in_row
yield
@first_cell_in_row = false
end
# A CSV Row being built.
struct Row
@builder : Builder
# :nodoc:
def initialize(@builder, @separator : Char = DEFAULT_SEPARATOR, @quote_char : Char = DEFAULT_QUOTE_CHAR, @quoting : Quoting = Quoting::RFC)
end
# Appends the given value to this row.
def <<(value : String) : Nil
if needs_quotes?(value)
@builder.quote_cell value
else
@builder.cell { |io| io << value }
end
end
# :ditto:
def <<(value : Nil | Bool | Number) : Nil
case @quoting
when .all?
@builder.cell { |io|
io << @quote_char
io << value
io << @quote_char
}
else
@builder.cell { |io| io << value }
end
end
# :ditto:
def <<(value) : Nil
self << value.to_s
end
# Appends the given values to this row.
def concat(values : Enumerable) : Nil
values.each do |value|
self << value
end
end
# :ditto:
def concat(*values) : Nil
concat values
end
# Appends a comma, thus skipping a cell.
def skip_cell : Nil
self << nil
end
private def needs_quotes?(value : String)
case @quoting
when .rfc?
value.each_byte do |byte|
case byte.unsafe_chr
when @separator, @quote_char, '\n'
return true
else
# keep scanning
end
end
false
when .all?
true
else
false
end
end
end
end