zlight_csv 0.5.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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +128 -0
- data/LICENSE +21 -0
- data/README.md +225 -0
- data/VERSION +1 -0
- data/ext/zlight_csv/Cargo.toml +30 -0
- data/ext/zlight_csv/extconf.rb +16 -0
- data/ext/zlight_csv/src/converter.rs +133 -0
- data/ext/zlight_csv/src/error.rs +122 -0
- data/ext/zlight_csv/src/lib.rs +55 -0
- data/ext/zlight_csv/src/options.rs +95 -0
- data/ext/zlight_csv/src/parser.rs +77 -0
- data/ext/zlight_csv/src/stream.rs +410 -0
- data/ext/zlight_csv/src/writer.rs +268 -0
- data/lib/zlight_csv/version.rb +6 -0
- data/lib/zlight_csv.rb +305 -0
- metadata +86 -0
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
//! CSV writer: converts Ruby data structures into CSV strings.
|
|
2
|
+
//!
|
|
3
|
+
//! Mirrors the reader's option/error patterns. The heavy lifting is done by
|
|
4
|
+
//! `csv::Writer`, with field values converted from Ruby objects following the
|
|
5
|
+
//! same semantics as Ruby's stdlib `CSV` (i.e. `nil` becomes an empty field and
|
|
6
|
+
//! every other value is stringified with `#to_s`).
|
|
7
|
+
|
|
8
|
+
use csv::{QuoteStyle, WriterBuilder};
|
|
9
|
+
use magnus::{
|
|
10
|
+
prelude::*,
|
|
11
|
+
scan_args::{get_kwargs, scan_args},
|
|
12
|
+
Error as MagnusError, RArray, RHash, RString, Ruby, Value,
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
use crate::error::ZlightError;
|
|
16
|
+
use crate::options::single_byte;
|
|
17
|
+
|
|
18
|
+
/// A CSV writer backed by an in-memory byte buffer.
|
|
19
|
+
type ByteWriter = csv::Writer<Vec<u8>>;
|
|
20
|
+
|
|
21
|
+
/// Configuration options for CSV writing.
|
|
22
|
+
#[derive(Debug, Clone, Copy)]
|
|
23
|
+
pub struct WriteOptions {
|
|
24
|
+
/// Whether to write a header row (only applicable for hash input).
|
|
25
|
+
pub write_headers: bool,
|
|
26
|
+
/// Field delimiter byte (defaults to comma).
|
|
27
|
+
pub delimiter: u8,
|
|
28
|
+
/// Quote byte (defaults to double quote).
|
|
29
|
+
pub quote: u8,
|
|
30
|
+
/// Whether to force quoting of every field.
|
|
31
|
+
pub force_quotes: bool,
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
impl Default for WriteOptions {
|
|
35
|
+
#[inline]
|
|
36
|
+
fn default() -> Self {
|
|
37
|
+
Self {
|
|
38
|
+
write_headers: true,
|
|
39
|
+
delimiter: b',',
|
|
40
|
+
quote: b'"',
|
|
41
|
+
force_quotes: false,
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
impl WriteOptions {
|
|
47
|
+
/// Extracts the `rows` argument and write options from Ruby arguments.
|
|
48
|
+
fn from_ruby_args(args: &[Value]) -> Result<(RArray, Self), ZlightError> {
|
|
49
|
+
let parsed = scan_args::<(RArray,), (), (), (), RHash, ()>(args)
|
|
50
|
+
.map_err(|_| ZlightError::MissingArgument("rows (expected an Array)"))?;
|
|
51
|
+
|
|
52
|
+
let (rows,) = parsed.required;
|
|
53
|
+
|
|
54
|
+
let kw = get_kwargs::<
|
|
55
|
+
_,
|
|
56
|
+
(),
|
|
57
|
+
(Option<bool>, Option<RString>, Option<RString>, Option<bool>),
|
|
58
|
+
(),
|
|
59
|
+
>(
|
|
60
|
+
parsed.keywords,
|
|
61
|
+
&[],
|
|
62
|
+
&["headers", "col_sep", "quote_char", "force_quotes"],
|
|
63
|
+
)
|
|
64
|
+
.map_err(|e| ZlightError::InvalidOption {
|
|
65
|
+
key: "keywords",
|
|
66
|
+
expected: "valid keyword arguments",
|
|
67
|
+
actual: e.to_string(),
|
|
68
|
+
})?;
|
|
69
|
+
|
|
70
|
+
let (headers_opt, col_sep_opt, quote_opt, force_quotes_opt) = kw.optional;
|
|
71
|
+
|
|
72
|
+
let delimiter = single_byte(col_sep_opt, "col_sep")?.unwrap_or(b',');
|
|
73
|
+
let quote = single_byte(quote_opt, "quote_char")?.unwrap_or(b'"');
|
|
74
|
+
|
|
75
|
+
Ok((
|
|
76
|
+
rows,
|
|
77
|
+
WriteOptions {
|
|
78
|
+
write_headers: headers_opt.unwrap_or(true),
|
|
79
|
+
delimiter,
|
|
80
|
+
quote,
|
|
81
|
+
force_quotes: force_quotes_opt.unwrap_or(false),
|
|
82
|
+
},
|
|
83
|
+
))
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/// Builds a CSV writer with the given options.
|
|
88
|
+
#[inline]
|
|
89
|
+
fn build_writer(options: &WriteOptions) -> ByteWriter {
|
|
90
|
+
let mut builder = WriterBuilder::new();
|
|
91
|
+
builder
|
|
92
|
+
.delimiter(options.delimiter)
|
|
93
|
+
.quote(options.quote)
|
|
94
|
+
// Rows may legitimately have differing field counts (e.g. hashes with
|
|
95
|
+
// missing keys, or jagged arrays), so never enforce a uniform width.
|
|
96
|
+
.flexible(true);
|
|
97
|
+
|
|
98
|
+
if options.force_quotes {
|
|
99
|
+
builder.quote_style(QuoteStyle::Always);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
builder.from_writer(Vec::new())
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/// Converts a csv error into a Ruby exception.
|
|
106
|
+
#[inline]
|
|
107
|
+
fn csv_err(e: csv::Error) -> MagnusError {
|
|
108
|
+
ZlightError::from(e).into()
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/// Returns a UTF-8 encoded Ruby string for an arbitrary field value.
|
|
112
|
+
///
|
|
113
|
+
/// Semantics mirror Ruby's stdlib CSV writer:
|
|
114
|
+
/// * `nil` becomes an empty field.
|
|
115
|
+
/// * Strings are used as-is (transcoded to UTF-8 when needed).
|
|
116
|
+
/// * Everything else is stringified via `#to_s`.
|
|
117
|
+
#[inline]
|
|
118
|
+
fn field_string(ruby: &Ruby, value: Value) -> Result<RString, MagnusError> {
|
|
119
|
+
if value.is_nil() {
|
|
120
|
+
return Ok(ruby.str_new(""));
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
let string = match RString::from_value(value) {
|
|
124
|
+
Some(s) => s,
|
|
125
|
+
None => value.funcall("to_s", ())?,
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
if string.is_utf8_compatible_encoding() {
|
|
129
|
+
Ok(string)
|
|
130
|
+
} else {
|
|
131
|
+
// Transcode foreign encodings (e.g. ISO-8859-1, Shift_JIS) so the
|
|
132
|
+
// resulting buffer is always valid UTF-8.
|
|
133
|
+
string.conv_enc(ruby.utf8_encoding())
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/// Writes a single field value to the writer.
|
|
138
|
+
#[inline]
|
|
139
|
+
fn write_field(ruby: &Ruby, writer: &mut ByteWriter, value: Value) -> Result<(), MagnusError> {
|
|
140
|
+
let string = field_string(ruby, value)?;
|
|
141
|
+
// SAFETY: `string` is held on the stack and no Ruby allocation happens between
|
|
142
|
+
// taking the slice and handing it to the writer, so it cannot be collected.
|
|
143
|
+
let bytes = unsafe { string.as_slice() };
|
|
144
|
+
writer.write_field(bytes).map_err(csv_err)
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/// Terminates the current record (row).
|
|
148
|
+
#[inline]
|
|
149
|
+
fn end_record(writer: &mut ByteWriter) -> Result<(), MagnusError> {
|
|
150
|
+
writer.write_record(None::<&[u8]>).map_err(csv_err)
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/// Finalizes the writer and returns the buffer as a UTF-8 Ruby string.
|
|
154
|
+
#[inline]
|
|
155
|
+
fn finish(ruby: &Ruby, writer: ByteWriter) -> Result<RString, MagnusError> {
|
|
156
|
+
let data = writer
|
|
157
|
+
.into_inner()
|
|
158
|
+
.map_err(|e| MagnusError::from(ZlightError::Io(e.into_error())))?;
|
|
159
|
+
Ok(ruby.enc_str_new(&data, ruby.utf8_encoding()))
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/// Coerces a row value into an `RArray`, raising a clear error otherwise.
|
|
163
|
+
#[inline]
|
|
164
|
+
fn expect_array(value: Value) -> Result<RArray, MagnusError> {
|
|
165
|
+
RArray::from_value(value).ok_or_else(|| {
|
|
166
|
+
MagnusError::from(ZlightError::InvalidOption {
|
|
167
|
+
key: "rows",
|
|
168
|
+
expected: "an Array of Arrays",
|
|
169
|
+
actual: format!("element of type {}", value.class()),
|
|
170
|
+
})
|
|
171
|
+
})
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/// Coerces a row value into an `RHash`, raising a clear error otherwise.
|
|
175
|
+
#[inline]
|
|
176
|
+
fn expect_hash(value: Value) -> Result<RHash, MagnusError> {
|
|
177
|
+
RHash::from_value(value).ok_or_else(|| {
|
|
178
|
+
MagnusError::from(ZlightError::InvalidOption {
|
|
179
|
+
key: "rows",
|
|
180
|
+
expected: "an Array of Hashes",
|
|
181
|
+
actual: format!("element of type {}", value.class()),
|
|
182
|
+
})
|
|
183
|
+
})
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/// Generates CSV from an array of arrays (positional fields, no header row).
|
|
187
|
+
fn generate_from_arrays(
|
|
188
|
+
ruby: &Ruby,
|
|
189
|
+
rows: RArray,
|
|
190
|
+
options: &WriteOptions,
|
|
191
|
+
) -> Result<RString, MagnusError> {
|
|
192
|
+
let mut writer = build_writer(options);
|
|
193
|
+
|
|
194
|
+
for i in 0..rows.len() {
|
|
195
|
+
let row = expect_array(rows.entry(i as isize)?)?;
|
|
196
|
+
for j in 0..row.len() {
|
|
197
|
+
write_field(ruby, &mut writer, row.entry(j as isize)?)?;
|
|
198
|
+
}
|
|
199
|
+
end_record(&mut writer)?;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
finish(ruby, writer)
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/// Generates CSV from an array of hashes, using the first hash's keys as the
|
|
206
|
+
/// column order. Missing keys in later rows are written as empty fields.
|
|
207
|
+
fn generate_from_hashes(
|
|
208
|
+
ruby: &Ruby,
|
|
209
|
+
rows: RArray,
|
|
210
|
+
options: &WriteOptions,
|
|
211
|
+
) -> Result<RString, MagnusError> {
|
|
212
|
+
let mut writer = build_writer(options);
|
|
213
|
+
|
|
214
|
+
// Column order is defined by the keys of the first hash.
|
|
215
|
+
let first_hash = expect_hash(rows.entry(0)?)?;
|
|
216
|
+
let keys: RArray = first_hash.funcall("keys", ())?;
|
|
217
|
+
let key_count = keys.len();
|
|
218
|
+
let header_keys: Vec<Value> = (0..key_count)
|
|
219
|
+
.map(|i| keys.entry(i as isize))
|
|
220
|
+
.collect::<Result<_, _>>()?;
|
|
221
|
+
|
|
222
|
+
// Emit the header row from the keys (skipped for keyless/empty hashes).
|
|
223
|
+
if options.write_headers && !header_keys.is_empty() {
|
|
224
|
+
for &key in &header_keys {
|
|
225
|
+
write_field(ruby, &mut writer, key)?;
|
|
226
|
+
}
|
|
227
|
+
end_record(&mut writer)?;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
for i in 0..rows.len() {
|
|
231
|
+
let hash = expect_hash(rows.entry(i as isize)?)?;
|
|
232
|
+
for &key in &header_keys {
|
|
233
|
+
let value = hash.get(key).unwrap_or_else(|| ruby.qnil().as_value());
|
|
234
|
+
write_field(ruby, &mut writer, value)?;
|
|
235
|
+
}
|
|
236
|
+
end_record(&mut writer)?;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
finish(ruby, writer)
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/// Ruby: `ZLight.generate(rows, **options) -> String`.
|
|
243
|
+
///
|
|
244
|
+
/// Dispatches on the type of the first row: an array of hashes produces a CSV
|
|
245
|
+
/// with a header row (unless `headers: false`); an array of arrays produces a
|
|
246
|
+
/// headerless CSV.
|
|
247
|
+
pub fn generate(ruby: &Ruby, args: &[Value]) -> Result<RString, MagnusError> {
|
|
248
|
+
let (rows, options) = WriteOptions::from_ruby_args(args)?;
|
|
249
|
+
|
|
250
|
+
if rows.is_empty() {
|
|
251
|
+
return Ok(ruby.str_new(""));
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
let first = rows.entry(0)?;
|
|
255
|
+
|
|
256
|
+
if RHash::from_value(first).is_some() {
|
|
257
|
+
generate_from_hashes(ruby, rows, &options)
|
|
258
|
+
} else if RArray::from_value(first).is_some() {
|
|
259
|
+
generate_from_arrays(ruby, rows, &options)
|
|
260
|
+
} else {
|
|
261
|
+
Err(ZlightError::InvalidOption {
|
|
262
|
+
key: "rows",
|
|
263
|
+
expected: "an Array of Arrays or an Array of Hashes",
|
|
264
|
+
actual: format!("an Array whose first element is {}", first.class()),
|
|
265
|
+
}
|
|
266
|
+
.into())
|
|
267
|
+
}
|
|
268
|
+
}
|
data/lib/zlight_csv.rb
ADDED
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'zlight_csv/version'
|
|
4
|
+
|
|
5
|
+
# Load the prebuilt native extension for this Ruby version
|
|
6
|
+
begin
|
|
7
|
+
ruby_version = RUBY_VERSION.match(/(\d+\.\d+)/)[1]
|
|
8
|
+
require_relative "zlight_csv/#{ruby_version}/zlight_csv"
|
|
9
|
+
rescue LoadError
|
|
10
|
+
require_relative 'zlight_csv/zlight_csv'
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
# ZLight is a high-performance CSV parser for Ruby, powered by Rust.
|
|
14
|
+
#
|
|
15
|
+
# It provides a simple, Ruby CSV-compatible API for parsing CSV files
|
|
16
|
+
# up to 40x faster than the standard library.
|
|
17
|
+
#
|
|
18
|
+
# @example Basic parsing with headers
|
|
19
|
+
# result = ZLight.parse("name,age\nAlice,30\nBob,25")
|
|
20
|
+
# # => [{:name=>"Alice", :age=>"30"}, {:name=>"Bob", :age=>"25"}]
|
|
21
|
+
#
|
|
22
|
+
# @example Parsing with numeric conversion
|
|
23
|
+
# result = ZLight.parse("name,age\nAlice,30", converters: :numeric)
|
|
24
|
+
# # => [{:name=>"Alice", :age=>30}]
|
|
25
|
+
#
|
|
26
|
+
# @example Parsing without headers
|
|
27
|
+
# result = ZLight.parse("Alice,30\nBob,25", headers: false)
|
|
28
|
+
# # => [["Alice", "30"], ["Bob", "25"]]
|
|
29
|
+
#
|
|
30
|
+
# @example Reading from a file
|
|
31
|
+
# result = ZLight.read("path/to/file.csv", headers: true)
|
|
32
|
+
#
|
|
33
|
+
# @example Streaming large files
|
|
34
|
+
# ZLight.stream_file("large.csv") do |reader|
|
|
35
|
+
# reader.each { |row| process(row) }
|
|
36
|
+
# end
|
|
37
|
+
#
|
|
38
|
+
# @example Lazy iteration with Enumerator
|
|
39
|
+
# reader = ZLight.stream_file("large.csv")
|
|
40
|
+
# reader.lazy.select { |row| row[:age] > 30 }.first(10)
|
|
41
|
+
# reader.close
|
|
42
|
+
#
|
|
43
|
+
# @see https://github.com/codebyisaad/zlight
|
|
44
|
+
module ZLight
|
|
45
|
+
# Base error class for all ZLight errors
|
|
46
|
+
class Error < StandardError; end
|
|
47
|
+
|
|
48
|
+
# Raised when CSV parsing fails due to malformed data
|
|
49
|
+
class ParseError < Error; end
|
|
50
|
+
|
|
51
|
+
# Raised when CSV contains invalid UTF-8 encoding
|
|
52
|
+
class EncodingError < Error; end
|
|
53
|
+
|
|
54
|
+
# Raised when attempting to use a closed stream reader
|
|
55
|
+
class StreamClosedError < Error; end
|
|
56
|
+
|
|
57
|
+
# Parses a CSV string and returns an array of rows.
|
|
58
|
+
#
|
|
59
|
+
# This method is implemented as a native Rust extension for maximum performance.
|
|
60
|
+
#
|
|
61
|
+
# @param csv_string [String] The CSV data to parse
|
|
62
|
+
# @param headers [Boolean] Treat first row as headers (default: true)
|
|
63
|
+
# @param converters [Symbol, nil] Set to :numeric for auto-conversion of integers/floats
|
|
64
|
+
# @param col_sep [String] Column separator character (default: ",")
|
|
65
|
+
# @param quote_char [String] Quote character for escaping (default: '"')
|
|
66
|
+
# @param flexible [Boolean] Allow variable-length records (default: true)
|
|
67
|
+
#
|
|
68
|
+
# @return [Array<Hash>] When headers: true, returns array of Hashes with Symbol keys
|
|
69
|
+
# @return [Array<Array>] When headers: false, returns array of Arrays
|
|
70
|
+
#
|
|
71
|
+
# @raise [ArgumentError] If options are invalid
|
|
72
|
+
# @raise [ZLight::ParseError] If CSV data is malformed
|
|
73
|
+
# @raise [ZLight::EncodingError] If headers contain invalid UTF-8
|
|
74
|
+
#
|
|
75
|
+
# @example Parse with headers (default)
|
|
76
|
+
# ZLight.parse("name,age\nAlice,30")
|
|
77
|
+
# # => [{:name=>"Alice", :age=>"30"}]
|
|
78
|
+
#
|
|
79
|
+
# @example Parse without headers
|
|
80
|
+
# ZLight.parse("Alice,30", headers: false)
|
|
81
|
+
# # => [["Alice", "30"]]
|
|
82
|
+
#
|
|
83
|
+
# @example Parse with numeric conversion
|
|
84
|
+
# ZLight.parse("name,age\nAlice,30", converters: :numeric)
|
|
85
|
+
# # => [{:name=>"Alice", :age=>30}]
|
|
86
|
+
#
|
|
87
|
+
# @example Parse TSV (tab-separated values)
|
|
88
|
+
# ZLight.parse("name\tage\nAlice\t30", col_sep: "\t")
|
|
89
|
+
# # => [{:name=>"Alice", :age=>"30"}]
|
|
90
|
+
#
|
|
91
|
+
# @note The parse method is defined in the Rust native extension.
|
|
92
|
+
# See ext/zlight_csv/src/lib.rs for implementation.
|
|
93
|
+
|
|
94
|
+
# Generates a CSV string from an array of rows.
|
|
95
|
+
#
|
|
96
|
+
# This method is implemented as a native Rust extension for maximum performance.
|
|
97
|
+
#
|
|
98
|
+
# @param rows [Array<Hash>, Array<Array>] The data to convert to CSV
|
|
99
|
+
# @param headers [Boolean] Write header row for hash input (default: true)
|
|
100
|
+
# @param col_sep [String] Column separator character (default: ",")
|
|
101
|
+
# @param quote_char [String] Quote character for escaping (default: '"')
|
|
102
|
+
# @param force_quotes [Boolean] Force quoting all fields (default: false)
|
|
103
|
+
#
|
|
104
|
+
# @return [String] The generated CSV string
|
|
105
|
+
#
|
|
106
|
+
# @raise [ArgumentError] If options are invalid or rows format is mixed
|
|
107
|
+
#
|
|
108
|
+
# @example Generate CSV from array of hashes
|
|
109
|
+
# ZLight.generate([{name: "Alice", age: 30}, {name: "Bob", age: 25}])
|
|
110
|
+
# # => "name,age\nAlice,30\nBob,25\n"
|
|
111
|
+
#
|
|
112
|
+
# @example Generate CSV from array of arrays (no headers)
|
|
113
|
+
# ZLight.generate([["Alice", 30], ["Bob", 25]])
|
|
114
|
+
# # => "Alice,30\nBob,25\n"
|
|
115
|
+
#
|
|
116
|
+
# @example Generate without headers
|
|
117
|
+
# ZLight.generate([{name: "Alice"}], headers: false)
|
|
118
|
+
# # => "Alice\n"
|
|
119
|
+
#
|
|
120
|
+
# @example Generate TSV
|
|
121
|
+
# ZLight.generate([{a: 1, b: 2}], col_sep: "\t")
|
|
122
|
+
# # => "a\tb\n1\t2\n"
|
|
123
|
+
#
|
|
124
|
+
# @note The generate method is defined in the Rust native extension.
|
|
125
|
+
# See ext/zlight_csv/src/writer.rs for implementation.
|
|
126
|
+
|
|
127
|
+
# StreamReader is defined in the Rust extension.
|
|
128
|
+
# It provides lazy/streaming CSV parsing for large files.
|
|
129
|
+
#
|
|
130
|
+
# @see ZLight.stream
|
|
131
|
+
# @see ZLight.stream_file
|
|
132
|
+
|
|
133
|
+
class << self
|
|
134
|
+
# Reads and parses a CSV file from disk.
|
|
135
|
+
#
|
|
136
|
+
# Convenience method that reads the file contents and passes them to {.parse}.
|
|
137
|
+
# The file is read with UTF-8 encoding.
|
|
138
|
+
#
|
|
139
|
+
# @param path [String] Path to the CSV file
|
|
140
|
+
# @param headers [Boolean] Treat first row as headers (default: true)
|
|
141
|
+
# @param converters [Symbol, nil] Set to :numeric for auto-conversion
|
|
142
|
+
# @param col_sep [String] Column separator character (default: ",")
|
|
143
|
+
# @param quote_char [String] Quote character for escaping (default: '"')
|
|
144
|
+
# @param flexible [Boolean] Allow variable-length records (default: true)
|
|
145
|
+
#
|
|
146
|
+
# @return [Array<Hash>] When headers: true, returns array of Hashes with Symbol keys
|
|
147
|
+
# @return [Array<Array>] When headers: false, returns array of Arrays
|
|
148
|
+
#
|
|
149
|
+
# @raise [Errno::ENOENT] If the file does not exist
|
|
150
|
+
# @raise [ArgumentError] If options are invalid
|
|
151
|
+
# @raise [ZLight::ParseError] If CSV data is malformed
|
|
152
|
+
#
|
|
153
|
+
# @example Read a CSV file with headers
|
|
154
|
+
# ZLight.read("users.csv")
|
|
155
|
+
# # => [{:name=>"Alice", :age=>"30"}, ...]
|
|
156
|
+
#
|
|
157
|
+
# @example Read with numeric conversion
|
|
158
|
+
# ZLight.read("users.csv", converters: :numeric)
|
|
159
|
+
# # => [{:name=>"Alice", :age=>30}, ...]
|
|
160
|
+
#
|
|
161
|
+
# @example Read a TSV file
|
|
162
|
+
# ZLight.read("data.tsv", col_sep: "\t")
|
|
163
|
+
#
|
|
164
|
+
# @see .parse
|
|
165
|
+
def read(path, **options)
|
|
166
|
+
parse(File.read(path, encoding: 'UTF-8'), **options)
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
# Writes CSV data to a file.
|
|
170
|
+
#
|
|
171
|
+
# Convenience method that generates CSV and writes to disk.
|
|
172
|
+
# The file is written with UTF-8 encoding.
|
|
173
|
+
#
|
|
174
|
+
# @param path [String] Path to the output CSV file
|
|
175
|
+
# @param rows [Array<Hash>, Array<Array>] The data to write
|
|
176
|
+
# @param headers [Boolean] Write header row for hash input (default: true)
|
|
177
|
+
# @param col_sep [String] Column separator character (default: ",")
|
|
178
|
+
# @param quote_char [String] Quote character for escaping (default: '"')
|
|
179
|
+
# @param force_quotes [Boolean] Force quoting all fields (default: false)
|
|
180
|
+
#
|
|
181
|
+
# @return [Integer] Number of bytes written
|
|
182
|
+
#
|
|
183
|
+
# @raise [Errno::ENOENT] If the directory does not exist
|
|
184
|
+
# @raise [Errno::EACCES] If the file is not writable
|
|
185
|
+
# @raise [ArgumentError] If options are invalid
|
|
186
|
+
#
|
|
187
|
+
# @example Write hashes to CSV
|
|
188
|
+
# ZLight.write("users.csv", [{name: "Alice", age: 30}])
|
|
189
|
+
#
|
|
190
|
+
# @example Write arrays to CSV
|
|
191
|
+
# ZLight.write("data.csv", [["a", "b"], [1, 2]])
|
|
192
|
+
#
|
|
193
|
+
# @example Write TSV
|
|
194
|
+
# ZLight.write("data.tsv", rows, col_sep: "\t")
|
|
195
|
+
#
|
|
196
|
+
# @see .generate
|
|
197
|
+
def write(path, rows, **options)
|
|
198
|
+
File.write(path, generate(rows, **options), encoding: 'UTF-8')
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
# Iterates over each row in the CSV string.
|
|
202
|
+
#
|
|
203
|
+
# When called with a block, yields each row and returns nil.
|
|
204
|
+
# When called without a block, returns an Enumerator for lazy iteration.
|
|
205
|
+
#
|
|
206
|
+
# @param csv_string [String] The CSV data to parse
|
|
207
|
+
# @param headers [Boolean] Treat first row as headers (default: true)
|
|
208
|
+
# @param converters [Symbol, nil] Set to :numeric for auto-conversion
|
|
209
|
+
# @param col_sep [String] Column separator character (default: ",")
|
|
210
|
+
# @param quote_char [String] Quote character for escaping (default: '"')
|
|
211
|
+
# @param flexible [Boolean] Allow variable-length records (default: true)
|
|
212
|
+
#
|
|
213
|
+
# @yield [row] Yields each row to the block
|
|
214
|
+
# @yieldparam row [Hash, Array] A single row (Hash with headers, Array without)
|
|
215
|
+
#
|
|
216
|
+
# @return [nil] When a block is given
|
|
217
|
+
# @return [Enumerator] When no block is given, for lazy iteration
|
|
218
|
+
#
|
|
219
|
+
# @example Iterate with a block
|
|
220
|
+
# ZLight.foreach("name,age\nAlice,30\nBob,25") do |row|
|
|
221
|
+
# puts row[:name]
|
|
222
|
+
# end
|
|
223
|
+
# # Output:
|
|
224
|
+
# # Alice
|
|
225
|
+
# # Bob
|
|
226
|
+
#
|
|
227
|
+
# @example Use as Enumerator
|
|
228
|
+
# names = ZLight.foreach("name,age\nAlice,30").map { |row| row[:name] }
|
|
229
|
+
# # => ["Alice"]
|
|
230
|
+
#
|
|
231
|
+
# @example Chain with Enumerable methods
|
|
232
|
+
# adults = ZLight.foreach(csv_data, converters: :numeric)
|
|
233
|
+
# .select { |row| row[:age] >= 18 }
|
|
234
|
+
# .map { |row| row[:name] }
|
|
235
|
+
#
|
|
236
|
+
# @see .parse
|
|
237
|
+
def foreach(csv_string, **options, &)
|
|
238
|
+
return to_enum(:foreach, csv_string, **options) unless block_given?
|
|
239
|
+
|
|
240
|
+
parse(csv_string, **options).each(&)
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
# Creates a streaming reader for a CSV file with automatic resource management.
|
|
244
|
+
#
|
|
245
|
+
# This is the most memory-efficient way to process large CSV files.
|
|
246
|
+
# Rows are read one at a time directly from disk.
|
|
247
|
+
#
|
|
248
|
+
# When called with a block, automatically closes the reader after
|
|
249
|
+
# the block completes (even if an exception is raised).
|
|
250
|
+
#
|
|
251
|
+
# @param path [String] Path to the CSV file
|
|
252
|
+
# @param headers [Boolean] Treat first row as headers (default: true)
|
|
253
|
+
# @param converters [Symbol, nil] Set to :numeric for auto-conversion
|
|
254
|
+
# @param col_sep [String] Column separator character (default: ",")
|
|
255
|
+
# @param quote_char [String] Quote character (default: '"')
|
|
256
|
+
# @param flexible [Boolean] Allow variable-length records (default: true)
|
|
257
|
+
#
|
|
258
|
+
# @yield [reader] If a block is given, yields the reader and auto-closes
|
|
259
|
+
# @yieldparam reader [ZLight::StreamReader] The streaming reader
|
|
260
|
+
#
|
|
261
|
+
# @return [ZLight::StreamReader] The streaming reader (if no block)
|
|
262
|
+
# @return [Object] The block's return value (if block given)
|
|
263
|
+
#
|
|
264
|
+
# @raise [IOError] If the file does not exist or cannot be read
|
|
265
|
+
# @raise [ZLight::StreamClosedError] If a row is read after the reader
|
|
266
|
+
# has been closed
|
|
267
|
+
#
|
|
268
|
+
# @example Process a large file row by row
|
|
269
|
+
# ZLight.open("users.csv") do |reader|
|
|
270
|
+
# reader.each do |row|
|
|
271
|
+
# User.create!(name: row[:name], email: row[:email])
|
|
272
|
+
# end
|
|
273
|
+
# end
|
|
274
|
+
#
|
|
275
|
+
# @example Get first 100 matching rows lazily
|
|
276
|
+
# ZLight.open("data.csv", converters: :numeric) do |reader|
|
|
277
|
+
# reader.lazy
|
|
278
|
+
# .select { |row| row[:score] > 90 }
|
|
279
|
+
# .first(100)
|
|
280
|
+
# end
|
|
281
|
+
#
|
|
282
|
+
# @example Manual resource management
|
|
283
|
+
# reader = ZLight.open("large.csv")
|
|
284
|
+
# begin
|
|
285
|
+
# reader.each { |row| process(row) }
|
|
286
|
+
# ensure
|
|
287
|
+
# reader.close
|
|
288
|
+
# end
|
|
289
|
+
#
|
|
290
|
+
# @see ZLight::StreamReader
|
|
291
|
+
def open(path, **options)
|
|
292
|
+
reader = stream_file(path, **options)
|
|
293
|
+
|
|
294
|
+
if block_given?
|
|
295
|
+
begin
|
|
296
|
+
yield reader
|
|
297
|
+
ensure
|
|
298
|
+
reader.close unless reader.closed?
|
|
299
|
+
end
|
|
300
|
+
else
|
|
301
|
+
reader
|
|
302
|
+
end
|
|
303
|
+
end
|
|
304
|
+
end
|
|
305
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: zlight_csv
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.5.1
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Zaidan Chaudhary
|
|
8
|
+
- Saad Chaudhary
|
|
9
|
+
autorequire:
|
|
10
|
+
bindir: bin
|
|
11
|
+
cert_chain: []
|
|
12
|
+
date: 2026-09-12 00:00:00.000000000 Z
|
|
13
|
+
dependencies:
|
|
14
|
+
- !ruby/object:Gem::Dependency
|
|
15
|
+
name: rb_sys
|
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
|
17
|
+
requirements:
|
|
18
|
+
- - "~>"
|
|
19
|
+
- !ruby/object:Gem::Version
|
|
20
|
+
version: '0.9'
|
|
21
|
+
type: :runtime
|
|
22
|
+
prerelease: false
|
|
23
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
24
|
+
requirements:
|
|
25
|
+
- - "~>"
|
|
26
|
+
- !ruby/object:Gem::Version
|
|
27
|
+
version: '0.9'
|
|
28
|
+
description: |
|
|
29
|
+
ZLight brings the speed of Rust to Ruby's CSV parsing. Built as a native
|
|
30
|
+
extension, it delivers up to 40x faster performance than Ruby's standard
|
|
31
|
+
CSV library while maintaining full API compatibility. Whether you're parsing
|
|
32
|
+
small configuration files or processing millions of rows, ZLight handles it
|
|
33
|
+
efficiently. The streaming API enables memory-efficient processing of large
|
|
34
|
+
datasets by reading rows one at a time, and lazy enumeration lets you stop
|
|
35
|
+
early without wasting resources on unneeded data.
|
|
36
|
+
email:
|
|
37
|
+
- zaidan@arxiron.com
|
|
38
|
+
executables: []
|
|
39
|
+
extensions:
|
|
40
|
+
- ext/zlight_csv/extconf.rb
|
|
41
|
+
extra_rdoc_files: []
|
|
42
|
+
files:
|
|
43
|
+
- CHANGELOG.md
|
|
44
|
+
- LICENSE
|
|
45
|
+
- README.md
|
|
46
|
+
- VERSION
|
|
47
|
+
- ext/zlight_csv/Cargo.toml
|
|
48
|
+
- ext/zlight_csv/extconf.rb
|
|
49
|
+
- ext/zlight_csv/src/converter.rs
|
|
50
|
+
- ext/zlight_csv/src/error.rs
|
|
51
|
+
- ext/zlight_csv/src/lib.rs
|
|
52
|
+
- ext/zlight_csv/src/options.rs
|
|
53
|
+
- ext/zlight_csv/src/parser.rs
|
|
54
|
+
- ext/zlight_csv/src/stream.rs
|
|
55
|
+
- ext/zlight_csv/src/writer.rs
|
|
56
|
+
- lib/zlight_csv.rb
|
|
57
|
+
- lib/zlight_csv/version.rb
|
|
58
|
+
homepage: https://github.com/codebyisaad/zlight
|
|
59
|
+
licenses:
|
|
60
|
+
- MIT
|
|
61
|
+
metadata:
|
|
62
|
+
source_code_uri: https://github.com/codebyisaad/zlight
|
|
63
|
+
changelog_uri: https://github.com/codebyisaad/zlight/blob/main/CHANGELOG.md
|
|
64
|
+
bug_tracker_uri: https://github.com/codebyisaad/zlight/issues
|
|
65
|
+
documentation_uri: https://codebyisaad.github.io/zlight-docs/
|
|
66
|
+
rubygems_mfa_required: 'true'
|
|
67
|
+
post_install_message:
|
|
68
|
+
rdoc_options: []
|
|
69
|
+
require_paths:
|
|
70
|
+
- lib
|
|
71
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
72
|
+
requirements:
|
|
73
|
+
- - ">="
|
|
74
|
+
- !ruby/object:Gem::Version
|
|
75
|
+
version: 3.1.0
|
|
76
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
77
|
+
requirements:
|
|
78
|
+
- - ">="
|
|
79
|
+
- !ruby/object:Gem::Version
|
|
80
|
+
version: '0'
|
|
81
|
+
requirements: []
|
|
82
|
+
rubygems_version: 3.5.22
|
|
83
|
+
signing_key:
|
|
84
|
+
specification_version: 4
|
|
85
|
+
summary: High-performance CSV parser for Ruby, powered by Rust
|
|
86
|
+
test_files: []
|