File: ./ascii.go
   1 package stringsplus
   2 
   3 // whitespaceASCII helps func IsWhitespace do its job
   4 var whitespaceASCII = [256]byte{
   5     '\t': 1, '\n': 1, '\v': 1, '\f': 1, '\r': 1, ' ': 1,
   6 }
   7 
   8 // IsWhitespaceASCII checks if the rune given is an ASCII whitespace symbol.
   9 func IsWhitespaceASCII(r rune) bool {
  10     b := byte(r)
  11     return rune(b) == r && whitespaceASCII[b] != 0
  12 }

     File: ./ascii_test.go
   1 package stringsplus
   2 
   3 import "testing"
   4 
   5 // identityASCII is copyable template to make other byte-lookup tables from
   6 var identityASCII = [256]byte{
   7     000, 001, 002, 003, 004, 005, 006, 007,
   8     010, 011, 012, 013, 014, 015, 016, 017,
   9     020, 021, 022, 023, 024, 025, 026, 027,
  10     030, 031, 032, 033, 034, 035, 036, 037,
  11     ' ', '!', '"', '#', '$', '%', '&', +39,
  12     '(', ')', '*', '+', ',', '-', '.', '/',
  13     '0', '1', '2', '3', '4', '5', '6', '7',
  14     '8', '9', ':', ';', '<', '=', '>', '?',
  15     '@', 'A', 'B', 'C', 'D', 'E', 'F', 'G',
  16     'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O',
  17     'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W',
  18     'X', 'Y', 'Z', '[', +92, ']', '^', '_',
  19     '`', 'a', 'b', 'c', 'd', 'e', 'f', 'g',
  20     'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o',
  21     'p', 'q', 'r', 's', 't', 'u', 'v', 'w',
  22     'x', 'y', 'z', '{', '|', '}', '~', 127,
  23     128, 129, 130, 131, 132, 133, 134, 135,
  24     136, 137, 138, 139, 140, 141, 142, 143,
  25     144, 145, 146, 147, 148, 149, 150, 151,
  26     152, 153, 154, 155, 156, 157, 158, 159,
  27     160, 161, 162, 163, 164, 165, 166, 167,
  28     168, 169, 170, 171, 172, 173, 174, 175,
  29     176, 177, 178, 179, 180, 181, 182, 183,
  30     184, 185, 186, 187, 188, 189, 190, 191,
  31     192, 193, 194, 195, 196, 197, 198, 199,
  32     200, 201, 202, 203, 204, 205, 206, 207,
  33     208, 209, 210, 211, 212, 213, 214, 215,
  34     216, 217, 218, 219, 220, 221, 222, 223,
  35     224, 225, 226, 227, 228, 229, 230, 231,
  36     232, 233, 234, 235, 236, 237, 238, 239,
  37     240, 241, 242, 243, 244, 245, 246, 247,
  38     248, 249, 250, 251, 252, 253, 254, 255,
  39 }
  40 
  41 func TestIdentityTable(t *testing.T) {
  42     if len(identityASCII) != 256 {
  43         const fs = `template table has %d items, instead of 256`
  44         t.Fatalf(fs, len(identityASCII))
  45         return
  46     }
  47 
  48     for i, v := range identityASCII {
  49         if byte(i) != v {
  50             t.Fatalf(`expected %d at index %d, instead of %d`, i, i, v)
  51             return
  52         }
  53     }
  54 }

     File: ./constants.go
   1 package stringsplus
   2 
   3 const (
   4     // Spaces is a string with 256 spaces and nothing else in it.
   5     Spaces = `` +
   6         `                                                                ` +
   7         `                                                                ` +
   8         `                                                                ` +
   9         `                                                                `
  10 
  11     // Tabs is a string with 256 tabs and nothing else in it.
  12     Tabs = `` +
  13         "\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t" +
  14         "\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t" +
  15         "\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t" +
  16         "\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t" +
  17         "\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t" +
  18         "\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t" +
  19         "\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t" +
  20         "\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t"
  21 )
  22 
  23 const (
  24     LowerLetters = `abcdefghijklmnopqrstuvwxyz`
  25     UpperLetters = `ABCDEFGHIJKLMNOPQRSTUVWXYZ`
  26 
  27     Digits      = `0123456789`
  28     Letters     = UpperLetters + LowerLetters
  29     Punctuation = "!\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~"
  30     Whitespace  = " \t\n\v\r"
  31 
  32     // Printable is a string with all ASCII codes which can be shown easily.
  33     Printable = `` +
  34         ` !"#$%&'()*+,-./0123456789:;<=>?@` +
  35         "ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~"
  36 
  37     // ASCII is a string with all 128 ASCII codes in order.
  38     ASCII = `` +
  39         "\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f" +
  40         "\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f" +
  41         Printable + "\x7f"
  42 )

     File: ./dates.go
   1 package stringsplus
   2 
   3 import (
   4     "strconv"
   5 )
   6 
   7 // ExtractYMD tries to parse year, month, and day out of an international-style date
   8 // string: examples of these are 2009/03/10 and 09/03/10.
   9 func ExtractYMD(s string) (year, month, day int, ok bool) {
  10     var err error
  11     var y, m, d int
  12 
  13     switch len(s) {
  14     case 8: // yy-mm-dd or yyyymmdd
  15         switch s[2] {
  16         case '-', ':', ' ', '/': // yy-mm-dd
  17             // both field separators must be the same
  18             if s[2] != s[5] {
  19                 return 0, 0, 0, false
  20             }
  21 
  22             y, err = strconv.Atoi(s[:2])
  23             if err != nil {
  24                 return 0, 0, 0, false
  25             }
  26             m, err = strconv.Atoi(s[3:5])
  27             if err != nil {
  28                 return 0, 0, 0, false
  29             }
  30             d, err = strconv.Atoi(s[6:])
  31             if err != nil {
  32                 return 0, 0, 0, false
  33             }
  34 
  35         default: // yyyymmdd
  36             y, err = strconv.Atoi(s[:4])
  37             if err != nil {
  38                 return 0, 0, 0, false
  39             }
  40             m, err = strconv.Atoi(s[4:6])
  41             if err != nil {
  42                 return 0, 0, 0, false
  43             }
  44             d, err = strconv.Atoi(s[6:])
  45             if err != nil {
  46                 return 0, 0, 0, false
  47             }
  48         }
  49 
  50         if y < 70 {
  51             y += 2000 // 2-digit years below 70 are the years after 2000
  52         } else {
  53             y += 1900 // 2-digit years since 70 are the years after 1900
  54         }
  55 
  56     case 10: // yyyy-mm-dd
  57         switch s[4] {
  58         case '-', ':', ' ', '/':
  59             // both field separators must be the same
  60             if s[4] != s[7] {
  61                 return 0, 0, 0, false
  62             }
  63         default:
  64             // unrecognized field separator
  65             return 0, 0, 0, false
  66         }
  67 
  68         y, err = strconv.Atoi(s[:4])
  69         if err != nil {
  70             return 0, 0, 0, false
  71         }
  72         m, err = strconv.Atoi(s[5:7])
  73         if err != nil {
  74             return 0, 0, 0, false
  75         }
  76         d, err = strconv.Atoi(s[8:])
  77         if err != nil {
  78             return 0, 0, 0, false
  79         }
  80 
  81     default:
  82         return 0, 0, 0, false
  83     }
  84 
  85     // check whether date parts make sense
  86     if m < 1 || d < 1 || y < 1 || m > 12 || d > 31 {
  87         return 0, 0, 0, false
  88     }
  89 
  90     // success
  91     return y, m, d, true
  92 }
  93 
  94 // ExtractMDY tries to parse month, day, and year out of an English-style date string:
  95 // examples of these are 03/10/2009 and 03/10/09.
  96 func ExtractMDY(s string) (month, day, year int, ok bool) {
  97     var err error
  98     var m, d, y int
  99 
 100     switch len(s) {
 101     case 8: // mm-dd-yy or mmddyyyy
 102         switch s[2] {
 103         case '-', ':', ' ', '/': // mm-dd-yy
 104             // both field separators must be the same
 105             if s[2] != s[5] {
 106                 return 0, 0, 0, false
 107             }
 108 
 109             m, err = strconv.Atoi(s[:2])
 110             if err != nil {
 111                 return 0, 0, 0, false
 112             }
 113             d, err = strconv.Atoi(s[3:5])
 114             if err != nil {
 115                 return 0, 0, 0, false
 116             }
 117             y, err = strconv.Atoi(s[6:])
 118             if err != nil {
 119                 return 0, 0, 0, false
 120             }
 121 
 122         default: // mmddyyyy
 123             m, err = strconv.Atoi(s[:2])
 124             if err != nil {
 125                 return 0, 0, 0, false
 126             }
 127             d, err = strconv.Atoi(s[2:4])
 128             if err != nil {
 129                 return 0, 0, 0, false
 130             }
 131             y, err = strconv.Atoi(s[4:])
 132             if err != nil {
 133                 return 0, 0, 0, false
 134             }
 135         }
 136 
 137         if y < 70 {
 138             y += 2000 // 2-digit years below 70 are the years after 2000
 139         } else {
 140             y += 1900 // 2-digit years since 70 are the years after 1900
 141         }
 142 
 143     case 10: // mm-dd-yyyy
 144         switch s[2] {
 145         case '-', ':', ' ', '/':
 146             // both field separators must be the same
 147             if s[2] != s[5] {
 148                 return 0, 0, 0, false
 149             }
 150         default:
 151             // unrecognized field separator
 152             return 0, 0, 0, false
 153         }
 154 
 155         m, err = strconv.Atoi(s[:2])
 156         if err != nil {
 157             return 0, 0, 0, false
 158         }
 159         d, err = strconv.Atoi(s[3:5])
 160         if err != nil {
 161             return 0, 0, 0, false
 162         }
 163         y, err = strconv.Atoi(s[6:])
 164         if err != nil {
 165             return 0, 0, 0, false
 166         }
 167 
 168     default:
 169         return 0, 0, 0, false
 170     }
 171 
 172     // check whether date parts make sense
 173     if m < 1 || d < 1 || y < 1 || m > 12 || d > 31 {
 174         return 0, 0, 0, false
 175     }
 176 
 177     // success
 178     return m, d, y, true
 179 }

     File: ./dates_test.go
   1 package stringsplus
   2 
   3 import (
   4     "testing"
   5 )
   6 
   7 func TestExtractYMD(t *testing.T) {
   8     var tests = []struct {
   9         Input   string
  10         Year    int
  11         Month   int
  12         Day     int
  13         Success bool
  14     }{
  15         {"1997/03/19", 1997, 3, 19, true},
  16         {"1997-03-19", 1997, 3, 19, true},
  17         {"1997/13/40", 0, 0, 0, false},
  18     }
  19 
  20     for _, tc := range tests {
  21         t.Run(tc.Input, func(t *testing.T) {
  22             y, m, d, ok := ExtractYMD(tc.Input)
  23             if !ok && !tc.Success {
  24                 return
  25             }
  26 
  27             if y != tc.Year || m != tc.Month || d != tc.Day || ok != tc.Success {
  28                 t.Fatalf("expected %d %d %d and %v, but got %d %d %d and %v instead",
  29                     tc.Year, tc.Month, tc.Day, tc.Success, y, m, d, ok)
  30             }
  31         })
  32     }
  33 }
  34 
  35 func TestExtractMDY(t *testing.T) {
  36     var tests = []struct {
  37         Input   string
  38         Year    int
  39         Month   int
  40         Day     int
  41         Success bool
  42     }{
  43         {"03-19-1997", 3, 19, 1997, true},
  44         {"13/19/1997", 0, 0, 0, false},
  45         {"11/19/1998", 11, 19, 1998, true},
  46     }
  47 
  48     for _, tc := range tests {
  49         t.Run(tc.Input, func(t *testing.T) {
  50             y, m, d, ok := ExtractMDY(tc.Input)
  51             if !ok && !tc.Success {
  52                 return
  53             }
  54 
  55             if y != tc.Year || m != tc.Month || d != tc.Day || ok != tc.Success {
  56                 t.Fatalf("expected %d %d %d and %v, but got %d %d %d and %v instead",
  57                     tc.Year, tc.Month, tc.Day, tc.Success, y, m, d, ok)
  58             }
  59         })
  60     }
  61 }

     File: ./doc.go
   1 /*
   2 # stringsplus
   3 
   4 This package has functionality which arguably should be part of `strings`.
   5 */
   6 package stringsplus

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

     File: ./numbers.go
   1 package stringsplus
   2 
   3 import (
   4     "bytes"
   5     "math"
   6     "strconv"
   7     "strings"
   8 )
   9 
  10 // isWhitespaceASCII is a quick lookup table to tell ASCII-whitespace from
  11 // all other bytes, just by checking for a non-zero value
  12 var isWhitespaceASCII = [256]uint8{'\t': 1, '\n': 1, '\v': 1, '\f': 1, '\r': 1}
  13 
  14 // CountDecimalDigits finds how many decimal digits there are in the numeric
  15 // string given: if the string isn't a valid number (say it ends with a percent
  16 // sign) or has more than a single dot in it, the result will be misleading;
  17 // integers are handled correctly as having 0 decimal digits
  18 func CountDecimalDigits(s string) int {
  19     i := strings.LastIndex(s, ".")
  20     // no dot means it's an integer, which means 0 decimal digits
  21     if i < 0 {
  22         return 0
  23     }
  24     return len(s) - i - 1
  25 }
  26 
  27 // TrimDecimals ensures numeric strings don't have unneeded trailing 0s
  28 // after the decimal point, if that's present.
  29 func TrimDecimals(s string) string {
  30     dot := -1
  31     nzero := -1
  32     for i, r := range s {
  33         switch r {
  34         case '.':
  35             dot = i
  36         case '0':
  37             // make the default case match anything besides 0s and dots
  38         default:
  39             nzero = i
  40         }
  41     }
  42 
  43     if dot < 0 {
  44         return s
  45     }
  46     if nzero < dot {
  47         return s[:dot]
  48     }
  49     return s[:nzero+1]
  50 }
  51 
  52 // func TrimDecimals(s string) string {
  53 //  for strings.HasSuffix(s, "0") {
  54 //      s = s[:len(s)-1]
  55 //  }
  56 //  return strings.TrimSuffix(s, ".")
  57 // }
  58 
  59 // trimDecimals is the byte-slice counterpart to func TrimDecimals
  60 func trimDecimals(s []byte) []byte {
  61     dot := -1
  62     nzero := -1
  63     for i, r := range s {
  64         switch r {
  65         case '.':
  66             dot = i
  67         case '0':
  68             // make the default case match anything besides 0s and dots
  69         default:
  70             nzero = i
  71         }
  72     }
  73 
  74     if dot < 0 {
  75         return s
  76     }
  77     if nzero < dot {
  78         return s[:dot]
  79     }
  80     return s[:nzero+1]
  81 }
  82 
  83 // TrimTrails ignores all trailing ASCII-whitespace symbols from strings.
  84 func TrimTrails(s string) string {
  85     for len(s) > 0 && isWhitespaceASCII[s[len(s)-1]] != 0 {
  86         s = s[:len(s)-1]
  87     }
  88     return s
  89 }
  90 
  91 // OrdinalSuffix matches the right ordinal suffix for the non-negative number
  92 // given; negatives return an empty string
  93 func OrdinalSuffix(n int) string {
  94     if n < 0 {
  95         return ""
  96     }
  97 
  98     // remainder by 100 is to identify numbers ending in 11, 12, and 13, to
  99     // give them a `th` suffix instead of the usual `st`, `nd`, and `rd`
 100     if rem100 := n % 100; 11 <= rem100 && rem100 <= 20 {
 101         return "th"
 102     }
 103 
 104     switch n % 10 {
 105     case 1:
 106         return "st"
 107     case 2:
 108         return "nd"
 109     case 3:
 110         return "rd"
 111     default:
 112         return "th"
 113     }
 114 }
 115 
 116 // LoopThousandsGroups chunks a byte-string representation of the integer
 117 // given into 3-digits groups, possibly except for the first one, since that
 118 // one may have fewer than 3 digits.
 119 //
 120 // Using this func with a byte array/slice, you can convert numbers into your
 121 // custom string representations, such as by inserting commas between groups
 122 // of digits.
 123 func LoopThousandsGroups(n uint64, fn func(i int, digits []byte)) {
 124     i := 0
 125     var buf [32]byte
 126     s := strconv.AppendUint(buf[:0], n, 10)
 127 
 128     lead := len(s) % 3
 129     if lead > 0 {
 130         fn(i, s[:lead])
 131         s = s[lead:]
 132         i++
 133     }
 134 
 135     for ; len(s) > 0; i++ {
 136         fn(i, s[:3])
 137         s = s[3:]
 138     }
 139 }
 140 
 141 // AppendNiceInt64 lets you format an integer value to be more readable,
 142 // by separating 3-digit groups using commas, underscores, or some other
 143 // ASCII symbol of your choice, passed as the separator-byte argument.
 144 //
 145 // For example, to emit bytes "-12,345,678", you can call this func as
 146 //
 147 //  var buf[32]byte
 148 //  res := stringsplus.WriteNiceInt64(buf[:0], -12_345_678, ',')
 149 func AppendNiceInt64(buf []byte, num int64, sep byte) []byte {
 150     // ensure the rest of this func handles non-negative values
 151     if num < 0 {
 152         num = -num
 153         buf = append(buf, '-')
 154     }
 155 
 156     LoopThousandsGroups(uint64(num), func(i int, digits []byte) {
 157         if i > 0 {
 158             // put commas between 3-digit groups
 159             buf = append(buf, ',')
 160         }
 161         buf = append(buf, digits...)
 162     })
 163 
 164     // final result is the extended buffer
 165     return buf
 166 }
 167 
 168 // AppendNiceUint64 is like func AppendNiceInt64, but for uint64 values.
 169 func AppendNiceUint64(buf []byte, num uint64, sep byte) []byte {
 170     LoopThousandsGroups(uint64(num), func(i int, digits []byte) {
 171         if i > 0 {
 172             // put commas between 3-digit groups
 173             buf = append(buf, ',')
 174         }
 175         buf = append(buf, digits...)
 176     })
 177 
 178     // final result is the extended buffer
 179     return buf
 180 }
 181 
 182 // dotZeroDecimals lets func AppendNiceFloat64 handle a special case
 183 var dotZeroDecimals = [17]byte{
 184     '.',
 185     '0', '0', '0', '0', '0', '0', '0', '0',
 186     '0', '0', '0', '0', '0', '0', '0', '0',
 187 }
 188 
 189 // AppendNiceFloat64 is the counterpart of func AppendNiceInt64, except it's
 190 // for float64 values: that's why it also requires a precision argument, to
 191 // specify the number of decimal digits to emit.
 192 //
 193 // For example, to emit bytes "-12,345,678.123", you can call this func as
 194 //
 195 //  var buf[32]byte
 196 //  res := stringsplus.AppendNiceFloat64(buf[:0], -12_345_678.123, ',', 3)
 197 func AppendNiceFloat64(buf []byte, f float64, sep byte, prec int) []byte {
 198     // avoid emitting a non-sense integer part for special float64 values
 199     if math.IsNaN(f) {
 200         return append(buf, 'N', 'a', 'N')
 201     }
 202     if math.IsInf(f, -1) {
 203         return append(buf, '-', 'I', 'n', 'f')
 204     }
 205     if math.IsInf(f, +1) {
 206         return append(buf, '+', 'I', 'n', 'f')
 207     }
 208 
 209     if prec > len(dotZeroDecimals)-1 {
 210         prec = len(dotZeroDecimals) - 1
 211     }
 212 
 213     // avoid writing -0 when the sign bit is on
 214     if f == 0 {
 215         buf = append(buf, '0')
 216         if prec < 1 {
 217             return buf
 218         }
 219         return append(buf, dotZeroDecimals[:prec+1]...)
 220     }
 221 
 222     // emit the digit-grouped integer part
 223     buf = AppendNiceInt64(buf, int64(f), sep)
 224 
 225     // see if any decimals need be emitted
 226     if prec < 1 {
 227         return buf
 228     }
 229 
 230     // emit the decimal part
 231     var decbuf [32]byte
 232     decstr := strconv.AppendFloat(decbuf[:0], f, 'f', prec, 64)
 233     i := bytes.IndexByte(decstr, '.')
 234     if i < 0 {
 235         // there are no decimal digits
 236         return append(buf, dotZeroDecimals[:prec+1]...)
 237     }
 238 
 239     // emit decimal digits, starting from the dot
 240     return append(buf, decstr[i:]...)
 241 }

     File: ./numbers_test.go
   1 package stringsplus
   2 
   3 import (
   4     "strconv"
   5     "testing"
   6 )
   7 
   8 func TestCountDecimalDigits(t *testing.T) {
   9     var tests = []struct {
  10         Input    string
  11         Expected int
  12     }{
  13         {Input: "", Expected: 0},
  14         {Input: "42.50779", Expected: 5},
  15         {Input: "+.64", Expected: 2},
  16         {Input: "-8", Expected: 0},
  17     }
  18 
  19     for _, test := range tests {
  20         dec := CountDecimalDigits(test.Input)
  21         if dec != test.Expected {
  22             t.Errorf("%s has %d decimal digits, not %d",
  23                 test.Input, test.Expected, dec)
  24         }
  25     }
  26 }
  27 
  28 func TestTrimDecimals(t *testing.T) {
  29     var tests = []struct {
  30         Input    string
  31         Expected string
  32     }{
  33         {"-3", "-3"},
  34         {"-3.", "-3"},
  35         {"-3.0", "-3"},
  36         {"-300.12", "-300.12"},
  37         {"-300.12000", "-300.12"},
  38         {"-300.120006", "-300.120006"},
  39     }
  40 
  41     for _, tc := range tests {
  42         if s := TrimDecimals(tc.Input); s != tc.Expected {
  43             const fs = "trimming unneeded decimals from %s gave %s, instead of %s"
  44             t.Fatalf(fs, tc.Input, s, tc.Expected)
  45         }
  46     }
  47 }
  48 
  49 func TestOrdinalSuffix(t *testing.T) {
  50     // jf -from=null -to=plain '(0^100).map(v.nth).chunk(10).join(lf)'
  51     var tests = []string{
  52         "th", "st", "nd", "rd", "th", "th", "th", "th", "th", "th",
  53         "th", "th", "th", "th", "th", "th", "th", "th", "th", "th",
  54         "th", "st", "nd", "rd", "th", "th", "th", "th", "th", "th",
  55         "th", "st", "nd", "rd", "th", "th", "th", "th", "th", "th",
  56         "th", "st", "nd", "rd", "th", "th", "th", "th", "th", "th",
  57         "th", "st", "nd", "rd", "th", "th", "th", "th", "th", "th",
  58         "th", "st", "nd", "rd", "th", "th", "th", "th", "th", "th",
  59         "th", "st", "nd", "rd", "th", "th", "th", "th", "th", "th",
  60         "th", "st", "nd", "rd", "th", "th", "th", "th", "th", "th",
  61         "th", "st", "nd", "rd", "th", "th", "th", "th", "th", "th",
  62         "th",
  63     }
  64 
  65     for i, exp := range tests {
  66         if s := OrdinalSuffix(i); s != exp {
  67             const fs = "ordinal suffix for %d gave %s, instead of %s"
  68             t.Fatalf(fs, i, s, exp)
  69         }
  70     }
  71 }
  72 
  73 func TestLoopThousandsGroups(t *testing.T) {
  74     var tests = []struct {
  75         Input    uint64
  76         Expected []int
  77     }{
  78         {0, []int{0}},
  79         {999, []int{999}},
  80         {1_670, []int{1, 670}},
  81         {3_490, []int{3, 490}},
  82         {12_332, []int{12, 332}},
  83         {999_999, []int{999, 999}},
  84         {1_000_000, []int{1, 0, 0}},
  85         {1_000_001, []int{1, 0, 1}},
  86         {1_234_567, []int{1, 234, 567}},
  87     }
  88 
  89     // return
  90     for _, tc := range tests {
  91         count := 0
  92         LoopThousandsGroups(tc.Input, func(i int, digits []byte) {
  93             n, _ := strconv.ParseInt(string(digits), 10, 64)
  94             // t.Log(tc.Input, i, n)
  95             if int(n) != tc.Expected[i] {
  96                 const fs = "group %d in %d: got %d instead of %d"
  97                 t.Errorf(fs, i, tc.Input, n, tc.Expected[i])
  98             }
  99             count++
 100         })
 101 
 102         if count != len(tc.Expected) {
 103             const fs = "thousands-groups from %d: got %d instead of %d"
 104             t.Errorf(fs, tc.Input, count, len(tc.Expected))
 105         }
 106     }
 107 }
 108 
 109 func TestAppendNiceFloat64(t *testing.T) {
 110     var tests = []struct {
 111         Number   float64
 112         Prec     int
 113         Expected string
 114     }{
 115         {17, 1, "17.0"},
 116         {17.5, 1, "17.5"},
 117     }
 118 
 119     for _, tc := range tests {
 120         var buf [32]byte
 121         s := AppendNiceFloat64(buf[:0], tc.Number, ',', tc.Prec)
 122 
 123         if got := string(s); got != tc.Expected {
 124             const fs = "expected %q but got %q instead"
 125             t.Fatalf(fs, tc.Expected, got)
 126             return
 127         }
 128     }
 129 }

     File: ./parsers.go
   1 package stringsplus
   2 
   3 import (
   4     "errors"
   5     "math"
   6     "strconv"
   7     "strings"
   8 )
   9 
  10 var (
  11     ErrNoDigits        = errors.New("can't parse number without any digits")
  12     ErrInvalidNumber   = errors.New("can't parse number out of the string given")
  13     ErrIntegerDecimals = errors.New("can't parse an integer from a string with decimal dots")
  14     ErrIntegerOverflow = errors.New("parsing the string caused integer overflow")
  15 
  16     // ErrOutsideInt32Range = errors.New("parsed integer is outside int32 range")
  17 )
  18 
  19 // precomputed powers of 10 below 2**63
  20 var pow10 = [19]int64{
  21     1,
  22     10,
  23     100,
  24     1_000,
  25     10_000,
  26     100_000,
  27     1_000_000,
  28     10_000_000,
  29     100_000_000,
  30     1_000_000_000,
  31     10_000_000_000,
  32     100_000_000_000,
  33     1_000_000_000_000,
  34     10_000_000_000_000,
  35     100_000_000_000_000,
  36     1_000_000_000_000_000,
  37     10_000_000_000_000_000,
  38     100_000_000_000_000_000,
  39     1_000_000_000_000_000_000,
  40 }
  41 
  42 // ParseFloat64 parses a few more strings than the stdlib's float parser,
  43 // by allowing a leading dash/negative symbol, and ignoring early dollar
  44 // signs in strings.
  45 func ParseFloat64(s string) (float64, error) {
  46     s = strings.TrimSpace(s)
  47 
  48     // handle strings starting as "-$"
  49     if strings.HasPrefix(s, "-$") {
  50         s = s[2:]
  51         f, err := strconv.ParseFloat(s, 64)
  52         return -f, err
  53     }
  54 
  55     // handle strings startings as "−" or "−$"
  56     if strings.HasPrefix(s, "") {
  57         s = s[len(""):]
  58         s = trimPrefixByte(s, '$')
  59         f, err := strconv.ParseFloat(s, 64)
  60         return -f, err
  61     }
  62 
  63     // also handle strings starting as "$" or "+$"
  64     s = trimPrefixByte(s, '+')
  65     s = trimPrefixByte(s, '$')
  66     return strconv.ParseFloat(s, 64)
  67 }
  68 
  69 func trimPrefixByte(s string, pre byte) string {
  70     if len(s) > 0 && s[0] == pre {
  71         return s[1:]
  72     }
  73     return s
  74 }
  75 
  76 // NumberParser parses numbers while offering additional info via its public properties,
  77 // which change according to each string parsed.
  78 type NumberParser struct {
  79     Value    float64 // value of the latest parsed number
  80     Decimals int     // counts decimal digits of latest parsed number
  81     sb       strings.Builder
  82 
  83     Dollars    bool // whether latest parsed number had a dollar sign in it
  84     Percentage bool // whether latest parsed number was a percentage
  85 }
  86 
  87 // Parse is a method which parses numbers in a more forgiving way, compared to the stdlib,
  88 // and lets the parser remember a few additional details the stdlib doesn't even allow.
  89 func (p *NumberParser) Parse(s string) (float64, error) {
  90     return p.ParseFloat(s, 64)
  91 }
  92 
  93 // ParseFloat is like func strconv.ParseFloat, so it lets you choose between 32-bit or
  94 // 64-bit accuracy: when choosing 32, the float64 is convertible into float32 losslessly
  95 func (p *NumberParser) ParseFloat(s string, bitSize int) (float64, error) {
  96     p.reset()
  97 
  98     // forbid numbers in computer scientific notation
  99     // for _, r := range s {
 100     //  if r == 'e' || r == 'E' {
 101     //      return math.NaN(), ErrInvalidNumber
 102     //  }
 103     // }
 104 
 105     // forbid letters anywhere: this rule excludes strings like `nan` and scientific
 106     // notation using `e` or `E` in it
 107     for _, r := range s {
 108         // if ('a' <= r && r <= 'z') || ('A' <= r && r <= 'Z') {
 109         if (':' <= r && r <= '^') || ('`' <= r && r <= 127) {
 110             return math.NaN(), ErrInvalidNumber
 111         }
 112     }
 113 
 114     // assuming most strings given would parse fine using the stdlib, this should speed things up
 115     if f, err := strconv.ParseFloat(s, bitSize); err == nil {
 116         p.Value = f
 117         if i := strings.IndexByte(s, '.'); i >= 0 {
 118             p.Decimals = len(s) - i - 1
 119         }
 120         p.sb.WriteString(s)
 121         return f, nil
 122     }
 123 
 124     s, err := p.prepareString(s)
 125     if err != nil {
 126         return math.NaN(), err
 127     }
 128 
 129     f, err := strconv.ParseFloat(s, bitSize)
 130     p.Value = f
 131     return p.Value, err
 132 }
 133 
 134 // ParseInt is like func strconv.ParseInt, but you also get to support strings with dollars
 135 // and percentages. Parsing strings with an explicit decimal dot always results in an error,
 136 // even if all decimal digits are zeros.
 137 func (p *NumberParser) ParseInt(s string, base int, bitSize int) (int64, error) {
 138     p.reset()
 139 
 140     // assuming most strings given would parse fine using the stdlib, this should speed things up
 141     if n, err := strconv.ParseInt(s, base, bitSize); err == nil {
 142         p.Value = float64(n)
 143         p.sb.WriteString(s)
 144         return n, nil
 145     }
 146 
 147     if strings.IndexByte(s, '.') >= 0 {
 148         return 0, ErrIntegerDecimals
 149     }
 150 
 151     s, err := p.prepareString(s)
 152     if err != nil {
 153         return 0, err
 154     }
 155 
 156     n, err := strconv.ParseInt(s, base, bitSize)
 157     p.Value = float64(n)
 158     return n, err
 159 }
 160 
 161 // String gives a string representation of the latest number parsed, which is guaranteed
 162 // to parse correctly if the latest parsing attempt was successful.
 163 func (p *NumberParser) String() string {
 164     return p.sb.String()
 165 }
 166 
 167 // Grow allows you to ensure no further allocations for strings up to the length given.
 168 // Negative values are safely ignored, instead of panicking.
 169 func (p *NumberParser) Grow(n int) {
 170     if n < 0 {
 171         return
 172     }
 173     p.sb.Grow(n)
 174 }
 175 
 176 func (p *NumberParser) reset() {
 177     p.Decimals = 0
 178     p.Dollars = false
 179     p.Percentage = false
 180     p.Value = math.NaN()
 181     p.sb.Reset()
 182 }
 183 
 184 // prepareString is a string preprocessor for the stdlib float-parsing funcs
 185 func (p *NumberParser) prepareString(s string) (simplified string, err error) {
 186     if s == "" {
 187         return "", ErrNoDigits
 188     }
 189 
 190     decimals := false
 191     s, sign := p.trimString(s)
 192     if sign < 0 {
 193         p.sb.Grow(len(s) + 1)
 194         p.sb.WriteRune('-')
 195     } else {
 196         p.sb.Grow(len(s))
 197     }
 198 
 199     for _, r := range s {
 200         // after a decimal dot there can only be digits
 201         if decimals {
 202             if r < '0' || r > '9' {
 203                 return "", ErrInvalidNumber
 204             }
 205             p.sb.WriteRune(r)
 206             p.Decimals++
 207             continue
 208         }
 209 
 210         // handle the integer part and the decimal dot
 211         switch {
 212         case '0' <= r && r <= '9':
 213             p.sb.WriteRune(r)
 214         case r == '.':
 215             decimals = true
 216             p.sb.WriteRune('.')
 217         case r == ',':
 218             // ignore commas to allow numbers formatted using digit-grouping commas
 219         default:
 220             return "", ErrInvalidNumber
 221         }
 222     }
 223 
 224     return p.sb.String(), nil
 225 }
 226 
 227 // strip string to parse of decorations at either end, and update parser state accordingly
 228 func (p *NumberParser) trimString(s string) (simpler string, sign float64) {
 229     sign = 1
 230     s = strings.TrimSpace(s)
 231 
 232     // handle dollars, signs and dollars, then signs without dollars
 233     if strings.HasPrefix(s, "$") {
 234         p.Dollars = true
 235         s = s[1:]
 236     } else if strings.HasPrefix(s, "+$") {
 237         p.Dollars = true
 238         s = s[2:]
 239     } else if strings.HasPrefix(s, "-$") {
 240         sign = -1
 241         p.Dollars = true
 242         s = s[2:]
 243     } else if strings.HasPrefix(s, "−$") {
 244         sign = -1
 245         p.Dollars = true
 246         s = s[4:] // len("−$") is 4; the minus isn't the ascii dash
 247     } else if strings.HasPrefix(s, "+") {
 248         s = s[1:]
 249     } else if strings.HasPrefix(s, "-") {
 250         sign = -1
 251         s = s[1:]
 252     } else if strings.HasPrefix(s, "") {
 253         sign = -1
 254         s = s[3:] // len("−") is 3; the minus isn't the ascii dash
 255     }
 256 
 257     // handle percentages
 258     if strings.HasSuffix(s, "%") {
 259         p.Percentage = true
 260         s = strings.TrimSpace(s[:len(s)-1])
 261     }
 262 
 263     return s, sign
 264 }
 265 
 266 // ParseIntegers parses a string full of integer numbers and/or integer ranges in it.
 267 func ParseIntegers(s string) ([]int, error) {
 268     s = strings.TrimSpace(s)
 269     if len(s) == 0 {
 270         return nil, nil
 271     }
 272 
 273     // count items to preallocate slice: if there are ranges, reallocations may happen anyway
 274     n := 0
 275     for _, r := range s {
 276         if r == ' ' || r == ',' {
 277             n++
 278         }
 279     }
 280     n++
 281 
 282     start := 0
 283     nums := make([]int, 0, n)
 284     for i, r := range s {
 285         if r == ' ' || r == ',' {
 286             piece := s[start:i]
 287             start = i + 1
 288             // ignore extra spaces
 289             if piece == "" {
 290                 continue
 291             }
 292 
 293             // handle integer ranges of type m:n or m..n
 294             first, last, err := ParseIntRange(piece)
 295             if err == nil {
 296                 inc := +1
 297                 if last < first {
 298                     inc = -1
 299                     first, last = last, first
 300                 }
 301                 for n := first; n <= last; n += inc {
 302                     nums = append(nums, n)
 303                 }
 304             }
 305 
 306             // handle integer numbers
 307             n, err := strconv.Atoi(piece)
 308             if err != nil {
 309                 return nums, err
 310             }
 311             nums = append(nums, n)
 312         }
 313     }
 314     return nums, nil
 315 }
 316 
 317 // ParseNumbers parses a string full of numbers and/or integer ranges in it.
 318 func ParseNumbers(s string) ([]float64, error) {
 319     start := 0
 320     var nums []float64
 321     for i, v := range s {
 322         if v == ' ' || v == ',' {
 323             piece := s[start:i]
 324             start = i + 1
 325             // ignore extra spaces
 326             if piece == "" {
 327                 continue
 328             }
 329 
 330             // handle integer ranges of type m:n or m..n
 331             first, last, err := ParseIntRange(piece)
 332             if err == nil {
 333                 inc := +1
 334                 if last < first {
 335                     inc = -1
 336                     first, last = last, first
 337                 }
 338                 for n := first; n <= last; n += inc {
 339                     nums = append(nums, float64(n))
 340                 }
 341             }
 342 
 343             // handle real numbers
 344             f, err := strconv.ParseFloat(piece, 64)
 345             if err != nil {
 346                 return nums, err
 347             }
 348             nums = append(nums, f)
 349         }
 350     }
 351     return nums, nil
 352 }
 353 
 354 // ParseIntRange extracts 2 integers from strings of type m:n, m..n, or even m...n,
 355 // or returns an error if unsuccessful.
 356 func ParseIntRange(s string) (first, last int, err error) {
 357     if i := strings.Index(s, ":"); i >= 0 {
 358         return parseIntPair(s[:i], s[i+1:])
 359     }
 360 
 361     i := strings.Index(s, ".")
 362     // no colons and no dots means it's a single number: treat it as a 1-item range
 363     if i < 0 {
 364         n, err := strconv.Atoi(s)
 365         return n, n, err
 366     }
 367 
 368     // don't care how many dots are between
 369     j := strings.LastIndex(s, ".")
 370     return parseIntPair(s[:i], s[:j+1])
 371 }
 372 
 373 func parseIntPair(s, t string) (m, n int, err error) {
 374     m, err = strconv.Atoi(s)
 375     if err != nil {
 376         return 0, 0, err
 377     }
 378     n, err = strconv.Atoi(t)
 379     if err != nil {
 380         return 0, 0, err
 381     }
 382     return m, n, nil
 383 }
 384 
 385 // ParseHex tries to parse a non-negative number from hexadecimal notation. Common
 386 // prefixes used for hex values are also supported.
 387 func ParseHex(s string) (n int, err error) {
 388     s = strings.TrimPrefix(s, "#")
 389     s = strings.TrimPrefix(s, "0x")
 390     if len(s) == 0 {
 391         return 0, ErrNoDigits
 392     }
 393     if len(s) >= len(pow10) {
 394         return 0, ErrIntegerOverflow
 395     }
 396 
 397     var res int64
 398     mul := pow10[len(s)] // start with the digit multiplier at its maximum
 399     for _, r := range s {
 400         switch {
 401         case '0' <= r && r <= '9':
 402             res += mul * int64(r-'0')
 403         case 'A' <= r && r <= 'F':
 404             res += mul * (10 + int64(r-'A'))
 405         case 'a' <= r && r <= 'f':
 406             res += mul * (10 + int64(r-'a'))
 407         default:
 408             return 0, ErrInvalidNumber
 409         }
 410         mul /= 10
 411     }
 412     return int(res), nil
 413 }
 414 
 415 // ParseBin tries to parse a non-negative number from binary notation. Common
 416 // prefixes used for binary values are also supported.
 417 func ParseBin(s string) (n int, err error) {
 418     s = strings.TrimPrefix(s, "0b")
 419     if len(s) == 0 {
 420         return 0, ErrNoDigits
 421     }
 422 
 423     n = 0
 424     mul := 1 << len(s) // start with the digit multiplier at its maximum
 425     for _, r := range s {
 426         switch r {
 427         case '0':
 428             // 0 is valid, but adds nothing to the result
 429         case '1':
 430             n += mul
 431         default:
 432             return 0, ErrInvalidNumber
 433         }
 434         mul /= 2
 435     }
 436     return n, nil
 437 }

     File: ./parsers_test.go
   1 package stringsplus
   2 
   3 import (
   4     "math"
   5     "testing"
   6 )
   7 
   8 func TestNumberParser(t *testing.T) {
   9     var tests = []struct {
  10         name  string
  11         input string
  12         value float64
  13         state NumberParser
  14         err   error
  15     }{
  16         {
  17             "empty string", "", math.NaN(),
  18             NumberParser{}, ErrNoDigits,
  19         },
  20         {
  21             "simple number", "-234.2", -234.2,
  22             NumberParser{Decimals: 1}, nil,
  23         },
  24         {
  25             "digit-grouping commas", "+123,456,789.53", 123_456_789.53,
  26             NumberParser{Decimals: 2}, nil,
  27         },
  28         {
  29             "dollar amount", "-$234.2", -234.2,
  30             NumberParser{Dollars: true, Decimals: 1}, nil,
  31         },
  32         {
  33             "dollars and commas", "-$234,342.2", -234_342.2,
  34             NumberParser{Dollars: true, Decimals: 1}, nil,
  35         },
  36         {
  37             "dollars and commas", "−$234,342.2", -234_342.2,
  38             NumberParser{Dollars: true, Decimals: 1}, nil,
  39         },
  40         {
  41             "percent", "+35.23%", 35.23,
  42             NumberParser{Decimals: 2, Percentage: true}, nil,
  43         },
  44     }
  45 
  46     for _, tc := range tests {
  47         t.Run(tc.name, func(t *testing.T) {
  48             var np NumberParser
  49             f, err := np.Parse(tc.input)
  50             if err != tc.err {
  51                 const fs = "for input %q got error %s\n"
  52                 t.Fatalf(fs, tc.input, err.Error())
  53                 return
  54             }
  55 
  56             if !numbersEqual(f, tc.value) {
  57                 const fs = "for input: %q got %f, instead of %f\n"
  58                 t.Fatalf(fs, tc.input, f, tc.value)
  59                 return
  60             }
  61 
  62             if false ||
  63                 np.Dollars != tc.state.Dollars ||
  64                 np.Percentage != tc.state.Percentage ||
  65                 np.Decimals != tc.state.Decimals {
  66                 const fs = "for input %q got unexpected parser state: %#v\n"
  67                 t.Fatalf(fs, tc.input, np)
  68             }
  69         })
  70     }
  71 }
  72 
  73 func TestParse64(t *testing.T) {
  74     var tests = []struct {
  75         name  string
  76         input string
  77         value float64
  78     }{
  79         {"simple number", "-234.2", -234.2},
  80         {"digit-grouping commas", "+123_456_789.53", 123_456_789.53},
  81         {"dollar amount", "-$234.2", -234.2},
  82         {"dollars and commas", "-$234_342.2", -234_342.2},
  83         {"dollars and commas", "−$234_342.2", -234_342.2},
  84     }
  85 
  86     for _, tc := range tests {
  87         t.Run(tc.name, func(t *testing.T) {
  88             f, err := ParseFloat64(tc.input)
  89             if err != nil {
  90                 const fs = "for input %q got error %s\n"
  91                 t.Fatalf(fs, tc.input, err.Error())
  92                 return
  93             }
  94 
  95             if !numbersEqual(f, tc.value) {
  96                 const fs = "for input: %q got %f, instead of %f\n"
  97                 t.Fatalf(fs, tc.input, f, tc.value)
  98                 return
  99             }
 100         })
 101     }
 102 }
 103 
 104 // to allow comparing nans too
 105 func numbersEqual(x, y float64) bool {
 106     if math.IsNaN(x) && math.IsNaN(y) {
 107         return true
 108     }
 109     return x == y
 110 }

     File: ./strings.go
   1 package stringsplus
   2 
   3 import (
   4     "math"
   5     "strconv"
   6     "strings"
   7     "unicode"
   8     "unicode/utf8"
   9 )
  10 
  11 // BoolToString can help you flatten source-code which already uses if/else,
  12 // making it easier to read.
  13 func BoolToString(b bool, trueValue, falseValue string) string {
  14     if b {
  15         return trueValue
  16     }
  17     return falseValue
  18 }
  19 
  20 // HasAnyPrefix checks for any possible prefix given after the string to check.
  21 func HasAnyPrefix(s string, prefixes ...string) bool {
  22     for _, pre := range prefixes {
  23         if strings.HasPrefix(s, pre) {
  24             return true
  25         }
  26     }
  27     return false
  28 }
  29 
  30 // HasAnySuffix checks for any possible suffix given after the string to check.
  31 func HasAnySuffix(s string, suffixes ...string) bool {
  32     for _, suf := range suffixes {
  33         if strings.HasSuffix(s, suf) {
  34             return true
  35         }
  36     }
  37     return false
  38 }
  39 
  40 // IndexFold is the case-insensitive counterpart to func strings.Index.
  41 // This func's worst-case time-complexity is O(m*n).
  42 func IndexFold(s, t string) int {
  43     if t == `` {
  44         // mimic behavior of strings.Index to pass unit-tests
  45         return 0
  46     }
  47 
  48     // avoid searching when it's obvious no full match can happen
  49     if len(s) < len(t) {
  50         return -1
  51     }
  52 
  53     r, _ := utf8.DecodeRuneInString(t)
  54     lowerLead := unicode.ToLower(r)
  55 
  56     for i, r := range s {
  57         // end search early when it's obvious no full match can happen
  58         if len(s)-i < len(t) {
  59             return -1
  60         }
  61 
  62         r = unicode.ToLower(r)
  63         // the implicit loop in func HasPrefixFold gives this func a
  64         // O(m*n) time-complexity, which isn't too bad in practice
  65         if r == lowerLead && HasPrefixFold(s[i:], t) {
  66             return i
  67         }
  68     }
  69 
  70     // no match found
  71     return -1
  72 }
  73 
  74 // LimitRunes prevents strings from having more runes than specified.
  75 func LimitRunes(s string, max int) string {
  76     if max < 1 {
  77         return ``
  78     }
  79 
  80     // avoid looping when string obviously isn't exceeding the max allowed,
  81     // since you can't have more runes than the total bytes the string uses
  82     if len(s) <= max {
  83         return s
  84     }
  85 
  86     n := 0
  87     for i := range s {
  88         if n == max {
  89             return s[:i]
  90         }
  91         n++
  92     }
  93     return s
  94 }
  95 
  96 // Decapitate trims up to n leading lines from the string given. The name makes
  97 // sense in contrast to the Linux program called `head`, which emits the first
  98 // n lines of text from its input.
  99 func Decapitate(s string, n int) string {
 100     for ; n > 0; n-- {
 101         i := strings.IndexRune(s, '\n')
 102         if i < 0 {
 103             return s
 104         }
 105         s = s[i+1:]
 106     }
 107     return s
 108 }
 109 
 110 // RepeatSafe is like strings.Repeat, except it's safe to use even when it's
 111 // given negative counts, which result in empty strings.
 112 func RepeatSafe(what string, times int) string {
 113     if times < 1 {
 114         return ``
 115     }
 116     return strings.Repeat(what, times)
 117 }
 118 
 119 // RepeatJoin is a generalization of strings.RepeatJoin, also giving you a
 120 // separator to insert between repetitions. This func handles negative counts
 121 // as 0, giving an empty string back: this makes it a safer alternative to
 122 // strings.Repeat, which panics when given a negative count.
 123 func RepeatJoin(what string, times int, sep string) string {
 124     // ensure (times - 1) is positive when growing the string-buffer later
 125     if times <= 0 {
 126         return ``
 127     }
 128 
 129     var sb strings.Builder
 130     sb.Grow(RepeatJoinLen(what, times, sep))
 131     RepeatJoinInto(what, times, sep, &sb)
 132     return sb.String()
 133 }
 134 
 135 // RepeatJoinLen calculates how many bytes the same arguments passed to func
 136 // RepeatJoin will need to generate the final string.
 137 func RepeatJoinLen(what string, times int, sep string) int {
 138     if times <= 0 {
 139         return 0
 140     }
 141     return times*len(what) + (times-1)*len(sep)
 142 }
 143 
 144 // RepeatJoinInto repeatedly appends a string, putting a separator between
 145 // every 2 repeated items.
 146 func RepeatJoinInto(what string, times int, sep string, sb *strings.Builder) {
 147     for i := 0; i < times; i++ {
 148         if i > 0 {
 149             sb.WriteString(sep)
 150         }
 151         sb.WriteString(what)
 152     }
 153 }
 154 
 155 // Squeeze space-trims a string aggressively, which means trimming spaces at
 156 // both ends, as well as preventing any repeating streak of consecutive spaces
 157 // anywhere inside the result.
 158 func Squeeze(s string) string {
 159     if strings.Contains(s, `  `) {
 160         var sb strings.Builder
 161         SqueezeInto(s, &sb)
 162         return sb.String()
 163     }
 164     return strings.TrimSpace(s)
 165 }
 166 
 167 // SqueezeInto space-trims a string aggressively into the string-builder given.
 168 // Aggressive trimming implies not just ignoring spaces at both ends, but also
 169 // avoiding repeating spaces anywhere in the source string.
 170 func SqueezeInto(s string, sb *strings.Builder) {
 171     s = strings.TrimSpace(s)
 172 
 173     var prev rune
 174     for _, r := range s {
 175         if r == ' ' && prev == ' ' {
 176             // skip repeating spaces
 177             continue
 178         }
 179         sb.WriteRune(r)
 180         prev = r
 181     }
 182 }
 183 
 184 // CapitalizeInto ensures each word-leading letter from the string given is
 185 // uppercased for the string-builder provided.
 186 func CapitalizeInto(s string, sb *strings.Builder) {
 187     // initialize using a space, to ensure the leading letter is capitalized
 188     prev := ' '
 189 
 190     for _, r := range s {
 191         // capitalize right after a space, and at start of string
 192         if (prev == ' ' || prev == '\t') && 'a' <= r && r <= 'z' {
 193             // r -= 32
 194             r = unicode.ToUpper(r)
 195         }
 196         sb.WriteRune(r)
 197         prev = r
 198     }
 199 }
 200 
 201 // PluralizeInto turns a word into its likely English plural, emitting the
 202 // result into the string-builder provided.
 203 func PluralizeInto(s string, sb *strings.Builder) {
 204     switch len(s) {
 205     case 0:
 206         // do nothing
 207 
 208     case 1:
 209         last := s[0]
 210         sb.WriteString(s)
 211 
 212         // s pluralizes as ses
 213         if last == 's' {
 214             sb.WriteString(`es`)
 215             return
 216         }
 217 
 218         if 'a' <= last && last <= 'z' {
 219             sb.WriteByte('s')
 220             return
 221         }
 222         if 'A' <= last && last <= 'Z' {
 223             sb.WriteByte('S')
 224             return
 225         }
 226 
 227     default:
 228         l := len(s)
 229         prev := s[l-2]
 230         last := s[l-1]
 231 
 232         // words ending in ss or sh
 233         if prev == 's' && (last == 's' || last == 'h') {
 234             sb.WriteString(s)
 235             sb.WriteString(`es`)
 236             return
 237         }
 238         if prev == 'S' && (last == 'S' || last == 'H') {
 239             sb.WriteString(s)
 240             sb.WriteString(`ES`)
 241             return
 242         }
 243 
 244         // words ending in y, but not in ay/ey/oy/uy, pluralize by dropping
 245         // the y and ending in ies
 246         if last == 'y' && !is_aeiou(prev) {
 247             sb.WriteString(s[:l-1])
 248             sb.WriteString(`ies`)
 249             return
 250         }
 251         if last == 'Y' && !is_AEIOU(prev) {
 252             sb.WriteString(s[:l-1])
 253             sb.WriteString(`IES`)
 254             return
 255         }
 256 
 257         // words ending in s or z
 258         if last == 's' || last == 'z' {
 259             sb.WriteString(s)
 260             sb.WriteString(`es`)
 261             return
 262         }
 263         if last == 'S' || last == 'Z' {
 264             sb.WriteString(s)
 265             sb.WriteString(`ES`)
 266             return
 267         }
 268 
 269         // all the other words
 270         sb.WriteString(s)
 271         if 'a' <= last && last <= 'z' {
 272             sb.WriteByte('s')
 273             return
 274         }
 275         if 'A' <= last && last <= 'Z' {
 276             sb.WriteByte('S')
 277             return
 278         }
 279     }
 280 }
 281 
 282 // is_aeiou simplifies code in func PluralizeInto
 283 func is_aeiou(c byte) bool {
 284     return c == 'a' || c == 'e' || c == 'i' || c == 'o' || c == 'u'
 285 }
 286 
 287 // is_AEIOU simplifies code in func PluralizeInto
 288 func is_AEIOU(c byte) bool {
 289     return c == 'A' || c == 'E' || c == 'I' || c == 'O' || c == 'U'
 290 }
 291 
 292 // SplitClean gives you an empty slice for an empty string: this is in contrast
 293 // to strings.Split, which in that case inconveniently returns a 1-item slice
 294 // with an an empty string as its only item.
 295 func SplitClean(s string, sep string) []string {
 296     if len(s) == 0 {
 297         return nil
 298     }
 299     return strings.Split(s, sep)
 300 }
 301 
 302 // MatchNames is a convenience function to match a slice of strings given to a
 303 // slice of candidates available, either using 1-based indices (negatives count
 304 // backward from the end), or as case-insensitive string matches. Result tells
 305 // whether each candidate was mentioned one way or another, and is always the
 306 // same length as the candidate-names slice.
 307 //
 308 // The original use case for this func is to help parsing cmd-line arguments
 309 // which select columns from tabular datasets.
 310 func MatchNames(indices []string, names []string) []bool {
 311     index := make([]bool, len(names))
 312     if len(indices) == 0 {
 313         return index
 314     }
 315 
 316     for _, v := range indices {
 317         // try 1-based numeric indices
 318         if p, err := strconv.Atoi(v); err == nil {
 319             // negative indices count backward from the end
 320             if p < 0 {
 321                 p += len(names) + 1
 322             }
 323             if p-1 < len(index) {
 324                 index[p-1] = true
 325             }
 326             continue
 327         }
 328 
 329         // try to match names case-insensitively
 330         for i, n := range names {
 331             if strings.EqualFold(v, n) {
 332                 index[i] = true
 333                 break
 334             }
 335         }
 336     }
 337 
 338     return index
 339 }
 340 
 341 // TrimBoth is a convenient shortcut to trim both prefix and suffix in 1 swoop.
 342 // Depending on the string value, trimming may or may not happen on each side,
 343 // independent of the other side.
 344 func TrimBoth(s string, prefix, suffix string) string {
 345     return strings.TrimSuffix(strings.TrimPrefix(s, prefix), suffix)
 346 }
 347 
 348 // After starts a string right after the other substring given ends. If the
 349 // latter string doesn't appear, the result is an empty string.
 350 func After(s string, prefix string) string {
 351     if i := strings.Index(s, prefix); i >= 0 {
 352         return s[i+len(prefix):]
 353     }
 354     return ``
 355 }
 356 
 357 // Before ends a string right before the other substring given begins. If
 358 // the latter string doesn't appear, the result is an empty string.
 359 func Before(s string, suffix string) string {
 360     if i := strings.Index(s, suffix); i >= 0 {
 361         return s[:i]
 362     }
 363     return s
 364 }
 365 
 366 // Since starts a string right when the other substring given begins. If the
 367 // latter string doesn't appear, the result is an empty string.
 368 func Since(s string, prefix string) string {
 369     if i := strings.Index(s, prefix); i >= 0 {
 370         return s[i:]
 371     }
 372     return ``
 373 }
 374 
 375 // Until ends a string right after the other substring given ends. If the
 376 // latter string doesn't appear, the result is an empty string.
 377 func Until(s string, suffix string) string {
 378     if i := strings.Index(s, suffix); i >= 0 {
 379         return s[:i+len(suffix)]
 380     }
 381     return s
 382 }
 383 
 384 // Default returns the first non-empty string among the values given. The result
 385 // is an empty string when all strings given are empty, or when none are given.
 386 func Default(args ...string) string {
 387     for _, s := range args {
 388         if len(s) > 0 {
 389             return s
 390         }
 391     }
 392     return ``
 393 }
 394 
 395 // SplitKeyValue is a shortcut to get a key-value string pair out of a string.
 396 // When the separator given isn't found, the last result if false, which means
 397 // you should check it to be sure the input was actually a key-value pair.
 398 func SplitKeyValue(kv string, sep string) (key string, value string, ok bool) {
 399     if i := strings.Index(kv, sep); i >= 0 {
 400         return kv[:i], kv[i+len(sep):], true
 401     }
 402     return ``, kv, false
 403 }
 404 
 405 func SplitRest(s string, sep string) (cur string, rest string, ok bool) {
 406     if i := strings.Index(s, sep); i >= 0 {
 407         return s[:i], s[i+len(sep):], true
 408     }
 409     return s, ``, false
 410 }
 411 
 412 func SplitRestByte(s string, sep byte) (cur string, rest string, ok bool) {
 413     if i := strings.IndexByte(s, sep); i >= 0 {
 414         return s[:i], s[i+1:], true
 415     }
 416     return s, ``, false
 417 }
 418 
 419 func SplitRestRune(s string, sep rune) (cur string, rest string, ok bool) {
 420     n := utf8.RuneLen(sep)
 421     if i := strings.IndexRune(s, sep); i >= 0 {
 422         return s[:i], s[i+n:], true
 423     }
 424     return s, ``, false
 425 }
 426 
 427 // SplitRuneIterate splits a string without allocating a slice, handing over
 428 // substring slices to the callback given instead.
 429 //
 430 // # Examples
 431 //
 432 //  // append split-items from some string into an existing string slice
 433 //  slice = slice[:0] // reuse existing allocated capacity
 434 //  stringsplus.SplitRuneIterate(line, sep, func(i int, s string) {
 435 //      slice = append(slice, s)
 436 //  })
 437 //
 438 //  // process Tab-Separated Values (TSV) with no extra allocations
 439 //  stringsplus.SplitRuneIterate(tsv, '\n', func(i int, line string) {
 440 //      line = strings.TrimSuffix(line, '\r') // ignore any carriage-returns
 441 //      stringsplus.SplitRuneIterate(line, '\t', func(i int, item string) {
 442 //          ... // do something with each item
 443 //      })
 444 //  })
 445 func SplitRuneIterate(s string, sep rune, f func(i int, s string)) {
 446     if len(s) == 0 {
 447         // return explicitly, or the loop would call the callback once on an
 448         // empty string, which isn't the expected behavior
 449         return
 450     }
 451 
 452     start := 0
 453     seplen := utf8.RuneLen(sep)
 454 
 455     for {
 456         if i := strings.IndexRune(s, sep); i >= 0 {
 457             f(start, s[:i])
 458             start += i + seplen
 459             s = s[i+seplen:]
 460             continue
 461         }
 462 
 463         // don't forget the last item
 464         f(start, s)
 465         return
 466     }
 467 }
 468 
 469 // IterateLines splits a string along its lines, without allocating a slice.
 470 // Trailing carriage returns are ignored before giving lines to the callback.
 471 //
 472 // Also, if string ends with a line-feed, the callback isn't called for the
 473 // last/trailing empty line.
 474 func IterateLines(s string, f func(i int, s string)) {
 475     if len(s) == 0 {
 476         // return explicitly, or the loop would call the callback once on an
 477         // empty string, which isn't the expected behavior
 478         return
 479     }
 480 
 481     // if string ends in \n or \r\n, ensure they don't count as an extra empty
 482     // last line
 483     s = TrimLastByte(s, '\n')
 484     s = TrimLastByte(s, '\r')
 485     start := 0
 486 
 487     for {
 488         i := strings.IndexByte(s, '\n')
 489         if i >= 0 {
 490             f(start, TrimLastByte(s[:i], '\r'))
 491             start += i + 1
 492             s = s[i+1:]
 493             continue
 494         }
 495 
 496         // don't forget the last item
 497         f(start, s)
 498         return
 499     }
 500 }
 501 
 502 // TrimLastByte ignores the byte given, but only when it's the last one in the
 503 // string: only 1 byte at most is ignored.
 504 func TrimLastByte(s string, last byte) string {
 505     if len(s) > 0 && s[len(s)-1] == last {
 506         return s[:len(s)-1]
 507     }
 508     return s
 509 }
 510 
 511 // TrimEOL ignores a trailing \n or a trailing \r\n sequence, but only once.
 512 // The name stands for `trim end of line`.
 513 func TrimEOL(s string) string {
 514     s = TrimLastByte(s, '\n')
 515     s = TrimLastByte(s, '\r')
 516     return s
 517 }
 518 
 519 // IterateFields works like SplitIterate, except it splits a string using runs
 520 // of spaces (1 or more) and tabs, ignoring leading/trailing spaces. This is
 521 // similar to how the `awk` program splits fields by default.
 522 func IterateFields(s string, f func(i int, s string)) {
 523     s = strings.TrimSpace(s)
 524 
 525     start := 0
 526     afterspace := false
 527     for i, r := range s {
 528         switch r {
 529         case ' ':
 530             if !afterspace {
 531                 f(start, strings.TrimSpace(s[start:i]))
 532                 start = i + 1
 533             }
 534             afterspace = true
 535 
 536         case '\t':
 537             f(start, strings.TrimSpace(s[start:i]))
 538             start = i + 1
 539             afterspace = true
 540 
 541         default:
 542             afterspace = false
 543         }
 544     }
 545 
 546     // don't forget the last item
 547     if start < len(s) {
 548         sub := strings.TrimSpace(s[start:])
 549         f(start, sub)
 550     }
 551 }
 552 
 553 // ReverseIterate works like func ReverseIterateRunes, looping on runes
 554 // backwards: the difference is that here your callback is given 1-rune
 555 // subslices of the original string, instead of individual runes.
 556 func ReverseIterate(s string, f func(i int, s string) (keepgoing bool)) {
 557     for i := len(s); len(s) > 0; {
 558         _, size := utf8.DecodeLastRuneInString(s)
 559         start := i - size
 560         if !f(start, s[start:i]) {
 561             return
 562         }
 563 
 564         s = s[:start]
 565         i -= size
 566     }
 567 }
 568 
 569 // ReverseIterateRunes loops over a string's runes in reverse order: it's
 570 // a `for i, r := range s` loop on a string, but going backwards.
 571 func ReverseIterateRunes(s string, f func(i int, r rune) (keepgoing bool)) {
 572     for i := len(s); len(s) > 0; {
 573         r, size := utf8.DecodeLastRuneInString(s)
 574         start := i - size
 575         if !f(start, r) {
 576             return
 577         }
 578         s = s[:start]
 579         i -= size
 580     }
 581 }
 582 
 583 // CountPieces returns how many items a string would split into, using the
 584 // separator given. Unlike strings.Split, an empty string doesn't contain 1
 585 // element when split (an empty string), but will result in 0 instead.
 586 func CountPieces(s string, sep string) int {
 587     if len(s) == 0 {
 588         return 0
 589     }
 590 
 591     if len(sep) == 0 {
 592         return utf8.RuneCountInString(s)
 593     }
 594     return strings.Count(s, sep) + 1
 595 }
 596 
 597 // MakeFieldSplitter is like func MakeSplitter, but handles space-separated
 598 // fields, as these may separate items using runs of multiple spaces, thus
 599 // requiring slightly different treatment than other kinds of separated-value
 600 // strings.
 601 func MakeFieldSplitter(cap int) func(s string) []string {
 602     var items []string
 603     if cap > 0 {
 604         items = make([]string, 0, cap)
 605     }
 606 
 607     return func(s string) []string {
 608         items = splitFieldsInto(s, items[:0])
 609         return items
 610     }
 611 }
 612 
 613 // splitFieldsInto helps funcs returned by func MakeFieldSplitter do their job
 614 func splitFieldsInto(s string, acc []string) []string {
 615     // some strings of fields may start with the 1st item indented to align
 616     // vertically as part of some multi-line output
 617     s = strings.TrimSpace(s)
 618 
 619     for len(s) > 0 {
 620         // try runs-of-spaces as the separator
 621         i := strings.IndexByte(s, ' ')
 622         if i >= 0 {
 623             // append substring until right before matched separator
 624             acc = append(acc, s[:i])
 625             // advance rest of the string right past the run of spaces
 626             s = trimPrefixByte(s[i+1:], ' ')
 627             continue
 628         }
 629 
 630         // try a single tab as the separator
 631         i = strings.IndexByte(s, '\t')
 632         if i >= 0 {
 633             // append substring until right before matched separator
 634             acc = append(acc, s[:i])
 635             // advance rest of the string right past separator
 636             s = s[i+1:]
 637             // while also ignoring any leading spaces after it
 638             s = trimPrefixByte(s, ' ')
 639         }
 640 
 641         // no more separators, so it's time to quit
 642         acc = append(acc, s)
 643         return acc
 644     }
 645 
 646     // needed only to make the compiler happy
 647     return acc
 648 }
 649 
 650 // MakeSplitter returns a func which splits strings using the separator given,
 651 // while also reusing/overwriting the slice across calls, thus minimizing
 652 // allocations.
 653 //
 654 // Like other split funcs from this package, it also returns empty slices when
 655 // given empty strings.
 656 //
 657 // It's safe to use negative values for the initial capacity, if preallocation
 658 // is not important.
 659 func MakeSplitter(sep string, cap int) func(s string) []string {
 660     var items []string
 661     if cap > 0 {
 662         items = make([]string, 0, cap)
 663     }
 664 
 665     return func(s string) []string {
 666         items = splitInto(s, sep, items[:0])
 667         return items
 668     }
 669 }
 670 
 671 // splitInto helps funcs returned by func MakeSplitter do their job
 672 func splitInto(s string, sep string, acc []string) []string {
 673     for len(s) > 0 {
 674         i := strings.Index(s, sep)
 675         if i >= 0 {
 676             // append substring until right before matched separator
 677             acc = append(acc, s[:i])
 678             // advance rest of the string right past separator
 679             s = s[i+len(sep):]
 680             continue
 681         }
 682 
 683         // no more separators, so it's time to quit
 684         acc = append(acc, s)
 685         return acc
 686     }
 687 
 688     // needed only to make the compiler happy
 689     return acc
 690 }
 691 
 692 // PickPiece is like using strings.Split(s, sep)[i], except it's both safer
 693 // and more efficient, never allocating memory. When given invalid indices,
 694 // it spares you a panic and returns an empty string instead.
 695 func PickPiece(s string, sep string, i int) string {
 696     // allow negative indices, which count backward from the end
 697     if i < 0 {
 698         i += CountPieces(s, sep)
 699         // if there aren't enough pieces, the result is the empty string
 700         if i < 0 {
 701             return ``
 702         }
 703     }
 704 
 705     for {
 706         j := strings.Index(s, sep)
 707         // see if this is the last piece
 708         if j < 0 {
 709             if i == 0 {
 710                 // found the right piece, which was the last one
 711                 return s
 712             }
 713 
 714             // no more pieces
 715             return ``
 716         }
 717 
 718         if i == 0 {
 719             // found the right piece
 720             return s[:j]
 721         }
 722 
 723         // advance to the next piece
 724         s = s[j+len(sep):]
 725         // one fewer piece to skip
 726         i--
 727     }
 728 }
 729 
 730 // TrimSlice gets rid of empty strings at both ends of a string slice: empty
 731 // strings can still be anywhere between the non-empty extremes in what's left
 732 // of the original slice.
 733 func TrimSlice(v []string) []string {
 734     // ignore empty strings from the beginning
 735     for len(v) > 0 && len(v[0]) == 0 {
 736         v = v[1:]
 737     }
 738 
 739     // ignore empty strings backward from the end
 740     for len(v) > 0 && len(v[len(v)-1]) == 0 {
 741         v = v[:len(v)-1]
 742     }
 743 
 744     // return what's left
 745     return v
 746 }
 747 
 748 // FirstRunes returns the first n runes, or the whole string if it's too short.
 749 func FirstRunes(s string, n int) string {
 750     return LimitRunes(s, n)
 751 }
 752 
 753 // LastRunes returns the last n runes, or the whole string if it's too short.
 754 func LastRunes(x string, n int) string {
 755     if n < 1 {
 756         return ``
 757     }
 758 
 759     // can't return a string longer than the one given
 760     if n >= len(x) {
 761         return x
 762     }
 763 
 764     i := 0
 765     s := x
 766     for len(s) > 0 && n > 0 {
 767         _, size := utf8.DecodeLastRuneInString(s)
 768         s = s[:len(s)-size]
 769         i += size
 770         n--
 771     }
 772 
 773     return x[len(x)-i:]
 774 }
 775 
 776 // SkipRunes skips up to n runes at the beginning of a string.
 777 func SkipRunes(s string, n int) string {
 778     return SkipFirstRunes(s, n)
 779 }
 780 
 781 // SkipFirstRunes skips up to n runes at the beginning of a string.
 782 func SkipFirstRunes(s string, n int) string {
 783     if len(s) <= n {
 784         return ``
 785     }
 786 
 787     for i := 0; i < n && len(s) > 0; i++ {
 788         _, size := utf8.DecodeRuneInString(s)
 789         s = s[size:]
 790     }
 791     return s
 792 }
 793 
 794 // SkipFirstRunes skips up to n runes at the beginning of a string.
 795 func SkipLastRunes(s string, n int) string {
 796     if len(s) <= n {
 797         return ``
 798     }
 799 
 800     for i := 0; i < n && len(s) > 0; i++ {
 801         _, size := utf8.DecodeLastRuneInString(s)
 802         s = s[:len(s)-size]
 803     }
 804     return s
 805 }
 806 
 807 // SliceRunes lets you slice a string as if it were an array of runes. It
 808 // also lets you slice away safely, as invalid indices result in an empty
 809 // string, instead of a panic.
 810 func SliceRunes(s string, i, j int) string {
 811     if len(s) == 0 || i >= j || i >= len(s) || i < 0 || j < 0 {
 812         return s
 813     }
 814 
 815     end := 0
 816     s = SkipFirstRunes(s, i)
 817     for ; i < j && len(s) > 0; i++ {
 818         _, size := utf8.DecodeRuneInString(s)
 819         end += size
 820     }
 821     return s[:end]
 822 }
 823 
 824 // HasPrefixFold checks whether a string starts as given, ignoring any
 825 // letter-case differences. This is unlike strings.HasPrefix, which does
 826 // care about different letter-casing.
 827 func HasPrefixFold(s, prefix string) bool {
 828     n := len(prefix)
 829     return len(s) >= n && strings.EqualFold(s[:n], prefix)
 830 }
 831 
 832 // HasSuffixFold checks whether a string ends as given, ignoring any
 833 // letter-case differences. This is unlike strings.HasSuffix, which does
 834 // care about different letter-casing.
 835 func HasSuffixFold(s string, suffix string) bool {
 836     n := len(s) - len(suffix)
 837     return n >= 0 && strings.EqualFold(s[n:], suffix)
 838 }
 839 
 840 // IndexRuneFold returns the index of the first occurrence of the rune given,
 841 // ignoring letter-case; an invalid negative index is returned when no match
 842 // is found. This func is unlike strings.IndexRune, which does care about
 843 // different letter-casing.
 844 func IndexRuneFold(s string, r rune) int {
 845     r = unicode.ToLower(r)
 846     for i, v := range s {
 847         if unicode.ToLower(v) == r {
 848             return i
 849         }
 850     }
 851 
 852     // no match found
 853     return -1
 854 }
 855 
 856 // IndexEOL finds where the next end-of-line sequence starts and how many
 857 // bytes it lasts: a lone LF byte is just 1 byte, while a CRLF combo is 2
 858 // bytes long. When no match is found, the index returned is less than 0.
 859 func IndexEOL(s string) (index int, size int) {
 860     for i := 0; len(s) > 0; i++ {
 861         b := s[0]
 862 
 863         // handle stand-along line-feeds
 864         if b == '\n' {
 865             return i, 1
 866         }
 867 
 868         // handle CRLF byte-pairs
 869         if b == '\r' {
 870             if len(s) > 1 && s[1] == '\n' {
 871                 return i, 2
 872             }
 873         }
 874 
 875         s = s[1:]
 876     }
 877 
 878     // no match found
 879     return -1, 0
 880 }
 881 
 882 // CountNonANSI counts how many runes a string has, excluding those belonging
 883 // to ANSI styling sequences, which start with the escape rune and end with
 884 // the 'm' letter.
 885 //
 886 // For strings with no ANSI style-sequences, this func's results are the same
 887 // as func utf8.RuneCountInString's.
 888 func CountNonANSI(s string) int {
 889     n := 0
 890     ansi := false
 891 
 892     for _, r := range s {
 893         // the only ANSI style-sequences supported always end with 'm'
 894         if ansi {
 895             ansi = r != 'm'
 896             continue
 897         }
 898 
 899         // escape starts an ANSI style-sequence
 900         if r == 0x1b {
 901             ansi = true
 902             continue
 903         }
 904 
 905         // count all runes outside ANSI style-sequences
 906         n++
 907     }
 908 
 909     return n
 910 }
 911 
 912 // CompareNumericStrings first tries to compare the strings as parsed numbers,
 913 // and only failing that it compares the usual way.
 914 func CompareNumericStrings(s, t string) int {
 915     x, errx := strconv.ParseFloat(s, 64)
 916     if errx != nil || math.IsNaN(x) || math.IsInf(x, 0) {
 917         return strings.Compare(s, t)
 918     }
 919 
 920     y, erry := strconv.ParseFloat(t, 64)
 921     if erry != nil || math.IsNaN(y) || math.IsInf(y, 0) {
 922         return strings.Compare(s, t)
 923     }
 924 
 925     diff := x - y
 926     if diff > 0 {
 927         return +1
 928     }
 929     if diff < 0 {
 930         return -1
 931     }
 932     return 0
 933 }
 934 
 935 // ToString turns most simple values into a suitable string representation.
 936 func ToString(x interface{}) (s string, ok bool) {
 937     switch x := x.(type) {
 938     case nil:
 939         return `null`, true
 940 
 941     case bool:
 942         if x {
 943             return `true`, true
 944         }
 945         return `false`, true
 946 
 947     case rune:
 948         return string(x), true
 949 
 950     case int:
 951         return strconv.FormatInt(int64(x), 10), true
 952 
 953     case uint:
 954         return strconv.FormatUint(uint64(x), 10), true
 955 
 956     case int64:
 957         return strconv.FormatInt(x, 10), true
 958 
 959     case uint64:
 960         return strconv.FormatUint(x, 10), true
 961 
 962     case float64:
 963         var buf [64]byte
 964         res := strconv.AppendFloat(buf[:0], x, 'f', 16, 64)
 965         return string(trimDecimals(res)), true
 966 
 967     case string:
 968         return x, true
 969 
 970     default:
 971         return ``, false
 972     }
 973 }

     File: ./strings_test.go
   1 package stringsplus
   2 
   3 import (
   4     "bytes"
   5     "strconv"
   6     "strings"
   7     "testing"
   8 )
   9 
  10 func TestConstants(t *testing.T) {
  11     if len(Spaces) != 256 {
  12         const fs = `spaces string contant is %d bytes long`
  13         t.Fatalf(fs, len(Spaces))
  14     }
  15 
  16     if len(Tabs) != 256 {
  17         const fs = `tabs string contant is %d bytes long`
  18         t.Fatalf(fs, len(Tabs))
  19     }
  20 
  21     if len(ASCII) != 128 {
  22         t.Fatalf(`ascii string constant is %d bytes long`, len(ASCII))
  23     }
  24 }
  25 
  26 func TestPluralize(t *testing.T) {
  27     tests := []struct {
  28         singular string
  29         plural   string
  30     }{
  31         {``, ``},
  32         {`a`, `as`},
  33 
  34         {`abc`, `abcs`},
  35         {`car`, `cars`},
  36         {`tree`, `trees`},
  37         {`horse`, `horses`},
  38 
  39         {`fish`, `fishes`},
  40         {`bliss`, `blisses`},
  41 
  42         {`bay`, `bays`},
  43         {`harley`, `harleys`},
  44         {`toy`, `toys`},
  45         {`guy`, `guys`},
  46 
  47         {`wy`, `wies`},
  48         {`lolly`, `lollies`},
  49         {`party`, `parties`},
  50     }
  51 
  52     var sb strings.Builder
  53     for _, tc := range tests {
  54         t.Run(tc.singular, func(t *testing.T) {
  55             sb.Reset()
  56             src := tc.singular
  57             exp := tc.plural
  58             PluralizeInto(src, &sb)
  59 
  60             got := sb.String()
  61             if got != exp {
  62                 const fs = `plural of %q is %q, but got %q instead`
  63                 t.Fatalf(fs, src, exp, got)
  64                 return
  65             }
  66 
  67             sb.Reset()
  68             src = strings.ToUpper(tc.singular)
  69             exp = strings.ToUpper(tc.plural)
  70             PluralizeInto(src, &sb)
  71 
  72             got = sb.String()
  73             if got != exp {
  74                 const fs = `plural of %q is %q, but got %q instead`
  75                 t.Fatalf(fs, src, exp, got)
  76                 return
  77             }
  78         })
  79     }
  80 }
  81 
  82 func TestIndexFold(t *testing.T) {
  83     var tests = []struct {
  84         s string
  85         t string
  86     }{
  87         {``, ``},
  88         {`abc`, ``},
  89         {``, `def`},
  90         {`abc`, `def`},
  91         {`abcdef`, `def`},
  92         {`abcdef`, `DEF`},
  93         {`AbCdEf`, `def`},
  94         {`AbCdEf`, `cde`},
  95         {`abcd`, `abc`},
  96     }
  97 
  98     for _, tc := range tests {
  99         t.Run(tc.s+"\t"+tc.t, func(t *testing.T) {
 100             got := IndexFold(tc.s, tc.t)
 101             exp := strings.Index(strings.ToLower(tc.s), strings.ToLower(tc.t))
 102             if got != exp {
 103                 t.Fatalf(`got %d instead of %d`, got, exp)
 104             }
 105         })
 106     }
 107 }
 108 
 109 func TestPrefixFold(t *testing.T) {
 110     var tests = []struct {
 111         s string
 112         t string
 113     }{
 114         {``, ``},
 115         {`abc`, ``},
 116         {``, `def`},
 117         {`abc`, `def`},
 118         {`abc`, `Ab`},
 119         {`abc`, `aB`},
 120         {`abcdef`, `def`},
 121         {`abcdef`, `DEF`},
 122         {`abcdef`, `ABC`},
 123         {`abcdef`, `abc`},
 124         {`AbCdEf`, `def`},
 125         {`abcd`, `abc`},
 126     }
 127 
 128     for _, tc := range tests {
 129         t.Run(tc.s+"\t"+tc.t, func(t *testing.T) {
 130             got := HasPrefixFold(tc.s, tc.t)
 131             exp := strings.HasPrefix(strings.ToLower(tc.s), strings.ToLower(tc.t))
 132             if got != exp {
 133                 t.Fatalf(`got %v instead of %v`, got, exp)
 134             }
 135         })
 136     }
 137 }
 138 
 139 func TestRepeatJoin(t *testing.T) {
 140     tests := []struct {
 141         Name      string
 142         What      string
 143         Times     int
 144         Separator string
 145         Expected  string
 146     }{
 147         {`negative`, `asd sasd`, -4, `.`, ``},
 148         {`zero`, `asd sasd`, 0, `.`, ``},
 149         {`normal`, `asd sasd`, 3, `.`, `asd sasd.asd sasd.asd sasd`},
 150         {`empty separator`, `asd sasd`, 3, ``, `asd sasdasd sasdasd sasd`},
 151     }
 152 
 153     for _, tc := range tests {
 154         t.Run(tc.Name, func(t *testing.T) {
 155             const fs = `repeat(%q%d%q): expected %q, but got %q instead`
 156             got := RepeatJoin(tc.What, tc.Times, tc.Separator)
 157 
 158             if got != tc.Expected {
 159                 t.Fatalf(fs, tc.What, tc.Times, tc.Separator, tc.Expected, got)
 160             }
 161         })
 162     }
 163 }
 164 
 165 func TestSplitRuneIterate(t *testing.T) {
 166     tests := []struct {
 167         Input     string
 168         Separator rune
 169         Expected  []string
 170     }{
 171         {
 172             Input:     ``,
 173             Separator: '\t',
 174             Expected:  []string{},
 175         },
 176         {
 177             Input:     "abc\tdef\t\t123 test",
 178             Separator: '\t',
 179             Expected:  []string{`abc`, `def`, ``, `123 test`},
 180         },
 181         {
 182             Input:     "abc\tdef\t",
 183             Separator: '\t',
 184             Expected:  []string{`abc`, `def`, ``},
 185         },
 186         {
 187             Input:     "abc\tdef\t\t",
 188             Separator: '\t',
 189             Expected:  []string{`abc`, `def`, ``, ``},
 190         },
 191     }
 192 
 193     for _, tc := range tests {
 194         t.Run(tc.Input, func(t *testing.T) {
 195             var items []string
 196             SplitRuneIterate(tc.Input, tc.Separator, func(i int, s string) {
 197                 items = append(items, s)
 198             })
 199 
 200             got := items
 201             exp := tc.Expected
 202             for len(got) > 0 && len(exp) > 0 {
 203                 if got[0] != exp[0] {
 204                     t.Fatalf(`expected %#v, but got %#v`, tc.Expected, items)
 205                     return
 206                 }
 207                 got = got[1:]
 208                 exp = exp[1:]
 209             }
 210 
 211             if len(got) != len(exp) {
 212                 t.Fatalf(`expected %#v, but got %#v`, tc.Expected, items)
 213             }
 214         })
 215     }
 216 }
 217 
 218 func TestIterateLines(t *testing.T) {
 219     tests := []struct {
 220         Input    string
 221         Expected []string
 222     }{
 223         {``, []string{}},
 224         {"abc\ndef\n\n123  test", []string{`abc`, `def`, ``, `123  test`}},
 225         {"abc\ndef\n\n123  test\n", []string{`abc`, `def`, ``, `123  test`}},
 226     }
 227 
 228     for _, tc := range tests {
 229         t.Run(tc.Input, func(t *testing.T) {
 230             var items []string
 231             IterateLines(tc.Input, func(i int, s string) {
 232                 items = append(items, s)
 233             })
 234 
 235             got := items
 236             exp := tc.Expected
 237             for len(got) > 0 && len(exp) > 0 {
 238                 if got[0] != exp[0] {
 239                     t.Fatalf(`expected %#v, but got %#v`, tc.Expected, items)
 240                     return
 241                 }
 242                 got = got[1:]
 243                 exp = exp[1:]
 244             }
 245 
 246             if len(got) != len(exp) {
 247                 t.Fatalf(`expected %#v, but got %#v`, tc.Expected, items)
 248             }
 249         })
 250     }
 251 }
 252 
 253 func TestIterateFields(t *testing.T) {
 254     tests := []struct {
 255         Input    string
 256         Expected []string
 257     }{
 258         {``, []string{}},
 259         {"abc\tdef\t\t 123  test", []string{`abc`, `def`, ``, `123`, `test`}},
 260     }
 261 
 262     for _, tc := range tests {
 263         t.Run(tc.Input, func(t *testing.T) {
 264             var items []string
 265 
 266             IterateFields(tc.Input, func(i int, s string) {
 267                 items = append(items, s)
 268             })
 269 
 270             got := items
 271             exp := tc.Expected
 272             for len(got) > 0 && len(exp) > 0 {
 273                 if got[0] != exp[0] {
 274                     t.Fatalf(`expected %#v, but got %#v`, tc.Expected, items)
 275                     return
 276                 }
 277                 got = got[1:]
 278                 exp = exp[1:]
 279             }
 280 
 281             if len(got) != len(exp) {
 282                 t.Fatalf(`expected %#v, but got %#v`, tc.Expected, items)
 283             }
 284         })
 285     }
 286 }
 287 
 288 func TestCountPieces(t *testing.T) {
 289     tests := []struct {
 290         Input     string
 291         Separator string
 292         Expected  int
 293     }{
 294         {``, `/`, 0},
 295         {`a`, `/`, 1},
 296         {`a/`, `/`, 2},
 297         {`a/bcx`, `/`, 2},
 298         {`a/bcx/123`, `/`, 3},
 299         {`a/bcx/123/55`, `/`, 4},
 300         {`a/##/bcx/##/123##/##55`, `/#`, 4},
 301     }
 302 
 303     for _, tc := range tests {
 304         t.Run(tc.Input, func(t *testing.T) {
 305             got := CountPieces(tc.Input, tc.Separator)
 306             if got != tc.Expected {
 307                 const fs = `item-count should be %d, got %d instead`
 308                 t.Fatalf(fs, tc.Expected, got)
 309             }
 310         })
 311     }
 312 }
 313 
 314 func TestPickPiece(t *testing.T) {
 315     tests := []struct {
 316         Input     string
 317         Index     int
 318         Separator string
 319         Expected  string
 320     }{
 321         {``, 4, `/`, ``},
 322         {`a/bcx/123/55`, 2, `/`, `123`},
 323 &n