summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--fmt/README.md212
-rw-r--r--fmt/TODO12
-rw-r--r--fmt/sprintf.ts677
-rw-r--r--fmt/sprintf_test.ts670
4 files changed, 1571 insertions, 0 deletions
diff --git a/fmt/README.md b/fmt/README.md
new file mode 100644
index 000000000..15d9e18fd
--- /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:
+
+ %<verb>
+
+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 000000000..bfc3efdd1
--- /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 000000000..596f90c85
--- /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;
+ }
+
+ // %[<positional>]<flag>...<verb>
+ 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 000000000..3eb2a3176
--- /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, " <nil>"], // TODO null, undefined ...
+ // ["%-10v", null, "<nil> "],
+
+ // 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 <nil>)"},
+ // {"", 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();