From 8dcbd15df2ba16f6406c2fe667c2f1ff94f642a2 Mon Sep 17 00:00:00 2001 From: Pavel Zavora Date: Wed, 29 Apr 2020 09:24:56 +0200 Subject: [PATCH] feat(pkg/csv2lp): add SkipHeaderLinesReader --- pkg/csv2lp/skip_header_lines.go | 54 ++++++++++++++++++ pkg/csv2lp/skip_header_lines_test.go | 82 ++++++++++++++++++++++++++++ 2 files changed, 136 insertions(+) create mode 100644 pkg/csv2lp/skip_header_lines.go create mode 100644 pkg/csv2lp/skip_header_lines_test.go diff --git a/pkg/csv2lp/skip_header_lines.go b/pkg/csv2lp/skip_header_lines.go new file mode 100644 index 0000000000..a8231da13f --- /dev/null +++ b/pkg/csv2lp/skip_header_lines.go @@ -0,0 +1,54 @@ +package csv2lp + +import ( + "io" +) + +// skipFirstLines is an io.Reader that skips first lines +type skipFirstLines struct { + reader io.Reader + skipLines int + // line is a mutable variable that increases until skipLines is reached + line int +} + +// Read implements io.Reader +func (state *skipFirstLines) Read(p []byte) (n int, err error) { +skipHeaderLines: + for state.line < state.skipLines { + n, err := state.reader.Read(p) + if n == 0 { + return n, err + } + for i := 0; i < n; i++ { + if p[i] == '\n' { + state.line++ + if state.line == state.skipLines { + // modify the buffer and return + if i == n-1 { + if err != nil { + return 0, err + } + // continue with the next chunk + break skipHeaderLines + } else { + // copy all bytes after the newline + for j := i + 1; j < n; j++ { + p[j-i-1] = p[j] + } + return n - i - 1, err + } + } + } + } + } + return state.reader.Read(p) +} + +// SkipHeaderLinesReader wraps a reader to skip the first skipLines lines +func SkipHeaderLinesReader(skipLines int, reader io.Reader) io.Reader { + return &skipFirstLines{ + skipLines: skipLines, + reader: reader, + } +} diff --git a/pkg/csv2lp/skip_header_lines_test.go b/pkg/csv2lp/skip_header_lines_test.go new file mode 100644 index 0000000000..cbec63a651 --- /dev/null +++ b/pkg/csv2lp/skip_header_lines_test.go @@ -0,0 +1,82 @@ +package csv2lp + +import ( + "io" + "strconv" + "strings" + "testing" + + "github.com/stretchr/testify/require" +) + +// simulates the reader that returns all data together with EOF +type readOnceWithEOF struct { + reader io.Reader +} + +func (r *readOnceWithEOF) Read(p []byte) (n int, err error) { + n, _ = r.reader.Read(p) + return n, io.EOF +} + +// Test_SkipHeaderLines checks that first lines are skipped +func Test_SkipHeaderLines(t *testing.T) { + input := "1\n2\n3\n4\n5\n6\n7\n8\n9\n0\n" + + var tests = []struct { + skipCount int + result string + }{ + { + 10, + "", + }, + { + 0, + "1\n2\n3\n4\n5\n6\n7\n8\n9\n0\n", + }, + { + 1, + "2\n3\n4\n5\n6\n7\n8\n9\n0\n", + }, + { + 5, + "6\n7\n8\n9\n0\n", + }, + { + 20, + "", + }, + } + bufferSizes := []int{1, 2, 7, 0, len(input), len(input) + 1} + + for i, test := range tests { + for _, bufferSize := range bufferSizes { + t.Run(strconv.Itoa(i)+"_"+strconv.Itoa(bufferSize), func(t *testing.T) { + var reader io.Reader + if bufferSize == 0 { + // emulate a reader that returns EOF together with data + bufferSize = len(input) + reader = SkipHeaderLinesReader(test.skipCount, &readOnceWithEOF{strings.NewReader(input)}) + } else { + reader = SkipHeaderLinesReader(test.skipCount, strings.NewReader(input)) + } + buffer := make([]byte, bufferSize) + result := make([]byte, 0, 100) + for { + n, err := reader.Read(buffer) + if n > 0 { + result = append(result, buffer[:n]...) + } + if err != nil { + if err != io.EOF { + require.Nil(t, err.Error()) + } + break + } + } + require.Equal(t, test.result, string(result)) + }) + } + } +}