Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add skip_blank_lines option #308

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 27 additions & 1 deletion csv-core/src/reader.rs
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,8 @@ pub struct Reader {
/// If enabled (the default), then quotes are respected. When disabled,
/// quotes are not treated specially.
quoting: bool,
/// If enabled (the default) blank lines are ignored.
skip_blank_lines: bool,
/// Whether to use the NFA for parsing.
///
/// Generally this is for debugging. There's otherwise no good reason
Expand All @@ -141,6 +143,7 @@ impl Default for Reader {
double_quote: true,
comment: None,
quoting: true,
skip_blank_lines: true,
use_nfa: false,
line: 1,
has_read: false,
Expand Down Expand Up @@ -227,6 +230,16 @@ impl ReaderBuilder {
self
}

/// Enable or disable skipping of blank lines
///
/// This is enabled by default, but it may be disabled. When enabled,
/// blank lines are ignored. If present, the finally trailing blank line
/// will be ignored.
pub fn skip_blank_lines(&mut self, yes: bool) -> &mut ReaderBuilder {
self.rdr.skip_blank_lines = yes;
self
}

/// The comment character to use when parsing CSV.
///
/// If the start of a record begins with the byte given here, then that
Expand Down Expand Up @@ -992,7 +1005,11 @@ impl Reader {
End => (End, NfaInputAction::Epsilon),
StartRecord => {
if self.term.equals(c) {
(StartRecord, NfaInputAction::Discard)
if self.skip_blank_lines {
(StartRecord, NfaInputAction::Discard)
} else {
(CRLF, NfaInputAction::Discard)
}
} else if self.comment == Some(c) {
(InComment, NfaInputAction::Discard)
} else {
Expand Down Expand Up @@ -1726,6 +1743,15 @@ mod tests {
}
);

fn enable_blank(builder: &mut ReaderBuilder) -> &mut ReaderBuilder {
builder.skip_blank_lines(false)
}
parses_to!(blank_lines_one_row_one_field, "a", csv![["a"]], enable_blank);
parses_to!(blank_lines_one_row_one_field_lf, "a\n", csv![["a"]], enable_blank);
parses_to!(blank_lines_one_row_one_field_lf_lf, "a\n\n", csv![["a"], [""]], enable_blank);
parses_to!(blank_lines_one_row_lf_one_field_lf, "\na\n", csv![[""], ["a"]], enable_blank);
parses_to!(blank_lines_crlf, "\r\na\r\n", csv![[""], ["a"]], enable_blank);

macro_rules! assert_read {
(
$rdr:expr, $input:expr, $output:expr,
Expand Down
61 changes: 61 additions & 0 deletions src/reader.rs
Original file line number Diff line number Diff line change
Expand Up @@ -362,6 +362,41 @@ impl ReaderBuilder {
self
}

/// Whether blank lines are ignored.
///
/// By default blank lines are ignored.
///
/// When disabled
///
/// # Example
///
/// ```
/// use std::error::Error;
/// use csv::ReaderBuilder;
///
/// # fn main() { example().unwrap(); }
/// fn example() -> Result<(), Box<dyn Error>> {
/// let data = "\na,b";
/// let mut rdr = ReaderBuilder::new()
/// .skip_blank_lines(false)
/// .flexible(true)
/// .has_headers(false)
/// .from_reader(data.as_bytes());
///
/// if let Some(result) = rdr.records().next() {
/// let record = result?;
/// assert_eq!(record, vec![""]);
/// Ok(())
/// } else {
/// Err(From::from("expected at least one record but got none"))
/// }
/// }
/// ```
pub fn skip_blank_lines(&mut self, yes: bool) -> &mut ReaderBuilder {
self.builder.skip_blank_lines(yes);
self
}

/// The record terminator to use when parsing CSV.
///
/// A record terminator can be any single byte. The default is a special
Expand Down Expand Up @@ -2508,6 +2543,32 @@ mod tests {
assert_eq!("baz", &headers[2]);
}

#[test]
fn read_record_blank_lines() {
let data = b("foo,bar,baz\n\na,b,c\nd,e,f");
let mut rdr = ReaderBuilder::new()
.flexible(true).skip_blank_lines(false).has_headers(false).from_reader(data);
let mut rec = StringRecord::new();

assert!(rdr.read_record(&mut rec).unwrap());
assert_eq!(3, rec.len());
assert_eq!("foo", &rec[0]);

assert!(rdr.read_record(&mut rec).unwrap());
assert_eq!(1, rec.len());
assert_eq!("", &rec[0]);

assert!(rdr.read_record(&mut rec).unwrap());
assert_eq!(3, rec.len());
assert_eq!("a", &rec[0]);

assert!(rdr.read_record(&mut rec).unwrap());
assert_eq!(3, rec.len());
assert_eq!("d", &rec[0]);

assert!(!rdr.read_record(&mut rec).unwrap());
}

#[test]
fn seek() {
let data = b("foo,bar,baz\na,b,c\nd,e,f\ng,h,i");
Expand Down