From 16e134d8a88cfb7c51c92881e6e7292a6ec00f3b Mon Sep 17 00:00:00 2001 From: a2800276 Date: Thu, 15 Aug 2019 17:57:24 +0200 Subject: [PATCH] Add fmt modules (printf implementation) (denoland/deno_std#566) Original: https://github.com/denoland/deno_std/commit/f7b511611ca5bc3801d6f210d82bddce9b0a61e4 --- fmt/README.md | 212 ++++++++++++++ fmt/TODO | 12 + fmt/sprintf.ts | 677 ++++++++++++++++++++++++++++++++++++++++++++ fmt/sprintf_test.ts | 670 +++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 1571 insertions(+) create mode 100644 fmt/README.md create mode 100644 fmt/TODO create mode 100644 fmt/sprintf.ts create mode 100644 fmt/sprintf_test.ts diff --git a/fmt/README.md b/fmt/README.md new file mode 100644 index 00000000000000..15d9e18fd54dab --- /dev/null +++ b/fmt/README.md @@ -0,0 +1,212 @@ +# Printf for Deno + +This is very much a work-in-progress. I'm actively soliciting feedback. +What immediately follows are points for discussion. + +If you are looking for the documentation proper, skip to: + + "printf: prints formatted output" + +below. + +## Discussion + +This is very much a work-in-progress. I'm actively soliciting feedback. + +- What useful features are available in other languages apart from + Golang and C? + +- behaviour of `%v` verb. In Golang, this is a shortcut verb to "print the + default format" of the argument. It is currently implemented to format + using `toString` in the default case and `inpect` if the `%#v` + alternative format flag is used in the format directive. Alternativly, + `%V` could be used to distinguish the two. + + `inspect` output is not defined, however. This may be problematic if using + this code on other plattforms (and expecting interoperability). To my + knowledge, no suitable specification of object representation aside from JSON + and `toString` exist. ( Aside: see "[Common object formats][3]" in the + "Console Living Standard" which basically says "do whatever" ) + +- `%j` verb. This is an extension particular to this implementation. Currently + not very sophisticated, it just runs `JSON.stringify` on the argument. + Consider possible modifier flags, etc. + +- `<` verb. This is an extension that assumes the argument is an array and will + format each element according to the format (surrounded by [] and seperated + by comma) (`<` Mnemonic: pull each element out of array) + +- how to deal with more newfangled Javascript features ( generic Iterables, + Map and Set types, typed Arrays, ...) + +- the implementation is fairly rough around the edges: + +- currently contains little in the way of checking for + correctness. Conceivably, there will be a 'strict' form, e.g. + that ensures only Number-ish arguments are passed to %f flags + +- assembles output using string concatenation instead of + utilizing buffers or other optimizations. It would be nice to + have printf / sprintf / fprintf (etc) all in one. + +- float formatting is handled by toString() and to `toExponential` + along with a mess of Regexp. Would be nice to use fancy match + +- some flags that are potentially applicable ( POSIX long and unsigned + modifiers are not likely useful) are missing, namely %q (print quoted), %U + (unicode format) + +## Author + +Tim Becker (tim@presseverykey.com) + +## License + +MIT + +The implementation is inspired by POSIX and Golang (see above) but does +not port implementation code. A number of Golang test-cases based on: + + https://golang.org/src/fmt/fmt_test.go + ( BSD: Copyright (c) 2009 The Go Authors. All rights reserved. ) + +were used. + +# printf: prints formatted output + +sprintf converts and formats a variable number of arguments as is +specified by a `format string`. In it's basic form, a format string +may just be a literal. In case arguments are meant to be formatted, +a `directive` is contained in the format string, preceded by a '%' character: + + % + +E.g. the verb `s` indicates the directive should be replaced by the +string representation of the argument in the corresponding position of +the argument list. E.g.: + + Hello %s! + +applied to the arguments "World" yields "Hello World!" + +The meaning of the format string is modelled after [POSIX][1] format +strings as well as well as [Golang format strings][2]. Both contain +elements specific to the respective programming language that don't +apply to JavaScript, so they can not be fully supported. Furthermore we +implement some functionality that is specific to JS. + +## Verbs + +The following verbs are supported: + +| Verb | Meaning | +| ----- | -------------------------------------------------------------- | +| `%` | print a literal percent | +| `t` | evaluate arg as boolean, print `true` or `false` | +| `b` | eval as number, print binary | +| `c` | eval as number, print character corresponding to the codePoint | +| `o` | eval as number, print octal | +| `x X` | print as hex (ff FF), treat string as list of bytes | +| `e E` | print number in scientific/exponent format 1.123123e+01 | +| `f F` | print number as float with decimal point and no exponent | +| `g G` | use %e %E or %f %F depending on size of argument | +| `s` | interpolate string | +| `T` | type of arg, as returned by `typeof` | +| `v` | value of argument in 'default' format (see below) | +| `j` | argument as formatted by `JSON.stringify` | + +## Width and Precision + +Verbs may be modified by providing them with width and precision, either or +both may be omitted: + + %9f width 9, default precision + %.9f default width, precision 9 + %8.9f width 8, precision 9 + %8.f width 9, precision 0 + +In general, 'width' describes the minimum length of the output, while 'precision' +limits the output. + +| verb | precision | +| --------- | -------------------------------------------------------------- | +| `t` | n/a | +| `b c o` | n/a | +| `x X` | n/a for number, strings are truncated to p bytes(!) | +| `e E f F` | number of places after decimal, default 6 | +| `g G` | set maximum number of digits | +| `s` | truncate input | +| `T` | truncate | +| `v` | tuncate, or depth if used with # see "'default' format", below | +| `j` | n/a | + +Numerical values for width and precision can be substituted for the `*` char, in +which case the values are obtained from the next args, e.g.: + + sprintf ("%*.*f", 9,8,456.0) + +is equivalent to + + sprintf ("%9.9f", 456.0) + +## Flags + +The effects of the verb may be further influenced by using flags to modify the +directive: + +| Flag | Verb | Meaning | +| ----- | --------- | -------------------------------------------------------------------------- | +| `+` | numeric | always print sign | +| `-` | all | pad to the right (left justify) | +| `#` | | alternate format | +| `#` | `b o x X` | prefix with `0b 0 0x` | +| `#` | `g G` | don't remove trailing zeros | +| `#` | `v` | ues output of `inspect` instead of `toString` | +| `' '` | | space character | +| `' '` | `x X` | leave spaces between bytes when printing string | +| `' '` | `d` | insert space for missing `+` sign character | +| `0` | all | pad with zero, `-` takes precedence, sign is appended in front of padding | +| `<` | all | format elements of the passed array according to the directive (extension) | + +## 'default' format + +The default format used by `%v` is the result of calling `toString()` on the +relevant argument. If the `#` flags is used, the result of calling `inspect()` +is interpolated. In this case, the precision, if set is passed to `inspect()` as +the 'depth' config parameter + +## Positional arguments + +Arguments do not need to be consumed in the order they are provded and may +be consumed more than once. E.g.: + + sprintf("%[2]s %[1]s", "World", "Hello") + +returns "Hello World". The precence of a positional indicator resets the arg counter +allowing args to be reused: + + sprintf("dec[%d]=%d hex[%[1]d]=%x oct[%[1]d]=%#o %s", 1, 255, "Third") + +returns `dec[1]=255 hex[1]=0xff oct[1]=0377 Third` + +Width and precision my also use positionals: + + "%[2]*.[1]*d", 1, 2 + +This follows the golang conventions and not POSIX. + +## Errors + +The following errors are handled: + +Incorrect verb: + + S("%h", "") %!(BAD VERB 'h') + +Too few arguments: + + S("%d") %!(MISSING 'd')" + +[1]: https://pubs.opengroup.org/onlinepubs/009695399/functions/fprintf.html +[2]: https://golang.org/pkg/fmt/ +[3]: https://console.spec.whatwg.org/#object-formats diff --git a/fmt/TODO b/fmt/TODO new file mode 100644 index 00000000000000..bfc3efdd17ca5e --- /dev/null +++ b/fmt/TODO @@ -0,0 +1,12 @@ + +* "native" formatting, json, arrays, object/structs, functions ... +* %q %U +* Java has a %n flag to print the plattform native newline... in POSIX + that means "number of chars printed so far", though. +* use of Writer and Buffer internally in order to make FPrintf, Printf, etc. + easier and more elegant. +* see "Discussion" in README + +*scanf , pack,unpack, annotated hex +* error handling, consistantly +* probably rewrite, now that I konw how it's done. diff --git a/fmt/sprintf.ts b/fmt/sprintf.ts new file mode 100644 index 00000000000000..596f90c858e3d8 --- /dev/null +++ b/fmt/sprintf.ts @@ -0,0 +1,677 @@ +enum State { + PASSTHROUGH, + PERCENT, + POSITIONAL, + PRECISION, + WIDTH +} +enum WorP { + WIDTH, + PRECISION +} + +class Flags { + plus?: boolean; + dash?: boolean; + sharp?: boolean; + space?: boolean; + zero?: boolean; + lessthan?: boolean; + width: number = -1; + precision: number = -1; +} + +// eslint-disable-next-line @typescript-eslint/no-explicit-any + +const min = Math.min; + +const UNICODE_REPLACEMENT_CHARACTER = "\ufffd"; +const DEFAULT_PRECISION = 6; + +const FLOAT_REGEXP = /(-?)(\d)\.?(\d*)e([+-])(\d+)/; +enum F { + sign = 1, + mantissa, + fractional, + esign, + exponent +} + +class Printf { + format: string; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + args: any[]; + i: number; + + state: State = State.PASSTHROUGH; + verb: string = ""; + buf: string = ""; + argNum: number = 0; + flags: Flags = new Flags(); + + haveSeen: boolean[]; + + // barf, store precision and width errors for later processing ... + tmpError?: string; + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + constructor(format: string, ...args: any[]) { + this.format = format; + this.args = args; + this.haveSeen = new Array(args.length); + this.i = 0; + } + + doPrintf(): string { + for (; this.i < this.format.length; ++this.i) { + let c = this.format[this.i]; + switch (this.state) { + case State.PASSTHROUGH: + if (c === "%") { + this.state = State.PERCENT; + } else { + this.buf += c; + } + break; + case State.PERCENT: + if (c === "%") { + this.buf += c; + this.state = State.PASSTHROUGH; + } else { + this.handleFormat(); + } + break; + default: + throw Error("Should be unreachable, certainly a bug in the lib."); + } + } + // check for unhandled args + let extras = false; + let err = "%!(EXTRA"; + for (let i = 0; i !== this.haveSeen.length; ++i) { + if (!this.haveSeen[i]) { + extras = true; + err += ` '${Deno.inspect(this.args[i])}'`; + } + } + err += ")"; + if (extras) { + this.buf += err; + } + return this.buf; + } + + // %[]... + handleFormat(): void { + this.flags = new Flags(); + let flags = this.flags; + for (; this.i < this.format.length; ++this.i) { + let c = this.format[this.i]; + switch (this.state) { + case State.PERCENT: + switch (c) { + case "[": + this.handlePositional(); + this.state = State.POSITIONAL; + break; + case "+": + flags.plus = true; + break; + case "<": + flags.lessthan = true; + break; + case "-": + flags.dash = true; + flags.zero = false; // only left pad zeros, dash takes precedence + break; + case "#": + flags.sharp = true; + break; + case " ": + flags.space = true; + break; + case "0": + // only left pad zeros, dash takes precedence + flags.zero = !flags.dash; + break; + default: + if (("1" <= c && c <= "9") || c === "." || c === "*") { + if (c === ".") { + this.flags.precision = 0; + this.state = State.PRECISION; + this.i++; + } else { + this.state = State.WIDTH; + } + this.handleWidthAndPrecision(flags); + } else { + this.handleVerb(); + return; // always end in verb + } + } // switch c + break; + case State.POSITIONAL: // either a verb or * only verb for now, TODO + if (c === "*") { + let worp = + this.flags.precision === -1 ? WorP.WIDTH : WorP.PRECISION; + this.handleWidthOrPrecisionRef(worp); + this.state = State.PERCENT; + break; + } else { + this.handleVerb(); + return; // always end in verb + } + default: + throw new Error(`Should not be here ${this.state}, library bug!`); + } // switch state + } + } + handleWidthOrPrecisionRef(wOrP: WorP): void { + if (this.argNum >= this.args.length) { + // handle Positional should have already taken care of it... + return; + } + let arg = this.args[this.argNum]; + this.haveSeen[this.argNum] = true; + if (typeof arg === "number") { + switch (wOrP) { + case WorP.WIDTH: + this.flags.width = arg; + break; + default: + this.flags.precision = arg; + } + } else { + let tmp = wOrP === WorP.WIDTH ? "WIDTH" : "PREC"; + this.tmpError = `%!(BAD ${tmp} '${this.args[this.argNum]}')`; + } + this.argNum++; + } + handleWidthAndPrecision(flags: Flags): void { + const fmt = this.format; + for (; this.i !== this.format.length; ++this.i) { + const c = fmt[this.i]; + switch (this.state) { + case State.WIDTH: + switch (c) { + case ".": + // initialize precision, %9.f -> precision=0 + this.flags.precision = 0; + this.state = State.PRECISION; + break; + case "*": + this.handleWidthOrPrecisionRef(WorP.WIDTH); + // force . or flag at this point + break; + default: + const val = parseInt(c); + // most likely parseInt does something stupid that makes + // it unusuable for this scenario ... + // if we encounter a non (number|*|.) we're done with prec & wid + if (isNaN(val)) { + this.i--; + this.state = State.PERCENT; + return; + } + flags.width = flags.width == -1 ? 0 : flags.width; + flags.width *= 10; + flags.width += val; + } // switch c + break; + case State.PRECISION: + if (c === "*") { + this.handleWidthOrPrecisionRef(WorP.PRECISION); + break; + } + const val = parseInt(c); + if (isNaN(val)) { + // one too far, rewind + this.i--; + this.state = State.PERCENT; + return; + } + flags.precision *= 10; + flags.precision += val; + break; + default: + throw new Error("can't be here. bug."); + } // switch state + } + } + + handlePositional(): void { + if (this.format[this.i] !== "[") { + // sanity only + throw new Error("Can't happen? Bug."); + } + let positional = 0; + const format = this.format; + this.i++; + let err = false; + for (; this.i !== this.format.length; ++this.i) { + if (format[this.i] === "]") { + break; + } + positional *= 10; + const val = parseInt(format[this.i]); + if (isNaN(val)) { + //throw new Error( + // `invalid character in positional: ${format}[${format[this.i]}]` + //); + this.tmpError = "%!(BAD INDEX)"; + err = true; + } + positional += val; + } + if (positional - 1 >= this.args.length) { + this.tmpError = "%!(BAD INDEX)"; + err = true; + } + this.argNum = err ? this.argNum : positional - 1; + return; + } + handleLessThan(): string { + let arg = this.args[this.argNum]; + if ((arg || {}).constructor.name !== "Array") { + throw new Error(`arg ${arg} is not an array. Todo better error handling`); + } + let str = "[ "; + for (let i = 0; i !== arg.length; ++i) { + if (i !== 0) str += ", "; + str += this._handleVerb(arg[i]); + } + return str + " ]"; + } + handleVerb(): void { + const verb = this.format[this.i]; + this.verb = verb; + if (this.tmpError) { + this.buf += this.tmpError; + this.tmpError = undefined; + if (this.argNum < this.haveSeen.length) { + this.haveSeen[this.argNum] = true; // keep track of used args + } + } else if (this.args.length <= this.argNum) { + this.buf += `%!(MISSING '${verb}')`; + } else { + let arg = this.args[this.argNum]; // check out of range + this.haveSeen[this.argNum] = true; // keep track of used args + if (this.flags.lessthan) { + this.buf += this.handleLessThan(); + } else { + this.buf += this._handleVerb(arg); + } + } + this.argNum++; // if there is a further positional, it will reset. + this.state = State.PASSTHROUGH; + } + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + _handleVerb(arg: any): string { + switch (this.verb) { + case "t": + return this.pad(arg.toString()); + break; + case "b": + return this.fmtNumber(arg as number, 2); + break; + case "c": + return this.fmtNumberCodePoint(arg as number); + break; + case "d": + return this.fmtNumber(arg as number, 10); + break; + case "o": + return this.fmtNumber(arg as number, 8); + break; + case "x": + return this.fmtHex(arg); + break; + case "X": + return this.fmtHex(arg, true); + break; + case "e": + return this.fmtFloatE(arg as number); + break; + case "E": + return this.fmtFloatE(arg as number, true); + break; + case "f": + case "F": + return this.fmtFloatF(arg as number); + break; + case "g": + return this.fmtFloatG(arg as number); + break; + case "G": + return this.fmtFloatG(arg as number, true); + break; + case "s": + return this.fmtString(arg as string); + break; + case "T": + return this.fmtString(typeof arg); + break; + case "v": + return this.fmtV(arg); + break; + case "j": + return this.fmtJ(arg); + break; + default: + return `%!(BAD VERB '${this.verb}')`; + } + } + + pad(s: string): string { + const padding = this.flags.zero ? "0" : " "; + while (s.length < this.flags.width) { + if (this.flags.dash) { + s += padding; + } else { + s = padding + s; + } + } + return s; + } + padNum(nStr: string, neg: boolean): string { + let sign: string; + if (neg) { + sign = "-"; + } else if (this.flags.plus || this.flags.space) { + sign = this.flags.plus ? "+" : " "; + } else { + sign = ""; + } + const zero = this.flags.zero; + if (!zero) { + // sign comes in front of padding when padding w/ zero, + // in from of value if padding with spaces. + nStr = sign + nStr; + } + + const pad = zero ? "0" : " "; + const len = zero ? this.flags.width - sign.length : this.flags.width; + + while (nStr.length < len) { + if (this.flags.dash) { + nStr += pad; // left justify - right pad + } else { + nStr = pad + nStr; // right just - left pad + } + } + if (zero) { + // see above + nStr = sign + nStr; + } + return nStr; + } + + fmtNumber(n: number, radix: number, upcase: boolean = false): string { + let num = Math.abs(n).toString(radix); + const prec = this.flags.precision; + if (prec !== -1) { + this.flags.zero = false; + num = n === 0 && prec === 0 ? "" : num; + while (num.length < prec) { + num = "0" + num; + } + } + let prefix = ""; + if (this.flags.sharp) { + switch (radix) { + case 2: + prefix += "0b"; + break; + case 8: + // don't annotate octal 0 with 0... + prefix += num.startsWith("0") ? "" : "0"; + break; + case 16: + prefix += "0x"; + break; + default: + throw new Error("cannot handle base: " + radix); + } + } + // don't add prefix in front of value truncated by precision=0, val=0 + num = num.length === 0 ? num : prefix + num; + if (upcase) { + num = num.toUpperCase(); + } + return this.padNum(num, n < 0); + } + + fmtNumberCodePoint(n: number): string { + let s = ""; + try { + s = String.fromCodePoint(n); + } catch (RangeError) { + s = UNICODE_REPLACEMENT_CHARACTER; + } + return this.pad(s); + } + + fmtFloatSpecial(n: number): string { + // formatting of NaN and Inf are pants-on-head + // stupid and more or less arbitrary. + + if (isNaN(n)) { + this.flags.zero = false; + return this.padNum("NaN", false); + } + if (n === Number.POSITIVE_INFINITY) { + this.flags.zero = false; + this.flags.plus = true; + return this.padNum("Inf", false); + } + if (n === Number.NEGATIVE_INFINITY) { + this.flags.zero = false; + return this.padNum("Inf", true); + } + return ""; + } + + roundFractionToPrecision(fractional: string, precision: number): string { + if (fractional.length > precision) { + fractional = "1" + fractional; // prepend a 1 in case of leading 0 + let tmp = parseInt(fractional.substr(0, precision + 2)) / 10; + tmp = Math.round(tmp); + fractional = Math.floor(tmp).toString(); + fractional = fractional.substr(1); // remove extra 1 + } else { + while (fractional.length < precision) { + fractional += "0"; + } + } + return fractional; + } + + fmtFloatE(n: number, upcase: boolean = false): string { + const special = this.fmtFloatSpecial(n); + if (special !== "") { + return special; + } + + const m = n.toExponential().match(FLOAT_REGEXP); + if (!m) { + throw Error("can't happen, bug"); + } + + let fractional = m[F.fractional]; + const precision = + this.flags.precision !== -1 ? this.flags.precision : DEFAULT_PRECISION; + fractional = this.roundFractionToPrecision(fractional, precision); + + let e = m[F.exponent]; + // scientific notation output with exponent padded to minlen 2 + e = e.length == 1 ? "0" + e : e; + + const val = `${m[F.mantissa]}.${fractional}${upcase ? "E" : "e"}${ + m[F.esign] + }${e}`; + return this.padNum(val, n < 0); + } + + fmtFloatF(n: number): string { + const special = this.fmtFloatSpecial(n); + if (special !== "") { + return special; + } + + // stupid helper that turns a number into a (potentially) + // VERY long string. + function expandNumber(n: number): string { + if (Number.isSafeInteger(n)) { + return n.toString() + "."; + } + + const t = n.toExponential().split("e"); + let m = t[0].replace(".", ""); + const e = parseInt(t[1]); + if (e < 0) { + let nStr = "0."; + for (let i = 0; i !== Math.abs(e) - 1; ++i) { + nStr += "0"; + } + return (nStr += m); + } else { + const splIdx = e + 1; + while (m.length < splIdx) { + m += "0"; + } + return m.substr(0, splIdx) + "." + m.substr(splIdx); + } + } + // avoiding sign makes padding easier + const val = expandNumber(Math.abs(n)) as string; + const arr = val.split("."); + const dig = arr[0]; + let fractional = arr[1]; + + const precision = + this.flags.precision !== -1 ? this.flags.precision : DEFAULT_PRECISION; + fractional = this.roundFractionToPrecision(fractional, precision); + + return this.padNum(`${dig}.${fractional}`, n < 0); + } + + fmtFloatG(n: number, upcase: boolean = false): string { + const special = this.fmtFloatSpecial(n); + if (special !== "") { + return special; + } + + // The double argument representing a floating-point number shall be + // converted in the style f or e (or in the style F or E in + // the case of a G conversion specifier), depending on the + // value converted and the precision. Let P equal the + // precision if non-zero, 6 if the precision is omitted, or 1 + // if the precision is zero. Then, if a conversion with style E would + // have an exponent of X: + + // - If P > X>=-4, the conversion shall be with style f (or F ) + // and precision P -( X+1). + + // - Otherwise, the conversion shall be with style e (or E ) + // and precision P -1. + + // Finally, unless the '#' flag is used, any trailing zeros shall be + // removed from the fractional portion of the result and the + // decimal-point character shall be removed if there is no + // fractional portion remaining. + + // A double argument representing an infinity or NaN shall be + // converted in the style of an f or F conversion specifier. + // https://pubs.opengroup.org/onlinepubs/9699919799/functions/fprintf.html + + let P = + this.flags.precision !== -1 ? this.flags.precision : DEFAULT_PRECISION; + P = P === 0 ? 1 : P; + + const m = n.toExponential().match(FLOAT_REGEXP); + if (!m) { + throw Error("can't happen"); + } + + let X = parseInt(m[F.exponent]) * (m[F.esign] === "-" ? -1 : 1); + let nStr = ""; + if (P > X && X >= -4) { + this.flags.precision = P - (X + 1); + nStr = this.fmtFloatF(n); + if (!this.flags.sharp) { + nStr = nStr.replace(/\.?0*$/, ""); + } + } else { + this.flags.precision = P - 1; + nStr = this.fmtFloatE(n); + if (!this.flags.sharp) { + nStr = nStr.replace(/\.?0*e/, upcase ? "E" : "e"); + } + } + return nStr; + } + + fmtString(s: string): string { + if (this.flags.precision !== -1) { + s = s.substr(0, this.flags.precision); + } + return this.pad(s); + } + + fmtHex(val: string | number, upper: boolean = false): string { + // allow others types ? + switch (typeof val) { + case "number": + return this.fmtNumber(val as number, 16, upper); + break; + case "string": + let sharp = this.flags.sharp && val.length !== 0; + let hex = sharp ? "0x" : ""; + const prec = this.flags.precision; + const end = prec !== -1 ? min(prec, val.length) : val.length; + for (let i = 0; i !== end; ++i) { + if (i !== 0 && this.flags.space) { + hex += sharp ? " 0x" : " "; + } + // TODO: for now only taking into account the + // lower half of the codePoint, ie. as if a string + // is a list of 8bit values instead of UCS2 runes + let c = (val.charCodeAt(i) & 0xff).toString(16); + hex += c.length === 1 ? `0${c}` : c; + } + if (upper) { + hex = hex.toUpperCase(); + } + return this.pad(hex); + break; + default: + throw new Error( + "currently only number and string are implemented for hex" + ); + } + } + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + fmtV(val: any): string { + if (this.flags.sharp) { + let options = + this.flags.precision !== -1 ? { depth: this.flags.precision } : {}; + return this.pad(Deno.inspect(val, options)); + } else { + const p = this.flags.precision; + return p === -1 ? val.toString() : val.toString().substr(0, p); + } + } + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + fmtJ(val: any): string { + return JSON.stringify(val); + } +} + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export function sprintf(format: string, ...args: any[]): string { + let printf = new Printf(format, ...args); + return printf.doPrintf(); +} diff --git a/fmt/sprintf_test.ts b/fmt/sprintf_test.ts new file mode 100644 index 00000000000000..3eb2a3176aab67 --- /dev/null +++ b/fmt/sprintf_test.ts @@ -0,0 +1,670 @@ +import { sprintf } from "./sprintf.ts"; + +import { assertEquals } from "https://deno.land/std/testing/asserts.ts"; +import { test, runTests } from "https://deno.land/std/testing/mod.ts"; + +let S = sprintf; + +test(function noVerb(): void { + assertEquals(sprintf("bla"), "bla"); +}); + +test(function percent(): void { + assertEquals(sprintf("%%"), "%"); + assertEquals(sprintf("!%%!"), "!%!"); + assertEquals(sprintf("!%%"), "!%"); + assertEquals(sprintf("%%!"), "%!"); +}); +test(function testBoolean(): void { + assertEquals(sprintf("%t", true), "true"); + assertEquals(sprintf("%10t", true), " true"); + assertEquals(sprintf("%-10t", false), "false "); + assertEquals(sprintf("%t", false), "false"); + assertEquals(sprintf("bla%t", true), "blatrue"); + assertEquals(sprintf("%tbla", false), "falsebla"); +}); + +test(function testIntegerB(): void { + assertEquals(S("%b", 4), "100"); + assertEquals(S("%b", -4), "-100"); + assertEquals( + S("%b", 4.1), + "100.0001100110011001100110011001100110011001100110011" + ); + assertEquals( + S("%b", -4.1), + "-100.0001100110011001100110011001100110011001100110011" + ); + assertEquals( + S("%b", Number.MAX_SAFE_INTEGER), + "11111111111111111111111111111111111111111111111111111" + ); + assertEquals( + S("%b", Number.MIN_SAFE_INTEGER), + "-11111111111111111111111111111111111111111111111111111" + ); + // width + + assertEquals(S("%4b", 4), " 100"); +}); + +test(function testIntegerC(): void { + assertEquals(S("%c", 0x31), "1"); + assertEquals(S("%c%b", 0x31, 1), "11"); + assertEquals(S("%c", 0x1f4a9), "💩"); + //width + assertEquals(S("%4c", 0x31), " 1"); +}); + +test(function testIntegerD(): void { + assertEquals(S("%d", 4), "4"); + assertEquals(S("%d", -4), "-4"); + assertEquals(S("%d", Number.MAX_SAFE_INTEGER), "9007199254740991"); + assertEquals(S("%d", Number.MIN_SAFE_INTEGER), "-9007199254740991"); +}); + +test(function testIntegerO(): void { + assertEquals(S("%o", 4), "4"); + assertEquals(S("%o", -4), "-4"); + assertEquals(S("%o", 9), "11"); + assertEquals(S("%o", -9), "-11"); + assertEquals(S("%o", Number.MAX_SAFE_INTEGER), "377777777777777777"); + assertEquals(S("%o", Number.MIN_SAFE_INTEGER), "-377777777777777777"); + // width + assertEquals(S("%4o", 4), " 4"); +}); +test(function testIntegerx(): void { + assertEquals(S("%x", 4), "4"); + assertEquals(S("%x", -4), "-4"); + assertEquals(S("%x", 9), "9"); + assertEquals(S("%x", -9), "-9"); + assertEquals(S("%x", Number.MAX_SAFE_INTEGER), "1fffffffffffff"); + assertEquals(S("%x", Number.MIN_SAFE_INTEGER), "-1fffffffffffff"); + // width + assertEquals(S("%4x", -4), " -4"); + assertEquals(S("%-4x", -4), "-4 "); + // plus + assertEquals(S("%+4x", 4), " +4"); + assertEquals(S("%-+4x", 4), "+4 "); +}); +test(function testIntegerX(): void { + assertEquals(S("%X", 4), "4"); + assertEquals(S("%X", -4), "-4"); + assertEquals(S("%X", 9), "9"); + assertEquals(S("%X", -9), "-9"); + assertEquals(S("%X", Number.MAX_SAFE_INTEGER), "1FFFFFFFFFFFFF"); + assertEquals(S("%X", Number.MIN_SAFE_INTEGER), "-1FFFFFFFFFFFFF"); +}); + +test(function testFloate(): void { + assertEquals(S("%e", 4), "4.000000e+00"); + assertEquals(S("%e", -4), "-4.000000e+00"); + assertEquals(S("%e", 4.1), "4.100000e+00"); + assertEquals(S("%e", -4.1), "-4.100000e+00"); + assertEquals(S("%e", Number.MAX_SAFE_INTEGER), "9.007199e+15"); + assertEquals(S("%e", Number.MIN_SAFE_INTEGER), "-9.007199e+15"); +}); +test(function testFloatE(): void { + assertEquals(S("%E", 4), "4.000000E+00"); + assertEquals(S("%E", -4), "-4.000000E+00"); + assertEquals(S("%E", 4.1), "4.100000E+00"); + assertEquals(S("%E", -4.1), "-4.100000E+00"); + assertEquals(S("%E", Number.MAX_SAFE_INTEGER), "9.007199E+15"); + assertEquals(S("%E", Number.MIN_SAFE_INTEGER), "-9.007199E+15"); + assertEquals(S("%E", Number.MIN_VALUE), "5.000000E-324"); + assertEquals(S("%E", Number.MAX_VALUE), "1.797693E+308"); +}); +test(function testFloatfF(): void { + assertEquals(S("%f", 4), "4.000000"); + assertEquals(S("%F", 4), "4.000000"); + assertEquals(S("%f", -4), "-4.000000"); + assertEquals(S("%F", -4), "-4.000000"); + assertEquals(S("%f", 4.1), "4.100000"); + assertEquals(S("%F", 4.1), "4.100000"); + assertEquals(S("%f", -4.1), "-4.100000"); + assertEquals(S("%F", -4.1), "-4.100000"); + assertEquals(S("%f", Number.MAX_SAFE_INTEGER), "9007199254740991.000000"); + assertEquals(S("%F", Number.MAX_SAFE_INTEGER), "9007199254740991.000000"); + assertEquals(S("%f", Number.MIN_SAFE_INTEGER), "-9007199254740991.000000"); + assertEquals(S("%F", Number.MIN_SAFE_INTEGER), "-9007199254740991.000000"); + assertEquals(S("%f", Number.MIN_VALUE), "0.000000"); + assertEquals( + S("%.324f", Number.MIN_VALUE), + // eslint-disable-next-line max-len + "0.000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000005" + ); + assertEquals(S("%F", Number.MIN_VALUE), "0.000000"); + assertEquals( + S("%f", Number.MAX_VALUE), + // eslint-disable-next-line max-len + "179769313486231570000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000.000000" + ); + assertEquals( + S("%F", Number.MAX_VALUE), + // eslint-disable-next-line max-len + "179769313486231570000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000.000000" + ); +}); + +test(function testString(): void { + assertEquals(S("%s World%s", "Hello", "!"), "Hello World!"); +}); + +test(function testHex(): void { + assertEquals(S("%x", "123"), "313233"); + assertEquals(S("%x", "n"), "6e"); +}); +test(function testHeX(): void { + assertEquals(S("%X", "123"), "313233"); + assertEquals(S("%X", "n"), "6E"); +}); + +test(function testType(): void { + assertEquals(S("%T", new Date()), "object"); + assertEquals(S("%T", 123), "number"); + assertEquals(S("%T", "123"), "string"); + assertEquals(S("%.3T", "123"), "str"); +}); + +test(function testPositional(): void { + assertEquals(S("%[1]d%[2]d", 1, 2), "12"); + assertEquals(S("%[2]d%[1]d", 1, 2), "21"); +}); + +test(function testSharp(): void { + assertEquals(S("%#x", "123"), "0x313233"); + assertEquals(S("%#X", "123"), "0X313233"); + assertEquals(S("%#x", 123), "0x7b"); + assertEquals(S("%#X", 123), "0X7B"); + assertEquals(S("%#o", 123), "0173"); + assertEquals(S("%#b", 4), "0b100"); +}); + +test(function testWidthAndPrecision(): void { + assertEquals( + S("%9.99d", 9), + // eslint-disable-next-line max-len + "000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000009" + ); + assertEquals(S("%1.12d", 9), "000000000009"); + assertEquals(S("%2s", "a"), " a"); + assertEquals(S("%2d", 1), " 1"); + assertEquals(S("%#4x", 1), " 0x1"); + + assertEquals( + S("%*.99d", 9, 9), + // eslint-disable-next-line max-len + "000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000009" + ); + assertEquals( + S("%9.*d", 99, 9), + // eslint-disable-next-line max-len + "000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000009" + ); + assertEquals(S("%*s", 2, "a"), " a"); + assertEquals(S("%*d", 2, 1), " 1"); + assertEquals(S("%#*x", 4, 1), " 0x1"); +}); + +test(function testDash(): void { + assertEquals(S("%-2s", "a"), "a "); + assertEquals(S("%-2d", 1), "1 "); +}); +test(function testPlus(): void { + assertEquals(S("%-+3d", 1), "+1 "); + assertEquals(S("%+3d", 1), " +1"); + assertEquals(S("%+3d", -1), " -1"); +}); + +test(function testSpace(): void { + assertEquals(S("% -3d", 3), " 3 "); +}); + +test(function testZero(): void { + assertEquals(S("%04s", "a"), "000a"); +}); + +// relevant test cases from fmt_test.go +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const tests: Array<[string, any, string]> = [ + ["%d", 12345, "12345"], + ["%v", 12345, "12345"], + ["%t", true, "true"], + + // basic string + ["%s", "abc", "abc"], + // ["%q", "abc", `"abc"`], // TODO: need %q? + ["%x", "abc", "616263"], + ["%x", "\xff\xf0\x0f\xff", "fff00fff"], + ["%X", "\xff\xf0\x0f\xff", "FFF00FFF"], + ["%x", "", ""], + ["% x", "", ""], + ["%#x", "", ""], + ["%# x", "", ""], + ["%x", "xyz", "78797a"], + ["%X", "xyz", "78797A"], + ["% x", "xyz", "78 79 7a"], + ["% X", "xyz", "78 79 7A"], + ["%#x", "xyz", "0x78797a"], + ["%#X", "xyz", "0X78797A"], + ["%# x", "xyz", "0x78 0x79 0x7a"], + ["%# X", "xyz", "0X78 0X79 0X7A"], + + // basic bytes : TODO special handling for Buffer? other std types? + // escaped strings : TODO decide whether to have %q + + // characters + ["%c", "x".charCodeAt(0), "x"], + ["%c", 0xe4, "ä"], + ["%c", 0x672c, "本"], + ["%c", "日".charCodeAt(0), "日"], + // Specifying precision should have no effect. + ["%.0c", "⌘".charCodeAt(0), "⌘"], + ["%3c", "⌘".charCodeAt(0), " ⌘"], + ["%-3c", "⌘".charCodeAt(0), "⌘ "], + + // Runes that are not printable. + // {"%c", '\U00000e00', "\u0e00"}, // TODO check if \U escape exists in js + //["%c", '\U0010ffff'.codePointAt(0), "\U0010ffff"], + + // Runes that are not valid. + ["%c", -1, "�"], + // TODO surrogate half, doesn't make sense in itself, how + // to determine in JS? + // ["%c", 0xDC80, "�"], + ["%c", 0x110000, "�"], + ["%c", 0xfffffffff, "�"], + + // TODO + // escaped characters + // Runes that are not printable. + // Runes that are not valid. + + // width + ["%5s", "abc", " abc"], + ["%2s", "\u263a", " ☺"], + ["%-5s", "abc", "abc "], + ["%05s", "abc", "00abc"], + ["%5s", "abcdefghijklmnopqrstuvwxyz", "abcdefghijklmnopqrstuvwxyz"], + ["%.5s", "abcdefghijklmnopqrstuvwxyz", "abcde"], + ["%.0s", "日本語日本語", ""], + ["%.5s", "日本語日本語", "日本語日本"], + ["%.10s", "日本語日本語", "日本語日本語"], + // ["%08q", "abc", `000"abc"`], // TODO verb q + // ["%-8q", "abc", `"abc" `], + //["%.5q", "abcdefghijklmnopqrstuvwxyz", `"abcde"`], + ["%.5x", "abcdefghijklmnopqrstuvwxyz", "6162636465"], + //["%.3q", "日本語日本語", `"日本語"`], + //["%.1q", "日本語", `"日"`] + // change of go testcase utf-8([日]) = 0xe697a5, utf-16= 65e5 and + // our %x takes lower byte of string "%.1x", "日本語", "e6"],, + ["%.1x", "日本語", "e5"], + //["%10.1q", "日本語日本語", ` "日"`], + // ["%10v", null, " "], // TODO null, undefined ... + // ["%-10v", null, " "], + + // integers + ["%d", 12345, "12345"], + ["%d", -12345, "-12345"], + // ["%d", ^uint8(0), "255"], + //["%d", ^uint16(0), "65535"], + //["%d", ^uint32(0), "4294967295"], + //["%d", ^uint64(0), "18446744073709551615"], + ["%d", -1 << 7, "-128"], + ["%d", -1 << 15, "-32768"], + ["%d", -1 << 31, "-2147483648"], + //["%d", (-1 << 63), "-9223372036854775808"], + ["%.d", 0, ""], + ["%.0d", 0, ""], + ["%6.0d", 0, " "], + ["%06.0d", 0, " "], // 0 flag should be ignored + ["% d", 12345, " 12345"], + ["%+d", 12345, "+12345"], + ["%+d", -12345, "-12345"], + ["%b", 7, "111"], + ["%b", -6, "-110"], + // ["%b", ^uint32(0), "11111111111111111111111111111111"], + // ["%b", ^uint64(0), + // "1111111111111111111111111111111111111111111111111111111111111111"], + // ["%b", int64(-1 << 63), zeroFill("-1", 63, "")], + // 0 octal notation not allowed in struct node... + ["%o", parseInt("01234", 8), "1234"], + ["%#o", parseInt("01234", 8), "01234"], + // ["%o", ^uint32(0), "37777777777"], + // ["%o", ^uint64(0), "1777777777777777777777"], + ["%#X", 0, "0X0"], + ["%x", 0x12abcdef, "12abcdef"], + ["%X", 0x12abcdef, "12ABCDEF"], + // ["%x", ^uint32(0), "ffffffff"], + // ["%X", ^uint64(0), "FFFFFFFFFFFFFFFF"], + ["%.20b", 7, "00000000000000000111"], + ["%10d", 12345, " 12345"], + ["%10d", -12345, " -12345"], + ["%+10d", 12345, " +12345"], + ["%010d", 12345, "0000012345"], + ["%010d", -12345, "-000012345"], + ["%20.8d", 1234, " 00001234"], + ["%20.8d", -1234, " -00001234"], + ["%020.8d", 1234, " 00001234"], + ["%020.8d", -1234, " -00001234"], + ["%-20.8d", 1234, "00001234 "], + ["%-20.8d", -1234, "-00001234 "], + ["%-#20.8x", 0x1234abc, "0x01234abc "], + ["%-#20.8X", 0x1234abc, "0X01234ABC "], + ["%-#20.8o", parseInt("01234", 8), "00001234 "], + + // Test correct f.intbuf overflow checks. // TODO, lazy + // unicode format // TODO, decide whether unicode verb makes sense %U + + // floats + ["%+.3e", 0.0, "+0.000e+00"], + ["%+.3e", 1.0, "+1.000e+00"], + ["%+.3f", -1.0, "-1.000"], + ["%+.3F", -1.0, "-1.000"], + //["%+.3F", float32(-1.0), "-1.000"], + ["%+07.2f", 1.0, "+001.00"], + ["%+07.2f", -1.0, "-001.00"], + ["%-07.2f", 1.0, "1.00 "], + ["%-07.2f", -1.0, "-1.00 "], + ["%+-07.2f", 1.0, "+1.00 "], + ["%+-07.2f", -1.0, "-1.00 "], + ["%-+07.2f", 1.0, "+1.00 "], + ["%-+07.2f", -1.0, "-1.00 "], + ["%+10.2f", +1.0, " +1.00"], + ["%+10.2f", -1.0, " -1.00"], + ["% .3E", -1.0, "-1.000E+00"], + ["% .3e", 1.0, " 1.000e+00"], + ["%+.3g", 0.0, "+0"], + ["%+.3g", 1.0, "+1"], + ["%+.3g", -1.0, "-1"], + ["% .3g", -1.0, "-1"], + ["% .3g", 1.0, " 1"], + // //["%b", float32(1.0), "8388608p-23"], + // ["%b", 1.0, "4503599627370496p-52"], + // // Test sharp flag used with floats. + ["%#g", 1e-323, "1.00000e-323"], + ["%#g", -1.0, "-1.00000"], + ["%#g", 1.1, "1.10000"], + ["%#g", 123456.0, "123456."], + //["%#g", 1234567.0, "1.234567e+06"], + // the line above is incorrect in go (according to + // my posix reading) %f-> prec = prec-1 + ["%#g", 1234567.0, "1.23457e+06"], + ["%#g", 1230000.0, "1.23000e+06"], + ["%#g", 1000000.0, "1.00000e+06"], + ["%#.0f", 1.0, "1."], + ["%#.0e", 1.0, "1.e+00"], + ["%#.0g", 1.0, "1."], + ["%#.0g", 1100000.0, "1.e+06"], + ["%#.4f", 1.0, "1.0000"], + ["%#.4e", 1.0, "1.0000e+00"], + ["%#.4g", 1.0, "1.000"], + ["%#.4g", 100000.0, "1.000e+05"], + ["%#.0f", 123.0, "123."], + ["%#.0e", 123.0, "1.e+02"], + ["%#.0g", 123.0, "1.e+02"], + ["%#.4f", 123.0, "123.0000"], + ["%#.4e", 123.0, "1.2300e+02"], + ["%#.4g", 123.0, "123.0"], + ["%#.4g", 123000.0, "1.230e+05"], + ["%#9.4g", 1.0, " 1.000"], + // The sharp flag has no effect for binary float format. + // ["%#b", 1.0, "4503599627370496p-52"], // TODO binary for floats + // Precision has no effect for binary float format. + //["%.4b", float32(1.0), "8388608p-23"], // TODO s.above + // ["%.4b", -1.0, "-4503599627370496p-52"], + // Test correct f.intbuf boundary checks. + //["%.68f", 1.0, zeroFill("1.", 68, "")], // TODO zerofill + //["%.68f", -1.0, zeroFill("-1.", 68, "")], //TODO s.a. + // float infinites and NaNs + ["%f", Number.POSITIVE_INFINITY, "+Inf"], + ["%.1f", Number.NEGATIVE_INFINITY, "-Inf"], + ["% f", NaN, " NaN"], + ["%20f", Number.POSITIVE_INFINITY, " +Inf"], + // ["% 20F", Number.POSITIVE_INFINITY, " Inf"], // TODO : wut? + ["% 20e", Number.NEGATIVE_INFINITY, " -Inf"], + ["%+20E", Number.NEGATIVE_INFINITY, " -Inf"], + ["% +20g", Number.NEGATIVE_INFINITY, " -Inf"], + ["%+-20G", Number.POSITIVE_INFINITY, "+Inf "], + ["%20e", NaN, " NaN"], + ["% +20E", NaN, " +NaN"], + ["% -20g", NaN, " NaN "], + ["%+-20G", NaN, "+NaN "], + // Zero padding does not apply to infinities and NaN. + ["%+020e", Number.POSITIVE_INFINITY, " +Inf"], + ["%-020f", Number.NEGATIVE_INFINITY, "-Inf "], + ["%-020E", NaN, "NaN "], + + // complex values // go specific + // old test/fmt_test.go + ["%e", 1.0, "1.000000e+00"], + ["%e", 1234.5678e3, "1.234568e+06"], + ["%e", 1234.5678e-8, "1.234568e-05"], + ["%e", -7.0, "-7.000000e+00"], + ["%e", -1e-9, "-1.000000e-09"], + ["%f", 1234.5678e3, "1234567.800000"], + ["%f", 1234.5678e-8, "0.000012"], + ["%f", -7.0, "-7.000000"], + ["%f", -1e-9, "-0.000000"], + // ["%g", 1234.5678e3, "1.2345678e+06"], + // I believe the above test from go is incorrect according to posix, s. above. + ["%g", 1234.5678e3, "1.23457e+06"], + //["%g", float32(1234.5678e3), "1.2345678e+06"], + //["%g", 1234.5678e-8, "1.2345678e-05"], // posix, see above + ["%g", 1234.5678e-8, "1.23457e-05"], + ["%g", -7.0, "-7"], + ["%g", -1e-9, "-1e-09"], + //["%g", float32(-1e-9), "-1e-09"], + ["%E", 1.0, "1.000000E+00"], + ["%E", 1234.5678e3, "1.234568E+06"], + ["%E", 1234.5678e-8, "1.234568E-05"], + ["%E", -7.0, "-7.000000E+00"], + ["%E", -1e-9, "-1.000000E-09"], + //["%G", 1234.5678e3, "1.2345678E+06"], // posix, see above + ["%G", 1234.5678e3, "1.23457E+06"], + //["%G", float32(1234.5678e3), "1.2345678E+06"], + //["%G", 1234.5678e-8, "1.2345678E-05"], // posic, see above + ["%G", 1234.5678e-8, "1.23457E-05"], + ["%G", -7.0, "-7"], + ["%G", -1e-9, "-1E-09"], + //["%G", float32(-1e-9), "-1E-09"], + ["%20.5s", "qwertyuiop", " qwert"], + ["%.5s", "qwertyuiop", "qwert"], + ["%-20.5s", "qwertyuiop", "qwert "], + ["%20c", "x".charCodeAt(0), " x"], + ["%-20c", "x".charCodeAt(0), "x "], + ["%20.6e", 1.2345e3, " 1.234500e+03"], + ["%20.6e", 1.2345e-3, " 1.234500e-03"], + ["%20e", 1.2345e3, " 1.234500e+03"], + ["%20e", 1.2345e-3, " 1.234500e-03"], + ["%20.8e", 1.2345e3, " 1.23450000e+03"], + ["%20f", 1.23456789e3, " 1234.567890"], + ["%20f", 1.23456789e-3, " 0.001235"], + ["%20f", 12345678901.23456789, " 12345678901.234568"], + ["%-20f", 1.23456789e3, "1234.567890 "], + ["%20.8f", 1.23456789e3, " 1234.56789000"], + ["%20.8f", 1.23456789e-3, " 0.00123457"], + // ["%g", 1.23456789e3, "1234.56789"], + // posix ... precision(2) = precision(def=6) - (exp(3)+1) + ["%g", 1.23456789e3, "1234.57"], + // ["%g", 1.23456789e-3, "0.00123456789"], posix... + ["%g", 1.23456789e-3, "0.00123457"], // see above prec6 = precdef6 - (-3+1) + //["%g", 1.23456789e20, "1.23456789e+20"], + ["%g", 1.23456789e20, "1.23457e+20"], + + // arrays // TODO + // slice : go specific + + // TODO decide how to handle deeper types, arrays, objects + // byte arrays and slices with %b,%c,%d,%o,%U and %v + // f.space should and f.plus should not have an effect with %v. + // f.space and f.plus should have an effect with %d. + + // Padding with byte slices. + // Same for strings + ["%2x", "", " "], // 103 + ["%#2x", "", " "], + ["% 02x", "", "00"], + ["%# 02x", "", "00"], + ["%-2x", "", " "], + ["%-02x", "", " "], + ["%8x", "\xab", " ab"], + ["% 8x", "\xab", " ab"], + ["%#8x", "\xab", " 0xab"], + ["%# 8x", "\xab", " 0xab"], + ["%08x", "\xab", "000000ab"], + ["% 08x", "\xab", "000000ab"], + ["%#08x", "\xab", "00000xab"], + ["%# 08x", "\xab", "00000xab"], + ["%10x", "\xab\xcd", " abcd"], + ["% 10x", "\xab\xcd", " ab cd"], + ["%#10x", "\xab\xcd", " 0xabcd"], + ["%# 10x", "\xab\xcd", " 0xab 0xcd"], + ["%010x", "\xab\xcd", "000000abcd"], + ["% 010x", "\xab\xcd", "00000ab cd"], + ["%#010x", "\xab\xcd", "00000xabcd"], + ["%# 010x", "\xab\xcd", "00xab 0xcd"], + ["%-10X", "\xab", "AB "], + ["% -010X", "\xab", "AB "], + ["%#-10X", "\xab\xcd", "0XABCD "], + ["%# -010X", "\xab\xcd", "0XAB 0XCD "], + + // renamings + // Formatter + // GoStringer + + // %T TODO possibly %#T object(constructor) + ["%T", {}, "object"], + ["%T", 1, "number"], + ["%T", "", "string"], + ["%T", undefined, "undefined"], + ["%T", null, "object"], + ["%T", S, "function"], + ["%T", true, "boolean"], + ["%T", Symbol(), "symbol"], + + // %p with pointers + + // erroneous things + // {"", nil, "%!(EXTRA )"}, + // {"", 2, "%!(EXTRA int=2)"}, + // {"no args", "hello", "no args%!(EXTRA string=hello)"}, + // {"%s %", "hello", "hello %!(NOVERB)"}, + // {"%s %.2", "hello", "hello %!(NOVERB)"}, + // {"%017091901790959340919092959340919017929593813360", 0, + // "%!(NOVERB)%!(EXTRA int=0)"}, + // {"%184467440737095516170v", 0, "%!(NOVERB)%!(EXTRA int=0)"}, + // // Extra argument errors should format without flags set. + // {"%010.2", "12345", "%!(NOVERB)%!(EXTRA string=12345)"}, + // + // // Test that maps with non-reflexive keys print all keys and values. + // {"%v", map[float64]int{NaN: 1, NaN: 1}, "map[NaN:1 NaN:1]"}, + + // more floats + + ["%.2f", 1.0, "1.00"], + ["%.2f", -1.0, "-1.00"], + ["% .2f", 1.0, " 1.00"], + ["% .2f", -1.0, "-1.00"], + ["%+.2f", 1.0, "+1.00"], + ["%+.2f", -1.0, "-1.00"], + ["%7.2f", 1.0, " 1.00"], + ["%7.2f", -1.0, " -1.00"], + ["% 7.2f", 1.0, " 1.00"], + ["% 7.2f", -1.0, " -1.00"], + ["%+7.2f", 1.0, " +1.00"], + ["%+7.2f", -1.0, " -1.00"], + ["% +7.2f", 1.0, " +1.00"], + ["% +7.2f", -1.0, " -1.00"], + ["%07.2f", 1.0, "0001.00"], + ["%07.2f", -1.0, "-001.00"], + ["% 07.2f", 1.0, " 001.00"], //153 here + ["% 07.2f", -1.0, "-001.00"], + ["%+07.2f", 1.0, "+001.00"], + ["%+07.2f", -1.0, "-001.00"], + ["% +07.2f", 1.0, "+001.00"], + ["% +07.2f", -1.0, "-001.00"] +]; + +test(function testThorough(): void { + tests.forEach( + (t, i): void => { + // p(t) + let is = S(t[0], t[1]); + let should = t[2]; + assertEquals( + is, + should, + `failed case[${i}] : is >${is}< should >${should}<` + ); + } + ); +}); + +test(function testWeirdos(): void { + assertEquals(S("%.d", 9), "9"); + assertEquals( + S("dec[%d]=%d hex[%[1]d]=%#x oct[%[1]d]=%#o %s", 1, 255, "Third"), + "dec[1]=255 hex[1]=0xff oct[1]=0377 Third" + ); +}); + +test(function formatV(): void { + let a = { a: { a: { a: { a: { a: { a: { a: {} } } } } } } }; + assertEquals(S("%v", a), "[object Object]"); + assertEquals(S("%#v", a), "{ a: { a: { a: { a: [Object] } } } }"); + assertEquals( + S("%#.8v", a), + "{ a: { a: { a: { a: { a: { a: { a: {} } } } } } } }" + ); + assertEquals(S("%#.1v", a), "{ a: [Object] }"); +}); + +test(function formatJ(): void { + let a = { a: { a: { a: { a: { a: { a: { a: {} } } } } } } }; + assertEquals(S("%j", a), `{"a":{"a":{"a":{"a":{"a":{"a":{"a":{}}}}}}}}`); +}); + +test(function flagLessThan(): void { + let a = { a: { a: { a: { a: { a: { a: { a: {} } } } } } } }; + let aArray = [a, a, a]; + assertEquals( + S("%<#.1v", aArray), + "[ { a: [Object] }, { a: [Object] }, { a: [Object] } ]" + ); + let fArray = [1.2345, 0.98765, 123456789.5678]; + assertEquals(S("%<.2f", fArray), "[ 1.23, 0.99, 123456789.57 ]"); +}); + +test(function testErrors(): void { + // wrong type : TODO strict mode ... + //assertEquals(S("%f", "not a number"), "%!(BADTYPE flag=f type=string)") + assertEquals(S("A %h", ""), "A %!(BAD VERB 'h')"); + assertEquals(S("%J", ""), "%!(BAD VERB 'J')"); + assertEquals(S("bla%J", ""), "bla%!(BAD VERB 'J')"); + assertEquals(S("%Jbla", ""), "%!(BAD VERB 'J')bla"); + + assertEquals(S("%d"), "%!(MISSING 'd')"); + assertEquals(S("%d %d", 1), "1 %!(MISSING 'd')"); + assertEquals(S("%d %f A", 1), "1 %!(MISSING 'f') A"); + + assertEquals(S("%*.2f", "a", 1.1), "%!(BAD WIDTH 'a')"); + assertEquals(S("%.*f", "a", 1.1), "%!(BAD PREC 'a')"); + assertEquals(S("%.[2]*f", 1.23, "p"), "%!(BAD PREC 'p')%!(EXTRA '1.23')"); + assertEquals(S("%.[2]*[1]f Yippie!", 1.23, "p"), "%!(BAD PREC 'p') Yippie!"); + + assertEquals(S("%[1]*.2f", "a", "p"), "%!(BAD WIDTH 'a')"); + + assertEquals(S("A", "a", "p"), "A%!(EXTRA 'a' 'p')"); + assertEquals(S("%[2]s %[2]s", "a", "p"), "p p%!(EXTRA 'a')"); + + // remains to be determined how to handle bad indices ... + // (realistically) the entire error handling is still up for grabs. + assertEquals(S("%[hallo]s %d %d %d", 1, 2, 3, 4), "%!(BAD INDEX) 2 3 4"); + assertEquals(S("%[5]s", 1, 2, 3, 4), "%!(BAD INDEX)%!(EXTRA '2' '3' '4')"); + assertEquals(S("%[5]f"), "%!(BAD INDEX)"); + assertEquals(S("%.[5]f"), "%!(BAD INDEX)"); + assertEquals(S("%.[5]*f"), "%!(BAD INDEX)"); +}); + +runTests();