File: j0/errors.go 1 package main 2 3 import ( 4 "errors" 5 "io" 6 "strconv" 7 ) 8 9 var ( 10 errCommentEarlyEnd = errors.New(`unexpected early-end of comment`) 11 errInputEarlyEnd = errors.New(`expected end of input data`) 12 errInvalidComment = errors.New(`expected / or *`) 13 errInvalidHex = errors.New(`expected a base-16 digit`) 14 errInvalidToken = errors.New(`invalid JSON token`) 15 errNoDigits = errors.New(`expected numeric digits`) 16 errNoStringQuote = errors.New(`expected " or '`) 17 errNoArrayComma = errors.New(`missing comma between array values`) 18 errNoObjectComma = errors.New(`missing comma between key-value pairs`) 19 errStringEarlyEnd = errors.New(`unexpected early-end of string`) 20 errExtraBytes = errors.New(`unexpected extra input bytes`) 21 22 errMultipleInputs = errors.New(`multiple inputs not allowed`) 23 24 // errNoMoreOutput is a generic dummy output-error, which is meant to be 25 // ultimately ignored, being just an excuse to quit the app immediately 26 // and successfully 27 errNoMoreOutput = errors.New(`no more output`) 28 ) 29 30 // isActualError is to figure out whether not to ignore an error, and thus 31 // show it as an error message 32 func isActualError(err error) bool { 33 return err != nil && err != io.EOF && err != errNoMoreOutput 34 } 35 36 // linePosError is a more descriptive kind of error, showing the source of 37 // the input-related problem, as 1-based a line/pos number pair in front 38 // of the error message 39 type linePosError struct { 40 // line is the 1-based line count from the input 41 line int 42 43 // pos is the 1-based `horizontal` position in its line 44 pos int 45 46 // err is the error message to `decorate` with the position info 47 err error 48 } 49 50 // Error satisfies the error interface 51 func (lpe linePosError) Error() string { 52 where := strconv.Itoa(lpe.line) + `:` + strconv.Itoa(lpe.pos) 53 return where + `: ` + lpe.err.Error() 54 } File: j0/info.txt 1 j0 [options...] [filepath...] 2 3 4 Json-0 converts/fixes JSON/pseudo-JSON input into valid minimal JSON output. 5 6 Besides minimizing bytes, this tool also adapts almost-JSON input into valid 7 JSON, since it ignores things like comments and trailing commas, neither of 8 which are supported in JSON, but which are still commonly used. 9 10 Output is always a single line, which ends with a line-feed. When not given 11 a filepath, input is read from the standard input. 12 13 14 When the input deviates from strictly-valid JSON, it can also use 15 16 - leading + signs in numbers 17 - leading . decimal-markers in numbers 18 - unquoted object keys 19 - single-quoted strings 20 - \x in strings, followed by hexadecimal pairs 21 - trailing commas in arrays and objects 22 - rest-of-line //-style comments 23 - /*-style comments, which end with */ and can span multiple lines 24 25 26 All options can either start with a single or a double-dash; some of the 27 options available, and their aliases, are 28 29 -help show this help message 30 -jsonl read input as lines, each of which is (pseudo)-JSON 31 -bytes turn input bytes into an array of numbers 32 -text read input as the unquoted/inner-part of a string 33 34 -b same as option -bytes 35 -h same as option -help 36 -jl same as option -jsonl 37 -s same as option -text 38 -str same as option -text 39 -string same as option -text 40 -t same as option -text File: j0/json.go 1 package main 2 3 import ( 4 "bufio" 5 "bytes" 6 "errors" 7 "io" 8 ) 9 10 // isIdentifier improves control-flow of func handleKey, when it handles 11 // unquoted object keys 12 var isIdentifier = [256]bool{ 13 '_': true, 14 15 '0': true, '1': true, '2': true, '3': true, '4': true, 16 '5': true, '6': true, '7': true, '8': true, '9': true, 17 18 'A': true, 'B': true, 'C': true, 'D': true, 'E': true, 'F': true, 19 'G': true, 'H': true, 'I': true, 'J': true, 'K': true, 'L': true, 20 'M': true, 'N': true, 'O': true, 'P': true, 'Q': true, 'R': true, 21 'S': true, 'T': true, 'U': true, 'V': true, 'W': true, 'X': true, 22 'Y': true, 'Z': true, 23 24 'a': true, 'b': true, 'c': true, 'd': true, 'e': true, 'f': true, 25 'g': true, 'h': true, 'i': true, 'j': true, 'k': true, 'l': true, 26 'm': true, 'n': true, 'o': true, 'p': true, 'q': true, 'r': true, 27 's': true, 't': true, 'u': true, 'v': true, 'w': true, 'x': true, 28 'y': true, 'z': true, 29 } 30 31 // matchHex both figures out if a byte is a valid ASCII hex-digit, by not 32 // being 0, and normalizes letter-case for the hex letters 33 var matchHex = [256]byte{ 34 '0': '0', '1': '1', '2': '2', '3': '3', '4': '4', 35 '5': '5', '6': '6', '7': '7', '8': '8', '9': '9', 36 'A': 'A', 'B': 'B', 'C': 'C', 'D': 'D', 'E': 'E', 'F': 'F', 37 'a': 'A', 'b': 'B', 'c': 'C', 'd': 'D', 'e': 'E', 'f': 'F', 38 } 39 40 // json0 converts JSON/pseudo-JSON into (valid) minimal JSON; this func 41 // avoids writing a trailing line-feed, leaving that up to its caller 42 func json0(w *bufio.Writer, r *bufio.Reader) error { 43 jr := jsonReader{r, 1, 1} 44 45 // input is already assumed to be UTF-8: a leading UTF-8 BOM (byte-order 46 // mark) gives no useful info if present, as UTF-8 leaves no ambiguity 47 // about byte-order by design 48 jr.skipUTF8BOM() 49 50 // ignore leading whitespace and/or comments 51 if err := jr.seekNext(); err != nil { 52 return err 53 } 54 55 // handle a single top-level JSON value 56 if err := handleValue(w, &jr); err != nil { 57 return err 58 } 59 60 // ignore trailing whitespace and/or comments 61 if err := jr.seekNext(); err != nil { 62 return err 63 } 64 65 // beyond trailing whitespace and/or comments, any more bytes 66 // make the whole input data invalid JSON 67 if _, ok := jr.peekByte(); ok { 68 return jr.improveError(errExtraBytes) 69 } 70 return nil 71 } 72 73 // jsonReader reads data via a buffer, keeping track of the input position: 74 // this in turn allows showing much more useful errors, when these happen 75 type jsonReader struct { 76 // r is the actual reader 77 r *bufio.Reader 78 79 // line is the 1-based line-counter for input bytes, and gives errors 80 // useful position info 81 line int 82 83 // pos is the 1-based `horizontal` position in its line, and gives 84 // errors useful position info 85 pos int 86 } 87 88 // improveError makes any error more useful, by giving it info about the 89 // current input-position, as a 1-based line/within-line-position pair 90 func (jr jsonReader) improveError(err error) error { 91 if _, ok := err.(linePosError); ok { 92 return err 93 } 94 95 if err == io.EOF { 96 return linePosError{jr.line, jr.pos, errInputEarlyEnd} 97 } 98 if err != nil { 99 return linePosError{jr.line, jr.pos, err} 100 } 101 return nil 102 } 103 104 // demandSyntax fails with an error when the next byte isn't the one given; 105 // when it is, the byte is then read/skipped, and a nil error is returned 106 func (jr *jsonReader) demandSyntax(syntax byte) error { 107 chunk, err := jr.r.Peek(1) 108 if err == io.EOF { 109 return jr.improveError(errInputEarlyEnd) 110 } 111 if err != nil { 112 return jr.improveError(err) 113 } 114 115 if len(chunk) < 1 || chunk[0] != syntax { 116 msg := `expected ` + string(rune(syntax)) 117 return jr.improveError(errors.New(msg)) 118 } 119 120 jr.readByte() 121 return nil 122 } 123 124 // updatePosInfo does what it says, given the byte just read separately 125 func (jr *jsonReader) updatePosInfo(b byte) { 126 if b == '\n' { 127 jr.line += 1 128 jr.pos = 1 129 } else { 130 jr.pos++ 131 } 132 } 133 134 // peekByte simplifies control-flow for various other funcs 135 func (jr jsonReader) peekByte() (b byte, ok bool) { 136 chunk, err := jr.r.Peek(1) 137 if err == nil && len(chunk) >= 1 { 138 return chunk[0], true 139 } 140 return 0, false 141 } 142 143 // readByte does what it says, updating the reader's position info 144 func (jr *jsonReader) readByte() (b byte, err error) { 145 b, err = jr.r.ReadByte() 146 if err == nil { 147 jr.updatePosInfo(b) 148 return b, nil 149 } 150 return b, jr.improveError(err) 151 } 152 153 // seekNext skips/seeks the next token, ignoring runs of whitespace symbols 154 // and comments, either single-line (starting with //) or general (starting 155 // with /* and ending with */) 156 func (jr *jsonReader) seekNext() error { 157 for { 158 b, ok := jr.peekByte() 159 if !ok { 160 return nil 161 } 162 163 // case ' ', '\t', '\f', '\v', '\r', '\n': 164 if b <= 32 { 165 // keep skipping whitespace bytes 166 b, _ := jr.readByte() 167 jr.updatePosInfo(b) 168 continue 169 } 170 171 if b != '/' { 172 // reached the next token 173 return nil 174 } 175 176 if err := jr.skipComment(); err != nil { 177 return err 178 } 179 180 // after comments, keep looking for more whitespace and/or comments 181 } 182 } 183 184 // skipComment helps func seekNext skip over comments, simplifying the latter 185 // func's control-flow 186 func (jr *jsonReader) skipComment() error { 187 err := jr.demandSyntax('/') 188 if err != nil { 189 return err 190 } 191 192 b, ok := jr.peekByte() 193 if !ok { 194 return jr.improveError(errInputEarlyEnd) 195 } 196 197 switch b { 198 case '/': 199 // handle single-line comments 200 return jr.skipLine() 201 202 case '*': 203 // handle (potentially) multi-line comments 204 return jr.skipGeneralComment() 205 206 default: 207 return jr.improveError(errInvalidComment) 208 } 209 } 210 211 // skipLine handles single-line comments for func skipComment 212 func (jr *jsonReader) skipLine() error { 213 for { 214 b, err := jr.r.ReadByte() 215 if err == io.EOF { 216 // end of input is fine in this case 217 return nil 218 } 219 if err != nil { 220 return err 221 } 222 223 jr.updatePosInfo(b) 224 if b == '\n' { 225 jr.line++ 226 return nil 227 } 228 } 229 } 230 231 // skipGeneralComment handles (potentially) multi-line comments for func 232 // skipComment 233 func (jr *jsonReader) skipGeneralComment() error { 234 var prev byte 235 for { 236 b, err := jr.readByte() 237 if err != nil { 238 return jr.improveError(errCommentEarlyEnd) 239 } 240 241 if prev == '*' && b == '/' { 242 return nil 243 } 244 if b == '\n' { 245 jr.line++ 246 } 247 prev = b 248 } 249 } 250 251 // skipUTF8BOM does what it says, if a UTF-8 BOM is present 252 func (jr *jsonReader) skipUTF8BOM() { 253 lead, err := jr.r.Peek(3) 254 if err == nil && bytes.HasPrefix(lead, []byte{0xef, 0xbb, 0xbf}) { 255 jr.readByte() 256 jr.readByte() 257 jr.readByte() 258 jr.pos += 3 259 } 260 } 261 262 // outputByte is a small wrapper on func WriteByte, which adapts any error 263 // into a custom dummy output-error, which is in turn meant to be ignored, 264 // being just an excuse to quit the app immediately and successfully 265 func outputByte(w *bufio.Writer, b byte) error { 266 err := w.WriteByte(b) 267 if err == nil { 268 return nil 269 } 270 return errNoMoreOutput 271 } 272 273 // handleArray handles arrays for func handleValue 274 func handleArray(w *bufio.Writer, jr *jsonReader) error { 275 if err := jr.demandSyntax('['); err != nil { 276 return err 277 } 278 w.WriteByte('[') 279 280 for n := 0; true; n++ { 281 // there may be whitespace/comments before the next comma 282 if err := jr.seekNext(); err != nil { 283 return err 284 } 285 286 // handle commas between values, as well as trailing ones 287 comma := false 288 b, _ := jr.peekByte() 289 if b == ',' { 290 jr.readByte() 291 comma = true 292 293 // there may be whitespace/comments before an ending ']' 294 if err := jr.seekNext(); err != nil { 295 return err 296 } 297 b, _ = jr.peekByte() 298 } 299 300 // handle end of array 301 if b == ']' { 302 jr.readByte() 303 w.WriteByte(']') 304 return nil 305 } 306 307 // don't forget commas between adjacent values 308 if n > 0 { 309 if !comma { 310 return errNoArrayComma 311 } 312 if err := outputByte(w, ','); err != nil { 313 return err 314 } 315 } 316 317 // handle the next value 318 if err := jr.seekNext(); err != nil { 319 return err 320 } 321 if err := handleValue(w, jr); err != nil { 322 return err 323 } 324 } 325 326 // make the compiler happy 327 return nil 328 } 329 330 // handleDigits helps various number-handling funcs do their job 331 func handleDigits(w *bufio.Writer, jr *jsonReader) error { 332 for n := 0; true; n++ { 333 b, _ := jr.peekByte() 334 335 // support `nice` long numbers by ignoring their underscores 336 if b == '_' { 337 jr.readByte() 338 continue 339 } 340 341 if '0' <= b && b <= '9' { 342 jr.readByte() 343 w.WriteByte(b) 344 continue 345 } 346 347 if n == 0 { 348 return errNoDigits 349 } 350 return nil 351 } 352 353 // make the compiler happy 354 return nil 355 } 356 357 // handleDot handles pseudo-JSON numbers which start with a decimal dot 358 func handleDot(w *bufio.Writer, jr *jsonReader) error { 359 if err := jr.demandSyntax('.'); err != nil { 360 return err 361 } 362 w.Write([]byte{'0', '.'}) 363 return handleDigits(w, jr) 364 } 365 366 // handleKey is used by func handleObjects and generalizes func handleString, 367 // by allowing unquoted object keys; it's not used anywhere else, as allowing 368 // unquoted string values is ambiguous with actual JSON-keyword values null, 369 // false, and true. 370 func handleKey(w *bufio.Writer, jr *jsonReader) error { 371 quote, ok := jr.peekByte() 372 if quote == '"' || quote == '\'' { 373 return handleString(w, jr) 374 } 375 if !ok { 376 return jr.improveError(errStringEarlyEnd) 377 } 378 379 w.WriteByte('"') 380 for { 381 if b, _ := jr.peekByte(); isIdentifier[b] { 382 jr.readByte() 383 w.WriteByte(b) 384 continue 385 } 386 387 w.WriteByte('"') 388 return nil 389 } 390 } 391 392 // trySimpleInner tries to handle (more quickly) inner-strings where all bytes 393 // are unescaped ASCII symbols: this is a very common case for strings, and is 394 // almost always the case for object keys; returns whether it succeeded, so 395 // this func's caller knows knows if it needs to do anything, the slower way 396 func trySimpleInner(w *bufio.Writer, jr *jsonReader, quote byte) (gotIt bool) { 397 chunk, _ := jr.r.Peek(64) 398 399 for i, b := range chunk { 400 if b < 32 || b > 127 || b == '\\' { 401 return false 402 } 403 if b != quote { 404 continue 405 } 406 407 // bulk-writing the chunk is this func's whole point 408 w.WriteByte('"') 409 w.Write(chunk[:i]) 410 w.WriteByte('"') 411 412 jr.r.Discard(i + 1) 413 return true 414 } 415 416 // maybe the inner-string is ok, but it's just longer than the chunk 417 return false 418 } 419 420 // handleKeyword is used by funcs handleFalse, handleNull, and handleTrue 421 func handleKeyword(w *bufio.Writer, jr *jsonReader, kw []byte) error { 422 for rest := kw; len(rest) > 0; rest = rest[1:] { 423 b, err := jr.readByte() 424 if err == nil && b == rest[0] { 425 // keywords given to this func have no line-feeds 426 jr.pos++ 427 continue 428 } 429 430 msg := `expected JSON value ` + string(kw) 431 return jr.improveError(errors.New(msg)) 432 } 433 434 w.Write(kw) 435 return nil 436 } 437 438 // handleNegative handles numbers starting with a negative sign for func 439 // handleValue 440 func handleNegative(w *bufio.Writer, jr *jsonReader) error { 441 if err := jr.demandSyntax('-'); err != nil { 442 return err 443 } 444 445 w.WriteByte('-') 446 if b, _ := jr.peekByte(); b == '.' { 447 jr.readByte() 448 w.Write([]byte{'0', '.'}) 449 return handleDigits(w, jr) 450 } 451 return handleNumber(w, jr) 452 } 453 454 // handleNumber handles numeric values/tokens, including invalid-JSON cases, 455 // such as values starting with a decimal dot 456 func handleNumber(w *bufio.Writer, jr *jsonReader) error { 457 // handle integer digits 458 if err := handleDigits(w, jr); err != nil { 459 return err 460 } 461 462 // handle optional decimal digits, starting with a leading dot 463 if b, _ := jr.peekByte(); b == '.' { 464 jr.readByte() 465 w.WriteByte('.') 466 return handleDigits(w, jr) 467 } 468 return nil 469 } 470 471 // handleObject handles objects for func handleValue 472 func handleObject(w *bufio.Writer, jr *jsonReader) error { 473 if err := jr.demandSyntax('{'); err != nil { 474 return err 475 } 476 w.WriteByte('{') 477 478 for npairs := 0; true; npairs++ { 479 // there may be whitespace/comments before the next comma 480 if err := jr.seekNext(); err != nil { 481 return err 482 } 483 484 // handle commas between key-value pairs, as well as trailing ones 485 comma := false 486 b, _ := jr.peekByte() 487 if b == ',' { 488 jr.readByte() 489 comma = true 490 491 // there may be whitespace/comments before an ending '}' 492 if err := jr.seekNext(); err != nil { 493 return err 494 } 495 b, _ = jr.peekByte() 496 } 497 498 // handle end of object 499 if b == '}' { 500 jr.readByte() 501 w.WriteByte('}') 502 return nil 503 } 504 505 // don't forget commas between adjacent key-value pairs 506 if npairs > 0 { 507 if !comma { 508 return errNoObjectComma 509 } 510 if err := outputByte(w, ','); err != nil { 511 return err 512 } 513 } 514 515 // handle the next pair's key 516 if err := jr.seekNext(); err != nil { 517 return err 518 } 519 if err := handleKey(w, jr); err != nil { 520 return err 521 } 522 523 // demand a colon right after the key 524 if err := jr.seekNext(); err != nil { 525 return err 526 } 527 if err := jr.demandSyntax(':'); err != nil { 528 return err 529 } 530 w.WriteByte(':') 531 532 // handle the next pair's value 533 if err := jr.seekNext(); err != nil { 534 return err 535 } 536 if err := handleValue(w, jr); err != nil { 537 return err 538 } 539 } 540 541 // make the compiler happy 542 return nil 543 } 544 545 // handlePositive handles numbers starting with a positive sign for func 546 // handleValue 547 func handlePositive(w *bufio.Writer, jr *jsonReader) error { 548 if err := jr.demandSyntax('+'); err != nil { 549 return err 550 } 551 552 // valid JSON isn't supposed to have leading pluses on numbers, so 553 // emit nothing for it, unlike for negative numbers 554 555 if b, _ := jr.peekByte(); b == '.' { 556 jr.readByte() 557 w.Write([]byte{'0', '.'}) 558 return handleDigits(w, jr) 559 } 560 return handleNumber(w, jr) 561 } 562 563 // handleString handles strings for funcs handleValue and handleObject, and 564 // supports both single-quotes and double-quotes, always emitting the latter 565 // in the output, of course 566 func handleString(w *bufio.Writer, jr *jsonReader) error { 567 quote, ok := jr.peekByte() 568 if !ok || (quote != '"' && quote != '\'') { 569 return errNoStringQuote 570 } 571 572 jr.readByte() 573 // try the quicker all-unescaped-ASCII handler 574 if trySimpleInner(w, jr, quote) { 575 return nil 576 } 577 578 // it's a non-trivial inner-string, so handle it byte-by-byte 579 w.WriteByte('"') 580 escaped := false 581 582 for { 583 b, err := jr.r.ReadByte() 584 if err != nil { 585 if err == io.EOF { 586 return jr.improveError(errStringEarlyEnd) 587 } 588 return jr.improveError(err) 589 } 590 591 if !escaped { 592 if b == '\\' { 593 escaped = true 594 continue 595 } 596 597 // handle end of string 598 if b == quote { 599 return outputByte(w, '"') 600 } 601 602 w.Write(escapedStringBytes[b]) 603 jr.updatePosInfo(b) 604 continue 605 } 606 607 // handle escaped items 608 escaped = false 609 610 switch b { 611 case 'u': 612 // \u needs exactly 4 hex-digits to follow it 613 w.Write([]byte{'\\', 'u'}) 614 if err := copyHex(w, 4, jr); err != nil { 615 return jr.improveError(err) 616 } 617 618 case 'x': 619 // JSON only supports 4 escaped hex-digits, so pad the 2 620 // expected hex-digits with 2 zeros 621 w.Write([]byte{'\\', 'u', '0', '0'}) 622 if err := copyHex(w, 2, jr); err != nil { 623 return jr.improveError(err) 624 } 625 626 case 't', 'f', 'r', 'n', 'b', '\\', '"': 627 // handle valid-JSON escaped string sequences 628 w.WriteByte('\\') 629 w.WriteByte(b) 630 631 // case '\'': 632 // // escaped single-quotes aren't standard JSON, but they can 633 // // be handy when the input uses non-standard single-quoted 634 // // strings 635 // w.WriteByte('\'') 636 637 default: 638 // return jr.decorateError(unexpectedByte{b}) 639 w.Write(escapedStringBytes[b]) 640 } 641 } 642 } 643 644 // copyHex handles a run of hex-digits for func handleString, starting right 645 // after the leading `\u` (or `\x`) part; this func doesn't `improve` its 646 // errors with position info: that's up to the caller 647 func copyHex(w *bufio.Writer, n int, jr *jsonReader) error { 648 for i := 0; i < n; i++ { 649 b, err := jr.r.ReadByte() 650 if err == io.EOF { 651 return errStringEarlyEnd 652 } 653 if err != nil { 654 return err 655 } 656 657 jr.updatePosInfo(b) 658 659 if b := matchHex[b]; b != 0 { 660 w.WriteByte(b) 661 continue 662 } 663 664 return errInvalidHex 665 } 666 667 return nil 668 } 669 670 // handleValue is a generic JSON-token handler, which allows the recursive 671 // behavior to handle any kind of JSON/pseudo-JSON input 672 func handleValue(w *bufio.Writer, jr *jsonReader) error { 673 chunk, err := jr.r.Peek(1) 674 if err == nil && len(chunk) >= 1 { 675 return handleValueDispatch(w, jr, chunk[0]) 676 } 677 678 if err == io.EOF { 679 return jr.improveError(errInputEarlyEnd) 680 } 681 return jr.improveError(errInputEarlyEnd) 682 } 683 684 // handleValueDispatch simplifies control-flow for func handleValue 685 func handleValueDispatch(w *bufio.Writer, jr *jsonReader, b byte) error { 686 switch b { 687 case 'f': 688 return handleKeyword(w, jr, []byte{'f', 'a', 'l', 's', 'e'}) 689 case 'n': 690 return handleKeyword(w, jr, []byte{'n', 'u', 'l', 'l'}) 691 case 't': 692 return handleKeyword(w, jr, []byte{'t', 'r', 'u', 'e'}) 693 case '.': 694 return handleDot(w, jr) 695 case '+': 696 return handlePositive(w, jr) 697 case '-': 698 return handleNegative(w, jr) 699 case '0', '1', '2', '3', '4', '5', '6', '7', '8', '9': 700 return handleNumber(w, jr) 701 case '\'', '"': 702 return handleString(w, jr) 703 case '[': 704 return handleArray(w, jr) 705 case '{': 706 return handleObject(w, jr) 707 default: 708 return jr.improveError(errInvalidToken) 709 } 710 } File: j0/json_test.go 1 package main 2 3 import ( 4 "bufio" 5 "bytes" 6 "strings" 7 "testing" 8 ) 9 10 func TestJSON0(t *testing.T) { 11 var tests = []struct { 12 Input string 13 Expected string 14 }{ 15 {`false`, `false`}, 16 {`null`, `null`}, 17 {` true `, `true`}, 18 19 {`0`, `0`}, 20 {`1`, `1`}, 21 {`2`, `2`}, 22 {`3`, `3`}, 23 {`4`, `4`}, 24 {`5`, `5`}, 25 {`6`, `6`}, 26 {`7`, `7`}, 27 {`8`, `8`}, 28 {`9`, `9`}, 29 30 {` .345`, `0.345`}, 31 {` -.345`, `-0.345`}, 32 {` +.345`, `0.345`}, 33 {` +123.345`, `123.345`}, 34 {` +.345`, `0.345`}, 35 {` 123.34523`, `123.34523`}, 36 {` 123.34_523`, `123.34523`}, 37 {` 123_456.123`, `123456.123`}, 38 39 {`""`, `""`}, 40 {`''`, `""`}, 41 {`"\""`, `"\""`}, 42 {`'\"'`, `"\""`}, 43 {`'\''`, `"'"`}, 44 {`'abc\u0e9A'`, `"abc\u0E9A"`}, 45 {`'abc\x1f[0m'`, `"abc\u001F[0m"`}, 46 47 {`[ ]`, `[]`}, 48 {`[ , ]`, `[]`}, 49 {`[.345, false,null , ]`, `[0.345,false,null]`}, 50 51 {`{ }`, `{}`}, 52 {`{ , }`, `{}`}, 53 54 { 55 `{ 'abc': .345, "def" : false, 'xyz':null , }`, 56 `{"abc":0.345,"def":false,"xyz":null}`, 57 }, 58 59 {`{0problems:123,}`, `{"0problems":123}`}, 60 {`{0_problems:123}`, `{"0_problems":123}`}, 61 } 62 63 for _, tc := range tests { 64 t.Run(tc.Input, func(t *testing.T) { 65 var out strings.Builder 66 w := bufio.NewWriter(&out) 67 r := bufio.NewReader(strings.NewReader(tc.Input)) 68 if err := json0(w, r); isActualError(err) { 69 t.Fatal(err) 70 return 71 } 72 // don't forget to flush the buffer, or output will be empty 73 w.Flush() 74 75 s := out.String() 76 if s != tc.Expected { 77 t.Fatalf("<got>\n%s\n<expected>\n%s", s, tc.Expected) 78 return 79 } 80 }) 81 } 82 } 83 84 func TestEscapedStringBytes(t *testing.T) { 85 var escaped = map[rune][]byte{ 86 '\x00': {'\\', 'u', '0', '0', '0', '0'}, 87 '\x01': {'\\', 'u', '0', '0', '0', '1'}, 88 '\x02': {'\\', 'u', '0', '0', '0', '2'}, 89 '\x03': {'\\', 'u', '0', '0', '0', '3'}, 90 '\x04': {'\\', 'u', '0', '0', '0', '4'}, 91 '\x05': {'\\', 'u', '0', '0', '0', '5'}, 92 '\x06': {'\\', 'u', '0', '0', '0', '6'}, 93 '\x07': {'\\', 'u', '0', '0', '0', '7'}, 94 '\x0b': {'\\', 'u', '0', '0', '0', 'b'}, 95 '\x0e': {'\\', 'u', '0', '0', '0', 'e'}, 96 '\x0f': {'\\', 'u', '0', '0', '0', 'f'}, 97 '\x10': {'\\', 'u', '0', '0', '1', '0'}, 98 '\x11': {'\\', 'u', '0', '0', '1', '1'}, 99 '\x12': {'\\', 'u', '0', '0', '1', '2'}, 100 '\x13': {'\\', 'u', '0', '0', '1', '3'}, 101 '\x14': {'\\', 'u', '0', '0', '1', '4'}, 102 '\x15': {'\\', 'u', '0', '0', '1', '5'}, 103 '\x16': {'\\', 'u', '0', '0', '1', '6'}, 104 '\x17': {'\\', 'u', '0', '0', '1', '7'}, 105 '\x18': {'\\', 'u', '0', '0', '1', '8'}, 106 '\x19': {'\\', 'u', '0', '0', '1', '9'}, 107 '\x1a': {'\\', 'u', '0', '0', '1', 'a'}, 108 '\x1b': {'\\', 'u', '0', '0', '1', 'b'}, 109 '\x1c': {'\\', 'u', '0', '0', '1', 'c'}, 110 '\x1d': {'\\', 'u', '0', '0', '1', 'd'}, 111 '\x1e': {'\\', 'u', '0', '0', '1', 'e'}, 112 '\x1f': {'\\', 'u', '0', '0', '1', 'f'}, 113 114 '\t': {'\\', 't'}, 115 '\f': {'\\', 'f'}, 116 '\b': {'\\', 'b'}, 117 '\r': {'\\', 'r'}, 118 '\n': {'\\', 'n'}, 119 '\\': {'\\', '\\'}, 120 '"': {'\\', '"'}, 121 } 122 123 if n := len(escapedStringBytes); n != 256 { 124 t.Fatalf(`expected 256 entries, instead of %d`, n) 125 return 126 } 127 128 for i, v := range escapedStringBytes { 129 exp := []byte{byte(i)} 130 if esc, ok := escaped[rune(i)]; ok { 131 exp = esc 132 } 133 134 if !bytes.Equal(v, exp) { 135 t.Fatalf("%d: expected %#v, got %#v", i, exp, v) 136 return 137 } 138 } 139 } File: j0/loading.go 1 package main 2 3 import ( 4 "errors" 5 "io" 6 "os" 7 ) 8 9 func open(path string) (io.ReadCloser, error) { 10 if path == `-` { 11 // return io.NopCloser(os.Stdin), nil 12 return os.Stdin, nil 13 } 14 15 f, err := os.Open(path) 16 if err != nil { 17 // on windows, file-not-found error messages may mention `CreateFile`, 18 // even when trying to open files in read-only mode 19 return nil, errors.New(`can't open file named ` + path) 20 } 21 return f, nil 22 } File: j0/loading_web.go.txt 1 //go:build web 2 3 package main 4 5 import ( 6 "errors" 7 "io" 8 "net/http" 9 "os" 10 "strings" 11 ) 12 13 func open(path string) (io.ReadCloser, error) { 14 if path == `-` { 15 return io.NopCloser(os.Stdin), nil 16 } 17 18 if f := strings.HasPrefix; f(path, `https://`) || f(path, `http://`) { 19 resp, err := http.Get(path) 20 if err != nil { 21 return nil, err 22 } 23 return resp.Body, nil 24 } 25 26 f, err := os.Open(path) 27 if err != nil { 28 // on windows, file-not-found error messages may mention `CreateFile`, 29 // even when trying to open files in read-only mode 30 return nil, errors.New(`can't open file named ` + path) 31 } 32 return f, nil 33 } File: j0/main.go 1 package main 2 3 import ( 4 "bufio" 5 "bytes" 6 "io" 7 "os" 8 9 _ "embed" 10 ) 11 12 /* 13 You can build smaller executables using either of these 2 commands: 14 15 go build -ldflags "-s -w" -trimpath 16 go build -compiler gccgo -gccgoflags "-s -O2" -trimpath 17 18 The latter command will make a much smaller file than the first, but it 19 requires gccgo to be installed beside the standard go(lang) compiler. 20 */ 21 22 // Note: the code is avoiding using the fmt package to save hundreds of 23 // kilobytes on the resulting executable, which is a noticeable difference. 24 25 //go:embed info.txt 26 var info string 27 28 // bufSize is trying to be a good buffer-size for modern CPU cores 29 const bufSize = 16 * 1024 30 31 func main() { 32 if len(os.Args) > 1 { 33 switch os.Args[1] { 34 case `-h`, `--h`, `-help`, `--help`: 35 os.Stderr.WriteString(info) 36 return 37 } 38 } 39 40 if err := run(os.Args[1:]); isActualError(err) { 41 os.Stderr.WriteString("\x1b[31m") 42 os.Stderr.WriteString(err.Error()) 43 os.Stderr.WriteString("\x1b[0m\n") 44 os.Exit(1) 45 } 46 } 47 48 func run(args []string) error { 49 convert := convertPseudoJSON 50 if len(args) > 0 { 51 switch args[0] { 52 case `-jl`, `--jl`, `-jsonl`, `--jsonl`: 53 args = args[1:] 54 convert = convertJSONL 55 case `-t`, `--t`, `-text`, `--text`, `-s`, `--s`, `-str`, `--str`, 56 `-string`, `--string`: 57 args = args[1:] 58 convert = convertString 59 case `-b`, `--b`, `-bytes`, `--bytes`: 60 args = args[1:] 61 convert = convertBytes 62 } 63 } 64 65 if len(args) > 1 { 66 return errMultipleInputs 67 } 68 69 path := `-` 70 if len(args) > 0 { 71 path = args[0] 72 } 73 return handleInput(os.Stdout, path, convert) 74 } 75 76 type converter func(w io.Writer, r io.Reader) error 77 78 // handleInput simplifies control-flow for func main 79 func handleInput(w io.Writer, path string, convert converter) error { 80 // pro, _ := os.Create(`j0.prof`) 81 // defer pro.Close() 82 // pprof.StartCPUProfile(pro) 83 // defer pprof.StopCPUProfile() 84 85 if path == `-` { 86 return convert(w, os.Stdin) 87 } 88 89 f, err := open(path) 90 if err != nil { 91 return err 92 } 93 defer f.Close() 94 95 return convert(w, f) 96 } 97 98 // convertPseudoJSON handles regular JSON and pseudo-JSON input 99 func convertPseudoJSON(w io.Writer, r io.Reader) error { 100 bw := bufio.NewWriterSize(w, bufSize) 101 br := bufio.NewReaderSize(r, bufSize) 102 defer bw.Flush() 103 104 if err := json0(bw, br); err != nil { 105 return err 106 } 107 108 // end the only output-line with a line-feed; this also avoids showing 109 // error messages on the same line as the main output, since JSON-0 110 // output has no line-feeds before its last byte 111 return outputByte(bw, '\n') 112 } 113 114 // convertBytes handles all input bytes, emitting a JSON array of numbers 115 func convertBytes(w io.Writer, r io.Reader) error { 116 bw := bufio.NewWriterSize(w, bufSize) 117 defer bw.Flush() 118 119 var buf [bufSize]byte 120 bw.WriteByte('[') 121 122 for i := 0; true; { 123 n, err := r.Read(buf[:]) 124 if err == io.EOF { 125 if n == 0 { 126 break 127 } 128 err = nil 129 } 130 131 if err != nil { 132 return err 133 } 134 135 for _, b := range buf[:n] { 136 if i > 0 { 137 bw.WriteByte(',') 138 } 139 i++ 140 141 _, err := bw.Write(bytes2digits[b]) 142 if err != nil { 143 return errNoMoreOutput 144 } 145 } 146 } 147 148 bw.WriteByte(']') 149 return outputByte(bw, '\n') 150 } 151 152 // convertString handles all input as the unquote/inner-part of a string 153 func convertString(w io.Writer, r io.Reader) error { 154 bw := bufio.NewWriterSize(w, bufSize) 155 br := bufio.NewReaderSize(r, bufSize) 156 defer bw.Flush() 157 158 bw.WriteByte('"') 159 160 for { 161 r, size, err := br.ReadRune() 162 if err == io.EOF { 163 if size == 0 { 164 break 165 } 166 err = nil 167 } 168 169 if err != nil { 170 return err 171 } 172 173 if r < 128 { 174 _, err = bw.Write(escapedStringBytes[r]) 175 } else { 176 _, err = bw.WriteRune(r) 177 } 178 179 if err != nil { 180 return errNoMoreOutput 181 } 182 } 183 184 bw.WriteByte('"') 185 return outputByte(bw, '\n') 186 } 187 188 // convertJSONL handles JSON lines and pseudo-JSON lines 189 func convertJSONL(w io.Writer, r io.Reader) error { 190 bw := bufio.NewWriterSize(w, bufSize) 191 defer bw.Flush() 192 193 items := 0 194 sc := bufio.NewScanner(r) 195 lr := bytes.NewReader(nil) 196 blr := bufio.NewReaderSize(nil, bufSize) 197 198 bw.WriteByte('[') 199 200 for sc.Scan() { 201 line := sc.Bytes() 202 trimmed := bytes.TrimSpace(line) 203 if len(trimmed) == 0 || bytes.HasPrefix(trimmed, []byte{'/', '/'}) { 204 continue 205 } 206 207 if items > 0 { 208 if err := outputByte(bw, ','); err != nil { 209 return err 210 } 211 } 212 items++ 213 214 lr.Reset(line) 215 blr.Reset(lr) 216 if err := json0(bw, blr); err != nil { 217 return err 218 } 219 } 220 221 if err := sc.Err(); err != nil { 222 return err 223 } 224 225 bw.WriteByte(']') 226 return outputByte(bw, '\n') 227 } File: j0/mit-license.txt 1 The MIT License (MIT) 2 3 Copyright © 2024 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: j0/tables.go 1 package main 2 3 // escapedStringBytes helps func handleString treat all string bytes quickly 4 // and correctly, using their officially-supported JSON escape sequences 5 // 6 // https://www.rfc-editor.org/rfc/rfc8259#section-7 7 var escapedStringBytes = [256][]byte{ 8 {'\\', 'u', '0', '0', '0', '0'}, {'\\', 'u', '0', '0', '0', '1'}, 9 {'\\', 'u', '0', '0', '0', '2'}, {'\\', 'u', '0', '0', '0', '3'}, 10 {'\\', 'u', '0', '0', '0', '4'}, {'\\', 'u', '0', '0', '0', '5'}, 11 {'\\', 'u', '0', '0', '0', '6'}, {'\\', 'u', '0', '0', '0', '7'}, 12 {'\\', 'b'}, {'\\', 't'}, 13 {'\\', 'n'}, {'\\', 'u', '0', '0', '0', 'b'}, 14 {'\\', 'f'}, {'\\', 'r'}, 15 {'\\', 'u', '0', '0', '0', 'e'}, {'\\', 'u', '0', '0', '0', 'f'}, 16 {'\\', 'u', '0', '0', '1', '0'}, {'\\', 'u', '0', '0', '1', '1'}, 17 {'\\', 'u', '0', '0', '1', '2'}, {'\\', 'u', '0', '0', '1', '3'}, 18 {'\\', 'u', '0', '0', '1', '4'}, {'\\', 'u', '0', '0', '1', '5'}, 19 {'\\', 'u', '0', '0', '1', '6'}, {'\\', 'u', '0', '0', '1', '7'}, 20 {'\\', 'u', '0', '0', '1', '8'}, {'\\', 'u', '0', '0', '1', '9'}, 21 {'\\', 'u', '0', '0', '1', 'a'}, {'\\', 'u', '0', '0', '1', 'b'}, 22 {'\\', 'u', '0', '0', '1', 'c'}, {'\\', 'u', '0', '0', '1', 'd'}, 23 {'\\', 'u', '0', '0', '1', 'e'}, {'\\', 'u', '0', '0', '1', 'f'}, 24 {32}, {33}, {'\\', '"'}, {35}, {36}, {37}, {38}, {39}, 25 {40}, {41}, {42}, {43}, {44}, {45}, {46}, {47}, 26 {48}, {49}, {50}, {51}, {52}, {53}, {54}, {55}, 27 {56}, {57}, {58}, {59}, {60}, {61}, {62}, {63}, 28 {64}, {65}, {66}, {67}, {68}, {69}, {70}, {71}, 29 {72}, {73}, {74}, {75}, {76}, {77}, {78}, {79}, 30 {80}, {81}, {82}, {83}, {84}, {85}, {86}, {87}, 31 {88}, {89}, {90}, {91}, {'\\', '\\'}, {93}, {94}, {95}, 32 {96}, {97}, {98}, {99}, {100}, {101}, {102}, {103}, 33 {104}, {105}, {106}, {107}, {108}, {109}, {110}, {111}, 34 {112}, {113}, {114}, {115}, {116}, {117}, {118}, {119}, 35 {120}, {121}, {122}, {123}, {124}, {125}, {126}, {127}, 36 {128}, {129}, {130}, {131}, {132}, {133}, {134}, {135}, 37 {136}, {137}, {138}, {139}, {140}, {141}, {142}, {143}, 38 {144}, {145}, {146}, {147}, {148}, {149}, {150}, {151}, 39 {152}, {153}, {154}, {155}, {156}, {157}, {158}, {159}, 40 {160}, {161}, {162}, {163}, {164}, {165}, {166}, {167}, 41 {168}, {169}, {170}, {171}, {172}, {173}, {174}, {175}, 42 {176}, {177}, {178}, {179}, {180}, {181}, {182}, {183}, 43 {184}, {185}, {186}, {187}, {188}, {189}, {190}, {191}, 44 {192}, {193}, {194}, {195}, {196}, {197}, {198}, {199}, 45 {200}, {201}, {202}, {203}, {204}, {205}, {206}, {207}, 46 {208}, {209}, {210}, {211}, {212}, {213}, {214}, {215}, 47 {216}, {217}, {218}, {219}, {220}, {221}, {222}, {223}, 48 {224}, {225}, {226}, {227}, {228}, {229}, {230}, {231}, 49 {232}, {233}, {234}, {235}, {236}, {237}, {238}, {239}, 50 {240}, {241}, {242}, {243}, {244}, {245}, {246}, {247}, 51 {248}, {249}, {250}, {251}, {252}, {253}, {254}, {255}, 52 } 53 54 // bytes2digits has all bytes `pre-rendered` as numeric ASCII strings 55 var bytes2digits = [256][]byte{ 56 {'0'}, {'1'}, {'2'}, {'3'}, {'4'}, {'5'}, {'6'}, {'7'}, 57 {'8'}, {'9'}, {'1', '0'}, {'1', '1'}, {'1', '2'}, 58 {'1', '3'}, {'1', '4'}, {'1', '5'}, {'1', '6'}, 59 {'1', '7'}, {'1', '8'}, {'1', '9'}, {'2', '0'}, 60 {'2', '1'}, {'2', '2'}, {'2', '3'}, {'2', '4'}, 61 {'2', '5'}, {'2', '6'}, {'2', '7'}, {'2', '8'}, 62 {'2', '9'}, {'3', '0'}, {'3', '1'}, {'3', '2'}, 63 {'3', '3'}, {'3', '4'}, {'3', '5'}, {'3', '6'}, 64 {'3', '7'}, {'3', '8'}, {'3', '9'}, {'4', '0'}, 65 {'4', '1'}, {'4', '2'}, {'4', '3'}, {'4', '4'}, 66 {'4', '5'}, {'4', '6'}, {'4', '7'}, {'4', '8'}, 67 {'4', '9'}, {'5', '0'}, {'5', '1'}, {'5', '2'}, 68 {'5', '3'}, {'5', '4'}, {'5', '5'}, {'5', '6'}, 69 {'5', '7'}, {'5', '8'}, {'5', '9'}, {'6', '0'}, 70 {'6', '1'}, {'6', '2'}, {'6', '3'}, {'6', '4'}, 71 {'6', '5'}, {'6', '6'}, {'6', '7'}, {'6', '8'}, 72 {'6', '9'}, {'7', '0'}, {'7', '1'}, {'7', '2'}, 73 {'7', '3'}, {'7', '4'}, {'7', '5'}, {'7', '6'}, 74 {'7', '7'}, {'7', '8'}, {'7', '9'}, {'8', '0'}, 75 {'8', '1'}, {'8', '2'}, {'8', '3'}, {'8', '4'}, 76 {'8', '5'}, {'8', '6'}, {'8', '7'}, {'8', '8'}, 77 {'8', '9'}, {'9', '0'}, {'9', '1'}, {'9', '2'}, 78 {'9', '3'}, {'9', '4'}, {'9', '5'}, {'9', '6'}, 79 {'9', '7'}, {'9', '8'}, {'9', '9'}, {'1', '0', '0'}, 80 {'1', '0', '1'}, {'1', '0', '2'}, {'1', '0', '3'}, {'1', '0', '4'}, 81 {'1', '0', '5'}, {'1', '0', '6'}, {'1', '0', '7'}, {'1', '0', '8'}, 82 {'1', '0', '9'}, {'1', '1', '0'}, {'1', '1', '1'}, {'1', '1', '2'}, 83 {'1', '1', '3'}, {'1', '1', '4'}, {'1', '1', '5'}, {'1', '1', '6'}, 84 {'1', '1', '7'}, {'1', '1', '8'}, {'1', '1', '9'}, {'1', '2', '0'}, 85 {'1', '2', '1'}, {'1', '2', '2'}, {'1', '2', '3'}, {'1', '2', '4'}, 86 {'1', '2', '5'}, {'1', '2', '6'}, {'1', '2', '7'}, {'1', '2', '8'}, 87 {'1', '2', '9'}, {'1', '3', '0'}, {'1', '3', '1'}, {'1', '3', '2'}, 88 {'1', '3', '3'}, {'1', '3', '4'}, {'1', '3', '5'}, {'1', '3', '6'}, 89 {'1', '3', '7'}, {'1', '3', '8'}, {'1', '3', '9'}, {'1', '4', '0'}, 90 {'1', '4', '1'}, {'1', '4', '2'}, {'1', '4', '3'}, {'1', '4', '4'}, 91 {'1', '4', '5'}, {'1', '4', '6'}, {'1', '4', '7'}, {'1', '4', '8'}, 92 {'1', '4', '9'}, {'1', '5', '0'}, {'1', '5', '1'}, {'1', '5', '2'}, 93 {'1', '5', '3'}, {'1', '5', '4'}, {'1', '5', '5'}, {'1', '5', '6'}, 94 {'1', '5', '7'}, {'1', '5', '8'}, {'1', '5', '9'}, {'1', '6', '0'}, 95 {'1', '6', '1'}, {'1', '6', '2'}, {'1', '6', '3'}, {'1', '6', '4'}, 96 {'1', '6', '5'}, {'1', '6', '6'}, {'1', '6', '7'}, {'1', '6', '8'}, 97 {'1', '6', '9'}, {'1', '7', '0'}, {'1', '7', '1'}, {'1', '7', '2'}, 98 {'1', '7', '3'}, {'1', '7', '4'}, {'1', '7', '5'}, {'1', '7', '6'}, 99 {'1', '7', '7'}, {'1', '7', '8'}, {'1', '7', '9'}, {'1', '8', '0'}, 100 {'1', '8', '1'}, {'1', '8', '2'}, {'1', '8', '3'}, {'1', '8', '4'}, 101 {'1', '8', '5'}, {'1', '8', '6'}, {'1', '8', '7'}, {'1', '8', '8'}, 102 {'1', '8', '9'}, {'1', '9', '0'}, {'1', '9', '1'}, {'1', '9', '2'}, 103 {'1', '9', '3'}, {'1', '9', '4'}, {'1', '9', '5'}, {'1', '9', '6'}, 104 {'1', '9', '7'}, {'1', '9', '8'}, {'1', '9', '9'}, {'2', '0', '0'}, 105 {'2', '0', '1'}, {'2', '0', '2'}, {'2', '0', '3'}, {'2', '0', '4'}, 106 {'2', '0', '5'}, {'2', '0', '6'}, {'2', '0', '7'}, {'2', '0', '8'}, 107 {'2', '0', '9'}, {'2', '1', '0'}, {'2', '1', '1'}, {'2', '1', '2'}, 108 {'2', '1', '3'}, {'2', '1', '4'}, {'2', '1', '5'}, {'2', '1', '6'}, 109 {'2', '1', '7'}, {'2', '1', '8'}, {'2', '1', '9'}, {'2', '2', '0'}, 110 {'2', '2', '1'}, {'2', '2', '2'}, {'2', '2', '3'}, {'2', '2', '4'}, 111 {'2', '2', '5'}, {'2', '2', '6'}, {'2', '2', '7'}, {'2', '2', '8'}, 112 {'2', '2', '9'}, {'2', '3', '0'}, {'2', '3', '1'}, {'2', '3', '2'}, 113 {'2', '3', '3'}, {'2', '3', '4'}, {'2', '3', '5'}, {'2', '3', '6'}, 114 {'2', '3', '7'}, {'2', '3', '8'}, {'2', '3', '9'}, {'2', '4', '0'}, 115 {'2', '4', '1'}, {'2', '4', '2'}, {'2', '4', '3'}, {'2', '4', '4'}, 116 {'2', '4', '5'}, {'2', '4', '6'}, {'2', '4', '7'}, {'2', '4', '8'}, 117 {'2', '4', '9'}, {'2', '5', '0'}, {'2', '5', '1'}, {'2', '5', '2'}, 118 {'2', '5', '3'}, {'2', '5', '4'}, {'2', '5', '5'}, 119 } File: j0/tables_test.go 1 package main 2 3 import ( 4 "bytes" 5 "fmt" 6 "testing" 7 ) 8 9 func TestTables(t *testing.T) { 10 if l := len(bytes2digits); l != 256 { 11 t.Fatalf(`bytes-table has %d entries, instead of 256`, l) 12 return 13 } 14 15 for i := 0; i < len(bytes2digits); i++ { 16 exp := bytes2digits[i] 17 got := fmt.Sprintf(`%d`, byte(i)) 18 if !bytes.Equal([]byte(got), exp) { 19 const fs = `bytes-table entry for %d is %s, instead of %s` 20 t.Fatalf(fs, i, got, string(exp)) 21 return 22 } 23 } 24 }