File: ./constants.go
   1 package ioplus
   2 
   3 var (
   4     spaces = [256]byte{
   5         32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32,
   6         32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32,
   7         32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32,
   8         32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32,
   9         32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32,
  10         32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32,
  11         32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32,
  12         32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32,
  13         32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32,
  14         32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32,
  15         32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32,
  16         32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32,
  17         32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32,
  18         32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32,
  19         32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32,
  20         32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32,
  21     }
  22 
  23     tabs = [256]byte{
  24         9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9,
  25         9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9,
  26         9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9,
  27         9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9,
  28         9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9,
  29         9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9,
  30         9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9,
  31         9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9,
  32         9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9,
  33         9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9,
  34         9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9,
  35         9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9,
  36         9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9,
  37     }
  38 
  39     nulls = [256]byte{
  40         0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
  41         0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
  42         0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
  43         0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
  44         0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
  45         0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
  46         0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
  47         0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
  48         0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
  49         0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
  50         0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
  51         0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
  52         0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
  53     }
  54 
  55     fulls = [256]byte{
  56         255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
  57         255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
  58         255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
  59         255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
  60         255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
  61         255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
  62         255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
  63         255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
  64         255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
  65         255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
  66         255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
  67         255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
  68         255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
  69         255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
  70         255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
  71         255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
  72         255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
  73         255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
  74         255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
  75         255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
  76         255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
  77         255, 255, 255, 255,
  78     }
  79 )

     File: ./doc.go
   1 /*
   2 # ioplus
   3 
   4 A few extras which should arguably be part of go's io stdlib package.
   5 */
   6 package ioplus

     File: ./go.mod
   1 module ioplus
   2 
   3 go 1.16

     File: ./null.go
   1 package ioplus
   2 
   3 import "io"
   4 
   5 // Null is like /dev/null and generalizes io.Discard from the stdlib, being
   6 // an io.ReadWriter. As a reader, it's an empty source of data, while as a
   7 // writer it ignores everyting it's given. Its methods do nothing and always
   8 // succeed: for method Read, success means returning 0 and io.EOF.
   9 var Null io.ReadWriter = nullIO{}
  10 
  11 // nullIO is a no-action io.ReadWriter, and a generalization of the stdlib's
  12 // io.Discard: using it, no data are ever read/written
  13 type nullIO struct{}
  14 
  15 // Read implements io.Reader, while doing nothing.
  16 func (nio nullIO) Read(p []byte) (n int, err error) {
  17     return 0, io.EOF
  18 }
  19 
  20 // Write implements io.Writer, while doing nothing.
  21 func (nio nullIO) Write(p []byte) (n int, err error) {
  22     return 0, nil
  23 }
  24 
  25 // NullReader is an io.Reader which never reads anything: you can think of
  26 // it as a counterpart to io.Discard, the do-nothing writer.
  27 var NullReader io.Reader = emptyReader{}
  28 
  29 // emptyReader is a reader which never reads anything
  30 type emptyReader struct{}
  31 
  32 // Read implements io.Reader, while doing nothing.
  33 func (r emptyReader) Read(p []byte) (n int, err error) {
  34     return 0, io.EOF
  35 }

     File: ./numbers.go
   1 package ioplus
   2 
   3 import (
   4     "bytes"
   5     "io"
   6     "math"
   7     "strconv"
   8 )
   9 
  10 // WriteNiceInt64 lets you format an integer value to be more readable,
  11 // by separating 3-digit groups using commas, underscores, or some other
  12 // ASCII symbol of your choice, passed as the separator-byte argument.
  13 //
  14 // For example, to emit bytes "-12,345,678", you can call this func as
  15 //
  16 //  ioplus.WriteNiceInt64(w, -12_345_678, ',')
  17 func WriteNiceInt64(w io.Writer, i int64, sep byte) (n int, err error) {
  18     // Given an int64
  19     // - there's possibly a leading negative-sign byte (+1)
  20     // - every 3 digits there's an extra separator byte (4/3)
  21     // - 63 bits of range are available in either direction (log10(2**63))
  22     //
  23     // echo 'scale=10; 1 + 4/3 * (l(2^63)/l(10))' | bc -l
  24     // gives 26.2865196360, which means up to 27 bytes are needed for
  25     // the digits, which becomes up to 28 when counting negative signs
  26     var buf [32]byte
  27     return w.Write(appendNiceInt64(buf[:0], i, sep))
  28 }
  29 
  30 // appendNiceInt64 is similar to WriteNiceInt64, except it appends the result
  31 // to the byte-slice given, instead of emitting to an io.Writer
  32 func appendNiceInt64(buf []byte, n int64, sep byte) []byte {
  33     // handle negative numbers
  34     if n < 0 {
  35         buf = append(buf, '-')
  36         n = -n
  37     }
  38     return appendNiceUint64(buf, uint64(n), sep)
  39 }
  40 
  41 // appendNiceUint64 is the uint64 counterpart of func appendNiceInt64
  42 func appendNiceUint64(buf []byte, n uint64, sep byte) []byte {
  43     loopThousandsGroups(uint64(n), func(i int, chunk []byte) {
  44         if i == 0 {
  45             // the 1st digit-group is the only one which isn't zero-padded
  46             // and which isn't preceded by the ASCII separator given
  47             buf = append(buf, chunk...)
  48             return
  49         }
  50 
  51         // separate thousands-groups with the ASCII symbol given
  52         buf = append(buf, sep)
  53 
  54         // emit all digit-groups after the first
  55         buf = append(buf, chunk...)
  56     })
  57 
  58     return buf
  59 }
  60 
  61 // dotZeroDecimals lets func WriteNiceFloat64 handle a special case
  62 var dotZeroDecimals = [17]byte{
  63     '.',
  64     '0', '0', '0', '0', '0', '0', '0', '0',
  65     '0', '0', '0', '0', '0', '0', '0', '0',
  66 }
  67 
  68 // WriteNiceFloat64 is the counterpart of func WriteNiceInt64, except it's
  69 // for float64 values: that's why it also requires a precision argument, to
  70 // specify the number of decimal digits to emit.
  71 //
  72 // For example, to emit bytes "-12,345,678.123", you can call this func as
  73 //
  74 //  ioplus.WriteNiceFloat64(w, -12_345_678.123, ',', 3)
  75 func WriteNiceFloat64(w io.Writer, f float64, sep byte, prec int) (n int, err error) {
  76     if prec > 16 {
  77         prec = 16
  78     }
  79 
  80     // avoid writing -0 when the sign bit is on
  81     if f == 0 {
  82         n1, err := w.Write([]byte{'0'})
  83         if err != nil {
  84             return n1, err
  85         }
  86         if prec < 1 {
  87             return n1, err
  88         }
  89         n2, err := w.Write(dotZeroDecimals[:prec+1])
  90         return n1 + n2, err
  91     }
  92 
  93     // avoid emitting a non-sense integer part for special float64 values
  94     if math.IsNaN(f) {
  95         return w.Write([]byte{'N', 'a', 'N'})
  96     }
  97     if math.IsInf(f, -1) {
  98         return w.Write([]byte{'-', 'I', 'n', 'f'})
  99     }
 100     if math.IsInf(f, +1) {
 101         return w.Write([]byte{'+', 'I', 'n', 'f'})
 102     }
 103 
 104     // emit the digit-grouped integer part
 105     n1, err := WriteNiceInt64(w, int64(f), sep)
 106     if err != nil {
 107         return n1, err
 108     }
 109 
 110     // see if any decimals need be emitted
 111     if prec < 1 {
 112         return n1, err
 113     }
 114 
 115     // emit the decimal part
 116     var decbuf [32]byte
 117     decstr := strconv.AppendFloat(decbuf[:0], f, 'f', prec, 64)
 118     i := bytes.IndexByte(decstr, '.')
 119     if i < 0 {
 120         // there are no decimal digits
 121         n2, err := w.Write(dotZeroDecimals[:prec+1])
 122         return n1 + n2, err
 123     }
 124 
 125     // emit decimal digits, starting from the dot
 126     n2, err := w.Write(decstr[i:])
 127     return n1 + n2, err
 128 }
 129 
 130 // WriteNiceInt64 lets you format an integer value to be more readable,
 131 // by separating 3-digit groups using commas, underscores, or some other
 132 // ASCII symbol of your choice, passed as the separator-byte argument.
 133 //
 134 // For example, to emit bytes "12,345,678", you can call this func as
 135 //
 136 //  ioplus.WriteNiceUint64(w, 12_345_678, ',')
 137 func WriteNiceUint64(w io.Writer, i uint64, sep byte) (n int, err error) {
 138     // 32 bytes are more than enough for uint64; see comment in func
 139     // WriteNiceInt64 for similar reasoning behind this conclusion
 140     var buf [32]byte
 141     return w.Write(appendNiceUint64(buf[:0], i, sep))
 142 }
 143 
 144 // loopThousandsGroups chunks a byte-string representation of the integer
 145 // given into 3-digits groups, possibly except for the first one, since that
 146 // one may have fewer than 3 digits
 147 func loopThousandsGroups(n uint64, fn func(i int, digits []byte)) {
 148     i := 0
 149     var buf [32]byte
 150     s := strconv.AppendUint(buf[:0], n, 10)
 151 
 152     lead := len(s) % 3
 153     if lead > 0 {
 154         fn(i, s[:lead])
 155         s = s[lead:]
 156         i++
 157     }
 158 
 159     for ; len(s) > 0; i++ {
 160         fn(i, s[:3])
 161         s = s[3:]
 162     }
 163 }

     File: ./numbers_test.go
   1 package ioplus
   2 
   3 import (
   4     "strconv"
   5     "strings"
   6     "testing"
   7 )
   8 
   9 func TestLoopThousandsGroups(t *testing.T) {
  10     var tests = []struct {
  11         Input    uint64
  12         Expected []int
  13     }{
  14         {0, []int{0}},
  15         {999, []int{999}},
  16         {1_670, []int{1, 670}},
  17         {3_490, []int{3, 490}},
  18         {12_332, []int{12, 332}},
  19         {999_999, []int{999, 999}},
  20         {1_000_000, []int{1, 0, 0}},
  21         {1_000_001, []int{1, 0, 1}},
  22         {1_234_567, []int{1, 234, 567}},
  23     }
  24 
  25     for _, tc := range tests {
  26         count := 0
  27         loopThousandsGroups(tc.Input, func(i int, chunk []byte) {
  28             // t.Log(tc.Input, i, n)
  29             n, err := strconv.Atoi(string(chunk))
  30             if err != nil {
  31                 t.Error(err)
  32             }
  33             if n != tc.Expected[i] {
  34                 const fs = "group %d in %d: got %d instead of %d"
  35                 t.Errorf(fs, i, tc.Input, n, tc.Expected[i])
  36             }
  37             count++
  38         })
  39 
  40         if count != len(tc.Expected) {
  41             const fs = "thousands-groups from %d: got %d instead of %d"
  42             t.Errorf(fs, tc.Input, count, len(tc.Expected))
  43         }
  44     }
  45 }
  46 
  47 func TestWriteNiceInt(t *testing.T) {
  48     var tests = []struct {
  49         Input    int64
  50         Expected string
  51     }{
  52         {0, `0`},
  53         {-32, `-32`},
  54         {999, `999`},
  55         {1_670, `1,670`},
  56         {3_490, `3,490`},
  57         {12_332, `12,332`},
  58         {999_999, `999,999`},
  59         {1_000_000, `1,000,000`},
  60         {1_000_001, `1,000,001`},
  61         {1_234_567, `1,234,567`},
  62     }
  63 
  64     for _, tc := range tests {
  65         t.Run(tc.Expected, func(t *testing.T) {
  66             var sb strings.Builder
  67             WriteNiceInt64(&sb, tc.Input, ',')
  68             got := sb.String()
  69 
  70             if got != tc.Expected {
  71                 const fs = "got %q instead of %q"
  72                 t.Errorf(fs, got, tc.Expected)
  73             }
  74         })
  75     }
  76 }

     File: ./readers.go
   1 package ioplus
   2 
   3 import (
   4     "bytes"
   5     "io"
   6 )
   7 
   8 // Read calls an io.Reader's Read method but never returns io.EOF along with
   9 // a positive size. In other words, this func will return an io.EOF error only
  10 // with a zero/negative size.
  11 // func Read(r io.Reader, p []byte) (n int, err error) {
  12 //  n, err = r.Read(p)
  13 //  if n > 0 && err == io.EOF {
  14 //      return n, nil
  15 //  }
  16 //  return n, err
  17 // }
  18 
  19 // ReadNoEOF calls an io.Reader's Read method but never returns io.EOF as the
  20 // error: the only way this func's callers can detect io.EOF-type situations
  21 // is by checking if 0 bytes were read, using the first return value.
  22 func ReadNoEOF(r io.Reader, p []byte) (n int, err error) {
  23     n, err = r.Read(p)
  24     if err == io.EOF {
  25         return n, nil
  26     }
  27     return n, err
  28 }
  29 
  30 // Skip tries to advance a reader by the number of bytes given, ignoring all
  31 // data read in the process. Fewer bytes may be skipped than asked for.
  32 func Skip(r io.Reader, n int) (skipped int, err error) {
  33     skipped = 0
  34     var dummy [64]byte
  35 
  36     for n > len(dummy) {
  37         got, err := r.Read(dummy[:])
  38         if got < len(dummy) || err != nil {
  39             skipped += got
  40             return skipped, err
  41         }
  42 
  43         n -= len(dummy)
  44         skipped += len(dummy)
  45     }
  46 
  47     if n > 0 {
  48         got, err := r.Read(dummy[:n])
  49         skipped += got
  50         return skipped, err
  51     }
  52     return skipped, nil
  53 }
  54 
  55 // UnixLinesSplitFunc lets you get any final carriage-returns when splitting
  56 // lines via bufio.Scanner; just give this function to your scanner's Split
  57 // method.
  58 //
  59 // # Examples
  60 //
  61 // sc := bufio.NewScanner(f)
  62 // sc.Split(ioplus.UnixLinesSplitFunc)
  63 // // now you can detect windows-style lines, by checking if lines end in "\r"
  64 func UnixLinesSplitFunc(b []byte, eof bool) (int, []byte, error) {
  65     if eof && len(b) == 0 {
  66         return 0, nil, nil
  67     }
  68 
  69     i := bytes.IndexByte(b, '\n')
  70     if i >= 0 {
  71         return i + 1, b[0:i], nil
  72     }
  73 
  74     // last line
  75     if eof {
  76         return len(b), b, nil
  77     }
  78     return 0, nil, nil
  79 }
  80 
  81 const (
  82     startBufferSize = 4 * 1024
  83 )
  84 
  85 // Chunker is similar to bufio.Scanner, but has no max buffer size, which
  86 // means it reallocates its buffer as needed to get the next full chunk.
  87 //
  88 // To use, call NewChunker to make a new instance, then keep calling func
  89 // Chunk on it, as if it were func Read on its io.Reader, the difference
  90 // being its first returned value is the chunk itself, instead of its size.
  91 type Chunker struct {
  92     // the data source
  93     r io.Reader
  94 
  95     // the allocated buffer where all bytes read end up
  96     buf []byte
  97 
  98     // the leftover/unused bytes from the previous read,
  99     // as a subslice of the main buffer
 100     cur []byte
 101 }
 102 
 103 // NewChunker is the constructor for type Chunker.
 104 func NewChunker(r io.Reader) Chunker {
 105     return Chunker{r: r}
 106 }
 107 
 108 // Buffer lets you assign a preallocated buffer before chunking.
 109 func (c *Chunker) Buffer(buf []byte) {
 110     c.buf = buf
 111 }
 112 
 113 // Chunk gets the next chunk from the reader. The chunk ends with the byte
 114 // given, or the end of the input itself: either way, the chunk delimiter is
 115 // never part of the slices returned.
 116 func (c *Chunker) Chunk(delim byte) (chunk []byte, err error) {
 117     return c.ChunkFunc(func(buf []byte) int {
 118         return bytes.IndexByte(buf, delim)
 119     })
 120 }
 121 
 122 // ChunkFunc gets the next chunk from the reader. The chunk ends at the index
 123 // calculated using the func given, or the end of the input itself: either way,
 124 // the bytes at those indices delimiter are never part of the slices returned.
 125 //
 126 // The func given must return a negative value to signal no end-index for the
 127 // current chunk was found.
 128 //
 129 // # Example
 130 //
 131 //  ch := ioplus.NewChunker(r)
 132 //  ...
 133 //  // get the next line as a byte-slice
 134 //  line, err := c.ChunkFunc(func(buf []byte) int {
 135 //      return bytes.IndexByte(buf, '\n')
 136 //  })
 137 //  // ignore trailing carriage returns
 138 //  line = bytes.TrimSuffix(line, []byte{'\r'})
 139 func (c *Chunker) ChunkFunc(f func(buf []byte) int) (chunk []byte, err error) {
 140     // before reading any bytes, try checking any leftovers
 141     i := f(c.cur)
 142     if i >= 0 {
 143         chunk = c.cur[:i]
 144         c.cur = c.cur[i+1:]
 145         return chunk, nil
 146     }
 147 
 148     for {
 149         // reallocates larger buffer and reads more bytes at its end
 150         n, err := c.readExtend()
 151         if err != nil {
 152             return c.buf[:n], err
 153         }
 154 
 155         // try to find the delimiter in the new extended buffer
 156         i := f(c.buf[:n])
 157         if i >= 0 {
 158             chunk = c.buf[:i]
 159             c.cur = c.buf[i+1:]
 160             return chunk, nil
 161         }
 162     }
 163 }
 164 
 165 // readExtend extends the internal buffer ands reads more bytes into the end
 166 // of the enlarged buffer space, to avoid overwriting existing content
 167 func (c *Chunker) readExtend() (newlen int, err error) {
 168     if len(c.buf) > 0 {
 169         c.buf = make([]byte, 2*len(c.buf))
 170     } else {
 171         c.buf = make([]byte, startBufferSize)
 172     }
 173 
 174     // copy leftover bytes into the first part of new buffer
 175     i := len(c.cur)
 176     copy(c.buf[:i], c.cur)
 177     c.cur = nil
 178 
 179     // read new bytes into the latter part of new buffer
 180     n, err := c.r.Read(c.buf[i : len(c.buf)-i])
 181     return i + n, err
 182 }

     File: ./writers.go
   1 package ioplus
   2 
   3 import (
   4     "bufio"
   5     "bytes"
   6     "io"
   7     "strconv"
   8     "strings"
   9     "time"
  10 )
  11 
  12 // WriteSpaces writes the given number of spaces, minimizing calls to Write.
  13 func WriteSpaces(w io.Writer, count int) (n int, err error) {
  14     return RingWrite(w, count, spaces[:])
  15 }
  16 
  17 // WriteTabs writes the given number of tabs, minimizing calls to Write.
  18 func WriteTabs(w io.Writer, count int) (n int, err error) {
  19     return RingWrite(w, count, tabs[:])
  20 }
  21 
  22 // WriteNulls writes the given number of null (all bits off) bytes, minimizing
  23 // calls to Write.
  24 func WriteNulls(w io.Writer, count int) (n int, err error) {
  25     return RingWrite(w, count, nulls[:])
  26 }
  27 
  28 // WriteFulls writes the given number of full (all bits on) bytes, minimizing
  29 // calls to Write.
  30 func WriteFulls(w io.Writer, count int) (n int, err error) {
  31     return RingWrite(w, count, fulls[:])
  32 }
  33 
  34 // RingWrite keeps emitting bytes from the buffer given in ring-like/circular
  35 // fashion, until it reaches the byte-count given: when the buffer given has
  36 // the same repeating byte/byte-pattern, this func lets you use it to minimize
  37 // calls to Write.
  38 //
  39 // An empty buffer results in no calls to Write, no matter the count given.
  40 func RingWrite(w io.Writer, count int, buf []byte) (n int, err error) {
  41     // avoid an infinite loop when counter is 0 and buffer is empty
  42     if count <= 0 || len(buf) == 0 {
  43         return 0, nil
  44     }
  45 
  46     // emit all full-buffer writes
  47     for l := len(buf); count >= l; count -= l {
  48         m, err := w.Write(buf)
  49         n += m
  50 
  51         if err != nil {
  52             return n, err
  53         }
  54     }
  55 
  56     // emit any remainder bytes
  57     if count > 0 {
  58         m, err := w.Write(buf[:count])
  59         return n + m, err
  60     }
  61     return n, nil
  62 }
  63 
  64 // WriteTimeYMDHMS emits a time value using a common rendition of its main
  65 // date/time components.
  66 func WriteTimeYMDHMS(w io.Writer, t time.Time) (n int, err error) {
  67     var buf [32]byte
  68     s := t.AppendFormat(buf[:0], "2006-01-02 15:04:05")
  69     return w.Write(s)
  70 }
  71 
  72 // Flusher represents presumably-buffered types which enable their explicit
  73 // flushing. A commonly-used such type is bufio.Writer from the stdlib.
  74 type Flusher interface {
  75     Flush() error
  76 }
  77 
  78 // WriteFlusher is a Flusher which can also write data.
  79 type WriteFlusher interface {
  80     io.Writer
  81     Flusher
  82 }
  83 
  84 // WriteFlushCloser is a Flusher which can also write data, and can be closed.
  85 type WriteFlushCloser interface {
  86     io.WriteCloser
  87     Flusher
  88 }
  89 
  90 // ReadWriteFlushCloser implements io methods Read, Write, Flush, and Close.
  91 type ReadWriteFlushCloser interface {
  92     io.ReadWriteCloser
  93     Flusher
  94 }
  95 
  96 // Flush calls method Flush only if the writer given is a Flusher.
  97 func Flush(w io.Writer) error {
  98     if f, ok := w.(Flusher); ok {
  99         return f.Flush()
 100     }
 101     return nil
 102 }
 103 
 104 // WriteTracker is an io.Writer-wrapper which remembers how many bytes it's
 105 // ever written. The first error it encounters disables it, preventing it
 106 // from writing anything ever again.
 107 type WriteTracker struct {
 108     dest io.Writer
 109     err  error
 110     n    int
 111 }
 112 
 113 // NewWriteTracker is the constuctor for WriteTracker, taking a destination
 114 // Writer as its only argument.
 115 func NewWriteTracker(w io.Writer) *WriteTracker {
 116     return &WriteTracker{dest: w}
 117 }
 118 
 119 // Len returns how many bytes this writer has ever written up to now.
 120 func (wt *WriteTracker) Len() int {
 121     return wt.n
 122 }
 123 
 124 // Err returns the first error encountered, or nil if all is still good.
 125 func (wt *WriteTracker) Err() error {
 126     return wt.err
 127 }
 128 
 129 // Write implements the writing/tracking part: remember that previous errors
 130 // disable any further writing.
 131 func (wt *WriteTracker) Write(buf []byte) (n int, err error) {
 132     if err := wt.err; err != nil {
 133         return 0, err
 134     }
 135 
 136     m, err := wt.dest.Write(buf)
 137     wt.n += m
 138     if err != nil {
 139         wt.err = err
 140     }
 141     return m, err
 142 }
 143 
 144 // Flush flushes the underlying writer, if it can be flushed.
 145 func (wt *WriteTracker) Flush() error {
 146     if fl, ok := wt.dest.(Flusher); ok {
 147         return fl.Flush()
 148     }
 149     return nil
 150 }
 151 
 152 // Close closes the underlying writer, if it can be closed.
 153 func (wt *WriteTracker) Close() error {
 154     if cl, ok := wt.dest.(io.Closer); ok {
 155         return cl.Close()
 156     }
 157     return nil
 158 }
 159 
 160 // nopFlusher is a writer with an added Flush method which does nothing, but
 161 // still implements the WriteFlusher interface
 162 type nopFlusher struct {
 163     w io.Writer
 164 }
 165 
 166 // Write implements interface io.Writer, delegating to the wrapped writer.
 167 func (w nopFlusher) Write(p []byte) (n int, err error) {
 168     return w.w.Write(p)
 169 }
 170 
 171 // Flush implements interfaces Flusher and WriteFlusher, while doing nothing.
 172 func (w nopFlusher) Flush() error {
 173     return nil
 174 }
 175 
 176 // NopFlusher wraps a writer into a WriteFlusher, whose method Flush does
 177 // nothing. Ensure the Writer given isn't flushable, unless you want to
 178 // lose its advertised ability/need to flush results, which may indeed be
 179 // what you temporarily want/need.
 180 func NopFlusher(w io.Writer) WriteFlusher {
 181     return nopFlusher{w: w}
 182 }
 183 
 184 // ValueWriter is both an io.Writer, as well as an io.StringWriter, and lets
 185 // you avoid type-dispatching via the likes of fmt.Fprint, by using Write-like
 186 // funcs for commonly used types.
 187 //
 188 // Using it leads to code which in practice is both easier to read, as well
 189 // as (often) noticeably faster when emitting lots of output. This is because
 190 // you're avoiding the extra levels of indirection and reflective dispatching
 191 // of funcs from the fmt package, which are normally used instead.
 192 //
 193 // Concrete values matching this interface are *bufio.Writer from the stdlib,
 194 // and this package's own LineFlusher type.
 195 type ValueWriter interface {
 196     // io.Writer has func Write(p []byte) (n int, err error)
 197     io.Writer
 198 
 199     // io.StringWriter has func WriteString(s string) (n int, err error)
 200     io.StringWriter
 201 }
 202 
 203 // NewValueWriter is an adapter from any io.Writer into a ValueWriter.
 204 // func NewValueWriter(w io.Writer) ValueWriter {
 205 //  if lf, ok := w.(LineFlusher); ok {
 206 //      return lf
 207 //  }
 208 //  return bufio.NewWriter(w)
 209 // }
 210 
 211 // WriteFloat64 does what it says, letting you give the number of decimal
 212 // digits you want: when negative, this value means use as many decimals
 213 // as automatically deemed appropriate.
 214 func WriteFloat64(w ValueWriter, f float64, decimals int) (n int, err error) {
 215     var buf [32]byte
 216     return w.Write(strconv.AppendFloat(buf[:0], f, 'f', decimals, 64))
 217 }
 218 
 219 // WriteInt64 does what it says.
 220 func WriteInt64(w ValueWriter, i int64) (n int, err error) {
 221     var buf [32]byte
 222     return w.Write(strconv.AppendInt(buf[:0], i, 10))
 223 }
 224 
 225 // WriteUint64 does what it says.
 226 func WriteUint64(w ValueWriter, i uint64) (n int, err error) {
 227     var buf [32]byte
 228     return w.Write(strconv.AppendUint(buf[:0], i, 10))
 229 }
 230 
 231 // LineFlusher is a buffered-writer wrapper which automatically flushes when
 232 // any line-feeds are given to it to write; you still need to ensure func
 233 // Flush is called after you're done with output, to ensure all output bytes
 234 // are actually sent to their ultimate destination.
 235 //
 236 // This type is an io.Writer, a io.StringWriter, an ioplus.Flusher, and even
 237 // an ioplus.ValueWriter.
 238 //
 239 // This type offers an interesting/reasonable compromise between efficiency
 240 // and liveness of output, as it seems to give enough of both in practice.
 241 //
 242 // Compare it both to the fully-buffered *bufio.Writer, which minimizes OS
 243 // data-transfer requests for efficiency at the cost of preventing liveness
 244 // of output, and to writing directly to unbuffered destinations, which is
 245 // much less efficient but provides fully-live output.
 246 //
 247 // Note: this type seems to provide speed-ups over directly using the fmt
 248 // funcs on unbuffered writers, even when output lines are very short, thus
 249 // forcing frequent flushing.
 250 type LineFlusher struct {
 251     bw *bufio.Writer
 252 }
 253 
 254 // NewLineFlusher is the main constructor for type LineFlusher.
 255 func NewLineFlusher(w io.Writer) LineFlusher {
 256     if lf, ok := w.(LineFlusher); ok {
 257         return lf
 258     }
 259     return LineFlusher{bufio.NewWriter(w)}
 260 }
 261 
 262 // NewLineFlusherSize is another constructor for type LineFlusher, which
 263 // lets you ask for a minimum buffer size.
 264 func NewLineFlusherSize(w io.Writer, size int) LineFlusher {
 265     if lf, ok := w.(LineFlusher); ok {
 266         return LineFlusher{bufio.NewWriterSize(lf.bw, size)}
 267     }
 268     return LineFlusher{bufio.NewWriterSize(w, size)}
 269 }
 270 
 271 // Write implements the io.Writer interface, and automatically flushes the
 272 // internal buffer whenever it's given line-feeds, which is the whole point
 273 // of this custom writer type.
 274 func (lf LineFlusher) Write(p []byte) (int, error) {
 275     // not flushing lines when fewer than 64 bytes are waiting doesn't seem
 276     // to speed things up too much, even when being repeatedly given very
 277     // short output lines: best to keep line-updates as lively as possible,
 278     // which means flushing every time line-feeds are `written`
 279     if bytes.IndexByte(p, '\n') >= 0 {
 280         defer lf.bw.Flush()
 281     }
 282     return lf.bw.Write(p)
 283 }
 284 
 285 // WriteFloat64 does what it says, letting you give the number of decimal
 286 // digits you want: when negative, this value means use as many decimals
 287 // as automatically deemed appropriate.
 288 func (lf LineFlusher) WriteFloat64(f float64, decimals int) (n int, err error) {
 289     var buf [32]byte
 290     return lf.bw.Write(strconv.AppendFloat(buf[:0], f, 'f', decimals, 64))
 291 }
 292 
 293 // WriteInt64 does what it says.
 294 func (lf LineFlusher) WriteInt64(i int64) (n int, err error) {
 295     var buf [32]byte
 296     return lf.bw.Write(strconv.AppendInt(buf[:0], i, 10))
 297 }
 298 
 299 // WriteString implements the io.StringWriter interface, and automatically
 300 // flushes the internal buffer whenever it's given line-feeds, which is the
 301 // whole point of this custom writer type.
 302 func (lf LineFlusher) WriteString(s string) (int, error) {
 303     // not flushing lines when fewer than 64 bytes are waiting doesn't seem
 304     // to speed things up too much, even when being repeatedly given very
 305     // short output lines: best to keep line-updates as lively as possible,
 306     // which means flushing every time line-feeds are `written`
 307     if strings.IndexByte(s, '\n') >= 0 {
 308         defer lf.bw.Flush()
 309     }
 310     return lf.bw.WriteString(s)
 311 }
 312 
 313 // WriteUint64 does what it says.
 314 func (lf LineFlusher) WriteUint64(i uint64) (n int, err error) {
 315     var buf [32]byte
 316     return lf.bw.Write(strconv.AppendUint(buf[:0], i, 10))
 317 }
 318 
 319 // Flush does what it says, and implements the flusher interface.
 320 func (lf LineFlusher) Flush() error {
 321     return lf.bw.Flush()
 322 }

     File: ./writers_test.go
   1 package ioplus
   2 
   3 import (
   4     "strconv"
   5     "strings"
   6     "testing"
   7 )
   8 
   9 func TestRingWrite(t *testing.T) {
  10     for n := -10; n < 3*len(spaces); n++ {
  11         t.Run(strconv.Itoa(n)+` spaces`, func(t *testing.T) {
  12             testRingWrite(t, n, spaces[:])
  13         })
  14     }
  15 
  16     for n := -10; n < 3*len(tabs); n++ {
  17         t.Run(strconv.Itoa(n)+` tabs`, func(t *testing.T) {
  18             testRingWrite(t, n, tabs[:])
  19         })
  20     }
  21 
  22     for n := -10; n < 3*len(nulls); n++ {
  23         t.Run(strconv.Itoa(n)+` nulls`, func(t *testing.T) {
  24             testRingWrite(t, n, nulls[:])
  25         })
  26     }
  27 }
  28 
  29 func testRingWrite(t *testing.T, n int, buf []byte) {
  30     var sb strings.Builder
  31     _, err := RingWrite(&sb, n, buf)
  32     if err != nil {
  33         t.Fatal(err.Error())
  34         return
  35     }
  36 
  37     exp := ""
  38     if n >= 0 {
  39         exp = strings.Repeat(string(buf[0]), n)
  40     }
  41 
  42     got := sb.String()
  43     if got != exp {
  44         const fs = "expected %q (%d),\nbut got %q (%d) instead"
  45         t.Fatalf(fs, exp, len(exp), got, len(got))
  46     }
  47 }
  48 
  49 func TestWriteTracker(t *testing.T) {
  50     var tests = [][]string{
  51         nil,
  52         {""},
  53         {"abcdef"},
  54         {"abcdef xyz", "12345 abc"},
  55         {"12345 abc", " ", "--------------"},
  56     }
  57 
  58     for _, tc := range tests {
  59         t.Run(strings.Join(tc, ""), func(t *testing.T) {
  60             var sb strings.Builder
  61             w := NewWriteTracker(&sb)
  62 
  63             for _, s := range tc {
  64                 _, err := w.Write([]byte(s))
  65                 if err != nil {
  66                     t.Fatal(err.Error())
  67                     return
  68                 }
  69             }
  70 
  71             exp := sb.Len()
  72             got := w.Len()
  73             if got != exp {
  74                 const fs = "expected %d, but got %d instead"
  75                 t.Fatalf(fs, exp, got)
  76             }
  77         })
  78     }
  79 }