Skip to content

Commit

Permalink
Merge branch 'release-1.1.0'
Browse files Browse the repository at this point in the history
  • Loading branch information
charphi committed Nov 25, 2019
2 parents 2e99f98 + 02c3639 commit bd9bfbc
Show file tree
Hide file tree
Showing 7 changed files with 418 additions and 242 deletions.
2 changes: 1 addition & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

<groupId>be.nbb.rd</groupId>
<artifactId>picocsv</artifactId>
<version>1.0.1</version>
<version>1.1.0</version>
<packaging>jar</packaging>

<name>picocsv</name>
Expand Down
160 changes: 103 additions & 57 deletions src/main/java/nbbrd/picocsv/Csv.java
Original file line number Diff line number Diff line change
Expand Up @@ -307,21 +307,25 @@ private static Reader make(Format format, OptionalInt charBufferSize, java.io.Re
}

private final Input input;
private final int quote;
private final int delimiter;
private final int quoteCode;
private final int delimiterCode;
private final EndOfLineReader endOfLine;
private char[] fieldChars;
private int fieldLength;
private boolean fieldQuoted;
private State state;
private boolean parsedByLine;

private Reader(Input input, int quote, int delimiter, EndOfLineReader endOfLine) {
private static final int INITIAL_FIELD_CAPACITY = 64;

private Reader(Input input, int quoteCode, int delimiterCode, EndOfLineReader endOfLine) {
this.input = input;
this.quote = quote;
this.delimiter = delimiter;
this.quoteCode = quoteCode;
this.delimiterCode = delimiterCode;
this.endOfLine = endOfLine;
this.fieldChars = new char[64];
this.fieldChars = new char[INITIAL_FIELD_CAPACITY];
this.fieldLength = 0;
this.fieldQuoted = false;
this.state = State.READY;
this.parsedByLine = false;
}
Expand Down Expand Up @@ -367,7 +371,7 @@ public boolean readField() throws IOException {
case LAST:
if (parsedByLine) {
parsedByLine = false;
return fieldLength > 0;
return isFieldNotNull();
}
return false;
case NOT_LAST:
Expand Down Expand Up @@ -399,12 +403,12 @@ private State parseNextField(boolean skip) throws IOException {
: this::append;

// first char
boolean quoted = false;
fieldQuoted = false;
if ((val = input.read()) != Input.EOF) {
if (val == quote) {
quoted = true;
if (val == quoteCode) {
fieldQuoted = true;
} else {
if (val == delimiter) {
if (val == delimiterCode) {
return State.NOT_LAST;
}
if (endOfLine.isEndOfLine(val, input)) {
Expand All @@ -416,11 +420,11 @@ private State parseNextField(boolean skip) throws IOException {
return State.DONE;
}

if (quoted) {
if (fieldQuoted) {
// subsequent chars with escape
boolean escaped = false;
while ((val = input.read()) != Input.EOF) {
if (val == quote) {
if (val == quoteCode) {
if (!escaped) {
escaped = true;
} else {
Expand All @@ -430,7 +434,7 @@ private State parseNextField(boolean skip) throws IOException {
continue;
}
if (escaped) {
if (val == delimiter) {
if (val == delimiterCode) {
return State.NOT_LAST;
}
if (endOfLine.isEndOfLine(val, input)) {
Expand All @@ -442,7 +446,7 @@ private State parseNextField(boolean skip) throws IOException {
} else {
// subsequent chars without escape
while ((val = input.read()) != Input.EOF) {
if (val == delimiter) {
if (val == delimiterCode) {
return State.NOT_LAST;
}
if (endOfLine.isEndOfLine(val, input)) {
Expand All @@ -452,7 +456,7 @@ private State parseNextField(boolean skip) throws IOException {
}
}

return fieldLength > 0 ? State.LAST : State.DONE;
return isFieldNotNull() ? State.LAST : State.DONE;
}

private void ensureFieldSize() {
Expand All @@ -465,13 +469,17 @@ private void resetField() {
fieldLength = 0;
}

private void swallow(int c) {
private boolean isFieldNotNull() {
return fieldLength > 0 || fieldQuoted;
}

private void swallow(int code) {
// do nothing
}

private void append(int c) {
private void append(int code) {
ensureFieldSize();
fieldChars[fieldLength++] = (char) c;
fieldChars[fieldLength++] = (char) code;
}

@Override
Expand All @@ -486,11 +494,17 @@ public int length() {

@Override
public char charAt(int index) {
if (index >= fieldLength) {
throw new IndexOutOfBoundsException(String.valueOf(index));
}
return fieldChars[index];
}

@Override
public CharSequence subSequence(int start, int end) {
if (end > fieldLength) {
throw new IndexOutOfBoundsException(String.valueOf(end));
}
return new String(fieldChars, start, end - start);
}

Expand Down Expand Up @@ -559,17 +573,20 @@ public void discardAheadOfTimeChar() throws IOException {
@FunctionalInterface
private interface EndOfLineReader {

boolean isEndOfLine(int c, Input input) throws IOException;
boolean isEndOfLine(int code, Input input) throws IOException;

static final int CR_CODE = NewLine.CR;
static final int LF_CODE = NewLine.LF;

static EndOfLineReader of(NewLine newLine) {
switch (newLine) {
case MACINTOSH:
return (c, input) -> c == NewLine.CR;
return (code, input) -> code == CR_CODE;
case UNIX:
return (c, input) -> c == NewLine.LF;
return (code, input) -> code == LF_CODE;
case WINDOWS:
return (c, input) -> {
if (c == NewLine.CR && ((ReadAheadInput) input).peek(NewLine.LF)) {
return (code, input) -> {
if (code == CR_CODE && ((ReadAheadInput) input).peek(LF_CODE)) {
((ReadAheadInput) input).discardAheadOfTimeChar();
return true;
}
Expand Down Expand Up @@ -676,27 +693,69 @@ private static Writer make(Format format, OptionalInt charBufferSize, java.io.Wr
private final char quote;
private final char delimiter;
private final EndOfLineWriter endOfLine;
private boolean requiresDelimiter;
private State state;

private Writer(Output output, char quote, char delimiter, EndOfLineWriter endOfLine) {
this.output = output;
this.quote = quote;
this.delimiter = delimiter;
this.endOfLine = endOfLine;
this.requiresDelimiter = false;
this.state = State.NO_FIELD;
}

/**
* Writes a new field.
* Writes a new field. Null field is handled as empty.
*
* @param field a non-null field
* @param field a nullable field
* @throws IOException if an I/O error occurs
*/
public void writeField(CharSequence field) throws IOException {
if (pushFields()) {
output.write(delimiter);
switch (state) {
case NO_FIELD:
if (!isNullOrEmpty(field)) {
state = State.MULTI_FIELD;
writeNonEmptyField(field);
} else {
state = State.SINGLE_EMPTY_FIELD;
}
break;
case SINGLE_EMPTY_FIELD:
state = State.MULTI_FIELD;
output.write(delimiter);
if (!isNullOrEmpty(field)) {
writeNonEmptyField(field);
}
break;
case MULTI_FIELD:
output.write(delimiter);
if (!isNullOrEmpty(field)) {
writeNonEmptyField(field);
}
break;
}
}

/**
* Writes an end of line.
*
* @throws IOException if an I/O error occurs
*/
public void writeEndOfLine() throws IOException {
flushField();
endOfLine.write(output);
}

@Override
public void close() throws IOException {
flushField();
output.close();
}

private boolean isNullOrEmpty(CharSequence field) {
return field == null || field.length() == 0;
}

private void writeNonEmptyField(CharSequence field) throws IOException {
switch (getQuoting(field)) {
case NONE:
output.write(field);
Expand All @@ -720,29 +779,12 @@ public void writeField(CharSequence field) throws IOException {
}
}

/**
* Writes an end of line.
*
* @throws IOException if an I/O error occurs
*/
public void writeEndOfLine() throws IOException {
endOfLine.write(output);
resetFields();
}

@Override
public void close() throws IOException {
output.close();
}

private boolean pushFields() {
boolean result = requiresDelimiter;
requiresDelimiter = true;
return result;
}

private void resetFields() {
requiresDelimiter = false;
private void flushField() throws IOException {
if (state == State.SINGLE_EMPTY_FIELD) {
output.write(quote);
output.write(quote);
}
state = State.NO_FIELD;
}

private Quoting getQuoting(CharSequence field) {
Expand All @@ -763,6 +805,10 @@ private enum Quoting {
NONE, PARTIAL, FULL
}

private enum State {
NO_FIELD, SINGLE_EMPTY_FIELD, MULTI_FIELD
}

private static final class Output implements Closeable {

private final java.io.Writer charWriter;
Expand Down Expand Up @@ -816,16 +862,16 @@ private void flush() throws IOException {
@FunctionalInterface
private interface EndOfLineWriter {

void write(Output stream) throws IOException;
void write(Output output) throws IOException;

static EndOfLineWriter of(NewLine newLine) {
switch (newLine) {
case MACINTOSH:
return stream -> stream.write(NewLine.CR);
return output -> output.write(NewLine.CR);
case UNIX:
return stream -> stream.write(NewLine.LF);
return output -> output.write(NewLine.LF);
case WINDOWS:
return stream -> stream.write(NewLine.CRLF);
return output -> output.write(NewLine.CRLF);
default:
throw new RuntimeException();
}
Expand Down
8 changes: 5 additions & 3 deletions src/test/java/_test/QuickReader.java
Original file line number Diff line number Diff line change
Expand Up @@ -64,10 +64,10 @@ public <T> T readValue(QuickReader.Parser<T> parser, Charset encoding, Csv.Forma
}
}
},
READER(StreamType.OBJECT) {
CHAR_READER(StreamType.OBJECT) {
@Override
public <T> T readValue(QuickReader.Parser<T> parser, Charset encoding, Csv.Format format, String input) throws IOException {
try (Reader object = newReader(input)) {
try (Reader object = newCharReader(input)) {
try (Csv.Reader reader = Csv.Reader.of(object, format)) {
return parser.accept(reader);
}
Expand All @@ -87,11 +87,13 @@ public void read(VoidParser parser, Charset encoding, Csv.Format format, String
}, encoding, format, input);
}

@FunctionalInterface
public interface Parser<T> {

T accept(Csv.Reader reader) throws IOException;
}

@FunctionalInterface
public interface VoidParser {

void accept(Csv.Reader reader) throws IOException;
Expand All @@ -108,7 +110,7 @@ public static InputStream newInputStream(String content, Charset charset) {
return new ByteArrayInputStream(content.getBytes(charset));
}

public static Reader newReader(String content) {
public static Reader newCharReader(String content) {
return new StringReader(content);
}
}
8 changes: 5 additions & 3 deletions src/test/java/_test/QuickWriter.java
Original file line number Diff line number Diff line change
Expand Up @@ -66,10 +66,10 @@ public <T> String writeValue(T value, QuickWriter.Formatter<T> formatter, Charse
}
}
},
WRITER(StreamType.OBJECT) {
CHAR_WRITER(StreamType.OBJECT) {
@Override
public <T> String writeValue(T value, QuickWriter.Formatter<T> formatter, Charset encoding, Csv.Format format) throws IOException {
try (Writer result = newWriter()) {
try (Writer result = newCharWriter()) {
try (Csv.Writer writer = Csv.Writer.of(result, format)) {
formatter.accept(value, writer);
}
Expand All @@ -87,11 +87,13 @@ public String write(VoidFormatter formatter, Charset encoding, Csv.Format format
return writeValue(null, (value, stream) -> formatter.accept(stream), encoding, format);
}

@FunctionalInterface
public interface Formatter<T> {

void accept(T value, Csv.Writer writer) throws IOException;
}

@FunctionalInterface
public interface VoidFormatter {

void accept(Csv.Writer writer) throws IOException;
Expand All @@ -107,7 +109,7 @@ public static OutputStream newOutputStream() {
return new ByteArrayOutputStream();
}

public static Writer newWriter() {
public static Writer newCharWriter() {
return new StringWriter();
}

Expand Down
Loading

0 comments on commit bd9bfbc

Please sign in to comment.