First version
diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c56069f --- /dev/null +++ b/.gitignore
@@ -0,0 +1 @@ +*.test \ No newline at end of file
diff --git a/README.md b/README.md new file mode 100644 index 0000000..f7b469a --- /dev/null +++ b/README.md
@@ -0,0 +1,59 @@ +# buffruneio + +Buffruneio is a wrapper around bufio to provide buffered runes access with +unlimited unreads. + +```go +import "github.com/pelletier/go-buffruneio" +``` + +## Examples + +```go +import ( + "fmt" + "github.com/pelletier/go-buffruneio" + "strings" +) + +reader := buffruneio.NewReader(strings.NewReader("abcd")) +fmt.Println(reader.ReadRune()) // 'a' +fmt.Println(reader.ReadRune()) // 'b' +fmt.Println(reader.ReadRune()) // 'c' +reader.UnreadRune() +reader.UnreadRune() +fmt.Println(reader.ReadRune()) // 'b' +fmt.Println(reader.ReadRune()) // 'c' +``` + +## Documentation + +The documentation and additional examples are available at +[godoc.org](http://godoc.org/github.com/pelletier/go-buffruneio). + +## Contribute + +Feel free to report bugs and patches using GitHub's pull requests system on +[pelletier/go-toml](https://github.com/pelletier/go-buffruneio). Any feedback is +much appreciated! + +## LICENSE + +Copyright (c) 2016 Thomas Pelletier + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
diff --git a/buffruneio.go b/buffruneio.go new file mode 100644 index 0000000..9e47ca2 --- /dev/null +++ b/buffruneio.go
@@ -0,0 +1,85 @@ +// Package buffruneio is a wrapper around bufio to provide buffered runes access with unlimited unreads. +package buffruneio + +import ( + "bufio" + "container/list" + "errors" + "io" +) + +// Rune to indicate end of file. +const ( + EOF = -(iota + 1) +) + +// ErrNoRuneToUnread is returned by UnreadRune() when the read index is already at the beginning of the buffer. +var ErrNoRuneToUnread = errors.New("no rune to unwind") + +// Reader implements runes buffering for an io.Reader object. +type Reader struct { + buffer *list.List + current *list.Element + input *bufio.Reader +} + +// NewReader returns a new Reader. +func NewReader(rd io.Reader) *Reader { + return &Reader{ + buffer: list.New(), + input: bufio.NewReader(rd), + } +} + +func (rd *Reader) feedBuffer() error { + r, _, err := rd.input.ReadRune() + + if err != nil { + if err != io.EOF { + return err + } + r = EOF + } + + rd.buffer.PushBack(r) + if rd.current == nil { + rd.current = rd.buffer.Back() + } + return nil +} + +// ReadRune reads the next rune from buffer, or from the underlying reader if needed. +func (rd *Reader) ReadRune() (rune, error) { + if rd.current == rd.buffer.Back() || rd.current == nil { + err := rd.feedBuffer() + if err != nil { + return EOF, err + } + } + + r := rd.current.Value + rd.current = rd.current.Next() + return r.(rune), nil +} + +// UnreadRune pushes back the previously read rune in the buffer, extending it if needed. +func (rd *Reader) UnreadRune() error { + if rd.current == rd.buffer.Front() { + return ErrNoRuneToUnread + } + if rd.current == nil { + rd.current = rd.buffer.Back() + } else { + rd.current = rd.current.Prev() + } + return nil +} + +// Forget removes runes stored before the current stream position index. +func (rd *Reader) Forget() { + if rd.current == nil { + rd.current = rd.buffer.Back() + } + for ; rd.current != rd.buffer.Front(); rd.buffer.Remove(rd.current.Prev()) { + } +}
diff --git a/buffruneio_test.go b/buffruneio_test.go new file mode 100644 index 0000000..95946b6 --- /dev/null +++ b/buffruneio_test.go
@@ -0,0 +1,84 @@ +package buffruneio + +import ( + "strings" + "testing" +) + +func assumeRune(t *testing.T, rd *Reader, r rune) { + gotRune, err := rd.ReadRune() + if err != nil { + t.Fatal("unexpected error", err) + } + if gotRune != r { + t.Fatal("got", string(gotRune), + "(", []byte(string(gotRune)), ")", + "expected", string(r), + "(", []byte(string(r)), ")") + } +} + +func TestReadString(t *testing.T) { + s := "hello" + rd := NewReader(strings.NewReader(s)) + + assumeRune(t, rd, 'h') + assumeRune(t, rd, 'e') + assumeRune(t, rd, 'l') + assumeRune(t, rd, 'l') + assumeRune(t, rd, 'o') + assumeRune(t, rd, EOF) +} + +func TestMultipleEOF(t *testing.T) { + s := "" + rd := NewReader(strings.NewReader(s)) + + assumeRune(t, rd, EOF) + assumeRune(t, rd, EOF) +} + +func TestUnread(t *testing.T) { + s := "ab" + rd := NewReader(strings.NewReader(s)) + + assumeRune(t, rd, 'a') + assumeRune(t, rd, 'b') + rd.UnreadRune() + assumeRune(t, rd, 'b') + assumeRune(t, rd, EOF) +} + +func TestUnreadEOF(t *testing.T) { + s := "" + rd := NewReader(strings.NewReader(s)) + + rd.UnreadRune() + assumeRune(t, rd, EOF) + assumeRune(t, rd, EOF) + rd.UnreadRune() + assumeRune(t, rd, EOF) +} + +func TestForget(t *testing.T) { + s := "hello" + rd := NewReader(strings.NewReader(s)) + + assumeRune(t, rd, 'h') + assumeRune(t, rd, 'e') + assumeRune(t, rd, 'l') + assumeRune(t, rd, 'l') + rd.Forget() + if rd.UnreadRune() != ErrNoRuneToUnread { + t.Fatal("no rune should be available") + } +} + +func TestForgetEmpty(t *testing.T) { + s := "" + rd := NewReader(strings.NewReader(s)) + + rd.Forget() + assumeRune(t, rd, EOF) + rd.Forget() +}