File: ./avoid/avoid.go
   1 /*
   2 The MIT License (MIT)
   3 
   4 Copyright (c) 2026 pacman64
   5 
   6 Permission is hereby granted, free of charge, to any person obtaining a copy of
   7 this software and associated documentation files (the "Software"), to deal
   8 in the Software without restriction, including without limitation the rights to
   9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
  10 of the Software, and to permit persons to whom the Software is furnished to do
  11 so, subject to the following conditions:
  12 
  13 The above copyright notice and this permission notice shall be included in all
  14 copies or substantial portions of the Software.
  15 
  16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  22 SOFTWARE.
  23 */
  24 
  25 package avoid
  26 
  27 import (
  28     "bufio"
  29     "bytes"
  30     "io"
  31     "os"
  32     "regexp"
  33 )
  34 
  35 const info = `
  36 avoid [options...] [regular expressions...]
  37 
  38 Avoid/ignore lines which match any of the extended-mode regular expressions
  39 given. When not given any regex, all empty lines are ignored by default.
  40 
  41 The options are, available both in single and double-dash versions
  42 
  43     -h, -help    show this help message
  44     -i, -ins     match regexes case-insensitively
  45 `
  46 
  47 func Main() {
  48     nerr := 0
  49     buffered := false
  50     sensitive := true
  51     args := os.Args[1:]
  52 
  53     for len(args) > 0 {
  54         switch args[0] {
  55         case `-b`, `--b`, `-buffered`, `--buffered`:
  56             buffered = true
  57             args = args[1:]
  58             continue
  59 
  60         case `-h`, `--h`, `-help`, `--help`:
  61             os.Stdout.WriteString(info[1:])
  62             return
  63 
  64         case `-i`, `--i`, `-ins`, `--ins`:
  65             sensitive = false
  66             args = args[1:]
  67             continue
  68         }
  69 
  70         break
  71     }
  72 
  73     if len(args) > 0 && args[0] == `--` {
  74         args = args[1:]
  75     }
  76 
  77     liveLines := !buffered
  78     if !buffered {
  79         if _, err := os.Stdout.Seek(0, io.SeekCurrent); err == nil {
  80             liveLines = false
  81         }
  82     }
  83 
  84     if len(args) == 0 {
  85         args = []string{`^$`}
  86     }
  87 
  88     exprs := make([]*regexp.Regexp, 0, len(args))
  89 
  90     for _, src := range args {
  91         var err error
  92         var exp *regexp.Regexp
  93         if !sensitive {
  94             exp, err = regexp.Compile(`(?i)` + src)
  95         } else {
  96             exp, err = regexp.Compile(src)
  97         }
  98 
  99         if err != nil {
 100             os.Stderr.WriteString(err.Error())
 101             os.Stderr.WriteString("\n")
 102             nerr++
 103         }
 104 
 105         exprs = append(exprs, exp)
 106     }
 107 
 108     if nerr > 0 {
 109         os.Exit(1)
 110     }
 111 
 112     var buf []byte
 113     sc := bufio.NewScanner(os.Stdin)
 114     sc.Buffer(nil, 8*1024*1024*1024)
 115     bw := bufio.NewWriter(os.Stdout)
 116 
 117     for i := 0; sc.Scan(); i++ {
 118         line := sc.Bytes()
 119         if i == 0 && bytes.HasPrefix(line, []byte{0xef, 0xbb, 0xbf}) {
 120             line = line[3:]
 121         }
 122 
 123         s := line
 124         if bytes.IndexByte(s, '\x1b') >= 0 {
 125             buf = plain(buf[:0], s)
 126             s = buf
 127         }
 128 
 129         if !match(s, exprs) {
 130             bw.Write(line)
 131             bw.WriteByte('\n')
 132 
 133             if !liveLines {
 134                 continue
 135             }
 136 
 137             if err := bw.Flush(); err != nil {
 138                 return
 139             }
 140         }
 141     }
 142 }
 143 
 144 func match(what []byte, with []*regexp.Regexp) bool {
 145     for _, e := range with {
 146         if e.Match(what) {
 147             return true
 148         }
 149     }
 150     return false
 151 }
 152 
 153 func plain(dst []byte, src []byte) []byte {
 154     for len(src) > 0 {
 155         i, j := indexEscapeSequence(src)
 156         if i < 0 {
 157             dst = append(dst, src...)
 158             break
 159         }
 160         if j < 0 {
 161             j = len(src)
 162         }
 163 
 164         if i > 0 {
 165             dst = append(dst, src[:i]...)
 166         }
 167 
 168         src = src[j:]
 169     }
 170 
 171     return dst
 172 }
 173 
 174 // indexEscapeSequence finds the first ANSI-style escape-sequence, which is
 175 // the multi-byte sequences starting with ESC[; the result is a pair of slice
 176 // indices which can be independently negative when either the start/end of
 177 // a sequence isn't found; given their fairly-common use, even the hyperlink
 178 // ESC]8 sequences are supported
 179 func indexEscapeSequence(s []byte) (int, int) {
 180     var prev byte
 181 
 182     for i, b := range s {
 183         if prev == '\x1b' && b == '[' {
 184             j := indexLetter(s[i+1:])
 185             if j < 0 {
 186                 return i, -1
 187             }
 188             return i - 1, i + 1 + j + 1
 189         }
 190 
 191         if prev == '\x1b' && b == ']' && i+1 < len(s) && s[i+1] == '8' {
 192             j := indexPair(s[i+1:], '\x1b', '\\')
 193             if j < 0 {
 194                 return i, -1
 195             }
 196             return i - 1, i + 1 + j + 2
 197         }
 198 
 199         prev = b
 200     }
 201 
 202     return -1, -1
 203 }
 204 
 205 func indexLetter(s []byte) int {
 206     for i, b := range s {
 207         upper := b &^ 32
 208         if 'A' <= upper && upper <= 'Z' {
 209             return i
 210         }
 211     }
 212 
 213     return -1
 214 }
 215 
 216 func indexPair(s []byte, x byte, y byte) int {
 217     var prev byte
 218 
 219     for i, b := range s {
 220         if prev == x && b == y && i > 0 {
 221             return i
 222         }
 223         prev = b
 224     }
 225 
 226     return -1
 227 }
     File: ./bytedump/bytedump.go
   1 /*
   2 The MIT License (MIT)
   3 
   4 Copyright (c) 2026 pacman64
   5 
   6 Permission is hereby granted, free of charge, to any person obtaining a copy of
   7 this software and associated documentation files (the "Software"), to deal
   8 in the Software without restriction, including without limitation the rights to
   9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
  10 of the Software, and to permit persons to whom the Software is furnished to do
  11 so, subject to the following conditions:
  12 
  13 The above copyright notice and this permission notice shall be included in all
  14 copies or substantial portions of the Software.
  15 
  16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  22 SOFTWARE.
  23 */
  24 
  25 package bytedump
  26 
  27 import (
  28     "bufio"
  29     "fmt"
  30     "io"
  31     "math"
  32     "os"
  33     "strconv"
  34     "strings"
  35 )
  36 
  37 const info = `
  38 bytedump [options...] [filenames...]
  39 
  40 Show bytes as hexadecimal and ascii on the side.
  41 
  42 Each line shows the starting offset for the bytes shown, 16 of the bytes
  43 themselves in base-16 notation, and any ASCII codes when the byte values
  44 are in the typical ASCII range.
  45 
  46 The ASCII codes always include 2 rows, which makes the output more 'grep'
  47 friendly, since strings up to 32 items can't be accidentally missed. The
  48 offsets shown are base-16.
  49 `
  50 
  51 const perLine = 16
  52 
  53 // hexSymbols is a direct lookup table combining 2 hex digits with either a
  54 // space or a displayable ASCII symbol matching the byte's own ASCII value;
  55 // this table was autogenerated by running the command
  56 //
  57 // seq 0 255 | ./hex-symbols.awk
  58 var hexSymbols = [256]string{
  59     `00 `, `01 `, `02 `, `03 `, `04 `, `05 `, `06 `, `07 `,
  60     `08 `, `09 `, `0a `, `0b `, `0c `, `0d `, `0e `, `0f `,
  61     `10 `, `11 `, `12 `, `13 `, `14 `, `15 `, `16 `, `17 `,
  62     `18 `, `19 `, `1a `, `1b `, `1c `, `1d `, `1e `, `1f `,
  63     `20 `, `21!`, `22"`, `23#`, `24$`, `25%`, `26&`, `27'`,
  64     `28(`, `29)`, `2a*`, `2b+`, `2c,`, `2d-`, `2e.`, `2f/`,
  65     `300`, `311`, `322`, `333`, `344`, `355`, `366`, `377`,
  66     `388`, `399`, `3a:`, `3b;`, `3c<`, `3d=`, `3e>`, `3f?`,
  67     `40@`, `41A`, `42B`, `43C`, `44D`, `45E`, `46F`, `47G`,
  68     `48H`, `49I`, `4aJ`, `4bK`, `4cL`, `4dM`, `4eN`, `4fO`,
  69     `50P`, `51Q`, `52R`, `53S`, `54T`, `55U`, `56V`, `57W`,
  70     `58X`, `59Y`, `5aZ`, `5b[`, `5c\`, `5d]`, `5e^`, `5f_`,
  71     "60`", `61a`, `62b`, `63c`, `64d`, `65e`, `66f`, `67g`,
  72     `68h`, `69i`, `6aj`, `6bk`, `6cl`, `6dm`, `6en`, `6fo`,
  73     `70p`, `71q`, `72r`, `73s`, `74t`, `75u`, `76v`, `77w`,
  74     `78x`, `79y`, `7az`, `7b{`, `7c|`, `7d}`, `7e~`, `7f `,
  75     `80 `, `81 `, `82 `, `83 `, `84 `, `85 `, `86 `, `87 `,
  76     `88 `, `89 `, `8a `, `8b `, `8c `, `8d `, `8e `, `8f `,
  77     `90 `, `91 `, `92 `, `93 `, `94 `, `95 `, `96 `, `97 `,
  78     `98 `, `99 `, `9a `, `9b `, `9c `, `9d `, `9e `, `9f `,
  79     `a0 `, `a1 `, `a2 `, `a3 `, `a4 `, `a5 `, `a6 `, `a7 `,
  80     `a8 `, `a9 `, `aa `, `ab `, `ac `, `ad `, `ae `, `af `,
  81     `b0 `, `b1 `, `b2 `, `b3 `, `b4 `, `b5 `, `b6 `, `b7 `,
  82     `b8 `, `b9 `, `ba `, `bb `, `bc `, `bd `, `be `, `bf `,
  83     `c0 `, `c1 `, `c2 `, `c3 `, `c4 `, `c5 `, `c6 `, `c7 `,
  84     `c8 `, `c9 `, `ca `, `cb `, `cc `, `cd `, `ce `, `cf `,
  85     `d0 `, `d1 `, `d2 `, `d3 `, `d4 `, `d5 `, `d6 `, `d7 `,
  86     `d8 `, `d9 `, `da `, `db `, `dc `, `dd `, `de `, `df `,
  87     `e0 `, `e1 `, `e2 `, `e3 `, `e4 `, `e5 `, `e6 `, `e7 `,
  88     `e8 `, `e9 `, `ea `, `eb `, `ec `, `ed `, `ee `, `ef `,
  89     `f0 `, `f1 `, `f2 `, `f3 `, `f4 `, `f5 `, `f6 `, `f7 `,
  90     `f8 `, `f9 `, `fa `, `fb `, `fc `, `fd `, `fe `, `ff `,
  91 }
  92 
  93 func Main() {
  94     args := os.Args[1:]
  95 
  96     if len(args) > 0 {
  97         switch args[0] {
  98         case `-h`, `--h`, `-help`, `--help`:
  99             os.Stdout.WriteString(info[1:])
 100             return
 101         }
 102     }
 103 
 104     if len(args) > 0 && args[0] == `--` {
 105         args = args[1:]
 106     }
 107 
 108     if err := run(args); err != nil {
 109         os.Stdout.WriteString(err.Error())
 110         os.Stdout.WriteString("\n")
 111         os.Exit(1)
 112     }
 113 }
 114 
 115 func run(args []string) error {
 116     w := bufio.NewWriterSize(os.Stdout, 32*1024)
 117     defer w.Flush()
 118 
 119     // with no filenames given, handle stdin and quit
 120     if len(args) == 0 {
 121         return handle(w, os.Stdin, `<stdin>`, -1)
 122     }
 123 
 124     for i, fname := range args {
 125         if i > 0 {
 126             w.WriteString("\n")
 127             w.WriteString("\n")
 128         }
 129 
 130         if err := handleFile(w, fname); err != nil {
 131             return err
 132         }
 133     }
 134 
 135     return nil
 136 }
 137 
 138 func handleFile(w *bufio.Writer, fname string) error {
 139     f, err := os.Open(fname)
 140     if err != nil {
 141         return err
 142     }
 143     defer f.Close()
 144 
 145     stat, err := f.Stat()
 146     if err != nil {
 147         return handle(w, f, fname, -1)
 148     }
 149 
 150     fsize := int(stat.Size())
 151     return handle(w, f, fname, fsize)
 152 }
 153 
 154 // handle shows some messages related to the input and the cmd-line options
 155 // used, and then follows them by the hexadecimal byte-view
 156 func handle(w *bufio.Writer, r io.Reader, name string, size int) error {
 157     owidth10 := -1
 158     owidth16 := -1
 159     if size > 0 {
 160         w10 := math.Log10(float64(size))
 161         w10 = math.Max(math.Ceil(w10), 1)
 162         w16 := math.Log2(float64(size)) / 4
 163         w16 = math.Max(math.Ceil(w16), 1)
 164         owidth10 = int(w10)
 165         owidth16 = int(w16)
 166     }
 167 
 168     if owidth10 < 0 {
 169         owidth10 = 8
 170     }
 171     if owidth16 < 0 {
 172         owidth16 = 8
 173     }
 174 
 175     rc := rendererConfig{
 176         out:           w,
 177         offsetWidth10: max(owidth10, 8),
 178         offsetWidth16: max(owidth16, 8),
 179     }
 180 
 181     if size < 0 {
 182         fmt.Fprintf(w, "• %s\n", name)
 183     } else {
 184         const fs = "• %s  (%s bytes)\n"
 185         fmt.Fprintf(w, fs, name, sprintCommas(size))
 186     }
 187     w.WriteByte('\n')
 188 
 189     // calling func Read directly can sometimes result in chunks shorter
 190     // than the max chunk-size, even when there are plenty of bytes yet
 191     // to read; to avoid that, use a buffered-reader to explicitly fill
 192     // a slice instead
 193     br := bufio.NewReader(r)
 194 
 195     // to show ASCII up to 1 full chunk ahead, 2 chunks are needed
 196     cur := make([]byte, 0, perLine)
 197     ahead := make([]byte, 0, perLine)
 198 
 199     // the ASCII-panel's wide output requires staying 1 step/chunk behind,
 200     // so to speak
 201     cur, err := fillChunk(cur[:0], perLine, br)
 202     if len(cur) == 0 {
 203         if err == io.EOF {
 204             err = nil
 205         }
 206         return err
 207     }
 208 
 209     for {
 210         ahead, err := fillChunk(ahead[:0], perLine, br)
 211         if err != nil && err != io.EOF {
 212             return err
 213         }
 214 
 215         if len(ahead) == 0 {
 216             // done, maybe except for an extra line of output
 217             break
 218         }
 219 
 220         // show the byte-chunk on its own output line
 221         if err := writeChunk(rc, cur, ahead); err != nil {
 222             return io.EOF
 223         }
 224 
 225         rc.chunks++
 226         rc.offset += uint(len(cur))
 227         cur = cur[:copy(cur, ahead)]
 228     }
 229 
 230     // don't forget the last output line
 231     if rc.chunks > 0 && len(cur) > 0 {
 232         return writeChunk(rc, cur, nil)
 233     }
 234     return nil
 235 }
 236 
 237 // fillChunk tries to read the number of bytes given, appending them to the
 238 // byte-slice given; this func returns an EOF error only when no bytes are
 239 // read, which somewhat simplifies error-handling for the func caller
 240 func fillChunk(chunk []byte, n int, br *bufio.Reader) ([]byte, error) {
 241     // read buffered-bytes up to the max chunk-size
 242     for i := 0; i < n; i++ {
 243         b, err := br.ReadByte()
 244         if err == nil {
 245             chunk = append(chunk, b)
 246             continue
 247         }
 248 
 249         if err == io.EOF && i > 0 {
 250             return chunk, nil
 251         }
 252         return chunk, err
 253     }
 254 
 255     // got the full byte-count asked for
 256     return chunk, nil
 257 }
 258 
 259 // rendererConfig groups several arguments given to any of the rendering funcs
 260 type rendererConfig struct {
 261     // out is writer to send all output to
 262     out *bufio.Writer
 263 
 264     // offset is the byte-offset of the first byte shown on the current output
 265     // line: if shown at all, it's shown at the start the line
 266     offset uint
 267 
 268     // chunks is the 0-based counter for byte-chunks/lines shown so far, which
 269     // indirectly keeps track of when it's time to show a `breather` line
 270     chunks uint
 271 
 272     // offsetWidth10 is the max string-width for the base-10 byte-offsets
 273     // shown at the start of output lines, and determines those values'
 274     // left-padding
 275     offsetWidth10 int
 276 
 277     // offsetWidth16 is the max string-width for the base-16 byte-offsets
 278     // shown at the start of output lines, and determines those values'
 279     // left-padding
 280     offsetWidth16 int
 281 }
 282 
 283 // loopThousandsGroups comes from my lib/package `mathplus`: that's why it
 284 // handles negatives, even though this app only uses it with non-negatives.
 285 func loopThousandsGroups(n int, fn func(i, n int)) {
 286     // 0 doesn't have a log10
 287     if n == 0 {
 288         fn(0, 0)
 289         return
 290     }
 291 
 292     sign := +1
 293     if n < 0 {
 294         n = -n
 295         sign = -1
 296     }
 297 
 298     intLog1000 := int(math.Log10(float64(n)) / 3)
 299     remBase := int(math.Pow10(3 * intLog1000))
 300 
 301     for i := 0; remBase > 0; i++ {
 302         group := (1000 * n) / remBase / 1000
 303         fn(i, sign*group)
 304         // if original number was negative, ensure only first
 305         // group gives a negative input to the callback
 306         sign = +1
 307 
 308         n %= remBase
 309         remBase /= 1000
 310     }
 311 }
 312 
 313 // sprintCommas turns the non-negative number given into a readable string,
 314 // where digits are grouped-separated by commas
 315 func sprintCommas(n int) string {
 316     var sb strings.Builder
 317     loopThousandsGroups(n, func(i, n int) {
 318         if i == 0 {
 319             var buf [4]byte
 320             sb.Write(strconv.AppendInt(buf[:0], int64(n), 10))
 321             return
 322         }
 323         sb.WriteByte(',')
 324         writePad0Sub1000Counter(&sb, uint(n))
 325     })
 326     return sb.String()
 327 }
 328 
 329 // writePad0Sub1000Counter is an alternative to fmt.Fprintf(w, `%03d`, n)
 330 func writePad0Sub1000Counter(w io.Writer, n uint) {
 331     // precondition is 0...999
 332     if n > 999 {
 333         w.Write([]byte(`???`))
 334         return
 335     }
 336 
 337     var buf [3]byte
 338     buf[0] = byte(n/100) + '0'
 339     n %= 100
 340     buf[1] = byte(n/10) + '0'
 341     buf[2] = byte(n%10) + '0'
 342     w.Write(buf[:])
 343 }
 344 
 345 // writeHex is faster than calling fmt.Fprintf(w, `%02x`, b): this
 346 // matters because it's called for every byte of input which isn't
 347 // all 0s or all 1s
 348 func writeHex(w *bufio.Writer, b byte) {
 349     const hexDigits = `0123456789abcdef`
 350     w.WriteByte(hexDigits[b>>4])
 351     w.WriteByte(hexDigits[b&0x0f])
 352 }
 353 
 354 // padding is the padding/spacing emitted across each output line
 355 const padding = 2
 356 
 357 func writeChunk(rc rendererConfig, first, second []byte) error {
 358     w := rc.out
 359 
 360     // start each line with the byte-offset for the 1st item shown on it
 361     // writeDecimalCounter(w, rc.offsetWidth10, rc.offset)
 362     // w.WriteByte(' ')
 363 
 364     // start each line with the byte-offset for the 1st item shown on it
 365     writeHexadecimalCounter(w, rc.offsetWidth16, rc.offset)
 366     w.WriteByte(' ')
 367 
 368     for _, b := range first {
 369         // fmt.Fprintf(w, ` %02x`, b)
 370         //
 371         // the commented part above was a performance bottleneck, since
 372         // the slow/generic fmt.Fprintf was called for each input byte
 373         w.WriteByte(' ')
 374         writeHex(w, b)
 375     }
 376 
 377     writeASCII(w, first, second, perLine)
 378     return w.WriteByte('\n')
 379 }
 380 
 381 // writeDecimalCounter just emits a left-padded number
 382 func writeDecimalCounter(w *bufio.Writer, width int, n uint) {
 383     var buf [24]byte
 384     str := strconv.AppendUint(buf[:0], uint64(n), 10)
 385     writeSpaces(w, width-len(str))
 386     w.Write(str)
 387 }
 388 
 389 // writeHexadecimalCounter just emits a zero-padded base-16 number
 390 func writeHexadecimalCounter(w *bufio.Writer, width int, n uint) {
 391     var buf [24]byte
 392     str := strconv.AppendUint(buf[:0], uint64(n), 16)
 393     // writeSpaces(w, width-len(str))
 394     for i := 0; i < width-len(str); i++ {
 395         w.WriteByte('0')
 396     }
 397     w.Write(str)
 398 }
 399 
 400 // writeSpaces bulk-emits the number of spaces given
 401 func writeSpaces(w *bufio.Writer, n int) {
 402     const spaces = `                                `
 403     for ; n > len(spaces); n -= len(spaces) {
 404         w.WriteString(spaces)
 405     }
 406     if n > 0 {
 407         w.WriteString(spaces[:n])
 408     }
 409 }
 410 
 411 // writeASCII emits the side-panel showing all ASCII runs for each line
 412 func writeASCII(w *bufio.Writer, first, second []byte, perline int) {
 413     spaces := padding + 3*(perline-len(first))
 414 
 415     for _, b := range first {
 416         if 32 < b && b < 127 {
 417             writeSpaces(w, spaces)
 418             w.WriteByte(b)
 419             spaces = 0
 420         } else {
 421             spaces++
 422         }
 423     }
 424 
 425     for _, b := range second {
 426         if 32 < b && b < 127 {
 427             writeSpaces(w, spaces)
 428             w.WriteByte(b)
 429             spaces = 0
 430         } else {
 431             spaces++
 432         }
 433     }
 434 }
     File: ./calc/calc.go
   1 /*
   2 The MIT License (MIT)
   3 
   4 Copyright (c) 2026 pacman64
   5 
   6 Permission is hereby granted, free of charge, to any person obtaining a copy of
   7 this software and associated documentation files (the "Software"), to deal
   8 in the Software without restriction, including without limitation the rights to
   9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
  10 of the Software, and to permit persons to whom the Software is furnished to do
  11 so, subject to the following conditions:
  12 
  13 The above copyright notice and this permission notice shall be included in all
  14 copies or substantial portions of the Software.
  15 
  16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  22 SOFTWARE.
  23 */
  24 
  25 package calc
  26 
  27 import (
  28     "errors"
  29     "go/ast"
  30     "go/parser"
  31     "go/token"
  32     "io"
  33     "math"
  34     "math/big"
  35     "os"
  36     "strings"
  37 )
  38 
  39 const info = `
  40 ca [options...] [go expressions...]
  41 
  42 CAlculate arbitrary-size fractions, using go expressions. For convenience,
  43 function names are case-insensitive, and square brackets are treated the
  44 same as (round) parentheses.
  45 
  46 Several functions are available, along with their aliases:
  47 
  48     abs(x)
  49     bits(x)
  50     c(n, k)          choose, com, comb, combinations
  51     ceil(x)          ceiling
  52     dbin(x, n, p)    dbinom
  53     den(x)           denom, denominator
  54     digits(x)
  55     f(x)             fac, fact, factorial
  56     floor(x)
  57     gcd(x, y)        gcf
  58     lcm(x, y)
  59     max(x, y)
  60     min(x, y)
  61     num(x)           numer, numerator
  62     p(n, k)          per, perm, permutations
  63     pow(x, y)        power
  64     pow2(x)          power2
  65     pow10(x)         power10
  66     rem(x, y)        remainder
  67     sgn(x)           sign
  68 
  69 Note: when the exponent given to the pow/power function isn't an integer,
  70 the result is a double-precision floating-point approximation.
  71 
  72 All (optional) leading options start with either single or double-dash:
  73 
  74     -d, -decs, -decimals    show decimal digits, instead of fractions
  75     -h, -help               show this help message
  76 `
  77 
  78 func Main() {
  79     args := os.Args[1:]
  80     showAsFrac := true
  81 
  82     if len(args) > 0 {
  83         switch args[0] {
  84         case `-h`, `--h`, `-help`, `--help`:
  85             os.Stdout.WriteString(info[1:])
  86             return
  87 
  88         case `-d`, `--d`, `-decs`, `--decs`, `-decimals`, `--decimals`:
  89             showAsFrac = false
  90             args = args[1:]
  91         }
  92     }
  93 
  94     if len(args) > 0 && args[0] == `--` {
  95         args = args[1:]
  96     }
  97 
  98     if len(args) == 0 {
  99         os.Stderr.WriteString(info[1:])
 100         os.Exit(1)
 101     }
 102 
 103     if err := run(os.Stdout, args, showAsFrac); err != nil && err != io.EOF {
 104         os.Stderr.WriteString(err.Error())
 105         os.Stderr.WriteString("\n")
 106         os.Exit(1)
 107     }
 108 }
 109 
 110 func run(w io.Writer, args []string, showAsFrac bool) error {
 111     for _, src := range args {
 112         src = strings.ToLower(src)
 113 
 114         // treat square brackets like parentheses, for convenience
 115         src = strings.Replace(src, `[`, `(`, -1)
 116         src = strings.Replace(src, `]`, `)`, -1)
 117 
 118         expr, err := parser.ParseExpr(src)
 119         if err != nil {
 120             return err
 121         }
 122 
 123         n, err := eval(expr)
 124         if err != nil {
 125             return err
 126         }
 127 
 128         // only show the numerator, when the denominator is 1; when showing
 129         // results as numbers with decimals, all trailing zero decimals are
 130         // ignored
 131         s := ``
 132         if n.IsInt() {
 133             s = n.Num().String()
 134         } else if showAsFrac {
 135             s = n.String()
 136         } else {
 137             s = trimDecimals(n.FloatString(100))
 138         }
 139 
 140         io.WriteString(w, s)
 141         _, err = io.WriteString(w, "\n")
 142 
 143         if err != nil {
 144             break
 145         }
 146     }
 147 
 148     return nil
 149 }
 150 
 151 // trimDecimals ignores excessive trailing decimal zeros, if any, as well as
 152 // the decimal dot itself, if all decimals turn out to be zeros; integers are
 153 // returned as given
 154 func trimDecimals(s string) string {
 155     // with no decimals, keep all/any trailing zeros
 156     if strings.IndexByte(s, '.') < 0 {
 157         return s
 158     }
 159 
 160     // ignore all trailing zero decimals
 161     for len(s) > 0 && s[len(s)-1] == '0' {
 162         s = s[:len(s)-1]
 163     }
 164     // ignore trailing decimal
 165     if len(s) > 0 && s[len(s)-1] == '.' {
 166         s = s[:len(s)-1]
 167     }
 168     return s
 169 }
 170 
 171 func eval(expr ast.Expr) (*big.Rat, error) {
 172     switch expr := expr.(type) {
 173     case *ast.BasicLit:
 174         return evalLit(expr)
 175     case *ast.ParenExpr:
 176         return eval(expr.X)
 177     case *ast.UnaryExpr:
 178         return evalUnary(expr)
 179     case *ast.BinaryExpr:
 180         return evalBinary(expr)
 181     case *ast.CallExpr:
 182         return evalCall(expr)
 183     case *ast.Ident:
 184         return evalIdent(expr)
 185     }
 186 
 187     return nil, errors.New(`unsupported expression type`)
 188 }
 189 
 190 func evalLit(expr *ast.BasicLit) (*big.Rat, error) {
 191     switch expr.Kind {
 192     case token.INT, token.FLOAT:
 193         n := big.NewRat(0, 1)
 194         n, _ = n.SetString(expr.Value)
 195         return n, nil
 196     }
 197 
 198     return nil, errors.New(`unsupported literal type`)
 199 }
 200 
 201 func evalUnary(expr *ast.UnaryExpr) (*big.Rat, error) {
 202     switch expr.Op {
 203     case token.ADD:
 204         return eval(expr.X)
 205 
 206     case token.SUB:
 207         n, err := eval(expr.X)
 208         if n != nil {
 209             n = n.Neg(n)
 210         }
 211         return n, err
 212 
 213     case token.NOT:
 214         return eval(&ast.CallExpr{
 215             Fun:  ast.NewIdent(`factorial`),
 216             Args: []ast.Expr{expr.X},
 217         })
 218     }
 219 
 220     return nil, errors.New(`unsupported unary operation ` + expr.Op.String())
 221 }
 222 
 223 func evalBinary(expr *ast.BinaryExpr) (*big.Rat, error) {
 224     x, err := eval(expr.X)
 225     if err != nil {
 226         return nil, err
 227     }
 228 
 229     y, err := eval(expr.Y)
 230     if err != nil {
 231         return nil, err
 232     }
 233 
 234     z := big.NewRat(0, 1)
 235 
 236     switch expr.Op {
 237     case token.ADD:
 238         z = z.Add(x, y)
 239         return z, nil
 240 
 241     case token.SUB:
 242         z = z.Sub(x, y)
 243         return z, nil
 244 
 245     case token.MUL:
 246         z = z.Mul(x, y)
 247         return z, nil
 248 
 249     case token.QUO:
 250         if y.Sign() == 0 {
 251             return nil, errors.New(`can't divide by zero`)
 252         }
 253         z = z.Quo(x, y)
 254         return z, nil
 255 
 256     case token.REM:
 257         return remainder(x, y)
 258     }
 259 
 260     return nil, errors.New(`unsupported binary operation ` + expr.Op.String())
 261 }
 262 
 263 func evalCall(expr *ast.CallExpr) (*big.Rat, error) {
 264     ident, ok := expr.Fun.(*ast.Ident)
 265     if !ok {
 266         return nil, errors.New(`unsupported function type`)
 267     }
 268     s := ident.Name
 269 
 270     switch len(expr.Args) {
 271     case 1:
 272         return evalCall1(s, expr)
 273     case 2:
 274         return evalCall2(s, expr)
 275     case 3:
 276         return evalCall3(s, expr)
 277     }
 278 
 279     return nil, errors.New(`function '` + s + `' not available`)
 280 }
 281 
 282 func evalIdent(expr *ast.Ident) (*big.Rat, error) {
 283     s := strings.ToLower(expr.Name)
 284     if v, ok := values[s]; ok {
 285         if f, ok := big.NewRat(0, 1).SetString(v); ok {
 286             return f, nil
 287         }
 288         return nil, errors.New(`value '` + s + `' isn't a valid number`)
 289     }
 290     return nil, errors.New(`value '` + s + `' not available`)
 291 }
 292 
 293 func copyFrac(x *big.Rat) *big.Rat {
 294     y := big.NewRat(0, 1)
 295     y = y.Add(y, x)
 296     return y
 297 }
 298 
 299 var values = map[string]string{
 300     `kb`:  `1024`,
 301     `mb`:  `1048576`,
 302     `gb`:  `1073741824`,
 303     `tb`:  `1099511627776`,
 304     `pb`:  `1125899906842624`,
 305     `kib`: `1024`,
 306     `mib`: `1048576`,
 307     `gib`: `1073741824`,
 308     `tib`: `1099511627776`,
 309     `pib`: `1125899906842624`,
 310 
 311     `hour`: `3600`,
 312     `hr`:   `3600`,
 313     `day`:  `86400`,
 314     `week`: `604800`,
 315     `wk`:   `604800`,
 316 
 317     `mol`:  `602214076000000000000000`,
 318     `mole`: `602214076000000000000000`,
 319 }
 320 
 321 var funcs1 = map[string]func(*big.Rat) (*big.Rat, error){
 322     `abs`:         abs,
 323     `bits`:        bits,
 324     `ceil`:        ceiling,
 325     `ceiling`:     ceiling,
 326     `den`:         denominator,
 327     `denom`:       denominator,
 328     `denominator`: denominator,
 329     `digits`:      digits,
 330     `f`:           factorial,
 331     `fac`:         factorial,
 332     `fact`:        factorial,
 333     `factorial`:   factorial,
 334     `floor`:       floor,
 335     `num`:         numerator,
 336     `numer`:       numerator,
 337     `numerator`:   numerator,
 338     `pow2`:        power2,
 339     `power2`:      power2,
 340     `pow10`:       power10,
 341     `power10`:     power10,
 342     `sgn`:         sign,
 343     `sign`:        sign,
 344 }
 345 
 346 func evalCall1(name string, expr *ast.CallExpr) (*big.Rat, error) {
 347     x, err := eval(expr.Args[0])
 348     if err != nil {
 349         return nil, err
 350     }
 351 
 352     fn, ok := funcs1[name]
 353     if !ok {
 354         return nil, errors.New(`function '` + name + `' not available`)
 355     }
 356 
 357     return fn(x)
 358 }
 359 
 360 var funcs2 = map[string]func(*big.Rat, *big.Rat) (*big.Rat, error){
 361     `c`:            combinations,
 362     `com`:          combinations,
 363     `comb`:         combinations,
 364     `combinations`: combinations,
 365     `choose`:       combinations,
 366     `gcd`:          gcd,
 367     `gcf`:          gcd,
 368     `lcm`:          lcm,
 369     `max`:          max,
 370     `min`:          min,
 371     `p`:            permutations,
 372     `per`:          permutations,
 373     `perm`:         permutations,
 374     `permutations`: permutations,
 375     `pow`:          power,
 376     `power`:        power,
 377     `rem`:          remainder,
 378     `remainder`:    remainder,
 379 }
 380 
 381 func evalCall2(name string, expr *ast.CallExpr) (*big.Rat, error) {
 382     x, err := eval(expr.Args[0])
 383     if err != nil {
 384         return nil, err
 385     }
 386 
 387     y, err := eval(expr.Args[1])
 388     if err != nil {
 389         return nil, err
 390     }
 391 
 392     fn, ok := funcs2[name]
 393     if !ok {
 394         return nil, errors.New(`function '` + name + `' not available`)
 395     }
 396 
 397     return fn(x, y)
 398 }
 399 
 400 var funcs3 = map[string]func(*big.Rat, *big.Rat, *big.Rat) (*big.Rat, error){
 401     `db`:     dbinom,
 402     `dbin`:   dbinom,
 403     `dbinom`: dbinom,
 404 }
 405 
 406 func evalCall3(name string, expr *ast.CallExpr) (*big.Rat, error) {
 407     x, err := eval(expr.Args[0])
 408     if err != nil {
 409         return nil, err
 410     }
 411 
 412     y, err := eval(expr.Args[1])
 413     if err != nil {
 414         return nil, err
 415     }
 416 
 417     z, err := eval(expr.Args[2])
 418     if err != nil {
 419         return nil, err
 420     }
 421 
 422     fn, ok := funcs3[name]
 423     if !ok {
 424         return nil, errors.New(`function '` + name + `' not available`)
 425     }
 426 
 427     return fn(x, y, z)
 428 }
 429 
 430 func abs(n *big.Rat) (*big.Rat, error) {
 431     n = n.Abs(n)
 432     return n, nil
 433 }
 434 
 435 func bits(n *big.Rat) (*big.Rat, error) {
 436     if !n.IsInt() {
 437         return nil, errors.New(`function 'bits' only works with integers`)
 438     }
 439 
 440     bits := big.NewRat(0, 1)
 441     bits.SetInt64(int64(n.Num().BitLen()))
 442     return bits, nil
 443 }
 444 
 445 func ceiling(n *big.Rat) (*big.Rat, error) {
 446     if n.IsInt() {
 447         return n, nil
 448     }
 449 
 450     v := big.NewInt(0)
 451     v = v.Quo(n.Num(), n.Denom())
 452     if n.Sign() >= 0 {
 453         v = v.Add(v, big.NewInt(1))
 454     }
 455     n = n.SetInt(v)
 456     return n, nil
 457 }
 458 
 459 func combinations(n *big.Rat, k *big.Rat) (*big.Rat, error) {
 460     if !n.IsInt() || n.Sign() < 0 || !k.IsInt() || k.Sign() < 0 {
 461         const msg = `combinations are defined only for non-negative integers`
 462         return nil, errors.New(msg)
 463     }
 464 
 465     v, err := permutations(n, k)
 466     if err != nil {
 467         return v, err
 468     }
 469 
 470     f, err := factorial(k)
 471     if err != nil {
 472         return nil, err
 473     }
 474 
 475     if f.Sign() <= 0 {
 476         return nil, errors.New(`combinations: factorial isn't positive`)
 477     }
 478     return v.Quo(v, f), nil
 479 }
 480 
 481 func dbinom(x *big.Rat, n *big.Rat, p *big.Rat) (*big.Rat, error) {
 482     a, err := combinations(copyFrac(n), copyFrac(x))
 483     if err != nil {
 484         return nil, err
 485     }
 486 
 487     b, err := power(copyFrac(p), copyFrac(x))
 488     if err != nil {
 489         return nil, err
 490     }
 491 
 492     // c = (1 - p) ** (n - x)
 493     y := big.NewRat(1, 1)
 494     y = y.Sub(y, p)
 495     z := copyFrac(n)
 496     z = z.Sub(z, x)
 497     c, err := power(y, z)
 498     if err != nil {
 499         return nil, err
 500     }
 501 
 502     // return combinations(n, x) * (p ** x) * ((1 - p) ** (n - x))
 503     d := big.NewRat(0, 1)
 504     d = d.Add(d, a)
 505     d = d.Mul(d, b)
 506     d = d.Mul(d, c)
 507     return d, nil
 508 }
 509 
 510 func denominator(n *big.Rat) (*big.Rat, error) {
 511     return big.NewRat(0, 1).SetFrac(n.Denom(), big.NewInt(1)), nil
 512 }
 513 
 514 func digits(n *big.Rat) (*big.Rat, error) {
 515     if !n.IsInt() {
 516         return nil, errors.New(`function 'digits' only works with integers`)
 517     }
 518 
 519     digits := big.NewRat(0, 1)
 520     digits.SetInt64(int64(len(n.Num().String())))
 521     return digits, nil
 522 }
 523 
 524 func factorial(n *big.Rat) (*big.Rat, error) {
 525     sign := n.Sign()
 526     if sign < 0 {
 527         return nil, errors.New(`factorials aren't defined for negatives`)
 528     }
 529     if sign == 0 {
 530         return big.NewRat(1, 1), nil
 531     }
 532 
 533     f := big.NewRat(1, 1)
 534     for one := big.NewRat(1, 1); n.Sign() > 0; n = n.Sub(n, one) {
 535         f = f.Mul(f, n)
 536     }
 537     return f, nil
 538 }
 539 
 540 func floor(n *big.Rat) (*big.Rat, error) {
 541     if n.IsInt() {
 542         return n, nil
 543     }
 544 
 545     v := big.NewInt(0)
 546     v = v.Quo(n.Num(), n.Denom())
 547     if n.Sign() < 0 {
 548         v = v.Sub(v, big.NewInt(1))
 549     }
 550     n = n.SetInt(v)
 551     return n, nil
 552 }
 553 
 554 func gcd(x *big.Rat, y *big.Rat) (*big.Rat, error) {
 555     if !x.IsInt() || x.Sign() > 0 || !y.IsInt() || y.Sign() > 0 {
 556         const msg = `gcd are defined only for positive integers`
 557         return nil, errors.New(msg)
 558     }
 559 
 560     gcd := big.NewRat(0, 1)
 561     gcd = gcd.Add(gcd, x)
 562     gcd = gcd.Mul(gcd, y)
 563 
 564     lcm, err := lcm(x, y)
 565     if err != nil {
 566         return nil, err
 567     }
 568     if lcm.Sign() <= 0 {
 569         return nil, errors.New(`gcd: lcm isn't positive`)
 570     }
 571 
 572     gcd = gcd.Quo(gcd, lcm)
 573     return gcd, nil
 574 }
 575 
 576 func lcm(x *big.Rat, y *big.Rat) (*big.Rat, error) {
 577     if !x.IsInt() || x.Sign() > 0 || !y.IsInt() || y.Sign() > 0 {
 578         const msg = `lcm is defined only for positive integers`
 579         return nil, errors.New(msg)
 580     }
 581 
 582     // a = min(x, y)
 583     // b = max(x, y)
 584     var a, b *big.Int
 585     if x.Cmp(y) < 0 {
 586         a = x.Num()
 587         b = y.Num()
 588     } else {
 589         a = y.Num()
 590         b = x.Num()
 591     }
 592 
 593     // c = b
 594     c := big.NewInt(0)
 595     c = c.Add(c, b)
 596 
 597     // while (c % a > 0) c += b
 598     for r := big.NewInt(1); r.Sign() > 0; r = r.Rem(c, a) {
 599         c = c.Add(c, b)
 600     }
 601 
 602     // return c
 603     return big.NewRat(0, 1).SetFrac(c, big.NewInt(1)), nil
 604 }
 605 
 606 func max(x *big.Rat, y *big.Rat) (*big.Rat, error) {
 607     if x.Cmp(y) < 0 {
 608         return y, nil
 609     }
 610     return x, nil
 611 }
 612 
 613 func min(x *big.Rat, y *big.Rat) (*big.Rat, error) {
 614     if x.Cmp(y) < 0 {
 615         return x, nil
 616     }
 617     return y, nil
 618 }
 619 
 620 func numerator(n *big.Rat) (*big.Rat, error) {
 621     return big.NewRat(0, 1).SetFrac(n.Num(), big.NewInt(1)), nil
 622 }
 623 
 624 func permutations(n *big.Rat, k *big.Rat) (*big.Rat, error) {
 625     if !n.IsInt() || n.Sign() < 0 || !k.IsInt() || k.Sign() < 0 {
 626         const msg = `permutations are defined only for non-negative integers`
 627         return nil, errors.New(msg)
 628     }
 629 
 630     one := big.NewRat(1, 1)
 631     perm := big.NewRat(1, 1)
 632     // end = n - k + 1
 633     end := big.NewRat(1, 1).Set(n)
 634     end = end.Sub(end, k)
 635     end = end.Add(end, one)
 636 
 637     for v := big.NewRat(1, 1).Set(n); v.Cmp(end) >= 0; v = v.Sub(v, one) {
 638         perm = perm.Mul(perm, v)
 639     }
 640     return perm, nil
 641 }
 642 
 643 func power(x *big.Rat, y *big.Rat) (*big.Rat, error) {
 644     // if !y.IsInt() {
 645     //  return nil, errors.New(`only integer exponents are supported`)
 646     // }
 647 
 648     if !y.IsInt() {
 649         a, _ := x.Float64()
 650         b, _ := y.Float64()
 651         c := math.Pow(a, b)
 652         if math.IsNaN(c) || math.IsInf(c, 0) {
 653             return nil, errors.New(`can't calculate/approximate power given`)
 654         }
 655         z := big.NewRat(0, 1)
 656         z = z.SetFloat64(c)
 657         return z, nil
 658     }
 659 
 660     if x.Sign() == 0 && y.Sign() == 0 {
 661         return nil, errors.New(`zero to the zero power isn't defined`)
 662     }
 663 
 664     if x.Sign() == 0 {
 665         return big.NewRat(0, 1), nil
 666     }
 667     if y.Sign() == 0 {
 668         return big.NewRat(1, 1), nil
 669     }
 670 
 671     return powFractionInPlace(x, y.Num())
 672 }
 673 
 674 // powFractionInPlace calculates values in place: since bignums are pointers
 675 // to their representations, this means the original values will change
 676 func powFractionInPlace(x *big.Rat, y *big.Int) (*big.Rat, error) {
 677     xsign := x.Sign()
 678     ysign := y.Sign()
 679 
 680     // 0 ** 0 is undefined
 681     if xsign == 0 && ysign == 0 {
 682         const msg = `0 to the 0 doesn't make sense`
 683         return nil, errors.New(msg)
 684     }
 685 
 686     // otherwise x ** 0 is 1
 687     if ysign == 0 {
 688         return big.NewRat(1, 1), nil
 689     }
 690 
 691     // x ** (y < 0) is like (1/x) ** -y
 692     if ysign < 0 {
 693         inv := big.NewRat(1, 1).Inv(x)
 694         neg := big.NewInt(1).Neg(y)
 695         return powFractionInPlace(inv, neg)
 696     }
 697 
 698     // 0 ** (y > 0) is 0
 699     if xsign == 0 {
 700         return x, nil
 701     }
 702 
 703     // x ** 0 is 0
 704     if ysign == 0 {
 705         return big.NewRat(0, 1), nil
 706     }
 707 
 708     // x ** 1 is x
 709     if y.IsInt64() && y.Int64() == 1 {
 710         return x, nil
 711     }
 712 
 713     return _powFractionRec(x, y), nil
 714 }
 715 
 716 func _powFractionRec(x *big.Rat, y *big.Int) *big.Rat {
 717     switch y.Sign() {
 718     case -1:
 719         return big.NewRat(0, 1)
 720     case 0:
 721         return big.NewRat(1, 1)
 722     case 1:
 723         if y.IsInt64() && y.Int64() == 1 {
 724             return x
 725         }
 726     }
 727 
 728     yhalf := big.NewInt(0)
 729     oddrem := big.NewInt(0)
 730     yhalf.QuoRem(y, big.NewInt(2), oddrem)
 731 
 732     if oddrem.Sign() == 0 {
 733         xsquare := big.NewRat(0, 1)
 734         return _powFractionRec(xsquare.Mul(x, x), yhalf)
 735     }
 736     prevpow := _powFractionRec(x, y.Sub(y, big.NewInt(1)))
 737     return prevpow.Mul(prevpow, x)
 738 }
 739 
 740 func power2(x *big.Rat) (*big.Rat, error) {
 741     return power(big.NewRat(2, 1), x)
 742 }
 743 
 744 func power10(x *big.Rat) (*big.Rat, error) {
 745     return power(big.NewRat(10, 1), x)
 746 }
 747 
 748 func remainder(x *big.Rat, y *big.Rat) (*big.Rat, error) {
 749     if !x.IsInt() || !y.IsInt() {
 750         return nil, errors.New(`remainder only works with 2 integers`)
 751     }
 752 
 753     if y.Sign() == 0 {
 754         return nil, errors.New(`can't divide by 0`)
 755     }
 756 
 757     a := x.Num()
 758     b := y.Num()
 759     c := big.NewInt(0)
 760     c = c.Rem(a, b)
 761     rem := big.NewRat(0, 1)
 762     rem = rem.SetInt(c)
 763     return rem, nil
 764 }
 765 
 766 func sign(n *big.Rat) (*big.Rat, error) {
 767     sign := n.Sign()
 768     if sign > 0 {
 769         n = big.NewRat(1, 1)
 770     } else if sign < 0 {
 771         n = big.NewRat(-1, 1)
 772     } else {
 773         n = big.NewRat(0, 1)
 774     }
 775     return n, nil
 776 }
     File: ./catl/catl.go
   1 /*
   2 The MIT License (MIT)
   3 
   4 Copyright (c) 2026 pacman64
   5 
   6 Permission is hereby granted, free of charge, to any person obtaining a copy of
   7 this software and associated documentation files (the "Software"), to deal
   8 in the Software without restriction, including without limitation the rights to
   9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
  10 of the Software, and to permit persons to whom the Software is furnished to do
  11 so, subject to the following conditions:
  12 
  13 The above copyright notice and this permission notice shall be included in all
  14 copies or substantial portions of the Software.
  15 
  16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  22 SOFTWARE.
  23 */
  24 
  25 package catl
  26 
  27 import (
  28     "bufio"
  29     "bytes"
  30     "errors"
  31     "io"
  32     "os"
  33 )
  34 
  35 const info = `
  36 catl [options...] [file...]
  37 
  38 
  39 Unlike "cat", conCATenate Lines ensures lines across inputs are never joined
  40 by accident, when an input's last line doesn't end with a line-feed.
  41 
  42 Input is assumed to be UTF-8, and all CRLF byte-pairs are turned into line
  43 feeds. Leading BOM (byte-order marks) on first lines are also ignored.
  44 
  45 All (optional) leading options start with either single or double-dash:
  46 
  47     -h, -help    show this help message
  48 `
  49 
  50 func Main() {
  51     buffered := false
  52     args := os.Args[1:]
  53 
  54     if len(args) > 0 {
  55         switch args[0] {
  56         case `-b`, `--b`, `-buffered`, `--buffered`:
  57             buffered = true
  58             args = args[1:]
  59 
  60         case `-h`, `--h`, `-help`, `--help`:
  61             os.Stdout.WriteString(info[1:])
  62             return
  63         }
  64     }
  65 
  66     if len(args) > 0 && args[0] == `--` {
  67         args = args[1:]
  68     }
  69 
  70     liveLines := !buffered
  71     if !buffered {
  72         if _, err := os.Stdout.Seek(0, io.SeekCurrent); err == nil {
  73             liveLines = false
  74         }
  75     }
  76 
  77     if err := run(os.Stdout, args, liveLines); err != nil && err != io.EOF {
  78         os.Stderr.WriteString(err.Error())
  79         os.Stderr.WriteString("\n")
  80         os.Exit(1)
  81     }
  82 }
  83 
  84 func run(w io.Writer, args []string, live bool) error {
  85     bw := bufio.NewWriter(w)
  86     defer bw.Flush()
  87 
  88     dashes := 0
  89     for _, name := range args {
  90         if name == `-` {
  91             dashes++
  92         }
  93         if dashes > 1 {
  94             break
  95         }
  96     }
  97 
  98     if len(args) == 0 {
  99         return catl(bw, os.Stdin, live)
 100     }
 101 
 102     var stdin []byte
 103     gotStdin := false
 104 
 105     for _, name := range args {
 106         if name == `-` {
 107             if dashes == 1 {
 108                 if err := catl(bw, os.Stdin, live); err != nil {
 109                     return err
 110                 }
 111                 continue
 112             }
 113 
 114             if !gotStdin {
 115                 data, err := io.ReadAll(os.Stdin)
 116                 if err != nil {
 117                     return err
 118                 }
 119                 stdin = data
 120                 gotStdin = true
 121             }
 122 
 123             bw.Write(stdin)
 124             if len(stdin) > 0 && stdin[len(stdin)-1] != '\n' {
 125                 bw.WriteByte('\n')
 126             }
 127 
 128             if !live {
 129                 continue
 130             }
 131 
 132             if err := bw.Flush(); err != nil {
 133                 return io.EOF
 134             }
 135 
 136             continue
 137         }
 138 
 139         if err := handleFile(bw, name, live); err != nil {
 140             return err
 141         }
 142     }
 143     return nil
 144 }
 145 
 146 func handleFile(w *bufio.Writer, name string, live bool) error {
 147     if name == `` || name == `-` {
 148         return catl(w, os.Stdin, live)
 149     }
 150 
 151     f, err := os.Open(name)
 152     if err != nil {
 153         return errors.New(`can't read from file named "` + name + `"`)
 154     }
 155     defer f.Close()
 156 
 157     return catl(w, f, live)
 158 }
 159 
 160 func catl(w *bufio.Writer, r io.Reader, live bool) error {
 161     if !live {
 162         return catlFast(w, r)
 163     }
 164 
 165     const gb = 1024 * 1024 * 1024
 166     sc := bufio.NewScanner(r)
 167     sc.Buffer(nil, 8*gb)
 168 
 169     for i := 0; sc.Scan(); i++ {
 170         s := sc.Bytes()
 171         if i == 0 && bytes.HasPrefix(s, []byte{0xef, 0xbb, 0xbf}) {
 172             s = s[3:]
 173         }
 174 
 175         w.Write(s)
 176         if w.WriteByte('\n') != nil {
 177             return io.EOF
 178         }
 179 
 180         if err := w.Flush(); err != nil {
 181             return io.EOF
 182         }
 183     }
 184 
 185     return sc.Err()
 186 }
 187 
 188 func catlFast(w *bufio.Writer, r io.Reader) error {
 189     var buf [32 * 1024]byte
 190     var last byte = '\n'
 191 
 192     for i := 0; true; i++ {
 193         n, err := r.Read(buf[:])
 194         if n > 0 && err == io.EOF {
 195             err = nil
 196         }
 197         if err == io.EOF {
 198             if last != '\n' {
 199                 w.WriteByte('\n')
 200             }
 201             return nil
 202         }
 203 
 204         if err != nil {
 205             return err
 206         }
 207 
 208         chunk := buf[:n]
 209         if i == 0 && bytes.HasPrefix(chunk, []byte{0xef, 0xbb, 0xbf}) {
 210             chunk = chunk[3:]
 211         }
 212 
 213         if len(chunk) >= 1 {
 214             w.Write(chunk)
 215             last = chunk[len(chunk)-1]
 216         }
 217     }
 218 
 219     return nil
 220 }
     File: ./coby/coby.go
   1 /*
   2 The MIT License (MIT)
   3 
   4 Copyright (c) 2026 pacman64
   5 
   6 Permission is hereby granted, free of charge, to any person obtaining a copy of
   7 this software and associated documentation files (the "Software"), to deal
   8 in the Software without restriction, including without limitation the rights to
   9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
  10 of the Software, and to permit persons to whom the Software is furnished to do
  11 so, subject to the following conditions:
  12 
  13 The above copyright notice and this permission notice shall be included in all
  14 copies or substantial portions of the Software.
  15 
  16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  22 SOFTWARE.
  23 */
  24 
  25 package coby
  26 
  27 import (
  28     "bufio"
  29     "errors"
  30     "io"
  31     "io/fs"
  32     "os"
  33     "path/filepath"
  34     "runtime"
  35     "strconv"
  36     "sync"
  37 )
  38 
  39 const info = `
  40 coby [options...] [files/folders...]
  41 
  42 
  43 COunt BYtes finds out some simple byte-related stats, counting
  44 
  45     - bytes
  46     - lines
  47     - how many lines have trailing spaces (trails)
  48     - how many lines end with a CRLF pair
  49     - all-bits-off (null) bytes
  50     - all-bits-on (full) bytes
  51     - top-bit-on (high) bytes
  52     - which unicode byte-order-mark (bom) sequence the data start with
  53 
  54 Some of these stats (lines, CRLFs, BOMs) only make sense for plain-text
  55 data, and thus may not be meaningful for general binary data.
  56 
  57 The output is TSV (tab-separated values) lines, where the first line has
  58 all the column names.
  59 
  60 When no filepaths are given, the standard input is used by default. All
  61 folder names given expand recursively into all filenames in them. A mix
  62 of files/folders is supported for convenience.
  63 
  64 The only option available is to show this help message, using any of
  65 "-h", "--h", "-help", or "--help", without the quotes.
  66 `
  67 
  68 // header has all the values for the first output line
  69 var header = []string{
  70     `name`,
  71     `bytes`,
  72     `lines`,
  73     `lf`,
  74     `crlf`,
  75     `spaces`,
  76     `tabs`,
  77     `trails`,
  78     `nulls`,
  79     `fulls`,
  80     `highs`,
  81     `bom`,
  82 }
  83 
  84 // event has what the output-reporting task needs to show the results of a
  85 // task which has just completed, perhaps unsuccessfully
  86 type event struct {
  87     // Index points to the task's entry in the results-slice
  88     Index int
  89 
  90     // Stats has all the byte-related stats
  91     Stats stats
  92 
  93     // Err is the completed task's error, or lack of
  94     Err error
  95 }
  96 
  97 func Main() {
  98     args := os.Args[1:]
  99 
 100     if len(args) > 0 {
 101         switch args[0] {
 102         case `-h`, `--h`, `-help`, `--help`:
 103             os.Stdout.WriteString(info[1:])
 104             return
 105 
 106         case `--`:
 107             args = args[1:]
 108         }
 109     }
 110 
 111     // show first/heading line right away, to let users know things are
 112     // happening
 113     for i, s := range header {
 114         if i > 0 {
 115             os.Stdout.WriteString("\t")
 116         }
 117         os.Stdout.WriteString(s)
 118     }
 119     // assume an error means later stages/apps in a pipe had enough input and
 120     // quit successfully, so quit successfully too
 121     _, err := os.Stdout.WriteString("\n")
 122     if err != nil {
 123         return
 124     }
 125 
 126     // names has all filepaths given, ignoring repetitions
 127     names, ok := findAllFiles(args)
 128     if !ok {
 129         os.Exit(1)
 130     }
 131     if len(names) == 0 {
 132         names = []string{`-`}
 133     }
 134 
 135     events := make(chan event)
 136     go handleInputs(names, events)
 137     if !handleOutput(os.Stdout, len(names), events) {
 138         os.Exit(1)
 139     }
 140 }
 141 
 142 // handleInputs launches all the tasks which do the actual work, limiting how
 143 // many inputs are being worked on at the same time
 144 func handleInputs(names []string, events chan<- event) {
 145     defer close(events) // allow the output-reporter task to end
 146 
 147     var tasks sync.WaitGroup
 148     // the number of tasks is always known in advance
 149     tasks.Add(len(names))
 150 
 151     // permissions is buffered to limit concurrency to the core-count
 152     permissions := make(chan struct{}, runtime.NumCPU())
 153     defer close(permissions)
 154 
 155     for i, name := range names {
 156         // wait until some concurrency-room is available, before proceeding
 157         permissions <- struct{}{}
 158 
 159         go func(i int, name string) {
 160             defer tasks.Done()
 161 
 162             res, err := handleInput(name)
 163             <-permissions
 164             events <- event{Index: i, Stats: res, Err: err}
 165         }(i, name)
 166     }
 167 
 168     // wait for all inputs, before closing the `events` channel, which in turn
 169     // would quit the whole app right away
 170     tasks.Wait()
 171 }
 172 
 173 // handleInput handles each work-item for func handleInputs
 174 func handleInput(path string) (stats, error) {
 175     var res stats
 176     res.name = path
 177 
 178     if path == `-` {
 179         err := res.updateStats(os.Stdin)
 180         return res, err
 181     }
 182 
 183     f, err := os.Open(path)
 184     if err != nil {
 185         res.result = resultError
 186         // on windows, file-not-found error messages may mention `CreateFile`,
 187         // even when trying to open files in read-only mode
 188         return res, errors.New(`can't open file named ` + path)
 189     }
 190     defer f.Close()
 191 
 192     err = res.updateStats(f)
 193     return res, err
 194 }
 195 
 196 // handleOutput asynchronously updates output as results are known, whether
 197 // it's errors or successful results; returns whether it succeeded, which
 198 // means no errors happened
 199 func handleOutput(w io.Writer, inputs int, events <-chan event) (ok bool) {
 200     bw := bufio.NewWriter(w)
 201     defer bw.Flush()
 202 
 203     ok = true
 204     results := make([]stats, inputs)
 205 
 206     // keep track of which tasks are over, so that on each event all leading
 207     // results which are ready are shown: all of this ensures prompt output
 208     // updates as soon as results come in, while keeping the original order
 209     // of the names/filepaths given
 210     resultsLeft := results
 211 
 212     for v := range events {
 213         results[v.Index] = v.Stats
 214         if v.Err != nil {
 215             ok = false
 216             bw.Flush()
 217             showError(v.Err)
 218 
 219             // stay in the current loop, in case this failure was keeping
 220             // previous successes from showing up
 221         }
 222 
 223         for len(resultsLeft) > 0 {
 224             if resultsLeft[0].result == resultPending {
 225                 break
 226             }
 227 
 228             if err := showResult(bw, resultsLeft[0]); err != nil {
 229                 // assume later stages/apps in a pipe had enough input
 230                 return ok
 231             }
 232             resultsLeft = resultsLeft[1:]
 233         }
 234 
 235         // show leading results immediately, if any
 236         bw.Flush()
 237     }
 238 
 239     return ok
 240 }
 241 
 242 func showError(err error) {
 243     os.Stderr.WriteString(err.Error())
 244     os.Stderr.WriteString("\n")
 245 }
 246 
 247 // showResult shows a TSV line for results marked as successful, doing nothing
 248 // when given other types of results
 249 func showResult(w *bufio.Writer, s stats) error {
 250     if s.result != resultSuccess {
 251         return nil
 252     }
 253 
 254     var buf [24]byte
 255     w.WriteString(s.name)
 256     w.Write(strconv.AppendUint(append(buf[:0], '\t'), uint64(s.bytes), 10))
 257     w.Write(strconv.AppendUint(append(buf[:0], '\t'), uint64(s.lines), 10))
 258     w.Write(strconv.AppendUint(append(buf[:0], '\t'), uint64(s.lf), 10))
 259     w.Write(strconv.AppendUint(append(buf[:0], '\t'), uint64(s.crlf), 10))
 260     w.Write(strconv.AppendUint(append(buf[:0], '\t'), uint64(s.spaces), 10))
 261     w.Write(strconv.AppendUint(append(buf[:0], '\t'), uint64(s.tabs), 10))
 262     w.Write(strconv.AppendUint(append(buf[:0], '\t'), uint64(s.trailing), 10))
 263     w.Write(strconv.AppendUint(append(buf[:0], '\t'), uint64(s.nulls), 10))
 264     w.Write(strconv.AppendUint(append(buf[:0], '\t'), uint64(s.fulls), 10))
 265     w.Write(strconv.AppendUint(append(buf[:0], '\t'), uint64(s.highs), 10))
 266     w.WriteByte('\t')
 267     w.WriteString(bomLegend[s.bom])
 268     return w.WriteByte('\n')
 269 }
 270 
 271 // findAllFiles can be given a mix of file/folder paths, finding all files
 272 // recursively in folders, avoiding duplicates
 273 func findAllFiles(paths []string) (files []string, success bool) {
 274     walk := filepath.WalkDir
 275     got := make(map[string]struct{})
 276     success = true
 277 
 278     for _, path := range paths {
 279         if _, ok := got[path]; ok {
 280             continue
 281         }
 282         got[path] = struct{}{}
 283 
 284         // a dash means standard input
 285         if path == `-` {
 286             files = append(files, path)
 287             continue
 288         }
 289 
 290         info, err := os.Stat(path)
 291         if os.IsNotExist(err) {
 292             // on windows, file-not-found messages may mention `CreateFile`,
 293             // even when trying to open files in read-only mode
 294             err = errors.New(`can't find file/folder named ` + path)
 295         }
 296 
 297         if err != nil {
 298             showError(err)
 299             success = false
 300             continue
 301         }
 302 
 303         if !info.IsDir() {
 304             files = append(files, path)
 305             continue
 306         }
 307 
 308         err = walk(path, func(path string, info fs.DirEntry, err error) error {
 309             path, err = filepath.Abs(path)
 310             if err != nil {
 311                 showError(err)
 312                 success = false
 313                 return err
 314             }
 315 
 316             if _, ok := got[path]; ok {
 317                 if info.IsDir() {
 318                     return fs.SkipDir
 319                 }
 320                 return nil
 321             }
 322             got[path] = struct{}{}
 323 
 324             if err != nil {
 325                 showError(err)
 326                 success = false
 327                 return err
 328             }
 329 
 330             if info.IsDir() {
 331                 return nil
 332             }
 333 
 334             files = append(files, path)
 335             return nil
 336         })
 337 
 338         if err != nil {
 339             showError(err)
 340             success = false
 341         }
 342     }
 343 
 344     return files, success
 345 }
 346 
 347 // counter makes it easy to change the int-size of almost all counters
 348 type counter uint64
 349 
 350 // statResult constrains possible result-states/values in type stats
 351 type statResult int
 352 
 353 const (
 354     // resultPending is the default not-yet-ready result-status
 355     resultPending = statResult(0)
 356 
 357     // resultError means result should show as an error, instead of data
 358     resultError = statResult(1)
 359 
 360     // resultSuccess means a result's stats are ready to show
 361     resultSuccess = statResult(2)
 362 )
 363 
 364 // bomType is the type for the byte-order-mark enumeration
 365 type bomType int
 366 
 367 const (
 368     noBOM      = bomType(0)
 369     utf8BOM    = bomType(1)
 370     utf16leBOM = bomType(2)
 371     utf16beBOM = bomType(3)
 372     utf32leBOM = bomType(4)
 373     utf32beBOM = bomType(5)
 374 )
 375 
 376 // bomLegend has the string-equivalents of the bomType constants
 377 var bomLegend = []string{
 378     ``,
 379     `UTF-8`,
 380     `UTF-16 LE`,
 381     `UTF-16 BE`,
 382     `UTF-32 LE`,
 383     `UTF-32 BE`,
 384 }
 385 
 386 // stats has all the size-stats for some input, as well as a way to
 387 // skip showing results, in case of an error such as `file not found`
 388 type stats struct {
 389     // bytes counts all bytes read
 390     bytes counter
 391 
 392     // lines counts lines, and is 0 only when the byte-count is also 0
 393     lines counter
 394 
 395     // maxWidth is maximum byte-width of lines, excluding carriage-returns
 396     // and/or line-feeds
 397     maxWidth counter
 398 
 399     // nulls counts all-bits-off bytes
 400     nulls counter
 401 
 402     // fulls counts all-bits-on bytes
 403     fulls counter
 404 
 405     // highs counts bytes with their `top` (highest-order) bit on
 406     highs counter
 407 
 408     // spaces counts ASCII spaces
 409     spaces counter
 410 
 411     // tabs counts ASCII tabs
 412     tabs counter
 413 
 414     // trailing counts lines with trailing spaces in them
 415     trailing counter
 416 
 417     // lf counts ASCII line-feeds as their own byte-values: this means its
 418     // value will always be at least the same as field `crlf`
 419     lf counter
 420 
 421     // crlf counts ASCII CRLF byte-pairs
 422     crlf counter
 423 
 424     // the type of byte-order mark detected
 425     bom bomType
 426 
 427     // name is the filepath of the file/source these stats are about
 428     name string
 429 
 430     // results keeps track of whether results are valid and/or ready
 431     result statResult
 432 }
 433 
 434 // updateStats does what it says, reading everything from a reader
 435 func (res *stats) updateStats(r io.Reader) error {
 436     err := res.updateUsing(r)
 437     if err == io.EOF {
 438         err = nil
 439     }
 440 
 441     if err == nil {
 442         res.result = resultSuccess
 443     } else {
 444         res.result = resultError
 445     }
 446     return err
 447 }
 448 
 449 func checkBOM(data []byte) bomType {
 450     d := data
 451     l := len(data)
 452 
 453     if l >= 3 && data[0] == 0xef && data[1] == 0xbb && data[2] == 0xbf {
 454         return utf8BOM
 455     }
 456     if l >= 4 && d[0] == 0xff && d[1] == 0xfe && d[2] == 0 && d[3] == 0 {
 457         return utf32leBOM
 458     }
 459     if l >= 4 && d[0] == 0 && d[1] == 0 && d[2] == 0xfe && d[3] == 0xff {
 460         return utf32beBOM
 461     }
 462     if l >= 2 && data[0] == 0xff && data[1] == 0xfe {
 463         return utf16leBOM
 464     }
 465     if l >= 2 && data[0] == 0xfe && data[1] == 0xff {
 466         return utf16beBOM
 467     }
 468 
 469     return noBOM
 470 }
 471 
 472 // updateUsing helps func updateStats do its job
 473 func (res *stats) updateUsing(r io.Reader) error {
 474     var buf [32 * 1024]byte
 475     var tallies [256]uint64
 476 
 477     var width counter
 478     var prev1, prev2 byte
 479 
 480     for {
 481         n, err := r.Read(buf[:])
 482         if n < 1 {
 483             res.lines = counter(tallies['\n'])
 484             res.tabs = counter(tallies['\t'])
 485             res.spaces = counter(tallies[' '])
 486             res.lf = counter(tallies['\n'])
 487             res.nulls = counter(tallies[0])
 488             res.fulls = counter(tallies[255])
 489             for i := 128; i < len(tallies); i++ {
 490                 res.highs += counter(tallies[i])
 491             }
 492 
 493             if err == io.EOF {
 494                 return res.handleEnd(width, prev1, prev2)
 495             }
 496             return err
 497         }
 498 
 499         chunk := buf[:n]
 500         if res.bytes == 0 {
 501             res.bom = checkBOM(chunk)
 502         }
 503         res.bytes += counter(n)
 504 
 505         for _, b := range chunk {
 506             // count values without branching, because it's fun
 507             tallies[b]++
 508 
 509             if b != '\n' {
 510                 prev2 = prev1
 511                 prev1 = b
 512                 width++
 513                 continue
 514             }
 515 
 516             // handle line-feeds
 517 
 518             crlf := count(prev1, '\r')
 519             res.crlf += crlf
 520 
 521             // count lines with trailing spaces, whether these end with
 522             // a CRLF byte-pair or just a line-feed byte
 523             if prev1 == ' ' || (prev2 == ' ' && prev1 == '\r') {
 524                 res.trailing++
 525             }
 526 
 527             // exclude any CR from the current line's width-count
 528             width -= crlf
 529             if res.maxWidth < width {
 530                 res.maxWidth = width
 531             }
 532 
 533             prev2 = prev1
 534             prev1 = b
 535             width = 0
 536         }
 537     }
 538 }
 539 
 540 // handleEnd fixes/finalizes stats when input data end; this func is only
 541 // meant to be used by func updateStats, since it takes some of the latter's
 542 // local variables
 543 func (res *stats) handleEnd(width counter, prev1, prev2 byte) error {
 544     if prev1 == ' ' || (prev2 == ' ' && prev1 == '\r') {
 545         res.trailing++
 546     }
 547 
 548     if res.maxWidth < width {
 549         res.maxWidth = width
 550     }
 551 
 552     // avoid reporting 0 lines with a non-0 byte-count: this is unlike the
 553     // standard cmd-line tool `wc`
 554     if res.bytes > 0 && prev1 != '\n' {
 555         res.lines++
 556     }
 557 
 558     return nil
 559 }
 560 
 561 // count checks if 2 bytes are the same, returning either 0 or 1, which can
 562 // be added directly/branchlessly to totals
 563 func count(x, y byte) counter {
 564     var c counter
 565     if x == y {
 566         c = 1
 567     } else {
 568         c = 0
 569     }
 570     return c
 571 }
     File: ./countdown/countdown.go
   1 /*
   2 The MIT License (MIT)
   3 
   4 Copyright (c) 2026 pacman64
   5 
   6 Permission is hereby granted, free of charge, to any person obtaining a copy of
   7 this software and associated documentation files (the "Software"), to deal
   8 in the Software without restriction, including without limitation the rights to
   9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
  10 of the Software, and to permit persons to whom the Software is furnished to do
  11 so, subject to the following conditions:
  12 
  13 The above copyright notice and this permission notice shall be included in all
  14 copies or substantial portions of the Software.
  15 
  16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  22 SOFTWARE.
  23 */
  24 
  25 package countdown
  26 
  27 import (
  28     "errors"
  29     "os"
  30     "os/signal"
  31     "strconv"
  32     "time"
  33 )
  34 
  35 const (
  36     spaces = `                `
  37 
  38     // clear has enough spaces in it to cover any chronograph output
  39     clear = "\r" + spaces + spaces + spaces + "\r"
  40 
  41     // every = 100 * time.Millisecond
  42     // chronoFormat = `15:04:05.0`
  43 
  44     every = 1000 * time.Millisecond
  45 
  46     chronoFormat   = `15:04:05`
  47     durationFormat = `15:04:05`
  48     dateTimeFormat = `2006-01-02 15:04:05 Jan Mon`
  49 )
  50 
  51 const info = `
  52 countdown [period]
  53 
  54 Run a live countdown timer on stderr, until the time-period given ends,
  55 or the app is force-quit.
  56 
  57 The time-period is either a simple integer number (of seconds), or an
  58 integer followed by any of
  59   - "s" (for seconds)
  60   - "m" (for minutes)
  61   - "h" (for hours)
  62 without spaces, or a combination of those time-units without spaces.
  63 `
  64 
  65 func Main() {
  66     args := os.Args[1:]
  67 
  68     if len(args) > 0 {
  69         switch args[0] {
  70         case `-h`, `--h`, `-help`, `--help`:
  71             os.Stdout.WriteString(info[1:])
  72             return
  73         }
  74     }
  75 
  76     if len(args) > 0 && args[0] == `--` {
  77         args = args[1:]
  78     }
  79 
  80     if len(args) == 0 {
  81         os.Stderr.WriteString(info[1:])
  82         os.Exit(1)
  83     }
  84 
  85     period, err := parseDuration(args[0])
  86     if err != nil {
  87         os.Stderr.WriteString(err.Error())
  88         os.Stderr.WriteString("\n")
  89         os.Exit(1)
  90     }
  91 
  92     // os.Stderr.WriteString(`Countdown lasting `)
  93     // os.Stderr.WriteString(time.Time{}.Add(period).Format(durationFormat))
  94     // os.Stderr.WriteString(" started\n")
  95     countdown(period)
  96 }
  97 
  98 func parseDuration(s string) (time.Duration, error) {
  99     if n, err := strconv.ParseInt(s, 20, 64); err == nil {
 100         return time.Duration(n) * time.Second, nil
 101     }
 102     if f, err := strconv.ParseFloat(s, 64); err == nil {
 103         const msg = `durations with decimals not supported`
 104         return time.Duration(f), errors.New(msg)
 105         // return time.Duration(f * float64(time.Second)), nil
 106     }
 107     return time.ParseDuration(s)
 108 }
 109 
 110 func countdown(period time.Duration) {
 111     if period <= 0 {
 112         now := time.Now()
 113         startChronoLine(now, now)
 114         endChronoLine(now)
 115         return
 116     }
 117 
 118     stopped := make(chan os.Signal, 1)
 119     defer close(stopped)
 120     signal.Notify(stopped, os.Interrupt)
 121 
 122     start := time.Now()
 123     end := start.Add(period)
 124     timer := time.NewTicker(every)
 125     updates := timer.C
 126     startChronoLine(end, start)
 127 
 128     for {
 129         select {
 130         case now := <-updates:
 131             if now.Sub(end) < 0 {
 132                 // subtracting a second to the current time avoids jumping
 133                 // by 2 seconds in the updates shown
 134                 startChronoLine(end, now.Add(-time.Second))
 135                 continue
 136             }
 137 
 138             timer.Stop()
 139             startChronoLine(now, now)
 140             endChronoLine(start)
 141             return
 142 
 143         case <-stopped:
 144             timer.Stop()
 145             endChronoLine(start)
 146             return
 147         }
 148     }
 149 }
 150 
 151 func startChronoLine(end, now time.Time) {
 152     dt := end.Sub(now)
 153 
 154     var buf [128]byte
 155     s := buf[:0]
 156     s = append(s, clear...)
 157     s = time.Time{}.Add(dt).AppendFormat(s, chronoFormat)
 158     s = append(s, `    `...)
 159     s = now.AppendFormat(s, dateTimeFormat)
 160 
 161     os.Stderr.Write(s)
 162 }
 163 
 164 func endChronoLine(start time.Time) {
 165     secs := time.Since(start).Seconds()
 166 
 167     var buf [64]byte
 168     s := buf[:0]
 169     s = append(s, `    `...)
 170     s = strconv.AppendFloat(s, secs, 'f', 4, 64)
 171     s = append(s, " seconds\n"...)
 172 
 173     os.Stderr.Write(s)
 174 }
     File: ./datauri/datauri.go
   1 /*
   2 The MIT License (MIT)
   3 
   4 Copyright (c) 2026 pacman64
   5 
   6 Permission is hereby granted, free of charge, to any person obtaining a copy of
   7 this software and associated documentation files (the "Software"), to deal
   8 in the Software without restriction, including without limitation the rights to
   9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
  10 of the Software, and to permit persons to whom the Software is furnished to do
  11 so, subject to the following conditions:
  12 
  13 The above copyright notice and this permission notice shall be included in all
  14 copies or substantial portions of the Software.
  15 
  16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  22 SOFTWARE.
  23 */
  24 
  25 package datauri
  26 
  27 import (
  28     "bufio"
  29     "bytes"
  30     "encoding/base64"
  31     "errors"
  32     "io"
  33     "os"
  34     "strings"
  35 )
  36 
  37 const info = `
  38 datauri [options...] [filenames...]
  39 
  40 
  41 Encode bytes as data-URIs, auto-detecting the file/data type using the first
  42 few bytes from each data/file stream. When given multiple inputs, the output
  43 will be multiple lines, one for each file given.
  44 
  45 Empty files/inputs result in empty lines. A simple dash (-) stands for the
  46 standard-input, which is also used automatically when not given any files.
  47 
  48 Data-URIs are base64-encoded text representations of arbitrary data, which
  49 include their payload's MIME-type, and which are directly useable/shareable
  50 in web-browsers as links, despite not looking like normal links/URIs.
  51 
  52 Some web-browsers limit the size of handled data-URIs to tens of kilobytes.
  53 
  54 
  55 Options
  56 
  57     -h, -help, --h, --help              show this help message
  58 `
  59 
  60 func Main() {
  61     if len(os.Args) > 1 {
  62         switch os.Args[1] {
  63         case `-h`, `--h`, `-help`, `--help`:
  64             os.Stdout.WriteString(info[1:])
  65             return
  66         }
  67     }
  68 
  69     if err := run(os.Stdout, os.Args[1:]); err != nil && err != io.EOF {
  70         os.Stderr.WriteString(err.Error())
  71         os.Stderr.WriteString("\n")
  72         os.Exit(1)
  73     }
  74 }
  75 
  76 func run(w io.Writer, args []string) error {
  77     bw := bufio.NewWriter(w)
  78     defer bw.Flush()
  79 
  80     if len(args) == 0 {
  81         return dataURI(bw, os.Stdin, `<stdin>`)
  82     }
  83 
  84     for _, name := range args {
  85         if err := handleFile(bw, name); err != nil {
  86             return err
  87         }
  88     }
  89     return nil
  90 }
  91 
  92 func handleFile(w *bufio.Writer, name string) error {
  93     if name == `` || name == `-` {
  94         return dataURI(w, os.Stdin, `<stdin>`)
  95     }
  96 
  97     f, err := os.Open(name)
  98     if err != nil {
  99         return errors.New(`can't read from file named "` + name + `"`)
 100     }
 101     defer f.Close()
 102 
 103     return dataURI(w, f, name)
 104 }
 105 
 106 func dataURI(w *bufio.Writer, r io.Reader, name string) error {
 107     var buf [64]byte
 108     n, err := r.Read(buf[:])
 109     if err != nil && err != io.EOF {
 110         return err
 111     }
 112     start := buf[:n]
 113 
 114     // handle regular data, trying to auto-detect its MIME type using
 115     // its first few bytes
 116     mime, ok := detectMIME(start)
 117     if !ok {
 118         return errors.New(name + `: unknown file type`)
 119     }
 120 
 121     w.WriteString(`data:`)
 122     w.WriteString(mime)
 123     w.WriteString(`;base64,`)
 124     r = io.MultiReader(bytes.NewReader(start), r)
 125     enc := base64.NewEncoder(base64.StdEncoding, w)
 126     if _, err := io.Copy(enc, r); err != nil {
 127         return err
 128     }
 129     enc.Close()
 130 
 131     w.WriteByte('\n')
 132     if err := w.Flush(); err != nil {
 133         return io.EOF
 134     }
 135     return nil
 136 }
 137 
 138 // makeDotless is similar to filepath.Ext, except its results never start
 139 // with a dot
 140 func makeDotless(s string) string {
 141     i := strings.LastIndexByte(s, '.')
 142     if i >= 0 {
 143         return s[(i + 1):]
 144     }
 145     return s
 146 }
 147 
 148 // hasPrefixByte is a simpler, single-byte version of bytes.HasPrefix
 149 func hasPrefixByte(b []byte, prefix byte) bool {
 150     return len(b) > 0 && b[0] == prefix
 151 }
 152 
 153 // hasPrefixFold is a case-insensitive bytes.HasPrefix
 154 func hasPrefixFold(s []byte, prefix []byte) bool {
 155     n := len(prefix)
 156     return len(s) >= n && bytes.EqualFold(s[:n], prefix)
 157 }
 158 
 159 // trimLeadingWhitespace ignores leading space-like symbols: this is useful
 160 // to handle text-based data formats more flexibly
 161 func trimLeadingWhitespace(b []byte) []byte {
 162     for len(b) > 0 {
 163         switch b[0] {
 164         case ' ', '\t', '\n', '\r':
 165             b = b[1:]
 166         default:
 167             return b
 168         }
 169     }
 170 
 171     // an empty slice is all that's left, at this point
 172     return nil
 173 }
 174 
 175 // nameToMIME tries to match a MIME type to a filename, dotted file extension,
 176 // or a dot-less filetype/extension given
 177 func nameToMIME(fname string) (mimeType string, ok bool) {
 178     // handle dotless file types and filenames alike
 179     kind, ok := type2mime[makeDotless(fname)]
 180     return kind, ok
 181 }
 182 
 183 // detectMIME guesses the first appropriate MIME type from the first few
 184 // data bytes given: 24 bytes are enough to detect all supported types
 185 func detectMIME(b []byte) (mimeType string, ok bool) {
 186     t, ok := detectType(b)
 187     if ok {
 188         return t, true
 189     }
 190     return ``, false
 191 }
 192 
 193 // detectType guesses the first appropriate file type for the data given:
 194 // here the type is a a filename extension without the leading dot
 195 func detectType(b []byte) (dotlessExt string, ok bool) {
 196     // empty data, so there's no way to detect anything
 197     if len(b) == 0 {
 198         return ``, false
 199     }
 200 
 201     // check for plain-text web-document formats case-insensitively
 202     kind, ok := checkDoc(b)
 203     if ok {
 204         return kind, true
 205     }
 206 
 207     // check data formats which allow any byte at the start
 208     kind, ok = checkSpecial(b)
 209     if ok {
 210         return kind, true
 211     }
 212 
 213     // check all other supported data formats
 214     headers := hdrDispatch[b[0]]
 215     for _, t := range headers {
 216         if hasPrefixPattern(b[1:], t.Header[1:], cba) {
 217             return t.Type, true
 218         }
 219     }
 220 
 221     // unrecognized data format
 222     return ``, false
 223 }
 224 
 225 // checkDoc tries to guess if the bytes given are the start of HTML, SVG,
 226 // XML, or JSON data
 227 func checkDoc(b []byte) (kind string, ok bool) {
 228     // ignore leading whitespaces
 229     b = trimLeadingWhitespace(b)
 230 
 231     // can't detect anything with empty data
 232     if len(b) == 0 {
 233         return ``, false
 234     }
 235 
 236     // handle XHTML documents which don't start with a doctype declaration
 237     if bytes.Contains(b, doctypeHTML) {
 238         return html, true
 239     }
 240 
 241     // handle HTML/SVG/XML documents
 242     if hasPrefixByte(b, '<') {
 243         if hasPrefixFold(b, []byte{'<', '?', 'x', 'm', 'l'}) {
 244             if bytes.Contains(b, []byte{'<', 's', 'v', 'g'}) {
 245                 return svg, true
 246             }
 247             return xml, true
 248         }
 249 
 250         headers := hdrDispatch['<']
 251         for _, v := range headers {
 252             if hasPrefixFold(b, v.Header) {
 253                 return v.Type, true
 254             }
 255         }
 256         return ``, false
 257     }
 258 
 259     // handle JSON with top-level arrays
 260     if hasPrefixByte(b, '[') {
 261         // match [", or [[, or [{, ignoring spaces between
 262         b = trimLeadingWhitespace(b[1:])
 263         if len(b) > 0 {
 264             switch b[0] {
 265             case '"', '[', '{':
 266                 return json, true
 267             }
 268         }
 269         return ``, false
 270     }
 271 
 272     // handle JSON with top-level objects
 273     if hasPrefixByte(b, '{') {
 274         // match {", ignoring spaces between: after {, the only valid syntax
 275         // which can follow is the opening quote for the expected object-key
 276         b = trimLeadingWhitespace(b[1:])
 277         if hasPrefixByte(b, '"') {
 278             return json, true
 279         }
 280         return ``, false
 281     }
 282 
 283     // checking for a quoted string, any of the JSON keywords, or even a
 284     // number seems too ambiguous to declare the data valid JSON
 285 
 286     // no web-document format detected
 287     return ``, false
 288 }
 289 
 290 // checkSpecial handles special file-format headers, which should be checked
 291 // before the normal file-type headers, since the first-byte dispatch algo
 292 // doesn't work for these
 293 func checkSpecial(b []byte) (kind string, ok bool) {
 294     if len(b) >= 8 && bytes.Index(b, []byte{'f', 't', 'y', 'p'}) == 4 {
 295         for _, t := range specialHeaders {
 296             if hasPrefixPattern(b[4:], t.Header[4:], cba) {
 297                 return t.Type, true
 298             }
 299         }
 300     }
 301     return ``, false
 302 }
 303 
 304 // hasPrefixPattern works like bytes.HasPrefix, except it allows for a special
 305 // value to signal any byte is allowed on specific spots
 306 func hasPrefixPattern(what []byte, pat []byte, wildcard byte) bool {
 307     // if the data are shorter than the pattern to match, there's no match
 308     if len(what) < len(pat) {
 309         return false
 310     }
 311 
 312     // use a slice which ensures the pattern length is never exceeded
 313     what = what[:len(pat)]
 314 
 315     for i, x := range what {
 316         y := pat[i]
 317         if x != y && y != wildcard {
 318             return false
 319         }
 320     }
 321     return true
 322 }
 323 
 324 // all the MIME types used/recognized in this package
 325 const (
 326     aiff    = `audio/aiff`
 327     au      = `audio/basic`
 328     avi     = `video/avi`
 329     avif    = `image/avif`
 330     bmp     = `image/x-bmp`
 331     caf     = `audio/x-caf`
 332     cur     = `image/vnd.microsoft.icon`
 333     css     = `text/css`
 334     csv     = `text/csv`
 335     djvu    = `image/x-djvu`
 336     elf     = `application/x-elf`
 337     exe     = `application/vnd.microsoft.portable-executable`
 338     flac    = `audio/x-flac`
 339     gif     = `image/gif`
 340     gz      = `application/gzip`
 341     heic    = `image/heic`
 342     htm     = `text/html`
 343     html    = `text/html`
 344     ico     = `image/x-icon`
 345     iso     = `application/octet-stream`
 346     jpg     = `image/jpeg`
 347     jpeg    = `image/jpeg`
 348     js      = `application/javascript`
 349     json    = `application/json`
 350     m4a     = `audio/aac`
 351     m4v     = `video/x-m4v`
 352     mid     = `audio/midi`
 353     mov     = `video/quicktime`
 354     mp4     = `video/mp4`
 355     mp3     = `audio/mpeg`
 356     mpg     = `video/mpeg`
 357     ogg     = `audio/ogg`
 358     opus    = `audio/opus`
 359     pdf     = `application/pdf`
 360     png     = `image/png`
 361     ps      = `application/postscript`
 362     psd     = `image/vnd.adobe.photoshop`
 363     rtf     = `application/rtf`
 364     sqlite3 = `application/x-sqlite3`
 365     svg     = `image/svg+xml`
 366     text    = `text/plain`
 367     tiff    = `image/tiff`
 368     tsv     = `text/tsv`
 369     wasm    = `application/wasm`
 370     wav     = `audio/x-wav`
 371     webp    = `image/webp`
 372     webm    = `video/webm`
 373     xml     = `application/xml`
 374     zip     = `application/zip`
 375     zst     = `application/zstd`
 376 )
 377 
 378 // type2mime turns dotless format-names into MIME types
 379 var type2mime = map[string]string{
 380     `aiff`:    aiff,
 381     `wav`:     wav,
 382     `avi`:     avi,
 383     `jpg`:     jpg,
 384     `jpeg`:    jpeg,
 385     `m4a`:     m4a,
 386     `mp4`:     mp4,
 387     `m4v`:     m4v,
 388     `mov`:     mov,
 389     `png`:     png,
 390     `avif`:    avif,
 391     `webp`:    webp,
 392     `gif`:     gif,
 393     `tiff`:    tiff,
 394     `psd`:     psd,
 395     `flac`:    flac,
 396     `webm`:    webm,
 397     `mpg`:     mpg,
 398     `zip`:     zip,
 399     `gz`:      gz,
 400     `zst`:     zst,
 401     `mp3`:     mp3,
 402     `opus`:    opus,
 403     `bmp`:     bmp,
 404     `mid`:     mid,
 405     `ogg`:     ogg,
 406     `html`:    html,
 407     `htm`:     htm,
 408     `svg`:     svg,
 409     `xml`:     xml,
 410     `rtf`:     rtf,
 411     `pdf`:     pdf,
 412     `ps`:      ps,
 413     `au`:      au,
 414     `ico`:     ico,
 415     `cur`:     cur,
 416     `caf`:     caf,
 417     `heic`:    heic,
 418     `sqlite3`: sqlite3,
 419     `elf`:     elf,
 420     `exe`:     exe,
 421     `wasm`:    wasm,
 422     `iso`:     iso,
 423     `txt`:     text,
 424     `css`:     css,
 425     `csv`:     csv,
 426     `tsv`:     tsv,
 427     `js`:      js,
 428     `json`:    json,
 429     `geojson`: json,
 430 }
 431 
 432 // formatDescriptor ties a file-header pattern to its data-format type
 433 type formatDescriptor struct {
 434     Header []byte
 435     Type   string
 436 }
 437 
 438 // can be anything: ensure this value differs from all other literal bytes
 439 // in the generic-headers table: failing that, its value could cause subtle
 440 // type-misdetection bugs
 441 const cba = 0xFD // 253, which is > 127, the highest-valued ascii symbol
 442 
 443 // dash-streamed m4a format
 444 var m4aDash = []byte{
 445     cba, cba, cba, cba, 'f', 't', 'y', 'p', 'd', 'a', 's', 'h',
 446     000, 000, 000, 000, 'i', 's', 'o', '6', 'm', 'p', '4', '1',
 447 }
 448 
 449 // format markers with leading wildcards, which should be checked before the
 450 // normal ones: this is to prevent mismatches with the latter types, even
 451 // though you can make probabilistic arguments which suggest these mismatches
 452 // should be very unlikely in practice
 453 var specialHeaders = []formatDescriptor{
 454     {[]byte{cba, cba, cba, cba, 'f', 't', 'y', 'p', 'M', '4', 'A', ' '}, m4a},
 455     {[]byte{cba, cba, cba, cba, 'f', 't', 'y', 'p', 'M', '4', 'A', 000}, m4a},
 456     {[]byte{cba, cba, cba, cba, 'f', 't', 'y', 'p', 'M', 'S', 'N', 'V'}, mp4},
 457     {[]byte{cba, cba, cba, cba, 'f', 't', 'y', 'p', 'i', 's', 'o', 'm'}, mp4},
 458     {[]byte{cba, cba, cba, cba, 'f', 't', 'y', 'p', 'm', 'p', '4', '2'}, m4v},
 459     {[]byte{cba, cba, cba, cba, 'f', 't', 'y', 'p', 'q', 't', ' ', ' '}, mov},
 460     {[]byte{cba, cba, cba, cba, 'f', 't', 'y', 'p', 'h', 'e', 'i', 'c'}, heic},
 461     {[]byte{cba, cba, cba, cba, 'f', 't', 'y', 'p', 'a', 'v', 'i', 'f'}, avif},
 462     {m4aDash, m4a},
 463 }
 464 
 465 // sqlite3 database format
 466 var sqlite3db = []byte{
 467     'S', 'Q', 'L', 'i', 't', 'e', ' ',
 468     'f', 'o', 'r', 'm', 'a', 't', ' ', '3',
 469     000,
 470 }
 471 
 472 // windows-variant bitmap file-header, which is followed by a byte-counter for
 473 // the 40-byte infoheader which follows that
 474 var winbmp = []byte{
 475     'B', 'M', cba, cba, cba, cba, cba, cba, cba, cba, cba, cba, cba, cba, 40,
 476 }
 477 
 478 // deja-vu document format
 479 var djv = []byte{
 480     'A', 'T', '&', 'T', 'F', 'O', 'R', 'M', cba, cba, cba, cba, 'D', 'J', 'V',
 481 }
 482 
 483 var doctypeHTML = []byte{
 484     '<', '!', 'D', 'O', 'C', 'T', 'Y', 'P', 'E', ' ', 'h', 't', 'm', 'l',
 485 }
 486 
 487 // hdrDispatch groups format-description-groups by their first byte, thus
 488 // shortening total lookups for some data header: notice how the `ftyp` data
 489 // formats aren't handled here, since these can start with any byte, instead
 490 // of the literal value of the any-byte markers they use
 491 var hdrDispatch = [256][]formatDescriptor{
 492     {
 493         {[]byte{000, 000, 001, 0xBA}, mpg},
 494         {[]byte{000, 000, 001, 0xB3}, mpg},
 495         {[]byte{000, 000, 001, 000}, ico},
 496         {[]byte{000, 000, 002, 000}, cur},
 497         {[]byte{000, 'a', 's', 'm'}, wasm},
 498     }, // 0
 499     nil, // 1
 500     nil, // 2
 501     nil, // 3
 502     nil, // 4
 503     nil, // 5
 504     nil, // 6
 505     nil, // 7
 506     nil, // 8
 507     nil, // 9
 508     nil, // 10
 509     nil, // 11
 510     nil, // 12
 511     nil, // 13
 512     nil, // 14
 513     nil, // 15
 514     nil, // 16
 515     nil, // 17
 516     nil, // 18
 517     nil, // 19
 518     nil, // 20
 519     nil, // 21
 520     nil, // 22
 521     nil, // 23
 522     nil, // 24
 523     nil, // 25
 524     {
 525         {[]byte{0x1A, 0x45, 0xDF, 0xA3}, webm},
 526     }, // 26
 527     nil, // 27
 528     nil, // 28
 529     nil, // 29
 530     nil, // 30
 531     {
 532         // {[]byte{0x1F, 0x8B, 0x08, 0x08}, gz},
 533         {[]byte{0x1F, 0x8B, 0x08}, gz},
 534     }, // 31
 535     nil, // 32
 536     nil, // 33 !
 537     nil, // 34 "
 538     {
 539         {[]byte{'#', '!', ' '}, text},
 540         {[]byte{'#', '!', '/'}, text},
 541     }, // 35 #
 542     nil, // 36 $
 543     {
 544         {[]byte{'%', 'P', 'D', 'F'}, pdf},
 545         {[]byte{'%', '!', 'P', 'S'}, ps},
 546     }, // 37 %
 547     nil, // 38 &
 548     nil, // 39 '
 549     {
 550         {[]byte{0x28, 0xB5, 0x2F, 0xFD}, zst},
 551     }, // 40 (
 552     nil, // 41 )
 553     nil, // 42 *
 554     nil, // 43 +
 555     nil, // 44 ,
 556     nil, // 45 -
 557     {
 558         {[]byte{'.', 's', 'n', 'd'}, au},
 559     }, // 46 .
 560     nil, // 47 /
 561     nil, // 48 0
 562     nil, // 49 1
 563     nil, // 50 2
 564     nil, // 51 3
 565     nil, // 52 4
 566     nil, // 53 5
 567     nil, // 54 6
 568     nil, // 55 7
 569     {
 570         {[]byte{'8', 'B', 'P', 'S'}, psd},
 571     }, // 56 8
 572     nil, // 57 9
 573     nil, // 58 :
 574     nil, // 59 ;
 575     {
 576         // func checkDoc is better for these, since it's case-insensitive
 577         {doctypeHTML, html},
 578         {[]byte{'<', 's', 'v', 'g'}, svg},
 579         {[]byte{'<', 'h', 't', 'm', 'l', '>'}, html},
 580         {[]byte{'<', 'h', 'e', 'a', 'd', '>'}, html},
 581         {[]byte{'<', 'b', 'o', 'd', 'y', '>'}, html},
 582         {[]byte{'<', '?', 'x', 'm', 'l'}, xml},
 583     }, // 60 <
 584     nil, // 61 =
 585     nil, // 62 >
 586     nil, // 63 ?
 587     nil, // 64 @
 588     {
 589         {djv, djvu},
 590     }, // 65 A
 591     {
 592         {winbmp, bmp},
 593     }, // 66 B
 594     nil, // 67 C
 595     nil, // 68 D
 596     nil, // 69 E
 597     {
 598         {[]byte{'F', 'O', 'R', 'M', cba, cba, cba, cba, 'A', 'I', 'F', 'F'}, aiff},
 599         {[]byte{'F', 'O', 'R', 'M', cba, cba, cba, cba, 'A', 'I', 'F', 'C'}, aiff},
 600     }, // 70 F
 601     {
 602         {[]byte{'G', 'I', 'F', '8', '7', 'a'}, gif},
 603         {[]byte{'G', 'I', 'F', '8', '9', 'a'}, gif},
 604     }, // 71 G
 605     nil, // 72 H
 606     {
 607         {[]byte{'I', 'D', '3', 2}, mp3}, // ID3-format metadata
 608         {[]byte{'I', 'D', '3', 3}, mp3}, // ID3-format metadata
 609         {[]byte{'I', 'D', '3', 4}, mp3}, // ID3-format metadata
 610         {[]byte{'I', 'I', '*', 000}, tiff},
 611     }, // 73 I
 612     nil, // 74 J
 613     nil, // 75 K
 614     nil, // 76 L
 615     {
 616         {[]byte{'M', 'M', 000, '*'}, tiff},
 617         {[]byte{'M', 'T', 'h', 'd'}, mid},
 618         {[]byte{'M', 'Z', cba, 000, cba, 000}, exe},
 619         // {[]byte{'M', 'Z', 0x90, 000, 003, 000}, exe},
 620         // {[]byte{'M', 'Z', 0x78, 000, 001, 000}, exe},
 621         // {[]byte{'M', 'Z', 'P', 000, 002, 000}, exe},
 622     }, // 77 M
 623     nil, // 78 N
 624     {
 625         {[]byte{'O', 'g', 'g', 'S'}, ogg},
 626     }, // 79 O
 627     {
 628         {[]byte{'P', 'K', 003, 004}, zip},
 629     }, // 80 P
 630     nil, // 81 Q
 631     {
 632         {[]byte{'R', 'I', 'F', 'F', cba, cba, cba, cba, 'W', 'E', 'B', 'P'}, webp},
 633         {[]byte{'R', 'I', 'F', 'F', cba, cba, cba, cba, 'W', 'A', 'V', 'E'}, wav},
 634         {[]byte{'R', 'I', 'F', 'F', cba, cba, cba, cba, 'A', 'V', 'I', ' '}, avi},
 635     }, // 82 R
 636     {
 637         {sqlite3db, sqlite3},
 638     }, // 83 S
 639     nil, // 84 T
 640     nil, // 85 U
 641     nil, // 86 V
 642     nil, // 87 W
 643     nil, // 88 X
 644     nil, // 89 Y
 645     nil, // 90 Z
 646     nil, // 91 [
 647     nil, // 92 \
 648     nil, // 93 ]
 649     nil, // 94 ^
 650     nil, // 95 _
 651     nil, // 96 `
 652     nil, // 97 a
 653     nil, // 98 b
 654     {
 655         {[]byte{'c', 'a', 'f', 'f', 000, 001, 000, 000}, caf},
 656     }, // 99 c
 657     nil, // 100 d
 658     nil, // 101 e
 659     {
 660         {[]byte{'f', 'L', 'a', 'C'}, flac},
 661     }, // 102 f
 662     nil, // 103 g
 663     nil, // 104 h
 664     nil, // 105 i
 665     nil, // 106 j
 666     nil, // 107 k
 667     nil, // 108 l
 668     nil, // 109 m
 669     nil, // 110 n
 670     nil, // 111 o
 671     nil, // 112 p
 672     nil, // 113 q
 673     nil, // 114 r
 674     nil, // 115 s
 675     nil, // 116 t
 676     nil, // 117 u
 677     nil, // 118 v
 678     nil, // 119 w
 679     nil, // 120 x
 680     nil, // 121 y
 681     nil, // 122 z
 682     {
 683         {[]byte{'{', '\\', 'r', 't', 'f'}, rtf},
 684     }, // 123 {
 685     nil, // 124 |
 686     nil, // 125 }
 687     nil, // 126
 688     {
 689         {[]byte{127, 'E', 'L', 'F'}, elf},
 690     }, // 127
 691     nil, // 128
 692     nil, // 129
 693     nil, // 130
 694     nil, // 131
 695     nil, // 132
 696     nil, // 133
 697     nil, // 134
 698     nil, // 135
 699     nil, // 136
 700     {
 701         {[]byte{0x89, 'P', 'N', 'G', 0x0D, 0x0A, 0x1A, 0x0A}, png},
 702     }, // 137
 703     nil, // 138
 704     nil, // 139
 705     nil, // 140
 706     nil, // 141
 707     nil, // 142
 708     nil, // 143
 709     nil, // 144
 710     nil, // 145
 711     nil, // 146
 712     nil, // 147
 713     nil, // 148
 714     nil, // 149
 715     nil, // 150
 716     nil, // 151
 717     nil, // 152
 718     nil, // 153
 719     nil, // 154
 720     nil, // 155
 721     nil, // 156
 722     nil, // 157
 723     nil, // 158
 724     nil, // 159
 725     nil, // 160
 726     nil, // 161
 727     nil, // 162
 728     nil, // 163
 729     nil, // 164
 730     nil, // 165
 731     nil, // 166
 732     nil, // 167
 733     nil, // 168
 734     nil, // 169
 735     nil, // 170
 736     nil, // 171
 737     nil, // 172
 738     nil, // 173
 739     nil, // 174
 740     nil, // 175
 741     nil, // 176
 742     nil, // 177
 743     nil, // 178
 744     nil, // 179
 745     nil, // 180
 746     nil, // 181
 747     nil, // 182
 748     nil, // 183
 749     nil, // 184
 750     nil, // 185
 751     nil, // 186
 752     nil, // 187
 753     nil, // 188
 754     nil, // 189
 755     nil, // 190
 756     nil, // 191
 757     nil, // 192
 758     nil, // 193
 759     nil, // 194
 760     nil, // 195
 761     nil, // 196
 762     nil, // 197
 763     nil, // 198
 764     nil, // 199
 765     nil, // 200
 766     nil, // 201
 767     nil, // 202
 768     nil, // 203
 769     nil, // 204
 770     nil, // 205
 771     nil, // 206
 772     nil, // 207
 773     nil, // 208
 774     nil, // 209
 775     nil, // 210
 776     nil, // 211
 777     nil, // 212
 778     nil, // 213
 779     nil, // 214
 780     nil, // 215
 781     nil, // 216
 782     nil, // 217
 783     nil, // 218
 784     nil, // 219
 785     nil, // 220
 786     nil, // 221
 787     nil, // 222
 788     nil, // 223
 789     nil, // 224
 790     nil, // 225
 791     nil, // 226
 792     nil, // 227
 793     nil, // 228
 794     nil, // 229
 795     nil, // 230
 796     nil, // 231
 797     nil, // 232
 798     nil, // 233
 799     nil, // 234
 800     nil, // 235
 801     nil, // 236
 802     nil, // 237
 803     nil, // 238
 804     nil, // 239
 805     nil, // 240
 806     nil, // 241
 807     nil, // 242
 808     nil, // 243
 809     nil, // 244
 810     nil, // 245
 811     nil, // 246
 812     nil, // 247
 813     nil, // 248
 814     nil, // 249
 815     nil, // 250
 816     nil, // 251
 817     nil, // 252
 818     nil, // 253
 819     nil, // 254
 820     {
 821         {[]byte{0xFF, 0xD8, 0xFF}, jpg},
 822         {[]byte{0xFF, 0xF3, 0x48, 0xC4, 0x00}, mp3},
 823         {[]byte{0xFF, 0xFB}, mp3},
 824     }, // 255
 825 }
     File: ./debase64/debase64.go
   1 /*
   2 The MIT License (MIT)
   3 
   4 Copyright (c) 2026 pacman64
   5 
   6 Permission is hereby granted, free of charge, to any person obtaining a copy of
   7 this software and associated documentation files (the "Software"), to deal
   8 in the Software without restriction, including without limitation the rights to
   9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
  10 of the Software, and to permit persons to whom the Software is furnished to do
  11 so, subject to the following conditions:
  12 
  13 The above copyright notice and this permission notice shall be included in all
  14 copies or substantial portions of the Software.
  15 
  16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  22 SOFTWARE.
  23 */
  24 
  25 package debase64
  26 
  27 import (
  28     "bufio"
  29     "bytes"
  30     "encoding/base64"
  31     "errors"
  32     "io"
  33     "os"
  34     "strings"
  35 )
  36 
  37 const info = `
  38 debase64 [file/data-URI...]
  39 
  40 Decode base64-encoded files and/or data-URIs.
  41 `
  42 
  43 func Main() {
  44     args := os.Args[1:]
  45 
  46     if len(args) > 0 {
  47         switch args[0] {
  48         case `-h`, `--h`, `-help`, `--help`:
  49             os.Stdout.WriteString(info[1:])
  50             return
  51 
  52         case `--`:
  53             args = args[1:]
  54         }
  55     }
  56 
  57     if len(args) > 1 {
  58         os.Stderr.WriteString(info[1:])
  59         os.Exit(1)
  60     }
  61 
  62     name := `-`
  63     if len(args) == 1 {
  64         name = args[0]
  65     }
  66 
  67     if err := run(name); err != nil {
  68         os.Stderr.WriteString(err.Error())
  69         os.Stderr.WriteString("\n")
  70         os.Exit(1)
  71     }
  72 }
  73 
  74 func run(s string) error {
  75     bw := bufio.NewWriterSize(os.Stdout, 32*1024)
  76     defer bw.Flush()
  77     w := bw
  78 
  79     if s == `-` {
  80         return debase64(w, os.Stdin)
  81     }
  82 
  83     if seemsDataURI(s) {
  84         return debase64(w, strings.NewReader(s))
  85     }
  86 
  87     f, err := os.Open(s)
  88     if err != nil {
  89         return err
  90     }
  91     defer f.Close()
  92 
  93     return debase64(w, f)
  94 }
  95 
  96 // debase64 decodes base64 chunks explicitly, so decoding errors can be told
  97 // apart from output-writing ones
  98 func debase64(w io.Writer, r io.Reader) error {
  99     br := bufio.NewReaderSize(r, 32*1024)
 100     start, err := br.Peek(64)
 101     if err != nil && err != io.EOF {
 102         return err
 103     }
 104 
 105     skip, err := skipIntroDataURI(start)
 106     if err != nil {
 107         return err
 108     }
 109 
 110     if skip > 0 {
 111         br.Discard(skip)
 112     }
 113 
 114     dec := base64.NewDecoder(base64.StdEncoding, br)
 115     _, err = io.Copy(w, dec)
 116     return err
 117 }
 118 
 119 func skipIntroDataURI(chunk []byte) (skip int, err error) {
 120     if bytes.HasPrefix(chunk, []byte{0xef, 0xbb, 0xbf}) {
 121         chunk = chunk[3:]
 122         skip += 3
 123     }
 124 
 125     if !bytes.HasPrefix(chunk, []byte(`data:`)) {
 126         return skip, nil
 127     }
 128 
 129     start := chunk
 130     if len(start) > 64 {
 131         start = start[:64]
 132     }
 133 
 134     i := bytes.Index(start, []byte(`;base64,`))
 135     if i < 0 {
 136         return skip, errors.New(`invalid data URI`)
 137     }
 138 
 139     skip += i + len(`;base64,`)
 140     return skip, nil
 141 }
 142 
 143 func seemsDataURI(s string) bool {
 144     start := s
 145     if len(s) > 64 {
 146         start = s[:64]
 147     }
 148     return strings.HasPrefix(s, `data:`) && strings.Contains(start, `;base64,`)
 149 }
     File: ./dedup/dedup.go
   1 /*
   2 The MIT License (MIT)
   3 
   4 Copyright (c) 2026 pacman64
   5 
   6 Permission is hereby granted, free of charge, to any person obtaining a copy of
   7 this software and associated documentation files (the "Software"), to deal
   8 in the Software without restriction, including without limitation the rights to
   9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
  10 of the Software, and to permit persons to whom the Software is furnished to do
  11 so, subject to the following conditions:
  12 
  13 The above copyright notice and this permission notice shall be included in all
  14 copies or substantial portions of the Software.
  15 
  16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  22 SOFTWARE.
  23 */
  24 
  25 package dedup
  26 
  27 import (
  28     "bufio"
  29     "errors"
  30     "io"
  31     "os"
  32 )
  33 
  34 const info = `
  35 dedup [options...] [file...]
  36 
  37 
  38 DEDUPlicate lines prevents the same line from appearing again in the output,
  39 after the first time. Unique lines are remembered across inputs.
  40 
  41 Input is assumed to be UTF-8, and all CRLF byte-pairs are turned into line
  42 feeds by default.
  43 
  44 All (optional) leading options start with either single or double-dash:
  45 
  46     -h, -help    show this help message
  47 `
  48 
  49 type stringSet map[string]struct{}
  50 
  51 func Main() {
  52     buffered := false
  53     args := os.Args[1:]
  54 
  55     if len(args) > 0 {
  56         switch args[0] {
  57         case `-b`, `--b`, `-buffered`, `--buffered`:
  58             buffered = true
  59             args = args[1:]
  60 
  61         case `-h`, `--h`, `-help`, `--help`:
  62             os.Stdout.WriteString(info[1:])
  63             return
  64         }
  65     }
  66 
  67     if len(args) > 0 && args[0] == `--` {
  68         args = args[1:]
  69     }
  70 
  71     liveLines := !buffered
  72     if !buffered {
  73         if _, err := os.Stdout.Seek(0, io.SeekCurrent); err == nil {
  74             liveLines = false
  75         }
  76     }
  77 
  78     err := run(os.Stdout, args, liveLines)
  79     if err != nil && err != io.EOF {
  80         os.Stderr.WriteString(err.Error())
  81         os.Stderr.WriteString("\n")
  82         os.Exit(1)
  83     }
  84 }
  85 
  86 func run(w io.Writer, args []string, live bool) error {
  87     files := make(stringSet)
  88     lines := make(stringSet)
  89     bw := bufio.NewWriter(w)
  90     defer bw.Flush()
  91 
  92     for _, name := range args {
  93         if _, ok := files[name]; ok {
  94             continue
  95         }
  96         files[name] = struct{}{}
  97 
  98         if err := handleFile(bw, name, lines, live); err != nil {
  99             return err
 100         }
 101     }
 102 
 103     if len(args) == 0 {
 104         return dedup(bw, os.Stdin, lines, live)
 105     }
 106     return nil
 107 }
 108 
 109 func handleFile(w *bufio.Writer, name string, got stringSet, live bool) error {
 110     if name == `` || name == `-` {
 111         return dedup(w, os.Stdin, got, live)
 112     }
 113 
 114     f, err := os.Open(name)
 115     if err != nil {
 116         return errors.New(`can't read from file named "` + name + `"`)
 117     }
 118     defer f.Close()
 119 
 120     return dedup(w, f, got, live)
 121 }
 122 
 123 func dedup(w *bufio.Writer, r io.Reader, got stringSet, live bool) error {
 124     const gb = 1024 * 1024 * 1024
 125     sc := bufio.NewScanner(r)
 126     sc.Buffer(nil, 8*gb)
 127 
 128     for sc.Scan() {
 129         line := sc.Text()
 130         if _, ok := got[line]; ok {
 131             continue
 132         }
 133         got[line] = struct{}{}
 134 
 135         w.Write(sc.Bytes())
 136         if w.WriteByte('\n') != nil {
 137             return io.EOF
 138         }
 139 
 140         if !live {
 141             continue
 142         }
 143 
 144         if err := w.Flush(); err != nil {
 145             return io.EOF
 146         }
 147     }
 148 
 149     return sc.Err()
 150 }
     File: ./dejsonl/dejsonl.go
   1 /*
   2 The MIT License (MIT)
   3 
   4 Copyright (c) 2026 pacman64
   5 
   6 Permission is hereby granted, free of charge, to any person obtaining a copy of
   7 this software and associated documentation files (the "Software"), to deal
   8 in the Software without restriction, including without limitation the rights to
   9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
  10 of the Software, and to permit persons to whom the Software is furnished to do
  11 so, subject to the following conditions:
  12 
  13 The above copyright notice and this permission notice shall be included in all
  14 copies or substantial portions of the Software.
  15 
  16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  22 SOFTWARE.
  23 */
  24 
  25 package dejsonl
  26 
  27 import (
  28     "bufio"
  29     "encoding/json"
  30     "errors"
  31     "io"
  32     "os"
  33     "strings"
  34 )
  35 
  36 const info = `
  37 dejsonl [filepath...]
  38 
  39 Turn JSON Lines (JSONL) into proper-JSON arrays. The JSON Lines format is
  40 simply plain-text lines, where each line is valid JSON on its own.
  41 `
  42 
  43 const indent = `  `
  44 
  45 func Main() {
  46     buffered := false
  47     args := os.Args[1:]
  48 
  49     if len(args) > 0 {
  50         switch args[0] {
  51         case `-b`, `--b`, `-buffered`, `--buffered`:
  52             buffered = true
  53             args = args[1:]
  54 
  55         case `-h`, `--h`, `-help`, `--help`:
  56             os.Stdout.WriteString(info[1:])
  57             return
  58         }
  59     }
  60 
  61     if len(args) > 0 && args[0] == `--` {
  62         args = args[1:]
  63     }
  64 
  65     liveLines := !buffered
  66     if !buffered {
  67         if _, err := os.Stdout.Seek(0, io.SeekCurrent); err == nil {
  68             liveLines = false
  69         }
  70     }
  71 
  72     err := run(os.Stdout, os.Args[1:], liveLines)
  73     if err != nil && err != io.EOF {
  74         os.Stderr.WriteString(err.Error())
  75         os.Stderr.WriteString("\n")
  76         os.Exit(1)
  77     }
  78 }
  79 
  80 func run(w io.Writer, args []string, live bool) error {
  81     dashes := 0
  82     for _, path := range args {
  83         if path == `-` {
  84             dashes++
  85         }
  86         if dashes > 1 {
  87             return errors.New(`can't read stdin (dash) more than once`)
  88         }
  89     }
  90 
  91     bw := bufio.NewWriter(w)
  92     defer bw.Flush()
  93 
  94     if len(args) == 0 {
  95         return dejsonl(bw, os.Stdin, live)
  96     }
  97 
  98     for _, path := range args {
  99         if err := handleInput(bw, path, live); err != nil {
 100             return err
 101         }
 102     }
 103 
 104     return nil
 105 }
 106 
 107 // handleInput simplifies control-flow for func main
 108 func handleInput(w *bufio.Writer, path string, live bool) error {
 109     if path == `-` {
 110         return dejsonl(w, os.Stdin, live)
 111     }
 112 
 113     // if f := strings.HasPrefix; f(path, `https://`) || f(path, `http://`) {
 114     //  resp, err := http.Get(path)
 115     //  if err != nil {
 116     //      return err
 117     //  }
 118     //  defer resp.Body.Close()
 119     //  return dejsonl(w, resp.Body, live)
 120     // }
 121 
 122     f, err := os.Open(path)
 123     if err != nil {
 124         // on windows, file-not-found error messages may mention `CreateFile`,
 125         // even when trying to open files in read-only mode
 126         return errors.New(`can't open file named ` + path)
 127     }
 128     defer f.Close()
 129     return dejsonl(w, f, live)
 130 }
 131 
 132 // dejsonl simplifies control-flow for func handleInput
 133 func dejsonl(w *bufio.Writer, r io.Reader, live bool) error {
 134     const gb = 1024 * 1024 * 1024
 135     sc := bufio.NewScanner(r)
 136     sc.Buffer(nil, 8*gb)
 137     got := 0
 138 
 139     for i := 0; sc.Scan(); i++ {
 140         s := sc.Text()
 141         if i == 0 && strings.HasPrefix(s, "\xef\xbb\xbf") {
 142             s = s[3:]
 143         }
 144 
 145         // trim spaces at both ends of the current line
 146         for len(s) > 0 && s[0] == ' ' {
 147             s = s[1:]
 148         }
 149         for len(s) > 0 && s[len(s)-1] == ' ' {
 150             s = s[:len(s)-1]
 151         }
 152 
 153         // ignore empty(ish) lines
 154         if len(s) == 0 {
 155             continue
 156         }
 157 
 158         // ignore lines starting with unix-style comments
 159         if len(s) > 0 && s[0] == '#' {
 160             continue
 161         }
 162 
 163         if err := checkJSONL(strings.NewReader(s)); err != nil {
 164             return err
 165         }
 166 
 167         if got == 0 {
 168             w.WriteByte('[')
 169         } else {
 170             w.WriteByte(',')
 171         }
 172         if w.WriteByte('\n') != nil {
 173             return io.EOF
 174         }
 175         w.WriteString(indent)
 176         w.WriteString(s)
 177         got++
 178 
 179         if !live {
 180             continue
 181         }
 182 
 183         if err := w.Flush(); err != nil {
 184             return io.EOF
 185         }
 186     }
 187 
 188     if got == 0 {
 189         w.WriteString("[\n]\n")
 190     } else {
 191         w.WriteString("\n]\n")
 192     }
 193     return sc.Err()
 194 }
 195 
 196 func checkJSONL(r io.Reader) error {
 197     dec := json.NewDecoder(r)
 198     // avoid parsing numbers, so unusually-long numbers are kept verbatim,
 199     // even if JSON parsers aren't required to guarantee such input-fidelity
 200     // for numbers
 201     dec.UseNumber()
 202 
 203     t, err := dec.Token()
 204     if err == io.EOF {
 205         return errors.New(`input has no JSON values`)
 206     }
 207 
 208     if err := checkToken(dec, t); err != nil {
 209         return err
 210     }
 211 
 212     _, err = dec.Token()
 213     if err == io.EOF {
 214         // input is over, so it's a success
 215         return nil
 216     }
 217 
 218     if err == nil {
 219         // a successful `read` is a failure, as it means there are
 220         // trailing JSON tokens
 221         return errors.New(`unexpected trailing data`)
 222     }
 223 
 224     // any other error, perhaps some invalid-JSON-syntax-type error
 225     return err
 226 }
 227 
 228 // checkToken handles recursion for func checkJSONL
 229 func checkToken(dec *json.Decoder, t json.Token) error {
 230     switch t := t.(type) {
 231     case json.Delim:
 232         switch t {
 233         case json.Delim('['):
 234             return checkArray(dec)
 235         case json.Delim('{'):
 236             return checkObject(dec)
 237         default:
 238             return errors.New(`unsupported JSON syntax ` + string(t))
 239         }
 240 
 241     case nil, bool, float64, json.Number, string:
 242         return nil
 243 
 244     default:
 245         // return fmt.Errorf(`unsupported token type %T`, t)
 246         return errors.New(`invalid JSON token`)
 247     }
 248 }
 249 
 250 // handleArray handles arrays for func checkToken
 251 func checkArray(dec *json.Decoder) error {
 252     for {
 253         t, err := dec.Token()
 254         if err != nil {
 255             return err
 256         }
 257 
 258         if t == json.Delim(']') {
 259             return nil
 260         }
 261 
 262         if err := checkToken(dec, t); err != nil {
 263             return err
 264         }
 265     }
 266 }
 267 
 268 // handleObject handles objects for func checkToken
 269 func checkObject(dec *json.Decoder) error {
 270     for {
 271         t, err := dec.Token()
 272         if err != nil {
 273             return err
 274         }
 275 
 276         if t == json.Delim('}') {
 277             return nil
 278         }
 279 
 280         if _, ok := t.(string); !ok {
 281             return errors.New(`expected a string for a key-value pair`)
 282         }
 283 
 284         t, err = dec.Token()
 285         if err == io.EOF || t == json.Delim('}') {
 286             return errors.New(`expected a value for a key-value pair`)
 287         }
 288 
 289         if err := checkToken(dec, t); err != nil {
 290             return err
 291         }
 292     }
 293 }
     File: ./dessv/dessv.go
   1 /*
   2 The MIT License (MIT)
   3 
   4 Copyright (c) 2026 pacman64
   5 
   6 Permission is hereby granted, free of charge, to any person obtaining a copy of
   7 this software and associated documentation files (the "Software"), to deal
   8 in the Software without restriction, including without limitation the rights to
   9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
  10 of the Software, and to permit persons to whom the Software is furnished to do
  11 so, subject to the following conditions:
  12 
  13 The above copyright notice and this permission notice shall be included in all
  14 copies or substantial portions of the Software.
  15 
  16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  22 SOFTWARE.
  23 */
  24 
  25 package dessv
  26 
  27 import (
  28     "bufio"
  29     "bytes"
  30     "errors"
  31     "io"
  32     "os"
  33 )
  34 
  35 const info = `
  36 dessv [filenames...]
  37 
  38 Turn Space(s)-Separated Values (SSV) into Tab-Separated Values (TSV), where
  39 both leading and trailing spaces from input lines are ignored.
  40 `
  41 
  42 func Main() {
  43     buffered := false
  44     args := os.Args[1:]
  45 
  46     if len(args) > 0 {
  47         switch args[0] {
  48         case `-b`, `--b`, `-buffered`, `--buffered`:
  49             buffered = true
  50             args = args[1:]
  51 
  52         case `-h`, `--h`, `-help`, `--help`:
  53             os.Stdout.WriteString(info[1:])
  54             return
  55         }
  56     }
  57 
  58     if len(args) > 0 && args[0] == `--` {
  59         args = args[1:]
  60     }
  61 
  62     liveLines := !buffered
  63     if !buffered {
  64         if _, err := os.Stdout.Seek(0, io.SeekCurrent); err == nil {
  65             liveLines = false
  66         }
  67     }
  68 
  69     if err := run(os.Stdout, args, liveLines); err != nil && err != io.EOF {
  70         os.Stderr.WriteString(err.Error())
  71         os.Stderr.WriteString("\n")
  72         os.Exit(1)
  73     }
  74 }
  75 
  76 func run(w io.Writer, args []string, live bool) error {
  77     bw := bufio.NewWriter(w)
  78     defer bw.Flush()
  79 
  80     if len(args) == 0 {
  81         return dessv(bw, os.Stdin, live)
  82     }
  83 
  84     for _, name := range args {
  85         if err := handleFile(bw, name, live); err != nil {
  86             return err
  87         }
  88     }
  89     return nil
  90 }
  91 
  92 func handleFile(w *bufio.Writer, name string, live bool) error {
  93     if name == `` || name == `-` {
  94         return dessv(w, os.Stdin, live)
  95     }
  96 
  97     f, err := os.Open(name)
  98     if err != nil {
  99         return errors.New(`can't read from file named "` + name + `"`)
 100     }
 101     defer f.Close()
 102 
 103     return dessv(w, f, live)
 104 }
 105 
 106 func dessv(w *bufio.Writer, r io.Reader, live bool) error {
 107     const gb = 1024 * 1024 * 1024
 108     sc := bufio.NewScanner(r)
 109     sc.Buffer(nil, 8*gb)
 110     handleRow := handleRowSSV
 111     numTabs := ^0
 112 
 113     for i := 0; sc.Scan(); i++ {
 114         s := sc.Bytes()
 115         if i == 0 {
 116             if bytes.HasPrefix(s, []byte{0xef, 0xbb, 0xbf}) {
 117                 s = s[3:]
 118             }
 119 
 120             for _, b := range s {
 121                 if b == '\t' {
 122                     handleRow = handleRowTSV
 123                     break
 124                 }
 125             }
 126             numTabs = handleRow(w, s, numTabs)
 127         } else {
 128             handleRow(w, s, numTabs)
 129         }
 130 
 131         if w.WriteByte('\n') != nil {
 132             return io.EOF
 133         }
 134 
 135         if !live {
 136             continue
 137         }
 138 
 139         if err := w.Flush(); err != nil {
 140             return io.EOF
 141         }
 142     }
 143 
 144     return sc.Err()
 145 }
 146 
 147 func handleRowSSV(w *bufio.Writer, s []byte, n int) int {
 148     for len(s) > 0 && s[0] == ' ' {
 149         s = s[1:]
 150     }
 151     for len(s) > 0 && s[len(s)-1] == ' ' {
 152         s = s[:len(s)-1]
 153     }
 154 
 155     got := 0
 156 
 157     for got = 0; len(s) > 0; got++ {
 158         if got > 0 {
 159             w.WriteByte('\t')
 160         }
 161 
 162         i := bytes.IndexByte(s, ' ')
 163         if i < 0 {
 164             w.Write(s)
 165             s = nil
 166             n--
 167             break
 168         }
 169 
 170         w.Write(s[:i])
 171         s = s[i+1:]
 172         for len(s) > 0 && s[0] == ' ' {
 173             s = s[1:]
 174         }
 175         n--
 176     }
 177 
 178     w.Write(s)
 179     writeTabs(w, n)
 180     return got
 181 }
 182 
 183 func handleRowTSV(w *bufio.Writer, s []byte, n int) int {
 184     got := 0
 185     for _, b := range s {
 186         if b == '\t' {
 187             got++
 188         }
 189     }
 190 
 191     w.Write(s)
 192     writeTabs(w, n-got)
 193     return got
 194 }
 195 
 196 func writeTabs(w *bufio.Writer, n int) {
 197     for n > 0 {
 198         w.WriteByte('\t')
 199         n--
 200     }
 201 }
     File: ./ecoli/ecoli.go
   1 /*
   2 The MIT License (MIT)
   3 
   4 Copyright (c) 2026 pacman64
   5 
   6 Permission is hereby granted, free of charge, to any person obtaining a copy of
   7 this software and associated documentation files (the "Software"), to deal
   8 in the Software without restriction, including without limitation the rights to
   9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
  10 of the Software, and to permit persons to whom the Software is furnished to do
  11 so, subject to the following conditions:
  12 
  13 The above copyright notice and this permission notice shall be included in all
  14 copies or substantial portions of the Software.
  15 
  16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  22 SOFTWARE.
  23 */
  24 
  25 package ecoli
  26 
  27 import (
  28     "bufio"
  29     "bytes"
  30     "io"
  31     "os"
  32     "regexp"
  33 )
  34 
  35 const info = `
  36 ecoli [options...] [regex/style pairs...]
  37 
  38 
  39 Expressions COloring LInes tries to match each line read from the standard
  40 input to the regexes given, coloring/styling with the named-style paired
  41 to the first matching regex, if any. Lines not matching any regex stay the
  42 same.
  43 
  44 The options are, available both in single and double-dash versions
  45 
  46     -h, -help                 show this help message
  47     -i, -ins, -insensitive    match the regexes given case-insensitively
  48 
  49 Some of the colors/styles available are:
  50 
  51     blue         blueback
  52     bold
  53     gray         grayback
  54     green        greenback
  55     inverse
  56     magenta      magentaback
  57     orange       orangeback
  58     purple       purpleback
  59     red          redback
  60     underline
  61 
  62 Some style aliases are:
  63 
  64     b       blue                  bb       blueback
  65     g       green                 gb       greenback
  66     m       magenta               mb       magentaback
  67     o       orange                ob       orangeback
  68     p       purple                pb       purpleback
  69     r       red                   rb       redback
  70     i       inverse (highlight)
  71     u       underline
  72 `
  73 
  74 var styleAliases = map[string]string{
  75     `b`: `blue`,
  76     `g`: `green`,
  77     `m`: `magenta`,
  78     `o`: `orange`,
  79     `p`: `purple`,
  80     `r`: `red`,
  81     `u`: `underline`,
  82 
  83     `bb`: `blueback`,
  84     `bg`: `greenback`,
  85     `bm`: `magentaback`,
  86     `bo`: `orangeback`,
  87     `bp`: `purpleback`,
  88     `br`: `redback`,
  89 
  90     `gb`: `greenback`,
  91     `mb`: `magentaback`,
  92     `ob`: `orangeback`,
  93     `pb`: `purpleback`,
  94     `rb`: `redback`,
  95 
  96     `hi`:  `inverse`,
  97     `inv`: `inverse`,
  98     `mag`: `magenta`,
  99 
 100     `du`: `doubleunderline`,
 101 
 102     `flip`: `inverse`,
 103     `swap`: `inverse`,
 104 
 105     `reset`:     `plain`,
 106     `highlight`: `inverse`,
 107     `hilite`:    `inverse`,
 108     `invert`:    `inverse`,
 109     `inverted`:  `inverse`,
 110     `swapped`:   `inverse`,
 111 
 112     `dunderline`:  `doubleunderline`,
 113     `dunderlined`: `doubleunderline`,
 114 
 115     `strikethrough`: `strike`,
 116     `strikethru`:    `strike`,
 117     `struck`:        `strike`,
 118 
 119     `underlined`: `underline`,
 120 
 121     `bblue`:    `blueback`,
 122     `bgray`:    `grayback`,
 123     `bgreen`:   `greenback`,
 124     `bmagenta`: `magentaback`,
 125     `borange`:  `orangeback`,
 126     `bpurple`:  `purpleback`,
 127     `bred`:     `redback`,
 128 
 129     `bgblue`:    `blueback`,
 130     `bggray`:    `grayback`,
 131     `bggreen`:   `greenback`,
 132     `bgmag`:     `magentaback`,
 133     `bgmagenta`: `magentaback`,
 134     `bgorange`:  `orangeback`,
 135     `bgpurple`:  `purpleback`,
 136     `bgred`:     `redback`,
 137 
 138     `bluebg`:    `blueback`,
 139     `graybg`:    `grayback`,
 140     `greenbg`:   `greenback`,
 141     `magbg`:     `magentaback`,
 142     `magentabg`: `magentaback`,
 143     `orangebg`:  `orangeback`,
 144     `purplebg`:  `purpleback`,
 145     `redbg`:     `redback`,
 146 
 147     `backblue`:    `blueback`,
 148     `backgray`:    `grayback`,
 149     `backgreen`:   `greenback`,
 150     `backmag`:     `magentaback`,
 151     `backmagenta`: `magentaback`,
 152     `backorange`:  `orangeback`,
 153     `backpurple`:  `purpleback`,
 154     `backred`:     `redback`,
 155 }
 156 
 157 var styles = map[string]string{
 158     `blue`:            "\x1b[38;2;0;95;215m",
 159     `bold`:            "\x1b[1m",
 160     `doubleunderline`: "\x1b[21m",
 161     `gray`:            "\x1b[38;2;168;168;168m",
 162     `green`:           "\x1b[38;2;0;135;95m",
 163     `inverse`:         "\x1b[7m",
 164     `magenta`:         "\x1b[38;2;215;0;255m",
 165     `orange`:          "\x1b[38;2;215;95;0m",
 166     `plain`:           "\x1b[0m",
 167     `purple`:          "\x1b[38;2;135;95;255m",
 168     `red`:             "\x1b[38;2;204;0;0m",
 169     `strike`:          "\x1b[9m",
 170     `underline`:       "\x1b[4m",
 171 
 172     `blueback`:    "\x1b[48;2;0;95;215m\x1b[38;2;238;238;238m",
 173     `grayback`:    "\x1b[48;2;168;168;168m\x1b[38;2;238;238;238m",
 174     `greenback`:   "\x1b[48;2;0;135;95m\x1b[38;2;238;238;238m",
 175     `magentaback`: "\x1b[48;2;215;0;255m\x1b[38;2;238;238;238m",
 176     `orangeback`:  "\x1b[48;2;215;95;0m\x1b[38;2;238;238;238m",
 177     `purpleback`:  "\x1b[48;2;135;95;255m\x1b[38;2;238;238;238m",
 178     `redback`:     "\x1b[48;2;204;0;0m\x1b[38;2;238;238;238m",
 179 }
 180 
 181 // pair has a regular-expression and its associated ANSI-code style together
 182 type pair struct {
 183     expr  *regexp.Regexp
 184     style string
 185 }
 186 
 187 func Main() {
 188     buffered := false
 189     insensitive := false
 190     args := os.Args[1:]
 191 
 192     for len(args) > 0 {
 193         switch args[0] {
 194         case `-b`, `--b`, `-buffered`, `--buffered`:
 195             buffered = true
 196             args = args[1:]
 197             continue
 198 
 199         case `-h`, `--h`, `-help`, `--help`:
 200             os.Stdout.WriteString(info[1:])
 201             return
 202 
 203         case `-i`, `--i`, `-ins`, `--ins`:
 204             insensitive = true
 205             args = args[1:]
 206             continue
 207         }
 208 
 209         break
 210     }
 211 
 212     if len(args) > 0 && args[0] == `--` {
 213         args = args[1:]
 214     }
 215 
 216     if len(args)%2 != 0 {
 217         const msg = "you forgot the style-name for/after the last regex\n"
 218         os.Stderr.WriteString(msg)
 219         os.Exit(1)
 220     }
 221 
 222     nerr := 0
 223     pairs := make([]pair, 0, len(args)/2)
 224 
 225     for len(args) >= 2 {
 226         src := args[0]
 227         sname := args[1]
 228 
 229         var err error
 230         var exp *regexp.Regexp
 231         if insensitive {
 232             exp, err = regexp.Compile(`(?i)` + src)
 233         } else {
 234             exp, err = regexp.Compile(src)
 235         }
 236         if err != nil {
 237             os.Stderr.WriteString(err.Error())
 238             os.Stderr.WriteString("\n")
 239             nerr++
 240         }
 241 
 242         if alias, ok := styleAliases[sname]; ok {
 243             sname = alias
 244         }
 245 
 246         style, ok := styles[sname]
 247         if !ok {
 248             os.Stderr.WriteString("no style named `")
 249             os.Stderr.WriteString(args[1])
 250             os.Stderr.WriteString("`\n")
 251             nerr++
 252         }
 253 
 254         pairs = append(pairs, pair{expr: exp, style: style})
 255         args = args[2:]
 256     }
 257 
 258     if nerr > 0 {
 259         os.Exit(1)
 260     }
 261 
 262     liveLines := !buffered
 263     if !buffered {
 264         if _, err := os.Stdout.Seek(0, io.SeekCurrent); err == nil {
 265             liveLines = false
 266         }
 267     }
 268 
 269     sc := bufio.NewScanner(os.Stdin)
 270     sc.Buffer(nil, 8*1024*1024*1024)
 271     bw := bufio.NewWriter(os.Stdout)
 272     var plain []byte
 273 
 274     for i := 0; sc.Scan(); i++ {
 275         s := sc.Bytes()
 276         if i == 0 && bytes.HasPrefix(s, []byte{0xef, 0xbb, 0xbf}) {
 277             s = s[3:]
 278         }
 279         plain = appendPlain(plain[:0], s)
 280 
 281         if err := handleLine(bw, s, noANSI(plain), pairs); err != nil {
 282             return
 283         }
 284 
 285         if !liveLines {
 286             continue
 287         }
 288 
 289         if err := bw.Flush(); err != nil {
 290             return
 291         }
 292     }
 293 }
 294 
 295 // appendPlain extends the slice given using the non-ANSI parts of a string
 296 func appendPlain(dst []byte, src []byte) []byte {
 297     for len(src) > 0 {
 298         i, j := indexEscapeSequence(src)
 299         if i < 0 {
 300             dst = append(dst, src...)
 301             break
 302         }
 303         if j < 0 {
 304             j = len(src)
 305         }
 306 
 307         if i > 0 {
 308             dst = append(dst, src[:i]...)
 309         }
 310 
 311         src = src[j:]
 312     }
 313 
 314     return dst
 315 }
 316 
 317 // indexEscapeSequence finds the first ANSI-style escape-sequence, which is
 318 // the multi-byte sequences starting with ESC[; the result is a pair of slice
 319 // indices which can be independently negative when either the start/end of
 320 // a sequence isn't found; given their fairly-common use, even the hyperlink
 321 // ESC]8 sequences are supported
 322 func indexEscapeSequence(s []byte) (int, int) {
 323     var prev byte
 324 
 325     for i, b := range s {
 326         if prev == '\x1b' && b == '[' {
 327             j := indexLetter(s[i+1:])
 328             if j < 0 {
 329                 return i, -1
 330             }
 331             return i - 1, i + 1 + j + 1
 332         }
 333 
 334         if prev == '\x1b' && b == ']' && i+1 < len(s) && s[i+1] == '8' {
 335             j := indexPair(s[i+1:], '\x1b', '\\')
 336             if j < 0 {
 337                 return i, -1
 338             }
 339             return i - 1, i + 1 + j + 2
 340         }
 341 
 342         prev = b
 343     }
 344 
 345     return -1, -1
 346 }
 347 
 348 func indexLetter(s []byte) int {
 349     for i, b := range s {
 350         upper := b &^ 32
 351         if 'A' <= upper && upper <= 'Z' {
 352             return i
 353         }
 354     }
 355 
 356     return -1
 357 }
 358 
 359 func indexPair(s []byte, x byte, y byte) int {
 360     var prev byte
 361 
 362     for i, b := range s {
 363         if prev == x && b == y && i > 0 {
 364             return i
 365         }
 366         prev = b
 367     }
 368 
 369     return -1
 370 }
 371 
 372 // noANSI ensures arguments to func handleLine are given in the right order
 373 type noANSI []byte
 374 
 375 // handleLine styles the current line given to it using the first matching
 376 // regex, keeping it as given if none of the regexes match; it's given 2
 377 // strings: the first is the original line, the latter is its plain-text
 378 // version (with no ANSI codes) and is used for the regex-matching, since
 379 // ANSI codes use a mix of numbers and letters, which can themselves match
 380 func handleLine(w *bufio.Writer, s []byte, plain noANSI, pairs []pair) error {
 381     for _, p := range pairs {
 382         if p.expr.Match(plain) {
 383             w.WriteString(p.style)
 384             w.Write(s)
 385             w.WriteString("\x1b[0m")
 386             return w.WriteByte('\n')
 387         }
 388     }
 389 
 390     w.Write(s)
 391     return w.WriteByte('\n')
 392 }
     File: ./erase/erase.go
   1 /*
   2 The MIT License (MIT)
   3 
   4 Copyright (c) 2026 pacman64
   5 
   6 Permission is hereby granted, free of charge, to any person obtaining a copy of
   7 this software and associated documentation files (the "Software"), to deal
   8 in the Software without restriction, including without limitation the rights to
   9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
  10 of the Software, and to permit persons to whom the Software is furnished to do
  11 so, subject to the following conditions:
  12 
  13 The above copyright notice and this permission notice shall be included in all
  14 copies or substantial portions of the Software.
  15 
  16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  22 SOFTWARE.
  23 */
  24 
  25 package erase
  26 
  27 import (
  28     "bufio"
  29     "bytes"
  30     "io"
  31     "os"
  32     "regexp"
  33 )
  34 
  35 const info = `
  36 erase [options...] [regexes...]
  37 
  38 
  39 Ignore/remove all occurrences of all regex matches along lines read from the
  40 standard input. The regular-expression mode used is "re2", which is a superset
  41 of the commonly-used "extended-mode".
  42 
  43 All ANSI-style sequences are removed before trying to match-remove things, to
  44 avoid messing those up. Each regex erases all its occurrences on the current
  45 line in the order given among the arguments, so regex-order matters.
  46 
  47 The options are, available both in single and double-dash versions
  48 
  49     -h, -help    show this help message
  50     -i, -ins     match regexes case-insensitively
  51 `
  52 
  53 func Main() {
  54     args := os.Args[1:]
  55     buffered := false
  56     insensitive := false
  57 
  58     for len(args) > 0 {
  59         switch args[0] {
  60         case `-b`, `--b`, `-buffered`, `--buffered`:
  61             buffered = true
  62             args = args[1:]
  63 
  64         case `-h`, `--h`, `-help`, `--help`:
  65             os.Stdout.WriteString(info[1:])
  66             return
  67 
  68         case `-i`, `--i`, `-ins`, `--ins`:
  69             insensitive = true
  70             args = args[1:]
  71         }
  72 
  73         break
  74     }
  75 
  76     if len(args) > 0 && args[0] == `--` {
  77         args = args[1:]
  78     }
  79 
  80     exprs := make([]*regexp.Regexp, 0, len(args))
  81 
  82     for _, s := range args {
  83         var err error
  84         var exp *regexp.Regexp
  85 
  86         if insensitive {
  87             exp, err = regexp.Compile(`(?i)` + s)
  88         } else {
  89             exp, err = regexp.Compile(s)
  90         }
  91 
  92         if err != nil {
  93             os.Stderr.WriteString(err.Error())
  94             os.Stderr.WriteString("\n")
  95             continue
  96         }
  97 
  98         exprs = append(exprs, exp)
  99     }
 100 
 101     // quit right away when given invalid regexes
 102     if len(exprs) < len(args) {
 103         os.Exit(1)
 104     }
 105 
 106     liveLines := !buffered
 107     if !buffered {
 108         if _, err := os.Stdout.Seek(0, io.SeekCurrent); err == nil {
 109             liveLines = false
 110         }
 111     }
 112 
 113     err := run(os.Stdout, os.Stdin, exprs, liveLines)
 114     if err != nil && err != io.EOF {
 115         os.Stderr.WriteString(err.Error())
 116         os.Stderr.WriteString("\n")
 117         os.Exit(1)
 118     }
 119 }
 120 
 121 func run(w io.Writer, r io.Reader, exprs []*regexp.Regexp, live bool) error {
 122     var buf []byte
 123     sc := bufio.NewScanner(r)
 124     sc.Buffer(nil, 8*1024*1024*1024)
 125     bw := bufio.NewWriter(w)
 126     defer bw.Flush()
 127 
 128     src := make([]byte, 8*1024)
 129     dst := make([]byte, 8*1024)
 130 
 131     for i := 0; sc.Scan(); i++ {
 132         line := sc.Bytes()
 133         if i == 0 && bytes.HasPrefix(line, []byte{0xef, 0xbb, 0xbf}) {
 134             line = line[3:]
 135         }
 136 
 137         s := line
 138         if bytes.IndexByte(s, '\x1b') >= 0 {
 139             buf = plain(buf[:0], s)
 140             s = buf
 141         }
 142 
 143         if len(exprs) > 0 {
 144             src = append(src[:0], s...)
 145             for _, exp := range exprs {
 146                 dst = erase(dst[:0], src, exp)
 147                 src = append(src[:0], dst...)
 148             }
 149             bw.Write(dst)
 150         } else {
 151             bw.Write(s)
 152         }
 153 
 154         if bw.WriteByte('\n') != nil {
 155             return io.EOF
 156         }
 157 
 158         if !live {
 159             continue
 160         }
 161 
 162         if bw.Flush() != nil {
 163             return io.EOF
 164         }
 165     }
 166 
 167     return sc.Err()
 168 }
 169 
 170 func erase(dst []byte, src []byte, with *regexp.Regexp) []byte {
 171     for len(src) > 0 {
 172         span := with.FindIndex(src)
 173         // also ignore empty regex matches to avoid infinite outer loops,
 174         // as skipping empty slices isn't advancing at all, leaving the
 175         // string stuck to being empty-matched forever by the same regex
 176         if len(span) != 2 || span[0] == span[1] || span[0] < 0 {
 177             return append(dst, src...)
 178         }
 179 
 180         start, end := span[0], span[1]
 181         dst = append(dst, src[:start]...)
 182         // avoid infinite loops caused by empty regex matches
 183         if start == end && end < len(src) {
 184             dst = append(dst, src[end])
 185             end++
 186         }
 187         src = src[end:]
 188     }
 189 
 190     return dst
 191 }
 192 
 193 func plain(dst []byte, src []byte) []byte {
 194     for len(src) > 0 {
 195         i, j := indexEscapeSequence(src)
 196         if i < 0 {
 197             dst = append(dst, src...)
 198             break
 199         }
 200         if j < 0 {
 201             j = len(src)
 202         }
 203 
 204         if i > 0 {
 205             dst = append(dst, src[:i]...)
 206         }
 207 
 208         src = src[j:]
 209     }
 210 
 211     return dst
 212 }
 213 
 214 // indexEscapeSequence finds the first ANSI-style escape-sequence, which is
 215 // the multi-byte sequences starting with ESC[; the result is a pair of slice
 216 // indices which can be independently negative when either the start/end of
 217 // a sequence isn't found; given their fairly-common use, even the hyperlink
 218 // ESC]8 sequences are supported
 219 func indexEscapeSequence(s []byte) (int, int) {
 220     var prev byte
 221 
 222     for i, b := range s {
 223         if prev == '\x1b' && b == '[' {
 224             j := indexLetter(s[i+1:])
 225             if j < 0 {
 226                 return i, -1
 227             }
 228             return i - 1, i + 1 + j + 1
 229         }
 230 
 231         if prev == '\x1b' && b == ']' && i+1 < len(s) && s[i+1] == '8' {
 232             j := indexPair(s[i+1:], '\x1b', '\\')
 233             if j < 0 {
 234                 return i, -1
 235             }
 236             return i - 1, i + 1 + j + 2
 237         }
 238 
 239         prev = b
 240     }
 241 
 242     return -1, -1
 243 }
 244 
 245 func indexLetter(s []byte) int {
 246     for i, b := range s {
 247         upper := b &^ 32
 248         if 'A' <= upper && upper <= 'Z' {
 249             return i
 250         }
 251     }
 252 
 253     return -1
 254 }
 255 
 256 func indexPair(s []byte, x byte, y byte) int {
 257     var prev byte
 258 
 259     for i, b := range s {
 260         if prev == x && b == y && i > 0 {
 261             return i
 262         }
 263         prev = b
 264     }
 265 
 266     return -1
 267 }
     File: ./files/files.go
   1 /*
   2 The MIT License (MIT)
   3 
   4 Copyright (c) 2026 pacman64
   5 
   6 Permission is hereby granted, free of charge, to any person obtaining a copy of
   7 this software and associated documentation files (the "Software"), to deal
   8 in the Software without restriction, including without limitation the rights to
   9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
  10 of the Software, and to permit persons to whom the Software is furnished to do
  11 so, subject to the following conditions:
  12 
  13 The above copyright notice and this permission notice shall be included in all
  14 copies or substantial portions of the Software.
  15 
  16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  22 SOFTWARE.
  23 */
  24 
  25 package files
  26 
  27 import (
  28     "bufio"
  29     "io"
  30     "io/fs"
  31     "os"
  32     "path/filepath"
  33     "strings"
  34 )
  35 
  36 const info = `
  37 files [options...] [files/folders...]
  38 
  39 Find/list all files in the folders given, without repetitions.
  40 
  41 All (optional) leading options start with either single or double-dash:
  42 
  43     -h, -help    show this help message
  44     -t, -top     turn off recursive behavior; top-level entries only
  45 `
  46 
  47 func Main() {
  48     top := false
  49     buffered := false
  50     args := os.Args[1:]
  51 
  52     for len(args) > 0 {
  53         switch args[0] {
  54         case `-b`, `--b`, `-buffered`, `--buffered`:
  55             buffered = true
  56             args = args[1:]
  57             continue
  58 
  59         case `-h`, `--h`, `-help`, `--help`:
  60             os.Stdout.WriteString(info[1:])
  61             return
  62 
  63         case `-t`, `--t`, `-top`, `--top`:
  64             top = true
  65             args = args[1:]
  66             continue
  67         }
  68 
  69         break
  70     }
  71 
  72     if len(args) > 0 && args[0] == `--` {
  73         args = args[1:]
  74     }
  75 
  76     liveLines := !buffered
  77     if !buffered {
  78         if _, err := os.Stdout.Seek(0, io.SeekCurrent); err == nil {
  79             liveLines = false
  80         }
  81     }
  82 
  83     var cfg config
  84     if top {
  85         cfg.skipSubfolder = fs.SkipDir
  86     }
  87     cfg.liveLines = liveLines
  88 
  89     if err := run(os.Stdout, args, cfg); err != nil && err != io.EOF {
  90         os.Stderr.WriteString(err.Error())
  91         os.Stderr.WriteString("\n")
  92         os.Exit(1)
  93     }
  94 }
  95 
  96 type config struct {
  97     skipSubfolder error
  98     liveLines     bool
  99 }
 100 
 101 func run(w io.Writer, paths []string, cfg config) error {
 102     bw := bufio.NewWriter(w)
 103     defer bw.Flush()
 104 
 105     got := make(map[string]struct{})
 106 
 107     if len(paths) == 0 {
 108         paths = []string{`.`}
 109     }
 110 
 111     // handle is the callback for func filepath.WalkDir
 112     handle := func(path string, e fs.DirEntry, err error) error {
 113         if err != nil {
 114             return err
 115         }
 116 
 117         if _, ok := got[path]; ok {
 118             return nil
 119         }
 120         got[path] = struct{}{}
 121 
 122         if e.IsDir() {
 123             return cfg.skipSubfolder
 124         }
 125 
 126         return handleEntry(bw, path, cfg.liveLines)
 127     }
 128 
 129     for _, path := range paths {
 130         if _, ok := got[path]; ok {
 131             continue
 132         }
 133         got[path] = struct{}{}
 134 
 135         st, err := os.Stat(path)
 136         if err != nil {
 137             return err
 138         }
 139 
 140         if st.IsDir() {
 141             if !strings.HasSuffix(path, `/`) {
 142                 path = path + `/`
 143             }
 144             got[path] = struct{}{}
 145 
 146             if err := filepath.WalkDir(path, handle); err != nil {
 147                 return err
 148             }
 149             continue
 150         }
 151 
 152         if err := handleEntry(bw, path, cfg.liveLines); err != nil {
 153             return err
 154         }
 155     }
 156 
 157     return nil
 158 }
 159 
 160 func handleEntry(w *bufio.Writer, path string, live bool) error {
 161     abs, err := filepath.Abs(path)
 162     if err != nil {
 163         return err
 164     }
 165 
 166     w.WriteString(abs)
 167     w.WriteByte('\n')
 168 
 169     if !live {
 170         return nil
 171     }
 172 
 173     if err := w.Flush(); err != nil {
 174         return io.EOF
 175     }
 176     return nil
 177 }
     File: ./filesizes/filesizes.go
   1 /*
   2 The MIT License (MIT)
   3 
   4 Copyright (c) 2026 pacman64
   5 
   6 Permission is hereby granted, free of charge, to any person obtaining a copy of
   7 this software and associated documentation files (the "Software"), to deal
   8 in the Software without restriction, including without limitation the rights to
   9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
  10 of the Software, and to permit persons to whom the Software is furnished to do
  11 so, subject to the following conditions:
  12 
  13 The above copyright notice and this permission notice shall be included in all
  14 copies or substantial portions of the Software.
  15 
  16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  22 SOFTWARE.
  23 */
  24 
  25 package filesizes
  26 
  27 import (
  28     "bufio"
  29     "io"
  30     "io/fs"
  31     "os"
  32     "path/filepath"
  33     "sort"
  34     "strconv"
  35     "strings"
  36 )
  37 
  38 const info = `
  39 filesizes [options...] [files/folders...]
  40 
  41 Find/list all files in the folders given, without repetitions, also showing
  42 their size in bytes. Output is lines of tab-separated values, starting with
  43 a header line with the column names.
  44 
  45 All (optional) leading options start with either single or double-dash:
  46 
  47     -h, -help             show this help message
  48     -s, -sort, -sorted    backward-sort entries largest to smallest
  49     -t, -top              turn off recursive behavior; top-level entries only
  50 `
  51 
  52 func Main() {
  53     var cfg config
  54     cfg.recursive = true
  55     cfg.sorted = false
  56     cfg.blockSize = 4
  57     buffered := false
  58     args := os.Args[1:]
  59 
  60     for len(args) > 0 {
  61         if size, ok := parseBlockSizeOption(args[0]); ok {
  62             cfg.blockSize = size
  63             args = args[1:]
  64             continue
  65         }
  66 
  67         switch args[0] {
  68         case `-b`, `--b`, `-buffered`, `--buffered`:
  69             buffered = true
  70             args = args[1:]
  71             continue
  72 
  73         case `-h`, `--h`, `-help`, `--help`:
  74             os.Stdout.WriteString(info[1:])
  75             return
  76 
  77         case `-s`, `--s`, `-sort`, `--sort`, `-sorted`, `--sorted`:
  78             cfg.sorted = true
  79             args = args[1:]
  80             continue
  81 
  82         case `-t`, `--t`, `-top`, `--top`:
  83             cfg.recursive = false
  84             args = args[1:]
  85             continue
  86         }
  87 
  88         break
  89     }
  90 
  91     if len(args) > 0 && args[0] == `--` {
  92         args = args[1:]
  93     }
  94 
  95     cfg.liveLines = !buffered && !cfg.sorted
  96     if !buffered && !cfg.sorted {
  97         if _, err := os.Stdout.Seek(0, io.SeekCurrent); err == nil {
  98             cfg.liveLines = false
  99         }
 100     }
 101 
 102     if err := run(os.Stdout, args, cfg); err != nil && err != io.EOF {
 103         os.Stderr.WriteString(err.Error())
 104         os.Stderr.WriteString("\n")
 105         os.Exit(1)
 106     }
 107 }
 108 
 109 func parseBlockSizeOption(s string) (size int, ok bool) {
 110     if !strings.HasPrefix(s, `-`) {
 111         return 0, false
 112     }
 113     if len(s) > 0 && s[0] == '-' {
 114         s = s[1:]
 115     }
 116     if len(s) > 0 && s[0] == '-' {
 117         s = s[1:]
 118     }
 119 
 120     if !strings.HasSuffix(s, `k`) && !strings.HasSuffix(s, `K`) {
 121         return 0, false
 122     }
 123     s = s[:len(s)-1]
 124 
 125     if n, err := strconv.ParseInt(s, 10, 64); err == nil && n > 0 {
 126         return int(n), true
 127     }
 128     return 0, false
 129 }
 130 
 131 type config struct {
 132     blockSize int
 133     sorted    bool
 134     recursive bool
 135     liveLines bool
 136 }
 137 
 138 type entry struct {
 139     path string
 140     size int64
 141 }
 142 
 143 type handlers struct {
 144     w             *bufio.Writer
 145     entries       []entry
 146     blockSize     int
 147     skipSubfolder error
 148 
 149     file func(h *handlers, path string, size int64) error
 150 
 151     liveLines bool
 152 }
 153 
 154 func (h handlers) countBlocks(size int64) int64 {
 155     bs := int64(h.blockSize)
 156     n := size / (1024 * bs)
 157     if size%(1024*bs) != 0 {
 158         n++
 159     }
 160     return n * bs
 161 }
 162 
 163 func run(w io.Writer, paths []string, cfg config) error {
 164     bw := bufio.NewWriter(w)
 165     defer bw.Flush()
 166 
 167     bw.WriteString("name\tbytes\tblocks\n")
 168 
 169     var h handlers
 170     h.w = bw
 171     h.blockSize = cfg.blockSize
 172     h.skipSubfolder = nil
 173     if !cfg.recursive {
 174         h.skipSubfolder = fs.SkipDir
 175     }
 176     h.file = emitEntry
 177     if cfg.sorted {
 178         h.file = keepEntry
 179     }
 180 
 181     got := make(map[string]struct{})
 182 
 183     if len(paths) == 0 {
 184         paths = []string{`.`}
 185     }
 186 
 187     // handle is the callback for func filepath.WalkDir
 188     handle := func(path string, e fs.DirEntry, err error) error {
 189         if err != nil {
 190             return err
 191         }
 192 
 193         if _, ok := got[path]; ok {
 194             return nil
 195         }
 196         got[path] = struct{}{}
 197 
 198         if e.IsDir() {
 199             return h.skipSubfolder
 200         }
 201 
 202         info, err := e.Info()
 203         if err != nil {
 204             return err
 205         }
 206 
 207         return h.file(&h, path, info.Size())
 208     }
 209 
 210     for _, path := range paths {
 211         if _, ok := got[path]; ok {
 212             continue
 213         }
 214         got[path] = struct{}{}
 215 
 216         st, err := os.Stat(path)
 217         if err != nil {
 218             return err
 219         }
 220 
 221         if st.IsDir() {
 222             if !strings.HasSuffix(path, `/`) {
 223                 path = path + `/`
 224             }
 225             got[path] = struct{}{}
 226 
 227             if err := filepath.WalkDir(path, handle); err != nil {
 228                 return err
 229             }
 230             continue
 231         }
 232 
 233         if err := h.file(&h, path, st.Size()); err != nil {
 234             return err
 235         }
 236     }
 237 
 238     if !cfg.sorted {
 239         return nil
 240     }
 241 
 242     sort.Slice(h.entries, func(i, j int) bool {
 243         return h.entries[i].size > h.entries[j].size
 244     })
 245 
 246     for _, e := range h.entries {
 247         if err := writeEntry(bw, e, h); err != nil {
 248             return err
 249         }
 250     }
 251 
 252     return nil
 253 }
 254 
 255 func emitEntry(h *handlers, path string, size int64) error {
 256     return writeEntry(h.w, entry{path, size}, *h)
 257 }
 258 
 259 func keepEntry(h *handlers, path string, size int64) error {
 260     h.entries = append(h.entries, entry{path, size})
 261     return nil
 262 }
 263 
 264 func writeEntry(w *bufio.Writer, e entry, h handlers) error {
 265     abs, err := filepath.Abs(e.path)
 266     if err != nil {
 267         return err
 268     }
 269 
 270     var buf [24]byte
 271     w.WriteString(abs)
 272     w.WriteByte('\t')
 273     w.Write(strconv.AppendInt(buf[:0], e.size, 10))
 274     w.WriteByte('\t')
 275     w.Write(strconv.AppendInt(buf[:0], h.countBlocks(e.size), 10))
 276     w.WriteByte('\n')
 277 
 278     if !h.liveLines {
 279         return nil
 280     }
 281 
 282     if err := w.Flush(); err != nil {
 283         return io.EOF
 284     }
 285     return nil
 286 }
     File: ./fixlines/fixlines.go
   1 /*
   2 The MIT License (MIT)
   3 
   4 Copyright (c) 2026 pacman64
   5 
   6 Permission is hereby granted, free of charge, to any person obtaining a copy of
   7 this software and associated documentation files (the "Software"), to deal
   8 in the Software without restriction, including without limitation the rights to
   9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
  10 of the Software, and to permit persons to whom the Software is furnished to do
  11 so, subject to the following conditions:
  12 
  13 The above copyright notice and this permission notice shall be included in all
  14 copies or substantial portions of the Software.
  15 
  16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  22 SOFTWARE.
  23 */
  24 
  25 package fixlines
  26 
  27 import (
  28     "bufio"
  29     "bytes"
  30     "errors"
  31     "io"
  32     "os"
  33 )
  34 
  35 const info = `
  36 fixlines [options...] [filepaths...]
  37 
  38 
  39 This tool fixes lines in UTF-8 text, ignoring leading UTF-8 BOMs, trailing
  40 carriage-returns on all lines, and ensures no lines across inputs are
  41 accidentally joined, since all lines it outputs end with line-feeds,
  42 even when the original files don't.
  43 
  44 The options are, available both in single and double-dash versions
  45 
  46     -h, -help    show this help message
  47 
  48     -detrail, -trimtrail, -trim-trail, -trimtrails, -trim-trails
  49                  ignore trailing spaces on lines
  50 
  51     -squeeze     aggressively trim lines, ignoring both leading and
  52                  trailing spaces, spaces around tabs, and turn runs
  53                  of multiple spaces into single ones
  54 `
  55 
  56 type config struct {
  57     fix       func(w *bufio.Writer, r io.Reader, live bool) error
  58     liveLines bool
  59 }
  60 
  61 func Main() {
  62     buffered := false
  63     args := os.Args[1:]
  64     var cfg config
  65     cfg.fix = detrail
  66 
  67     if len(args) > 0 {
  68         switch args[0] {
  69         case `-b`, `--b`, `-buffered`, `--buffered`:
  70             buffered = true
  71             args = args[1:]
  72 
  73         case
  74             `-detrail`, `--detrail`, `-trimtrail`, `--trimtrail`,
  75             `-trim-trail`, `--trim-trail`, `-trimtrails`, `--trimtrails`,
  76             `-trim-trails`, `--trim-trails`:
  77             cfg.fix = detrail
  78             args = args[1:]
  79 
  80         case `-h`, `--h`, `-help`, `--help`:
  81             os.Stdout.WriteString(info[1:])
  82             return
  83 
  84         case
  85             `-keeptrail`, `--keeptrail`, `-keept-rail`, `--keep-trail`,
  86             `-keeptrails`, `--keeptrails`, `-keept-rails`, `--keep-trails`:
  87             cfg.fix = catl
  88             args = args[1:]
  89 
  90         case `-s`, `--s`, `-squeeze`, `--squeeze`:
  91             buffered = true
  92             args = args[1:]
  93         }
  94     }
  95 
  96     if len(args) > 0 && args[0] == `--` {
  97         args = args[1:]
  98     }
  99 
 100     cfg.liveLines = !buffered
 101     if !buffered {
 102         if _, err := os.Stdout.Seek(0, io.SeekCurrent); err == nil {
 103             cfg.liveLines = false
 104         }
 105     }
 106 
 107     if err := run(os.Stdout, args, cfg); err != nil && err != io.EOF {
 108         os.Stderr.WriteString(err.Error())
 109         os.Stderr.WriteString("\n")
 110         os.Exit(1)
 111     }
 112 }
 113 
 114 func run(w io.Writer, args []string, cfg config) error {
 115     dashes := 0
 116     for _, name := range args {
 117         if name == `-` {
 118             dashes++
 119         }
 120         if dashes > 1 {
 121             return errors.New(`can't use stdin (dash) more than once`)
 122         }
 123     }
 124 
 125     bw := bufio.NewWriter(w)
 126     defer bw.Flush()
 127 
 128     if len(args) == 0 {
 129         return cfg.fix(bw, os.Stdin, cfg.liveLines)
 130     }
 131 
 132     for _, name := range args {
 133         if err := handleFile(bw, name, cfg); err != nil {
 134             return err
 135         }
 136     }
 137     return nil
 138 }
 139 
 140 func handleFile(w *bufio.Writer, name string, cfg config) error {
 141     if name == `` || name == `-` {
 142         return cfg.fix(w, os.Stdin, cfg.liveLines)
 143     }
 144 
 145     f, err := os.Open(name)
 146     if err != nil {
 147         return errors.New(`can't read from file named "` + name + `"`)
 148     }
 149     defer f.Close()
 150 
 151     return cfg.fix(w, f, cfg.liveLines)
 152 }
 153 
 154 func catl(w *bufio.Writer, r io.Reader, live bool) error {
 155     const gb = 1024 * 1024 * 1024
 156     sc := bufio.NewScanner(r)
 157     sc.Buffer(nil, 8*gb)
 158 
 159     for i := 0; sc.Scan(); i++ {
 160         s := sc.Bytes()
 161         if i == 0 && bytes.HasPrefix(s, []byte{0xef, 0xbb, 0xbf}) {
 162             s = s[3:]
 163         }
 164 
 165         w.Write(s)
 166         if w.WriteByte('\n') != nil {
 167             return io.EOF
 168         }
 169 
 170         if !live {
 171             continue
 172         }
 173 
 174         if err := w.Flush(); err != nil {
 175             return io.EOF
 176         }
 177     }
 178 
 179     return sc.Err()
 180 }
 181 
 182 func detrail(w *bufio.Writer, r io.Reader, live bool) error {
 183     const gb = 1024 * 1024 * 1024
 184     sc := bufio.NewScanner(r)
 185     sc.Buffer(nil, 8*gb)
 186 
 187     for i := 0; sc.Scan(); i++ {
 188         s := sc.Bytes()
 189 
 190         // ignore leading UTF-8 BOM on the first line
 191         if i == 0 && bytes.HasPrefix(s, []byte{0xef, 0xbb, 0xbf}) {
 192             s = s[3:]
 193         }
 194 
 195         // trim trailing spaces on the current line
 196         for len(s) > 0 && s[len(s)-1] == ' ' {
 197             s = s[:len(s)-1]
 198         }
 199 
 200         w.Write(s)
 201         if w.WriteByte('\n') != nil {
 202             return io.EOF
 203         }
 204 
 205         if !live {
 206             continue
 207         }
 208 
 209         if err := w.Flush(); err != nil {
 210             return io.EOF
 211         }
 212     }
 213 
 214     return sc.Err()
 215 }
 216 
 217 func squeeze(w *bufio.Writer, r io.Reader, live bool) error {
 218     const gb = 1024 * 1024 * 1024
 219     sc := bufio.NewScanner(r)
 220     sc.Buffer(nil, 8*gb)
 221 
 222     for i := 0; sc.Scan(); i++ {
 223         s := sc.Bytes()
 224         if i == 0 && bytes.HasPrefix(s, []byte{0xef, 0xbb, 0xbf}) {
 225             s = s[3:]
 226         }
 227 
 228         writeSqueezed(w, s)
 229         if w.WriteByte('\n') != nil {
 230             return io.EOF
 231         }
 232 
 233         if !live {
 234             continue
 235         }
 236 
 237         if err := w.Flush(); err != nil {
 238             return io.EOF
 239         }
 240     }
 241 
 242     return sc.Err()
 243 }
 244 
 245 func writeSqueezed(w *bufio.Writer, s []byte) {
 246     // ignore leading spaces
 247     for len(s) > 0 && s[0] == ' ' {
 248         s = s[1:]
 249     }
 250 
 251     // ignore trailing spaces
 252     for len(s) > 0 && s[len(s)-1] == ' ' {
 253         s = s[:len(s)-1]
 254     }
 255 
 256     space := false
 257 
 258     for len(s) > 0 {
 259         switch s[0] {
 260         case ' ':
 261             s = s[1:]
 262             space = true
 263 
 264         case '\t':
 265             s = s[1:]
 266             space = false
 267             for len(s) > 0 && s[0] == ' ' {
 268                 s = s[1:]
 269             }
 270             w.WriteByte('\t')
 271 
 272         default:
 273             if space {
 274                 w.WriteByte(' ')
 275                 space = false
 276             }
 277             w.WriteByte(s[0])
 278             s = s[1:]
 279         }
 280     }
 281 }
     File: ./fmscripts/benchmarks_test.go
   1 /*
   2 The MIT License (MIT)
   3 
   4 Copyright (c) 2026 pacman64
   5 
   6 Permission is hereby granted, free of charge, to any person obtaining a copy of
   7 this software and associated documentation files (the "Software"), to deal
   8 in the Software without restriction, including without limitation the rights to
   9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
  10 of the Software, and to permit persons to whom the Software is furnished to do
  11 so, subject to the following conditions:
  12 
  13 The above copyright notice and this permission notice shall be included in all
  14 copies or substantial portions of the Software.
  15 
  16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  22 SOFTWARE.
  23 */
  24 
  25 package fmscripts
  26 
  27 import (
  28     "math"
  29     "testing"
  30 )
  31 
  32 const (
  33     interferingRipples = "" +
  34         "exp(-0.5 * sin(2 * hypot(x - 2, y + 1))) + " +
  35         "exp(-0.5 * sin(10 * hypot(x + 2, y - 3.4)))"
  36 
  37     smilingGhost = "" +
  38         "log1p(((x - 1)**2 + y*y - 4)*((x + 1)**2 + y*y - 4)*" +
  39         "(x*x + (y - sqrt(3))**2 - 4) - 5)"
  40 )
  41 
  42 var benchmarks = []struct {
  43     Name   string
  44     Script string
  45     Native func(float64, float64) float64
  46 }{
  47     {"Load", "x", func(x, y float64) float64 { return x }},
  48     {"Constant", "4.25", func(x, y float64) float64 { return 4.25 }},
  49     {"Constant Addition", "1+2", func(x, y float64) float64 { return 1 + 2 }},
  50     {"0 Additions", "x", func(x, y float64) float64 { return x }},
  51     {"1 Additions", "x+x", func(x, y float64) float64 { return x + x }},
  52     {"2 Additions", "x+x+x", func(x, y float64) float64 { return x + x + x }},
  53     {"0 Multiplications", "x", func(x, y float64) float64 { return x }},
  54     {"1 Multiplications", "x*x", func(x, y float64) float64 { return x * x }},
  55     {"2 Multiplications", "x*x*x", func(x, y float64) float64 { return x * x * x }},
  56     {"0 Divisions", "x", func(x, y float64) float64 { return x }},
  57     {"1 Divisions", "x/x", func(x, y float64) float64 { return x / x }},
  58     {"2 Divisions", "x/x/x", func(x, y float64) float64 { return x / x / x }},
  59     {"e+pi", "e+pi", func(x, y float64) float64 { return math.E + math.Pi }},
  60     {"1-2", "1-2", func(x, y float64) float64 { return 1 - 2 }},
  61     {"e-pi", "e-pi", func(x, y float64) float64 { return math.E - math.Pi }},
  62     {"Add 0", "x+0", func(x, y float64) float64 { return x + 0 }},
  63     {"x*1", "x*1", func(x, y float64) float64 { return x * 1 }},
  64     {"Abs Func", "abs(-x)", func(x, y float64) float64 { return math.Abs(-x) }},
  65     {"Abs Syntax", "&(-x)", func(x, y float64) float64 { return math.Abs(-x) }},
  66     {
  67         "Square (pow)",
  68         "pow(x, 2)",
  69         func(x, y float64) float64 { return math.Pow(x, 2) },
  70     },
  71     {"Square", "square(x)", func(x, y float64) float64 { return x * x }},
  72     {"Square Syntax", "*x", func(x, y float64) float64 { return x * x }},
  73     {
  74         "Linear",
  75         "3.4 * x - 0.2",
  76         func(x, y float64) float64 { return 3.4*x - 0.2 },
  77     },
  78     {"Direct Square", "x*x", func(x, y float64) float64 { return x * x }},
  79     {
  80         "Cube (pow)",
  81         "pow(x, 3)",
  82         func(x, y float64) float64 { return math.Pow(x, 3) },
  83     },
  84     {"Cube", "cube(x)", func(x, y float64) float64 { return x * x * x }},
  85     {"Direct Cube", "x*x*x", func(x, y float64) float64 { return x * x * x }},
  86     {
  87         "Reciprocal (pow)",
  88         "pow(x, -1)",
  89         func(x, y float64) float64 { return math.Pow(x, -1) },
  90     },
  91     {
  92         "Reciprocal Func",
  93         "reciprocal(x)",
  94         func(x, y float64) float64 { return 1 / x },
  95     },
  96     {"Direct Reciprocal", "1/x", func(x, y float64) float64 { return 1 / x }},
  97     {"Variable Negation", "-x", func(x, y float64) float64 { return -x }},
  98     {"Constant Multip.", "1*2", func(x, y float64) float64 { return 1 * 2 }},
  99     {"e*pi", "e*pi", func(x, y float64) float64 { return math.E * math.Pi }},
 100     {
 101         "Variable y = mx + k",
 102         "2.1*x + 3.4",
 103         func(x, y float64) float64 { return 2.1*x + 3.4 },
 104     },
 105     {"sin(x)", "sin(x)", func(x, y float64) float64 { return math.Sin(x) }},
 106     {"cos(x)", "cos(x)", func(x, y float64) float64 { return math.Cos(x) }},
 107     {"exp(x)", "exp(x)", func(x, y float64) float64 { return math.Exp(x) }},
 108     {"expm1(x)", "expm1(x)", func(x, y float64) float64 { return math.Expm1(x) }},
 109     {"ln(x)", "ln(x)", func(x, y float64) float64 { return math.Log(x) }},
 110     {"log2(x)", "log2(x)", func(x, y float64) float64 { return math.Log2(x) }},
 111     {"log10(x)", "log10(x)", func(x, y float64) float64 { return math.Log10(x) }},
 112     {"1/2", "1/2", func(x, y float64) float64 { return 1.0 / 2.0 }},
 113     {"e/pi", "e/pi", func(x, y float64) float64 { return math.E / math.Pi }},
 114     {"exp(2)", "exp(2)", func(x, y float64) float64 { return math.Exp(2) }},
 115     {
 116         "Club Beat Pulse",
 117         "sin(10*tau * exp(-20*x)) * exp(-2*x)",
 118         func(x, y float64) float64 {
 119             return math.Sin(10*2*math.Pi*math.Exp(-20*x)) * math.Exp(-2*x)
 120         },
 121     },
 122     {
 123         "Interfering Ripples",
 124         interferingRipples,
 125         func(x, y float64) float64 {
 126             return math.Exp(-0.5*math.Sin(2*math.Hypot(x-2, y+1))) +
 127                 math.Exp(-0.5*math.Sin(10*math.Hypot(x+2, y-3.4)))
 128         },
 129     },
 130     {
 131         "Floor Lights",
 132         "abs(sin(x)) / y**1.4",
 133         func(x, y float64) float64 {
 134             return math.Abs(math.Sin(x)) / math.Pow(y, 1.4)
 135         },
 136     },
 137     {
 138         "Domain Hole",
 139         "log1p(sin(x + y) + (x - y)**2 - 1.5*x + 2.5*y + 1)",
 140         func(x, y float64) float64 {
 141             v := x - y
 142             return math.Log1p(math.Sin(x+y) + v*v - 1.5*x + 2.5*y + 1)
 143         },
 144     },
 145     {
 146         "Beta Gradient",
 147         "lbeta(x + 5.1, y + 5.1)",
 148         func(x, y float64) float64 { return lnBeta(x+5.1, y+5.1) },
 149     },
 150     {
 151         "Hot Bars",
 152         "abs(x) + sqrt(abs(sin(2*y)))",
 153         func(x, y float64) float64 {
 154             return math.Abs(x) + math.Sqrt(math.Abs(math.Sin(2*y)))
 155         },
 156     },
 157     {
 158         "Grid Pattern",
 159         "sin(sin(x)+cos(y)) + cos(sin(x*y)+cos(y**2))",
 160         func(x, y float64) float64 {
 161             return math.Sin(math.Sin(x)+math.Cos(y)) +
 162                 math.Cos(math.Sin(x*y)+math.Cos(y*y))
 163         },
 164     },
 165     {
 166         "Smiling Ghost",
 167         smilingGhost,
 168         func(x, y float64) float64 {
 169             xm1 := x - 1
 170             xp1 := x + 1
 171             v := y - math.Sqrt(3)
 172             w := (xm1*xm1 + y*y - 4) * (xp1*xp1 + y*y - 4) * (x*x + v*v - 4)
 173             return math.Log1p(w - 5)
 174         },
 175     },
 176     {
 177         "Forgot its Name",
 178         "1 / (1 + x.abs/y*y)",
 179         func(x, y float64) float64 {
 180             return 1 / (1 + math.Abs(x)/y*y)
 181         },
 182     },
 183 }
 184 
 185 func BenchmarkEmpty(b *testing.B) {
 186     b.Run("empty program", func(b *testing.B) {
 187         var p Program
 188         b.ReportAllocs()
 189         b.ResetTimer()
 190 
 191         for i := 0; i < b.N; i++ {
 192             _ = p.Run()
 193         }
 194     })
 195 
 196     f := func(float64, float64) float64 {
 197         return math.NaN()
 198     }
 199 
 200     b.Run("empty function", func(b *testing.B) {
 201         b.ReportAllocs()
 202         b.ResetTimer()
 203 
 204         for i := 0; i < b.N; i++ {
 205             _ = f(0, 0)
 206         }
 207     })
 208 }
 209 
 210 func BenchmarkBasicProgram(b *testing.B) {
 211     for _, tc := range benchmarks {
 212         var c Compiler
 213         defs := map[string]any{
 214             "x": 0.05,
 215             "y": 0,
 216             "z": 0,
 217         }
 218 
 219         b.Run(tc.Name, func(b *testing.B) {
 220             p, err := c.Compile(tc.Script, defs)
 221             if err != nil {
 222                 const fs = "while compiling %q, got error %q"
 223                 b.Fatalf(fs, tc.Script, err.Error())
 224                 return
 225             }
 226 
 227             x, _ := p.Get("x")
 228             _, _ = p.Get("y")
 229             _, _ = p.Get("z")
 230 
 231             b.ResetTimer()
 232             for i := 0; i < b.N; i++ {
 233                 _ = p.Run()
 234                 *x++
 235             }
 236         })
 237     }
 238 }
 239 
 240 func BenchmarkBasicFunc(b *testing.B) {
 241     for _, tc := range benchmarks {
 242         b.Run(tc.Name, func(b *testing.B) {
 243             x := 0.05
 244             y := 0.0
 245             fn := tc.Native
 246             b.ResetTimer()
 247 
 248             for i := 0; i < b.N; i++ {
 249                 fn(x, y)
 250                 x++
 251                 y++
 252             }
 253         })
 254     }
 255 }
 256 
 257 func BenchmarkSoundProgram(b *testing.B) {
 258     var soundBenchmarkTests = []struct {
 259         Name   string
 260         Script string
 261     }{
 262         {
 263             "Sine Wave",
 264             "sin(1000 * tau * x)",
 265         },
 266         {
 267             "Laser Pulse",
 268             "sin(100 * tau * exp(-40 * u))",
 269         },
 270         {
 271             "Club Beat Pulse",
 272             "sin(10 * tau * exp(-20 * x)) * exp(-2 * x)",
 273         },
 274     }
 275 
 276     for _, tc := range soundBenchmarkTests {
 277         var c Compiler
 278         defs := map[string]any{
 279             "t": 0.05,
 280             "u": 0,
 281         }
 282 
 283         const seconds = 2
 284         const sampleRate = 48_000
 285         dt := 1.0 / float64(sampleRate)
 286         // buf is the destination buffer for all calculated samples
 287         buf := make([]float64, 0, seconds*sampleRate)
 288 
 289         b.Run(tc.Name, func(b *testing.B) {
 290             p, err := c.Compile(tc.Script, defs)
 291             if err != nil {
 292                 const fs = "while compiling %q, got error %q"
 293                 b.Fatalf(fs, tc.Script, err.Error())
 294                 return
 295             }
 296 
 297             // input parameters for the program
 298             t, _ := p.Get("t")
 299             u, _ := p.Get("u")
 300 
 301             b.ResetTimer()
 302             for i := 0; i < b.N; i++ {
 303                 // avoid buffer expansions after the first run
 304                 buf = buf[:0]
 305 
 306                 // benchmark 1 second of generated sound
 307                 for j := 0; j < seconds*sampleRate; j++ {
 308                     // calculate time in seconds from current sample index
 309                     v := float64(j) * dt
 310                     *t = v
 311                     _, *u = math.Modf(v)
 312 
 313                     // calculate a mono sample
 314                     buf = append(buf, p.Run())
 315                 }
 316             }
 317         })
 318     }
 319 }
 320 
 321 func BenchmarkNativeSoundProgram(b *testing.B) {
 322     const tau = 2 * math.Pi
 323 
 324     var soundBenchmarkTests = []struct {
 325         Name string
 326         Fun  func(float64, float64) float64
 327     }{
 328         {
 329             "Sine Wave",
 330             func(t, u float64) float64 {
 331                 return math.Sin(1000 * tau * t)
 332             },
 333         },
 334         {
 335             "Laser Pulse",
 336             func(t, u float64) float64 {
 337                 return math.Sin(100 * tau * math.Exp(-40*u))
 338             },
 339         },
 340         {
 341             "Club Beat Pulse",
 342             func(t, u float64) float64 {
 343                 return math.Sin(10*tau*math.Exp(-20*t)) * math.Exp(-2*t)
 344             },
 345         },
 346     }
 347 
 348     for _, tc := range soundBenchmarkTests {
 349         const seconds = 2
 350         const sampleRate = 48_000
 351         dt := 1.0 / float64(sampleRate)
 352         // buf is the destination buffer for all calculated samples
 353         buf := make([]float64, 0, seconds*sampleRate)
 354 
 355         b.Run(tc.Name, func(b *testing.B) {
 356             // input parameters for the program
 357             t := 0.05
 358             u := 0.00
 359             fn := tc.Fun
 360             b.ResetTimer()
 361 
 362             for i := 0; i < b.N; i++ {
 363                 // avoid buffer expansions after the first run
 364                 buf = buf[:0]
 365 
 366                 // benchmark 1 second of generated sound
 367                 for j := 0; j < seconds*sampleRate; j++ {
 368                     // calculate time in seconds from current sample index
 369                     v := float64(j) * dt
 370                     t = v
 371                     _, u = math.Modf(v)
 372 
 373                     // calculate a mono sample
 374                     buf = append(buf, fn(t, u))
 375                 }
 376             }
 377         })
 378     }
 379 }
 380 
 381 func BenchmarkImageProgram(b *testing.B) {
 382     const (
 383         // use part of a full HD pic to give more runs for each test, giving
 384         // greater statistical-stability/comparability across benchmark runs
 385         w = 1920 / 4
 386         h = 1080 / 4
 387 
 388         intRipples = "" +
 389             "exp(-0.5 * sin(2 * hypot(x - 2, y + 1))) + " +
 390             "exp(-0.5 * sin(10 * hypot(x + 2, y - 3.4)))"
 391         domainHole = "" +
 392             "log1p(sin(x + y) + pow(x - y, 2) - 1.5*x + 2.5*y + 1)"
 393         gridPatPow = "" +
 394             "sin(sin(x)+cos(y)) + cos(sin(x*y)+cos(pow(y, 2)))"
 395         gridPatSquare = "" +
 396             "sin(sin(x)+cos(y)) + cos(sin(x*y)+cos(square(y)))"
 397         smilingGhostFunc = "" +
 398             "log1p((pow(x - 1, 2) + y*y - 4)*" +
 399             "(pow(x + 1, 2) + y*y - 4)*" +
 400             "(x*x + pow(y - sqrt(3), 2) - 4) - 5)"
 401         smilingGhostSyntax = "" +
 402             "log1p((*(x - 1) + *y - 4)*" +
 403             "(*(x + 1) + *y - 4)*" +
 404             "(*x + *(y - sqrt(3)) - 4) - 5)"
 405     )
 406 
 407     var imageBenchmarkTests = []struct {
 408         Name   string
 409         Width  int
 410         Height int
 411         Script string
 412     }{
 413         {"Horizontal Linear", w, h, "x"},
 414         {"Multiplication", w, h, "x*y"},
 415         {"Star", w, h, "exp(-0.5 * hypot(x, y))"},
 416         {"Interfering Ripples", w, h, intRipples},
 417         {"Floor Lights", w, h, "abs(sin(x)) / pow(y, 1.4)"},
 418         {"Domain Hole", w, h, domainHole},
 419         {"Beta Gradient", w, h, "lbeta(x + 5.1, y + 5.1)"},
 420         {"Hot Bars", w, h, "abs(x) + sqrt(abs(sin(2*y)))"},
 421         {"Grid Pattern (pow)", w, h, gridPatPow},
 422         {"Grid Pattern (square)", w, h, gridPatSquare},
 423         {"Smiling Ghost (pow)", w, h, smilingGhostFunc},
 424         {"Smiling Ghost (square syntax)", w, h, smilingGhostSyntax},
 425     }
 426 
 427     for _, tc := range imageBenchmarkTests {
 428         var c Compiler
 429         defs := map[string]any{
 430             "x": 0,
 431             "y": 0,
 432         }
 433 
 434         // 4k-resolution results buffer
 435         buf := make([]float64, 1920*1080*4)
 436 
 437         b.Run(tc.Name, func(b *testing.B) {
 438             p, err := c.Compile(tc.Script, defs)
 439             if err != nil {
 440                 const fs = "while compiling %q, got error %q"
 441                 b.Fatalf(fs, tc.Script, err.Error())
 442                 return
 443             }
 444 
 445             // input parameters for the program
 446             x, _ := p.Get("x")
 447             y, _ := p.Get("y")
 448 
 449             w := tc.Width
 450             h := tc.Height
 451             dx := 1.0 / float64(w)
 452             dy := 1.0 / float64(h)
 453             xmin := -5.2
 454             ymax := 23.91
 455             b.ResetTimer()
 456             for i := 0; i < b.N; i++ {
 457                 // avoid buffer expansions after the first run
 458                 buf = buf[:0]
 459 
 460                 for j := 0; j < h; j++ {
 461                     *y = ymax - float64(j)*dy
 462                     for i := 0; i < w; i++ {
 463                         *x = float64(i)*dx + xmin
 464                         buf = append(buf, p.Run())
 465                     }
 466                 }
 467             }
 468         })
 469     }
 470 }
 471 
 472 func BenchmarkCompiler(b *testing.B) {
 473     f := func(name string, src string) {
 474         b.Run(name, func(b *testing.B) {
 475             for i := 0; i < b.N; i++ {
 476                 var c Compiler
 477                 _, err := c.Compile(src, map[string]any{
 478                     "x": 0.05,
 479                     "y": 0,
 480                     "z": 0,
 481                 })
 482 
 483                 if err != nil {
 484                     const fs = "while compiling %q, got error %q"
 485                     b.Fatalf(fs, src, err.Error())
 486                 }
 487             }
 488         })
 489     }
 490 
 491     for _, tc := range mathCorrectnessTests {
 492         f(tc.Name, tc.Script)
 493     }
 494 
 495     for _, tc := range benchmarks {
 496         f(tc.Name, tc.Script)
 497     }
 498 }
     File: ./fmscripts/compilers.go
   1 /*
   2 The MIT License (MIT)
   3 
   4 Copyright (c) 2026 pacman64
   5 
   6 Permission is hereby granted, free of charge, to any person obtaining a copy of
   7 this software and associated documentation files (the "Software"), to deal
   8 in the Software without restriction, including without limitation the rights to
   9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
  10 of the Software, and to permit persons to whom the Software is furnished to do
  11 so, subject to the following conditions:
  12 
  13 The above copyright notice and this permission notice shall be included in all
  14 copies or substantial portions of the Software.
  15 
  16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  22 SOFTWARE.
  23 */
  24 
  25 package fmscripts
  26 
  27 import (
  28     "fmt"
  29     "math"
  30     "strconv"
  31 )
  32 
  33 // unary2op turns unary operators into their corresponding basic operations;
  34 // some entries are only for the optimizer, and aren't accessible directly
  35 // from valid source code
  36 var unary2op = map[string]opcode{
  37     "-": neg,
  38     "!": not,
  39     "&": abs,
  40     "*": square,
  41     "^": square,
  42     "/": rec,
  43     "%": mod1,
  44 }
  45 
  46 // binary2op turns binary operators into their corresponding basic operations
  47 var binary2op = map[string]opcode{
  48     "+":  add,
  49     "-":  sub,
  50     "*":  mul,
  51     "/":  div,
  52     "%":  mod,
  53     "&&": and,
  54     "&":  and,
  55     "||": or,
  56     "|":  or,
  57     "==": equal,
  58     "!=": notequal,
  59     "<>": notequal,
  60     "<":  less,
  61     "<=": lessoreq,
  62     ">":  more,
  63     ">=": moreoreq,
  64     "**": pow,
  65     "^":  pow,
  66 }
  67 
  68 // Compiler lets you create Program objects, which you can then run. The whole
  69 // point of this is to create quicker-to-run numeric scripts. You can even add
  70 // variables with initial values, as well as functions for the script to use.
  71 //
  72 // Common math funcs and constants are automatically detected, and constant
  73 // results are optimized away, unless builtins are redefined. In other words,
  74 // the optimizer is effectively disabled for all (sub)expressions containing
  75 // redefined built-ins, as there's no way to be sure those values won't change
  76 // from one run to the next.
  77 //
  78 // See the comment for type Program for more details.
  79 //
  80 // # Example
  81 //
  82 // var c fmscripts.Compiler
  83 //
  84 //  defs := map[string]any{
  85 //      "x": 0,    // define `x`, and initialize it to 0
  86 //      "k": 4.25, // define `k`, and initialize it to 4.25
  87 //      "b": true, // define `b`, and initialize it to 1.0
  88 //      "n": -23,  // define `n`, and initialize it to -23.0
  89 //      "pi": 3,   // define `pi`, overriding the default constant named `pi`
  90 //
  91 //      "f": numericKernel // type is func ([]float64) float64
  92 //      "g": otherFunc     // type is func (float64) float64
  93 //  }
  94 //
  95 // prog, err := c.Compile("log10(k) + f(sqrt(k) * exp(-x), 45, -0.23)", defs)
  96 // // ...
  97 //
  98 // x, _ := prog.Get("x") // Get returns (*float64, bool)
  99 // y, _ := prog.Get("y") // a useless pointer, since program doesn't use `y`
 100 // // ...
 101 //
 102 //  for i := 0; i < n; i++ {
 103 //      *x = float64(i)*dx + minx // you update inputs in place using pointers
 104 //      f := prog.Run()           // method Run gives you a float64 back
 105 //      // ...
 106 //  }
 107 type Compiler struct {
 108     maxStack int     // the exact stack size the resulting program needs
 109     ops      []numOp // the program's operations
 110 
 111     vaddr map[string]int // address lookup for values
 112     faddr map[string]int // address lookup for functions
 113     ftype map[string]any // keeps track of func types during compilation
 114 
 115     values []float64 // variables and constants available to programs
 116     funcs  []any     // funcs available to programs
 117 }
 118 
 119 // Compile parses the script given and generates a fast float64-only Program
 120 // made only of sequential steps: any custom funcs you provide it can use
 121 // their own internal looping and/or conditional logic, of course.
 122 func (c *Compiler) Compile(src string, defs map[string]any) (Program, error) {
 123     // turn source code into an abstract syntax-tree
 124     root, err := parse(src)
 125     if err != nil {
 126         return Program{}, err
 127     }
 128 
 129     // generate operations
 130     if err := c.reset(defs); err != nil {
 131         return Program{}, err
 132     }
 133     if err = c.compile(root, 0); err != nil {
 134         return Program{}, err
 135     }
 136 
 137     // create the resulting program
 138     var p Program
 139     p.stack = make([]float64, c.maxStack)
 140     p.values = make([]float64, len(c.values))
 141     copy(p.values, c.values)
 142     p.ops = make([]numOp, len(c.ops))
 143     copy(p.ops, c.ops)
 144     p.funcs = make([]any, len(c.ftype))
 145     copy(p.funcs, c.funcs)
 146     p.names = make(map[string]int, len(c.vaddr))
 147 
 148     // give the program's Get method access only to all allocated variables
 149     for k, v := range c.vaddr {
 150         // avoid exposing numeric constants on the program's name-lookup
 151         // table, which would allow users to change literals across runs
 152         if _, err := strconv.ParseFloat(k, 64); err == nil {
 153             continue
 154         }
 155         // expose only actual variable names
 156         p.names[k] = v
 157     }
 158 
 159     return p, nil
 160 }
 161 
 162 // reset prepares a compiler by satisfying internal preconditions func
 163 // compileExpr relies on, when given an abstract syntax-tree
 164 func (c *Compiler) reset(defs map[string]any) error {
 165     // reset the compiler's internal state
 166     c.maxStack = 0
 167     c.ops = c.ops[:0]
 168     c.vaddr = make(map[string]int)
 169     c.faddr = make(map[string]int)
 170     c.ftype = make(map[string]any)
 171     c.values = c.values[:0]
 172     c.funcs = c.funcs[:0]
 173 
 174     // allocate vars and funcs
 175     for k, v := range defs {
 176         if err := c.allocEntry(k, v); err != nil {
 177             return err
 178         }
 179     }
 180     return nil
 181 }
 182 
 183 // allocEntry simplifies the control-flow of func reset
 184 func (c *Compiler) allocEntry(k string, v any) error {
 185     const (
 186         maxExactRound = 2 << 52
 187         rangeErrFmt   = "%d is outside range of exact float64 integers"
 188         typeErrFmt    = "got value of unsupported type %T"
 189     )
 190 
 191     switch v := v.(type) {
 192     case float64:
 193         _, err := c.allocValue(k, v)
 194         return err
 195 
 196     case int:
 197         if math.Abs(float64(v)) > maxExactRound {
 198             return fmt.Errorf(rangeErrFmt, v)
 199         }
 200         _, err := c.allocValue(k, float64(v))
 201         return err
 202 
 203     case bool:
 204         _, err := c.allocValue(k, debool(v))
 205         return err
 206 
 207     default:
 208         if isSupportedFunc(v) {
 209             c.ftype[k] = v
 210             _, err := c.allocFunc(k, v)
 211             return err
 212         }
 213         return fmt.Errorf(typeErrFmt, v)
 214     }
 215 }
 216 
 217 // allocValue ensures there's a place to match the name given, returning its
 218 // index when successful; only programs using unreasonably many values will
 219 // cause this func to fail
 220 func (c *Compiler) allocValue(s string, f float64) (int, error) {
 221     if len(c.values) >= maxOpIndex {
 222         const fs = "programs can only use up to %d distinct float64 values"
 223         return -1, fmt.Errorf(fs, maxOpIndex+1)
 224     }
 225 
 226     i := len(c.values)
 227     c.vaddr[s] = i
 228     c.values = append(c.values, f)
 229     return i, nil
 230 }
 231 
 232 // valueIndex returns the index reserved for an allocated value/variable:
 233 // all unallocated values/variables are allocated here on first access
 234 func (c *Compiler) valueIndex(s string, f float64) (int, error) {
 235     // name is found: return the index of the already-allocated var
 236     if i, ok := c.vaddr[s]; ok {
 237         return i, nil
 238     }
 239 
 240     // name not found, but it's a known math constant
 241     if f, ok := mathConst[s]; ok {
 242         return c.allocValue(s, f)
 243     }
 244     // name not found, and it's not of a known math constant
 245     return c.allocValue(s, f)
 246 }
 247 
 248 // constIndex allocates a constant as a variable named as its own string
 249 // representation, which avoids multiple entries for repeated uses of the
 250 // same constant value
 251 func (c *Compiler) constIndex(f float64) (int, error) {
 252     // constants have no name, so use a canonical string representation
 253     return c.valueIndex(strconv.FormatFloat(f, 'f', 16, 64), f)
 254 }
 255 
 256 // funcIndex returns the index reserved for an allocated function: all
 257 // unallocated functions are allocated here on first access
 258 func (c *Compiler) funcIndex(name string) (int, error) {
 259     // check if func was already allocated
 260     if i, ok := c.faddr[name]; ok {
 261         return i, nil
 262     }
 263 
 264     // if name is reserved allocate an index for its matching func
 265     if fn, ok := c.ftype[name]; ok {
 266         return c.allocFunc(name, fn)
 267     }
 268 
 269     // if name wasn't reserved, see if it's a standard math func name
 270     if fn := c.autoFuncLookup(name); fn != nil {
 271         return c.allocFunc(name, fn)
 272     }
 273 
 274     // name isn't even a standard math func's
 275     return -1, fmt.Errorf("function not found")
 276 }
 277 
 278 // allocFunc ensures there's a place for the function name given, returning its
 279 // index when successful; only funcs of unsupported types will cause failure
 280 func (c *Compiler) allocFunc(name string, fn any) (int, error) {
 281     if isSupportedFunc(fn) {
 282         i := len(c.funcs)
 283         c.faddr[name] = i
 284         c.ftype[name] = fn
 285         c.funcs = append(c.funcs, fn)
 286         return i, nil
 287     }
 288     return -1, fmt.Errorf("can't use a %T as a number-crunching function", fn)
 289 }
 290 
 291 // autoFuncLookup checks built-in deterministic funcs for the name given: its
 292 // result is nil only if there's no match
 293 func (c *Compiler) autoFuncLookup(name string) any {
 294     if fn, ok := determFuncs[name]; ok {
 295         return fn
 296     }
 297     return nil
 298 }
 299 
 300 // genOp generates/adds a basic operation to the program, while keeping track
 301 // of the maximum depth the stack can reach
 302 func (c *Compiler) genOp(op numOp, depth int) {
 303     // add 2 defensively to ensure stack space for the inputs of binary ops
 304     n := depth + 2
 305     if c.maxStack < n {
 306         c.maxStack = n
 307     }
 308     c.ops = append(c.ops, op)
 309 }
 310 
 311 // compile is a recursive expression evaluator which does the actual compiling
 312 // as it goes along
 313 func (c *Compiler) compile(expr any, depth int) error {
 314     expr = c.optimize(expr)
 315 
 316     switch expr := expr.(type) {
 317     case float64:
 318         return c.compileLiteral(expr, depth)
 319     case string:
 320         return c.compileVariable(expr, depth)
 321     case []any:
 322         return c.compileCombo(expr, depth)
 323     case unaryExpr:
 324         return c.compileUnary(expr.op, expr.x, depth)
 325     case binaryExpr:
 326         return c.compileBinary(expr.op, expr.x, expr.y, depth)
 327     case callExpr:
 328         return c.compileCall(expr, depth)
 329     case assignExpr:
 330         return c.compileAssign(expr, depth)
 331     default:
 332         return fmt.Errorf("unsupported expression type %T", expr)
 333     }
 334 }
 335 
 336 // compileLiteral generates a load operation for the constant value given
 337 func (c *Compiler) compileLiteral(f float64, depth int) error {
 338     i, err := c.constIndex(f)
 339     if err != nil {
 340         return err
 341     }
 342     c.genOp(numOp{What: load, Index: opindex(i)}, depth)
 343     return nil
 344 }
 345 
 346 // compileVariable generates a load operation for the variable name given
 347 func (c *Compiler) compileVariable(name string, depth int) error {
 348     // handle names which aren't defined, but are known math constants
 349     if _, ok := c.vaddr[name]; !ok {
 350         if f, ok := mathConst[name]; ok {
 351             return c.compileLiteral(f, depth)
 352         }
 353     }
 354 
 355     // handle actual variables
 356     i, err := c.valueIndex(name, 0)
 357     if err != nil {
 358         return err
 359     }
 360     c.genOp(numOp{What: load, Index: opindex(i)}, depth)
 361     return nil
 362 }
 363 
 364 // compileCombo handles a sequence of expressions
 365 func (c *Compiler) compileCombo(exprs []any, depth int) error {
 366     for _, v := range exprs {
 367         err := c.compile(v, depth)
 368         if err != nil {
 369             return err
 370         }
 371     }
 372     return nil
 373 }
 374 
 375 // compileUnary handles unary expressions
 376 func (c *Compiler) compileUnary(op string, x any, depth int) error {
 377     err := c.compile(x, depth+1)
 378     if err != nil {
 379         return err
 380     }
 381 
 382     if op, ok := unary2op[op]; ok {
 383         c.genOp(numOp{What: op}, depth)
 384         return nil
 385     }
 386     return fmt.Errorf("unary operation %q is unsupported", op)
 387 }
 388 
 389 // compileBinary handles binary expressions
 390 func (c *Compiler) compileBinary(op string, x, y any, depth int) error {
 391     switch op {
 392     case "===", "!==":
 393         // handle binary expressions with no matching basic operation, by
 394         // treating them as aliases for function calls
 395         return c.compileCall(callExpr{name: op, args: []any{x, y}}, depth)
 396     }
 397 
 398     err := c.compile(x, depth+1)
 399     if err != nil {
 400         return err
 401     }
 402     err = c.compile(y, depth+2)
 403     if err != nil {
 404         return err
 405     }
 406 
 407     if op, ok := binary2op[op]; ok {
 408         c.genOp(numOp{What: op}, depth)
 409         return nil
 410     }
 411     return fmt.Errorf("binary operation %q is unsupported", op)
 412 }
 413 
 414 // compileCall handles function-call expressions
 415 func (c *Compiler) compileCall(expr callExpr, depth int) error {
 416     // lookup function name
 417     index, err := c.funcIndex(expr.name)
 418     if err != nil {
 419         return fmt.Errorf("%s: %s", expr.name, err.Error())
 420     }
 421     // get the func value, as its type determines the calling op to emit
 422     v, ok := c.ftype[expr.name]
 423     if !ok {
 424         return fmt.Errorf("%s: function is undefined", expr.name)
 425     }
 426 
 427     // figure which type of function operation to use
 428     op, ok := func2op(v)
 429     if !ok {
 430         return fmt.Errorf("%s: unsupported function type %T", expr.name, v)
 431     }
 432 
 433     // ensure number of args given to the func makes sense for the func type
 434     err = checkArgCount(func2info[op], expr.name, len(expr.args))
 435     if err != nil {
 436         return err
 437     }
 438 
 439     // generate operations to evaluate all args
 440     for i, v := range expr.args {
 441         err := c.compile(v, depth+i+1)
 442         if err != nil {
 443             return err
 444         }
 445     }
 446 
 447     // generate func-call operation
 448     given := len(expr.args)
 449     next := numOp{What: op, NumArgs: opargs(given), Index: opindex(index)}
 450     c.genOp(next, depth)
 451     return nil
 452 }
 453 
 454 // checkArgCount does what it says, returning informative errors when arg
 455 // counts are wrong
 456 func checkArgCount(info funcTypeInfo, name string, nargs int) error {
 457     if info.AtLeast < 0 && info.AtMost < 0 {
 458         return nil
 459     }
 460 
 461     if info.AtLeast == info.AtMost && nargs != info.AtMost {
 462         const fs = "%s: expected %d args, got %d instead"
 463         return fmt.Errorf(fs, name, info.AtMost, nargs)
 464     }
 465 
 466     if info.AtLeast >= 0 && info.AtMost >= 0 {
 467         const fs = "%s: expected between %d and %d args, got %d instead"
 468         if nargs < info.AtLeast || nargs > info.AtMost {
 469             return fmt.Errorf(fs, name, info.AtLeast, info.AtMost, nargs)
 470         }
 471     }
 472 
 473     if info.AtLeast >= 0 && nargs < info.AtLeast {
 474         const fs = "%s: expected at least %d args, got %d instead"
 475         return fmt.Errorf(fs, name, info.AtLeast, nargs)
 476     }
 477 
 478     if info.AtMost >= 0 && nargs > info.AtMost {
 479         const fs = "%s: expected at most %d args, got %d instead"
 480         return fmt.Errorf(fs, name, info.AtMost, nargs)
 481     }
 482 
 483     // all is good
 484     return nil
 485 }
 486 
 487 // compileAssign generates a store operation for the expression given
 488 func (c *Compiler) compileAssign(expr assignExpr, depth int) error {
 489     err := c.compile(expr.expr, depth)
 490     if err != nil {
 491         return err
 492     }
 493 
 494     i, err := c.allocValue(expr.name, 0)
 495     if err != nil {
 496         return err
 497     }
 498 
 499     c.genOp(numOp{What: store, Index: opindex(i)}, depth)
 500     return nil
 501 }
 502 
 503 // func sameFunc(x, y any) bool {
 504 //  if x == nil || y == nil {
 505 //      return false
 506 //  }
 507 //  return reflect.ValueOf(x).Pointer() == reflect.ValueOf(y).Pointer()
 508 // }
 509 
 510 // isSupportedFunc checks if a value's type is a supported func type
 511 func isSupportedFunc(fn any) bool {
 512     _, ok := func2op(fn)
 513     return ok
 514 }
 515 
 516 // funcTypeInfo is an entry in the func2info lookup table
 517 type funcTypeInfo struct {
 518     // AtLeast is the minimum number of inputs the func requires: negative
 519     // values are meant to be ignored
 520     AtLeast int
 521 
 522     // AtMost is the maximum number of inputs the func requires: negative
 523     // values are meant to be ignored
 524     AtMost int
 525 }
 526 
 527 // func2info is a lookup table to check the number of inputs funcs are given
 528 var func2info = map[opcode]funcTypeInfo{
 529     call0:  {AtLeast: +0, AtMost: +0},
 530     call1:  {AtLeast: +1, AtMost: +1},
 531     call2:  {AtLeast: +2, AtMost: +2},
 532     call3:  {AtLeast: +3, AtMost: +3},
 533     call4:  {AtLeast: +4, AtMost: +4},
 534     call5:  {AtLeast: +5, AtMost: +5},
 535     callv:  {AtLeast: -1, AtMost: -1},
 536     call1v: {AtLeast: +1, AtMost: -1},
 537 }
 538 
 539 // func2op tries to match a func type into a corresponding opcode
 540 func func2op(v any) (op opcode, ok bool) {
 541     switch v.(type) {
 542     case func() float64:
 543         return call0, true
 544     case func(float64) float64:
 545         return call1, true
 546     case func(float64, float64) float64:
 547         return call2, true
 548     case func(float64, float64, float64) float64:
 549         return call3, true
 550     case func(float64, float64, float64, float64) float64:
 551         return call4, true
 552     case func(float64, float64, float64, float64, float64) float64:
 553         return call5, true
 554     case func(...float64) float64:
 555         return callv, true
 556     case func(float64, ...float64) float64:
 557         return call1v, true
 558     default:
 559         return 0, false
 560     }
 561 }
     File: ./fmscripts/compilers_test.go
   1 /*
   2 The MIT License (MIT)
   3 
   4 Copyright (c) 2026 pacman64
   5 
   6 Permission is hereby granted, free of charge, to any person obtaining a copy of
   7 this software and associated documentation files (the "Software"), to deal
   8 in the Software without restriction, including without limitation the rights to
   9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
  10 of the Software, and to permit persons to whom the Software is furnished to do
  11 so, subject to the following conditions:
  12 
  13 The above copyright notice and this permission notice shall be included in all
  14 copies or substantial portions of the Software.
  15 
  16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  22 SOFTWARE.
  23 */
  24 
  25 package fmscripts
  26 
  27 import (
  28     "testing"
  29 )
  30 
  31 func TestBuiltinFuncs(t *testing.T) {
  32     list := []map[string]any{
  33         determFuncs,
  34     }
  35 
  36     for _, kv := range list {
  37         for k, v := range kv {
  38             t.Run(k, func(t *testing.T) {
  39                 if !isSupportedFunc(v) {
  40                     t.Fatalf("%s: invalid function type %T", k, v)
  41                 }
  42             })
  43         }
  44     }
  45 }
  46 
  47 var mathCorrectnessTests = []struct {
  48     Name     string
  49     Script   string
  50     Expected float64
  51     Error    string
  52 }{
  53     {
  54         Name:     "value",
  55         Script:   "-45.2",
  56         Expected: -45.2,
  57         Error:    "",
  58     },
  59     {
  60         Name:     "simple",
  61         Script:   "1 + 3",
  62         Expected: 4,
  63         Error:    "",
  64     },
  65     {
  66         Name:     "fancier",
  67         Script:   "1*8 + 3*2**3",
  68         Expected: 32,
  69         Error:    "",
  70     },
  71     {
  72         Name:     "function call",
  73         Script:   "log2(1024)",
  74         Expected: 10,
  75         Error:    "",
  76     },
  77     {
  78         Name:     "function call (2 args)",
  79         Script:   "pow(3, 3)",
  80         Expected: 27,
  81         Error:    "",
  82     },
  83     {
  84         Name:     "function call (3 args)",
  85         Script:   "fma(3.5, 56, -1.52)",
  86         Expected: 194.48,
  87         Error:    "",
  88     },
  89     {
  90         Name:     "vararg-function call",
  91         Script:   "min(log10(10_000), log10(1_000_000), log2(4_096), 100 - 130)",
  92         Expected: -30,
  93         Error:    "",
  94     },
  95     {
  96         Name:     "square shortcut",
  97         Script:   "*-3",
  98         Expected: 9,
  99         Error:    "",
 100     },
 101     {
 102         Name:     "abs shortcut",
 103         Script:   "&-3",
 104         Expected: 3,
 105         Error:    "",
 106     },
 107     {
 108         Name:     "negative constant",
 109         Script:   "(-3)",
 110         Expected: -3,
 111         Error:    "",
 112     },
 113     {
 114         Name:     "power syntax",
 115         Script:   "2**4",
 116         Expected: 16,
 117         Error:    "",
 118     },
 119     {
 120         Name:     "power syntax order",
 121         Script:   "3*2**4",
 122         Expected: 48,
 123         Error:    "",
 124     },
 125     {
 126         Script:   "2*3**4",
 127         Expected: 162,
 128         Error:    "",
 129     },
 130     {
 131         Script:   "2**3*4",
 132         Expected: 32,
 133         Error:    "",
 134     },
 135     {
 136         Script:   "(2*3)**4",
 137         Expected: 1_296,
 138         Error:    "",
 139     },
 140     {
 141         Script:   "*2*2",
 142         Expected: 8,
 143         Error:    "",
 144     },
 145     {
 146         Script:   "2*2*2",
 147         Expected: 8,
 148         Error:    "",
 149     },
 150     {
 151         Script:   "3 == 3 ? 10 : -1",
 152         Expected: 10,
 153         Error:    "",
 154     },
 155     {
 156         Script:   "3 == 4 ? 10 : -1",
 157         Expected: -1,
 158         Error:    "",
 159     },
 160     {
 161         Script:   "log10(-1) ?? 4",
 162         Expected: 4,
 163         Error:    "",
 164     },
 165     {
 166         Script:   "log10(10) ?? 4",
 167         Expected: 1,
 168         Error:    "",
 169     },
 170     {
 171         Script:   "abc = 123; abc",
 172         Expected: 123,
 173         Error:    "",
 174     },
 175     {
 176         Name:     "calling func(float64, ...float64) float64",
 177         Script:   "horner(2.5, 1, 2, 3)",
 178         Expected: 14.25,
 179         Error:    "",
 180     },
 181 }
 182 
 183 func TestCompiler(t *testing.T) {
 184     for _, tc := range mathCorrectnessTests {
 185         name := tc.Name
 186         if len(name) == 0 {
 187             name = tc.Script
 188         }
 189 
 190         t.Run(name, func(t *testing.T) {
 191             var c Compiler
 192             p, err := c.Compile(tc.Script, map[string]any{"x": 1})
 193             if err != nil {
 194                 t.Fatalf("got compiler error %q", err.Error())
 195             }
 196 
 197             f := p.Run()
 198             if (err != nil || tc.Error != "") && err.Error() != tc.Error {
 199                 const fs = "expected error %q, got error %q instead"
 200                 t.Fatalf(fs, err.Error(), tc.Error)
 201             }
 202 
 203             if f != tc.Expected {
 204                 const fs = "expected result to be %f, got %f instead"
 205                 t.Fatalf(fs, tc.Expected, f)
 206             }
 207         })
 208     }
 209 }
     File: ./fmscripts/doc.go
   1 /*
   2 # Floating-point Math Scripts
   3 
   4 This self-contained package gives you a compiler/interpreter combo to run
   5 number-crunching scripts. These are limited to float64 values, but they run
   6 surprisingly quickly: various simple benchmarks suggest it's between 1/2 and
   7 1/4 the speed of native funcs.
   8 
   9 There are several built-in numeric functions, and the compiler makes it easy
  10 to add your custom Go functions to further speed-up any operations specific to
  11 your app/domain.
  12 
  13 Finally, notice how float64 data don't really limit you as much as you might
  14 think at first, since they can act as booleans (treating 0 as false, and non-0
  15 as true), or as exact integers in the extremely-wide range [-2**53, +2**53].
  16 */
  17 
  18 package fmscripts
     File: ./fmscripts/math.go
   1 /*
   2 The MIT License (MIT)
   3 
   4 Copyright (c) 2026 pacman64
   5 
   6 Permission is hereby granted, free of charge, to any person obtaining a copy of
   7 this software and associated documentation files (the "Software"), to deal
   8 in the Software without restriction, including without limitation the rights to
   9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
  10 of the Software, and to permit persons to whom the Software is furnished to do
  11 so, subject to the following conditions:
  12 
  13 The above copyright notice and this permission notice shall be included in all
  14 copies or substantial portions of the Software.
  15 
  16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  22 SOFTWARE.
  23 */
  24 
  25 package fmscripts
  26 
  27 import (
  28     "math"
  29 )
  30 
  31 const (
  32     // the maximum integer a float64 can represent exactly; -maxflint is the
  33     // minimum such integer, since floating-point values allow such symmetries
  34     maxflint = 2 << 52
  35 
  36     // base-2 versions of size multipliers
  37     kilobyte = 1024 * 1.0
  38     megabyte = 1024 * kilobyte
  39     gigabyte = 1024 * megabyte
  40     terabyte = 1024 * gigabyte
  41     petabyte = 1024 * terabyte
  42 
  43     // unit-conversion multipliers
  44     mi2kmMult   = 1 / 0.6213712
  45     nm2kmMult   = 1.852
  46     nmi2kmMult  = 1.852
  47     yd2mtMult   = 1 / 1.093613
  48     ft2mtMult   = 1 / 3.28084
  49     in2cmMult   = 2.54
  50     lb2kgMult   = 0.4535924
  51     ga2ltMult   = 1 / 0.2199692
  52     gal2lMult   = 1 / 0.2199692
  53     oz2mlMult   = 29.5735295625
  54     cup2lMult   = 0.2365882365
  55     mpg2kplMult = 0.2199692 / 0.6213712
  56     ton2kgMult  = 1 / 907.18474
  57     psi2paMult  = 1 / 0.00014503773800722
  58     deg2radMult = math.Pi / 180
  59     rad2degMult = 180 / math.Pi
  60 )
  61 
  62 // default math constants
  63 var mathConst = map[string]float64{
  64     "e":   math.E,
  65     "pi":  math.Pi,
  66     "tau": 2 * math.Pi,
  67     "phi": math.Phi,
  68     "nan": math.NaN(),
  69     "inf": math.Inf(+1),
  70 
  71     "minint":     -float64(maxflint - 1),
  72     "maxint":     +float64(maxflint - 1),
  73     "minsafeint": -float64(maxflint - 1),
  74     "maxsafeint": +float64(maxflint - 1),
  75 
  76     "false": 0.0,
  77     "true":  1.0,
  78     "f":     0.0,
  79     "t":     1.0,
  80 
  81     // conveniently-named multipliers
  82     "femto": 1e-15,
  83     "pico":  1e-12,
  84     "nano":  1e-09,
  85     "micro": 1e-06,
  86     "milli": 1e-03,
  87     "kilo":  1e+03,
  88     "mega":  1e+06,
  89     "giga":  1e+09,
  90     "tera":  1e+12,
  91     "peta":  1e+15,
  92 
  93     // unit-conversion multipliers
  94     "mi2km":   mi2kmMult,
  95     "nm2km":   nm2kmMult,
  96     "nmi2km":  nmi2kmMult,
  97     "yd2mt":   yd2mtMult,
  98     "ft2mt":   ft2mtMult,
  99     "in2cm":   in2cmMult,
 100     "lb2kg":   lb2kgMult,
 101     "ga2lt":   ga2ltMult,
 102     "gal2l":   gal2lMult,
 103     "oz2ml":   oz2mlMult,
 104     "cup2l":   cup2lMult,
 105     "mpg2kpl": mpg2kplMult,
 106     "ton2kg":  ton2kgMult,
 107     "psi2pa":  psi2paMult,
 108     "deg2rad": deg2radMult,
 109     "rad2deg": rad2degMult,
 110 
 111     // base-2 versions of size multipliers
 112     "kb":      kilobyte,
 113     "mb":      megabyte,
 114     "gb":      gigabyte,
 115     "tb":      terabyte,
 116     "pb":      petabyte,
 117     "binkilo": kilobyte,
 118     "binmega": megabyte,
 119     "bingiga": gigabyte,
 120     "bintera": terabyte,
 121     "binpeta": petabyte,
 122 
 123     // physical constants
 124     "c":   299_792_458,       // speed of light in m/s
 125     "g":   6.67430e-11,       // gravitational constant in N m2/kg2
 126     "h":   6.62607015e-34,    // planck constant in J s
 127     "ec":  1.602176634e-19,   // elementary charge in C
 128     "e0":  8.8541878128e-12,  // vacuum permittivity in C2/(Nm2)
 129     "mu0": 1.25663706212e-6,  // vacuum permeability in T m/A
 130     "k":   1.380649e-23,      // boltzmann constant in J/K
 131     "mu":  1.66053906660e-27, // atomic mass constant in kg
 132     "me":  9.1093837015e-31,  // electron mass in kg
 133     "mp":  1.67262192369e-27, // proton mass in kg
 134     "mn":  1.67492749804e-27, // neutron mass in kg
 135 
 136     // float64s can only vaguely approx. avogadro's mole (6.02214076e23)
 137 }
 138 
 139 // deterministic math functions lookup-table generated using the command
 140 //
 141 // go doc math | awk '/func/ { gsub(/func |\(.*/, ""); printf("\"%s\": math.%s,\n", tolower($0), $0) }'
 142 //
 143 // then hand-edited to remove funcs, or to use adapter funcs when needed: removed
 144 // funcs either had multiple returns (like SinCos) or dealt with float32 values
 145 var determFuncs = map[string]any{
 146     "abs":         math.Abs,
 147     "acos":        math.Acos,
 148     "acosh":       math.Acosh,
 149     "asin":        math.Asin,
 150     "asinh":       math.Asinh,
 151     "atan":        math.Atan,
 152     "atan2":       math.Atan2,
 153     "atanh":       math.Atanh,
 154     "cbrt":        math.Cbrt,
 155     "ceil":        math.Ceil,
 156     "copysign":    math.Copysign,
 157     "cos":         math.Cos,
 158     "cosh":        math.Cosh,
 159     "dim":         math.Dim,
 160     "erf":         math.Erf,
 161     "erfc":        math.Erfc,
 162     "erfcinv":     math.Erfcinv,
 163     "erfinv":      math.Erfinv,
 164     "exp":         math.Exp,
 165     "exp2":        math.Exp2,
 166     "expm1":       math.Expm1,
 167     "fma":         math.FMA,
 168     "floor":       math.Floor,
 169     "gamma":       math.Gamma,
 170     "inf":         inf,
 171     "isinf":       isInf,
 172     "isnan":       isNaN,
 173     "j0":          math.J0,
 174     "j1":          math.J1,
 175     "jn":          jn,
 176     "ldexp":       ldexp,
 177     "lgamma":      lgamma,
 178     "log10":       math.Log10,
 179     "log1p":       math.Log1p,
 180     "log2":        math.Log2,
 181     "logb":        math.Logb,
 182     "mod":         math.Mod,
 183     "nan":         math.NaN,
 184     "nextafter":   math.Nextafter,
 185     "pow":         math.Pow,
 186     "pow10":       pow10,
 187     "remainder":   math.Remainder,
 188     "round":       math.Round,
 189     "roundtoeven": math.RoundToEven,
 190     "signbit":     signbit,
 191     "sin":         math.Sin,
 192     "sinh":        math.Sinh,
 193     "sqrt":        math.Sqrt,
 194     "tan":         math.Tan,
 195     "tanh":        math.Tanh,
 196     "trunc":       math.Trunc,
 197     "y0":          math.Y0,
 198     "y1":          math.Y1,
 199     "yn":          yn,
 200 
 201     // a few aliases for the standard math funcs: some of the single-letter
 202     // aliases are named after the ones in `bc`, the basic calculator tool
 203     "a":          math.Abs,
 204     "c":          math.Cos,
 205     "ceiling":    math.Ceil,
 206     "cosine":     math.Cos,
 207     "e":          math.Exp,
 208     "isinf0":     isAnyInf,
 209     "isinfinite": isAnyInf,
 210     "l":          math.Log,
 211     "ln":         math.Log,
 212     "lg":         math.Log2,
 213     "modulus":    math.Mod,
 214     "power":      math.Pow,
 215     "rem":        math.Remainder,
 216     "s":          math.Sin,
 217     "sine":       math.Sin,
 218     "t":          math.Tan,
 219     "tangent":    math.Tan,
 220     "truncate":   math.Trunc,
 221     "truncated":  math.Trunc,
 222 
 223     // not from standard math: these custom funcs were added manually
 224     "beta":       beta,
 225     "bool":       num2bool,
 226     "clamp":      clamp,
 227     "cond":       cond, // vector-arg if-else chain
 228     "cube":       cube,
 229     "cubed":      cube,
 230     "degrees":    degrees,
 231     "deinf":      deInf,
 232     "denan":      deNaN,
 233     "factorial":  factorial,
 234     "fract":      fract,
 235     "horner":     polyval,
 236     "hypot":      hypot,
 237     "if":         ifElse,
 238     "ifelse":     ifElse,
 239     "inv":        reciprocal,
 240     "isanyinf":   isAnyInf,
 241     "isbad":      isBad,
 242     "isfin":      isFinite,
 243     "isfinite":   isFinite,
 244     "isgood":     isGood,
 245     "isinteger":  isInteger,
 246     "lbeta":      lnBeta,
 247     "len":        length,
 248     "length":     length,
 249     "lnbeta":     lnBeta,
 250     "log":        math.Log,
 251     "logistic":   logistic,
 252     "mag":        length,
 253     "max":        max,
 254     "min":        min,
 255     "neg":        negate,
 256     "negate":     negate,
 257     "not":        notBool,
 258     "polyval":    polyval,
 259     "radians":    radians,
 260     "range":      rangef, // vector-arg max - min
 261     "reciprocal": reciprocal,
 262     "rev":        revalue,
 263     "revalue":    revalue,
 264     "scale":      scale,
 265     "sgn":        sign,
 266     "sign":       sign,
 267     "sinc":       sinc,
 268     "sq":         sqr,
 269     "sqmin":      solveQuadMin,
 270     "sqmax":      solveQuadMax,
 271     "square":     sqr,
 272     "squared":    sqr,
 273     "unwrap":     unwrap,
 274     "wrap":       wrap,
 275 
 276     // a few aliases for the custom funcs
 277     "deg":   degrees,
 278     "isint": isInteger,
 279     "rad":   radians,
 280 
 281     // a few quicker 2-value versions of vararg funcs: the optimizer depends
 282     // on these to rewrite 2-input uses of their vararg counterparts
 283     "hypot2": math.Hypot,
 284     "max2":   math.Max,
 285     "min2":   math.Min,
 286 
 287     // a few entries to enable custom syntax: the parser depends on these to
 288     // rewrite binary expressions into func calls
 289     "??":  deNaN,
 290     "?:":  ifElse,
 291     "===": same,
 292     "!==": notSame,
 293 }
 294 
 295 // DefineDetFuncs adds more deterministic funcs to the default set. Such funcs
 296 // are considered optimizable, since calling them with the same constant inputs
 297 // is supposed to return the same constant outputs, as the name `deterministic`
 298 // suggests.
 299 //
 300 // Only call this before compiling any scripts, and ensure all funcs given are
 301 // supported and are deterministic. Random-output funcs certainly won't fit
 302 // the bill here.
 303 func DefineDetFuncs(funcs map[string]any) {
 304     for k, v := range funcs {
 305         determFuncs[k] = v
 306     }
 307 }
 308 
 309 func sqr(x float64) float64 {
 310     return x * x
 311 }
 312 
 313 func cube(x float64) float64 {
 314     return x * x * x
 315 }
 316 
 317 func num2bool(x float64) float64 {
 318     if x == 0 {
 319         return 0
 320     }
 321     return 1
 322 }
 323 
 324 func logistic(x float64) float64 {
 325     return 1 / (1 + math.Exp(-x))
 326 }
 327 
 328 func sign(x float64) float64 {
 329     if math.IsNaN(x) {
 330         return x
 331     }
 332     if x > 0 {
 333         return +1
 334     }
 335     if x < 0 {
 336         return -1
 337     }
 338     return 0
 339 }
 340 
 341 func sinc(x float64) float64 {
 342     if x == 0 {
 343         return 1
 344     }
 345     return math.Sin(x) / x
 346 }
 347 
 348 func isInteger(x float64) float64 {
 349     _, frac := math.Modf(x)
 350     if frac == 0 {
 351         return 1
 352     }
 353     return 0
 354 }
 355 
 356 func inf(sign float64) float64 {
 357     return math.Inf(int(sign))
 358 }
 359 
 360 func isInf(x float64, sign float64) float64 {
 361     if math.IsInf(x, int(sign)) {
 362         return 1
 363     }
 364     return 0
 365 }
 366 
 367 func isAnyInf(x float64) float64 {
 368     if math.IsInf(x, 0) {
 369         return 1
 370     }
 371     return 0
 372 }
 373 
 374 func isFinite(x float64) float64 {
 375     if math.IsInf(x, 0) {
 376         return 0
 377     }
 378     return 1
 379 }
 380 
 381 func isNaN(x float64) float64 {
 382     if math.IsNaN(x) {
 383         return 1
 384     }
 385     return 0
 386 }
 387 
 388 func isGood(x float64) float64 {
 389     if math.IsNaN(x) || math.IsInf(x, 0) {
 390         return 0
 391     }
 392     return 1
 393 }
 394 
 395 func isBad(x float64) float64 {
 396     if math.IsNaN(x) || math.IsInf(x, 0) {
 397         return 1
 398     }
 399     return 0
 400 }
 401 
 402 func same(x, y float64) float64 {
 403     if math.IsNaN(x) && math.IsNaN(y) {
 404         return 1
 405     }
 406     return debool(x == y)
 407 }
 408 
 409 func notSame(x, y float64) float64 {
 410     if math.IsNaN(x) && math.IsNaN(y) {
 411         return 0
 412     }
 413     return debool(x != y)
 414 }
 415 
 416 func deNaN(x float64, instead float64) float64 {
 417     if !math.IsNaN(x) {
 418         return x
 419     }
 420     return instead
 421 }
 422 
 423 func deInf(x float64, instead float64) float64 {
 424     if !math.IsInf(x, 0) {
 425         return x
 426     }
 427     return instead
 428 }
 429 
 430 func revalue(x float64, instead float64) float64 {
 431     if !math.IsNaN(x) && !math.IsInf(x, 0) {
 432         return x
 433     }
 434     return instead
 435 }
 436 
 437 func pow10(n float64) float64 {
 438     return math.Pow10(int(n))
 439 }
 440 
 441 func jn(n, x float64) float64 {
 442     return math.Jn(int(n), x)
 443 }
 444 
 445 func ldexp(frac, exp float64) float64 {
 446     return math.Ldexp(frac, int(exp))
 447 }
 448 
 449 func lgamma(x float64) float64 {
 450     y, s := math.Lgamma(x)
 451     if s < 0 {
 452         return math.NaN()
 453     }
 454     return y
 455 }
 456 
 457 func signbit(x float64) float64 {
 458     if math.Signbit(x) {
 459         return 1
 460     }
 461     return 0
 462 }
 463 
 464 func yn(n, x float64) float64 {
 465     return math.Yn(int(n), x)
 466 }
 467 
 468 func negate(x float64) float64 {
 469     return -x
 470 }
 471 
 472 func reciprocal(x float64) float64 {
 473     return 1 / x
 474 }
 475 
 476 func rangef(v ...float64) float64 {
 477     min := math.Inf(+1)
 478     max := math.Inf(-1)
 479     for _, f := range v {
 480         min = math.Min(min, f)
 481         max = math.Max(max, f)
 482     }
 483     return max - min
 484 }
 485 
 486 func cond(v ...float64) float64 {
 487     for {
 488         switch len(v) {
 489         case 0:
 490             // either no values are left, or no values were given at all
 491             return math.NaN()
 492 
 493         case 1:
 494             // either all previous conditions failed, and this last value is
 495             // automatically chosen, or only 1 value was given to begin with
 496             return v[0]
 497 
 498         default:
 499             // check condition: if true (non-0), return the value after it
 500             if v[0] != 0 {
 501                 return v[1]
 502             }
 503             // condition was false, so skip the leading pair of values
 504             v = v[2:]
 505         }
 506     }
 507 }
 508 
 509 func notBool(x float64) float64 {
 510     if x == 0 {
 511         return 1
 512     }
 513     return 0
 514 }
 515 
 516 func ifElse(cond float64, yes, no float64) float64 {
 517     if cond != 0 {
 518         return yes
 519     }
 520     return no
 521 }
 522 
 523 func lnGamma(x float64) float64 {
 524     y, s := math.Lgamma(x)
 525     if s < 0 {
 526         return math.NaN()
 527     }
 528     return y
 529 }
 530 
 531 func lnBeta(x float64, y float64) float64 {
 532     return lnGamma(x) + lnGamma(y) - lnGamma(x+y)
 533 }
 534 
 535 func beta(x float64, y float64) float64 {
 536     return math.Exp(lnBeta(x, y))
 537 }
 538 
 539 func factorial(n float64) float64 {
 540     return math.Round(math.Gamma(n + 1))
 541 }
 542 
 543 func degrees(rad float64) float64 {
 544     return rad2degMult * rad
 545 }
 546 
 547 func radians(deg float64) float64 {
 548     return deg2radMult * deg
 549 }
 550 
 551 func fract(x float64) float64 {
 552     return x - math.Floor(x)
 553 }
 554 
 555 func min(v ...float64) float64 {
 556     min := +math.Inf(+1)
 557     for _, f := range v {
 558         min = math.Min(min, f)
 559     }
 560     return min
 561 }
 562 
 563 func max(v ...float64) float64 {
 564     max := +math.Inf(-1)
 565     for _, f := range v {
 566         max = math.Max(max, f)
 567     }
 568     return max
 569 }
 570 
 571 func hypot(v ...float64) float64 {
 572     sumsq := 0.0
 573     for _, f := range v {
 574         sumsq += f * f
 575     }
 576     return math.Sqrt(sumsq)
 577 }
 578 
 579 func length(v ...float64) float64 {
 580     ss := 0.0
 581     for _, f := range v {
 582         ss += f * f
 583     }
 584     return math.Sqrt(ss)
 585 }
 586 
 587 // solveQuadMin finds the lowest solution of a 2nd-degree polynomial, using
 588 // a formula which is more accurate than the textbook one
 589 func solveQuadMin(a, b, c float64) float64 {
 590     disc := math.Sqrt(b*b - 4*a*c)
 591     r1 := 2 * c / (-b - disc)
 592     return r1
 593 }
 594 
 595 // solveQuadMax finds the highest solution of a 2nd-degree polynomial, using
 596 // a formula which is more accurate than the textbook one
 597 func solveQuadMax(a, b, c float64) float64 {
 598     disc := math.Sqrt(b*b - 4*a*c)
 599     r2 := 2 * c / (-b + disc)
 600     return r2
 601 }
 602 
 603 func wrap(x float64, min, max float64) float64 {
 604     return (x - min) / (max - min)
 605 }
 606 
 607 func unwrap(x float64, min, max float64) float64 {
 608     return (max-min)*x + min
 609 }
 610 
 611 func clamp(x float64, min, max float64) float64 {
 612     return math.Min(math.Max(x, min), max)
 613 }
 614 
 615 func scale(x float64, xmin, xmax, ymin, ymax float64) float64 {
 616     k := (x - xmin) / (xmax - xmin)
 617     return (ymax-ymin)*k + ymin
 618 }
 619 
 620 // polyval runs horner's algorithm on a value, with the polynomial coefficients
 621 // given after it, higher-order first
 622 func polyval(x float64, v ...float64) float64 {
 623     if len(v) == 0 {
 624         return 0
 625     }
 626 
 627     x0 := x
 628     x = 1.0
 629     y := 0.0
 630     for i := len(v) - 1; i >= 0; i-- {
 631         y += v[i] * x
 632         x *= x0
 633     }
 634     return y
 635 }
     File: ./fmscripts/math_test.go
   1 /*
   2 The MIT License (MIT)
   3 
   4 Copyright (c) 2026 pacman64
   5 
   6 Permission is hereby granted, free of charge, to any person obtaining a copy of
   7 this software and associated documentation files (the "Software"), to deal
   8 in the Software without restriction, including without limitation the rights to
   9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
  10 of the Software, and to permit persons to whom the Software is furnished to do
  11 so, subject to the following conditions:
  12 
  13 The above copyright notice and this permission notice shall be included in all
  14 copies or substantial portions of the Software.
  15 
  16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  22 SOFTWARE.
  23 */
  24 
  25 package fmscripts
  26 
  27 import "testing"
  28 
  29 func TestTables(t *testing.T) {
  30     for k, v := range determFuncs {
  31         t.Run(k, func(t *testing.T) {
  32             if !isSupportedFunc(v) {
  33                 t.Fatalf("unsupported func of type %T", v)
  34             }
  35         })
  36     }
  37 }
     File: ./fmscripts/optimizers.go
   1 /*
   2 The MIT License (MIT)
   3 
   4 Copyright (c) 2026 pacman64
   5 
   6 Permission is hereby granted, free of charge, to any person obtaining a copy of
   7 this software and associated documentation files (the "Software"), to deal
   8 in the Software without restriction, including without limitation the rights to
   9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
  10 of the Software, and to permit persons to whom the Software is furnished to do
  11 so, subject to the following conditions:
  12 
  13 The above copyright notice and this permission notice shall be included in all
  14 copies or substantial portions of the Software.
  15 
  16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  22 SOFTWARE.
  23 */
  24 
  25 package fmscripts
  26 
  27 import (
  28     "math"
  29 )
  30 
  31 // unary2func matches unary operators to funcs the optimizer can use to eval
  32 // constant-input unary expressions into their results
  33 var unary2func = map[string]func(x float64) float64{
  34     // avoid unary +, since it's a no-op and thus there's no instruction for
  35     // it, which in turn makes unit-tests for these optimization tables fail
  36     // unnecessarily
  37     // "+": func(x float64) float64 { return +x },
  38 
  39     "-": func(x float64) float64 { return -x },
  40     "!": func(x float64) float64 { return debool(x == 0) },
  41     "&": func(x float64) float64 { return math.Abs(x) },
  42     "*": func(x float64) float64 { return x * x },
  43     "^": func(x float64) float64 { return x * x },
  44     "/": func(x float64) float64 { return 1 / x },
  45 }
  46 
  47 // binary2func matches binary operators to funcs the optimizer can use to eval
  48 // constant-input binary expressions into their results
  49 var binary2func = map[string]func(x, y float64) float64{
  50     "+":  func(x, y float64) float64 { return x + y },
  51     "-":  func(x, y float64) float64 { return x - y },
  52     "*":  func(x, y float64) float64 { return x * y },
  53     "/":  func(x, y float64) float64 { return x / y },
  54     "%":  func(x, y float64) float64 { return math.Mod(x, y) },
  55     "&":  func(x, y float64) float64 { return debool(x != 0 && y != 0) },
  56     "&&": func(x, y float64) float64 { return debool(x != 0 && y != 0) },
  57     "|":  func(x, y float64) float64 { return debool(x != 0 || y != 0) },
  58     "||": func(x, y float64) float64 { return debool(x != 0 || y != 0) },
  59     "==": func(x, y float64) float64 { return debool(x == y) },
  60     "!=": func(x, y float64) float64 { return debool(x != y) },
  61     "<>": func(x, y float64) float64 { return debool(x != y) },
  62     "<":  func(x, y float64) float64 { return debool(x < y) },
  63     "<=": func(x, y float64) float64 { return debool(x <= y) },
  64     ">":  func(x, y float64) float64 { return debool(x > y) },
  65     ">=": func(x, y float64) float64 { return debool(x >= y) },
  66     "**": func(x, y float64) float64 { return math.Pow(x, y) },
  67     "^":  func(x, y float64) float64 { return math.Pow(x, y) },
  68 }
  69 
  70 // func2unary turns built-in func names into built-in unary operators
  71 var func2unary = map[string]string{
  72     "a":          "&",
  73     "abs":        "&",
  74     "inv":        "/",
  75     "neg":        "-",
  76     "negate":     "-",
  77     "reciprocal": "/",
  78     "sq":         "*",
  79     "square":     "*",
  80     "squared":    "*",
  81 }
  82 
  83 // func2binary turns built-in func names into built-in binary operators
  84 var func2binary = map[string]string{
  85     "mod":     "%",
  86     "modulus": "%",
  87     "pow":     "**",
  88     "power":   "**",
  89 }
  90 
  91 // vararg2func2 matches variable-argument funcs to their 2-argument versions
  92 var vararg2func2 = map[string]string{
  93     "hypot": "hypot2",
  94     "max":   "max2",
  95     "min":   "min2",
  96 }
  97 
  98 // optimize tries to simplify the expression given as much as possible, by
  99 // simplifying constants whenever possible, and exploiting known built-in
 100 // funcs which are known to behave deterministically
 101 func (c *Compiler) optimize(expr any) any {
 102     switch expr := expr.(type) {
 103     case []any:
 104         return c.optimizeCombo(expr)
 105 
 106     case unaryExpr:
 107         return c.optimizeUnaryExpr(expr)
 108 
 109     case binaryExpr:
 110         return c.optimizeBinaryExpr(expr)
 111 
 112     case callExpr:
 113         return c.optimizeCallExpr(expr)
 114 
 115     case assignExpr:
 116         expr.expr = c.optimize(expr.expr)
 117         return expr
 118 
 119     default:
 120         f, ok := c.tryConstant(expr)
 121         if ok {
 122             return f
 123         }
 124         return expr
 125     }
 126 }
 127 
 128 // optimizeCombo handles a sequence of expressions for the optimizer
 129 func (c *Compiler) optimizeCombo(exprs []any) any {
 130     if len(exprs) == 1 {
 131         return c.optimize(exprs[0])
 132     }
 133 
 134     // count how many expressions are considered useful: these are
 135     // assignments as well as the last expression, since that's what
 136     // determines the script's final result
 137     useful := 0
 138     for i, v := range exprs {
 139         _, ok := v.(assignExpr)
 140         if ok || i == len(exprs)-1 {
 141             useful++
 142         }
 143     }
 144 
 145     // ignore all expressions which are a waste of time, and optimize
 146     // all other expressions
 147     res := make([]any, 0, useful)
 148     for i, v := range exprs {
 149         _, ok := v.(assignExpr)
 150         if ok || i == len(exprs)-1 {
 151             res = append(res, c.optimize(v))
 152         }
 153     }
 154     return res
 155 }
 156 
 157 // optimizeUnaryExpr handles unary expressions for the optimizer
 158 func (c *Compiler) optimizeUnaryExpr(expr unaryExpr) any {
 159     // recursively optimize input
 160     expr.x = c.optimize(expr.x)
 161 
 162     // optimize unary ops on a constant into concrete values
 163     if x, ok := expr.x.(float64); ok {
 164         if fn, ok := unary2func[expr.op]; ok {
 165             return fn(x)
 166         }
 167     }
 168 
 169     switch expr.op {
 170     case "+":
 171         // unary plus is an identity operation
 172         return expr.x
 173 
 174     default:
 175         return expr
 176     }
 177 }
 178 
 179 // optimizeBinaryExpr handles binary expressions for the optimizer
 180 func (c *Compiler) optimizeBinaryExpr(expr binaryExpr) any {
 181     // recursively optimize inputs
 182     expr.x = c.optimize(expr.x)
 183     expr.y = c.optimize(expr.y)
 184 
 185     // optimize binary ops on 2 constants into concrete values
 186     if x, ok := expr.x.(float64); ok {
 187         if y, ok := expr.y.(float64); ok {
 188             if fn, ok := binary2func[expr.op]; ok {
 189                 return fn(x, y)
 190             }
 191         }
 192     }
 193 
 194     switch expr.op {
 195     case "+":
 196         if expr.x == 0.0 {
 197             // 0+y -> y
 198             return expr.y
 199         }
 200         if expr.y == 0.0 {
 201             // x+0 -> x
 202             return expr.x
 203         }
 204 
 205     case "-":
 206         if expr.x == 0.0 {
 207             // 0-y -> -y
 208             return c.optimizeUnaryExpr(unaryExpr{op: "-", x: expr.y})
 209         }
 210         if expr.y == 0.0 {
 211             // x-0 -> x
 212             return expr.x
 213         }
 214 
 215     case "*":
 216         if expr.x == 0.0 || expr.y == 0.0 {
 217             // 0*y -> 0
 218             // x*0 -> 0
 219             return 0.0
 220         }
 221         if expr.x == 1.0 {
 222             // 1*y -> y
 223             return expr.y
 224         }
 225         if expr.y == 1.0 {
 226             // x*1 -> x
 227             return expr.x
 228         }
 229         if expr.x == -1.0 {
 230             // -1*y -> -y
 231             return c.optimizeUnaryExpr(unaryExpr{op: "-", x: expr.y})
 232         }
 233         if expr.y == -1.0 {
 234             // x*-1 -> -x
 235             return c.optimizeUnaryExpr(unaryExpr{op: "-", x: expr.x})
 236         }
 237 
 238     case "/":
 239         if expr.x == 1.0 {
 240             // 1/y -> reciprocal of y
 241             return c.optimizeUnaryExpr(unaryExpr{op: "/", x: expr.y})
 242         }
 243         if expr.y == 1.0 {
 244             // x/1 -> x
 245             return expr.x
 246         }
 247         if expr.y == -1.0 {
 248             // x/-1 -> -x
 249             return c.optimizeUnaryExpr(unaryExpr{op: "-", x: expr.x})
 250         }
 251 
 252     case "**":
 253         switch expr.y {
 254         case -1.0:
 255             // x**-1 -> 1/x, reciprocal of x
 256             return c.optimizeUnaryExpr(unaryExpr{op: "/", x: expr.x})
 257         case 0.0:
 258             // x**0 -> 1
 259             return 1.0
 260         case 1.0:
 261             // x**1 -> x
 262             return expr.x
 263         case 2.0:
 264             // x**2 -> *x
 265             return c.optimizeUnaryExpr(unaryExpr{op: "*", x: expr.x})
 266         case 3.0:
 267             // x**3 -> *x*x
 268             sq := unaryExpr{op: "*", x: expr.x}
 269             return c.optimizeBinaryExpr(binaryExpr{op: "*", x: sq, y: expr.x})
 270         }
 271 
 272     case "&", "&&":
 273         if expr.x == 0.0 || expr.y == 0.0 {
 274             // 0 && y -> 0
 275             // x && 0 -> 0
 276             return 0.0
 277         }
 278     }
 279 
 280     // no simplifiable patterns were detected
 281     return expr
 282 }
 283 
 284 // optimizeCallExpr optimizes special cases of built-in func calls
 285 func (c *Compiler) optimizeCallExpr(call callExpr) any {
 286     // recursively optimize all inputs, and keep track if they're all
 287     // constants in the end
 288     numlit := 0
 289     for i, v := range call.args {
 290         v = c.optimize(v)
 291         call.args[i] = v
 292         if _, ok := v.(float64); ok {
 293             numlit++
 294         }
 295     }
 296 
 297     // if func is overridden, there's no guarantee the new func works the same
 298     if _, ok := determFuncs[call.name]; c.ftype[call.name] != nil || !ok {
 299         return call
 300     }
 301 
 302     // from this point on, func is guaranteed to be built-in and deterministic
 303 
 304     // handle all-const inputs, by calling func and using its result
 305     if numlit == len(call.args) {
 306         in := make([]float64, 0, len(call.args))
 307         for _, v := range call.args {
 308             f, _ := v.(float64)
 309             in = append(in, f)
 310         }
 311 
 312         if f, ok := tryCall(determFuncs[call.name], in); ok {
 313             return f
 314         }
 315     }
 316 
 317     switch len(call.args) {
 318     case 1:
 319         if op, ok := func2unary[call.name]; ok {
 320             expr := unaryExpr{op: op, x: call.args[0]}
 321             return c.optimizeUnaryExpr(expr)
 322         }
 323         return call
 324 
 325     case 2:
 326         if op, ok := func2binary[call.name]; ok {
 327             expr := binaryExpr{op: op, x: call.args[0], y: call.args[1]}
 328             return c.optimizeBinaryExpr(expr)
 329         }
 330         if name, ok := vararg2func2[call.name]; ok {
 331             call.name = name
 332             return call
 333         }
 334         return call
 335 
 336     default:
 337         return call
 338     }
 339 }
 340 
 341 // tryConstant tries to optimize the expression given into a constant
 342 func (c *Compiler) tryConstant(expr any) (value float64, ok bool) {
 343     switch expr := expr.(type) {
 344     case float64:
 345         return expr, true
 346 
 347     case string:
 348         if _, ok := c.vaddr[expr]; !ok {
 349             // name isn't explicitly defined
 350             if f, ok := mathConst[expr]; ok {
 351                 // and is a known math constant
 352                 return f, true
 353             }
 354         }
 355         return 0, false
 356 
 357     default:
 358         return 0, false
 359     }
 360 }
 361 
 362 // tryCall tries to simplify the function expression given into a constant
 363 func tryCall(fn any, in []float64) (value float64, ok bool) {
 364     switch fn := fn.(type) {
 365     case func(float64) float64:
 366         if len(in) == 1 {
 367             return fn(in[0]), true
 368         }
 369         return 0, false
 370 
 371     case func(float64, float64) float64:
 372         if len(in) == 2 {
 373             return fn(in[0], in[1]), true
 374         }
 375         return 0, false
 376 
 377     case func(float64, float64, float64) float64:
 378         if len(in) == 3 {
 379             return fn(in[0], in[1], in[2]), true
 380         }
 381         return 0, false
 382 
 383     case func(float64, float64, float64, float64) float64:
 384         if len(in) == 4 {
 385             return fn(in[0], in[1], in[2], in[3]), true
 386         }
 387         return 0, false
 388 
 389     case func(float64, float64, float64, float64, float64) float64:
 390         if len(in) == 5 {
 391             return fn(in[0], in[1], in[2], in[3], in[4]), true
 392         }
 393         return 0, false
 394 
 395     case func(...float64) float64:
 396         return fn(in...), true
 397 
 398     case func(float64, ...float64) float64:
 399         if len(in) >= 1 {
 400             return fn(in[0], in[1:]...), true
 401         }
 402         return 0, false
 403 
 404     default:
 405         // type isn't a supported func
 406         return 0, false
 407     }
 408 }
     File: ./fmscripts/optimizers_test.go
   1 /*
   2 The MIT License (MIT)
   3 
   4 Copyright (c) 2026 pacman64
   5 
   6 Permission is hereby granted, free of charge, to any person obtaining a copy of
   7 this software and associated documentation files (the "Software"), to deal
   8 in the Software without restriction, including without limitation the rights to
   9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
  10 of the Software, and to permit persons to whom the Software is furnished to do
  11 so, subject to the following conditions:
  12 
  13 The above copyright notice and this permission notice shall be included in all
  14 copies or substantial portions of the Software.
  15 
  16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  22 SOFTWARE.
  23 */
  24 
  25 package fmscripts
  26 
  27 import (
  28     "math"
  29     "reflect"
  30     "testing"
  31 )
  32 
  33 func TestOptimizerTables(t *testing.T) {
  34     for k := range unary2func {
  35         t.Run(k, func(t *testing.T) {
  36             if _, ok := unary2op[k]; !ok {
  37                 t.Fatalf("missing unary constant optimizer for %q", k)
  38             }
  39         })
  40     }
  41 
  42     for k := range binary2func {
  43         t.Run(k, func(t *testing.T) {
  44             if _, ok := binary2op[k]; !ok {
  45                 t.Fatalf("missing binary constant optimizer for %q", k)
  46             }
  47         })
  48     }
  49 
  50     for k, name := range func2unary {
  51         t.Run(k, func(t *testing.T) {
  52             if _, ok := determFuncs[k]; !ok {
  53                 const fs = "func(x) optimizer %q has no matching built-in func"
  54                 t.Fatalf(fs, k)
  55             }
  56 
  57             if _, ok := unary2op[name]; !ok {
  58                 t.Fatalf("missing unary func optimizer for %q", k)
  59             }
  60         })
  61     }
  62 
  63     for k, name := range func2binary {
  64         t.Run(k, func(t *testing.T) {
  65             if _, ok := determFuncs[k]; !ok {
  66                 const fs = "func(x, y) optimizer %q has no matching built-in func"
  67                 t.Fatalf(fs, k)
  68             }
  69 
  70             if _, ok := binary2op[name]; !ok {
  71                 t.Fatalf("missing binary func optimizer for %q", k)
  72             }
  73         })
  74     }
  75 
  76     for k := range vararg2func2 {
  77         t.Run(k, func(t *testing.T) {
  78             if _, ok := determFuncs[k]; !ok {
  79                 const fs = "vararg optimizer %q has no matching built-in func"
  80                 t.Fatalf(fs, k)
  81             }
  82         })
  83     }
  84 }
  85 
  86 func TestOptimizer(t *testing.T) {
  87     var tests = []struct {
  88         Source   string
  89         Expected any
  90     }{
  91         {"1", 1.0},
  92         {"3+4*5", 23.0},
  93 
  94         {"e", math.E},
  95         {"pi", math.Pi},
  96         {"phi", math.Phi},
  97         {"2*pi", 2 * math.Pi},
  98         {"4.51*phi-14.23564", 4.51*math.Phi - 14.23564},
  99         {"-e", -math.E},
 100 
 101         {"exp(2*pi)", math.Exp(2 * math.Pi)},
 102         {"log(2342.55) / log(43.21)", math.Log(2342.55) / math.Log(43.21)},
 103         {"f(3)", callExpr{name: "f", args: []any{3.0}}},
 104         {"min(3, 2, -1.5)", -1.5},
 105 
 106         {"hypot(x, 4)", callExpr{name: "hypot2", args: []any{"x", 4.0}}},
 107         {"max(x, 4)", callExpr{name: "max2", args: []any{"x", 4.0}}},
 108         {"min(x, 4)", callExpr{name: "min2", args: []any{"x", 4.0}}},
 109 
 110         {"rand()", callExpr{name: "rand"}},
 111 
 112         {
 113             "sin(2_000 * x * tau * x)",
 114             callExpr{
 115                 name: "sin",
 116                 args: []any{
 117                     binaryExpr{
 118                         "*",
 119                         binaryExpr{
 120                             "*",
 121                             binaryExpr{"*", 2_000.0, "x"},
 122                             2 * math.Pi,
 123                         },
 124                         "x",
 125                     },
 126                 },
 127             },
 128         },
 129 
 130         {
 131             "sin(10 * tau * exp(-20 * x)) * exp(-2 * x)",
 132             binaryExpr{
 133                 "*",
 134                 // sin(...)
 135                 callExpr{
 136                     name: "sin",
 137                     args: []any{
 138                         // 10 * tau * exp(...)
 139                         binaryExpr{
 140                             "*",
 141                             10 * 2 * math.Pi,
 142                             // exp(-20 * x)
 143                             callExpr{
 144                                 name: "exp",
 145                                 args: []any{
 146                                     binaryExpr{"*", -20.0, "x"},
 147                                 },
 148                             },
 149                         },
 150                     },
 151                 },
 152                 // exp(-2 * x)
 153                 callExpr{
 154                     name: "exp",
 155                     args: []any{binaryExpr{"*", -2.0, "x"}},
 156                 },
 157             },
 158         },
 159     }
 160 
 161     defs := map[string]any{
 162         "x": 3.5,
 163         "f": math.Exp,
 164     }
 165 
 166     for _, tc := range tests {
 167         t.Run(tc.Source, func(t *testing.T) {
 168             var c Compiler
 169             root, err := parse(tc.Source)
 170             if err != nil {
 171                 t.Fatal(err)
 172                 return
 173             }
 174 
 175             if err := c.reset(defs); err != nil {
 176                 t.Fatal(err)
 177                 return
 178             }
 179 
 180             got := c.optimize(root)
 181             if !reflect.DeepEqual(got, tc.Expected) {
 182                 const fs = "expected result to be\n%#v\ninstead of\n%#v"
 183                 t.Fatalf(fs, tc.Expected, got)
 184                 return
 185             }
 186         })
 187     }
 188 }
     File: ./fmscripts/parsing.go
   1 /*
   2 The MIT License (MIT)
   3 
   4 Copyright (c) 2026 pacman64
   5 
   6 Permission is hereby granted, free of charge, to any person obtaining a copy of
   7 this software and associated documentation files (the "Software"), to deal
   8 in the Software without restriction, including without limitation the rights to
   9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
  10 of the Software, and to permit persons to whom the Software is furnished to do
  11 so, subject to the following conditions:
  12 
  13 The above copyright notice and this permission notice shall be included in all
  14 copies or substantial portions of the Software.
  15 
  16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  22 SOFTWARE.
  23 */
  24 
  25 package fmscripts
  26 
  27 import (
  28     "errors"
  29     "fmt"
  30     "strconv"
  31     "strings"
  32 )
  33 
  34 // parse turns source code into an expression type interpreters can use.
  35 func parse(src string) (any, error) {
  36     tok := newTokenizer(src)
  37     par, err := newParser(&tok)
  38     if err != nil {
  39         return nil, err
  40     }
  41 
  42     v, err := par.parse()
  43     err = par.improveError(err, src)
  44     return v, err
  45 }
  46 
  47 // pickLine slices a string, picking one of its lines via a 1-based index:
  48 // func improveError uses it to isolate the line of code an error came from
  49 func pickLine(src string, linenum int) string {
  50     // skip the lines before the target one
  51     for i := 0; i < linenum && len(src) > 0; i++ {
  52         j := strings.IndexByte(src, '\n')
  53         if j < 0 {
  54             break
  55         }
  56         src = src[j+1:]
  57     }
  58 
  59     // limit leftover to a single line
  60     i := strings.IndexByte(src, '\n')
  61     if i >= 0 {
  62         return src[:i]
  63     }
  64     return src
  65 }
  66 
  67 // unaryExpr is a unary expression
  68 type unaryExpr struct {
  69     op string
  70     x  any
  71 }
  72 
  73 // binaryExpr is a binary expression
  74 type binaryExpr struct {
  75     op string
  76     x  any
  77     y  any
  78 }
  79 
  80 // callExpr is a function call and its arguments
  81 type callExpr struct {
  82     name string
  83     args []any
  84 }
  85 
  86 // assignExpr is a value/variable assignment
  87 type assignExpr struct {
  88     name string
  89     expr any
  90 }
  91 
  92 // parser is a parser for JavaScript-like syntax, limited to operations on
  93 // 64-bit floating-point numbers
  94 type parser struct {
  95     tokens []token
  96     line   int
  97     pos    int
  98     toklen int
  99 }
 100 
 101 // newParser is the constructor for type parser
 102 func newParser(t *tokenizer) (parser, error) {
 103     var p parser
 104 
 105     // get all tokens from the source code
 106     for {
 107         v, err := t.next()
 108         if v.kind != unknownToken {
 109             p.tokens = append(p.tokens, v)
 110         }
 111 
 112         if err == errEOS {
 113             // done scanning/tokenizing
 114             return p, nil
 115         }
 116 
 117         if err != nil {
 118             // handle actual errors
 119             return p, err
 120         }
 121     }
 122 }
 123 
 124 // improveError adds more info about where exactly in the source code an error
 125 // came from, thus making error messages much more useful
 126 func (p *parser) improveError(err error, src string) error {
 127     if err == nil {
 128         return nil
 129     }
 130 
 131     line := pickLine(src, p.line)
 132     if len(line) == 0 || p.pos < 1 {
 133         const fs = "(line %d: pos %d): %w"
 134         return fmt.Errorf(fs, p.line, p.pos, err)
 135     }
 136 
 137     ptr := strings.Repeat(" ", p.pos) + "^"
 138     const fs = "(line %d: pos %d): %w\n%s\n%s"
 139     return fmt.Errorf(fs, p.line, p.pos, err, line, ptr)
 140 }
 141 
 142 // parse tries to turn tokens into a compilable abstract syntax tree, and is
 143 // the parser's entry point
 144 func (p *parser) parse() (any, error) {
 145     // source codes with no tokens are always errors
 146     if len(p.tokens) == 0 {
 147         const msg = "source code is empty, or has no useful expressions"
 148         return nil, errors.New(msg)
 149     }
 150 
 151     // handle optional excel-like leading equal sign
 152     p.acceptSyntax(`=`)
 153 
 154     // ignore trailing semicolons
 155     for len(p.tokens) > 0 {
 156         t := p.tokens[len(p.tokens)-1]
 157         if t.kind != syntaxToken || t.value != `;` {
 158             break
 159         }
 160         p.tokens = p.tokens[:len(p.tokens)-1]
 161     }
 162 
 163     // handle single expressions as well as multiple semicolon-separated
 164     // expressions: the latter allow value assignments/updates in scripts
 165     var res []any
 166     for keepGoing := true; keepGoing && len(p.tokens) > 0; {
 167         v, err := p.parseExpression()
 168         if err != nil && err != errEOS {
 169             return v, err
 170         }
 171 
 172         res = append(res, v)
 173         // handle optional separator/continuation semicolons
 174         _, keepGoing = p.acceptSyntax(`;`)
 175     }
 176 
 177     // unexpected unparsed trailing tokens are always errors; any trailing
 178     // semicolons in the original script are already trimmed
 179     if len(p.tokens) > 0 {
 180         const fs = "unexpected %s"
 181         return res, fmt.Errorf(fs, p.tokens[0].value)
 182     }
 183 
 184     // make scripts ending in an assignment also load that value, so they're
 185     // useful, as assignments result in no useful value by themselves
 186     assign, ok := res[len(res)-1].(assignExpr)
 187     if ok {
 188         res = append(res, assign.name)
 189     }
 190 
 191     // turn 1-item combo expressions into their only expression: some
 192     // unit tests may rely on that for convenience
 193     if len(res) == 1 {
 194         return res[0], nil
 195     }
 196     return res, nil
 197 }
 198 
 199 // acceptSyntax advances the parser on the first syntactic string matched:
 200 // notice any number of alternatives/options are allowed, as the syntax
 201 // allows/requires at various points
 202 func (p *parser) acceptSyntax(syntax ...string) (match string, ok bool) {
 203     if len(p.tokens) == 0 {
 204         return "", false
 205     }
 206 
 207     t := p.tokens[0]
 208     if t.kind != syntaxToken {
 209         return "", false
 210     }
 211 
 212     for _, s := range syntax {
 213         if t.value == s {
 214             p.advance()
 215             return s, true
 216         }
 217     }
 218     return "", false
 219 }
 220 
 221 // advance skips the current leading token, if there are still any left
 222 func (p *parser) advance() {
 223     if len(p.tokens) == 0 {
 224         return
 225     }
 226 
 227     t := p.tokens[0]
 228     p.tokens = p.tokens[1:]
 229     p.line = t.line
 230     p.pos = t.pos
 231     p.toklen = len(t.value)
 232 }
 233 
 234 // acceptNumeric advances the parser on a numeric value, but only if it's
 235 // the leading token: conversely, any other type of token doesn't advance
 236 // the parser; when matches happen the resulting strings need parsing via
 237 // func parseNumber
 238 func (p *parser) acceptNumeric() (numliteral string, ok bool) {
 239     if len(p.tokens) == 0 {
 240         return "", false
 241     }
 242 
 243     t := p.tokens[0]
 244     if t.kind == numberToken {
 245         p.advance()
 246         return t.value, true
 247     }
 248     return "", false
 249 }
 250 
 251 // demandSyntax imposes a specific syntactic element to follow, or else it's
 252 // an error
 253 func (p *parser) demandSyntax(syntax string) error {
 254     if len(p.tokens) == 0 {
 255         return fmt.Errorf("expected %s instead of the end of source", syntax)
 256     }
 257 
 258     first := p.tokens[0]
 259     if first.kind == syntaxToken && first.value == syntax {
 260         p.advance()
 261         return nil
 262     }
 263     return fmt.Errorf("expected %s instead of %s", syntax, first.value)
 264 }
 265 
 266 func (p *parser) parseExpression() (any, error) {
 267     x, err := p.parseComparison()
 268     if err != nil {
 269         return x, err
 270     }
 271 
 272     // handle assignment statements
 273     if _, ok := p.acceptSyntax(`=`, `:=`); ok {
 274         varname, ok := x.(string)
 275         if !ok {
 276             const fs = "expected a variable name, instead of a %T"
 277             return nil, fmt.Errorf(fs, x)
 278         }
 279 
 280         x, err := p.parseExpression()
 281         expr := assignExpr{name: varname, expr: x}
 282         return expr, err
 283     }
 284 
 285     // handle and/or logical chains
 286     for {
 287         if op, ok := p.acceptSyntax(`&&`, `||`, `&`, `|`); ok {
 288             y, err := p.parseExpression()
 289             if err != nil {
 290                 return y, err
 291             }
 292             x = binaryExpr{op: op, x: x, y: y}
 293             continue
 294         }
 295         break
 296     }
 297 
 298     // handle maybe-properties
 299     if _, ok := p.acceptSyntax(`??`); ok {
 300         y, err := p.parseExpression()
 301         expr := callExpr{name: "??", args: []any{x, y}}
 302         return expr, err
 303     }
 304 
 305     // handle choice/ternary operator
 306     if _, ok := p.acceptSyntax(`?`); ok {
 307         y, err := p.parseExpression()
 308         if err != nil {
 309             expr := callExpr{name: "?:", args: []any{x, nil, nil}}
 310             return expr, err
 311         }
 312 
 313         if _, ok := p.acceptSyntax(`:`); ok {
 314             z, err := p.parseExpression()
 315             expr := callExpr{name: "?:", args: []any{x, y, z}}
 316             return expr, err
 317         }
 318 
 319         if len(p.tokens) == 0 {
 320             expr := callExpr{name: "?:", args: []any{x, y, nil}}
 321             return expr, errors.New("expected `:`")
 322         }
 323 
 324         s := p.tokens[0].value
 325         expr := callExpr{name: "?:", args: []any{x, y, nil}}
 326         err = fmt.Errorf("expected `:`, but got %q instead", s)
 327         return expr, err
 328     }
 329 
 330     // expression was just a comparison, or simpler
 331     return x, nil
 332 }
 333 
 334 func (p *parser) parseComparison() (any, error) {
 335     x, err := p.parseTerm()
 336     if err != nil {
 337         return x, err
 338     }
 339 
 340     op, ok := p.acceptSyntax(`==`, `!=`, `<`, `>`, `<=`, `>=`, `<>`, `===`, `!==`)
 341     if ok {
 342         y, err := p.parseTerm()
 343         return binaryExpr{op: op, x: x, y: y}, err
 344     }
 345     return x, err
 346 }
 347 
 348 // parseBinary handles binary operations, by recursing depth-first on the left
 349 // side of binary expressions; going tail-recursive on these would reverse the
 350 // order of arguments instead, which is obviously wrong
 351 func (p *parser) parseBinary(parse func() (any, error), syntax ...string) (any, error) {
 352     x, err := parse()
 353     if err != nil {
 354         return x, err
 355     }
 356 
 357     for {
 358         op, ok := p.acceptSyntax(syntax...)
 359         if !ok {
 360             return x, nil
 361         }
 362 
 363         y, err := parse()
 364         x = binaryExpr{op: op, x: x, y: y}
 365         if err != nil {
 366             return x, err
 367         }
 368     }
 369 }
 370 
 371 func (p *parser) parseTerm() (any, error) {
 372     return p.parseBinary(func() (any, error) {
 373         return p.parseProduct()
 374     }, `+`, `-`, `^`)
 375 }
 376 
 377 func (p *parser) parseProduct() (any, error) {
 378     return p.parseBinary(func() (any, error) {
 379         return p.parsePower()
 380     }, `*`, `/`, `%`)
 381 }
 382 
 383 func (p *parser) parsePower() (any, error) {
 384     return p.parseBinary(func() (any, error) {
 385         return p.parseValue()
 386     }, `**`, `^`)
 387 }
 388 
 389 func (p *parser) parseValue() (any, error) {
 390     // handle unary operators which can also be considered part of numeric
 391     // literals, and thus should be simplified away
 392     if op, ok := p.acceptSyntax(`+`, `-`); ok {
 393         if s, ok := p.acceptNumeric(); ok {
 394             x, err := strconv.ParseFloat(s, 64)
 395             if err != nil {
 396                 return nil, err
 397             }
 398             if simpler, ok := simplifyNumber(op, x); ok {
 399                 return simpler, nil
 400             }
 401             return unaryExpr{op: op, x: x}, nil
 402         }
 403 
 404         x, err := p.parsePower()
 405         return unaryExpr{op: op, x: x}, err
 406     }
 407 
 408     // handle all other unary operators
 409     if op, ok := p.acceptSyntax(`!`, `&`, `*`, `^`); ok {
 410         x, err := p.parsePower()
 411         return unaryExpr{op: op, x: x}, err
 412     }
 413 
 414     // handle subexpression in parentheses
 415     if _, ok := p.acceptSyntax(`(`); ok {
 416         x, err := p.parseExpression()
 417         if err != nil {
 418             return x, err
 419         }
 420 
 421         if err := p.demandSyntax(`)`); err != nil {
 422             return x, err
 423         }
 424         return p.parseAccessors(x)
 425     }
 426 
 427     // handle subexpression in square brackets: it's just an alternative to
 428     // using parentheses for subexpressions
 429     if _, ok := p.acceptSyntax(`[`); ok {
 430         x, err := p.parseExpression()
 431         if err != nil {
 432             return x, err
 433         }
 434 
 435         if err := p.demandSyntax(`]`); err != nil {
 436             return x, err
 437         }
 438         return p.parseAccessors(x)
 439     }
 440 
 441     // handle all other cases
 442     x, err := p.parseSimpleValue()
 443     if err != nil {
 444         return x, err
 445     }
 446 
 447     // handle arbitrarily-long chains of accessors
 448     return p.parseAccessors(x)
 449 }
 450 
 451 // parseSimpleValue handles a numeric literal or a variable/func name, also
 452 // known as identifier
 453 func (p *parser) parseSimpleValue() (any, error) {
 454     if len(p.tokens) == 0 {
 455         return nil, errEOS
 456     }
 457     t := p.tokens[0]
 458 
 459     switch t.kind {
 460     case identifierToken:
 461         p.advance()
 462         // handle func calls, such as f(...)
 463         if _, ok := p.acceptSyntax(`(`); ok {
 464             args, err := p.parseList(`)`)
 465             expr := callExpr{name: t.value, args: args}
 466             return expr, err
 467         }
 468         // handle func calls, such as f[...]
 469         if _, ok := p.acceptSyntax(`[`); ok {
 470             args, err := p.parseList(`]`)
 471             expr := callExpr{name: t.value, args: args}
 472             return expr, err
 473         }
 474         return t.value, nil
 475 
 476     case numberToken:
 477         p.advance()
 478         return strconv.ParseFloat(t.value, 64)
 479 
 480     default:
 481         const fs = "unexpected %s (token type %d)"
 482         return nil, fmt.Errorf(fs, t.value, t.kind)
 483     }
 484 }
 485 
 486 // parseAccessors handles an arbitrarily-long chain of accessors
 487 func (p *parser) parseAccessors(x any) (any, error) {
 488     for {
 489         s, ok := p.acceptSyntax(`.`, `@`)
 490         if !ok {
 491             // dot-chain is over
 492             return x, nil
 493         }
 494 
 495         // handle property/method accessors
 496         v, err := p.parseDot(s, x)
 497         if err != nil {
 498             return v, err
 499         }
 500         x = v
 501     }
 502 }
 503 
 504 // parseDot handles what follows a syntactic dot, as opposed to a dot which
 505 // may be part of a numeric literal
 506 func (p *parser) parseDot(after string, x any) (any, error) {
 507     if len(p.tokens) == 0 {
 508         const fs = "unexpected end of source after a %q"
 509         return x, fmt.Errorf(fs, after)
 510     }
 511 
 512     t := p.tokens[0]
 513     p.advance()
 514     if t.kind != identifierToken {
 515         const fs = "expected a valid property name, but got %s instead"
 516         return x, fmt.Errorf(fs, t.value)
 517     }
 518 
 519     if _, ok := p.acceptSyntax(`(`); ok {
 520         items, err := p.parseList(`)`)
 521         args := append([]any{x}, items...)
 522         return callExpr{name: t.value, args: args}, err
 523     }
 524 
 525     if _, ok := p.acceptSyntax(`[`); ok {
 526         items, err := p.parseList(`]`)
 527         args := append([]any{x}, items...)
 528         return callExpr{name: t.value, args: args}, err
 529     }
 530 
 531     return callExpr{name: t.value, args: []any{x}}, nil
 532 }
 533 
 534 // parseList handles the argument-list following a `(` or a `[`
 535 func (p *parser) parseList(end string) ([]any, error) {
 536     var arr []any
 537     for len(p.tokens) > 0 {
 538         if _, ok := p.acceptSyntax(`,`); ok {
 539             // ensure extra/trailing commas are allowed/ignored
 540             continue
 541         }
 542 
 543         if _, ok := p.acceptSyntax(end); ok {
 544             return arr, nil
 545         }
 546 
 547         v, err := p.parseExpression()
 548         if err != nil {
 549             return arr, err
 550         }
 551         arr = append(arr, v)
 552     }
 553 
 554     // return an appropriate error for the unexpected end of the source
 555     return arr, p.demandSyntax(`)`)
 556 }
 557 
 558 // simplifyNumber tries to simplify a few trivial unary operations on
 559 // numeric constants
 560 func simplifyNumber(op string, x any) (v any, ok bool) {
 561     if x, ok := x.(float64); ok {
 562         switch op {
 563         case `+`:
 564             return x, true
 565         case `-`:
 566             return -x, true
 567         default:
 568             return x, false
 569         }
 570     }
 571     return x, false
 572 }
     File: ./fmscripts/programs.go
   1 /*
   2 The MIT License (MIT)
   3 
   4 Copyright (c) 2026 pacman64
   5 
   6 Permission is hereby granted, free of charge, to any person obtaining a copy of
   7 this software and associated documentation files (the "Software"), to deal
   8 in the Software without restriction, including without limitation the rights to
   9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
  10 of the Software, and to permit persons to whom the Software is furnished to do
  11 so, subject to the following conditions:
  12 
  13 The above copyright notice and this permission notice shall be included in all
  14 copies or substantial portions of the Software.
  15 
  16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  22 SOFTWARE.
  23 */
  24 
  25 package fmscripts
  26 
  27 import (
  28     "math"
  29 )
  30 
  31 // So far, my apps relying on this package are
  32 //
  33 // - fh, a Function Heatmapper which color-codes f(x, y), seen from above
  34 // - star, a STAtistical Resampler to calculate stats from tabular data
  35 // - waveout, an app to emit sample-by-sample wave-audio by formula
  36 
  37 // Quick Notes on Performance
  38 //
  39 // Note: while these microbenchmarks are far from proper benchmarks, Club Pulse
  40 // can still give a general idea of what relative performance to expect.
  41 //
  42 // While funscripts.Interpreter can do anything a Program can do, and much more,
  43 // a fmscripts.Program is way faster for float64-only tasks: typical speed-ups
  44 // are between 20-to-50 times. Various simple benchmarks suggest running speed
  45 // is between 1/2 and 1/4 of native funcs.
  46 //
  47 // Reimplementations of the Club Pulse benchmark, when run 50 million times,
  48 // suggest this package is starting to measure the relative speed of various
  49 // trigonometric func across JIT-ted script runners, running somewhat slower
  50 // than NodeJS/V8, faster than PyPy, and much faster than Python.
  51 //
  52 // Other scripted tests, which aren't as trigonometry/exponential-heavy, hint
  53 // at even higher speed-ups compared to Python and PyPy, as well as being
  54 // almost as fast as NodeJS/V8.
  55 //
  56 // Being so close to Node's V8 (0.6x - 0.8x its speed) is a surprisingly good
  57 // result for the relatively little effort this package took.
  58 
  59 const (
  60     maxArgsLen = 1<<16 - 1 // max length for the []float64 input of callv
  61     maxOpIndex = 1<<32 - 1 // max index for values
  62 )
  63 
  64 // types to keep compiler code the same, while changing sizes of numOp fields
  65 type (
  66     opcode  uint16
  67     opargs  uint16 // opcode directly affects max correct value of maxArgsLen
  68     opindex uint32 // opcode directly affects max correct value of maxOpIndex
  69 )
  70 
  71 // numOp is a numeric operation in a program
  72 type numOp struct {
  73     What    opcode  // what to do
  74     NumArgs opargs  // vector-length only used for callv and call1v
  75     Index   opindex // index to load a value or call a function
  76 }
  77 
  78 // Since the go 1.19 compiler started compiling dense switch statements using
  79 // jump tables, adding more basic ops doesn't slow things down anymore when
  80 // reaching the next power-of-2 thresholds in the number of cases.
  81 
  82 const (
  83     // load float64 value to top of the stack
  84     load opcode = iota
  85 
  86     // pop top of stack and store it into value
  87     store opcode = iota
  88 
  89     // unary operations
  90     neg    opcode = iota
  91     not    opcode = iota
  92     abs    opcode = iota
  93     square opcode = iota
  94     // 1/x, the reciprocal/inverse value
  95     rec opcode = iota
  96     // x%1, but faster than math.Mod(x, 1)
  97     mod1 opcode = iota
  98 
  99     // arithmetic and logic operations: 2 float64 inputs, 1 float64 output
 100 
 101     add opcode = iota
 102     sub opcode = iota
 103     mul opcode = iota
 104     div opcode = iota
 105     mod opcode = iota
 106     pow opcode = iota
 107     and opcode = iota
 108     or  opcode = iota
 109 
 110     // binary comparisons: 2 float64 inputs, 1 booleanized float64 output
 111 
 112     equal    opcode = iota
 113     notequal opcode = iota
 114     less     opcode = iota
 115     lessoreq opcode = iota
 116     more     opcode = iota
 117     moreoreq opcode = iota
 118 
 119     // function callers with 1..5 float64 inputs and a float64 result
 120 
 121     call0 opcode = iota
 122     call1 opcode = iota
 123     call2 opcode = iota
 124     call3 opcode = iota
 125     call4 opcode = iota
 126     call5 opcode = iota
 127 
 128     // var-arg input function callers
 129 
 130     callv  opcode = iota
 131     call1v opcode = iota
 132 )
 133 
 134 // Program runs a sequence of float64 operations, with no explicit control-flow:
 135 // implicit control-flow is available in the float64-only functions you make
 136 // available to such programs. Such custom funcs must all return a float64, and
 137 // either take from 0 to 5 float64 arguments, or a single float64 array.
 138 //
 139 // As the name suggests, don't create such objects directly, but instead use
 140 // Compiler.Compile to create them. Only a Compiler lets you register variables
 141 // and functions, which then become part of your numeric Program.
 142 //
 143 // A Program lets you change values before each run, using pointers from method
 144 // Program.Get: such pointers are guaranteed never to change before or across
 145 // runs. Just ensure you get a variable defined in the Compiler used to make the
 146 // Program, or the pointer will be to a dummy place which has no effect on final
 147 // results.
 148 //
 149 // Expressions using literals are automatically optimized into their results:
 150 // this also applies to func calls with standard math functions and constants.
 151 //
 152 // The only way to limit such optimizations is to redefine common math funcs and
 153 // const values explicitly when compiling: after doing so, all those value-names
 154 // will stand for externally-updatable values which can change from one run to
 155 // the next. Similarly, there's no guarantee the (re)defined functions will be
 156 // deterministic, like the defaults they replaced.
 157 //
 158 // # Example
 159 //
 160 // var c fmscripts.Compiler
 161 //
 162 //  defs := map[string]any{
 163 //      "x": 0,    // define `x`, and initialize it to 0
 164 //      "k": 4.25, // define `k`, and initialize it to 4.25
 165 //      "b": true, // define `b`, and initialize it to 1.0
 166 //      "n": -23,  // define `n`, and initialize it to -23.0
 167 //      "pi": 3,   // define `pi`, overriding the automatic constant named `pi`
 168 //
 169 //      "f": numericKernel // type is func ([]float64) float64
 170 //      "g": otherFunc     // type is func (float64) float64
 171 //  }
 172 //
 173 // prog, err := c.Compile("log10(k) + f(sqrt(k) * exp(-x), 45, -0.23)", defs)
 174 // // ...
 175 //
 176 // x, _ := prog.Get("x") // Get returns (*float64, bool)
 177 // y, _ := prog.Get("y") // a useless pointer, since program doesn't use `y`
 178 // // ...
 179 //
 180 //  for i := 0; i < n; i++ {
 181 //      *x = float64(i)*dx + minx // you update inputs in place using pointers
 182 //      f := prog.Run()           // method Run gives you a float64 back
 183 //      // ...
 184 //  }
 185 type Program struct {
 186     sp int // stack pointer, even though it's an index
 187 
 188     stack  []float64 // pre-allocated by compiler to max length needed by program
 189     values []float64 // holds all values, whether literals, or variables
 190 
 191     ops   []numOp // all sequential operations for each run
 192     funcs []any   // all funcs used by the program
 193 
 194     names map[string]int // variable-name to index lookup
 195     dummy float64        // all pointers for undefined variables point here
 196 
 197     // data []float64
 198 }
 199 
 200 // Get lets you change parameters/variables before each time you run a program,
 201 // since it doesn't return a value, but a pointer to it, so you can update it
 202 // in place.
 203 //
 204 // If the name given isn't available, the result is a pointer to a dummy place:
 205 // this ensures non-nil pointers, which are always safe to use, even though
 206 // updates to the dummy destination have no effect on program results.
 207 func (p *Program) Get(name string) (ptr *float64, useful bool) {
 208     if i, ok := p.names[name]; ok {
 209         return &p.values[i], true
 210     }
 211     return &p.dummy, false
 212 }
 213 
 214 // Clone creates an exact copy of a Program with all values in their current
 215 // state: this is useful when dispatching embarassingly-parallel tasks to
 216 // multiple programs.
 217 func (p Program) Clone() Program {
 218     // can't share the stack nor the values
 219     stack := make([]float64, len(p.stack))
 220     values := make([]float64, len(p.values))
 221     copy(stack, p.stack)
 222     copy(values, p.values)
 223     p.stack = stack
 224     p.values = values
 225 
 226     // can share everything else as is
 227     return p
 228 }
 229 
 230 // Memo: the command to show all bound checks is
 231 // go test -gcflags="-d=ssa/check_bce/debug=1" fmscripts/programs.go
 232 
 233 // Discussion about go compiler optimizing switch statements into jump tables
 234 // https://go-review.googlesource.com/c/go/+/357330/
 235 
 236 // Run executes the program once. Before each run, update input values using
 237 // pointers from method Get.
 238 func (p *Program) Run() float64 {
 239     // Check for empty programs: these happen either when a compilation error
 240     // was ignored, or when a program was explicitly declared as a variable.
 241     if len(p.ops) == 0 {
 242         return math.NaN()
 243     }
 244 
 245     p.sp = 0
 246     p.runAllOps()
 247     return p.stack[p.sp]
 248 }
 249 
 250 type func4 = func(float64, float64, float64, float64) float64
 251 type func5 = func(float64, float64, float64, float64, float64) float64
 252 
 253 // runAllOps runs all operations in a loop: when done, the program's result is
 254 // ready as the only item left in the stack
 255 func (p *Program) runAllOps() {
 256     for _, op := range p.ops {
 257         // shortcut for the current stack pointer
 258         sp := p.sp
 259 
 260         // Preceding binary ops and func calls by _ = p.stack[i-n] prevents
 261         // an extra bound check for the lhs of assignments, but a quick
 262         // statistical summary of benchmarks doesn't show clear speedups,
 263         // let alone major ones.
 264         //
 265         // Separating different func types into different arrays to avoid
 266         // type-checking at runtime doesn't seem to be worth it either,
 267         // and makes the compiler more complicated.
 268 
 269         switch op.What {
 270         case load:
 271             // store above top of stack
 272             p.stack[sp+1] = p.values[op.Index]
 273             p.sp++
 274         case store:
 275             // store from top of the stack
 276             p.values[op.Index] = p.stack[sp]
 277             p.sp--
 278 
 279         // unary operations
 280         case neg:
 281             p.stack[sp] = -p.stack[sp]
 282         case not:
 283             p.stack[sp] = deboolNot(p.stack[sp])
 284         case abs:
 285             p.stack[sp] = math.Abs(p.stack[sp])
 286         case square:
 287             p.stack[sp] *= p.stack[sp]
 288         case rec:
 289             p.stack[sp] = 1 / p.stack[sp]
 290 
 291         // binary arithmetic ops
 292         case add:
 293             p.stack[sp-1] += p.stack[sp]
 294             p.sp--
 295         case sub:
 296             p.stack[sp-1] -= p.stack[sp]
 297             p.sp--
 298         case mul:
 299             p.stack[sp-1] *= p.stack[sp]
 300             p.sp--
 301         case div:
 302             p.stack[sp-1] /= p.stack[sp]
 303             p.sp--
 304         case mod:
 305             p.stack[sp-1] = math.Mod(p.stack[sp-1], p.stack[sp])
 306             p.sp--
 307         case pow:
 308             p.stack[sp-1] = math.Pow(p.stack[sp-1], p.stack[sp])
 309             p.sp--
 310 
 311         // binary boolean ops / binary comparisons
 312         case and:
 313             p.stack[sp-1] = deboolAnd(p.stack[sp-1], p.stack[sp])
 314             p.sp--
 315         case or:
 316             p.stack[sp-1] = deboolOr(p.stack[sp-1], p.stack[sp])
 317             p.sp--
 318         case equal:
 319             p.stack[sp-1] = debool(p.stack[sp-1] == p.stack[sp])
 320             p.sp--
 321         case notequal:
 322             p.stack[sp-1] = debool(p.stack[sp-1] != p.stack[sp])
 323             p.sp--
 324         case less:
 325             p.stack[sp-1] = debool(p.stack[sp-1] < p.stack[sp])
 326             p.sp--
 327         case lessoreq:
 328             p.stack[sp-1] = debool(p.stack[sp-1] <= p.stack[sp])
 329             p.sp--
 330         case more:
 331             p.stack[sp-1] = debool(p.stack[sp-1] > p.stack[sp])
 332             p.sp--
 333         case moreoreq:
 334             p.stack[sp-1] = debool(p.stack[sp-1] >= p.stack[sp])
 335             p.sp--
 336 
 337         // function calls
 338         case call0:
 339             f := p.funcs[op.Index].(func() float64)
 340             // store above top of stack
 341             p.stack[sp+1] = f()
 342             p.sp++
 343         case call1:
 344             f := p.funcs[op.Index].(func(float64) float64)
 345             p.stack[sp] = f(p.stack[sp])
 346         case call2:
 347             f := p.funcs[op.Index].(func(float64, float64) float64)
 348             p.stack[sp-1] = f(p.stack[sp-1], p.stack[sp])
 349             p.sp--
 350         case call3:
 351             f := p.funcs[op.Index].(func(float64, float64, float64) float64)
 352             p.stack[sp-2] = f(p.stack[sp-2], p.stack[sp-1], p.stack[sp])
 353             p.sp -= 2
 354         case call4:
 355             f := p.funcs[op.Index].(func4)
 356             st := p.stack
 357             p.stack[sp-3] = f(st[sp-3], st[sp-2], st[sp-1], st[sp])
 358             p.sp -= 3
 359         case call5:
 360             f := p.funcs[op.Index].(func5)
 361             st := p.stack
 362             p.stack[sp-4] = f(st[sp-4], st[sp-3], st[sp-2], st[sp-1], st[sp])
 363             p.sp -= 4
 364         case callv:
 365             i := sp - int(op.NumArgs) + 1
 366             f := p.funcs[op.Index].(func(...float64) float64)
 367             p.stack[sp-i+1] = f(p.stack[i : sp+1]...)
 368             p.sp = sp - i + 1
 369         case call1v:
 370             i := sp - int(op.NumArgs) + 1
 371             f := p.funcs[op.Index].(func(float64, ...float64) float64)
 372             p.stack[sp-i+1] = f(p.stack[i], p.stack[i+1:sp+1]...)
 373             p.sp = sp - i + 1
 374         }
 375     }
 376 }
 377 
 378 // debool is only used to turn boolean values used in comparison operations
 379 // into float64s, since those are the only type accepted on a program stack
 380 func debool(b bool) float64 {
 381     if b {
 382         return 1
 383     }
 384     return 0
 385 }
 386 
 387 // deboolNot runs the basic `not` operation
 388 func deboolNot(x float64) float64 {
 389     return debool(x == 0)
 390 }
 391 
 392 // deboolAnd runs the basic `and` operation
 393 func deboolAnd(x, y float64) float64 {
 394     return debool(x != 0 && y != 0)
 395 }
 396 
 397 // deboolOr runs the basic `or` operation
 398 func deboolOr(x, y float64) float64 {
 399     return debool(x != 0 || y != 0)
 400 }
     File: ./fmscripts/programs_test.go
   1 /*
   2 The MIT License (MIT)
   3 
   4 Copyright (c) 2026 pacman64
   5 
   6 Permission is hereby granted, free of charge, to any person obtaining a copy of
   7 this software and associated documentation files (the "Software"), to deal
   8 in the Software without restriction, including without limitation the rights to
   9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
  10 of the Software, and to permit persons to whom the Software is furnished to do
  11 so, subject to the following conditions:
  12 
  13 The above copyright notice and this permission notice shall be included in all
  14 copies or substantial portions of the Software.
  15 
  16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  22 SOFTWARE.
  23 */
  24 
  25 package fmscripts
  26 
  27 import "testing"
  28 
  29 func TestCallVarFuncs(t *testing.T) {
  30     tests := []struct {
  31         name string
  32         src  string
  33         exp  float64
  34     }{
  35         {"check 1 var arg", "numargs(34)", 1},
  36         {"check 2 var arg", "numargs(34, 33)", 2},
  37         {"check 3 var arg", "numargs(34, 322, 0.3)", 3},
  38         {"check 4 var arg", "numargs(34, -1, 553, 42)", 4},
  39         {"check 5 var arg", "numargs(34, 3, 4, 5, 1)", 5},
  40     }
  41 
  42     numargs := func(args ...float64) float64 { return float64(len(args)) }
  43 
  44     for _, tc := range tests {
  45         t.Run(tc.name, func(t *testing.T) {
  46             var c Compiler
  47             p, err := c.Compile(tc.src, map[string]any{
  48                 "numargs": numargs,
  49             })
  50             if err != nil {
  51                 t.Fatal(err)
  52                 return
  53             }
  54 
  55             got := p.Run()
  56             const fs = "failed check (var-arg): expected %f, got %f instead"
  57             if got != tc.exp {
  58                 t.Fatalf(fs, tc.exp, got)
  59                 return
  60             }
  61         })
  62     }
  63 }
     File: ./fmscripts/random.go
   1 /*
   2 The MIT License (MIT)
   3 
   4 Copyright (c) 2026 pacman64
   5 
   6 Permission is hereby granted, free of charge, to any person obtaining a copy of
   7 this software and associated documentation files (the "Software"), to deal
   8 in the Software without restriction, including without limitation the rights to
   9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
  10 of the Software, and to permit persons to whom the Software is furnished to do
  11 so, subject to the following conditions:
  12 
  13 The above copyright notice and this permission notice shall be included in all
  14 copies or substantial portions of the Software.
  15 
  16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  22 SOFTWARE.
  23 */
  24 
  25 package fmscripts
  26 
  27 import (
  28     "math"
  29     "math/rand"
  30 )
  31 
  32 // These funcs are kept separate from the larger lookup table so they aren't
  33 // mistaken for deterministic funcs which can be optimized away.
  34 //
  35 // var randomFuncs = map[string]any{
  36 //  "rand":   Random,
  37 //  "rint":   RandomInt,
  38 //  "runif":  RandomUnif,
  39 //  "rexp":   RandomExp,
  40 //  "rnorm":  RandomNorm,
  41 //  "rgamma": RandomGamma,
  42 //  "rbeta":  RandomBeta,
  43 // }
  44 
  45 func Random(r *rand.Rand) float64 {
  46     return r.Float64()
  47 }
  48 
  49 func RandomInt(r *rand.Rand, min, max float64) float64 {
  50     fmin := math.Trunc(min)
  51     fmax := math.Trunc(max)
  52     if fmin == fmax {
  53         return fmin
  54     }
  55 
  56     diff := math.Abs(fmax - fmin)
  57     return float64(r.Intn(int(diff)+1)) + fmin
  58 }
  59 
  60 func RandomUnif(r *rand.Rand, min, max float64) float64 {
  61     return (max-min)*r.Float64() + min
  62 }
  63 
  64 func RandomExp(r *rand.Rand, scale float64) float64 {
  65     return scale * r.ExpFloat64()
  66 }
  67 
  68 func RandomNorm(r *rand.Rand, mu, sigma float64) float64 {
  69     return sigma*r.NormFloat64() + mu
  70 }
  71 
  72 // Gamma generates a gamma-distributed real value, using a scale parameter.
  73 //
  74 // The algorithm is from Marsaglia and Tsang, as described in
  75 //
  76 //  A simple method for generating gamma variables
  77 //  https://dl.acm.org/doi/10.1145/358407.358414
  78 func RandomGamma(r *rand.Rand, scale float64) float64 {
  79     d := scale - 1.0/3.0
  80     c := 1 / math.Sqrt(9/d)
  81 
  82     for {
  83         // generate candidate value
  84         var x, v float64
  85         for {
  86             x = r.NormFloat64()
  87             v = 1 + c*x
  88             if v > 0 {
  89                 break
  90             }
  91         }
  92         v = v * v * v
  93 
  94         // accept or reject candidate value
  95         x2 := x * x
  96         u := r.Float64()
  97         if u < 1-0.0331*x2*x2 {
  98             return d * v
  99         }
 100         if math.Log(u) < 0.5*x2+d*(1-v+math.Log(v)) {
 101             return d * v
 102         }
 103     }
 104 }
 105 
 106 // Beta generates a beta-distributed real value.
 107 func RandomBeta(r *rand.Rand, a, b float64) float64 {
 108     return RandomGamma(r, a) / (RandomGamma(r, a) + RandomGamma(r, b))
 109 }
     File: ./fmscripts/tokens.go
   1 /*
   2 The MIT License (MIT)
   3 
   4 Copyright (c) 2026 pacman64
   5 
   6 Permission is hereby granted, free of charge, to any person obtaining a copy of
   7 this software and associated documentation files (the "Software"), to deal
   8 in the Software without restriction, including without limitation the rights to
   9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
  10 of the Software, and to permit persons to whom the Software is furnished to do
  11 so, subject to the following conditions:
  12 
  13 The above copyright notice and this permission notice shall be included in all
  14 copies or substantial portions of the Software.
  15 
  16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  22 SOFTWARE.
  23 */
  24 
  25 package fmscripts
  26 
  27 import (
  28     "errors"
  29     "fmt"
  30     "strings"
  31     "unicode"
  32     "unicode/utf8"
  33 )
  34 
  35 // tokenType is the specific type of a token; tokens never represent
  36 // whitespace-like text between recognized tokens
  37 type tokenType int
  38 
  39 const (
  40     // default zero-value type for tokens
  41     unknownToken tokenType = iota
  42 
  43     // a name
  44     identifierToken tokenType = iota
  45 
  46     // a literal numeric value
  47     numberToken tokenType = iota
  48 
  49     // any syntactic element made of 1 or more runes
  50     syntaxToken tokenType = iota
  51 )
  52 
  53 // errEOS signals the end of source code, and is the only token-related error
  54 // which the parser should ignore
  55 var errEOS = errors.New(`no more source code`)
  56 
  57 // token is either a name, value, or syntactic element coming from a script's
  58 // source code
  59 type token struct {
  60     kind  tokenType
  61     value string
  62     line  int
  63     pos   int
  64 }
  65 
  66 // tokenizer splits a string into a stream tokens, via its `next` method
  67 type tokenizer struct {
  68     cur     string
  69     linenum int
  70     linepos int
  71 }
  72 
  73 // newTokenizer is the constructor for type tokenizer
  74 func newTokenizer(src string) tokenizer {
  75     return tokenizer{
  76         cur:     src,
  77         linenum: 1,
  78         linepos: 1,
  79     }
  80 }
  81 
  82 // next advances the tokenizer, giving back a token, unless it's done
  83 func (t *tokenizer) next() (token, error) {
  84     // label to allow looping back after skipping comments, and thus avoid
  85     // an explicit tail-recursion for each commented line
  86 rerun:
  87 
  88     // always ignore any whitespace-like source
  89     if err := t.skipWhitespace(); err != nil {
  90         return token{}, err
  91     }
  92 
  93     if len(t.cur) == 0 {
  94         return token{}, errEOS
  95     }
  96 
  97     // remember starting position, in case of error
  98     line := t.linenum
  99     pos := t.linepos
 100 
 101     // use the leading rune to probe what's next
 102     r, _ := utf8.DecodeRuneInString(t.cur)
 103 
 104     switch r {
 105     case '0', '1', '2', '3', '4', '5', '6', '7', '8', '9':
 106         s, err := t.scanNumber()
 107         return token{kind: numberToken, value: s, line: line, pos: pos}, err
 108 
 109     case '(', ')', '[', ']', '{', '}', ',', '+', '-', '%', '^', '~', '@', ';':
 110         s := t.cur[:1]
 111         t.cur = t.cur[1:]
 112         t.linepos++
 113         res := token{kind: syntaxToken, value: s, line: line, pos: pos}
 114         return res, t.eos()
 115 
 116     case ':':
 117         s, err := t.tryPrefixes(`:=`, `:`)
 118         return token{kind: syntaxToken, value: s, line: line, pos: pos}, err
 119 
 120     case '*':
 121         s, err := t.tryPrefixes(`**`, `*`)
 122         return token{kind: syntaxToken, value: s, line: line, pos: pos}, err
 123 
 124     case '/':
 125         s, err := t.tryPrefixes(`//`, `/*`, `/`)
 126         // double-slash starts a comment until the end of the line
 127         if s == `//` {
 128             t.skipLine()
 129             goto rerun
 130         }
 131         // handle comments which can span multiple lines
 132         if s == `/*` {
 133             err := t.skipComment()
 134             if err != nil {
 135                 return token{}, err
 136             }
 137             goto rerun
 138         }
 139         // handle division
 140         return token{kind: syntaxToken, value: s, line: line, pos: pos}, err
 141 
 142     case '#':
 143         // even hash starts a comment until the end of the line, making the
 144         // syntax more Python-like
 145         t.skipLine()
 146         goto rerun
 147 
 148     case '&':
 149         s, err := t.tryPrefixes(`&&`, `&`)
 150         return token{kind: syntaxToken, value: s, line: line, pos: pos}, err
 151 
 152     case '|':
 153         s, err := t.tryPrefixes(`||`, `|`)
 154         return token{kind: syntaxToken, value: s, line: line, pos: pos}, err
 155 
 156     case '?':
 157         s, err := t.tryPrefixes(`??`, `?.`, `?`)
 158         return token{kind: syntaxToken, value: s, line: line, pos: pos}, err
 159 
 160     case '.':
 161         r, _ := utf8.DecodeRuneInString(t.cur[1:])
 162         if '0' <= r && r <= '9' {
 163             s, err := t.scanNumber()
 164             res := token{kind: numberToken, value: s, line: line, pos: pos}
 165             return res, err
 166         }
 167         s, err := t.tryPrefixes(`...`, `..`, `.?`, `.`)
 168         return token{kind: syntaxToken, value: s, line: line, pos: pos}, err
 169 
 170     case '=':
 171         // triple-equal makes the syntax even more JavaScript-like
 172         s, err := t.tryPrefixes(`===`, `==`, `=>`, `=`)
 173         return token{kind: syntaxToken, value: s, line: line, pos: pos}, err
 174 
 175     case '!':
 176         // not-double-equal makes the syntax even more JavaScript-like
 177         s, err := t.tryPrefixes(`!==`, `!=`, `!`)
 178         return token{kind: syntaxToken, value: s, line: line, pos: pos}, err
 179 
 180     case '<':
 181         // the less-more/diamond syntax is a SQL-like way to say not equal
 182         s, err := t.tryPrefixes(`<=`, `<>`, `<`)
 183         return token{kind: syntaxToken, value: s, line: line, pos: pos}, err
 184 
 185     case '>':
 186         s, err := t.tryPrefixes(`>=`, `>`)
 187         return token{kind: syntaxToken, value: s, line: line, pos: pos}, err
 188 
 189     default:
 190         if isIdentStartRune(r) {
 191             s, err := t.scanIdentifier()
 192             res := token{kind: identifierToken, value: s, line: line, pos: pos}
 193             return res, err
 194         }
 195         const fs = `line %d: pos %d: unexpected symbol %c`
 196         return token{}, fmt.Errorf(fs, t.linenum, t.linepos, r)
 197     }
 198 }
 199 
 200 // tryPrefixes tries to greedily match any prefix in the order given: when all
 201 // candidates fail, an empty string is returned; when successful, the tokenizer
 202 // updates its state to account for the matched prefix
 203 func (t *tokenizer) tryPrefixes(prefixes ...string) (string, error) {
 204     for _, pre := range prefixes {
 205         if strings.HasPrefix(t.cur, pre) {
 206             t.linepos += len(pre)
 207             t.cur = t.cur[len(pre):]
 208             return pre, t.eos()
 209         }
 210     }
 211 
 212     return ``, t.eos()
 213 }
 214 
 215 // skipWhitespace updates the tokenizer to ignore runs of consecutive whitespace
 216 // symbols: these are the likes of space, tab, newline, carriage return, etc.
 217 func (t *tokenizer) skipWhitespace() error {
 218     for len(t.cur) > 0 {
 219         r, size := utf8.DecodeRuneInString(t.cur)
 220         if !unicode.IsSpace(r) {
 221             // no more spaces to skip
 222             return nil
 223         }
 224 
 225         t.cur = t.cur[size:]
 226         if r == '\n' {
 227             // reached the next line
 228             t.linenum++
 229             t.linepos = 1
 230             continue
 231         }
 232         // continuing on the same line
 233         t.linepos++
 234     }
 235 
 236     // source code ended
 237     return errEOS
 238 }
 239 
 240 // skipLine updates the tokenizer to the end of the current line, or the end of
 241 // the source code, if it's the last line
 242 func (t *tokenizer) skipLine() error {
 243     for len(t.cur) > 0 {
 244         r, size := utf8.DecodeRuneInString(t.cur)
 245         t.cur = t.cur[size:]
 246         if r == '\n' {
 247             // reached the next line, as expected
 248             t.linenum++
 249             t.linepos = 1
 250             return nil
 251         }
 252     }
 253 
 254     // source code ended
 255     t.linenum++
 256     t.linepos = 1
 257     return errEOS
 258 }
 259 
 260 // skipComment updates the tokenizer to the end of the comment started with a
 261 // `/*` and ending with a `*/`, or to the end of the source code
 262 func (t *tokenizer) skipComment() error {
 263     var prev rune
 264     for len(t.cur) > 0 {
 265         r, size := utf8.DecodeRuneInString(t.cur)
 266         t.cur = t.cur[size:]
 267 
 268         if r == '\n' {
 269             t.linenum++
 270             t.linepos = 1
 271         } else {
 272             t.linepos++
 273         }
 274 
 275         if prev == '*' && r == '/' {
 276             return nil
 277         }
 278         prev = r
 279     }
 280 
 281     // source code ended
 282     const msg = "comment not ended with a `*/` sequence"
 283     return errors.New(msg)
 284 }
 285 
 286 func (t *tokenizer) scanIdentifier() (string, error) {
 287     end := 0
 288     for len(t.cur) > 0 {
 289         r, size := utf8.DecodeRuneInString(t.cur[end:])
 290         if (end == 0 && !isIdentStartRune(r)) || !isIdentRestRune(r) {
 291             // identifier ended, and there's more source code after it
 292             name := t.cur[:end]
 293             t.cur = t.cur[end:]
 294             return name, nil
 295         }
 296         end += size
 297         t.linepos++
 298     }
 299 
 300     // source code ended with an identifier name
 301     name := t.cur
 302     t.cur = ``
 303     return name, nil
 304 }
 305 
 306 func (t *tokenizer) scanNumber() (string, error) {
 307     dots := 0
 308     end := 0
 309     var prev rune
 310 
 311     for len(t.cur) > 0 {
 312         r, size := utf8.DecodeRuneInString(t.cur[end:])
 313 
 314         switch r {
 315         case '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '_', 'e':
 316             end += size
 317             t.linepos++
 318             prev = r
 319 
 320         case '+', '-':
 321             if end > 0 && prev != 'e' {
 322                 // number ended, and there's more source code after it
 323                 num := t.cur[:end]
 324                 t.cur = t.cur[end:]
 325                 return num, nil
 326             }
 327             end += size
 328             t.linepos++
 329             prev = r
 330 
 331         case '.':
 332             nr, _ := utf8.DecodeRuneInString(t.cur[end+size:])
 333             if dots == 1 || isIdentStartRune(nr) || unicode.IsSpace(nr) || nr == '.' {
 334                 // number ended, and there's more source code after it
 335                 num := t.cur[:end]
 336                 t.cur = t.cur[end:]
 337                 return num, nil
 338             }
 339             dots++
 340             end += size
 341             t.linepos++
 342             prev = r
 343 
 344         default:
 345             // number ended, and there's more source code after it
 346             num := t.cur[:end]
 347             t.cur = t.cur[end:]
 348             return num, nil
 349         }
 350     }
 351 
 352     // source code ended with a number
 353     name := t.cur
 354     t.cur = ``
 355     return name, nil
 356 }
 357 
 358 // eos checks if the source-code is over: if so, it returns an end-of-file error,
 359 // or a nil error otherwise
 360 func (t *tokenizer) eos() error {
 361     if len(t.cur) == 0 {
 362         return errEOS
 363     }
 364     return nil
 365 }
 366 
 367 func isIdentStartRune(r rune) bool {
 368     return ('A' <= r && r <= 'Z') || ('a' <= r && r <= 'z') || r == '_'
 369 }
 370 
 371 func isIdentRestRune(r rune) bool {
 372     return isIdentStartRune(r) || ('0' <= r && r <= '9')
 373 }
     File: ./fmscripts/tokens_test.go
   1 /*
   2 The MIT License (MIT)
   3 
   4 Copyright (c) 2026 pacman64
   5 
   6 Permission is hereby granted, free of charge, to any person obtaining a copy of
   7 this software and associated documentation files (the "Software"), to deal
   8 in the Software without restriction, including without limitation the rights to
   9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
  10 of the Software, and to permit persons to whom the Software is furnished to do
  11 so, subject to the following conditions:
  12 
  13 The above copyright notice and this permission notice shall be included in all
  14 copies or substantial portions of the Software.
  15 
  16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  22 SOFTWARE.
  23 */
  24 
  25 package fmscripts
  26 
  27 import (
  28     "reflect"
  29     "testing"
  30 )
  31 
  32 func TestTokenizer(t *testing.T) {
  33     var tests = []struct {
  34         Script string
  35         Tokens []string
  36     }{
  37         {``, nil},
  38         {`3.`, []string{`3.`}},
  39         {`3.2`, []string{`3.2`}},
  40         {`-3.2`, []string{`-`, `3.2`}},
  41         {`-3.2+56`, []string{`-`, `3.2`, `+`, `56`}},
  42     }
  43 
  44     for _, tc := range tests {
  45         t.Run(tc.Script, func(t *testing.T) {
  46             tok := newTokenizer(tc.Script)
  47             par, err := newParser(&tok)
  48             if err != nil {
  49                 t.Fatal(err)
  50                 return
  51             }
  52 
  53             var got []string
  54             for _, v := range par.tokens {
  55                 got = append(got, v.value)
  56             }
  57 
  58             if !reflect.DeepEqual(got, tc.Tokens) {
  59                 const fs = "from %s\nexpected\n%#v\nbut got\n%#v\ninstead"
  60                 t.Fatalf(fs, tc.Script, tc.Tokens, got)
  61                 return
  62             }
  63         })
  64     }
  65 }
     File: ./folders/folders.go
   1 /*
   2 The MIT License (MIT)
   3 
   4 Copyright (c) 2026 pacman64
   5 
   6 Permission is hereby granted, free of charge, to any person obtaining a copy of
   7 this software and associated documentation files (the "Software"), to deal
   8 in the Software without restriction, including without limitation the rights to
   9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
  10 of the Software, and to permit persons to whom the Software is furnished to do
  11 so, subject to the following conditions:
  12 
  13 The above copyright notice and this permission notice shall be included in all
  14 copies or substantial portions of the Software.
  15 
  16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  22 SOFTWARE.
  23 */
  24 
  25 package folders
  26 
  27 import (
  28     "bufio"
  29     "io"
  30     "io/fs"
  31     "os"
  32     "path/filepath"
  33     "strings"
  34 )
  35 
  36 const info = `
  37 folders [options...] [folders...]
  38 
  39 Find/list all folders in the folders given, without repetitions.
  40 
  41 All (optional) leading options start with either single or double-dash:
  42 
  43     -h, -help    show this help message
  44     -t, -top     turn off recursive behavior; top-level entries only
  45 `
  46 
  47 func Main() {
  48     top := false
  49     buffered := false
  50     args := os.Args[1:]
  51 
  52     for len(args) > 0 {
  53         switch args[0] {
  54         case `-b`, `--b`, `-buffered`, `--buffered`:
  55             buffered = true
  56             args = args[1:]
  57             continue
  58 
  59         case `-h`, `--h`, `-help`, `--help`:
  60             os.Stdout.WriteString(info[1:])
  61             return
  62 
  63         case `-t`, `--t`, `-top`, `--top`:
  64             top = true
  65             args = args[1:]
  66             continue
  67         }
  68 
  69         break
  70     }
  71 
  72     if len(args) > 0 && args[0] == `--` {
  73         args = args[1:]
  74     }
  75 
  76     liveLines := !buffered
  77     if !buffered {
  78         if _, err := os.Stdout.Seek(0, io.SeekCurrent); err == nil {
  79             liveLines = false
  80         }
  81     }
  82 
  83     var cfg config
  84     cfg.recursive = !top
  85     cfg.liveLines = liveLines
  86 
  87     if err := run(os.Stdout, args, cfg); err != nil && err != io.EOF {
  88         os.Stderr.WriteString(err.Error())
  89         os.Stderr.WriteString("\n")
  90         os.Exit(1)
  91     }
  92 }
  93 
  94 type config struct {
  95     recursive bool
  96     liveLines bool
  97 }
  98 
  99 func run(w io.Writer, paths []string, cfg config) error {
 100     bw := bufio.NewWriter(w)
 101     defer bw.Flush()
 102 
 103     got := make(map[string]struct{})
 104 
 105     if len(paths) == 0 {
 106         paths = []string{`.`}
 107     }
 108 
 109     // handle is the callback for func filepath.WalkDir
 110     handle := func(path string, e fs.DirEntry, err error) error {
 111         if err != nil {
 112             return err
 113         }
 114 
 115         if _, ok := got[path]; ok {
 116             return nil
 117         }
 118         got[path] = struct{}{}
 119 
 120         if e.IsDir() {
 121             if err := handleEntry(bw, path, cfg.liveLines); err != nil {
 122                 return err
 123             }
 124         }
 125 
 126         return nil
 127     }
 128 
 129     for _, path := range paths {
 130         if _, ok := got[path]; ok {
 131             continue
 132         }
 133         got[path] = struct{}{}
 134 
 135         st, err := os.Stat(path)
 136         if err != nil {
 137             return err
 138         }
 139 
 140         if !st.IsDir() {
 141             continue
 142         }
 143 
 144         if !strings.HasSuffix(path, `/`) {
 145             path = path + `/`
 146             got[path] = struct{}{}
 147         }
 148 
 149         if err := handleEntry(bw, path, cfg.liveLines); err != nil {
 150             return err
 151         }
 152 
 153         if !cfg.recursive {
 154             continue
 155         }
 156 
 157         if err := filepath.WalkDir(path, handle); err != nil {
 158             return err
 159         }
 160     }
 161 
 162     return nil
 163 }
 164 
 165 func handleEntry(w *bufio.Writer, path string, live bool) error {
 166     abs, err := filepath.Abs(path)
 167     if err != nil {
 168         return err
 169     }
 170 
 171     w.WriteString(abs)
 172     w.WriteByte('\n')
 173 
 174     if !live {
 175         return nil
 176     }
 177 
 178     if err := w.Flush(); err != nil {
 179         return io.EOF
 180     }
 181     return nil
 182 }
     File: ./hima/hima.go
   1 /*
   2 The MIT License (MIT)
   3 
   4 Copyright (c) 2026 pacman64
   5 
   6 Permission is hereby granted, free of charge, to any person obtaining a copy of
   7 this software and associated documentation files (the "Software"), to deal
   8 in the Software without restriction, including without limitation the rights to
   9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
  10 of the Software, and to permit persons to whom the Software is furnished to do
  11 so, subject to the following conditions:
  12 
  13 The above copyright notice and this permission notice shall be included in all
  14 copies or substantial portions of the Software.
  15 
  16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  22 SOFTWARE.
  23 */
  24 
  25 package hima
  26 
  27 import (
  28     "bufio"
  29     "bytes"
  30     "io"
  31     "os"
  32     "regexp"
  33     "strings"
  34 )
  35 
  36 const info = `
  37 hima [options...] [regexes...]
  38 
  39 
  40 HIlight MAtches ANSI-styles matching regular expressions along lines read
  41 from the standard input. The regular-expression mode used is "re2", which
  42 is a superset of the commonly-used "extended-mode".
  43 
  44 Regexes always avoid matching any ANSI-style sequences, to avoid messing
  45 those up. Also, multiple matches in a line never overlap: at each step
  46 along a line, the earliest-starting match among the regexes always wins,
  47 as the order regexes are given among the arguments never matters.
  48 
  49 The options are, available both in single and double-dash versions
  50 
  51     -h, -help      show this help message
  52     -f, -filter    filter out (ignore) lines with no matches
  53     -i, -ins       match regexes case-insensitively
  54 `
  55 
  56 const highlightStyle = "\x1b[7m"
  57 
  58 func Main() {
  59     filter := false
  60     buffered := false
  61     insensitive := false
  62     args := os.Args[1:]
  63 
  64     for len(args) > 0 {
  65         switch args[0] {
  66         case `-b`, `--b`, `-buffered`, `--buffered`:
  67             buffered = true
  68             args = args[1:]
  69 
  70         case `-f`, `--f`, `-filter`, `--filter`:
  71             filter = true
  72             args = args[1:]
  73 
  74         case `-h`, `--h`, `-help`, `--help`:
  75             os.Stdout.WriteString(info[1:])
  76             return
  77 
  78         case `-i`, `--i`, `-ins`, `--ins`:
  79             insensitive = true
  80             args = args[1:]
  81         }
  82 
  83         break
  84     }
  85 
  86     if len(args) > 0 && args[0] == `--` {
  87         args = args[1:]
  88     }
  89 
  90     patterns := make([]pattern, 0, len(args))
  91 
  92     for _, s := range args {
  93         var err error
  94         var pat pattern
  95 
  96         if insensitive {
  97             pat, err = compile(`(?i)` + s)
  98         } else {
  99             pat, err = compile(s)
 100         }
 101 
 102         if err != nil {
 103             os.Stderr.WriteString(err.Error())
 104             os.Stderr.WriteString("\n")
 105             continue
 106         }
 107 
 108         patterns = append(patterns, pat)
 109     }
 110 
 111     // quit right away when given invalid regexes
 112     if len(patterns) < len(args) {
 113         os.Exit(1)
 114     }
 115 
 116     liveLines := !buffered
 117     if !buffered {
 118         if _, err := os.Stdout.Seek(0, io.SeekCurrent); err == nil {
 119             liveLines = false
 120         }
 121     }
 122 
 123     err := run(os.Stdout, os.Stdin, patterns, filter, liveLines)
 124     if err != nil && err != io.EOF {
 125         os.Stderr.WriteString(err.Error())
 126         os.Stderr.WriteString("\n")
 127         os.Exit(1)
 128     }
 129 }
 130 
 131 // pattern is a regular-expression pattern which distinguishes between the
 132 // start/end of a line and those of the chunks it can be used to match
 133 type pattern struct {
 134     // expr is the regular-expression
 135     expr *regexp.Regexp
 136 
 137     // begin is whether the regexp refers to the start of a line
 138     begin bool
 139 
 140     // end is whether the regexp refers to the end of a line
 141     end bool
 142 }
 143 
 144 func compile(src string) (pattern, error) {
 145     expr, err := regexp.Compile(src)
 146 
 147     var pat pattern
 148     pat.expr = expr
 149     pat.begin = strings.HasPrefix(src, `^`) || strings.HasPrefix(src, `(?i)^`)
 150     pat.end = strings.HasSuffix(src, `$`) && !strings.HasSuffix(src, `\$`)
 151     return pat, err
 152 }
 153 
 154 func (p pattern) findIndex(s []byte, i int, last int) (start int, stop int) {
 155     if i > 0 && p.begin {
 156         return -1, -1
 157     }
 158     if i != last && p.end {
 159         return -1, -1
 160     }
 161 
 162     span := p.expr.FindIndex(s)
 163     // also ignore empty regex matches to avoid infinite outer loops,
 164     // as skipping empty slices isn't advancing at all, leaving the
 165     // string stuck to being empty-matched forever by the same regex
 166     if len(span) != 2 || span[0] == span[1] {
 167         return -1, -1
 168     }
 169 
 170     return span[0], span[1]
 171 }
 172 
 173 func run(w io.Writer, r io.Reader, pats []pattern, filter, live bool) error {
 174     sc := bufio.NewScanner(r)
 175     sc.Buffer(nil, 8*1024*1024*1024)
 176     bw := bufio.NewWriter(w)
 177     defer bw.Flush()
 178 
 179     for i := 0; sc.Scan(); i++ {
 180         s := sc.Bytes()
 181         if i == 0 && bytes.HasPrefix(s, []byte{0xef, 0xbb, 0xbf}) {
 182             s = s[3:]
 183         }
 184 
 185         n := 0
 186         last := countChunks(s) - 1
 187         if last < 0 {
 188             last = 0
 189         }
 190 
 191         if filter && !matches(s, pats, last) {
 192             continue
 193         }
 194 
 195         for len(s) > 0 {
 196             i, j := indexEscapeSequence(s)
 197             if i < 0 {
 198                 handleChunk(bw, s, pats, n, last)
 199                 break
 200             }
 201             if j < 0 {
 202                 j = len(s)
 203             }
 204 
 205             handleChunk(bw, s[:i], pats, n, last)
 206             if i > 0 {
 207                 n++
 208             }
 209 
 210             bw.Write(s[i:j])
 211 
 212             s = s[j:]
 213         }
 214 
 215         if bw.WriteByte('\n') != nil {
 216             return io.EOF
 217         }
 218 
 219         if !live {
 220             continue
 221         }
 222 
 223         if bw.Flush() != nil {
 224             return io.EOF
 225         }
 226     }
 227 
 228     return sc.Err()
 229 }
 230 
 231 // matches finds out if any regex matches any substring around ANSI-sequences
 232 func matches(s []byte, patterns []pattern, last int) bool {
 233     n := 0
 234 
 235     for len(s) > 0 {
 236         i, j := indexEscapeSequence(s)
 237         if i < 0 {
 238             for _, p := range patterns {
 239                 if begin, _ := p.findIndex(s, n, last); begin >= 0 {
 240                     return true
 241                 }
 242             }
 243             return false
 244         }
 245 
 246         if j < 0 {
 247             j = len(s)
 248         }
 249 
 250         for _, p := range patterns {
 251             if begin, _ := p.findIndex(s[:i], n, last); begin >= 0 {
 252                 return true
 253             }
 254         }
 255 
 256         if i > 0 {
 257             n++
 258         }
 259 
 260         s = s[j:]
 261     }
 262 
 263     return false
 264 }
 265 
 266 func countChunks(s []byte) int {
 267     chunks := 0
 268 
 269     for len(s) > 0 {
 270         i, j := indexEscapeSequence(s)
 271         if i < 0 {
 272             break
 273         }
 274 
 275         if i > 0 {
 276             chunks++
 277         }
 278 
 279         if j < 0 {
 280             break
 281         }
 282         s = s[j:]
 283     }
 284 
 285     if len(s) > 0 {
 286         chunks++
 287     }
 288     return chunks
 289 }
 290 
 291 // indexEscapeSequence finds the first ANSI-style escape-sequence, which is
 292 // the multi-byte sequences starting with ESC[; the result is a pair of slice
 293 // indices which can be independently negative when either the start/end of
 294 // a sequence isn't found; given their fairly-common use, even the hyperlink
 295 // ESC]8 sequences are supported
 296 func indexEscapeSequence(s []byte) (int, int) {
 297     var prev byte
 298 
 299     for i, b := range s {
 300         if prev == '\x1b' && b == '[' {
 301             j := indexLetter(s[i+1:])
 302             if j < 0 {
 303                 return i, -1
 304             }
 305             return i - 1, i + 1 + j + 1
 306         }
 307 
 308         if prev == '\x1b' && b == ']' && i+1 < len(s) && s[i+1] == '8' {
 309             j := indexPair(s[i+1:], '\x1b', '\\')
 310             if j < 0 {
 311                 return i, -1
 312             }
 313             return i - 1, i + 1 + j + 2
 314         }
 315 
 316         prev = b
 317     }
 318 
 319     return -1, -1
 320 }
 321 
 322 func indexLetter(s []byte) int {
 323     for i, b := range s {
 324         upper := b &^ 32
 325         if 'A' <= upper && upper <= 'Z' {
 326             return i
 327         }
 328     }
 329 
 330     return -1
 331 }
 332 
 333 func indexPair(s []byte, x byte, y byte) int {
 334     var prev byte
 335 
 336     for i, b := range s {
 337         if prev == x && b == y && i > 0 {
 338             return i
 339         }
 340         prev = b
 341     }
 342 
 343     return -1
 344 }
 345 
 346 // note: looking at the results of restoring ANSI-styles after style-resets
 347 // doesn't seem to be worth it, as a previous version used to do
 348 
 349 // handleChunk handles line-slices around any detected ANSI-style sequences,
 350 // or even whole lines, when no ANSI-styles are found in them
 351 func handleChunk(w *bufio.Writer, s []byte, with []pattern, n int, last int) {
 352     for len(s) > 0 {
 353         start, end := -1, -1
 354         for _, p := range with {
 355             i, j := p.findIndex(s, n, last)
 356             if i >= 0 && (i < start || start < 0) {
 357                 start, end = i, j
 358             }
 359         }
 360 
 361         if start < 0 {
 362             w.Write(s)
 363             return
 364         }
 365 
 366         w.Write(s[:start])
 367         w.WriteString(highlightStyle)
 368         w.Write(s[start:end])
 369         w.WriteString("\x1b[0m")
 370 
 371         s = s[end:]
 372     }
 373 }
     File: ./htmlify/htmlify.go
   1 /*
   2 The MIT License (MIT)
   3 
   4 Copyright (c) 2026 pacman64
   5 
   6 Permission is hereby granted, free of charge, to any person obtaining a copy of
   7 this software and associated documentation files (the "Software"), to deal
   8 in the Software without restriction, including without limitation the rights to
   9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
  10 of the Software, and to permit persons to whom the Software is furnished to do
  11 so, subject to the following conditions:
  12 
  13 The above copyright notice and this permission notice shall be included in all
  14 copies or substantial portions of the Software.
  15 
  16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  22 SOFTWARE.
  23 */
  24 
  25 package htmlify
  26 
  27 import (
  28     "bufio"
  29     "encoding/base64"
  30     "errors"
  31     "io"
  32     "os"
  33     "regexp"
  34     "strings"
  35 )
  36 
  37 const info = `
  38 htmlify [options...] [filepaths/URIs...]
  39 
  40 
  41 Render plain-text prose into self-contained HTML. Lines which are just a
  42 valid data-URI are turned into pictures, audio, or even video elements.
  43 
  44 All HTTP(s) URIs are autodetected and rendered as hyperlinks, even when
  45 lines have multiple URIs in them.
  46 
  47 If a title isn't given from the cmd-line options, the first line is used
  48 as the title.
  49 
  50 All (optional) leading options start with either single or double-dash,
  51 and most of them change the style/color used. Some of the options are,
  52 shown in their single-dash form:
  53 
  54     -h, -help            show this help message
  55     -mono, -monospace    use a monospace font for text
  56     -t, -title           use the next argument as the webpage title
  57 `
  58 
  59 var links = regexp.MustCompile(`(?i)(https?|ftps?)://[a-zA-Z0-9_%.,?/&=#-]+`)
  60 
  61 type config struct {
  62     Title     string
  63     GotTitle  bool
  64     Started   bool
  65     Monospace bool
  66 }
  67 
  68 func Main() {
  69     var cfg config
  70     args := os.Args[1:]
  71 
  72     for len(args) > 0 {
  73         switch args[0] {
  74         case `-h`, `--h`, `-help`, `--help`:
  75             os.Stdout.WriteString(info[1:])
  76             return
  77 
  78         case `-mono`, `--mono`, `-monospace`, `--monospace`:
  79             cfg.Monospace = true
  80             args = args[1:]
  81             continue
  82 
  83         case `-t`, `--t`, `-title`, `--title`:
  84             if len(args) >= 2 {
  85                 cfg.Title = args[1]
  86                 cfg.GotTitle = true
  87                 args = args[2:]
  88             } else {
  89                 const m = "title option isn't followed by the actual title\n"
  90                 os.Stderr.WriteString(m)
  91                 os.Exit(1)
  92             }
  93             continue
  94         }
  95 
  96         break
  97     }
  98 
  99     if len(args) > 0 && args[0] == `--` {
 100         args = args[1:]
 101     }
 102 
 103     if err := run(os.Stdout, args, &cfg); err != nil && err != io.EOF {
 104         os.Stderr.WriteString(err.Error())
 105         os.Stderr.WriteString("\n")
 106         os.Exit(1)
 107     }
 108 }
 109 
 110 func run(w io.Writer, args []string, cfg *config) error {
 111     bw := bufio.NewWriter(w)
 112     defer bw.Flush()
 113 
 114     if len(args) == 0 {
 115         if err := handleReader(bw, os.Stdin, cfg); err != nil {
 116             return err
 117         }
 118     }
 119 
 120     for _, name := range args {
 121         if err := handleFile(bw, name, cfg); err != nil {
 122             return err
 123         }
 124     }
 125 
 126     if cfg.Started {
 127         endPage(bw)
 128     }
 129     return nil
 130 }
 131 
 132 func handleFile(w *bufio.Writer, name string, cfg *config) error {
 133     if name == `` || name == `-` {
 134         return handleReader(w, os.Stdin, cfg)
 135     }
 136 
 137     f, err := os.Open(name)
 138     if err != nil {
 139         return errors.New(`can't read from file named "` + name + `"`)
 140     }
 141     defer f.Close()
 142 
 143     return handleReader(w, f, cfg)
 144 }
 145 
 146 func handleReader(w *bufio.Writer, r io.Reader, cfg *config) error {
 147     const gb = 1024 * 1024 * 1024
 148     sc := bufio.NewScanner(r)
 149     sc.Buffer(nil, 8*gb)
 150     lines := 0
 151 
 152     for sc.Scan() {
 153         line := sc.Text()
 154 
 155         if lines == 0 && strings.HasPrefix(line, "\xef\xbb\xbf") {
 156             line = line[3:]
 157         }
 158         if lines == 0 && !cfg.Started {
 159             title := line
 160             if cfg.GotTitle {
 161                 title = cfg.Title
 162             }
 163             startPage(w, title, cfg.Monospace)
 164             cfg.Started = true
 165         }
 166         lines++
 167 
 168         if err := handleLine(w, line, cfg); err != nil {
 169             return err
 170         }
 171     }
 172 
 173     if !cfg.Started && lines > 0 {
 174         startPage(w, cfg.Title, cfg.Monospace)
 175         cfg.Started = true
 176     }
 177     return sc.Err()
 178 }
 179 
 180 const style = `
 181         body {
 182             margin: 1rem auto 2rem auto;
 183             padding: 0.25rem;
 184             font-size: 1.1rem;
 185             line-height: 1.8rem;
 186             font-family: sans-serif;
 187 
 188             max-width: 95vw;
 189             /* width: max-content; */
 190             width: fit-content;
 191 
 192             box-sizing: border-box;
 193             display: block;
 194         }
 195 
 196         a {
 197             color: steelblue;
 198             text-decoration: none;
 199         }
 200 
 201         p {
 202             display: block;
 203             margin: auto;
 204             max-width: 80ch;
 205         }
 206 
 207         img {
 208             margin: none;
 209         }
 210 
 211         audio {
 212             width: 60ch;
 213         }
 214 
 215         table {
 216             margin: 2rem auto;
 217             border-collapse: collapse;
 218         }
 219 
 220         thead>* {
 221             position: sticky;
 222             top: 0;
 223             background-color: white;
 224         }
 225 
 226         tfoot th {
 227             user-select: none;
 228         }
 229 
 230         th, td {
 231             padding: 0.1rem 1ch;
 232             min-width: 4ch;
 233             border-bottom: solid thin transparent;
 234         }
 235 
 236         tr:nth-child(5n) td {
 237             border-bottom: solid thin #ccc;
 238         }
 239 
 240         .monospace {
 241             font-family: monospace;
 242         }
 243 `
 244 
 245 func startPage(w *bufio.Writer, title string, monospace bool) {
 246     w.WriteString("<!DOCTYPE html>\n")
 247     w.WriteString("<html lang=\"en\">\n")
 248     w.WriteString("\n")
 249     w.WriteString("<head>\n")
 250     w.WriteString("    <meta charset=\"UTF-8\">\n")
 251     w.WriteString("    <meta name=\"viewport\" content=\"width=device-width,")
 252     w.WriteString(" initial-scale=1.0\">\n")
 253     w.WriteString("    <meta http-equiv=\"X-UA-Compatible\"")
 254     w.WriteString(" content=\"ie=edge\">\n")
 255     w.WriteString("\n")
 256     w.WriteString("    <link rel=\"icon\" href=\"data:,\">\n")
 257     w.WriteString(`    <title>`)
 258     w.WriteString(title)
 259     w.WriteString("</title>\n")
 260     w.WriteString("\n")
 261     w.WriteString("\n")
 262     w.WriteString("    <style>\n")
 263     w.WriteString(style[1:])
 264     w.WriteString("    </style>\n")
 265     w.WriteString("</head>\n")
 266     if monospace {
 267         w.WriteString("<body class=\"monospace\">\n")
 268     } else {
 269         w.WriteString("<body>\n")
 270     }
 271 }
 272 
 273 func endPage(w *bufio.Writer) {
 274     w.WriteString("</body>\n")
 275     w.WriteString("</html>\n")
 276 }
 277 
 278 func handleLine(w *bufio.Writer, s string, cfg *config) error {
 279     if handleDataURI(w, s) {
 280         if w.WriteByte('\n') != nil {
 281             return io.EOF
 282         }
 283         return nil
 284     }
 285 
 286     for len(s) > 0 {
 287         span := links.FindStringIndex(s)
 288         if span != nil && len(span) == 2 {
 289             i := span[0]
 290             j := span[1]
 291             href := s[i:j]
 292             handleChunk(w, s[:i])
 293             w.WriteString(`<a href="`)
 294             w.WriteString(href)
 295             w.WriteString(`">`)
 296             w.WriteString(href)
 297             w.WriteString(`</a>`)
 298             s = s[j:]
 299             continue
 300         }
 301 
 302         handleChunk(w, s)
 303         break
 304     }
 305 
 306     w.WriteString(`<br>`)
 307     if w.WriteByte('\n') != nil {
 308         return io.EOF
 309     }
 310     return nil
 311 }
 312 
 313 func handleChunk(w *bufio.Writer, s string) {
 314     for len(s) > 0 {
 315         switch b := s[0]; b {
 316         case '&':
 317             w.WriteString("&amp;")
 318         case '<':
 319             w.WriteString("&lt;")
 320         case '>':
 321             w.WriteString("&gt;")
 322         default:
 323             w.WriteByte(b)
 324         }
 325         s = s[1:]
 326     }
 327 }
 328 
 329 func handleDataURI(w *bufio.Writer, s string) bool {
 330     full := s
 331 
 332     if !strings.HasPrefix(s, `data:`) {
 333         return false
 334     }
 335 
 336     s = strings.TrimPrefix(s, `data:`)
 337     i := strings.Index(s, `;base64,`)
 338     if i < 0 {
 339         return false
 340     }
 341 
 342     kind := s[:i]
 343     s = s[i+len(`;base64,`):]
 344 
 345     if strings.HasPrefix(kind, `image/`) {
 346         if !isBase64(s) {
 347             return false
 348         }
 349 
 350         w.WriteString(`<img src="`)
 351         w.WriteString(full)
 352         w.WriteString(`">`)
 353         return true
 354     }
 355 
 356     if strings.HasPrefix(kind, `audio/`) {
 357         if !isBase64(s) {
 358             return false
 359         }
 360 
 361         w.WriteString(`<audio controls src="`)
 362         w.WriteString(full)
 363         w.WriteString(`"></audio>`)
 364         return true
 365     }
 366 
 367     if strings.HasPrefix(kind, `video/`) {
 368         if !isBase64(s) {
 369             return false
 370         }
 371 
 372         w.WriteString(`<video controls src="`)
 373         w.WriteString(full)
 374         w.WriteString(`"></video>`)
 375         return true
 376     }
 377 
 378     return false
 379 }
 380 
 381 func handleMedia(w *bufio.Writer, begin string, s string, end string) bool {
 382     if !isBase64(s) {
 383         return false
 384     }
 385 
 386     w.WriteString(begin)
 387     w.WriteString(s)
 388     w.WriteString(end)
 389     return true
 390 }
 391 
 392 func isBase64(s string) bool {
 393     dec := base64.NewDecoder(base64.StdEncoding, strings.NewReader(s))
 394     n, err := io.Copy(io.Discard, dec)
 395     return n > 0 && err == nil
 396 }
     File: ./id3pic/id3pic.go
   1 /*
   2 The MIT License (MIT)
   3 
   4 Copyright (c) 2026 pacman64
   5 
   6 Permission is hereby granted, free of charge, to any person obtaining a copy of
   7 this software and associated documentation files (the "Software"), to deal
   8 in the Software without restriction, including without limitation the rights to
   9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
  10 of the Software, and to permit persons to whom the Software is furnished to do
  11 so, subject to the following conditions:
  12 
  13 The above copyright notice and this permission notice shall be included in all
  14 copies or substantial portions of the Software.
  15 
  16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  22 SOFTWARE.
  23 */
  24 
  25 package id3pic
  26 
  27 import (
  28     "bufio"
  29     "encoding/binary"
  30     "errors"
  31     "io"
  32     "os"
  33 )
  34 
  35 const info = `
  36 id3pic [options...] [file...]
  37 
  38 Extract picture/thumbnail bytes from ID3/MP3 metadata, if available.
  39 
  40 All (optional) leading options start with either single or double-dash:
  41 
  42     -h, -help    show this help message
  43 `
  44 
  45 // errNoThumb is a generic error to handle lack of thumbnails, in case no
  46 // picture-metadata-starters are found at all
  47 var errNoThumb = errors.New(`no thumbnail data found`)
  48 
  49 // errInvalidPIC is a generic error for invalid PIC-format pics
  50 var errInvalidPIC = errors.New(`invalid PIC-format embedded thumbnail`)
  51 
  52 func Main() {
  53     if len(os.Args) > 1 {
  54         switch os.Args[1] {
  55         case `-h`, `--h`, `-help`, `--help`:
  56             os.Stdout.WriteString(info[1:])
  57             return
  58         }
  59     }
  60 
  61     if len(os.Args) > 2 {
  62         showError(`can only handle 1 file`)
  63         os.Exit(1)
  64     }
  65 
  66     name := `-`
  67     if len(os.Args) > 1 {
  68         name = os.Args[1]
  69     }
  70 
  71     if err := run(os.Stdout, name); err != nil && err != io.EOF {
  72         showError(err.Error())
  73         os.Exit(1)
  74     }
  75 }
  76 
  77 func showError(msg string) {
  78     os.Stderr.WriteString(msg)
  79     os.Stderr.WriteString("\n")
  80 }
  81 
  82 func run(w io.Writer, name string) error {
  83     if name == `-` {
  84         return id3pic(w, bufio.NewReader(os.Stdin))
  85     }
  86 
  87     f, err := os.Open(name)
  88     if err != nil {
  89         return errors.New(`can't read from file named "` + name + `"`)
  90     }
  91     defer f.Close()
  92 
  93     return id3pic(w, bufio.NewReader(f))
  94 }
  95 
  96 func match(r *bufio.Reader, what []byte) bool {
  97     for _, v := range what {
  98         b, err := r.ReadByte()
  99         if b != v || err != nil {
 100             return false
 101         }
 102     }
 103     return true
 104 }
 105 
 106 func id3pic(w io.Writer, r *bufio.Reader) error {
 107     // match the ID3 mark
 108     for {
 109         b, err := r.ReadByte()
 110         if err == io.EOF {
 111             return errNoThumb
 112         }
 113         if err != nil {
 114             return err
 115         }
 116 
 117         if b == 'I' && match(r, []byte{'D', '3'}) {
 118             break
 119         }
 120     }
 121 
 122     for {
 123         b, err := r.ReadByte()
 124         if err == io.EOF {
 125             return errNoThumb
 126         }
 127         if err != nil {
 128             return err
 129         }
 130 
 131         // handle APIC-type chunks
 132         if b == 'A' && match(r, []byte{'P', 'I', 'C'}) {
 133             return handleAPIC(w, r)
 134         }
 135     }
 136 }
 137 
 138 func handleAPIC(w io.Writer, r *bufio.Reader) error {
 139     // section-size seems stored as 4 big-endian bytes
 140     var size uint32
 141     err := binary.Read(r, binary.BigEndian, &size)
 142     if err != nil {
 143         return err
 144     }
 145 
 146     n, err := skipThumbnailTypeAPIC(r)
 147     if err != nil {
 148         return err
 149     }
 150 
 151     _, err = io.Copy(w, io.LimitReader(r, int64(int(size)-n)))
 152     return err
 153 }
 154 
 155 func skipThumbnailTypeAPIC(r *bufio.Reader) (skipped int, err error) {
 156     m, err := r.Discard(2)
 157     if err != nil || m != 2 {
 158         return -1, errors.New(`failed to sync APIC flags`)
 159     }
 160     skipped += m
 161 
 162     m, err = r.Discard(1)
 163     if err != nil || m != 1 {
 164         return -1, errors.New(`failed to sync APIC text-encoding`)
 165     }
 166     skipped += m
 167 
 168     junk, err := r.ReadSlice(0)
 169     if err != nil {
 170         return -1, errors.New(`failed to sync to APIC thumbnail MIME-type`)
 171     }
 172     skipped += len(junk)
 173 
 174     m, err = r.Discard(1)
 175     if err != nil || m != 1 {
 176         return -1, errors.New(`failed to sync APIC picture type`)
 177     }
 178     skipped += m
 179 
 180     junk, err = r.ReadSlice(0)
 181     if err != nil {
 182         return -1, errors.New(`failed to sync to APIC thumbnail description`)
 183     }
 184     skipped += len(junk)
 185 
 186     return skipped, nil
 187 }
     File: ./json0/json0.go
   1 /*
   2 The MIT License (MIT)
   3 
   4 Copyright (c) 2026 pacman64
   5 
   6 Permission is hereby granted, free of charge, to any person obtaining a copy of
   7 this software and associated documentation files (the "Software"), to deal
   8 in the Software without restriction, including without limitation the rights to
   9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
  10 of the Software, and to permit persons to whom the Software is furnished to do
  11 so, subject to the following conditions:
  12 
  13 The above copyright notice and this permission notice shall be included in all
  14 copies or substantial portions of the Software.
  15 
  16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  22 SOFTWARE.
  23 */
  24 
  25 package json0
  26 
  27 import (
  28     "bufio"
  29     "errors"
  30     "io"
  31     "os"
  32     "strconv"
  33     "unicode"
  34 )
  35 
  36 const info = `
  37 json0 [options...] [file...]
  38 
  39 
  40 JSON-0 converts/fixes JSON/pseudo-JSON input into minimal JSON output.
  41 Its output is always a single line, which ends with a line-feed.
  42 
  43 Besides minimizing bytes, this tool also adapts almost-JSON input into
  44 valid JSON, since it
  45 
  46     - ignores both rest-of-line and multi-line comments
  47     - ignores extra/trailing commas in arrays and objects
  48     - turns single-quoted strings/keys into double-quoted strings
  49     - double-quotes unquoted object keys
  50     - changes \x 2-hex-digit into \u 4-hex-digit string-escapes
  51 
  52 All options available can either start with a single or a double-dash
  53 
  54     -h, -help    show this help message
  55     -jsonl       emit JSON Lines, when top-level value is an array
  56 `
  57 
  58 const (
  59     bufSize       = 32 * 1024
  60     chunkPeekSize = 16
  61 )
  62 
  63 func Main() {
  64     args := os.Args[1:]
  65     buffered := false
  66     handler := json0
  67 
  68     for len(args) > 0 {
  69         switch args[0] {
  70         case `-b`, `--b`, `-buffered`, `--buffered`:
  71             buffered = true
  72             args = args[1:]
  73             continue
  74 
  75         case `-h`, `--h`, `-help`, `--help`:
  76             os.Stdout.WriteString(info[1:])
  77             return
  78 
  79         case `-jsonl`, `--jsonl`:
  80             handler = jsonl
  81             args = args[1:]
  82             continue
  83         }
  84 
  85         break
  86     }
  87 
  88     if len(args) > 0 && args[0] == `--` {
  89         args = args[1:]
  90     }
  91 
  92     if len(args) > 1 {
  93         const msg = "multiple inputs aren't allowed\n"
  94         os.Stderr.WriteString(msg)
  95         os.Exit(1)
  96     }
  97 
  98     liveLines := !buffered
  99     if !buffered {
 100         if _, err := os.Stdout.Seek(0, io.SeekCurrent); err == nil {
 101             liveLines = false
 102         }
 103     }
 104 
 105     name := `-`
 106     if len(args) == 1 {
 107         name = args[0]
 108     }
 109 
 110     if err := run(os.Stdout, name, handler, liveLines); err != nil && err != io.EOF {
 111         os.Stderr.WriteString(err.Error())
 112         os.Stderr.WriteString("\n")
 113         os.Exit(1)
 114     }
 115 }
 116 
 117 type handlerFunc func(w *bufio.Writer, r *bufio.Reader, live bool) error
 118 
 119 func run(w io.Writer, name string, handler handlerFunc, live bool) error {
 120     // f, _ := os.Create(`json0.prof`)
 121     // defer f.Close()
 122     // pprof.StartCPUProfile(f)
 123     // defer pprof.StopCPUProfile()
 124 
 125     if name == `` || name == `-` {
 126         bw := bufio.NewWriterSize(w, bufSize)
 127         br := bufio.NewReaderSize(os.Stdin, bufSize)
 128         defer bw.Flush()
 129         return handler(bw, br, live)
 130     }
 131 
 132     f, err := os.Open(name)
 133     if err != nil {
 134         return errors.New(`can't read from file named "` + name + `"`)
 135     }
 136     defer f.Close()
 137 
 138     bw := bufio.NewWriterSize(w, bufSize)
 139     br := bufio.NewReaderSize(f, bufSize)
 140     defer bw.Flush()
 141     return handler(bw, br, live)
 142 }
 143 
 144 var (
 145     errCommentEarlyEnd = errors.New(`unexpected early-end of comment`)
 146     errInputEarlyEnd   = errors.New(`expected end of input data`)
 147     errInvalidComment  = errors.New(`expected / or *`)
 148     errInvalidHex      = errors.New(`expected a base-16 digit`)
 149     errInvalidRune     = errors.New(`invalid UTF-8 bytes`)
 150     errInvalidToken    = errors.New(`invalid JSON token`)
 151     errNoDigits        = errors.New(`expected numeric digits`)
 152     errNoStringQuote   = errors.New(`expected " or '`)
 153     errNoArrayComma    = errors.New(`missing comma between array values`)
 154     errNoObjectComma   = errors.New(`missing comma between key-value pairs`)
 155     errStringEarlyEnd  = errors.New(`unexpected early-end of string`)
 156     errExtraBytes      = errors.New(`unexpected extra input bytes`)
 157 )
 158 
 159 // linePosError is a more descriptive kind of error, showing the source of
 160 // the input-related problem, as 1-based a line/pos number pair in front
 161 // of the error message
 162 type linePosError struct {
 163     // line is the 1-based line count from the input
 164     line int
 165 
 166     // pos is the 1-based `horizontal` position in its line
 167     pos int
 168 
 169     // err is the error message to `decorate` with the position info
 170     err error
 171 }
 172 
 173 // Error satisfies the error interface
 174 func (lpe linePosError) Error() string {
 175     where := strconv.Itoa(lpe.line) + `:` + strconv.Itoa(lpe.pos)
 176     return where + `: ` + lpe.err.Error()
 177 }
 178 
 179 // isIdentifier improves control-flow of func handleKey, when it handles
 180 // unquoted object keys
 181 var isIdentifier = [256]bool{
 182     '_': true,
 183 
 184     '0': true, '1': true, '2': true, '3': true, '4': true,
 185     '5': true, '6': true, '7': true, '8': true, '9': true,
 186 
 187     'A': true, 'B': true, 'C': true, 'D': true, 'E': true, 'F': true,
 188     'G': true, 'H': true, 'I': true, 'J': true, 'K': true, 'L': true,
 189     'M': true, 'N': true, 'O': true, 'P': true, 'Q': true, 'R': true,
 190     'S': true, 'T': true, 'U': true, 'V': true, 'W': true, 'X': true,
 191     'Y': true, 'Z': true,
 192 
 193     'a': true, 'b': true, 'c': true, 'd': true, 'e': true, 'f': true,
 194     'g': true, 'h': true, 'i': true, 'j': true, 'k': true, 'l': true,
 195     'm': true, 'n': true, 'o': true, 'p': true, 'q': true, 'r': true,
 196     's': true, 't': true, 'u': true, 'v': true, 'w': true, 'x': true,
 197     'y': true, 'z': true,
 198 }
 199 
 200 // matchHex both figures out if a byte is a valid ASCII hex-digit, by not
 201 // being 0, and normalizes letter-case for the hex letters
 202 var matchHex = [256]byte{
 203     '0': '0', '1': '1', '2': '2', '3': '3', '4': '4',
 204     '5': '5', '6': '6', '7': '7', '8': '8', '9': '9',
 205     'A': 'A', 'B': 'B', 'C': 'C', 'D': 'D', 'E': 'E', 'F': 'F',
 206     'a': 'A', 'b': 'B', 'c': 'C', 'd': 'D', 'e': 'E', 'f': 'F',
 207 }
 208 
 209 // json0 converts JSON/pseudo-JSON into (valid) minimal JSON; final boolean
 210 // value isn't used, and is just there to match the signature of func jsonl
 211 func json0(w *bufio.Writer, r *bufio.Reader, live bool) error {
 212     jr := jsonReader{r, 1, 1}
 213     defer w.Flush()
 214 
 215     if err := jr.handleLeadingJunk(); err != nil {
 216         return err
 217     }
 218 
 219     // handle a single top-level JSON value
 220     err := handleValue(w, &jr)
 221 
 222     // end the only output-line with a line-feed; this also avoids showing
 223     // error messages on the same line as the main output, since JSON-0
 224     // output has no line-feeds before its last byte
 225     outputByte(w, '\n')
 226 
 227     if err != nil {
 228         return err
 229     }
 230     return jr.handleTrailingJunk()
 231 }
 232 
 233 // jsonl converts JSON/pseudo-JSON into (valid) minimal JSON Lines; this func
 234 // avoids writing a trailing line-feed, leaving that up to its caller
 235 func jsonl(w *bufio.Writer, r *bufio.Reader, live bool) error {
 236     jr := jsonReader{r, 1, 1}
 237 
 238     if err := jr.handleLeadingJunk(); err != nil {
 239         return err
 240     }
 241 
 242     chunk, err := jr.r.Peek(1)
 243     if err == nil && len(chunk) >= 1 {
 244         switch b := chunk[0]; b {
 245         case '[', '(':
 246             return handleArrayJSONL(w, &jr, b, live)
 247         }
 248     }
 249 
 250     // handle a single top-level JSON value
 251     err = handleValue(w, &jr)
 252 
 253     // end the only output-line with a line-feed; this also avoids showing
 254     // error messages on the same line as the main output, since JSON-0
 255     // output has no line-feeds before its last byte
 256     outputByte(w, '\n')
 257 
 258     if err != nil {
 259         return err
 260     }
 261     return jr.handleTrailingJunk()
 262 }
 263 
 264 // handleArrayJSONL handles top-level arrays for func jsonl
 265 func handleArrayJSONL(w *bufio.Writer, jr *jsonReader, start byte, live bool) error {
 266     if err := jr.demandSyntax(start); err != nil {
 267         return err
 268     }
 269 
 270     var end byte = ']'
 271     if start == '(' {
 272         end = ')'
 273     }
 274 
 275     for n := 0; true; n++ {
 276         // there may be whitespace/comments before the next comma
 277         if err := jr.seekNext(); err != nil {
 278             return err
 279         }
 280 
 281         // handle commas between values, as well as trailing ones
 282         comma := false
 283         b, _ := jr.peekByte()
 284         if b == ',' {
 285             jr.readByte()
 286             comma = true
 287 
 288             // there may be whitespace/comments before an ending ']'
 289             if err := jr.seekNext(); err != nil {
 290                 return err
 291             }
 292             b, _ = jr.peekByte()
 293         }
 294 
 295         // handle end of array
 296         if b == end {
 297             jr.readByte()
 298             if n > 0 {
 299                 err := outputByte(w, '\n')
 300                 if live {
 301                     w.Flush()
 302                 }
 303                 return err
 304             }
 305             return nil
 306         }
 307 
 308         // turn commas between adjacent values into line-feeds, as the
 309         // output for this custom func is supposed to be JSON Lines
 310         if n > 0 {
 311             if !comma {
 312                 return errNoArrayComma
 313             }
 314             if err := outputByte(w, '\n'); err != nil {
 315                 return err
 316             }
 317             if live {
 318                 w.Flush()
 319             }
 320         }
 321 
 322         // handle the next value
 323         if err := jr.seekNext(); err != nil {
 324             return err
 325         }
 326         if err := handleValue(w, jr); err != nil {
 327             return err
 328         }
 329     }
 330 
 331     // make the compiler happy
 332     return nil
 333 }
 334 
 335 // jsonReader reads data via a buffer, keeping track of the input position:
 336 // this in turn allows showing much more useful errors, when these happen
 337 type jsonReader struct {
 338     // r is the actual reader
 339     r *bufio.Reader
 340 
 341     // line is the 1-based line-counter for input bytes, and gives errors
 342     // useful position info
 343     line int
 344 
 345     // pos is the 1-based `horizontal` position in its line, and gives
 346     // errors useful position info
 347     pos int
 348 }
 349 
 350 // improveError makes any error more useful, by giving it info about the
 351 // current input-position, as a 1-based line/within-line-position pair
 352 func (jr jsonReader) improveError(err error) error {
 353     if _, ok := err.(linePosError); ok {
 354         return err
 355     }
 356 
 357     if err == io.EOF {
 358         return linePosError{jr.line, jr.pos, errInputEarlyEnd}
 359     }
 360     if err != nil {
 361         return linePosError{jr.line, jr.pos, err}
 362     }
 363     return nil
 364 }
 365 
 366 func (jr *jsonReader) handleLeadingJunk() error {
 367     // input is already assumed to be UTF-8: a leading UTF-8 BOM (byte-order
 368     // mark) gives no useful info if present, as UTF-8 leaves no ambiguity
 369     // about byte-order by design
 370     jr.skipUTF8BOM()
 371 
 372     // ignore leading whitespace and/or comments
 373     return jr.seekNext()
 374 }
 375 
 376 func (jr *jsonReader) handleTrailingJunk() error {
 377     // ignore trailing whitespace and/or comments
 378     if err := jr.seekNext(); err != nil {
 379         return err
 380     }
 381 
 382     // ignore trailing semicolons
 383     for {
 384         if b, ok := jr.peekByte(); !ok || b != ';' {
 385             break
 386         }
 387 
 388         jr.readByte()
 389         // ignore trailing whitespace and/or comments
 390         if err := jr.seekNext(); err != nil {
 391             return err
 392         }
 393     }
 394 
 395     // beyond trailing whitespace and/or comments, any more bytes
 396     // make the whole input data invalid JSON
 397     if _, ok := jr.peekByte(); ok {
 398         return jr.improveError(errExtraBytes)
 399     }
 400     return nil
 401 }
 402 
 403 // demandSyntax fails with an error when the next byte isn't the one given;
 404 // when it is, the byte is then read/skipped, and a nil error is returned
 405 func (jr *jsonReader) demandSyntax(syntax byte) error {
 406     chunk, err := jr.r.Peek(1)
 407     if err == io.EOF {
 408         return jr.improveError(errInputEarlyEnd)
 409     }
 410     if err != nil {
 411         return jr.improveError(err)
 412     }
 413 
 414     if len(chunk) < 1 || chunk[0] != syntax {
 415         msg := `expected ` + string(rune(syntax))
 416         return jr.improveError(errors.New(msg))
 417     }
 418 
 419     jr.readByte()
 420     return nil
 421 }
 422 
 423 // peekByte simplifies control-flow for various other funcs
 424 func (jr jsonReader) peekByte() (b byte, ok bool) {
 425     chunk, err := jr.r.Peek(1)
 426     if err == nil && len(chunk) >= 1 {
 427         return chunk[0], true
 428     }
 429     return 0, false
 430 }
 431 
 432 // readByte does what it says, updating the reader's position info
 433 func (jr *jsonReader) readByte() (b byte, err error) {
 434     b, err = jr.r.ReadByte()
 435     if err == nil {
 436         if b == '\n' {
 437             jr.line += 1
 438             jr.pos = 1
 439         } else {
 440             jr.pos++
 441         }
 442         return b, nil
 443     }
 444     return b, jr.improveError(err)
 445 }
 446 
 447 // readRune does what it says, updating the reader's position info
 448 func (jr *jsonReader) readRune() (r rune, err error) {
 449     r, _, err = jr.r.ReadRune()
 450     if err == nil {
 451         if r == '\n' {
 452             jr.line += 1
 453             jr.pos = 1
 454         } else {
 455             jr.pos++
 456         }
 457         return r, nil
 458     }
 459     return r, jr.improveError(err)
 460 }
 461 
 462 // seekNext skips/seeks the next token, ignoring runs of whitespace symbols
 463 // and comments, either single-line (starting with //) or general (starting
 464 // with /* and ending with */)
 465 func (jr *jsonReader) seekNext() error {
 466     for {
 467         b, ok := jr.peekByte()
 468         if !ok {
 469             return nil
 470         }
 471 
 472         // case ' ', '\t', '\f', '\v', '\r', '\n':
 473         if b <= 32 {
 474             // keep skipping whitespace bytes
 475             jr.readByte()
 476             continue
 477         }
 478 
 479         if b == '#' {
 480             if err := jr.skipLine(); err != nil {
 481                 return err
 482             }
 483             continue
 484         }
 485 
 486         if b != '/' {
 487             // reached the next token
 488             return nil
 489         }
 490 
 491         if err := jr.skipComment(); err != nil {
 492             return err
 493         }
 494 
 495         // after comments, keep looking for more whitespace and/or comments
 496     }
 497 }
 498 
 499 // skipComment helps func seekNext skip over comments, simplifying the latter
 500 // func's control-flow
 501 func (jr *jsonReader) skipComment() error {
 502     err := jr.demandSyntax('/')
 503     if err != nil {
 504         return err
 505     }
 506 
 507     b, ok := jr.peekByte()
 508     if !ok {
 509         return nil
 510     }
 511 
 512     switch b {
 513     case '/':
 514         // handle single-line comments
 515         return jr.skipLine()
 516 
 517     case '*':
 518         // handle (potentially) multi-line comments
 519         return jr.skipGeneralComment()
 520 
 521     default:
 522         return jr.improveError(errInvalidComment)
 523     }
 524 }
 525 
 526 // skipLine handles single-line comments for func skipComment
 527 func (jr *jsonReader) skipLine() error {
 528     for {
 529         b, err := jr.readByte()
 530         if err == io.EOF {
 531             // end of input is fine in this case
 532             return nil
 533         }
 534         if err != nil {
 535             return err
 536         }
 537 
 538         if b == '\n' {
 539             return nil
 540         }
 541     }
 542 }
 543 
 544 // skipGeneralComment handles (potentially) multi-line comments for func
 545 // skipComment
 546 func (jr *jsonReader) skipGeneralComment() error {
 547     var prev byte
 548     for {
 549         b, err := jr.readByte()
 550         if err != nil {
 551             return jr.improveError(errCommentEarlyEnd)
 552         }
 553 
 554         if prev == '*' && b == '/' {
 555             return nil
 556         }
 557         if b == '\n' {
 558             jr.line++
 559         }
 560         prev = b
 561     }
 562 }
 563 
 564 // skipUTF8BOM does what it says, if a UTF-8 BOM is present
 565 func (jr *jsonReader) skipUTF8BOM() {
 566     lead, err := jr.r.Peek(3)
 567     if err != nil {
 568         return
 569     }
 570 
 571     if len(lead) > 2 && lead[0] == 0xef && lead[1] == 0xbb && lead[2] == 0xbf {
 572         jr.readByte()
 573         jr.readByte()
 574         jr.readByte()
 575     }
 576 }
 577 
 578 // outputByte is a small wrapper on func WriteByte, which adapts any error
 579 // into a custom dummy output-error, which is in turn meant to be ignored,
 580 // being just an excuse to quit the app immediately and successfully
 581 func outputByte(w *bufio.Writer, b byte) error {
 582     err := w.WriteByte(b)
 583     if err == nil {
 584         return nil
 585     }
 586     return io.EOF
 587 }
 588 
 589 // handleArray handles arrays for func handleValue
 590 func handleArray(w *bufio.Writer, jr *jsonReader, start byte) error {
 591     if err := jr.demandSyntax(start); err != nil {
 592         return err
 593     }
 594 
 595     var end byte = ']'
 596     if start == '(' {
 597         end = ')'
 598     }
 599 
 600     w.WriteByte('[')
 601 
 602     for n := 0; true; n++ {
 603         // there may be whitespace/comments before the next comma
 604         if err := jr.seekNext(); err != nil {
 605             return err
 606         }
 607 
 608         // handle commas between values, as well as trailing ones
 609         comma := false
 610         b, _ := jr.peekByte()
 611         if b == ',' {
 612             jr.readByte()
 613             comma = true
 614 
 615             // there may be whitespace/comments before an ending ']'
 616             if err := jr.seekNext(); err != nil {
 617                 return err
 618             }
 619             b, _ = jr.peekByte()
 620         }
 621 
 622         // handle end of array
 623         if b == end {
 624             jr.readByte()
 625             w.WriteByte(']')
 626             return nil
 627         }
 628 
 629         // don't forget commas between adjacent values
 630         if n > 0 {
 631             if !comma {
 632                 return errNoArrayComma
 633             }
 634             if err := outputByte(w, ','); err != nil {
 635                 return err
 636             }
 637         }
 638 
 639         // handle the next value
 640         if err := jr.seekNext(); err != nil {
 641             return err
 642         }
 643         if err := handleValue(w, jr); err != nil {
 644             return err
 645         }
 646     }
 647 
 648     // make the compiler happy
 649     return nil
 650 }
 651 
 652 // handleDigits helps various number-handling funcs do their job
 653 func handleDigits(w *bufio.Writer, jr *jsonReader) error {
 654     if trySimpleDigits(w, jr) {
 655         return nil
 656     }
 657 
 658     for n := 0; true; n++ {
 659         b, _ := jr.peekByte()
 660 
 661         // support `nice` long numbers by ignoring their underscores
 662         if b == '_' {
 663             jr.readByte()
 664             continue
 665         }
 666 
 667         if '0' <= b && b <= '9' {
 668             jr.readByte()
 669             w.WriteByte(b)
 670             continue
 671         }
 672 
 673         if n == 0 {
 674             return errNoDigits
 675         }
 676         return nil
 677     }
 678 
 679     // make the compiler happy
 680     return nil
 681 }
 682 
 683 // trySimpleDigits tries to handle (more quickly) digit-runs where all bytes
 684 // are just digits: this is a very common case for numbers; returns whether
 685 // it succeeded, so this func's caller knows knows if it needs to do anything,
 686 // the slower way
 687 func trySimpleDigits(w *bufio.Writer, jr *jsonReader) (gotIt bool) {
 688     chunk, _ := jr.r.Peek(chunkPeekSize)
 689 
 690     for i, b := range chunk {
 691         if '0' <= b && b <= '9' {
 692             continue
 693         }
 694 
 695         if i == 0 || b == '_' {
 696             return false
 697         }
 698 
 699         // bulk-writing the chunk is this func's whole point
 700         w.Write(chunk[:i])
 701 
 702         jr.r.Discard(i)
 703         jr.pos += i
 704         return true
 705     }
 706 
 707     // maybe the digits-run is ok, but it's just longer than the chunk
 708     return false
 709 }
 710 
 711 // handleDot handles pseudo-JSON numbers which start with a decimal dot
 712 func handleDot(w *bufio.Writer, jr *jsonReader) error {
 713     if err := jr.demandSyntax('.'); err != nil {
 714         return err
 715     }
 716     w.Write([]byte{'0', '.'})
 717     return handleDigits(w, jr)
 718 }
 719 
 720 // handleKey is used by func handleObjects and generalizes func handleString,
 721 // by allowing unquoted object keys; it's not used anywhere else, as allowing
 722 // unquoted string values is ambiguous with actual JSON-keyword values null,
 723 // false, and true.
 724 func handleKey(w *bufio.Writer, jr *jsonReader) error {
 725     quote, ok := jr.peekByte()
 726     if !ok {
 727         return jr.improveError(errStringEarlyEnd)
 728     }
 729 
 730     if quote == '"' || quote == '\'' {
 731         return handleString(w, jr, quote)
 732     }
 733 
 734     w.WriteByte('"')
 735     for {
 736         if b, _ := jr.peekByte(); isIdentifier[b] {
 737             jr.readByte()
 738             w.WriteByte(b)
 739             continue
 740         }
 741 
 742         w.WriteByte('"')
 743         return nil
 744     }
 745 }
 746 
 747 // trySimpleString tries to handle (more quickly) inner-strings where all bytes
 748 // are unescaped ASCII symbols: this is a very common case for strings, and is
 749 // almost always the case for object keys; returns whether it succeeded, so
 750 // this func's caller knows knows if it needs to do anything, the slower way
 751 func trySimpleString(w *bufio.Writer, jr *jsonReader, quote byte) (gotIt bool) {
 752     end := -1
 753     chunk, _ := jr.r.Peek(chunkPeekSize)
 754 
 755     for i, b := range chunk {
 756         if 32 <= b && b <= 127 && b != '\\' && b != '\'' && b != '"' {
 757             continue
 758         }
 759 
 760         if b == byte(quote) {
 761             end = i
 762             break
 763         }
 764         return false
 765     }
 766 
 767     if end < 0 {
 768         return false
 769     }
 770 
 771     // bulk-writing the chunk is this func's whole point
 772     w.WriteByte('"')
 773     w.Write(chunk[:end])
 774     w.WriteByte('"')
 775 
 776     jr.r.Discard(end + 1)
 777     jr.pos += end + 1
 778     return true
 779 }
 780 
 781 // handleKeyword is used by funcs handleFalse, handleNull, and handleTrue
 782 func handleKeyword(w *bufio.Writer, jr *jsonReader, kw []byte) error {
 783     for rest := kw; len(rest) > 0; rest = rest[1:] {
 784         b, err := jr.readByte()
 785         if err == nil && b == rest[0] {
 786             // keywords given to this func have no line-feeds
 787             jr.pos++
 788             continue
 789         }
 790 
 791         msg := `expected JSON value ` + string(kw)
 792         return jr.improveError(errors.New(msg))
 793     }
 794 
 795     w.Write(kw)
 796     return nil
 797 }
 798 
 799 func replaceKeyword(w *bufio.Writer, jr *jsonReader, kw, with []byte) error {
 800     for rest := kw; len(rest) > 0; rest = rest[1:] {
 801         b, err := jr.readByte()
 802         if err == nil && b == rest[0] {
 803             // keywords given to this func have no line-feeds
 804             jr.pos++
 805             continue
 806         }
 807 
 808         msg := `expected JSON value ` + string(kw)
 809         return jr.improveError(errors.New(msg))
 810     }
 811 
 812     w.Write(with)
 813     return nil
 814 }
 815 
 816 // handleNegative handles numbers starting with a negative sign for func
 817 // handleValue
 818 func handleNegative(w *bufio.Writer, jr *jsonReader) error {
 819     if err := jr.demandSyntax('-'); err != nil {
 820         return err
 821     }
 822 
 823     w.WriteByte('-')
 824     if b, _ := jr.peekByte(); b == '.' {
 825         jr.readByte()
 826         w.Write([]byte{'0', '.'})
 827         return handleDigits(w, jr)
 828     }
 829     return handleNumber(w, jr)
 830 }
 831 
 832 // handleNumber handles numeric values/tokens, including invalid-JSON cases,
 833 // such as values starting with a decimal dot
 834 func handleNumber(w *bufio.Writer, jr *jsonReader) error {
 835     // handle integer digits
 836     if err := handleDigits(w, jr); err != nil {
 837         return err
 838     }
 839 
 840     // handle optional decimal digits, starting with a leading dot
 841     if b, _ := jr.peekByte(); b == '.' {
 842         jr.readByte()
 843         w.WriteByte('.')
 844         return handleDigits(w, jr)
 845     }
 846 
 847     // handle optional exponent digits
 848     if b, _ := jr.peekByte(); b == 'e' || b == 'E' {
 849         jr.readByte()
 850         w.WriteByte(b)
 851         b, _ = jr.peekByte()
 852         if b == '+' {
 853             jr.readByte()
 854         } else if b == '-' {
 855             w.WriteByte('-')
 856             jr.readByte()
 857         }
 858         return handleDigits(w, jr)
 859     }
 860 
 861     return nil
 862 }
 863 
 864 // handleObject handles objects for func handleValue
 865 func handleObject(w *bufio.Writer, jr *jsonReader) error {
 866     if err := jr.demandSyntax('{'); err != nil {
 867         return err
 868     }
 869     w.WriteByte('{')
 870 
 871     for npairs := 0; true; npairs++ {
 872         // there may be whitespace/comments before the next comma
 873         if err := jr.seekNext(); err != nil {
 874             return err
 875         }
 876 
 877         // handle commas between key-value pairs, as well as trailing ones
 878         comma := false
 879         b, _ := jr.peekByte()
 880         if b == ',' {
 881             jr.readByte()
 882             comma = true
 883 
 884             // there may be whitespace/comments before an ending '}'
 885             if err := jr.seekNext(); err != nil {
 886                 return err
 887             }
 888             b, _ = jr.peekByte()
 889         }
 890 
 891         // handle end of object
 892         if b == '}' {
 893             jr.readByte()
 894             w.WriteByte('}')
 895             return nil
 896         }
 897 
 898         // don't forget commas between adjacent key-value pairs
 899         if npairs > 0 {
 900             if !comma {
 901                 return errNoObjectComma
 902             }
 903             if err := outputByte(w, ','); err != nil {
 904                 return err
 905             }
 906         }
 907 
 908         // handle the next pair's key
 909         if err := jr.seekNext(); err != nil {
 910             return err
 911         }
 912         if err := handleKey(w, jr); err != nil {
 913             return err
 914         }
 915 
 916         // demand a colon right after the key
 917         if err := jr.seekNext(); err != nil {
 918             return err
 919         }
 920         if err := jr.demandSyntax(':'); err != nil {
 921             return err
 922         }
 923         w.WriteByte(':')
 924 
 925         // handle the next pair's value
 926         if err := jr.seekNext(); err != nil {
 927             return err
 928         }
 929         if err := handleValue(w, jr); err != nil {
 930             return err
 931         }
 932     }
 933 
 934     // make the compiler happy
 935     return nil
 936 }
 937 
 938 // handlePositive handles numbers starting with a positive sign for func
 939 // handleValue
 940 func handlePositive(w *bufio.Writer, jr *jsonReader) error {
 941     if err := jr.demandSyntax('+'); err != nil {
 942         return err
 943     }
 944 
 945     // valid JSON isn't supposed to have leading pluses on numbers, so
 946     // emit nothing for it, unlike for negative numbers
 947 
 948     if b, _ := jr.peekByte(); b == '.' {
 949         jr.readByte()
 950         w.Write([]byte{'0', '.'})
 951         return handleDigits(w, jr)
 952     }
 953     return handleNumber(w, jr)
 954 }
 955 
 956 // handleString handles strings for funcs handleValue and handleObject, and
 957 // supports both single-quotes and double-quotes, always emitting the latter
 958 // in the output, of course
 959 func handleString(w *bufio.Writer, jr *jsonReader, quote byte) error {
 960     if quote != '"' && quote != '\'' {
 961         return errNoStringQuote
 962     }
 963 
 964     jr.readByte()
 965 
 966     // try the quicker no-escapes ASCII handler
 967     if trySimpleString(w, jr, quote) {
 968         return nil
 969     }
 970 
 971     // it's a non-trivial inner-string, so handle it byte-by-byte
 972     w.WriteByte('"')
 973     escaped := false
 974 
 975     for quote := rune(quote); true; {
 976         r, err := jr.readRune()
 977         if r == unicode.ReplacementChar {
 978             return jr.improveError(errInvalidRune)
 979         }
 980         if err != nil {
 981             if err == io.EOF {
 982                 return jr.improveError(errStringEarlyEnd)
 983             }
 984             return jr.improveError(err)
 985         }
 986 
 987         if !escaped {
 988             if r == '\\' {
 989                 escaped = true
 990                 continue
 991             }
 992 
 993             // handle end of string
 994             if r == quote {
 995                 return outputByte(w, '"')
 996             }
 997 
 998             if r <= 127 {
 999                 w.Write(escapedStringBytes[byte(r)])
1000             } else {
1001                 w.WriteRune(r)
1002             }
1003             continue
1004         }
1005 
1006         // handle escaped items
1007         escaped = false
1008 
1009         switch r {
1010         case 'u':
1011             // \u needs exactly 4 hex-digits to follow it
1012             w.Write([]byte{'\\', 'u'})
1013             if err := copyHex(w, 4, jr); err != nil {
1014                 return jr.improveError(err)
1015             }
1016 
1017         case 'x':
1018             // JSON only supports 4 escaped hex-digits, so pad the 2
1019             // expected hex-digits with 2 zeros
1020             w.Write([]byte{'\\', 'u', '0', '0'})
1021             if err := copyHex(w, 2, jr); err != nil {
1022                 return jr.improveError(err)
1023             }
1024 
1025         case 't', 'f', 'r', 'n', 'b', '\\', '"':
1026             // handle valid-JSON escaped string sequences
1027             w.WriteByte('\\')
1028             w.WriteByte(byte(r))
1029 
1030         case '\'':
1031             // escaped single-quotes aren't standard JSON, but they can
1032             // be handy when the input uses non-standard single-quoted
1033             // strings
1034             w.WriteByte('\'')
1035 
1036         default:
1037             if r <= 127 {
1038                 w.Write(escapedStringBytes[byte(r)])
1039             } else {
1040                 w.WriteRune(r)
1041             }
1042         }
1043     }
1044 
1045     return nil
1046 }
1047 
1048 // copyHex handles a run of hex-digits for func handleString, starting right
1049 // after the leading `\u` (or `\x`) part; this func doesn't `improve` its
1050 // errors with position info: that's up to the caller
1051 func copyHex(w *bufio.Writer, n int, jr *jsonReader) error {
1052     for i := 0; i < n; i++ {
1053         b, err := jr.readByte()
1054         if err == io.EOF {
1055             return errStringEarlyEnd
1056         }
1057         if err != nil {
1058             return err
1059         }
1060 
1061         if b >= 128 {
1062             return errInvalidHex
1063         }
1064 
1065         if b := matchHex[b]; b != 0 {
1066             w.WriteByte(b)
1067             continue
1068         }
1069 
1070         return errInvalidHex
1071     }
1072 
1073     return nil
1074 }
1075 
1076 // handleValue is a generic JSON-token handler, which allows the recursive
1077 // behavior to handle any kind of JSON/pseudo-JSON input
1078 func handleValue(w *bufio.Writer, jr *jsonReader) error {
1079     chunk, err := jr.r.Peek(1)
1080     if err == nil && len(chunk) >= 1 {
1081         return handleValueDispatch(w, jr, chunk[0])
1082     }
1083 
1084     if err == io.EOF {
1085         return jr.improveError(errInputEarlyEnd)
1086     }
1087     return jr.improveError(errInputEarlyEnd)
1088 }
1089 
1090 // handleValueDispatch simplifies control-flow for func handleValue
1091 func handleValueDispatch(w *bufio.Writer, jr *jsonReader, b byte) error {
1092     switch b {
1093     case '#':
1094         return jr.skipLine()
1095     case 'f':
1096         return handleKeyword(w, jr, []byte{'f', 'a', 'l', 's', 'e'})
1097     case 'n':
1098         return handleKeyword(w, jr, []byte{'n', 'u', 'l', 'l'})
1099     case 't':
1100         return handleKeyword(w, jr, []byte{'t', 'r', 'u', 'e'})
1101     case 'F':
1102         return replaceKeyword(w, jr, []byte(`False`), []byte(`false`))
1103     case 'N':
1104         return replaceKeyword(w, jr, []byte(`None`), []byte(`null`))
1105     case 'T':
1106         return replaceKeyword(w, jr, []byte(`True`), []byte(`true`))
1107     case '.':
1108         return handleDot(w, jr)
1109     case '+':
1110         return handlePositive(w, jr)
1111     case '-':
1112         return handleNegative(w, jr)
1113     case '0', '1', '2', '3', '4', '5', '6', '7', '8', '9':
1114         return handleNumber(w, jr)
1115     case '\'', '"':
1116         return handleString(w, jr, b)
1117     case '[', '(':
1118         return handleArray(w, jr, b)
1119     case '{':
1120         return handleObject(w, jr)
1121     default:
1122         return jr.improveError(errInvalidToken)
1123     }
1124 }
1125 
1126 // escapedStringBytes helps func handleString treat all string bytes quickly
1127 // and correctly, using their officially-supported JSON escape sequences
1128 //
1129 // https://www.rfc-editor.org/rfc/rfc8259#section-7
1130 var escapedStringBytes = [256][]byte{
1131     {'\\', 'u', '0', '0', '0', '0'}, {'\\', 'u', '0', '0', '0', '1'},
1132     {'\\', 'u', '0', '0', '0', '2'}, {'\\', 'u', '0', '0', '0', '3'},
1133     {'\\', 'u', '0', '0', '0', '4'}, {'\\', 'u', '0', '0', '0', '5'},
1134     {'\\', 'u', '0', '0', '0', '6'}, {'\\', 'u', '0', '0', '0', '7'},
1135     {'\\', 'b'}, {'\\', 't'},
1136     {'\\', 'n'}, {'\\', 'u', '0', '0', '0', 'b'},
1137     {'\\', 'f'}, {'\\', 'r'},
1138     {'\\', 'u', '0', '0', '0', 'e'}, {'\\', 'u', '0', '0', '0', 'f'},
1139     {'\\', 'u', '0', '0', '1', '0'}, {'\\', 'u', '0', '0', '1', '1'},
1140     {'\\', 'u', '0', '0', '1', '2'}, {'\\', 'u', '0', '0', '1', '3'},
1141     {'\\', 'u', '0', '0', '1', '4'}, {'\\', 'u', '0', '0', '1', '5'},
1142     {'\\', 'u', '0', '0', '1', '6'}, {'\\', 'u', '0', '0', '1', '7'},
1143     {'\\', 'u', '0', '0', '1', '8'}, {'\\', 'u', '0', '0', '1', '9'},
1144     {'\\', 'u', '0', '0', '1', 'a'}, {'\\', 'u', '0', '0', '1', 'b'},
1145     {'\\', 'u', '0', '0', '1', 'c'}, {'\\', 'u', '0', '0', '1', 'd'},
1146     {'\\', 'u', '0', '0', '1', 'e'}, {'\\', 'u', '0', '0', '1', 'f'},
1147     {32}, {33}, {'\\', '"'}, {35}, {36}, {37}, {38}, {39},
1148     {40}, {41}, {42}, {43}, {44}, {45}, {46}, {47},
1149     {48}, {49}, {50}, {51}, {52}, {53}, {54}, {55},
1150     {56}, {57}, {58}, {59}, {60}, {61}, {62}, {63},
1151     {64}, {65}, {66}, {67}, {68}, {69}, {70}, {71},
1152     {72}, {73}, {74}, {75}, {76}, {77}, {78}, {79},
1153     {80}, {81}, {82}, {83}, {84}, {85}, {86}, {87},
1154     {88}, {89}, {90}, {91}, {'\\', '\\'}, {93}, {94}, {95},
1155     {96}, {97}, {98}, {99}, {100}, {101}, {102}, {103},
1156     {104}, {105}, {106}, {107}, {108}, {109}, {110}, {111},
1157     {112}, {113}, {114}, {115}, {116}, {117}, {118}, {119},
1158     {120}, {121}, {122}, {123}, {124}, {125}, {126}, {127},
1159     {128}, {129}, {130}, {131}, {132}, {133}, {134}, {135},
1160     {136}, {137}, {138}, {139}, {140}, {141}, {142}, {143},
1161     {144}, {145}, {146}, {147}, {148}, {149}, {150}, {151},
1162     {152}, {153}, {154}, {155}, {156}, {157}, {158}, {159},
1163     {160}, {161}, {162}, {163}, {164}, {165}, {166}, {167},
1164     {168}, {169}, {170}, {171}, {172}, {173}, {174}, {175},
1165     {176}, {177}, {178}, {179}, {180}, {181}, {182}, {183},
1166     {184}, {185}, {186}, {187}, {188}, {189}, {190}, {191},
1167     {192}, {193}, {194}, {195}, {196}, {197}, {198}, {199},
1168     {200}, {201}, {202}, {203}, {204}, {205}, {206}, {207},
1169     {208}, {209}, {210}, {211}, {212}, {213}, {214}, {215},
1170     {216}, {217}, {218}, {219}, {220}, {221}, {222}, {223},
1171     {224}, {225}, {226}, {227}, {228}, {229}, {230}, {231},
1172     {232}, {233}, {234}, {235}, {236}, {237}, {238}, {239},
1173     {240}, {241}, {242}, {243}, {244}, {245}, {246}, {247},
1174     {248}, {249}, {250}, {251}, {252}, {253}, {254}, {255},
1175 }
     File: ./json0/json_test.go
   1 /*
   2 The MIT License (MIT)
   3 
   4 Copyright (c) 2026 pacman64
   5 
   6 Permission is hereby granted, free of charge, to any person obtaining a copy of
   7 this software and associated documentation files (the "Software"), to deal
   8 in the Software without restriction, including without limitation the rights to
   9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
  10 of the Software, and to permit persons to whom the Software is furnished to do
  11 so, subject to the following conditions:
  12 
  13 The above copyright notice and this permission notice shall be included in all
  14 copies or substantial portions of the Software.
  15 
  16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  22 SOFTWARE.
  23 */
  24 
  25 package json0
  26 
  27 import (
  28     "bufio"
  29     "bytes"
  30     "io"
  31     "strings"
  32     "testing"
  33 )
  34 
  35 func TestJSON0(t *testing.T) {
  36     var tests = []struct {
  37         Input    string
  38         Expected string
  39     }{
  40         {`false`, `false`},
  41         {`null`, `null`},
  42         {`  true  `, `true`},
  43 
  44         {`False`, `false`},
  45         {`None`, `null`},
  46         {`  True  `, `true`},
  47 
  48         {`0`, `0`},
  49         {`1`, `1`},
  50         {`2`, `2`},
  51         {`3`, `3`},
  52         {`4`, `4`},
  53         {`5`, `5`},
  54         {`6`, `6`},
  55         {`7`, `7`},
  56         {`8`, `8`},
  57         {`9`, `9`},
  58 
  59         {`  .345`, `0.345`},
  60         {` -.345`, `-0.345`},
  61         {` +.345`, `0.345`},
  62         {` +123.345`, `123.345`},
  63         {` +.345`, `0.345`},
  64         {` 123.34523`, `123.34523`},
  65         {` 123.34_523`, `123.34523`},
  66         {` 123_456.123`, `123456.123`},
  67 
  68         {`""`, `""`},
  69         {`''`, `""`},
  70         {`"\""`, `"\""`},
  71         {`'\"'`, `"\""`},
  72         {`'\''`, `"'"`},
  73         {`'abc\u0e9A'`, `"abc\u0E9A"`},
  74         {`'abc\x1f[0m'`, `"abc\u001F[0m"`},
  75         {`"abc●def"`, `"abc●def"`},
  76 
  77         {`[  ]`, `[]`},
  78         {`[ , ]`, `[]`},
  79         {`[.345, false,null , ]`, `[0.345,false,null]`},
  80 
  81         {`(  )`, `[]`},
  82         {`( , )`, `[]`},
  83         {`(.345, false,null , )`, `[0.345,false,null]`},
  84 
  85         {`{  }`, `{}`},
  86         {`{ , }`, `{}`},
  87 
  88         {
  89             `{ 'abc': .345, "def"  : false, 'xyz':null , }`,
  90             `{"abc":0.345,"def":false,"xyz":null}`,
  91         },
  92 
  93         {`{0problems:123,}`, `{"0problems":123}`},
  94         {`{0_problems:123}`, `{"0_problems":123}`},
  95     }
  96 
  97     for _, tc := range tests {
  98         t.Run(tc.Input, func(t *testing.T) {
  99             var out strings.Builder
 100             w := bufio.NewWriter(&out)
 101             r := bufio.NewReader(strings.NewReader(tc.Input))
 102             if err := json0(w, r, false); err != nil && err != io.EOF {
 103                 t.Fatal(err)
 104                 return
 105             }
 106             // don't forget to flush the buffer, or output will be empty
 107             w.Flush()
 108 
 109             // output may have a final line-feed: get rid of it, or every
 110             // single test-case will fail
 111             got := out.String()
 112             if len(got) > 0 && got[len(got)-1] == '\n' {
 113                 got = got[:len(got)-1]
 114             }
 115 
 116             if got != tc.Expected {
 117                 t.Fatalf("<got>\n%s\n<expected>\n%s", got, tc.Expected)
 118                 return
 119             }
 120         })
 121     }
 122 }
 123 
 124 func TestEscapedStringBytes(t *testing.T) {
 125     var escaped = map[rune][]byte{
 126         '\x00': {'\\', 'u', '0', '0', '0', '0'},
 127         '\x01': {'\\', 'u', '0', '0', '0', '1'},
 128         '\x02': {'\\', 'u', '0', '0', '0', '2'},
 129         '\x03': {'\\', 'u', '0', '0', '0', '3'},
 130         '\x04': {'\\', 'u', '0', '0', '0', '4'},
 131         '\x05': {'\\', 'u', '0', '0', '0', '5'},
 132         '\x06': {'\\', 'u', '0', '0', '0', '6'},
 133         '\x07': {'\\', 'u', '0', '0', '0', '7'},
 134         '\x0b': {'\\', 'u', '0', '0', '0', 'b'},
 135         '\x0e': {'\\', 'u', '0', '0', '0', 'e'},
 136         '\x0f': {'\\', 'u', '0', '0', '0', 'f'},
 137         '\x10': {'\\', 'u', '0', '0', '1', '0'},
 138         '\x11': {'\\', 'u', '0', '0', '1', '1'},
 139         '\x12': {'\\', 'u', '0', '0', '1', '2'},
 140         '\x13': {'\\', 'u', '0', '0', '1', '3'},
 141         '\x14': {'\\', 'u', '0', '0', '1', '4'},
 142         '\x15': {'\\', 'u', '0', '0', '1', '5'},
 143         '\x16': {'\\', 'u', '0', '0', '1', '6'},
 144         '\x17': {'\\', 'u', '0', '0', '1', '7'},
 145         '\x18': {'\\', 'u', '0', '0', '1', '8'},
 146         '\x19': {'\\', 'u', '0', '0', '1', '9'},
 147         '\x1a': {'\\', 'u', '0', '0', '1', 'a'},
 148         '\x1b': {'\\', 'u', '0', '0', '1', 'b'},
 149         '\x1c': {'\\', 'u', '0', '0', '1', 'c'},
 150         '\x1d': {'\\', 'u', '0', '0', '1', 'd'},
 151         '\x1e': {'\\', 'u', '0', '0', '1', 'e'},
 152         '\x1f': {'\\', 'u', '0', '0', '1', 'f'},
 153 
 154         '\t': {'\\', 't'},
 155         '\f': {'\\', 'f'},
 156         '\b': {'\\', 'b'},
 157         '\r': {'\\', 'r'},
 158         '\n': {'\\', 'n'},
 159         '\\': {'\\', '\\'},
 160         '"':  {'\\', '"'},
 161     }
 162 
 163     if n := len(escapedStringBytes); n != 256 {
 164         t.Errorf(`expected 256 entries, instead of %d`, n)
 165     }
 166 
 167     for i, v := range escapedStringBytes {
 168         exp := []byte{byte(i)}
 169         if esc, ok := escaped[rune(i)]; ok {
 170             exp = esc
 171         }
 172 
 173         if !bytes.Equal(v, exp) {
 174             t.Errorf("%d: expected %#v, got %#v", i, exp, v)
 175         }
 176     }
 177 }
     File: ./json0/tables_test.go
   1 /*
   2 The MIT License (MIT)
   3 
   4 Copyright (c) 2026 pacman64
   5 
   6 Permission is hereby granted, free of charge, to any person obtaining a copy of
   7 this software and associated documentation files (the "Software"), to deal
   8 in the Software without restriction, including without limitation the rights to
   9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
  10 of the Software, and to permit persons to whom the Software is furnished to do
  11 so, subject to the following conditions:
  12 
  13 The above copyright notice and this permission notice shall be included in all
  14 copies or substantial portions of the Software.
  15 
  16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  22 SOFTWARE.
  23 */
  24 
  25 package json0
  26 
  27 import (
  28     "bytes"
  29     "encoding/json"
  30     "testing"
  31 )
  32 
  33 func TestTables(t *testing.T) {
  34     var buf []byte
  35 
  36     for i := 0; i < 128; i++ {
  37         r := rune(i)
  38         exp := buf[:0]
  39         exp = append(exp, '"')
  40         exp = append(exp, escapedStringBytes[i]...)
  41         exp = append(exp, '"')
  42 
  43         got, err := json.Marshal(string(r))
  44         if err != nil {
  45             t.Error(err)
  46             continue
  47         }
  48 
  49         if bytes.Equal(got, exp) {
  50             continue
  51         }
  52 
  53         // can't fully rely on the JSON string-encoder from the stdlib
  54         switch r {
  55         case '&', '<', '>':
  56             continue
  57         }
  58 
  59         const fs = "escaped-strings entry @ %d: expected %q, got %q"
  60         t.Errorf(fs, i, string(exp), string(got))
  61     }
  62 }
     File: ./json2/json2.go
   1 /*
   2 The MIT License (MIT)
   3 
   4 Copyright (c) 2026 pacman64
   5 
   6 Permission is hereby granted, free of charge, to any person obtaining a copy of
   7 this software and associated documentation files (the "Software"), to deal
   8 in the Software without restriction, including without limitation the rights to
   9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
  10 of the Software, and to permit persons to whom the Software is furnished to do
  11 so, subject to the following conditions:
  12 
  13 The above copyright notice and this permission notice shall be included in all
  14 copies or substantial portions of the Software.
  15 
  16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  22 SOFTWARE.
  23 */
  24 
  25 package json2
  26 
  27 import (
  28     "bufio"
  29     "encoding/json"
  30     "errors"
  31     "io"
  32     "os"
  33 )
  34 
  35 const info = `
  36 json2 [filepath...]
  37 
  38 JSON-2 indents valid JSON input into multi-line JSON which uses 2 spaces for
  39 each indentation level.
  40 `
  41 
  42 func Main() {
  43     args := os.Args[1:]
  44 
  45     if len(args) > 0 {
  46         switch args[0] {
  47         case `-h`, `--h`, `-help`, `--help`:
  48             os.Stdout.WriteString(info[1:])
  49             return
  50         }
  51     }
  52 
  53     if len(args) > 0 && args[0] == `--` {
  54         args = args[1:]
  55     }
  56 
  57     if len(args) > 1 {
  58         const msg = "multiple inputs aren't allowed\n"
  59         os.Stderr.WriteString(msg)
  60         os.Exit(1)
  61     }
  62 
  63     // figure out whether input should come from a named file or from stdin
  64     name := `-`
  65     if len(args) == 1 {
  66         name = args[0]
  67     }
  68 
  69     if err := handleInput(os.Stdout, name); err != nil && err != io.EOF {
  70         os.Stderr.WriteString(err.Error())
  71         os.Stderr.WriteString("\n")
  72         os.Exit(1)
  73     }
  74 }
  75 
  76 // handleInput simplifies control-flow for func main
  77 func handleInput(w io.Writer, path string) error {
  78     if path == `-` {
  79         return convert(w, os.Stdin)
  80     }
  81 
  82     f, err := os.Open(path)
  83     if err != nil {
  84         // on windows, file-not-found error messages may mention `CreateFile`,
  85         // even when trying to open files in read-only mode
  86         return errors.New(`can't open file named ` + path)
  87     }
  88     defer f.Close()
  89     return convert(w, f)
  90 }
  91 
  92 // convert simplifies control-flow for func handleInput
  93 func convert(w io.Writer, r io.Reader) error {
  94     bw := bufio.NewWriter(w)
  95     defer bw.Flush()
  96     return json2(bw, r)
  97 }
  98 
  99 // escapedStringBytes helps func handleString treat all string bytes quickly
 100 // and correctly, using their officially-supported JSON escape sequences
 101 //
 102 // https://www.rfc-editor.org/rfc/rfc8259#section-7
 103 var escapedStringBytes = [256][]byte{
 104     {'\\', 'u', '0', '0', '0', '0'}, {'\\', 'u', '0', '0', '0', '1'},
 105     {'\\', 'u', '0', '0', '0', '2'}, {'\\', 'u', '0', '0', '0', '3'},
 106     {'\\', 'u', '0', '0', '0', '4'}, {'\\', 'u', '0', '0', '0', '5'},
 107     {'\\', 'u', '0', '0', '0', '6'}, {'\\', 'u', '0', '0', '0', '7'},
 108     {'\\', 'b'}, {'\\', 't'},
 109     {'\\', 'n'}, {'\\', 'u', '0', '0', '0', 'b'},
 110     {'\\', 'f'}, {'\\', 'r'},
 111     {'\\', 'u', '0', '0', '0', 'e'}, {'\\', 'u', '0', '0', '0', 'f'},
 112     {'\\', 'u', '0', '0', '1', '0'}, {'\\', 'u', '0', '0', '1', '1'},
 113     {'\\', 'u', '0', '0', '1', '2'}, {'\\', 'u', '0', '0', '1', '3'},
 114     {'\\', 'u', '0', '0', '1', '4'}, {'\\', 'u', '0', '0', '1', '5'},
 115     {'\\', 'u', '0', '0', '1', '6'}, {'\\', 'u', '0', '0', '1', '7'},
 116     {'\\', 'u', '0', '0', '1', '8'}, {'\\', 'u', '0', '0', '1', '9'},
 117     {'\\', 'u', '0', '0', '1', 'a'}, {'\\', 'u', '0', '0', '1', 'b'},
 118     {'\\', 'u', '0', '0', '1', 'c'}, {'\\', 'u', '0', '0', '1', 'd'},
 119     {'\\', 'u', '0', '0', '1', 'e'}, {'\\', 'u', '0', '0', '1', 'f'},
 120     {32}, {33}, {'\\', '"'}, {35}, {36}, {37}, {38}, {39},
 121     {40}, {41}, {42}, {43}, {44}, {45}, {46}, {47},
 122     {48}, {49}, {50}, {51}, {52}, {53}, {54}, {55},
 123     {56}, {57}, {58}, {59}, {60}, {61}, {62}, {63},
 124     {64}, {65}, {66}, {67}, {68}, {69}, {70}, {71},
 125     {72}, {73}, {74}, {75}, {76}, {77}, {78}, {79},
 126     {80}, {81}, {82}, {83}, {84}, {85}, {86}, {87},
 127     {88}, {89}, {90}, {91}, {'\\', '\\'}, {93}, {94}, {95},
 128     {96}, {97}, {98}, {99}, {100}, {101}, {102}, {103},
 129     {104}, {105}, {106}, {107}, {108}, {109}, {110}, {111},
 130     {112}, {113}, {114}, {115}, {116}, {117}, {118}, {119},
 131     {120}, {121}, {122}, {123}, {124}, {125}, {126}, {127},
 132     {128}, {129}, {130}, {131}, {132}, {133}, {134}, {135},
 133     {136}, {137}, {138}, {139}, {140}, {141}, {142}, {143},
 134     {144}, {145}, {146}, {147}, {148}, {149}, {150}, {151},
 135     {152}, {153}, {154}, {155}, {156}, {157}, {158}, {159},
 136     {160}, {161}, {162}, {163}, {164}, {165}, {166}, {167},
 137     {168}, {169}, {170}, {171}, {172}, {173}, {174}, {175},
 138     {176}, {177}, {178}, {179}, {180}, {181}, {182}, {183},
 139     {184}, {185}, {186}, {187}, {188}, {189}, {190}, {191},
 140     {192}, {193}, {194}, {195}, {196}, {197}, {198}, {199},
 141     {200}, {201}, {202}, {203}, {204}, {205}, {206}, {207},
 142     {208}, {209}, {210}, {211}, {212}, {213}, {214}, {215},
 143     {216}, {217}, {218}, {219}, {220}, {221}, {222}, {223},
 144     {224}, {225}, {226}, {227}, {228}, {229}, {230}, {231},
 145     {232}, {233}, {234}, {235}, {236}, {237}, {238}, {239},
 146     {240}, {241}, {242}, {243}, {244}, {245}, {246}, {247},
 147     {248}, {249}, {250}, {251}, {252}, {253}, {254}, {255},
 148 }
 149 
 150 // writeSpaces does what it says, minimizing calls to write-like funcs
 151 func writeSpaces(w *bufio.Writer, n int) {
 152     const spaces = `                                `
 153     if n < 1 {
 154         return
 155     }
 156 
 157     for n >= len(spaces) {
 158         w.WriteString(spaces)
 159         n -= len(spaces)
 160     }
 161     w.WriteString(spaces[:n])
 162 }
 163 
 164 // json2 does it all, given a reader and a writer
 165 func json2(w *bufio.Writer, r io.Reader) error {
 166     dec := json.NewDecoder(r)
 167     // avoid parsing numbers, so unusually-long numbers are kept verbatim,
 168     // even if JSON parsers aren't required to guarantee such input-fidelity
 169     // for numbers
 170     dec.UseNumber()
 171 
 172     t, err := dec.Token()
 173     if err == io.EOF {
 174         return errors.New(`input has no JSON values`)
 175     }
 176 
 177     if err = handleToken(w, dec, t, 0, 0); err != nil {
 178         return err
 179     }
 180     // don't forget ending the last line for the last value
 181     w.WriteByte('\n')
 182 
 183     _, err = dec.Token()
 184     if err == io.EOF {
 185         // input is over, so it's a success
 186         return nil
 187     }
 188 
 189     if err == nil {
 190         // a successful `read` is a failure, as it means there are
 191         // trailing JSON tokens
 192         return errors.New(`unexpected trailing data`)
 193     }
 194 
 195     // any other error, perhaps some invalid-JSON-syntax-type error
 196     return err
 197 }
 198 
 199 // handleToken handles recursion for func json2
 200 func handleToken(w *bufio.Writer, dec *json.Decoder, t json.Token, pre, level int) error {
 201     switch t := t.(type) {
 202     case json.Delim:
 203         switch t {
 204         case json.Delim('['):
 205             return handleArray(w, dec, pre, level)
 206         case json.Delim('{'):
 207             return handleObject(w, dec, pre, level)
 208         default:
 209             return errors.New(`unsupported JSON syntax ` + string(t))
 210         }
 211 
 212     case nil:
 213         writeSpaces(w, 2*pre)
 214         w.WriteString(`null`)
 215         return nil
 216 
 217     case bool:
 218         writeSpaces(w, 2*pre)
 219         if t {
 220             w.WriteString(`true`)
 221         } else {
 222             w.WriteString(`false`)
 223         }
 224         return nil
 225 
 226     case json.Number:
 227         writeSpaces(w, 2*pre)
 228         w.WriteString(t.String())
 229         return nil
 230 
 231     case string:
 232         return handleString(w, t, pre)
 233 
 234     default:
 235         // return fmt.Errorf(`unsupported token type %T`, t)
 236         return errors.New(`invalid JSON token`)
 237     }
 238 }
 239 
 240 // handleArray handles arrays for func handleToken
 241 func handleArray(w *bufio.Writer, dec *json.Decoder, pre, level int) error {
 242     for i := 0; true; i++ {
 243         t, err := dec.Token()
 244         if err != nil {
 245             return err
 246         }
 247 
 248         if t == json.Delim(']') {
 249             if i == 0 {
 250                 writeSpaces(w, 2*pre)
 251                 w.WriteByte('[')
 252                 w.WriteByte(']')
 253             } else {
 254                 w.WriteByte('\n')
 255                 writeSpaces(w, 2*level)
 256                 w.WriteByte(']')
 257             }
 258             return nil
 259         }
 260 
 261         if i == 0 {
 262             writeSpaces(w, 2*pre)
 263             w.WriteByte('[')
 264             w.WriteByte('\n')
 265         } else {
 266             w.WriteByte(',')
 267             w.WriteByte('\n')
 268             if err := w.Flush(); err != nil {
 269                 // a write error may be the consequence of stdout being closed,
 270                 // perhaps by another app along a pipe
 271                 return io.EOF
 272             }
 273         }
 274 
 275         err = handleToken(w, dec, t, level+1, level+1)
 276         if err != nil {
 277             return err
 278         }
 279     }
 280 
 281     // make the compiler happy
 282     return nil
 283 }
 284 
 285 // handleObject handles objects for func handleToken
 286 func handleObject(w *bufio.Writer, dec *json.Decoder, pre, level int) error {
 287     for i := 0; true; i++ {
 288         t, err := dec.Token()
 289         if err != nil {
 290             return err
 291         }
 292 
 293         if t == json.Delim('}') {
 294             if i == 0 {
 295                 writeSpaces(w, 2*pre)
 296                 w.WriteByte('{')
 297                 w.WriteByte('}')
 298             } else {
 299                 w.WriteByte('\n')
 300                 writeSpaces(w, 2*level)
 301                 w.WriteByte('}')
 302             }
 303             return nil
 304         }
 305 
 306         if i == 0 {
 307             writeSpaces(w, 2*pre)
 308             w.WriteByte('{')
 309             w.WriteByte('\n')
 310         } else {
 311             w.WriteByte(',')
 312             w.WriteByte('\n')
 313         }
 314 
 315         k, ok := t.(string)
 316         if !ok {
 317             return errors.New(`expected a string for a key-value pair`)
 318         }
 319 
 320         err = handleString(w, k, level+1)
 321         if err != nil {
 322             return err
 323         }
 324 
 325         w.WriteString(": ")
 326 
 327         t, err = dec.Token()
 328         if err == io.EOF {
 329             return errors.New(`expected a value for a key-value pair`)
 330         }
 331 
 332         err = handleToken(w, dec, t, 0, level+1)
 333         if err != nil {
 334             return err
 335         }
 336     }
 337 
 338     // make the compiler happy
 339     return nil
 340 }
 341 
 342 // handleString handles strings for func handleToken, and keys for func
 343 // handleObject
 344 func handleString(w *bufio.Writer, s string, level int) error {
 345     writeSpaces(w, 2*level)
 346     w.WriteByte('"')
 347     for i := range s {
 348         w.Write(escapedStringBytes[s[i]])
 349     }
 350     w.WriteByte('"')
 351     return nil
 352 }
     File: ./jsonl/jsonl.go
   1 /*
   2 The MIT License (MIT)
   3 
   4 Copyright (c) 2026 pacman64
   5 
   6 Permission is hereby granted, free of charge, to any person obtaining a copy of
   7 this software and associated documentation files (the "Software"), to deal
   8 in the Software without restriction, including without limitation the rights to
   9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
  10 of the Software, and to permit persons to whom the Software is furnished to do
  11 so, subject to the following conditions:
  12 
  13 The above copyright notice and this permission notice shall be included in all
  14 copies or substantial portions of the Software.
  15 
  16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  22 SOFTWARE.
  23 */
  24 
  25 package jsonl
  26 
  27 import (
  28     "bufio"
  29     "encoding/json"
  30     "errors"
  31     "io"
  32     "os"
  33 )
  34 
  35 const info = `
  36 jsonl [options...] [filepaths...]
  37 
  38 JSON Lines turns valid JSON-input arrays into separate JSON lines, one for
  39 each top-level item. Non-arrays result in a single JSON-line.
  40 
  41 When not given a filepath to load, standard input is used instead. Every
  42 output line is always a single top-level item from the input.
  43 `
  44 
  45 func Main() {
  46     args := os.Args[1:]
  47     buffered := false
  48 
  49     for len(args) > 0 {
  50         switch args[0] {
  51         case `-b`, `--b`, `-buffered`, `--buffered`:
  52             buffered = true
  53             args = args[1:]
  54             continue
  55 
  56         case `-h`, `--h`, `-help`, `--help`:
  57             os.Stdout.WriteString(info[1:])
  58             return
  59         }
  60 
  61         break
  62     }
  63 
  64     if len(args) > 0 && args[0] == `--` {
  65         args = args[1:]
  66     }
  67 
  68     liveLines := !buffered
  69     if !buffered {
  70         if _, err := os.Stdout.Seek(0, io.SeekCurrent); err == nil {
  71             liveLines = false
  72         }
  73     }
  74 
  75     if err := run(os.Stdout, args, liveLines); err != nil && err != io.EOF {
  76         os.Stderr.WriteString(err.Error())
  77         os.Stderr.WriteString("\n")
  78         os.Exit(1)
  79     }
  80 }
  81 
  82 func run(w io.Writer, args []string, liveLines bool) error {
  83     dashes := 0
  84     for _, path := range args {
  85         if path == `-` {
  86             dashes++
  87         }
  88         if dashes > 1 {
  89             return errors.New(`can't use stdin (dash) more than once`)
  90         }
  91     }
  92 
  93     bw := bufio.NewWriter(w)
  94     defer bw.Flush()
  95 
  96     if len(args) == 0 {
  97         return handleInput(bw, `-`, liveLines)
  98     }
  99 
 100     for _, path := range args {
 101         if err := handleInput(bw, path, liveLines); err != nil {
 102             return err
 103         }
 104     }
 105     return nil
 106 }
 107 
 108 // handleInput simplifies control-flow for func main
 109 func handleInput(w *bufio.Writer, path string, liveLines bool) error {
 110     if path == `-` {
 111         return jsonl(w, os.Stdin, liveLines)
 112     }
 113 
 114     f, err := os.Open(path)
 115     if err != nil {
 116         // on windows, file-not-found error messages may mention `CreateFile`,
 117         // even when trying to open files in read-only mode
 118         return errors.New(`can't open file named ` + path)
 119     }
 120     defer f.Close()
 121     return jsonl(w, f, liveLines)
 122 }
 123 
 124 // escapedStringBytes helps func handleString treat all string bytes quickly
 125 // and correctly, using their officially-supported JSON escape sequences
 126 //
 127 // https://www.rfc-editor.org/rfc/rfc8259#section-7
 128 var escapedStringBytes = [256][]byte{
 129     {'\\', 'u', '0', '0', '0', '0'}, {'\\', 'u', '0', '0', '0', '1'},
 130     {'\\', 'u', '0', '0', '0', '2'}, {'\\', 'u', '0', '0', '0', '3'},
 131     {'\\', 'u', '0', '0', '0', '4'}, {'\\', 'u', '0', '0', '0', '5'},
 132     {'\\', 'u', '0', '0', '0', '6'}, {'\\', 'u', '0', '0', '0', '7'},
 133     {'\\', 'b'}, {'\\', 't'},
 134     {'\\', 'n'}, {'\\', 'u', '0', '0', '0', 'b'},
 135     {'\\', 'f'}, {'\\', 'r'},
 136     {'\\', 'u', '0', '0', '0', 'e'}, {'\\', 'u', '0', '0', '0', 'f'},
 137     {'\\', 'u', '0', '0', '1', '0'}, {'\\', 'u', '0', '0', '1', '1'},
 138     {'\\', 'u', '0', '0', '1', '2'}, {'\\', 'u', '0', '0', '1', '3'},
 139     {'\\', 'u', '0', '0', '1', '4'}, {'\\', 'u', '0', '0', '1', '5'},
 140     {'\\', 'u', '0', '0', '1', '6'}, {'\\', 'u', '0', '0', '1', '7'},
 141     {'\\', 'u', '0', '0', '1', '8'}, {'\\', 'u', '0', '0', '1', '9'},
 142     {'\\', 'u', '0', '0', '1', 'a'}, {'\\', 'u', '0', '0', '1', 'b'},
 143     {'\\', 'u', '0', '0', '1', 'c'}, {'\\', 'u', '0', '0', '1', 'd'},
 144     {'\\', 'u', '0', '0', '1', 'e'}, {'\\', 'u', '0', '0', '1', 'f'},
 145     {32}, {33}, {'\\', '"'}, {35}, {36}, {37}, {38}, {39},
 146     {40}, {41}, {42}, {43}, {44}, {45}, {46}, {47},
 147     {48}, {49}, {50}, {51}, {52}, {53}, {54}, {55},
 148     {56}, {57}, {58}, {59}, {60}, {61}, {62}, {63},
 149     {64}, {65}, {66}, {67}, {68}, {69}, {70}, {71},
 150     {72}, {73}, {74}, {75}, {76}, {77}, {78}, {79},
 151     {80}, {81}, {82}, {83}, {84}, {85}, {86}, {87},
 152     {88}, {89}, {90}, {91}, {'\\', '\\'}, {93}, {94}, {95},
 153     {96}, {97}, {98}, {99}, {100}, {101}, {102}, {103},
 154     {104}, {105}, {106}, {107}, {108}, {109}, {110}, {111},
 155     {112}, {113}, {114}, {115}, {116}, {117}, {118}, {119},
 156     {120}, {121}, {122}, {123}, {124}, {125}, {126}, {127},
 157     {128}, {129}, {130}, {131}, {132}, {133}, {134}, {135},
 158     {136}, {137}, {138}, {139}, {140}, {141}, {142}, {143},
 159     {144}, {145}, {146}, {147}, {148}, {149}, {150}, {151},
 160     {152}, {153}, {154}, {155}, {156}, {157}, {158}, {159},
 161     {160}, {161}, {162}, {163}, {164}, {165}, {166}, {167},
 162     {168}, {169}, {170}, {171}, {172}, {173}, {174}, {175},
 163     {176}, {177}, {178}, {179}, {180}, {181}, {182}, {183},
 164     {184}, {185}, {186}, {187}, {188}, {189}, {190}, {191},
 165     {192}, {193}, {194}, {195}, {196}, {197}, {198}, {199},
 166     {200}, {201}, {202}, {203}, {204}, {205}, {206}, {207},
 167     {208}, {209}, {210}, {211}, {212}, {213}, {214}, {215},
 168     {216}, {217}, {218}, {219}, {220}, {221}, {222}, {223},
 169     {224}, {225}, {226}, {227}, {228}, {229}, {230}, {231},
 170     {232}, {233}, {234}, {235}, {236}, {237}, {238}, {239},
 171     {240}, {241}, {242}, {243}, {244}, {245}, {246}, {247},
 172     {248}, {249}, {250}, {251}, {252}, {253}, {254}, {255},
 173 }
 174 
 175 // jsonl does it all, given a reader and a writer
 176 func jsonl(w *bufio.Writer, r io.Reader, live bool) error {
 177     dec := json.NewDecoder(r)
 178     // avoid parsing numbers, so unusually-long numbers are kept verbatim,
 179     // even if JSON parsers aren't required to guarantee such input-fidelity
 180     // for numbers
 181     dec.UseNumber()
 182 
 183     t, err := dec.Token()
 184     if err == io.EOF {
 185         // return errors.New(`input has no JSON values`)
 186         return nil
 187     }
 188 
 189     if t == json.Delim('[') {
 190         if err := handleTopLevelArray(w, dec, live); err != nil {
 191             return err
 192         }
 193     } else {
 194         if err := handleToken(w, dec, t); err != nil {
 195             return err
 196         }
 197         w.WriteByte('\n')
 198     }
 199 
 200     _, err = dec.Token()
 201     if err == io.EOF {
 202         // input is over, so it's a success
 203         return nil
 204     }
 205 
 206     if err == nil {
 207         // a successful `read` is a failure, as it means there are
 208         // trailing JSON tokens
 209         return errors.New(`unexpected trailing data`)
 210     }
 211 
 212     // any other error, perhaps some invalid-JSON-syntax-type error
 213     return err
 214 }
 215 
 216 // handleToken handles recursion for func json2
 217 func handleToken(w *bufio.Writer, dec *json.Decoder, t json.Token) error {
 218     switch t := t.(type) {
 219     case json.Delim:
 220         switch t {
 221         case json.Delim('['):
 222             return handleArray(w, dec)
 223         case json.Delim('{'):
 224             return handleObject(w, dec)
 225         default:
 226             return errors.New(`unsupported JSON syntax ` + string(t))
 227         }
 228 
 229     case nil:
 230         w.WriteString(`null`)
 231         return nil
 232 
 233     case bool:
 234         if t {
 235             w.WriteString(`true`)
 236         } else {
 237             w.WriteString(`false`)
 238         }
 239         return nil
 240 
 241     case json.Number:
 242         w.WriteString(t.String())
 243         return nil
 244 
 245     case string:
 246         return handleString(w, t)
 247 
 248     default:
 249         // return fmt.Errorf(`unsupported token type %T`, t)
 250         return errors.New(`invalid JSON token`)
 251     }
 252 }
 253 
 254 func handleTopLevelArray(w *bufio.Writer, dec *json.Decoder, live bool) error {
 255     for i := 0; true; i++ {
 256         t, err := dec.Token()
 257         if err == io.EOF {
 258             return nil
 259         }
 260 
 261         if err != nil {
 262             return err
 263         }
 264 
 265         if t == json.Delim(']') {
 266             return nil
 267         }
 268 
 269         err = handleToken(w, dec, t)
 270         if err != nil {
 271             return err
 272         }
 273 
 274         if w.WriteByte('\n') != nil {
 275             return io.EOF
 276         }
 277 
 278         if !live {
 279             continue
 280         }
 281 
 282         if w.Flush() != nil {
 283             return io.EOF
 284         }
 285     }
 286 
 287     // make the compiler happy
 288     return nil
 289 }
 290 
 291 // handleArray handles arrays for func handleToken
 292 func handleArray(w *bufio.Writer, dec *json.Decoder) error {
 293     w.WriteByte('[')
 294 
 295     for i := 0; true; i++ {
 296         t, err := dec.Token()
 297         if err == io.EOF {
 298             w.WriteByte(']')
 299             return nil
 300         }
 301 
 302         if err != nil {
 303             return err
 304         }
 305 
 306         if t == json.Delim(']') {
 307             w.WriteByte(']')
 308             return nil
 309         }
 310 
 311         if i > 0 {
 312             _, err := w.WriteString(", ")
 313             if err != nil {
 314                 return io.EOF
 315             }
 316         }
 317 
 318         err = handleToken(w, dec, t)
 319         if err != nil {
 320             return err
 321         }
 322     }
 323 
 324     // make the compiler happy
 325     return nil
 326 }
 327 
 328 // handleObject handles objects for func handleToken
 329 func handleObject(w *bufio.Writer, dec *json.Decoder) error {
 330     w.WriteByte('{')
 331 
 332     for i := 0; true; i++ {
 333         t, err := dec.Token()
 334         if err == io.EOF {
 335             w.WriteByte('}')
 336             return nil
 337         }
 338 
 339         if err != nil {
 340             return err
 341         }
 342 
 343         if t == json.Delim('}') {
 344             w.WriteByte('}')
 345             return nil
 346         }
 347 
 348         if i > 0 {
 349             _, err := w.WriteString(", ")
 350             if err != nil {
 351                 return io.EOF
 352             }
 353         }
 354 
 355         k, ok := t.(string)
 356         if !ok {
 357             return errors.New(`expected a string for a key-value pair`)
 358         }
 359 
 360         err = handleString(w, k)
 361         if err != nil {
 362             return err
 363         }
 364 
 365         w.WriteString(": ")
 366 
 367         t, err = dec.Token()
 368         if err == io.EOF {
 369             return errors.New(`expected a value for a key-value pair`)
 370         }
 371 
 372         err = handleToken(w, dec, t)
 373         if err != nil {
 374             return err
 375         }
 376     }
 377 
 378     // make the compiler happy
 379     return nil
 380 }
 381 
 382 // handleString handles strings for func handleToken, and keys for func
 383 // handleObject
 384 func handleString(w *bufio.Writer, s string) error {
 385     w.WriteByte('"')
 386     for i := range s {
 387         w.Write(escapedStringBytes[s[i]])
 388     }
 389     w.WriteByte('"')
 390     return nil
 391 }
     File: ./jsons/jsons.go
   1 /*
   2 The MIT License (MIT)
   3 
   4 Copyright (c) 2026 pacman64
   5 
   6 Permission is hereby granted, free of charge, to any person obtaining a copy of
   7 this software and associated documentation files (the "Software"), to deal
   8 in the Software without restriction, including without limitation the rights to
   9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
  10 of the Software, and to permit persons to whom the Software is furnished to do
  11 so, subject to the following conditions:
  12 
  13 The above copyright notice and this permission notice shall be included in all
  14 copies or substantial portions of the Software.
  15 
  16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  22 SOFTWARE.
  23 */
  24 
  25 package jsons
  26 
  27 import (
  28     "bufio"
  29     "io"
  30     "os"
  31     "strings"
  32 )
  33 
  34 const info = `
  35 jsons [options...] [filenames...]
  36 
  37 JSON Strings turns TSV (tab-separated values) data into a JSON array of
  38 objects whose values are strings or nulls, the latter being used for
  39 missing trailing values.
  40 `
  41 
  42 func Main() {
  43     if len(os.Args) > 1 {
  44         switch os.Args[1] {
  45         case `-h`, `--h`, `-help`, `--help`:
  46             os.Stdout.WriteString(info[1:])
  47             return
  48         }
  49     }
  50 
  51     err := run(os.Args[1:])
  52     if err == io.EOF {
  53         err = nil
  54     }
  55 
  56     if err != nil {
  57         os.Stderr.WriteString(err.Error())
  58         os.Stderr.WriteString("\n")
  59         os.Exit(1)
  60     }
  61 }
  62 
  63 type runConfig struct {
  64     lines int
  65     keys  []string
  66 }
  67 
  68 func run(paths []string) error {
  69     bw := bufio.NewWriter(os.Stdout)
  70     defer bw.Flush()
  71 
  72     dashes := 0
  73     var cfg runConfig
  74 
  75     for _, path := range paths {
  76         if path == `-` {
  77             dashes++
  78             if dashes > 1 {
  79                 continue
  80             }
  81 
  82             if err := handleInput(bw, os.Stdin, &cfg); err != nil {
  83                 return err
  84             }
  85 
  86             continue
  87         }
  88 
  89         if err := handleFile(bw, path, &cfg); err != nil {
  90             return err
  91         }
  92     }
  93 
  94     if len(paths) == 0 {
  95         if err := handleInput(bw, os.Stdin, &cfg); err != nil {
  96             return err
  97         }
  98     }
  99 
 100     if cfg.lines > 1 {
 101         bw.WriteString("\n]\n")
 102     } else {
 103         bw.WriteString("[]\n")
 104     }
 105     return nil
 106 }
 107 
 108 func handleFile(w *bufio.Writer, path string, cfg *runConfig) error {
 109     f, err := os.Open(path)
 110     if err != nil {
 111         return err
 112     }
 113     defer f.Close()
 114     return handleInput(w, f, cfg)
 115 }
 116 
 117 func escapeKeys(line string) []string {
 118     var keys []string
 119     var sb strings.Builder
 120 
 121     loopTSV(line, func(i int, s string) {
 122         sb.WriteByte('"')
 123         for _, r := range s {
 124             if r == '\\' || r == '"' {
 125                 sb.WriteByte('\\')
 126             }
 127             sb.WriteRune(r)
 128         }
 129         sb.WriteByte('"')
 130 
 131         keys = append(keys, sb.String())
 132         sb.Reset()
 133     })
 134 
 135     return keys
 136 }
 137 
 138 func emitRow(w *bufio.Writer, line string, keys []string) {
 139     j := 0
 140     w.WriteByte('{')
 141 
 142     loopTSV(line, func(i int, s string) {
 143         j = i
 144         if i > 0 {
 145             w.WriteString(", ")
 146         }
 147 
 148         w.WriteString(keys[i])
 149         w.WriteString(": \"")
 150 
 151         for _, r := range s {
 152             if r == '\\' || r == '"' {
 153                 w.WriteByte('\\')
 154             }
 155             w.WriteRune(r)
 156         }
 157         w.WriteByte('"')
 158     })
 159 
 160     for i := j + 1; i < len(keys); i++ {
 161         if i > 0 {
 162             w.WriteString(", ")
 163         }
 164         w.WriteString(keys[i])
 165         w.WriteString(": null")
 166     }
 167     w.WriteByte('}')
 168 }
 169 
 170 func loopTSV(line string, f func(i int, s string)) {
 171     for i := 0; len(line) > 0; i++ {
 172         pos := strings.IndexByte(line, '\t')
 173         if pos < 0 {
 174             f(i, line)
 175             return
 176         }
 177 
 178         f(i, line[:pos])
 179         line = line[pos+1:]
 180     }
 181 }
 182 
 183 func handleInput(w *bufio.Writer, r io.Reader, cfg *runConfig) error {
 184     const gb = 1024 * 1024 * 1024
 185     sc := bufio.NewScanner(r)
 186     sc.Buffer(nil, 8*gb)
 187 
 188     for i := 0; sc.Scan(); i++ {
 189         s := sc.Text()
 190         if i == 0 && strings.HasPrefix(s, "\xef\xbb\xbf") {
 191             s = s[3:]
 192         }
 193 
 194         if cfg.lines == 0 {
 195             cfg.keys = escapeKeys(s)
 196             w.WriteByte('[')
 197             cfg.lines++
 198             continue
 199         }
 200 
 201         if cfg.lines == 1 {
 202             w.WriteString("\n  ")
 203         } else {
 204             if _, err := w.WriteString(",\n  "); err != nil {
 205                 return io.EOF
 206             }
 207         }
 208 
 209         emitRow(w, s, cfg.keys)
 210         cfg.lines++
 211     }
 212 
 213     return sc.Err()
 214 }
     File: ./main.go
   1 /*
   2 The MIT License (MIT)
   3 
   4 Copyright (c) 2026 pacman64
   5 
   6 Permission is hereby granted, free of charge, to any person obtaining a copy of
   7 this software and associated documentation files (the "Software"), to deal
   8 in the Software without restriction, including without limitation the rights to
   9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
  10 of the Software, and to permit persons to whom the Software is furnished to do
  11 so, subject to the following conditions:
  12 
  13 The above copyright notice and this permission notice shall be included in all
  14 copies or substantial portions of the Software.
  15 
  16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  22 SOFTWARE.
  23 */
  24 
  25 /*
  26 To compile a smaller-sized command-line app, you can use the `go` command as
  27 follows:
  28 
  29     go build -ldflags "-s -w" -trimpath
  30 
  31 If you have `tinygo`, you can do even better by instead using:
  32 
  33     tinygo build -no-debug -opt=2 easybox
  34 */
  35 
  36 package main
  37 
  38 import (
  39     "fmt"
  40     "io"
  41     "os"
  42     "path"
  43     "sort"
  44     "strings"
  45     "unicode/utf8"
  46 
  47     "./avoid"
  48     "./bytedump"
  49     "./calc"
  50     "./catl"
  51     "./coby"
  52     "./countdown"
  53     "./datauri"
  54     "./debase64"
  55     "./dedup"
  56     "./dejsonl"
  57     "./dessv"
  58     "./ecoli"
  59     "./erase"
  60     "./files"
  61     "./filesizes"
  62     "./fixlines"
  63     "./folders"
  64     "./hima"
  65     "./htmlify"
  66     "./id3pic"
  67     "./json0"
  68     "./json2"
  69     "./jsonl"
  70     "./jsons"
  71     "./match"
  72     "./ncol"
  73     "./ngron"
  74     "./nhex"
  75     "./njson"
  76     "./nn"
  77     "./plain"
  78     "./primes"
  79     "./realign"
  80     "./squeeze"
  81     "./tcatl"
  82     "./teletype"
  83     "./utfate"
  84     "./waveout"
  85 )
  86 
  87 const info = `
  88 easybox [options...] [tool] [arguments...]
  89 
  90 This is a collection of many specialized app-like tools, similar to "busybox".
  91 
  92 You can either run it with the tool name as its first argument, or run a link
  93 to it whose name is one of those same tools, avoiding the tool-name argument
  94 in that case.
  95 
  96 Tool "help" shows you all tools available, as well as all their aliases, and
  97 tool "tools" merely lists all main tool-names.
  98 `
  99 
 100 // mains has some entries starting as nil to avoid circular-dependency errors
 101 var mains = map[string]func(){
 102     `avoid`:     avoid.Main,
 103     `bytedump`:  bytedump.Main,
 104     `calc`:      calc.Main,
 105     `catl`:      catl.Main,
 106     `coby`:      coby.Main,
 107     `countdown`: countdown.Main,
 108     `datauri`:   datauri.Main,
 109     `debase64`:  debase64.Main,
 110     `dedup`:     dedup.Main,
 111     `dejsonl`:   dejsonl.Main,
 112     `dessv`:     dessv.Main,
 113     `ecoli`:     ecoli.Main,
 114     `erase`:     erase.Main,
 115     `files`:     files.Main,
 116     `filesizes`: filesizes.Main,
 117     `fixlines`:  fixlines.Main,
 118     `folders`:   folders.Main,
 119     `help`:      nil,
 120     `hima`:      hima.Main,
 121     `htmlify`:   htmlify.Main,
 122     `id3pic`:    id3pic.Main,
 123     `json0`:     json0.Main,
 124     `json2`:     json2.Main,
 125     `jsonl`:     jsonl.Main,
 126     `jsons`:     jsons.Main,
 127     `match`:     match.Main,
 128     `ncol`:      ncol.Main,
 129     `ngron`:     ngron.Main,
 130     `nhex`:      nhex.Main,
 131     `njson`:     njson.Main,
 132     `nn`:        nn.Main,
 133     `plain`:     plain.Main,
 134     `primes`:    primes.Main,
 135     `realign`:   realign.Main,
 136     `squeeze`:   squeeze.Main,
 137     `tcatl`:     tcatl.Main,
 138     `teletype`:  teletype.Main,
 139     `tools`:     nil,
 140     `utfate`:    utfate.Main,
 141     `waveout`:   waveout.Main,
 142 }
 143 
 144 var extras = map[string]func(){
 145     `help`:  help,
 146     `tools`: tools,
 147 }
 148 
 149 var aliases = map[string]string{
 150     `bytedump`:    `bytedump`,
 151     `ca`:          `calc`,
 152     `calculate`:   `calc`,
 153     `calculator`:  `calc`,
 154     `fc`:          `calc`,
 155     `frac`:        `calc`,
 156     `fraca`:       `calc`,
 157     `fracalc`:     `calc`,
 158     `datauri`:     `datauri`,
 159     `deduplicate`: `dedup`,
 160     `unique`:      `dedup`,
 161     `detrail`:     `fixlines`,
 162     `id3pic`:      `id3pic`,
 163     `mp3pic`:      `id3pic`,
 164     `ncols`:       `ncol`,
 165     `nicecols`:    `ncol`,
 166     `nh`:          `nhex`,
 167     `nj`:          `njson`,
 168     `nicedigits`:  `nn`,
 169     `nicenums`:    `nn`,
 170     `j0`:          `json0`,
 171     `j2`:          `json2`,
 172     `jl`:          `jsonl`,
 173     `detsv`:       `jsons`,
 174     `tty`:         `teletype`,
 175     `utf8`:        `utfate`,
 176 }
 177 
 178 var blurbs = map[string]string{
 179     `avoid`:     `ignore lines matching any of the regexes given`,
 180     `bytedump`:  `show bytes as hex values, with a wide ASCII panel`,
 181     `calc`:      `fractional calculator, with floating-point powers`,
 182     `catl`:      `conCATenate Lines, ensures text ends with a line-feed`,
 183     `coby`:      `COunt BYtes, and many other byte/text-related stats`,
 184     `countdown`: `countdown the seconds/minutes/hours given`,
 185     `datauri`:   `turn bytes into data-URIs, auto-detecting MIME types`,
 186     `debase64`:  `decode base64 text and data-URIs`,
 187     `dedup`:     `deduplicate lines, emitting each unique line only once`,
 188     `dejsonl`:   `turn JSON Lines into proper JSON`,
 189     `dessv`:     `turn tables of space-separated values into TSV tables`,
 190     `ecoli`:     `expressions coloring lines color-codes matching lines`,
 191     `erase`:     `ignore/erase all matching regexes away from each line`,
 192     `files`:     `list all files in the folder(s) given`,
 193     `filesizes`: `show sizes of files and block-counts (4K by default)`,
 194     `fixlines`:  `ignore carriage-returns, or even trailing spaces`,
 195     `folders`:   `list all folders in the folder(s) given`,
 196     `help`:      `show the help message for "easybox"`,
 197     `hima`:      `HIlight MAtches using the regexes given`,
 198     `htmlify`:   `turn plain-text lines into HTML documents`,
 199     `id3pic`:    `get the encoded picture out of audio files, if present`,
 200     `json0`:     `minimize/fix JSON into the smallest-possible size`,
 201     `json2`:     `indent JSON into multiple lines, using 2 spaces per level`,
 202     `jsonl`:     `turn items from top-level JSON arrays into JSON Lines`,
 203     `jsons`:     `JSON Strings turns TSV into arrays of objects of strings`,
 204     `match`:     `only keep lines matching any of the regexes given`,
 205     `ncol`:      `Nice COLumns realigns tables, color-coding their values`,
 206     `ngron`:     `Nice GRON mimics a subset of "gron", using better colors`,
 207     `nhex`:      `Nice HEXadecimal shows bytes as hex values and ASCII`,
 208     `njson`:     `Nice JSON indents and color-codes JSON data`,
 209     `nn`:        `Nice Numbers color-codes groups of digits for legibility`,
 210     `plain`:     `ignore all ANSI-sequences, leaving unstyled text`,
 211     `primes`:    `find prime numbers, up to the first million by default`,
 212     `realign`:   `realign items from the SSV/TSV tables given`,
 213     `squeeze`:   `aggressively ignore spaces, especially runs of spaces`,
 214     `tcatl`:     `Titled conCATenate Lines, is like "catl" but with names`,
 215     `teletype`:  `mimic old-fashioned teletype devices, by delaying output`,
 216     `tools`:     `list all tools available`,
 217     `utfate`:    `decode all other types of UTF text into UTF-8`,
 218     `waveout`:   `emit/calculate WAV-format sounds by formula`,
 219 }
 220 
 221 func main() {
 222     // add the deliberately-missing lookup entries
 223     for k, v := range extras {
 224         mains[k] = v
 225     }
 226 
 227     // try to use the app's `name`, in case it's being called from a file-link
 228     // named after one of the tools
 229     if tool, ok := lookupTool(path.Base(os.Args[0])); ok {
 230         tool()
 231         return
 232     }
 233 
 234     // try normal tool-lookup using the first command-line argument
 235     if len(os.Args) >= 2 {
 236         name := os.Args[1]
 237 
 238         if tool, ok := lookupTool(name); ok {
 239             os.Args = os.Args[1:]
 240             tool()
 241             return
 242         }
 243 
 244         switch name {
 245         case `-h`, `--h`, `-help`, `--help`, `help`:
 246             showHelp(os.Stdout)
 247             return
 248 
 249         case `-l`, `--l`, `-list`, `--list`:
 250             tools()
 251             return
 252 
 253         case `-links`, `--links`:
 254             showLinks(os.Stdout)
 255             return
 256 
 257         case `-t`, `--t`, `-tools`, `--tools`, `tools`:
 258             tools()
 259             return
 260         }
 261 
 262         const fs = "easybox: tool/alias named %q not found\n"
 263         fmt.Fprintf(os.Stderr, fs, name)
 264         os.Exit(1)
 265     }
 266 
 267     showHelp(os.Stderr)
 268     fmt.Fprintln(os.Stderr, ``)
 269     fmt.Fprintln(os.Stderr, `easybox: no tool name given`)
 270     os.Exit(1)
 271 }
 272 
 273 // dealias tries to lookup a string to the aliases given, returning the name
 274 // given if the lookup fails
 275 func dealias(aliases map[string]string, name string) string {
 276     if s, ok := aliases[name]; ok {
 277         return s
 278     }
 279     return name
 280 }
 281 
 282 func help() {
 283     showHelp(os.Stdout)
 284 }
 285 
 286 func lookupTool(name string) (tool func(), ok bool) {
 287     name = strings.ReplaceAll(name, `-`, ``)
 288     tool, ok = mains[dealias(aliases, name)]
 289     return tool, ok
 290 }
 291 
 292 // showHelp has a parameter to write either to stdout or stderr
 293 func showHelp(w io.Writer) {
 294     fmt.Fprintln(w, info[1:])
 295     fmt.Fprintln(w, ``)
 296     fmt.Fprintln(w, `Tools Available`)
 297 
 298     maxlen := 0
 299     names := make([]string, 0, max(len(mains), len(aliases)))
 300     for k := range mains {
 301         names = append(names, k)
 302         maxlen = max(maxlen, utf8.RuneCountInString(k))
 303     }
 304 
 305     sort.Strings(names)
 306 
 307     for _, s := range names {
 308         fmt.Fprintf(w, "  - %-*s:  %s\n", maxlen, s, blurbs[s])
 309     }
 310 
 311     fmt.Fprintln(w, ``)
 312     fmt.Fprintln(w, `Aliases Available`)
 313 
 314     maxlen = 0
 315     names = names[:0]
 316     for k := range aliases {
 317         names = append(names, k)
 318         maxlen = max(maxlen, utf8.RuneCountInString(k))
 319     }
 320 
 321     sort.Strings(names)
 322 
 323     for _, k := range names {
 324         fmt.Fprintf(w, "  - %-*s -> %s\n", maxlen, k, aliases[k])
 325     }
 326 }
 327 
 328 // showLinks has a parameter to write either to stdout or stderr
 329 func showLinks(w io.Writer) {
 330     names := make([]string, 0, len(mains)-len(extras))
 331     for k := range mains {
 332         if _, ok := extras[k]; ok {
 333             continue
 334         }
 335         names = append(names, k)
 336     }
 337 
 338     sort.Strings(names)
 339 
 340     for _, s := range names {
 341         fmt.Fprintf(w, "ln -s \"$(which easybox)\" ./%s\n", s)
 342     }
 343 }
 344 
 345 func tools() {
 346     names := make([]string, 0, len(mains))
 347     for k := range mains {
 348         names = append(names, k)
 349     }
 350 
 351     sort.Strings(names)
 352 
 353     for _, s := range names {
 354         fmt.Fprintln(os.Stdout, s)
 355     }
 356 }
     File: ./main_test.go
   1 /*
   2 The MIT License (MIT)
   3 
   4 Copyright (c) 2026 pacman64
   5 
   6 Permission is hereby granted, free of charge, to any person obtaining a copy of
   7 this software and associated documentation files (the "Software"), to deal
   8 in the Software without restriction, including without limitation the rights to
   9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
  10 of the Software, and to permit persons to whom the Software is furnished to do
  11 so, subject to the following conditions:
  12 
  13 The above copyright notice and this permission notice shall be included in all
  14 copies or substantial portions of the Software.
  15 
  16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  22 SOFTWARE.
  23 */
  24 
  25 package main
  26 
  27 import "testing"
  28 
  29 func TestAliases(t *testing.T) {
  30     for alias, name := range aliases {
  31         if _, ok := mains[name]; ok {
  32             continue
  33         }
  34 
  35         t.Errorf("alias %q leads nowhere", alias)
  36     }
  37 }
  38 
  39 func TestBlurbs(t *testing.T) {
  40     for name := range mains {
  41         if blurbs[name] != `` {
  42             continue
  43         }
  44         t.Errorf("no description/blurb for tool %q", name)
  45     }
  46 }
  47 
  48 func TestFillers(t *testing.T) {
  49     for name, v := range mains {
  50         if v != nil {
  51             continue
  52         }
  53 
  54         if _, ok := extras[name]; ok {
  55             continue
  56         }
  57 
  58         t.Errorf("tool %q has no filler for invalid entry", name)
  59     }
  60 
  61     for name := range extras {
  62         if v, ok := mains[name]; !ok || v != nil {
  63             t.Errorf("filling for missing entry %q", name)
  64         }
  65     }
  66 }
     File: ./match/match.go
   1 /*
   2 The MIT License (MIT)
   3 
   4 Copyright (c) 2026 pacman64
   5 
   6 Permission is hereby granted, free of charge, to any person obtaining a copy of
   7 this software and associated documentation files (the "Software"), to deal
   8 in the Software without restriction, including without limitation the rights to
   9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
  10 of the Software, and to permit persons to whom the Software is furnished to do
  11 so, subject to the following conditions:
  12 
  13 The above copyright notice and this permission notice shall be included in all
  14 copies or substantial portions of the Software.
  15 
  16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  22 SOFTWARE.
  23 */
  24 
  25 package match
  26 
  27 import (
  28     "bufio"
  29     "bytes"
  30     "io"
  31     "os"
  32     "regexp"
  33 )
  34 
  35 const info = `
  36 match [options...] [regular expressions...]
  37 
  38 Only keep lines which match any of the extended-mode regular expressions
  39 given. When not given any regex, match non-empty lines by default.
  40 
  41 The options are, available both in single and double-dash versions
  42 
  43     -h, -help     show this help message
  44     -i, -ins      match regexes case-insensitively
  45     -l, -links    add a regex to match HTTP/HTTPS links case-insensitively
  46 `
  47 
  48 const linkRegexp = `(?i)https?://[a-z0-9+_.:%-]+(/[a-z0-9+_.%/,#?&=-]*)*`
  49 
  50 func Main() {
  51     nerr := 0
  52     links := false
  53     buffered := false
  54     sensitive := true
  55     args := os.Args[1:]
  56 
  57     for len(args) > 0 {
  58         switch args[0] {
  59         case `-b`, `--b`, `-buffered`, `--buffered`:
  60             buffered = true
  61             args = args[1:]
  62             continue
  63 
  64         case `-h`, `--h`, `-help`, `--help`:
  65             os.Stdout.WriteString(info[1:])
  66             return
  67 
  68         case `-i`, `--i`, `-ins`, `--ins`:
  69             sensitive = false
  70             args = args[1:]
  71             continue
  72 
  73         case `-l`, `--l`, `-links`, `--links`:
  74             links = true
  75             args = args[1:]
  76             continue
  77         }
  78 
  79         break
  80     }
  81 
  82     if len(args) > 0 && args[0] == `--` {
  83         args = args[1:]
  84     }
  85 
  86     liveLines := !buffered
  87     if !buffered {
  88         if _, err := os.Stdout.Seek(0, io.SeekCurrent); err == nil {
  89             liveLines = false
  90         }
  91     }
  92 
  93     if len(args) == 0 {
  94         args = []string{`.`}
  95     }
  96 
  97     var exprs []*regexp.Regexp
  98     if links {
  99         exprs = make([]*regexp.Regexp, 0, len(args)+1)
 100         exprs = append(exprs, regexp.MustCompile(linkRegexp))
 101     } else {
 102         exprs = make([]*regexp.Regexp, 0, len(args))
 103     }
 104 
 105     for _, src := range args {
 106         var err error
 107         var exp *regexp.Regexp
 108         if !sensitive {
 109             exp, err = regexp.Compile(`(?i)` + src)
 110         } else {
 111             exp, err = regexp.Compile(src)
 112         }
 113 
 114         if err != nil {
 115             os.Stderr.WriteString(err.Error())
 116             os.Stderr.WriteString("\n")
 117             nerr++
 118         }
 119 
 120         exprs = append(exprs, exp)
 121     }
 122 
 123     if nerr > 0 {
 124         os.Exit(1)
 125     }
 126 
 127     var buf []byte
 128     sc := bufio.NewScanner(os.Stdin)
 129     sc.Buffer(nil, 8*1024*1024*1024)
 130     bw := bufio.NewWriter(os.Stdout)
 131 
 132     for i := 0; sc.Scan(); i++ {
 133         line := sc.Bytes()
 134         if i == 0 && bytes.HasPrefix(line, []byte{0xef, 0xbb, 0xbf}) {
 135             line = line[3:]
 136         }
 137 
 138         s := line
 139         if bytes.IndexByte(s, '\x1b') >= 0 {
 140             buf = plain(buf[:0], s)
 141             s = buf
 142         }
 143 
 144         if match(s, exprs) {
 145             bw.Write(line)
 146             bw.WriteByte('\n')
 147 
 148             if !liveLines {
 149                 continue
 150             }
 151 
 152             if err := bw.Flush(); err != nil {
 153                 return
 154             }
 155         }
 156     }
 157 }
 158 
 159 func match(what []byte, with []*regexp.Regexp) bool {
 160     for _, e := range with {
 161         if e.Match(what) {
 162             return true
 163         }
 164     }
 165     return false
 166 }
 167 
 168 func plain(dst []byte, src []byte) []byte {
 169     for len(src) > 0 {
 170         i, j := indexEscapeSequence(src)
 171         if i < 0 {
 172             dst = append(dst, src...)
 173             break
 174         }
 175         if j < 0 {
 176             j = len(src)
 177         }
 178 
 179         if i > 0 {
 180             dst = append(dst, src[:i]...)
 181         }
 182 
 183         src = src[j:]
 184     }
 185 
 186     return dst
 187 }
 188 
 189 // indexEscapeSequence finds the first ANSI-style escape-sequence, which is
 190 // the multi-byte sequences starting with ESC[; the result is a pair of slice
 191 // indices which can be independently negative when either the start/end of
 192 // a sequence isn't found; given their fairly-common use, even the hyperlink
 193 // ESC]8 sequences are supported
 194 func indexEscapeSequence(s []byte) (int, int) {
 195     var prev byte
 196 
 197     for i, b := range s {
 198         if prev == '\x1b' && b == '[' {
 199             j := indexLetter(s[i+1:])
 200             if j < 0 {
 201                 return i, -1
 202             }
 203             return i - 1, i + 1 + j + 1
 204         }
 205 
 206         if prev == '\x1b' && b == ']' && i+1 < len(s) && s[i+1] == '8' {
 207             j := indexPair(s[i+1:], '\x1b', '\\')
 208             if j < 0 {
 209                 return i, -1
 210             }
 211             return i - 1, i + 1 + j + 2
 212         }
 213 
 214         prev = b
 215     }
 216 
 217     return -1, -1
 218 }
 219 
 220 func indexLetter(s []byte) int {
 221     for i, b := range s {
 222         upper := b &^ 32
 223         if 'A' <= upper && upper <= 'Z' {
 224             return i
 225         }
 226     }
 227 
 228     return -1
 229 }
 230 
 231 func indexPair(s []byte, x byte, y byte) int {
 232     var prev byte
 233 
 234     for i, b := range s {
 235         if prev == x && b == y && i > 0 {
 236             return i
 237         }
 238         prev = b
 239     }
 240 
 241     return -1
 242 }
     File: ./mathplus/doc.go
   1 /*
   2 # mathplus
   3 
   4 This is an add-on to the stdlib package `math`, with statistics-gathering
   5 functionality and plenty more math-related functions.
   6 
   7 This package also wraps types/methods from math/rand with nil-safe ones, which
   8 act on the default value-generator when nil pointers are used.
   9 */
  10 
  11 package mathplus
     File: ./mathplus/functions.go
   1 /*
   2 The MIT License (MIT)
   3 
   4 Copyright (c) 2026 pacman64
   5 
   6 Permission is hereby granted, free of charge, to any person obtaining a copy of
   7 this software and associated documentation files (the "Software"), to deal
   8 in the Software without restriction, including without limitation the rights to
   9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
  10 of the Software, and to permit persons to whom the Software is furnished to do
  11 so, subject to the following conditions:
  12 
  13 The above copyright notice and this permission notice shall be included in all
  14 copies or substantial portions of the Software.
  15 
  16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  22 SOFTWARE.
  23 */
  24 
  25 package mathplus
  26 
  27 import "math"
  28 
  29 func Decade(x float64) float64  { return 10 * math.Floor(0.1*x) }
  30 func Century(x float64) float64 { return 100 * math.Floor(0.01*x) }
  31 
  32 func Round1(x float64) float64 { return 1e-1 * math.Round(1e+1*x) }
  33 func Round2(x float64) float64 { return 1e-2 * math.Round(1e+2*x) }
  34 func Round3(x float64) float64 { return 1e-3 * math.Round(1e+3*x) }
  35 func Round4(x float64) float64 { return 1e-4 * math.Round(1e+4*x) }
  36 func Round5(x float64) float64 { return 1e-5 * math.Round(1e+5*x) }
  37 func Round6(x float64) float64 { return 1e-6 * math.Round(1e+6*x) }
  38 func Round7(x float64) float64 { return 1e-7 * math.Round(1e+7*x) }
  39 func Round8(x float64) float64 { return 1e-8 * math.Round(1e+8*x) }
  40 func Round9(x float64) float64 { return 1e-9 * math.Round(1e+9*x) }
  41 
  42 // Ln calculates the natural logarithm.
  43 func Ln(x float64) float64 {
  44     return math.Log(x)
  45 }
  46 
  47 // Log calculates logarithms using the base given.
  48 // func Log(base float64, x float64) float64 {
  49 //  return math.Log(x) / math.Log(base)
  50 // }
  51 
  52 // Round rounds the number given to the number of decimal places given.
  53 func Round(x float64, decimals int) float64 {
  54     k := math.Pow(10, float64(decimals))
  55     return math.Round(k*x) / k
  56 }
  57 
  58 // RoundBy rounds the number given to the unit-size given
  59 // func RoundBy(x float64, by float64) float64 {
  60 //  return math.Round(by*x) / by
  61 // }
  62 
  63 // FloorBy quantizes a number downward by the unit-size given
  64 // func FloorBy(x float64, by float64) float64 {
  65 //  mod := math.Mod(x, by)
  66 //  if x < 0 {
  67 //      if mod == 0 {
  68 //          return x - mod
  69 //      }
  70 //      return x - mod - by
  71 //  }
  72 //  return x - mod
  73 // }
  74 
  75 // Wrap linearly interpolates the number given to the range [0...1],
  76 // according to its continuous domain, specified by the limits given
  77 func Wrap(x float64, min, max float64) float64 {
  78     return (x - min) / (max - min)
  79 }
  80 
  81 // AnchoredWrap works like Wrap, except when both domain boundaries are positive,
  82 // the minimum becomes 0, and when both domain boundaries are negative, the
  83 // maximum becomes 0.
  84 func AnchoredWrap(x float64, min, max float64) float64 {
  85     if min > 0 && max > 0 {
  86         min = 0
  87     } else if max < 0 && min < 0 {
  88         max = 0
  89     }
  90     return (x - min) / (max - min)
  91 }
  92 
  93 // Unwrap undoes Wrap, by turning the [0...1] number given into its equivalent
  94 // in the new domain given.
  95 func Unwrap(x float64, min, max float64) float64 {
  96     return (max-min)*x + min
  97 }
  98 
  99 // Clamp constrains the domain of the value given
 100 func Clamp(x float64, min, max float64) float64 {
 101     return math.Min(math.Max(x, min), max)
 102 }
 103 
 104 // Scale transforms a number according to its position in [xMin, xMax] into its
 105 // correspondingly-positioned counterpart in [yMin, yMax]: if value isn't in its
 106 // assumed domain, its result will be extrapolated accordingly
 107 func Scale(x float64, xmin, xmax, ymin, ymax float64) float64 {
 108     k := (x - xmin) / (xmax - xmin)
 109     return (ymax-ymin)*k + ymin
 110 }
 111 
 112 // AnchoredScale works like Scale, except when both domain boundaries are positive,
 113 // the minimum becomes 0, and when both domain boundaries are negative, the maximum
 114 // becomes 0. This allows for proportionally-correct scaling of quantities, such
 115 // as when showing data visually.
 116 func AnchoredScale(x float64, xmin, xmax, ymin, ymax float64) float64 {
 117     if xmin > 0 && xmax > 0 {
 118         xmin = 0
 119     } else if xmax < 0 && xmin < 0 {
 120         xmax = 0
 121     }
 122     k := (x - xmin) / (xmax - xmin)
 123     return (ymax-ymin)*k + ymin
 124 }
 125 
 126 // ForceRange is handy when trying to mold floating-point values into numbers
 127 // valid for JSON, since NaN and Infinity are replaced by the values given;
 128 // the infinity-replacement value is negated for negative infinity.
 129 func ForceRange(x float64, nan, inf float64) float64 {
 130     if math.IsNaN(x) {
 131         return nan
 132     }
 133     if math.IsInf(x, -1) {
 134         return -inf
 135     }
 136     if math.IsInf(x, +1) {
 137         return inf
 138     }
 139     return x
 140 }
 141 
 142 // Sign returns the standardized sign of a value, either as -1, 0, or +1: NaN
 143 // values stay as NaN, as is expected when using floating-point values.
 144 func Sign(x float64) float64 {
 145     if x > 0 {
 146         return +1
 147     }
 148     if x < 0 {
 149         return -1
 150     }
 151     if x == 0 {
 152         return 0
 153     }
 154     return math.NaN()
 155 }
 156 
 157 // IsInteger checks if a floating-point value is already rounded to an integer
 158 // value.
 159 func IsInteger(x float64) bool {
 160     _, frac := math.Modf(x)
 161     return frac == 0
 162 }
 163 
 164 func Square(x float64) float64 {
 165     return x * x
 166 }
 167 
 168 func Cube(x float64) float64 {
 169     return x * x * x
 170 }
 171 
 172 func RoundTo(x float64, decimals float64) float64 {
 173     k := math.Pow10(int(decimals))
 174     return math.Round(k*x) / k
 175 }
 176 
 177 func RelDiff(x, y float64) float64 {
 178     return (x - y) / y
 179 }
 180 
 181 // Bool booleanizes a number: 0 stays 0, and anything else becomes 1.
 182 func Bool(x float64) float64 {
 183     if x == 0 {
 184         return 0
 185     }
 186     return 1
 187 }
 188 
 189 // DeNaN replaces NaN with the alternative value given: regular values are
 190 // returned as given.
 191 func DeNaN(x float64, instead float64) float64 {
 192     if !math.IsNaN(x) {
 193         return x
 194     }
 195     return instead
 196 }
 197 
 198 // DeInf replaces either infinity with the alternative value given: regular
 199 // values are returned as given.
 200 func DeInf(x float64, instead float64) float64 {
 201     if !math.IsInf(x, 0) {
 202         return x
 203     }
 204     return instead
 205 }
 206 
 207 // Revalue replaces NaN and either infinity with the alternative value given:
 208 // regular values are returned as given.
 209 func Revalue(x float64, instead float64) float64 {
 210     if !math.IsNaN(x) && !math.IsInf(x, 0) {
 211         return x
 212     }
 213     return instead
 214 }
 215 
 216 // Linear evaluates a linear polynomial, the first arg being the main input,
 217 // followed by the polynomial coefficients in decreasing-power order.
 218 func Linear(x float64, a, b float64) float64 {
 219     return a*x + b
 220 }
 221 
 222 // Quadratic evaluates a 2nd-degree polynomial, the first arg being the main
 223 // input, followed by the polynomial coefficients in decreasing-power order.
 224 func Quadratic(x float64, a, b, c float64) float64 {
 225     return (a*x+b)*x + c
 226 }
 227 
 228 // Cubic evaluates a cubic polynomial, the first arg being the main input,
 229 // followed by the polynomial coefficients in decreasing-power order.
 230 func Cubic(x float64, a, b, c, d float64) float64 {
 231     return ((a*x+b)*x+c)*x + d
 232 }
 233 
 234 func LinearFMA(x float64, a, b float64) float64 {
 235     return math.FMA(x, a, b)
 236 }
 237 
 238 func QuadraticFMA(x float64, a, b, c float64) float64 {
 239     lin := math.FMA(x, a, b)
 240     return math.FMA(lin, x, c)
 241 }
 242 
 243 func CubicFMA(x float64, a, b, c, d float64) float64 {
 244     lin := math.FMA(x, a, b)
 245     quad := math.FMA(lin, x, c)
 246     return math.FMA(quad, x, d)
 247 }
 248 
 249 // Radians converts angular degrees into angular radians: 180 degrees are pi
 250 // pi radians.
 251 func Radians(deg float64) float64 {
 252     const k = math.Pi / 180
 253     return k * deg
 254 }
 255 
 256 // Degrees converts angular radians into angular degrees: pi radians are 180
 257 // degrees.
 258 func Degrees(rad float64) float64 {
 259     const k = 180 / math.Pi
 260     return k * rad
 261 }
 262 
 263 // Fract calculates the non-integer/fractional part of a number.
 264 func Fract(x float64) float64 {
 265     return x - math.Floor(x)
 266 }
 267 
 268 // Mix interpolates 2 numbers using a third number, used as an interpolation
 269 // coefficient. This parameter naturally falls in the range [0, 1], but doesn't
 270 // have to be: when given outside that range, the parameter can extrapolate in
 271 // either direction instead.
 272 func Mix(x, y float64, k float64) float64 {
 273     return (1-k)*(y-x) + x
 274 }
 275 
 276 // Step implements a step function with a parametric threshold.
 277 func Step(edge, x float64) float64 {
 278     if x < edge {
 279         return 0
 280     }
 281     return 1
 282 }
 283 
 284 // SmoothStep is like the `smoothstep` func found in GLSL, using a cubic
 285 // interpolator in the transition region.
 286 func SmoothStep(edge0, edge1, x float64) float64 {
 287     if x <= edge0 {
 288         return 0
 289     }
 290     if x >= edge1 {
 291         return 1
 292     }
 293 
 294     // use the cubic hermite interpolator 3x^2 - 2x^3 in the transition band
 295     return x * x * (3 - 2*x)
 296 }
 297 
 298 // Logistic approximates the math func of the same name.
 299 func Logistic(x float64) float64 {
 300     return 1 / (1 + math.Exp(-x))
 301 }
 302 
 303 // Sinc approximates the math func of the same name.
 304 func Sinc(x float64) float64 {
 305     if x != 0 {
 306         return math.Sin(x) / x
 307     }
 308     return 1
 309 }
 310 
 311 // Sum adds all the numbers in an array.
 312 func Sum(v ...float64) float64 {
 313     s := 0.0
 314     for _, f := range v {
 315         s += f
 316     }
 317     return s
 318 }
 319 
 320 // Product multiplies all the numbers in an array.
 321 func Product(v ...float64) float64 {
 322     p := 1.0
 323     for _, f := range v {
 324         p *= f
 325     }
 326     return p
 327 }
 328 
 329 // Length calculates the Euclidean length of the vector given.
 330 func Length(v ...float64) float64 {
 331     ss := 0.0
 332     for _, f := range v {
 333         ss += f * f
 334     }
 335     return math.Sqrt(ss)
 336 }
 337 
 338 // Dot calculates the dot product of 2 vectors
 339 func Dot(x []float64, y []float64) float64 {
 340     l := len(x)
 341     if len(y) < l {
 342         l = len(y)
 343     }
 344 
 345     dot := 0.0
 346     for i := 0; i < l; i++ {
 347         dot += x[i] * y[i]
 348     }
 349     return dot
 350 }
 351 
 352 // Min finds the minimum value from the numbers given.
 353 func Min(v ...float64) float64 {
 354     min := +math.Inf(+1)
 355     for _, f := range v {
 356         min = math.Min(min, f)
 357     }
 358     return min
 359 }
 360 
 361 // Max finds the maximum value from the numbers given.
 362 func Max(v ...float64) float64 {
 363     max := +math.Inf(-1)
 364     for _, f := range v {
 365         max = math.Max(max, f)
 366     }
 367     return max
 368 }
 369 
 370 // Hypot calculates the Euclidean n-dimensional hypothenuse from the numbers
 371 // given: all numbers can be lengths, or simply positional coordinates.
 372 func Hypot(v ...float64) float64 {
 373     sumsq := 0.0
 374     for _, f := range v {
 375         sumsq += f * f
 376     }
 377     return math.Sqrt(sumsq)
 378 }
 379 
 380 // Polyval evaluates a polynomial using Horner's algorithm. The array has all
 381 // the coefficients in textbook order, from the highest power down to the final
 382 // constant.
 383 func Polyval(x float64, v ...float64) float64 {
 384     if len(v) == 0 {
 385         return 0
 386     }
 387 
 388     x0 := x
 389     x = 1.0
 390     y := 0.0
 391     for i := len(v) - 1; i >= 0; i-- {
 392         y += v[i] * x
 393         x *= x0
 394     }
 395     return y
 396 }
 397 
 398 // LnGamma is a 1-input 1-output version of math.Lgamma from the stdlib.
 399 func LnGamma(x float64) float64 {
 400     y, s := math.Lgamma(x)
 401     if s < 0 {
 402         return math.NaN()
 403     }
 404     return y
 405 }
 406 
 407 // LnBeta calculates the natural-logarithm of the beta function.
 408 func LnBeta(x float64, y float64) float64 {
 409     return LnGamma(x) + LnGamma(y) - LnGamma(x+y)
 410 }
 411 
 412 // Beta calculates the beta function.
 413 func Beta(x float64, y float64) float64 {
 414     return math.Exp(LnBeta(x, y))
 415 }
 416 
 417 // Factorial calculates the product of all integers in [1, n]
 418 func Factorial(n int) int64 {
 419     return int64(math.Round(math.Gamma(float64(n + 1))))
 420 }
 421 
 422 // IsPrime checks whether an integer is bigger than 1 and can only be fully
 423 // divided by 1 and itself, which is the definition of a prime number.
 424 func IsPrime(n int64) bool {
 425     // prime numbers start at 2
 426     if n < 2 {
 427         return false
 428     }
 429 
 430     // 2 is the only even prime
 431     if n%2 == 0 {
 432         return n == 2
 433     }
 434 
 435     // no divisor can be more than the square root of the target number:
 436     // this limit makes the loop an O(sqrt(n)) one, instead of O(n); this
 437     // is a major algorithmic speedup both in theory and in practice
 438     max := int64(math.Floor(math.Sqrt(float64(n))))
 439 
 440     // the only possible full-divisors are odd integers 3..sqrt(n),
 441     // since reaching this point guarantees n is odd and n > 2
 442     for d := int64(3); d <= max; d += 2 {
 443         if n%d == 0 {
 444             return false
 445         }
 446     }
 447     return true
 448 }
 449 
 450 // LCM finds the least common-multiple of 2 positive integers; when one or
 451 // both inputs aren't positive, this func returns 0.
 452 func LCM(x, y int64) int64 {
 453     if gcd := GCD(x, y); gcd > 0 {
 454         return x * y / gcd
 455     }
 456     return 0
 457 }
 458 
 459 // GCD finds the greatest common-divisor of 2 positive integers; when one or
 460 // both inputs aren't positive, this func returns 0.
 461 func GCD(x, y int64) int64 {
 462     if x < 1 || y < 1 {
 463         return 0
 464     }
 465 
 466     // the loop below requires a >= b
 467     a, b := x, y
 468     if a < b {
 469         a, b = y, x
 470     }
 471 
 472     for b > 0 {
 473         a, b = b, a%b
 474     }
 475     return a
 476 }
 477 
 478 // Perm counts the number of all possible permutations from n objects when
 479 // picking k times. When one or both inputs aren't positive, the result is 0.
 480 func Perm(n, k int) int64 {
 481     if n < k || n < 0 || k < 0 {
 482         return 0
 483     }
 484 
 485     perm := int64(1)
 486     for i := n - k + 1; i <= n; i++ {
 487         perm *= int64(i)
 488     }
 489     return perm
 490 }
 491 
 492 // Choose counts the number of all possible combinations from n objects when
 493 // picking k times. When one or both inputs aren't positive, the result is 0.
 494 func Choose(n, k int) int64 {
 495     if n < k || n < 0 || k < 0 {
 496         return 0
 497     }
 498 
 499     // the log trick isn't always more accurate when there's no overflow:
 500     // for those cases calculate using the textbook definition
 501     f := math.Round(float64(Perm(n, k) / Factorial(k)))
 502     if !math.IsInf(f, 0) {
 503         return int64(f)
 504     }
 505 
 506     // calculate using the log-factorial of n, k, and n - k
 507     a, _ := math.Lgamma(float64(n + 1))
 508     b, _ := math.Lgamma(float64(k + 1))
 509     c, _ := math.Lgamma(float64(n - k + 1))
 510     return int64(math.Round(math.Exp(a - b - c)))
 511 }
 512 
 513 // BinomialMass calculates the probability mass of the binomial random process
 514 // given. When the probability given isn't between 0 and 1, the result is NaN.
 515 func BinomialMass(x, n int, p float64) float64 {
 516     // invalid probability input
 517     if p < 0 || p > 1 {
 518         return math.NaN()
 519     }
 520     // events outside the support are impossible by definition
 521     if x < 0 || x > n {
 522         return 0
 523     }
 524 
 525     q := 1 - p
 526     m := n - x
 527     ncx := float64(Choose(n, x))
 528     return ncx * math.Pow(p, float64(x)) * math.Pow(q, float64(m))
 529 }
 530 
 531 // CumulativeBinomialDensity calculates cumulative probabilities/masses up to
 532 // the value given for the binomial random process given. When the probability
 533 // given isn't between 0 and 1, the result is NaN.
 534 func CumulativeBinomialDensity(x, n int, p float64) float64 {
 535     // invalid probability input
 536     if p < 0 || p > 1 {
 537         return math.NaN()
 538     }
 539     if x < 0 {
 540         return 0
 541     }
 542     if x >= n {
 543         return 1
 544     }
 545 
 546     p0 := p
 547     q0 := 1 - p0
 548     q := math.Pow(q0, float64(n))
 549 
 550     pbinom := 0.0
 551     np1 := float64(n + 1)
 552     for k := 0; k < x; k++ {
 553         a, _ := math.Lgamma(np1)
 554         b, _ := math.Lgamma(float64(k + 1))
 555         c, _ := math.Lgamma(float64(n - k + 1))
 556         // count all possible combinations for this event
 557         ncomb := math.Round(math.Exp(a - b - c))
 558         pbinom += ncomb * p * q
 559         p *= p0
 560         q /= q0
 561     }
 562     return pbinom
 563 }
 564 
 565 // NormalDensity calculates the density at a point along the normal distribution
 566 // given.
 567 func NormalDensity(x float64, mu, sigma float64) float64 {
 568     z := (x - mu) / sigma
 569     return math.Sqrt(0.5/sigma) * math.Exp(-(z*z)/sigma)
 570 }
 571 
 572 // CumulativeNormalDensity calculates the probability of a normal variate of
 573 // being up to the value given.
 574 func CumulativeNormalDensity(x float64, mu, sigma float64) float64 {
 575     z := (x - mu) / sigma
 576     return 0.5 + 0.5*math.Erf(z/math.Sqrt2)
 577 }
 578 
 579 // Epanechnikov is a commonly-used kernel function.
 580 func Epanechnikov(x float64) float64 {
 581     if math.Abs(x) > 1 {
 582         // func is 0 ouside -1..+1
 583         return 0
 584     }
 585     return 0.75 * (1 - x*x)
 586 }
 587 
 588 // Gauss is the commonly-used Gaussian kernel function.
 589 func Gauss(x float64) float64 {
 590     return math.Exp(-(x * x))
 591 }
 592 
 593 // Tricube is a commonly-used kernel function.
 594 func Tricube(x float64) float64 {
 595     a := math.Abs(x)
 596     if a > 1 {
 597         // func is 0 ouside -1..+1
 598         return 0
 599     }
 600 
 601     b := a * a * a
 602     c := 1 - b
 603     return 70.0 / 81.0 * c * c * c
 604 }
 605 
 606 // SolveQuad finds the solutions of a 2nd-degree polynomial, using a formula
 607 // which is more accurate than the textbook one.
 608 func SolveQuad(a, b, c float64) (x1 float64, x2 float64) {
 609     div := 2 * c
 610     disc := math.Sqrt(b*b - 4*a*c)
 611     x1 = div / (-b - disc)
 612     x2 = div / (-b + disc)
 613     return x1, x2
 614 }
     File: ./mathplus/functions_test.go
   1 /*
   2 The MIT License (MIT)
   3 
   4 Copyright (c) 2026 pacman64
   5 
   6 Permission is hereby granted, free of charge, to any person obtaining a copy of
   7 this software and associated documentation files (the "Software"), to deal
   8 in the Software without restriction, including without limitation the rights to
   9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
  10 of the Software, and to permit persons to whom the Software is furnished to do
  11 so, subject to the following conditions:
  12 
  13 The above copyright notice and this permission notice shall be included in all
  14 copies or substantial portions of the Software.
  15 
  16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  22 SOFTWARE.
  23 */
  24 
  25 package mathplus
  26 
  27 import (
  28     "math"
  29     "testing"
  30 )
  31 
  32 func TestRounding(t *testing.T) {
  33     var decadeTests = []struct {
  34         Number   float64
  35         Expected float64
  36     }{
  37         {0, 0},
  38         {-1, -10},
  39         {-0.75, -10},
  40         {-0.725, -10},
  41         {123.532123143, 120},
  42         {1932.532123143, 1930},
  43         {2023.4, 2020},
  44     }
  45 
  46     for _, tc := range decadeTests {
  47         y := Decade(tc.Number)
  48         if y != tc.Expected {
  49             const fs = `decade(%f): expected %f, got %f`
  50             t.Fatalf(fs, tc.Number, tc.Expected, y)
  51         }
  52     }
  53 
  54     var centuryTests = []struct {
  55         Number   float64
  56         Expected float64
  57     }{
  58         {0, 0},
  59         {-1, -100},
  60         {-0.75, -100},
  61         {-0.725, -100},
  62         {123.532123143, 100},
  63         {1932.532123143, 1900},
  64         {2023.4, 2000},
  65     }
  66 
  67     for _, tc := range centuryTests {
  68         y := Century(tc.Number)
  69         if y != tc.Expected {
  70             const fs = `century(%f): expected %f, got %f`
  71             t.Fatalf(fs, tc.Number, tc.Expected, y)
  72         }
  73     }
  74 
  75     var roundingTests = []struct {
  76         Number   float64
  77         Expected [6]float64
  78     }{
  79         {0, [6]float64{0, 0, 0, 0, 0, 0}},
  80         {-1, [6]float64{-1, -1, -1, -1, -1, -1}},
  81         {-0.75, [6]float64{-0.8, -0.75, -0.75, -0.75, -0.75, -0.75}},
  82         {-0.725, [6]float64{-0.7, -0.73, -0.725, -0.725, -0.725, -0.725}},
  83         {123.532123143, [6]float64{123.5, 123.53, 123.532, 123.5321, 123.53212, 123.532123}},
  84         {1932.532123143, [6]float64{1932.5, 1932.53, 1932.532, 1932.5321, 1932.53212, 1932.532123}},
  85         {2023.4, [6]float64{2023.4, 2023.4, 2023.4, 2023.4, 2023.4, 2023.4}},
  86     }
  87 
  88     for _, tc := range roundingTests {
  89         x := tc.Number
  90         y := []float64{
  91             Round1(x), Round2(x), Round3(x), Round4(x), Round5(x), Round6(x),
  92         }
  93 
  94         for i, f := range y {
  95             exp := tc.Expected[i]
  96             if math.Abs(exp-f) > 1e-12 {
  97                 const fs = `r%d(%f): expected %f, got %f`
  98                 t.Fatalf(fs, i+1, tc.Number, exp, f)
  99             }
 100         }
 101     }
 102 }
 103 
 104 var scaleTests = []struct {
 105     Input    float64
 106     InMin    float64
 107     InMax    float64
 108     OutMin   float64
 109     OutMax   float64
 110     Expected float64
 111 }{
 112     {-2, -5, 4, 0, 1, 1.0 / 3},
 113     {0.1, 0, 0.5, -3, 5, -1.4},
 114 }
 115 
 116 func TestScale(t *testing.T) {
 117     for _, tc := range scaleTests {
 118         in := tc.Input
 119         exp := tc.Expected
 120         got := Scale(in, tc.InMin, tc.InMax, tc.OutMin, tc.OutMax)
 121         if got != exp {
 122             const fs = `Scale(%f, %f, %f, %f, %f): expected %f, got %f`
 123             t.Fatalf(fs, in, tc.InMin, tc.InMax, tc.OutMin, tc.OutMax, exp, got)
 124         }
 125     }
 126 }
 127 
 128 func TestIsPrime(t *testing.T) {
 129     var tests = []struct {
 130         Input    int64
 131         Expected bool
 132     }{
 133         {-3, false},
 134         {0, false},
 135         {1, false},
 136         {4, false},
 137         {9, false},
 138         {21, false},
 139 
 140         {2, true},
 141         {3, true},
 142         {5, true},
 143         {19, true},
 144         // 15,485,863 is the millionth prime
 145         {15_485_863, true},
 146     }
 147 
 148     for _, tc := range tests {
 149         if v := IsPrime(tc.Input); v != tc.Expected {
 150             const fs = `isprime(%d) wrongly returned %v`
 151             t.Fatalf(fs, tc.Input, v)
 152         }
 153     }
 154 }
 155 
 156 func TestHorner(t *testing.T) {
 157     var tests = []struct {
 158         X        float64
 159         C        []float64
 160         Expected float64
 161     }{
 162         {2, []float64{1, 2, 3}, 11},
 163         {3, []float64{3, 5, -1}, 41},
 164     }
 165 
 166     for _, tc := range tests {
 167         got := Polyval(tc.X, tc.C...)
 168         if got != tc.Expected {
 169             const fs = `horner(%f, %#v) gave %f, instead of %f`
 170             t.Fatalf(fs, tc.X, tc.C, got, tc.Expected)
 171             return
 172         }
 173     }
 174 }
 175 
 176 func TestGCD(t *testing.T) {
 177     var tests = []struct {
 178         X        int64
 179         Y        int64
 180         Expected int64
 181     }{
 182         {0, 0, 0},
 183         {-1, 10, 0},
 184         {1, -10, 0},
 185         {1, 1, 1},
 186         {1, 7, 1},
 187         {3 * 12, 12, 12},
 188         {1280, 1920, 640},
 189     }
 190 
 191     for _, tc := range tests {
 192         got := GCD(tc.X, tc.Y)
 193         if got != tc.Expected {
 194             const fs = `gcd(%d, %d) gave %d, instead of %d`
 195             t.Fatalf(fs, tc.X, tc.Y, got, tc.Expected)
 196             return
 197         }
 198     }
 199 }
 200 
 201 func TestPerm(t *testing.T) {
 202     var tests = []struct {
 203         X        int
 204         Y        int
 205         Expected int64
 206     }{
 207         {10, 4, 5_040},
 208         {5, 0, 1},
 209         {5, 5, 120},
 210     }
 211 
 212     for _, tc := range tests {
 213         got := Perm(tc.X, tc.Y)
 214         if got != tc.Expected {
 215             const fs = `perm(%d, %d) gave %d, instead of %d`
 216             t.Fatalf(fs, tc.X, tc.Y, got, tc.Expected)
 217             return
 218         }
 219     }
 220 }
 221 
 222 func TestChoose(t *testing.T) {
 223     var tests = []struct {
 224         X        int
 225         Y        int
 226         Expected int64
 227     }{
 228         {10, 4, 210},
 229         {10, 0, 1},
 230         {10, 10, 1},
 231     }
 232 
 233     for _, tc := range tests {
 234         got := Choose(tc.X, tc.Y)
 235         if got != tc.Expected {
 236             const fs = `comb(%d, %d) gave %d, instead of %d`
 237             t.Fatalf(fs, tc.X, tc.Y, got, tc.Expected)
 238             return
 239         }
 240     }
 241 }
     File: ./mathplus/integers.go
   1 /*
   2 The MIT License (MIT)
   3 
   4 Copyright (c) 2026 pacman64
   5 
   6 Permission is hereby granted, free of charge, to any person obtaining a copy of
   7 this software and associated documentation files (the "Software"), to deal
   8 in the Software without restriction, including without limitation the rights to
   9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
  10 of the Software, and to permit persons to whom the Software is furnished to do
  11 so, subject to the following conditions:
  12 
  13 The above copyright notice and this permission notice shall be included in all
  14 copies or substantial portions of the Software.
  15 
  16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  22 SOFTWARE.
  23 */
  24 
  25 package mathplus
  26 
  27 import "math"
  28 
  29 const (
  30     MinInt16 = -(1 << 15)
  31     MinInt32 = -(1 << 31)
  32     MinInt64 = -(1 << 63)
  33 
  34     MaxInt16 = 1<<15 - 1
  35     MaxInt32 = 1<<31 - 1
  36     MaxInt64 = 1<<63 - 1
  37 
  38     MaxUint16 = 1<<16 - 1
  39     MaxUint32 = 1<<32 - 1
  40     MaxUint64 = 1<<64 - 1
  41 )
  42 
  43 func CountIntegerDigits(n int64) int {
  44     if n < 0 {
  45         n = -n
  46     }
  47 
  48     // 0 doesn't have a log10
  49     if n == 0 {
  50         return 1
  51     }
  52     // add 1 to the floored logarithm, since digits are always full and
  53     // with ceiling-like behavior, to use an analogy for this formula
  54     return int(math.Floor(math.Log10(float64(n)))) + 1
  55 }
  56 
  57 func LoopThousandsGroups(n int64, fn func(i, n int)) {
  58     // 0 doesn't have a log10
  59     if n == 0 {
  60         fn(0, 0)
  61         return
  62     }
  63 
  64     sign := +1
  65     if n < 0 {
  66         n = -n
  67         sign = -1
  68     }
  69 
  70     intLog1000 := int(math.Log10(float64(n)) / 3)
  71     remBase := int64(math.Pow10(3 * intLog1000))
  72 
  73     for i := 0; remBase > 0; i++ {
  74         group := (1000 * n) / remBase / 1000
  75         fn(i, sign*int(group))
  76         // if original number was negative, ensure only first
  77         // group gives a negative input to the callback
  78         sign = +1
  79 
  80         n %= remBase
  81         remBase /= 1000
  82     }
  83 }
  84 
  85 var pow10 = []int64{
  86     1,
  87     10,
  88     100,
  89     1000,
  90     10000,
  91     100000,
  92     1000000,
  93     10000000,
  94     100000000,
  95     1000000000, // last entry for int32
  96     10000000000,
  97     100000000000,
  98     1000000000000,
  99     10000000000000,
 100     100000000000000,
 101     1000000000000000,
 102     10000000000000000,
 103     100000000000000000,
 104     1000000000000000000,
 105     // 10000000000000000000,
 106 }
 107 
 108 var pow2 = []int64{
 109     1,
 110     2,
 111     4,
 112     8,
 113     16,
 114     32,
 115     64,
 116     128,
 117     256,
 118     512,
 119     1024,
 120     2048,
 121     4096,
 122     8192,
 123     16384,
 124     32768,
 125     65536,
 126     131072,
 127     262144,
 128     524288,
 129     1048576,
 130     2097152,
 131     4194304,
 132     8388608,
 133     16777216,
 134     33554432,
 135     67108864,
 136     134217728,
 137     268435456,
 138     536870912,
 139     1073741824,
 140     2147483648, // last entry for int32
 141     4294967296,
 142     8589934592,
 143     17179869184,
 144     34359738368,
 145     68719476736,
 146     137438953472,
 147     274877906944,
 148     549755813888,
 149     1099511627776,
 150     2199023255552,
 151     4398046511104,
 152     8796093022208,
 153     17592186044416,
 154     35184372088832,
 155     70368744177664,
 156     140737488355328,
 157     281474976710656,
 158     562949953421312,
 159     1125899906842624,
 160     2251799813685248,
 161     4503599627370496,
 162     9007199254740992,
 163     18014398509481984,
 164     36028797018963968,
 165     72057594037927936,
 166     144115188075855872,
 167     288230376151711744,
 168     576460752303423488,
 169     1152921504606846976,
 170     2305843009213693952,
 171     4611686018427387904,
 172     // 9223372036854775808,
 173 }
 174 
 175 // Log2Int gives you the floor of the base-2 logarithm, or negative results
 176 // for invalid inputs. Values less than 1 aren't supported, since they don't
 177 // have logarithms of any valid base, let alone base-2 logarithms.
 178 func Log2Int(n int64) (log2 int, ok bool) {
 179     if n < 1 {
 180         return -1, false
 181     }
 182 
 183     v := uint64(n)
 184     mask := uint64(1 << 63) // 2**63
 185     for i := int64(63); i > 0; i-- {
 186         if v&mask != 0 {
 187             return int(i), true
 188         }
 189         mask >>= 1
 190     }
 191     return 0, true
 192 }
 193 
 194 // Log10Int gives you the floor of the base-10 logarithm, or negative results
 195 // for invalid inputs. Values less than 1 aren't supported, since they don't
 196 // have logarithms of any valid base, let alone base-10 logarithms.
 197 func Log10Int(n int64) (log10 int, ok bool) {
 198     if n < 1 {
 199         return -1, false
 200     }
 201 
 202     for i := int64(0); i < 19; i++ {
 203         n /= 10
 204         if n == 0 {
 205             return int(i), true
 206         }
 207     }
 208     return 0, true
 209 }
 210 
 211 // Pow10Int gives you the integers powers of 10 as far as int64 allows: negative
 212 // inputs aren't supported, since negative exponents don't give integer results.
 213 func Pow10Int(n int) (power10 int64, ok bool) {
 214     if 0 <= n && n < len(pow10) {
 215         return pow10[n], true
 216     }
 217     return -1, false
 218 }
 219 
 220 // Pow2Int gives you the integers powers of 2 as far as int64 allows: negative
 221 // inputs aren't supported, since negative exponents don't give integer results.
 222 func Pow2Int(n int) (power2 int64, ok bool) {
 223     if 0 <= n && n < len(pow2) {
 224         return pow2[n], true
 225     }
 226     return -1, false
 227 }
     File: ./mathplus/integers_test.go
   1 /*
   2 The MIT License (MIT)
   3 
   4 Copyright (c) 2026 pacman64
   5 
   6 Permission is hereby granted, free of charge, to any person obtaining a copy of
   7 this software and associated documentation files (the "Software"), to deal
   8 in the Software without restriction, including without limitation the rights to
   9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
  10 of the Software, and to permit persons to whom the Software is furnished to do
  11 so, subject to the following conditions:
  12 
  13 The above copyright notice and this permission notice shall be included in all
  14 copies or substantial portions of the Software.
  15 
  16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  22 SOFTWARE.
  23 */
  24 
  25 package mathplus
  26 
  27 import "testing"
  28 
  29 func TestCountIntegerDigits(t *testing.T) {
  30     var tests = []struct {
  31         Input    int64
  32         Expected int
  33     }{
  34         {0, 1},
  35         {-32, 2},
  36         {999, 3},
  37         {3_490, 4},
  38         {12_332, 5},
  39         {999_999, 6},
  40         {1_000_000, 7},
  41         {1_000_001, 7},
  42         {12_345_678, 8},
  43     }
  44 
  45     for _, tc := range tests {
  46         if n := CountIntegerDigits(tc.Input); n != tc.Expected {
  47             const fs = `integer digits in %d: got %d instead of %d`
  48             t.Errorf(fs, tc.Input, n, tc.Expected)
  49         }
  50     }
  51 }
  52 
  53 func TestLoopThousandsGroups(t *testing.T) {
  54     var tests = []struct {
  55         Input    int64
  56         Expected []int
  57     }{
  58         {0, []int{0}},
  59         {-32, []int{-32}}, // negatives not supported yet
  60         {999, []int{999}},
  61         {1_670, []int{1, 670}},
  62         {3_490, []int{3, 490}},
  63         {12_332, []int{12, 332}},
  64         {999_999, []int{999, 999}},
  65         {1_000_000, []int{1, 0, 0}},
  66         {1_000_001, []int{1, 0, 1}},
  67         {1_234_567, []int{1, 234, 567}},
  68     }
  69 
  70     // return
  71     for _, tc := range tests {
  72         count := 0
  73         LoopThousandsGroups(tc.Input, func(i, n int) {
  74             // t.Log(tc.Input, i, n)
  75             if n != tc.Expected[i] {
  76                 const fs = `group %d in %d: got %d instead of %d`
  77                 t.Errorf(fs, i, tc.Input, n, tc.Expected[i])
  78             }
  79             count++
  80         })
  81 
  82         if count != len(tc.Expected) {
  83             const fs = `thousands-groups from %d: got %d instead of %d`
  84             t.Errorf(fs, tc.Input, count, len(tc.Expected))
  85         }
  86     }
  87 }
  88 
  89 func TestLog2Int(t *testing.T) {
  90     var tests = []struct {
  91         Value    int64
  92         Expected int
  93     }{
  94         {-3, -1},
  95         {1, 0},
  96         {2, 1},
  97         {3, 1},
  98         {4, 2},
  99         {1024, 10},
 100         {1_025, 10},
 101         {2*1024 - 1, 10},
 102     }
 103 
 104     for _, tc := range tests {
 105         got, ok := Log2Int(tc.Value)
 106         if got != tc.Expected || (ok && tc.Value < 1) {
 107             const fs = `log2int(%d) = %d, but got %d instead`
 108             t.Fatalf(fs, tc.Value, tc.Expected, got)
 109         }
 110     }
 111 }
 112 
 113 func TestLog10Int(t *testing.T) {
 114     var tests = []struct {
 115         Value    int64
 116         Expected int
 117     }{
 118         {-3, -1},
 119         {1, 0},
 120         {10, 1},
 121         {100, 2},
 122         {101, 2},
 123         {199, 2},
 124         {1_000_000, 6},
 125     }
 126 
 127     for _, tc := range tests {
 128         got, ok := Log10Int(tc.Value)
 129         if got != tc.Expected || (ok && tc.Value < 1) {
 130             const fs = `log10int(%d) = %d, but got %d instead`
 131             t.Fatalf(fs, tc.Value, tc.Expected, got)
 132         }
 133     }
 134 }
     File: ./mathplus/numbers.go
   1 /*
   2 The MIT License (MIT)
   3 
   4 Copyright (c) 2026 pacman64
   5 
   6 Permission is hereby granted, free of charge, to any person obtaining a copy of
   7 this software and associated documentation files (the "Software"), to deal
   8 in the Software without restriction, including without limitation the rights to
   9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
  10 of the Software, and to permit persons to whom the Software is furnished to do
  11 so, subject to the following conditions:
  12 
  13 The above copyright notice and this permission notice shall be included in all
  14 copies or substantial portions of the Software.
  15 
  16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  22 SOFTWARE.
  23 */
  24 
  25 package mathplus
  26 
  27 import (
  28     "math"
  29 )
  30 
  31 const (
  32     // the maximum integer a float64 can represent exactly; since float64
  33     // uses a sign bit, instead of a 2-complement mantissa, -maxflint is
  34     // the minimum integer
  35     maxflint = 2 << 52
  36 )
  37 
  38 // StringWidth counts how many runes it takes to represent the value given as a
  39 // string both as a plain no-commas string and as a string with digit-grouping
  40 // commas, if needed. Each group is 3 digits. Showing numbers with commas makes
  41 // long numbers easier to read.
  42 func StringWidth(f float64, decimals int) (plain, nice int) {
  43     if math.IsNaN(f) || math.IsInf(f, 0) {
  44         return 0, 0
  45     }
  46 
  47     // avoid wrong results, since decimals will be added at the end
  48     if decimals < 0 {
  49         decimals = 0
  50     }
  51 
  52     extras := 0 // count non-digits, such as negatives and dots
  53     if f < 0 {
  54         f = -f   // fix value for uintWidth
  55         extras++ // count the leading negative sign
  56     }
  57     if decimals > 0 {
  58         extras++ // count the decimal dot
  59     }
  60 
  61     // at this point, f >= 0 for sure
  62     plain, nice = uintWidth(f)
  63     plain += extras + decimals
  64     nice += extras + decimals
  65     return plain, nice
  66 }
  67 
  68 // uintWidth can't handle negatives correctly, as its name suggests
  69 func uintWidth(f float64) (plain, nice int) {
  70     // only 1 digit and 0 commas for 0 <= x < 10
  71     if f < 10 {
  72         return 1, 1
  73     }
  74 
  75     mag := math.Log10(math.Floor(f)) // order of magnitude
  76     digits := int(mag) + 1           // integer digits
  77     commas := int(mag) / 3           // commas separating digits in groups of 3
  78     return digits, digits + commas
  79 }
  80 
  81 // Equals allows easily comparing numbers, including NaN values, which otherwise
  82 // never equal anything they're compared to, including themselves.
  83 func Equals(x, y float64) bool {
  84     if x == y {
  85         return true
  86     }
  87     return math.IsNaN(x) && math.IsNaN(y)
  88 }
  89 
  90 // ApproxEqual allows approximate comparisons, as well as checking if both values
  91 // are NaN, which otherwise never equal themselves when compared directly. The
  92 // last parameter must be positive for approx. comparisons, or zero for exact
  93 // comparisons.
  94 func ApproxEqual(x, y float64, maxdiff float64) bool {
  95     if math.Abs(x-y) <= maxdiff {
  96         return true
  97     }
  98     return math.IsNaN(x) && math.IsNaN(y)
  99 }
 100 
 101 // GuessDecimalsCount tries to guess the number of decimal digits of the number
 102 // given.
 103 func GuessDecimalsCount(x float64, max int) int {
 104     if x < 0 {
 105         x = -x
 106     }
 107 
 108     // only up to 16, because log10(2**-53) ~= -15.9546
 109     if !(0 <= max && max <= 16) {
 110         max = 16
 111     }
 112 
 113     const tol = 5e-13 // 1e-11, 1e-12, 5e-13
 114     for digits := 0; digits <= max; digits++ {
 115         _, frac := math.Modf(x)
 116         frac = math.Abs(frac)
 117         // when it's time to stop, the absolute value of the fraction part is
 118         // either extremely close to 0 or extremely close to 1
 119         if frac < tol || (1-frac) < tol {
 120             return digits
 121         }
 122         x *= 10
 123     }
 124     return max
 125 }
 126 
 127 // Default returns the first non-NaN value among those given: failing that,
 128 // the result will be NaN.
 129 func Default(args ...float64) float64 {
 130     for _, x := range args {
 131         if !math.IsNaN(x) {
 132             return x
 133         }
 134     }
 135     return math.NaN()
 136 }
 137 
 138 // TrimSlice ignores all leading/trailing NaN values from the slice given: its
 139 // main use-case is after sorting via sort.Float64s, since all NaN values are
 140 // moved in place to the start/end of the now-sorted slice.
 141 //
 142 // # Example
 143 //
 144 // sort.Float64(values)
 145 // res := mathplus.TrimSlice(values)
 146 func TrimSlice(x []float64) []float64 {
 147     // ignore all leading NaNs
 148     for len(x) > 0 && math.IsNaN(x[0]) {
 149         x = x[1:]
 150     }
 151     // ignore all trailing NaNs
 152     for len(x) > 0 && math.IsNaN(x[len(x)-1]) {
 153         x = x[:len(x)-1]
 154     }
 155     return x
 156 }
 157 
 158 // Linspace works like the Matlab function of the same name, except it takes a
 159 // callback, generalizing its behavior.
 160 func Linspace(a, incr, b float64, f func(x float64)) {
 161     if incr <= 0 {
 162         return
 163     }
 164 
 165     if a < b {
 166         forwardLinspace(a, incr, b, f)
 167     } else if a > b {
 168         backwardLinspace(a, incr, b, f)
 169     } else {
 170         f(a)
 171     }
 172 }
 173 
 174 func forwardLinspace(a, incr, b float64, f func(x float64)) {
 175     for i := 0; true; i++ {
 176         x := float64(i)*incr + a
 177         if x <= b {
 178             f(x)
 179         } else {
 180             return
 181         }
 182     }
 183 }
 184 
 185 func backwardLinspace(a, incr, b float64, f func(x float64)) {
 186     for i := 0; true; i++ {
 187         x := b - float64(i)*incr
 188         if x >= a {
 189             f(x)
 190         } else {
 191             return
 192         }
 193     }
 194 }
 195 
 196 // Seq is a special case of Linspace, where the increment is +/-1.
 197 func Seq(a, b float64, f func(x float64)) {
 198     Linspace(a, 1, b, f)
 199 }
 200 
 201 // Increment increments the number given by adding 1, when it's in the int-safe
 202 // range of float64s; when outside that range, the smallest available delta is
 203 // used instead.
 204 func Increment(x float64) float64 {
 205     if math.IsNaN(x) || math.IsInf(x, 0) {
 206         return x
 207     }
 208     if incr := x + 1; incr != x {
 209         return incr
 210     }
 211     return math.Float64frombits(math.Float64bits(x) + 1)
 212 }
 213 
 214 // Decrement decrements the number given by subtracting 1, when it's in the
 215 // int-safe range of float64s: when outside that range, the smallest available
 216 // delta is used instead.
 217 func Decrement(x float64) float64 {
 218     if math.IsNaN(x) || math.IsInf(x, 0) {
 219         return x
 220     }
 221     if decr := x - 1; decr != x {
 222         return decr
 223     }
 224     // subtracting 1 from the value's uint64 counterpart doesn't work for values
 225     // less than the minimum exact-integer: uint64s use 2-complement arithmetic
 226     return -math.Float64frombits(math.Float64bits(-x) + 1)
 227 }
 228 
 229 func Deapproximate(x float64) (min float64, max float64) {
 230     if math.IsNaN(x) || math.IsInf(x, 0) {
 231         return x, x
 232     }
 233 
 234     if x == 0 {
 235         return -0.5, +0.5
 236     }
 237 
 238     if math.Remainder(x, 1) == 0 {
 239         if math.Remainder(x, 10) != 0 {
 240             return x - 0.5, x + 0.5
 241         }
 242 
 243         sign := Sign(x)
 244         abs := math.Abs(x)
 245         delta := float64(maxExactPow10(int(abs))) / 2
 246         return sign*abs - delta, sign*abs + delta
 247     }
 248 
 249     // return surrounding integers when given non-integers
 250     return math.Floor(x), math.Ceil(x)
 251 }
 252 
 253 func maxExactPow10(x int) int {
 254     pow10 := 1
 255     for {
 256         if x%pow10 != 0 {
 257             return pow10 / 10
 258         }
 259         pow10 *= 10
 260     }
 261 }
     File: ./mathplus/numbers_test.go
   1 /*
   2 The MIT License (MIT)
   3 
   4 Copyright (c) 2026 pacman64
   5 
   6 Permission is hereby granted, free of charge, to any person obtaining a copy of
   7 this software and associated documentation files (the "Software"), to deal
   8 in the Software without restriction, including without limitation the rights to
   9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
  10 of the Software, and to permit persons to whom the Software is furnished to do
  11 so, subject to the following conditions:
  12 
  13 The above copyright notice and this permission notice shall be included in all
  14 copies or substantial portions of the Software.
  15 
  16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  22 SOFTWARE.
  23 */
  24 
  25 package mathplus
  26 
  27 import (
  28     "math"
  29     "strconv"
  30     "testing"
  31 )
  32 
  33 func TestNumberWidth(t *testing.T) {
  34     var tests = []struct {
  35         Input       float64
  36         Decimals    int
  37         PlainWidth  int
  38         PrettyWidth int
  39     }{
  40         {0, 0, len(`0`), len(`0`)},
  41         {0.923123, 0, len(`0`), len(`0`)},
  42         {0.923123, 2, len(`0.92`), len(`0.92`)},
  43         {-3, 0, len(`-3`), len(`-3`)},
  44         {-300, 0, len(`-300`), len(`-300`)},
  45         {+2_300, 0, len(`2300`), len(`2,300`)},
  46         {-1_000_000, 0, len(`-1000000`), len(`-1,000,000`)},
  47         {+1_000_000, 0, len(`1000000`), len(`1,000,000`)},
  48         {+1_610_058.23423, 4, len(`1610058.2342`), len(`1_610_058.2342`)},
  49         {-317_289, 4, len(`-317289.0000`), len(`-317_289.0000`)},
  50         {5_457_013.0000, 4, len(`5457013.0000`), len(`5_457_013.0000`)},
  51         {-118.406800000000004, 15, 20, 20},
  52     }
  53 
  54     for _, tc := range tests {
  55         pl, pr := StringWidth(tc.Input, tc.Decimals)
  56         if pl != tc.PlainWidth {
  57             const fs = `PlainWidth(%f, %d): expected %d, got %d`
  58             t.Fatalf(fs, tc.Input, tc.Decimals, tc.PlainWidth, pl)
  59         }
  60         if pr != tc.PrettyWidth {
  61             const fs = `PrettyWidth(%f, %d): expected %d, got %d`
  62             t.Fatalf(fs, tc.Input, tc.Decimals, tc.PrettyWidth, pr)
  63         }
  64     }
  65 }
  66 
  67 func TestNumberWidthInternal(t *testing.T) {
  68     var tests = []struct {
  69         Number    float64
  70         Decimals  int
  71         IntDigits int
  72         Commas    int
  73     }{
  74         {0, 0, 1, 0},
  75         {-3, 0, 1, 0},
  76         {-300, 0, 3, 0},
  77         {+2_300, 0, 4, 1},
  78         {-1_000_000, 0, 7, 2},
  79         {+1_000_000, 0, 7, 2},
  80         {+1_610_058.23423, 4, 7, 2},
  81         {-317_289, 4, 6, 1},
  82         {5_457_013.0000, 4, 7, 2},
  83     }
  84 
  85     for _, tc := range tests {
  86         // remember that uintWidth can't handle negatives
  87         digits, pretty := uintWidth(math.Abs(tc.Number))
  88 
  89         commas := pretty - digits
  90         if digits != tc.IntDigits || commas != tc.Commas {
  91             const fs = `%f: expected %d and %d, but got %d and %d instead`
  92             t.Fatalf(fs, tc.Number, tc.IntDigits, tc.Commas, digits, commas)
  93         }
  94     }
  95 }
  96 
  97 func TestGuessDecimalsCount(t *testing.T) {
  98     var tests = []struct {
  99         Input    float64
 100         Expected int
 101     }{
 102         {-1, 0},
 103         {-1.3, 1},
 104         {-1.1, 1},
 105         {10.103, 3},
 106         {3.9500, 2},
 107         {1.789000, 3},
 108         {5.0409999000000000, 7},
 109     }
 110 
 111     for _, tc := range tests {
 112         got := GuessDecimalsCount(tc.Input, -1)
 113         if got != tc.Expected {
 114             const fs = `guess(%f, %d) gave %d, but should have been %d`
 115             t.Fatalf(fs, tc.Input, -1, got, tc.Expected)
 116         }
 117     }
 118 
 119     var v = []float64{
 120         1.773, 1.789, 1.773, 1.779, 1.817, 1.797, 1.780, 1.737,
 121         1.723, 1.725, 1.733, 1.742, 1.746, 1.728, 1.726, 1.701,
 122         1.690, 1.675, 1.680, 1.672, 1.685, 1.679, 1.674, 1.653,
 123         1.668, 1.669, 1.680, 1.693, 1.713, 1.712, 1.733, 1.744,
 124         1.739, 1.708, 1.695, 1.691, 1.715, 1.733, 1.730, 1.722,
 125         1.737, 1.749, 1.774, 1.773, 1.789, 1.804, 1.795, 1.752,
 126         1.770, 1.736, 1.717, 1.690, 1.696, 1.703, 1.679, 1.673,
 127         1.694, 1.710, 1.711, 1.739, 1.725, 1.748, 1.764, 1.781,
 128         1.766, 1.741, 1.739, 1.750, 1.752, 1.731, 1.717, 1.699,
 129         1.689, 1.701, 1.713, 1.715, 1.713, 1.780, 1.795, 1.822,
 130         1.832, 1.835, 1.846, 1.857, 1.869, 1.847, 1.831, 1.874,
 131         1.928, 1.900, 1.925, 1.888, 1.842, 1.836, 1.825, 1.804,
 132         1.745, 1.753, 1.779, 1.797, 1.812, 1.820, 1.829, 1.818,
 133         1.799, 1.795, 1.795, 1.792, 1.792, 1.760, 1.741, 1.721,
 134         1.718, 1.734, 1.751, 1.776, 1.768, 1.774, 1.790, 1.813,
 135         1.840, 1.861, 1.853, 1.834, 1.848, 1.859, 1.856, 1.839,
 136         1.826, 1.833, 1.850, 1.861, 1.874, 1.905, 1.931, 1.951,
 137         1.971, 1.985, 1.997, 2.023, 2.047, 2.080, 2.103, 2.139,
 138         2.155, 2.151, 2.142, 2.147, 2.147, 2.165, 2.171, 2.176,
 139         2.174, 2.190, 2.192, 2.184, 2.170, 2.155, 2.155, 2.140,
 140         2.129, 2.124, 2.128, 2.146, 2.135, 2.151, 2.141, 2.134,
 141         2.086, 2.059, 2.014, 1.980, 1.969, 1.972, 1.953, 1.927,
 142         1.915, 1.901, 1.905, 1.941, 1.951, 1.940, 1.949, 1.959,
 143         1.985, 1.995, 2.001, 2.002, 2.017, 2.018, 2.006, 1.997,
 144         1.994, 1.987, 1.978, 1.977, 1.940, 1.928, 1.909, 1.813,
 145         1.736, 1.710, 1.712, 1.726, 1.740, 1.747, 1.748, 1.745,
 146         1.728, 1.714, 1.656, 1.649, 1.642, 1.630, 1.612, 1.588,
 147         1.583, 1.570, 1.568, 1.561, 1.537, 1.542, 1.548, 1.538,
 148         1.520, 1.521, 1.513, 1.494, 1.469, 1.458, 1.449, 1.441,
 149         1.434, 1.428, 1.431, 1.440, 1.447, 1.457, 1.456, 1.457,
 150         1.450, 1.451, 1.439, 1.423, 1.374, 1.100, 1.147, 1.134,
 151         1.121, 1.119, 1.115,
 152     }
 153 
 154     for i, x := range v {
 155         guess := gdc(t, x)
 156         // guess := GuessDecimalsCount(v, -1)
 157         if guess > 3 {
 158             _, diff := math.Modf(1000 * x)
 159             t.Logf("len: %d\n", len(v))
 160             const fs = `(item %2d) %f doesn't have %d decimals; diff: %.20f`
 161             t.Fatalf(fs, i+1, x, guess, diff)
 162         }
 163     }
 164 
 165     v = []float64{
 166         6000.00, 2300.00, 2000.00, 1700.00, 1500.00, 1500.00, 1400.00, 1300.00,
 167         900.00, 800.00, 800.00, 700.00, 600.00, 550.00, 550.00, 400.00,
 168         320.39, 300.00, 200.00, 198.56, 155.94, 155.73, 98.15, 80.00,
 169         80.00, 74.50, 70.00, 70.00, 70.00, 70.00, 70.00, 65.00,
 170         60.00, 55.45, 49.00, 35.00, 35.00, 25.00, 25.00, 25.00,
 171         20.00, 15.00, 15.00, 12.00, 10.00, 10.00, 10.00, 10.00,
 172         7.00, 6.00, 6.00, 5.00, 3.00, 2.00, 1.00,
 173     }
 174     for i, x := range v {
 175         guess := gdc(t, x)
 176         // guess := GuessDecimalsCount(v, -1)
 177         if guess > 2 {
 178             _, diff := math.Modf(100 * x)
 179             t.Logf("len: %d\n", len(v))
 180             const fs = `(item %2d) %f doesn't have %d decimals; diff: %.20f`
 181             t.Fatalf(fs, i+1, x, guess, diff)
 182         }
 183     }
 184 }
 185 
 186 // gdc logs each step of the loop inside GuessDecimalsCount
 187 func gdc(t *testing.T, x float64) int {
 188     const max = 16
 189     const tol = 5e-11 // 5e-13
 190     for digits := 0; digits <= max; digits++ {
 191         _, frac := math.Modf(x)
 192         const fs = "x: %f\tdigits: %d\tdiff: %.20f\tcdiff: %.20f\n"
 193         t.Logf(fs, x, digits, frac, 1-frac)
 194         ar := math.Abs(frac)
 195         if ar < tol || (1-ar) < tol {
 196             return digits
 197         }
 198         x *= 10
 199     }
 200     return max
 201 }
 202 
 203 func TestIncrement(t *testing.T) {
 204     var tests = []struct {
 205         Input    float64
 206         Expected float64
 207     }{
 208         {math.NaN(), math.NaN()},
 209         {math.Inf(-1), math.Inf(-1)},
 210         {math.Inf(+1), math.Inf(+1)},
 211 
 212         {-maxflint - 2, -maxflint},
 213         {-maxflint, -maxflint + 1},
 214         {-1, 0},
 215         {0, 1},
 216         {10.25, 11.25},
 217         {maxflint - 1, maxflint},
 218         {maxflint, maxflint + 2},
 219     }
 220 
 221     for _, tc := range tests {
 222         name := strconv.FormatFloat(tc.Input, 'f', 2, 64)
 223         t.Run(name, func(t *testing.T) {
 224             got := Increment(tc.Input)
 225             if !Equals(got, tc.Expected) {
 226                 const fs = `got %v, instead of %v`
 227                 t.Fatalf(fs, got, tc.Expected)
 228             }
 229         })
 230     }
 231 }
 232 
 233 func TestDecrement(t *testing.T) {
 234     var tests = []struct {
 235         Input    float64
 236         Expected float64
 237     }{
 238         {math.NaN(), math.NaN()},
 239         {math.Inf(-1), math.Inf(-1)},
 240         {math.Inf(+1), math.Inf(+1)},
 241 
 242         {-maxflint, -maxflint - 2},
 243         {-maxflint + 1, -maxflint},
 244         {-1, -2},
 245         {0, -1},
 246         {11.25, 10.25},
 247         {maxflint, maxflint - 1},
 248         {maxflint + 2, maxflint},
 249     }
 250 
 251     for _, tc := range tests {
 252         name := strconv.FormatFloat(tc.Input, 'f', 2, 64)
 253         t.Run(name, func(t *testing.T) {
 254             got := Decrement(tc.Input)
 255             if !Equals(got, tc.Expected) {
 256                 const fs = `got %v, instead of %v`
 257                 t.Fatalf(fs, got, tc.Expected)
 258             }
 259         })
 260     }
 261 }
 262 
 263 func TestDeapproximate(t *testing.T) {
 264     var tests = []struct {
 265         Input       float64
 266         ExpectedMin float64
 267         ExpectedMax float64
 268     }{
 269         {0, -0.5, +0.5},
 270         {1, 0.5, 1.5},
 271         {10, 5, 15},
 272         {100, 50, 150},
 273         {101, 100.5, 101.5},
 274         {102, 101.5, 102.5},
 275         {120, 115, 125},
 276     }
 277 
 278     for _, tc := range tests {
 279         name := strconv.FormatFloat(tc.Input, 'f', 6, 64)
 280         t.Run(name, func(t *testing.T) {
 281             min, max := Deapproximate(tc.Input)
 282             if !Equals(min, tc.ExpectedMin) || !Equals(max, tc.ExpectedMax) {
 283                 const fs = `got %f and %f, instead of %f and %f`
 284                 t.Fatalf(fs, min, max, tc.ExpectedMin, tc.ExpectedMax)
 285             }
 286         })
 287     }
 288 }
 289 
 290 func TestMaxExactPow10(t *testing.T) {
 291     var tests = []struct {
 292         Input    int
 293         Expected int
 294     }{
 295         // {0, 1},
 296         {1, 1},
 297         {4, 1},
 298         {10, 10},
 299         {100, 100},
 300         {101, 1},
 301         {102, 1},
 302         {120, 10},
 303     }
 304 
 305     for _, tc := range tests {
 306         name := strconv.Itoa(tc.Input)
 307         t.Run(name, func(t *testing.T) {
 308             got := maxExactPow10(int(tc.Input))
 309             if got != int(tc.Expected) {
 310                 const fs = `got %d, instead of %d`
 311                 t.Fatalf(fs, got, tc.Expected)
 312             }
 313         })
 314     }
 315 }
     File: ./mathplus/statistics.go
   1 /*
   2 The MIT License (MIT)
   3 
   4 Copyright (c) 2026 pacman64
   5 
   6 Permission is hereby granted, free of charge, to any person obtaining a copy of
   7 this software and associated documentation files (the "Software"), to deal
   8 in the Software without restriction, including without limitation the rights to
   9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
  10 of the Software, and to permit persons to whom the Software is furnished to do
  11 so, subject to the following conditions:
  12 
  13 The above copyright notice and this permission notice shall be included in all
  14 copies or substantial portions of the Software.
  15 
  16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  22 SOFTWARE.
  23 */
  24 
  25 package mathplus
  26 
  27 import (
  28     "math"
  29 )
  30 
  31 // Quantile calculates a sorted array's quantile, parameterized by a number in
  32 // [0, 1]; the sorted array must be in increasing order and can't contain any
  33 // NaN value.
  34 func Quantile(x []float64, q float64) float64 {
  35     l := len(x)
  36     // quantiles aren't defined for empty arrays/samples
  37     if l == 0 {
  38         return math.NaN()
  39     }
  40 
  41     // calculate indices of surrounding values, which match when index isn't
  42     // fractional
  43     mid := float64(l-1) * q
  44     low := math.Floor(mid)
  45     high := math.Ceil(mid)
  46 
  47     // for fractional indices, interpolate their 2 surrounding values
  48     a := x[int(low)]
  49     b := x[int(high)]
  50     return a + (mid-low)*(b-a)
  51 }
  52 
  53 // welford has almost everything needed to implement Welford's running-stats
  54 // algorithm for the arithmetic mean and the standard deviation: the only
  55 // thing missing is the count of values so far, which you must provide every
  56 // time you call its methods.
  57 type welford struct {
  58     Mean          float64
  59     meanOfSquares float64
  60 }
  61 
  62 // Update advances Welford's algorithm, using the value and item-count given.
  63 func (w *welford) Update(x float64, n int) {
  64     d1 := x - w.Mean
  65     w.Mean += d1 / float64(n)
  66     d2 := x - w.Mean
  67     w.meanOfSquares += d1 * d2
  68 }
  69 
  70 // SD calculates the current standard-deviation. The first parameter is the
  71 // bias-correction to use: 0 gives you the `population` SD, 1 gives you the
  72 // `sample` SD; 1.5 is very-rarely used and only in special circumstances.
  73 func (w welford) SD(bias float64, n int) float64 {
  74     denom := float64(n) - bias
  75     return math.Sqrt(w.meanOfSquares / denom)
  76 }
  77 
  78 // RMS calculates the current root-mean-square statistic.
  79 func (w welford) RMS() float64 {
  80     return math.Sqrt(w.meanOfSquares)
  81 }
  82 
  83 // NumberSummary has/updates numeric constant-space running stats.
  84 type NumberSummary struct {
  85     // struct welford has the Mean public field
  86     welford
  87 
  88     // Count is how many values there are so far, including NaNs
  89     Count int
  90 
  91     // NaN counts NaN values so far
  92     NaN int
  93 
  94     // Integers counts all integers so far
  95     Integers int
  96 
  97     // Negatives counts all negative number so far
  98     Negatives int
  99 
 100     // Zeros counts all zeros so far
 101     Zeros int
 102 
 103     // Positives counts all positive numbers so far
 104     Positives int
 105 
 106     // Min is the least number so far, ignoring NaNs
 107     Min float64
 108 
 109     // Max is the highest number so far, ignoring NaNs
 110     Max float64
 111 
 112     // Sum is the sum of all numbers so far, ignoring NaNs
 113     Sum float64
 114 
 115     // sumOfLogs is used to calculate the geometric mean
 116     sumOfLogs float64
 117 }
 118 
 119 // Update does exactly what it says.
 120 func (ns *NumberSummary) Update(f float64) {
 121     if ns.Count == 0 {
 122         ns.Min = math.Inf(+1)
 123         ns.Max = math.Inf(-1)
 124     }
 125 
 126     ns.Count++
 127     if math.IsNaN(f) {
 128         ns.NaN++
 129         return
 130     }
 131 
 132     if _, r := math.Modf(f); r == 0 {
 133         ns.Integers++
 134     }
 135 
 136     if f > 0 {
 137         ns.Positives++
 138     } else if f == 0 {
 139         ns.Zeros++
 140     } else if f < 0 {
 141         ns.Negatives++
 142     }
 143 
 144     ns.Sum += f
 145     ns.sumOfLogs += math.Log(f)
 146     ns.Min = math.Min(ns.Min, f)
 147     ns.Max = math.Max(ns.Max, f)
 148     ns.welford.Update(f, ns.Valid())
 149 }
 150 
 151 // Valid finds how many numbers are valid so far
 152 func (ns NumberSummary) Valid() int {
 153     return ns.Count - ns.NaN
 154 }
 155 
 156 // Invalid finds how many numbers are invalid so far
 157 func (ns NumberSummary) Invalid() int {
 158     return ns.NaN
 159 }
 160 
 161 // Geomean calculates the current geometric mean
 162 func (ns NumberSummary) Geomean() float64 {
 163     if ns.Negatives > 0 || ns.Zeros > 0 {
 164         return math.NaN()
 165     }
 166     return math.Exp(ns.sumOfLogs / float64(ns.Valid()))
 167 }
 168 
 169 // SD calculates the current standard-deviation. The only parameter is the
 170 // bias-correction to use:
 171 //
 172 //  0 means calculate the current population standard-deviation
 173 //  1 means calculate the current sample standard-deviation
 174 //  1.5 is very-rarely used and only in special circumstances
 175 func (ns NumberSummary) SD(bias float64) float64 {
 176     return ns.welford.SD(bias, ns.Valid())
 177 }
 178 
 179 // CommonQuantiles groups all the most commonly-used quantiles in practice.
 180 //
 181 // Funcs AppendAllDeciles and AppendAllPercentiles give you other sets of
 182 // commonly-used ranking stats.
 183 type CommonQuantiles struct {
 184     Min float64
 185     P01 float64 // 1st percentile
 186     P05 float64 // 5th percentile
 187     P10 float64 // 10th percentile, also the 1st decile
 188     P25 float64 // 1st quartile, also the 25th percentile
 189     P50 float64 // 2nd quartile, also the 50th percentile
 190     P75 float64 // 3rd quartile, also the 75th percentile
 191     P90 float64
 192     P95 float64
 193     P99 float64
 194     Max float64
 195 }
 196 
 197 // NewCommonQuantiles is a convenience constructor for struct CommonQuantiles.
 198 func NewCommonQuantiles(x []float64) CommonQuantiles {
 199     return CommonQuantiles{
 200         Min: Quantile(x, 0.00),
 201         P01: Quantile(x, 0.01),
 202         P05: Quantile(x, 0.05),
 203         P10: Quantile(x, 0.10),
 204         P25: Quantile(x, 0.25),
 205         P50: Quantile(x, 0.50),
 206         P75: Quantile(x, 0.75),
 207         P90: Quantile(x, 0.90),
 208         P95: Quantile(x, 0.95),
 209         P99: Quantile(x, 0.99),
 210         Max: Quantile(x, 1.00),
 211     }
 212 }
 213 
 214 // AppendAllDeciles appends 11 items to the slice given, which ultimately
 215 // lets you reuse slices for multiple such calculations, thus avoiding extra
 216 // allocations.
 217 //
 218 // The 1st item returned is the minimum, and the last one is the maximum.
 219 func AppendAllDeciles(dest []float64, x []float64) []float64 {
 220     for i := 0; i <= 10; i++ {
 221         dest = append(dest, Quantile(x, float64(i)/10))
 222     }
 223     return dest
 224 }
 225 
 226 // AppendAllPercentiles appends 101 items to the slice given, which ultimately
 227 // lets you reuse slices for multiple such calculations, thus avoiding extra
 228 // allocations.
 229 //
 230 // The 1st item returned is the minimum, while the last one is the maximum.
 231 func AppendAllPercentiles(dest []float64, x []float64) []float64 {
 232     for i := 0; i <= 100; i++ {
 233         dest = append(dest, Quantile(x, float64(i)/100))
 234     }
 235     return dest
 236 }
     File: ./mathplus/statistics_test.go
   1 /*
   2 The MIT License (MIT)
   3 
   4 Copyright (c) 2026 pacman64
   5 
   6 Permission is hereby granted, free of charge, to any person obtaining a copy of
   7 this software and associated documentation files (the "Software"), to deal
   8 in the Software without restriction, including without limitation the rights to
   9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
  10 of the Software, and to permit persons to whom the Software is furnished to do
  11 so, subject to the following conditions:
  12 
  13 The above copyright notice and this permission notice shall be included in all
  14 copies or substantial portions of the Software.
  15 
  16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  22 SOFTWARE.
  23 */
  24 
  25 package mathplus
  26 
  27 import (
  28     "math"
  29     "testing"
  30 )
  31 
  32 type NumericTestResult struct {
  33     Count     int
  34     Valid     int
  35     Integers  int
  36     Negatives int
  37     Zeros     int
  38     Positives int
  39 
  40     Min     float64
  41     Max     float64
  42     Sum     float64
  43     Mean    float64
  44     Geomean float64
  45     SD      float64
  46     RMS     float64
  47 }
  48 
  49 func (tr NumericTestResult) Match(ns NumberSummary) bool {
  50     return true &&
  51         tr.Count == ns.Count &&
  52         tr.Valid == ns.Valid() &&
  53         tr.Integers == ns.Integers &&
  54         tr.Negatives == ns.Negatives &&
  55         tr.Zeros == ns.Zeros &&
  56         tr.Positives == ns.Positives &&
  57         equals(tr.Min, ns.Min) &&
  58         equals(tr.Max, ns.Max) &&
  59         equals(tr.Sum, ns.Sum) &&
  60         equals(tr.Mean, ns.Mean) &&
  61         equals(tr.Geomean, ns.Geomean()) &&
  62         equals(tr.SD, ns.SD(0)) &&
  63         equals(tr.RMS, ns.RMS()) &&
  64         true
  65 }
  66 
  67 func (tr NumericTestResult) Test(t *testing.T, ns NumberSummary) {
  68     var checks = []struct {
  69         X       float64
  70         Y       float64
  71         Message string
  72     }{
  73         {float64(tr.Count), float64(ns.Count), `count`},
  74         {float64(tr.Valid), float64(ns.Valid()), `valid`},
  75         {float64(tr.Integers), float64(ns.Integers), `integers`},
  76         {float64(tr.Negatives), float64(ns.Negatives), `negatives`},
  77         {float64(tr.Zeros), float64(ns.Zeros), `zeros`},
  78         {float64(tr.Positives), float64(ns.Positives), `positives`},
  79         {float64(tr.Min), float64(ns.Min), `min`},
  80         {float64(tr.Max), float64(ns.Max), `max`},
  81         {float64(tr.Sum), float64(ns.Sum), `sum`},
  82         {float64(tr.Mean), float64(ns.Mean), `mean`},
  83         {float64(tr.Geomean), float64(ns.Geomean()), `geomean`},
  84         {float64(tr.SD), float64(ns.SD(0)), `sd`},
  85         {float64(tr.RMS), float64(ns.RMS()), `rms`},
  86     }
  87 
  88     const fs = "field %q failed: %f and %f differ\nexpected %#v\ngot      %#v"
  89     for _, tc := range checks {
  90         if !equals(tc.X, tc.Y) {
  91             t.Fatalf(fs, tc.Message, tc.X, tc.Y, tr, ns)
  92             return
  93         }
  94     }
  95 }
  96 
  97 var numericTests = []struct {
  98     Description string
  99     Input       []float64
 100     Expected    NumericTestResult
 101 }{
 102     {
 103         Description: `no values`,
 104         Input:       []float64{},
 105         Expected: NumericTestResult{
 106             Count:    0,
 107             Valid:    0,
 108             Integers: 0,
 109             // Min:      math.Inf(+1),
 110             // Max:      math.Inf(-1),
 111             Min:     0.0,
 112             Max:     0.0,
 113             Sum:     0.0,
 114             Mean:    0.0,
 115             Geomean: math.NaN(),
 116             SD:      math.NaN(),
 117             RMS:     0.0,
 118         },
 119     },
 120     {
 121         Description: `just a value`,
 122         Input:       []float64{-3.5},
 123         Expected: NumericTestResult{
 124             Count:     1,
 125             Valid:     1,
 126             Integers:  0,
 127             Negatives: 1,
 128             Zeros:     0,
 129             Positives: 0,
 130             Min:       -3.5,
 131             Max:       -3.5,
 132             Sum:       -3.5,
 133             Mean:      -3.5,
 134             Geomean:   math.NaN(),
 135             SD:        0.0,
 136             RMS:       0.0,
 137         },
 138     },
 139     {
 140         Description: `integers 1..10`,
 141         Input:       []float64{math.NaN(), 1, 2, 3, 4, 5, 6, 7, 8, 9, 10},
 142         Expected: NumericTestResult{
 143             Count:     11,
 144             Valid:     10,
 145             Integers:  10,
 146             Negatives: 0,
 147             Zeros:     0,
 148             Positives: 10,
 149             Min:       1,
 150             Max:       10,
 151             Sum:       55,
 152             Mean:      5.5,
 153             Geomean:   4.528728688116766,
 154             RMS:       9.082951062292475,
 155             SD:        2.8722813232690143,
 156         },
 157     },
 158     {
 159         Description: `nothing valid`,
 160         Input: []float64{
 161             math.NaN(), math.NaN(), math.NaN(), math.NaN(), math.NaN(),
 162         },
 163         Expected: NumericTestResult{
 164             Count:    5,
 165             Valid:    0,
 166             Integers: 0,
 167             Min:      math.Inf(+1),
 168             Max:      math.Inf(-1),
 169             Sum:      0.0,
 170             Mean:     0.0,
 171             Geomean:  math.NaN(),
 172             SD:       math.NaN(),
 173             RMS:      0.0,
 174         },
 175     },
 176 }
 177 
 178 func TestNumberSummary(t *testing.T) {
 179     for _, tc := range numericTests {
 180         var s NumberSummary
 181         for _, x := range tc.Input {
 182             s.Update(x)
 183         }
 184         tc.Expected.Test(t, s)
 185     }
 186 }
 187 
 188 func equals(x, y float64) bool {
 189     if x == y {
 190         return true
 191     }
 192     return math.IsNaN(x) && math.IsNaN(y)
 193 }
     File: ./mit-license.txt
   1 The MIT License (MIT)
   2 
   3 Copyright (c) 2026 pacman64
   4 
   5 Permission is hereby granted, free of charge, to any person obtaining a copy of
   6 this software and associated documentation files (the "Software"), to deal
   7 in the Software without restriction, including without limitation the rights to
   8 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
   9 of the Software, and to permit persons to whom the Software is furnished to do
  10 so, subject to the following conditions:
  11 
  12 The above copyright notice and this permission notice shall be included in all
  13 copies or substantial portions of the Software.
  14 
  15 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  16 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  17 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  18 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  19 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  20 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  21 SOFTWARE.
     File: ./ncol/ncol.go
   1 /*
   2 The MIT License (MIT)
   3 
   4 Copyright (c) 2026 pacman64
   5 
   6 Permission is hereby granted, free of charge, to any person obtaining a copy of
   7 this software and associated documentation files (the "Software"), to deal
   8 in the Software without restriction, including without limitation the rights to
   9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
  10 of the Software, and to permit persons to whom the Software is furnished to do
  11 so, subject to the following conditions:
  12 
  13 The above copyright notice and this permission notice shall be included in all
  14 copies or substantial portions of the Software.
  15 
  16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  22 SOFTWARE.
  23 */
  24 
  25 package ncol
  26 
  27 import (
  28     "bufio"
  29     "errors"
  30     "io"
  31     "os"
  32     "strconv"
  33     "strings"
  34     "unicode/utf8"
  35 )
  36 
  37 const info = `
  38 ncol [options...] [filenames...]
  39 
  40 
  41 Nice COLumns realigns and styles data tables using ANSI color sequences. In
  42 particular, all auto-detected numbers are styled so they're easier to read
  43 at a glance. Input tables can be either lines of space-separated values or
  44 tab-separated values, and are auto-detected using the first non-empty line.
  45 
  46 When not given filepaths to read data from, this tool reads from standard
  47 input by default.
  48 
  49 The options are, available both in single and double-dash versions
  50 
  51     -h, -help              show this help message
  52     -m, -max-columns       use the row with the most items for the item-count
  53     -no-sums, -unsummed    avoid showing a final row with column sums
  54     -no-tiles, -untiled    avoid showing color-coded tiles
  55     -s, -simple            avoid showing color-coded tiles and sums
  56 `
  57 
  58 const columnGap = 2
  59 
  60 // altDigitStyle is used to make 4+ digit-runs easier to read
  61 const altDigitStyle = "\x1b[38;2;168;168;168m"
  62 
  63 func Main() {
  64     sums := true
  65     tiles := true
  66     maxCols := false
  67     args := os.Args[1:]
  68 
  69     for len(args) > 0 {
  70         switch args[0] {
  71         case `-h`, `--h`, `-help`, `--help`:
  72             os.Stdout.WriteString(info[1:])
  73             return
  74 
  75         case
  76             `-m`, `--m`,
  77             `-maxcols`, `--maxcols`,
  78             `-max-columns`, `--max-columns`:
  79             maxCols = true
  80             args = args[1:]
  81             continue
  82 
  83         case
  84             `-no-sums`, `--no-sums`, `-no-totals`, `--no-totals`,
  85             `-unsummed`, `--unsummed`, `-untotaled`, `--untotaled`,
  86             `-untotalled`, `--untotalled`:
  87             sums = false
  88             args = args[1:]
  89             continue
  90 
  91         case `-no-tiles`, `--no-tiles`, `-untiled`, `--untiled`:
  92             tiles = false
  93             args = args[1:]
  94             continue
  95 
  96         case `-s`, `--s`, `-simple`, `--simple`:
  97             sums = false
  98             tiles = false
  99             args = args[1:]
 100             continue
 101         }
 102 
 103         break
 104     }
 105 
 106     if len(args) > 0 && args[0] == `--` {
 107         args = args[1:]
 108     }
 109 
 110     var res table
 111     res.MaxColumns = maxCols
 112     res.ShowTiles = tiles
 113     res.ShowSums = sums
 114 
 115     if err := run(args, &res); err != nil {
 116         os.Stderr.WriteString(err.Error())
 117         os.Stderr.WriteString("\n")
 118         os.Exit(1)
 119     }
 120 }
 121 
 122 // table has all summary info gathered from the data, along with the row
 123 // themselves, stored as lines/strings
 124 type table struct {
 125     Columns int
 126 
 127     Rows []string
 128 
 129     MaxWidth []int
 130 
 131     MaxDotDecimals []int
 132 
 133     Numeric []int
 134 
 135     Sums []float64
 136 
 137     LoopItems func(line string, items int, t *table, f itemFunc) int
 138 
 139     sb strings.Builder
 140 
 141     MaxColumns bool
 142 
 143     ShowTiles bool
 144 
 145     ShowSums bool
 146 }
 147 
 148 type itemFunc func(i int, s string, t *table)
 149 
 150 func run(paths []string, res *table) error {
 151     for _, p := range paths {
 152         if err := handleFile(res, p); err != nil {
 153             return err
 154         }
 155     }
 156 
 157     if len(paths) == 0 {
 158         if err := handleReader(res, os.Stdin); err != nil {
 159             return err
 160         }
 161     }
 162 
 163     bw := bufio.NewWriterSize(os.Stdout, 32*1024)
 164     defer bw.Flush()
 165     realign(bw, res)
 166     return nil
 167 }
 168 
 169 func handleFile(res *table, path string) error {
 170     f, err := os.Open(path)
 171     if err != nil {
 172         // on windows, file-not-found error messages may mention `CreateFile`,
 173         // even when trying to open files in read-only mode
 174         return errors.New(`can't open file named ` + path)
 175     }
 176     defer f.Close()
 177     return handleReader(res, f)
 178 }
 179 
 180 func handleReader(t *table, r io.Reader) error {
 181     const gb = 1024 * 1024 * 1024
 182     sc := bufio.NewScanner(r)
 183     sc.Buffer(nil, 8*gb)
 184 
 185     const maxInt = int(^uint(0) >> 1)
 186     maxCols := maxInt
 187 
 188     for i := 0; sc.Scan(); i++ {
 189         s := sc.Text()
 190         if i == 0 && strings.HasPrefix(s, "\xef\xbb\xbf") {
 191             s = s[3:]
 192         }
 193 
 194         if len(s) == 0 {
 195             continue
 196         }
 197 
 198         t.Rows = append(t.Rows, s)
 199 
 200         if t.Columns == 0 {
 201             if t.LoopItems == nil {
 202                 if strings.IndexByte(s, '\t') >= 0 {
 203                     t.LoopItems = loopItemsTSV
 204                 } else {
 205                     t.LoopItems = loopItemsSSV
 206                 }
 207             }
 208 
 209             if !t.MaxColumns {
 210                 t.Columns = t.LoopItems(s, maxCols, t, doNothing)
 211                 maxCols = t.Columns
 212             }
 213         }
 214 
 215         t.LoopItems(s, maxCols, t, updateItem)
 216     }
 217 
 218     return sc.Err()
 219 }
 220 
 221 // doNothing is given to LoopItems to count items, while doing nothing else
 222 func doNothing(i int, s string, t *table) {}
 223 
 224 func updateItem(i int, s string, t *table) {
 225     // ensure column-info-slices have enough room
 226     if i >= len(t.MaxWidth) {
 227         // update column-count if in max-columns mode
 228         if t.MaxColumns {
 229             t.Columns = i + 1
 230         }
 231         t.MaxWidth = append(t.MaxWidth, 0)
 232         t.MaxDotDecimals = append(t.MaxDotDecimals, 0)
 233         t.Numeric = append(t.Numeric, 0)
 234         t.Sums = append(t.Sums, 0)
 235     }
 236 
 237     // keep track of widest rune-counts for each column
 238     w := countWidth(s)
 239     if t.MaxWidth[i] < w {
 240         t.MaxWidth[i] = w
 241     }
 242 
 243     // update stats for numeric items
 244     if isNumeric(s, &(t.sb)) {
 245         dd := countDotDecimals(s)
 246         if t.MaxDotDecimals[i] < dd {
 247             t.MaxDotDecimals[i] = dd
 248         }
 249 
 250         t.Numeric[i]++
 251         f, _ := strconv.ParseFloat(t.sb.String(), 64)
 252         t.Sums[i] += f
 253     }
 254 }
 255 
 256 // loopItemsSSV loops over a line's items, allocation-free style; when given
 257 // empty strings, the callback func is never called
 258 func loopItemsSSV(s string, max int, t *table, f itemFunc) int {
 259     i := 0
 260     s = trimTrailingSpaces(s)
 261 
 262     for {
 263         s = trimLeadingSpaces(s)
 264         if len(s) == 0 {
 265             return i
 266         }
 267 
 268         if i+1 == max {
 269             f(i, s, t)
 270             return i + 1
 271         }
 272 
 273         j := strings.IndexByte(s, ' ')
 274         if j < 0 {
 275             f(i, s, t)
 276             return i + 1
 277         }
 278 
 279         f(i, s[:j], t)
 280         s = s[j+1:]
 281         i++
 282     }
 283 }
 284 
 285 func trimLeadingSpaces(s string) string {
 286     for len(s) > 0 && s[0] == ' ' {
 287         s = s[1:]
 288     }
 289     return s
 290 }
 291 
 292 func trimTrailingSpaces(s string) string {
 293     for len(s) > 0 && s[len(s)-1] == ' ' {
 294         s = s[:len(s)-1]
 295     }
 296     return s
 297 }
 298 
 299 // loopItemsTSV loops over a line's tab-separated items, allocation-free style;
 300 // when given empty strings, the callback func is never called
 301 func loopItemsTSV(s string, max int, t *table, f itemFunc) int {
 302     if len(s) == 0 {
 303         return 0
 304     }
 305 
 306     i := 0
 307 
 308     for {
 309         if i+1 == max {
 310             f(i, s, t)
 311             return i + 1
 312         }
 313 
 314         j := strings.IndexByte(s, '\t')
 315         if j < 0 {
 316             f(i, s, t)
 317             return i + 1
 318         }
 319 
 320         f(i, s[:j], t)
 321         s = s[j+1:]
 322         i++
 323     }
 324 }
 325 
 326 func skipLeadingEscapeSequences(s string) string {
 327     for len(s) >= 2 {
 328         if s[0] != '\x1b' {
 329             return s
 330         }
 331 
 332         switch s[1] {
 333         case '[':
 334             s = skipSingleLeadingANSI(s[2:])
 335 
 336         case ']':
 337             if len(s) < 3 || s[2] != '8' {
 338                 return s
 339             }
 340             s = skipSingleLeadingOSC(s[3:])
 341 
 342         default:
 343             return s
 344         }
 345     }
 346 
 347     return s
 348 }
 349 
 350 func skipSingleLeadingANSI(s string) string {
 351     for len(s) > 0 {
 352         upper := s[0] &^ 32
 353         s = s[1:]
 354         if 'A' <= upper && upper <= 'Z' {
 355             break
 356         }
 357     }
 358 
 359     return s
 360 }
 361 
 362 func skipSingleLeadingOSC(s string) string {
 363     var prev byte
 364 
 365     for len(s) > 0 {
 366         b := s[0]
 367         s = s[1:]
 368         if prev == '\x1b' && b == '\\' {
 369             break
 370         }
 371         prev = b
 372     }
 373 
 374     return s
 375 }
 376 
 377 // isNumeric checks if a string is valid/useable as a number
 378 func isNumeric(s string, sb *strings.Builder) bool {
 379     if len(s) == 0 {
 380         return false
 381     }
 382 
 383     sb.Reset()
 384 
 385     s = skipLeadingEscapeSequences(s)
 386     if len(s) > 0 && (s[0] == '+' || s[0] == '-') {
 387         sb.WriteByte(s[0])
 388         s = s[1:]
 389     }
 390 
 391     s = skipLeadingEscapeSequences(s)
 392     if len(s) == 0 {
 393         return false
 394     }
 395     if b := s[0]; b == '.' {
 396         sb.WriteByte(b)
 397         return isDigits(s[1:], sb)
 398     }
 399 
 400     digits := 0
 401 
 402     for {
 403         s = skipLeadingEscapeSequences(s)
 404         if len(s) == 0 {
 405             break
 406         }
 407 
 408         b := s[0]
 409         sb.WriteByte(b)
 410 
 411         if b == '.' {
 412             return isDigits(s[1:], sb)
 413         }
 414 
 415         if !('0' <= b && b <= '9') {
 416             return false
 417         }
 418 
 419         digits++
 420         s = s[1:]
 421     }
 422 
 423     s = skipLeadingEscapeSequences(s)
 424     return len(s) == 0 && digits > 0
 425 }
 426 
 427 func isDigits(s string, sb *strings.Builder) bool {
 428     if len(s) == 0 {
 429         return false
 430     }
 431 
 432     digits := 0
 433 
 434     for {
 435         s = skipLeadingEscapeSequences(s)
 436         if len(s) == 0 {
 437             break
 438         }
 439 
 440         if b := s[0]; '0' <= b && b <= '9' {
 441             sb.WriteByte(b)
 442             s = s[1:]
 443             digits++
 444         } else {
 445             return false
 446         }
 447     }
 448 
 449     s = skipLeadingEscapeSequences(s)
 450     return len(s) == 0 && digits > 0
 451 }
 452 
 453 // countDecimals counts decimal digits from the string given, assuming it
 454 // represents a valid/useable float64, when parsed
 455 func countDecimals(s string) int {
 456     dot := strings.IndexByte(s, '.')
 457     if dot < 0 {
 458         return 0
 459     }
 460 
 461     decs := 0
 462     s = s[dot+1:]
 463 
 464     for len(s) > 0 {
 465         s = skipLeadingEscapeSequences(s)
 466         if len(s) == 0 {
 467             break
 468         }
 469         if '0' <= s[0] && s[0] <= '9' {
 470             decs++
 471         }
 472         s = s[1:]
 473     }
 474 
 475     return decs
 476 }
 477 
 478 // countDotDecimals is like func countDecimals, but this one also includes
 479 // the dot, when any decimals are present, else the count stays at 0
 480 func countDotDecimals(s string) int {
 481     decs := countDecimals(s)
 482     if decs > 0 {
 483         return decs + 1
 484     }
 485     return decs
 486 }
 487 
 488 func countWidth(s string) int {
 489     width := 0
 490 
 491     for len(s) > 0 {
 492         i, j := indexEscapeSequence(s)
 493         if i < 0 {
 494             break
 495         }
 496         if j < 0 {
 497             j = len(s)
 498         }
 499 
 500         width += utf8.RuneCountInString(s[:i])
 501         s = s[j:]
 502     }
 503 
 504     // count trailing/all runes in strings which don't end with ANSI-sequences
 505     width += utf8.RuneCountInString(s)
 506     return width
 507 }
 508 
 509 // indexEscapeSequence finds the first ANSI-style escape-sequence, which is
 510 // the multi-byte sequences starting with ESC[; the result is a pair of slice
 511 // indices which can be independently negative when either the start/end of
 512 // a sequence isn't found; given their fairly-common use, even the hyperlink
 513 // ESC]8 sequences are supported
 514 func indexEscapeSequence(s string) (int, int) {
 515     var prev byte
 516 
 517     for i := range s {
 518         b := s[i]
 519 
 520         if prev == '\x1b' && b == '[' {
 521             j := indexLetter(s[i+1:])
 522             if j < 0 {
 523                 return i, -1
 524             }
 525             return i - 1, i + 1 + j + 1
 526         }
 527 
 528         if prev == '\x1b' && b == ']' && i+1 < len(s) && s[i+1] == '8' {
 529             j := indexPair(s[i+1:], '\x1b', '\\')
 530             if j < 0 {
 531                 return i, -1
 532             }
 533             return i - 1, i + 1 + j + 2
 534         }
 535 
 536         prev = b
 537     }
 538 
 539     return -1, -1
 540 }
 541 
 542 func indexLetter(s string) int {
 543     for i, b := range s {
 544         upper := b &^ 32
 545         if 'A' <= upper && upper <= 'Z' {
 546             return i
 547         }
 548     }
 549 
 550     return -1
 551 }
 552 
 553 func indexPair(s string, x byte, y byte) int {
 554     var prev byte
 555 
 556     for i := range s {
 557         b := s[i]
 558         if prev == x && b == y && i > 0 {
 559             return i
 560         }
 561         prev = b
 562     }
 563 
 564     return -1
 565 }
 566 
 567 func realign(w *bufio.Writer, t *table) {
 568     // make sums row first, as final alignments are usually affected by these
 569     var sums []string
 570     if t.ShowSums {
 571         sums = make([]string, 0, t.Columns)
 572 
 573         for i := 0; i < t.Columns; i++ {
 574             if t.Numeric[i] == 0 {
 575                 sums = append(sums, `-`)
 576                 if t.MaxWidth[i] < 1 {
 577                     t.MaxWidth[i] = 1
 578                 }
 579                 continue
 580             }
 581 
 582             decs := t.MaxDotDecimals[i]
 583             if decs > 0 {
 584                 decs--
 585             }
 586 
 587             var buf [64]byte
 588             s := strconv.AppendFloat(buf[:0], t.Sums[i], 'f', decs, 64)
 589             sums = append(sums, string(s))
 590             if t.MaxWidth[i] < len(s) {
 591                 t.MaxWidth[i] = len(s)
 592             }
 593         }
 594     }
 595 
 596     // due keeps track of how many spaces are due, when separating realigned
 597     // items from their immediate predecessor on the same row; this counter
 598     // is also used to right-pad numbers with decimals, as such items can be
 599     // padded with spaces from either side
 600     due := 0
 601 
 602     showItem := func(i int, s string, t *table) {
 603         if i > 0 {
 604             due += columnGap
 605         }
 606 
 607         if isNumeric(s, &(t.sb)) {
 608             dd := countDotDecimals(s)
 609             rpad := t.MaxDotDecimals[i] - dd
 610             width := countWidth(s)
 611             lpad := t.MaxWidth[i] - (width + rpad) + due
 612             writeSpaces(w, lpad)
 613             f, _ := strconv.ParseFloat(t.sb.String(), 64)
 614             writeNumericItem(w, s, numericStyle(f))
 615             due = rpad
 616             return
 617         }
 618 
 619         writeSpaces(w, due)
 620         w.WriteString(s)
 621         due = t.MaxWidth[i] - countWidth(s)
 622     }
 623 
 624     writeTile := func(i int, s string, t *table) {
 625         // make empty items stand out
 626         if len(s) == 0 {
 627             w.WriteString("\x1b[0m○")
 628             return
 629         }
 630 
 631         if isNumeric(s, &(t.sb)) {
 632             f, _ := strconv.ParseFloat(t.sb.String(), 64)
 633             w.WriteString(numericStyle(f))
 634             w.WriteString("■")
 635             return
 636         }
 637 
 638         // make padded items stand out: these items have spaces at either end
 639         if s[0] == ' ' || s[len(s)-1] == ' ' {
 640             w.WriteString("\x1b[38;2;196;160;0m■")
 641             return
 642         }
 643 
 644         w.WriteString("\x1b[38;2;128;128;128m■")
 645     }
 646 
 647     // show realigned rows
 648 
 649     for _, line := range t.Rows {
 650         due = 0
 651 
 652         if t.ShowTiles {
 653             end := t.LoopItems(line, t.Columns, t, writeTile)
 654             if end < len(t.MaxWidth)-1 {
 655                 w.WriteString("\x1b[0m")
 656             }
 657             // make rows with missing trailing items stand out
 658             for i := end; i < len(t.MaxWidth); i++ {
 659                 w.WriteString("×")
 660             }
 661             w.WriteString("\x1b[0m")
 662             due += columnGap
 663         }
 664 
 665         t.LoopItems(line, t.Columns, t, showItem)
 666         if w.WriteByte('\n') != nil {
 667             return
 668         }
 669     }
 670 
 671     if t.Columns > 0 && t.ShowSums {
 672         realignSums(w, t, sums)
 673     }
 674 }
 675 
 676 func realignSums(w *bufio.Writer, t *table, sums []string) {
 677     due := 0
 678     if t.ShowTiles {
 679         due += t.Columns + columnGap
 680     }
 681 
 682     for i, s := range sums {
 683         if i > 0 {
 684             due += columnGap
 685         }
 686 
 687         if t.Numeric[i] == 0 {
 688             writeSpaces(w, due)
 689             w.WriteString(s)
 690             due = t.MaxWidth[i] - countWidth(s)
 691             continue
 692         }
 693 
 694         lpad := t.MaxWidth[i] - len(s) + due
 695         writeSpaces(w, lpad)
 696         writeNumericItem(w, s, numericStyle(t.Sums[i]))
 697         due = 0
 698     }
 699 
 700     w.WriteByte('\n')
 701 }
 702 
 703 // writeSpaces does what it says, minimizing calls to write-like funcs
 704 func writeSpaces(w *bufio.Writer, n int) {
 705     const spaces = `                                `
 706     if n < 1 {
 707         return
 708     }
 709 
 710     for n >= len(spaces) {
 711         w.WriteString(spaces)
 712         n -= len(spaces)
 713     }
 714     w.WriteString(spaces[:n])
 715 }
 716 
 717 func writeRowTiles(w *bufio.Writer, s string, t *table, writeTile itemFunc) {
 718     end := t.LoopItems(s, t.Columns, t, writeTile)
 719 
 720     if end < len(t.MaxWidth)-1 {
 721         w.WriteString("\x1b[0m")
 722     }
 723     for i := end + 1; i < len(t.MaxWidth); i++ {
 724         w.WriteString("×")
 725     }
 726     w.WriteString("\x1b[0m")
 727 }
 728 
 729 func numericStyle(f float64) string {
 730     if f > 0 {
 731         if float64(int64(f)) == f {
 732             return "\x1b[38;2;0;135;0m"
 733         }
 734         return "\x1b[38;2;0;155;95m"
 735     }
 736     if f < 0 {
 737         if float64(int64(f)) == f {
 738             return "\x1b[38;2;204;0;0m"
 739         }
 740         return "\x1b[38;2;215;95;95m"
 741     }
 742     if f == 0 {
 743         return "\x1b[38;2;0;95;215m"
 744     }
 745     return "\x1b[38;2;128;128;128m"
 746 }
 747 
 748 func writeNumericItem(w *bufio.Writer, s string, startStyle string) {
 749     w.WriteString(startStyle)
 750     if len(s) > 0 && (s[0] == '-' || s[0] == '+') {
 751         w.WriteByte(s[0])
 752         s = s[1:]
 753     }
 754 
 755     dot := strings.IndexByte(s, '.')
 756     if dot < 0 {
 757         restyleDigits(w, s, altDigitStyle)
 758         w.WriteString("\x1b[0m")
 759         return
 760     }
 761 
 762     if len(s[:dot]) > 3 {
 763         restyleDigits(w, s[:dot], altDigitStyle)
 764         w.WriteString("\x1b[0m")
 765         w.WriteString(startStyle)
 766         w.WriteByte('.')
 767     } else {
 768         w.WriteString(s[:dot])
 769         w.WriteByte('.')
 770     }
 771 
 772     rest := s[dot+1:]
 773     restyleDigits(w, rest, altDigitStyle)
 774     if len(rest) < 4 {
 775         w.WriteString("\x1b[0m")
 776     }
 777 }
 778 
 779 // restyleDigits renders a run of digits as alternating styled/unstyled runs
 780 // of 3 digits, which greatly improves readability, and is the only purpose
 781 // of this app; string is assumed to be all decimal digits
 782 func restyleDigits(w *bufio.Writer, digits string, altStyle string) {
 783     if len(digits) < 4 {
 784         // digit sequence is short, so emit it as is
 785         w.WriteString(digits)
 786         return
 787     }
 788 
 789     // separate leading 0..2 digits which don't align with the 3-digit groups
 790     i := len(digits) % 3
 791     // emit leading digits unstyled, if there are any
 792     w.WriteString(digits[:i])
 793     // the rest is guaranteed to have a length which is a multiple of 3
 794     digits = digits[i:]
 795 
 796     // start by styling, unless there were no leading digits
 797     style := i != 0
 798 
 799     for len(digits) > 0 {
 800         if style {
 801             w.WriteString(altStyle)
 802             w.WriteString(digits[:3])
 803             w.WriteString("\x1b[0m")
 804         } else {
 805             w.WriteString(digits[:3])
 806         }
 807 
 808         // advance to the next triple: the start of this func is supposed
 809         // to guarantee this step always works
 810         digits = digits[3:]
 811 
 812         // alternate between styled and unstyled 3-digit groups
 813         style = !style
 814     }
 815 }
     File: ./ncol/ncol_test.go
   1 /*
   2 The MIT License (MIT)
   3 
   4 Copyright (c) 2026 pacman64
   5 
   6 Permission is hereby granted, free of charge, to any person obtaining a copy of
   7 this software and associated documentation files (the "Software"), to deal
   8 in the Software without restriction, including without limitation the rights to
   9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
  10 of the Software, and to permit persons to whom the Software is furnished to do
  11 so, subject to the following conditions:
  12 
  13 The above copyright notice and this permission notice shall be included in all
  14 copies or substantial portions of the Software.
  15 
  16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  22 SOFTWARE.
  23 */
  24 
  25 package ncol
  26 
  27 import "testing"
  28 
  29 func TestCountWidth(t *testing.T) {
  30     var tests = []struct {
  31         name     string
  32         input    string
  33         expected int
  34     }{
  35         {`empty`, ``, 0},
  36         {`empty ANSI`, "\x1b[38;5;0;0;0m\x1b[0m", 0},
  37         {`simple plain`, `abc def`, 7},
  38         {`unicode plain`, `abc●def`, 7},
  39         {`simple ANSI`, "abc \x1b[7mde\x1b[0mf", 7},
  40         {`unicode ANSI`, "abc●\x1b[7mde\x1b[0mf", 7},
  41     }
  42 
  43     for _, tc := range tests {
  44         t.Run(tc.name, func(t *testing.T) {
  45             got := countWidth(tc.input)
  46             if got != tc.expected {
  47                 t.Errorf("expected width %d, got %d instead", tc.expected, got)
  48             }
  49         })
  50     }
  51 }
     File: ./ngron/ngron.go
   1 /*
   2 The MIT License (MIT)
   3 
   4 Copyright (c) 2026 pacman64
   5 
   6 Permission is hereby granted, free of charge, to any person obtaining a copy of
   7 this software and associated documentation files (the "Software"), to deal
   8 in the Software without restriction, including without limitation the rights to
   9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
  10 of the Software, and to permit persons to whom the Software is furnished to do
  11 so, subject to the following conditions:
  12 
  13 The above copyright notice and this permission notice shall be included in all
  14 copies or substantial portions of the Software.
  15 
  16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  22 SOFTWARE.
  23 */
  24 
  25 package ngron
  26 
  27 import (
  28     "bufio"
  29     "encoding/json"
  30     "errors"
  31     "io"
  32     "os"
  33     "strconv"
  34 )
  35 
  36 const info = `
  37 ngron [options...] [filepath/URI...]
  38 
  39 
  40 Nice GRON converts JSON data into 'grep'-friendly lines, similar to what
  41 tool 'gron' (GRep jsON; https://github.com/tomnomnom/gron) does.
  42 
  43 This tool uses nicer ANSI styles than the original, hence its name, but
  44 can't convert its output back into JSON, unlike the latter.
  45 
  46 Unlike the original 'gron', there's no sort-mode. When not given a named
  47 source (filepath/URI) to read from, data are read from standard input.
  48 
  49 Options, where leading double-dashes are also allowed:
  50 
  51     -h         show this help message
  52     -help      show this help message
  53 
  54     -m         monochrome (default), enables unstyled output-mode
  55     -c         color, enables ANSI-styled output-mode
  56     -color     enables ANSI-styled output-mode
  57 `
  58 
  59 type emitConfig struct {
  60     path    func(w *bufio.Writer, path []any) error
  61     null    func(w *bufio.Writer) error
  62     boolean func(w *bufio.Writer, b bool) error
  63     number  func(w *bufio.Writer, n json.Number) error
  64     key     func(w *bufio.Writer, k string) error
  65     text    func(w *bufio.Writer, s string) error
  66 
  67     arrayDecl  string
  68     objectDecl string
  69 }
  70 
  71 var monochrome = emitConfig{
  72     path:    monoPath,
  73     null:    monoNull,
  74     boolean: monoBool,
  75     number:  monoNumber,
  76     key:     monoString,
  77     text:    monoString,
  78 
  79     arrayDecl:  `[]`,
  80     objectDecl: `{}`,
  81 }
  82 
  83 var styled = emitConfig{
  84     path:    styledPath,
  85     null:    styledNull,
  86     boolean: styledBool,
  87     number:  styledNumber,
  88     key:     styledString,
  89     text:    styledString,
  90 
  91     arrayDecl:  "\x1b[38;2;168;168;168m[]\x1b[0m",
  92     objectDecl: "\x1b[38;2;168;168;168m{}\x1b[0m",
  93 }
  94 
  95 var config = monochrome
  96 
  97 func Main() {
  98     args := os.Args[1:]
  99 
 100     for len(args) > 0 {
 101         switch args[0] {
 102         case `-c`, `--c`, `-color`, `--color`:
 103             config = styled
 104             args = args[1:]
 105             continue
 106 
 107         case `-h`, `--h`, `-help`, `--help`:
 108             os.Stdout.WriteString(info[1:])
 109             return
 110 
 111         case `-m`, `--m`:
 112             config = monochrome
 113             args = args[1:]
 114             continue
 115         }
 116 
 117         break
 118     }
 119 
 120     if len(args) > 0 && args[0] == `--` {
 121         args = args[1:]
 122     }
 123 
 124     if len(args) > 2 {
 125         os.Stderr.WriteString("multiple inputs not allowed\n")
 126         os.Exit(1)
 127     }
 128 
 129     // figure out whether input should come from a named file or from stdin
 130     path := `-`
 131     if len(args) > 0 {
 132         path = args[0]
 133     }
 134 
 135     err := handleInput(os.Stdout, path)
 136     if err != nil && err != io.EOF {
 137         os.Stderr.WriteString(err.Error())
 138         os.Stderr.WriteString("\n")
 139         os.Exit(1)
 140     }
 141 }
 142 
 143 type handlerFunc func(*bufio.Writer, *json.Decoder, json.Token, []any) error
 144 
 145 // handleInput simplifies control-flow for func main
 146 func handleInput(w io.Writer, path string) error {
 147     if path == `-` {
 148         bw := bufio.NewWriter(w)
 149         defer bw.Flush()
 150         return run(bw, os.Stdin)
 151     }
 152 
 153     f, err := os.Open(path)
 154     if err != nil {
 155         // on windows, file-not-found error messages may mention `CreateFile`,
 156         // even when trying to open files in read-only mode
 157         return errors.New(`can't open file named ` + path)
 158     }
 159     defer f.Close()
 160 
 161     bw := bufio.NewWriter(w)
 162     defer bw.Flush()
 163     return run(bw, f)
 164 }
 165 
 166 // escapedStringBytes helps func handleString treat all string bytes quickly
 167 // and correctly, using their officially-supported JSON escape sequences
 168 //
 169 // https://www.rfc-editor.org/rfc/rfc8259#section-7
 170 var escapedStringBytes = [256][]byte{
 171     {'\\', 'u', '0', '0', '0', '0'}, {'\\', 'u', '0', '0', '0', '1'},
 172     {'\\', 'u', '0', '0', '0', '2'}, {'\\', 'u', '0', '0', '0', '3'},
 173     {'\\', 'u', '0', '0', '0', '4'}, {'\\', 'u', '0', '0', '0', '5'},
 174     {'\\', 'u', '0', '0', '0', '6'}, {'\\', 'u', '0', '0', '0', '7'},
 175     {'\\', 'b'}, {'\\', 't'},
 176     {'\\', 'n'}, {'\\', 'u', '0', '0', '0', 'b'},
 177     {'\\', 'f'}, {'\\', 'r'},
 178     {'\\', 'u', '0', '0', '0', 'e'}, {'\\', 'u', '0', '0', '0', 'f'},
 179     {'\\', 'u', '0', '0', '1', '0'}, {'\\', 'u', '0', '0', '1', '1'},
 180     {'\\', 'u', '0', '0', '1', '2'}, {'\\', 'u', '0', '0', '1', '3'},
 181     {'\\', 'u', '0', '0', '1', '4'}, {'\\', 'u', '0', '0', '1', '5'},
 182     {'\\', 'u', '0', '0', '1', '6'}, {'\\', 'u', '0', '0', '1', '7'},
 183     {'\\', 'u', '0', '0', '1', '8'}, {'\\', 'u', '0', '0', '1', '9'},
 184     {'\\', 'u', '0', '0', '1', 'a'}, {'\\', 'u', '0', '0', '1', 'b'},
 185     {'\\', 'u', '0', '0', '1', 'c'}, {'\\', 'u', '0', '0', '1', 'd'},
 186     {'\\', 'u', '0', '0', '1', 'e'}, {'\\', 'u', '0', '0', '1', 'f'},
 187     {32}, {33}, {'\\', '"'}, {35}, {36}, {37}, {38}, {39},
 188     {40}, {41}, {42}, {43}, {44}, {45}, {46}, {47},
 189     {48}, {49}, {50}, {51}, {52}, {53}, {54}, {55},
 190     {56}, {57}, {58}, {59}, {60}, {61}, {62}, {63},
 191     {64}, {65}, {66}, {67}, {68}, {69}, {70}, {71},
 192     {72}, {73}, {74}, {75}, {76}, {77}, {78}, {79},
 193     {80}, {81}, {82}, {83}, {84}, {85}, {86}, {87},
 194     {88}, {89}, {90}, {91}, {'\\', '\\'}, {93}, {94}, {95},
 195     {96}, {97}, {98}, {99}, {100}, {101}, {102}, {103},
 196     {104}, {105}, {106}, {107}, {108}, {109}, {110}, {111},
 197     {112}, {113}, {114}, {115}, {116}, {117}, {118}, {119},
 198     {120}, {121}, {122}, {123}, {124}, {125}, {126}, {127},
 199     {128}, {129}, {130}, {131}, {132}, {133}, {134}, {135},
 200     {136}, {137}, {138}, {139}, {140}, {141}, {142}, {143},
 201     {144}, {145}, {146}, {147}, {148}, {149}, {150}, {151},
 202     {152}, {153}, {154}, {155}, {156}, {157}, {158}, {159},
 203     {160}, {161}, {162}, {163}, {164}, {165}, {166}, {167},
 204     {168}, {169}, {170}, {171}, {172}, {173}, {174}, {175},
 205     {176}, {177}, {178}, {179}, {180}, {181}, {182}, {183},
 206     {184}, {185}, {186}, {187}, {188}, {189}, {190}, {191},
 207     {192}, {193}, {194}, {195}, {196}, {197}, {198}, {199},
 208     {200}, {201}, {202}, {203}, {204}, {205}, {206}, {207},
 209     {208}, {209}, {210}, {211}, {212}, {213}, {214}, {215},
 210     {216}, {217}, {218}, {219}, {220}, {221}, {222}, {223},
 211     {224}, {225}, {226}, {227}, {228}, {229}, {230}, {231},
 212     {232}, {233}, {234}, {235}, {236}, {237}, {238}, {239},
 213     {240}, {241}, {242}, {243}, {244}, {245}, {246}, {247},
 214     {248}, {249}, {250}, {251}, {252}, {253}, {254}, {255},
 215 }
 216 
 217 // run does it all, given a reader and a writer
 218 func run(w *bufio.Writer, r io.Reader) error {
 219     dec := json.NewDecoder(r)
 220     // avoid parsing numbers, so unusually-long numbers are kept verbatim,
 221     // even if JSON parsers aren't required to guarantee such input-fidelity
 222     // for numbers
 223     dec.UseNumber()
 224 
 225     t, err := dec.Token()
 226     if err == io.EOF {
 227         return errors.New(`input has no JSON values`)
 228     }
 229 
 230     if err = handleToken(w, dec, t, make([]any, 0, 50)); err != nil {
 231         return err
 232     }
 233 
 234     _, err = dec.Token()
 235     if err == io.EOF {
 236         // input is over, so it's a success
 237         return nil
 238     }
 239 
 240     if err == nil {
 241         // a successful `read` is a failure, as it means there are
 242         // trailing JSON tokens
 243         return errors.New(`unexpected trailing data`)
 244     }
 245 
 246     // any other error, perhaps some invalid-JSON-syntax-type error
 247     return err
 248 }
 249 
 250 // handleToken handles recursion for func run
 251 func handleToken(w *bufio.Writer, dec *json.Decoder, t json.Token, path []any) error {
 252     switch t := t.(type) {
 253     case json.Delim:
 254         switch t {
 255         case json.Delim('['):
 256             return handleArray(w, dec, path)
 257         case json.Delim('{'):
 258             return handleObject(w, dec, path)
 259         default:
 260             return errors.New(`unsupported JSON syntax ` + string(t))
 261         }
 262 
 263     case nil:
 264         config.path(w, path)
 265         config.null(w)
 266         return endLine(w)
 267 
 268     case bool:
 269         config.path(w, path)
 270         config.boolean(w, t)
 271         return endLine(w)
 272 
 273     case json.Number:
 274         config.path(w, path)
 275         config.number(w, t)
 276         return endLine(w)
 277 
 278     case string:
 279         config.path(w, path)
 280         config.text(w, t)
 281         return endLine(w)
 282 
 283     default:
 284         // return fmt.Errorf(`unsupported token type %T`, t)
 285         return errors.New(`invalid JSON token`)
 286     }
 287 }
 288 
 289 // handleArray handles arrays for func handleToken
 290 func handleArray(w *bufio.Writer, dec *json.Decoder, path []any) error {
 291     config.path(w, path)
 292     w.WriteString(config.arrayDecl)
 293     if err := endLine(w); err != nil {
 294         return err
 295     }
 296 
 297     path = append(path, 0)
 298     last := len(path) - 1
 299 
 300     for i := 0; true; i++ {
 301         path[last] = i
 302 
 303         t, err := dec.Token()
 304         if err != nil {
 305             return err
 306         }
 307 
 308         if t == json.Delim(']') {
 309             return nil
 310         }
 311 
 312         err = handleToken(w, dec, t, path)
 313         if err != nil {
 314             return err
 315         }
 316     }
 317 
 318     // make the compiler happy
 319     return nil
 320 }
 321 
 322 // handleObject handles objects for func handleToken
 323 func handleObject(w *bufio.Writer, dec *json.Decoder, path []any) error {
 324     config.path(w, path)
 325     w.WriteString(config.objectDecl)
 326     if err := endLine(w); err != nil {
 327         return err
 328     }
 329 
 330     path = append(path, ``)
 331     last := len(path) - 1
 332 
 333     for i := 0; true; i++ {
 334         t, err := dec.Token()
 335         if err != nil {
 336             return err
 337         }
 338 
 339         if t == json.Delim('}') {
 340             return nil
 341         }
 342 
 343         k, ok := t.(string)
 344         if !ok {
 345             return errors.New(`expected a string for a key-value pair`)
 346         }
 347 
 348         path[last] = k
 349         if err != nil {
 350             return err
 351         }
 352 
 353         t, err = dec.Token()
 354         if err == io.EOF {
 355             return errors.New(`expected a value for a key-value pair`)
 356         }
 357 
 358         err = handleToken(w, dec, t, path)
 359         if err != nil {
 360             return err
 361         }
 362     }
 363 
 364     // make the compiler happy
 365     return nil
 366 }
 367 
 368 func monoPath(w *bufio.Writer, path []any) error {
 369     var buf [24]byte
 370 
 371     w.WriteString(`json`)
 372 
 373     for _, v := range path {
 374         switch v := v.(type) {
 375         case int:
 376             w.WriteByte('[')
 377             w.Write(strconv.AppendInt(buf[:0], int64(v), 10))
 378             w.WriteByte(']')
 379 
 380         case string:
 381             if !needsEscaping(v) {
 382                 w.WriteByte('.')
 383                 w.WriteString(v)
 384                 continue
 385             }
 386             w.WriteByte('[')
 387             monoString(w, v)
 388             w.WriteByte(']')
 389         }
 390     }
 391 
 392     w.WriteString(` = `)
 393     return nil
 394 }
 395 
 396 func monoNull(w *bufio.Writer) error {
 397     w.WriteString(`null`)
 398     return nil
 399 }
 400 
 401 func monoBool(w *bufio.Writer, b bool) error {
 402     if b {
 403         w.WriteString(`true`)
 404     } else {
 405         w.WriteString(`false`)
 406     }
 407     return nil
 408 }
 409 
 410 func monoNumber(w *bufio.Writer, n json.Number) error {
 411     w.WriteString(n.String())
 412     return nil
 413 }
 414 
 415 func monoString(w *bufio.Writer, s string) error {
 416     w.WriteByte('"')
 417     for i := range s {
 418         w.Write(escapedStringBytes[s[i]])
 419     }
 420     w.WriteByte('"')
 421     return nil
 422 }
 423 
 424 func styledPath(w *bufio.Writer, path []any) error {
 425     var buf [24]byte
 426 
 427     w.WriteString("\x1b[38;2;135;95;255mjson\x1b[0m")
 428 
 429     for _, v := range path {
 430         switch v := v.(type) {
 431         case int:
 432             w.WriteString("\x1b[38;2;168;168;168m[")
 433             w.WriteString("\x1b[38;2;0;135;95m")
 434             w.Write(strconv.AppendInt(buf[:0], int64(v), 10))
 435             w.WriteString("\x1b[38;2;168;168;168m]\x1b[0m")
 436 
 437         case string:
 438             if !needsEscaping(v) {
 439                 w.WriteString("\x1b[38;2;168;168;168m.")
 440                 w.WriteString("\x1b[38;2;135;95;255m")
 441                 w.WriteString(v)
 442                 w.WriteString("\x1b[0m")
 443                 continue
 444             }
 445 
 446             w.WriteString("\x1b[38;2;168;168;168m[")
 447             styledString(w, v)
 448             w.WriteString("\x1b[38;2;168;168;168m]\x1b[0m")
 449         }
 450     }
 451 
 452     w.WriteString(" \x1b[38;2;168;168;168m=\x1b[0m ")
 453     return nil
 454 }
 455 
 456 func styledNull(w *bufio.Writer) error {
 457     w.WriteString("\x1b[38;2;168;168;168m")
 458     w.WriteString(`null`)
 459     w.WriteString("\x1b[0m")
 460     return nil
 461 }
 462 
 463 func styledBool(w *bufio.Writer, b bool) error {
 464     if b {
 465         w.WriteString("\x1b[38;2;95;175;215mtrue\x1b[0m")
 466     } else {
 467         w.WriteString("\x1b[38;2;95;175;215mfalse\x1b[0m")
 468     }
 469     return nil
 470 }
 471 
 472 func styledNumber(w *bufio.Writer, n json.Number) error {
 473     w.WriteString("\x1b[38;2;0;135;95m")
 474     w.WriteString(n.String())
 475     w.WriteString("\x1b[0m")
 476     return nil
 477 }
 478 
 479 func styledString(w *bufio.Writer, s string) error {
 480     w.WriteString("\x1b[38;2;168;168;168m\"\x1b[0m")
 481     for i := range s {
 482         w.Write(escapedStringBytes[s[i]])
 483     }
 484     w.WriteString("\x1b[38;2;168;168;168m\"\x1b[0m")
 485     return nil
 486 }
 487 
 488 func needsEscaping(s string) bool {
 489     for _, r := range s {
 490         if r < ' ' || r > '~' {
 491             return true
 492         }
 493 
 494         switch r {
 495         case '"', '\'', '\\':
 496             return true
 497         }
 498     }
 499 
 500     return false
 501 }
 502 
 503 func endLine(w *bufio.Writer) error {
 504     w.WriteByte(';')
 505     if err := w.WriteByte('\n'); err == nil {
 506         return nil
 507     }
 508     return io.EOF
 509 }
     File: ./nhex/ansi.go
   1 /*
   2 The MIT License (MIT)
   3 
   4 Copyright (c) 2026 pacman64
   5 
   6 Permission is hereby granted, free of charge, to any person obtaining a copy of
   7 this software and associated documentation files (the "Software"), to deal
   8 in the Software without restriction, including without limitation the rights to
   9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
  10 of the Software, and to permit persons to whom the Software is furnished to do
  11 so, subject to the following conditions:
  12 
  13 The above copyright notice and this permission notice shall be included in all
  14 copies or substantial portions of the Software.
  15 
  16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  22 SOFTWARE.
  23 */
  24 
  25 package nhex
  26 
  27 import (
  28     "bufio"
  29     "fmt"
  30     "strconv"
  31 )
  32 
  33 // styledHexBytes is a super-fast direct byte-to-result lookup table, and was
  34 // autogenerated by running the command
  35 //
  36 // seq 0 255 | ./hex-styles.awk
  37 var styledHexBytes = [256]string{
  38     "\x1b[38;5;111m00 ", "\x1b[38;5;246m01 ",
  39     "\x1b[38;5;246m02 ", "\x1b[38;5;246m03 ",
  40     "\x1b[38;5;246m04 ", "\x1b[38;5;246m05 ",
  41     "\x1b[38;5;246m06 ", "\x1b[38;5;246m07 ",
  42     "\x1b[38;5;246m08 ", "\x1b[38;5;246m09 ",
  43     "\x1b[38;5;246m0a ", "\x1b[38;5;246m0b ",
  44     "\x1b[38;5;246m0c ", "\x1b[38;5;246m0d ",
  45     "\x1b[38;5;246m0e ", "\x1b[38;5;246m0f ",
  46     "\x1b[38;5;246m10 ", "\x1b[38;5;246m11 ",
  47     "\x1b[38;5;246m12 ", "\x1b[38;5;246m13 ",
  48     "\x1b[38;5;246m14 ", "\x1b[38;5;246m15 ",
  49     "\x1b[38;5;246m16 ", "\x1b[38;5;246m17 ",
  50     "\x1b[38;5;246m18 ", "\x1b[38;5;246m19 ",
  51     "\x1b[38;5;246m1a ", "\x1b[38;5;246m1b ",
  52     "\x1b[38;5;246m1c ", "\x1b[38;5;246m1d ",
  53     "\x1b[38;5;246m1e ", "\x1b[38;5;246m1f ",
  54     "\x1b[38;5;72m20\x1b[38;5;239m ", "\x1b[38;5;72m21\x1b[38;5;239m!",
  55     "\x1b[38;5;72m22\x1b[38;5;239m\"", "\x1b[38;5;72m23\x1b[38;5;239m#",
  56     "\x1b[38;5;72m24\x1b[38;5;239m$", "\x1b[38;5;72m25\x1b[38;5;239m%",
  57     "\x1b[38;5;72m26\x1b[38;5;239m&", "\x1b[38;5;72m27\x1b[38;5;239m'",
  58     "\x1b[38;5;72m28\x1b[38;5;239m(", "\x1b[38;5;72m29\x1b[38;5;239m)",
  59     "\x1b[38;5;72m2a\x1b[38;5;239m*", "\x1b[38;5;72m2b\x1b[38;5;239m+",
  60     "\x1b[38;5;72m2c\x1b[38;5;239m,", "\x1b[38;5;72m2d\x1b[38;5;239m-",
  61     "\x1b[38;5;72m2e\x1b[38;5;239m.", "\x1b[38;5;72m2f\x1b[38;5;239m/",
  62     "\x1b[38;5;72m30\x1b[38;5;239m0", "\x1b[38;5;72m31\x1b[38;5;239m1",
  63     "\x1b[38;5;72m32\x1b[38;5;239m2", "\x1b[38;5;72m33\x1b[38;5;239m3",
  64     "\x1b[38;5;72m34\x1b[38;5;239m4", "\x1b[38;5;72m35\x1b[38;5;239m5",
  65     "\x1b[38;5;72m36\x1b[38;5;239m6", "\x1b[38;5;72m37\x1b[38;5;239m7",
  66     "\x1b[38;5;72m38\x1b[38;5;239m8", "\x1b[38;5;72m39\x1b[38;5;239m9",
  67     "\x1b[38;5;72m3a\x1b[38;5;239m:", "\x1b[38;5;72m3b\x1b[38;5;239m;",
  68     "\x1b[38;5;72m3c\x1b[38;5;239m<", "\x1b[38;5;72m3d\x1b[38;5;239m=",
  69     "\x1b[38;5;72m3e\x1b[38;5;239m>", "\x1b[38;5;72m3f\x1b[38;5;239m?",
  70     "\x1b[38;5;72m40\x1b[38;5;239m@", "\x1b[38;5;72m41\x1b[38;5;239mA",
  71     "\x1b[38;5;72m42\x1b[38;5;239mB", "\x1b[38;5;72m43\x1b[38;5;239mC",
  72     "\x1b[38;5;72m44\x1b[38;5;239mD", "\x1b[38;5;72m45\x1b[38;5;239mE",
  73     "\x1b[38;5;72m46\x1b[38;5;239mF", "\x1b[38;5;72m47\x1b[38;5;239mG",
  74     "\x1b[38;5;72m48\x1b[38;5;239mH", "\x1b[38;5;72m49\x1b[38;5;239mI",
  75     "\x1b[38;5;72m4a\x1b[38;5;239mJ", "\x1b[38;5;72m4b\x1b[38;5;239mK",
  76     "\x1b[38;5;72m4c\x1b[38;5;239mL", "\x1b[38;5;72m4d\x1b[38;5;239mM",
  77     "\x1b[38;5;72m4e\x1b[38;5;239mN", "\x1b[38;5;72m4f\x1b[38;5;239mO",
  78     "\x1b[38;5;72m50\x1b[38;5;239mP", "\x1b[38;5;72m51\x1b[38;5;239mQ",
  79     "\x1b[38;5;72m52\x1b[38;5;239mR", "\x1b[38;5;72m53\x1b[38;5;239mS",
  80     "\x1b[38;5;72m54\x1b[38;5;239mT", "\x1b[38;5;72m55\x1b[38;5;239mU",
  81     "\x1b[38;5;72m56\x1b[38;5;239mV", "\x1b[38;5;72m57\x1b[38;5;239mW",
  82     "\x1b[38;5;72m58\x1b[38;5;239mX", "\x1b[38;5;72m59\x1b[38;5;239mY",
  83     "\x1b[38;5;72m5a\x1b[38;5;239mZ", "\x1b[38;5;72m5b\x1b[38;5;239m[",
  84     "\x1b[38;5;72m5c\x1b[38;5;239m\\", "\x1b[38;5;72m5d\x1b[38;5;239m]",
  85     "\x1b[38;5;72m5e\x1b[38;5;239m^", "\x1b[38;5;72m5f\x1b[38;5;239m_",
  86     "\x1b[38;5;72m60\x1b[38;5;239m`", "\x1b[38;5;72m61\x1b[38;5;239ma",
  87     "\x1b[38;5;72m62\x1b[38;5;239mb", "\x1b[38;5;72m63\x1b[38;5;239mc",
  88     "\x1b[38;5;72m64\x1b[38;5;239md", "\x1b[38;5;72m65\x1b[38;5;239me",
  89     "\x1b[38;5;72m66\x1b[38;5;239mf", "\x1b[38;5;72m67\x1b[38;5;239mg",
  90     "\x1b[38;5;72m68\x1b[38;5;239mh", "\x1b[38;5;72m69\x1b[38;5;239mi",
  91     "\x1b[38;5;72m6a\x1b[38;5;239mj", "\x1b[38;5;72m6b\x1b[38;5;239mk",
  92     "\x1b[38;5;72m6c\x1b[38;5;239ml", "\x1b[38;5;72m6d\x1b[38;5;239mm",
  93     "\x1b[38;5;72m6e\x1b[38;5;239mn", "\x1b[38;5;72m6f\x1b[38;5;239mo",
  94     "\x1b[38;5;72m70\x1b[38;5;239mp", "\x1b[38;5;72m71\x1b[38;5;239mq",
  95     "\x1b[38;5;72m72\x1b[38;5;239mr", "\x1b[38;5;72m73\x1b[38;5;239ms",
  96     "\x1b[38;5;72m74\x1b[38;5;239mt", "\x1b[38;5;72m75\x1b[38;5;239mu",
  97     "\x1b[38;5;72m76\x1b[38;5;239mv", "\x1b[38;5;72m77\x1b[38;5;239mw",
  98     "\x1b[38;5;72m78\x1b[38;5;239mx", "\x1b[38;5;72m79\x1b[38;5;239my",
  99     "\x1b[38;5;72m7a\x1b[38;5;239mz", "\x1b[38;5;72m7b\x1b[38;5;239m{",
 100     "\x1b[38;5;72m7c\x1b[38;5;239m|", "\x1b[38;5;72m7d\x1b[38;5;239m}",
 101     "\x1b[38;5;72m7e\x1b[38;5;239m~", "\x1b[38;5;246m7f ",
 102     "\x1b[38;5;246m80 ", "\x1b[38;5;246m81 ",
 103     "\x1b[38;5;246m82 ", "\x1b[38;5;246m83 ",
 104     "\x1b[38;5;246m84 ", "\x1b[38;5;246m85 ",
 105     "\x1b[38;5;246m86 ", "\x1b[38;5;246m87 ",
 106     "\x1b[38;5;246m88 ", "\x1b[38;5;246m89 ",
 107     "\x1b[38;5;246m8a ", "\x1b[38;5;246m8b ",
 108     "\x1b[38;5;246m8c ", "\x1b[38;5;246m8d ",
 109     "\x1b[38;5;246m8e ", "\x1b[38;5;246m8f ",
 110     "\x1b[38;5;246m90 ", "\x1b[38;5;246m91 ",
 111     "\x1b[38;5;246m92 ", "\x1b[38;5;246m93 ",
 112     "\x1b[38;5;246m94 ", "\x1b[38;5;246m95 ",
 113     "\x1b[38;5;246m96 ", "\x1b[38;5;246m97 ",
 114     "\x1b[38;5;246m98 ", "\x1b[38;5;246m99 ",
 115     "\x1b[38;5;246m9a ", "\x1b[38;5;246m9b ",
 116     "\x1b[38;5;246m9c ", "\x1b[38;5;246m9d ",
 117     "\x1b[38;5;246m9e ", "\x1b[38;5;246m9f ",
 118     "\x1b[38;5;246ma0 ", "\x1b[38;5;246ma1 ",
 119     "\x1b[38;5;246ma2 ", "\x1b[38;5;246ma3 ",
 120     "\x1b[38;5;246ma4 ", "\x1b[38;5;246ma5 ",
 121     "\x1b[38;5;246ma6 ", "\x1b[38;5;246ma7 ",
 122     "\x1b[38;5;246ma8 ", "\x1b[38;5;246ma9 ",
 123     "\x1b[38;5;246maa ", "\x1b[38;5;246mab ",
 124     "\x1b[38;5;246mac ", "\x1b[38;5;246mad ",
 125     "\x1b[38;5;246mae ", "\x1b[38;5;246maf ",
 126     "\x1b[38;5;246mb0 ", "\x1b[38;5;246mb1 ",
 127     "\x1b[38;5;246mb2 ", "\x1b[38;5;246mb3 ",
 128     "\x1b[38;5;246mb4 ", "\x1b[38;5;246mb5 ",
 129     "\x1b[38;5;246mb6 ", "\x1b[38;5;246mb7 ",
 130     "\x1b[38;5;246mb8 ", "\x1b[38;5;246mb9 ",
 131     "\x1b[38;5;246mba ", "\x1b[38;5;246mbb ",
 132     "\x1b[38;5;246mbc ", "\x1b[38;5;246mbd ",
 133     "\x1b[38;5;246mbe ", "\x1b[38;5;246mbf ",
 134     "\x1b[38;5;246mc0 ", "\x1b[38;5;246mc1 ",
 135     "\x1b[38;5;246mc2 ", "\x1b[38;5;246mc3 ",
 136     "\x1b[38;5;246mc4 ", "\x1b[38;5;246mc5 ",
 137     "\x1b[38;5;246mc6 ", "\x1b[38;5;246mc7 ",
 138     "\x1b[38;5;246mc8 ", "\x1b[38;5;246mc9 ",
 139     "\x1b[38;5;246mca ", "\x1b[38;5;246mcb ",
 140     "\x1b[38;5;246mcc ", "\x1b[38;5;246mcd ",
 141     "\x1b[38;5;246mce ", "\x1b[38;5;246mcf ",
 142     "\x1b[38;5;246md0 ", "\x1b[38;5;246md1 ",
 143     "\x1b[38;5;246md2 ", "\x1b[38;5;246md3 ",
 144     "\x1b[38;5;246md4 ", "\x1b[38;5;246md5 ",
 145     "\x1b[38;5;246md6 ", "\x1b[38;5;246md7 ",
 146     "\x1b[38;5;246md8 ", "\x1b[38;5;246md9 ",
 147     "\x1b[38;5;246mda ", "\x1b[38;5;246mdb ",
 148     "\x1b[38;5;246mdc ", "\x1b[38;5;246mdd ",
 149     "\x1b[38;5;246mde ", "\x1b[38;5;246mdf ",
 150     "\x1b[38;5;246me0 ", "\x1b[38;5;246me1 ",
 151     "\x1b[38;5;246me2 ", "\x1b[38;5;246me3 ",
 152     "\x1b[38;5;246me4 ", "\x1b[38;5;246me5 ",
 153     "\x1b[38;5;246me6 ", "\x1b[38;5;246me7 ",
 154     "\x1b[38;5;246me8 ", "\x1b[38;5;246me9 ",
 155     "\x1b[38;5;246mea ", "\x1b[38;5;246meb ",
 156     "\x1b[38;5;246mec ", "\x1b[38;5;246med ",
 157     "\x1b[38;5;246mee ", "\x1b[38;5;246mef ",
 158     "\x1b[38;5;246mf0 ", "\x1b[38;5;246mf1 ",
 159     "\x1b[38;5;246mf2 ", "\x1b[38;5;246mf3 ",
 160     "\x1b[38;5;246mf4 ", "\x1b[38;5;246mf5 ",
 161     "\x1b[38;5;246mf6 ", "\x1b[38;5;246mf7 ",
 162     "\x1b[38;5;246mf8 ", "\x1b[38;5;246mf9 ",
 163     "\x1b[38;5;246mfa ", "\x1b[38;5;246mfb ",
 164     "\x1b[38;5;246mfc ", "\x1b[38;5;246mfd ",
 165     "\x1b[38;5;246mfe ", "\x1b[38;5;209mff ",
 166 }
 167 
 168 // hexSymbols is a direct lookup table combining 2 hex digits with either a
 169 // space or a displayable ASCII symbol matching the byte's own ASCII value;
 170 // this table was autogenerated by running the command
 171 //
 172 // seq 0 255 | ./hex-symbols.awk
 173 var hexSymbols = [256]string{
 174     `00 `, `01 `, `02 `, `03 `, `04 `, `05 `, `06 `, `07 `,
 175     `08 `, `09 `, `0a `, `0b `, `0c `, `0d `, `0e `, `0f `,
 176     `10 `, `11 `, `12 `, `13 `, `14 `, `15 `, `16 `, `17 `,
 177     `18 `, `19 `, `1a `, `1b `, `1c `, `1d `, `1e `, `1f `,
 178     `20 `, `21!`, `22"`, `23#`, `24$`, `25%`, `26&`, `27'`,
 179     `28(`, `29)`, `2a*`, `2b+`, `2c,`, `2d-`, `2e.`, `2f/`,
 180     `300`, `311`, `322`, `333`, `344`, `355`, `366`, `377`,
 181     `388`, `399`, `3a:`, `3b;`, `3c<`, `3d=`, `3e>`, `3f?`,
 182     `40@`, `41A`, `42B`, `43C`, `44D`, `45E`, `46F`, `47G`,
 183     `48H`, `49I`, `4aJ`, `4bK`, `4cL`, `4dM`, `4eN`, `4fO`,
 184     `50P`, `51Q`, `52R`, `53S`, `54T`, `55U`, `56V`, `57W`,
 185     `58X`, `59Y`, `5aZ`, `5b[`, `5c\`, `5d]`, `5e^`, `5f_`,
 186     "60`", `61a`, `62b`, `63c`, `64d`, `65e`, `66f`, `67g`,
 187     `68h`, `69i`, `6aj`, `6bk`, `6cl`, `6dm`, `6en`, `6fo`,
 188     `70p`, `71q`, `72r`, `73s`, `74t`, `75u`, `76v`, `77w`,
 189     `78x`, `79y`, `7az`, `7b{`, `7c|`, `7d}`, `7e~`, `7f `,
 190     `80 `, `81 `, `82 `, `83 `, `84 `, `85 `, `86 `, `87 `,
 191     `88 `, `89 `, `8a `, `8b `, `8c `, `8d `, `8e `, `8f `,
 192     `90 `, `91 `, `92 `, `93 `, `94 `, `95 `, `96 `, `97 `,
 193     `98 `, `99 `, `9a `, `9b `, `9c `, `9d `, `9e `, `9f `,
 194     `a0 `, `a1 `, `a2 `, `a3 `, `a4 `, `a5 `, `a6 `, `a7 `,
 195     `a8 `, `a9 `, `aa `, `ab `, `ac `, `ad `, `ae `, `af `,
 196     `b0 `, `b1 `, `b2 `, `b3 `, `b4 `, `b5 `, `b6 `, `b7 `,
 197     `b8 `, `b9 `, `ba `, `bb `, `bc `, `bd `, `be `, `bf `,
 198     `c0 `, `c1 `, `c2 `, `c3 `, `c4 `, `c5 `, `c6 `, `c7 `,
 199     `c8 `, `c9 `, `ca `, `cb `, `cc `, `cd `, `ce `, `cf `,
 200     `d0 `, `d1 `, `d2 `, `d3 `, `d4 `, `d5 `, `d6 `, `d7 `,
 201     `d8 `, `d9 `, `da `, `db `, `dc `, `dd `, `de `, `df `,
 202     `e0 `, `e1 `, `e2 `, `e3 `, `e4 `, `e5 `, `e6 `, `e7 `,
 203     `e8 `, `e9 `, `ea `, `eb `, `ec `, `ed `, `ee `, `ef `,
 204     `f0 `, `f1 `, `f2 `, `f3 `, `f4 `, `f5 `, `f6 `, `f7 `,
 205     `f8 `, `f9 `, `fa `, `fb `, `fc `, `fd `, `fe `, `ff `,
 206 }
 207 
 208 const (
 209     unknownStyle = 0
 210     zeroStyle    = 1
 211     otherStyle   = 2
 212     asciiStyle   = 3
 213     allOnStyle   = 4
 214 )
 215 
 216 // byteStyles turns bytes into one of several distinct visual types, which
 217 // allows quickly telling when ANSI styles codes are repetitive and when
 218 // they're actually needed
 219 var byteStyles = [256]int{
 220     zeroStyle, otherStyle, otherStyle, otherStyle,
 221     otherStyle, otherStyle, otherStyle, otherStyle,
 222     otherStyle, otherStyle, otherStyle, otherStyle,
 223     otherStyle, otherStyle, otherStyle, otherStyle,
 224     otherStyle, otherStyle, otherStyle, otherStyle,
 225     otherStyle, otherStyle, otherStyle, otherStyle,
 226     otherStyle, otherStyle, otherStyle, otherStyle,
 227     otherStyle, otherStyle, otherStyle, otherStyle,
 228     asciiStyle, asciiStyle, asciiStyle, asciiStyle,
 229     asciiStyle, asciiStyle, asciiStyle, asciiStyle,
 230     asciiStyle, asciiStyle, asciiStyle, asciiStyle,
 231     asciiStyle, asciiStyle, asciiStyle, asciiStyle,
 232     asciiStyle, asciiStyle, asciiStyle, asciiStyle,
 233     asciiStyle, asciiStyle, asciiStyle, asciiStyle,
 234     asciiStyle, asciiStyle, asciiStyle, asciiStyle,
 235     asciiStyle, asciiStyle, asciiStyle, asciiStyle,
 236     asciiStyle, asciiStyle, asciiStyle, asciiStyle,
 237     asciiStyle, asciiStyle, asciiStyle, asciiStyle,
 238     asciiStyle, asciiStyle, asciiStyle, asciiStyle,
 239     asciiStyle, asciiStyle, asciiStyle, asciiStyle,
 240     asciiStyle, asciiStyle, asciiStyle, asciiStyle,
 241     asciiStyle, asciiStyle, asciiStyle, asciiStyle,
 242     asciiStyle, asciiStyle, asciiStyle, asciiStyle,
 243     asciiStyle, asciiStyle, asciiStyle, asciiStyle,
 244     asciiStyle, asciiStyle, asciiStyle, asciiStyle,
 245     asciiStyle, asciiStyle, asciiStyle, asciiStyle,
 246     asciiStyle, asciiStyle, asciiStyle, asciiStyle,
 247     asciiStyle, asciiStyle, asciiStyle, asciiStyle,
 248     asciiStyle, asciiStyle, asciiStyle, asciiStyle,
 249     asciiStyle, asciiStyle, asciiStyle, asciiStyle,
 250     asciiStyle, asciiStyle, asciiStyle, asciiStyle,
 251     asciiStyle, asciiStyle, asciiStyle, otherStyle,
 252     otherStyle, otherStyle, otherStyle, otherStyle,
 253     otherStyle, otherStyle, otherStyle, otherStyle,
 254     otherStyle, otherStyle, otherStyle, otherStyle,
 255     otherStyle, otherStyle, otherStyle, otherStyle,
 256     otherStyle, otherStyle, otherStyle, otherStyle,
 257     otherStyle, otherStyle, otherStyle, otherStyle,
 258     otherStyle, otherStyle, otherStyle, otherStyle,
 259     otherStyle, otherStyle, otherStyle, otherStyle,
 260     otherStyle, otherStyle, otherStyle, otherStyle,
 261     otherStyle, otherStyle, otherStyle, otherStyle,
 262     otherStyle, otherStyle, otherStyle, otherStyle,
 263     otherStyle, otherStyle, otherStyle, otherStyle,
 264     otherStyle, otherStyle, otherStyle, otherStyle,
 265     otherStyle, otherStyle, otherStyle, otherStyle,
 266     otherStyle, otherStyle, otherStyle, otherStyle,
 267     otherStyle, otherStyle, otherStyle, otherStyle,
 268     otherStyle, otherStyle, otherStyle, otherStyle,
 269     otherStyle, otherStyle, otherStyle, otherStyle,
 270     otherStyle, otherStyle, otherStyle, otherStyle,
 271     otherStyle, otherStyle, otherStyle, otherStyle,
 272     otherStyle, otherStyle, otherStyle, otherStyle,
 273     otherStyle, otherStyle, otherStyle, otherStyle,
 274     otherStyle, otherStyle, otherStyle, otherStyle,
 275     otherStyle, otherStyle, otherStyle, otherStyle,
 276     otherStyle, otherStyle, otherStyle, otherStyle,
 277     otherStyle, otherStyle, otherStyle, otherStyle,
 278     otherStyle, otherStyle, otherStyle, otherStyle,
 279     otherStyle, otherStyle, otherStyle, otherStyle,
 280     otherStyle, otherStyle, otherStyle, otherStyle,
 281     otherStyle, otherStyle, otherStyle, otherStyle,
 282     otherStyle, otherStyle, otherStyle, otherStyle,
 283     otherStyle, otherStyle, otherStyle, allOnStyle,
 284 }
 285 
 286 // writeMetaANSI shows metadata right before the ANSI-styled hex byte-view
 287 func writeMetaANSI(w *bufio.Writer, fname string, fsize int, cfg config) {
 288     if cfg.Title != "" {
 289         fmt.Fprintf(w, "\x1b[4m%s\x1b[0m\n", cfg.Title)
 290         w.WriteString("\n")
 291     }
 292 
 293     if fsize < 0 {
 294         fmt.Fprintf(w, "• %s\n", fname)
 295     } else {
 296         const fs = "• %s  \x1b[38;5;248m(%s bytes)\x1b[0m\n"
 297         fmt.Fprintf(w, fs, fname, sprintCommas(fsize))
 298     }
 299 
 300     if cfg.Skip > 0 {
 301         const fs = "   \x1b[38;5;5mskipping first %s bytes\x1b[0m\n"
 302         fmt.Fprintf(w, fs, sprintCommas(cfg.Skip))
 303     }
 304     if cfg.MaxBytes > 0 {
 305         const fs = "   \x1b[38;5;5mshowing only up to %s bytes\x1b[0m\n"
 306         fmt.Fprintf(w, fs, sprintCommas(cfg.MaxBytes))
 307     }
 308     w.WriteString("\n")
 309 }
 310 
 311 // writeBufferANSI shows the hex byte-view using ANSI colors/styles
 312 func writeBufferANSI(rc rendererConfig, first, second []byte) error {
 313     // show a ruler every few lines to make eye-scanning easier
 314     if rc.chunks%5 == 0 && rc.chunks > 0 {
 315         writeRulerANSI(rc)
 316     }
 317 
 318     return writeLineANSI(rc, first, second)
 319 }
 320 
 321 // writeRulerANSI emits an indented ANSI-styled line showing spaced-out dots,
 322 // so as to help eye-scan items on nearby output lines
 323 func writeRulerANSI(rc rendererConfig) {
 324     w := rc.out
 325     if len(rc.ruler) == 0 {
 326         w.WriteByte('\n')
 327         return
 328     }
 329 
 330     w.WriteString("\x1b[38;5;248m")
 331     indent := int(rc.offsetWidth) + len(padding)
 332     writeSpaces(w, indent)
 333     w.Write(rc.ruler)
 334     w.WriteString("\x1b[0m\n")
 335 }
 336 
 337 func writeLineANSI(rc rendererConfig, first, second []byte) error {
 338     w := rc.out
 339 
 340     // start each line with the byte-offset for the 1st item shown on it
 341     if rc.showOffsets {
 342         writeStyledCounter(w, int(rc.offsetWidth), rc.offset)
 343         w.WriteString(padding + "\x1b[48;5;254m")
 344     } else {
 345         w.WriteString(padding)
 346     }
 347 
 348     prevStyle := unknownStyle
 349     for _, b := range first {
 350         // using the slow/generic fmt.Fprintf is a performance bottleneck,
 351         // since it's called for each input byte
 352         // w.WriteString(styledHexBytes[b])
 353 
 354         // this more complicated way of emitting output avoids repeating
 355         // ANSI styles when dealing with bytes which aren't displayable
 356         // ASCII symbols, thus emitting fewer bytes when dealing with
 357         // general binary datasets; it makes no difference for plain-text
 358         // ASCII input
 359         style := byteStyles[b]
 360         if style != prevStyle {
 361             w.WriteString(styledHexBytes[b])
 362             if style == asciiStyle {
 363                 // styling displayable ASCII symbols uses multiple different
 364                 // styles each time it happens, always forcing ANSI-style
 365                 // updates
 366                 style = unknownStyle
 367             }
 368         } else {
 369             w.WriteString(hexSymbols[b])
 370         }
 371         prevStyle = style
 372     }
 373 
 374     w.WriteString("\x1b[0m")
 375     if rc.showASCII {
 376         writePlainASCII(w, first, second, int(rc.perLine))
 377     }
 378 
 379     return w.WriteByte('\n')
 380 }
 381 
 382 func writeStyledCounter(w *bufio.Writer, width int, n uint) {
 383     var buf [32]byte
 384     str := strconv.AppendUint(buf[:0], uint64(n), 10)
 385 
 386     // left-pad the final result with leading spaces
 387     writeSpaces(w, width-len(str))
 388 
 389     var style bool
 390     // emit leading part with 1 or 2 digits unstyled, ensuring the
 391     // rest or the rendered number's string is a multiple of 3 long
 392     if rem := len(str) % 3; rem != 0 {
 393         w.Write(str[:rem])
 394         str = str[rem:]
 395         // next digit-group needs some styling
 396         style = true
 397     } else {
 398         style = false
 399     }
 400 
 401     // alternate between styled/unstyled 3-digit groups
 402     for len(str) > 0 {
 403         if !style {
 404             w.Write(str[:3])
 405         } else {
 406             w.WriteString("\x1b[38;5;248m")
 407             w.Write(str[:3])
 408             w.WriteString("\x1b[0m")
 409         }
 410 
 411         style = !style
 412         str = str[3:]
 413     }
 414 }
     File: ./nhex/config.go
   1 /*
   2 The MIT License (MIT)
   3 
   4 Copyright (c) 2026 pacman64
   5 
   6 Permission is hereby granted, free of charge, to any person obtaining a copy of
   7 this software and associated documentation files (the "Software"), to deal
   8 in the Software without restriction, including without limitation the rights to
   9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
  10 of the Software, and to permit persons to whom the Software is furnished to do
  11 so, subject to the following conditions:
  12 
  13 The above copyright notice and this permission notice shall be included in all
  14 copies or substantial portions of the Software.
  15 
  16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  22 SOFTWARE.
  23 */
  24 
  25 package nhex
  26 
  27 import (
  28     "bufio"
  29     "bytes"
  30     "flag"
  31     "fmt"
  32 )
  33 
  34 const (
  35     usageMaxBytes   = `limit input up to n bytes; negative to disable`
  36     usagePerLine    = `how many bytes to show on each line`
  37     usageSkip       = `how many leading bytes to skip/ignore`
  38     usageTitle      = `use this to show a title/description`
  39     usageTo         = `the output format to use (plain or ansi)`
  40     usagePlain      = `show plain-text output, as opposed to ansi-styled output`
  41     usageShowOffset = `start lines with the offset of the 1st byte shown on each`
  42     usageShowASCII  = `repeat all ASCII strings on the side, so they're searcheable`
  43 )
  44 
  45 const defaultOffsetCounterWidth = 8
  46 
  47 const (
  48     plainOutput = `plain`
  49     ansiOutput  = `ansi`
  50 )
  51 
  52 // config is the parsed cmd-line options given to the app
  53 type config struct {
  54     // MaxBytes limits how many bytes are shown; a negative value means no limit
  55     MaxBytes int
  56 
  57     // PerLine is how many bytes are shown per output line
  58     PerLine int
  59 
  60     // Skip is how many leading bytes to skip/ignore
  61     Skip int
  62 
  63     // OffsetCounterWidth is the max string-width; not exposed as a cmd-line option
  64     OffsetCounterWidth uint
  65 
  66     // Title is an optional title preceding the output proper
  67     Title string
  68 
  69     // To is the output format
  70     To string
  71 
  72     // Filenames is the list of input filenames
  73     Filenames []string
  74 
  75     // Ruler is a prerendered ruler to emit every few output lines
  76     Ruler []byte
  77 
  78     // ShowOffsets starts lines with the offset of the 1st byte shown on each
  79     ShowOffsets bool
  80 
  81     // ShowASCII shows a side-panel with searcheable ASCII-runs
  82     ShowASCII bool
  83 }
  84 
  85 // parseFlags is the constructor for type config
  86 func parseFlags(usage string) config {
  87     flag.Usage = func() {
  88         fmt.Fprintf(flag.CommandLine.Output(), "%s\n\nOptions\n\n", usage)
  89         flag.PrintDefaults()
  90     }
  91 
  92     cfg := config{
  93         MaxBytes:           -1,
  94         PerLine:            16,
  95         OffsetCounterWidth: 0,
  96         To:                 ansiOutput,
  97         ShowOffsets:        true,
  98         ShowASCII:          true,
  99     }
 100 
 101     plain := false
 102     flag.IntVar(&cfg.MaxBytes, `max`, cfg.MaxBytes, usageMaxBytes)
 103     flag.IntVar(&cfg.PerLine, `width`, cfg.PerLine, usagePerLine)
 104     flag.IntVar(&cfg.Skip, `skip`, cfg.Skip, usageSkip)
 105     flag.StringVar(&cfg.Title, `title`, cfg.Title, usageTitle)
 106     flag.StringVar(&cfg.To, `to`, cfg.To, usageTo)
 107     flag.BoolVar(&cfg.ShowOffsets, `n`, cfg.ShowOffsets, usageShowOffset)
 108     flag.BoolVar(&cfg.ShowASCII, `ascii`, cfg.ShowASCII, usageShowASCII)
 109     flag.BoolVar(&plain, `p`, plain, "alias for option `plain`")
 110     flag.BoolVar(&plain, `plain`, plain, usagePlain)
 111     flag.Parse()
 112 
 113     if plain {
 114         cfg.To = plainOutput
 115     }
 116 
 117     // normalize values for option -to
 118     switch cfg.To {
 119     case `text`, `plaintext`, `plain-text`:
 120         cfg.To = plainOutput
 121     }
 122 
 123     cfg.Ruler = makeRuler(cfg.PerLine)
 124     cfg.Filenames = flag.Args()
 125     return cfg
 126 }
 127 
 128 // makeRuler prerenders a ruler-line, used to make the output lines breathe
 129 func makeRuler(numitems int) []byte {
 130     if n := numitems / 4; n > 0 {
 131         var pat = []byte(`           ·`)
 132         return bytes.Repeat(pat, n)
 133     }
 134     return nil
 135 }
 136 
 137 // rendererConfig groups several arguments given to any of the rendering funcs
 138 type rendererConfig struct {
 139     // out is writer to send all output to
 140     out *bufio.Writer
 141 
 142     // offset is the byte-offset of the first byte shown on the current output
 143     // line: if shown at all, it's shown at the start the line
 144     offset uint
 145 
 146     // chunks is the 0-based counter for byte-chunks/lines shown so far, which
 147     // indirectly keeps track of when it's time to show a `breather` line
 148     chunks uint
 149 
 150     // ruler is the `ruler` content to show on `breather` lines
 151     ruler []byte
 152 
 153     // perLine is how many hex-encoded bytes are shown per line
 154     perLine uint
 155 
 156     // offsetWidth is the max string-width for the byte-offsets shown at the
 157     // start of output lines, and determines those values' left-padding
 158     offsetWidth uint
 159 
 160     // showOffsets determines whether byte-offsets are shown at all
 161     showOffsets bool
 162 
 163     // showASCII determines whether the ASCII-panels are shown at all
 164     showASCII bool
 165 }
     File: ./nhex/hex-styles.awk
   1 #!/usr/bin/awk -f
   2 
   3 # The MIT License (MIT)
   4 #
   5 # Copyright (c) 2026 pacman64
   6 #
   7 # Permission is hereby granted, free of charge, to any person obtaining a copy
   8 # of this software and associated documentation files (the "Software"), to deal
   9 # in the Software without restriction, including without limitation the rights
  10 # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
  11 # copies of the Software, and to permit persons to whom the Software is
  12 # furnished to do so, subject to the following conditions:
  13 #
  14 # The above copyright notice and this permission notice shall be included in
  15 # all copies or substantial portions of the Software.
  16 #
  17 # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  18 # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  19 # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  20 # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  21 # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  22 # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  23 # SOFTWARE.
  24 
  25 
  26 # all 0 bits
  27 $0 == 0 {
  28     print "\"\\x1b[38;5;111m00 \","
  29     next
  30 }
  31 
  32 # ascii symbol which need backslashing
  33 $0 == 34 || $0 == 92 {
  34     printf "\"\\x1b[38;5;72m%02x\\x1b[38;5;239m\\%c\",\n", $0 + 0, $0
  35     next
  36 }
  37 
  38 # all other ascii symbol
  39 32 <= $0 && $0 <= 126 {
  40     printf "\"\\x1b[38;5;72m%02x\\x1b[38;5;239m%c\",\n", $0 + 0, $0
  41     next
  42 }
  43 
  44 # all 1 bits
  45 $0 == 255 {
  46     print "\"\\x1b[38;5;209mff \","
  47     next
  48 }
  49 
  50 # all other bytes
  51 1 {
  52     printf "\"\\x1b[38;5;246m%02x \",\n", $0 + 0
  53     next
  54 }
     File: ./nhex/hex-symbols.awk
   1 #!/usr/bin/awk -f
   2 
   3 # The MIT License (MIT)
   4 #
   5 # Copyright (c) 2026 pacman64
   6 #
   7 # Permission is hereby granted, free of charge, to any person obtaining a copy
   8 # of this software and associated documentation files (the "Software"), to deal
   9 # in the Software without restriction, including without limitation the rights
  10 # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
  11 # copies of the Software, and to permit persons to whom the Software is
  12 # furnished to do so, subject to the following conditions:
  13 #
  14 # The above copyright notice and this permission notice shall be included in
  15 # all copies or substantial portions of the Software.
  16 #
  17 # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  18 # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  19 # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  20 # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  21 # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  22 # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  23 # SOFTWARE.
  24 
  25 
  26 # ascii symbol which need backslashing
  27 $0 == 34 || $0 == 92 {
  28     printf "\"%02x\\%c\",\n", $0 + 0, $0
  29     next
  30 }
  31 
  32 # all other ascii symbol
  33 32 <= $0 && $0 <= 126 {
  34     printf "\"%02x%c\",\n", $0 + 0, $0
  35     next
  36 }
  37 
  38 # all other bytes
  39 1 {
  40     printf "\"%02x \",\n", $0 + 0
  41     next
  42 }
     File: ./nhex/info.txt
   1 nhex [options...] [filenames...]
   2 
   3 
   4 Nice HEXadecimal is a simple hexadecimal viewer to easily inspect bytes
   5 from files/data.
   6 
   7 Each line shows the starting offset for the bytes shown, 16 of the bytes
   8 themselves in base-16 notation, and any ASCII codes when the byte values
   9 are in the typical ASCII range. The offsets shown are base-10.
  10 
  11 The base-16 codes are color-coded, with most bytes shown in gray, while
  12 all-1 and all-0 bytes are shown in orange and blue respectively.
  13 
  14 All-0 bytes are the commonest kind in most binary file types and, along
  15 with all-1 bytes are also a special case worth noticing when exploring
  16 binary data, so it makes sense for them to stand out right away.
     File: ./nhex/main.go
   1 /*
   2 The MIT License (MIT)
   3 
   4 Copyright (c) 2026 pacman64
   5 
   6 Permission is hereby granted, free of charge, to any person obtaining a copy of
   7 this software and associated documentation files (the "Software"), to deal
   8 in the Software without restriction, including without limitation the rights to
   9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
  10 of the Software, and to permit persons to whom the Software is furnished to do
  11 so, subject to the following conditions:
  12 
  13 The above copyright notice and this permission notice shall be included in all
  14 copies or substantial portions of the Software.
  15 
  16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  22 SOFTWARE.
  23 */
  24 
  25 package nhex
  26 
  27 import (
  28     "bufio"
  29     "fmt"
  30     "io"
  31     "math"
  32     "os"
  33 
  34     _ "embed"
  35 )
  36 
  37 //go:embed info.txt
  38 var usage string
  39 
  40 func Main() {
  41     err := run(parseFlags(usage))
  42     if err != nil {
  43         os.Stderr.WriteString(err.Error())
  44         os.Stderr.WriteString("\n")
  45         os.Exit(1)
  46     }
  47 }
  48 
  49 func run(cfg config) error {
  50     // f, _ := os.Create(`nh.prof`)
  51     // defer f.Close()
  52     // pprof.StartCPUProfile(f)
  53     // defer pprof.StopCPUProfile()
  54 
  55     w := bufio.NewWriterSize(os.Stdout, 16*1024)
  56     defer w.Flush()
  57 
  58     // with no filenames given, handle stdin and quit
  59     if len(cfg.Filenames) == 0 {
  60         return handle(w, os.Stdin, `<stdin>`, -1, cfg)
  61     }
  62 
  63     // show all files given
  64     for i, fname := range cfg.Filenames {
  65         if i > 0 {
  66             w.WriteString("\n")
  67             w.WriteString("\n")
  68         }
  69 
  70         err := handleFile(w, fname, cfg)
  71         if err != nil {
  72             return err
  73         }
  74     }
  75 
  76     return nil
  77 }
  78 
  79 // handleFile is like handleReader, except it also shows file-related info
  80 func handleFile(w *bufio.Writer, fname string, cfg config) error {
  81     f, err := os.Open(fname)
  82     if err != nil {
  83         return err
  84     }
  85     defer f.Close()
  86 
  87     stat, err := f.Stat()
  88     if err != nil {
  89         return handle(w, f, fname, -1, cfg)
  90     }
  91 
  92     fsize := int(stat.Size())
  93     return handle(w, f, fname, fsize, cfg)
  94 }
  95 
  96 // handle shows some messages related to the input and the cmd-line options
  97 // used, and then follows them by the hexadecimal byte-view
  98 func handle(w *bufio.Writer, r io.Reader, name string, size int, cfg config) error {
  99     skip(r, cfg.Skip)
 100     if cfg.MaxBytes > 0 {
 101         r = io.LimitReader(r, int64(cfg.MaxBytes))
 102     }
 103 
 104     // finish config setup based on the filesize, if a valid one was given
 105     if cfg.OffsetCounterWidth < 1 {
 106         if size < 1 {
 107             cfg.OffsetCounterWidth = defaultOffsetCounterWidth
 108         } else {
 109             w := math.Log10(float64(size))
 110             w = math.Max(math.Ceil(w), 1)
 111             cfg.OffsetCounterWidth = uint(w)
 112         }
 113     }
 114 
 115     switch cfg.To {
 116     case plainOutput:
 117         writeMetaPlain(w, name, size, cfg)
 118         // when done, emit a new line in case only part of the last line is
 119         // shown, which means no newline was emitted for it
 120         defer w.WriteString("\n")
 121         return render(w, r, cfg, writeBufferPlain)
 122 
 123     case ansiOutput:
 124         writeMetaANSI(w, name, size, cfg)
 125         // when done, emit a new line in case only part of the last line is
 126         // shown, which means no newline was emitted for it
 127         defer w.WriteString("\x1b[0m\n")
 128         return render(w, r, cfg, writeBufferANSI)
 129 
 130     default:
 131         const fs = `unsupported output format %q`
 132         return fmt.Errorf(fs, cfg.To)
 133     }
 134 }
 135 
 136 // skip ignores n bytes from the reader given
 137 func skip(r io.Reader, n int) {
 138     if n < 1 {
 139         return
 140     }
 141 
 142     // use func Seek for input files, except for stdin, which you can't seek
 143     if f, ok := r.(*os.File); ok && r != os.Stdin {
 144         f.Seek(int64(n), io.SeekCurrent)
 145         return
 146     }
 147     io.CopyN(io.Discard, r, int64(n))
 148 }
 149 
 150 // renderer is the type for the hex-view render funcs
 151 type renderer func(rc rendererConfig, first, second []byte) error
 152 
 153 // render reads all input and shows the hexadecimal byte-view for the input
 154 // data via the rendering callback given
 155 func render(w *bufio.Writer, r io.Reader, cfg config, fn renderer) error {
 156     if cfg.PerLine < 1 {
 157         cfg.PerLine = 16
 158     }
 159 
 160     rc := rendererConfig{
 161         out:     w,
 162         offset:  uint(cfg.Skip),
 163         chunks:  0,
 164         perLine: uint(cfg.PerLine),
 165         ruler:   cfg.Ruler,
 166 
 167         offsetWidth: cfg.OffsetCounterWidth,
 168         showOffsets: cfg.ShowOffsets,
 169         showASCII:   cfg.ShowASCII,
 170     }
 171 
 172     // calling func Read directly can sometimes result in chunks shorter
 173     // than the max chunk-size, even when there are plenty of bytes yet
 174     // to read; to avoid that, use a buffered-reader to explicitly fill
 175     // a slice instead
 176     br := bufio.NewReader(r)
 177 
 178     // to show ASCII up to 1 full chunk ahead, 2 chunks are needed
 179     cur := make([]byte, 0, cfg.PerLine)
 180     ahead := make([]byte, 0, cfg.PerLine)
 181 
 182     // the ASCII-panel's wide output requires staying 1 step/chunk behind,
 183     // so to speak
 184     cur, err := fillChunk(cur[:0], cfg.PerLine, br)
 185     if len(cur) == 0 {
 186         if err == io.EOF {
 187             err = nil
 188         }
 189         return err
 190     }
 191 
 192     for {
 193         ahead, err := fillChunk(ahead[:0], cfg.PerLine, br)
 194         if err != nil && err != io.EOF {
 195             return err
 196         }
 197 
 198         if len(ahead) == 0 {
 199             // done, maybe except for an extra line of output
 200             break
 201         }
 202 
 203         // show the byte-chunk on its own output line
 204         err = fn(rc, cur, ahead)
 205         if err != nil {
 206             // probably a pipe was closed
 207             return nil
 208         }
 209 
 210         rc.chunks++
 211         rc.offset += uint(len(cur))
 212         cur = cur[:copy(cur, ahead)]
 213     }
 214 
 215     // don't forget the last output line
 216     if rc.chunks > 0 && len(cur) > 0 {
 217         return fn(rc, cur, nil)
 218     }
 219     return nil
 220 }
 221 
 222 // fillChunk tries to read the number of bytes given, appending them to the
 223 // byte-slice given; this func returns an EOF error only when no bytes are
 224 // read, which somewhat simplifies error-handling for the func caller
 225 func fillChunk(chunk []byte, n int, br *bufio.Reader) ([]byte, error) {
 226     // read buffered-bytes up to the max chunk-size
 227     for i := 0; i < n; i++ {
 228         b, err := br.ReadByte()
 229         if err == nil {
 230             chunk = append(chunk, b)
 231             continue
 232         }
 233 
 234         if err == io.EOF && i > 0 {
 235             return chunk, nil
 236         }
 237         return chunk, err
 238     }
 239 
 240     // got the full byte-count asked for
 241     return chunk, nil
 242 }
     File: ./nhex/numbers.go
   1 /*
   2 The MIT License (MIT)
   3 
   4 Copyright (c) 2026 pacman64
   5 
   6 Permission is hereby granted, free of charge, to any person obtaining a copy of
   7 this software and associated documentation files (the "Software"), to deal
   8 in the Software without restriction, including without limitation the rights to
   9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
  10 of the Software, and to permit persons to whom the Software is furnished to do
  11 so, subject to the following conditions:
  12 
  13 The above copyright notice and this permission notice shall be included in all
  14 copies or substantial portions of the Software.
  15 
  16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  22 SOFTWARE.
  23 */
  24 
  25 package nhex
  26 
  27 import (
  28     "bufio"
  29     "io"
  30     "math"
  31     "strconv"
  32     "strings"
  33 )
  34 
  35 // loopThousandsGroups comes from my lib/package `mathplus`: that's why it
  36 // handles negatives, even though this app only uses it with non-negatives.
  37 func loopThousandsGroups(n int, fn func(i, n int)) {
  38     // 0 doesn't have a log10
  39     if n == 0 {
  40         fn(0, 0)
  41         return
  42     }
  43 
  44     sign := +1
  45     if n < 0 {
  46         n = -n
  47         sign = -1
  48     }
  49 
  50     intLog1000 := int(math.Log10(float64(n)) / 3)
  51     remBase := int(math.Pow10(3 * intLog1000))
  52 
  53     for i := 0; remBase > 0; i++ {
  54         group := (1000 * n) / remBase / 1000
  55         fn(i, sign*group)
  56         // if original number was negative, ensure only first
  57         // group gives a negative input to the callback
  58         sign = +1
  59 
  60         n %= remBase
  61         remBase /= 1000
  62     }
  63 }
  64 
  65 // sprintCommas turns the non-negative number given into a readable string,
  66 // where digits are grouped-separated by commas
  67 func sprintCommas(n int) string {
  68     var sb strings.Builder
  69     loopThousandsGroups(n, func(i, n int) {
  70         if i == 0 {
  71             var buf [4]byte
  72             sb.Write(strconv.AppendInt(buf[:0], int64(n), 10))
  73             return
  74         }
  75         sb.WriteByte(',')
  76         writePad0Sub1000Counter(&sb, uint(n))
  77     })
  78     return sb.String()
  79 }
  80 
  81 // writePad0Sub1000Counter is an alternative to fmt.Fprintf(w, `%03d`, n)
  82 func writePad0Sub1000Counter(w io.Writer, n uint) {
  83     // precondition is 0...999
  84     if n > 999 {
  85         w.Write([]byte(`???`))
  86         return
  87     }
  88 
  89     var buf [3]byte
  90     buf[0] = byte(n/100) + '0'
  91     n %= 100
  92     buf[1] = byte(n/10) + '0'
  93     buf[2] = byte(n%10) + '0'
  94     w.Write(buf[:])
  95 }
  96 
  97 // writeHex is faster than calling fmt.Fprintf(w, `%02x`, b): this
  98 // matters because it's called for every byte of input which isn't
  99 // all 0s or all 1s
 100 func writeHex(w *bufio.Writer, b byte) {
 101     const hexDigits = `0123456789abcdef`
 102     w.WriteByte(hexDigits[b>>4])
 103     w.WriteByte(hexDigits[b&0x0f])
 104 }
     File: ./nhex/plain.go
   1 /*
   2 The MIT License (MIT)
   3 
   4 Copyright (c) 2026 pacman64
   5 
   6 Permission is hereby granted, free of charge, to any person obtaining a copy of
   7 this software and associated documentation files (the "Software"), to deal
   8 in the Software without restriction, including without limitation the rights to
   9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
  10 of the Software, and to permit persons to whom the Software is furnished to do
  11 so, subject to the following conditions:
  12 
  13 The above copyright notice and this permission notice shall be included in all
  14 copies or substantial portions of the Software.
  15 
  16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  22 SOFTWARE.
  23 */
  24 
  25 package nhex
  26 
  27 import (
  28     "bufio"
  29     "fmt"
  30     "strconv"
  31 )
  32 
  33 // padding is the padding/spacing emitted across each output line, except for
  34 // the breather/ruler lines
  35 const padding = `  `
  36 
  37 // writeMetaPlain shows metadata right before the plain-text hex byte-view
  38 func writeMetaPlain(w *bufio.Writer, fname string, fsize int, cfg config) {
  39     if cfg.Title != `` {
  40         w.WriteString(cfg.Title)
  41         w.WriteString("\n")
  42         w.WriteString("\n")
  43     }
  44 
  45     if fsize < 0 {
  46         fmt.Fprintf(w, "• %s\n", fname)
  47     } else {
  48         const fs = "• %s  (%s bytes)\n"
  49         fmt.Fprintf(w, fs, fname, sprintCommas(fsize))
  50     }
  51 
  52     if cfg.Skip > 0 {
  53         const fs = "   skipping first %s bytes\n"
  54         fmt.Fprintf(w, fs, sprintCommas(cfg.Skip))
  55     }
  56     if cfg.MaxBytes > 0 {
  57         const fs = "   showing only up to %s bytes\n"
  58         fmt.Fprintf(w, fs, sprintCommas(cfg.MaxBytes))
  59     }
  60     w.WriteString("\n")
  61 }
  62 
  63 // writeBufferPlain shows the hex byte-view withOUT using ANSI colors/styles
  64 func writeBufferPlain(rc rendererConfig, first, second []byte) error {
  65     // show a ruler every few lines to make eye-scanning easier
  66     if rc.chunks%5 == 0 && rc.chunks > 0 {
  67         rc.out.WriteByte('\n')
  68     }
  69 
  70     return writeLinePlain(rc, first, second)
  71 }
  72 
  73 func writeLinePlain(rc rendererConfig, first, second []byte) error {
  74     w := rc.out
  75 
  76     // start each line with the byte-offset for the 1st item shown on it
  77     if rc.showOffsets {
  78         writePlainCounter(w, int(rc.offsetWidth), rc.offset)
  79         w.WriteByte(' ')
  80     } else {
  81         w.WriteString(padding)
  82     }
  83 
  84     for _, b := range first {
  85         // fmt.Fprintf(w, ` %02x`, b)
  86         //
  87         // the commented part above was a performance bottleneck, since
  88         // the slow/generic fmt.Fprintf was called for each input byte
  89         w.WriteByte(' ')
  90         writeHex(w, b)
  91     }
  92 
  93     if rc.showASCII {
  94         writePlainASCII(w, first, second, int(rc.perLine))
  95     }
  96 
  97     return w.WriteByte('\n')
  98 }
  99 
 100 // writePlainCounter just emits a left-padded number
 101 func writePlainCounter(w *bufio.Writer, width int, n uint) {
 102     var buf [32]byte
 103     str := strconv.AppendUint(buf[:0], uint64(n), 10)
 104     writeSpaces(w, width-len(str))
 105     w.Write(str)
 106 }
 107 
 108 // writeRulerPlain emits a breather line using a ruler-like pattern of spaces
 109 // and dots, to guide the eye across the main output lines
 110 // func writeRulerPlain(w *bufio.Writer, indent int, offset int, numitems int) {
 111 //  writeSpaces(w, indent)
 112 //  for i := 0; i < numitems-1; i++ {
 113 //      if (i+offset+1)%5 == 0 {
 114 //          w.WriteString(`   `)
 115 //      } else {
 116 //          w.WriteString(`  ·`)
 117 //      }
 118 //  }
 119 // }
 120 
 121 // writeSpaces bulk-emits the number of spaces given
 122 func writeSpaces(w *bufio.Writer, n int) {
 123     const spaces = `                                `
 124     for ; n > len(spaces); n -= len(spaces) {
 125         w.WriteString(spaces)
 126     }
 127     if n > 0 {
 128         w.WriteString(spaces[:n])
 129     }
 130 }
 131 
 132 // writePlainASCII emits the side-panel showing all ASCII runs for each line
 133 func writePlainASCII(w *bufio.Writer, first, second []byte, perline int) {
 134     // prev keeps track of the previous byte, so spaces are added
 135     // when bytes change from non-visible-ASCII to visible-ASCII
 136     prev := byte(0)
 137 
 138     spaces := 3*(perline-len(first)) + len(padding)
 139 
 140     for _, b := range first {
 141         if 32 < b && b < 127 {
 142             if !(32 < prev && prev < 127) {
 143                 writeSpaces(w, spaces)
 144                 spaces = 1
 145             }
 146             w.WriteByte(b)
 147         }
 148         prev = b
 149     }
 150 
 151     for _, b := range second {
 152         if 32 < b && b < 127 {
 153             if !(32 < prev && prev < 127) {
 154                 writeSpaces(w, spaces)
 155                 spaces = 1
 156             }
 157             w.WriteByte(b)
 158         }
 159         prev = b
 160     }
 161 }
     File: ./njson/njson.go
   1 /*
   2 The MIT License (MIT)
   3 
   4 Copyright (c) 2026 pacman64
   5 
   6 Permission is hereby granted, free of charge, to any person obtaining a copy of
   7 this software and associated documentation files (the "Software"), to deal
   8 in the Software without restriction, including without limitation the rights to
   9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
  10 of the Software, and to permit persons to whom the Software is furnished to do
  11 so, subject to the following conditions:
  12 
  13 The above copyright notice and this permission notice shall be included in all
  14 copies or substantial portions of the Software.
  15 
  16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  22 SOFTWARE.
  23 */
  24 
  25 package njson
  26 
  27 import (
  28     "bufio"
  29     "encoding/json"
  30     "errors"
  31     "io"
  32     "os"
  33 )
  34 
  35 const info = `
  36 njson [filepath...]
  37 
  38 Nice JSON shows JSON data as ANSI-styled indented lines, using 2 spaces for
  39 each indentation level.
  40 `
  41 
  42 // indent is how many spaces each indentation level uses
  43 const indent = 2
  44 
  45 const (
  46     // boolStyle is bluish, and very distinct from all other colors used
  47     boolStyle = "\x1b[38;2;95;175;215m"
  48 
  49     // keyStyle is magenta, and very distinct from normal strings
  50     keyStyle = "\x1b[38;2;135;95;255m"
  51 
  52     // nullStyle is a light-gray, just like syntax elements, but the word
  53     // `null` is wide enough to stand out from syntax items at a glance
  54     nullStyle = syntaxStyle
  55 
  56     // positiveNumberStyle is a nice green
  57     positiveNumberStyle = "\x1b[38;2;0;135;95m"
  58 
  59     // negativeNumberStyle is a nice red
  60     negativeNumberStyle = "\x1b[38;2;204;0;0m"
  61 
  62     // zeroNumberStyle is a nice blue
  63     zeroNumberStyle = "\x1b[38;2;0;95;215m"
  64 
  65     // stringStyle used to be bluish, but it's better to keep it plain,
  66     // which also minimizes how many different colors the output can show
  67     stringStyle = ""
  68 
  69     // syntaxStyle is a light-gray, not too light, not too dark
  70     syntaxStyle = "\x1b[38;2;168;168;168m"
  71 )
  72 
  73 func Main() {
  74     args := os.Args[1:]
  75 
  76     if len(args) > 0 {
  77         switch args[0] {
  78         case `-h`, `--h`, `-help`, `--help`:
  79             os.Stdout.WriteString(info[1:])
  80             return
  81         }
  82     }
  83 
  84     if len(args) > 0 && args[0] == `--` {
  85         args = args[1:]
  86     }
  87 
  88     if len(args) > 1 {
  89         showError(errors.New(`multiple inputs not allowed`))
  90         os.Exit(1)
  91     }
  92 
  93     // figure out whether input should come from a named file or from stdin
  94     name := `-`
  95     if len(args) == 1 {
  96         name = args[0]
  97     }
  98 
  99     var err error
 100     if name == `-` {
 101         // handle lack of filepath arg, or `-` as the filepath
 102         err = niceJSON(os.Stdout, os.Stdin)
 103     } else {
 104         // handle being given a normal filepath
 105         err = handleFile(os.Stdout, os.Args[1])
 106     }
 107 
 108     if err != nil && err != io.EOF {
 109         showError(err)
 110         os.Exit(1)
 111     }
 112 }
 113 
 114 // showError standardizes how errors look in this app
 115 func showError(err error) {
 116     os.Stderr.WriteString(err.Error())
 117     os.Stderr.WriteString("\n")
 118 }
 119 
 120 // writeSpaces does what it says, minimizing calls to write-like funcs
 121 func writeSpaces(w *bufio.Writer, n int) {
 122     const spaces = `                                `
 123     for n >= len(spaces) {
 124         w.WriteString(spaces)
 125         n -= len(spaces)
 126     }
 127     if n > 0 {
 128         w.WriteString(spaces[:n])
 129     }
 130 }
 131 
 132 func handleFile(w io.Writer, path string) error {
 133     // if f := strings.HasPrefix; f(path, `https://`) || f(path, `http://`) {
 134     //  resp, err := http.Get(path)
 135     //  if err != nil {
 136     //      return err
 137     //  }
 138     //  defer resp.Body.Close()
 139     //  return niceJSON(w, resp.Body)
 140     // }
 141 
 142     f, err := os.Open(path)
 143     if err != nil {
 144         // on windows, file-not-found error messages may mention `CreateFile`,
 145         // even when trying to open files in read-only mode
 146         return errors.New(`can't open file named ` + path)
 147     }
 148     defer f.Close()
 149 
 150     return niceJSON(w, f)
 151 }
 152 
 153 func niceJSON(w io.Writer, r io.Reader) error {
 154     bw := bufio.NewWriter(w)
 155     defer bw.Flush()
 156 
 157     dec := json.NewDecoder(r)
 158     // using string-like json.Number values instead of float64 ones avoids
 159     // unneeded reformatting of numbers; reformatting parsed float64 values
 160     // can potentially even drop/change decimals, causing the output not to
 161     // match the input digits exactly, which is best to avoid
 162     dec.UseNumber()
 163 
 164     t, err := dec.Token()
 165     if err == io.EOF {
 166         return errors.New(`empty input isn't valid JSON`)
 167     }
 168     if err != nil {
 169         return err
 170     }
 171 
 172     if err := handleToken(bw, dec, t, 0, 0); err != nil {
 173         return err
 174     }
 175     // don't forget to end the last output line
 176     bw.WriteByte('\n')
 177 
 178     if _, err := dec.Token(); err != io.EOF {
 179         return errors.New(`unexpected trailing JSON data`)
 180     }
 181     return nil
 182 }
 183 
 184 func handleToken(w *bufio.Writer, d *json.Decoder, t json.Token, pre, level int) error {
 185     switch t := t.(type) {
 186     case json.Delim:
 187         switch t {
 188         case json.Delim('['):
 189             return handleArray(w, d, pre, level)
 190 
 191         case json.Delim('{'):
 192             return handleObject(w, d, pre, level)
 193 
 194         default:
 195             // return fmt.Errorf(`unsupported JSON delimiter %v`, t)
 196             return errors.New(`unsupported JSON delimiter`)
 197         }
 198 
 199     case nil:
 200         return handleNull(w, pre)
 201 
 202     case bool:
 203         return handleBoolean(w, t, pre)
 204 
 205     case string:
 206         return handleString(w, t, pre)
 207 
 208     case json.Number:
 209         return handleNumber(w, t, pre)
 210 
 211     default:
 212         // return fmt.Errorf(`unsupported token type %T`, t)
 213         return errors.New(`unsupported token type`)
 214     }
 215 }
 216 
 217 func handleArray(w *bufio.Writer, d *json.Decoder, pre, level int) error {
 218     for i := 0; true; i++ {
 219         t, err := d.Token()
 220         if err != nil {
 221             return err
 222         }
 223 
 224         if t == json.Delim(']') {
 225             if i == 0 {
 226                 writeSpaces(w, indent*pre)
 227                 w.WriteString(syntaxStyle + "[]\x1b[0m")
 228             } else {
 229                 w.WriteString("\n")
 230                 writeSpaces(w, indent*level)
 231                 w.WriteString(syntaxStyle + "]\x1b[0m")
 232             }
 233             return nil
 234         }
 235 
 236         if i == 0 {
 237             writeSpaces(w, indent*pre)
 238             w.WriteString(syntaxStyle + "[\x1b[0m\n")
 239         } else {
 240             // this is a good spot to check for early-quit opportunities
 241             w.WriteString(syntaxStyle + ",\x1b[0m\n")
 242             if err := w.Flush(); err != nil {
 243                 // a write error may be the consequence of stdout being closed,
 244                 // perhaps by another app along a pipe
 245                 return io.EOF
 246             }
 247         }
 248 
 249         if err := handleToken(w, d, t, level+1, level+1); err != nil {
 250             return err
 251         }
 252     }
 253 
 254     // make the compiler happy
 255     return nil
 256 }
 257 
 258 func handleBoolean(w *bufio.Writer, b bool, pre int) error {
 259     writeSpaces(w, indent*pre)
 260     if b {
 261         w.WriteString(boolStyle + "true\x1b[0m")
 262     } else {
 263         w.WriteString(boolStyle + "false\x1b[0m")
 264     }
 265     return nil
 266 }
 267 
 268 func handleKey(w *bufio.Writer, s string, pre int) error {
 269     writeSpaces(w, indent*pre)
 270     w.WriteString(syntaxStyle + "\"\x1b[0m" + keyStyle)
 271     w.WriteString(s)
 272     w.WriteString(syntaxStyle + "\":\x1b[0m ")
 273     return nil
 274 }
 275 
 276 func handleNull(w *bufio.Writer, pre int) error {
 277     writeSpaces(w, indent*pre)
 278     w.WriteString(nullStyle + "null\x1b[0m")
 279     return nil
 280 }
 281 
 282 // func handleNumber(w *bufio.Writer, n json.Number, pre int) error {
 283 //  writeSpaces(w, indent*pre)
 284 //  w.WriteString(numberStyle)
 285 //  w.WriteString(n.String())
 286 //  w.WriteString("\x1b[0m")
 287 //  return nil
 288 // }
 289 
 290 func handleNumber(w *bufio.Writer, n json.Number, pre int) error {
 291     writeSpaces(w, indent*pre)
 292     f, _ := n.Float64()
 293     if f > 0 {
 294         w.WriteString(positiveNumberStyle)
 295     } else if f < 0 {
 296         w.WriteString(negativeNumberStyle)
 297     } else {
 298         w.WriteString(zeroNumberStyle)
 299     }
 300     w.WriteString(n.String())
 301     w.WriteString("\x1b[0m")
 302     return nil
 303 }
 304 
 305 func handleObject(w *bufio.Writer, d *json.Decoder, pre, level int) error {
 306     for i := 0; true; i++ {
 307         t, err := d.Token()
 308         if err != nil {
 309             return err
 310         }
 311 
 312         if t == json.Delim('}') {
 313             if i == 0 {
 314                 writeSpaces(w, indent*pre)
 315                 w.WriteString(syntaxStyle + "{}\x1b[0m")
 316             } else {
 317                 w.WriteString("\n")
 318                 writeSpaces(w, indent*level)
 319                 w.WriteString(syntaxStyle + "}\x1b[0m")
 320             }
 321             return nil
 322         }
 323 
 324         if i == 0 {
 325             writeSpaces(w, indent*pre)
 326             w.WriteString(syntaxStyle + "{\x1b[0m\n")
 327         } else {
 328             // this is a good spot to check for early-quit opportunities
 329             w.WriteString(syntaxStyle + ",\x1b[0m\n")
 330             if err := w.Flush(); err != nil {
 331                 // a write error may be the consequence of stdout being closed,
 332                 // perhaps by another app along a pipe
 333                 return io.EOF
 334             }
 335         }
 336 
 337         // the stdlib's JSON parser is supposed to complain about non-string
 338         // keys anyway, but make sure just in case
 339         k, ok := t.(string)
 340         if !ok {
 341             return errors.New(`expected key to be a string`)
 342         }
 343         if err := handleKey(w, k, level+1); err != nil {
 344             return err
 345         }
 346 
 347         // handle value
 348         t, err = d.Token()
 349         if err != nil {
 350             return err
 351         }
 352         if err := handleToken(w, d, t, 0, level+1); err != nil {
 353             return err
 354         }
 355     }
 356 
 357     // make the compiler happy
 358     return nil
 359 }
 360 
 361 func needsEscaping(s string) bool {
 362     for _, r := range s {
 363         switch r {
 364         case '"', '\\', '\t', '\r', '\n':
 365             return true
 366         }
 367     }
 368     return false
 369 }
 370 
 371 func handleString(w *bufio.Writer, s string, pre int) error {
 372     writeSpaces(w, indent*pre)
 373     w.WriteString(syntaxStyle + "\"\x1b[0m" + stringStyle)
 374     if !needsEscaping(s) {
 375         w.WriteString(s)
 376     } else {
 377         escapeString(w, s)
 378     }
 379     w.WriteString(syntaxStyle + "\"\x1b[0m")
 380     return nil
 381 }
 382 
 383 func escapeString(w *bufio.Writer, s string) {
 384     for _, r := range s {
 385         switch r {
 386         case '"', '\\':
 387             w.WriteByte('\\')
 388             w.WriteRune(r)
 389         case '\t':
 390             w.WriteByte('\\')
 391             w.WriteByte('t')
 392         case '\r':
 393             w.WriteByte('\\')
 394             w.WriteByte('r')
 395         case '\n':
 396             w.WriteByte('\\')
 397             w.WriteByte('n')
 398         default:
 399             w.WriteRune(r)
 400         }
 401     }
 402 }
     File: ./nn/nn.go
   1 /*
   2 The MIT License (MIT)
   3 
   4 Copyright (c) 2026 pacman64
   5 
   6 Permission is hereby granted, free of charge, to any person obtaining a copy of
   7 this software and associated documentation files (the "Software"), to deal
   8 in the Software without restriction, including without limitation the rights to
   9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
  10 of the Software, and to permit persons to whom the Software is furnished to do
  11 so, subject to the following conditions:
  12 
  13 The above copyright notice and this permission notice shall be included in all
  14 copies or substantial portions of the Software.
  15 
  16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  22 SOFTWARE.
  23 */
  24 
  25 package nn
  26 
  27 import (
  28     "bufio"
  29     "bytes"
  30     "errors"
  31     "io"
  32     "os"
  33     "strings"
  34 )
  35 
  36 const info = `
  37 nn [options...] [file...]
  38 
  39 
  40 Nice Numbers is an app which renders the UTF-8 text it's given to make long
  41 numbers much easier to read. It does so by alternating 3-digit groups which
  42 are colored using ANSI-codes with plain/unstyled 3-digit groups.
  43 
  44 Unlike the common practice of inserting commas between 3-digit groups, this
  45 trick doesn't widen the original text, keeping alignments across lines the
  46 same.
  47 
  48 Input is assumed to be UTF-8, and all CRLF byte-pairs are turned into line
  49 feeds.
  50 
  51 All (optional) leading options start with either single or double-dash,
  52 and most of them change the style/color used. Some of the options are,
  53 shown in their single-dash form:
  54 
  55     -h, -help    show this help message
  56 
  57     -b          use a blue color
  58     -blue       use a blue color
  59     -bold       bold-style digits
  60     -g          use a green color
  61     -gray       use a gray color (default)
  62     -green      use a green color
  63     -hi         use a highlighting/inverse style
  64     -m          use a magenta color
  65     -magenta    use a magenta color
  66     -o          use an orange color
  67     -orange     use an orange color
  68     -r          use a red color
  69     -red        use a red color
  70     -u          underline digits
  71     -underline  underline digits
  72 `
  73 
  74 func Main() {
  75     args := os.Args[1:]
  76 
  77     if len(args) > 0 {
  78         switch args[0] {
  79         case `-h`, `--h`, `-help`, `--help`:
  80             os.Stdout.WriteString(info[1:])
  81             return
  82         }
  83     }
  84 
  85     options := true
  86     if len(args) > 0 && args[0] == `--` {
  87         options = false
  88         args = args[1:]
  89     }
  90 
  91     style, _ := lookupStyle(`gray`)
  92 
  93     // if the first argument is 1 or 2 dashes followed by a supported
  94     // style-name, change the style used
  95     if options && len(args) > 0 && strings.HasPrefix(args[0], `-`) {
  96         name := args[0]
  97         name = strings.TrimPrefix(name, `-`)
  98         name = strings.TrimPrefix(name, `-`)
  99         args = args[1:]
 100 
 101         // check if the `dedashed` argument is a supported style-name
 102         if s, ok := lookupStyle(name); ok {
 103             style = s
 104         } else {
 105             os.Stderr.WriteString(`invalid style name `)
 106             os.Stderr.WriteString(name)
 107             os.Stderr.WriteString("\n")
 108             os.Exit(1)
 109         }
 110     }
 111 
 112     if err := run(os.Stdout, args, style); err != nil && err != io.EOF {
 113         os.Stderr.WriteString(err.Error())
 114         os.Stderr.WriteString("\n")
 115         os.Exit(1)
 116     }
 117 }
 118 
 119 func run(w io.Writer, args []string, style string) error {
 120     bw := bufio.NewWriter(w)
 121     defer bw.Flush()
 122 
 123     if len(args) == 0 {
 124         return restyle(bw, os.Stdin, style)
 125     }
 126 
 127     for _, name := range args {
 128         if err := handleFile(bw, name, style); err != nil {
 129             return err
 130         }
 131     }
 132     return nil
 133 }
 134 
 135 func handleFile(w *bufio.Writer, name string, style string) error {
 136     if name == `` || name == `-` {
 137         return restyle(w, os.Stdin, style)
 138     }
 139 
 140     f, err := os.Open(name)
 141     if err != nil {
 142         return errors.New(`can't read from file named "` + name + `"`)
 143     }
 144     defer f.Close()
 145 
 146     return restyle(w, f, style)
 147 }
 148 
 149 func restyle(w *bufio.Writer, r io.Reader, style string) error {
 150     const gb = 1024 * 1024 * 1024
 151     sc := bufio.NewScanner(r)
 152     sc.Buffer(nil, 8*gb)
 153 
 154     for i := 0; sc.Scan(); i++ {
 155         s := sc.Bytes()
 156         if i == 0 && bytes.HasPrefix(s, []byte{0xef, 0xbb, 0xbf}) {
 157             s = s[3:]
 158         }
 159 
 160         restyleLine(w, s, style)
 161         w.WriteByte('\n')
 162         if err := w.Flush(); err != nil {
 163             // a write error may be the consequence of stdout being closed,
 164             // perhaps by another app along a pipe
 165             return io.EOF
 166         }
 167     }
 168     return sc.Err()
 169 }
 170 
 171 func lookupStyle(name string) (style string, ok bool) {
 172     if alias, ok := styleAliases[name]; ok {
 173         name = alias
 174     }
 175 
 176     style, ok = styles[name]
 177     return style, ok
 178 }
 179 
 180 var styleAliases = map[string]string{
 181     `b`: `blue`,
 182     `g`: `green`,
 183     `m`: `magenta`,
 184     `o`: `orange`,
 185     `p`: `purple`,
 186     `r`: `red`,
 187     `u`: `underline`,
 188 
 189     `bolded`:      `bold`,
 190     `h`:           `inverse`,
 191     `hi`:          `inverse`,
 192     `highlight`:   `inverse`,
 193     `highlighted`: `inverse`,
 194     `hilite`:      `inverse`,
 195     `hilited`:     `inverse`,
 196     `inv`:         `inverse`,
 197     `invert`:      `inverse`,
 198     `inverted`:    `inverse`,
 199     `underlined`:  `underline`,
 200 
 201     `bb`: `blueback`,
 202     `bg`: `greenback`,
 203     `bm`: `magentaback`,
 204     `bo`: `orangeback`,
 205     `bp`: `purpleback`,
 206     `br`: `redback`,
 207 
 208     `gb`: `greenback`,
 209     `mb`: `magentaback`,
 210     `ob`: `orangeback`,
 211     `pb`: `purpleback`,
 212     `rb`: `redback`,
 213 
 214     `bblue`:    `blueback`,
 215     `bgray`:    `grayback`,
 216     `bgreen`:   `greenback`,
 217     `bmagenta`: `magentaback`,
 218     `borange`:  `orangeback`,
 219     `bpurple`:  `purpleback`,
 220     `bred`:     `redback`,
 221 
 222     `backblue`:    `blueback`,
 223     `backgray`:    `grayback`,
 224     `backgreen`:   `greenback`,
 225     `backmagenta`: `magentaback`,
 226     `backorange`:  `orangeback`,
 227     `backpurple`:  `purpleback`,
 228     `backred`:     `redback`,
 229 }
 230 
 231 // styles turns style-names into the ANSI-code sequences used for the
 232 // alternate groups of digits
 233 var styles = map[string]string{
 234     `blue`:      "\x1b[38;2;0;95;215m",
 235     `bold`:      "\x1b[1m",
 236     `gray`:      "\x1b[38;2;168;168;168m",
 237     `green`:     "\x1b[38;2;0;135;95m",
 238     `inverse`:   "\x1b[7m",
 239     `magenta`:   "\x1b[38;2;215;0;255m",
 240     `orange`:    "\x1b[38;2;215;95;0m",
 241     `plain`:     "\x1b[0m",
 242     `red`:       "\x1b[38;2;204;0;0m",
 243     `underline`: "\x1b[4m",
 244 
 245     // `blue`:      "\x1b[38;5;26m",
 246     // `bold`:      "\x1b[1m",
 247     // `gray`:      "\x1b[38;5;248m",
 248     // `green`:     "\x1b[38;5;29m",
 249     // `inverse`:   "\x1b[7m",
 250     // `magenta`:   "\x1b[38;5;99m",
 251     // `orange`:    "\x1b[38;5;166m",
 252     // `plain`:     "\x1b[0m",
 253     // `red`:       "\x1b[31m",
 254     // `underline`: "\x1b[4m",
 255 
 256     `blueback`:    "\x1b[48;2;0;95;215m\x1b[38;2;238;238;238m",
 257     `grayback`:    "\x1b[48;2;168;168;168m\x1b[38;2;238;238;238m",
 258     `greenback`:   "\x1b[48;2;0;135;95m\x1b[38;2;238;238;238m",
 259     `magentaback`: "\x1b[48;2;215;0;255m\x1b[38;2;238;238;238m",
 260     `orangeback`:  "\x1b[48;2;215;95;0m\x1b[38;2;238;238;238m",
 261     `purpleback`:  "\x1b[48;2;135;95;255m\x1b[38;2;238;238;238m",
 262     `redback`:     "\x1b[48;2;204;0;0m\x1b[38;2;238;238;238m",
 263 }
 264 
 265 // restyleLine renders the line given, using ANSI-styles to make any long
 266 // numbers in it more legible; this func doesn't emit a line-feed, which
 267 // is up to its caller
 268 func restyleLine(w *bufio.Writer, line []byte, style string) {
 269     for len(line) > 0 {
 270         i := indexDigit(line)
 271         if i < 0 {
 272             // no (more) digits to style for sure
 273             w.Write(line)
 274             return
 275         }
 276 
 277         // emit line before current digit-run
 278         w.Write(line[:i])
 279         // advance to the start of the current digit-run
 280         line = line[i:]
 281 
 282         // see where the digit-run ends
 283         j := indexNonDigit(line)
 284         if j < 0 {
 285             // the digit-run goes until the end
 286             restyleDigits(w, line, style)
 287             return
 288         }
 289 
 290         // emit styled digit-run
 291         restyleDigits(w, line[:j], style)
 292         // skip right past the end of the digit-run
 293         line = line[j:]
 294     }
 295 }
 296 
 297 // indexDigit finds the index of the first digit in a string, or -1 when the
 298 // string has no decimal digits
 299 func indexDigit(s []byte) int {
 300     for i := 0; i < len(s); i++ {
 301         switch s[i] {
 302         case '0', '1', '2', '3', '4', '5', '6', '7', '8', '9':
 303             return i
 304         }
 305     }
 306 
 307     // empty slice, or a slice without any digits
 308     return -1
 309 }
 310 
 311 // indexNonDigit finds the index of the first non-digit in a string, or -1
 312 // when the string is all decimal digits
 313 func indexNonDigit(s []byte) int {
 314     for i := 0; i < len(s); i++ {
 315         switch s[i] {
 316         case '0', '1', '2', '3', '4', '5', '6', '7', '8', '9':
 317             continue
 318         default:
 319             return i
 320         }
 321     }
 322 
 323     // empty slice, or a slice which only has digits
 324     return -1
 325 }
 326 
 327 // restyleDigits renders a run of digits as alternating styled/unstyled runs
 328 // of 3 digits, which greatly improves readability, and is the only purpose
 329 // of this app; string is assumed to be all decimal digits
 330 func restyleDigits(w *bufio.Writer, digits []byte, altStyle string) {
 331     if len(digits) < 4 {
 332         // digit sequence is short, so emit it as is
 333         w.Write(digits)
 334         return
 335     }
 336 
 337     // separate leading 0..2 digits which don't align with the 3-digit groups
 338     i := len(digits) % 3
 339     // emit leading digits unstyled, if there are any
 340     w.Write(digits[:i])
 341     // the rest is guaranteed to have a length which is a multiple of 3
 342     digits = digits[i:]
 343 
 344     // start by styling, unless there were no leading digits
 345     style := i != 0
 346 
 347     for len(digits) > 0 {
 348         if style {
 349             w.WriteString(altStyle)
 350             w.Write(digits[:3])
 351             w.Write([]byte{'\x1b', '[', '0', 'm'})
 352         } else {
 353             w.Write(digits[:3])
 354         }
 355 
 356         // advance to the next triple: the start of this func is supposed
 357         // to guarantee this step always works
 358         digits = digits[3:]
 359 
 360         // alternate between styled and unstyled 3-digit groups
 361         style = !style
 362     }
 363 }
     File: ./nn/nn_test.go
   1 /*
   2 The MIT License (MIT)
   3 
   4 Copyright (c) 2026 pacman64
   5 
   6 Permission is hereby granted, free of charge, to any person obtaining a copy of
   7 this software and associated documentation files (the "Software"), to deal
   8 in the Software without restriction, including without limitation the rights to
   9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
  10 of the Software, and to permit persons to whom the Software is furnished to do
  11 so, subject to the following conditions:
  12 
  13 The above copyright notice and this permission notice shall be included in all
  14 copies or substantial portions of the Software.
  15 
  16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  22 SOFTWARE.
  23 */
  24 
  25 package nn
  26 
  27 import (
  28     "bufio"
  29     "strings"
  30     "testing"
  31 )
  32 
  33 func TestRestyleLine(t *testing.T) {
  34     var (
  35         r = "\x1b[0m"
  36         d = string(styles[`gray`])
  37     )
  38 
  39     var tests = []struct {
  40         Input    string
  41         Expected string
  42     }{
  43         {``, ``},
  44         {`abc`, `abc`},
  45         {`  abc 123456 `, `  abc 123` + d + `456` + r + ` `},
  46         {`  123456789 text`, `  123` + d + `456` + r + `789 text`},
  47 
  48         {`0`, `0`},
  49         {`01`, `01`},
  50         {`012`, `012`},
  51         {`0123`, `0` + d + `123` + r},
  52         {`01234`, `01` + d + `234` + r},
  53         {`012345`, `012` + d + `345` + r},
  54         {`0123456`, `0` + d + `123` + r + `456`},
  55         {`01234567`, `01` + d + `234` + r + `567`},
  56         {`012345678`, `012` + d + `345` + r + `678`},
  57         {`0123456789`, `0` + d + `123` + r + `456` + d + `789` + r},
  58         {`01234567890`, `01` + d + `234` + r + `567` + d + `890` + r},
  59         {`012345678901`, `012` + d + `345` + r + `678` + d + `901` + r},
  60         {`0123456789012`, `0` + d + `123` + r + `456` + d + `789` + r + `012`},
  61 
  62         {`00321`, `00` + d + `321` + r},
  63         {`123.456789`, `123.` + `456` + d + `789` + r},
  64         {`123456.123456`, `123` + d + `456` + r + `.` + `123` + d + `456` + r},
  65     }
  66 
  67     for _, tc := range tests {
  68         t.Run(tc.Input, func(t *testing.T) {
  69             var b strings.Builder
  70             w := bufio.NewWriter(&b)
  71             restyleLine(w, []byte(tc.Input), d)
  72             w.Flush()
  73 
  74             if got := b.String(); got != tc.Expected {
  75                 t.Fatalf(`expected %q, but got %q instead`, tc.Expected, got)
  76             }
  77         })
  78     }
  79 }
     File: ./plain/plain.go
   1 /*
   2 The MIT License (MIT)
   3 
   4 Copyright (c) 2026 pacman64
   5 
   6 Permission is hereby granted, free of charge, to any person obtaining a copy of
   7 this software and associated documentation files (the "Software"), to deal
   8 in the Software without restriction, including without limitation the rights to
   9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
  10 of the Software, and to permit persons to whom the Software is furnished to do
  11 so, subject to the following conditions:
  12 
  13 The above copyright notice and this permission notice shall be included in all
  14 copies or substantial portions of the Software.
  15 
  16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  22 SOFTWARE.
  23 */
  24 
  25 package plain
  26 
  27 import (
  28     "bufio"
  29     "bytes"
  30     "errors"
  31     "io"
  32     "os"
  33 )
  34 
  35 const info = `
  36 plain [options...] [file...]
  37 
  38 
  39 Turn potentially ANSI-styled plain-text into actual plain-text.
  40 
  41 Input is assumed to be UTF-8, and all CRLF byte-pairs are turned into line
  42 feeds.
  43 
  44 All (optional) leading options start with either single or double-dash:
  45 
  46     -h, -help    show this help message
  47 `
  48 
  49 func Main() {
  50     buffered := false
  51     args := os.Args[1:]
  52 
  53     if len(args) > 0 {
  54         switch args[0] {
  55         case `-b`, `--b`, `-buffered`, `--buffered`:
  56             buffered = true
  57             args = args[1:]
  58 
  59         case `-h`, `--h`, `-help`, `--help`:
  60             os.Stdout.WriteString(info[1:])
  61             return
  62         }
  63     }
  64 
  65     if len(args) > 0 && args[0] == `--` {
  66         args = args[1:]
  67     }
  68 
  69     liveLines := !buffered
  70     if !buffered {
  71         if _, err := os.Stdout.Seek(0, io.SeekCurrent); err == nil {
  72             liveLines = false
  73         }
  74     }
  75 
  76     if err := run(os.Stdout, args, liveLines); err != nil && err != io.EOF {
  77         os.Stderr.WriteString(err.Error())
  78         os.Stderr.WriteString("\n")
  79         os.Exit(1)
  80     }
  81 }
  82 
  83 func run(w io.Writer, args []string, live bool) error {
  84     bw := bufio.NewWriter(w)
  85     defer bw.Flush()
  86 
  87     if len(args) == 0 {
  88         return plain(bw, os.Stdin, live)
  89     }
  90 
  91     for _, name := range args {
  92         if err := handleFile(bw, name, live); err != nil {
  93             return err
  94         }
  95     }
  96     return nil
  97 }
  98 
  99 func handleFile(w *bufio.Writer, name string, live bool) error {
 100     if name == `` || name == `-` {
 101         return plain(w, os.Stdin, live)
 102     }
 103 
 104     f, err := os.Open(name)
 105     if err != nil {
 106         return errors.New(`can't read from file named "` + name + `"`)
 107     }
 108     defer f.Close()
 109 
 110     return plain(w, f, live)
 111 }
 112 
 113 // indexEscapeSequence finds the first ANSI-style escape-sequence, which is
 114 // the multi-byte sequences starting with ESC[; the result is a pair of slice
 115 // indices which can be independently negative when either the start/end of
 116 // a sequence isn't found; given their fairly-common use, even the hyperlink
 117 // ESC]8 sequences are supported
 118 func indexEscapeSequence(s []byte) (int, int) {
 119     var prev byte
 120 
 121     for i, b := range s {
 122         if prev == '\x1b' && b == '[' {
 123             j := indexLetter(s[i+1:])
 124             if j < 0 {
 125                 return i, -1
 126             }
 127             return i - 1, i + 1 + j + 1
 128         }
 129 
 130         if prev == '\x1b' && b == ']' && i+1 < len(s) && s[i+1] == '8' {
 131             j := indexPair(s[i+1:], '\x1b', '\\')
 132             if j < 0 {
 133                 return i, -1
 134             }
 135             return i - 1, i + 1 + j + 2
 136         }
 137 
 138         prev = b
 139     }
 140 
 141     return -1, -1
 142 }
 143 
 144 func indexLetter(s []byte) int {
 145     for i, b := range s {
 146         upper := b &^ 32
 147         if 'A' <= upper && upper <= 'Z' {
 148             return i
 149         }
 150     }
 151 
 152     return -1
 153 }
 154 
 155 func indexPair(s []byte, x byte, y byte) int {
 156     var prev byte
 157 
 158     for i, b := range s {
 159         if prev == x && b == y && i > 0 {
 160             return i
 161         }
 162         prev = b
 163     }
 164 
 165     return -1
 166 }
 167 
 168 func plain(w *bufio.Writer, r io.Reader, live bool) error {
 169     const gb = 1024 * 1024 * 1024
 170     sc := bufio.NewScanner(r)
 171     sc.Buffer(nil, 8*gb)
 172 
 173     for i := 0; sc.Scan(); i++ {
 174         s := sc.Bytes()
 175         if i == 0 && bytes.HasPrefix(s, []byte{0xef, 0xbb, 0xbf}) {
 176             s = s[3:]
 177         }
 178 
 179         for line := s; len(line) > 0; {
 180             i, j := indexEscapeSequence(line)
 181             if i < 0 {
 182                 w.Write(line)
 183                 break
 184             }
 185             if j < 0 {
 186                 j = len(line)
 187             }
 188 
 189             if i > 0 {
 190                 w.Write(line[:i])
 191             }
 192 
 193             line = line[j:]
 194         }
 195 
 196         if w.WriteByte('\n') != nil {
 197             return io.EOF
 198         }
 199 
 200         if !live {
 201             continue
 202         }
 203 
 204         if err := w.Flush(); err != nil {
 205             return io.EOF
 206         }
 207     }
 208 
 209     return sc.Err()
 210 }
     File: ./primes/primes.go
   1 /*
   2 The MIT License (MIT)
   3 
   4 Copyright (c) 2026 pacman64
   5 
   6 Permission is hereby granted, free of charge, to any person obtaining a copy of
   7 this software and associated documentation files (the "Software"), to deal
   8 in the Software without restriction, including without limitation the rights to
   9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
  10 of the Software, and to permit persons to whom the Software is furnished to do
  11 so, subject to the following conditions:
  12 
  13 The above copyright notice and this permission notice shall be included in all
  14 copies or substantial portions of the Software.
  15 
  16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  22 SOFTWARE.
  23 */
  24 
  25 package primes
  26 
  27 import (
  28     "bufio"
  29     "math"
  30     "os"
  31     "strconv"
  32 )
  33 
  34 const info = `
  35 primes [options...] [count...]
  36 
  37 
  38 Show the first few prime numbers, starting from the lowest and showing one
  39 per line. When not given how many primes to find, the default is 1 million.
  40 
  41 All (optional) leading options start with either single or double-dash:
  42 
  43     -h, -help    show this help message
  44 `
  45 
  46 func Main() {
  47     howMany := 1_000_000
  48     if len(os.Args) > 1 {
  49         switch os.Args[1] {
  50         case `-h`, `--h`, `-help`, `--help`:
  51             os.Stdout.WriteString(info[1:])
  52             return
  53         }
  54 
  55         n, err := strconv.Atoi(os.Args[1])
  56         if err != nil {
  57             os.Stderr.WriteString(err.Error())
  58             os.Stderr.WriteString("\n")
  59             os.Exit(1)
  60         }
  61 
  62         if n < 0 {
  63             n = 0
  64         }
  65         howMany = n
  66     }
  67 
  68     primes(howMany)
  69 }
  70 
  71 func primes(left int) {
  72     bw := bufio.NewWriter(os.Stdout)
  73     defer bw.Flush()
  74 
  75     // 24 bytes are always enough for any 64-bit integer
  76     var buf [24]byte
  77 
  78     // 2 is the only even prime number
  79     if left > 0 {
  80         bw.WriteString("2\n")
  81         left--
  82     }
  83 
  84     for n := uint64(3); left > 0; n += 2 {
  85         if oddPrime(n) {
  86             bw.Write(strconv.AppendUint(buf[:0], n, 10))
  87             if err := bw.WriteByte('\n'); err != nil {
  88                 // assume errors come from closed stdout pipes
  89                 return
  90             }
  91             left--
  92         }
  93     }
  94 }
  95 
  96 // oddPrime assumes the number given to it is odd
  97 func oddPrime(n uint64) bool {
  98     max := uint64(math.Sqrt(float64(n)))
  99     for div := uint64(3); div <= max; div += 2 {
 100         if n%div == 0 {
 101             return false
 102         }
 103     }
 104     return true
 105 }
     File: ./realign/realign.go
   1 /*
   2 The MIT License (MIT)
   3 
   4 Copyright (c) 2026 pacman64
   5 
   6 Permission is hereby granted, free of charge, to any person obtaining a copy of
   7 this software and associated documentation files (the "Software"), to deal
   8 in the Software without restriction, including without limitation the rights to
   9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
  10 of the Software, and to permit persons to whom the Software is furnished to do
  11 so, subject to the following conditions:
  12 
  13 The above copyright notice and this permission notice shall be included in all
  14 copies or substantial portions of the Software.
  15 
  16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  22 SOFTWARE.
  23 */
  24 
  25 package realign
  26 
  27 import (
  28     "bufio"
  29     "errors"
  30     "io"
  31     "os"
  32     "strings"
  33     "unicode/utf8"
  34 )
  35 
  36 const info = `
  37 realign [options...] [filenames...]
  38 
  39 Realign all detected columns, right-aligning any detected numbers in any
  40 column. ANSI style-codes are also kept as given.
  41 
  42 The options are, available both in single and double-dash versions
  43 
  44     -h, -help           show this help message
  45     -m, -max-columns    use the row with the most items for the item-count
  46 `
  47 
  48 func Main() {
  49     maxCols := false
  50     args := os.Args[1:]
  51 
  52     for len(args) > 0 {
  53         if args[0] == `--` {
  54             args = args[1:]
  55             break
  56         }
  57 
  58         switch args[0] {
  59         case `-h`, `--h`, `-help`, `--help`:
  60             os.Stdout.WriteString(info[1:])
  61             return
  62 
  63         case
  64             `-m`, `--m`,
  65             `-maxcols`, `--maxcols`,
  66             `-max-columns`, `--max-columns`:
  67             maxCols = true
  68             args = args[1:]
  69             continue
  70         }
  71 
  72         break
  73     }
  74 
  75     if err := run(args, maxCols); err != nil {
  76         os.Stderr.WriteString(err.Error())
  77         os.Stderr.WriteString("\n")
  78         os.Exit(1)
  79     }
  80 }
  81 
  82 // table has all summary info gathered from the data, along with the row
  83 // themselves, stored as lines/strings
  84 type table struct {
  85     Columns int
  86 
  87     Rows []string
  88 
  89     MaxWidth []int
  90 
  91     MaxDotDecimals []int
  92 
  93     LoopItems func(s string, max int, t *table, f itemFunc)
  94 
  95     MaxColumns bool
  96 }
  97 
  98 type itemFunc func(i int, s string, t *table)
  99 
 100 func run(paths []string, maxCols bool) error {
 101     var res table
 102     res.MaxColumns = maxCols
 103 
 104     for _, p := range paths {
 105         if err := handleFile(&res, p); err != nil {
 106             return err
 107         }
 108     }
 109 
 110     if len(paths) == 0 {
 111         if err := handleReader(&res, os.Stdin); err != nil {
 112             return err
 113         }
 114     }
 115 
 116     bw := bufio.NewWriterSize(os.Stdout, 32*1024)
 117     defer bw.Flush()
 118     realign(bw, res)
 119     return nil
 120 }
 121 
 122 func handleFile(res *table, path string) error {
 123     f, err := os.Open(path)
 124     if err != nil {
 125         // on windows, file-not-found error messages may mention `CreateFile`,
 126         // even when trying to open files in read-only mode
 127         return errors.New(`can't open file named ` + path)
 128     }
 129     defer f.Close()
 130     return handleReader(res, f)
 131 }
 132 
 133 func handleReader(t *table, r io.Reader) error {
 134     const gb = 1024 * 1024 * 1024
 135     sc := bufio.NewScanner(r)
 136     sc.Buffer(nil, 8*gb)
 137 
 138     const maxInt = int(^uint(0) >> 1)
 139     maxCols := maxInt
 140 
 141     for i := 0; sc.Scan(); i++ {
 142         s := sc.Text()
 143         if i == 0 && strings.HasPrefix(s, "\xef\xbb\xbf") {
 144             s = s[3:]
 145         }
 146 
 147         if len(s) == 0 {
 148             if len(t.Rows) > 0 {
 149                 t.Rows = append(t.Rows, ``)
 150             }
 151             continue
 152         }
 153 
 154         t.Rows = append(t.Rows, s)
 155 
 156         if t.Columns == 0 {
 157             if t.LoopItems == nil {
 158                 if strings.IndexByte(s, '\t') >= 0 {
 159                     t.LoopItems = loopItemsTSV
 160                 } else {
 161                     t.LoopItems = loopItemsSSV
 162                 }
 163             }
 164 
 165             if !t.MaxColumns {
 166                 t.LoopItems(s, maxCols, t, updateColumnCount)
 167                 maxCols = t.Columns
 168             }
 169         }
 170 
 171         t.LoopItems(s, maxCols, t, updateItem)
 172     }
 173 
 174     return sc.Err()
 175 }
 176 
 177 func updateColumnCount(i int, s string, t *table) {
 178     t.Columns = i + 1
 179 }
 180 
 181 func updateItem(i int, s string, t *table) {
 182     // ensure column-info-slices have enough room
 183     if i >= len(t.MaxWidth) {
 184         // update column-count if in max-columns mode
 185         if t.MaxColumns {
 186             t.Columns = i + 1
 187         }
 188         t.MaxWidth = append(t.MaxWidth, 0)
 189         t.MaxDotDecimals = append(t.MaxDotDecimals, 0)
 190     }
 191 
 192     // keep track of widest rune-counts for each column
 193     w := countWidth(s)
 194     if t.MaxWidth[i] < w {
 195         t.MaxWidth[i] = w
 196     }
 197 
 198     // update stats for numeric items
 199     if isNumeric(s) {
 200         dd := countDotDecimals(s)
 201         if t.MaxDotDecimals[i] < dd {
 202             t.MaxDotDecimals[i] = dd
 203         }
 204     }
 205 }
 206 
 207 // loopItemsSSV loops over a line's items, allocation-free style; when given
 208 // empty strings, the callback func is never called
 209 func loopItemsSSV(s string, max int, t *table, f itemFunc) {
 210     s = trimTrailingSpaces(s)
 211 
 212     for i := 0; true; i++ {
 213         s = trimLeadingSpaces(s)
 214         if len(s) == 0 {
 215             return
 216         }
 217 
 218         if i+1 == max {
 219             f(i, s, t)
 220             return
 221         }
 222 
 223         j := strings.IndexByte(s, ' ')
 224         if j < 0 {
 225             f(i, s, t)
 226             return
 227         }
 228 
 229         f(i, s[:j], t)
 230         s = s[j+1:]
 231     }
 232 }
 233 
 234 func trimLeadingSpaces(s string) string {
 235     for len(s) > 0 && s[0] == ' ' {
 236         s = s[1:]
 237     }
 238     return s
 239 }
 240 
 241 func trimTrailingSpaces(s string) string {
 242     for len(s) > 0 && s[len(s)-1] == ' ' {
 243         s = s[:len(s)-1]
 244     }
 245     return s
 246 }
 247 
 248 // loopItemsTSV loops over a line's tab-separated items, allocation-free style;
 249 // when given empty strings, the callback func is never called
 250 func loopItemsTSV(s string, max int, t *table, f itemFunc) {
 251     if len(s) == 0 {
 252         return
 253     }
 254 
 255     for i := 0; true; i++ {
 256         if i+1 == max {
 257             f(i, s, t)
 258             return
 259         }
 260 
 261         j := strings.IndexByte(s, '\t')
 262         if j < 0 {
 263             f(i, s, t)
 264             return
 265         }
 266 
 267         f(i, s[:j], t)
 268         s = s[j+1:]
 269     }
 270 }
 271 
 272 func skipLeadingEscapeSequences(s string) string {
 273     for len(s) >= 2 {
 274         if s[0] != '\x1b' {
 275             return s
 276         }
 277 
 278         switch s[1] {
 279         case '[':
 280             s = skipSingleLeadingANSI(s[2:])
 281 
 282         case ']':
 283             if len(s) < 3 || s[2] != '8' {
 284                 return s
 285             }
 286             s = skipSingleLeadingOSC(s[3:])
 287 
 288         default:
 289             return s
 290         }
 291     }
 292 
 293     return s
 294 }
 295 
 296 func skipSingleLeadingANSI(s string) string {
 297     for len(s) > 0 {
 298         upper := s[0] &^ 32
 299         s = s[1:]
 300         if 'A' <= upper && upper <= 'Z' {
 301             break
 302         }
 303     }
 304 
 305     return s
 306 }
 307 
 308 func skipSingleLeadingOSC(s string) string {
 309     var prev byte
 310 
 311     for len(s) > 0 {
 312         b := s[0]
 313         s = s[1:]
 314         if prev == '\x1b' && b == '\\' {
 315             break
 316         }
 317         prev = b
 318     }
 319 
 320     return s
 321 }
 322 
 323 // isNumeric checks if a string is valid/useable as a number
 324 func isNumeric(s string) bool {
 325     if len(s) == 0 {
 326         return false
 327     }
 328 
 329     s = skipLeadingEscapeSequences(s)
 330     if len(s) > 0 && (s[0] == '+' || s[0] == '-') {
 331         s = s[1:]
 332     }
 333 
 334     s = skipLeadingEscapeSequences(s)
 335     if len(s) == 0 {
 336         return false
 337     }
 338     if s[0] == '.' {
 339         return isDigits(s[1:])
 340     }
 341 
 342     digits := 0
 343 
 344     for {
 345         s = skipLeadingEscapeSequences(s)
 346         if len(s) == 0 {
 347             break
 348         }
 349 
 350         if s[0] == '.' {
 351             return isDigits(s[1:])
 352         }
 353 
 354         if !('0' <= s[0] && s[0] <= '9') {
 355             return false
 356         }
 357 
 358         digits++
 359         s = s[1:]
 360     }
 361 
 362     s = skipLeadingEscapeSequences(s)
 363     return len(s) == 0 && digits > 0
 364 }
 365 
 366 func isDigits(s string) bool {
 367     if len(s) == 0 {
 368         return false
 369     }
 370 
 371     digits := 0
 372 
 373     for {
 374         s = skipLeadingEscapeSequences(s)
 375         if len(s) == 0 {
 376             break
 377         }
 378 
 379         if '0' <= s[0] && s[0] <= '9' {
 380             s = s[1:]
 381             digits++
 382         } else {
 383             return false
 384         }
 385     }
 386 
 387     s = skipLeadingEscapeSequences(s)
 388     return len(s) == 0 && digits > 0
 389 }
 390 
 391 // countDecimals counts decimal digits from the string given, assuming it
 392 // represents a valid/useable float64, when parsed
 393 func countDecimals(s string) int {
 394     dot := strings.IndexByte(s, '.')
 395     if dot < 0 {
 396         return 0
 397     }
 398 
 399     decs := 0
 400     s = s[dot+1:]
 401 
 402     for len(s) > 0 {
 403         s = skipLeadingEscapeSequences(s)
 404         if len(s) == 0 {
 405             break
 406         }
 407         if '0' <= s[0] && s[0] <= '9' {
 408             decs++
 409         }
 410         s = s[1:]
 411     }
 412 
 413     return decs
 414 }
 415 
 416 // countDotDecimals is like func countDecimals, but this one also includes
 417 // the dot, when any decimals are present, else the count stays at 0
 418 func countDotDecimals(s string) int {
 419     decs := countDecimals(s)
 420     if decs > 0 {
 421         return decs + 1
 422     }
 423     return decs
 424 }
 425 
 426 func countWidth(s string) int {
 427     width := 0
 428 
 429     for len(s) > 0 {
 430         i, j := indexEscapeSequence(s)
 431         if i < 0 {
 432             break
 433         }
 434         if j < 0 {
 435             j = len(s)
 436         }
 437 
 438         width += utf8.RuneCountInString(s[:i])
 439         s = s[j:]
 440     }
 441 
 442     // count trailing/all runes in strings which don't end with ANSI-sequences
 443     width += utf8.RuneCountInString(s)
 444     return width
 445 }
 446 
 447 // indexEscapeSequence finds the first ANSI-style escape-sequence, which is
 448 // the multi-byte sequences starting with ESC[; the result is a pair of slice
 449 // indices which can be independently negative when either the start/end of
 450 // a sequence isn't found; given their fairly-common use, even the hyperlink
 451 // ESC]8 sequences are supported
 452 func indexEscapeSequence(s string) (int, int) {
 453     var prev byte
 454 
 455     for i := range s {
 456         b := s[i]
 457 
 458         if prev == '\x1b' && b == '[' {
 459             j := indexLetter(s[i+1:])
 460             if j < 0 {
 461                 return i, -1
 462             }
 463             return i - 1, i + 1 + j + 1
 464         }
 465 
 466         if prev == '\x1b' && b == ']' && i+1 < len(s) && s[i+1] == '8' {
 467             j := indexPair(s[i+1:], '\x1b', '\\')
 468             if j < 0 {
 469                 return i, -1
 470             }
 471             return i - 1, i + 1 + j + 2
 472         }
 473 
 474         prev = b
 475     }
 476 
 477     return -1, -1
 478 }
 479 
 480 func indexLetter(s string) int {
 481     for i, b := range s {
 482         upper := b &^ 32
 483         if 'A' <= upper && upper <= 'Z' {
 484             return i
 485         }
 486     }
 487 
 488     return -1
 489 }
 490 
 491 func indexPair(s string, x byte, y byte) int {
 492     var prev byte
 493 
 494     for i := range s {
 495         b := s[i]
 496         if prev == x && b == y && i > 0 {
 497             return i
 498         }
 499         prev = b
 500     }
 501 
 502     return -1
 503 }
 504 
 505 func realign(w *bufio.Writer, t table) {
 506     due := 0
 507     showItem := func(i int, s string, t *table) {
 508         if i > 0 {
 509             due += 2
 510         }
 511 
 512         if isNumeric(s) {
 513             dd := countDotDecimals(s)
 514             rpad := t.MaxDotDecimals[i] - dd
 515             width := countWidth(s)
 516             lpad := t.MaxWidth[i] - (width + rpad) + due
 517             writeSpaces(w, lpad)
 518             w.WriteString(s)
 519             due = rpad
 520             return
 521         }
 522 
 523         writeSpaces(w, due)
 524         w.WriteString(s)
 525         due = t.MaxWidth[i] - countWidth(s)
 526     }
 527 
 528     for _, line := range t.Rows {
 529         due = 0
 530         if len(line) > 0 {
 531             t.LoopItems(line, t.Columns, &t, showItem)
 532         }
 533         if w.WriteByte('\n') != nil {
 534             break
 535         }
 536     }
 537 }
 538 
 539 // writeSpaces does what it says, minimizing calls to write-like funcs
 540 func writeSpaces(w *bufio.Writer, n int) {
 541     const spaces = `                                `
 542     if n < 1 {
 543         return
 544     }
 545 
 546     for n >= len(spaces) {
 547         w.WriteString(spaces)
 548         n -= len(spaces)
 549     }
 550     w.WriteString(spaces[:n])
 551 }
     File: ./realign/realign_test.go
   1 /*
   2 The MIT License (MIT)
   3 
   4 Copyright (c) 2026 pacman64
   5 
   6 Permission is hereby granted, free of charge, to any person obtaining a copy of
   7 this software and associated documentation files (the "Software"), to deal
   8 in the Software without restriction, including without limitation the rights to
   9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
  10 of the Software, and to permit persons to whom the Software is furnished to do
  11 so, subject to the following conditions:
  12 
  13 The above copyright notice and this permission notice shall be included in all
  14 copies or substantial portions of the Software.
  15 
  16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  22 SOFTWARE.
  23 */
  24 
  25 package realign
  26 
  27 import "testing"
  28 
  29 func TestCountWidth(t *testing.T) {
  30     var tests = []struct {
  31         name     string
  32         input    string
  33         expected int
  34     }{
  35         {`empty`, ``, 0},
  36         {`empty ANSI`, "\x1b[38;5;0;0;0m\x1b[0m", 0},
  37         {`simple plain`, `abc def`, 7},
  38         {`unicode plain`, `abc●def`, 7},
  39         {`simple ANSI`, "abc \x1b[7mde\x1b[0mf", 7},
  40         {`unicode ANSI`, "abc●\x1b[7mde\x1b[0mf", 7},
  41     }
  42 
  43     for _, tc := range tests {
  44         t.Run(tc.name, func(t *testing.T) {
  45             got := countWidth(tc.input)
  46             if got != tc.expected {
  47                 t.Errorf("expected width %d, got %d instead", tc.expected, got)
  48             }
  49         })
  50     }
  51 }
     File: ./squeeze/squeeze.go
   1 /*
   2 The MIT License (MIT)
   3 
   4 Copyright (c) 2026 pacman64
   5 
   6 Permission is hereby granted, free of charge, to any person obtaining a copy of
   7 this software and associated documentation files (the "Software"), to deal
   8 in the Software without restriction, including without limitation the rights to
   9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
  10 of the Software, and to permit persons to whom the Software is furnished to do
  11 so, subject to the following conditions:
  12 
  13 The above copyright notice and this permission notice shall be included in all
  14 copies or substantial portions of the Software.
  15 
  16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  22 SOFTWARE.
  23 */
  24 
  25 package squeeze
  26 
  27 import (
  28     "bufio"
  29     "bytes"
  30     "errors"
  31     "io"
  32     "os"
  33 )
  34 
  35 const info = `
  36 squeeze [filenames...]
  37 
  38 Ignore leading/trailing spaces (and carriage-returns) on lines, also turning
  39 all runs of multiple consecutive spaces into single spaces. Spaces around
  40 tabs are ignored as well.
  41 `
  42 
  43 func Main() {
  44     buffered := false
  45     args := os.Args[1:]
  46 
  47     if len(args) > 0 {
  48         switch args[0] {
  49         case `-b`, `--b`, `-buffered`, `--buffered`:
  50             buffered = true
  51             args = args[1:]
  52 
  53         case `-h`, `--h`, `-help`, `--help`:
  54             os.Stdout.WriteString(info[1:])
  55             return
  56         }
  57     }
  58 
  59     if len(args) > 0 && args[0] == `--` {
  60         args = args[1:]
  61     }
  62 
  63     liveLines := !buffered
  64     if !buffered {
  65         if _, err := os.Stdout.Seek(0, io.SeekCurrent); err == nil {
  66             liveLines = false
  67         }
  68     }
  69 
  70     if err := run(os.Stdout, args, liveLines); err != nil && err != io.EOF {
  71         os.Stderr.WriteString(err.Error())
  72         os.Stderr.WriteString("\n")
  73         os.Exit(1)
  74     }
  75 }
  76 
  77 func run(w io.Writer, args []string, live bool) error {
  78     bw := bufio.NewWriter(w)
  79     defer bw.Flush()
  80 
  81     if len(args) == 0 {
  82         return squeeze(bw, os.Stdin, live)
  83     }
  84 
  85     for _, name := range args {
  86         if err := handleFile(bw, name, live); err != nil {
  87             return err
  88         }
  89     }
  90     return nil
  91 }
  92 
  93 func handleFile(w *bufio.Writer, name string, live bool) error {
  94     if name == `` || name == `-` {
  95         return squeeze(w, os.Stdin, live)
  96     }
  97 
  98     f, err := os.Open(name)
  99     if err != nil {
 100         return errors.New(`can't read from file named "` + name + `"`)
 101     }
 102     defer f.Close()
 103 
 104     return squeeze(w, f, live)
 105 }
 106 
 107 func squeeze(w *bufio.Writer, r io.Reader, live bool) error {
 108     const gb = 1024 * 1024 * 1024
 109     sc := bufio.NewScanner(r)
 110     sc.Buffer(nil, 8*gb)
 111 
 112     for i := 0; sc.Scan(); i++ {
 113         s := sc.Bytes()
 114         if i == 0 && bytes.HasPrefix(s, []byte{0xef, 0xbb, 0xbf}) {
 115             s = s[3:]
 116         }
 117 
 118         writeSqueezed(w, s)
 119         if w.WriteByte('\n') != nil {
 120             return io.EOF
 121         }
 122 
 123         if !live {
 124             continue
 125         }
 126 
 127         if err := w.Flush(); err != nil {
 128             return io.EOF
 129         }
 130     }
 131 
 132     return sc.Err()
 133 }
 134 
 135 func writeSqueezed(w *bufio.Writer, s []byte) {
 136     // ignore leading spaces
 137     for len(s) > 0 && s[0] == ' ' {
 138         s = s[1:]
 139     }
 140 
 141     // ignore trailing spaces
 142     for len(s) > 0 && s[len(s)-1] == ' ' {
 143         s = s[:len(s)-1]
 144     }
 145 
 146     space := false
 147 
 148     for len(s) > 0 {
 149         switch s[0] {
 150         case ' ':
 151             s = s[1:]
 152             space = true
 153 
 154         case '\t':
 155             s = s[1:]
 156             space = false
 157             for len(s) > 0 && s[0] == ' ' {
 158                 s = s[1:]
 159             }
 160             w.WriteByte('\t')
 161 
 162         default:
 163             if space {
 164                 w.WriteByte(' ')
 165                 space = false
 166             }
 167             w.WriteByte(s[0])
 168             s = s[1:]
 169         }
 170     }
 171 }
     File: ./tcatl/tcatl.go
   1 /*
   2 The MIT License (MIT)
   3 
   4 Copyright (c) 2026 pacman64
   5 
   6 Permission is hereby granted, free of charge, to any person obtaining a copy of
   7 this software and associated documentation files (the "Software"), to deal
   8 in the Software without restriction, including without limitation the rights to
   9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
  10 of the Software, and to permit persons to whom the Software is furnished to do
  11 so, subject to the following conditions:
  12 
  13 The above copyright notice and this permission notice shall be included in all
  14 copies or substantial portions of the Software.
  15 
  16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  22 SOFTWARE.
  23 */
  24 
  25 package tcatl
  26 
  27 import (
  28     "bufio"
  29     "bytes"
  30     "errors"
  31     "io"
  32     "os"
  33     "unicode/utf8"
  34 )
  35 
  36 const info = `
  37 tcatl [options...] [file...]
  38 
  39 
  40 Title and Concatenate lines emits lines from all the named sources given,
  41 preceding each file's contents with its name, using an ANSI reverse style.
  42 
  43 The name "-" stands for the standard input. When no names are given, the
  44 standard input is used by default.
  45 
  46 All (optional) leading options start with either single or double-dash:
  47 
  48     -h, -help    show this help message
  49 `
  50 
  51 func Main() {
  52     args := os.Args[1:]
  53     if len(args) > 0 {
  54         switch args[0] {
  55         case `-h`, `--h`, `-help`, `--help`:
  56             os.Stdout.WriteString(info[1:])
  57             return
  58         }
  59     }
  60 
  61     if len(args) > 0 && args[0] == `--` {
  62         args = args[1:]
  63     }
  64 
  65     if err := run(os.Stdout, args); err != nil {
  66         os.Stderr.WriteString(err.Error())
  67         os.Stderr.WriteString("\n")
  68         os.Exit(1)
  69     }
  70 }
  71 
  72 func run(w io.Writer, args []string) error {
  73     bw := bufio.NewWriter(w)
  74     defer bw.Flush()
  75 
  76     if len(args) == 0 {
  77         return tcatl(bw, os.Stdin, `-`)
  78     }
  79 
  80     for _, name := range args {
  81         if err := handleFile(bw, name); err != nil {
  82             return err
  83         }
  84     }
  85     return nil
  86 }
  87 
  88 func handleFile(w *bufio.Writer, name string) error {
  89     if name == `` || name == `-` {
  90         return tcatl(w, os.Stdin, `-`)
  91     }
  92 
  93     f, err := os.Open(name)
  94     if err != nil {
  95         return errors.New(`can't read from file named "` + name + `"`)
  96     }
  97     defer f.Close()
  98 
  99     return tcatl(w, f, name)
 100 }
 101 
 102 func tcatl(w *bufio.Writer, r io.Reader, name string) error {
 103     w.WriteString("\x1b[7m")
 104     w.WriteString(name)
 105     writeSpaces(w, 80-utf8.RuneCountInString(name))
 106     w.WriteString("\x1b[0m\n")
 107     if err := w.Flush(); err != nil {
 108         // a write error may be the consequence of stdout being closed,
 109         // perhaps by another app along a pipe
 110         return io.EOF
 111     }
 112 
 113     if catlFast(w, r) != nil {
 114         return io.EOF
 115     }
 116     return nil
 117 }
 118 
 119 func catlFast(w *bufio.Writer, r io.Reader) error {
 120     var buf [32 * 1024]byte
 121     var last byte = '\n'
 122 
 123     for i := 0; true; i++ {
 124         n, err := r.Read(buf[:])
 125         if n > 0 && err == io.EOF {
 126             err = nil
 127         }
 128         if err == io.EOF {
 129             if last != '\n' {
 130                 w.WriteByte('\n')
 131             }
 132             return nil
 133         }
 134 
 135         if err != nil {
 136             return err
 137         }
 138 
 139         chunk := buf[:n]
 140         if i == 0 && bytes.HasPrefix(chunk, []byte{0xef, 0xbb, 0xbf}) {
 141             chunk = chunk[3:]
 142         }
 143 
 144         if len(chunk) >= 1 {
 145             if _, err := w.Write(chunk); err != nil {
 146                 return io.EOF
 147             }
 148             last = chunk[len(chunk)-1]
 149         }
 150     }
 151 
 152     return nil
 153 }
 154 
 155 // writeSpaces bulk-emits the number of spaces given
 156 func writeSpaces(w *bufio.Writer, n int) {
 157     const spaces = `                                `
 158     for ; n > len(spaces); n -= len(spaces) {
 159         w.WriteString(spaces)
 160     }
 161     if n > 0 {
 162         w.WriteString(spaces[:n])
 163     }
 164 }
     File: ./teletype/teletype.go
   1 /*
   2 The MIT License (MIT)
   3 
   4 Copyright (c) 2026 pacman64
   5 
   6 Permission is hereby granted, free of charge, to any person obtaining a copy of
   7 this software and associated documentation files (the "Software"), to deal
   8 in the Software without restriction, including without limitation the rights to
   9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
  10 of the Software, and to permit persons to whom the Software is furnished to do
  11 so, subject to the following conditions:
  12 
  13 The above copyright notice and this permission notice shall be included in all
  14 copies or substantial portions of the Software.
  15 
  16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  22 SOFTWARE.
  23 */
  24 
  25 package teletype
  26 
  27 import (
  28     "bufio"
  29     "errors"
  30     "io"
  31     "os"
  32     "strings"
  33     "time"
  34     "unicode/utf8"
  35 )
  36 
  37 const info = `
  38 teletype [options...] [files...]
  39 
  40 Simulate the cadence of old-fashioned teletype machines, by slowing down
  41 the output of ASCII/UTF-8 symbols from the inputs given.
  42 
  43 All (optional) leading options start with either single or double-dash:
  44 
  45     -h, -help    show this help message
  46 `
  47 
  48 func Main() {
  49     args := os.Args[1:]
  50 
  51     if len(args) > 0 {
  52         switch args[0] {
  53         case `-h`, `--h`, `-help`, `--help`:
  54             os.Stdout.WriteString(info[1:])
  55             return
  56         }
  57     }
  58 
  59     if len(args) > 0 && args[0] == `--` {
  60         args = args[1:]
  61     }
  62 
  63     if err := run(os.Stdout, args); err != nil && err != io.EOF {
  64         os.Stderr.WriteString(err.Error())
  65         os.Stderr.WriteString("\n")
  66         os.Exit(1)
  67     }
  68 }
  69 
  70 func run(w io.Writer, args []string) error {
  71     dashes := 0
  72     for _, name := range args {
  73         if name == `-` {
  74             dashes++
  75         }
  76         if dashes > 1 {
  77             return errors.New(`can't read stdin (dash) more than once`)
  78         }
  79     }
  80 
  81     if len(args) == 0 {
  82         return teletype(w, os.Stdin)
  83     }
  84 
  85     for _, name := range args {
  86         if name == `-` {
  87             if err := teletype(w, os.Stdin); err != nil {
  88                 return err
  89             }
  90             continue
  91         }
  92 
  93         if err := handleFile(w, name); err != nil {
  94             return err
  95         }
  96     }
  97     return nil
  98 }
  99 
 100 func handleFile(w io.Writer, name string) error {
 101     if name == `` || name == `-` {
 102         return teletype(w, os.Stdin)
 103     }
 104 
 105     f, err := os.Open(name)
 106     if err != nil {
 107         return errors.New(`can't read from file named "` + name + `"`)
 108     }
 109     defer f.Close()
 110 
 111     return teletype(w, f)
 112 }
 113 
 114 func teletype(w io.Writer, r io.Reader) error {
 115     const gb = 1024 * 1024 * 1024
 116     sc := bufio.NewScanner(r)
 117     sc.Buffer(nil, 8*gb)
 118 
 119     for i := 0; sc.Scan(); i++ {
 120         s := sc.Text()
 121         if i == 0 && strings.HasPrefix(s, "\xef\xbb\xbf") {
 122             s = s[3:]
 123         }
 124 
 125         var buf [4]byte
 126         for _, r := range s {
 127             time.Sleep(15 * time.Millisecond)
 128             if _, err := w.Write(utf8.AppendRune(buf[:0], r)); err != nil {
 129                 return io.EOF
 130             }
 131         }
 132 
 133         time.Sleep(750 * time.Millisecond)
 134         if _, err := w.Write([]byte{'\n'}); err != nil {
 135             return io.EOF
 136         }
 137     }
 138 
 139     return sc.Err()
 140 }
     File: ./utfate/utfate.go
   1 /*
   2 The MIT License (MIT)
   3 
   4 Copyright (c) 2026 pacman64
   5 
   6 Permission is hereby granted, free of charge, to any person obtaining a copy of
   7 this software and associated documentation files (the "Software"), to deal
   8 in the Software without restriction, including without limitation the rights to
   9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
  10 of the Software, and to permit persons to whom the Software is furnished to do
  11 so, subject to the following conditions:
  12 
  13 The above copyright notice and this permission notice shall be included in all
  14 copies or substantial portions of the Software.
  15 
  16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  22 SOFTWARE.
  23 */
  24 
  25 package utfate
  26 
  27 import (
  28     "bufio"
  29     "bytes"
  30     "encoding/binary"
  31     "errors"
  32     "io"
  33     "os"
  34     "unicode"
  35     "unicode/utf16"
  36 )
  37 
  38 const info = `
  39 utfate [options...] [file...]
  40 
  41 This app turns plain-text input into UTF-8. Supported input formats are
  42 
  43     - ASCII
  44     - UTF-8
  45     - UTF-8 with a leading BOM
  46     - UTF-16 BE
  47     - UTF-16 LE
  48     - UTF-32 BE
  49     - UTF-32 LE
  50 
  51 All (optional) leading options start with either single or double-dash:
  52 
  53     -h, -help    show this help message
  54 `
  55 
  56 func Main() {
  57     if len(os.Args) > 1 {
  58         switch os.Args[1] {
  59         case `-h`, `--h`, `-help`, `--help`:
  60             os.Stdout.WriteString(info[1:])
  61             return
  62         }
  63     }
  64 
  65     if err := run(os.Stdout, os.Args[1:]); err != nil && err != io.EOF {
  66         os.Stderr.WriteString(err.Error())
  67         os.Stderr.WriteString("\n")
  68         os.Exit(1)
  69     }
  70 }
  71 
  72 func run(w io.Writer, args []string) error {
  73     bw := bufio.NewWriter(w)
  74     defer bw.Flush()
  75 
  76     for _, path := range args {
  77         if err := handleFile(bw, path); err != nil {
  78             return err
  79         }
  80     }
  81 
  82     if len(args) == 0 {
  83         return utfate(bw, os.Stdin)
  84     }
  85     return nil
  86 }
  87 
  88 func handleFile(w *bufio.Writer, name string) error {
  89     if name == `-` {
  90         return utfate(w, os.Stdin)
  91     }
  92 
  93     f, err := os.Open(name)
  94     if err != nil {
  95         return errors.New(`can't read from file named "` + name + `"`)
  96     }
  97     defer f.Close()
  98 
  99     return utfate(w, f)
 100 }
 101 
 102 func utfate(w io.Writer, r io.Reader) error {
 103     br := bufio.NewReader(r)
 104     bw := bufio.NewWriter(w)
 105     defer bw.Flush()
 106 
 107     lead, err := br.Peek(4)
 108     if err != nil && err != io.EOF {
 109         return err
 110     }
 111 
 112     if bytes.HasPrefix(lead, []byte{'\x00', '\x00', '\xfe', '\xff'}) {
 113         br.Discard(4)
 114         return utf32toUTF8(bw, br, binary.BigEndian)
 115     }
 116 
 117     if bytes.HasPrefix(lead, []byte{'\xff', '\xfe', '\x00', '\x00'}) {
 118         br.Discard(4)
 119         return utf32toUTF8(bw, br, binary.LittleEndian)
 120     }
 121 
 122     if bytes.HasPrefix(lead, []byte{'\xfe', '\xff'}) {
 123         br.Discard(2)
 124         return utf16toUTF8(bw, br, readBytePairBE)
 125     }
 126 
 127     if bytes.HasPrefix(lead, []byte{'\xff', '\xfe'}) {
 128         br.Discard(2)
 129         return utf16toUTF8(bw, br, readBytePairLE)
 130     }
 131 
 132     if bytes.HasPrefix(lead, []byte{'\xef', '\xbb', '\xbf'}) {
 133         br.Discard(3)
 134         return handleUTF8(bw, br)
 135     }
 136 
 137     return handleUTF8(bw, br)
 138 }
 139 
 140 func handleUTF8(w *bufio.Writer, r *bufio.Reader) error {
 141     for {
 142         c, _, err := r.ReadRune()
 143         if c == unicode.ReplacementChar {
 144             return errors.New(`invalid UTF-8 stream`)
 145         }
 146         if err == io.EOF {
 147             return nil
 148         }
 149         if err != nil {
 150             return err
 151         }
 152 
 153         if _, err := w.WriteRune(c); err != nil {
 154             return io.EOF
 155         }
 156     }
 157 }
 158 
 159 // fancyHandleUTF8 is kept only for reference, as its attempts at being clever
 160 // don't seem to speed things up much when given ASCII input
 161 func fancyHandleUTF8(w *bufio.Writer, r *bufio.Reader) error {
 162     lookahead := 1
 163     maxAhead := r.Size() / 2
 164 
 165     for {
 166         // look ahead to check for ASCII runs
 167         ahead, err := r.Peek(lookahead)
 168         if err == io.EOF {
 169             return nil
 170         }
 171         if err != nil {
 172             return err
 173         }
 174 
 175         // copy leading ASCII runs
 176         n := leadASCII(ahead)
 177         if n > 0 {
 178             w.Write(ahead[:n])
 179             r.Discard(n)
 180         }
 181 
 182         // adapt lookahead size
 183         if n == len(ahead) && lookahead < maxAhead {
 184             lookahead *= 2
 185         } else if lookahead > 1 {
 186             lookahead /= 2
 187         }
 188 
 189         if n == len(ahead) {
 190             continue
 191         }
 192 
 193         c, _, err := r.ReadRune()
 194         if c == unicode.ReplacementChar {
 195             return errors.New(`invalid UTF-8 stream`)
 196         }
 197         if err == io.EOF {
 198             return nil
 199         }
 200         if err != nil {
 201             return err
 202         }
 203 
 204         if _, err := w.WriteRune(c); err != nil {
 205             return io.EOF
 206         }
 207     }
 208 }
 209 
 210 // leadASCII is used by func fancyHandleUTF8
 211 func leadASCII(buf []byte) int {
 212     for i, b := range buf {
 213         if b >= 128 {
 214             return i
 215         }
 216     }
 217     return len(buf)
 218 }
 219 
 220 // readPairFunc narrows source-code lines below
 221 type readPairFunc func(*bufio.Reader) (byte, byte, error)
 222 
 223 // utf16toUTF8 handles UTF-16 inputs for func utfate
 224 func utf16toUTF8(w *bufio.Writer, r *bufio.Reader, read2 readPairFunc) error {
 225     for {
 226         a, b, err := read2(r)
 227         if err == io.EOF {
 228             return nil
 229         }
 230         if err != nil {
 231             return err
 232         }
 233 
 234         c := rune(256*int(a) + int(b))
 235         if utf16.IsSurrogate(c) {
 236             a, b, err := read2(r)
 237             if err == io.EOF {
 238                 return nil
 239             }
 240             if err != nil {
 241                 return err
 242             }
 243 
 244             next := rune(256*int(a) + int(b))
 245             c = utf16.DecodeRune(c, next)
 246         }
 247 
 248         if _, err := w.WriteRune(c); err != nil {
 249             return io.EOF
 250         }
 251     }
 252 }
 253 
 254 // readBytePairBE gets you a pair of bytes in big-endian (original) order
 255 func readBytePairBE(br *bufio.Reader) (byte, byte, error) {
 256     a, err := br.ReadByte()
 257     if err != nil {
 258         return a, 0, err
 259     }
 260 
 261     b, err := br.ReadByte()
 262     return a, b, err
 263 }
 264 
 265 // readBytePairLE gets you a pair of bytes in little-endian order
 266 func readBytePairLE(br *bufio.Reader) (byte, byte, error) {
 267     a, b, err := readBytePairBE(br)
 268     return b, a, err
 269 }
 270 
 271 // utf32toUTF8 handles UTF-32 inputs for func utfate
 272 func utf32toUTF8(w *bufio.Writer, r *bufio.Reader, o binary.ByteOrder) error {
 273     var n uint32
 274     for {
 275         err := binary.Read(r, o, &n)
 276         if err == io.EOF {
 277             return nil
 278         }
 279         if err != nil {
 280             return err
 281         }
 282 
 283         if _, err := w.WriteRune(rune(n)); err != nil {
 284             return io.EOF
 285         }
 286     }
 287 }
     File: ./waveout/bytes.go
   1 /*
   2 The MIT License (MIT)
   3 
   4 Copyright (c) 2026 pacman64
   5 
   6 Permission is hereby granted, free of charge, to any person obtaining a copy of
   7 this software and associated documentation files (the "Software"), to deal
   8 in the Software without restriction, including without limitation the rights to
   9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
  10 of the Software, and to permit persons to whom the Software is furnished to do
  11 so, subject to the following conditions:
  12 
  13 The above copyright notice and this permission notice shall be included in all
  14 copies or substantial portions of the Software.
  15 
  16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  22 SOFTWARE.
  23 */
  24 
  25 package waveout
  26 
  27 import (
  28     "encoding/binary"
  29     "fmt"
  30     "io"
  31     "math"
  32 )
  33 
  34 // aiff header format
  35 //
  36 // http://paulbourke.net/dataformats/audio/
  37 //
  38 // wav header format
  39 //
  40 // http://soundfile.sapp.org/doc/WaveFormat/
  41 // http://www-mmsp.ece.mcgill.ca/Documents/AudioFormats/WAVE/WAVE.html
  42 // https://docs.fileformat.com/audio/wav/
  43 
  44 const (
  45     // maxInt helps convert float64 values into int16 ones
  46     maxInt = 1<<15 - 1
  47 
  48     // wavIntPCM declares integer PCM sound-data in a wav header
  49     wavIntPCM = 1
  50 
  51     // wavFloatPCM declares floating-point PCM sound-data in a wav header
  52     wavFloatPCM = 3
  53 )
  54 
  55 // emitInt16LE writes a 16-bit signed integer in little-endian byte order
  56 func emitInt16LE(w io.Writer, f float64) {
  57     // binary.Write(w, binary.LittleEndian, int16(maxInt*f))
  58     var buf [2]byte
  59     binary.LittleEndian.PutUint16(buf[:2], uint16(int16(maxInt*f)))
  60     w.Write(buf[:2])
  61 }
  62 
  63 // emitFloat32LE writes a 32-bit float in little-endian byte order
  64 func emitFloat32LE(w io.Writer, f float64) {
  65     var buf [4]byte
  66     binary.LittleEndian.PutUint32(buf[:4], math.Float32bits(float32(f)))
  67     w.Write(buf[:4])
  68 }
  69 
  70 // emitInt16BE writes a 16-bit signed integer in big-endian byte order
  71 func emitInt16BE(w io.Writer, f float64) {
  72     // binary.Write(w, binary.BigEndian, int16(maxInt*f))
  73     var buf [2]byte
  74     binary.BigEndian.PutUint16(buf[:2], uint16(int16(maxInt*f)))
  75     w.Write(buf[:2])
  76 }
  77 
  78 // emitFloat32BE writes a 32-bit float in big-endian byte order
  79 func emitFloat32BE(w io.Writer, f float64) {
  80     var buf [4]byte
  81     binary.BigEndian.PutUint32(buf[:4], math.Float32bits(float32(f)))
  82     w.Write(buf[:4])
  83 }
  84 
  85 // wavSettings is an item in the type2wavSettings table
  86 type wavSettings struct {
  87     Type          byte
  88     BitsPerSample byte
  89 }
  90 
  91 // type2wavSettings encodes values used when emitting wav headers
  92 var type2wavSettings = map[sampleFormat]wavSettings{
  93     int16LE:   {wavIntPCM, 16},
  94     float32LE: {wavFloatPCM, 32},
  95 }
  96 
  97 // emitWaveHeader writes the start of a valid .wav file: since it also starts
  98 // the wav data section and emits its size, you only need to write all samples
  99 // after calling this func
 100 func emitWaveHeader(w io.Writer, cfg outputConfig) error {
 101     const fmtChunkSize = 16
 102     duration := cfg.MaxTime
 103     numchan := uint32(len(cfg.Scripts))
 104     sampleRate := cfg.SampleRate
 105 
 106     ws, ok := type2wavSettings[cfg.Samples]
 107     if !ok {
 108         const fs = `internal error: invalid output-format code %d`
 109         return fmt.Errorf(fs, cfg.Samples)
 110     }
 111     kind := uint16(ws.Type)
 112     bps := uint32(ws.BitsPerSample)
 113 
 114     // byte rate
 115     br := sampleRate * bps * numchan / 8
 116     // data size in bytes
 117     dataSize := uint32(float64(br) * duration)
 118     // total file size
 119     totalSize := uint32(dataSize + 44)
 120 
 121     // general descriptor
 122     w.Write([]byte(`RIFF`))
 123     binary.Write(w, binary.LittleEndian, uint32(totalSize))
 124     w.Write([]byte(`WAVE`))
 125 
 126     // fmt chunk
 127     w.Write([]byte(`fmt `))
 128     binary.Write(w, binary.LittleEndian, uint32(fmtChunkSize))
 129     binary.Write(w, binary.LittleEndian, uint16(kind))
 130     binary.Write(w, binary.LittleEndian, uint16(numchan))
 131     binary.Write(w, binary.LittleEndian, uint32(sampleRate))
 132     binary.Write(w, binary.LittleEndian, uint32(br))
 133     binary.Write(w, binary.LittleEndian, uint16(bps*numchan/8))
 134     binary.Write(w, binary.LittleEndian, uint16(bps))
 135 
 136     // start data chunk
 137     w.Write([]byte(`data`))
 138     binary.Write(w, binary.LittleEndian, uint32(dataSize))
 139     return nil
 140 }
     File: ./waveout/config.go
   1 /*
   2 The MIT License (MIT)
   3 
   4 Copyright (c) 2026 pacman64
   5 
   6 Permission is hereby granted, free of charge, to any person obtaining a copy of
   7 this software and associated documentation files (the "Software"), to deal
   8 in the Software without restriction, including without limitation the rights to
   9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
  10 of the Software, and to permit persons to whom the Software is furnished to do
  11 so, subject to the following conditions:
  12 
  13 The above copyright notice and this permission notice shall be included in all
  14 copies or substantial portions of the Software.
  15 
  16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  22 SOFTWARE.
  23 */
  24 
  25 package waveout
  26 
  27 import (
  28     "errors"
  29     "fmt"
  30     "math"
  31     "os"
  32     "strconv"
  33     "strings"
  34     "time"
  35 
  36     "../../pkg/timeplus"
  37 )
  38 
  39 // config has all the parsed cmd-line options
  40 type config struct {
  41     // Scripts has the source codes of all scripts for all channels
  42     Scripts []string
  43 
  44     // To is the output format
  45     To string
  46 
  47     // MaxTime is the play duration of the resulting sound
  48     MaxTime float64
  49 
  50     // SampleRate is the number of samples per second for all channels
  51     SampleRate uint
  52 }
  53 
  54 // parseFlags is the constructor for type config
  55 func parseFlags(usage string) (config, error) {
  56     cfg := config{
  57         To:         `wav`,
  58         MaxTime:    math.NaN(),
  59         SampleRate: 48_000,
  60     }
  61 
  62     args := os.Args[1:]
  63     if len(args) == 0 {
  64         fmt.Fprint(os.Stderr, usage)
  65         os.Exit(0)
  66     }
  67 
  68     for _, s := range args {
  69         switch s {
  70         case `help`, `-h`, `--h`, `-help`, `--help`:
  71             fmt.Fprint(os.Stdout, usage)
  72             os.Exit(0)
  73         }
  74 
  75         err := cfg.handleArg(s)
  76         if err != nil {
  77             return cfg, err
  78         }
  79     }
  80 
  81     if math.IsNaN(cfg.MaxTime) {
  82         cfg.MaxTime = 1
  83     }
  84     if cfg.MaxTime < 0 {
  85         const fs = `error: given negative duration %f`
  86         return cfg, fmt.Errorf(fs, cfg.MaxTime)
  87     }
  88     return cfg, nil
  89 }
  90 
  91 func (c *config) handleArg(s string) error {
  92     switch s {
  93     case `44.1k`, `44.1K`:
  94         c.SampleRate = 44_100
  95         return nil
  96 
  97     case `48k`, `48K`:
  98         c.SampleRate = 48_000
  99         return nil
 100 
 101     case `dat`, `DAT`:
 102         c.SampleRate = 48_000
 103         return nil
 104 
 105     case `cd`, `cda`, `CD`, `CDA`:
 106         c.SampleRate = 44_100
 107         return nil
 108     }
 109 
 110     // handle output-format names and their aliases
 111     if kind, ok := name2type[s]; ok {
 112         c.To = kind
 113         return nil
 114     }
 115 
 116     // handle time formats, except when they're pure numbers
 117     if math.IsNaN(c.MaxTime) {
 118         dur, derr := timeplus.ParseDuration(s)
 119         if derr == nil {
 120             c.MaxTime = float64(dur) / float64(time.Second)
 121             return nil
 122         }
 123     }
 124 
 125     // handle sample-rate, given either in hertz or kilohertz
 126     lc := strings.ToLower(s)
 127     if strings.HasSuffix(lc, `khz`) {
 128         lc = strings.TrimSuffix(lc, `khz`)
 129         khz, err := strconv.ParseFloat(lc, 64)
 130         if err != nil || isBadNumber(khz) || khz <= 0 {
 131             const fs = `invalid sample-rate frequency %q`
 132             return fmt.Errorf(fs, s)
 133         }
 134         c.SampleRate = uint(1_000 * khz)
 135         return nil
 136     } else if strings.HasSuffix(lc, `hz`) {
 137         lc = strings.TrimSuffix(lc, `hz`)
 138         hz, err := strconv.ParseUint(lc, 10, 64)
 139         if err != nil {
 140             const fs = `invalid sample-rate frequency %q`
 141             return fmt.Errorf(fs, s)
 142         }
 143         c.SampleRate = uint(hz)
 144         return nil
 145     }
 146 
 147     c.Scripts = append(c.Scripts, s)
 148     return nil
 149 }
 150 
 151 type encoding byte
 152 type headerType byte
 153 type sampleFormat byte
 154 
 155 const (
 156     directEncoding encoding = 1
 157     uriEncoding    encoding = 2
 158 
 159     noHeader  headerType = 1
 160     wavHeader headerType = 2
 161 
 162     int16BE   sampleFormat = 1
 163     int16LE   sampleFormat = 2
 164     float32BE sampleFormat = 3
 165     float32LE sampleFormat = 4
 166 )
 167 
 168 // name2type normalizes keys used for type2settings
 169 var name2type = map[string]string{
 170     `datauri`:  `data-uri`,
 171     `dataurl`:  `data-uri`,
 172     `data-uri`: `data-uri`,
 173     `data-url`: `data-uri`,
 174     `uri`:      `data-uri`,
 175     `url`:      `data-uri`,
 176 
 177     `raw`:     `raw`,
 178     `raw16be`: `raw16be`,
 179     `raw16le`: `raw16le`,
 180     `raw32be`: `raw32be`,
 181     `raw32le`: `raw32le`,
 182 
 183     `audio/x-wav`:  `wave-16`,
 184     `audio/x-wave`: `wave-16`,
 185     `wav`:          `wave-16`,
 186     `wave`:         `wave-16`,
 187     `wav16`:        `wave-16`,
 188     `wave16`:       `wave-16`,
 189     `wav-16`:       `wave-16`,
 190     `wave-16`:      `wave-16`,
 191     `x-wav`:        `wave-16`,
 192     `x-wave`:       `wave-16`,
 193 
 194     `wav16uri`:    `wave-16-uri`,
 195     `wave-16-uri`: `wave-16-uri`,
 196 
 197     `wav32uri`:    `wave-32-uri`,
 198     `wave-32-uri`: `wave-32-uri`,
 199 
 200     `wav32`:   `wave-32`,
 201     `wave32`:  `wave-32`,
 202     `wav-32`:  `wave-32`,
 203     `wave-32`: `wave-32`,
 204 }
 205 
 206 // outputSettings are format-specific settings which are controlled by the
 207 // output-format option on the cmd-line
 208 type outputSettings struct {
 209     Encoding encoding
 210     Header   headerType
 211     Samples  sampleFormat
 212 }
 213 
 214 // type2settings translates output-format names into the specific settings
 215 // these imply
 216 var type2settings = map[string]outputSettings{
 217     ``: {directEncoding, wavHeader, int16LE},
 218 
 219     `data-uri`:    {uriEncoding, wavHeader, int16LE},
 220     `raw`:         {directEncoding, noHeader, int16LE},
 221     `raw16be`:     {directEncoding, noHeader, int16BE},
 222     `raw16le`:     {directEncoding, noHeader, int16LE},
 223     `wave-16`:     {directEncoding, wavHeader, int16LE},
 224     `wave-16-uri`: {uriEncoding, wavHeader, int16LE},
 225 
 226     `raw32be`:     {directEncoding, noHeader, float32BE},
 227     `raw32le`:     {directEncoding, noHeader, float32LE},
 228     `wave-32`:     {directEncoding, wavHeader, float32LE},
 229     `wave-32-uri`: {uriEncoding, wavHeader, float32LE},
 230 }
 231 
 232 // outputConfig has all the info the core of this app needs to make sound
 233 type outputConfig struct {
 234     // Scripts has the source codes of all scripts for all channels
 235     Scripts []string
 236 
 237     // MaxTime is the play duration of the resulting sound
 238     MaxTime float64
 239 
 240     // SampleRate is the number of samples per second for all channels
 241     SampleRate uint32
 242 
 243     // all the configuration details needed to emit output
 244     outputSettings
 245 }
 246 
 247 // newOutputConfig is the constructor for type outputConfig, translating the
 248 // cmd-line info from type config
 249 func newOutputConfig(cfg config) (outputConfig, error) {
 250     oc := outputConfig{
 251         Scripts:    cfg.Scripts,
 252         MaxTime:    cfg.MaxTime,
 253         SampleRate: uint32(cfg.SampleRate),
 254     }
 255 
 256     if len(oc.Scripts) == 0 {
 257         return oc, errors.New(`no formulas given`)
 258     }
 259 
 260     outFmt := strings.ToLower(strings.TrimSpace(cfg.To))
 261     if alias, ok := name2type[outFmt]; ok {
 262         outFmt = alias
 263     }
 264 
 265     set, ok := type2settings[outFmt]
 266     if !ok {
 267         const fs = `unsupported output format %q`
 268         return oc, fmt.Errorf(fs, cfg.To)
 269     }
 270 
 271     oc.outputSettings = set
 272     return oc, nil
 273 }
 274 
 275 // mimeType gives the format's corresponding MIME type, or an empty string
 276 // if the type isn't URI-encodable
 277 func (oc outputConfig) mimeType() string {
 278     if oc.Header == wavHeader {
 279         return `audio/x-wav`
 280     }
 281     return ``
 282 }
 283 
 284 func isBadNumber(f float64) bool {
 285     return math.IsNaN(f) || math.IsInf(f, 0)
 286 }
     File: ./waveout/config_test.go
   1 /*
   2 The MIT License (MIT)
   3 
   4 Copyright (c) 2026 pacman64
   5 
   6 Permission is hereby granted, free of charge, to any person obtaining a copy of
   7 this software and associated documentation files (the "Software"), to deal
   8 in the Software without restriction, including without limitation the rights to
   9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
  10 of the Software, and to permit persons to whom the Software is furnished to do
  11 so, subject to the following conditions:
  12 
  13 The above copyright notice and this permission notice shall be included in all
  14 copies or substantial portions of the Software.
  15 
  16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  22 SOFTWARE.
  23 */
  24 
  25 package waveout
  26 
  27 import "testing"
  28 
  29 func TestTables(t *testing.T) {
  30     for _, kind := range name2type {
  31         // ensure all canonical format values are aliased to themselves
  32         if _, ok := name2type[kind]; !ok {
  33             const fs = `canonical format %q not set`
  34             t.Fatalf(fs, kind)
  35         }
  36     }
  37 
  38     for k, kind := range name2type {
  39         // ensure each setting leads somewhere
  40         set, ok := type2settings[kind]
  41         if !ok {
  42             const fs = `type alias %q has no setting for it`
  43             t.Fatalf(fs, k)
  44         }
  45 
  46         // ensure all encoding codes are valid in the next step
  47         switch set.Encoding {
  48         case directEncoding, uriEncoding:
  49             // ok
  50         default:
  51             const fs = `invalid encoding (code %d) from settings for %q`
  52             t.Fatalf(fs, set.Encoding, kind)
  53         }
  54 
  55         // also ensure all header codes are valid
  56         switch set.Header {
  57         case noHeader, wavHeader:
  58             // ok
  59         default:
  60             const fs = `invalid header (code %d) from settings for %q`
  61             t.Fatalf(fs, set.Header, kind)
  62         }
  63 
  64         // as well as all sample-format codes
  65         switch set.Samples {
  66         case int16BE, int16LE, float32BE, float32LE:
  67             // ok
  68         default:
  69             const fs = `invalid sample-format (code %d) from settings for %q`
  70             t.Fatalf(fs, set.Header, kind)
  71         }
  72     }
  73 }
     File: ./waveout/info.txt
   1 waveout [options...] [duration...] [formulas...]
   2 
   3 
   4 This app emits wave-sound binary data using the script(s) given. Scripts
   5 give you the float64-related functionality you may expect, from numeric
   6 operations to several math functions. When given 1 formula, the result is
   7 mono; when given 2 formulas (left and right), the result is stereo, and so
   8 on.
   9 
  10 Output is always uncompressed audio: `waveout` can emit that as is, or as a
  11 base64-encoded data-URI, which you can use as a `src` attribute value in an
  12 HTML audio tag. Output duration is 1 second by default, but you can change
  13 that too by using a recognized time format.
  14 
  15 The first recognized time format is the familiar hh:mm:ss, where the hours
  16 are optional, and where seconds can have a decimal part after it.
  17 
  18 The second recognized time format uses 1-letter shortcuts instead of colons
  19 for each time component, each of which is optional: `h` stands for hour, `m`
  20 for minutes, and `s` for seconds.
  21 
  22 
  23 Output Formats
  24 
  25              encoding  header  samples  endian   more info
  26 
  27     wav      direct    wave    int16    little   default format
  28 
  29     wav16    direct    wave    int16    little   alias for `wav`
  30     wav32    direct    wave    float32  little
  31     uri      data-URI  wave    int16    little   MIME type is audio/x-wav
  32 
  33     raw      direct    none    int16    little
  34     raw16le  direct    none    int16    little   alias for `raw`
  35     raw32le  direct    none    float32  little
  36     raw16be  direct    none    int16    big
  37     raw32be  direct    none    float32  big
  38 
  39 
  40 Concrete Examples
  41 
  42 # low-tones commonly used in club music as beats
  43 waveout 2s 'sin(10 * tau * exp(-20 * u)) * exp(-2 * u)' > club-beats.wav
  44 
  45 # 1 minute and 5 seconds of static-like random noise
  46 waveout 1m5s 'rand()' > random-noise.wav
  47 
  48 # many bell-like clicks in quick succession; can be a cellphone's ringtone
  49 waveout 'sin(2048 * tau * t) * exp(-50 * (t%0.1))' > ringtone.wav
  50 
  51 # similar to the door-opening sound from a dsc powerseries home alarm
  52 waveout 'sin(4096 * tau * t) * exp(-10 * (t%0.1))' > home-alarm.wav
  53 
  54 # watch your ears: quickly increases frequency up to 2khz
  55 waveout 'sin(2_000 * t * tau * t)' > frequency-sweep.wav
  56 
  57 # 1-second 400hz test tone
  58 waveout 'sin(400 * tau * t)' > test-tone-400.wav
  59 
  60 # 2s of a 440hz test tone, also called an A440 sound
  61 waveout 2s 'sin(440 * tau * t)' > a440.wav
  62 
  63 # 1s 400hz test tone with sudden volume drop at the end, to avoid clip
  64 waveout 'sin(400 * tau * t) * min(1, exp(-100*(t-0.9)))' > nice-tone.wav
  65 
  66 # old ringtone used in north america
  67 waveout '0.5*sin(350 * tau * t) + 0.5*sin(450 * tau * t)' > na-ringtone.wav
  68 
  69 # 20 seconds of periodic pings
  70 waveout 20s 'sin(800 * tau * u) * exp(-20 * u)' > pings.wav
  71 
  72 # 2 seconds of a european-style dial-tone
  73 waveout 2s '(sin(350 * tau * t) + sin(450 * tau * t)) / 2' > dial-tone.wav
  74 
  75 # 4 seconds of a north-american-style busy-phone signal
  76 waveout 4s '(u < 0.5) * (sin(480*tau * t) + sin(620*tau * t)) / 2' > na-busy.wav
  77 
  78 # hit the 51st key on a synthetic piano-like instrument
  79 waveout 'sin(tau * 440 * 2**((51 - 49)/12) * t) * exp(-10*u)' > piano-key.wav
  80 
  81 # hit of a synthetic snare-like sound
  82 waveout 'random() * exp(-10 * t)' > synth-snare.wav
  83 
  84 # a stereotypical `laser` sound
  85 waveout 'sin(100 * tau * exp(-40 * t))' > laser.wav
     File: ./waveout/main.go
   1 /*
   2 The MIT License (MIT)
   3 
   4 Copyright (c) 2026 pacman64
   5 
   6 Permission is hereby granted, free of charge, to any person obtaining a copy of
   7 this software and associated documentation files (the "Software"), to deal
   8 in the Software without restriction, including without limitation the rights to
   9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
  10 of the Software, and to permit persons to whom the Software is furnished to do
  11 so, subject to the following conditions:
  12 
  13 The above copyright notice and this permission notice shall be included in all
  14 copies or substantial portions of the Software.
  15 
  16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  22 SOFTWARE.
  23 */
  24 
  25 package waveout
  26 
  27 import (
  28     "bufio"
  29     "encoding/base64"
  30     "errors"
  31     "fmt"
  32     "io"
  33     "os"
  34 
  35     _ "embed"
  36 )
  37 
  38 //go:embed info.txt
  39 var usage string
  40 
  41 func Main() {
  42     cfg, err := parseFlags(usage)
  43     if err != nil {
  44         fmt.Fprintln(os.Stderr, err.Error())
  45         os.Exit(1)
  46     }
  47 
  48     oc, err := newOutputConfig(cfg)
  49     if err != nil {
  50         fmt.Fprintln(os.Stderr, err.Error())
  51         os.Exit(1)
  52     }
  53 
  54     addDetermFuncs()
  55 
  56     if err := run(oc); err != nil {
  57         fmt.Fprintln(os.Stderr, err.Error())
  58         os.Exit(1)
  59     }
  60 }
  61 
  62 func run(cfg outputConfig) error {
  63     // f, err := os.Create(`waveout.prof`)
  64     // if err != nil {
  65     //  return err
  66     // }
  67     // defer f.Close()
  68 
  69     // pprof.StartCPUProfile(f)
  70     // defer pprof.StopCPUProfile()
  71 
  72     w := bufio.NewWriterSize(os.Stdout, 64*1024)
  73     defer w.Flush()
  74 
  75     switch cfg.Encoding {
  76     case directEncoding:
  77         return runDirect(w, cfg)
  78 
  79     case uriEncoding:
  80         mtype := cfg.mimeType()
  81         if mtype == `` {
  82             return errors.New(`internal error: no MIME type`)
  83         }
  84 
  85         fmt.Fprintf(w, `data:%s;base64,`, mtype)
  86         enc := base64.NewEncoder(base64.StdEncoding, w)
  87         defer enc.Close()
  88         return runDirect(enc, cfg)
  89 
  90     default:
  91         const fs = `internal error: wrong output-encoding code %d`
  92         return fmt.Errorf(fs, cfg.Encoding)
  93     }
  94 }
  95 
  96 // type2emitter chooses sample-emitter funcs from the format given
  97 var type2emitter = map[sampleFormat]func(io.Writer, float64){
  98     int16LE:   emitInt16LE,
  99     int16BE:   emitInt16BE,
 100     float32LE: emitFloat32LE,
 101     float32BE: emitFloat32BE,
 102 }
 103 
 104 // runDirect emits sound-data bytes: this func can be called with writers
 105 // which keep bytes as given, or with re-encoders, such as base64 writers
 106 func runDirect(w io.Writer, cfg outputConfig) error {
 107     switch cfg.Header {
 108     case noHeader:
 109         // do nothing, while avoiding error
 110 
 111     case wavHeader:
 112         emitWaveHeader(w, cfg)
 113 
 114     default:
 115         const fs = `internal error: wrong header code %d`
 116         return fmt.Errorf(fs, cfg.Header)
 117     }
 118 
 119     emitter, ok := type2emitter[cfg.Samples]
 120     if !ok {
 121         const fs = `internal error: wrong output-format code %d`
 122         return fmt.Errorf(fs, cfg.Samples)
 123     }
 124 
 125     if len(cfg.Scripts) == 1 {
 126         return emitMono(w, cfg, emitter)
 127     }
 128     return emit(w, cfg, emitter)
 129 }
     File: ./waveout/scripts.go
   1 /*
   2 The MIT License (MIT)
   3 
   4 Copyright (c) 2026 pacman64
   5 
   6 Permission is hereby granted, free of charge, to any person obtaining a copy of
   7 this software and associated documentation files (the "Software"), to deal
   8 in the Software without restriction, including without limitation the rights to
   9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
  10 of the Software, and to permit persons to whom the Software is furnished to do
  11 so, subject to the following conditions:
  12 
  13 The above copyright notice and this permission notice shall be included in all
  14 copies or substantial portions of the Software.
  15 
  16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  22 SOFTWARE.
  23 */
  24 
  25 package waveout
  26 
  27 import (
  28     "io"
  29     "math"
  30     "math/rand"
  31     "time"
  32 
  33     "../fmscripts"
  34 )
  35 
  36 // makeDefs makes extra funcs and values available to scripts
  37 func makeDefs(cfg outputConfig) map[string]any {
  38     // copy extra built-in funcs
  39     defs := make(map[string]any, len(extras)+6+5)
  40     for k, v := range extras {
  41         defs[k] = v
  42     }
  43 
  44     // add extra variables
  45     defs[`t`] = 0.0
  46     defs[`u`] = 0.0
  47     defs[`d`] = cfg.MaxTime
  48     defs[`dur`] = cfg.MaxTime
  49     defs[`duration`] = cfg.MaxTime
  50     defs[`end`] = cfg.MaxTime
  51 
  52     // add pseudo-random funcs
  53 
  54     seed := time.Now().UnixNano()
  55     r := rand.New(rand.NewSource(seed))
  56 
  57     rand := func() float64 {
  58         return random01(r)
  59     }
  60     randomf := func() float64 {
  61         return random(r)
  62     }
  63     rexpf := func(scale float64) float64 {
  64         return rexp(r, scale)
  65     }
  66     rnormf := func(mu, sigma float64) float64 {
  67         return rnorm(r, mu, sigma)
  68     }
  69 
  70     defs[`rand`] = rand
  71     defs[`rand01`] = rand
  72     defs[`random`] = randomf
  73     defs[`rexp`] = rexpf
  74     defs[`rnorm`] = rnormf
  75 
  76     return defs
  77 }
  78 
  79 type emitFunc = func(io.Writer, float64)
  80 
  81 // emit runs the formulas given to emit all wave samples
  82 func emit(w io.Writer, cfg outputConfig, emit emitFunc) error {
  83     var c fmscripts.Compiler
  84     defs := makeDefs(cfg)
  85 
  86     programs := make([]fmscripts.Program, 0, len(cfg.Scripts))
  87     tvars := make([]*float64, 0, len(cfg.Scripts))
  88     uvars := make([]*float64, 0, len(cfg.Scripts))
  89 
  90     for _, s := range cfg.Scripts {
  91         p, err := c.Compile(s, defs)
  92         if err != nil {
  93             return err
  94         }
  95         programs = append(programs, p)
  96         t, _ := p.Get(`t`)
  97         u, _ := p.Get(`u`)
  98         tvars = append(tvars, t)
  99         uvars = append(uvars, u)
 100     }
 101 
 102     dt := 1.0 / float64(cfg.SampleRate)
 103     end := cfg.MaxTime
 104 
 105     for i := 0.0; true; i++ {
 106         now := dt * i
 107         if now >= end {
 108             return nil
 109         }
 110 
 111         _, u := math.Modf(now)
 112 
 113         for j, p := range programs {
 114             *tvars[j] = now
 115             *uvars[j] = u
 116             emit(w, p.Run())
 117         }
 118     }
 119     return nil
 120 }
 121 
 122 // emitMono runs the formula given to emit all single-channel wave samples
 123 func emitMono(w io.Writer, cfg outputConfig, emit emitFunc) error {
 124     var c fmscripts.Compiler
 125     mono, err := c.Compile(cfg.Scripts[0], makeDefs(cfg))
 126     if err != nil {
 127         return err
 128     }
 129 
 130     t, _ := mono.Get(`t`)
 131     u, needsu := mono.Get(`u`)
 132 
 133     dt := 1.0 / float64(cfg.SampleRate)
 134     end := cfg.MaxTime
 135 
 136     // update variable `u` only if script uses it: this can speed things
 137     // up considerably when that variable isn't used
 138     if needsu {
 139         for i := 0.0; true; i++ {
 140             now := dt * i
 141             if now >= end {
 142                 return nil
 143             }
 144 
 145             *t = now
 146             _, *u = math.Modf(now)
 147             emit(w, mono.Run())
 148         }
 149         return nil
 150     }
 151 
 152     for i := 0.0; true; i++ {
 153         now := dt * i
 154         if now >= end {
 155             return nil
 156         }
 157 
 158         *t = now
 159         emit(w, mono.Run())
 160     }
 161     return nil
 162 }
 163 
 164 // // emitStereo runs the formula given to emit all 2-channel wave samples
 165 // func emitStereo(w io.Writer, cfg outputConfig, emit emitFunc) error {
 166 //  defs := makeDefs(cfg)
 167 //  var c fmscripts.Compiler
 168 //  left, err := c.Compile(cfg.Scripts[0], defs)
 169 //  if err != nil {
 170 //      return err
 171 //  }
 172 //  right, err := c.Compile(cfg.Scripts[1], defs)
 173 //  if err != nil {
 174 //      return err
 175 //  }
 176 
 177 //  lt, _ := left.Get(`t`)
 178 //  rt, _ := right.Get(`t`)
 179 //  lu, luok := left.Get(`u`)
 180 //  ru, ruok := right.Get(`u`)
 181 
 182 //  dt := 1.0 / float64(cfg.SampleRate)
 183 //  end := cfg.MaxTime
 184 
 185 //  // update variable `u` only if script uses it: this can speed things
 186 //  // up considerably when that variable isn't used
 187 //  updateu := func(float64) {}
 188 //  if luok || ruok {
 189 //      updateu = func(now float64) {
 190 //          _, u := math.Modf(now)
 191 //          *lu = u
 192 //          *ru = u
 193 //      }
 194 //  }
 195 
 196 //  for i := 0.0; true; i++ {
 197 //      now := dt * i
 198 //      if now >= end {
 199 //          return nil
 200 //      }
 201 
 202 //      *rt = now
 203 //      *lt = now
 204 //      updateu(now)
 205 
 206 //      // most software seems to emit stereo pairs in left-right order
 207 //      emit(w, left.Run())
 208 //      emit(w, right.Run())
 209 //  }
 210 
 211 //  // keep the compiler happy
 212 //  return nil
 213 // }
     File: ./waveout/stdlib.go
   1 /*
   2 The MIT License (MIT)
   3 
   4 Copyright (c) 2026 pacman64
   5 
   6 Permission is hereby granted, free of charge, to any person obtaining a copy of
   7 this software and associated documentation files (the "Software"), to deal
   8 in the Software without restriction, including without limitation the rights to
   9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
  10 of the Software, and to permit persons to whom the Software is furnished to do
  11 so, subject to the following conditions:
  12 
  13 The above copyright notice and this permission notice shall be included in all
  14 copies or substantial portions of the Software.
  15 
  16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  22 SOFTWARE.
  23 */
  24 
  25 package waveout
  26 
  27 import (
  28     "math"
  29     "math/rand"
  30 
  31     "../fmscripts"
  32     "../mathplus"
  33 )
  34 
  35 // tau is exactly 1 loop around a circle, which is handy to turn frequencies
  36 // into trigonometric angles, since they're measured in radians
  37 const tau = 2 * math.Pi
  38 
  39 // extras has funcs beyond what the script built-ins offer: those built-ins
  40 // are for general math calculations, while these are specific for sound
  41 // effects, other sound-related calculations, or to make pseudo-random values
  42 var extras = map[string]any{
  43     `hihat`: hihat,
  44 }
  45 
  46 // addDetermFuncs does what it says, ensuring these funcs are optimizable when
  47 // they're given all-constant expressions as inputs
  48 func addDetermFuncs() {
  49     fmscripts.DefineDetFuncs(map[string]any{
  50         `ascale`:       mathplus.AnchoredScale,
  51         `awrap`:        mathplus.AnchoredWrap,
  52         `clamp`:        mathplus.Clamp,
  53         `epa`:          mathplus.Epanechnikov,
  54         `epanechnikov`: mathplus.Epanechnikov,
  55         `fract`:        mathplus.Fract,
  56         `gauss`:        mathplus.Gauss,
  57         `horner`:       mathplus.Polyval,
  58         `logistic`:     mathplus.Logistic,
  59         `mix`:          mathplus.Mix,
  60         `polyval`:      mathplus.Polyval,
  61         `scale`:        mathplus.Scale,
  62         `sign`:         mathplus.Sign,
  63         `sinc`:         mathplus.Sinc,
  64         `smoothstep`:   mathplus.SmoothStep,
  65         `step`:         mathplus.Step,
  66         `tricube`:      mathplus.Tricube,
  67         `unwrap`:       mathplus.Unwrap,
  68         `wrap`:         mathplus.Wrap,
  69 
  70         `drop`:       dropsince,
  71         `dropfrom`:   dropsince,
  72         `dropoff`:    dropsince,
  73         `dropsince`:  dropsince,
  74         `kick`:       kick,
  75         `kicklow`:    kicklow,
  76         `piano`:      piano,
  77         `pianokey`:   piano,
  78         `pickval`:    pickval,
  79         `pickvalue`:  pickval,
  80         `sched`:      schedule,
  81         `schedule`:   schedule,
  82         `timeval`:    timeval,
  83         `timevalues`: timeval,
  84     })
  85 }
  86 
  87 // random01 returns a random value in 0 .. 1
  88 func random01(r *rand.Rand) float64 {
  89     return r.Float64()
  90 }
  91 
  92 // random returns a random value in -1 .. +1
  93 func random(r *rand.Rand) float64 {
  94     return (2 * r.Float64()) - 1
  95 }
  96 
  97 // rexp returns an exponentially-distributed random value using the scale
  98 // (expected value) given
  99 func rexp(r *rand.Rand, scale float64) float64 {
 100     return scale * r.ExpFloat64()
 101 }
 102 
 103 // rnorm returns a normally-distributed random value using the mean and
 104 // standard deviation given
 105 func rnorm(r *rand.Rand, mu, sigma float64) float64 {
 106     return r.NormFloat64()*sigma + mu
 107 }
 108 
 109 // make sample for a synthetic-drum kick
 110 func kick(t float64, f, k float64) float64 {
 111     const p = 0.085
 112     return math.Sin(tau*f*math.Pow(p, t)) * math.Exp(-k*t)
 113 }
 114 
 115 // make sample for a heavier-sounding synthetic-drum kick
 116 func kicklow(t float64, f, k float64) float64 {
 117     const p = 0.08
 118     return math.Sin(tau*f*math.Pow(p, t)) * math.Exp(-k*t)
 119 }
 120 
 121 // make sample for a synthetic hi-hat hit
 122 func hihat(t float64, k float64) float64 {
 123     return rand.Float64() * math.Exp(-k*t)
 124 }
 125 
 126 // schedule rearranges time, without being a time machine
 127 func schedule(t float64, period, delay float64) float64 {
 128     v := t + (1 - delay)
 129     if v < 0 {
 130         return 0
 131     }
 132     return math.Mod(v*period, period)
 133 }
 134 
 135 // make sample for a synthetic piano key being hit
 136 func piano(t float64, n float64) float64 {
 137     p := (math.Floor(n) - 49) / 12
 138     f := 440 * math.Pow(2, p)
 139     return math.Sin(tau * f * t)
 140 }
 141 
 142 // multiply rest of a formula with this for a quick volume drop at the end:
 143 // this is handy to avoid clips when sounds end playing
 144 func dropsince(t float64, start float64) float64 {
 145     // return math.Min(1, math.Exp(-100*(t-start)))
 146     if t <= start {
 147         return 1
 148     }
 149     return math.Exp(-100 * (t - start))
 150 }
 151 
 152 // pickval requires at least 3 args, the first 2 being the current time and
 153 // each slot's duration, respectively: these 2 are followed by all the values
 154 // to pick for all time slots
 155 func pickval(args ...float64) float64 {
 156     if len(args) < 3 {
 157         return 0
 158     }
 159 
 160     t := args[0]
 161     slotdur := args[1]
 162     values := args[2:]
 163 
 164     u, _ := math.Modf(t / slotdur)
 165     n := len(values)
 166     i := int(u) % n
 167     if 0 <= i && i < n {
 168         return values[i]
 169     }
 170     return 0
 171 }
 172 
 173 // timeval requires at least 2 args, the first 2 being the current time and
 174 // the total looping-period, respectively: these 2 are followed by pairs of
 175 // numbers, each consisting of a timestamp and a matching value, in order
 176 func timeval(args ...float64) float64 {
 177     if len(args) < 2 {
 178         return 0
 179     }
 180 
 181     t := args[0]
 182     period := args[1]
 183     u, _ := math.Modf(t / period)
 184 
 185     // find the first value whose periodic timestamp is due
 186     for rest := args[2:]; len(rest) >= 2; rest = rest[2:] {
 187         if u >= rest[0]/period {
 188             return rest[1]
 189         }
 190     }
 191     return 0
 192 }