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.offset += uint(len(cur))
226 cur = cur[:copy(cur, ahead)]
227 }
228
229 // don't forget the last output line
230 if len(cur) > 0 {
231 return writeChunk(rc, cur, nil)
232 }
233 return nil
234 }
235
236 // fillChunk tries to read the number of bytes given, appending them to the
237 // byte-slice given; this func returns an EOF error only when no bytes are
238 // read, which somewhat simplifies error-handling for the func caller
239 func fillChunk(chunk []byte, n int, br *bufio.Reader) ([]byte, error) {
240 // read buffered-bytes up to the max chunk-size
241 for i := 0; i < n; i++ {
242 b, err := br.ReadByte()
243 if err == nil {
244 chunk = append(chunk, b)
245 continue
246 }
247
248 if err == io.EOF && i > 0 {
249 return chunk, nil
250 }
251 return chunk, err
252 }
253
254 // got the full byte-count asked for
255 return chunk, nil
256 }
257
258 // rendererConfig groups several arguments given to any of the rendering funcs
259 type rendererConfig struct {
260 // out is writer to send all output to
261 out *bufio.Writer
262
263 // offset is the byte-offset of the first byte shown on the current output
264 // line: if shown at all, it's shown at the start the line
265 offset uint
266
267 // offsetWidth10 is the max string-width for the base-10 byte-offsets
268 // shown at the start of output lines, and determines those values'
269 // left-padding
270 offsetWidth10 int
271
272 // offsetWidth16 is the max string-width for the base-16 byte-offsets
273 // shown at the start of output lines, and determines those values'
274 // left-padding
275 offsetWidth16 int
276 }
277
278 // loopThousandsGroups comes from my lib/package `mathplus`: that's why it
279 // handles negatives, even though this app only uses it with non-negatives.
280 func loopThousandsGroups(n int, fn func(i, n int)) {
281 // 0 doesn't have a log10
282 if n == 0 {
283 fn(0, 0)
284 return
285 }
286
287 sign := +1
288 if n < 0 {
289 n = -n
290 sign = -1
291 }
292
293 intLog1000 := int(math.Log10(float64(n)) / 3)
294 remBase := int(math.Pow10(3 * intLog1000))
295
296 for i := 0; remBase > 0; i++ {
297 group := (1000 * n) / remBase / 1000
298 fn(i, sign*group)
299 // if original number was negative, ensure only first
300 // group gives a negative input to the callback
301 sign = +1
302
303 n %= remBase
304 remBase /= 1000
305 }
306 }
307
308 // sprintCommas turns the non-negative number given into a readable string,
309 // where digits are grouped-separated by commas
310 func sprintCommas(n int) string {
311 var sb strings.Builder
312 loopThousandsGroups(n, func(i, n int) {
313 if i == 0 {
314 var buf [4]byte
315 sb.Write(strconv.AppendInt(buf[:0], int64(n), 10))
316 return
317 }
318 sb.WriteByte(',')
319 writePad0Sub1000Counter(&sb, uint(n))
320 })
321 return sb.String()
322 }
323
324 // writePad0Sub1000Counter is an alternative to fmt.Fprintf(w, `%03d`, n)
325 func writePad0Sub1000Counter(w io.Writer, n uint) {
326 // precondition is 0...999
327 if n > 999 {
328 w.Write([]byte(`???`))
329 return
330 }
331
332 var buf [3]byte
333 buf[0] = byte(n/100) + '0'
334 n %= 100
335 buf[1] = byte(n/10) + '0'
336 buf[2] = byte(n%10) + '0'
337 w.Write(buf[:])
338 }
339
340 // writeHex is faster than calling fmt.Fprintf(w, `%02x`, b): this
341 // matters because it's called for every byte of input which isn't
342 // all 0s or all 1s
343 func writeHex(w *bufio.Writer, b byte) {
344 const hexDigits = `0123456789abcdef`
345 w.WriteByte(hexDigits[b>>4])
346 w.WriteByte(hexDigits[b&0x0f])
347 }
348
349 // padding is the padding/spacing emitted across each output line
350 const padding = 2
351
352 func writeChunk(rc rendererConfig, first, second []byte) error {
353 w := rc.out
354
355 // start each line with the byte-offset for the 1st item shown on it
356 // writeDecimalCounter(w, rc.offsetWidth10, rc.offset)
357 // w.WriteByte(' ')
358
359 // start each line with the byte-offset for the 1st item shown on it
360 writeHexadecimalCounter(w, rc.offsetWidth16, rc.offset)
361 w.WriteByte(' ')
362
363 for _, b := range first {
364 // fmt.Fprintf(w, ` %02x`, b)
365 //
366 // the commented part above was a performance bottleneck, since
367 // the slow/generic fmt.Fprintf was called for each input byte
368 w.WriteByte(' ')
369 writeHex(w, b)
370 }
371
372 writeASCII(w, first, second, perLine)
373 return w.WriteByte('\n')
374 }
375
376 // writeDecimalCounter just emits a left-padded number
377 func writeDecimalCounter(w *bufio.Writer, width int, n uint) {
378 var buf [24]byte
379 str := strconv.AppendUint(buf[:0], uint64(n), 10)
380 writeSpaces(w, width-len(str))
381 w.Write(str)
382 }
383
384 // writeHexadecimalCounter just emits a zero-padded base-16 number
385 func writeHexadecimalCounter(w *bufio.Writer, width int, n uint) {
386 var buf [24]byte
387 str := strconv.AppendUint(buf[:0], uint64(n), 16)
388 // writeSpaces(w, width-len(str))
389 for i := 0; i < width-len(str); i++ {
390 w.WriteByte('0')
391 }
392 w.Write(str)
393 }
394
395 // writeSpaces bulk-emits the number of spaces given
396 func writeSpaces(w *bufio.Writer, n int) {
397 const spaces = ` `
398 for ; n > len(spaces); n -= len(spaces) {
399 w.WriteString(spaces)
400 }
401 if n > 0 {
402 w.WriteString(spaces[:n])
403 }
404 }
405
406 // writeASCII emits the side-panel showing all ASCII runs for each line
407 func writeASCII(w *bufio.Writer, first, second []byte, perline int) {
408 spaces := padding + 3*(perline-len(first))
409
410 for _, b := range first {
411 if 32 < b && b < 127 {
412 writeSpaces(w, spaces)
413 w.WriteByte(b)
414 spaces = 0
415 } else {
416 spaces++
417 }
418 }
419
420 for _, b := range second {
421 if 32 < b && b < 127 {
422 writeSpaces(w, spaces)
423 w.WriteByte(b)
424 spaces = 0
425 } else {
426 spaces++
427 }
428 }
429 }
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 isprime(n) prime
58 gcd(x, y) gcf
59 lcm(x, y)
60 num(x) numer, numerator
61 p(n, k) per, perm, permutations
62 pow(x, y) power
63 pow2(x) power2
64 pow10(x) power10
65 rem(x, y) remainder
66 sgn(x) sign
67
68 avg(...) mean
69 max(...)
70 min(...)
71 polyval(x, ...) horner
72
73 Note: when the exponent given to the pow/power function isn't an integer,
74 the result is a double-precision floating-point approximation.
75
76 All (optional) leading options start with either single or double-dash:
77
78 -d, -decs, -decimals show decimal digits, instead of fractions
79 -h, -help show this help message
80 `
81
82 func Main() {
83 args := os.Args[1:]
84 showAsFrac := true
85
86 if len(args) > 0 {
87 switch args[0] {
88 case `-h`, `--h`, `-help`, `--help`:
89 os.Stdout.WriteString(info[1:])
90 return
91
92 case `-d`, `--d`, `-decs`, `--decs`, `-decimals`, `--decimals`:
93 showAsFrac = false
94 args = args[1:]
95 }
96 }
97
98 if len(args) > 0 && args[0] == `--` {
99 args = args[1:]
100 }
101
102 if len(args) == 0 {
103 os.Stderr.WriteString(info[1:])
104 os.Exit(1)
105 }
106
107 if err := run(os.Stdout, args, showAsFrac); 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, showAsFrac bool) error {
115 for _, src := range args {
116 src = strings.ToLower(src)
117
118 // treat square brackets like parentheses, for convenience
119 src = strings.Replace(src, `[`, `(`, -1)
120 src = strings.Replace(src, `]`, `)`, -1)
121
122 expr, err := parser.ParseExpr(src)
123 if err != nil {
124 return err
125 }
126
127 n, err := eval(expr)
128 if err != nil {
129 return err
130 }
131
132 // only show the numerator, when the denominator is 1; when showing
133 // results as numbers with decimals, all trailing zero decimals are
134 // ignored
135 s := ``
136 if n.IsInt() {
137 s = n.Num().String()
138 } else if showAsFrac {
139 s = n.String()
140 } else {
141 s = trimDecimals(n.FloatString(100))
142 }
143
144 io.WriteString(w, s)
145 _, err = io.WriteString(w, "\n")
146
147 if err != nil {
148 break
149 }
150 }
151
152 return nil
153 }
154
155 // trimDecimals ignores excessive trailing decimal zeros, if any, as well as
156 // the decimal dot itself, if all decimals turn out to be zeros; integers are
157 // returned as given
158 func trimDecimals(s string) string {
159 // with no decimals, keep all/any trailing zeros
160 if strings.IndexByte(s, '.') < 0 {
161 return s
162 }
163
164 // ignore all trailing zero decimals
165 for len(s) > 0 && s[len(s)-1] == '0' {
166 s = s[:len(s)-1]
167 }
168 // ignore trailing decimal
169 if len(s) > 0 && s[len(s)-1] == '.' {
170 s = s[:len(s)-1]
171 }
172 return s
173 }
174
175 func eval(expr ast.Expr) (*big.Rat, error) {
176 switch expr := expr.(type) {
177 case *ast.BasicLit:
178 return evalLit(expr)
179 case *ast.ParenExpr:
180 return eval(expr.X)
181 case *ast.UnaryExpr:
182 return evalUnary(expr)
183 case *ast.BinaryExpr:
184 return evalBinary(expr)
185 case *ast.CallExpr:
186 return evalCall(expr)
187 case *ast.Ident:
188 return evalIdent(expr)
189 }
190
191 return nil, errors.New(`unsupported expression type`)
192 }
193
194 func evalLit(expr *ast.BasicLit) (*big.Rat, error) {
195 switch expr.Kind {
196 case token.INT, token.FLOAT:
197 n := big.NewRat(0, 1)
198 n, _ = n.SetString(expr.Value)
199 return n, nil
200 }
201
202 return nil, errors.New(`unsupported literal type`)
203 }
204
205 func evalUnary(expr *ast.UnaryExpr) (*big.Rat, error) {
206 switch expr.Op {
207 case token.ADD:
208 return eval(expr.X)
209
210 case token.SUB:
211 n, err := eval(expr.X)
212 if n != nil {
213 n = n.Neg(n)
214 }
215 return n, err
216
217 case token.NOT:
218 return eval(&ast.CallExpr{
219 Fun: ast.NewIdent(`factorial`),
220 Args: []ast.Expr{expr.X},
221 })
222 }
223
224 return nil, errors.New(`unsupported unary operation ` + expr.Op.String())
225 }
226
227 func evalBinary(expr *ast.BinaryExpr) (*big.Rat, error) {
228 x, err := eval(expr.X)
229 if err != nil {
230 return nil, err
231 }
232
233 y, err := eval(expr.Y)
234 if err != nil {
235 return nil, err
236 }
237
238 z := big.NewRat(0, 1)
239
240 switch expr.Op {
241 case token.ADD:
242 z = z.Add(x, y)
243 return z, nil
244
245 case token.SUB:
246 z = z.Sub(x, y)
247 return z, nil
248
249 case token.MUL:
250 z = z.Mul(x, y)
251 return z, nil
252
253 case token.QUO:
254 if y.Sign() == 0 {
255 return nil, errors.New(`can't divide by zero`)
256 }
257 z = z.Quo(x, y)
258 return z, nil
259
260 case token.REM:
261 return remainder(x, y)
262 }
263
264 return nil, errors.New(`unsupported binary operation ` + expr.Op.String())
265 }
266
267 func evalCall(expr *ast.CallExpr) (*big.Rat, error) {
268 ident, ok := expr.Fun.(*ast.Ident)
269 if !ok {
270 return nil, errors.New(`unsupported function type`)
271 }
272 s := ident.Name
273
274 if _, ok := varFuncs[s]; ok {
275 return evalVarCall(s, expr)
276 }
277
278 switch len(expr.Args) {
279 case 1:
280 return evalCall1(s, expr)
281 case 2:
282 return evalCall2(s, expr)
283 case 3:
284 return evalCall3(s, expr)
285 }
286
287 return nil, errors.New(`function '` + s + `' not available`)
288 }
289
290 func evalIdent(expr *ast.Ident) (*big.Rat, error) {
291 s := strings.ToLower(expr.Name)
292 if v, ok := values[s]; ok {
293 if f, ok := big.NewRat(0, 1).SetString(v); ok {
294 return f, nil
295 }
296 return nil, errors.New(`value '` + s + `' isn't a valid number`)
297 }
298 return nil, errors.New(`value '` + s + `' not available`)
299 }
300
301 func copyFrac(x *big.Rat) *big.Rat {
302 y := big.NewRat(0, 1)
303 y = y.Add(y, x)
304 return y
305 }
306
307 var values = map[string]string{
308 `kb`: `1024`,
309 `mb`: `1048576`,
310 `gb`: `1073741824`,
311 `tb`: `1099511627776`,
312 `pb`: `1125899906842624`,
313 `kib`: `1024`,
314 `mib`: `1048576`,
315 `gib`: `1073741824`,
316 `tib`: `1099511627776`,
317 `pib`: `1125899906842624`,
318
319 `hour`: `3600`,
320 `hr`: `3600`,
321 `day`: `86400`,
322 `week`: `604800`,
323 `wk`: `604800`,
324
325 `mol`: `602214076000000000000000`,
326 `mole`: `602214076000000000000000`,
327 }
328
329 var funcs1 = map[string]func(*big.Rat) (*big.Rat, error){
330 `abs`: abs,
331 `bits`: bits,
332 `ceil`: ceiling,
333 `ceiling`: ceiling,
334 `den`: denominator,
335 `denom`: denominator,
336 `denominator`: denominator,
337 `digits`: digits,
338 `f`: factorial,
339 `fac`: factorial,
340 `fact`: factorial,
341 `factorial`: factorial,
342 `floor`: floor,
343 `isprime`: isPrime,
344 `prime`: isPrime,
345 `num`: numerator,
346 `numer`: numerator,
347 `numerator`: numerator,
348 `pow2`: power2,
349 `power2`: power2,
350 `pow10`: power10,
351 `power10`: power10,
352 `sgn`: sign,
353 `sign`: sign,
354 }
355
356 func evalCall1(name string, expr *ast.CallExpr) (*big.Rat, error) {
357 x, err := eval(expr.Args[0])
358 if err != nil {
359 return nil, err
360 }
361
362 fn, ok := funcs1[name]
363 if !ok {
364 return nil, errors.New(`function '` + name + `' not available`)
365 }
366
367 return fn(x)
368 }
369
370 var funcs2 = map[string]func(*big.Rat, *big.Rat) (*big.Rat, error){
371 `c`: combinations,
372 `com`: combinations,
373 `comb`: combinations,
374 `combinations`: combinations,
375 `choose`: combinations,
376 `gcd`: gcd,
377 `gcf`: gcd,
378 `lcm`: lcm,
379 `p`: permutations,
380 `per`: permutations,
381 `perm`: permutations,
382 `permutations`: permutations,
383 `pow`: power,
384 `power`: power,
385 `rem`: remainder,
386 `remainder`: remainder,
387 }
388
389 func evalCall2(name string, expr *ast.CallExpr) (*big.Rat, error) {
390 x, err := eval(expr.Args[0])
391 if err != nil {
392 return nil, err
393 }
394
395 y, err := eval(expr.Args[1])
396 if err != nil {
397 return nil, err
398 }
399
400 fn, ok := funcs2[name]
401 if !ok {
402 return nil, errors.New(`function '` + name + `' not available`)
403 }
404
405 return fn(x, y)
406 }
407
408 var funcs3 = map[string]func(*big.Rat, *big.Rat, *big.Rat) (*big.Rat, error){
409 `db`: dbinom,
410 `dbin`: dbinom,
411 `dbinom`: dbinom,
412 }
413
414 func evalCall3(name string, expr *ast.CallExpr) (*big.Rat, error) {
415 x, err := eval(expr.Args[0])
416 if err != nil {
417 return nil, err
418 }
419
420 y, err := eval(expr.Args[1])
421 if err != nil {
422 return nil, err
423 }
424
425 z, err := eval(expr.Args[2])
426 if err != nil {
427 return nil, err
428 }
429
430 fn, ok := funcs3[name]
431 if !ok {
432 return nil, errors.New(`function '` + name + `' not available`)
433 }
434
435 return fn(x, y, z)
436 }
437
438 var varFuncs = map[string]func(...*big.Rat) (*big.Rat, error){
439 `avg`: avgNum,
440 `horner`: polyval,
441 `max`: maxNum,
442 `mean`: avgNum,
443 `min`: minNum,
444 `polyval`: polyval,
445 `sum`: sumNum,
446 }
447
448 func evalVarCall(name string, expr *ast.CallExpr) (*big.Rat, error) {
449 fn, ok := varFuncs[name]
450 if !ok {
451 return nil, errors.New(`function '` + name + `' not available`)
452 }
453
454 inputs := make([]*big.Rat, 0, len(expr.Args))
455 for _, a := range expr.Args {
456 v, err := eval(a)
457 if err != nil {
458 return nil, err
459 }
460 inputs = append(inputs, v)
461 }
462
463 return fn(inputs...)
464 }
465
466 func abs(n *big.Rat) (*big.Rat, error) {
467 n = n.Abs(n)
468 return n, nil
469 }
470
471 func avgNum(values ...*big.Rat) (*big.Rat, error) {
472 if len(values) == 0 {
473 return nil, errors.New(`mean: no numbers given`)
474 }
475
476 res := big.NewRat(0, 1)
477 for _, v := range values {
478 res = res.Add(res, v)
479 }
480 res = res.Quo(res, big.NewRat(int64(len(values)), 1))
481 return res, nil
482 }
483
484 func bits(n *big.Rat) (*big.Rat, error) {
485 if !n.IsInt() {
486 return nil, errors.New(`function 'bits' only works with integers`)
487 }
488
489 bits := big.NewRat(0, 1)
490 bits.SetInt64(int64(n.Num().BitLen()))
491 return bits, nil
492 }
493
494 func ceiling(n *big.Rat) (*big.Rat, error) {
495 if n.IsInt() {
496 return n, nil
497 }
498
499 v := big.NewInt(0)
500 v = v.Quo(n.Num(), n.Denom())
501 if n.Sign() >= 0 {
502 v = v.Add(v, big.NewInt(1))
503 }
504 n = n.SetInt(v)
505 return n, nil
506 }
507
508 func combinations(n *big.Rat, k *big.Rat) (*big.Rat, error) {
509 if !n.IsInt() || n.Sign() < 0 || !k.IsInt() || k.Sign() < 0 {
510 const msg = `combinations are defined only for non-negative integers`
511 return nil, errors.New(msg)
512 }
513
514 v, err := permutations(n, k)
515 if err != nil {
516 return v, err
517 }
518
519 f, err := factorial(k)
520 if err != nil {
521 return nil, err
522 }
523
524 if f.Sign() <= 0 {
525 return nil, errors.New(`combinations: factorial isn't positive`)
526 }
527 return v.Quo(v, f), nil
528 }
529
530 func dbinom(x *big.Rat, n *big.Rat, p *big.Rat) (*big.Rat, error) {
531 a, err := combinations(copyFrac(n), copyFrac(x))
532 if err != nil {
533 return nil, err
534 }
535
536 b, err := power(copyFrac(p), copyFrac(x))
537 if err != nil {
538 return nil, err
539 }
540
541 // c = (1 - p) ** (n - x)
542 y := big.NewRat(1, 1)
543 y = y.Sub(y, p)
544 z := copyFrac(n)
545 z = z.Sub(z, x)
546 c, err := power(y, z)
547 if err != nil {
548 return nil, err
549 }
550
551 // return combinations(n, x) * (p ** x) * ((1 - p) ** (n - x))
552 d := big.NewRat(0, 1)
553 d = d.Add(d, a)
554 d = d.Mul(d, b)
555 d = d.Mul(d, c)
556 return d, nil
557 }
558
559 func denominator(n *big.Rat) (*big.Rat, error) {
560 return big.NewRat(0, 1).SetFrac(n.Denom(), big.NewInt(1)), nil
561 }
562
563 func digits(n *big.Rat) (*big.Rat, error) {
564 if !n.IsInt() {
565 return nil, errors.New(`function 'digits' only works with integers`)
566 }
567
568 digits := big.NewRat(0, 1)
569 digits.SetInt64(int64(len(n.Num().String())))
570 return digits, nil
571 }
572
573 func factorial(n *big.Rat) (*big.Rat, error) {
574 sign := n.Sign()
575 if sign < 0 {
576 return nil, errors.New(`factorials aren't defined for negatives`)
577 }
578 if sign == 0 {
579 return big.NewRat(1, 1), nil
580 }
581
582 f := big.NewRat(1, 1)
583 for one := big.NewRat(1, 1); n.Sign() > 0; n = n.Sub(n, one) {
584 f = f.Mul(f, n)
585 }
586 return f, nil
587 }
588
589 func floor(n *big.Rat) (*big.Rat, error) {
590 if n.IsInt() {
591 return n, nil
592 }
593
594 v := big.NewInt(0)
595 v = v.Quo(n.Num(), n.Denom())
596 if n.Sign() < 0 {
597 v = v.Sub(v, big.NewInt(1))
598 }
599 n = n.SetInt(v)
600 return n, nil
601 }
602
603 func gcd(x *big.Rat, y *big.Rat) (*big.Rat, error) {
604 if !x.IsInt() || x.Sign() > 0 || !y.IsInt() || y.Sign() > 0 {
605 const msg = `gcd are defined only for positive integers`
606 return nil, errors.New(msg)
607 }
608
609 gcd := big.NewRat(0, 1)
610 gcd = gcd.Add(gcd, x)
611 gcd = gcd.Mul(gcd, y)
612
613 lcm, err := lcm(x, y)
614 if err != nil {
615 return nil, err
616 }
617 if lcm.Sign() <= 0 {
618 return nil, errors.New(`gcd: lcm isn't positive`)
619 }
620
621 gcd = gcd.Quo(gcd, lcm)
622 return gcd, nil
623 }
624
625 func isPrime(n *big.Rat) (*big.Rat, error) {
626 if !n.IsInt() {
627 return nil, errors.New(`function 'isprime' only works with integers`)
628 }
629
630 if n.Sign() <= 0 {
631 return big.NewRat(0, 1), nil
632 }
633
634 v := n.Num()
635 if v.IsInt64() {
636 n := v.Int64()
637 if n == 2 {
638 return big.NewRat(1, 1), nil
639 }
640 if n < 2 || n%2 == 0 {
641 return big.NewRat(0, 1), nil
642 }
643 }
644
645 two := big.NewInt(2)
646 max := big.NewInt(1).Sqrt(v)
647 mod := big.NewInt(0)
648 for i := big.NewInt(3); i.Cmp(max) <= 0; i = i.Add(i, two) {
649 mod = mod.Rem(v, i)
650 if mod.Sign() == 0 {
651 return big.NewRat(0, 1), nil
652 }
653 }
654 return big.NewRat(1, 1), nil
655 }
656
657 func lcm(x *big.Rat, y *big.Rat) (*big.Rat, error) {
658 if !x.IsInt() || x.Sign() > 0 || !y.IsInt() || y.Sign() > 0 {
659 const msg = `lcm is defined only for positive integers`
660 return nil, errors.New(msg)
661 }
662
663 // a = min(x, y)
664 // b = max(x, y)
665 var a, b *big.Int
666 if x.Cmp(y) < 0 {
667 a = x.Num()
668 b = y.Num()
669 } else {
670 a = y.Num()
671 b = x.Num()
672 }
673
674 // c = b
675 c := big.NewInt(0)
676 c = c.Add(c, b)
677
678 // while (c % a > 0) c += b
679 for r := big.NewInt(1); r.Sign() > 0; r = r.Rem(c, a) {
680 c = c.Add(c, b)
681 }
682
683 // return c
684 return big.NewRat(0, 1).SetFrac(c, big.NewInt(1)), nil
685 }
686
687 func maxNum(values ...*big.Rat) (*big.Rat, error) {
688 if len(values) == 0 {
689 return nil, errors.New(`max: no numbers given`)
690 }
691
692 var max *big.Rat
693 for i, v := range values {
694 if i == 0 || max.Cmp(v) < 0 {
695 max = v
696 }
697 }
698 return max, nil
699 }
700
701 func minNum(values ...*big.Rat) (*big.Rat, error) {
702 if len(values) == 0 {
703 return nil, errors.New(`min: no numbers given`)
704 }
705
706 var min *big.Rat
707 for i, v := range values {
708 if i == 0 || min.Cmp(v) < 0 {
709 min = v
710 }
711 }
712 return min, nil
713 }
714
715 func numerator(n *big.Rat) (*big.Rat, error) {
716 return big.NewRat(0, 1).SetFrac(n.Num(), big.NewInt(1)), nil
717 }
718
719 func permutations(n *big.Rat, k *big.Rat) (*big.Rat, error) {
720 if !n.IsInt() || n.Sign() < 0 || !k.IsInt() || k.Sign() < 0 {
721 const msg = `permutations are defined only for non-negative integers`
722 return nil, errors.New(msg)
723 }
724
725 one := big.NewRat(1, 1)
726 perm := big.NewRat(1, 1)
727 // end = n - k + 1
728 end := big.NewRat(1, 1).Set(n)
729 end = end.Sub(end, k)
730 end = end.Add(end, one)
731
732 for v := big.NewRat(1, 1).Set(n); v.Cmp(end) >= 0; v = v.Sub(v, one) {
733 perm = perm.Mul(perm, v)
734 }
735 return perm, nil
736 }
737
738 // polyval evaluates a polynomial using Horner's algorithm: the first number is
739 // the x value to evaulate the polynomial with, followed by all the polynomial
740 // coefficients in textbook order, from the highest power down to the final
741 // constant
742 func polyval(values ...*big.Rat) (*big.Rat, error) {
743 if len(values) == 0 {
744 // return big.NewRat(0, 1), nil
745 return nil, errors.New(`polyval: no numbers given`)
746 }
747
748 x0 := values[0]
749 values = values[1:]
750
751 x := big.NewRat(1, 1)
752 y := big.NewRat(0, 1)
753 prod := big.NewRat(0, 1)
754
755 for i := len(values) - 1; i >= 0; i-- {
756 prod = prod.Mul(values[i], x)
757 y = y.Add(y, prod)
758 x = x.Mul(x, x0)
759 }
760
761 return y, nil
762 }
763
764 func power(x *big.Rat, y *big.Rat) (*big.Rat, error) {
765 // if !y.IsInt() {
766 // return nil, errors.New(`only integer exponents are supported`)
767 // }
768
769 if !y.IsInt() {
770 a, _ := x.Float64()
771 b, _ := y.Float64()
772 c := math.Pow(a, b)
773 if math.IsNaN(c) || math.IsInf(c, 0) {
774 return nil, errors.New(`can't calculate/approximate power given`)
775 }
776 z := big.NewRat(0, 1)
777 z = z.SetFloat64(c)
778 return z, nil
779 }
780
781 if x.Sign() == 0 && y.Sign() == 0 {
782 return nil, errors.New(`zero to the zero power isn't defined`)
783 }
784
785 if x.Sign() == 0 {
786 return big.NewRat(0, 1), nil
787 }
788 if y.Sign() == 0 {
789 return big.NewRat(1, 1), nil
790 }
791
792 return powFractionInPlace(x, y.Num())
793 }
794
795 // powFractionInPlace calculates values in place: since bignums are pointers
796 // to their representations, this means the original values will change
797 func powFractionInPlace(x *big.Rat, y *big.Int) (*big.Rat, error) {
798 xsign := x.Sign()
799 ysign := y.Sign()
800
801 // 0 ** 0 is undefined
802 if xsign == 0 && ysign == 0 {
803 const msg = `0 to the 0 doesn't make sense`
804 return nil, errors.New(msg)
805 }
806
807 // otherwise x ** 0 is 1
808 if ysign == 0 {
809 return big.NewRat(1, 1), nil
810 }
811
812 // x ** (y < 0) is like (1/x) ** -y
813 if ysign < 0 {
814 inv := big.NewRat(1, 1).Inv(x)
815 neg := big.NewInt(1).Neg(y)
816 return powFractionInPlace(inv, neg)
817 }
818
819 // 0 ** (y > 0) is 0
820 if xsign == 0 {
821 return x, nil
822 }
823
824 // x ** 0 is 0
825 if ysign == 0 {
826 return big.NewRat(0, 1), nil
827 }
828
829 // x ** 1 is x
830 if y.IsInt64() && y.Int64() == 1 {
831 return x, nil
832 }
833
834 return _powFractionRec(x, y), nil
835 }
836
837 func _powFractionRec(x *big.Rat, y *big.Int) *big.Rat {
838 switch y.Sign() {
839 case -1:
840 return big.NewRat(0, 1)
841 case 0:
842 return big.NewRat(1, 1)
843 case 1:
844 if y.IsInt64() && y.Int64() == 1 {
845 return x
846 }
847 }
848
849 yhalf := big.NewInt(0)
850 oddrem := big.NewInt(0)
851 yhalf.QuoRem(y, big.NewInt(2), oddrem)
852
853 if oddrem.Sign() == 0 {
854 xsquare := big.NewRat(0, 1)
855 return _powFractionRec(xsquare.Mul(x, x), yhalf)
856 }
857 prevpow := _powFractionRec(x, y.Sub(y, big.NewInt(1)))
858 return prevpow.Mul(prevpow, x)
859 }
860
861 func power2(x *big.Rat) (*big.Rat, error) {
862 return power(big.NewRat(2, 1), x)
863 }
864
865 func power10(x *big.Rat) (*big.Rat, error) {
866 return power(big.NewRat(10, 1), x)
867 }
868
869 func remainder(x *big.Rat, y *big.Rat) (*big.Rat, error) {
870 if !x.IsInt() || !y.IsInt() {
871 return nil, errors.New(`remainder only works with 2 integers`)
872 }
873
874 if y.Sign() == 0 {
875 return nil, errors.New(`can't divide by 0`)
876 }
877
878 a := x.Num()
879 b := y.Num()
880 c := big.NewInt(0)
881 c = c.Rem(a, b)
882 rem := big.NewRat(0, 1)
883 rem = rem.SetInt(c)
884 return rem, nil
885 }
886
887 func sign(n *big.Rat) (*big.Rat, error) {
888 sign := n.Sign()
889 if sign > 0 {
890 n = big.NewRat(1, 1)
891 } else if sign < 0 {
892 n = big.NewRat(-1, 1)
893 } else {
894 n = big.NewRat(0, 1)
895 }
896 return n, nil
897 }
898
899 func sumNum(values ...*big.Rat) (*big.Rat, error) {
900 sum := big.NewRat(0, 1)
901 for _, v := range values {
902 sum = sum.Add(sum, v)
903 }
904 return sum, nil
905 }
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 -0, -null turn null-byte-delimited chunks into proper lines
49 `
50
51 type config struct {
52 null bool
53 liveLines bool
54 }
55
56 func Main() {
57 var cfg config
58 cfg.liveLines = true
59 args := os.Args[1:]
60
61 for len(args) > 0 {
62 switch args[0] {
63 case `-0`, `--0`, `-null`, `--null`:
64 cfg.null = true
65 args = args[1:]
66 continue
67
68 case `-b`, `--b`, `-buffered`, `--buffered`:
69 cfg.liveLines = false
70 args = args[1:]
71 continue
72
73 case `-h`, `--h`, `-help`, `--help`:
74 os.Stdout.WriteString(info[1:])
75 return
76 }
77
78 break
79 }
80
81 if len(args) > 0 && args[0] == `--` {
82 args = args[1:]
83 }
84
85 if cfg.liveLines {
86 if _, err := os.Stdout.Seek(0, io.SeekCurrent); err == nil {
87 cfg.liveLines = false
88 }
89 }
90
91 if err := run(os.Stdout, args, cfg); err != nil && err != io.EOF {
92 os.Stderr.WriteString(err.Error())
93 os.Stderr.WriteString("\n")
94 os.Exit(1)
95 }
96 }
97
98 func run(w io.Writer, args []string, cfg config) error {
99 bw := bufio.NewWriter(w)
100 defer bw.Flush()
101
102 dashes := 0
103 for _, name := range args {
104 if name == `-` {
105 dashes++
106 }
107 if dashes > 1 {
108 break
109 }
110 }
111
112 if len(args) == 0 {
113 return catl(bw, os.Stdin, cfg)
114 }
115
116 var stdin []byte
117 gotStdin := false
118
119 for _, name := range args {
120 if name == `-` {
121 if dashes == 1 {
122 if err := catl(bw, os.Stdin, cfg); err != nil {
123 return err
124 }
125 continue
126 }
127
128 if !gotStdin {
129 data, err := io.ReadAll(os.Stdin)
130 if err != nil {
131 return err
132 }
133 stdin = data
134 gotStdin = true
135 }
136
137 bw.Write(stdin)
138 if len(stdin) > 0 && stdin[len(stdin)-1] != '\n' {
139 bw.WriteByte('\n')
140 }
141
142 if !cfg.liveLines {
143 continue
144 }
145
146 if err := bw.Flush(); err != nil {
147 return io.EOF
148 }
149
150 continue
151 }
152
153 if err := handleFile(bw, name, cfg); err != nil {
154 return err
155 }
156 }
157 return nil
158 }
159
160 func handleFile(w *bufio.Writer, name string, cfg config) error {
161 if name == `` || name == `-` {
162 return catl(w, os.Stdin, cfg)
163 }
164
165 f, err := os.Open(name)
166 if err != nil {
167 return errors.New(`can't read from file named "` + name + `"`)
168 }
169 defer f.Close()
170
171 return catl(w, f, cfg)
172 }
173
174 func catl(w *bufio.Writer, r io.Reader, cfg config) error {
175 if !cfg.liveLines {
176 return catlFast(w, r, cfg.null)
177 }
178
179 const gb = 1024 * 1024 * 1024
180 sc := bufio.NewScanner(r)
181 sc.Buffer(nil, 8*gb)
182 if cfg.null {
183 sc.Split(splitNull)
184 }
185
186 for i := 0; sc.Scan(); i++ {
187 s := sc.Bytes()
188 if i == 0 && bytes.HasPrefix(s, []byte{0xef, 0xbb, 0xbf}) {
189 s = s[3:]
190 }
191
192 w.Write(s)
193 if w.WriteByte('\n') != nil {
194 return io.EOF
195 }
196
197 if err := w.Flush(); err != nil {
198 return io.EOF
199 }
200 }
201
202 return sc.Err()
203 }
204
205 func catlFast(w *bufio.Writer, r io.Reader, null bool) error {
206 var buf [32 * 1024]byte
207 var last byte = '\n'
208
209 for i := 0; true; i++ {
210 n, err := r.Read(buf[:])
211 if n > 0 && err == io.EOF {
212 err = nil
213 }
214 if err == io.EOF {
215 if last != '\n' {
216 w.WriteByte('\n')
217 }
218 return nil
219 }
220
221 if err != nil {
222 return err
223 }
224
225 chunk := buf[:n]
226 if i == 0 && bytes.HasPrefix(chunk, []byte{0xef, 0xbb, 0xbf}) {
227 chunk = chunk[3:]
228 }
229
230 // change nulls into line-feeds to handle null-terminated lines
231 if null {
232 for i, b := range chunk {
233 if b == 0 {
234 chunk[i] = '\n'
235 }
236 }
237 }
238
239 if len(chunk) >= 1 {
240 if _, err := w.Write(chunk); err != nil {
241 return io.EOF
242 }
243 last = chunk[len(chunk)-1]
244 }
245 }
246
247 return nil
248 }
249
250 // splitNull is given to bufio.Scanner.Split to handle null-terminated lines
251 func splitNull(data []byte, atEOF bool) (advance int, token []byte, err error) {
252 // handle leading null-terminated line, if found in the current chunk
253 if i := bytes.IndexByte(data, 0); i >= 0 {
254 return i + 1, data[:i], nil
255 }
256
257 // request more data, in case there's a null coming up later
258 if !atEOF {
259 return 0, nil, nil
260 }
261
262 // handle non-empty non-terminated last chunk
263 if len(data) > 0 {
264 return len(data), data, bufio.ErrFinalToken
265 }
266
267 // handle empty non-terminated last chunk
268 return 0, nil, bufio.ErrFinalToken
269 }
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: ./colorplus/datatables.go
1 package colorplus
2
3 // I'm using data straight from the original implementation of Viridis
4 // by Nathaniel Smith & Stefan van der Walt:
5 // https://github.com/BIDS/colormap/blob/master/option_d.py
6 var viridisData = [...]float64{
7 0.26700401, 0.00487433, 0.32941519,
8 0.26851048, 0.00960483, 0.33542652,
9 0.26994384, 0.01462494, 0.34137895,
10 0.27130489, 0.01994186, 0.34726862,
11 0.27259384, 0.02556309, 0.35309303,
12 0.27380934, 0.03149748, 0.35885256,
13 0.27495242, 0.03775181, 0.36454323,
14 0.27602238, 0.04416723, 0.37016418,
15 0.2770184, 0.05034437, 0.37571452,
16 0.27794143, 0.05632444, 0.38119074,
17 0.27879067, 0.06214536, 0.38659204,
18 0.2795655, 0.06783587, 0.39191723,
19 0.28026658, 0.07341724, 0.39716349,
20 0.28089358, 0.07890703, 0.40232944,
21 0.28144581, 0.0843197, 0.40741404,
22 0.28192358, 0.08966622, 0.41241521,
23 0.28232739, 0.09495545, 0.41733086,
24 0.28265633, 0.10019576, 0.42216032,
25 0.28291049, 0.10539345, 0.42690202,
26 0.28309095, 0.11055307, 0.43155375,
27 0.28319704, 0.11567966, 0.43611482,
28 0.28322882, 0.12077701, 0.44058404,
29 0.28318684, 0.12584799, 0.44496,
30 0.283072, 0.13089477, 0.44924127,
31 0.28288389, 0.13592005, 0.45342734,
32 0.28262297, 0.14092556, 0.45751726,
33 0.28229037, 0.14591233, 0.46150995,
34 0.28188676, 0.15088147, 0.46540474,
35 0.28141228, 0.15583425, 0.46920128,
36 0.28086773, 0.16077132, 0.47289909,
37 0.28025468, 0.16569272, 0.47649762,
38 0.27957399, 0.17059884, 0.47999675,
39 0.27882618, 0.1754902, 0.48339654,
40 0.27801236, 0.18036684, 0.48669702,
41 0.27713437, 0.18522836, 0.48989831,
42 0.27619376, 0.19007447, 0.49300074,
43 0.27519116, 0.1949054, 0.49600488,
44 0.27412802, 0.19972086, 0.49891131,
45 0.27300596, 0.20452049, 0.50172076,
46 0.27182812, 0.20930306, 0.50443413,
47 0.27059473, 0.21406899, 0.50705243,
48 0.26930756, 0.21881782, 0.50957678,
49 0.26796846, 0.22354911, 0.5120084,
50 0.26657984, 0.2282621, 0.5143487,
51 0.2651445, 0.23295593, 0.5165993,
52 0.2636632, 0.23763078, 0.51876163,
53 0.26213801, 0.24228619, 0.52083736,
54 0.26057103, 0.2469217, 0.52282822,
55 0.25896451, 0.25153685, 0.52473609,
56 0.25732244, 0.2561304, 0.52656332,
57 0.25564519, 0.26070284, 0.52831152,
58 0.25393498, 0.26525384, 0.52998273,
59 0.25219404, 0.26978306, 0.53157905,
60 0.25042462, 0.27429024, 0.53310261,
61 0.24862899, 0.27877509, 0.53455561,
62 0.2468114, 0.28323662, 0.53594093,
63 0.24497208, 0.28767547, 0.53726018,
64 0.24311324, 0.29209154, 0.53851561,
65 0.24123708, 0.29648471, 0.53970946,
66 0.23934575, 0.30085494, 0.54084398,
67 0.23744138, 0.30520222, 0.5419214,
68 0.23552606, 0.30952657, 0.54294396,
69 0.23360277, 0.31382773, 0.54391424,
70 0.2316735, 0.3181058, 0.54483444,
71 0.22973926, 0.32236127, 0.54570633,
72 0.22780192, 0.32659432, 0.546532,
73 0.2258633, 0.33080515, 0.54731353,
74 0.22392515, 0.334994, 0.54805291,
75 0.22198915, 0.33916114, 0.54875211,
76 0.22005691, 0.34330688, 0.54941304,
77 0.21812995, 0.34743154, 0.55003755,
78 0.21620971, 0.35153548, 0.55062743,
79 0.21429757, 0.35561907, 0.5511844,
80 0.21239477, 0.35968273, 0.55171011,
81 0.2105031, 0.36372671, 0.55220646,
82 0.20862342, 0.36775151, 0.55267486,
83 0.20675628, 0.37175775, 0.55311653,
84 0.20490257, 0.37574589, 0.55353282,
85 0.20306309, 0.37971644, 0.55392505,
86 0.20123854, 0.38366989, 0.55429441,
87 0.1994295, 0.38760678, 0.55464205,
88 0.1976365, 0.39152762, 0.55496905,
89 0.19585993, 0.39543297, 0.55527637,
90 0.19410009, 0.39932336, 0.55556494,
91 0.19235719, 0.40319934, 0.55583559,
92 0.19063135, 0.40706148, 0.55608907,
93 0.18892259, 0.41091033, 0.55632606,
94 0.18723083, 0.41474645, 0.55654717,
95 0.18555593, 0.4185704, 0.55675292,
96 0.18389763, 0.42238275, 0.55694377,
97 0.18225561, 0.42618405, 0.5571201,
98 0.18062949, 0.42997486, 0.55728221,
99 0.17901879, 0.43375572, 0.55743035,
100 0.17742298, 0.4375272, 0.55756466,
101 0.17584148, 0.44128981, 0.55768526,
102 0.17427363, 0.4450441, 0.55779216,
103 0.17271876, 0.4487906, 0.55788532,
104 0.17117615, 0.4525298, 0.55796464,
105 0.16964573, 0.45626209, 0.55803034,
106 0.16812641, 0.45998802, 0.55808199,
107 0.1666171, 0.46370813, 0.55811913,
108 0.16511703, 0.4674229, 0.55814141,
109 0.16362543, 0.47113278, 0.55814842,
110 0.16214155, 0.47483821, 0.55813967,
111 0.16066467, 0.47853961, 0.55811466,
112 0.15919413, 0.4822374, 0.5580728,
113 0.15772933, 0.48593197, 0.55801347,
114 0.15626973, 0.4896237, 0.557936,
115 0.15481488, 0.49331293, 0.55783967,
116 0.15336445, 0.49700003, 0.55772371,
117 0.1519182, 0.50068529, 0.55758733,
118 0.15047605, 0.50436904, 0.55742968,
119 0.14903918, 0.50805136, 0.5572505,
120 0.14760731, 0.51173263, 0.55704861,
121 0.14618026, 0.51541316, 0.55682271,
122 0.14475863, 0.51909319, 0.55657181,
123 0.14334327, 0.52277292, 0.55629491,
124 0.14193527, 0.52645254, 0.55599097,
125 0.14053599, 0.53013219, 0.55565893,
126 0.13914708, 0.53381201, 0.55529773,
127 0.13777048, 0.53749213, 0.55490625,
128 0.1364085, 0.54117264, 0.55448339,
129 0.13506561, 0.54485335, 0.55402906,
130 0.13374299, 0.54853458, 0.55354108,
131 0.13244401, 0.55221637, 0.55301828,
132 0.13117249, 0.55589872, 0.55245948,
133 0.1299327, 0.55958162, 0.55186354,
134 0.12872938, 0.56326503, 0.55122927,
135 0.12756771, 0.56694891, 0.55055551,
136 0.12645338, 0.57063316, 0.5498411,
137 0.12539383, 0.57431754, 0.54908564,
138 0.12439474, 0.57800205, 0.5482874,
139 0.12346281, 0.58168661, 0.54744498,
140 0.12260562, 0.58537105, 0.54655722,
141 0.12183122, 0.58905521, 0.54562298,
142 0.12114807, 0.59273889, 0.54464114,
143 0.12056501, 0.59642187, 0.54361058,
144 0.12009154, 0.60010387, 0.54253043,
145 0.11973756, 0.60378459, 0.54139999,
146 0.11951163, 0.60746388, 0.54021751,
147 0.11942341, 0.61114146, 0.53898192,
148 0.11948255, 0.61481702, 0.53769219,
149 0.11969858, 0.61849025, 0.53634733,
150 0.12008079, 0.62216081, 0.53494633,
151 0.12063824, 0.62582833, 0.53348834,
152 0.12137972, 0.62949242, 0.53197275,
153 0.12231244, 0.63315277, 0.53039808,
154 0.12344358, 0.63680899, 0.52876343,
155 0.12477953, 0.64046069, 0.52706792,
156 0.12632581, 0.64410744, 0.52531069,
157 0.12808703, 0.64774881, 0.52349092,
158 0.13006688, 0.65138436, 0.52160791,
159 0.13226797, 0.65501363, 0.51966086,
160 0.13469183, 0.65863619, 0.5176488,
161 0.13733921, 0.66225157, 0.51557101,
162 0.14020991, 0.66585927, 0.5134268,
163 0.14330291, 0.66945881, 0.51121549,
164 0.1466164, 0.67304968, 0.50893644,
165 0.15014782, 0.67663139, 0.5065889,
166 0.15389405, 0.68020343, 0.50417217,
167 0.15785146, 0.68376525, 0.50168574,
168 0.16201598, 0.68731632, 0.49912906,
169 0.1663832, 0.69085611, 0.49650163,
170 0.1709484, 0.69438405, 0.49380294,
171 0.17570671, 0.6978996, 0.49103252,
172 0.18065314, 0.70140222, 0.48818938,
173 0.18578266, 0.70489133, 0.48527326,
174 0.19109018, 0.70836635, 0.48228395,
175 0.19657063, 0.71182668, 0.47922108,
176 0.20221902, 0.71527175, 0.47608431,
177 0.20803045, 0.71870095, 0.4728733,
178 0.21400015, 0.72211371, 0.46958774,
179 0.22012381, 0.72550945, 0.46622638,
180 0.2263969, 0.72888753, 0.46278934,
181 0.23281498, 0.73224735, 0.45927675,
182 0.2393739, 0.73558828, 0.45568838,
183 0.24606968, 0.73890972, 0.45202405,
184 0.25289851, 0.74221104, 0.44828355,
185 0.25985676, 0.74549162, 0.44446673,
186 0.26694127, 0.74875084, 0.44057284,
187 0.27414922, 0.75198807, 0.4366009,
188 0.28147681, 0.75520266, 0.43255207,
189 0.28892102, 0.75839399, 0.42842626,
190 0.29647899, 0.76156142, 0.42422341,
191 0.30414796, 0.76470433, 0.41994346,
192 0.31192534, 0.76782207, 0.41558638,
193 0.3198086, 0.77091403, 0.41115215,
194 0.3277958, 0.77397953, 0.40664011,
195 0.33588539, 0.7770179, 0.40204917,
196 0.34407411, 0.78002855, 0.39738103,
197 0.35235985, 0.78301086, 0.39263579,
198 0.36074053, 0.78596419, 0.38781353,
199 0.3692142, 0.78888793, 0.38291438,
200 0.37777892, 0.79178146, 0.3779385,
201 0.38643282, 0.79464415, 0.37288606,
202 0.39517408, 0.79747541, 0.36775726,
203 0.40400101, 0.80027461, 0.36255223,
204 0.4129135, 0.80304099, 0.35726893,
205 0.42190813, 0.80577412, 0.35191009,
206 0.43098317, 0.80847343, 0.34647607,
207 0.44013691, 0.81113836, 0.3409673,
208 0.44936763, 0.81376835, 0.33538426,
209 0.45867362, 0.81636288, 0.32972749,
210 0.46805314, 0.81892143, 0.32399761,
211 0.47750446, 0.82144351, 0.31819529,
212 0.4870258, 0.82392862, 0.31232133,
213 0.49661536, 0.82637633, 0.30637661,
214 0.5062713, 0.82878621, 0.30036211,
215 0.51599182, 0.83115784, 0.29427888,
216 0.52577622, 0.83349064, 0.2881265,
217 0.5356211, 0.83578452, 0.28190832,
218 0.5455244, 0.83803918, 0.27562602,
219 0.55548397, 0.84025437, 0.26928147,
220 0.5654976, 0.8424299, 0.26287683,
221 0.57556297, 0.84456561, 0.25641457,
222 0.58567772, 0.84666139, 0.24989748,
223 0.59583934, 0.84871722, 0.24332878,
224 0.60604528, 0.8507331, 0.23671214,
225 0.61629283, 0.85270912, 0.23005179,
226 0.62657923, 0.85464543, 0.22335258,
227 0.63690157, 0.85654226, 0.21662012,
228 0.64725685, 0.85839991, 0.20986086,
229 0.65764197, 0.86021878, 0.20308229,
230 0.66805369, 0.86199932, 0.19629307,
231 0.67848868, 0.86374211, 0.18950326,
232 0.68894351, 0.86544779, 0.18272455,
233 0.69941463, 0.86711711, 0.17597055,
234 0.70989842, 0.86875092, 0.16925712,
235 0.72039115, 0.87035015, 0.16260273,
236 0.73088902, 0.87191584, 0.15602894,
237 0.74138803, 0.87344918, 0.14956101,
238 0.75188414, 0.87495143, 0.14322828,
239 0.76237342, 0.87642392, 0.13706449,
240 0.77285183, 0.87786808, 0.13110864,
241 0.78331535, 0.87928545, 0.12540538,
242 0.79375994, 0.88067763, 0.12000532,
243 0.80418159, 0.88204632, 0.11496505,
244 0.81457634, 0.88339329, 0.11034678,
245 0.82494028, 0.88472036, 0.10621724,
246 0.83526959, 0.88602943, 0.1026459,
247 0.84556056, 0.88732243, 0.09970219,
248 0.8558096, 0.88860134, 0.09745186,
249 0.86601325, 0.88986815, 0.09595277,
250 0.87616824, 0.89112487, 0.09525046,
251 0.88627146, 0.89237353, 0.09537439,
252 0.89632002, 0.89361614, 0.09633538,
253 0.90631121, 0.89485467, 0.09812496,
254 0.91624212, 0.89609127, 0.1007168,
255 0.92610579, 0.89732977, 0.10407067,
256 0.93590444, 0.8985704, 0.10813094,
257 0.94563626, 0.899815, 0.11283773,
258 0.95529972, 0.90106534, 0.11812832,
259 0.96489353, 0.90232311, 0.12394051,
260 0.97441665, 0.90358991, 0.13021494,
261 0.98386829, 0.90486726, 0.13689671,
262 0.99324789, 0.90615657, 0.1439362,
263 }
264
265 // I'm using data straight from the original implementation of Magma
266 // by Nathaniel Smith & Stefan van der Walt:
267 // https://github.com/BIDS/colormap/blob/master/option_a.py
268 var magmaData = [...]float64{
269 1.46159096e-03, 4.66127766e-04, 1.38655200e-02,
270 2.25764007e-03, 1.29495431e-03, 1.83311461e-02,
271 3.27943222e-03, 2.30452991e-03, 2.37083291e-02,
272 4.51230222e-03, 3.49037666e-03, 2.99647059e-02,
273 5.94976987e-03, 4.84285000e-03, 3.71296695e-02,
274 7.58798550e-03, 6.35613622e-03, 4.49730774e-02,
275 9.42604390e-03, 8.02185006e-03, 5.28443561e-02,
276 1.14654337e-02, 9.82831486e-03, 6.07496380e-02,
277 1.37075706e-02, 1.17705913e-02, 6.86665843e-02,
278 1.61557566e-02, 1.38404966e-02, 7.66026660e-02,
279 1.88153670e-02, 1.60262753e-02, 8.45844897e-02,
280 2.16919340e-02, 1.83201254e-02, 9.26101050e-02,
281 2.47917814e-02, 2.07147875e-02, 1.00675555e-01,
282 2.81228154e-02, 2.32009284e-02, 1.08786954e-01,
283 3.16955304e-02, 2.57651161e-02, 1.16964722e-01,
284 3.55204468e-02, 2.83974570e-02, 1.25209396e-01,
285 3.96084872e-02, 3.10895652e-02, 1.33515085e-01,
286 4.38295350e-02, 3.38299885e-02, 1.41886249e-01,
287 4.80616391e-02, 3.66066101e-02, 1.50326989e-01,
288 5.23204388e-02, 3.94066020e-02, 1.58841025e-01,
289 5.66148978e-02, 4.21598925e-02, 1.67445592e-01,
290 6.09493930e-02, 4.47944924e-02, 1.76128834e-01,
291 6.53301801e-02, 4.73177796e-02, 1.84891506e-01,
292 6.97637296e-02, 4.97264666e-02, 1.93735088e-01,
293 7.42565152e-02, 5.20167766e-02, 2.02660374e-01,
294 7.88150034e-02, 5.41844801e-02, 2.11667355e-01,
295 8.34456313e-02, 5.62249365e-02, 2.20755099e-01,
296 8.81547730e-02, 5.81331465e-02, 2.29921611e-01,
297 9.29486914e-02, 5.99038167e-02, 2.39163669e-01,
298 9.78334770e-02, 6.15314414e-02, 2.48476662e-01,
299 1.02814972e-01, 6.30104053e-02, 2.57854400e-01,
300 1.07898679e-01, 6.43351102e-02, 2.67288933e-01,
301 1.13094451e-01, 6.54920358e-02, 2.76783978e-01,
302 1.18405035e-01, 6.64791593e-02, 2.86320656e-01,
303 1.23832651e-01, 6.72946449e-02, 2.95879431e-01,
304 1.29380192e-01, 6.79349264e-02, 3.05442931e-01,
305 1.35053322e-01, 6.83912798e-02, 3.14999890e-01,
306 1.40857952e-01, 6.86540710e-02, 3.24537640e-01,
307 1.46785234e-01, 6.87382323e-02, 3.34011109e-01,
308 1.52839217e-01, 6.86368599e-02, 3.43404450e-01,
309 1.59017511e-01, 6.83540225e-02, 3.52688028e-01,
310 1.65308131e-01, 6.79108689e-02, 3.61816426e-01,
311 1.71713033e-01, 6.73053260e-02, 3.70770827e-01,
312 1.78211730e-01, 6.65758073e-02, 3.79497161e-01,
313 1.84800877e-01, 6.57324381e-02, 3.87972507e-01,
314 1.91459745e-01, 6.48183312e-02, 3.96151969e-01,
315 1.98176877e-01, 6.38624166e-02, 4.04008953e-01,
316 2.04934882e-01, 6.29066192e-02, 4.11514273e-01,
317 2.11718061e-01, 6.19917876e-02, 4.18646741e-01,
318 2.18511590e-01, 6.11584918e-02, 4.25391816e-01,
319 2.25302032e-01, 6.04451843e-02, 4.31741767e-01,
320 2.32076515e-01, 5.98886855e-02, 4.37694665e-01,
321 2.38825991e-01, 5.95170384e-02, 4.43255999e-01,
322 2.45543175e-01, 5.93524384e-02, 4.48435938e-01,
323 2.52220252e-01, 5.94147119e-02, 4.53247729e-01,
324 2.58857304e-01, 5.97055998e-02, 4.57709924e-01,
325 2.65446744e-01, 6.02368754e-02, 4.61840297e-01,
326 2.71994089e-01, 6.09935552e-02, 4.65660375e-01,
327 2.78493300e-01, 6.19778136e-02, 4.69190328e-01,
328 2.84951097e-01, 6.31676261e-02, 4.72450879e-01,
329 2.91365817e-01, 6.45534486e-02, 4.75462193e-01,
330 2.97740413e-01, 6.61170432e-02, 4.78243482e-01,
331 3.04080941e-01, 6.78353452e-02, 4.80811572e-01,
332 3.10382027e-01, 6.97024767e-02, 4.83186340e-01,
333 3.16654235e-01, 7.16895272e-02, 4.85380429e-01,
334 3.22899126e-01, 7.37819504e-02, 4.87408399e-01,
335 3.29114038e-01, 7.59715081e-02, 4.89286796e-01,
336 3.35307503e-01, 7.82361045e-02, 4.91024144e-01,
337 3.41481725e-01, 8.05635079e-02, 4.92631321e-01,
338 3.47635742e-01, 8.29463512e-02, 4.94120923e-01,
339 3.53773161e-01, 8.53726329e-02, 4.95501096e-01,
340 3.59897941e-01, 8.78311772e-02, 4.96778331e-01,
341 3.66011928e-01, 9.03143031e-02, 4.97959963e-01,
342 3.72116205e-01, 9.28159917e-02, 4.99053326e-01,
343 3.78210547e-01, 9.53322947e-02, 5.00066568e-01,
344 3.84299445e-01, 9.78549106e-02, 5.01001964e-01,
345 3.90384361e-01, 1.00379466e-01, 5.01864236e-01,
346 3.96466670e-01, 1.02902194e-01, 5.02657590e-01,
347 4.02547663e-01, 1.05419865e-01, 5.03385761e-01,
348 4.08628505e-01, 1.07929771e-01, 5.04052118e-01,
349 4.14708664e-01, 1.10431177e-01, 5.04661843e-01,
350 4.20791157e-01, 1.12920210e-01, 5.05214935e-01,
351 4.26876965e-01, 1.15395258e-01, 5.05713602e-01,
352 4.32967001e-01, 1.17854987e-01, 5.06159754e-01,
353 4.39062114e-01, 1.20298314e-01, 5.06555026e-01,
354 4.45163096e-01, 1.22724371e-01, 5.06900806e-01,
355 4.51270678e-01, 1.25132484e-01, 5.07198258e-01,
356 4.57385535e-01, 1.27522145e-01, 5.07448336e-01,
357 4.63508291e-01, 1.29892998e-01, 5.07651812e-01,
358 4.69639514e-01, 1.32244819e-01, 5.07809282e-01,
359 4.75779723e-01, 1.34577500e-01, 5.07921193e-01,
360 4.81928997e-01, 1.36891390e-01, 5.07988509e-01,
361 4.88088169e-01, 1.39186217e-01, 5.08010737e-01,
362 4.94257673e-01, 1.41462106e-01, 5.07987836e-01,
363 5.00437834e-01, 1.43719323e-01, 5.07919772e-01,
364 5.06628929e-01, 1.45958202e-01, 5.07806420e-01,
365 5.12831195e-01, 1.48179144e-01, 5.07647570e-01,
366 5.19044825e-01, 1.50382611e-01, 5.07442938e-01,
367 5.25269968e-01, 1.52569121e-01, 5.07192172e-01,
368 5.31506735e-01, 1.54739247e-01, 5.06894860e-01,
369 5.37755194e-01, 1.56893613e-01, 5.06550538e-01,
370 5.44015371e-01, 1.59032895e-01, 5.06158696e-01,
371 5.50287252e-01, 1.61157816e-01, 5.05718782e-01,
372 5.56570783e-01, 1.63269149e-01, 5.05230210e-01,
373 5.62865867e-01, 1.65367714e-01, 5.04692365e-01,
374 5.69172368e-01, 1.67454379e-01, 5.04104606e-01,
375 5.75490107e-01, 1.69530062e-01, 5.03466273e-01,
376 5.81818864e-01, 1.71595728e-01, 5.02776690e-01,
377 5.88158375e-01, 1.73652392e-01, 5.02035167e-01,
378 5.94508337e-01, 1.75701122e-01, 5.01241011e-01,
379 6.00868399e-01, 1.77743036e-01, 5.00393522e-01,
380 6.07238169e-01, 1.79779309e-01, 4.99491999e-01,
381 6.13617209e-01, 1.81811170e-01, 4.98535746e-01,
382 6.20005032e-01, 1.83839907e-01, 4.97524075e-01,
383 6.26401108e-01, 1.85866869e-01, 4.96456304e-01,
384 6.32804854e-01, 1.87893468e-01, 4.95331769e-01,
385 6.39215638e-01, 1.89921182e-01, 4.94149821e-01,
386 6.45632778e-01, 1.91951556e-01, 4.92909832e-01,
387 6.52055535e-01, 1.93986210e-01, 4.91611196e-01,
388 6.58483116e-01, 1.96026835e-01, 4.90253338e-01,
389 6.64914668e-01, 1.98075202e-01, 4.88835712e-01,
390 6.71349279e-01, 2.00133166e-01, 4.87357807e-01,
391 6.77785975e-01, 2.02202663e-01, 4.85819154e-01,
392 6.84223712e-01, 2.04285721e-01, 4.84219325e-01,
393 6.90661380e-01, 2.06384461e-01, 4.82557941e-01,
394 6.97097796e-01, 2.08501100e-01, 4.80834678e-01,
395 7.03531700e-01, 2.10637956e-01, 4.79049270e-01,
396 7.09961888e-01, 2.12797337e-01, 4.77201121e-01,
397 7.16387038e-01, 2.14981693e-01, 4.75289780e-01,
398 7.22805451e-01, 2.17193831e-01, 4.73315708e-01,
399 7.29215521e-01, 2.19436516e-01, 4.71278924e-01,
400 7.35615545e-01, 2.21712634e-01, 4.69179541e-01,
401 7.42003713e-01, 2.24025196e-01, 4.67017774e-01,
402 7.48378107e-01, 2.26377345e-01, 4.64793954e-01,
403 7.54736692e-01, 2.28772352e-01, 4.62508534e-01,
404 7.61077312e-01, 2.31213625e-01, 4.60162106e-01,
405 7.67397681e-01, 2.33704708e-01, 4.57755411e-01,
406 7.73695380e-01, 2.36249283e-01, 4.55289354e-01,
407 7.79967847e-01, 2.38851170e-01, 4.52765022e-01,
408 7.86212372e-01, 2.41514325e-01, 4.50183695e-01,
409 7.92426972e-01, 2.44242250e-01, 4.47543155e-01,
410 7.98607760e-01, 2.47039798e-01, 4.44848441e-01,
411 8.04751511e-01, 2.49911350e-01, 4.42101615e-01,
412 8.10854841e-01, 2.52861399e-01, 4.39304963e-01,
413 8.16914186e-01, 2.55894550e-01, 4.36461074e-01,
414 8.22925797e-01, 2.59015505e-01, 4.33572874e-01,
415 8.28885740e-01, 2.62229049e-01, 4.30643647e-01,
416 8.34790818e-01, 2.65539703e-01, 4.27671352e-01,
417 8.40635680e-01, 2.68952874e-01, 4.24665620e-01,
418 8.46415804e-01, 2.72473491e-01, 4.21631064e-01,
419 8.52126490e-01, 2.76106469e-01, 4.18572767e-01,
420 8.57762870e-01, 2.79856666e-01, 4.15496319e-01,
421 8.63320397e-01, 2.83729003e-01, 4.12402889e-01,
422 8.68793368e-01, 2.87728205e-01, 4.09303002e-01,
423 8.74176342e-01, 2.91858679e-01, 4.06205397e-01,
424 8.79463944e-01, 2.96124596e-01, 4.03118034e-01,
425 8.84650824e-01, 3.00530090e-01, 4.00047060e-01,
426 8.89731418e-01, 3.05078817e-01, 3.97001559e-01,
427 8.94700194e-01, 3.09773445e-01, 3.93994634e-01,
428 8.99551884e-01, 3.14616425e-01, 3.91036674e-01,
429 9.04281297e-01, 3.19609981e-01, 3.88136889e-01,
430 9.08883524e-01, 3.24755126e-01, 3.85308008e-01,
431 9.13354091e-01, 3.30051947e-01, 3.82563414e-01,
432 9.17688852e-01, 3.35500068e-01, 3.79915138e-01,
433 9.21884187e-01, 3.41098112e-01, 3.77375977e-01,
434 9.25937102e-01, 3.46843685e-01, 3.74959077e-01,
435 9.29845090e-01, 3.52733817e-01, 3.72676513e-01,
436 9.33606454e-01, 3.58764377e-01, 3.70540883e-01,
437 9.37220874e-01, 3.64929312e-01, 3.68566525e-01,
438 9.40687443e-01, 3.71224168e-01, 3.66761699e-01,
439 9.44006448e-01, 3.77642889e-01, 3.65136328e-01,
440 9.47179528e-01, 3.84177874e-01, 3.63701130e-01,
441 9.50210150e-01, 3.90819546e-01, 3.62467694e-01,
442 9.53099077e-01, 3.97562894e-01, 3.61438431e-01,
443 9.55849237e-01, 4.04400213e-01, 3.60619076e-01,
444 9.58464079e-01, 4.11323666e-01, 3.60014232e-01,
445 9.60949221e-01, 4.18323245e-01, 3.59629789e-01,
446 9.63310281e-01, 4.25389724e-01, 3.59469020e-01,
447 9.65549351e-01, 4.32518707e-01, 3.59529151e-01,
448 9.67671128e-01, 4.39702976e-01, 3.59810172e-01,
449 9.69680441e-01, 4.46935635e-01, 3.60311120e-01,
450 9.71582181e-01, 4.54210170e-01, 3.61030156e-01,
451 9.73381238e-01, 4.61520484e-01, 3.61964652e-01,
452 9.75082439e-01, 4.68860936e-01, 3.63111292e-01,
453 9.76690494e-01, 4.76226350e-01, 3.64466162e-01,
454 9.78209957e-01, 4.83612031e-01, 3.66024854e-01,
455 9.79645181e-01, 4.91013764e-01, 3.67782559e-01,
456 9.81000291e-01, 4.98427800e-01, 3.69734157e-01,
457 9.82279159e-01, 5.05850848e-01, 3.71874301e-01,
458 9.83485387e-01, 5.13280054e-01, 3.74197501e-01,
459 9.84622298e-01, 5.20712972e-01, 3.76698186e-01,
460 9.85692925e-01, 5.28147545e-01, 3.79370774e-01,
461 9.86700017e-01, 5.35582070e-01, 3.82209724e-01,
462 9.87646038e-01, 5.43015173e-01, 3.85209578e-01,
463 9.88533173e-01, 5.50445778e-01, 3.88365009e-01,
464 9.89363341e-01, 5.57873075e-01, 3.91670846e-01,
465 9.90138201e-01, 5.65296495e-01, 3.95122099e-01,
466 9.90871208e-01, 5.72706259e-01, 3.98713971e-01,
467 9.91558165e-01, 5.80106828e-01, 4.02441058e-01,
468 9.92195728e-01, 5.87501706e-01, 4.06298792e-01,
469 9.92784669e-01, 5.94891088e-01, 4.10282976e-01,
470 9.93325561e-01, 6.02275297e-01, 4.14389658e-01,
471 9.93834412e-01, 6.09643540e-01, 4.18613221e-01,
472 9.94308514e-01, 6.16998953e-01, 4.22949672e-01,
473 9.94737698e-01, 6.24349657e-01, 4.27396771e-01,
474 9.95121854e-01, 6.31696376e-01, 4.31951492e-01,
475 9.95480469e-01, 6.39026596e-01, 4.36607159e-01,
476 9.95809924e-01, 6.46343897e-01, 4.41360951e-01,
477 9.96095703e-01, 6.53658756e-01, 4.46213021e-01,
478 9.96341406e-01, 6.60969379e-01, 4.51160201e-01,
479 9.96579803e-01, 6.68255621e-01, 4.56191814e-01,
480 9.96774784e-01, 6.75541484e-01, 4.61314158e-01,
481 9.96925427e-01, 6.82827953e-01, 4.66525689e-01,
482 9.97077185e-01, 6.90087897e-01, 4.71811461e-01,
483 9.97186253e-01, 6.97348991e-01, 4.77181727e-01,
484 9.97253982e-01, 7.04610791e-01, 4.82634651e-01,
485 9.97325180e-01, 7.11847714e-01, 4.88154375e-01,
486 9.97350983e-01, 7.19089119e-01, 4.93754665e-01,
487 9.97350583e-01, 7.26324415e-01, 4.99427972e-01,
488 9.97341259e-01, 7.33544671e-01, 5.05166839e-01,
489 9.97284689e-01, 7.40771893e-01, 5.10983331e-01,
490 9.97228367e-01, 7.47980563e-01, 5.16859378e-01,
491 9.97138480e-01, 7.55189852e-01, 5.22805996e-01,
492 9.97019342e-01, 7.62397883e-01, 5.28820775e-01,
493 9.96898254e-01, 7.69590975e-01, 5.34892341e-01,
494 9.96726862e-01, 7.76794860e-01, 5.41038571e-01,
495 9.96570645e-01, 7.83976508e-01, 5.47232992e-01,
496 9.96369065e-01, 7.91167346e-01, 5.53498939e-01,
497 9.96162309e-01, 7.98347709e-01, 5.59819643e-01,
498 9.95932448e-01, 8.05527126e-01, 5.66201824e-01,
499 9.95680107e-01, 8.12705773e-01, 5.72644795e-01,
500 9.95423973e-01, 8.19875302e-01, 5.79140130e-01,
501 9.95131288e-01, 8.27051773e-01, 5.85701463e-01,
502 9.94851089e-01, 8.34212826e-01, 5.92307093e-01,
503 9.94523666e-01, 8.41386618e-01, 5.98982818e-01,
504 9.94221900e-01, 8.48540474e-01, 6.05695903e-01,
505 9.93865767e-01, 8.55711038e-01, 6.12481798e-01,
506 9.93545285e-01, 8.62858846e-01, 6.19299300e-01,
507 9.93169558e-01, 8.70024467e-01, 6.26189463e-01,
508 9.92830963e-01, 8.77168404e-01, 6.33109148e-01,
509 9.92439881e-01, 8.84329694e-01, 6.40099465e-01,
510 9.92089454e-01, 8.91469549e-01, 6.47116021e-01,
511 9.91687744e-01, 8.98627050e-01, 6.54201544e-01,
512 9.91331929e-01, 9.05762748e-01, 6.61308839e-01,
513 9.90929685e-01, 9.12915010e-01, 6.68481201e-01,
514 9.90569914e-01, 9.20048699e-01, 6.75674592e-01,
515 9.90174637e-01, 9.27195612e-01, 6.82925602e-01,
516 9.89814839e-01, 9.34328540e-01, 6.90198194e-01,
517 9.89433736e-01, 9.41470354e-01, 6.97518628e-01,
518 9.89077438e-01, 9.48604077e-01, 7.04862519e-01,
519 9.88717064e-01, 9.55741520e-01, 7.12242232e-01,
520 9.88367028e-01, 9.62878026e-01, 7.19648627e-01,
521 9.88032885e-01, 9.70012413e-01, 7.27076773e-01,
522 9.87690702e-01, 9.77154231e-01, 7.34536205e-01,
523 9.87386827e-01, 9.84287561e-01, 7.42001547e-01,
524 9.87052509e-01, 9.91437853e-01, 7.49504188e-01,
525 }
526
527 // I'm using data straight from
528 // https://github.com/BIDS/colormap/blob/master/parula.py
529 var parulaData = [...]float64{
530 0.2081, 0.1663, 0.5292, 0.2116238095, 0.1897809524, 0.5776761905,
531 0.212252381, 0.2137714286, 0.6269714286, 0.2081, 0.2386, 0.6770857143,
532 0.1959047619, 0.2644571429, 0.7279, 0.1707285714, 0.2919380952,
533 0.779247619, 0.1252714286, 0.3242428571, 0.8302714286,
534 0.0591333333, 0.3598333333, 0.8683333333, 0.0116952381, 0.3875095238,
535 0.8819571429, 0.0059571429, 0.4086142857, 0.8828428571,
536 0.0165142857, 0.4266, 0.8786333333, 0.032852381, 0.4430428571,
537 0.8719571429, 0.0498142857, 0.4585714286, 0.8640571429,
538 0.0629333333, 0.4736904762, 0.8554380952, 0.0722666667, 0.4886666667,
539 0.8467, 0.0779428571, 0.5039857143, 0.8383714286,
540 0.079347619, 0.5200238095, 0.8311809524, 0.0749428571, 0.5375428571,
541 0.8262714286, 0.0640571429, 0.5569857143, 0.8239571429,
542 0.0487714286, 0.5772238095, 0.8228285714, 0.0343428571, 0.5965809524,
543 0.819852381, 0.0265, 0.6137, 0.8135, 0.0238904762, 0.6286619048,
544 0.8037619048, 0.0230904762, 0.6417857143, 0.7912666667,
545 0.0227714286, 0.6534857143, 0.7767571429, 0.0266619048, 0.6641952381,
546 0.7607190476, 0.0383714286, 0.6742714286, 0.743552381,
547 0.0589714286, 0.6837571429, 0.7253857143,
548 0.0843, 0.6928333333, 0.7061666667, 0.1132952381, 0.7015, 0.6858571429,
549 0.1452714286, 0.7097571429, 0.6646285714, 0.1801333333, 0.7176571429,
550 0.6424333333, 0.2178285714, 0.7250428571, 0.6192619048,
551 0.2586428571, 0.7317142857, 0.5954285714, 0.3021714286, 0.7376047619,
552 0.5711857143, 0.3481666667, 0.7424333333, 0.5472666667,
553 0.3952571429, 0.7459, 0.5244428571, 0.4420095238, 0.7480809524,
554 0.5033142857, 0.4871238095, 0.7490619048, 0.4839761905,
555 0.5300285714, 0.7491142857, 0.4661142857, 0.5708571429, 0.7485190476,
556 0.4493904762, 0.609852381, 0.7473142857, 0.4336857143,
557 0.6473, 0.7456, 0.4188, 0.6834190476, 0.7434761905, 0.4044333333,
558 0.7184095238, 0.7411333333, 0.3904761905,
559 0.7524857143, 0.7384, 0.3768142857, 0.7858428571, 0.7355666667,
560 0.3632714286, 0.8185047619, 0.7327333333, 0.3497904762,
561 0.8506571429, 0.7299, 0.3360285714, 0.8824333333, 0.7274333333, 0.3217,
562 0.9139333333, 0.7257857143, 0.3062761905, 0.9449571429, 0.7261142857,
563 0.2886428571, 0.9738952381, 0.7313952381, 0.266647619,
564 0.9937714286, 0.7454571429, 0.240347619, 0.9990428571, 0.7653142857,
565 0.2164142857, 0.9955333333, 0.7860571429, 0.196652381,
566 0.988, 0.8066, 0.1793666667, 0.9788571429, 0.8271428571, 0.1633142857,
567 0.9697, 0.8481380952, 0.147452381, 0.9625857143, 0.8705142857, 0.1309,
568 0.9588714286, 0.8949, 0.1132428571, 0.9598238095, 0.9218333333,
569 0.0948380952, 0.9661, 0.9514428571, 0.0755333333,
570 0.9763, 0.9831, 0.0538,
571 }
572
573 // I'm using data straight from
574 // https://github.com/matplotlib/cmocean/blob/master/cmocean/rgb/haline-rgb.txt
575 var halineData = [...]float64{
576 1.629529545569048110e-01, 9.521591660747855124e-02, 4.225729247643043585e-01,
577 1.648101130638113809e-01, 9.635115909727909322e-02, 4.318459659833655540e-01,
578 1.666161667445505146e-01, 9.744967053737302320e-02, 4.412064832719169161e-01,
579 1.683662394047173716e-01, 9.851521320092249123e-02, 4.506510991070378780e-01,
580 1.700547063176806595e-01, 9.955275459284393391e-02, 4.601751103492678907e-01,
581 1.716750780810941956e-01, 1.005687314559364776e-01, 4.697722208210775574e-01,
582 1.732198670017069397e-01, 1.015713570251385311e-01, 4.794342308257477092e-01,
583 1.746804342417165035e-01, 1.025709733421875103e-01, 4.891506793097686878e-01,
584 1.760433654254164593e-01, 1.035658402770499587e-01, 4.989416012077843576e-01,
585 1.772982333235153807e-01, 1.045802467658180357e-01, 5.087715885336102639e-01,
586 1.784322966250933284e-01, 1.056380265564063059e-01, 5.186108302832771466e-01,
587 1.794226692010022772e-01, 1.067416562108134404e-01, 5.284836071020164727e-01,
588 1.802542327126359922e-01, 1.079356346679062328e-01, 5.383245681077661882e-01,
589 1.808975365813079994e-01, 1.092386640641496154e-01, 5.481352134375515606e-01,
590 1.813298273265454008e-01, 1.107042924622455293e-01, 5.578435355461390799e-01,
591 1.815069308605478937e-01, 1.123613365530294061e-01, 5.674471854200233700e-01,
592 1.813959559086370799e-01, 1.142804413027345978e-01, 5.768505865319291104e-01,
593 1.809499433760710929e-01, 1.165251530113385336e-01, 5.859821014031293407e-01,
594 1.801166524094891808e-01, 1.191682999758127970e-01, 5.947494236872948870e-01,
595 1.788419557731087683e-01, 1.222886104999623413e-01, 6.030366129604394221e-01,
596 1.770751344832933727e-01, 1.259620672997293078e-01, 6.107077426144936760e-01,
597 1.747764954226868062e-01, 1.302486445940692350e-01, 6.176174300439590814e-01,
598 1.719255883800615836e-01, 1.351768519397535118e-01, 6.236290832033221099e-01,
599 1.685302279919113078e-01, 1.407308818346016399e-01, 6.286357211183263294e-01,
600 1.646373543798159977e-01, 1.468433194330099889e-01, 6.325796572366660930e-01,
601 1.603141656593721487e-01, 1.534074847391770635e-01, 6.354701889106297852e-01,
602 1.556539455727427579e-01, 1.602911795924207572e-01, 6.373742153046678682e-01,
603 1.507373567977903506e-01, 1.673688895313445446e-01, 6.383989700654711941e-01,
604 1.456427577979826360e-01, 1.745293312408868480e-01, 6.386687569056349600e-01,
605 1.404368075255880977e-01, 1.816841459042554952e-01, 6.383089542091028301e-01,
606 1.351726504089350855e-01, 1.887688275072176014e-01, 6.374350053971095109e-01,
607 1.298906561807787186e-01, 1.957398580438490798e-01, 6.361469852044080442e-01,
608 1.246205125693149729e-01, 2.025703385486158914e-01, 6.345282558695404251e-01,
609 1.193859004780570554e-01, 2.092446623034395214e-01, 6.326478270215730726e-01,
610 1.142294912197052703e-01, 2.157456284251405010e-01, 6.305768690676523125e-01,
611 1.091404911375367659e-01, 2.220831206181900774e-01, 6.283455167242665285e-01,
612 1.041438584244326337e-01, 2.282546518282705383e-01, 6.259979600528258192e-01,
613 9.926304855671816418e-02, 2.342609767388125763e-01, 6.235717761795677161e-01,
614 9.449512580805050077e-02, 2.401139958170242505e-01, 6.210816676451920149e-01,
615 8.986951574154733446e-02, 2.458147193889331228e-01, 6.185591936304666305e-01,
616 8.539285840535987271e-02, 2.513729453557033144e-01, 6.160166295227810229e-01,
617 8.106756674391193962e-02, 2.567997050291829786e-01, 6.134596708213713168e-01,
618 7.694418932732069449e-02, 2.620915612909199277e-01, 6.109238301118911085e-01,
619 7.300703739422578775e-02, 2.672655035330154250e-01, 6.083965011432549419e-01,
620 6.927650669442811382e-02, 2.723273008096985248e-01, 6.058873223830183452e-01,
621 6.578801445169751849e-02, 2.772789491245566951e-01, 6.034141752438300088e-01,
622 6.255595479554787453e-02, 2.821282390686886132e-01, 6.009787922718963227e-01,
623 5.959205181913470456e-02, 2.868831717247524726e-01, 5.985797542127682114e-01,
624 5.691772151374491912e-02, 2.915488716281217640e-01, 5.962214962176209943e-01,
625 5.455347307306369214e-02, 2.961302536799313989e-01, 5.939076044300871660e-01,
626 5.251889627870443694e-02, 3.006318409387543356e-01, 5.916414807294642086e-01,
627 5.083877347247430650e-02, 3.050562292020492783e-01, 5.894315315373579445e-01,
628 4.951454037014189208e-02, 3.094102218220906031e-01, 5.872714908845412252e-01,
629 4.855490408104565919e-02, 3.136977658751046727e-01, 5.851627302038014955e-01,
630 4.796369156225028380e-02, 3.179225992994973993e-01, 5.831062484926557987e-01,
631 4.773946305380068894e-02, 3.220882581897950847e-01, 5.811027291021745311e-01,
632 4.787545181415154422e-02, 3.261980852298422273e-01, 5.791525884087008746e-01,
633 4.835984720504159923e-02, 3.302552388254387794e-01, 5.772560174768572860e-01,
634 4.917638757411300215e-02, 3.342627026038174076e-01, 5.754130176762238813e-01,
635 5.030518671680884318e-02, 3.382232950310170572e-01, 5.736234310853615126e-01,
636 5.172369283691292258e-02, 3.421396789632700219e-01, 5.718869664041401624e-01,
637 5.340767549062541003e-02, 3.460143709989857430e-01, 5.702032209969470911e-01,
638 5.533215100674954839e-02, 3.498497505368459159e-01, 5.685716996038682192e-01,
639 5.747218306369886870e-02, 3.536480684754851334e-01, 5.669918301827847618e-01,
640 5.980352430527090951e-02, 3.574114555130643578e-01, 5.654629772812437283e-01,
641 6.230309069250609261e-02, 3.611419300223894235e-01, 5.639844532816007394e-01,
642 6.494927925026378057e-02, 3.648414054902228698e-01, 5.625555278152573058e-01,
643 6.772215122553368327e-02, 3.685116975190721456e-01, 5.611754356007422340e-01,
644 7.060350747784929770e-02, 3.721545303967777607e-01, 5.598433829250933913e-01,
645 7.357840396611611822e-02, 3.757710087912914942e-01, 5.585612898846836760e-01,
646 7.663101364217325684e-02, 3.793629991309988569e-01, 5.573268773673928367e-01,
647 7.974739830752586300e-02, 3.829322364932883360e-01, 5.561380319387883020e-01,
648 8.291580548873803136e-02, 3.864801413799028307e-01, 5.549938581786431069e-01,
649 8.612580955679122185e-02, 3.900080689665953448e-01, 5.538934528024703763e-01,
650 8.936817980172057085e-02, 3.935173134989205512e-01, 5.528359060062189023e-01,
651 9.263475301781654014e-02, 3.970091123550867351e-01, 5.518203023495191761e-01,
652 9.591831391237073956e-02, 4.004846497947374129e-01, 5.508457212473490960e-01,
653 9.921323508992196949e-02, 4.039446967979293257e-01, 5.499133654313189679e-01,
654 1.025139614653171327e-01, 4.073902646845309894e-01, 5.490228475752887416e-01,
655 1.058144853522852702e-01, 4.108228877965505177e-01, 5.481703859285301794e-01,
656 1.091104131658914012e-01, 4.142435690118807523e-01, 5.473550236322454188e-01,
657 1.123978495089298091e-01, 4.176532725696484594e-01, 5.465757987301745890e-01,
658 1.156733362160069500e-01, 4.210529263321469151e-01, 5.458317432175192607e-01,
659 1.189341701380315364e-01, 4.244432161011251203e-01, 5.451231873866256850e-01,
660 1.221781892974011241e-01, 4.278246691926565481e-01, 5.444513010684173260e-01,
661 1.254018943745988379e-01, 4.311987106338182607e-01, 5.438113725374379426e-01,
662 1.286031296249826039e-01, 4.345661396549306832e-01, 5.432023919026625070e-01,
663 1.317799711665625373e-01, 4.379277275711909168e-01, 5.426233385794481112e-01,
664 1.349307019221451520e-01, 4.412842189714309415e-01, 5.420731800699918335e-01,
665 1.380549051543558114e-01, 4.446356900078314855e-01, 5.415550926551251365e-01,
666 1.411501069188544899e-01, 4.479834819017672332e-01, 5.410638066180113448e-01,
667 1.442150210041465985e-01, 4.513283116704150388e-01, 5.405979268760919831e-01,
668 1.472485820103837661e-01, 4.546708245755168298e-01, 5.401563599016087069e-01,
669 1.502499341655997578e-01, 4.580115969309521140e-01, 5.397383042732707414e-01,
670 1.532192022679761678e-01, 4.613507130792227628e-01, 5.393460827274304537e-01,
671 1.561545863558880531e-01, 4.646893652629560667e-01, 5.389744586257710912e-01,
672 1.590554825896313418e-01, 4.680281092699005163e-01, 5.386222668134232894e-01,
673 1.619213854747377779e-01, 4.713674796485894380e-01, 5.382883230992108192e-01,
674 1.647524478987731911e-01, 4.747077026242289555e-01, 5.379733478625169374e-01,
675 1.675482630437002962e-01, 4.780493319379570116e-01, 5.376757027790604049e-01,
676 1.703080688452488778e-01, 4.813931150452205876e-01, 5.373922894579649112e-01,
677 1.730317245949949956e-01, 4.847395013664480001e-01, 5.371218460871988176e-01,
678 1.757193664968157432e-01, 4.880888303994977417e-01, 5.368636833242294015e-01,
679 1.783716503259393793e-01, 4.914412289641479359e-01, 5.366183612997760255e-01,
680 1.809878167761821144e-01, 4.947975030047193079e-01, 5.363817525707026412e-01,
681 1.835680719416625251e-01, 4.981580161432020426e-01, 5.361525222751042374e-01,
682 1.861127112291278973e-01, 5.015231101060642072e-01, 5.359293193132266264e-01,
683 1.886230405888700001e-01, 5.048927132825591357e-01, 5.357133548543864254e-01,
684 1.910985251486422565e-01, 5.082675669534003626e-01, 5.355003101591436776e-01,
685 1.935397227717326196e-01, 5.116479477164066481e-01, 5.352887824024628038e-01,
686 1.959472883340568350e-01, 5.150341080561433582e-01, 5.350773667248226451e-01,
687 1.983227076664061950e-01, 5.184259856373716335e-01, 5.348665525352744865e-01,
688 2.006660342507454731e-01, 5.218241099237964642e-01, 5.346528031121534630e-01,
689 2.029781360478647434e-01, 5.252286912572143862e-01, 5.344345169358406533e-01,
690 2.052600444742044838e-01, 5.286398903414566419e-01, 5.342102605335536936e-01,
691 2.075134652380221101e-01, 5.320576287402744020e-01, 5.339799959048032729e-01,
692 2.097389494614402272e-01, 5.354822793223580346e-01, 5.337406085215398166e-01,
693 2.119377691272176234e-01, 5.389139515314046447e-01, 5.334905459074957834e-01,
694 2.141113437844118228e-01, 5.423527133674995726e-01, 5.332283775266504211e-01,
695 2.162615771757636085e-01, 5.457984712571943842e-01, 5.329535750363618707e-01,
696 2.183895286205785324e-01, 5.492514494924778390e-01, 5.326634150194080597e-01,
697 2.204969036245257863e-01, 5.527116487772890663e-01, 5.323564832742772035e-01,
698 2.225855272635121618e-01, 5.561790393438251767e-01, 5.320314302436226495e-01,
699 2.246573660062902156e-01, 5.596535532346759156e-01, 5.316870266023980829e-01,
700 2.267141352300188206e-01, 5.631352211554988552e-01, 5.313212748929951879e-01,
701 2.287579175696445311e-01, 5.666239548700177098e-01, 5.309328290550865415e-01,
702 2.307908679682200148e-01, 5.701196510565367248e-01, 5.305203252817071169e-01,
703 2.328150701112509102e-01, 5.736222405040750649e-01, 5.300820599240353426e-01,
704 2.348328561421904603e-01, 5.771315766359990107e-01, 5.296167282704775658e-01,
705 2.368466495707550745e-01, 5.806474896434283828e-01, 5.291230766564496424e-01,
706 2.388588283644081933e-01, 5.841698333340915594e-01, 5.285995915811123602e-01,
707 2.408716822240541400e-01, 5.876984999166237067e-01, 5.280443921862176815e-01,
708 2.428880608722543410e-01, 5.912331932277818947e-01, 5.274567814051942527e-01,
709 2.449106759672304290e-01, 5.947736703172152861e-01, 5.268356212312692577e-01,
710 2.469420290965465559e-01, 5.983197676291379663e-01, 5.261791312000029253e-01,
711 2.489846702882144713e-01, 6.018713111492172141e-01, 5.254854952988164962e-01,
712 2.510418446331669773e-01, 6.054278820406324702e-01, 5.247545535679302153e-01,
713 2.531164830585315162e-01, 6.089891735215487989e-01, 5.239852980594518206e-01,
714 2.552111567013787274e-01, 6.125550130731924892e-01, 5.231756545447472373e-01,
715 2.573286340449815746e-01, 6.161251617120727664e-01, 5.223239185167128928e-01,
716 2.594724447386158594e-01, 6.196990932330638246e-01, 5.214304767886038805e-01,
717 2.616456589963800927e-01, 6.232764459181282524e-01, 5.204944566826074093e-01,
718 2.638509342960791981e-01, 6.268570181766053295e-01, 5.195136536074571598e-01,
719 2.660910828965943331e-01, 6.304405546707460006e-01, 5.184861377534477622e-01,
720 2.683698467163787016e-01, 6.340264147511259774e-01, 5.174130230514244477e-01,
721 2.706903509949471487e-01, 6.376141889650659422e-01, 5.162935692788781505e-01,
722 2.730554221838172868e-01, 6.412035896296301996e-01, 5.151259285643695618e-01,
723 2.754676310512871873e-01, 6.447944545228798674e-01, 5.139069764892589820e-01,
724 2.779309010023177096e-01, 6.483859905965968506e-01, 5.126390269795514376e-01,
725 2.804483318408275694e-01, 6.519777450281342146e-01, 5.113214639163841113e-01,
726 2.830229969716622218e-01, 6.555692556652558123e-01, 5.099537040511894492e-01,
727 2.856569925022096612e-01, 6.591606030439277619e-01, 5.085297306531970651e-01,
728 2.883543502639873135e-01, 6.627507929293073863e-01, 5.070537569679475220e-01,
729 2.911180907975667309e-01, 6.663393219020087299e-01, 5.055253556302098383e-01,
730 2.939511790083492726e-01, 6.699256847308838747e-01, 5.039440536705714901e-01,
731 2.968562131418951422e-01, 6.735096490751359966e-01, 5.023062065413991251e-01,
732 2.998362114418811064e-01, 6.770906951012128916e-01, 5.006109626120457401e-01,
733 3.028943961763405635e-01, 6.806680059292820051e-01, 4.988609220555944579e-01,
734 3.060335830069556007e-01, 6.842410278074210206e-01, 4.970557164988521071e-01,
735 3.092565373453909361e-01, 6.878091952372648032e-01, 4.951950035800954386e-01,
736 3.125659486948474952e-01, 6.913723055472413836e-01, 4.932732004330610542e-01,
737 3.159647522698377231e-01, 6.949295261702347348e-01, 4.912927476409480465e-01,
738 3.194556489243873809e-01, 6.984800979280558764e-01, 4.892553378582480961e-01,
739 3.230412442793148542e-01, 7.020233920397538352e-01, 4.871607422199746851e-01,
740 3.267240985578743762e-01, 7.055587631720373620e-01, 4.850087606010107799e-01,
741 3.305070751488592418e-01, 7.090857414439620809e-01, 4.827955626459811689e-01,
742 3.343929739199408280e-01, 7.126036449951604901e-01, 4.805201317683439055e-01,
743 3.383839618515475101e-01, 7.161115318028217214e-01, 4.781864627637871235e-01,
744 3.424824761777380822e-01, 7.196086676710338192e-01, 4.757945201032026117e-01,
745 3.466909265050957534e-01, 7.230942938245445983e-01, 4.733443136156250675e-01,
746 3.510117003671430203e-01, 7.265676248366644829e-01, 4.708359034900377327e-01,
747 3.554477481971078934e-01, 7.300279237012574640e-01, 4.682667163182038794e-01,
748 3.600022066505755847e-01, 7.334743741555846963e-01, 4.656343331651458528e-01,
749 3.646764457841936702e-01, 7.369059239634064840e-01, 4.629441295765394648e-01,
750 3.694728210602395979e-01, 7.403216514270205550e-01, 4.601965063235068931e-01,
751 3.743936915522128039e-01, 7.437205957454258165e-01, 4.573919681273960758e-01,
752 3.794414259131371203e-01, 7.471017539541063845e-01, 4.545311394783269621e-01,
753 3.846184079557829483e-01, 7.504640777072076885e-01, 4.516147832933739559e-01,
754 3.899270416022538321e-01, 7.538064699246833644e-01, 4.486438228496626990e-01,
755 3.953697549145612777e-01, 7.571277813375660859e-01, 4.456193674826627871e-01,
756 4.009508646485598904e-01, 7.604267477869489644e-01, 4.425380639654961090e-01,
757 4.066714019599443342e-01, 7.637021069222051928e-01, 4.394056497009877216e-01,
758 4.125334807152798988e-01, 7.669525443587899005e-01, 4.362249727525224774e-01,
759 4.185395667527040398e-01, 7.701766751192415938e-01, 4.329983295596441795e-01,
760 4.246921350385852723e-01, 7.733730477083216037e-01, 4.297284080553480656e-01,
761 4.309936593663836191e-01, 7.765401418648604226e-01, 4.264183470604332449e-01,
762 4.374465975304094312e-01, 7.796763669997491819e-01, 4.230718037095967943e-01,
763 4.440533711015541840e-01, 7.827800615723168320e-01, 4.196930295353452078e-01,
764 4.508163388346139722e-01, 7.858494937119429036e-01, 4.162869557148224930e-01,
765 4.577377626480225170e-01, 7.888828634531382944e-01, 4.128592877810690620e-01,
766 4.648197650471433406e-01, 7.918783070193569085e-01, 4.094166097874233912e-01,
767 4.720642768256655963e-01, 7.948339036614172626e-01, 4.059664974607166132e-01,
768 4.794729738954752185e-01, 7.977476856267914362e-01, 4.025176392507668344e-01,
769 4.870472021865835388e-01, 8.006176519006481529e-01, 3.990799633442549399e-01,
770 4.947878897554374711e-01, 8.034417864099652196e-01, 3.956647676266520364e-01,
771 5.026954455748450235e-01, 8.062180814071850943e-01, 3.922848482216537147e-01,
772 5.107722312752203120e-01, 8.089441566688712060e-01, 3.889513459863399025e-01,
773 5.190175807229889804e-01, 8.116180108735555621e-01, 3.856804341377490508e-01,
774 5.274270476594079549e-01, 8.142382455983395717e-01, 3.824934902796895964e-01,
775 5.359974387485314518e-01, 8.168032862890818313e-01, 3.794106529717772847e-01,
776 5.447242959015131669e-01, 8.193118012377970105e-01, 3.764539238160731771e-01,
777 5.536017493685388979e-01, 8.217627673204914718e-01, 3.736470734604069865e-01,
778 5.626223858732353200e-01, 8.241555398979113489e-01, 3.710154595070918049e-01,
779 5.717801732913297963e-01, 8.264893011624766528e-01, 3.685830453573430421e-01,
780 5.810619798273547465e-01, 8.287648131945639651e-01, 3.663798607459672341e-01,
781 5.904522695833789303e-01, 8.309836316507100973e-01, 3.644363382969363352e-01,
782 5.999363056699199559e-01, 8.331474672067843423e-01, 3.627802030453921023e-01,
783 6.094978370184862548e-01, 8.352587020450518152e-01, 3.614380620192426119e-01,
784 6.191178753985615568e-01, 8.373207419267220120e-01, 3.604354982890065617e-01,
785 6.287753075069072439e-01, 8.393379672623436649e-01, 3.597956834322972863e-01,
786 6.384515865712356852e-01, 8.413146218129586851e-01, 3.595361765343614291e-01,
787 6.481275660781142811e-01, 8.432555569199260415e-01, 3.596707668706327632e-01,
788 6.577845458065525452e-01, 8.451659780810962808e-01, 3.602086612732601778e-01,
789 6.674047070379289792e-01, 8.470513147981415525e-01, 3.611542742755425861e-01,
790 6.769617561788032756e-01, 8.489198363378159806e-01, 3.625096347273936148e-01,
791 6.864487597795673191e-01, 8.507748949282539774e-01, 3.642665641101618390e-01,
792 6.958544314564799604e-01, 8.526212799355240568e-01, 3.664154684319869681e-01,
793 7.051685608468114541e-01, 8.544637256207057163e-01, 3.689436786046771388e-01,
794 7.143830272263600456e-01, 8.563065614925247093e-01, 3.718358172740615641e-01,
795 7.234917624309317175e-01, 8.581536540348341235e-01, 3.750744951817871486e-01,
796 7.324906484647774052e-01, 8.600083717860309562e-01, 3.786409937540124448e-01,
797 7.413773645706981386e-01, 8.618735721244006331e-01, 3.825158976261986421e-01,
798 7.501511992657796668e-01, 8.637516069265696039e-01, 3.866796514151401021e-01,
799 7.588128421172509741e-01, 8.656443435632366068e-01, 3.911130257986148440e-01,
800 7.673641682613402404e-01, 8.675531974405399360e-01, 3.957974875667232828e-01,
801 7.758080262912036007e-01, 8.694791724028596569e-01, 4.007154759905482422e-01,
802 7.841480375461038488e-01, 8.714229056785223193e-01, 4.058505933213660266e-01,
803 7.923777293356836227e-01, 8.733886508286130557e-01, 4.111824877427081026e-01,
804 8.004976303968860396e-01, 8.753781889640523950e-01, 4.166935560611597644e-01,
805 8.085242857272323391e-01, 8.773871783186646400e-01, 4.223760515178233144e-01,
806 8.164630734789560806e-01, 8.794151844938148388e-01, 4.282186311869483064e-01,
807 8.243194501554683695e-01, 8.814616081475756815e-01, 4.342112747863317024e-01,
808 8.320860387263339097e-01, 8.835308362945183402e-01, 4.403358497380424619e-01,
809 8.397544323444476877e-01, 8.856278479633188372e-01, 4.465723185371322512e-01,
810 8.473543986252204396e-01, 8.877421380157632935e-01, 4.529306634208433713e-01,
811 8.548913210362114601e-01, 8.898726582646793171e-01, 4.594053245849991085e-01,
812 8.623409541601502193e-01, 8.920307544658760968e-01, 4.659650403812060637e-01,
813 8.697191661117472661e-01, 8.942111604773197442e-01, 4.726125804953009157e-01,
814 8.770479480763140323e-01, 8.964055876253053112e-01, 4.793596015320920611e-01,
815 8.843061388708378656e-01, 8.986242192828276520e-01, 4.861771826919926709e-01,
816 8.914967901846199139e-01, 9.008669432468144889e-01, 4.930589607663434237e-01,
817 8.986506618699680038e-01, 9.031210853874221955e-01, 5.000298200873778409e-01,
818 9.057328617844134788e-01, 9.054032588350297006e-01, 5.070444917818385244e-01,
819 9.127681739145864226e-01, 9.077032841253237505e-01, 5.141229319104883011e-01,
820 9.197719823668246697e-01, 9.100148294768658497e-01, 5.212774789419143406e-01,
821 9.266999503758117651e-01, 9.123594905222979223e-01, 5.284479861571415027e-01,
822 9.336093927737403320e-01, 9.147111822755604749e-01, 5.356989519972219504e-01,
823 9.404610328906413130e-01, 9.170893351552455997e-01, 5.429753047678268496e-01,
824 9.472803518326599059e-01, 9.194825361628593541e-01, 5.503044468166357062e-01,
825 9.540659681238262690e-01, 9.218921210725944393e-01, 5.576799909240343078e-01,
826 9.608049809199471492e-01, 9.243252266483533708e-01, 5.650790057480892248e-01,
827 9.675287370768704820e-01, 9.267668902399696096e-01, 5.725413863443260531e-01,
828 9.741967269037244970e-01, 9.292382142036349491e-01, 5.800041593344547053e-01,
829 9.808627042040826138e-01, 9.317124815536732552e-01, 5.875425838151492330e-01,
830 9.874684104099172854e-01, 9.342202886448683907e-01, 5.950648878797101249e-01,
831 9.940805805099582892e-01, 9.367275819156850591e-01, 6.026699962989522374e-01,
832 }
File: ./colorplus/scales.go
1 package colorplus
2
3 import (
4 "image/color"
5 "math"
6 )
7
8 // VegaHex is a hexadecimal-notation categorical color palette with 10 entries.
9 var VegaHex = []string{
10 "#1f77b4", "#ff7f0e", "#2ca02c", "#d62728", "#9467bd",
11 "#8c564b", "#e377c2", "#7f7f7f", "#bcbd22", "#17becf",
12 }
13
14 // Wrap linearly interpolates the number given into the range [0...1]: the
15 // `min` and `max` parameters refer to the min/max values the input can take.
16 //
17 // Its results will work with the colorscale funcs in this package, unless
18 // the inputs are NaN.
19 func Wrap(x float64, min, max float64) float64 {
20 return (x - min) / (max - min)
21 }
22
23 // AnchoredWrap is like Wrap, except it ensures the source domain includes 0:
24 // anchoring to or around 0 allows results to be proportionally comparable.
25 //
26 // As with func Wrap, use its results as inputs for the colorscale funcs.
27 func AnchoredWrap(x float64, min, max float64) float64 {
28 return Wrap(x, math.Max(min, 0), math.Min(max, 1))
29 }
30
31 // Viridize turns a normalized (0..1) number into its Viridis color representation.
32 // The color returned is always full-alpha, except when input isn't valid: in that
33 // case, the result's alpha is 0.
34 func Viridize(x float64) color.RGBA {
35 return interpolate(x, viridisData[:])
36 }
37
38 // Magmify turns a normalized (0..1) number into its Magma color representation.
39 // The color returned is always full-alpha, except when input isn't valid: in that
40 // case, the result's alpha is 0.
41 func Magmify(x float64) color.RGBA {
42 return interpolate(x, magmaData[:])
43 }
44
45 // Parulate turns a normalized (0..1) number into its Parula color representation,
46 // the same one used in modern MatLab. The color returned is always full-alpha,
47 // except when input isn't valid: in that case, the result's alpha is 0.
48 func Parulate(x float64) color.RGBA {
49 return interpolate(x, parulaData[:])
50 }
51
52 // Halinate turns a normalized (0..1) number into its Parula color representation,
53 // the same one used in matplotlib/cmocean. The color returned is always full-alpha,
54 // except when input isn't valid: in that case, the result's alpha is 0.
55 func Halinate(x float64) color.RGBA {
56 return interpolate(x, halineData[:])
57 }
58
59 // turn a normalized (0-to-1) number into its color representation according to
60 // the color-scale coefficients given
61 func interpolate(x float64, v []float64) color.RGBA {
62 if math.IsNaN(x) || x < 0 || x > 1 {
63 return color.RGBA{R: 0, G: 0, B: 0, A: 0}
64 }
65
66 max := float64((len(v) - 1) / 3)
67 // get indices of the first color components (the reds) of the colors to mix
68 mid := max * x
69 low := int(math.Floor(mid))
70 high := int(math.Ceil(mid))
71
72 k := mid - float64(low) // interpolation factor for the 2 surrounding colors
73 c := 1 - k // the complement of k
74 l := 3 * low
75 h := 3 * high
76
77 return color.RGBA{
78 R: uint8(math.Round(255 * (c*v[l+0] + k*v[h+0]))),
79 G: uint8(math.Round(255 * (c*v[l+1] + k*v[h+1]))),
80 B: uint8(math.Round(255 * (c*v[l+2] + k*v[h+2]))),
81 A: 255,
82 }
83 }
File: ./colorplus/scales_test.go
1 package colorplus
2
3 import (
4 "math"
5 "testing"
6 )
7
8 func TestInterpolate(t *testing.T) {
9 var tests = []struct {
10 name string
11 value float64
12 scale []float64
13 }{
14 {`viridis 0`, 0, viridisData[:]},
15 {`viridis 1`, 1, viridisData[:]},
16 {`magma 0`, 0, magmaData[:]},
17 {`magma 1`, 1, magmaData[:]},
18 }
19
20 for _, tc := range tests {
21 t.Run(tc.name, func(t *testing.T) {
22 i, j := interp(tc.value, tc.scale)
23
24 if i < 0 || i >= len(tc.scale) {
25 const fs = `invalid index i %d is outside range 0..%d`
26 t.Fatalf(fs, i, len(tc.scale)-1)
27 }
28 if j < 0 || j >= len(tc.scale) {
29 const fs = `invalid index j %d is outside range 0..%d`
30 t.Fatalf(fs, j, len(tc.scale)-1)
31 }
32 })
33 }
34 }
35
36 func interp(x float64, v []float64) (int, int) {
37 max := float64((len(v) - 1) / 3)
38 // get the indices of the first color components of the colors to mix
39 mid := max * x
40 low := int(math.Floor(mid))
41 high := int(math.Ceil(mid))
42
43 k := mid - float64(low) // interpolation factor for the 2 surrounding colors
44 c := 1 - k // the complement of k
45 i := 3 * low
46 j := 3 * high
47 _ = c
48 return i, j
49 }
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: ./decsv/decsv.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 decsv
26
27 import (
28 "bufio"
29 "encoding/csv"
30 "encoding/json"
31 "errors"
32 "io"
33 "os"
34 "strings"
35 "unicode"
36 )
37
38 const info = `
39 decsv [options...] [filepath...]
40
41
42 This cmd-line app turns CSV (comma-separated values) data into either TSV
43 (tab-separated values), JSONS (JSON Strings), or general JSON (JavaScript
44 Object Notation).
45
46 When not given a filepath, the input is read from the standard input.
47
48 Options, when given, can either start with a single or a double-dash:
49
50 -h, -help show this help message
51 -json emit JSON, where numbers are auto-detected
52 -jsonl emit JSON Lines, where numbers are auto-detected
53 -jsons emit JSON Strings, where object values are strings or null
54 -tsv emit TSV (tab-separated values) lines
55 `
56
57 // handler is the type all CSV-converter funcs adhere to
58 type handler func(*bufio.Writer, *csv.Reader) error
59
60 var handlers = map[string]handler{
61 `-json`: emitJSON,
62 `--json`: emitJSON,
63 `-jsonl`: emitJSONL,
64 `--jsonl`: emitJSONL,
65 `-jsons`: emitJSONS,
66 `--jsons`: emitJSONS,
67 `-tsv`: emitTSV,
68 `--tsv`: emitTSV,
69 }
70
71 func Main() {
72 emit := emitTSV
73 buffered := false
74 args := os.Args[1:]
75
76 for len(args) > 0 {
77 switch args[0] {
78 case `-b`, `--b`, `-buffered`, `--buffered`:
79 buffered = true
80 args = args[1:]
81 continue
82
83 case `-h`, `--h`, `-help`, `--help`:
84 os.Stdout.WriteString(info[1:])
85 return
86 }
87
88 if v, ok := handlers[args[0]]; ok {
89 emit = v
90 args = args[1:]
91 continue
92 }
93
94 break
95 }
96
97 if len(args) > 0 && args[0] == `--` {
98 args = args[1:]
99 }
100
101 if len(args) > 1 {
102 os.Stdout.WriteString(info[1:])
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 path := `-`
114 if len(args) > 0 {
115 path = args[0]
116 }
117
118 if err := run(os.Stdout, path, emit, liveLines); err != nil {
119 if err == io.EOF {
120 return
121 }
122
123 os.Stderr.WriteString(err.Error())
124 os.Stderr.WriteString("\n")
125 os.Exit(1)
126 }
127 }
128
129 func run(w io.Writer, path string, handle handler, live bool) error {
130 bw := bufio.NewWriter(w)
131 defer bw.Flush()
132
133 if path == `-` {
134 return handle(bw, makeRowReader(os.Stdin))
135 }
136
137 f, err := os.Open(path)
138 if err != nil {
139 // on windows, file-not-found error messages may mention `CreateFile`,
140 // even when trying to open files in read-only mode
141 return errors.New(`can't open file named ` + path)
142 }
143 defer f.Close()
144
145 return handle(bw, makeRowReader(f))
146 }
147
148 func emitJSON(w *bufio.Writer, rr *csv.Reader) error {
149 got := 0
150 var keys []string
151
152 err := loopCSV(rr, func(i int, row []string) error {
153 got++
154
155 if i == 0 {
156 keys = make([]string, 0, len(row))
157 for _, s := range row {
158 keys = append(keys, strings.Clone(s))
159 }
160 return nil
161 }
162
163 if i == 1 {
164 w.WriteByte('[')
165 } else {
166 err := w.WriteByte(',')
167 if err != nil {
168 return io.EOF
169 }
170 }
171
172 w.WriteByte('{')
173 for i, s := range row {
174 if i > 0 {
175 w.WriteByte(',')
176 }
177
178 if numberLike(s) {
179 w.WriteByte('"')
180 writeInnerStringJSON(w, keys[i])
181 w.WriteString(`":`)
182 w.WriteString(s)
183 continue
184 }
185
186 writeKV(w, keys[i], s)
187 }
188
189 for i := len(row); i < len(keys); i++ {
190 if i > 0 {
191 w.WriteByte(',')
192 }
193 w.WriteByte('"')
194 writeInnerStringJSON(w, keys[i])
195 w.WriteString(`":null`)
196 }
197 w.WriteByte('}')
198
199 return nil
200 })
201
202 if err != nil {
203 return err
204 }
205
206 if got > 1 {
207 w.WriteString("]\n")
208 }
209 return nil
210 }
211
212 func emitJSONL(w *bufio.Writer, rr *csv.Reader) error {
213 var keys []string
214
215 return loopCSV(rr, func(i int, row []string) error {
216 if i == 0 {
217 keys = make([]string, 0, len(row))
218 for _, s := range row {
219 c := string(append([]byte{}, s...))
220 keys = append(keys, c)
221 }
222 return nil
223 }
224
225 w.WriteByte('{')
226 for i, s := range row {
227 if i > 0 {
228 w.WriteByte(',')
229 w.WriteByte(' ')
230 }
231
232 if numberLike(s) {
233 w.WriteByte('"')
234 writeInnerStringJSON(w, keys[i])
235 w.WriteString(`": `)
236 w.WriteString(s)
237 continue
238 }
239
240 writeKV(w, keys[i], s)
241 }
242
243 for i := len(row); i < len(keys); i++ {
244 if i > 0 {
245 w.WriteByte(',')
246 w.WriteByte(' ')
247 }
248 w.WriteByte('"')
249 writeInnerStringJSON(w, keys[i])
250 w.WriteString(`": null`)
251 }
252 w.WriteByte('}')
253
254 w.WriteByte('\n')
255 if err := w.Flush(); err != nil {
256 return io.EOF
257 }
258 return nil
259 })
260 }
261
262 func emitJSONS(w *bufio.Writer, rr *csv.Reader) error {
263 got := 0
264 var keys []string
265
266 err := loopCSV(rr, func(i int, row []string) error {
267 got++
268
269 if i == 0 {
270 keys = make([]string, 0, len(row))
271 for _, s := range row {
272 c := string(append([]byte{}, s...))
273 keys = append(keys, c)
274 }
275 return nil
276 }
277
278 if i == 1 {
279 w.WriteByte('[')
280 } else {
281 err := w.WriteByte(',')
282 if err != nil {
283 return io.EOF
284 }
285 }
286
287 w.WriteByte('{')
288 for i, s := range row {
289 if i > 0 {
290 w.WriteByte(',')
291 }
292 writeKV(w, keys[i], s)
293 }
294
295 for i := len(row); i < len(keys); i++ {
296 if i > 0 {
297 w.WriteByte(',')
298 }
299 w.WriteByte('"')
300 writeInnerStringJSON(w, keys[i])
301 w.WriteString(`":null`)
302 }
303 w.WriteByte('}')
304
305 return nil
306 })
307
308 if err != nil {
309 return err
310 }
311
312 if got > 1 {
313 w.WriteString("]\n")
314 }
315 return nil
316 }
317
318 func emitTSV(w *bufio.Writer, rr *csv.Reader) error {
319 width := -1
320
321 return loopCSV(rr, func(i int, row []string) error {
322 if width < 0 {
323 width = len(row)
324 }
325
326 for i, s := range row {
327 if strings.IndexByte(s, '\t') >= 0 {
328 const msg = `can't convert CSV whose items have tabs to TSV`
329 return errors.New(msg)
330 }
331 if i > 0 {
332 w.WriteByte('\t')
333 }
334 w.WriteString(s)
335 }
336
337 for i := len(row); i < width; i++ {
338 w.WriteByte('\t')
339 }
340
341 w.WriteByte('\n')
342 if err := w.Flush(); err != nil {
343 // a write error may be the consequence of stdout being closed,
344 // perhaps by another app along a pipe
345 return io.EOF
346 }
347 return nil
348 })
349 }
350
351 // writeInnerStringJSON helps JSON-encode strings more quickly
352 func writeInnerStringJSON(w *bufio.Writer, s string) {
353 needsEscaping := false
354 for _, r := range s {
355 if '#' <= r && r <= '~' && r != '\\' {
356 continue
357 }
358 if r == ' ' || r == '!' || unicode.IsLetter(r) {
359 continue
360 }
361
362 needsEscaping = true
363 break
364 }
365
366 if !needsEscaping {
367 w.WriteString(s)
368 return
369 }
370
371 outer, err := json.Marshal(s)
372 if err != nil {
373 return
374 }
375 inner := outer[1 : len(outer)-1]
376 w.Write(inner)
377 }
378
379 func writeKV(w *bufio.Writer, k string, s string) {
380 w.WriteByte('"')
381 writeInnerStringJSON(w, k)
382 w.WriteString(`": "`)
383 writeInnerStringJSON(w, s)
384 w.WriteByte('"')
385 }
386
387 func numberLike(s string) bool {
388 if len(s) == 0 {
389 return false
390 }
391
392 if s[0] == '-' {
393 s = s[1:]
394 }
395
396 if len(s) == 0 || s[0] < '0' || s[0] > '9' {
397 return false
398 }
399
400 for len(s) > 0 {
401 lead := s[0]
402 s = s[1:]
403
404 if lead == '.' {
405 return allDigits(s)
406 }
407 if lead < '0' || lead > '9' {
408 return false
409 }
410 }
411
412 return true
413 }
414
415 func allDigits(s string) bool {
416 if len(s) == 0 {
417 return false
418 }
419
420 for _, r := range s {
421 if r < '0' || r > '9' {
422 return false
423 }
424 }
425 return true
426 }
427
428 func makeRowReader(r io.Reader) *csv.Reader {
429 rr := csv.NewReader(r)
430 rr.LazyQuotes = true
431 rr.ReuseRecord = true
432 rr.FieldsPerRecord = -1
433 return rr
434 }
435
436 func loopCSV(rr *csv.Reader, handle func(i int, row []string) error) error {
437 width := 0
438
439 for i := 0; true; i++ {
440 row, err := rr.Read()
441 if err == io.EOF {
442 return nil
443 }
444
445 if err != nil {
446 return err
447 }
448
449 if i == 0 {
450 width = len(row)
451 }
452
453 if len(row) > width {
454 return errors.New(`data-row has more items than the header`)
455 }
456
457 if err := handle(i, row); err != nil {
458 return err
459 }
460 }
461
462 return nil
463 }
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 continue
64
65 case `-h`, `--h`, `-help`, `--help`:
66 os.Stdout.WriteString(info[1:])
67 return
68
69 case `-i`, `--i`, `-ins`, `--ins`:
70 insensitive = true
71 args = args[1:]
72 continue
73 }
74
75 break
76 }
77
78 if len(args) > 0 && args[0] == `--` {
79 args = args[1:]
80 }
81
82 exprs := make([]*regexp.Regexp, 0, len(args))
83
84 for _, s := range args {
85 var err error
86 var exp *regexp.Regexp
87
88 if insensitive {
89 exp, err = regexp.Compile(`(?i)` + s)
90 } else {
91 exp, err = regexp.Compile(s)
92 }
93
94 if err != nil {
95 os.Stderr.WriteString(err.Error())
96 os.Stderr.WriteString("\n")
97 continue
98 }
99
100 exprs = append(exprs, exp)
101 }
102
103 // quit right away when given invalid regexes
104 if len(exprs) < len(args) {
105 os.Exit(1)
106 }
107
108 liveLines := !buffered
109 if !buffered {
110 if _, err := os.Stdout.Seek(0, io.SeekCurrent); err == nil {
111 liveLines = false
112 }
113 }
114
115 err := run(os.Stdout, os.Stdin, exprs, liveLines)
116 if err != nil && err != io.EOF {
117 os.Stderr.WriteString(err.Error())
118 os.Stderr.WriteString("\n")
119 os.Exit(1)
120 }
121 }
122
123 func run(w io.Writer, r io.Reader, exprs []*regexp.Regexp, live bool) error {
124 var buf []byte
125 sc := bufio.NewScanner(r)
126 sc.Buffer(nil, 8*1024*1024*1024)
127 bw := bufio.NewWriter(w)
128 defer bw.Flush()
129
130 src := make([]byte, 8*1024)
131 dst := make([]byte, 8*1024)
132
133 for i := 0; sc.Scan(); i++ {
134 line := sc.Bytes()
135 if i == 0 && bytes.HasPrefix(line, []byte{0xef, 0xbb, 0xbf}) {
136 line = line[3:]
137 }
138
139 s := line
140 if bytes.IndexByte(s, '\x1b') >= 0 {
141 buf = plain(buf[:0], s)
142 s = buf
143 }
144
145 if len(exprs) > 0 {
146 src = append(src[:0], s...)
147 for _, exp := range exprs {
148 dst = erase(dst[:0], src, exp)
149 src = append(src[:0], dst...)
150 }
151 bw.Write(dst)
152 } else {
153 bw.Write(s)
154 }
155
156 if bw.WriteByte('\n') != nil {
157 return io.EOF
158 }
159
160 if !live {
161 continue
162 }
163
164 if bw.Flush() != nil {
165 return io.EOF
166 }
167 }
168
169 return sc.Err()
170 }
171
172 func erase(dst []byte, src []byte, with *regexp.Regexp) []byte {
173 for len(src) > 0 {
174 span := with.FindIndex(src)
175 // also ignore empty regex matches to avoid infinite outer loops,
176 // as skipping empty slices isn't advancing at all, leaving the
177 // string stuck to being empty-matched forever by the same regex
178 if len(span) != 2 || span[0] == span[1] || span[0] < 0 {
179 return append(dst, src...)
180 }
181
182 start, end := span[0], span[1]
183 dst = append(dst, src[:start]...)
184 // avoid infinite loops caused by empty regex matches
185 if start == end && end < len(src) {
186 dst = append(dst, src[end])
187 end++
188 }
189 src = src[end:]
190 }
191
192 return dst
193 }
194
195 func plain(dst []byte, src []byte) []byte {
196 for len(src) > 0 {
197 i, j := indexEscapeSequence(src)
198 if i < 0 {
199 dst = append(dst, src...)
200 break
201 }
202 if j < 0 {
203 j = len(src)
204 }
205
206 if i > 0 {
207 dst = append(dst, src[:i]...)
208 }
209
210 src = src[j:]
211 }
212
213 return dst
214 }
215
216 // indexEscapeSequence finds the first ANSI-style escape-sequence, which is
217 // the multi-byte sequences starting with ESC[; the result is a pair of slice
218 // indices which can be independently negative when either the start/end of
219 // a sequence isn't found; given their fairly-common use, even the hyperlink
220 // ESC]8 sequences are supported
221 func indexEscapeSequence(s []byte) (int, int) {
222 var prev byte
223
224 for i, b := range s {
225 if prev == '\x1b' && b == '[' {
226 j := indexLetter(s[i+1:])
227 if j < 0 {
228 return i, -1
229 }
230 return i - 1, i + 1 + j + 1
231 }
232
233 if prev == '\x1b' && b == ']' && i+1 < len(s) && s[i+1] == '8' {
234 j := indexPair(s[i+1:], '\x1b', '\\')
235 if j < 0 {
236 return i, -1
237 }
238 return i - 1, i + 1 + j + 2
239 }
240
241 prev = b
242 }
243
244 return -1, -1
245 }
246
247 func indexLetter(s []byte) int {
248 for i, b := range s {
249 upper := b &^ 32
250 if 'A' <= upper && upper <= 'Z' {
251 return i
252 }
253 }
254
255 return -1
256 }
257
258 func indexPair(s []byte, x byte, y byte) int {
259 var prev byte
260
261 for i, b := range s {
262 if prev == x && b == y && i > 0 {
263 return i
264 }
265 prev = b
266 }
267
268 return -1
269 }
File: ./fh/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 fh
26
27 import (
28 "errors"
29 "fmt"
30 "image/color"
31 "math"
32 "os"
33 "strconv"
34 "strings"
35 )
36
37 const (
38 // all output formats as constants, to prevent typos
39 pngOutput = `png`
40 pngFastOutput = `fast-png`
41 pngSmallestOutput = `smallest-png`
42 pngUncompressedOutput = `uncompressed-png`
43 bmpOutput = `bmp`
44 jpegOutput = `jpeg`
45
46 // all colorscales as constants, to prevent typos
47 magmaScale = `magma`
48 parulaScale = `parula`
49 viridisScale = `viridis`
50 grayScale = `gray`
51 binaryScale = `binary`
52 signScale = `sign`
53 )
54
55 // fmtAliases normalizes values for the output-format option
56 var fmtAliases = map[string]string{
57 `b`: bmpOutput,
58 `bitmap`: bmpOutput,
59 `bmp`: bmpOutput,
60 `j`: jpegOutput,
61 `jpeg`: jpegOutput,
62 `jpg`: jpegOutput,
63 `p`: pngOutput,
64 `ping`: pngOutput,
65 `png`: pngOutput,
66
67 `f`: pngFastOutput,
68 `fast`: pngFastOutput,
69 `fast-png`: pngFastOutput,
70 `fp`: pngFastOutput,
71 `fpng`: pngFastOutput,
72 `s`: pngSmallestOutput,
73 `small`: pngSmallestOutput,
74 `smallest-png`: pngSmallestOutput,
75 `small-png`: pngSmallestOutput,
76 `sp`: pngSmallestOutput,
77 `spng`: pngSmallestOutput,
78 `u`: pngUncompressedOutput,
79 `unc`: pngUncompressedOutput,
80 `uncompressed-png`: pngUncompressedOutput,
81 }
82
83 // paletteAliases normalizes values for the colorscale/palette option
84 var paletteAliases = map[string]string{
85 `b`: binaryScale,
86 `bin`: binaryScale,
87 `binary`: binaryScale,
88
89 `g`: grayScale,
90 `gr`: grayScale,
91 `gray`: grayScale,
92
93 `m`: magmaScale,
94 `mag`: magmaScale,
95 `magma`: magmaScale,
96
97 `s`: signScale,
98 `sgn`: signScale,
99 `sign`: signScale,
100
101 `py`: viridisScale,
102 `python`: viridisScale,
103 `numpy`: viridisScale,
104 `v`: viridisScale,
105 `vir`: viridisScale,
106 `viridis`: viridisScale,
107
108 `matlab`: parulaScale,
109 `p`: parulaScale,
110 `par`: parulaScale,
111 `parula`: parulaScale,
112 }
113
114 // outputSize is the value type for the resAliases lookup table
115 type outputSize struct {
116 Width int
117 Height int
118 }
119
120 // resAliases normalizes values for option -res
121 var resAliases = map[string]outputSize{
122 `sq`: {2160, 2160},
123 `sqr`: {2160, 2160},
124 `square`: {2160, 2160},
125 `squared`: {2160, 2160},
126
127 `4k`: {3840, 2160},
128 `2160`: {3840, 2160},
129 `2160p`: {3840, 2160},
130 `3840`: {3840, 2160},
131
132 `2.5k`: {2560, 1440},
133 `1440`: {2560, 1440},
134 `1440p`: {2560, 1440},
135 `2560`: {2560, 1440},
136
137 `2k`: {1920, 1080},
138 `hd`: {1920, 1080},
139 `fhd`: {1920, 1080},
140 `fullhd`: {1920, 1080},
141 `1080`: {1920, 1080},
142 `1080p`: {1920, 1080},
143 `1920`: {1920, 1080},
144
145 `720`: {1280, 720},
146 `720p`: {1280, 720},
147
148 `480p`: {640, 480},
149 `480`: {640, 480},
150
151 `2ks`: {1080, 1080},
152 `4ks`: {2160, 2160},
153 `2160s`: {2160, 2160},
154 `1440s`: {1440, 1440},
155 `1080s`: {1080, 1080},
156 `720s`: {720, 720},
157 `480s`: {480, 480},
158 }
159
160 // config has all parsed cmd-line arguments
161 type config struct {
162 Width int
163 Height int
164
165 XMin float64
166 XMax float64
167 YMin float64
168 YMax float64
169
170 Formula string
171 Output string
172
173 Palette func(float64) color.RGBA
174 Bad color.RGBA
175
176 Integers bool
177 }
178
179 // parseFlags is the constructor for type config
180 func parseFlags(usage string) (config, error) {
181 cfg := config{
182 Width: 3840,
183 Height: 2160,
184
185 XMin: 0,
186 XMax: 1,
187 YMin: 0,
188 YMax: 1,
189
190 Output: pngOutput,
191 }
192
193 cfg.Output = pngOutput
194 pal := palettes[parulaScale]
195 cfg.Palette = pal.Func
196 cfg.Bad = pal.Bad
197
198 args := os.Args[1:]
199 if len(args) == 0 {
200 fmt.Fprint(os.Stderr, usage)
201 os.Exit(0)
202
203 }
204
205 for _, s := range args {
206 switch s {
207 case `help`, `-h`, `--h`, `-help`, `--help`:
208 fmt.Fprint(os.Stderr, usage)
209 os.Exit(0)
210 }
211
212 err := cfg.handleArg(s)
213 if err != nil {
214 return cfg, err
215 }
216 }
217
218 if cfg.Integers {
219 cfg.XMin = math.Ceil(float64(cfg.XMin))
220 cfg.XMax = math.Floor(float64(cfg.XMax))
221 cfg.YMin = math.Ceil(float64(cfg.YMin))
222 cfg.YMax = math.Floor(float64(cfg.YMax))
223 }
224
225 if strings.TrimSpace(cfg.Formula) == `` {
226 return cfg, errors.New(`no main formula given`)
227 }
228 return cfg, nil
229 }
230
231 // handleArg parses/uses the cmd-line argument given, except for the help
232 // option and its aliases, which can only be detected separately
233 func (c *config) handleArg(s string) error {
234 switch s {
235 case `int`, `ints`, `integers`:
236 c.Integers = true
237 return nil
238 }
239
240 lcDotless := strings.TrimPrefix(strings.ToLower(s), `.`)
241 if alias, ok := fmtAliases[lcDotless]; ok {
242 c.Output = alias
243 return nil
244 }
245
246 if w, h, ok := parseResolution(s); ok {
247 c.Width = w
248 c.Height = h
249 return nil
250 }
251
252 if colors, ok := paletteAliases[s]; ok {
253 pal := palettes[colors]
254 c.Palette = pal.Func
255 c.Bad = pal.Bad
256 return nil
257 }
258
259 varname, min, max, err := parseDomain(s)
260 if err != nil {
261 return err
262 }
263
264 switch varname {
265 case ``:
266 // no variable name means it's the main formula
267 if c.Formula != `` {
268 const fs = `%q: can't use more than 1 main formula`
269 return fmt.Errorf(fs, s)
270 }
271 c.Formula = s
272 return nil
273
274 case `x`:
275 c.XMin = min
276 c.XMax = max
277 return nil
278
279 case `y`:
280 c.YMin = min
281 c.YMax = max
282 return nil
283
284 case `xy`:
285 c.XMin = min
286 c.XMax = max
287 c.YMin = min
288 c.YMax = max
289 return nil
290
291 default:
292 const fs = "domain variable %q isn't any of `x`, `y`, or `xy`"
293 return fmt.Errorf(fs, varname)
294 }
295 }
296
297 // parseResolution tries to get a width/height resolution out of the
298 // cmd-line argument given to it
299 func parseResolution(s string) (width int, height int, ok bool) {
300 if res, ok := resAliases[s]; ok {
301 return res.Width, res.Height, true
302 }
303
304 i := strings.IndexByte(s, 'x')
305 if i < 0 {
306 return 0, 0, false
307 }
308
309 w, werr := strconv.ParseInt(s[:i], 10, 64)
310 h, herr := strconv.ParseInt(s[i+1:], 10, 64)
311 if werr == nil && herr == nil && w > 0 && h > 0 {
312 return int(w), int(h), true
313 }
314 return 0, 0, false
315 }
316
317 func (c config) IntegerSize() (w, h int) {
318 w = int(math.Abs(c.XMax - c.XMin + 1))
319 h = int(math.Abs(c.YMax - c.YMin + 1))
320 return w, h
321 }
322
323 // parseDomain tries to parse domain/variable-range formulas of the form(s)
324 //
325 // - x:=a..b
326 // - y:=a..b
327 // - xy:=a..b
328 //
329 // where a and b represent valid floating-point numbers; when an empty is
330 // returned, it means the strings given wasn't recognized as a variable's
331 // domain, suggesting it may be another option, or the main formula instead
332 func parseDomain(s string) (string, float64, float64, error) {
333 i := strings.Index(s, `:=`)
334 if i < 0 {
335 return ``, 0, 0, nil
336 }
337
338 v := strings.TrimSpace(s[:i])
339 rng := strings.TrimSpace(s[i+2:])
340 min, max, err := parseSpan(rng)
341 return v, min, max, err
342 }
343
344 // parseSpan tries to parse a pair of numbers with `..` between them
345 func parseSpan(s string) (float64, float64, error) {
346 pair := strings.Split(s, `..`)
347 if len(pair) != 2 {
348 const fs = "missing `..` in domain-span %s"
349 return 0, 1, fmt.Errorf(fs, s)
350 }
351
352 a, err := strconv.ParseFloat(pair[0], 64)
353 if err != nil {
354 const fs = `can't parse %q in domain-span %s`
355 return 0, 1, fmt.Errorf(fs, pair[0], s)
356 }
357 b, err := strconv.ParseFloat(pair[1], 64)
358 if err != nil {
359 const fs = `can't parse %q in domain-span %s`
360 return 0, 1, fmt.Errorf(fs, pair[1], s)
361 }
362 return a, b, nil
363 }
File: ./fh/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 fh
26
27 import "testing"
28
29 func TestTables(t *testing.T) {
30 for _, kind := range fmtAliases {
31 // check all canonical format names are in the table
32 if _, ok := fmtAliases[kind]; !ok {
33 const fs = `format %q itself isn't in the format-table`
34 t.Fatalf(fs, kind)
35 return
36 }
37
38 if _, ok := encoders[kind]; !ok {
39 const fs = `no encoder for %q`
40 t.Fatalf(fs, kind)
41 return
42 }
43 }
44
45 for _, kind := range paletteAliases {
46 // check all canonical colorscale names are in the table
47 if _, ok := paletteAliases[kind]; !ok {
48 const fs = `format %q itself isn't in the format-table`
49 t.Fatalf(fs, kind)
50 return
51 }
52
53 if _, ok := palettes[kind]; !ok {
54 const fs = `no palette for %q`
55 t.Fatalf(fs, kind)
56 return
57 }
58 }
59 }
File: ./fh/examples.txt
1 # ripples
2 fh xy:=-3..3 'exp(-0.5 * sin(2 * hypot(x - 2, y + 1))) + exp(-0.5 * sin(10 * hypot(x + 2, y - 3.4)))' | si
3
4 # floor lights
5 fh x:=-5..5 y:=1..5 'x.sin.abs / y**1.4' | si
6
7 # beta gradient
8 fh x:=-5..5 y:=1..5 'lbeta(x + 5.1, y + 5.1)' | si
9
10 # hot bars / horizontal bars
11 fh xy:=-5..5 'x.abs + sqrt(abs(sin(2*y)))' | si
12 fh xy:=-5..5 'x.abs + sqrt(abs(sin(2*y)))' | si
13
14 # domain hole
15 fh xy:=-5..5 'log1p(sin(x + y) + (x - y)**2 - 1.5*x + 2.5*y + 1)' | si
16
17 # crazy grids
18 fh xy:=-10..10 'sin(x.sin+y.cos) + cos(sin(x*y)+cos(y*y))' | si
19
20 # panda / smiling ghost
21 fh xy:=-5..5 'log1p(((x - 1)**2 + y*y - 4)*((x + 1)**2 + y*y - 4)*(x*x + (y - sqrt(3))**2 - 4) - 5)' | si
22
23 # lcm 200
24 fh xy:=0.01..199.99 'lcm(x.ceil, y.ceil)' | si
25
26 # light tiles
27 fh 'gauss(2*(sin(50.0*x)*cos(50.0*9/16*y) + 1.0)/2.0)' | si
28
29 # shaky results... at least for me
30 fh 'cos(160*tau*x) + sin(90*tau*y)' | si
31
32 # 90-degree square tiles
33 fh 'sign(cos(160*tau*x) + sin(90*tau*y))' | si
File: ./fh/info.txt
1 fh [options...] [x/y ranges...] formula
2
3
4 Function Heatmapper emits a picture showing a heatmap view of the function
5 f(x, y) implied by the math expression given. Plenty of math functions and
6 constants are available, all their names being lowercase; the syntax is
7 almost identical to Python/JavaScript's math notation, and has no keywords.
8
9 For convenience, you can treat any 1-input func as a fake-property of its
10 only input; you can also pretend all functions are fake-methods, where the
11 1st input comes before the dot preceding the func name, followed by all the
12 other args to it. All values and functions are global: without namespaces
13 of any kind.
14
15 Ranges for variables `x` and `y` are 0 to 1 by default, but you can change
16 them via the special syntax shown on some of the examples below. Using the
17 keyword `int`, `ints`, or `integers` enables integer-mode, where both `x`
18 and `y` values are only sampled as integers: in that case, formula results
19 will be used to fill whole tiles, instead of single pixels.
20
21 By default, output is PNG-encoded using a good tradeoff between encoding
22 speed and final payload size. Output resolutions can be as shown below, or
23 consist of the width, followed by `x`, followed by the height wanted, such
24 as `1024x768`, for example.
25
26 Options have no flags/prefixes, and are accepted in any order.
27
28
29 Options
30
31 resolution resolution
32
33 4k 3840x2160 4ks 2160x2160
34 hd 1920x1080 hds 1080x1080
35
36 2160p 3840x2160 2160s 2160x2160
37 1440p 2560x1440 1440s 1440x1440
38 1080p 1920x1080 1080s 1080x1080
39 720p 1280x720 720s 720x720
40
41
42 output aliases colorscale aliases
43
44 png magma mag, m
45 bmp bitmap parula par, p
46 jpg jpeg viridis vir, v
47
48
49 Concrete Examples
50
51
52 fh 'x/(x+y)' > corner-fan-1.png
53
54 fh 'y/(x+y)' > corner-fan-2.png
55
56 fh 4k x:=-5..5 y:=1..5 'x.sin.abs / y**1.4' > floor-lights.png
57
58 fh vir x:=-5..5 y:=1..5 'lbeta(x + 5.1, y + 5.1)' > beta-gradient.png
59
60 fh mag 4k xy:=0.01..199.99 'lcm(x.ceil, y.ceil)' > lcm-200.png
61
62 fh par 4k xy:=-5..5 'x.abs + sqrt(abs(sin(2*y)))' > bars.png
63
64 fh x:=-1.5..0.5 y:=-1..1 'mandel(16/9*x, y)' > mandelbrot.png
65
66 fh x:=-1.5..0.5 y:=-1..1 'absmandel(16/9*x, y)' > wobbly-mandelbrot.png
67
68 fh 4k 'sign(cos(160*tau*x) + sin(90*tau*y))' > 90-deg-square-tiles.png
69
70 fh xy:=-10..10 'sin(x.sin+y.cos) + cos(sin(x*y)+cos(y*y))' > crazy-grids.png
71
72 fh 'gauss(sin(50*x) * cos(50*9/16*y) + 1)' > light-tiles.png
73
74 fh xy:=-2..3 'sgn(log((x*x-1)*(x-2-y)/(x*x+2+2*y)))' > abstract-shapes.png
75
76 fh xy:=-10..10 square 'sinc(0.55 * hypot(x, y))' > central-ripple.png
File: ./fh/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 fh
26
27 import (
28 "bufio"
29 "fmt"
30 "image"
31 "os"
32
33 _ "embed"
34 )
35
36 //go:embed info.txt
37 var usage string
38
39 func Main() {
40 cfg, err := parseFlags(usage)
41 if err != nil {
42 fmt.Fprintln(os.Stderr, err.Error())
43 os.Exit(1)
44 }
45
46 if _, ok := encoders[cfg.Output]; !ok {
47 const fs = "unsupported output format %s\n"
48 fmt.Fprintf(os.Stderr, fs, cfg.Output)
49 os.Exit(1)
50 }
51
52 addDetermFuncs()
53
54 if err := run(cfg); err != nil {
55 fmt.Fprintln(os.Stderr, err.Error())
56 os.Exit(1)
57 }
58 }
59
60 func run(cfg config) error {
61 // f, err := os.Create(`fh.prof`)
62 // if err != nil {
63 // return err
64 // }
65 // defer f.Close()
66
67 // pprof.StartCPUProfile(f)
68 // defer pprof.StopCPUProfile()
69
70 encode, ok := encoders[cfg.Output]
71 if !ok {
72 const fs = `unsupported output format %q`
73 return fmt.Errorf(fs, cfg.Output)
74 }
75
76 if cfg.Integers {
77 w, h := cfg.IntegerSize()
78 cfg.Width = w
79 cfg.Height = h
80 }
81
82 // allow runner to use up to 32 cores
83 r, err := newRunner(cfg, 32)
84 if err != nil {
85 return err
86 }
87
88 res, err := r.Run(cfg)
89 if err != nil {
90 return err
91 }
92
93 img := image.NewRGBA(image.Rectangle{
94 Min: image.Point{X: 0, Y: 0},
95 Max: image.Point{X: cfg.Width, Y: cfg.Height},
96 })
97
98 w := bufio.NewWriterSize(os.Stdout, 64*1024)
99 defer w.Flush()
100
101 // give back a blank picture if results aren't usable
102 if !res.isValid() {
103 return encode(w, img, cfg)
104 }
105
106 // handle integers-only coordinate-inputs
107 if cfg.Integers {
108 width, height := cfg.IntegerSize()
109 fillExpandedImage(img, res, cfg, width, height)
110 return encode(w, img, cfg)
111 }
112
113 // handle domain-sampled images
114 fillImage(img, res, cfg)
115 return encode(w, img, cfg)
116 }
File: ./fh/output.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 fh
26
27 import (
28 "bufio"
29 "encoding/binary"
30 "image"
31 "image/color"
32 "image/jpeg"
33 "image/png"
34 "math"
35
36 "../colorplus"
37 )
38
39 var (
40 // red is the invalid color for all palettes with dark/black colors
41 red = color.RGBA{R: 255, G: 0, B: 0, A: 255}
42
43 // black is the invalid color for the more colorful palettes
44 black = color.RGBA{R: 0, G: 0, B: 0, A: 255}
45 )
46
47 // paletteSettings describes the full behavior of a palette
48 type paletteSettings struct {
49 Func func(float64) color.RGBA
50 Bad color.RGBA
51 }
52
53 // palettes completely describes the behavior of all supported palettes
54 var palettes = map[string]paletteSettings{
55 grayScale: {gray, red},
56 magmaScale: {colorplus.Magmify, red},
57 viridisScale: {colorplus.Viridize, black},
58 parulaScale: {colorplus.Parulate, black},
59 binaryScale: {colorBinary, black},
60 signScale: {colorSign, black},
61 }
62
63 // gray implements the grayscale coloring option, and is meant to be paired
64 // with a red color for invalid inputs, such as NaNs
65 func gray(x float64) color.RGBA {
66 // restrict input to range 0..1
67 if x < 0 {
68 x = 0
69 } else if x > 1 {
70 x = 1
71 }
72
73 v := uint8(math.Round(255 * x))
74 return color.RGBA{R: v, G: v, B: v, A: 255}
75 }
76
77 // colorBinary assigns 2 colors, thresholding the number given on 0.5
78 func colorBinary(x float64) color.RGBA {
79 if x < 0.5 {
80 return color.RGBA{R: 234, G: 85, B: 58, A: 255}
81 }
82 return color.RGBA{R: 0, G: 95, B: 0, A: 255}
83 }
84
85 // colorSign assigns 3 colors, depending on the sign of the number given
86 func colorSign(x float64) color.RGBA {
87 if x > 0 {
88 return color.RGBA{R: 0, G: 95, B: 0, A: 255}
89 }
90 if x < 0 {
91 return color.RGBA{R: 234, G: 85, B: 58, A: 255}
92 }
93 return color.RGBA{R: 0, G: 135, B: 215, A: 255}
94 }
95
96 // encoders translates output-format settings into the right func to call
97 var encoders = map[string]func(*bufio.Writer, *image.RGBA, config) error{
98 pngOutput: encodePNG,
99 bmpOutput: encodeBMP,
100 jpegOutput: encodeJPEG,
101
102 pngFastOutput: encodeFastPNG,
103 pngSmallestOutput: encodeSmallestPNG,
104 pngUncompressedOutput: encodeUncompressedPNG,
105 }
106
107 // fillImage fills/renders an image using previously calculated values
108 func fillImage(img *image.RGBA, res result, cfg config) {
109 k := 0
110 f := cfg.Palette
111
112 for i := 0; i < cfg.Height; i++ {
113 for j := 0; j < cfg.Width; j++ {
114 v := res.Values[k]
115
116 var c color.RGBA
117 if math.IsNaN(v) || math.IsInf(v, 0) {
118 c = cfg.Bad
119 } else {
120 c = f(colorplus.Wrap(v, res.Min, res.Max))
121 }
122
123 img.SetRGBA(j, i, c)
124 k++
125 }
126 }
127 }
128
129 // fillExpandedImage is like func fillImage, but rendering stretches what
130 // would otherwise be single pixels into rectangles, representing regions
131 // where the integer-parts of x/y inputs stay the same
132 func fillExpandedImage(img *image.RGBA, res result, cfg config, w, h int) {
133 width := img.Rect.Max.X
134 xmax := float64(img.Rect.Max.X)
135 ymax := float64(img.Rect.Max.Y)
136
137 f := cfg.Palette
138 dx := float64(w) / xmax
139 dy := float64(h) / ymax
140
141 for i := 0; i < cfg.Height; i++ {
142 y := int(dy * float64(i))
143 for j := 0; j < cfg.Width; j++ {
144 x := int(dx * float64(j))
145 k := y*width + x
146 v := res.Values[k]
147
148 var c color.RGBA
149 if math.IsNaN(v) || math.IsInf(v, 0) {
150 c = cfg.Bad
151 } else {
152 c = f(colorplus.Wrap(v, res.Min, res.Max))
153 }
154 img.SetRGBA(j, i, c)
155 }
156 }
157 }
158
159 // encodePNG seems a good default both for its main format (PNG), as well as
160 // its reasonable default tradeoff between speed and output size, compared
161 // to the PNG-encoding alternatives available
162 func encodePNG(w *bufio.Writer, img *image.RGBA, cfg config) error {
163 var enc png.Encoder
164 return enc.Encode(w, img)
165 }
166
167 // encodeFastPNG may not always be much faster than the default PNG encoder
168 func encodeFastPNG(w *bufio.Writer, img *image.RGBA, cfg config) error {
169 var enc png.Encoder
170 enc.CompressionLevel = png.BestSpeed
171 return enc.Encode(w, img)
172 }
173
174 // encodeSmallestPNG is substantially slower than the other PNG encoders
175 func encodeSmallestPNG(w *bufio.Writer, img *image.RGBA, cfg config) error {
176 var enc png.Encoder
177 enc.CompressionLevel = png.BestCompression
178 return enc.Encode(w, img)
179 }
180
181 // encodeUncompressedPNG is mostly to compare it to BMP output: it turns out
182 // BMP is slightly smaller than this
183 func encodeUncompressedPNG(w *bufio.Writer, img *image.RGBA, cfg config) error {
184 var enc png.Encoder
185 enc.CompressionLevel = png.NoCompression
186 return enc.Encode(w, img)
187 }
188
189 // encodeJPEG encodes result at max JPEG setting: this usually results in
190 // highly-detailed results for substantially-fewer bytes, compared to PNG
191 // output
192 func encodeJPEG(w *bufio.Writer, img *image.RGBA, cfg config) error {
193 opt := jpeg.Options{Quality: 100}
194 return jpeg.Encode(w, img, &opt)
195 }
196
197 // https://en.wikipedia.org/wiki/BMP_file_format
198
199 // encodeBMP encodes as BMP/bitmap, a simple uncompressed format, which has
200 // been widely supported for many decades
201 func encodeBMP(w *bufio.Writer, img *image.RGBA, cfg config) error {
202 const (
203 dibsize = 40 // the DIB is the 2nd header
204 hdrsize = 14 + dibsize // total size of all headers
205 )
206 imgsize := 3 * cfg.Width * cfg.Height
207
208 w.WriteString(`BM`)
209 binary.Write(w, binary.LittleEndian, uint32(hdrsize+imgsize))
210 binary.Write(w, binary.LittleEndian, uint16(0))
211 binary.Write(w, binary.LittleEndian, uint16(0))
212 binary.Write(w, binary.LittleEndian, uint32(hdrsize))
213 binary.Write(w, binary.LittleEndian, uint32(dibsize))
214 binary.Write(w, binary.LittleEndian, int32(cfg.Width))
215 binary.Write(w, binary.LittleEndian, int32(cfg.Height))
216
217 // 1 color plane
218 binary.Write(w, binary.LittleEndian, uint16(1))
219 // 24 bits per pixel
220 binary.Write(w, binary.LittleEndian, uint16(24))
221 // no compression
222 binary.Write(w, binary.LittleEndian, uint32(0))
223 // number of bytes for the pixels
224 binary.Write(w, binary.LittleEndian, uint32(imgsize))
225 // horizontal & vertical pixels/m
226 binary.Write(w, binary.LittleEndian, int32(0))
227 binary.Write(w, binary.LittleEndian, int32(0))
228 // 2**n palette colors
229 binary.Write(w, binary.LittleEndian, uint32(0))
230 // all colors are important
231 binary.Write(w, binary.LittleEndian, uint32(0))
232
233 stride := img.Stride
234 // rows/lines are apparently stored bottom-to-top
235 for y := cfg.Height - 1; y >= 0; y-- {
236 start := y * stride
237 buf := img.Pix[start : start+stride]
238
239 for len(buf) >= 3 {
240 // color-channel order seems to be BGR, instead of RGB
241 w.WriteByte(buf[2])
242 w.WriteByte(buf[1])
243 err := w.WriteByte(buf[0])
244 if err != nil {
245 // use errors to quit immediately: chances are
246 // the error is the result of a closed-pipe
247 return nil
248 }
249
250 // also skip the alpha channel
251 buf = buf[4:]
252 }
253 }
254 return nil
255 }
File: ./fh/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 fh
26
27 import (
28 "math"
29 "math/cmplx"
30 "math/rand"
31 "runtime"
32 "sync"
33 "time"
34
35 "../fmscripts"
36 "../mathplus"
37 )
38
39 // result has all results, including a summary of the range of values, so the
40 // image renderer can normalize values accordingly
41 type result struct {
42 // Values has all results, which can be normalized into 0..1 using
43 // fields Min and Max.
44 Values []float64
45
46 // Min is the lowest value in Values.
47 Min float64
48
49 // Max is the highest value in Values.
50 Max float64
51 }
52
53 // isValid checks if the result should be a non-blank picture
54 func (r result) isValid() bool {
55 return r.Min <= r.Max && !math.IsInf(r.Min, 0) && !math.IsInf(r.Max, 0)
56 }
57
58 // runner has various twin script-runners, and automatically multicore-splits
59 // the load among tasks along alternating groups of lines, in a striping
60 // manner
61 type runner struct {
62 numTasks int
63
64 // values can have its items updated concurrently, since each vertical
65 // image line is changed by a single task.
66 values []float64
67
68 programs []fmscripts.Program
69 }
70
71 // newRunner is the constructor for type runner
72 func newRunner(cfg config, maxtasks int) (runner, error) {
73 numtasks := runtime.NumCPU()
74 if maxtasks > 0 && numtasks > maxtasks {
75 numtasks = maxtasks
76 }
77 progs := make([]fmscripts.Program, 0, numtasks)
78
79 for i := 0; i < numtasks; i++ {
80 // compiling the same formula multiple times seems wasteful, but
81 // each compilation is very quick; this repetition is necessary
82 // to isolate each task's input variables and pseudo-random state,
83 // anyway
84 p, err := compile(cfg.Formula, cfg, time.Now().UnixNano())
85 if err != nil {
86 return runner{}, err
87 }
88 progs = append(progs, p)
89 }
90
91 return runner{
92 numTasks: numtasks,
93 values: make([]float64, cfg.Width*cfg.Height),
94 programs: progs,
95 }, nil
96 }
97
98 // Run is the entry-point func which handles everything from start to finish.
99 func (r *runner) Run(cfg config) (res result, err error) {
100 var wg sync.WaitGroup
101 wg.Add(r.numTasks)
102
103 // fully allocate min/max slices, as appending is concurrently unsafe
104 lmin := make([]float64, r.numTasks)
105 lmax := make([]float64, r.numTasks)
106
107 // run parallel tasks: updating the shared value-slice works, as long
108 // as each process sticks to its own index and output lines
109 for i := 0; i < r.numTasks; i++ {
110 go func(i int) {
111 defer wg.Done()
112 min, max := r.runSlice(i, cfg)
113 lmin[i] = min
114 lmax[i] = max
115 }(i)
116 }
117 wg.Wait()
118
119 // get overall min/max
120 min := math.Inf(+1)
121 max := math.Inf(-1)
122 for i := range lmin {
123 min = math.Min(min, lmin[i])
124 max = math.Max(max, lmax[i])
125 }
126 return result{Values: r.values, Min: min, Max: max}, nil
127 }
128
129 // runSlice handles the task a specific core is supposed to handle: call
130 // run instead of this func directly
131 func (r *runner) runSlice(task int, cfg config) (min, max float64) {
132 p := r.programs[task]
133 x, _ := p.Get(`x`)
134 y, _ := p.Get(`y`)
135 zs := r.values
136
137 w := cfg.Width
138 h := cfg.Height
139 n := r.numTasks
140 xmin := math.Min(cfg.XMin, cfg.XMax)
141 ymax := math.Max(cfg.YMax, cfg.YMin)
142 wf := float64(w)
143
144 zmin := math.Inf(+1)
145 zmax := math.Inf(-1)
146 dx := math.Abs(cfg.XMax-cfg.XMin) / float64(cfg.Width-1)
147 dy := math.Abs(cfg.YMax-cfg.YMin) / float64(cfg.Height-1)
148
149 for i := task; i < h; i += n {
150 k := w * i
151 *y = ymax - dy*float64(i)
152
153 for j := 0.0; j < wf; j++ {
154 *x = dx*j + xmin
155 z := p.Run()
156 zs[k] = z
157 k++
158
159 if !math.IsNaN(z) {
160 zmin = math.Min(zmin, z)
161 zmax = math.Max(zmax, z)
162 }
163 }
164 }
165 return zmin, zmax
166 }
167
168 // compile extends the built-in fast-math script functionality by adding
169 // pseudo-random generators initialized with the seed number given
170 func compile(src string, cfg config, seed int64) (fmscripts.Program, error) {
171 r := rand.New(rand.NewSource(seed))
172 rand01 := func() float64 {
173 return fmscripts.Random(r)
174 }
175 rint := func(min, max float64) float64 {
176 return fmscripts.RandomInt(r, min, max)
177 }
178 runif := func(min, max float64) float64 {
179 return fmscripts.RandomUnif(r, min, max)
180 }
181 rexp := func(scale float64) float64 {
182 return fmscripts.RandomExp(r, scale)
183 }
184 rnorm := func(mu, sigma float64) float64 {
185 return fmscripts.RandomNorm(r, mu, sigma)
186 }
187 rgamma := func(scale float64) float64 {
188 return fmscripts.RandomGamma(r, scale)
189 }
190 rbeta := func(a, b float64) float64 {
191 return fmscripts.RandomBeta(r, a, b)
192 }
193
194 var c fmscripts.Compiler
195 return c.Compile(src, map[string]any{
196 `x`: 0.0,
197 `y`: 0.0,
198
199 `w`: float64(cfg.Width),
200 `h`: float64(cfg.Height),
201 `ar`: float64(cfg.Width) / float64(cfg.Height),
202 `aspratio`: float64(cfg.Width) / float64(cfg.Height),
203
204 `rand`: rand01,
205 `rbeta`: rbeta,
206 `rexp`: rexp,
207 `rgamma`: rgamma,
208 `rint`: rint,
209 `rnorm`: rnorm,
210 `runif`: runif,
211
212 `randbeta`: rbeta,
213 `randexp`: rexp,
214 `randexpo`: rexp,
215 `randgam`: rgamma,
216 `randgamma`: rgamma,
217 `randint`: rint,
218 `randnorm`: rnorm,
219 `randunif`: runif,
220
221 `random`: rand01,
222 `rbet`: rbeta,
223 `rgam`: rgamma,
224 `rnd`: rand01,
225 })
226 }
227
228 // addDetermFuncs does what it says, ensuring these funcs are optimizable when
229 // they're given all-constant expressions as inputs
230 func addDetermFuncs() {
231 fmscripts.DefineDetFuncs(map[string]any{
232 `ascale`: mathplus.AnchoredScale,
233 `awrap`: mathplus.AnchoredWrap,
234 `choose`: comb,
235 `clamp`: mathplus.Clamp,
236 `comb`: comb,
237 `dbinom`: dbinom,
238 `dnorm`: mathplus.NormalDensity,
239 `epa`: mathplus.Epanechnikov,
240 `epanechnikov`: mathplus.Epanechnikov,
241 `etamag`: etamag,
242 `etamagcap`: etamagcap,
243 `fract`: mathplus.Fract,
244 `gauss`: mathplus.Gauss,
245 `gcd`: gcd,
246 `horner`: mathplus.Polyval,
247 `ieta`: etaimag,
248 `isprime`: isPrime,
249 `lcm`: lcm,
250 `logistic`: mathplus.Logistic,
251 `mageta`: etamag,
252 `magetacap`: etamagcap,
253 `magzeta`: zetamag,
254 `magzetacap`: zetamagcap,
255 `mix`: mathplus.Mix,
256 `perm`: perm,
257 `pbinom`: pbinom,
258 `pnorm`: mathplus.CumulativeNormalDensity,
259 `polyval`: mathplus.Polyval,
260 `reta`: etare,
261 `scale`: mathplus.Scale,
262 `sign`: mathplus.Sign,
263 `sinc`: mathplus.Sinc,
264 `smoothstep`: mathplus.SmoothStep,
265 `step`: mathplus.Step,
266 `tricube`: mathplus.Tricube,
267 `unwrap`: mathplus.Unwrap,
268 `wrap`: mathplus.Wrap,
269 `zetamag`: zetamag,
270 `zetamagcap`: zetamagcap,
271
272 `absmandel`: absmandel,
273 `absmandelcap`: absmandelcap,
274 `itermandel`: itermandel,
275 `itermandelcap`: itermandelcap,
276 `mandel`: itermandel,
277 })
278 }
279
280 // absmandel returns the abs value of the complex number used in the mandelbrot
281 // recurrence relation; recurrence is automatically truncated to a default
282 // threshold and/or max number of loops
283 func absmandel(x, y float64) float64 {
284 return absmandelcap(x, y, 50)
285 }
286
287 // absmandelcap is like func absmandel, except the cap/threshold is an explicit
288 // parameter
289 func absmandelcap(x, y, threshold float64) float64 {
290 z := 0 + 0i
291 c := complex(x, y)
292 const max = 1000
293 // using the threshold's square to avoid using sqrt
294 ts := threshold * threshold
295
296 for n := 0.0; n < max; n++ {
297 sqmag := real(z)*real(z) + imag(z)*imag(z)
298 if sqmag > ts {
299 return math.Sqrt(sqmag)
300 }
301 z = z*z + c
302 }
303 return cmplx.Abs(z)
304 }
305
306 // itermandel returns the number of iterations used in the mandelbrot
307 // recurrence relation; recurrence is automatically truncated to a default
308 // threshold and/or max number of loops
309 func itermandel(x, y float64) float64 {
310 return itermandelcap(x, y, 50)
311 }
312
313 // itermandelcap returns the number of mandelbrot recurrence iterations like
314 // func itermandel, except the cap/threshold is an explicit parameter
315 func itermandelcap(x, y, threshold float64) float64 {
316 z := 0 + 0i
317 c := complex(x, y)
318 const max = 1000
319 // using the threshold's square to avoid using sqrt
320 ts := threshold * threshold
321
322 for n := 0.0; n < max; n++ {
323 sqmag := real(z)*real(z) + imag(z)*imag(z)
324 if sqmag > ts {
325 return n
326 }
327 z = z*z + c
328 }
329 return max
330 }
331
332 func comb(x, y float64) float64 {
333 return float64(mathplus.Choose(int(x), int(y)))
334 }
335
336 func perm(x, y float64) float64 {
337 return float64(mathplus.Perm(int(x), int(y)))
338 }
339
340 func gcd(x, y float64) float64 {
341 return float64(mathplus.GCD(int64(x), int64(y)))
342 }
343
344 func lcm(x, y float64) float64 {
345 return float64(mathplus.LCM(int64(x), int64(y)))
346 }
347
348 func dbinom(x, n, p float64) float64 {
349 return mathplus.BinomialMass(int(x), int(n), p)
350 }
351
352 func pbinom(x, n, p float64) float64 {
353 return mathplus.CumulativeBinomialDensity(int(x), int(n), p)
354 }
355
356 func isPrime(x float64) float64 {
357 if mathplus.IsPrime(int64(x)) {
358 return 1
359 }
360 return 0
361 }
362
363 const (
364 // etaTrunc is when the summation for the eta funcs stops by default
365 etaTrunc = 50
366
367 // zetaTrunc is when the summation for the zeta funcs stops by default
368 zetaTrunc = 50
369 )
370
371 // etamag call func etamagcap with a default truncation
372 func etamag(x, y float64) float64 {
373 return cmplx.Abs(eta(complex(x, y)))
374 }
375
376 // etamagcap is the real-valued magnitude of the truncated approx. of func eta
377 func etamagcap(x, y float64, max float64) float64 {
378 return cmplx.Abs(etacap(complex(x, y), int(max)))
379 }
380
381 // etare is the real part of the truncated approx. of func eta
382 func etare(x, y float64) float64 { return real(eta(complex(x, y))) }
383
384 // etaimag is the imaginary part of the truncated approx. of func eta
385 func etaimag(x, y float64) float64 { return imag(eta(complex(x, y))) }
386
387 // eta approximates the dirichlet eta function by truncation
388 func eta(x complex128) complex128 { return etacap(x, etaTrunc) }
389
390 // etacap accepts a cap/max iteration value for the eta func truncation
391 func etacap(x complex128, max int) complex128 {
392 y := 0 + 0i
393 v := 1 + 0i
394 sign := 1 + 0i
395
396 for n := 1; n <= max; n++ {
397 y += sign * v
398 sign *= -1
399 v /= x
400 }
401 return y
402 }
403
404 // zetamag call func zetamagcap with a default truncation
405 func zetamag(x, y float64) float64 {
406 return cmplx.Abs(zetacap(complex(x, y), zetaTrunc))
407 }
408
409 // etamagcap is the real-valued magnitude of the truncated approx. of func eta
410 func zetamagcap(x, y float64, max float64) float64 {
411 return cmplx.Abs(etacap(complex(x, y), int(max)))
412 }
413
414 // zetacap accepts a cap/max iteration value for the zeta func truncation
415 func zetacap(x complex128, max int) complex128 {
416 y := 0 + 0i
417 v := 1 + 0i
418
419 for n := 1; n <= max; n++ {
420 y += v
421 v /= x
422 }
423 return y
424 }
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: ./filetypes/filetypes.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 filetypes
26
27 import "bytes"
28
29 // nameToMIME tries to match a MIME type to a filename, dotted file extension,
30 // or a dot-less filetype/extension given
31 func nameToMIME(fname string) (mimeType string, ok bool) {
32 // handle dotless file types and filenames alike
33 kind, ok := type2mime[makeDotless(fname)]
34 return kind, ok
35 }
36
37 // DetectMIME guesses the first appropriate MIME type from the first few
38 // data bytes given: 24 bytes are enough to detect all supported types
39 func DetectMIME(b []byte) (mimeType string, ok bool) {
40 if t, ok := detectType(b); ok {
41 return t, true
42 }
43 return ``, false
44 }
45
46 // detectType guesses the first appropriate file type for the data given:
47 // here the type is a a filename extension without the leading dot
48 func detectType(b []byte) (dotlessExt string, ok bool) {
49 // empty data, so there's no way to detect anything
50 if len(b) == 0 {
51 return ``, false
52 }
53
54 // check for plain-text web-document formats case-insensitively
55 kind, ok := checkDoc(b)
56 if ok {
57 return kind, true
58 }
59
60 // check data formats which allow any byte at the start
61 kind, ok = checkSpecial(b)
62 if ok {
63 return kind, true
64 }
65
66 // check all other supported data formats
67 headers := hdrDispatch[b[0]]
68 for _, t := range headers {
69 if hasPrefixPattern(b[1:], t.Header[1:], cba) {
70 return t.Type, true
71 }
72 }
73
74 // unrecognized data format
75 return ``, false
76 }
77
78 // checkDoc tries to guess if the bytes given are the start of HTML, SVG,
79 // XML, or JSON data
80 func checkDoc(b []byte) (kind string, ok bool) {
81 // ignore leading whitespaces
82 b = trimLeadingWhitespace(b)
83
84 // can't detect anything with empty data
85 if len(b) == 0 {
86 return ``, false
87 }
88
89 // handle XHTML documents which don't start with a doctype declaration
90 if bytes.Contains(b, doctypeHTML) {
91 return html, true
92 }
93
94 // handle HTML/SVG/XML documents
95 if hasPrefixByte(b, '<') {
96 if hasPrefixFold(b, []byte{'<', '?', 'x', 'm', 'l'}) {
97 if bytes.Contains(b, []byte{'<', 's', 'v', 'g'}) {
98 return svg, true
99 }
100 return xml, true
101 }
102
103 headers := hdrDispatch['<']
104 for _, v := range headers {
105 if hasPrefixFold(b, v.Header) {
106 return v.Type, true
107 }
108 }
109 return ``, false
110 }
111
112 // handle JSON with top-level arrays
113 if hasPrefixByte(b, '[') {
114 // match [", or [[, or [{, ignoring spaces between
115 b = trimLeadingWhitespace(b[1:])
116 if len(b) > 0 {
117 switch b[0] {
118 case '"', '[', '{':
119 return json, true
120 }
121 }
122 return ``, false
123 }
124
125 // handle JSON with top-level objects
126 if hasPrefixByte(b, '{') {
127 // match {", ignoring spaces between: after {, the only valid syntax
128 // which can follow is the opening quote for the expected object-key
129 b = trimLeadingWhitespace(b[1:])
130 if hasPrefixByte(b, '"') {
131 return json, true
132 }
133 return ``, false
134 }
135
136 // checking for a quoted string, any of the JSON keywords, or even a
137 // number seems too ambiguous to declare the data valid JSON
138
139 // no web-document format detected
140 return ``, false
141 }
142
143 // checkSpecial handles special file-format headers, which should be checked
144 // before the normal file-type headers, since the first-byte dispatch algo
145 // doesn't work for these
146 func checkSpecial(b []byte) (kind string, ok bool) {
147 if len(b) >= 8 && bytes.Index(b, []byte{'f', 't', 'y', 'p'}) == 4 {
148 for _, t := range specialHeaders {
149 if hasPrefixPattern(b[4:], t.Header[4:], cba) {
150 return t.Type, true
151 }
152 }
153 }
154 return ``, false
155 }
156
157 // hasPrefixPattern works like bytes.HasPrefix, except it allows for a special
158 // value to signal any byte is allowed on specific spots
159 func hasPrefixPattern(what []byte, pat []byte, wildcard byte) bool {
160 // if the data are shorter than the pattern to match, there's no match
161 if len(what) < len(pat) {
162 return false
163 }
164
165 // use a slice which ensures the pattern length is never exceeded
166 what = what[:len(pat)]
167
168 for i, x := range what {
169 y := pat[i]
170 if x != y && y != wildcard {
171 return false
172 }
173 }
174 return true
175 }
File: ./filetypes/filetypes_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 filetypes
26
27 import (
28 "bytes"
29 "testing"
30 )
31
32 func TestCheckDoc(t *testing.T) {
33 const (
34 lf = "\n"
35 crlf = "\r\n"
36 tab = "\t"
37 xmlIntro = `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>`
38 )
39
40 tests := []struct {
41 Input string
42 Expected string
43 }{
44 {``, ``},
45 {`{"abc":123}`, json},
46 {`[` + lf + ` {"abc":123}`, json},
47 {`[` + lf + ` {"abc":123}`, json},
48 {`[` + crlf + tab + `{"abc":123}`, json},
49
50 {``, ``},
51 {`<?xml?>`, xml},
52 {`<?xml?><records>`, xml},
53 {`<?xml?>` + lf + `<records>`, xml},
54 {`<?xml?><svg>`, svg},
55 {`<?xml?>` + crlf + `<svg>`, svg},
56 {xmlIntro + lf + `<svg`, svg},
57 {xmlIntro + crlf + `<svg`, svg},
58 }
59
60 for _, tc := range tests {
61 t.Run(tc.Input, func(t *testing.T) {
62 res, _ := checkDoc([]byte(tc.Input))
63 if res != tc.Expected {
64 t.Fatalf(`got %v, expected %v instead`, res, tc.Expected)
65 }
66 })
67 }
68 }
69
70 func TestHasPrefixPattern(t *testing.T) {
71 var (
72 data = []byte{
73 'R', 'I', 'F', 'F', 0xf0, 0xba, 0xc8, 0x2b, 'A', 'V', 'I', ' ',
74 }
75 pat = []byte{
76 'R', 'I', 'F', 'F', cba, cba, cba, cba, 'A', 'V', 'I', ' ',
77 }
78 )
79
80 if !hasPrefixPattern(data, pat, cba) {
81 t.Fatal(`wildcard pattern not working`)
82 }
83 }
84
85 func BenchmarkHasPrefixMatch(b *testing.B) {
86 var (
87 data = []byte{
88 'R', 'I', 'F', 'F', 0xf0, 0xba, 0xc8, 0x2b, 'A', 'V', 'I', ' ',
89 }
90 pat = []byte{
91 'R', 'I', 'F', 'F', cba, cba, cba, cba, 'A', 'V', 'I', ' ',
92 }
93 )
94
95 b.ReportAllocs()
96 b.ResetTimer()
97
98 for i := 0; i < b.N; i++ {
99 if !bytes.HasPrefix(data, pat) {
100 b.Fatal(`pattern was specifically chosen to match, but didn't`)
101 }
102 }
103 }
104
105 func BenchmarkHasPrefixPatternMatch(b *testing.B) {
106 var (
107 data = []byte{
108 'R', 'I', 'F', 'F', 0xf0, 0xba, 0xc8, 0x2b, 'A', 'V', 'I', ' ',
109 }
110 pat = []byte{
111 'R', 'I', 'F', 'F', cba, cba, cba, cba, 'A', 'V', 'I', ' ',
112 }
113 )
114
115 b.ReportAllocs()
116 b.ResetTimer()
117
118 for i := 0; i < b.N; i++ {
119 if !hasPrefixPattern(data, pat, cba) {
120 b.Fatal(`pattern was specifically chosen to match, but didn't`)
121 }
122 }
123 }
File: ./filetypes/mimedata.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 filetypes
26
27 // all the MIME types used/recognized in this package
28 const (
29 aiff = `audio/aiff`
30 au = `audio/basic`
31 avi = `video/avi`
32 avif = `image/avif`
33 bmp = `image/x-bmp`
34 caf = `audio/x-caf`
35 cur = `image/vnd.microsoft.icon`
36 css = `text/css`
37 csv = `text/csv`
38 djvu = `image/x-djvu`
39 elf = `application/x-elf`
40 exe = `application/vnd.microsoft.portable-executable`
41 flac = `audio/x-flac`
42 gif = `image/gif`
43 gz = `application/gzip`
44 heic = `image/heic`
45 htm = `text/html`
46 html = `text/html`
47 ico = `image/x-icon`
48 iso = `application/octet-stream`
49 jpg = `image/jpeg`
50 jpeg = `image/jpeg`
51 js = `application/javascript`
52 json = `application/json`
53 m4a = `audio/aac`
54 m4v = `video/x-m4v`
55 mid = `audio/midi`
56 mov = `video/quicktime`
57 mp4 = `video/mp4`
58 mp3 = `audio/mpeg`
59 mpg = `video/mpeg`
60 ogg = `audio/ogg`
61 opus = `audio/opus`
62 pdf = `application/pdf`
63 png = `image/png`
64 ps = `application/postscript`
65 psd = `image/vnd.adobe.photoshop`
66 rtf = `application/rtf`
67 sqlite3 = `application/x-sqlite3`
68 svg = `image/svg+xml`
69 text = `text/plain`
70 tiff = `image/tiff`
71 tsv = `text/tsv`
72 wasm = `application/wasm`
73 wav = `audio/x-wav`
74 webp = `image/webp`
75 webm = `video/webm`
76 xml = `application/xml`
77 zip = `application/zip`
78 zst = `application/zstd`
79 )
80
81 // type2mime turns dotless format-names into MIME types
82 var type2mime = map[string]string{
83 `aiff`: aiff,
84 `wav`: wav,
85 `avi`: avi,
86 `jpg`: jpg,
87 `jpeg`: jpeg,
88 `m4a`: m4a,
89 `mp4`: mp4,
90 `m4v`: m4v,
91 `mov`: mov,
92 `png`: png,
93 `avif`: avif,
94 `webp`: webp,
95 `gif`: gif,
96 `tiff`: tiff,
97 `psd`: psd,
98 `flac`: flac,
99 `webm`: webm,
100 `mpg`: mpg,
101 `zip`: zip,
102 `gz`: gz,
103 `zst`: zst,
104 `mp3`: mp3,
105 `opus`: opus,
106 `bmp`: bmp,
107 `mid`: mid,
108 `ogg`: ogg,
109 `html`: html,
110 `htm`: htm,
111 `svg`: svg,
112 `xml`: xml,
113 `rtf`: rtf,
114 `pdf`: pdf,
115 `ps`: ps,
116 `au`: au,
117 `ico`: ico,
118 `cur`: cur,
119 `caf`: caf,
120 `heic`: heic,
121 `sqlite3`: sqlite3,
122 `elf`: elf,
123 `exe`: exe,
124 `wasm`: wasm,
125 `iso`: iso,
126 `txt`: text,
127 `css`: css,
128 `csv`: csv,
129 `tsv`: tsv,
130 `js`: js,
131 `json`: json,
132 `geojson`: json,
133 }
134
135 // formatDescriptor ties a file-header pattern to its data-format type
136 type formatDescriptor struct {
137 Header []byte
138 Type string
139 }
140
141 // can be anything: ensure this value differs from all other literal bytes
142 // in the generic-headers table: failing that, its value could cause subtle
143 // type-misdetection bugs
144 const cba = 0xFD // 253, which is > 127, the highest-valued ascii symbol
145
146 // dash-streamed m4a format
147 var m4aDash = []byte{
148 cba, cba, cba, cba, 'f', 't', 'y', 'p', 'd', 'a', 's', 'h',
149 000, 000, 000, 000, 'i', 's', 'o', '6', 'm', 'p', '4', '1',
150 }
151
152 // format markers with leading wildcards, which should be checked before the
153 // normal ones: this is to prevent mismatches with the latter types, even
154 // though you can make probabilistic arguments which suggest these mismatches
155 // should be very unlikely in practice
156 var specialHeaders = []formatDescriptor{
157 {[]byte{cba, cba, cba, cba, 'f', 't', 'y', 'p', 'M', '4', 'A', ' '}, m4a},
158 {[]byte{cba, cba, cba, cba, 'f', 't', 'y', 'p', 'M', '4', 'A', 000}, m4a},
159 {[]byte{cba, cba, cba, cba, 'f', 't', 'y', 'p', 'M', 'S', 'N', 'V'}, mp4},
160 {[]byte{cba, cba, cba, cba, 'f', 't', 'y', 'p', 'i', 's', 'o', 'm'}, mp4},
161 {[]byte{cba, cba, cba, cba, 'f', 't', 'y', 'p', 'm', 'p', '4', '2'}, m4v},
162 {[]byte{cba, cba, cba, cba, 'f', 't', 'y', 'p', 'q', 't', ' ', ' '}, mov},
163 {[]byte{cba, cba, cba, cba, 'f', 't', 'y', 'p', 'h', 'e', 'i', 'c'}, heic},
164 {[]byte{cba, cba, cba, cba, 'f', 't', 'y', 'p', 'a', 'v', 'i', 'f'}, avif},
165 {m4aDash, m4a},
166 }
167
168 // sqlite3 database format
169 var sqlite3db = []byte{
170 'S', 'Q', 'L', 'i', 't', 'e', ' ',
171 'f', 'o', 'r', 'm', 'a', 't', ' ', '3',
172 000,
173 }
174
175 // windows-variant bitmap file-header, which is followed by a byte-counter for
176 // the 40-byte infoheader which follows that
177 var winbmp = []byte{
178 'B', 'M', cba, cba, cba, cba, cba, cba, cba, cba, cba, cba, cba, cba, 40,
179 }
180
181 // deja-vu document format
182 var djv = []byte{
183 'A', 'T', '&', 'T', 'F', 'O', 'R', 'M', cba, cba, cba, cba, 'D', 'J', 'V',
184 }
185
186 var doctypeHTML = []byte{
187 '<', '!', 'D', 'O', 'C', 'T', 'Y', 'P', 'E', ' ', 'h', 't', 'm', 'l',
188 }
189
190 // hdrDispatch groups format-description-groups by their first byte, thus
191 // shortening total lookups for some data header: notice how the `ftyp` data
192 // formats aren't handled here, since these can start with any byte, instead
193 // of the literal value of the any-byte markers they use
194 var hdrDispatch = [256][]formatDescriptor{
195 {
196 {[]byte{000, 000, 001, 0xBA}, mpg},
197 {[]byte{000, 000, 001, 0xB3}, mpg},
198 {[]byte{000, 000, 001, 000}, ico},
199 {[]byte{000, 000, 002, 000}, cur},
200 {[]byte{000, 'a', 's', 'm'}, wasm},
201 }, // 0
202 nil, // 1
203 nil, // 2
204 nil, // 3
205 nil, // 4
206 nil, // 5
207 nil, // 6
208 nil, // 7
209 nil, // 8
210 nil, // 9
211 nil, // 10
212 nil, // 11
213 nil, // 12
214 nil, // 13
215 nil, // 14
216 nil, // 15
217 nil, // 16
218 nil, // 17
219 nil, // 18
220 nil, // 19
221 nil, // 20
222 nil, // 21
223 nil, // 22
224 nil, // 23
225 nil, // 24
226 nil, // 25
227 {
228 {[]byte{0x1A, 0x45, 0xDF, 0xA3}, webm},
229 }, // 26
230 nil, // 27
231 nil, // 28
232 nil, // 29
233 nil, // 30
234 {
235 // {[]byte{0x1F, 0x8B, 0x08, 0x08}, gz},
236 {[]byte{0x1F, 0x8B, 0x08}, gz},
237 }, // 31
238 nil, // 32
239 nil, // 33 !
240 nil, // 34 "
241 {
242 {[]byte{'#', '!', ' '}, text},
243 {[]byte{'#', '!', '/'}, text},
244 }, // 35 #
245 nil, // 36 $
246 {
247 {[]byte{'%', 'P', 'D', 'F'}, pdf},
248 {[]byte{'%', '!', 'P', 'S'}, ps},
249 }, // 37 %
250 nil, // 38 &
251 nil, // 39 '
252 {
253 {[]byte{0x28, 0xB5, 0x2F, 0xFD}, zst},
254 }, // 40 (
255 nil, // 41 )
256 nil, // 42 *
257 nil, // 43 +
258 nil, // 44 ,
259 nil, // 45 -
260 {
261 {[]byte{'.', 's', 'n', 'd'}, au},
262 }, // 46 .
263 nil, // 47 /
264 nil, // 48 0
265 nil, // 49 1
266 nil, // 50 2
267 nil, // 51 3
268 nil, // 52 4
269 nil, // 53 5
270 nil, // 54 6
271 nil, // 55 7
272 {
273 {[]byte{'8', 'B', 'P', 'S'}, psd},
274 }, // 56 8
275 nil, // 57 9
276 nil, // 58 :
277 nil, // 59 ;
278 {
279 // func checkDoc is better for these, since it's case-insensitive
280 {doctypeHTML, html},
281 {[]byte{'<', 's', 'v', 'g'}, svg},
282 {[]byte{'<', 'h', 't', 'm', 'l', '>'}, html},
283 {[]byte{'<', 'h', 'e', 'a', 'd', '>'}, html},
284 {[]byte{'<', 'b', 'o', 'd', 'y', '>'}, html},
285 {[]byte{'<', '?', 'x', 'm', 'l'}, xml},
286 }, // 60 <
287 nil, // 61 =
288 nil, // 62 >
289 nil, // 63 ?
290 nil, // 64 @
291 {
292 {djv, djvu},
293 }, // 65 A
294 {
295 {winbmp, bmp},
296 }, // 66 B
297 nil, // 67 C
298 nil, // 68 D
299 nil, // 69 E
300 {
301 {[]byte{'F', 'O', 'R', 'M', cba, cba, cba, cba, 'A', 'I', 'F', 'F'}, aiff},
302 {[]byte{'F', 'O', 'R', 'M', cba, cba, cba, cba, 'A', 'I', 'F', 'C'}, aiff},
303 }, // 70 F
304 {
305 {[]byte{'G', 'I', 'F', '8', '7', 'a'}, gif},
306 {[]byte{'G', 'I', 'F', '8', '9', 'a'}, gif},
307 }, // 71 G
308 nil, // 72 H
309 {
310 {[]byte{'I', 'D', '3', 2}, mp3}, // ID3-format metadata
311 {[]byte{'I', 'D', '3', 3}, mp3}, // ID3-format metadata
312 {[]byte{'I', 'D', '3', 4}, mp3}, // ID3-format metadata
313 {[]byte{'I', 'I', '*', 000}, tiff},
314 }, // 73 I
315 nil, // 74 J
316 nil, // 75 K
317 nil, // 76 L
318 {
319 {[]byte{'M', 'M', 000, '*'}, tiff},
320 {[]byte{'M', 'T', 'h', 'd'}, mid},
321 {[]byte{'M', 'Z', cba, 000, cba, 000}, exe},
322 // {[]byte{'M', 'Z', 0x90, 000, 003, 000}, exe},
323 // {[]byte{'M', 'Z', 0x78, 000, 001, 000}, exe},
324 // {[]byte{'M', 'Z', 'P', 000, 002, 000}, exe},
325 }, // 77 M
326 nil, // 78 N
327 {
328 {[]byte{'O', 'g', 'g', 'S'}, ogg},
329 }, // 79 O
330 {
331 {[]byte{'P', 'K', 003, 004}, zip},
332 }, // 80 P
333 nil, // 81 Q
334 {
335 {[]byte{'R', 'I', 'F', 'F', cba, cba, cba, cba, 'W', 'E', 'B', 'P'}, webp},
336 {[]byte{'R', 'I', 'F', 'F', cba, cba, cba, cba, 'W', 'A', 'V', 'E'}, wav},
337 {[]byte{'R', 'I', 'F', 'F', cba, cba, cba, cba, 'A', 'V', 'I', ' '}, avi},
338 }, // 82 R
339 {
340 {sqlite3db, sqlite3},
341 }, // 83 S
342 nil, // 84 T
343 nil, // 85 U
344 nil, // 86 V
345 nil, // 87 W
346 nil, // 88 X
347 nil, // 89 Y
348 nil, // 90 Z
349 nil, // 91 [
350 nil, // 92 \
351 nil, // 93 ]
352 nil, // 94 ^
353 nil, // 95 _
354 nil, // 96 `
355 nil, // 97 a
356 nil, // 98 b
357 {
358 {[]byte{'c', 'a', 'f', 'f', 000, 001, 000, 000}, caf},
359 }, // 99 c
360 nil, // 100 d
361 nil, // 101 e
362 {
363 {[]byte{'f', 'L', 'a', 'C'}, flac},
364 }, // 102 f
365 nil, // 103 g
366 nil, // 104 h
367 nil, // 105 i
368 nil, // 106 j
369 nil, // 107 k
370 nil, // 108 l
371 nil, // 109 m
372 nil, // 110 n
373 nil, // 111 o
374 nil, // 112 p
375 nil, // 113 q
376 nil, // 114 r
377 nil, // 115 s
378 nil, // 116 t
379 nil, // 117 u
380 nil, // 118 v
381 nil, // 119 w
382 nil, // 120 x
383 nil, // 121 y
384 nil, // 122 z
385 {
386 {[]byte{'{', '\\', 'r', 't', 'f'}, rtf},
387 }, // 123 {
388 nil, // 124 |
389 nil, // 125 }
390 nil, // 126
391 {
392 {[]byte{127, 'E', 'L', 'F'}, elf},
393 }, // 127
394 nil, // 128
395 nil, // 129
396 nil, // 130
397 nil, // 131
398 nil, // 132
399 nil, // 133
400 nil, // 134
401 nil, // 135
402 nil, // 136
403 {
404 {[]byte{0x89, 'P', 'N', 'G', 0x0D, 0x0A, 0x1A, 0x0A}, png},
405 }, // 137
406 nil, // 138
407 nil, // 139
408 nil, // 140
409 nil, // 141
410 nil, // 142
411 nil, // 143
412 nil, // 144
413 nil, // 145
414 nil, // 146
415 nil, // 147
416 nil, // 148
417 nil, // 149
418 nil, // 150
419 nil, // 151
420 nil, // 152
421 nil, // 153
422 nil, // 154
423 nil, // 155
424 nil, // 156
425 nil, // 157
426 nil, // 158
427 nil, // 159
428 nil, // 160
429 nil, // 161
430 nil, // 162
431 nil, // 163
432 nil, // 164
433 nil, // 165
434 nil, // 166
435 nil, // 167
436 nil, // 168
437 nil, // 169
438 nil, // 170
439 nil, // 171
440 nil, // 172
441 nil, // 173
442 nil, // 174
443 nil, // 175
444 nil, // 176
445 nil, // 177
446 nil, // 178
447 nil, // 179
448 nil, // 180
449 nil, // 181
450 nil, // 182
451 nil, // 183
452 nil, // 184
453 nil, // 185
454 nil, // 186
455 nil, // 187
456 nil, // 188
457 nil, // 189
458 nil, // 190
459 nil, // 191
460 nil, // 192
461 nil, // 193
462 nil, // 194
463 nil, // 195
464 nil, // 196
465 nil, // 197
466 nil, // 198
467 nil, // 199
468 nil, // 200
469 nil, // 201
470 nil, // 202
471 nil, // 203
472 nil, // 204
473 nil, // 205
474 nil, // 206
475 nil, // 207
476 nil, // 208
477 nil, // 209
478 nil, // 210
479 nil, // 211
480 nil, // 212
481 nil, // 213
482 nil, // 214
483 nil, // 215
484 nil, // 216
485 nil, // 217
486 nil, // 218
487 nil, // 219
488 nil, // 220
489 nil, // 221
490 nil, // 222
491 nil, // 223
492 nil, // 224
493 nil, // 225
494 nil, // 226
495 nil, // 227
496 nil, // 228
497 nil, // 229
498 nil, // 230
499 nil, // 231
500 nil, // 232
501 nil, // 233
502 nil, // 234
503 nil, // 235
504 nil, // 236
505 nil, // 237
506 nil, // 238
507 nil, // 239
508 nil, // 240
509 nil, // 241
510 nil, // 242
511 nil, // 243
512 nil, // 244
513 nil, // 245
514 nil, // 246
515 nil, // 247
516 nil, // 248
517 nil, // 249
518 nil, // 250
519 nil, // 251
520 nil, // 252
521 nil, // 253
522 nil, // 254
523 {
524 {[]byte{0xFF, 0xD8, 0xFF}, jpg},
525 {[]byte{0xFF, 0xF3, 0x48, 0xC4, 0x00}, mp3},
526 {[]byte{0xFF, 0xFB}, mp3},
527 }, // 255
528 }
File: ./filetypes/mimedata_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 filetypes
26
27 import (
28 "strconv"
29 "testing"
30 )
31
32 func TestData(t *testing.T) {
33 t.Run(`could-be-anything constant`, func(t *testing.T) {
34 if len(hdrDispatch[cba]) != 0 {
35 const fs = `chosen constant %d collides with header entries`
36 t.Fatalf(fs, cba)
37 }
38 })
39
40 for i, v := range hdrDispatch {
41 t.Run(`dispatch @ `+strconv.Itoa(i), func(t *testing.T) {
42 const fs = `expected leading byte to be %d, but got %d instead`
43 for _, e := range v {
44 if e.Header[0] != byte(i) {
45 t.Fatalf(fs, i, e.Header[0])
46 return
47 }
48 }
49 })
50 }
51 }
File: ./filetypes/strings.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 filetypes
26
27 import (
28 "bytes"
29 "strings"
30 )
31
32 // makeDotless is similar to filepath.Ext, except its results never start
33 // with a dot
34 func makeDotless(s string) string {
35 if i := strings.LastIndexByte(s, '.'); i >= 0 {
36 return s[(i + 1):]
37 }
38 return s
39 }
40
41 // hasPrefixByte is a simpler, single-byte version of bytes.HasPrefix
42 func hasPrefixByte(b []byte, prefix byte) bool {
43 return len(b) > 0 && b[0] == prefix
44 }
45
46 // hasPrefixFold is a case-insensitive bytes.HasPrefix
47 func hasPrefixFold(s []byte, prefix []byte) bool {
48 n := len(prefix)
49 return len(s) >= n && bytes.EqualFold(s[:n], prefix)
50 }
51
52 // trimLeadingWhitespace ignores leading space-like symbols: this is useful
53 // to handle text-based data formats more flexibly
54 func trimLeadingWhitespace(b []byte) []byte {
55 for len(b) > 0 {
56 switch b[0] {
57 case ' ', '\t', '\n', '\r':
58 b = b[1:]
59 default:
60 return b
61 }
62 }
63
64 // an empty slice is all that's left, at this point
65 return nil
66 }
File: ./filetypes/strings_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 filetypes
26
27 import (
28 "bytes"
29 "testing"
30 )
31
32 func TestHasPrefixByte(t *testing.T) {
33 var tests = []struct {
34 Data []byte
35 Prefix byte
36 Expected bool
37 }{
38 {nil, 'x', false},
39 {[]byte(`x`), 'x', true},
40 {[]byte(` x`), 'x', false},
41 {[]byte(`xyz`), 'a', false},
42 {[]byte(`abcxyz`), 'a', true},
43 }
44
45 for _, tc := range tests {
46 t.Run(string(tc.Data), func(t *testing.T) {
47 got := hasPrefixByte(tc.Data, tc.Prefix)
48 if got != tc.Expected {
49 const fs = `expected %v, but got %v instead`
50 t.Fatalf(fs, tc.Expected, got)
51 }
52 })
53 }
54 }
55
56 func TestHasPrefixFold(t *testing.T) {
57 var tests = []struct {
58 Data []byte
59 Prefix []byte
60 Expected bool
61 }{
62 {[]byte("<!docTYPE html>\n<html>"), []byte(`<!doctype HTML`), true},
63 }
64
65 for _, tc := range tests {
66 t.Run("", func(t *testing.T) {
67 got := hasPrefixFold(tc.Data, tc.Prefix)
68 if got != tc.Expected {
69 const fs = `expected %v, but got %v instead`
70 t.Fatalf(fs, tc.Expected, got)
71 }
72 })
73 }
74 }
75
76 func TestTrimLeadingWhitespaces(t *testing.T) {
77 var tests = []struct {
78 Data []byte
79 Expected []byte
80 }{
81 {[]byte(`abc`), []byte(`abc`)},
82 {[]byte(" \t"), nil},
83 {[]byte(" \tabc"), []byte(`abc`)},
84 {[]byte("\r\nabc"), []byte(`abc`)},
85 }
86
87 for _, tc := range tests {
88 t.Run("", func(t *testing.T) {
89 got := trimLeadingWhitespace(tc.Data)
90 if !bytes.Equal(got, tc.Expected) {
91 const fs = `expected %#v, but got %#v instead`
92 t.Fatalf(fs, tc.Expected, got)
93 }
94 })
95 }
96 }
File: ./finfo/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 finfo
26
27 import (
28 "flag"
29 "fmt"
30 "strings"
31 )
32
33 type config struct {
34 To string // output format: any of TSV, JSON, or HTML
35 SortBy string // how to sort results
36 Title string // title to use when emitting HTML
37
38 Bytes bool // show file sizes in bytes
39 KiB bool // show file sizes in kib
40 MiB bool // show file sizes in mib
41 GiB bool // show file sizes in gib
42
43 Lines bool // calculate and show lines (treating all files as plain-text)
44 Text bool // calculate and show plain-text-related info (treating all files as such)
45 Duration bool // calculate and show playing duration (for supported media files)
46 HMS bool // also show playing duration in hour-minute-seconds format
47 Picture bool // find and show picture widths and heights (for supported files)
48 Type bool // show file types (its extension without the starting dot)
49 MIMEType bool // find and show MIME file types
50 Ext bool // show the filename extension
51 Folder bool // show the folders files are in
52 }
53
54 const (
55 picResUsage = "show width, height, and bits per pixel for supported picture files"
56 linesUsage = "show the number of lines by treating files as plain-text ones"
57 textUsage = "show plain-text-related info, treating all files as plain-text"
58 durationUsage = "show the playing duration for supported media files"
59 hmsUsage = "also show the playing duration in hour-minute-seconds format"
60 typeUsage = "show file types using dotless filename extensions"
61 )
62
63 func parseFlags(usage string) config {
64 flag.Usage = func() {
65 fmt.Fprintf(flag.CommandLine.Output(), "%s\n\nOptions\n\n", usage)
66 flag.PrintDefaults()
67 }
68
69 cfg := config{
70 To: "tsv",
71 SortBy: "bytes",
72 Title: "File Info",
73
74 Type: true,
75 Bytes: true,
76 }
77
78 flag.StringVar(&cfg.To, "to", cfg.To, "output format: one of tsv, json, or html")
79 flag.StringVar(&cfg.SortBy, "sort", cfg.SortBy, "what to (reverse-)sort results by")
80 flag.StringVar(&cfg.Title, "title", cfg.Title, "title to use when emitting HTML")
81 flag.BoolVar(&cfg.Bytes, "bytes", cfg.Bytes, "show file sizes in bytes")
82 flag.BoolVar(&cfg.KiB, "kib", cfg.KiB, "show file sizes in KiBs")
83 flag.BoolVar(&cfg.MiB, "mib", cfg.MiB, "show file sizes in MiBs")
84 flag.BoolVar(&cfg.GiB, "gib", cfg.GiB, "show file sizes in GiBs")
85 flag.BoolVar(&cfg.Lines, "l", cfg.Lines, "alias for option -lines")
86 flag.BoolVar(&cfg.Duration, "d", cfg.Duration, "alias for option -duration")
87 flag.BoolVar(&cfg.Picture, "res", cfg.Picture, "alias for option -resolution")
88 flag.BoolVar(&cfg.Picture, "resolution", cfg.Picture, picResUsage)
89 flag.BoolVar(&cfg.Lines, "lines", cfg.Lines, linesUsage)
90 flag.BoolVar(&cfg.Text, "text", cfg.Text, textUsage)
91 flag.BoolVar(&cfg.Duration, "duration", cfg.Duration, durationUsage)
92 flag.BoolVar(&cfg.HMS, "hms", cfg.HMS, hmsUsage)
93 flag.BoolVar(&cfg.Type, "type", cfg.Type, typeUsage)
94 flag.BoolVar(&cfg.MIMEType, "mime", cfg.MIMEType, "show the file's MIME type")
95 flag.BoolVar(&cfg.Folder, "folder", cfg.Folder, "show folder names")
96 flag.Parse()
97
98 // normalize values for option `-to`
99 cfg.To = strings.ToLower(strings.TrimPrefix(cfg.To, "."))
100
101 // normalize value aliases for option -sort and auto-enable any settings these imply
102 switch strings.ToLower(cfg.SortBy) {
103 case "", "byte", "bytes", "size", "kb", "mb", "b", "s":
104 cfg.SortBy = "bytes"
105 case "line", "lines", "ln", "l":
106 cfg.SortBy = "lines"
107 // auto-enable the line-counter if sorting by lines
108 cfg.Lines = true
109 case "duration", "time", "dur", "d":
110 cfg.SortBy = "duration"
111 // auto-enable the time-duration-counter if sorting by duration
112 cfg.Duration = true
113 case "column", "columns", "c":
114 cfg.SortBy = "columns"
115 // auto-enable text-specific info
116 cfg.Text = true
117 case "cr":
118 cfg.SortBy = "cr"
119 // auto-enable text-specific info
120 cfg.Text = true
121 case "bom":
122 cfg.SortBy = "bom"
123 // auto-enable text-specific info
124 cfg.Text = true
125 case "name", "n":
126 cfg.SortBy = "name"
127 }
128
129 // auto-enable duration when asked to also show it in HH:MM:SS foramt
130 if cfg.HMS {
131 cfg.Duration = true
132 }
133 return cfg
134 }
135
136 func (c config) NeedsExtraInfo() bool {
137 return c.Lines || c.Text || c.Duration || c.Picture || c.MIMEType
138 }
File: ./finfo/fileinfo.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 finfo
26
27 import (
28 "io"
29 "os"
30 "path/filepath"
31 "strings"
32
33 "../filetypes"
34 "../mediainfo"
35 )
36
37 // FileInfo has all sorts of summary stats about files, including its auto-detected
38 // type and multimedia properties when these are available.
39 type FileInfo struct {
40 // general info
41 Name string `json:"name"`
42 Folder string `json:"folder"`
43 Size int `json:"size"`
44
45 // plain-text stats
46 Lines int `json:"lines"`
47 Columns int `json:"columns"`
48 Separator rune `json:"separator"`
49 CarriageReturns int `json:"cr"`
50 ByteOrderMarks int `json:"bom"`
51
52 // sound/image stats
53 Duration float64 `json:"duration"`
54 Width int `json:"width"`
55 Height int `json:"height"`
56 BitsPerPixel int `json:"bpp"`
57
58 MIMEType string `json:"mime"`
59 Problem error `json:"-"`
60 }
61
62 func newFileInfo(fname string, size int) FileInfo {
63 return FileInfo{
64 Name: fname,
65 Folder: filepath.Dir(fname),
66 Size: size,
67
68 Lines: -1,
69 Columns: -1,
70 CarriageReturns: -1,
71 ByteOrderMarks: -1,
72
73 Duration: -1,
74 Width: -1,
75 Height: -1,
76 BitsPerPixel: -1,
77
78 MIMEType: "",
79 Problem: nil,
80 }
81 }
82
83 func (fi *FileInfo) hasLines() bool {
84 switch fi.MIMEType {
85 case "", "application/xml", "application/json":
86 return true
87 }
88 return strings.HasPrefix(fi.MIMEType, "text/") || strings.HasPrefix(fi.MIMEType, "image/svg")
89 }
90
91 func (r *FileInfo) calculateStats(cfg config) {
92 // empty files have no lines and last no time: no need to open a file in this case
93 if r.Size == 0 {
94 return
95 }
96
97 f, err := os.Open(r.Name)
98 if err != nil {
99 r.Problem = err
100 return
101 }
102 defer f.Close()
103
104 if cfg.Duration {
105 d, err := mediainfo.FileDuration(f)
106 if err != nil {
107 r.Problem = err
108 } else {
109 r.Duration = d
110 }
111
112 // later readers must start from the beginning of the file
113 f.Seek(0, io.SeekStart)
114 }
115
116 if cfg.Picture {
117 w, h, bd, err := mediainfo.Resolution(f, r.Name)
118 if err == nil {
119 r.Width = w
120 r.Height = h
121 r.BitsPerPixel = bd
122 } else {
123 r.Problem = err
124 }
125
126 // later readers must start from the beginning of the file
127 f.Seek(0, io.SeekStart)
128 }
129
130 if cfg.MIMEType || cfg.Text {
131 var b [128]byte
132 n, err := f.Read(b[:])
133 mime, ok := filetypes.DetectMIME(b[:n])
134 if !ok || (err != nil && err != io.EOF) {
135 mime = ""
136 }
137 r.MIMEType = mime
138
139 // later readers must start from the beginning of the file
140 f.Seek(0, io.SeekStart)
141 }
142
143 // don't count lines for most mime types
144 if (cfg.Lines || cfg.Text) && r.hasLines() {
145 st, err := summarizePlainText(f)
146 // csv files need special handling to count their # of columns and autodetect their separator
147 if strings.HasSuffix(r.Name, ".csv") || strings.HasSuffix(r.Name, ".CSV") {
148 f.Seek(0, io.SeekStart)
149 adjustForCSV(f, &st)
150 }
151
152 if err == nil {
153 r.Lines = st.Lines
154 // non-empty text files without newlines count as having 1 line
155 if r.Lines == 0 && r.Size > 0 {
156 r.Lines = 1
157 }
158 // non-empty text files with a detected field-separator count as having at least 1 column
159 r.Columns = st.Columns
160 if r.Columns == 0 && r.Size > 0 && st.EmptyLines < r.Lines {
161 r.Columns = 1
162 }
163 r.Separator = st.Separator
164 r.CarriageReturns = st.CarriageReturns
165 r.ByteOrderMarks = st.ByteOrderMarks
166 } else {
167 r.Problem = err
168 }
169
170 // xml and json data have no separators nor columns to count
171 if strings.HasPrefix(r.MIMEType, "application/") {
172 r.Columns = -1
173 r.Separator = rune(0)
174 }
175 }
176 }
File: ./finfo/grouping.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 finfo
26
27 import (
28 "path/filepath"
29 "strings"
30 )
31
32 type FolderInfo struct {
33 Name string `json:"name"`
34 Path string `json:"path"`
35 Folders []FolderInfo `json:"folders"`
36 Files []FileInfo `json:"files"`
37
38 NumItems int `json:"items"`
39 Size int `json:"size"`
40 Lines int `json:"lines"`
41 CarriageReturns int `json:"cr"`
42 ByteOrderMarks int `json:"bom"`
43 Duration float64 `json:"duration"`
44 }
45
46 func (fi *FolderInfo) Update() {
47 fi.NumItems = len(fi.Files)
48 fi.Size = 0
49 fi.Lines = 0
50 fi.CarriageReturns = 0
51 fi.ByteOrderMarks = 0
52 fi.Duration = 0
53
54 for i := range fi.Folders {
55 fi.Folders[i].Update()
56 }
57
58 for _, v := range fi.Folders {
59 fi.NumItems += v.NumItems
60 fi.Size += v.Size
61 fi.Lines += v.Lines
62 fi.CarriageReturns += v.CarriageReturns
63 fi.ByteOrderMarks += v.ByteOrderMarks
64 fi.Duration += v.Duration
65 }
66
67 for _, v := range fi.Files {
68 fi.Size += v.Size
69 fi.Lines += v.Lines
70 fi.CarriageReturns += v.CarriageReturns
71 fi.ByteOrderMarks += v.ByteOrderMarks
72 fi.Duration += v.Duration
73 }
74
75 if fi.Size < 0 {
76 fi.Size = 0
77 }
78 if fi.Lines < 0 {
79 fi.Lines = 0
80 }
81 if fi.CarriageReturns < 0 {
82 fi.CarriageReturns = 0
83 }
84 if fi.ByteOrderMarks < 0 {
85 fi.ByteOrderMarks = 0
86 }
87 if fi.Duration < 0 {
88 fi.Duration = 0
89 }
90 }
91
92 func (fi *FolderInfo) FindFolder(path string) *FolderInfo {
93 parent := fi
94 parts := strings.Split(path, string(filepath.Separator))
95 for _, s := range parts {
96 parent = parent.findFolder(path, s)
97 }
98 return parent
99 }
100
101 func (fi *FolderInfo) findFolder(path, s string) *FolderInfo {
102 for i, v := range fi.Folders {
103 if v.Name == s {
104 return &fi.Folders[i]
105 }
106 }
107
108 fi.Folders = append(fi.Folders, FolderInfo{Name: s, Path: path})
109 return &fi.Folders[len(fi.Folders)-1]
110 }
111
112 func (fi *FolderInfo) Inspect(f func(fi *FolderInfo)) {
113 f(fi)
114 for i := range fi.Folders {
115 f(&fi.Folders[i])
116 }
117 }
118
119 func group(files []FileInfo) FolderInfo {
120 var res FolderInfo
121 cache := make(map[string]*FolderInfo)
122
123 for _, v := range files {
124 parent, _ := filepath.Split(v.Name)
125 parent = strings.TrimSuffix(parent, "\\")
126 parent = strings.TrimSuffix(parent, "/")
127
128 ptr, ok := cache[parent]
129 if !ok {
130 ptr = res.FindFolder(parent)
131 cache[parent] = ptr
132 }
133 ptr.Files = append(ptr.Files, v)
134 }
135
136 res.Update()
137 return res
138 }
File: ./finfo/info.txt
1 finfo [options...] [files/folders...]
2
3 Show various file info, mainly filesizes in decreasing order (biggest to
4 smallest): all folders given are explored recursively to find all files in
5 them.
6
7 When exploring files in the current folder, use a dot as the folder name;
8 when not given any file/folder names, it reads those from standard input one
9 name per line, so file paths with spaces don't cause any problem.
10
11 Besides file size and name, it can show other info
12
13 - line counts, except for recognized media files
14 - column counts, in the context of delimiter-separated tabular text data
15 - carriage-return and byte-order-mark counts, except for known media types
16 - width, height, and bits per pixel for pictures and video files
17 - duration/play-length in seconds for all common audio/video files
18 - path of containing folder
19 - extension
20 - MIME type
21
22 Results can also be reverse-sorted by line count, by duration, or any other
23 numeric option/column: there's no way to increase-sort for any of the numeric
24 ranking options.
File: ./finfo/json.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 finfo
26
27 import (
28 "fmt"
29 "io"
30 "strings"
31 )
32
33 func folders2JSON(w io.Writer, x []FolderInfo) error {
34 fmt.Fprint(w, "[")
35 for i, v := range x {
36 if i > 0 {
37 fmt.Fprint(w, ", ")
38 }
39 if err := folder2JSON(w, v); err != nil {
40 return err
41 }
42 }
43 _, err := fmt.Fprint(w, "]")
44 return err
45 }
46
47 func folder2JSON(w io.Writer, fi FolderInfo) error {
48 // return json.NewEncoder(w).Encode(fi)
49
50 fmt.Fprint(w, "{")
51 writeStringJSON(w, "name", unixPath(fi.Name))
52 writeStringJSON(w, "path", unixPath(fi.Path))
53 fmt.Fprintf(w, `"folders": [`)
54 for i, v := range fi.Folders {
55 if i > 0 {
56 fmt.Fprint(w, ", ")
57 }
58 if err := folder2JSON(w, v); err != nil {
59 return err
60 }
61 }
62 fmt.Fprint(w, "], ")
63 fmt.Fprintf(w, `"files": [`)
64 for i, v := range fi.Files {
65 if i > 0 {
66 fmt.Fprint(w, ", ")
67 }
68 if err := file2JSON(w, v); err != nil {
69 return err
70 }
71 }
72 fmt.Fprint(w, "]")
73 writeIntJSON(w, "items", fi.NumItems)
74 writeIntJSON(w, "size", fi.Size)
75 writeIntJSON(w, "lines", fi.Lines)
76 writeIntJSON(w, "cr", fi.CarriageReturns)
77 writeIntJSON(w, "bom", fi.ByteOrderMarks)
78 if fi.Duration >= 0 {
79 fmt.Fprintf(w, `, "duration": %.2f`, fi.Duration)
80 }
81 _, err := fmt.Fprint(w, "}")
82 return err
83 }
84
85 func file2JSON(w io.Writer, fi FileInfo) error {
86 // return json.NewEncoder(w).Encode(fi)
87 fmt.Fprint(w, "{")
88 writeStringJSON(w, "name", unixPath(fi.Name))
89 writeStringJSON(w, "folder", unixPath(fi.Folder))
90 writeStringJSON(w, "mime", fi.MIMEType)
91 fmt.Fprintf(w, `"size": %d`, fi.Size)
92 writeIntJSON(w, "lines", fi.Lines)
93 writeIntJSON(w, "columns", fi.Columns)
94 // fi.Separator
95 writeIntJSON(w, "cr", fi.CarriageReturns)
96 writeIntJSON(w, "bom", fi.ByteOrderMarks)
97 if fi.Duration >= 0 {
98 fmt.Fprintf(w, `, "duration": %.2f`, fi.Duration)
99 }
100 writeIntJSON(w, "width", fi.Width)
101 writeIntJSON(w, "height", fi.Height)
102 writeIntJSON(w, "bpp", fi.BitsPerPixel)
103 _, err := fmt.Fprint(w, "}")
104 return err
105 }
106
107 func writeStringJSON(w io.Writer, k, s string) {
108 s = strings.TrimSpace(s)
109 if strings.Contains(s, `"`) {
110 s = strings.ReplaceAll(s, `"`, `\"`)
111 }
112 if strings.Contains(s, `\`) {
113 s = strings.ReplaceAll(s, `\`, `\\`)
114 }
115 fmt.Fprintf(w, `"%s": "%s", `, k, s)
116 }
117
118 func writeIntJSON(w io.Writer, k string, n int) {
119 if n >= 0 {
120 fmt.Fprintf(w, `, "%s": %d`, k, n)
121 }
122 }
123
124 func unixPath(s string) string {
125 if strings.Contains(s, `\`) {
126 return strings.ReplaceAll(s, `\`, `/`)
127 }
128 return s
129 }
File: ./finfo/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 finfo
26
27 import (
28 "bufio"
29 "flag"
30 "fmt"
31 "os"
32 "sort"
33
34 _ "embed"
35 )
36
37 //go:embed info.txt
38 var usage string
39
40 // //go:embed style.css
41 // var css string
42
43 const maxbufsize = 8 * 1024 * 1024 * 1024
44
45 func Main() {
46 cfg := parseFlags(usage)
47 if err := run(cfg); err != nil {
48 fmt.Fprintln(os.Stderr, err.Error())
49 os.Exit(1)
50 }
51 }
52
53 func run(cfg config) error {
54 w := bufio.NewWriter(os.Stdout)
55 defer w.Flush()
56
57 switch cfg.To {
58 case "tsv":
59 td := newTableDisplay(cfg)
60 // show header immediately to reassure user in case scanning/sorting is taking a while
61 td.FprintlnHeader(os.Stdout)
62
63 data := scan(flag.Args(), cfg)
64 sortItems(data, cfg.SortBy)
65 for _, e := range data {
66 if err := td.Fprintln(w, e); err != nil {
67 return nil // probably a pipe was closed
68 }
69 }
70 return nil
71
72 case "json":
73 data := scan(flag.Args(), cfg)
74 grouped := group(data)
75 grouped.Inspect(func(fi *FolderInfo) {
76 sortItems(fi.Files, cfg.SortBy)
77
78 // avoid null values anywhere
79 if fi.Folders == nil {
80 fi.Folders = []FolderInfo{}
81 }
82 if fi.Files == nil {
83 fi.Files = []FileInfo{}
84 }
85 })
86
87 folders2JSON(w, grouped.Folders)
88 w.Write([]byte("\n"))
89 return nil
90
91 default:
92 return fmt.Errorf("unknown output-type %q", cfg.To)
93 }
94 }
95
96 func scan(args []string, cfg config) []FileInfo {
97 // get all file/folder names to check, then check them all: if no arguments
98 // were given, use stdin to read them line by line
99 if len(args) == 0 {
100 sc := bufio.NewScanner(os.Stdin)
101 sc.Buffer(nil, maxbufsize)
102 for sc.Scan() {
103 args = append(args, sc.Text())
104 }
105 }
106
107 // get file sizes, count lines, count media duration, etc.
108 agg := newAggregator(cfg)
109 agg.Scan(args)
110 data := agg.Results()
111 return data
112 }
113
114 func sortItems(data []FileInfo, by string) {
115 // show the list sorted by decreasing size, text lines, time duration, # of columns,
116 // # of carriage-returns, # of byte-order marks, or forward-sorted by filename
117 switch by {
118 case "lines":
119 sort.Sort(sortableLineCountInfo(data))
120 case "duration":
121 sort.Sort(sortableDurationInfo(data))
122 case "columns":
123 sort.Sort(sortableColumnCountInfo(data))
124 case "cr":
125 sort.Sort(sortableCarriageReturnInfo(data))
126 case "bom":
127 sort.Sort(sortableByteOrderMarkInfo(data))
128 case "name":
129 sort.Sort(sortableNameInfo(data))
130 default:
131 sort.Sort(sortableSizeInfo(data))
132 }
133 }
File: ./finfo/plaintext.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 finfo
26
27 import (
28 "bufio"
29 "bytes"
30 "encoding/csv"
31 "io"
32 "strings"
33 )
34
35 // 0xefbbbf
36 var bom = []byte{0xef, 0xbb, 0xbf}
37
38 type plainTextStats struct {
39 Lines int
40 Columns int
41 CarriageReturns int
42 ByteOrderMarks int
43 EmptyLines int
44 AlphanumASCII int
45 Separator rune
46 }
47
48 func summarizePlainText(f io.Reader) (plainTextStats, error) {
49 st := plainTextStats{}
50 sc := bufio.NewScanner(f)
51 sc.Buffer(nil, maxbufsize)
52 sc.Split(splitUnixLines)
53
54 nonempty := 0
55 for ; sc.Scan(); st.Lines++ {
56 err := sc.Err()
57 if err != nil {
58 return st, err
59 }
60
61 if st.Lines == 0 && bytes.HasPrefix(sc.Bytes(), bom) {
62 st.ByteOrderMarks = 1
63 }
64
65 line := sc.Text()
66 // ignore empty lines
67 if line == "" {
68 st.EmptyLines++
69 continue
70 }
71 nonempty++
72
73 // first line: count tabs, pipes, and colons to update # columns
74 if nonempty == 1 {
75 summarizePlainTextHeader(&st, line)
76 continue
77 }
78
79 for _, r := range line {
80 if r == '\r' {
81 st.CarriageReturns++
82 } else if ('A' <= r && r <= 'Z') || ('a' <= r && r <= 'z') || ('0' <= r && r <= '9') {
83 st.AlphanumASCII++
84 }
85 }
86 }
87
88 return st, nil
89 }
90
91 // used only in function summarizePlainText
92 func summarizePlainTextHeader(st *plainTextStats, line string) {
93 tabs := 0
94 pipes := 0
95 colons := 0
96 for _, r := range line {
97 switch r {
98 case '\t':
99 tabs++
100 case '|':
101 pipes++
102 case ':':
103 colons++
104 case '\r':
105 st.CarriageReturns++
106 }
107 }
108
109 // # of columns is # of tabs + 1
110 if tabs > 0 && tabs+1 > st.Columns {
111 st.Columns = tabs + 1
112 st.Separator = '\t'
113 return
114 }
115
116 if pipes > 0 && pipes+1 > st.Columns {
117 st.Columns = pipes + 1
118 st.Separator = '|'
119 return
120 }
121
122 // 2 as the minimum avoids file patterns (text or not) where there are no colon separators:
123 // in practice they're only used in unix-like config files where there are many fields anyway
124
125 // note: still use 1 as the minimum count for now
126 if colons > 0 && colons+1 > st.Columns {
127 st.Columns = colons + 1
128 st.Separator = ':'
129 }
130
131 if st.Columns == 0 && st.Separator != 0 {
132 st.Columns = 1
133 }
134 }
135
136 // used only in function summarizePlainText
137 func splitUnixLines(b []byte, eof bool) (int, []byte, error) {
138 if eof && len(b) == 0 {
139 return 0, nil, nil
140 }
141 i := bytes.IndexByte(b, '\n')
142 if i >= 0 {
143 return i + 1, b[0:i], nil
144 }
145 // last line
146 if eof {
147 return len(b), b, nil
148 }
149 return 0, nil, nil
150 }
151
152 func adjustForCSV(f io.Reader, st *plainTextStats) {
153 // get the first nonempty line
154 line := ""
155 sc := bufio.NewScanner(f)
156 sc.Buffer(nil, maxbufsize)
157 for sc.Scan() {
158 if sc.Err() != nil {
159 break
160 }
161 line = sc.Text()
162 if line != "" {
163 break
164 }
165 }
166
167 w := 0
168 if st.Separator != rune(0) {
169 w = csvCountHeader(strings.NewReader(line), st.Separator)
170 }
171 wc := csvCountHeader(strings.NewReader(line), ',')
172 ws := csvCountHeader(strings.NewReader(line), ';')
173 if w > wc && w > ws {
174 st.Columns = w
175 } else if wc > w && wc > ws {
176 st.Columns = wc
177 st.Separator = ','
178 } else if ws > w && ws > wc {
179 st.Columns = ws
180 st.Separator = ';'
181 }
182 }
183
184 func csvCountHeader(f io.Reader, sep rune) int {
185 sc := csv.NewReader(f)
186 sc.LazyQuotes = true
187 sc.ReuseRecord = true
188 if sep == rune(0) {
189 sep = ','
190 }
191 sc.Comma = sep
192 for {
193 row, err := sc.Read()
194 if err != nil {
195 return 0
196 }
197 // just use the first non-empty line to estimate the # of columns
198 if len(row) > 0 {
199 return len(row)
200 }
201 }
202 }
File: ./finfo/sorting.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 finfo
26
27 import (
28 "strings"
29 )
30
31 // allow reverse-sorting by size in bytes
32 type sortableSizeInfo []FileInfo
33
34 func (r sortableSizeInfo) Len() int { return len(r) }
35 func (r sortableSizeInfo) Swap(i, j int) { r[i], r[j] = r[j], r[i] }
36 func (r sortableSizeInfo) Less(i, j int) bool {
37 if diff := r[i].Size - r[j].Size; diff != 0 {
38 return diff > 0
39 }
40 return strings.Compare(r[i].Name, r[j].Name) < 0
41 }
42
43 // allow reverse-sorting by lines of text counted
44 type sortableLineCountInfo []FileInfo
45
46 func (r sortableLineCountInfo) Len() int { return len(r) }
47 func (r sortableLineCountInfo) Swap(i, j int) { r[i], r[j] = r[j], r[i] }
48 func (r sortableLineCountInfo) Less(i, j int) bool {
49 if diff := r[i].Lines - r[j].Lines; diff != 0 {
50 return diff > 0
51 }
52 return strings.Compare(r[i].Name, r[j].Name) < 0
53 }
54
55 // allow reverse-sorting by time duration
56 type sortableDurationInfo []FileInfo
57
58 func (r sortableDurationInfo) Len() int { return len(r) }
59 func (r sortableDurationInfo) Swap(i, j int) { r[i], r[j] = r[j], r[i] }
60 func (r sortableDurationInfo) Less(i, j int) bool {
61 v1 := r[i].Duration >= 0 //!math.IsNaN(r[i].Duration)
62 v2 := r[j].Duration >= 0 //!math.IsNaN(r[j].Duration)
63 if v1 && v2 {
64 if diff := r[i].Duration - r[j].Duration; diff != 0 {
65 return diff > 0
66 }
67 return strings.Compare(r[i].Name, r[j].Name) < 0
68 }
69
70 // treat nan < valid
71 if !v1 && v2 {
72 return false
73 }
74 if v1 && !v2 {
75 return true
76 }
77
78 // if neither has a time duration, sort by size
79 if diff := r[i].Size - r[j].Size; diff != 0 {
80 return diff > 0
81 }
82 return strings.Compare(r[i].Name, r[j].Name) < 0
83 }
84
85 // allow reverse-sorting by data columns counted
86 type sortableColumnCountInfo []FileInfo
87
88 func (r sortableColumnCountInfo) Len() int { return len(r) }
89 func (r sortableColumnCountInfo) Swap(i, j int) { r[i], r[j] = r[j], r[i] }
90 func (r sortableColumnCountInfo) Less(i, j int) bool {
91 if diff := r[i].Columns - r[j].Columns; diff != 0 {
92 return diff > 0
93 }
94 return strings.Compare(r[i].Name, r[j].Name) < 0
95 }
96
97 // allow reverse-sorting by carriage-returns counted
98 type sortableCarriageReturnInfo []FileInfo
99
100 func (r sortableCarriageReturnInfo) Len() int { return len(r) }
101 func (r sortableCarriageReturnInfo) Swap(i, j int) { r[i], r[j] = r[j], r[i] }
102 func (r sortableCarriageReturnInfo) Less(i, j int) bool {
103 if diff := r[i].CarriageReturns - r[j].CarriageReturns; diff != 0 {
104 return diff > 0
105 }
106 return strings.Compare(r[i].Name, r[j].Name) < 0
107 }
108
109 // allow reverse-sorting by byte-order marks counted
110 type sortableByteOrderMarkInfo []FileInfo
111
112 func (r sortableByteOrderMarkInfo) Len() int { return len(r) }
113 func (r sortableByteOrderMarkInfo) Swap(i, j int) { r[i], r[j] = r[j], r[i] }
114 func (r sortableByteOrderMarkInfo) Less(i, j int) bool {
115 if diff := r[i].ByteOrderMarks - r[j].ByteOrderMarks; diff != 0 {
116 return diff > 0
117 }
118 return strings.Compare(r[i].Name, r[j].Name) < 0
119 }
120
121 // allow forward-sorting by filename
122 type sortableNameInfo []FileInfo
123
124 func (r sortableNameInfo) Len() int { return len(r) }
125 func (r sortableNameInfo) Swap(i, j int) { r[i], r[j] = r[j], r[i] }
126 func (r sortableNameInfo) Less(i, j int) bool {
127 return strings.Compare(r[i].Name, r[j].Name) < 0
128 }
File: ./finfo/tables.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 finfo
26
27 import (
28 "fmt"
29 "io"
30 "math"
31 "path/filepath"
32 "strings"
33 )
34
35 // tableDisplay handles the final display of all the info gathered
36 type tableDisplay struct {
37 Headers []string
38 Formats []string
39 Conditions []bool
40
41 values []any // to minimize memory allocations
42 }
43
44 func newTableDisplay(c config) tableDisplay {
45 return tableDisplay{
46 Headers: []string{
47 "bytes", "KiB", "MiB", "GiB", // file size
48 "duration", "hh:mm:ss", "width", "height", "bpp", // sound/picture info
49 "lines", "columns", "cells", "CR", "BOM", "sep", // plain-text-related
50 "name", "folder", "ext", "MIME", // name and type
51 },
52 Formats: []string{
53 "%d", "%.2f", "%.2f", "%.2f", // file size
54 "%.2f", "%v", "%d", "%d", "%d", // sound/picture info
55 "%d", "%d", "%d", "%d", "%d", "%s", // plain-text-related
56 "%s", "%s", "%s", "%s", // name and type
57 },
58 Conditions: []bool{
59 c.Bytes, c.KiB, c.MiB, c.GiB, // file size
60 c.Duration, c.HMS, c.Picture, c.Picture, c.Picture, // sound/picture info
61 c.Lines || c.Text, c.Text, c.Text, c.Text, c.Text, c.Text, // plain-text-related
62 true, c.Folder, c.Type, c.MIMEType, // name and type
63 },
64 values: make([]any, 0, 20),
65 }
66 }
67
68 func (c tableDisplay) FprintlnHeader(w io.Writer) {
69 first := true
70 for i, cond := range c.Conditions {
71 if !cond {
72 continue
73 }
74 if !first {
75 fmt.Fprint(w, "\t")
76 }
77 first = false
78 fmt.Fprint(w, c.Headers[i])
79 }
80 fmt.Fprintln(w)
81 }
82
83 func (c tableDisplay) Fprintln(w io.Writer, e FileInfo) error {
84 kib := float64(e.Size) / 1024
85 mib := kib / 1024
86 gib := mib / 1024
87 name := strings.ReplaceAll(e.Name, "\\", "/") // show unix-style folder separators
88 ext := strings.TrimLeft(filepath.Ext(e.Name), ".") // file extension with no leading dot
89 ext = strings.ToLower(ext) // handle uppercase file extensions
90 folder := strings.ReplaceAll(e.Folder, "\\", "/")
91 sep := ""
92 switch e.Separator {
93 case '\t':
94 sep = "tab"
95 case rune(0):
96 sep = ""
97 case ',':
98 sep = "comma"
99 case ';':
100 sep = "semicolon"
101 case '|':
102 sep = "pipe"
103 case ':':
104 sep = "colon"
105 default:
106 sep = string(e.Separator)
107 }
108
109 hms := ""
110 if c.Conditions[5] {
111 hms = s2hms(e.Duration)
112 }
113 ncells := e.Lines * e.Columns
114 if e.Lines < 2 || e.Columns < 2 {
115 ncells = -1
116 }
117
118 c.values = c.values[:0]
119 c.values = append(c.values,
120 e.Size, kib, mib, gib, // file size
121 e.Duration, hms, e.Width, e.Height, e.BitsPerPixel, // sound/picture info
122 e.Lines, e.Columns, ncells, e.CarriageReturns, e.ByteOrderMarks, sep, // plain-text-related
123 name, folder, ext, e.MIMEType, // name and type
124 )
125
126 first := true
127 for i, cond := range c.Conditions {
128 if !cond {
129 continue
130 }
131 if !first {
132 fmt.Fprint(w, "\t")
133 }
134 first = false
135
136 v := c.values[i]
137 // avoid emitting nan values as "NaN"
138 f, ok := v.(float64)
139 if ok && (math.IsNaN(f) || f < 0) {
140 continue
141 }
142 // negative integer counters result from errors
143 n, ok := v.(int)
144 if ok && n < 0 {
145 continue
146 }
147 fmt.Fprintf(w, c.Formats[i], v)
148 }
149
150 _, err := fmt.Fprintln(w)
151 return err
152 }
153
154 func s2hms(t float64) string {
155 if t < 0 || math.IsNaN(t) {
156 return ""
157 }
158 h := math.Floor(t / 3600)
159 m := math.Floor(math.Mod(t, 3600) / 60)
160 s := math.Mod(t, 60)
161 return fmt.Sprintf("%02d:%02d:%05.2f", int(h), int(m), s)
162 }
File: ./finfo/tasks.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 finfo
26
27 import (
28 "fmt"
29 "io/fs"
30 "os"
31 "path/filepath"
32 "runtime"
33 "sync"
34 )
35
36 // a parallel results-collector
37 type aggregator struct {
38 cfg config
39 seen map[string]struct{} // avoid duplicate results for files
40 res []FileInfo
41 }
42
43 func newAggregator(cfg config) aggregator {
44 return aggregator{
45 cfg: cfg,
46 seen: make(map[string]struct{}),
47 res: make([]FileInfo, 0),
48 }
49 }
50
51 func (a *aggregator) Results() []FileInfo {
52 return a.res
53 }
54
55 func (a *aggregator) Scan(filenames []string) {
56 for _, fname := range filenames {
57 info, err := os.Stat(fname)
58 if err != nil {
59 fmt.Fprintln(os.Stderr, err.Error())
60 continue
61 }
62
63 if info.IsDir() {
64 err = a.handleFolder(fname)
65 if err != nil {
66 fmt.Fprintln(os.Stderr, err.Error())
67 continue
68 }
69 continue
70 }
71 a.addFileEntry(fname, info)
72 }
73
74 // no need to open files when only name and file size are going to be shown
75 if !a.cfg.NeedsExtraInfo() {
76 return
77 }
78
79 var wg sync.WaitGroup
80 wg.Add(len(a.res))
81
82 // calculate stats/results asynchronously when told to; use a channel to limit how many
83 // file-stats calculations are running at the same time: the limit is the number of cores
84 exitTickets := make(chan struct{}, runtime.NumCPU())
85 defer close(exitTickets)
86 for i := range a.res {
87 exitTickets <- struct{}{}
88 go func(i int) {
89 defer func() {
90 <-exitTickets
91 wg.Done()
92 }()
93 a.res[i].calculateStats(a.cfg)
94 }(i)
95 }
96
97 // ensure all jobs are finished before returning
98 wg.Wait()
99 }
100
101 func (a *aggregator) addFileEntry(fname string, info os.FileInfo) {
102 // avoid going over the same places more than once
103 _, ok := a.seen[fname]
104 if ok {
105 return
106 }
107 a.seen[fname] = struct{}{}
108 a.res = append(a.res, newFileInfo(fname, int(info.Size())))
109 }
110
111 func (a *aggregator) handleFolder(fpath string) error {
112 return filepath.WalkDir(fpath, func(path string, d fs.DirEntry, err error) error {
113 // nothing to do when there's either an error or it's a folder
114 if err != nil {
115 return err
116 }
117 if d.IsDir() {
118 return nil
119 }
120
121 info, err := d.Info()
122 if err != nil {
123 return err
124 }
125 a.addFileEntry(path, info)
126 return nil
127 })
128 }
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.got = make(map[string]struct{})
85 cfg.recursive = !top
86 cfg.liveLines = liveLines
87
88 if err := run(os.Stdout, args, cfg); err != nil && err != io.EOF {
89 os.Stderr.WriteString(err.Error())
90 os.Stderr.WriteString("\n")
91 os.Exit(1)
92 }
93 }
94
95 type config struct {
96 got map[string]struct{}
97 recursive bool
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 if len(paths) == 0 {
106 paths = []string{`.`}
107 }
108
109 if cfg.recursive {
110 return runRecursive(bw, paths, cfg)
111 }
112 return runFlat(bw, paths, cfg)
113 }
114
115 func runRecursive(w *bufio.Writer, paths []string, cfg config) error {
116 if len(paths) == 0 {
117 paths = []string{`.`}
118 }
119
120 // handle is the callback for func filepath.WalkDir
121 handle := func(path string, e fs.DirEntry, err error) error {
122 if err != nil {
123 return err
124 }
125
126 if _, ok := cfg.got[path]; ok {
127 return nil
128 }
129 cfg.got[path] = struct{}{}
130
131 if e.IsDir() {
132 if err := handleEntry(w, path, cfg.liveLines); err != nil {
133 return err
134 }
135 }
136
137 return nil
138 }
139
140 for _, path := range paths {
141 if _, ok := cfg.got[path]; ok {
142 continue
143 }
144 cfg.got[path] = struct{}{}
145
146 st, err := os.Stat(path)
147 if err != nil {
148 return err
149 }
150
151 if !strings.HasSuffix(path, `/`) {
152 path = path + `/`
153 cfg.got[path] = struct{}{}
154 }
155
156 if !st.IsDir() {
157 continue
158 }
159
160 if err := filepath.WalkDir(path, handle); err != nil {
161 return err
162 }
163 }
164
165 return nil
166 }
167
168 func runFlat(w *bufio.Writer, paths []string, cfg config) error {
169 if len(paths) == 0 {
170 paths = []string{`.`}
171 }
172
173 for _, path := range paths {
174 if _, ok := cfg.got[path]; ok {
175 continue
176 }
177 cfg.got[path] = struct{}{}
178
179 st, err := os.Stat(path)
180 if err != nil {
181 return err
182 }
183
184 if !strings.HasSuffix(path, `/`) {
185 path = path + `/`
186 cfg.got[path] = struct{}{}
187 }
188
189 if !st.IsDir() {
190 continue
191 }
192
193 entries, err := os.ReadDir(path)
194 if err != nil {
195 return err
196 }
197
198 for _, e := range entries {
199 if !e.IsDir() {
200 continue
201 }
202
203 path := filepath.Join(path, e.Name())
204 if err := handleEntry(w, path, cfg.liveLines); err != nil {
205 return err
206 }
207 }
208 }
209
210 return nil
211 }
212
213 func handleEntry(w *bufio.Writer, path string, live bool) error {
214 abs, err := filepath.Abs(path)
215 if err != nil {
216 return err
217 }
218
219 w.WriteString(abs)
220 w.WriteByte('\n')
221
222 if !live {
223 return nil
224 }
225
226 if err := w.Flush(); err != nil {
227 return io.EOF
228 }
229 return nil
230 }
File: ./gsub/gsub.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 gsub
26
27 import (
28 "bufio"
29 "bytes"
30 "io"
31 "os"
32 "regexp"
33 "strings"
34 )
35
36 const info = `
37 gsub [options...] [regex / replacement pairs...]
38
39
40 Named after the AWK function 'gsub' (global substitute), this tool replaces
41 all matches found with the substitution pattern associated to it.
42
43 Regexes and replacements are given as pairs. Input always comes from standard
44 input. The regular-expression mode used is "re2", which is a superset of the
45 commonly-used "extended-mode".
46
47 All ANSI-style sequences are removed before trying to replace all matches, to
48 avoid messing those up. Each regex replaces all its occurrences on the current
49 line in the order given among the arguments, so regex-order matters.
50
51 As with the AWK function 'gsub', any '&' symbols substitute into the substring
52 matched, except for any '&' preceded by a backslash.
53
54 The options are, available both in single and double-dash versions
55
56 -h, -help show this help message
57 -e, -erase all arguments are regexes which are replaced with nothing
58 -i, -ins match regexes case-insensitively
59 `
60
61 type pair struct {
62 expr *regexp.Regexp
63 repl []string
64 }
65
66 func Main() {
67 args := os.Args[1:]
68 erase := false
69 buffered := false
70 insensitive := false
71
72 for len(args) > 0 {
73 switch args[0] {
74 case `-b`, `--b`, `-buffered`, `--buffered`:
75 buffered = true
76 args = args[1:]
77 continue
78
79 case `-e`, `--e`, `-erase`, `--erase`:
80 erase = true
81 args = args[1:]
82 continue
83
84 case `-h`, `--h`, `-help`, `--help`:
85 os.Stdout.WriteString(info[1:])
86 return
87
88 case `-i`, `--i`, `-ins`, `--ins`:
89 insensitive = true
90 args = args[1:]
91 continue
92 }
93
94 break
95 }
96
97 if len(args) > 0 && args[0] == `--` {
98 args = args[1:]
99 }
100
101 errcount := 0
102 pairs := make([]pair, 0, (len(args)+1)/2)
103
104 for len(args) > 0 {
105 var err error
106 var what *regexp.Regexp
107
108 s := args[0]
109 if insensitive {
110 what, err = regexp.Compile(`(?i)` + s)
111 } else {
112 what, err = regexp.Compile(s)
113 }
114
115 if err != nil {
116 os.Stderr.WriteString(err.Error())
117 os.Stderr.WriteString("\n")
118 errcount++
119 continue
120 }
121
122 args = args[1:]
123 var with []string
124 if !erase && len(args) > 0 {
125 with = splitReplacement(args[0])
126 args = args[1:]
127 }
128 pairs = append(pairs, pair{what, with})
129 }
130
131 // quit right away when given invalid regexes
132 if errcount > 0 {
133 os.Exit(1)
134 }
135
136 liveLines := !buffered
137 if !buffered {
138 if _, err := os.Stdout.Seek(0, io.SeekCurrent); err == nil {
139 liveLines = false
140 }
141 }
142
143 err := run(os.Stdout, os.Stdin, pairs, liveLines)
144 if err != nil && err != io.EOF {
145 os.Stderr.WriteString(err.Error())
146 os.Stderr.WriteString("\n")
147 os.Exit(1)
148 }
149 }
150
151 func splitReplacement(s string) []string {
152 if s == `` {
153 return nil
154 }
155
156 n := 1
157 var prev rune
158 for _, r := range s {
159 if prev != '\\' && r == '&' {
160 n += 2
161 }
162 prev = r
163 }
164
165 repl := make([]string, 0, n)
166
167 for len(s) > 0 {
168 i := strings.IndexByte(s, '&')
169 if i == 0 || (i > 0 && s[i-1] != '\\') {
170 repl = append(repl, unescapeBackslashes(s[:i]))
171 repl = append(repl, ``)
172 s = s[i+1:]
173 continue
174 }
175 break
176 }
177
178 if len(s) > 0 {
179 repl = append(repl, unescapeBackslashes(s))
180 }
181 return repl
182 }
183
184 func unescapeBackslashes(s string) string {
185 if strings.IndexByte(s, '\\') < 0 {
186 return s
187 }
188
189 var prev byte
190 unesc := make([]byte, 0, len(s))
191
192 for i := range s {
193 b := s[i]
194
195 if prev != '\\' {
196 if b != '\\' {
197 unesc = append(unesc, s[i])
198 }
199 prev = b
200 continue
201 }
202
203 switch b {
204 case 'e':
205 unesc = append(unesc, '\x1b')
206 case 'n':
207 unesc = append(unesc, '\n')
208 case 'r':
209 unesc = append(unesc, '\r')
210 case 't':
211 unesc = append(unesc, '\t')
212 case 'v':
213 unesc = append(unesc, '\v')
214 default:
215 unesc = append(unesc, s[i])
216 }
217
218 prev = b
219 }
220
221 return string(unesc)
222 }
223
224 func run(w io.Writer, r io.Reader, pairs []pair, live bool) error {
225 var buf []byte
226 sc := bufio.NewScanner(r)
227 sc.Buffer(nil, 8*1024*1024*1024)
228 bw := bufio.NewWriter(w)
229 defer bw.Flush()
230
231 src := make([]byte, 8*1024)
232 dst := make([]byte, 8*1024)
233
234 for i := 0; sc.Scan(); i++ {
235 line := sc.Bytes()
236 if i == 0 && bytes.HasPrefix(line, []byte{0xef, 0xbb, 0xbf}) {
237 line = line[3:]
238 }
239
240 s := line
241 if bytes.IndexByte(s, '\x1b') >= 0 {
242 buf = plain(buf[:0], s)
243 s = buf
244 }
245
246 if len(pairs) > 0 {
247 src = append(src[:0], s...)
248 for _, p := range pairs {
249 dst = gsub(dst[:0], src, p.expr, p.repl)
250 src = append(src[:0], dst...)
251 }
252 bw.Write(dst)
253 } else {
254 bw.Write(s)
255 }
256
257 if bw.WriteByte('\n') != nil {
258 return io.EOF
259 }
260
261 if !live {
262 continue
263 }
264
265 if bw.Flush() != nil {
266 return io.EOF
267 }
268 }
269
270 return sc.Err()
271 }
272
273 func gsub(dst []byte, src []byte, what *regexp.Regexp, with []string) []byte {
274 for len(src) > 0 {
275 span := what.FindIndex(src)
276 // also ignore empty regex matches to avoid infinite outer loops,
277 // as skipping empty slices isn't advancing at all, leaving the
278 // string stuck to being empty-matched forever by the same regex
279 if len(span) != 2 || span[0] == span[1] || span[0] < 0 {
280 return append(dst, src...)
281 }
282
283 start, end := span[0], span[1]
284 dst = append(dst, src[:start]...)
285 // avoid infinite loops caused by empty regex matches
286 if start == end {
287 if end >= len(src) {
288 break
289 }
290 dst = append(dst, src[end])
291 end++
292 src = src[end:]
293 continue
294 }
295
296 match := src[start:end]
297 for _, sub := range with {
298 if sub == `` {
299 dst = append(dst, match...)
300 continue
301 }
302 dst = append(dst, sub...)
303 }
304
305 src = src[end:]
306 }
307
308 return dst
309 }
310
311 func plain(dst []byte, src []byte) []byte {
312 for len(src) > 0 {
313 i, j := indexEscapeSequence(src)
314 if i < 0 {
315 dst = append(dst, src...)
316 break
317 }
318 if j < 0 {
319 j = len(src)
320 }
321
322 if i > 0 {
323 dst = append(dst, src[:i]...)
324 }
325
326 src = src[j:]
327 }
328
329 return dst
330 }
331
332 // indexEscapeSequence finds the first ANSI-style escape-sequence, which is
333 // the multi-byte sequences starting with ESC[; the result is a pair of slice
334 // indices which can be independently negative when either the start/end of
335 // a sequence isn't found; given their fairly-common use, even the hyperlink
336 // ESC]8 sequences are supported
337 func indexEscapeSequence(s []byte) (int, int) {
338 var prev byte
339
340 for i, b := range s {
341 if prev == '\x1b' && b == '[' {
342 j := indexLetter(s[i+1:])
343 if j < 0 {
344 return i, -1
345 }
346 return i - 1, i + 1 + j + 1
347 }
348
349 if prev == '\x1b' && b == ']' && i+1 < len(s) && s[i+1] == '8' {
350 j := indexPair(s[i+1:], '\x1b', '\\')
351 if j < 0 {
352 return i, -1
353 }
354 return i - 1, i + 1 + j + 2
355 }
356
357 prev = b
358 }
359
360 return -1, -1
361 }
362
363 func indexLetter(s []byte) int {
364 for i, b := range s {
365 upper := b &^ 32
366 if 'A' <= upper && upper <= 'Z' {
367 return i
368 }
369 }
370
371 return -1
372 }
373
374 func indexPair(s []byte, x byte, y byte) int {
375 var prev byte
376
377 for i, b := range s {
378 if prev == x && b == y && i > 0 {
379 return i
380 }
381 prev = b
382 }
383
384 return -1
385 }
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 continue
70
71 case `-f`, `--f`, `-filter`, `--filter`:
72 filter = true
73 args = args[1:]
74 continue
75
76 case `-fi`, `--fi`, `-if`, `--if`:
77 filter = true
78 insensitive = true
79 args = args[1:]
80 continue
81
82 case `-h`, `--h`, `-help`, `--help`:
83 os.Stdout.WriteString(info[1:])
84 return
85
86 case `-i`, `--i`, `-ins`, `--ins`:
87 insensitive = true
88 args = args[1:]
89 continue
90 }
91
92 break
93 }
94
95 if len(args) > 0 && args[0] == `--` {
96 args = args[1:]
97 }
98
99 patterns := make([]pattern, 0, len(args))
100
101 for _, s := range args {
102 var err error
103 var pat pattern
104
105 if insensitive {
106 pat, err = compile(`(?i)` + s)
107 } else {
108 pat, err = compile(s)
109 }
110
111 if err != nil {
112 os.Stderr.WriteString(err.Error())
113 os.Stderr.WriteString("\n")
114 continue
115 }
116
117 patterns = append(patterns, pat)
118 }
119
120 // quit right away when given invalid regexes
121 if len(patterns) < len(args) {
122 os.Exit(1)
123 }
124
125 liveLines := !buffered
126 if !buffered {
127 if _, err := os.Stdout.Seek(0, io.SeekCurrent); err == nil {
128 liveLines = false
129 }
130 }
131
132 err := run(os.Stdout, os.Stdin, patterns, filter, liveLines)
133 if err != nil && err != io.EOF {
134 os.Stderr.WriteString(err.Error())
135 os.Stderr.WriteString("\n")
136 os.Exit(1)
137 }
138 }
139
140 // pattern is a regular-expression pattern which distinguishes between the
141 // start/end of a line and those of the chunks it can be used to match
142 type pattern struct {
143 // expr is the regular-expression
144 expr *regexp.Regexp
145
146 // begin is whether the regexp refers to the start of a line
147 begin bool
148
149 // end is whether the regexp refers to the end of a line
150 end bool
151 }
152
153 func compile(src string) (pattern, error) {
154 expr, err := regexp.Compile(src)
155
156 var pat pattern
157 pat.expr = expr
158 pat.begin = strings.HasPrefix(src, `^`) || strings.HasPrefix(src, `(?i)^`)
159 pat.end = strings.HasSuffix(src, `$`) && !strings.HasSuffix(src, `\$`)
160 return pat, err
161 }
162
163 func (p pattern) findIndex(s []byte, i int, last int) (start int, stop int) {
164 if i > 0 && p.begin {
165 return -1, -1
166 }
167 if i != last && p.end {
168 return -1, -1
169 }
170
171 span := p.expr.FindIndex(s)
172 // also ignore empty regex matches to avoid infinite outer loops,
173 // as skipping empty slices isn't advancing at all, leaving the
174 // string stuck to being empty-matched forever by the same regex
175 if len(span) != 2 || span[0] == span[1] {
176 return -1, -1
177 }
178
179 return span[0], span[1]
180 }
181
182 func run(w io.Writer, r io.Reader, pats []pattern, filter, live bool) error {
183 sc := bufio.NewScanner(r)
184 sc.Buffer(nil, 8*1024*1024*1024)
185 bw := bufio.NewWriter(w)
186 defer bw.Flush()
187
188 for i := 0; sc.Scan(); i++ {
189 s := sc.Bytes()
190 if i == 0 && bytes.HasPrefix(s, []byte{0xef, 0xbb, 0xbf}) {
191 s = s[3:]
192 }
193
194 n := 0
195 last := countChunks(s) - 1
196 if last < 0 {
197 last = 0
198 }
199
200 if filter && !matches(s, pats, last) {
201 continue
202 }
203
204 for len(s) > 0 {
205 i, j := indexEscapeSequence(s)
206 if i < 0 {
207 handleChunk(bw, s, pats, n, last)
208 break
209 }
210 if j < 0 {
211 j = len(s)
212 }
213
214 handleChunk(bw, s[:i], pats, n, last)
215 if i > 0 {
216 n++
217 }
218
219 bw.Write(s[i:j])
220
221 s = s[j:]
222 }
223
224 if bw.WriteByte('\n') != nil {
225 return io.EOF
226 }
227
228 if !live {
229 continue
230 }
231
232 if bw.Flush() != nil {
233 return io.EOF
234 }
235 }
236
237 return sc.Err()
238 }
239
240 // matches finds out if any regex matches any substring around ANSI-sequences
241 func matches(s []byte, patterns []pattern, last int) bool {
242 n := 0
243
244 for len(s) > 0 {
245 i, j := indexEscapeSequence(s)
246 if i < 0 {
247 for _, p := range patterns {
248 if begin, _ := p.findIndex(s, n, last); begin >= 0 {
249 return true
250 }
251 }
252 return false
253 }
254
255 if j < 0 {
256 j = len(s)
257 }
258
259 for _, p := range patterns {
260 if begin, _ := p.findIndex(s[:i], n, last); begin >= 0 {
261 return true
262 }
263 }
264
265 if i > 0 {
266 n++
267 }
268
269 s = s[j:]
270 }
271
272 return false
273 }
274
275 func countChunks(s []byte) int {
276 chunks := 0
277
278 for len(s) > 0 {
279 i, j := indexEscapeSequence(s)
280 if i < 0 {
281 break
282 }
283
284 if i > 0 {
285 chunks++
286 }
287
288 if j < 0 {
289 break
290 }
291 s = s[j:]
292 }
293
294 if len(s) > 0 {
295 chunks++
296 }
297 return chunks
298 }
299
300 // indexEscapeSequence finds the first ANSI-style escape-sequence, which is
301 // the multi-byte sequences starting with ESC[; the result is a pair of slice
302 // indices which can be independently negative when either the start/end of
303 // a sequence isn't found; given their fairly-common use, even the hyperlink
304 // ESC]8 sequences are supported
305 func indexEscapeSequence(s []byte) (int, int) {
306 var prev byte
307
308 for i, b := range s {
309 if prev == '\x1b' && b == '[' {
310 j := indexLetter(s[i+1:])
311 if j < 0 {
312 return i, -1
313 }
314 return i - 1, i + 1 + j + 1
315 }
316
317 if prev == '\x1b' && b == ']' && i+1 < len(s) && s[i+1] == '8' {
318 j := indexPair(s[i+1:], '\x1b', '\\')
319 if j < 0 {
320 return i, -1
321 }
322 return i - 1, i + 1 + j + 2
323 }
324
325 prev = b
326 }
327
328 return -1, -1
329 }
330
331 func indexLetter(s []byte) int {
332 for i, b := range s {
333 upper := b &^ 32
334 if 'A' <= upper && upper <= 'Z' {
335 return i
336 }
337 }
338
339 return -1
340 }
341
342 func indexPair(s []byte, x byte, y byte) int {
343 var prev byte
344
345 for i, b := range s {
346 if prev == x && b == y && i > 0 {
347 return i
348 }
349 prev = b
350 }
351
352 return -1
353 }
354
355 // note: looking at the results of restoring ANSI-styles after style-resets
356 // doesn't seem to be worth it, as a previous version used to do
357
358 // handleChunk handles line-slices around any detected ANSI-style sequences,
359 // or even whole lines, when no ANSI-styles are found in them
360 func handleChunk(w *bufio.Writer, s []byte, with []pattern, n int, last int) {
361 for len(s) > 0 {
362 start, end := -1, -1
363 for _, p := range with {
364 i, j := p.findIndex(s, n, last)
365 if i >= 0 && (i < start || start < 0) {
366 start, end = i, j
367 }
368 }
369
370 if start < 0 {
371 w.Write(s)
372 return
373 }
374
375 w.Write(s[:start])
376 w.WriteString(highlightStyle)
377 w.Write(s[start:end])
378 w.WriteString("\x1b[0m")
379
380 s = s[end:]
381 }
382 }
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("&")
318 case '<':
319 w.WriteString("<")
320 case '>':
321 w.WriteString(">")
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: ./info.txt
1 easybox [options...] [tool] [arguments...]
2
3 This is a collection of many specialized app-like tools, similar to "busybox".
4
5 You can either run it with the tool name as its first argument, or run a link
6 to it whose name is one of those same tools, avoiding the tool-name argument
7 in that case.
8
9 Tool "help" shows you all tools available, as well as all their aliases, and
10 tool "tools" merely lists all main tool-names.
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 == io.EOF {
245 return errors.New(`end of JSON before array was closed`)
246 }
247 if err != nil {
248 return err
249 }
250
251 if t == json.Delim(']') {
252 if i == 0 {
253 writeSpaces(w, 2*pre)
254 w.WriteByte('[')
255 w.WriteByte(']')
256 } else {
257 w.WriteByte('\n')
258 writeSpaces(w, 2*level)
259 w.WriteByte(']')
260 }
261 return nil
262 }
263
264 if i == 0 {
265 writeSpaces(w, 2*pre)
266 w.WriteByte('[')
267 w.WriteByte('\n')
268 } else {
269 w.WriteByte(',')
270 w.WriteByte('\n')
271 if err := w.Flush(); err != nil {
272 // a write error may be the consequence of stdout being closed,
273 // perhaps by another app along a pipe
274 return io.EOF
275 }
276 }
277
278 err = handleToken(w, dec, t, level+1, level+1)
279 if err != nil {
280 return err
281 }
282 }
283
284 // make the compiler happy
285 return nil
286 }
287
288 // handleObject handles objects for func handleToken
289 func handleObject(w *bufio.Writer, dec *json.Decoder, pre, level int) error {
290 for i := 0; true; i++ {
291 t, err := dec.Token()
292 if err == io.EOF {
293 return errors.New(`end of JSON before object was closed`)
294 }
295 if err != nil {
296 return err
297 }
298
299 if t == json.Delim('}') {
300 if i == 0 {
301 writeSpaces(w, 2*pre)
302 w.WriteByte('{')
303 w.WriteByte('}')
304 } else {
305 w.WriteByte('\n')
306 writeSpaces(w, 2*level)
307 w.WriteByte('}')
308 }
309 return nil
310 }
311
312 if i == 0 {
313 writeSpaces(w, 2*pre)
314 w.WriteByte('{')
315 w.WriteByte('\n')
316 } else {
317 w.WriteByte(',')
318 w.WriteByte('\n')
319 }
320
321 k, ok := t.(string)
322 if !ok {
323 return errors.New(`expected a string for a key-value pair`)
324 }
325
326 err = handleString(w, k, level+1)
327 if err != nil {
328 return err
329 }
330
331 w.WriteString(": ")
332
333 t, err = dec.Token()
334 if err == io.EOF {
335 return errors.New(`expected a value for a key-value pair`)
336 }
337
338 err = handleToken(w, dec, t, 0, level+1)
339 if err != nil {
340 return err
341 }
342 }
343
344 // make the compiler happy
345 return nil
346 }
347
348 // handleString handles strings for func handleToken, and keys for func
349 // handleObject
350 func handleString(w *bufio.Writer, s string, level int) error {
351 writeSpaces(w, 2*level)
352 w.WriteByte('"')
353 for i := range s {
354 w.Write(escapedStringBytes[s[i]])
355 }
356 w.WriteByte('"')
357 return nil
358 }
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 return errors.New(`end of JSON before array was closed`)
299 }
300 if err != nil {
301 return err
302 }
303
304 if t == json.Delim(']') {
305 w.WriteByte(']')
306 return nil
307 }
308
309 if i > 0 {
310 _, err := w.WriteString(", ")
311 if err != nil {
312 return io.EOF
313 }
314 }
315
316 err = handleToken(w, dec, t)
317 if err != nil {
318 return err
319 }
320 }
321
322 // make the compiler happy
323 return nil
324 }
325
326 // handleObject handles objects for func handleToken
327 func handleObject(w *bufio.Writer, dec *json.Decoder) error {
328 w.WriteByte('{')
329
330 for i := 0; true; i++ {
331 t, err := dec.Token()
332 if err == io.EOF {
333 return errors.New(`end of JSON before object was closed`)
334 }
335 if err != nil {
336 return err
337 }
338
339 if t == json.Delim('}') {
340 w.WriteByte('}')
341 return nil
342 }
343
344 if i > 0 {
345 _, err := w.WriteString(", ")
346 if err != nil {
347 return io.EOF
348 }
349 }
350
351 k, ok := t.(string)
352 if !ok {
353 return errors.New(`expected a string for a key-value pair`)
354 }
355
356 err = handleString(w, k)
357 if err != nil {
358 return err
359 }
360
361 w.WriteString(": ")
362
363 t, err = dec.Token()
364 if err == io.EOF {
365 return errors.New(`expected a value for a key-value pair`)
366 }
367
368 err = handleToken(w, dec, t)
369 if err != nil {
370 return err
371 }
372 }
373
374 // make the compiler happy
375 return nil
376 }
377
378 // handleString handles strings for func handleToken, and keys for func
379 // handleObject
380 func handleString(w *bufio.Writer, s string) error {
381 w.WriteByte('"')
382 for i := range s {
383 w.Write(escapedStringBytes[s[i]])
384 }
385 w.WriteByte('"')
386 return nil
387 }
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 "bufio"
40 "fmt"
41 "io"
42 "os"
43 "path"
44 "sort"
45 "strings"
46 "unicode/utf8"
47
48 "./avoid"
49 "./bytedump"
50 "./calc"
51 "./catl"
52 "./coby"
53 "./countdown"
54 "./datauri"
55 "./debase64"
56 "./decsv"
57 "./dedup"
58 "./dejsonl"
59 "./dessv"
60 "./ecoli"
61 "./erase"
62 "./fh"
63 "./files"
64 "./filesizes"
65 "./finfo"
66 "./fixlines"
67 "./folders"
68 "./gsub"
69 "./hima"
70 "./htmlify"
71 "./id3pic"
72 "./json0"
73 "./json2"
74 "./jsonl"
75 "./jsons"
76 "./match"
77 "./n"
78 "./ncol"
79 "./ngron"
80 "./nhex"
81 "./njson"
82 "./nn"
83 "./now"
84 "./plain"
85 "./primes"
86 "./realign"
87 "./squeeze"
88 "./tcatl"
89 "./teletype"
90 "./units"
91 "./utfate"
92 "./verdict"
93 "./waveout"
94 "./zj"
95
96 "./remakes/cat"
97 "./remakes/head"
98 "./remakes/ls"
99
100 _ "embed"
101 )
102
103 //go:embed info.txt
104 var info string
105
106 // mains has some entries starting as nil to avoid circular-dependency errors
107 var mains = map[string]func(){
108 `args`: args,
109 `avoid`: avoid.Main,
110 `bytedump`: bytedump.Main,
111 `calc`: calc.Main,
112 `catl`: catl.Main,
113 `coby`: coby.Main,
114 `countdown`: countdown.Main,
115 `datauri`: datauri.Main,
116 `debase64`: debase64.Main,
117 `decsv`: decsv.Main,
118 `dedup`: dedup.Main,
119 `dejsonl`: dejsonl.Main,
120 `dessv`: dessv.Main,
121 `ecoli`: ecoli.Main,
122 `erase`: erase.Main,
123 `files`: files.Main,
124 `filesizes`: filesizes.Main,
125 `finfo`: finfo.Main,
126 `fixlines`: fixlines.Main,
127 `folders`: folders.Main,
128 `gsub`: gsub.Main,
129 `help`: nil,
130 `hima`: hima.Main,
131 `htmlify`: htmlify.Main,
132 `id3pic`: id3pic.Main,
133 `json0`: json0.Main,
134 `json2`: json2.Main,
135 `jsonl`: jsonl.Main,
136 `jsons`: jsons.Main,
137 `match`: match.Main,
138 `n`: n.Main,
139 `ncol`: ncol.Main,
140 `ngron`: ngron.Main,
141 `nhex`: nhex.Main,
142 `njson`: njson.Main,
143 `nn`: nn.Main,
144 `now`: now.Main,
145 `plain`: plain.Main,
146 `primes`: primes.Main,
147 `realign`: realign.Main,
148 `squeeze`: squeeze.Main,
149 `tcatl`: tcatl.Main,
150 `teletype`: teletype.Main,
151 `timezones`: timezones,
152 `tools`: nil,
153 `units`: units.Main,
154 `utfate`: utfate.Main,
155 `waveout`: waveout.Main,
156 `zj`: zj.Main,
157 }
158
159 var experimental = map[string]func(){
160 `fh`: fh.Main,
161 `verdict`: verdict.Main,
162 }
163
164 var extras = map[string]func(){
165 `help`: help,
166 `tools`: tools,
167 }
168
169 var remakes = map[string]func(){
170 `cat`: cat.Main,
171 `head`: head.Main,
172 `ls`: ls.Main,
173 }
174
175 var aliases = map[string]string{
176 `bytedump`: `bytedump`,
177 `ca`: `calc`,
178 `calculate`: `calc`,
179 `calculator`: `calc`,
180 `fc`: `calc`,
181 `frac`: `calc`,
182 `fraca`: `calc`,
183 `fracalc`: `calc`,
184 `datauri`: `datauri`,
185 `unbase64`: `debase64`,
186 `uncsv`: `decsv`,
187 `deduplicate`: `dedup`,
188 `undup`: `dedup`,
189 `unique`: `dedup`,
190 `unjsonl`: `dejsonl`,
191 `unssv`: `dessv`,
192 `fileinfo`: `finfo`,
193 `detrail`: `fixlines`,
194 `id3pic`: `id3pic`,
195 `mp3pic`: `id3pic`,
196 `ncols`: `ncol`,
197 `nicecols`: `ncol`,
198 `nh`: `nhex`,
199 `nj`: `njson`,
200 `nicedigits`: `nn`,
201 `nicenums`: `nn`,
202 `j0`: `json0`,
203 `j2`: `json2`,
204 `jl`: `jsonl`,
205 `detsv`: `jsons`,
206 `tty`: `teletype`,
207 `timezone`: `timezones`,
208 `utf8`: `utfate`,
209 `zoomjson`: `zj`,
210 }
211
212 var blurbs = map[string]string{
213 `args`: `show all ARGumentS given after the tool name, one per line`,
214 `avoid`: `ignore lines matching any of the regexes given`,
215 `bytedump`: `show bytes as hex values, with a wide ASCII panel`,
216 `calc`: `fractional calculator, with floating-point powers`,
217 `catl`: `conCATenate Lines, ensures text ends with a line-feed`,
218 `coby`: `COunt BYtes, and many other byte/text-related stats`,
219 `countdown`: `countdown the seconds/minutes/hours given`,
220 `datauri`: `turn bytes into data-URIs, auto-detecting MIME types`,
221 `debase64`: `decode base64 text and data-URIs`,
222 `decsv`: `convert CSV tables into TSV tables, or into JSON`,
223 `dedup`: `deduplicate lines, emitting each unique line only once`,
224 `dejsonl`: `turn JSON Lines into proper JSON`,
225 `dessv`: `turn tables of space-separated values into TSV tables`,
226 `ecoli`: `expressions coloring lines color-codes matching lines`,
227 `erase`: `ignore/erase all matching regexes away from each line`,
228 `fh`: `Function Heatmapper draws 2-input functions`,
229 `files`: `list all files in the folder(s) given`,
230 `filesizes`: `show sizes of files and block-counts (4K by default)`,
231 `finfo`: `show various file info, for plain-text and/or media files`,
232 `fixlines`: `ignore carriage-returns, or even trailing spaces`,
233 `folders`: `list all folders in the folder(s) given`,
234 `gsub`: `Globally SUBstitute all regular expression matches`,
235 `help`: `show the help message for "easybox"`,
236 `hima`: `HIlight MAtches using the regexes given`,
237 `htmlify`: `turn plain-text lines into HTML documents`,
238 `id3pic`: `get the encoded picture out of audio files, if present`,
239 `json0`: `minimize/fix JSON into the smallest-possible size`,
240 `json2`: `indent JSON into multiple lines, using 2 spaces per level`,
241 `jsonl`: `turn items from top-level JSON arrays into JSON Lines`,
242 `jsons`: `JSON Strings turns TSV into arrays of objects of strings`,
243 `match`: `only keep lines matching any of the regexes given`,
244 `n`: `Number lines, putting tabs between numbers and contents`,
245 `ncol`: `Nice COLumns realigns tables, color-coding their values`,
246 `ngron`: `Nice GRON mimics a subset of "gron", using better colors`,
247 `nhex`: `Nice HEXadecimal shows bytes as hex values and ASCII`,
248 `njson`: `Nice JSON indents and color-codes JSON data`,
249 `nn`: `Nice Numbers color-codes groups of digits for legibility`,
250 `now`: `show the current date and time, also for other timezones`,
251 `plain`: `ignore all ANSI-sequences, leaving unstyled text`,
252 `primes`: `find prime numbers, up to the first million by default`,
253 `realign`: `realign items from the SSV/TSV tables given`,
254 `squeeze`: `aggressively ignore spaces, especially runs of spaces`,
255 `tcatl`: `Titled conCATenate Lines, is like "catl" but with names`,
256 `teletype`: `mimic old-fashioned teletype devices, by delaying output`,
257 `timezones`: `lookup full timezone names from the city/place names given`,
258 `tools`: `list all tools available`,
259 `units`: `convert weird units into the international standard ones`,
260 `utfate`: `decode all other types of UTF text into UTF-8`,
261 `verdict`: `run the command given, showing its success/failure code`,
262 `waveout`: `emit/calculate WAV-format sounds by formula`,
263 `zj`: `Zoom Json, using the keys/indices given as arguments`,
264 }
265
266 func main() {
267 // add the deliberately-missing lookup entries
268 for k, v := range extras {
269 mains[k] = v
270 }
271
272 // try to use the app's `name`, in case it's being called from a file-link
273 // named after one of the tools
274 if tool, ok := lookupTool(path.Base(os.Args[0])); ok {
275 tool()
276 return
277 }
278
279 // try normal tool-lookup using the first command-line argument
280 if len(os.Args) >= 2 {
281 name := os.Args[1]
282
283 if tool, ok := lookupTool(name); ok {
284 os.Args = os.Args[1:]
285 tool()
286 return
287 }
288
289 switch name {
290 case `-h`, `--h`, `-help`, `--help`, `help`:
291 showHelp(os.Stdout)
292 return
293
294 case `-l`, `--l`, `-list`, `--list`:
295 tools()
296 return
297
298 case `-links`, `--links`:
299 showLinksCommands(os.Stdout)
300 return
301
302 case `-t`, `--t`, `-tools`, `--tools`, `tools`:
303 tools()
304 return
305 }
306
307 const fs = "easybox: tool/alias named %q not found\n"
308 fmt.Fprintf(os.Stderr, fs, name)
309 os.Exit(1)
310 }
311
312 showHelp(os.Stderr)
313 fmt.Fprintln(os.Stderr, ``)
314 fmt.Fprintln(os.Stderr, `easybox: no tool name given`)
315 os.Exit(1)
316 }
317
318 // dealias tries to lookup a string to the aliases given, returning the name
319 // given if the lookup fails
320 func dealias(aliases map[string]string, name string) string {
321 if s, ok := aliases[name]; ok {
322 return s
323 }
324 return name
325 }
326
327 func lookupTool(name string) (tool func(), ok bool) {
328 name = strings.ReplaceAll(name, `-`, ``)
329
330 if tool, ok := mains[dealias(aliases, name)]; ok {
331 return tool, ok
332 }
333
334 if tool, ok := remakes[name]; ok {
335 return tool, ok
336 }
337
338 tool, ok = experimental[name]
339 return tool, ok
340 }
341
342 // showHelp has a parameter to write either to stdout or stderr
343 func showHelp(w io.Writer) {
344 fmt.Fprintln(w, info)
345
346 fmt.Fprintln(w, "\nTools Available")
347
348 maxlen := 0
349 names := make([]string, 0, max(len(mains), len(aliases)))
350 for k := range mains {
351 names = append(names, k)
352 maxlen = max(maxlen, utf8.RuneCountInString(k))
353 }
354
355 sort.Strings(names)
356
357 for _, s := range names {
358 fmt.Fprintf(w, " - %-*s %s\n", maxlen, s, blurbs[s])
359 }
360
361 fmt.Fprintln(w, "\nAliases Available")
362
363 maxlen = 0
364 names = names[:0]
365 for k := range aliases {
366 names = append(names, k)
367 maxlen = max(maxlen, utf8.RuneCountInString(k))
368 }
369
370 sort.Strings(names)
371
372 for _, k := range names {
373 fmt.Fprintf(w, " - %-*s -> %s\n", maxlen, k, aliases[k])
374 }
375
376 names = names[:0]
377 for k := range experimental {
378 names = append(names, k)
379 }
380
381 if len(names) == 0 {
382 return
383 }
384
385 fmt.Fprintln(w, "\nExperimental Tools Available")
386
387 for _, k := range names {
388 fmt.Fprintf(w, " - %s\n", k)
389 }
390 }
391
392 // showLinksCommands has a parameter to write either to stdout or stderr
393 func showLinksCommands(w io.Writer) {
394 names := make([]string, 0, len(mains)-len(extras))
395 for k := range mains {
396 if _, ok := extras[k]; ok {
397 continue
398 }
399 names = append(names, k)
400 }
401
402 sort.Strings(names)
403
404 for _, s := range names {
405 fmt.Fprintf(w, "ln -s \"$(which easybox)\" ./%s\n", s)
406 }
407 }
408
409 func args() {
410 args := os.Args[1:]
411 if len(args) == 0 {
412 return
413 }
414
415 w := bufio.NewWriterSize(os.Stdout, 32*1024)
416 defer w.Flush()
417
418 for _, s := range args {
419 w.WriteString(s)
420 if err := w.WriteByte('\n'); err != nil {
421 break
422 }
423 }
424 }
425
426 func help() {
427 showHelp(os.Stdout)
428 }
429
430 func timezones() {
431 args := os.Args[1:]
432
433 if len(args) > 0 {
434 switch args[0] {
435 case `-h`, `--h`, `-help`, `--help`, `help`:
436 os.Stdout.WriteString(blurbs[`timezones`])
437 os.Stdout.WriteString("\n")
438 return
439 }
440 }
441
442 if len(args) > 0 && args[0] == `--` {
443 args = args[1:]
444 }
445
446 if len(args) == 0 {
447 os.Stderr.WriteString(blurbs[`timezones`])
448 os.Stderr.WriteString("\n")
449 return
450 }
451
452 nerr := 0
453 w := bufio.NewWriterSize(os.Stdout, 32*1024)
454 defer w.Flush()
455
456 for _, place := range args {
457 name, ok := now.LookupName(place)
458
459 if !ok {
460 w.Flush()
461 fmt.Fprintf(os.Stderr, "timezone for %q not found\n", place)
462 nerr++
463 continue
464 }
465
466 w.WriteString(name)
467 w.WriteByte('\n')
468 }
469
470 if nerr > 0 {
471 w.Flush()
472 os.Exit(1)
473 }
474 }
475
476 func tools() {
477 names := make([]string, 0, len(mains))
478 for k := range mains {
479 names = append(names, k)
480 }
481
482 sort.Strings(names)
483
484 for _, s := range names {
485 fmt.Fprintln(os.Stdout, s)
486 }
487 }
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 for name := range blurbs {
48 if _, ok := mains[name]; ok {
49 continue
50 }
51 if _, ok := experimental[name]; ok {
52 continue
53 }
54 t.Errorf("description/blurb for name %q, but no tool", name)
55 }
56 }
57
58 func TestFillers(t *testing.T) {
59 for name, v := range mains {
60 if v != nil {
61 continue
62 }
63
64 if _, ok := extras[name]; ok {
65 continue
66 }
67
68 t.Errorf("tool %q has no filler for invalid entry", name)
69 }
70
71 for name := range extras {
72 if v, ok := mains[name]; !ok || v != nil {
73 t.Errorf("filling for missing entry %q", name)
74 }
75 }
76 }
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 avoid := false
55 sensitive := true
56 args := os.Args[1:]
57
58 for len(args) > 0 {
59 switch args[0] {
60 case `-b`, `--b`, `-buffered`, `--buffered`:
61 buffered = true
62 args = args[1:]
63 continue
64
65 case `-h`, `--h`, `-help`, `--help`:
66 os.Stdout.WriteString(info[1:])
67 return
68
69 case `-i`, `--i`, `-ins`, `--ins`:
70 sensitive = false
71 args = args[1:]
72 continue
73
74 case `-iv`, `-vi`:
75 sensitive = false
76 avoid = true
77 args = args[1:]
78 continue
79
80 case `-l`, `--l`, `-links`, `--links`:
81 links = true
82 args = args[1:]
83 continue
84
85 case `-v`, `--v`, `-inv`, `--inv`, `-neg`, `--neg`:
86 avoid = true
87 args = args[1:]
88 continue
89 }
90
91 break
92 }
93
94 if len(args) > 0 && args[0] == `--` {
95 args = args[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 if len(args) == 0 {
106 args = []string{`.`}
107 }
108
109 var exprs []*regexp.Regexp
110 if links {
111 exprs = make([]*regexp.Regexp, 0, len(args)+1)
112 exprs = append(exprs, regexp.MustCompile(linkRegexp))
113 } else {
114 exprs = make([]*regexp.Regexp, 0, len(args))
115 }
116
117 for _, src := range args {
118 var err error
119 var exp *regexp.Regexp
120 if !sensitive {
121 exp, err = regexp.Compile(`(?i)` + src)
122 } else {
123 exp, err = regexp.Compile(src)
124 }
125
126 if err != nil {
127 os.Stderr.WriteString(err.Error())
128 os.Stderr.WriteString("\n")
129 nerr++
130 }
131
132 exprs = append(exprs, exp)
133 }
134
135 if nerr > 0 {
136 os.Exit(1)
137 }
138
139 var buf []byte
140 sc := bufio.NewScanner(os.Stdin)
141 sc.Buffer(nil, 8*1024*1024*1024)
142 bw := bufio.NewWriter(os.Stdout)
143
144 for i := 0; sc.Scan(); i++ {
145 line := sc.Bytes()
146 if i == 0 && bytes.HasPrefix(line, []byte{0xef, 0xbb, 0xbf}) {
147 line = line[3:]
148 }
149
150 s := line
151 if bytes.IndexByte(s, '\x1b') >= 0 {
152 buf = plain(buf[:0], s)
153 s = buf
154 }
155
156 if match(s, exprs) {
157 if avoid {
158 continue
159 }
160
161 if err := emit(bw, line, liveLines); err != nil {
162 return
163 }
164 }
165
166 if avoid {
167 if err := emit(bw, line, liveLines); err != nil {
168 return
169 }
170 }
171 }
172 }
173
174 func emit(w *bufio.Writer, line []byte, live bool) error {
175 w.Write(line)
176 w.WriteByte('\n')
177
178 if !live {
179 return nil
180 }
181
182 return w.Flush()
183 }
184
185 func match(what []byte, with []*regexp.Regexp) bool {
186 for _, e := range with {
187 if e.Match(what) {
188 return true
189 }
190 }
191 return false
192 }
193
194 func plain(dst []byte, src []byte) []byte {
195 for len(src) > 0 {
196 i, j := indexEscapeSequence(src)
197 if i < 0 {
198 dst = append(dst, src...)
199 break
200 }
201 if j < 0 {
202 j = len(src)
203 }
204
205 if i > 0 {
206 dst = append(dst, src[:i]...)
207 }
208
209 src = src[j:]
210 }
211
212 return dst
213 }
214
215 // indexEscapeSequence finds the first ANSI-style escape-sequence, which is
216 // the multi-byte sequences starting with ESC[; the result is a pair of slice
217 // indices which can be independently negative when either the start/end of
218 // a sequence isn't found; given their fairly-common use, even the hyperlink
219 // ESC]8 sequences are supported
220 func indexEscapeSequence(s []byte) (int, int) {
221 var prev byte
222
223 for i, b := range s {
224 if prev == '\x1b' && b == '[' {
225 j := indexLetter(s[i+1:])
226 if j < 0 {
227 return i, -1
228 }
229 return i - 1, i + 1 + j + 1
230 }
231
232 if prev == '\x1b' && b == ']' && i+1 < len(s) && s[i+1] == '8' {
233 j := indexPair(s[i+1:], '\x1b', '\\')
234 if j < 0 {
235 return i, -1
236 }
237 return i - 1, i + 1 + j + 2
238 }
239
240 prev = b
241 }
242
243 return -1, -1
244 }
245
246 func indexLetter(s []byte) int {
247 for i, b := range s {
248 upper := b &^ 32
249 if 'A' <= upper && upper <= 'Z' {
250 return i
251 }
252 }
253
254 return -1
255 }
256
257 func indexPair(s []byte, x byte, y byte) int {
258 var prev byte
259
260 for i, b := range s {
261 if prev == x && b == y && i > 0 {
262 return i
263 }
264 prev = b
265 }
266
267 return -1
268 }
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: ./mediainfo/aiff.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 mediainfo
26
27 import (
28 "errors"
29 "io"
30 "math"
31 )
32
33 // http://paulbourke.net/dataformats/audio/
34
35 var errTruncatedAIFF = errors.New("unexpected end of AIFF data")
36
37 func aiffDuration(r io.Reader) (seconds float64, err error) {
38 data, err := io.ReadAll(r)
39 if err != nil {
40 return 0, err
41 }
42
43 // all these are read when in the COMM block
44 numChan := 0
45 sampleSize := 0
46 sampleRate := 0
47
48 for size := 8; len(data) >= size; data = data[size:] {
49 if len(data) < 8 {
50 return seconds, errTruncatedAIFF
51 }
52 size = int(bytes2uint(data[4:8])) + 8
53 if len(data) < size {
54 return seconds, errTruncatedAIFF
55 }
56
57 switch id := string(data[:4]); id {
58 case "FORM":
59 if len(data) < 12 || !match4(data[8:12], 'A', 'I', 'F', 'F') {
60 return math.NaN(), errTruncatedAIFF
61 }
62 size = 12
63
64 case "COMM":
65 if len(data) < 25 {
66 return math.NaN(), errTruncatedAIFF
67 }
68 numChan = int(bytes2uint(data[8:10]))
69 sampleSize = int(bytes2uint(data[14:16])) / 8
70 sampleRate = int(float80(data[16:26]))
71
72 case "SSND":
73 if len(data) < 12 {
74 return math.NaN(), errTruncatedAIFF
75 }
76 offset := int(bytes2uint(data[8:12]))
77 n := (size - offset - 4) / (sampleSize * numChan)
78 seconds += float64(n) / float64(sampleRate)
79 }
80 }
81
82 return seconds, nil
83 }
File: ./mediainfo/au.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 mediainfo
26
27 import (
28 "encoding/binary"
29 "errors"
30 "io"
31 "math"
32 )
33
34 var (
35 errInvalidAuData = errors.New("invalid AU data")
36 errUnsupportedAuEncoding = errors.New("unsupported AU data encoding")
37 )
38
39 type auHeader struct {
40 Magic uint32 // ".snd" if data are valid
41 Offset uint32
42 Size uint32
43 Encoding uint32
44 SampleRate uint32
45 Channels uint32
46 }
47
48 func auDuration(r io.Reader, n int) (seconds float64, err error) {
49 var header auHeader
50 err = binary.Read(r, binary.BigEndian, &header)
51 if err != nil {
52 return 0, err
53 }
54
55 // check if first 4 bytes are ".snd" in ascii
56 if header.Magic != 0x2e736e64 {
57 return math.NaN(), errInvalidAuData
58 }
59
60 // find how many bytes each sample takes
61 itemSize := 0
62 switch header.Encoding {
63 case 2:
64 itemSize = 1
65 case 3:
66 itemSize = 2
67 case 4:
68 itemSize = 3
69 case 5, 6:
70 itemSize = 4
71 case 7:
72 itemSize = 8
73 default:
74 return math.NaN(), errUnsupportedAuEncoding
75 }
76
77 rate := header.SampleRate * header.Channels * uint32(itemSize)
78 // if header has an unknown data size, calculate it from the file size
79 if header.Size == 0xffffffff {
80 // the au file header is 24 bytes
81 return float64(n-int(header.Offset)-24) / float64(rate), nil
82 }
83 return float64(header.Size) / float64(rate), nil
84 }
File: ./mediainfo/avi.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 mediainfo
26
27 import (
28 "bytes"
29 "encoding/binary"
30 "io"
31 "math"
32 )
33
34 // https://docs.microsoft.com/en-us/previous-versions/windows/desktop/api/Aviriff/ns-aviriff-avimainheader
35 type aviMainHeader struct {
36 Type [4]byte // "avih"
37 Size uint32 // structure size minus 8
38 MicroSecPerFrame uint32
39 MaxBytesPerSec uint32
40 PaddingGranularity uint32
41 Flags uint32
42 TotalFrames uint32
43 InitialFrames uint32
44 Streams uint32
45 SuggestedBufferSize uint32
46 Width uint32
47 Height uint32
48 Reserved [4]uint32
49 }
50
51 // The stream header chunk ('strh') consists of an AVISTREAMHEADER structure
52 // Note: there seem to be 4 more bytes in between "strh" and the start of the struct
53 // https://docs.microsoft.com/en-us/windows/win32/directshow/avi-riff-file-reference
54 // https://docs.microsoft.com/en-us/previous-versions/windows/desktop/api/avifmt/ns-avifmt-avistreamheader
55 type aviStreamHeader struct {
56 Type [4]byte // either "vids" or "auds"
57 Handler [4]byte
58 Flags uint32
59 Priority uint16
60 Language uint16
61 InitialFrames uint32
62
63 Scale uint32
64 Rate uint32
65 Start uint32
66 Length uint32
67
68 SugBufferSize uint32 // suggested buffer size
69 Quality uint32
70 SampleSize uint32
71
72 // `frame info` data are supposed to follow, whatever those are
73 }
74
75 func aviDuration(r io.Reader) (seconds float64, err error) {
76 buf := make([]byte, 2048)
77 n, err := r.Read(buf)
78 if err != io.EOF && err != nil {
79 return math.NaN(), err
80 }
81
82 sec := 0.0
83 buf = buf[:n]
84 for {
85 i := bytes.Index(buf, []byte{'s', 't', 'r', 'h'})
86 if i < 0 {
87 break
88 }
89 i += 8
90 buf = buf[i:]
91
92 dur, err := aviStreamDuration(buf)
93 if err != nil {
94 break
95 }
96 if math.IsNaN(dur) {
97 continue
98 }
99 sec = math.Max(sec, dur)
100 }
101
102 if sec == 0 && n > 0 {
103 return math.NaN(), nil
104 }
105 return sec, nil
106 }
107
108 func aviStreamDuration(data []byte) (seconds float64, err error) {
109 var sh aviStreamHeader
110 r := bytes.NewReader(data)
111 err = binary.Read(r, binary.LittleEndian, &sh)
112 if err != nil {
113 return math.NaN(), err
114 }
115 return float64(sh.Length) * float64(sh.Scale) / float64(sh.Rate), nil
116 }
117
118 func aviResolution(r io.Reader) (int, int, int, error) {
119 buf := make([]byte, 2048)
120 n, err := r.Read(buf)
121 if err != io.EOF && err != nil {
122 return -1, -1, -1, err
123 }
124
125 buf = buf[:n]
126 i := bytes.Index(buf, []byte{'a', 'v', 'i', 'h'})
127 if i < 0 {
128 return -1, -1, -1, nil
129 }
130
131 var mh aviMainHeader
132 r = bytes.NewReader(buf[i:])
133 err = binary.Read(r, binary.LittleEndian, &mh)
134 if err != nil {
135 return -1, -1, -1, err
136 }
137
138 return int(mh.Width), int(mh.Height), -1, nil
139 }
File: ./mediainfo/bmp.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 mediainfo
26
27 import (
28 "encoding/binary"
29 "errors"
30 "io"
31 )
32
33 var errUnsupportedBMPFormat = errors.New("unsupported BMP format")
34
35 type bmpHeader struct {
36 Type [2]byte
37 Size uint32
38 Reserved uint32
39 PixelArrayOffset uint32
40 InfoHeaderSize uint32
41 Width int32
42 Height int32
43 ColorPlanes uint16
44 BitsPerPixel uint16
45 }
46
47 func bmpResolution(r io.Reader) (int, int, int, error) {
48 var header bmpHeader
49 err := binary.Read(r, binary.LittleEndian, &header)
50 if err != nil {
51 return 0, 0, 0, err
52 }
53 // only windows bitmaps are supported
54 if header.Type[0] != 'B' || header.Type[1] != 'M' {
55 return 0, 0, 0, errUnsupportedBMPFormat
56 }
57 return int(header.Width), int(header.Height), int(header.BitsPerPixel), nil
58 }
File: ./mediainfo/doc.go
1 /*
2 # mediainfo
3
4 Package to extract all sorts of information from media (pics, audio, video)
5 files/data.
6
7 Right now it can find picture resolution for
8 - GIF
9 - PNG
10 - WEBP (only some of its variants)
11
12 and the play length in seconds for many common audio/video files, such as
13 - AAC
14 - AIFF
15 - AVI
16 - FLAC
17 - MP3
18 - MP4
19 - WAVE
20
21 Notably missing in the list of supported formats is JPEG.
22 */
23
24 package mediainfo
File: ./mediainfo/flac.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 mediainfo
26
27 import (
28 "encoding/binary"
29 "errors"
30 "io"
31 "math"
32 )
33
34 // https://gist.github.com/lukasklein/8c474782ed66c7115e10904fecbed86a
35
36 var (
37 errShortFlacInfoStream = errors.New("not enough bytes in the stream-info block")
38 errInvalidFlacSampleRate = errors.New("invalid sample rate")
39 errNoFlacMarker = errors.New("data not marked with \"fLaC\"")
40 )
41
42 func flacDuration(r io.Reader) (seconds float64, err error) {
43 // check if file starts with a flac marker
44 var flac [4]byte
45 err = binary.Read(r, binary.BigEndian, &flac)
46 if err == io.EOF {
47 return 0, nil
48 }
49 if err != nil {
50 return 0, err
51 }
52 if flac[0] != 'f' || flac[1] != 'L' || flac[2] != 'a' || flac[3] != 'C' {
53 return 0, errNoFlacMarker
54 }
55
56 for {
57 // read block-marker and block-size packed in 4 bytes
58 var meta [4]byte
59 err := binary.Read(r, binary.BigEndian, &meta)
60 if err == io.EOF {
61 return 0, nil
62 }
63 if err != nil {
64 return 0, err
65 }
66
67 blockType := meta[0] & 0x7f
68 size := bytes2uint(meta[1:4])
69 // block-type 0 means it's a stream-info block
70 if blockType == 0 {
71 info := make([]byte, size)
72 err := binary.Read(r, binary.BigEndian, &info)
73 if err == io.EOF {
74 return 0, nil
75 }
76 if err != nil {
77 return 0, err
78 }
79
80 // https://xiph.org/flac/format.html#metadata_block_streaminfo
81 if len(info) < 18 {
82 return math.NaN(), errShortFlacInfoStream
83 }
84 sr := bytes2uint(info[10:13]) >> 4 // lowest bits are metadata unrelated to sample rate
85 n := bytes2uint([]byte{info[13] & 0x0f, info[14], info[15], info[16], info[17]})
86 if sr == 0 {
87 return math.NaN(), errInvalidFlacSampleRate
88 }
89 return float64(n) / float64(sr), nil
90 }
91 }
92 }
File: ./mediainfo/gif.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 mediainfo
26
27 import (
28 "encoding/binary"
29 "errors"
30 "io"
31 )
32
33 var errInvalidGIFSignature = errors.New("invalid GIF signature")
34
35 // http://www33146ue.sakura.ne.jp/staff/iz/formats/gif.html
36 // global color table length is determined by the bits of its related info field
37 // when top bit is 0 it's 0, else it's 2 to the (lowest 3 bits + 1)
38 type gifHeader struct {
39 Signature [6]byte // "GIF87a" or "GIF89a"
40 LogicalScreenWidth uint16
41 LogicalScreenHeight uint16
42 GlobalColorTableInfo byte
43 BackgroundColorIndex byte
44 PixelAspectRatio byte
45 // GlobalColorTable: variable-length RGB array
46 }
47
48 func gifResolution(r io.ReadSeeker) (int, int, int, error) {
49 var header gifHeader
50 err := binary.Read(r, binary.LittleEndian, &header)
51 if err != nil {
52 return 0, 0, 0, err
53 }
54
55 if !gifSignatureIsValid(header.Signature) {
56 return 0, 0, 0, errInvalidGIFSignature
57 }
58 return int(header.LogicalScreenWidth), int(header.LogicalScreenHeight), 8, nil
59 }
60
61 func gifSignatureIsValid(s [6]byte) bool {
62 // valid GIF data must start either with "GIF87a" or "GIF89a"
63 if s[0] != 'G' || s[1] != 'I' || s[2] != 'F' {
64 return false
65 }
66 if s[3] != '8' || (s[4] != '7' && s[4] != '9') || s[5] != 'a' {
67 return false
68 }
69 return true
70 }
File: ./mediainfo/heic.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 mediainfo
26
27 import (
28 "bytes"
29 "io"
30 )
31
32 func heicResolution(r io.Reader) (int, int, int, error) {
33 var buf [4 * 1024]byte
34 n, err := r.Read(buf[:])
35 data := buf[:n]
36
37 // seek the 1st `ispe` marker
38 i := bytes.Index(data, []byte("ispe"))
39 if i < 0 {
40 return -1, -1, -1, ErrResolutionNotFound
41 }
42
43 // seek the 2nd `ispe` marker
44 data = data[i+len("ispe"):]
45 i = bytes.Index(data, []byte("ispe"))
46 if i < 0 {
47 return -1, -1, -1, ErrResolutionNotFound
48 }
49
50 data = data[i:]
51 if len(data) < 16 {
52 return -1, -1, -1, ErrResolutionNotFound
53 }
54
55 // width starts 8 bytes after the 2nd `ispe` marker and is big-endian
56 data = data[8:]
57 width := 16_777_216 * int(data[0])
58 width += 65_536 * int(data[1])
59 width += 256 * int(data[2])
60 width += int(data[3])
61
62 // height starts 4 bytes after the width and is big-endian
63 data = data[4:]
64 height := 16_777_216 * int(data[0])
65 height += 65_536 * int(data[1])
66 height += 256 * int(data[2])
67 height += int(data[3])
68
69 // bits-per-pixel are unknown, unless `pixi` metadata are found next
70 bpp := -1
71
72 // seek the `pixi` marker after the `ispe` markers
73 if i := bytes.Index(data, []byte("pixi")); i >= 0 {
74 if data := data[i+len("pixi"):]; len(data) >= 8 {
75 // color-depth info starts 4 bytes after the `pixi` marker
76 data = data[4:]
77
78 // get the number of color channels/components
79 n := data[0]
80 data = data[1:]
81
82 // add the bits-per-pixel count for each color channel/component
83 bpp = 0
84 for i := 0; i < int(n) && len(data) > 0; i++ {
85 bpp += int(data[0])
86 data = data[1:]
87 }
88 }
89 }
90
91 if err == io.EOF {
92 err = nil
93 }
94 return width, height, bpp, err
95 }
File: ./mediainfo/ico.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 mediainfo
26
27 import (
28 "encoding/binary"
29 "io"
30 )
31
32 // https://en.wikipedia.org/wiki/ICO_(file_format)
33
34 const (
35 IconDirTypeIcon = 1
36 IconDirTypeCursor = 2
37 )
38
39 type IconDir struct {
40 Reserved uint16 // must be 0
41 Type uint16 // 1 means icon, 2 means cursor
42 NumPics uint16 // how many differently-resoluted pics are available
43 }
44
45 type IconDirEntry struct {
46 Width uint8 // image width in pixels: 0 means 256
47 Height uint8 // image height in pixels: 0 means 256
48
49 ColorCount uint8
50 Reserved uint8
51
52 Extra1 uint16 // color palettes for icons, horizontal hotspot position for cursors
53 Extra2 uint16 // bits-per-pixel for icons, vertical hotspot position for cursors
54
55 Size uint32 // how many bytes image data use
56 Offset uint32 // where image bytes start from the beginning of the stream
57 }
58
59 func icoResolution(r io.ReadSeeker) (int, int, int, error) {
60 var header IconDir
61 err := binary.Read(r, binary.LittleEndian, &header)
62 if err != nil {
63 return 0, 0, 0, err
64 }
65
66 width := 0
67 height := 0
68 maxbpp := 8
69 for i := 0; i < int(header.NumPics); i++ {
70 var e IconDirEntry
71 err = binary.Read(r, binary.LittleEndian, &e)
72 if err == io.EOF {
73 return width, height, maxbpp, nil
74 }
75 if err != nil {
76 return 0, 0, 0, err
77 }
78
79 w := int(e.Width)
80 // 0 width means 256
81 if w == 0 {
82 w = 256
83 }
84 h := int(e.Height)
85 // 0 height means 256
86 if h == 0 {
87 h = 256
88 }
89 bpp := 8
90 if e.ColorCount == 0 {
91 bpp = int(e.Extra2)
92 }
93
94 // keep track of max width, height, and bpp
95 if w > width {
96 width = w
97 }
98 if h > height {
99 height = h
100 }
101 if bpp > maxbpp {
102 maxbpp = bpp
103 }
104 }
105
106 return width, height, maxbpp, nil
107 }
File: ./mediainfo/id3v2.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 mediainfo
26
27 import (
28 "bytes"
29 "errors"
30 "io"
31 )
32
33 // calcSizeID3v2 finds the ID3v2 size in bytes from the ID3v2 header given; if
34 // the slice given isn't a valid/complete ID3v2 header, the result is 0
35 func calcSizeID3v2(b []byte) int {
36 if len(b) >= 10 && bytes.HasPrefix(b, []byte{'I', 'D', '3'}) {
37 n := 0
38 // each byte has top bit 0, and the other 7 bits as payload: the 4
39 // bytes thus result in a 28-bit value
40 n += int(b[6]) * 128 * 128 * 128
41 n += int(b[7]) * 128 * 128
42 n += int(b[8]) * 128
43 n += int(b[9])
44 return n
45 }
46 return 0
47 }
48
49 func CopyThumbnailMP3(w io.Writer, r io.Reader) (mimetype string, err error) {
50 const bufsize = 128 * 1024
51 var buf [bufsize]byte
52
53 for {
54 n, err := r.Read(buf[:])
55 data := buf[:n]
56
57 if i := bytes.Index(data, []byte{'A', 'P', 'I', 'C'}); i >= 0 {
58 return handleAPIC(w, r, data[i+len("APIC"):])
59 }
60
61 if err == io.EOF {
62 return mimetype, errors.New("no thumbnail found")
63 }
64 if err != nil {
65 return mimetype, err
66 }
67 }
68 }
69
70 func handleAPIC(w io.Writer, r io.Reader, data []byte) (mimetype string, err error) {
71 const bufsize = 128 * 1024
72 var buf [bufsize]byte
73
74 if len(data) < 4 {
75 const msg = "failed to detect thumbnail-payload size"
76 return "", errors.New(msg)
77 }
78
79 size := 0
80 // section-size seems stored as 4 little-endian bytes
81 size += int(data[3]) * 128 * 128 * 128
82 size += int(data[2]) * 128 * 128
83 size += int(data[1]) * 128
84 size += int(data[0])
85
86 i, j := findThumbnailMIME(data)
87 if i < 0 {
88 const msg = "failed to sync to start of thumbnail data"
89 return mimetype, errors.New(msg)
90 }
91
92 mimetype = string(data[i:j])
93 data = data[j:]
94 size -= j
95 if len(data) < 2 {
96 n, _ := r.Read(buf[:])
97 data = buf[:n]
98 }
99 data = data[2:]
100 size -= 2
101 if i := bytes.IndexByte(data, 0); i >= 0 {
102 data = data[i+1:]
103 size -= i + 1
104 } else {
105 const msg = "failed to sync to comment before thumbnail"
106 return mimetype, errors.New(msg)
107 }
108
109 start := bytes.NewReader(data)
110 rest := io.LimitReader(r, int64(size-len(data)))
111 _, err = io.Copy(w, io.MultiReader(start, rest))
112 return mimetype, err
113 }
114
115 func findThumbnailMIME(data []byte) (start int, stop int) {
116 if i := bytes.Index(data, []byte("image/")); i >= 0 {
117 if j := bytes.IndexByte(data[i:], 0); j >= 0 {
118 return i, i + j
119 }
120 }
121 return -1, -1
122 }
File: ./mediainfo/jpeg.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 mediainfo
26
27 import (
28 "bytes"
29 "encoding/binary"
30 "io"
31 )
32
33 // https://www.media.mit.edu/pia/Research/deepview/exif.html
34
35 // https://en.wikipedia.org/wiki/JPEG#Syntax_and_structure
36
37 func jpegResolution(r io.Reader) (int, int, int, error) {
38 var data [1024 * 8]byte
39 n, err := r.Read(data[:])
40 if err != nil {
41 return 0, 0, 0, err
42 }
43 buf := data[:n]
44
45 if bytes.Contains(buf, []byte{'J', 'F', 'I', 'F'}) {
46 return jpegResolutionJFIF(buf, binary.BigEndian)
47 }
48 if bytes.Contains(buf, []byte{'E', 'x', 'i', 'f', 0, 0, 'I', 'I'}) {
49 return jpegResolutionEXIF(buf, binary.LittleEndian)
50 }
51 if bytes.Contains(buf, []byte{'E', 'x', 'i', 'f', 0, 0, 'M', 'M'}) {
52 return jpegResolutionEXIF(buf, binary.BigEndian)
53 }
54 return jpegResolutionJFIF(buf, binary.BigEndian)
55 }
56
57 func jpegResolutionEXIF(data []byte, order binary.ByteOrder) (int, int, int, error) {
58 var imageWidth [2]byte = [2]byte{0xa0, 0x02}
59 var imageHeight [2]byte = [2]byte{0xa0, 0x03}
60 if order == binary.LittleEndian {
61 imageWidth[0], imageWidth[1] = imageWidth[1], imageWidth[0]
62 imageHeight[0], imageHeight[1] = imageHeight[1], imageHeight[0]
63 }
64
65 var w, h int
66 for sub := data; len(sub) > 0; {
67 wt, ht, rest := jpegWidthHeightEXIF(sub, order, imageWidth, imageHeight)
68 if wt > w {
69 w = wt
70 }
71 if ht > h {
72 h = ht
73 }
74 sub = rest
75 }
76
77 // if resolution not found in EXIF metadata, try as a JFIF: sometimes it works
78 if w == 0 && h == 0 {
79 // return jpegResolutionJFIF(buf, order)
80 return jpegResolutionJFIF(data, binary.BigEndian)
81 }
82 return int(w), int(h), -1, nil
83 }
84
85 func jpegWidthHeightEXIF(data []byte, order binary.ByteOrder, imageWidth, imageHeight [2]byte) (int, int, []byte) {
86 var w, h uint16
87 const markerLen = len(imageWidth)
88
89 if i := bytes.Index(data, imageWidth[:]); i >= 0 && i+markerLen+2 < len(data) {
90 data = data[i+markerLen:]
91 offset := order.Uint16(data)
92 i = 2 + int(offset)
93 if i+2 < len(data) {
94 data = data[i:]
95 w = order.Uint16(data)
96 }
97 } else {
98 return -1, -1, nil
99 }
100
101 if i := bytes.Index(data, imageHeight[:]); i >= 0 && i+markerLen+2 < len(data) {
102 data = data[i+markerLen:]
103 offset := order.Uint16(data)
104 i = 2 + int(offset)
105 if i+2 < len(data) {
106 data = data[i:]
107 h = order.Uint16(data)
108 }
109 } else {
110 return int(w), -1, nil
111 }
112
113 return int(w), int(h), data
114 }
115
116 func jpegResolutionJFIF(buf []byte, order binary.ByteOrder) (int, int, int, error) {
117 var i int
118 // start of frame baseline-DCT
119 i = bytes.Index(buf, []byte{0xff, 0xc0, 0x00, 0x11, 0x08})
120 if i >= 0 && i+5+2*2 < len(buf) {
121 h := order.Uint16(buf[i+5:])
122 w := order.Uint16(buf[i+7:])
123 return int(w), int(h), -1, nil
124 }
125 // start of frame progressive-DCT
126 i = bytes.Index(buf, []byte{0xff, 0xc2, 0x00, 0x11, 0x08})
127 if i >= 0 && i+5+2*2 < len(buf) {
128 h := order.Uint16(buf[i+5:])
129 w := order.Uint16(buf[i+7:])
130 return int(w), int(h), -1, nil
131 }
132 return 0, 0, 0, ErrResolutionNotFound
133 }
File: ./mediainfo/mediainfo.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 mediainfo
26
27 import (
28 "errors"
29 "io"
30 "math"
31 "os"
32 "path/filepath"
33 "strings"
34 )
35
36 var (
37 ErrUnsupportedFormat = errors.New("not a supported media format")
38 ErrNotPlayableMedia = errors.New("not a playable media format")
39 ErrNotPicture = errors.New("not a supported picture format")
40
41 ErrDurationNotFound = errors.New("duration not found")
42 ErrResolutionNotFound = errors.New("resolution not found")
43
44 errNotSeeker = errors.New("a reader which could also seek was needed")
45 )
46
47 // Duration returns the time duration of the stream given, assuming if it's audio/video
48 func Duration(r io.Reader, n int, typeHint string) (seconds float64, err error) {
49 switch normalizeTypeHint(typeHint) {
50 case "aiff", "aif", "snd", "AIFF", "AIF", "SND":
51 return aiffDuration(r)
52 case "au", "AU":
53 return auDuration(r, n)
54 case "flac", "x-flac", "FLAC", "X-FLAC": // the flac MIME-type is audio/x-flac
55 return flacDuration(r)
56 case "mp3", "MP3", "mpeg", "MPEG": // the mp3 MIME-type is audio/mpeg
57 rs, ok := r.(io.ReadSeeker)
58 if !ok {
59 return math.NaN(), errNotSeeker
60 }
61 return mp3Duration(rs)
62 case
63 "aac", "m4a", "m4b", "mp4", "m4v", "mov", "3gp",
64 "AAC", "M4A", "M4B", "MP4", "M4V", "MOV", "3GP":
65 rs, ok := r.(io.ReadSeeker)
66 if !ok {
67 return math.NaN(), errNotSeeker
68 }
69 return mpeg4Duration(rs)
70 case "wav", "x-wav", "WAV", "X-WAV": // the wav MIME-type is audio/x-wav
71 return waveDuration(r)
72 case "avi", "AVI":
73 return aviDuration(r)
74 case "webm", "WEBM":
75 return webmDuration(r)
76 case "aifc", "wma", "AIFC", "WMA":
77 return math.NaN(), ErrUnsupportedFormat
78 case "mkv", "MKV":
79 // only works if it's a WEBM from youtube
80 return webmDuration(r)
81 case "wmv", "divx", "mpg", "ogg", "opus":
82 return math.NaN(), ErrUnsupportedFormat
83 case "WMV", "DIVX", "MPG", "OGG", "OPUS":
84 return math.NaN(), ErrUnsupportedFormat
85 default:
86 return math.NaN(), ErrNotPlayableMedia
87 }
88 }
89
90 // FileDuration returns the time duration of the file given, assuming if it's audio/video,
91 // otherwise it's NaN: the reading position must be at the beginning before calling this function
92 func FileDuration(f *os.File) (seconds float64, err error) {
93 n := 0
94 info, err := f.Stat()
95 if err == nil {
96 n = int(info.Size())
97 }
98 return Duration(f, n, f.Name())
99 }
100
101 // Resolution returns the size of the file given, assuming it's a picture
102 func Resolution(r io.ReadSeeker, typeHint string) (w int, h int, bitDepth int, err error) {
103 switch normalizeTypeHint(typeHint) {
104 case "mp4", "MP4":
105 return mpeg4Resolution(r)
106 case "avi", "AVI":
107 return aviResolution(r)
108 case "png", "PNG":
109 return pngResolution(r)
110 case "jpeg", "jpg", "JPEG", "JPG":
111 return jpegResolution(r)
112 case "heic", "HEIC":
113 return heicResolution(r)
114 case "gif", "GIF":
115 return gifResolution(r)
116 case "bmp", "BMP":
117 return bmpResolution(r)
118 case "webp", "WEBP":
119 return webpResolution(r)
120 case "svg", "SVG":
121 return svgResolution(r)
122 case "psd", "PSD":
123 return psdResolution(r)
124 case "tiff", "TIFF", "tif", "TIF":
125 return tiffResolution(r)
126 case "ico", "cur", "ICO", "CUR":
127 return icoResolution(r)
128 case "tga", "jp2", "TGA", "JP2":
129 return 0, 0, 0, ErrUnsupportedFormat
130 default:
131 return 0, 0, 0, ErrNotPicture
132 }
133 }
134
135 func normalizeTypeHint(s string) string {
136 // use the file extension if given a filename, ignoring leading dots
137 if ext := filepath.Ext(s); ext != "" {
138 return strings.TrimPrefix(ext, ".")
139 }
140
141 // trim major MIME types
142 if i := strings.LastIndex(s, "/"); i >= 0 {
143 s = s[i+1:]
144 }
145 // trim charset type, which is preceded by a semicolon in MIME types
146 if i := strings.LastIndex(s, ";"); i >= 0 {
147 s = s[:i]
148 }
149 return s
150 }
File: ./mediainfo/mp3.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 mediainfo
26
27 /*
28 The MIT License (MIT)
29
30 Copyright (c) 2026 pacman64
31
32 Permission is hereby granted, free of charge, to any person obtaining a copy of
33 this software and associated documentation files (the "Software"), to deal
34 in the Software without restriction, including without limitation the rights to
35 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
36 of the Software, and to permit persons to whom the Software is furnished to do
37 so, subject to the following conditions:
38
39 The above copyright notice and this permission notice shall be included in all
40 copies or substantial portions of the Software.
41
42 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
43 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
44 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
45 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
46 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
47 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
48 SOFTWARE.
49 */
50
51 import (
52 "errors"
53 "io"
54 "math"
55 )
56
57 // http://www.mp3-tech.org/programmer/frame_header.html
58
59 /*
60 aaaaaaaaaaa bb cc d eeee ff g h ii jj k l mm
61 11111111111 00 00 0 0000 00 0 0 00 00 0 0 00 frame-sync mask
62 00000000000 11 00 0 0000 00 0 0 00 00 0 0 00 audio version mask
63 00000000000 00 11 0 0000 00 0 0 00 00 0 0 00 audio layer mask
64 00000000000 00 00 1 0000 00 0 0 00 00 0 0 00 CRC check mask
65 00000000000 00 00 0 1111 00 0 0 00 00 0 0 00 bit-rate index mask
66 00000000000 00 00 0 0000 11 0 0 00 00 0 0 00 sample-rate index mask
67 00000000000 00 00 0 0000 00 1 0 00 00 0 0 00 frame-padding check mask
68
69 aaaaaaaa aaabbccd eeeeffgh iijjklmm
70 */
71
72 // these errors are for the rarely-used reserved options in frame headers
73 var (
74 ErrMP3ReservedLayer = errors.New("MP3 data use reserved format layer")
75 ErrMP3ReservedVersion = errors.New("MP3 data use reserved format versions")
76 )
77
78 var mp3BitRates = []int{
79 0, 0, 0, 0, 0, 0, // free bit-rates
80 32000, 32000, 32000, 32000, 8000, 8000,
81 64000, 48000, 40000, 48000, 16000, 16000,
82 96000, 56000, 48000, 56000, 24000, 24000,
83 128000, 64000, 56000, 64000, 32000, 32000,
84 160000, 80000, 64000, 80000, 40000, 40000,
85 192000, 96000, 80000, 96000, 48000, 48000,
86 224000, 112000, 96000, 112000, 56000, 56000,
87 256000, 128000, 112000, 128000, 64000, 64000,
88 288000, 160000, 128000, 144000, 80000, 80000,
89 320000, 192000, 160000, 160000, 96000, 96000,
90 352000, 224000, 192000, 176000, 112000, 112000,
91 384000, 256000, 224000, 192000, 128000, 128000,
92 416000, 320000, 256000, 224000, 144000, 144000,
93 448000, 384000, 320000, 256000, 160000, 160000,
94 0, 0, 0, 0, 0, 0, // reserved (invalid) space for bit-rates
95 }
96
97 var mp3SampleRates = []int{
98 44100, 22050, 11025,
99 48000, 24000, 12000,
100 32000, 16000, 8000,
101 0, 0, 0,
102 }
103
104 // https://stackoverflow.com/questions/6220660/calculating-the-length-of-mp3-frames-in-milliseconds
105 var mp3SamplesPerFrame = []int{
106 384, 1152, 1152, // MPEG 1
107 384, 1152, 576, // MPEG 2
108 384, 1152, 576, // MPEG 2.5
109 }
110
111 // mp3Duration tries to find the duration in seconds of the MP3 stream given
112 func mp3Duration(r io.ReadSeeker) (seconds float64, err error) {
113 // buffers larger than 32kb don't seem to speed things up further
114 var b [32 * 1024]byte
115
116 // how many leading bytes to skip/ignore from current chunk
117 skip := 0
118
119 for i := 0; true; i++ {
120 n, err := r.Read(b[:])
121 if n <= 0 {
122 return seconds, nil
123 }
124 if err != nil && err != io.EOF {
125 return seconds, err
126 }
127
128 // only check for ID3v2 metadata on the first chunk read
129 if i == 0 && n >= 10 {
130 skip = calcSizeID3v2(b[:n])
131 }
132
133 // done, if there aren't enough data for a frame intro
134 if n < 3 {
135 return seconds, nil
136 }
137
138 // check whether whole chunk needs skipping, as unlikely as that is
139 if n < skip {
140 skip -= n
141 continue
142 }
143
144 dt, skipnext, err := mp3SliceDuration(b[skip:n])
145 if err != nil {
146 return seconds, err
147 }
148
149 skip = skipnext
150 seconds += dt
151 }
152
153 return seconds, nil
154 }
155
156 // mp3SliceDuration handles the slice logic for func mp3Duration
157 func mp3SliceDuration(b []byte) (sec float64, skip int, err error) {
158 // when there aren't enough data for a frame, the duration is 0 seconds
159 if len(b) < 3 {
160 return 0, 0, nil
161 }
162
163 // upper-limit for index is 2 less than length, since there's a 2-byte
164 // look-ahead in loop
165 for i := 0; i < len(b)-2; i++ {
166 // frames start with their first 11 bits all on
167 syn := b[i]
168 if syn != 255 {
169 // not all the first 8 bits are on: not a frame-sync
170 continue
171 }
172 h1 := b[i+1]
173 if h1 < 224 {
174 // not all the 3 extra bits are on: not a frame-sync
175 continue
176 }
177 h2 := b[i+2]
178
179 // check the audio layer number using the 3rd-last and 2nd-last bits
180 layer := 0
181 switch h1 & 0b00000110 {
182 case 0:
183 // return t, ErrMP3ReservedLayer
184 continue
185 case 2:
186 layer = 3
187 case 4:
188 layer = 2
189 case 6:
190 layer = 1
191 }
192
193 // check the MPEG version using the 5th-last and 4th-last bits:
194 // version 3 means MPEG 2.5
195 version := 0
196 switch h1 & 0b00011000 {
197 case 0: // MPEG 2.5
198 version = 3
199 case 8: // reserved (invalid) // 0b00001000
200 // return t, ErrMP3ReservedVersion
201 // ignore frames with a reserved-value version, instead of
202 // giving an error
203 continue
204 case 16: // MPEG 2 // 0b00010000
205 version = 2
206 case 24: // MPEG 1 // 0b00011000
207 version = 1
208 }
209
210 // check for frame padding using the 2nd-last bit
211 padding := 0
212 if h2&0b00000010 != 0 {
213 padding = 1
214 }
215
216 bitRateRow := int(h2 >> 4)
217 if bitRateRow == 0 || bitRateRow == 15 {
218 continue
219 }
220
221 sampleRateRow := int(h2 & 0b00001100 >> 2)
222 if sampleRateRow == 3 {
223 continue
224 }
225
226 bitRate := 0
227 switch version {
228 case 1:
229 bitRate = mp3BitRates[6*bitRateRow+layer-1]
230 case 2, 3:
231 bitRate = mp3BitRates[6*bitRateRow+2*layer-1]
232 }
233 sampleRate := mp3SampleRates[3*sampleRateRow+version-1]
234
235 // update time duration value
236 spf := mp3SamplesPerFrame[3*(version-1)+layer-1]
237 sec += float64(spf) / float64(sampleRate)
238
239 // calculate how many bytes to jump forward
240 //
241 // http://www.mp3-converter.com/mp3codec/frames.htm
242 // the formula suggested there seems wrong
243 // frame_size = 144 * bit_rate / (sample_rate + padding)
244 // and should instead be
245 // frame_size = floor(144 * bit_rate / sample_rate) + padding
246 n := int(math.Floor(float64(144*bitRate)/float64(sampleRate))) + padding
247
248 // handle skipping inside current slice
249 if i+n < len(b)-3 {
250 // jump ahead by n - 1 instead of n, since the loop already adds 1
251 i += n - 1
252 continue
253 }
254
255 // handle skipping beyond current slice
256 skip = n - (len(b) - i)
257 if skip < 0 {
258 skip = 0
259 }
260 return sec, skip, nil
261 }
262
263 return sec, 0, nil
264 }
File: ./mediainfo/mp3_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 mediainfo
26
27 import (
28 "io"
29 "math"
30 "os"
31 "testing"
32 )
33
34 func TestDurationMP3(t *testing.T) {
35 fname := `testdata/test.mp3`
36 f, err := os.Open(fname)
37 if os.IsNotExist(err) {
38 t.Skipf(`file %s not available: skipping test`, fname)
39 return
40 }
41 if err != nil {
42 t.Error(err.Error())
43 return
44 }
45 defer f.Close()
46
47 d, err := Duration(f, 0, ".mp3")
48 if err != nil {
49 t.Error(err.Error())
50 return
51 }
52
53 const exp = 10.187755
54 if math.Abs(d-exp) > 1e-6 {
55 const fs = "expected duration of %f seconds, but got %f instead"
56 t.Fatalf(fs, exp, d)
57 }
58 }
59
60 // BenchmarkDurationMP3 mainly tests how the buffer-size used in func
61 // mp3Duration affects performance
62 func BenchmarkDurationMP3(b *testing.B) {
63 fname := `testdata/test.mp3`
64 f, err := os.Open(fname)
65 if os.IsNotExist(err) {
66 b.Skipf(`file %s not available: skipping benchmark`, fname)
67 return
68 }
69 if err != nil {
70 b.Error(err.Error())
71 return
72 }
73 defer f.Close()
74
75 b.Run("mp3-duration", func(b *testing.B) {
76 f.Seek(0, io.SeekStart)
77 b.ResetTimer()
78 mp3Duration(f)
79 })
80 }
File: ./mediainfo/mpeg4.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 mediainfo
26
27 import (
28 "bytes"
29 "encoding/binary"
30 "io"
31 "math"
32 )
33
34 // For a general description of the mpeg4 container format see
35 // http://www.cimarronsystems.com/wp-content/uploads/2017/04/Elements-of-the-H.264-VideoAAC-Audio-MP4-Movie-v2_0.pdf
36 // especially pages 4 and 5 describing the "moov" chunk
37
38 // Details of the 9-item matrix are in section "Matrices" (page 199) from the official Quicktime Format spec
39 // https://developer.apple.com/standards/qtff-2001.pdf
40
41 type mpeg4ChunkHeader struct {
42 Size uint32
43 Type [4]byte
44 }
45
46 type mpeg4MOOVChunkInfo struct {
47 // what follows is the info section for the first track
48 mpeg4ChunkHeader
49
50 Version byte
51 Flags [3]byte
52
53 CreationTime uint32 // seconds since the start of 1904
54 ModificationTime uint32 // seconds since the start of 1904
55 TimeScale uint32 // number of time units per seconds
56 Duration uint32 // total play length in time units
57
58 PreferredRate uint32
59 PreferredVolume uint16
60 Reserved [10]byte // should all be 0s
61
62 // more fields which I don't care about
63 }
64
65 // this one comes right after a chunk header of type "trak"
66 type mpeg4TrackHeaderInfo struct {
67 mpeg4ChunkHeader // type is "tkhd"
68
69 Version byte
70 Flags [3]byte
71
72 CreationTime uint32
73 ModificationTime uint32
74 TrackID uint32
75 Reserved uint32
76 Duration uint32
77
78 ReservedZeros [8]byte // should all be 0s
79 Layer uint16
80 AlternativeGroup uint16
81 Matrix [9]uint32
82
83 TrackWidth uint32 // divide by 65,536 for video width
84 TrackHeight uint32 // divide by 65,536 for video height
85 }
86
87 func mpeg4Duration(r io.ReadSeeker) (float64, error) {
88 var h mpeg4ChunkHeader
89 for {
90 err := binary.Read(r, binary.BigEndian, &h)
91 if err == io.EOF {
92 return math.NaN(), nil
93 }
94 if err != nil {
95 return math.NaN(), err
96 }
97
98 if h.Type[0] == 'm' && h.Type[1] == 'o' && h.Type[2] == 'o' && h.Type[3] == 'v' {
99 var info mpeg4MOOVChunkInfo
100 err := binary.Read(r, binary.BigEndian, &info)
101 return float64(info.Duration) / float64(info.TimeScale), err
102 }
103 r.Seek(int64(h.Size)-8, io.SeekCurrent)
104 }
105 }
106
107 func mpeg4Resolution(r io.ReadSeeker) (int, int, int, error) {
108 buf := make([]byte, 2048)
109 n, err := r.Read(buf)
110 if err != nil {
111 return -1, -1, -1, err
112 }
113
114 buf = buf[:n]
115 i := bytes.Index(buf, []byte{'t', 'r', 'a', 'k'})
116 if i < 0 {
117 return -1, -1, -1, nil
118 }
119
120 i += 8 // skip over the "trak" chunk header
121 r = bytes.NewReader(buf[i:])
122 var info mpeg4TrackHeaderInfo
123 err = binary.Read(r, binary.BigEndian, &info)
124 return int(info.TrackWidth / 65_536), int(info.TrackHeight / 65_536), -1, err
125 }
File: ./mediainfo/png.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 mediainfo
26
27 import (
28 "encoding/binary"
29 "errors"
30 "io"
31 )
32
33 var (
34 errInvalidPNGSignature = errors.New("invalid PNG signature")
35 errInvalidPNGColorType = errors.New("invalid PNG color type")
36 )
37
38 type pngHeader struct {
39 Signature uint64
40 Image struct {
41 ChunkLength uint32
42 ChunkType [4]byte
43 Width int32
44 Height int32
45 BitDepth byte
46 ColorType byte
47 CompressionMethod byte
48 FilterMethod byte
49 InterlaceMethod byte
50 }
51 }
52
53 func pngResolution(r io.Reader) (int, int, int, error) {
54 var header pngHeader
55 err := binary.Read(r, binary.BigEndian, &header)
56 if err != nil {
57 return 0, 0, 0, err
58 }
59 if header.Signature != 9894494448401390090 {
60 return 0, 0, 0, errInvalidPNGSignature
61 }
62
63 var bpp int
64 switch header.Image.ColorType {
65 case 0: // grayscale
66 bpp = int(1 * header.Image.BitDepth)
67 case 2: // truecolor
68 bpp = int(3 * header.Image.BitDepth)
69 case 3: // indexed
70 bpp = int(1 * header.Image.BitDepth)
71 case 4: // grayscale + alpha
72 bpp = int(2 * header.Image.BitDepth)
73 case 6: // truecolor + alpha
74 bpp = int(4 * header.Image.BitDepth)
75 default:
76 return 0, 0, 0, errInvalidPNGColorType
77 }
78 return int(header.Image.Width), int(header.Image.Height), bpp, nil
79 }
File: ./mediainfo/psd.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 mediainfo
26
27 import (
28 "encoding/binary"
29 "errors"
30 "io"
31 )
32
33 // https://docs.fileformat.com/image/psd/
34
35 type psdHeader struct {
36 Signature [4]byte // "8BPS"
37 Version uint16 // always 1
38 Reserved [6]byte // all zero bits
39 NumChannels uint16 // range allowed is 1..56
40 Height int32 // range allowed is 1..30000
41 Width int32 // range allowed is 1..30000
42 Depth uint16 // bits per channel
43 ColorMode uint16
44 }
45
46 var errUnsupportedPSDFormat = errors.New("data doesn't start with PSD file signature")
47
48 func psdResolution(r io.Reader) (int, int, int, error) {
49 var h psdHeader
50 err := binary.Read(r, binary.BigEndian, &h)
51 if err != nil {
52 return 0, 0, 0, err
53 }
54 s := h.Signature
55 if s[0] != '8' || s[1] != 'B' || s[2] != 'P' || s[3] != 'S' {
56 return 0, 0, 0, errUnsupportedPSDFormat
57 }
58 return int(h.Width), int(h.Height), int(h.NumChannels * h.Depth), nil
59 }
File: ./mediainfo/read.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 mediainfo
26
27 import (
28 "math"
29 )
30
31 func bytes2uint(s []byte) uint {
32 total := uint(0)
33 for _, b := range s {
34 total <<= 8
35 total += uint(b)
36 }
37 return total
38 }
39
40 // https://www.onicos.com/staff/iz/formats/ieee.c
41
42 func float80(s []byte) float64 {
43 f := 0.0
44 expon := (int(s[0]&0x7F) << 8) | int(s[1]&0xFF)
45 hiMant := (int(s[2]&0xFF) << 24) | (int(s[3]&0xFF) << 16) | (int(s[4]&0xFF) << 8) | (int(s[5] & 0xFF))
46 loMant := (int(s[6]&0xFF) << 24) | (int(s[7]&0xFF) << 16) | (int(s[8]&0xFF) << 8) | (int(s[9] & 0xFF))
47 sign := 1.0
48 if s[0]&0x80 != 0 {
49 sign = -1.0
50 }
51
52 if expon == 0 && hiMant == 0 && loMant == 0 {
53 // floating-point can have value -0
54 return sign * 0
55 }
56
57 // detect Infinity or NaN
58 if expon == 0x7FFF {
59 f = math.Inf(1)
60 } else {
61 expon -= 16383
62 f = math.Ldexp(float64(hiMant), expon-31) + math.Ldexp(float64(loMant), expon-31-32)
63 }
64
65 return sign * f
66 }
67
68 func match4(buf []byte, a, b, c, d byte) bool {
69 return len(buf) >= 4 && buf[0] == a && buf[1] == b && buf[2] == c && buf[3] == d
70 }
File: ./mediainfo/svg.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 mediainfo
26
27 import (
28 "bytes"
29 "io"
30 "strconv"
31 )
32
33 func svgResolution(r io.Reader) (int, int, int, error) {
34 var data [1024]byte
35 n, err := r.Read(data[:])
36 if err != nil {
37 return -1, -1, -1, err
38 }
39
40 w := -1
41 h := -1
42 buf := data[:n]
43
44 if i := bytes.Index(buf, []byte("width=\"")); i >= 0 {
45 sub := buf[i+len("width=\""):]
46 if i = bytes.IndexRune(sub, '"'); i >= 0 {
47 if n, err := strconv.Atoi(string(sub[:i])); err == nil {
48 w = n
49 }
50 }
51 }
52
53 if i := bytes.Index(buf, []byte("height=\"")); i >= 0 {
54 sub := buf[i+len("height=\""):]
55 if i = bytes.IndexRune(sub, '"'); i >= 0 {
56 if n, err := strconv.Atoi(string(sub[:i])); err == nil {
57 h = n
58 }
59 }
60 }
61
62 return w, h, -1, nil
63 }
File: ./mediainfo/tiff.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 mediainfo
26
27 import (
28 "io"
29 )
30
31 func tiffResolution(r io.Reader) (int, int, int, error) {
32 return jpegResolution(r)
33 }
File: ./mediainfo/wav.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 mediainfo
26
27 import (
28 "encoding/binary"
29 "errors"
30 "io"
31 "math"
32 "os"
33 )
34
35 var errFileNeeded = errors.New(
36 "a file was needed, since declared audio-data size exceeds 4GB",
37 )
38
39 type riffHeader struct {
40 Format [4]byte
41 Size uint32
42 Type [4]byte // "WAVE"
43 Chunk [4]byte // "fmt "
44 }
45
46 // http://www.topherlee.com/software/pcm-tut-wavformat.html
47 type riffWave32Info struct {
48 FormatLength uint32
49 Format uint16
50 Channels uint16
51 SampleRate uint32
52 BytesPerSecond uint32
53
54 HardToName uint16
55 BitsPerSample uint16
56
57 Data [4]byte // "data"
58 DataLength uint32
59 }
60
61 // https://tech.ebu.ch/docs/tech/tech3306v1_1.pdf pages 8 and 9
62 // type rf64WaveInfo struct {
63 // Neg1 uint32 // the 32-bite size field should be -1 in rf64 format
64 // Wave [4]byte // "WAVE"
65 // DS64 [4]byte // "ds64"
66
67 // _size uint32
68 // RIFFSize uint64
69 // DataSize uint64
70 // SampleCount uint64
71 // TableLength uint32
72 // Table uint32
73
74 // // variable-length area should follow here
75
76 // // riffWave32Info
77 // }
78
79 type waveFormat int
80
81 const (
82 riffWaveFormat = waveFormat(1)
83 rf64WaveFormat = waveFormat(2)
84 unknownWaveFormat = waveFormat(3)
85 )
86
87 func detectWaveType(h riffHeader) waveFormat {
88 t := h.Type
89 if t[0] != 'W' || t[1] != 'A' || t[2] != 'V' || t[3] != 'E' {
90 return unknownWaveFormat
91 }
92
93 c := h.Chunk
94 if c[0] != 'f' || c[1] != 'm' || c[2] != 't' || c[3] != ' ' {
95 return unknownWaveFormat
96 }
97
98 f := h.Format
99 if f[0] == 'R' && f[1] == 'I' && f[2] == 'F' && f[3] == 'F' {
100 return riffWaveFormat
101 }
102 if f[0] == 'B' && f[1] == 'W' && f[2] == '6' && f[3] == '4' {
103 // return rf64WaveFormat
104 return riffWaveFormat
105 }
106 return unknownWaveFormat
107 }
108
109 var errInvalidWave = errors.New("invalid wave audio format")
110
111 func waveDuration(r io.Reader) (seconds float64, err error) {
112 var h riffHeader
113 err = binary.Read(r, binary.LittleEndian, &h)
114 if err != nil {
115 return math.NaN(), err
116 }
117
118 switch detectWaveType(h) {
119 case riffWaveFormat:
120 return riffWave32Duration(r, h.Size)
121 case rf64WaveFormat:
122 return rf64WaveDuration(r)
123 default:
124 return math.NaN(), errInvalidWave
125 }
126 }
127
128 func riffWave32Duration(r io.Reader, size uint32) (seconds float64, err error) {
129 if size != ^uint32(0) {
130 return _riffWave32Duration(r, uint64(size))
131 }
132
133 // handle case where size is >= 4GB, using a file
134 f, ok := r.(*os.File)
135 if !ok {
136 return math.NaN(), errFileNeeded
137 }
138 st, err := f.Stat()
139 if err != nil {
140 return math.NaN(), err
141 }
142 return _riffWave32Duration(r, uint64(st.Size()))
143 }
144
145 func _riffWave32Duration(r io.Reader, size uint64) (seconds float64, err error) {
146 var h riffWave32Info
147 err = binary.Read(r, binary.LittleEndian, &h)
148 if err != nil {
149 return math.NaN(), err
150 }
151 dlen := uint64(h.DataLength)
152 n := math.Max(float64(size-dlen), float64(h.DataLength))
153 seconds = n / float64(h.BytesPerSecond)
154 return seconds, nil
155 }
156
157 func rf64WaveDuration(r io.Reader) (seconds float64, err error) {
158 _ = r
159 return math.NaN(), errInvalidWave
160 }
File: ./mediainfo/webm.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 mediainfo
26
27 import (
28 "bytes"
29 "io"
30 "math"
31 "strconv"
32 "strings"
33 )
34
35 // youtube, which is the dominant source/standard for webm files, gives string-format durations
36 func webmDuration(r io.Reader) (seconds float64, err error) {
37 buf := make([]byte, 2048) // 2 kb is more than enough to get all DURATIOND fields
38 n, err := r.Read(buf)
39 if err != io.EOF && err != nil {
40 return math.NaN(), err
41 }
42
43 sec := 0.0
44 buf = buf[:n]
45 // since there are 2 DURATIOND fields near the start of youtube webm files,
46 // keep the highest of these to get a more accurate result
47 for {
48 i := bytes.Index(buf, []byte{'D', 'U', 'R', 'A', 'T', 'I', 'O', 'N', 'D'})
49 if i < 0 {
50 break
51 }
52
53 start := i + len("DURATIOND")
54 buf = buf[start:]
55 dur := parseWEBMDurationJunk(buf)
56 sec = math.Max(sec, dur)
57 }
58
59 if sec == 0 && n > 0 {
60 return math.NaN(), nil
61 }
62 return sec, nil
63 }
64
65 func parseWEBMDurationJunk(data []byte) (seconds float64) {
66 // get starting index of duration string
67 start := 0
68 for i, b := range data {
69 if '0' <= b && b <= '9' {
70 start = i
71 break
72 }
73 }
74
75 numpieces := 0
76 dotsAvailable := 1
77 // get limit index of duration string
78 stop := 0
79 for i, b := range data[start:] {
80 if b == ':' {
81 numpieces++
82 continue
83 }
84 if '0' <= b && b <= '9' {
85 continue
86 }
87 if b == '.' && dotsAvailable > 0 {
88 dotsAvailable--
89 continue
90 }
91 stop = start + i
92 break
93 }
94
95 // don't even bother splitting if there aren't any `:`s to separate time fields
96 if numpieces == 0 {
97 return math.NaN()
98 }
99
100 sec := 0.0
101 value := 1.0
102 pieces := strings.Split(string(data[start:stop]), ":")
103 // no need to worry about fields beyond hours, since youtube limits duration to 12 hours
104 // https://support.google.com/youtube/answer/71673?co=GENIE.Platform%3DDesktop&hl=en
105 for i := len(pieces) - 1; i >= 0; i-- {
106 f, err := strconv.ParseFloat(pieces[i], 64)
107 if err != nil {
108 return math.NaN()
109 }
110 sec += value * f
111 value *= 60
112 }
113 return sec
114 }
File: ./mediainfo/webp.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 mediainfo
26
27 import (
28 "encoding/binary"
29 "errors"
30 "io"
31 )
32
33 // https://developers.google.com/speed/webp/docs/webp_lossless_bitstream_specification
34 type webpHeader struct {
35 Signature [4]byte // "RIFF"
36 BlockLength uint32
37 ContainerName [4]byte // "WEBP"
38 ChunkTag [4]byte // "VP8L", or "VP8X", or "VP8 "
39 Extra [8]byte // dunno what to call this field
40 Data [20]byte
41
42 // Info [4]byte // StreamLength uint32
43 // HeaderEnd byte // 0x2f
44 }
45
46 var errInvalidWEBPFormat = errors.New("invalid WEBP format")
47
48 func webpResolution(r io.Reader) (int, int, int, error) {
49 var header webpHeader
50 err := binary.Read(r, binary.LittleEndian, &header)
51 if err != nil {
52 return 0, 0, 0, err
53 }
54 if !webpHeaderIsValid(header) {
55 return 0, 0, 0, errInvalidWEBPFormat
56 }
57
58 // https://github.com/golang/image/blob/master/webp/decode.go
59 switch header.ChunkTag[3] {
60 case 'L':
61 return -1, -1, -1, ErrUnsupportedFormat
62
63 case 'X':
64 b := header.Data
65 width := int(uint32(b[0])|uint32(b[1])<<8|uint32(b[2])<<16) + 1
66 height := int(uint32(b[3])|uint32(b[4])<<8|uint32(b[5])<<16) + 1
67 bpp := -1
68 return width, height, bpp, nil
69
70 case ' ':
71 return -1, -1, -1, ErrUnsupportedFormat
72
73 default:
74 // webpHeaderIsValid should have prevented reaching this point
75 return -1, -1, -1, errInvalidWEBPFormat
76 }
77 }
78
79 func webpHeaderIsValid(h webpHeader) bool {
80 s := h.Signature
81 if s[0] != 'R' || s[1] != 'I' || s[2] != 'F' || s[3] != 'F' {
82 return false
83 }
84 n := h.ContainerName
85 if n[0] != 'W' || n[1] != 'E' || n[2] != 'B' || n[3] != 'P' {
86 return false
87 }
88 t := h.ChunkTag
89 if t[0] != 'V' || t[1] != 'P' || t[2] != '8' || (t[3] != 'L' && t[3] != 'X' && t[3] != ' ') {
90 return false
91 }
92 return true
93 }
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: ./n/n.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 n
26
27 import (
28 "bufio"
29 "errors"
30 "io"
31 "os"
32 "strconv"
33 "strings"
34 )
35
36 const info = `
37 n [options...] [start...] [filenames...]
38
39 Number lines starting from the (optional) line-count given, or starting to
40 count from 1 by default. Line counts and line contents are separated by a
41 tab.
42
43 The options are, available both in single and double-dash versions
44
45 -h, -help show this help message
46 `
47
48 type config struct {
49 n int
50 liveLines bool
51 }
52
53 func Main() {
54 var cfg config
55 cfg.n = 1
56 cfg.liveLines = true
57 args := os.Args[1:]
58
59 for len(args) > 0 {
60 switch args[0] {
61 case `-b`, `--b`, `-buffered`, `--buffered`:
62 cfg.liveLines = false
63 args = args[1:]
64 continue
65
66 case `-h`, `--h`, `-help`, `--help`:
67 os.Stdout.WriteString(info[1:])
68 return
69 }
70
71 break
72 }
73
74 if len(args) > 0 {
75 if v, err := strconv.ParseInt(args[0], 10, 64); err == nil {
76 cfg.n = int(v)
77 args = args[1:]
78 }
79 }
80
81 if len(args) > 0 && args[0] == `--` {
82 args = args[1:]
83 }
84
85 if cfg.liveLines {
86 if _, err := os.Stdout.Seek(0, io.SeekCurrent); err == nil {
87 cfg.liveLines = false
88 }
89 }
90
91 if err := run(args, &cfg); err != nil && err != io.EOF {
92 os.Stderr.WriteString(err.Error())
93 os.Stderr.WriteString("\n")
94 os.Exit(1)
95 }
96 }
97
98 func run(paths []string, cfg *config) error {
99 bw := bufio.NewWriterSize(os.Stdout, 32*1024)
100 defer bw.Flush()
101
102 for _, p := range paths {
103 if err := handleFile(bw, p, cfg); err != nil {
104 return err
105 }
106 }
107
108 if len(paths) == 0 {
109 return handleReader(bw, os.Stdin, cfg)
110 }
111
112 return nil
113 }
114
115 func handleFile(w *bufio.Writer, path string, cfg *config) error {
116 f, err := os.Open(path)
117 if err != nil {
118 // on windows, file-not-found error messages may mention `CreateFile`,
119 // even when trying to open files in read-only mode
120 return errors.New(`can't open file named ` + path)
121 }
122 defer f.Close()
123 return handleReader(w, f, cfg)
124 }
125
126 func handleReader(w *bufio.Writer, r io.Reader, cfg *config) error {
127 var buf [24]byte
128 const gb = 1024 * 1024 * 1024
129 sc := bufio.NewScanner(r)
130 sc.Buffer(nil, 8*gb)
131
132 for i := 0; sc.Scan(); i++ {
133 s := sc.Text()
134 if i == 0 && strings.HasPrefix(s, "\xef\xbb\xbf") {
135 s = s[3:]
136 }
137
138 w.Write(strconv.AppendInt(buf[:0], int64(cfg.n), 10))
139 w.WriteByte('\t')
140 w.WriteString(s)
141 cfg.n++
142
143 if w.WriteByte('\n') != nil {
144 return io.EOF
145 }
146
147 if !cfg.liveLines {
148 continue
149 }
150
151 if err := w.Flush(); err != nil {
152 return io.EOF
153 }
154 }
155
156 return sc.Err()
157 }
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 == io.EOF {
305 return errors.New(`end of JSON before array was closed`)
306 }
307 if err != nil {
308 return err
309 }
310
311 if t == json.Delim(']') {
312 return nil
313 }
314
315 err = handleToken(w, dec, t, path)
316 if err != nil {
317 return err
318 }
319 }
320
321 // make the compiler happy
322 return nil
323 }
324
325 // handleObject handles objects for func handleToken
326 func handleObject(w *bufio.Writer, dec *json.Decoder, path []any) error {
327 config.path(w, path)
328 w.WriteString(config.objectDecl)
329 if err := endLine(w); err != nil {
330 return err
331 }
332
333 path = append(path, ``)
334 last := len(path) - 1
335
336 for i := 0; true; i++ {
337 t, err := dec.Token()
338 if err == io.EOF {
339 return errors.New(`end of JSON before object was closed`)
340 }
341 if err != nil {
342 return err
343 }
344
345 if t == json.Delim('}') {
346 return nil
347 }
348
349 k, ok := t.(string)
350 if !ok {
351 return errors.New(`expected a string for a key-value pair`)
352 }
353
354 path[last] = k
355 if err != nil {
356 return err
357 }
358
359 t, err = dec.Token()
360 if err == io.EOF {
361 return errors.New(`expected a value for a key-value pair`)
362 }
363
364 err = handleToken(w, dec, t, path)
365 if err != nil {
366 return err
367 }
368 }
369
370 // make the compiler happy
371 return nil
372 }
373
374 func monoPath(w *bufio.Writer, path []any) error {
375 var buf [24]byte
376
377 w.WriteString(`json`)
378
379 for _, v := range path {
380 switch v := v.(type) {
381 case int:
382 w.WriteByte('[')
383 w.Write(strconv.AppendInt(buf[:0], int64(v), 10))
384 w.WriteByte(']')
385
386 case string:
387 if !needsEscaping(v) {
388 w.WriteByte('.')
389 w.WriteString(v)
390 continue
391 }
392 w.WriteByte('[')
393 monoString(w, v)
394 w.WriteByte(']')
395 }
396 }
397
398 w.WriteString(` = `)
399 return nil
400 }
401
402 func monoNull(w *bufio.Writer) error {
403 w.WriteString(`null`)
404 return nil
405 }
406
407 func monoBool(w *bufio.Writer, b bool) error {
408 if b {
409 w.WriteString(`true`)
410 } else {
411 w.WriteString(`false`)
412 }
413 return nil
414 }
415
416 func monoNumber(w *bufio.Writer, n json.Number) error {
417 w.WriteString(n.String())
418 return nil
419 }
420
421 func monoString(w *bufio.Writer, s string) error {
422 w.WriteByte('"')
423 for i := range s {
424 w.Write(escapedStringBytes[s[i]])
425 }
426 w.WriteByte('"')
427 return nil
428 }
429
430 func styledPath(w *bufio.Writer, path []any) error {
431 var buf [24]byte
432
433 w.WriteString("\x1b[38;2;135;95;255mjson\x1b[0m")
434
435 for _, v := range path {
436 switch v := v.(type) {
437 case int:
438 w.WriteString("\x1b[38;2;168;168;168m[")
439 w.WriteString("\x1b[38;2;0;135;95m")
440 w.Write(strconv.AppendInt(buf[:0], int64(v), 10))
441 w.WriteString("\x1b[38;2;168;168;168m]\x1b[0m")
442
443 case string:
444 if !needsEscaping(v) {
445 w.WriteString("\x1b[38;2;168;168;168m.")
446 w.WriteString("\x1b[38;2;135;95;255m")
447 w.WriteString(v)
448 w.WriteString("\x1b[0m")
449 continue
450 }
451
452 w.WriteString("\x1b[38;2;168;168;168m[")
453 styledString(w, v)
454 w.WriteString("\x1b[38;2;168;168;168m]\x1b[0m")
455 }
456 }
457
458 w.WriteString(" \x1b[38;2;168;168;168m=\x1b[0m ")
459 return nil
460 }
461
462 func styledNull(w *bufio.Writer) error {
463 w.WriteString("\x1b[38;2;168;168;168m")
464 w.WriteString(`null`)
465 w.WriteString("\x1b[0m")
466 return nil
467 }
468
469 func styledBool(w *bufio.Writer, b bool) error {
470 if b {
471 w.WriteString("\x1b[38;2;95;175;215mtrue\x1b[0m")
472 } else {
473 w.WriteString("\x1b[38;2;95;175;215mfalse\x1b[0m")
474 }
475 return nil
476 }
477
478 func styledNumber(w *bufio.Writer, n json.Number) error {
479 w.WriteString("\x1b[38;2;0;135;95m")
480 w.WriteString(n.String())
481 w.WriteString("\x1b[0m")
482 return nil
483 }
484
485 func styledString(w *bufio.Writer, s string) error {
486 w.WriteString("\x1b[38;2;168;168;168m\"\x1b[0m")
487 for i := range s {
488 w.Write(escapedStringBytes[s[i]])
489 }
490 w.WriteString("\x1b[38;2;168;168;168m\"\x1b[0m")
491 return nil
492 }
493
494 func needsEscaping(s string) bool {
495 for _, r := range s {
496 if r < ' ' || r > '~' {
497 return true
498 }
499
500 switch r {
501 case '"', '\'', '\\':
502 return true
503 }
504 }
505
506 return false
507 }
508
509 func endLine(w *bufio.Writer) error {
510 w.WriteByte(';')
511 if err := w.WriteByte('\n'); err == nil {
512 return nil
513 }
514 return io.EOF
515 }
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 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 == io.EOF {
221 return errors.New(`end of JSON before array was closed`)
222 }
223 if err != nil {
224 return err
225 }
226
227 if t == json.Delim(']') {
228 if i == 0 {
229 writeSpaces(w, indent*pre)
230 w.WriteString(syntaxStyle + "[]\x1b[0m")
231 } else {
232 w.WriteString("\n")
233 writeSpaces(w, indent*level)
234 w.WriteString(syntaxStyle + "]\x1b[0m")
235 }
236 return nil
237 }
238
239 if i == 0 {
240 writeSpaces(w, indent*pre)
241 w.WriteString(syntaxStyle + "[\x1b[0m\n")
242 } else {
243 // this is a good spot to check for early-quit opportunities
244 w.WriteString(syntaxStyle + ",\x1b[0m\n")
245 if err := w.Flush(); err != nil {
246 // a write error may be the consequence of stdout being closed,
247 // perhaps by another app along a pipe
248 return io.EOF
249 }
250 }
251
252 if err := handleToken(w, d, t, level+1, level+1); err != nil {
253 return err
254 }
255 }
256
257 // make the compiler happy
258 return nil
259 }
260
261 func handleBoolean(w *bufio.Writer, b bool, pre int) error {
262 writeSpaces(w, indent*pre)
263 if b {
264 w.WriteString(boolStyle + "true\x1b[0m")
265 } else {
266 w.WriteString(boolStyle + "false\x1b[0m")
267 }
268 return nil
269 }
270
271 func handleKey(w *bufio.Writer, s string, pre int) error {
272 writeSpaces(w, indent*pre)
273 w.WriteString(syntaxStyle + "\"\x1b[0m" + keyStyle)
274 w.WriteString(s)
275 w.WriteString(syntaxStyle + "\":\x1b[0m ")
276 return nil
277 }
278
279 func handleNull(w *bufio.Writer, pre int) error {
280 writeSpaces(w, indent*pre)
281 w.WriteString(nullStyle + "null\x1b[0m")
282 return nil
283 }
284
285 // func handleNumber(w *bufio.Writer, n json.Number, pre int) error {
286 // writeSpaces(w, indent*pre)
287 // w.WriteString(numberStyle)
288 // w.WriteString(n.String())
289 // w.WriteString("\x1b[0m")
290 // return nil
291 // }
292
293 func handleNumber(w *bufio.Writer, n json.Number, pre int) error {
294 writeSpaces(w, indent*pre)
295 f, _ := n.Float64()
296 if f > 0 {
297 w.WriteString(positiveNumberStyle)
298 } else if f < 0 {
299 w.WriteString(negativeNumberStyle)
300 } else {
301 w.WriteString(zeroNumberStyle)
302 }
303 w.WriteString(n.String())
304 w.WriteString("\x1b[0m")
305 return nil
306 }
307
308 func handleObject(w *bufio.Writer, d *json.Decoder, pre, level int) error {
309 for i := 0; true; i++ {
310 t, err := d.Token()
311 if err == io.EOF {
312 return errors.New(`end of JSON before object was closed`)
313 }
314 if err != nil {
315 return err
316 }
317
318 if t == json.Delim('}') {
319 if i == 0 {
320 writeSpaces(w, indent*pre)
321 w.WriteString(syntaxStyle + "{}\x1b[0m")
322 } else {
323 w.WriteString("\n")
324 writeSpaces(w, indent*level)
325 w.WriteString(syntaxStyle + "}\x1b[0m")
326 }
327 return nil
328 }
329
330 if i == 0 {
331 writeSpaces(w, indent*pre)
332 w.WriteString(syntaxStyle + "{\x1b[0m\n")
333 } else {
334 // this is a good spot to check for early-quit opportunities
335 w.WriteString(syntaxStyle + ",\x1b[0m\n")
336 if err := w.Flush(); err != nil {
337 // a write error may be the consequence of stdout being closed,
338 // perhaps by another app along a pipe
339 return io.EOF
340 }
341 }
342
343 // the stdlib's JSON parser is supposed to complain about non-string
344 // keys anyway, but make sure just in case
345 k, ok := t.(string)
346 if !ok {
347 return errors.New(`expected key to be a string`)
348 }
349 if err := handleKey(w, k, level+1); err != nil {
350 return err
351 }
352
353 // handle value
354 t, err = d.Token()
355 if err != nil {
356 return err
357 }
358 if err := handleToken(w, d, t, 0, level+1); err != nil {
359 return err
360 }
361 }
362
363 // make the compiler happy
364 return nil
365 }
366
367 func needsEscaping(s string) bool {
368 for _, r := range s {
369 switch r {
370 case '"', '\\', '\t', '\r', '\n':
371 return true
372 }
373 }
374 return false
375 }
376
377 func handleString(w *bufio.Writer, s string, pre int) error {
378 writeSpaces(w, indent*pre)
379 w.WriteString(syntaxStyle + "\"\x1b[0m" + stringStyle)
380 if !needsEscaping(s) {
381 w.WriteString(s)
382 } else {
383 escapeString(w, s)
384 }
385 w.WriteString(syntaxStyle + "\"\x1b[0m")
386 return nil
387 }
388
389 func escapeString(w *bufio.Writer, s string) {
390 for _, r := range s {
391 switch r {
392 case '"', '\\':
393 w.WriteByte('\\')
394 w.WriteRune(r)
395 case '\t':
396 w.WriteByte('\\')
397 w.WriteByte('t')
398 case '\r':
399 w.WriteByte('\\')
400 w.WriteByte('r')
401 case '\n':
402 w.WriteByte('\\')
403 w.WriteByte('n')
404 default:
405 w.WriteRune(r)
406 }
407 }
408 }
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: ./now/info.txt
1 now [options...] [timezones/places...]
2
3 Show the current date and time for the places given, along with the local
4 date/time.
5
6 All (optional) leading options start with either single or double-dash:
7
8 -h, -help show this help message
File: ./now/lookup.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 now
26
27 import (
28 "strings"
29 "time"
30 )
31
32 var aliases = map[string]string{
33 // `istanbul`: `Asia/Istanbul`,
34 `istanbul`: `Europe/Istanbul`,
35
36 // `nicosia`: `Asia/Nicosia`,
37 `nicosia`: `Europe/Nicosia`,
38
39 `africa/abidjan`: `Africa/Abidjan`,
40 `africa/accra`: `Africa/Accra`,
41 `africa/addis ababa`: `Africa/Addis_Ababa`,
42 `africa/algiers`: `Africa/Algiers`,
43 `africa/asmara`: `Africa/Asmara`,
44 `africa/bamako`: `Africa/Bamako`,
45 `africa/bangui`: `Africa/Bangui`,
46 `africa/banjul`: `Africa/Banjul`,
47 `africa/bissau`: `Africa/Bissau`,
48 `africa/blantyre`: `Africa/Blantyre`,
49 `africa/brazzaville`: `Africa/Brazzaville`,
50 `africa/bujumbura`: `Africa/Bujumbura`,
51 `africa/cairo`: `Africa/Cairo`,
52 `africa/casablanca`: `Africa/Casablanca`,
53 `africa/ceuta`: `Africa/Ceuta`,
54 `africa/conakry`: `Africa/Conakry`,
55 `africa/dakar`: `Africa/Dakar`,
56 `africa/dar es salaam`: `Africa/Dar_es_Salaam`,
57 `africa/djibouti`: `Africa/Djibouti`,
58 `africa/douala`: `Africa/Douala`,
59 `africa/el aaiun`: `Africa/El_Aaiun`,
60 `africa/freetown`: `Africa/Freetown`,
61 `africa/gaborone`: `Africa/Gaborone`,
62 `africa/harare`: `Africa/Harare`,
63 `africa/johannesburg`: `Africa/Johannesburg`,
64 `africa/juba`: `Africa/Juba`,
65 `africa/kampala`: `Africa/Kampala`,
66 `africa/khartoum`: `Africa/Khartoum`,
67 `africa/kigali`: `Africa/Kigali`,
68 `africa/kinshasa`: `Africa/Kinshasa`,
69 `africa/lagos`: `Africa/Lagos`,
70 `africa/libreville`: `Africa/Libreville`,
71 `africa/lome`: `Africa/Lome`,
72 `africa/luanda`: `Africa/Luanda`,
73 `africa/lubumbashi`: `Africa/Lubumbashi`,
74 `africa/lusaka`: `Africa/Lusaka`,
75 `africa/malabo`: `Africa/Malabo`,
76 `africa/maputo`: `Africa/Maputo`,
77 `africa/maseru`: `Africa/Maseru`,
78 `africa/mbabane`: `Africa/Mbabane`,
79 `africa/mogadishu`: `Africa/Mogadishu`,
80 `africa/monrovia`: `Africa/Monrovia`,
81 `africa/nairobi`: `Africa/Nairobi`,
82 `africa/ndjamena`: `Africa/Ndjamena`,
83 `africa/niamey`: `Africa/Niamey`,
84 `africa/nouakchott`: `Africa/Nouakchott`,
85 `africa/ouagadougou`: `Africa/Ouagadougou`,
86 `africa/porto-novo`: `Africa/Porto-Novo`,
87 `africa/sao tome`: `Africa/Sao_Tome`,
88 `africa/timbuktu`: `Africa/Timbuktu`,
89 `africa/tripoli`: `Africa/Tripoli`,
90 `africa/tunis`: `Africa/Tunis`,
91 `africa/windhoek`: `Africa/Windhoek`,
92 `america/adak`: `America/Adak`,
93 `america/anchorage`: `America/Anchorage`,
94 `america/anguilla`: `America/Anguilla`,
95 `america/antigua`: `America/Antigua`,
96 `america/araguaina`: `America/Araguaina`,
97 `america/argentina/buenos aires`: `America/Argentina/Buenos_Aires`,
98 `america/argentina/catamarca`: `America/Argentina/Catamarca`,
99 `america/argentina/cordoba`: `America/Argentina/Cordoba`,
100 `america/argentina/jujuy`: `America/Argentina/Jujuy`,
101 `america/argentina/la rioja`: `America/Argentina/La_Rioja`,
102 `america/argentina/mendoza`: `America/Argentina/Mendoza`,
103 `america/argentina/rio gallegos`: `America/Argentina/Rio_Gallegos`,
104 `america/argentina/salta`: `America/Argentina/Salta`,
105 `america/argentina/san juan`: `America/Argentina/San_Juan`,
106 `america/argentina/san luis`: `America/Argentina/San_Luis`,
107 `america/argentina/tucuman`: `America/Argentina/Tucuman`,
108 `america/argentina/ushuaia`: `America/Argentina/Ushuaia`,
109 `america/aruba`: `America/Aruba`,
110 `america/asuncion`: `America/Asuncion`,
111 `america/atikokan`: `America/Atikokan`,
112 `america/atka`: `America/Atka`,
113 `america/bahia`: `America/Bahia`,
114 `america/bahia banderas`: `America/Bahia_Banderas`,
115 `america/barbados`: `America/Barbados`,
116 `america/belem`: `America/Belem`,
117 `america/belize`: `America/Belize`,
118 `america/blanc-sablon`: `America/Blanc-Sablon`,
119 `america/boa vista`: `America/Boa_Vista`,
120 `america/bogota`: `America/Bogota`,
121 `america/boise`: `America/Boise`,
122 `america/cambridge bay`: `America/Cambridge_Bay`,
123 `america/campo grande`: `America/Campo_Grande`,
124 `america/cancun`: `America/Cancun`,
125 `america/caracas`: `America/Caracas`,
126 `america/cayenne`: `America/Cayenne`,
127 `america/cayman`: `America/Cayman`,
128 `america/chicago`: `America/Chicago`,
129 `america/chihuahua`: `America/Chihuahua`,
130 `america/ciudad juarez`: `America/Ciudad_Juarez`,
131 `america/coral harbour`: `America/Coral_Harbour`,
132 `america/costa rica`: `America/Costa_Rica`,
133 `america/coyhaique`: `America/Coyhaique`,
134 `america/creston`: `America/Creston`,
135 `america/cuiaba`: `America/Cuiaba`,
136 `america/curacao`: `America/Curacao`,
137 `america/danmarkshavn`: `America/Danmarkshavn`,
138 `america/dawson`: `America/Dawson`,
139 `america/dawson creek`: `America/Dawson_Creek`,
140 `america/denver`: `America/Denver`,
141 `america/detroit`: `America/Detroit`,
142 `america/dominica`: `America/Dominica`,
143 `america/edmonton`: `America/Edmonton`,
144 `america/eirunepe`: `America/Eirunepe`,
145 `america/el salvador`: `America/El_Salvador`,
146 `america/ensenada`: `America/Ensenada`,
147 `america/fort nelson`: `America/Fort_Nelson`,
148 `america/fortaleza`: `America/Fortaleza`,
149 `america/glace bay`: `America/Glace_Bay`,
150 `america/goose bay`: `America/Goose_Bay`,
151 `america/grand turk`: `America/Grand_Turk`,
152 `america/grenada`: `America/Grenada`,
153 `america/guadeloupe`: `America/Guadeloupe`,
154 `america/guatemala`: `America/Guatemala`,
155 `america/guayaquil`: `America/Guayaquil`,
156 `america/guyana`: `America/Guyana`,
157 `america/halifax`: `America/Halifax`,
158 `america/havana`: `America/Havana`,
159 `america/hermosillo`: `America/Hermosillo`,
160 `america/indiana/indianapolis`: `America/Indiana/Indianapolis`,
161 `america/indiana/knox`: `America/Indiana/Knox`,
162 `america/indiana/marengo`: `America/Indiana/Marengo`,
163 `america/indiana/petersburg`: `America/Indiana/Petersburg`,
164 `america/indiana/tell city`: `America/Indiana/Tell_City`,
165 `america/indiana/vevay`: `America/Indiana/Vevay`,
166 `america/indiana/vincennes`: `America/Indiana/Vincennes`,
167 `america/indiana/winamac`: `America/Indiana/Winamac`,
168 `america/inuvik`: `America/Inuvik`,
169 `america/iqaluit`: `America/Iqaluit`,
170 `america/jamaica`: `America/Jamaica`,
171 `america/juneau`: `America/Juneau`,
172 `america/kentucky/louisville`: `America/Kentucky/Louisville`,
173 `america/kentucky/monticello`: `America/Kentucky/Monticello`,
174 `america/kralendijk`: `America/Kralendijk`,
175 `america/la paz`: `America/La_Paz`,
176 `america/lima`: `America/Lima`,
177 `america/los angeles`: `America/Los_Angeles`,
178 `america/lower princes`: `America/Lower_Princes`,
179 `america/maceio`: `America/Maceio`,
180 `america/managua`: `America/Managua`,
181 `america/manaus`: `America/Manaus`,
182 `america/marigot`: `America/Marigot`,
183 `america/martinique`: `America/Martinique`,
184 `america/matamoros`: `America/Matamoros`,
185 `america/mazatlan`: `America/Mazatlan`,
186 `america/menominee`: `America/Menominee`,
187 `america/merida`: `America/Merida`,
188 `america/metlakatla`: `America/Metlakatla`,
189 `america/mexico city`: `America/Mexico_City`,
190 `america/miquelon`: `America/Miquelon`,
191 `america/moncton`: `America/Moncton`,
192 `america/monterrey`: `America/Monterrey`,
193 `america/montevideo`: `America/Montevideo`,
194 `america/montreal`: `America/Montreal`,
195 `america/montserrat`: `America/Montserrat`,
196 `america/nassau`: `America/Nassau`,
197 `america/new york`: `America/New_York`,
198 `america/nipigon`: `America/Nipigon`,
199 `america/nome`: `America/Nome`,
200 `america/noronha`: `America/Noronha`,
201 `america/north dakota/beulah`: `America/North_Dakota/Beulah`,
202 `america/north dakota/center`: `America/North_Dakota/Center`,
203 `america/north dakota/new salem`: `America/North_Dakota/New_Salem`,
204 `america/nuuk`: `America/Nuuk`,
205 `america/ojinaga`: `America/Ojinaga`,
206 `america/panama`: `America/Panama`,
207 `america/pangnirtung`: `America/Pangnirtung`,
208 `america/paramaribo`: `America/Paramaribo`,
209 `america/phoenix`: `America/Phoenix`,
210 `america/port-au-prince`: `America/Port-au-Prince`,
211 `america/port of spain`: `America/Port_of_Spain`,
212 `america/porto acre`: `America/Porto_Acre`,
213 `america/porto velho`: `America/Porto_Velho`,
214 `america/puerto rico`: `America/Puerto_Rico`,
215 `america/punta arenas`: `America/Punta_Arenas`,
216 `america/rainy river`: `America/Rainy_River`,
217 `america/rankin inlet`: `America/Rankin_Inlet`,
218 `america/recife`: `America/Recife`,
219 `america/regina`: `America/Regina`,
220 `america/resolute`: `America/Resolute`,
221 `america/rio branco`: `America/Rio_Branco`,
222 `america/santa isabel`: `America/Santa_Isabel`,
223 `america/santarem`: `America/Santarem`,
224 `america/santiago`: `America/Santiago`,
225 `america/santo domingo`: `America/Santo_Domingo`,
226 `america/sao paulo`: `America/Sao_Paulo`,
227 `america/scoresbysund`: `America/Scoresbysund`,
228 `america/shiprock`: `America/Shiprock`,
229 `america/sitka`: `America/Sitka`,
230 `america/st barthelemy`: `America/St_Barthelemy`,
231 `america/st johns`: `America/St_Johns`,
232 `america/st kitts`: `America/St_Kitts`,
233 `america/st lucia`: `America/St_Lucia`,
234 `america/st thomas`: `America/St_Thomas`,
235 `america/st vincent`: `America/St_Vincent`,
236 `america/swift current`: `America/Swift_Current`,
237 `america/tegucigalpa`: `America/Tegucigalpa`,
238 `america/thule`: `America/Thule`,
239 `america/thunder bay`: `America/Thunder_Bay`,
240 `america/tijuana`: `America/Tijuana`,
241 `america/toronto`: `America/Toronto`,
242 `america/tortola`: `America/Tortola`,
243 `america/vancouver`: `America/Vancouver`,
244 `america/virgin`: `America/Virgin`,
245 `america/whitehorse`: `America/Whitehorse`,
246 `america/winnipeg`: `America/Winnipeg`,
247 `america/yakutat`: `America/Yakutat`,
248 `america/yellowknife`: `America/Yellowknife`,
249 `antarctica/casey`: `Antarctica/Casey`,
250 `antarctica/davis`: `Antarctica/Davis`,
251 `antarctica/dumontdurville`: `Antarctica/DumontDUrville`,
252 `antarctica/macquarie`: `Antarctica/Macquarie`,
253 `antarctica/mawson`: `Antarctica/Mawson`,
254 `antarctica/mcmurdo`: `Antarctica/McMurdo`,
255 `antarctica/palmer`: `Antarctica/Palmer`,
256 `antarctica/rothera`: `Antarctica/Rothera`,
257 `antarctica/syowa`: `Antarctica/Syowa`,
258 `antarctica/troll`: `Antarctica/Troll`,
259 `antarctica/vostok`: `Antarctica/Vostok`,
260 `arctic/longyearbyen`: `Arctic/Longyearbyen`,
261 `asia/aden`: `Asia/Aden`,
262 `asia/almaty`: `Asia/Almaty`,
263 `asia/amman`: `Asia/Amman`,
264 `asia/anadyr`: `Asia/Anadyr`,
265 `asia/aqtau`: `Asia/Aqtau`,
266 `asia/aqtobe`: `Asia/Aqtobe`,
267 `asia/ashgabat`: `Asia/Ashgabat`,
268 `asia/atyrau`: `Asia/Atyrau`,
269 `asia/baghdad`: `Asia/Baghdad`,
270 `asia/bahrain`: `Asia/Bahrain`,
271 `asia/baku`: `Asia/Baku`,
272 `asia/bangkok`: `Asia/Bangkok`,
273 `asia/barnaul`: `Asia/Barnaul`,
274 `asia/beirut`: `Asia/Beirut`,
275 `asia/bishkek`: `Asia/Bishkek`,
276 `asia/brunei`: `Asia/Brunei`,
277 `asia/chita`: `Asia/Chita`,
278 `asia/chongqing`: `Asia/Chongqing`,
279 `asia/colombo`: `Asia/Colombo`,
280 `asia/damascus`: `Asia/Damascus`,
281 `asia/dhaka`: `Asia/Dhaka`,
282 `asia/dili`: `Asia/Dili`,
283 `asia/dubai`: `Asia/Dubai`,
284 `asia/dushanbe`: `Asia/Dushanbe`,
285 `asia/famagusta`: `Asia/Famagusta`,
286 `asia/gaza`: `Asia/Gaza`,
287 `asia/harbin`: `Asia/Harbin`,
288 `asia/hebron`: `Asia/Hebron`,
289 `asia/ho chi minh`: `Asia/Ho_Chi_Minh`,
290 `asia/hong kong`: `Asia/Hong_Kong`,
291 `asia/hovd`: `Asia/Hovd`,
292 `asia/irkutsk`: `Asia/Irkutsk`,
293 `asia/istanbul`: `Asia/Istanbul`,
294 `asia/jakarta`: `Asia/Jakarta`,
295 `asia/jayapura`: `Asia/Jayapura`,
296 `asia/jerusalem`: `Asia/Jerusalem`,
297 `asia/kabul`: `Asia/Kabul`,
298 `asia/kamchatka`: `Asia/Kamchatka`,
299 `asia/karachi`: `Asia/Karachi`,
300 `asia/kashgar`: `Asia/Kashgar`,
301 `asia/kathmandu`: `Asia/Kathmandu`,
302 `asia/khandyga`: `Asia/Khandyga`,
303 `asia/kolkata`: `Asia/Kolkata`,
304 `asia/krasnoyarsk`: `Asia/Krasnoyarsk`,
305 `asia/kuala lumpur`: `Asia/Kuala_Lumpur`,
306 `asia/kuching`: `Asia/Kuching`,
307 `asia/kuwait`: `Asia/Kuwait`,
308 `asia/macau`: `Asia/Macau`,
309 `asia/magadan`: `Asia/Magadan`,
310 `asia/makassar`: `Asia/Makassar`,
311 `asia/manila`: `Asia/Manila`,
312 `asia/muscat`: `Asia/Muscat`,
313 `asia/nicosia`: `Asia/Nicosia`,
314 `asia/novokuznetsk`: `Asia/Novokuznetsk`,
315 `asia/novosibirsk`: `Asia/Novosibirsk`,
316 `asia/omsk`: `Asia/Omsk`,
317 `asia/oral`: `Asia/Oral`,
318 `asia/phnom penh`: `Asia/Phnom_Penh`,
319 `asia/pontianak`: `Asia/Pontianak`,
320 `asia/pyongyang`: `Asia/Pyongyang`,
321 `asia/qatar`: `Asia/Qatar`,
322 `asia/qostanay`: `Asia/Qostanay`,
323 `asia/qyzylorda`: `Asia/Qyzylorda`,
324 `asia/riyadh`: `Asia/Riyadh`,
325 `asia/sakhalin`: `Asia/Sakhalin`,
326 `asia/samarkand`: `Asia/Samarkand`,
327 `asia/seoul`: `Asia/Seoul`,
328 `asia/shanghai`: `Asia/Shanghai`,
329 `asia/singapore`: `Asia/Singapore`,
330 `asia/srednekolymsk`: `Asia/Srednekolymsk`,
331 `asia/taipei`: `Asia/Taipei`,
332 `asia/tashkent`: `Asia/Tashkent`,
333 `asia/tbilisi`: `Asia/Tbilisi`,
334 `asia/tehran`: `Asia/Tehran`,
335 `asia/tel aviv`: `Asia/Tel_Aviv`,
336 `asia/thimphu`: `Asia/Thimphu`,
337 `asia/tokyo`: `Asia/Tokyo`,
338 `asia/tomsk`: `Asia/Tomsk`,
339 `asia/ulaanbaatar`: `Asia/Ulaanbaatar`,
340 `asia/urumqi`: `Asia/Urumqi`,
341 `asia/ust-nera`: `Asia/Ust-Nera`,
342 `asia/vientiane`: `Asia/Vientiane`,
343 `asia/vladivostok`: `Asia/Vladivostok`,
344 `asia/yakutsk`: `Asia/Yakutsk`,
345 `asia/yangon`: `Asia/Yangon`,
346 `asia/yekaterinburg`: `Asia/Yekaterinburg`,
347 `asia/yerevan`: `Asia/Yerevan`,
348 `atlantic/azores`: `Atlantic/Azores`,
349 `atlantic/bermuda`: `Atlantic/Bermuda`,
350 `atlantic/canary`: `Atlantic/Canary`,
351 `atlantic/cape verde`: `Atlantic/Cape_Verde`,
352 `atlantic/faroe`: `Atlantic/Faroe`,
353 `atlantic/jan mayen`: `Atlantic/Jan_Mayen`,
354 `atlantic/madeira`: `Atlantic/Madeira`,
355 `atlantic/reykjavik`: `Atlantic/Reykjavik`,
356 `atlantic/south georgia`: `Atlantic/South_Georgia`,
357 `atlantic/st helena`: `Atlantic/St_Helena`,
358 `atlantic/stanley`: `Atlantic/Stanley`,
359 `australia/adelaide`: `Australia/Adelaide`,
360 `australia/brisbane`: `Australia/Brisbane`,
361 `australia/broken hill`: `Australia/Broken_Hill`,
362 `australia/canberra`: `Australia/Canberra`,
363 `australia/currie`: `Australia/Currie`,
364 `australia/darwin`: `Australia/Darwin`,
365 `australia/eucla`: `Australia/Eucla`,
366 `australia/hobart`: `Australia/Hobart`,
367 `australia/lindeman`: `Australia/Lindeman`,
368 `australia/lord howe`: `Australia/Lord_Howe`,
369 `australia/melbourne`: `Australia/Melbourne`,
370 `australia/perth`: `Australia/Perth`,
371 `australia/sydney`: `Australia/Sydney`,
372 `australia/yancowinna`: `Australia/Yancowinna`,
373 `etc/greenwich`: `Etc/Greenwich`,
374 `etc/uct`: `Etc/UCT`,
375 `etc/utc`: `Etc/UTC`,
376 `etc/universal`: `Etc/Universal`,
377 `etc/zulu`: `Etc/Zulu`,
378 `europe/amsterdam`: `Europe/Amsterdam`,
379 `europe/andorra`: `Europe/Andorra`,
380 `europe/astrakhan`: `Europe/Astrakhan`,
381 `europe/athens`: `Europe/Athens`,
382 `europe/belfast`: `Europe/Belfast`,
383 `europe/belgrade`: `Europe/Belgrade`,
384 `europe/berlin`: `Europe/Berlin`,
385 `europe/bratislava`: `Europe/Bratislava`,
386 `europe/brussels`: `Europe/Brussels`,
387 `europe/bucharest`: `Europe/Bucharest`,
388 `europe/budapest`: `Europe/Budapest`,
389 `europe/busingen`: `Europe/Busingen`,
390 `europe/chisinau`: `Europe/Chisinau`,
391 `europe/copenhagen`: `Europe/Copenhagen`,
392 `europe/dublin`: `Europe/Dublin`,
393 `europe/gibraltar`: `Europe/Gibraltar`,
394 `europe/guernsey`: `Europe/Guernsey`,
395 `europe/helsinki`: `Europe/Helsinki`,
396 `europe/isle of man`: `Europe/Isle_of_Man`,
397 `europe/istanbul`: `Europe/Istanbul`,
398 `europe/jersey`: `Europe/Jersey`,
399 `europe/kaliningrad`: `Europe/Kaliningrad`,
400 `europe/kirov`: `Europe/Kirov`,
401 `europe/kyiv`: `Europe/Kyiv`,
402 `europe/lisbon`: `Europe/Lisbon`,
403 `europe/ljubljana`: `Europe/Ljubljana`,
404 `europe/london`: `Europe/London`,
405 `europe/luxembourg`: `Europe/Luxembourg`,
406 `europe/madrid`: `Europe/Madrid`,
407 `europe/malta`: `Europe/Malta`,
408 `europe/mariehamn`: `Europe/Mariehamn`,
409 `europe/minsk`: `Europe/Minsk`,
410 `europe/monaco`: `Europe/Monaco`,
411 `europe/moscow`: `Europe/Moscow`,
412 `europe/nicosia`: `Europe/Nicosia`,
413 `europe/oslo`: `Europe/Oslo`,
414 `europe/paris`: `Europe/Paris`,
415 `europe/podgorica`: `Europe/Podgorica`,
416 `europe/prague`: `Europe/Prague`,
417 `europe/riga`: `Europe/Riga`,
418 `europe/rome`: `Europe/Rome`,
419 `europe/samara`: `Europe/Samara`,
420 `europe/san marino`: `Europe/San_Marino`,
421 `europe/sarajevo`: `Europe/Sarajevo`,
422 `europe/saratov`: `Europe/Saratov`,
423 `europe/simferopol`: `Europe/Simferopol`,
424 `europe/skopje`: `Europe/Skopje`,
425 `europe/sofia`: `Europe/Sofia`,
426 `europe/stockholm`: `Europe/Stockholm`,
427 `europe/tallinn`: `Europe/Tallinn`,
428 `europe/tirane`: `Europe/Tirane`,
429 `europe/tiraspol`: `Europe/Tiraspol`,
430 `europe/ulyanovsk`: `Europe/Ulyanovsk`,
431 `europe/vaduz`: `Europe/Vaduz`,
432 `europe/vatican`: `Europe/Vatican`,
433 `europe/vienna`: `Europe/Vienna`,
434 `europe/vilnius`: `Europe/Vilnius`,
435 `europe/volgograd`: `Europe/Volgograd`,
436 `europe/warsaw`: `Europe/Warsaw`,
437 `europe/zagreb`: `Europe/Zagreb`,
438 `europe/zurich`: `Europe/Zurich`,
439 `indian/antananarivo`: `Indian/Antananarivo`,
440 `indian/chagos`: `Indian/Chagos`,
441 `indian/christmas`: `Indian/Christmas`,
442 `indian/cocos`: `Indian/Cocos`,
443 `indian/comoro`: `Indian/Comoro`,
444 `indian/kerguelen`: `Indian/Kerguelen`,
445 `indian/mahe`: `Indian/Mahe`,
446 `indian/maldives`: `Indian/Maldives`,
447 `indian/mauritius`: `Indian/Mauritius`,
448 `indian/mayotte`: `Indian/Mayotte`,
449 `indian/reunion`: `Indian/Reunion`,
450 `pacific/apia`: `Pacific/Apia`,
451 `pacific/auckland`: `Pacific/Auckland`,
452 `pacific/bougainville`: `Pacific/Bougainville`,
453 `pacific/chatham`: `Pacific/Chatham`,
454 `pacific/chuuk`: `Pacific/Chuuk`,
455 `pacific/easter`: `Pacific/Easter`,
456 `pacific/efate`: `Pacific/Efate`,
457 `pacific/fakaofo`: `Pacific/Fakaofo`,
458 `pacific/fiji`: `Pacific/Fiji`,
459 `pacific/funafuti`: `Pacific/Funafuti`,
460 `pacific/galapagos`: `Pacific/Galapagos`,
461 `pacific/gambier`: `Pacific/Gambier`,
462 `pacific/guadalcanal`: `Pacific/Guadalcanal`,
463 `pacific/guam`: `Pacific/Guam`,
464 `pacific/honolulu`: `Pacific/Honolulu`,
465 `pacific/johnston`: `Pacific/Johnston`,
466 `pacific/kanton`: `Pacific/Kanton`,
467 `pacific/kiritimati`: `Pacific/Kiritimati`,
468 `pacific/kosrae`: `Pacific/Kosrae`,
469 `pacific/kwajalein`: `Pacific/Kwajalein`,
470 `pacific/majuro`: `Pacific/Majuro`,
471 `pacific/marquesas`: `Pacific/Marquesas`,
472 `pacific/midway`: `Pacific/Midway`,
473 `pacific/nauru`: `Pacific/Nauru`,
474 `pacific/niue`: `Pacific/Niue`,
475 `pacific/norfolk`: `Pacific/Norfolk`,
476 `pacific/noumea`: `Pacific/Noumea`,
477 `pacific/pago pago`: `Pacific/Pago_Pago`,
478 `pacific/palau`: `Pacific/Palau`,
479 `pacific/pitcairn`: `Pacific/Pitcairn`,
480 `pacific/pohnpei`: `Pacific/Pohnpei`,
481 `pacific/port moresby`: `Pacific/Port_Moresby`,
482 `pacific/rarotonga`: `Pacific/Rarotonga`,
483 `pacific/saipan`: `Pacific/Saipan`,
484 `pacific/samoa`: `Pacific/Samoa`,
485 `pacific/tahiti`: `Pacific/Tahiti`,
486 `pacific/tarawa`: `Pacific/Tarawa`,
487 `pacific/tongatapu`: `Pacific/Tongatapu`,
488 `pacific/wake`: `Pacific/Wake`,
489 `pacific/wallis`: `Pacific/Wallis`,
490 `pacific/yap`: `Pacific/Yap`,
491 `abidjan`: `Africa/Abidjan`,
492 `accra`: `Africa/Accra`,
493 `addis ababa`: `Africa/Addis_Ababa`,
494 `algiers`: `Africa/Algiers`,
495 `asmara`: `Africa/Asmara`,
496 `bamako`: `Africa/Bamako`,
497 `bangui`: `Africa/Bangui`,
498 `banjul`: `Africa/Banjul`,
499 `bissau`: `Africa/Bissau`,
500 `blantyre`: `Africa/Blantyre`,
501 `brazzaville`: `Africa/Brazzaville`,
502 `bujumbura`: `Africa/Bujumbura`,
503 `cairo`: `Africa/Cairo`,
504 `casablanca`: `Africa/Casablanca`,
505 `ceuta`: `Africa/Ceuta`,
506 `conakry`: `Africa/Conakry`,
507 `dakar`: `Africa/Dakar`,
508 `dar es salaam`: `Africa/Dar_es_Salaam`,
509 `djibouti`: `Africa/Djibouti`,
510 `douala`: `Africa/Douala`,
511 `el aaiun`: `Africa/El_Aaiun`,
512 `freetown`: `Africa/Freetown`,
513 `gaborone`: `Africa/Gaborone`,
514 `harare`: `Africa/Harare`,
515 `johannesburg`: `Africa/Johannesburg`,
516 `juba`: `Africa/Juba`,
517 `kampala`: `Africa/Kampala`,
518 `khartoum`: `Africa/Khartoum`,
519 `kigali`: `Africa/Kigali`,
520 `kinshasa`: `Africa/Kinshasa`,
521 `lagos`: `Africa/Lagos`,
522 `libreville`: `Africa/Libreville`,
523 `lome`: `Africa/Lome`,
524 `luanda`: `Africa/Luanda`,
525 `lubumbashi`: `Africa/Lubumbashi`,
526 `lusaka`: `Africa/Lusaka`,
527 `malabo`: `Africa/Malabo`,
528 `maputo`: `Africa/Maputo`,
529 `maseru`: `Africa/Maseru`,
530 `mbabane`: `Africa/Mbabane`,
531 `mogadishu`: `Africa/Mogadishu`,
532 `monrovia`: `Africa/Monrovia`,
533 `nairobi`: `Africa/Nairobi`,
534 `ndjamena`: `Africa/Ndjamena`,
535 `niamey`: `Africa/Niamey`,
536 `nouakchott`: `Africa/Nouakchott`,
537 `ouagadougou`: `Africa/Ouagadougou`,
538 `porto-novo`: `Africa/Porto-Novo`,
539 `sao tome`: `Africa/Sao_Tome`,
540 `timbuktu`: `Africa/Timbuktu`,
541 `tripoli`: `Africa/Tripoli`,
542 `tunis`: `Africa/Tunis`,
543 `windhoek`: `Africa/Windhoek`,
544 `adak`: `America/Adak`,
545 `anchorage`: `America/Anchorage`,
546 `anguilla`: `America/Anguilla`,
547 `antigua`: `America/Antigua`,
548 `araguaina`: `America/Araguaina`,
549 `buenos aires`: `America/Argentina/Buenos_Aires`,
550 `catamarca`: `America/Argentina/Catamarca`,
551 `cordoba`: `America/Argentina/Cordoba`,
552 `jujuy`: `America/Argentina/Jujuy`,
553 `la rioja`: `America/Argentina/La_Rioja`,
554 `mendoza`: `America/Argentina/Mendoza`,
555 `rio gallegos`: `America/Argentina/Rio_Gallegos`,
556 `salta`: `America/Argentina/Salta`,
557 `san juan`: `America/Argentina/San_Juan`,
558 `san luis`: `America/Argentina/San_Luis`,
559 `tucuman`: `America/Argentina/Tucuman`,
560 `ushuaia`: `America/Argentina/Ushuaia`,
561 `aruba`: `America/Aruba`,
562 `asuncion`: `America/Asuncion`,
563 `atikokan`: `America/Atikokan`,
564 `atka`: `America/Atka`,
565 `bahia`: `America/Bahia`,
566 `bahia banderas`: `America/Bahia_Banderas`,
567 `barbados`: `America/Barbados`,
568 `belem`: `America/Belem`,
569 `belize`: `America/Belize`,
570 `blanc-sablon`: `America/Blanc-Sablon`,
571 `boa vista`: `America/Boa_Vista`,
572 `bogota`: `America/Bogota`,
573 `boise`: `America/Boise`,
574 `cambridge bay`: `America/Cambridge_Bay`,
575 `campo grande`: `America/Campo_Grande`,
576 `cancun`: `America/Cancun`,
577 `caracas`: `America/Caracas`,
578 `cayenne`: `America/Cayenne`,
579 `cayman`: `America/Cayman`,
580 `chicago`: `America/Chicago`,
581 `chihuahua`: `America/Chihuahua`,
582 `ciudad juarez`: `America/Ciudad_Juarez`,
583 `coral harbour`: `America/Coral_Harbour`,
584 `costa rica`: `America/Costa_Rica`,
585 `coyhaique`: `America/Coyhaique`,
586 `creston`: `America/Creston`,
587 `cuiaba`: `America/Cuiaba`,
588 `curacao`: `America/Curacao`,
589 `danmarkshavn`: `America/Danmarkshavn`,
590 `dawson`: `America/Dawson`,
591 `dawson creek`: `America/Dawson_Creek`,
592 `denver`: `America/Denver`,
593 `detroit`: `America/Detroit`,
594 `dominica`: `America/Dominica`,
595 `edmonton`: `America/Edmonton`,
596 `eirunepe`: `America/Eirunepe`,
597 `el salvador`: `America/El_Salvador`,
598 `ensenada`: `America/Ensenada`,
599 `fort nelson`: `America/Fort_Nelson`,
600 `fortaleza`: `America/Fortaleza`,
601 `glace bay`: `America/Glace_Bay`,
602 `goose bay`: `America/Goose_Bay`,
603 `grand turk`: `America/Grand_Turk`,
604 `grenada`: `America/Grenada`,
605 `guadeloupe`: `America/Guadeloupe`,
606 `guatemala`: `America/Guatemala`,
607 `guayaquil`: `America/Guayaquil`,
608 `guyana`: `America/Guyana`,
609 `halifax`: `America/Halifax`,
610 `havana`: `America/Havana`,
611 `hermosillo`: `America/Hermosillo`,
612 `indianapolis`: `America/Indiana/Indianapolis`,
613 `knox`: `America/Indiana/Knox`,
614 `marengo`: `America/Indiana/Marengo`,
615 `petersburg`: `America/Indiana/Petersburg`,
616 `tell city`: `America/Indiana/Tell_City`,
617 `vevay`: `America/Indiana/Vevay`,
618 `vincennes`: `America/Indiana/Vincennes`,
619 `winamac`: `America/Indiana/Winamac`,
620 `inuvik`: `America/Inuvik`,
621 `iqaluit`: `America/Iqaluit`,
622 `jamaica`: `America/Jamaica`,
623 `juneau`: `America/Juneau`,
624 `louisville`: `America/Kentucky/Louisville`,
625 `monticello`: `America/Kentucky/Monticello`,
626 `kralendijk`: `America/Kralendijk`,
627 `la paz`: `America/La_Paz`,
628 `lima`: `America/Lima`,
629 `los angeles`: `America/Los_Angeles`,
630 `lower princes`: `America/Lower_Princes`,
631 `maceio`: `America/Maceio`,
632 `managua`: `America/Managua`,
633 `manaus`: `America/Manaus`,
634 `marigot`: `America/Marigot`,
635 `martinique`: `America/Martinique`,
636 `matamoros`: `America/Matamoros`,
637 `mazatlan`: `America/Mazatlan`,
638 `menominee`: `America/Menominee`,
639 `merida`: `America/Merida`,
640 `metlakatla`: `America/Metlakatla`,
641 `mexico city`: `America/Mexico_City`,
642 `miquelon`: `America/Miquelon`,
643 `moncton`: `America/Moncton`,
644 `monterrey`: `America/Monterrey`,
645 `montevideo`: `America/Montevideo`,
646 `montreal`: `America/Montreal`,
647 `montserrat`: `America/Montserrat`,
648 `nassau`: `America/Nassau`,
649 `new york`: `America/New_York`,
650 `nipigon`: `America/Nipigon`,
651 `nome`: `America/Nome`,
652 `noronha`: `America/Noronha`,
653 `beulah`: `America/North_Dakota/Beulah`,
654 `center`: `America/North_Dakota/Center`,
655 `new salem`: `America/North_Dakota/New_Salem`,
656 `nuuk`: `America/Nuuk`,
657 `ojinaga`: `America/Ojinaga`,
658 `panama`: `America/Panama`,
659 `pangnirtung`: `America/Pangnirtung`,
660 `paramaribo`: `America/Paramaribo`,
661 `phoenix`: `America/Phoenix`,
662 `port-au-prince`: `America/Port-au-Prince`,
663 `port of spain`: `America/Port_of_Spain`,
664 `porto acre`: `America/Porto_Acre`,
665 `porto velho`: `America/Porto_Velho`,
666 `puerto rico`: `America/Puerto_Rico`,
667 `punta arenas`: `America/Punta_Arenas`,
668 `rainy river`: `America/Rainy_River`,
669 `rankin inlet`: `America/Rankin_Inlet`,
670 `recife`: `America/Recife`,
671 `regina`: `America/Regina`,
672 `resolute`: `America/Resolute`,
673 `rio branco`: `America/Rio_Branco`,
674 `santa isabel`: `America/Santa_Isabel`,
675 `santarem`: `America/Santarem`,
676 `santiago`: `America/Santiago`,
677 `santo domingo`: `America/Santo_Domingo`,
678 `sao paulo`: `America/Sao_Paulo`,
679 `scoresbysund`: `America/Scoresbysund`,
680 `shiprock`: `America/Shiprock`,
681 `sitka`: `America/Sitka`,
682 `st barthelemy`: `America/St_Barthelemy`,
683 `st johns`: `America/St_Johns`,
684 `st kitts`: `America/St_Kitts`,
685 `st lucia`: `America/St_Lucia`,
686 `st thomas`: `America/St_Thomas`,
687 `st vincent`: `America/St_Vincent`,
688 `swift current`: `America/Swift_Current`,
689 `tegucigalpa`: `America/Tegucigalpa`,
690 `thule`: `America/Thule`,
691 `thunder bay`: `America/Thunder_Bay`,
692 `tijuana`: `America/Tijuana`,
693 `toronto`: `America/Toronto`,
694 `tortola`: `America/Tortola`,
695 `vancouver`: `America/Vancouver`,
696 `virgin`: `America/Virgin`,
697 `whitehorse`: `America/Whitehorse`,
698 `winnipeg`: `America/Winnipeg`,
699 `yakutat`: `America/Yakutat`,
700 `yellowknife`: `America/Yellowknife`,
701 `casey`: `Antarctica/Casey`,
702 `davis`: `Antarctica/Davis`,
703 `dumontdurville`: `Antarctica/DumontDUrville`,
704 `macquarie`: `Antarctica/Macquarie`,
705 `mawson`: `Antarctica/Mawson`,
706 `mcmurdo`: `Antarctica/McMurdo`,
707 `palmer`: `Antarctica/Palmer`,
708 `rothera`: `Antarctica/Rothera`,
709 `syowa`: `Antarctica/Syowa`,
710 `troll`: `Antarctica/Troll`,
711 `vostok`: `Antarctica/Vostok`,
712 `longyearbyen`: `Arctic/Longyearbyen`,
713 `aden`: `Asia/Aden`,
714 `almaty`: `Asia/Almaty`,
715 `amman`: `Asia/Amman`,
716 `anadyr`: `Asia/Anadyr`,
717 `aqtau`: `Asia/Aqtau`,
718 `aqtobe`: `Asia/Aqtobe`,
719 `ashgabat`: `Asia/Ashgabat`,
720 `atyrau`: `Asia/Atyrau`,
721 `baghdad`: `Asia/Baghdad`,
722 `bahrain`: `Asia/Bahrain`,
723 `baku`: `Asia/Baku`,
724 `bangkok`: `Asia/Bangkok`,
725 `barnaul`: `Asia/Barnaul`,
726 `beirut`: `Asia/Beirut`,
727 `bishkek`: `Asia/Bishkek`,
728 `brunei`: `Asia/Brunei`,
729 `chita`: `Asia/Chita`,
730 `chongqing`: `Asia/Chongqing`,
731 `colombo`: `Asia/Colombo`,
732 `damascus`: `Asia/Damascus`,
733 `dhaka`: `Asia/Dhaka`,
734 `dili`: `Asia/Dili`,
735 `dubai`: `Asia/Dubai`,
736 `dushanbe`: `Asia/Dushanbe`,
737 `famagusta`: `Asia/Famagusta`,
738 `gaza`: `Asia/Gaza`,
739 `harbin`: `Asia/Harbin`,
740 `hebron`: `Asia/Hebron`,
741 `ho chi minh`: `Asia/Ho_Chi_Minh`,
742 `hong kong`: `Asia/Hong_Kong`,
743 `hovd`: `Asia/Hovd`,
744 `irkutsk`: `Asia/Irkutsk`,
745 `jakarta`: `Asia/Jakarta`,
746 `jayapura`: `Asia/Jayapura`,
747 `jerusalem`: `Asia/Jerusalem`,
748 `kabul`: `Asia/Kabul`,
749 `kamchatka`: `Asia/Kamchatka`,
750 `karachi`: `Asia/Karachi`,
751 `kashgar`: `Asia/Kashgar`,
752 `kathmandu`: `Asia/Kathmandu`,
753 `khandyga`: `Asia/Khandyga`,
754 `kolkata`: `Asia/Kolkata`,
755 `krasnoyarsk`: `Asia/Krasnoyarsk`,
756 `kuala lumpur`: `Asia/Kuala_Lumpur`,
757 `kuching`: `Asia/Kuching`,
758 `kuwait`: `Asia/Kuwait`,
759 `macau`: `Asia/Macau`,
760 `magadan`: `Asia/Magadan`,
761 `makassar`: `Asia/Makassar`,
762 `manila`: `Asia/Manila`,
763 `muscat`: `Asia/Muscat`,
764 `novokuznetsk`: `Asia/Novokuznetsk`,
765 `novosibirsk`: `Asia/Novosibirsk`,
766 `omsk`: `Asia/Omsk`,
767 `oral`: `Asia/Oral`,
768 `phnom penh`: `Asia/Phnom_Penh`,
769 `pontianak`: `Asia/Pontianak`,
770 `pyongyang`: `Asia/Pyongyang`,
771 `qatar`: `Asia/Qatar`,
772 `qostanay`: `Asia/Qostanay`,
773 `qyzylorda`: `Asia/Qyzylorda`,
774 `riyadh`: `Asia/Riyadh`,
775 `sakhalin`: `Asia/Sakhalin`,
776 `samarkand`: `Asia/Samarkand`,
777 `seoul`: `Asia/Seoul`,
778 `shanghai`: `Asia/Shanghai`,
779 `singapore`: `Asia/Singapore`,
780 `srednekolymsk`: `Asia/Srednekolymsk`,
781 `taipei`: `Asia/Taipei`,
782 `tashkent`: `Asia/Tashkent`,
783 `tbilisi`: `Asia/Tbilisi`,
784 `tehran`: `Asia/Tehran`,
785 `tel aviv`: `Asia/Tel_Aviv`,
786 `thimphu`: `Asia/Thimphu`,
787 `tokyo`: `Asia/Tokyo`,
788 `tomsk`: `Asia/Tomsk`,
789 `ulaanbaatar`: `Asia/Ulaanbaatar`,
790 `urumqi`: `Asia/Urumqi`,
791 `ust-nera`: `Asia/Ust-Nera`,
792 `vientiane`: `Asia/Vientiane`,
793 `vladivostok`: `Asia/Vladivostok`,
794 `yakutsk`: `Asia/Yakutsk`,
795 `yangon`: `Asia/Yangon`,
796 `yekaterinburg`: `Asia/Yekaterinburg`,
797 `yerevan`: `Asia/Yerevan`,
798 `azores`: `Atlantic/Azores`,
799 `bermuda`: `Atlantic/Bermuda`,
800 `canary`: `Atlantic/Canary`,
801 `cape verde`: `Atlantic/Cape_Verde`,
802 `faroe`: `Atlantic/Faroe`,
803 `jan mayen`: `Atlantic/Jan_Mayen`,
804 `madeira`: `Atlantic/Madeira`,
805 `reykjavik`: `Atlantic/Reykjavik`,
806 `south georgia`: `Atlantic/South_Georgia`,
807 `st helena`: `Atlantic/St_Helena`,
808 `stanley`: `Atlantic/Stanley`,
809 `adelaide`: `Australia/Adelaide`,
810 `brisbane`: `Australia/Brisbane`,
811 `broken hill`: `Australia/Broken_Hill`,
812 `canberra`: `Australia/Canberra`,
813 `currie`: `Australia/Currie`,
814 `darwin`: `Australia/Darwin`,
815 `eucla`: `Australia/Eucla`,
816 `hobart`: `Australia/Hobart`,
817 `lindeman`: `Australia/Lindeman`,
818 `lord howe`: `Australia/Lord_Howe`,
819 `melbourne`: `Australia/Melbourne`,
820 `perth`: `Australia/Perth`,
821 `sydney`: `Australia/Sydney`,
822 `yancowinna`: `Australia/Yancowinna`,
823 `greenwich`: `Etc/Greenwich`,
824 `uct`: `Etc/UCT`,
825 `utc`: `Etc/UTC`,
826 `universal`: `Etc/Universal`,
827 `zulu`: `Etc/Zulu`,
828 `amsterdam`: `Europe/Amsterdam`,
829 `andorra`: `Europe/Andorra`,
830 `astrakhan`: `Europe/Astrakhan`,
831 `athens`: `Europe/Athens`,
832 `belfast`: `Europe/Belfast`,
833 `belgrade`: `Europe/Belgrade`,
834 `berlin`: `Europe/Berlin`,
835 `bratislava`: `Europe/Bratislava`,
836 `brussels`: `Europe/Brussels`,
837 `bucharest`: `Europe/Bucharest`,
838 `budapest`: `Europe/Budapest`,
839 `busingen`: `Europe/Busingen`,
840 `chisinau`: `Europe/Chisinau`,
841 `copenhagen`: `Europe/Copenhagen`,
842 `dublin`: `Europe/Dublin`,
843 `gibraltar`: `Europe/Gibraltar`,
844 `guernsey`: `Europe/Guernsey`,
845 `helsinki`: `Europe/Helsinki`,
846 `isle of man`: `Europe/Isle_of_Man`,
847 `jersey`: `Europe/Jersey`,
848 `kaliningrad`: `Europe/Kaliningrad`,
849 `kirov`: `Europe/Kirov`,
850 `kyiv`: `Europe/Kyiv`,
851 `lisbon`: `Europe/Lisbon`,
852 `ljubljana`: `Europe/Ljubljana`,
853 `london`: `Europe/London`,
854 `luxembourg`: `Europe/Luxembourg`,
855 `madrid`: `Europe/Madrid`,
856 `malta`: `Europe/Malta`,
857 `mariehamn`: `Europe/Mariehamn`,
858 `minsk`: `Europe/Minsk`,
859 `monaco`: `Europe/Monaco`,
860 `moscow`: `Europe/Moscow`,
861 `oslo`: `Europe/Oslo`,
862 `paris`: `Europe/Paris`,
863 `podgorica`: `Europe/Podgorica`,
864 `prague`: `Europe/Prague`,
865 `riga`: `Europe/Riga`,
866 `rome`: `Europe/Rome`,
867 `samara`: `Europe/Samara`,
868 `san marino`: `Europe/San_Marino`,
869 `sarajevo`: `Europe/Sarajevo`,
870 `saratov`: `Europe/Saratov`,
871 `simferopol`: `Europe/Simferopol`,
872 `skopje`: `Europe/Skopje`,
873 `sofia`: `Europe/Sofia`,
874 `stockholm`: `Europe/Stockholm`,
875 `tallinn`: `Europe/Tallinn`,
876 `tirane`: `Europe/Tirane`,
877 `tiraspol`: `Europe/Tiraspol`,
878 `ulyanovsk`: `Europe/Ulyanovsk`,
879 `vaduz`: `Europe/Vaduz`,
880 `vatican`: `Europe/Vatican`,
881 `vienna`: `Europe/Vienna`,
882 `vilnius`: `Europe/Vilnius`,
883 `volgograd`: `Europe/Volgograd`,
884 `warsaw`: `Europe/Warsaw`,
885 `zagreb`: `Europe/Zagreb`,
886 `zurich`: `Europe/Zurich`,
887 `antananarivo`: `Indian/Antananarivo`,
888 `chagos`: `Indian/Chagos`,
889 `christmas`: `Indian/Christmas`,
890 `cocos`: `Indian/Cocos`,
891 `comoro`: `Indian/Comoro`,
892 `kerguelen`: `Indian/Kerguelen`,
893 `mahe`: `Indian/Mahe`,
894 `maldives`: `Indian/Maldives`,
895 `mauritius`: `Indian/Mauritius`,
896 `mayotte`: `Indian/Mayotte`,
897 `reunion`: `Indian/Reunion`,
898 `apia`: `Pacific/Apia`,
899 `auckland`: `Pacific/Auckland`,
900 `bougainville`: `Pacific/Bougainville`,
901 `chatham`: `Pacific/Chatham`,
902 `chuuk`: `Pacific/Chuuk`,
903 `easter`: `Pacific/Easter`,
904 `efate`: `Pacific/Efate`,
905 `fakaofo`: `Pacific/Fakaofo`,
906 `fiji`: `Pacific/Fiji`,
907 `funafuti`: `Pacific/Funafuti`,
908 `galapagos`: `Pacific/Galapagos`,
909 `gambier`: `Pacific/Gambier`,
910 `guadalcanal`: `Pacific/Guadalcanal`,
911 `guam`: `Pacific/Guam`,
912 `honolulu`: `Pacific/Honolulu`,
913 `johnston`: `Pacific/Johnston`,
914 `kanton`: `Pacific/Kanton`,
915 `kiritimati`: `Pacific/Kiritimati`,
916 `kosrae`: `Pacific/Kosrae`,
917 `kwajalein`: `Pacific/Kwajalein`,
918 `majuro`: `Pacific/Majuro`,
919 `marquesas`: `Pacific/Marquesas`,
920 `midway`: `Pacific/Midway`,
921 `nauru`: `Pacific/Nauru`,
922 `niue`: `Pacific/Niue`,
923 `norfolk`: `Pacific/Norfolk`,
924 `noumea`: `Pacific/Noumea`,
925 `pago pago`: `Pacific/Pago_Pago`,
926 `palau`: `Pacific/Palau`,
927 `pitcairn`: `Pacific/Pitcairn`,
928 `pohnpei`: `Pacific/Pohnpei`,
929 `port moresby`: `Pacific/Port_Moresby`,
930 `rarotonga`: `Pacific/Rarotonga`,
931 `saipan`: `Pacific/Saipan`,
932 `samoa`: `Pacific/Samoa`,
933 `tahiti`: `Pacific/Tahiti`,
934 `tarawa`: `Pacific/Tarawa`,
935 `tongatapu`: `Pacific/Tongatapu`,
936 `wake`: `Pacific/Wake`,
937 `wallis`: `Pacific/Wallis`,
938 `yap`: `Pacific/Yap`,
939 }
940
941 // Lookup tries to find a timezone from the place/city name given
942 func Lookup(place string) (*time.Location, error) {
943 if loc, err := time.LoadLocation(place); err == nil {
944 return loc, err
945 }
946
947 if s, ok := lookupAlias(place); ok {
948 place = s
949 }
950
951 loc, err := time.LoadLocation(place)
952 return loc, err
953 }
954
955 // LookupName tries to find a timezone name from the place/city name given
956 func LookupName(place string) (string, bool) {
957 if s, ok := lookupAlias(place); ok {
958 return s, true
959 }
960
961 for _, s := range aliases {
962 if strings.EqualFold(place, s) {
963 return s, true
964 }
965 }
966 return place, false
967 }
968
969 // lookupAlias tries to find a timezone alias from the place/city name given
970 func lookupAlias(place string) (string, bool) {
971 key := strings.ToLower(place)
972 key = strings.ReplaceAll(key, `_`, ` `)
973 key = strings.ReplaceAll(key, `-`, ` `)
974
975 if s, ok := aliases[key]; ok {
976 return s, true
977 }
978 return place, false
979 }
File: ./now/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 now
26
27 import (
28 "fmt"
29 "io"
30 "os"
31 "time"
32
33 _ "embed"
34 )
35
36 //go:embed info.txt
37 var info string
38
39 const timeFormat = `2006-01-02 15:04:05 Mon Jan 02`
40
41 func Main() {
42 args := os.Args[1:]
43 if len(args) > 0 {
44 switch args[0] {
45 case `-h`, `--h`, `-help`, `--help`:
46 os.Stdout.WriteString(info[1:])
47 return
48 }
49 }
50
51 if len(args) > 0 && args[0] == `--` {
52 args = args[1:]
53 }
54
55 ok := true
56 w := os.Stdout
57
58 now := time.Now()
59 showDateTime(w, now)
60 place := now.Location().String()
61 fmt.Fprintf(w, " %s\n", place)
62
63 for _, place := range args {
64 loc, err := Lookup(place)
65 if err != nil {
66 fmt.Fprintln(os.Stderr, err.Error())
67 ok = false
68 continue
69 }
70
71 showDateTime(w, now.In(loc))
72 fmt.Fprintf(w, " %s\n", place)
73 }
74
75 if !ok {
76 os.Exit(1)
77 }
78 }
79
80 func showDateTime(w io.Writer, t time.Time) {
81 var buf [64]byte
82 w.Write(t.AppendFormat(buf[:0], timeFormat))
83 }
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: ./remakes/cat/cat.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 cat
26
27 import (
28 "io"
29 "os"
30 )
31
32 const info = `
33 cat [options...] [files...]
34
35 Concatenate files to the standard output.
36
37 Options
38
39 --help show this help message
40 `
41
42 func Main() {
43 args := os.Args[1:]
44 for len(args) > 0 {
45 switch args[0] {
46 case `--help`:
47 os.Stderr.WriteString(info[1:])
48 return
49 }
50
51 break
52 }
53
54 if len(args) > 0 && args[0] == `--` {
55 args = args[1:]
56 }
57
58 for _, path := range args {
59 if err := handleFile(os.Stdout, path); err != nil {
60 if err == io.EOF {
61 os.Exit(0)
62 }
63
64 os.Stderr.WriteString(err.Error())
65 os.Stderr.WriteString("\n")
66 os.Exit(1)
67 }
68 }
69
70 if len(args) == 0 {
71 cat(os.Stdout, os.Stdin)
72 }
73 }
74
75 func handleFile(w io.Writer, path string) error {
76 f, err := os.Open(path)
77 if err != nil {
78 return err
79 }
80 defer f.Close()
81 return cat(w, f)
82 }
83
84 func cat(w io.Writer, r io.Reader) error {
85 var buf [32 * 1024]byte
86
87 for {
88 got, err := r.Read(buf[:])
89 if err == io.EOF {
90 if got > 0 {
91 w.Write(buf[:got])
92 }
93 break
94 }
95
96 if err != nil {
97 return err
98 }
99
100 if _, err := w.Write(buf[:got]); err != nil {
101 return io.EOF
102 }
103 }
104
105 return nil
106 }
File: ./remakes/head/head.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 head
26
27 import (
28 "bufio"
29 "io"
30 "os"
31 "strconv"
32 )
33
34 const info = `
35 head [options...] [files...]
36
37 Keep at most the first n lines, or keep the first 10 lines by default. When
38 not given any filepaths, the standard input is used instead.
39
40 Options
41
42 -n [number] change max number of lines (default is 10)
43 `
44
45 type config struct {
46 max int
47 }
48
49 func Main() {
50 var cfg config
51 cfg.max = 10
52
53 args := os.Args[1:]
54 for len(args) > 0 {
55 switch args[0] {
56 case `-n`:
57 args = args[1:]
58 if len(args) == 0 {
59 os.Stderr.WriteString("missing number of lines\n")
60 os.Exit(1)
61 }
62 n, err := strconv.ParseInt(args[0], 10, 64)
63 if err != nil {
64 os.Stderr.WriteString("invalid number: ")
65 os.Stderr.WriteString(err.Error())
66 os.Stderr.WriteString("\n")
67 os.Exit(1)
68 }
69 args = args[1:]
70 cfg.max = int(n)
71 continue
72
73 case `--help`:
74 os.Stderr.WriteString(info[1:])
75 return
76 }
77
78 break
79 }
80
81 if len(args) > 0 && args[0] == `--` {
82 args = args[1:]
83 }
84
85 if cfg.max <= 0 {
86 os.Exit(0)
87 }
88
89 if err := run(args, &cfg); err != nil {
90 if err == io.EOF {
91 os.Exit(0)
92 }
93
94 os.Stderr.WriteString(err.Error())
95 os.Stderr.WriteString("\n")
96 os.Exit(1)
97 }
98 }
99
100 func run(paths []string, cfg *config) error {
101 w := bufio.NewWriterSize(os.Stdout, 32*1024)
102 defer w.Flush()
103
104 for _, path := range paths {
105 if cfg.max <= 0 {
106 return io.EOF
107 }
108 if err := handleFile(w, path, cfg); err != nil {
109 return err
110 }
111 }
112
113 if len(paths) == 0 {
114 if err := head(w, os.Stdin, cfg); err != nil {
115 return err
116 }
117 }
118 return nil
119 }
120
121 func handleFile(w *bufio.Writer, path string, cfg *config) error {
122 f, err := os.Open(path)
123 if err != nil {
124 return err
125 }
126 defer f.Close()
127 return head(w, f, cfg)
128 }
129
130 func head(w *bufio.Writer, r io.Reader, cfg *config) error {
131 const gb = 1024 * 1024 * 1024
132 sc := bufio.NewScanner(r)
133 sc.Buffer(nil, 8*gb)
134
135 for sc.Scan() {
136 if cfg.max <= 0 {
137 return io.EOF
138 }
139
140 w.Write(sc.Bytes())
141 if err := w.WriteByte('\n'); err != nil {
142 return io.EOF
143 }
144
145 cfg.max--
146 }
147
148 return sc.Err()
149 }
File: ./remakes/ls/ls.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 ls
26
27 import (
28 "bufio"
29 "io"
30 "os"
31 "sort"
32 "strings"
33 )
34
35 const info = `
36 ls [options...] [folders...]
37
38 List top-level entries in the folder paths given: if no paths are given, the
39 current folder is listed by default.
40
41 Options
42
43 -a show all files, including those whose name starts with a dot
44 --help show this help message
45 `
46
47 type config struct {
48 all bool
49 long bool
50 }
51
52 func Main() {
53 args := os.Args[1:]
54
55 var cfg config
56 for len(args) > 0 {
57 switch args[0] {
58 case `-a`:
59 cfg.all = true
60 args = args[1:]
61 continue
62
63 case `--help`:
64 os.Stderr.WriteString(info[1:])
65 return
66
67 case `-l`:
68 cfg.long = true
69 args = args[1:]
70 continue
71 }
72
73 break
74 }
75
76 if len(args) > 0 && args[0] == `--` {
77 args = args[1:]
78 }
79
80 if err := run(args, cfg); err != nil {
81 if err == io.EOF {
82 return
83 }
84
85 os.Stderr.WriteString(err.Error())
86 os.Stderr.WriteString("\n")
87 os.Exit(1)
88 }
89 }
90
91 func run(paths []string, cfg config) error {
92 w := bufio.NewWriterSize(os.Stdout, 32*1024)
93 defer w.Flush()
94
95 for i, path := range paths {
96 if len(paths) > 1 {
97 w.WriteString(path)
98 w.WriteString(":\n")
99 if i > 0 {
100 w.WriteString("\n")
101 }
102 }
103
104 if err := ls(w, path, cfg); err != nil {
105 return err
106 }
107 }
108
109 if len(paths) == 0 {
110 return ls(w, `.`, cfg)
111 }
112 return nil
113 }
114
115 func ls(w *bufio.Writer, path string, cfg config) error {
116 defer w.Flush()
117
118 entries, err := os.ReadDir(path)
119 if err != nil {
120 return err
121 }
122
123 sort.SliceStable(entries, func(i, j int) bool {
124 return compareNames(entries[i].Name(), entries[j].Name()) < 0
125 })
126
127 for _, e := range entries {
128 name := e.Name()
129
130 if !cfg.all && len(name) > 0 && name[0] == '.' {
131 continue
132 }
133
134 w.WriteString(name)
135 if _, err := w.WriteString("\n"); err != nil {
136 return io.EOF
137 }
138 }
139
140 return nil
141 }
142
143 func compareNames(x, y string) int {
144 if len(x) < len(y) && strings.HasPrefix(y, x) {
145 return -1
146 }
147 if len(y) < len(x) && strings.HasPrefix(x, y) {
148 return +1
149 }
150 return strings.Compare(x, y)
151 }
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 -0, -null turn null-byte-delimited chunks into proper lines
50 `
51
52 type config struct {
53 null bool
54 liveLines bool
55 }
56
57 func Main() {
58 var cfg config
59 cfg.liveLines = true
60 args := os.Args[1:]
61
62 for len(args) > 0 {
63 switch args[0] {
64 case `-0`, `--0`, `-null`, `--null`:
65 cfg.null = true
66 args = args[1:]
67 continue
68
69 case `-b`, `--b`, `-buffered`, `--buffered`:
70 cfg.liveLines = false
71 args = args[1:]
72 continue
73
74 case `-h`, `--h`, `-help`, `--help`:
75 os.Stdout.WriteString(info[1:])
76 return
77 }
78
79 break
80 }
81
82 if len(args) > 0 && args[0] == `--` {
83 args = args[1:]
84 }
85
86 if cfg.liveLines {
87 if _, err := os.Stdout.Seek(0, io.SeekCurrent); err == nil {
88 cfg.liveLines = false
89 }
90 }
91
92 if err := run(os.Stdout, args, cfg); err != nil && err != io.EOF {
93 os.Stderr.WriteString(err.Error())
94 os.Stderr.WriteString("\n")
95 os.Exit(1)
96 }
97 }
98
99 func run(w io.Writer, args []string, cfg config) error {
100 bw := bufio.NewWriter(w)
101 defer bw.Flush()
102
103 dashes := 0
104 for _, name := range args {
105 if name == `-` {
106 dashes++
107 }
108 if dashes > 1 {
109 break
110 }
111 }
112
113 if len(args) == 0 {
114 return tcatl(bw, os.Stdin, `<stdin>`, cfg)
115 }
116
117 var stdin []byte
118 gotStdin := false
119
120 for _, name := range args {
121 if name == `-` {
122 if dashes == 1 {
123 if err := tcatl(bw, os.Stdin, `<stdin>`, cfg); err != nil {
124 return err
125 }
126 continue
127 }
128
129 if !gotStdin {
130 data, err := io.ReadAll(os.Stdin)
131 if err != nil {
132 return err
133 }
134 stdin = data
135 gotStdin = true
136 }
137
138 bw.Write(stdin)
139 if len(stdin) > 0 && stdin[len(stdin)-1] != '\n' {
140 bw.WriteByte('\n')
141 }
142
143 if !cfg.liveLines {
144 continue
145 }
146
147 if err := bw.Flush(); err != nil {
148 return io.EOF
149 }
150
151 continue
152 }
153
154 if err := handleFile(bw, name, cfg); err != nil {
155 return err
156 }
157 }
158 return nil
159 }
160
161 func handleFile(w *bufio.Writer, name string, cfg config) error {
162 if name == `` || name == `-` {
163 return tcatl(w, os.Stdin, `<stdin>`, cfg)
164 }
165
166 f, err := os.Open(name)
167 if err != nil {
168 return errors.New(`can't read from file named "` + name + `"`)
169 }
170 defer f.Close()
171
172 return tcatl(w, f, name, cfg)
173 }
174
175 func tcatl(w *bufio.Writer, r io.Reader, name string, cfg config) error {
176 w.WriteString("\x1b[7m")
177 w.WriteString(name)
178 writeSpaces(w, 80-utf8.RuneCountInString(name))
179 w.WriteString("\x1b[0m\n")
180 if err := w.Flush(); err != nil {
181 // a write error may be the consequence of stdout being closed,
182 // perhaps by another app along a pipe
183 return io.EOF
184 }
185
186 if !cfg.liveLines {
187 return catlFast(w, r, cfg.null)
188 }
189
190 const gb = 1024 * 1024 * 1024
191 sc := bufio.NewScanner(r)
192 sc.Buffer(nil, 8*gb)
193 if cfg.null {
194 sc.Split(splitNull)
195 }
196
197 for i := 0; sc.Scan(); i++ {
198 s := sc.Bytes()
199 if i == 0 && bytes.HasPrefix(s, []byte{0xef, 0xbb, 0xbf}) {
200 s = s[3:]
201 }
202
203 w.Write(s)
204 if w.WriteByte('\n') != nil {
205 return io.EOF
206 }
207
208 if err := w.Flush(); err != nil {
209 return io.EOF
210 }
211 }
212
213 return sc.Err()
214 }
215
216 func catlFast(w *bufio.Writer, r io.Reader, null bool) error {
217 var buf [32 * 1024]byte
218 var last byte = '\n'
219
220 for i := 0; true; i++ {
221 n, err := r.Read(buf[:])
222 if n > 0 && err == io.EOF {
223 err = nil
224 }
225 if err == io.EOF {
226 if last != '\n' {
227 w.WriteByte('\n')
228 }
229 return nil
230 }
231
232 if err != nil {
233 return err
234 }
235
236 chunk := buf[:n]
237 if i == 0 && bytes.HasPrefix(chunk, []byte{0xef, 0xbb, 0xbf}) {
238 chunk = chunk[3:]
239 }
240
241 // change nulls into line-feeds to handle null-terminated lines
242 if null {
243 for i, b := range chunk {
244 if b == 0 {
245 chunk[i] = '\n'
246 }
247 }
248 }
249
250 if len(chunk) >= 1 {
251 if _, err := w.Write(chunk); err != nil {
252 return io.EOF
253 }
254 last = chunk[len(chunk)-1]
255 }
256 }
257
258 return nil
259 }
260
261 // splitNull is given to bufio.Scanner.Split to handle null-terminated lines
262 func splitNull(data []byte, atEOF bool) (advance int, token []byte, err error) {
263 // handle leading null-terminated line, if found in the current chunk
264 if i := bytes.IndexByte(data, 0); i >= 0 {
265 return i + 1, data[:i], nil
266 }
267
268 // request more data, in case there's a null coming up later
269 if !atEOF {
270 return 0, nil, nil
271 }
272
273 // handle non-empty non-terminated last chunk
274 if len(data) > 0 {
275 return len(data), data, bufio.ErrFinalToken
276 }
277
278 // handle empty non-terminated last chunk
279 return 0, nil, bufio.ErrFinalToken
280 }
281
282 // writeSpaces bulk-emits the number of spaces given
283 func writeSpaces(w *bufio.Writer, n int) {
284 const spaces = ` `
285 for ; n > len(spaces); n -= len(spaces) {
286 w.WriteString(spaces)
287 }
288 if n > 0 {
289 w.WriteString(spaces[:n])
290 }
291 }
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: ./units/units.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 units
26
27 import (
28 "bufio"
29 "fmt"
30 "io"
31 "math"
32 "os"
33 "sort"
34 "strconv"
35 "strings"
36 )
37
38 const info = `
39 units [options...] [quantities / source units...]
40
41 Convert quantities from weird units into equivalent better ones, usually from
42 the international systems of measurements: think kilometers instead of miles.
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 args := os.Args[1:]
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 len(args) == 0 {
64 // os.Stderr.WriteString(info[1:])
65 // os.Exit(1)
66 // return
67 // }
68
69 if err := run(os.Stdout, args); err != nil {
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 from := ``
81 low := ``
82 var values []float64
83
84 dump := func(unit string) bool {
85 if s, ok := aliases[unit]; ok {
86 unit = s
87 }
88
89 c, ok := converters[unit]
90 if !ok {
91 return false
92 }
93
94 for _, v := range values {
95 res := c.Mul*v + c.Add
96 const fs = "%.4f %-4s = %.4f %s\n"
97 fmt.Fprintf(bw, fs, v, unit, res, c.To)
98 }
99 return true
100 }
101
102 for _, s := range args {
103 f, err := strconv.ParseFloat(s, 64)
104 if err == nil && !math.IsNaN(f) && !math.IsInf(f, 0) {
105 values = append(values, f)
106 continue
107 }
108
109 if len(values) == 0 {
110 values = append(values, 1)
111 }
112 from = s
113 low = strings.ToLower(from)
114
115 if dump(low) {
116 values = values[:0]
117 from = ``
118 low = ``
119 continue
120 }
121
122 return fmt.Errorf("unit %q not supported\n", low)
123 }
124
125 if from == `` {
126 // return errors.New(`no source units given`)
127
128 if len(values) == 0 {
129 values = []float64{1}
130 }
131
132 units := make([]string, 0, len(converters))
133 for k := range converters {
134 units = append(units, k)
135 }
136 sort.Strings(units)
137 for _, from := range units {
138 dump(from)
139 }
140 return nil
141 }
142
143 if !dump(from) {
144 return fmt.Errorf("unit %q not supported\n", low)
145 }
146 return nil
147 }
148
149 var aliases = map[string]string{
150 `acre`: `ac`,
151 `acres`: `ac`,
152 `days`: `day`,
153 `foot`: `ft`,
154 `feet`: `ft`,
155 `feet2`: `ft²`,
156 `feet3`: `ft³`,
157 `foot2`: `ft²`,
158 `foot3`: `ft³`,
159 `ft2`: `ft²`,
160 `ft3`: `ft³`,
161 `gallon`: `gal`,
162 `gallons`: `gal`,
163 `gals`: `gal`,
164 `inch`: `in`,
165 `inches`: `in`,
166 `mile`: `mi`,
167 `miles`: `mi`,
168 `mile²`: `mi²`,
169 `miles²`: `mi²`,
170 `minute`: `min`,
171 `minutes`: `min`,
172 `nmile`: `nmi`,
173 `nmiles`: `nmi`,
174 `ounce`: `oz`,
175 `ounces`: `oz`,
176 `ozs`: `oz`,
177 `weeks`: `week`,
178 `wk`: `week`,
179 `wks`: `week`,
180 `yard`: `yd`,
181 `yards`: `yd`,
182 `yard2`: `yd²`,
183 `yard²`: `yd²`,
184 `yards2`: `yd²`,
185 `yards²`: `yd²`,
186 `yds`: `yd`,
187 `yds2`: `yd²`,
188 `yds²`: `yd²`,
189 }
190
191 type converter struct {
192 To string
193 Mul float64
194 Add float64
195 }
196
197 var converters = map[string]converter{
198 `ac`: converter{`m²`, 4046.8564224, 0},
199 `day`: converter{`s`, 86400, 0},
200 `ft`: converter{`m`, 0.3048, 0},
201 `ft²`: converter{`m²`, 0.09290304, 0},
202 `ft³`: converter{`m³`, 0.028316846592, 0},
203 `gal`: converter{`L`, 3.785411784, 0},
204 `in`: converter{`cm`, 2.54, 0},
205 `mi`: converter{`km`, 1.609344, 0},
206 `mi²`: converter{`km²`, 2.5899881103360, 0},
207 `min`: converter{`s`, 60, 0},
208 `mpg`: converter{`kpl`, 0.425143707, 0},
209 `mph`: converter{`kph`, 1.609344, 0},
210 `nmi`: converter{`km`, 1.852, 0},
211 `oz`: converter{`g`, 28.349523125, 0},
212 `week`: converter{`s`, 604800, 0},
213 `yd`: converter{`m`, 0.9144, 0},
214 `yd²`: converter{`m²`, 0.83612736, 0},
215 }
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: ./verdict/verdict.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 verdict
26
27 import (
28 "fmt"
29 "io"
30 "os"
31 "os/exec"
32 "strconv"
33 )
34
35 const info = `
36 verdict [options...] [command...] [arguments...]
37
38
39 Run the command given, colorfully showing its exit code on failure. On
40 success, the exit code is 0, but it's not shown explicitly.
41
42 A colorblind-friendly blue is used instead of green if either environment
43 variable COLORBLIND or COLOR_BLIND is declared and set to 1.
44
45 When variable NO_COLOR is declared and set to 1, an invert/highlight style
46 is used instead of colors, no matter the exit code of the command given.
47
48 All (optional) leading options start with either single or double-dash:
49
50 -h, -help show this help message
51 `
52
53 func Main() {
54 liveLines := true
55 args := os.Args[1:]
56
57 for len(args) > 0 {
58 switch args[0] {
59 case `-b`, `--b`, `-buffered`, `--buffered`:
60 liveLines = false
61 args = args[1:]
62 continue
63
64 case `-h`, `--h`, `-help`, `--help`:
65 os.Stdout.WriteString(info[1:])
66 return
67 }
68
69 break
70 }
71
72 if len(args) > 0 && args[0] == `--` {
73 args = args[1:]
74 }
75
76 if liveLines {
77 if _, err := os.Stdout.Seek(0, io.SeekCurrent); err == nil {
78 liveLines = false
79 }
80 }
81
82 if err := run(os.Stdout, args, liveLines); err != nil && err != io.EOF {
83 os.Stderr.WriteString(err.Error())
84 os.Stderr.WriteString("\n")
85 os.Exit(1)
86 }
87 }
88
89 func run(w io.Writer, args []string, liveLines bool) error {
90 if len(args) == 0 {
91 return nil
92 }
93
94 cmd := exec.Command(args[0], args[1:]...)
95
96 err := cmd.Wait()
97 if err == nil {
98 showVerdict(args, 0)
99 return nil
100 }
101
102 if err, ok := err.(*exec.ExitError); ok {
103 showVerdict(args, err.ExitCode())
104 return nil
105 }
106
107 return err
108 }
109
110 func showVerdict(args []string, code int) {
111 if checkBoolEnv(`NO_COLOR`) {
112 for _, s := range args {
113 os.Stderr.WriteString(s)
114 os.Stderr.WriteString(` `)
115 }
116
117 if code == 0 {
118 os.Stderr.WriteString("\x1b[7m succeeded \x1b[0m\n")
119 } else {
120 const fs = "\x1b[7m failed with error code %d \x1b[0m\n"
121 fmt.Fprintf(os.Stderr, fs, code)
122 }
123 return
124 }
125
126 if code == 0 {
127 if checkBoolEnv(`COLORBLIND`) || checkBoolEnv(`COLOR_BLIND`) {
128 os.Stderr.WriteString("\x1b[48;2;0;95;215m")
129 for _, s := range args {
130 os.Stderr.WriteString(s)
131 os.Stderr.WriteString(` `)
132 }
133 const style = "\x1b[48;2;0;95;215m\x1b[38;2;255;255;255m"
134 os.Stderr.WriteString(style + " succeeded \x1b[0m\n")
135 } else {
136 os.Stderr.WriteString("\x1b[38;2;0;135;95m")
137 for _, s := range args {
138 os.Stderr.WriteString(s)
139 os.Stderr.WriteString(` `)
140 }
141 const style = "\x1b[48;2;0;135;95m\x1b[38;2;255;255;255m"
142 os.Stderr.WriteString(style + " succeeded \x1b[0m\n")
143 }
144 return
145 }
146
147 os.Stderr.WriteString("\x1b[38;2;204;0;0m")
148 for _, s := range args {
149 os.Stderr.WriteString(s)
150 os.Stderr.WriteString(` `)
151 }
152
153 const style = "\x1b[48;2;204;0;0m\x1b[38;2;255;255;255m"
154 const fs = style + " failed with error code %d \x1b[0m\n"
155 fmt.Fprintf(os.Stderr, fs, code)
156 }
157
158 func checkBoolEnv(x string) bool {
159 s, ok := os.LookupEnv(x)
160 if !ok {
161 return false
162 }
163
164 n, err := strconv.ParseInt(s, 10, 64)
165 if err != nil {
166 return false
167 }
168
169 return n != 0
170 }
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
37 // config has all the parsed cmd-line options
38 type config struct {
39 // Scripts has the source codes of all scripts for all channels
40 Scripts []string
41
42 // To is the output format
43 To string
44
45 // MaxTime is the play duration of the resulting sound
46 MaxTime float64
47
48 // SampleRate is the number of samples per second for all channels
49 SampleRate uint
50 }
51
52 // parseFlags is the constructor for type config
53 func parseFlags(usage string) (config, error) {
54 cfg := config{
55 To: `wav`,
56 MaxTime: math.NaN(),
57 SampleRate: 48_000,
58 }
59
60 args := os.Args[1:]
61 if len(args) == 0 {
62 fmt.Fprint(os.Stderr, usage)
63 os.Exit(0)
64 }
65
66 for _, s := range args {
67 switch s {
68 case `help`, `-h`, `--h`, `-help`, `--help`:
69 fmt.Fprint(os.Stdout, usage)
70 os.Exit(0)
71 }
72
73 err := cfg.handleArg(s)
74 if err != nil {
75 return cfg, err
76 }
77 }
78
79 if math.IsNaN(cfg.MaxTime) {
80 cfg.MaxTime = 1
81 }
82 if cfg.MaxTime < 0 {
83 const fs = `error: given negative duration %f`
84 return cfg, fmt.Errorf(fs, cfg.MaxTime)
85 }
86 return cfg, nil
87 }
88
89 func (c *config) handleArg(s string) error {
90 switch s {
91 case `44.1k`, `44.1K`:
92 c.SampleRate = 44_100
93 return nil
94
95 case `48k`, `48K`:
96 c.SampleRate = 48_000
97 return nil
98
99 case `dat`, `DAT`:
100 c.SampleRate = 48_000
101 return nil
102
103 case `cd`, `cda`, `CD`, `CDA`:
104 c.SampleRate = 44_100
105 return nil
106 }
107
108 // handle output-format names and their aliases
109 if kind, ok := name2type[s]; ok {
110 c.To = kind
111 return nil
112 }
113
114 // handle time formats, except when they're pure numbers
115 if math.IsNaN(c.MaxTime) {
116 dur, derr := ParseDuration(s)
117 if derr == nil {
118 c.MaxTime = float64(dur) / float64(time.Second)
119 return nil
120 }
121 }
122
123 // handle sample-rate, given either in hertz or kilohertz
124 lc := strings.ToLower(s)
125 if strings.HasSuffix(lc, `khz`) {
126 lc = strings.TrimSuffix(lc, `khz`)
127 khz, err := strconv.ParseFloat(lc, 64)
128 if err != nil || isBadNumber(khz) || khz <= 0 {
129 const fs = `invalid sample-rate frequency %q`
130 return fmt.Errorf(fs, s)
131 }
132 c.SampleRate = uint(1_000 * khz)
133 return nil
134 } else if strings.HasSuffix(lc, `hz`) {
135 lc = strings.TrimSuffix(lc, `hz`)
136 hz, err := strconv.ParseUint(lc, 10, 64)
137 if err != nil {
138 const fs = `invalid sample-rate frequency %q`
139 return fmt.Errorf(fs, s)
140 }
141 c.SampleRate = uint(hz)
142 return nil
143 }
144
145 c.Scripts = append(c.Scripts, s)
146 return nil
147 }
148
149 type encoding byte
150 type headerType byte
151 type sampleFormat byte
152
153 const (
154 directEncoding encoding = 1
155 uriEncoding encoding = 2
156
157 noHeader headerType = 1
158 wavHeader headerType = 2
159
160 int16BE sampleFormat = 1
161 int16LE sampleFormat = 2
162 float32BE sampleFormat = 3
163 float32LE sampleFormat = 4
164 )
165
166 // name2type normalizes keys used for type2settings
167 var name2type = map[string]string{
168 `datauri`: `data-uri`,
169 `dataurl`: `data-uri`,
170 `data-uri`: `data-uri`,
171 `data-url`: `data-uri`,
172 `uri`: `data-uri`,
173 `url`: `data-uri`,
174
175 `raw`: `raw`,
176 `raw16be`: `raw16be`,
177 `raw16le`: `raw16le`,
178 `raw32be`: `raw32be`,
179 `raw32le`: `raw32le`,
180
181 `audio/x-wav`: `wave-16`,
182 `audio/x-wave`: `wave-16`,
183 `wav`: `wave-16`,
184 `wave`: `wave-16`,
185 `wav16`: `wave-16`,
186 `wave16`: `wave-16`,
187 `wav-16`: `wave-16`,
188 `wave-16`: `wave-16`,
189 `x-wav`: `wave-16`,
190 `x-wave`: `wave-16`,
191
192 `wav16uri`: `wave-16-uri`,
193 `wave-16-uri`: `wave-16-uri`,
194
195 `wav32uri`: `wave-32-uri`,
196 `wave-32-uri`: `wave-32-uri`,
197
198 `wav32`: `wave-32`,
199 `wave32`: `wave-32`,
200 `wav-32`: `wave-32`,
201 `wave-32`: `wave-32`,
202 }
203
204 // outputSettings are format-specific settings which are controlled by the
205 // output-format option on the cmd-line
206 type outputSettings struct {
207 Encoding encoding
208 Header headerType
209 Samples sampleFormat
210 }
211
212 // type2settings translates output-format names into the specific settings
213 // these imply
214 var type2settings = map[string]outputSettings{
215 ``: {directEncoding, wavHeader, int16LE},
216
217 `data-uri`: {uriEncoding, wavHeader, int16LE},
218 `raw`: {directEncoding, noHeader, int16LE},
219 `raw16be`: {directEncoding, noHeader, int16BE},
220 `raw16le`: {directEncoding, noHeader, int16LE},
221 `wave-16`: {directEncoding, wavHeader, int16LE},
222 `wave-16-uri`: {uriEncoding, wavHeader, int16LE},
223
224 `raw32be`: {directEncoding, noHeader, float32BE},
225 `raw32le`: {directEncoding, noHeader, float32LE},
226 `wave-32`: {directEncoding, wavHeader, float32LE},
227 `wave-32-uri`: {uriEncoding, wavHeader, float32LE},
228 }
229
230 // outputConfig has all the info the core of this app needs to make sound
231 type outputConfig struct {
232 // Scripts has the source codes of all scripts for all channels
233 Scripts []string
234
235 // MaxTime is the play duration of the resulting sound
236 MaxTime float64
237
238 // SampleRate is the number of samples per second for all channels
239 SampleRate uint32
240
241 // all the configuration details needed to emit output
242 outputSettings
243 }
244
245 // newOutputConfig is the constructor for type outputConfig, translating the
246 // cmd-line info from type config
247 func newOutputConfig(cfg config) (outputConfig, error) {
248 oc := outputConfig{
249 Scripts: cfg.Scripts,
250 MaxTime: cfg.MaxTime,
251 SampleRate: uint32(cfg.SampleRate),
252 }
253
254 if len(oc.Scripts) == 0 {
255 return oc, errors.New(`no formulas given`)
256 }
257
258 outFmt := strings.ToLower(strings.TrimSpace(cfg.To))
259 if alias, ok := name2type[outFmt]; ok {
260 outFmt = alias
261 }
262
263 set, ok := type2settings[outFmt]
264 if !ok {
265 const fs = `unsupported output format %q`
266 return oc, fmt.Errorf(fs, cfg.To)
267 }
268
269 oc.outputSettings = set
270 return oc, nil
271 }
272
273 // mimeType gives the format's corresponding MIME type, or an empty string
274 // if the type isn't URI-encodable
275 func (oc outputConfig) mimeType() string {
276 if oc.Header == wavHeader {
277 return `audio/x-wav`
278 }
279 return ``
280 }
281
282 func isBadNumber(f float64) bool {
283 return math.IsNaN(f) || math.IsInf(f, 0)
284 }
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/durations.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 "math"
30 "strconv"
31 "strings"
32 "time"
33 )
34
35 const (
36 day = 24 * time.Hour
37 week = 7 * day
38 normalYear = 365 * day
39
40 secondsInMinute = 60
41 secondsInHour = 3_600
42 secondsInDay = 24 * secondsInHour
43 secondsInWeek = 7 * secondsInDay
44 )
45
46 var (
47 ErrMisplacedDots = errors.New(`misplaced decimal dot in time-duration`)
48 )
49
50 // ParseDuration extends the stdlib time-duration parser to also allow some
51 // commonly-used time notations like
52 //
53 // MM:SS minutes and seconds
54 // HH:MM:SS hours, minutes, and seconds
55 // DD:HH:MM:SS days, hours, minutes, and seconds
56 // WW:DD:HH:MM:SS weeks, days, hours, minutes, and seconds
57 //
58 // Decimals are also supported, but only for the final seconds field.
59 // Again, this function also supports the stdlib time-duration notation.
60 func ParseDuration(s string) (time.Duration, error) {
61 s = strings.TrimSpace(s)
62 if s == "" {
63 return 0, errors.New(`can't parse time values from empty strings`)
64 }
65
66 // handle shortcuts for time units such as weeks and (normal) years
67 if s[len(s)-1] == 'w' {
68 f, err := strconv.ParseFloat(s[:len(s)-1], 64)
69 if err != nil {
70 return 0, err
71 }
72 return time.Duration(float64(week) * f), nil
73 }
74 if s[len(s)-1] == 'y' {
75 f, err := strconv.ParseFloat(s[:len(s)-1], 64)
76 if err != nil {
77 return 0, err
78 }
79 return time.Duration(float64(normalYear) * f), nil
80 }
81
82 // see if the stdlib can handle it directly
83 d, err := time.ParseDuration(s)
84 if err == nil {
85 return d, nil
86 }
87
88 return parseColonDuration(s)
89 }
90
91 // durationFragments helps func parseDuration keep track of all fields without
92 // depending on functionality from external packages, such as strings.Split
93 type durationFragments struct {
94 weeks int
95 days int
96 hours int
97 minutes int
98 seconds int
99
100 numfields int
101 }
102
103 func (f *durationFragments) update(n int) error {
104 f.numfields++
105 switch f.numfields {
106 case 1:
107 f.seconds = n
108 return nil
109
110 case 2:
111 f.minutes = f.seconds
112 f.seconds = n
113 return nil
114
115 case 3:
116 f.hours = f.minutes
117 f.minutes = f.seconds
118 f.seconds = n
119 return nil
120
121 case 4:
122 f.days = f.hours
123 f.hours = f.minutes
124 f.minutes = f.seconds
125 f.seconds = n
126 return nil
127
128 case 5:
129 f.weeks = f.days
130 f.days = f.hours
131 f.hours = f.minutes
132 f.minutes = f.seconds
133 f.seconds = n
134 return nil
135
136 default:
137 // weeks are the largest constant time unit there is
138 return errors.New(`semicolon-separated time fields stop at weeks`)
139 }
140 }
141
142 func (f durationFragments) duration() time.Duration {
143 d := time.Duration(f.weeks) * week
144 d += time.Duration(f.days) * day
145 d += time.Duration(f.hours) * time.Hour
146 d += time.Duration(f.minutes) * time.Minute
147 d += time.Duration(f.seconds) * time.Second
148 return d
149 }
150
151 // parseColonDuration handles HH:MM:SS-like strings for func ParseDuration
152 func parseColonDuration(s string) (time.Duration, error) {
153 n := 0 // value for current field
154 dec := false // was a decimal point found?
155 numdigits := 0 // how many digits current field has
156 frags := durationFragments{}
157
158 for _, r := range s {
159 switch r {
160 case '.':
161 // handle decimals
162 if dec {
163 return 0, ErrMisplacedDots
164 }
165 dec = true
166 // remember value for seconds
167 if err := frags.update(n); err != nil {
168 return 0, err
169 }
170 numdigits = 0
171 n = 0
172
173 case ':':
174 // switch to next fragment/group
175 if dec {
176 return 0, ErrMisplacedDots
177 }
178 if err := frags.update(n); err != nil {
179 return 0, err
180 }
181 numdigits = 0
182 n = 0
183
184 default:
185 // update value in current field
186 if r < '0' || r > '9' {
187 const m1 = `non-digits found in what's supposed`
188 const m2 = `to be a valid numeric substring`
189 const msg = m1 + ` ` + m2
190 return 0, errors.New(msg)
191 }
192 n *= 10
193 n += int(r - '0')
194 numdigits++
195 }
196 }
197
198 // handle subsecond values: seconds are already counted for in this case
199 if dec {
200 return frags.duration() + fractionalSecond(n, numdigits), nil
201 }
202
203 // remember value for seconds
204 if err := frags.update(n); err != nil {
205 return 0, err
206 }
207 return frags.duration(), nil
208 }
209
210 // fractionalSecond turns the int-pair (mantissa, -log10) into the sub-second
211 // time-duration it represents
212 func fractionalSecond(fraction int, numdigits int) time.Duration {
213 nd := math.Pow10(numdigits)
214 return time.Duration(fraction) * time.Second / time.Duration(int64(nd))
215 }
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 }
File: ./zj/zj.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 zj
26
27 import (
28 "bufio"
29 "encoding/json"
30 "errors"
31 "io"
32 "os"
33 "strconv"
34 "strings"
35 "unicode/utf8"
36 )
37
38 const info = `
39 zj [keys/indices...]
40
41 Zoom Json digs into subsets of the JSON data read from the standard input.
42 `
43
44 // sets keeps track of 2 sets: one for integer indices, the other for string
45 // keys; the sets are reused across recursive calls of func `zoom` to save
46 // on memory/allocations
47 type sets struct {
48 indices map[int]struct{}
49 keys map[string]struct{}
50 }
51
52 // dictionary is a map which also remembers the order of its keys
53 type dictionary struct {
54 Keys []string
55 Map map[string]any
56 }
57
58 func Main() {
59 args := os.Args[1:]
60
61 if len(args) > 0 {
62 switch args[0] {
63 case `-h`, `--h`, `-help`, `--help`:
64 os.Stdout.WriteString(info[1:])
65 return
66 }
67 }
68
69 if len(args) > 0 && args[0] == `--` {
70 args = args[1:]
71 }
72
73 data, err := load(os.Stdin)
74 if err != nil {
75 os.Stderr.WriteString(err.Error())
76 os.Stderr.WriteString("\n")
77 os.Exit(1)
78 return
79 }
80
81 var avoid sets
82 avoid.indices = make(map[int]struct{})
83 avoid.keys = make(map[string]struct{})
84
85 data, err = zoom(data, args, &avoid)
86 if err != nil && err != io.EOF {
87 os.Stderr.WriteString(err.Error())
88 os.Stderr.WriteString("\n")
89 os.Exit(1)
90 return
91 }
92
93 if err := json0(os.Stdout, data); err != nil && err != io.EOF {
94 os.Stderr.WriteString(err.Error())
95 os.Stderr.WriteString("\n")
96 os.Exit(1)
97 return
98 }
99 }
100
101 func load(r io.Reader) (any, error) {
102 // dec := json.NewDecoder(r)
103 dec := json.NewDecoder(bufio.NewReaderSize(r, 32*1024))
104 // avoid parsing numbers, so unusually-long numbers are kept verbatim,
105 // even if JSON parsers aren't required to guarantee such input-fidelity
106 // for numbers
107 dec.UseNumber()
108
109 t, err := dec.Token()
110 if err == io.EOF {
111 return nil, errors.New(`input has no JSON values`)
112 }
113
114 data, err := loadToken(dec, t)
115 if err != nil {
116 return data, err
117 }
118
119 _, err = dec.Token()
120 if err == io.EOF {
121 // input is over, so it's a success
122 return data, nil
123 }
124
125 if err == nil {
126 // a successful `read` is a failure, as it means there are
127 // trailing JSON tokens
128 return data, errors.New(`unexpected trailing data`)
129 }
130
131 // any other error
132 return data, err
133 }
134
135 // loadToken handles recursion for func load
136 func loadToken(dec *json.Decoder, t json.Token) (any, error) {
137 switch t := t.(type) {
138 case json.Delim:
139 switch t {
140 case json.Delim('['):
141 return loadArray(dec)
142 case json.Delim('{'):
143 return loadObject(dec)
144 default:
145 return nil, errors.New(`unsupported JSON syntax ` + string(t))
146 }
147
148 case nil, bool, json.Number, string:
149 return t, nil
150
151 default:
152 // return nil, fmt.Errorf(`unsupported token type %T`, t)
153 return nil, errors.New(`invalid JSON token`)
154 }
155 }
156
157 // loadArray handles arrays for func loadToken
158 func loadArray(dec *json.Decoder) ([]any, error) {
159 var items []any
160
161 for i := 0; true; i++ {
162 t, err := dec.Token()
163 if err == io.EOF {
164 return items, errors.New(`end of JSON before array was closed`)
165 }
166 if err != nil {
167 return items, err
168 }
169
170 if t == json.Delim(']') {
171 return items, nil
172 }
173
174 v, err := loadToken(dec, t)
175 if err != nil {
176 return items, err
177 }
178 items = append(items, v)
179 }
180
181 // make the compiler happy
182 return items, nil
183 }
184
185 // loadObject handles objects for func loadToken
186 func loadObject(dec *json.Decoder) (dictionary, error) {
187 var items dictionary
188
189 for i := 0; true; i++ {
190 t, err := dec.Token()
191 if err == io.EOF {
192 return items, errors.New(`end of JSON before object was closed`)
193 }
194 if err != nil {
195 return items, err
196 }
197
198 if t == json.Delim('}') {
199 return items, nil
200 }
201
202 k, ok := t.(string)
203 if !ok {
204 return items, errors.New(`expected a string for a key-value pair`)
205 }
206
207 t, err = dec.Token()
208 if err == io.EOF {
209 return items, errors.New(`expected a value for a key-value pair`)
210 }
211
212 v, err := loadToken(dec, t)
213 if err != nil {
214 return items, err
215 }
216
217 if i == 0 {
218 items.Map = make(map[string]any)
219 }
220 if _, ok := items.Map[k]; !ok {
221 items.Keys = append(items.Keys, k)
222 }
223 items.Map[k] = v
224 }
225
226 // make the compiler happy
227 return items, nil
228 }
229
230 func zoom(data any, keys []string, avoid *sets) (any, error) {
231 for i, k := range keys {
232 switch v := data.(type) {
233 case nil:
234 return v, errors.New(`too many keys: can't zoom a null value`)
235
236 case bool:
237 return v, errors.New(`too many keys: can't zoom a boolean value`)
238
239 case json.Number:
240 return v, errors.New(`too many keys: can't zoom a number`)
241
242 case string:
243 v, err := zoomString(v, k)
244 if err != nil {
245 return v, err
246 }
247 data = v
248
249 case []any:
250 switch k {
251 case `.`:
252 return loopZoomArray(v, keys[i+1:], avoid)
253 case `+`:
254 return pickArrayItems(v, keys[i+1:])
255 case `-`:
256 clear(avoid.indices)
257 appendIndices(avoid.indices, v, keys[i+1:])
258 return dropArrayItems(v, avoid.indices)
259 }
260
261 res, err := zoomArray(v, k)
262 if err != nil {
263 return data, err
264 }
265 data = res
266
267 case dictionary:
268 if v, ok := v.Map[k]; ok {
269 data = v
270 continue
271 }
272
273 switch k {
274 case `+`:
275 return pickObjectItems(v, keys[i+1:])
276 case `-`:
277 clear(avoid.keys)
278 for _, k := range keys[i+1:] {
279 avoid.keys[k] = struct{}{}
280 }
281 return dropObjectItems(v, avoid.keys)
282 }
283
284 if _, v, ok := matchObjectKey(v, k); ok {
285 data = v
286 } else {
287 data = nil
288 }
289
290 default:
291 return v, errors.New(`too many keys: can't zoom basic values`)
292 }
293 }
294 return data, nil
295 }
296
297 func zoomArray(items []any, k string) (any, error) {
298 // trim leading spaces
299 for len(k) > 0 && k[0] == ' ' {
300 k = k[1:]
301 }
302
303 // trim trailing spaces
304 for len(k) > 0 && k[len(k)-1] == ' ' {
305 k = k[:len(k)-1]
306 }
307
308 if i, j, ok := tryArraySlice(items, k); ok {
309 if j >= len(items) {
310 j = len(items)
311 }
312 if i < 0 || j < 0 || i > j {
313 return []any(nil), nil
314 }
315 return items[i:j], nil
316 }
317
318 i, err := strconv.ParseInt(k, 10, 64)
319 if err != nil {
320 return nil, nil
321 }
322
323 if i < 0 {
324 i += int64(len(items))
325 }
326
327 if 0 <= i && i < int64(len(items)) {
328 return items[i], nil
329 }
330 return nil, nil
331 }
332
333 func tryArraySlice(items []any, k string) (i int, j int, ok bool) {
334 if k == `` {
335 return 0, 0, false
336 }
337
338 colon := strings.IndexByte(k, ':')
339 if colon < 0 {
340 if dots := indexPair(k, '.', '.'); dots >= 0 {
341 return tryIncArraySlice(items, k, dots)
342 }
343
344 return 0, 0, false
345 }
346
347 // handle omitted/implied starting 0
348 if colon == 0 {
349 j, err := strconv.ParseInt(k[colon+1:], 10, 64)
350 if err != nil {
351 return 0, 0, false
352 }
353
354 if j < 0 {
355 j += int64(len(items))
356 }
357
358 return 0, int(j), true
359 }
360
361 // handle omitted/implied until the end
362 if colon == len(k)-1 {
363 i, err := strconv.ParseInt(k[:colon], 10, 64)
364 if err != nil {
365 return 0, 0, false
366 }
367
368 if i < 0 {
369 i += int64(len(items))
370 }
371
372 return int(i), len(items), true
373 }
374
375 start, err := strconv.ParseInt(k[:colon], 10, 64)
376 if err != nil {
377 return 0, 0, false
378 }
379
380 if start < 0 {
381 start += int64(len(items))
382 }
383
384 end, err := strconv.ParseInt(k[colon+1:], 10, 64)
385 if err != nil {
386 return 0, 0, false
387 }
388
389 if end < 0 {
390 end += int64(len(items))
391 }
392
393 return int(start), int(end), ok
394 }
395
396 func tryIncArraySlice(items []any, k string, dots int) (i int, j int, ok bool) {
397 if k == `` {
398 return 0, 0, false
399 }
400
401 if dots < 0 {
402 return 0, 0, false
403 }
404
405 // handle omitted/implied starting 0
406 if dots == 0 {
407 j, err := strconv.ParseInt(k[dots+2:], 10, 64)
408 if err != nil {
409 return 0, 0, false
410 }
411
412 if j < 0 {
413 j += int64(len(items))
414 }
415 if j >= 0 {
416 j++
417 }
418
419 return 0, int(j), true
420 }
421
422 // handle omitted/implied until the end
423 if dots == len(k)-1 {
424 i, err := strconv.ParseInt(k[:dots], 10, 64)
425 if err != nil {
426 return 0, 0, false
427 }
428
429 if i < 0 {
430 i += int64(len(items))
431 }
432
433 return int(i), len(items), true
434 }
435
436 start, err := strconv.ParseInt(k[:dots], 10, 64)
437 if err != nil {
438 return 0, 0, false
439 }
440
441 if start < 0 {
442 start += int64(len(items))
443 }
444
445 end, err := strconv.ParseInt(k[dots+2:], 10, 64)
446 if err != nil {
447 return 0, 0, false
448 }
449
450 if end < 0 {
451 end += int64(len(items))
452 }
453 if end >= 0 {
454 end++
455 }
456 return int(start), int(end), ok
457 }
458
459 func matchObjectKey(items dictionary, k string) (match string, v any, ok bool) {
460 // first, try direct key lookup
461 if v, ok := items.Map[k]; ok {
462 return k, v, true
463 }
464
465 // second, try case-insensitive key lookup
466 for s := range items.Map {
467 if strings.EqualFold(k, s) {
468 return s, items.Map[s], true
469 }
470 }
471
472 // finally, try integer/index lookup
473 i, err := strconv.ParseInt(k, 10, 64)
474 if err != nil {
475 return ``, nil, false
476 }
477
478 if i < 0 {
479 i += int64(len(items.Keys))
480 }
481
482 if 0 <= i && i < int64(len(items.Keys)) {
483 k := items.Keys[i]
484 return k, items.Map[k], true
485 }
486
487 // nothing worked
488 return ``, nil, false
489 }
490
491 func zoomString(s string, k string) (string, error) {
492 if i, j, ok := tryRuneSlice(s, k); ok {
493 if i > j {
494 return ``, nil
495 }
496 return sliceRunes(s, i, j), nil
497 }
498
499 i, err := strconv.ParseInt(k, 10, 64)
500 if err != nil {
501 return ``, err
502 }
503
504 // don't bother looping when the index given is obviously out of bounds
505 if int(i) >= len(s) || int(-i) > len(s) {
506 return ``, nil
507 }
508
509 if i < 0 {
510 // shrink string backward from the end
511 for len(s) > 0 && i < 0 {
512 _, size := utf8.DecodeLastRuneInString(s)
513 s = s[:len(s)-size]
514 i++
515 }
516
517 if len(s) > 0 && i == 0 {
518 _, size := utf8.DecodeLastRuneInString(s)
519 return s[len(s)-size:], nil
520 }
521 return ``, nil
522 }
523
524 // shrink string forward from the start
525 for len(s) > 0 && i > 0 {
526 _, size := utf8.DecodeRuneInString(s)
527 s = s[size:]
528 i--
529 }
530
531 if len(s) > 0 && i == 0 {
532 _, size := utf8.DecodeRuneInString(s)
533 return s[:size], nil
534 }
535 return ``, nil
536 }
537
538 func tryRuneSlice(s string, k string) (i int, j int, ok bool) {
539 if k == `` {
540 return 0, 0, false
541 }
542
543 colon := strings.IndexByte(k, ':')
544 if colon < 0 {
545 return 0, 0, false
546 }
547
548 // handle omitted/implied starting 0
549 if colon == 0 {
550 j, err := strconv.ParseInt(k[colon+1:], 10, 64)
551 if err != nil {
552 return 0, 0, false
553 }
554
555 return 0, int(j), true
556 }
557
558 // handle omitted/implied until the end
559 if colon == len(k)-1 {
560 i, err := strconv.ParseInt(k[:colon], 10, 64)
561 if err != nil {
562 return 0, 0, false
563 }
564
565 return int(i), len(s), true
566 }
567
568 start, err := strconv.ParseInt(k[:colon], 10, 64)
569 if err != nil {
570 return 0, 0, false
571 }
572
573 end, err := strconv.ParseInt(k[colon+1:], 10, 64)
574 if err != nil {
575 return 0, 0, false
576 }
577
578 return int(start), int(end), ok
579 }
580
581 func sliceRunes(s string, i int, j int) string {
582 if i >= j {
583 return ``
584 }
585
586 // to do: backward-indexing
587 if i < 0 || j < 0 {
588 return ``
589 }
590
591 // don't bother looping when the index given is obviously out of bounds
592 if int(i) >= len(s) || int(-i) > len(s) {
593 return ``
594 }
595
596 // skip leading runes, according to the first index
597 for len(s) > 0 && i > 0 {
598 _, size := utf8.DecodeRuneInString(s)
599 s = s[size:]
600 i--
601 }
602
603 if len(s) == 0 {
604 return ``
605 }
606
607 end := 0
608 rest := s
609 for len(rest) > 0 && j > 0 {
610 _, size := utf8.DecodeRuneInString(rest)
611 rest = rest[size:]
612 end += size
613 j--
614 }
615
616 if len(s) > 0 && j == 0 {
617 return s[:end]
618 }
619 return ``
620 }
621
622 func json0(w io.Writer, data any) error {
623 bw := bufio.NewWriterSize(w, 32*1024)
624 defer bw.Flush()
625
626 err := writeValue(bw, data)
627 bw.WriteByte('\n')
628
629 if err == io.EOF {
630 return nil
631 }
632 return err
633 }
634
635 func loopZoomArray(items []any, keys []string, avoid *sets) (any, error) {
636 res := items[:0]
637 for _, v := range items {
638 v, err := zoom(v, keys, avoid)
639 if err != nil {
640 return res, err
641 }
642 res = append(res, v)
643 }
644 return res, nil
645 }
646
647 func pickArrayItems(items []any, keys []string) (any, error) {
648 res := items[:0]
649 for _, k := range keys {
650 v, err := zoomArray(items, k)
651 if err != nil {
652 return res, err
653 }
654 res = append(res, v)
655 }
656 return res, nil
657 }
658
659 func dropArrayItems(items []any, avoid map[int]struct{}) (any, error) {
660 res := items[:0]
661 for i, v := range items {
662 if _, ok := avoid[i]; ok {
663 continue
664 }
665 res = append(res, v)
666 }
667 return res, nil
668 }
669
670 func pickObjectItems(items dictionary, keys []string) (any, error) {
671 var res dictionary
672 res.Keys = items.Keys[:0]
673 res.Map = items.Map
674
675 for _, k := range keys {
676 match, _, ok := matchObjectKey(items, k)
677 if !ok {
678 continue
679 }
680
681 if _, ok := res.Map[match]; !ok {
682 res.Keys = append(res.Keys, match)
683 }
684 }
685
686 return res, nil
687 }
688
689 func dropObjectItems(items dictionary, avoid map[string]struct{}) (any, error) {
690 var res dictionary
691 res.Keys = items.Keys[:0]
692 res.Map = items.Map
693
694 for _, k := range items.Keys {
695 if hasFold(avoid, k) {
696 continue
697 }
698
699 if _, ok := res.Map[k]; !ok {
700 res.Keys = append(res.Keys, k)
701 }
702 }
703
704 return res, nil
705 }
706
707 func hasFold(avoid map[string]struct{}, s string) bool {
708 for v := range avoid {
709 if v == s || strings.EqualFold(v, s) {
710 return true
711 }
712 }
713 return false
714 }
715
716 func appendIndices(dest map[int]struct{}, items []any, keys []string) {
717 for _, k := range keys {
718 i, err := strconv.ParseInt(k, 10, 64)
719 if err != nil {
720 continue
721 }
722
723 if i < 0 {
724 i += int64(len(items))
725 }
726
727 if 0 <= i && i < int64(len(items)) {
728 dest[int(i)] = struct{}{}
729 }
730 }
731 }
732
733 func writeValue(w *bufio.Writer, data any) error {
734 switch data := data.(type) {
735 case nil:
736 return writeKeyword(w, `null`)
737 case bool:
738 if data {
739 return writeKeyword(w, `true`)
740 }
741 return writeKeyword(w, `false`)
742 case json.Number:
743 if _, err := w.WriteString(data.String()); err != nil {
744 return io.EOF
745 }
746 return nil
747 case string:
748 return writeEscapedString(w, data)
749 case []any:
750 return writeArray(w, data)
751 case dictionary:
752 return writeObject(w, data)
753 default:
754 return errors.New(`invalid JSON value`)
755 }
756 }
757
758 func writeByte(w *bufio.Writer, b byte) error {
759 err := w.WriteByte(b)
760 if err != nil {
761 return io.EOF
762 }
763 return nil
764 }
765
766 func writeKeyword(w *bufio.Writer, s string) error {
767 if _, err := w.WriteString(s); err == nil {
768 return nil
769 }
770 return io.EOF
771 }
772
773 func writeEscapedString(w *bufio.Writer, s string) error {
774 if !needsEscaping(s) {
775 w.WriteByte('"')
776 w.WriteString(s)
777 return writeByte(w, '"')
778 }
779
780 w.WriteByte('"')
781
782 for _, r := range s {
783 if ' ' <= r && r <= '~' && r != '\\' && r != '"' {
784 w.WriteRune(r)
785 continue
786 }
787
788 switch r {
789 case '\\':
790 w.WriteString(`\\`)
791 case '"':
792 w.WriteString(`\"`)
793 default:
794 writeEscapedHex(w, r)
795 }
796 }
797
798 return writeByte(w, '"')
799 }
800
801 func writeEscapedHex(w *bufio.Writer, r rune) error {
802 w.WriteByte('\\')
803 w.WriteByte('u')
804 writeHex(w, byte(r>>24))
805 writeHex(w, byte(r>>16))
806 writeHex(w, byte(r>>8))
807 writeHex(w, byte(r))
808 return nil
809 }
810
811 // writeHex is faster than calling fmt.Fprintf(w, `%04x`, b)
812 func writeHex(w *bufio.Writer, b byte) {
813 const hexDigits = `0123456789abcdef`
814 w.WriteByte(hexDigits[b>>4])
815 w.WriteByte(hexDigits[b&0x0f])
816 }
817
818 func needsEscaping(s string) bool {
819 for i := range s {
820 if b := s[i]; ' ' <= b && b <= '~' && b != '\\' && b != '"' {
821 continue
822 }
823 return true
824 }
825
826 return false
827 }
828
829 func writeArray(w *bufio.Writer, items []any) error {
830 w.WriteByte('[')
831 for i, v := range items {
832 if i > 0 {
833 if err := writeByte(w, ','); err != nil {
834 return err
835 }
836 }
837 if err := writeValue(w, v); err != nil {
838 return err
839 }
840 }
841 return writeByte(w, ']')
842 }
843
844 func writeObject(w *bufio.Writer, items dictionary) error {
845 w.WriteByte('{')
846 for i, k := range items.Keys {
847 if i > 0 {
848 if err := writeByte(w, ','); err != nil {
849 return err
850 }
851 }
852 writeEscapedString(w, k)
853 w.WriteByte(':')
854 if err := writeValue(w, items.Map[k]); err != nil {
855 return err
856 }
857 }
858 return writeByte(w, '}')
859 }
860
861 func indexPair(s string, x byte, y byte) int {
862 var cur, prev byte
863
864 for i := range s {
865 cur = s[i]
866 if prev == x && cur == y && i > 0 {
867 return i
868 }
869 prev = cur
870 }
871
872 return -1
873 }
File: ./zj/zj_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 zj
26
27 import (
28 "io"
29 "strings"
30 "testing"
31 )
32
33 func TestZoomJson(t *testing.T) {
34 var tests = []struct {
35 name string
36 input string
37 expected string
38 zoom string
39 }{
40 {`no-zoom number`, `123.456`, `123.456`, ``},
41 }
42
43 for _, tc := range tests {
44 t.Run(tc.name, func(t *testing.T) {
45 data, err := load(strings.NewReader(tc.input))
46 if err != nil {
47 t.Error(err)
48 return
49 }
50
51 var avoid sets
52 avoid.indices = make(map[int]struct{})
53 avoid.keys = make(map[string]struct{})
54
55 var keys []string
56 if tc.zoom != `` {
57 keys = strings.Split(tc.zoom, ` `)
58 }
59
60 data, err = zoom(data, keys, &avoid)
61 if err != nil {
62 t.Error(err)
63 return
64 }
65
66 var sb strings.Builder
67 err = json0(&sb, data)
68 if err != nil && err != io.EOF {
69 t.Error(err)
70 return
71 }
72
73 got := sb.String()
74 got, _ = strings.CutSuffix(got, "\n")
75 if got != tc.expected {
76 t.Errorf(`expected %q but got %q instead`, tc.expected, got)
77 return
78 }
79 })
80 }
81 }