Skip to content

Commit

Permalink
Add Format#missingFieldAllowed option to follow the RFC4180 spec closely
Browse files Browse the repository at this point in the history
  • Loading branch information
charphi committed Mar 6, 2024
1 parent 3c9908c commit e9f36cd
Show file tree
Hide file tree
Showing 4 changed files with 76 additions and 10 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Added

- Add `Format#missingFieldAllowed` option to follow the RFC4180 spec closely [#208](https://github.com/nbbrd/picocsv/issues/208)

### Changed

- Java 8 minimum requirement
Expand Down
48 changes: 39 additions & 9 deletions src/main/java/nbbrd/picocsv/Csv.java
Original file line number Diff line number Diff line change
Expand Up @@ -92,11 +92,12 @@ public static final class Format {
private static final char DEFAULT_DELIMITER = ',';
private static final char DEFAULT_QUOTE = '"';
private static final char DEFAULT_COMMENT = '#';
private static final boolean DEFAULT_MISSING_FIELD_ALLOWED = true;

/**
* Predefined format as defined by <a href="https://tools.ietf.org/html/rfc4180">RFC 4180</a>.
*/
public static final Format RFC4180 = new Format(DEFAULT_SEPARATOR, DEFAULT_DELIMITER, DEFAULT_QUOTE, DEFAULT_COMMENT);
public static final Format RFC4180 = new Format(DEFAULT_SEPARATOR, DEFAULT_DELIMITER, DEFAULT_QUOTE, DEFAULT_COMMENT, DEFAULT_MISSING_FIELD_ALLOWED);

/**
* Predefined format as alias to {@link Format#RFC4180}.
Expand All @@ -107,12 +108,14 @@ public static final class Format {
private final char delimiter;
private final char quote;
private final char comment;
private final boolean missingFieldAllowed;

private Format(String separator, char delimiter, char quote, char comment) {
private Format(String separator, char delimiter, char quote, char comment, boolean missingFieldAllowed) {
this.separator = Objects.requireNonNull(separator, "separator");
this.delimiter = delimiter;
this.quote = quote;
this.comment = comment;
this.missingFieldAllowed = missingFieldAllowed;
}

/**
Expand Down Expand Up @@ -160,6 +163,15 @@ public char getComment() {
return comment;
}

/**
* Determines if missing field is allowed in a record.
*
* @return <code>true</code> if missing field is allowed, <code>false</code> otherwise
*/
public boolean isMissingFieldAllowed() {
return missingFieldAllowed;
}

/**
* Checks if the current format is valid.
*
Expand Down Expand Up @@ -197,6 +209,7 @@ public int hashCode() {
hash = 37 * hash + this.delimiter;
hash = 37 * hash + this.quote;
hash = 37 * hash + this.comment;
hash = 37 * hash + (this.missingFieldAllowed ? 1 : 0);
return hash;
}

Expand All @@ -210,6 +223,7 @@ public boolean equals(Object obj) {
if (this.delimiter != other.delimiter) return false;
if (this.quote != other.quote) return false;
if (this.comment != other.comment) return false;
if (this.missingFieldAllowed != other.missingFieldAllowed) return false;
return true;
}

Expand All @@ -220,6 +234,7 @@ public String toString() {
+ ", delimiter=" + prettyPrint(delimiter)
+ ", quote=" + prettyPrint(quote)
+ ", comment=" + prettyPrint(comment)
+ ", missingFieldAllowed=" + missingFieldAllowed
+ ')';
}

Expand All @@ -233,7 +248,8 @@ public Builder toBuilder() {
.separator(separator)
.delimiter(delimiter)
.quote(quote)
.comment(comment);
.comment(comment)
.missingFieldAllowed(missingFieldAllowed);
}

/**
Expand All @@ -254,6 +270,7 @@ public static final class Builder {
private char delimiter;
private char quote;
private char comment;
private boolean missingFieldAllowed;

private Builder() {
}
Expand Down Expand Up @@ -302,13 +319,24 @@ public Builder comment(char comment) {
return this;
}

/**
* Sets the {@link Format#isMissingFieldAllowed()} () comment} parameter of {@link Format}.
*
* @param missingFieldAllowed a boolean
* @return this builder
*/
public Builder missingFieldAllowed(boolean missingFieldAllowed) {
this.missingFieldAllowed = missingFieldAllowed;
return this;
}

/**
* Creates a new instance of {@link Format}.
*
* @return a non-null new instance
*/
public Format build() {
return new Format(separator, delimiter, quote, comment);
return new Format(separator, delimiter, quote, comment, missingFieldAllowed);
}
}
}
Expand Down Expand Up @@ -565,7 +593,7 @@ public static Reader of(Format format, ReaderOptions options, java.io.Reader cha

return new Reader(
ReadAheadInput.isNeeded(format, options) ? new ReadAheadInput(charReader, charBuffer) : new Input(charReader, charBuffer),
format.getQuote(), format.getDelimiter(), format.getComment(),
format.getQuote(), format.getDelimiter(), format.getComment(), format.isMissingFieldAllowed(),
EndOfLineDecoder.of(format, options),
new char[options.getMaxCharsPerField()]);
}
Expand All @@ -574,6 +602,7 @@ public static Reader of(Format format, ReaderOptions options, java.io.Reader cha
private final int quoteCode;
private final int delimiterCode;
private final int commentCode;
private final boolean missingFieldAllowed;
private final EndOfLineDecoder eolDecoder;
private final char[] fieldChars;

Expand All @@ -582,11 +611,12 @@ public static Reader of(Format format, ReaderOptions options, java.io.Reader cha
private int state = STATE_READY;
private boolean firstField = false;

private Reader(Input input, int quoteCode, int delimiterCode, int commentCode, EndOfLineDecoder eolDecoder, char[] fieldChars) {
private Reader(Input input, int quoteCode, int delimiterCode, int commentCode, boolean missingFieldAllowed, EndOfLineDecoder eolDecoder, char[] fieldChars) {
this.input = input;
this.quoteCode = quoteCode;
this.delimiterCode = delimiterCode;
this.commentCode = commentCode;
this.missingFieldAllowed = missingFieldAllowed;
this.eolDecoder = eolDecoder;
this.fieldChars = fieldChars;
}
Expand Down Expand Up @@ -631,7 +661,7 @@ public boolean readField() throws IOException {
case STATE_LAST:
if (firstField) {
firstField = false;
return isFieldNotNull();
return hasFirstField();
}
return false;
case STATE_NOT_LAST:
Expand Down Expand Up @@ -805,8 +835,8 @@ private void parseNextField() throws IOException {
}
}

private boolean isFieldNotNull() {
return fieldLength > 0 || fieldType != FIELD_TYPE_NORMAL;
private boolean hasFirstField() {
return !missingFieldAllowed || fieldLength > 0 || fieldType != FIELD_TYPE_NORMAL;
}

@Override
Expand Down
7 changes: 6 additions & 1 deletion src/test/java/nbbrd/picocsv/CsvFormatTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
*/
public class CsvFormatTest {

@SuppressWarnings("ResultOfMethodCallIgnored")
@Test
public void testFactories() {
assertThatNullPointerException()
Expand All @@ -45,6 +46,9 @@ public void testFactories() {

assertThat(Csv.Format.builder().comment('e').build().getComment())
.isEqualTo('e');

assertThat(Csv.Format.builder().missingFieldAllowed(false).build().isMissingFieldAllowed())
.isEqualTo(false);
}

@Test
Expand All @@ -66,6 +70,7 @@ public void testEqualsAndHashcode() {
.isNotEqualTo(Csv.Format.DEFAULT.toBuilder().quote('x').build())
.isNotEqualTo(Csv.Format.DEFAULT.toBuilder().separator(Csv.Format.MACINTOSH_SEPARATOR).build())
.isNotEqualTo(Csv.Format.DEFAULT.toBuilder().comment('x').build())
.isNotEqualTo(Csv.Format.DEFAULT.toBuilder().missingFieldAllowed(false).build())
.isNotEqualTo(null)
.isNotEqualTo("");

Expand All @@ -79,7 +84,7 @@ public void testToString() {
assertThat(Csv.Format.DEFAULT.toString())
.isEqualTo(Csv.Format.DEFAULT.toString())
.isNotEqualTo(other.toString())
.isEqualTo("Format(separator=\\r\\n, delimiter=,, quote=\\\", comment=#)");
.isEqualTo("Format(separator=\\r\\n, delimiter=,, quote=\\\", comment=#, missingFieldAllowed=true)");

for (char c : Sample.SPECIAL_CHARS) {
assertThat(Csv.Format.builder().delimiter(c).build().toString())
Expand Down
27 changes: 27 additions & 0 deletions src/test/java/nbbrd/picocsv/CsvReaderTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,33 @@ public void testEmptyLine() throws IOException {
}
}

@Test
void testEveryLineHasAtLeastOneField() throws IOException {
String csv = "A\r\n"
+ "\r\n"
+ "B\r\n";

Csv.Format validRFC4180 = RFC4180.toBuilder().missingFieldAllowed(false).build();
try (Csv.Reader reader = Csv.Reader.of(validRFC4180, Csv.ReaderOptions.DEFAULT, new StringReader(csv))) {
assertThat(reader.readLine()).isTrue();
assertThat(reader.readField()).isTrue();
assertThat(reader.toString()).isEqualTo("A");
assertThat(reader.readField()).isFalse();

assertThat(reader.readLine()).isTrue();
assertThat(reader.readField()).isTrue(); // here
assertThat(reader).hasToString("");
assertThat(reader.readField()).isFalse();

assertThat(reader.readLine()).isTrue();
assertThat(reader.readField()).isTrue();
assertThat(reader.toString()).isEqualTo("B");
assertThat(reader.readField()).isFalse();

assertThat(reader.readLine()).isFalse();
}
}

@Test
public void testEmptyFirstField() {
assertThat(Sample
Expand Down

0 comments on commit e9f36cd

Please sign in to comment.