Skip to content

Commit 0217552

Browse files
authored
Expose user-facing versions of CSV.writerow (#1003)
Implements #1001. We already had the internal methods here, so this PR just adds some higher-level user-facing methods that take a plain "row" (from the Tables.jl "Row" interface) or an `io` and row. This is convenient when you don't have a traditional iterator (and can use RowWriter), but just want to repeatedly call `CSV.writerow` yourself and get the delimited output.
1 parent 3ebd2c9 commit 0217552

File tree

2 files changed

+75
-45
lines changed

2 files changed

+75
-45
lines changed

src/write.jl

Lines changed: 65 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,28 @@ tup(x::AbstractString) = Tuple(codeunits(x))
6060
tlen(::UInt8) = 1
6161
tlen(::NTuple{N, UInt8}) where {N} = N
6262

63+
function Options(;
64+
delim::Union{Char, String}=',',
65+
quotechar::Char='"',
66+
openquotechar::Union{Char, Nothing}=nothing,
67+
closequotechar::Union{Char, Nothing}=nothing,
68+
escapechar::Char='"',
69+
newline::Union{Char, String}='\n',
70+
decimal::Char='.',
71+
dateformat=nothing,
72+
quotestrings::Bool=false,
73+
missingstring::AbstractString="",
74+
transform::Function=_identity,
75+
bom::Bool=false,
76+
kw...
77+
)
78+
checkvaliddelim(delim)
79+
(isascii(something(openquotechar, quotechar)) && isascii(something(closequotechar, quotechar)) && isascii(escapechar)) || throw(ArgumentError("quote and escape characters must be ASCII characters "))
80+
oq, cq = openquotechar !== nothing ? (openquotechar % UInt8, closequotechar % UInt8) : (quotechar % UInt8, quotechar % UInt8)
81+
e = escapechar % UInt8
82+
return Options(tup(delim), oq, cq, e, tup(newline), decimal % UInt8, dateformat, quotestrings, tup(missingstring), transform, bom)
83+
end
84+
6385
"""
6486
CSV.RowWriter(table; kwargs...)
6587
@@ -97,26 +119,11 @@ Base.eltype(r::RowWriter) = String
97119
_identity(col, val) = val
98120

99121
function RowWriter(table;
100-
delim::Union{Char, String}=',',
101-
quotechar::Char='"',
102-
openquotechar::Union{Char, Nothing}=nothing,
103-
closequotechar::Union{Char, Nothing}=nothing,
104-
escapechar::Char='"',
105-
newline::Union{Char, String}='\n',
106-
decimal::Char='.',
107-
dateformat=nothing,
108-
quotestrings::Bool=false,
109-
missingstring::AbstractString="",
110-
transform::Function=_identity,
111-
bom::Bool=false,
112122
header::Vector=String[],
113123
writeheader::Bool=true,
114-
bufsize::Int=2^22)
115-
checkvaliddelim(delim)
116-
(isascii(something(openquotechar, quotechar)) && isascii(something(closequotechar, quotechar)) && isascii(escapechar)) || throw(ArgumentError("quote and escape characters must be ASCII characters "))
117-
oq, cq = openquotechar !== nothing ? (openquotechar % UInt8, closequotechar % UInt8) : (quotechar % UInt8, quotechar % UInt8)
118-
e = escapechar % UInt8
119-
opts = Options(tup(delim), oq, cq, e, tup(newline), decimal % UInt8, dateformat, quotestrings, tup(missingstring), transform, bom)
124+
bufsize::Int=2^22,
125+
kw...)
126+
opts = Options(; kw...)
120127
source = Tables.rows(table)
121128
sch = Tables.schema(source)
122129
return RowWriter(source, sch, opts, Vector{UInt8}(undef, bufsize), header, writeheader)
@@ -151,34 +158,18 @@ end
151158

152159
write(file; kwargs...) = x->write(file, x; kwargs...)
153160
function write(file, itr;
154-
delim::Union{Char, String}=',',
155-
quotechar::Char='"',
156-
openquotechar::Union{Char, Nothing}=nothing,
157-
closequotechar::Union{Char, Nothing}=nothing,
158-
escapechar::Char='"',
159-
newline::Union{Char, String}='\n',
160-
decimal::Char='.',
161-
dateformat=nothing,
162-
quotestrings::Bool=false,
163-
missingstring::AbstractString="",
164-
transform::Function=_identity,
165-
bom::Bool=false,
166161
append::Bool=false,
167162
compress::Bool=false,
168163
writeheader=nothing,
169164
partition::Bool=false,
170-
kwargs...)
171-
checkvaliddelim(delim)
165+
kw...)
172166
if writeheader !== nothing
173167
Base.depwarn("`writeheader=$writeheader` is deprecated in favor of `header=$writeheader`", :write)
174168
header = writeheader
175169
else
176170
header = !append
177171
end
178-
(isascii(something(openquotechar, quotechar)) && isascii(something(closequotechar, quotechar)) && isascii(escapechar)) || throw(ArgumentError("quote and escape characters must be ASCII characters "))
179-
oq, cq = openquotechar !== nothing ? (openquotechar % UInt8, closequotechar % UInt8) : (quotechar % UInt8, quotechar % UInt8)
180-
e = escapechar % UInt8
181-
opts = Options(tup(delim), oq, cq, e, tup(newline), decimal % UInt8, dateformat, quotestrings, tup(missingstring), transform, bom)
172+
opts = Options(; kw...)
182173
if partition
183174
if file isa IO
184175
throw(ArgumentError("must pass single file name as a String, or iterable of filenames or IO arguments"))
@@ -190,32 +181,29 @@ function write(file, itr;
190181
if file isa String
191182
push!(outfiles, string(file, "_$i"))
192183
end
193-
write(outfiles[i], part; delim=delim, quotechar=quotechar, openquotechar=openquotechar, closequotechar=closequotechar, escapechar=escapechar, newline=newline,
194-
decimal=decimal, dateformat=dateformat, quotestrings=quotestrings, missingstring=missingstring, transform=transform, bom=bom, append=append, compress=compress,
195-
writeheader=writeheader, partition=false, kwargs...)
184+
write(outfiles[i], part; append=append, compress=compress, writeheader=writeheader, partition=false, kw...)
196185
end
197186
else
198187
if file isa String
199188
push!(outfiles, string(file, "_$i"))
200189
end
201-
write(outfiles[i], part; delim=delim, quotechar=quotechar, openquotechar=openquotechar, closequotechar=closequotechar, escapechar=escapechar, newline=newline,
202-
decimal=decimal, dateformat=dateformat, quotestrings=quotestrings, missingstring=missingstring, transform=transform, bom=bom, append=append, compress=compress,
203-
writeheader=writeheader, partition=false, kwargs...)
190+
write(outfiles[i], part; append=append, compress=compress, writeheader=writeheader, partition=false, kw...)
204191
end
205192
end
206193
return outfiles
207194
else
208195
rows = Tables.rows(itr)
209196
sch = Tables.schema(rows)
210-
return write(sch, rows, file, opts; append=append, compress=compress, header=header, kwargs...)
197+
return write(sch, rows, file, opts; append=append, compress=compress, header=header, kw...)
211198
end
212199
end
213200

214201
function write(sch::Tables.Schema, rows, file, opts;
215202
append::Bool=false,
216203
compress::Bool=false,
217204
header::Union{Bool, Vector}=String[],
218-
bufsize::Int=2^22
205+
bufsize::Int=2^22,
206+
kw...
219207
)
220208
colnames = !(header isa Vector) || isempty(header) ? sch.names : header
221209
cols = length(colnames)
@@ -244,7 +232,8 @@ function write(::Nothing, rows, file, opts;
244232
append::Bool=false,
245233
compress::Bool=false,
246234
header::Union{Bool, Vector}=String[],
247-
bufsize::Int=2^22
235+
bufsize::Int=2^22,
236+
kw...
248237
)
249238
len = bufsize
250239
buf = Vector{UInt8}(undef, len)
@@ -374,6 +363,37 @@ function writerow(buf, pos, len, io, sch, row, cols, opts)
374363
return
375364
end
376365

366+
function writerow(io::IO, row; opts::Union{Options, Nothing}=nothing, bufsize::Int=2^22, buf=Vector{UInt8}(undef, bufsize), len=bufsize, kw...)
367+
if opts === nothing
368+
opts = Options(; kw...)
369+
end
370+
nms = Tables.columnnames(row)
371+
pos = 1
372+
for i = 1:length(nms)
373+
val = opts.transform(i, Tables.getcolumn(row, i))
374+
val === nothing && nothingerror(col)
375+
pos = writecell(buf, pos, len, io, val, opts)
376+
pos = writedelimnewline(buf, pos, len, io, ifelse(i == length(nms), opts.newline, opts.delim))
377+
end
378+
Base.write(io, resize!(buf, pos - 1))
379+
end
380+
381+
function writerow(row; opts::Union{Options, Nothing}=nothing, bufsize::Int=2^22, buf=Vector{UInt8}(undef, bufsize), len=bufsize, kw...)
382+
if opts === nothing
383+
opts = Options(; kw...)
384+
end
385+
io = DummyIO()
386+
nms = Tables.columnnames(row)
387+
pos = 1
388+
for i = 1:length(nms)
389+
val = opts.transform(i, Tables.getcolumn(row, i))
390+
val === nothing && nothingerror(col)
391+
pos = writecell(buf, pos, len, io, val, opts)
392+
pos = writedelimnewline(buf, pos, len, io, ifelse(i == length(nms), opts.newline, opts.delim))
393+
end
394+
return unsafe_string(pointer(buf), pos - 1)
395+
end
396+
377397
function writecell(buf, pos, len, io, ::Missing, opts)
378398
str = opts.missingstring
379399
t = tlen(str)

test/write.jl

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -355,4 +355,14 @@ end
355355
ct = CSV.read(io, Tables.columntable)
356356
@test ct == default_table
357357

358+
# CSV.writerow
359+
row = (a=1, b=2.3, c="hey", d=Date(2022, 5, 4))
360+
str = CSV.writerow(row)
361+
@test str == "1,2.3,hey,2022-05-04\n"
362+
io = IOBuffer()
363+
CSV.writerow(io, row)
364+
@test String(take!(io)) == "1,2.3,hey,2022-05-04\n"
365+
str = CSV.writerow(row; delim='\t')
366+
@test str == "1\t2.3\they\t2022-05-04\n"
367+
358368
end # @testset "CSV.write"

0 commit comments

Comments
 (0)