Skip to content

Commit fdca3cb

Browse files
authored
Merge pull request #15 from jf-tech/scanline
Add ios.ByteReadLine
2 parents 4a3c455 + 40b2393 commit fdca3cb

File tree

2 files changed

+82
-46
lines changed

2 files changed

+82
-46
lines changed

ios/readers.go

+29-18
Original file line numberDiff line numberDiff line change
@@ -134,39 +134,50 @@ func (r *BytesReplacingReader) Read(p []byte) (int, error) {
134134
}
135135
}
136136

137-
// ReadLine reads in a single line from a bufio.Reader.
138-
func ReadLine(r *bufio.Reader) (string, error) {
139-
// Turns out even with various bufio.Reader.Read???() and bufio.Scanner, there is not simple clean
140-
// way of reading a single text line in:
141-
// - bufio.ReadSlice('\n') doesn't have '\r' dropping. We want a line returned without neither '\n' nor '\r'.
142-
// - bufio.ReadLine() drops '\r' and '\n', but has a fixed buf so may be unable to read a whole line in one call.
143-
// - bufio.ReadBytes no buf size issue, but doesn't offer '\r' and '\n' cleanup.
137+
// ByteReadLine reads in a single line from a bufio.Reader and returns it in []byte.
138+
// Note the returned []byte may be pointing directly into the bufio.Reader, so assume
139+
// the returned []byte will be invalidated and shouldn't be used upon next ByteReadLine
140+
// call.
141+
func ByteReadLine(r *bufio.Reader) ([]byte, error) {
142+
// We want to read a line delimited by '\n' in cleanly with trailing '\r' '\n' dropped. Turns out
143+
// none of the bufio.Reader.Read???() and bufio.Scanner can do that:
144+
// - bufio.ReadSlice('\n') doesn't have '\r' dropping. Also it may need multiple calls to get a single (long) line.
145+
// - bufio.ReadLine() drops '\r' and '\n', but may need multiple calls to get a single (long) line.
146+
// - bufio.ReadBytes doesn't need multiple calls, but doesn't offer '\r' and '\n' cleanup. Also tons of allocations.
144147
// - bufio.ReadString essentially the same as bufio.ReadBytes.
145-
// - bufio.Scanner deals with '\r' and '\n' but has fixed buf issue.
146-
// Oh, come on!!
148+
// - bufio.Scanner deals with '\r' and '\n' but may need multiple calls to get a single (long) line.
147149
//
148-
// Also found net/textproto's Reader.ReadLine() which meets all the requirements. But to use it
149-
// we need to create yet another type of Reader (net.textproto.Reader), as if the
150-
// io.Reader -> bufio.Reader isn't enough for us. So decided instead, just shamelessly copy
151-
// net.textproto.Reader.ReadLine() here, credit goes to
152-
// https://github.com/golang/go/blob/master/src/net/textproto/reader.go. However its test code
153-
// coverage is lacking, so create all the new test cases for this ReadLine implementation copy.
150+
// Found net/textproto's Reader.ReadLine() which meets all the requirements. But it would become another
151+
// dependency. So decided just shamelessly copy net.textproto.Reader.ReadLine() here, credit goes to
152+
// https://github.com/golang/go/blob/master/src/net/textproto/reader.go. Its test code coverage is lacking,
153+
// so create all the new test cases for this ReadLine implementation copy.
154154
var line []byte
155155
for {
156156
l, more, err := r.ReadLine()
157157
if err != nil {
158-
return "", err
158+
return nil, err
159159
}
160160
// Avoid the copy if the first call produced a full line.
161161
if line == nil && !more {
162-
return string(l), nil
162+
line = l
163+
goto returnLine
163164
}
164165
line = append(line, l...)
165166
if !more {
166167
break
167168
}
168169
}
169-
return string(line), nil
170+
returnLine:
171+
if len(line) == 0 {
172+
return nil, nil
173+
}
174+
return line, nil
175+
}
176+
177+
// ReadLine reads in a single line from a bufio.Reader and returns it in string.
178+
func ReadLine(r *bufio.Reader) (string, error) {
179+
b, err := ByteReadLine(r)
180+
return string(b), err
170181
}
171182

172183
const bom = '\uFEFF'

ios/readers_test.go

+53-28
Original file line numberDiff line numberDiff line change
@@ -163,57 +163,82 @@ func BenchmarkRegularReader_50KBLength_1000Targets(b *testing.B) {
163163
}
164164
}
165165

166-
func TestReadLine(t *testing.T) {
166+
func TestByteReadLineAndReadLine(t *testing.T) {
167167
for _, test := range []struct {
168-
name string
169-
input string
170-
bufsize int
171-
expectedOutput []string
168+
name string
169+
input string
170+
bufsize int
171+
expected [][]byte
172172
}{
173173
{
174-
name: "empty",
175-
input: "",
176-
bufsize: 1024,
177-
expectedOutput: []string{},
174+
name: "empty",
175+
input: "",
176+
bufsize: 1024,
177+
expected: nil,
178178
},
179179
{
180-
name: "single-line with no newline",
181-
input: " word1, word2 - word3 !@#$%^&*()",
182-
bufsize: 1024,
183-
expectedOutput: []string{" word1, word2 - word3 !@#$%^&*()"},
180+
name: "single-line with no newline",
181+
input: " word1, word2 - word3 !@#$%^&*()",
182+
bufsize: 1024,
183+
expected: [][]byte{
184+
[]byte(" word1, word2 - word3 !@#$%^&*()"),
185+
},
184186
},
185187
{
186-
name: "single-line with '\\r' and '\\n'",
187-
input: "line1\r\n",
188-
bufsize: 1024,
189-
expectedOutput: []string{"line1"},
188+
name: "single-line with CR and LF",
189+
input: "line1\r\n",
190+
bufsize: 1024,
191+
expected: [][]byte{[]byte("line1")},
190192
},
191193
{
192-
name: "multi-line - bufsize enough",
193-
input: "line1\r\nline2\nline3",
194-
bufsize: 1024,
195-
expectedOutput: []string{"line1", "line2", "line3"},
194+
name: "multi-line - bufsize enough",
195+
input: "line1\r\nline2\nline3",
196+
bufsize: 1024,
197+
expected: [][]byte{[]byte("line1"), []byte("line2"), []byte("line3")},
196198
},
197199
{
198-
name: "multi-line - bufsize not enough; also empty line",
199-
input: "line1-0123456789012345\r\n\nline3-0123456789012345",
200-
bufsize: 16, // bufio.minReadBufferSize is 16.
201-
expectedOutput: []string{"line1-0123456789012345", "", "line3-0123456789012345"},
200+
name: "multi-line - bufsize not enough; also empty line",
201+
input: "line1-0123456789012345\r\n\nline3-0123456789012345",
202+
bufsize: 16, // bufio.minReadBufferSize is 16.
203+
expected: [][]byte{[]byte("line1-0123456789012345"), nil, []byte("line3-0123456789012345")},
202204
},
203205
} {
204206
t.Run(test.name, func(t *testing.T) {
205207
r := bufio.NewReaderSize(strings.NewReader(test.input), test.bufsize)
206-
output := []string{}
208+
var outputBytes [][]byte
209+
for {
210+
line, err := ByteReadLine(r)
211+
if err != nil {
212+
assert.Nil(t, line)
213+
assert.Equal(t, io.EOF, err)
214+
break
215+
}
216+
if line != nil {
217+
// note the []byte returned by ByteReadLine can be invalidated upon next call, so
218+
// let's make a copy of it, thus the seemingly unnecessary `[]byte(string(line))`.
219+
outputBytes = append(outputBytes, []byte(string(line)))
220+
} else {
221+
outputBytes = append(outputBytes, line)
222+
}
223+
}
224+
assert.Equal(t, test.expected, outputBytes)
225+
226+
r = bufio.NewReaderSize(strings.NewReader(test.input), test.bufsize)
227+
outputBytes = outputBytes[:0]
207228
for {
208229
line, err := ReadLine(r)
209230
if err != nil {
210231
assert.Equal(t, "", line)
211232
assert.Equal(t, io.EOF, err)
212233
break
213234
}
214-
output = append(output, line)
235+
if line == "" {
236+
outputBytes = append(outputBytes, nil)
237+
} else {
238+
outputBytes = append(outputBytes, []byte(line))
239+
}
215240
}
216-
assert.Equal(t, test.expectedOutput, output)
241+
assert.Equal(t, test.expected, outputBytes)
217242
})
218243
}
219244
}

0 commit comments

Comments
 (0)