summaryrefslogtreecommitdiff
path: root/std/fmt/printf.ts
diff options
context:
space:
mode:
Diffstat (limited to 'std/fmt/printf.ts')
-rw-r--r--std/fmt/printf.ts682
1 files changed, 682 insertions, 0 deletions
diff --git a/std/fmt/printf.ts b/std/fmt/printf.ts
new file mode 100644
index 000000000..8c2f8d034
--- /dev/null
+++ b/std/fmt/printf.ts
@@ -0,0 +1,682 @@
+// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license.
+//
+// This implementation is inspired by POSIX and Golang but does not port
+// implementation code.
+
+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 = -1;
+ precision = -1;
+}
+
+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;
+ args: unknown[];
+ i: number;
+
+ state: State = State.PASSTHROUGH;
+ verb = "";
+ buf = "";
+ argNum = 0;
+ flags: Flags = new Flags();
+
+ haveSeen: boolean[];
+
+ // barf, store precision and width errors for later processing ...
+ tmpError?: string;
+
+ constructor(format: string, ...args: unknown[]) {
+ 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) {
+ const 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();
+ const flags = this.flags;
+ for (; this.i < this.format.length; ++this.i) {
+ const 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 === "*") {
+ const 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;
+ }
+ const 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 {
+ const 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 unusable 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 {
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ const arg = this.args[this.argNum] as any;
+ 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 {
+ const 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" : " ";
+
+ if (this.flags.dash) {
+ return s.padEnd(this.flags.width, padding);
+ }
+
+ return s.padStart(this.flags.width, padding);
+ }
+ 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;
+
+ if (this.flags.dash) {
+ nStr = nStr.padEnd(len, pad);
+ } else {
+ nStr = nStr.padStart(len, pad);
+ }
+
+ if (zero) {
+ // see above
+ nStr = sign + nStr;
+ }
+ return nStr;
+ }
+
+ fmtNumber(n: number, radix: number, upcase = 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 = 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 = 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");
+ }
+
+ const 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 = false): string {
+ // allow others types ?
+ switch (typeof val) {
+ case "number":
+ return this.fmtNumber(val as number, 16, upper);
+ break;
+ case "string":
+ const 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
+ const 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"
+ );
+ }
+ }
+
+ fmtV(val: object): string {
+ if (this.flags.sharp) {
+ const 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);
+ }
+ }
+
+ fmtJ(val: unknown): string {
+ return JSON.stringify(val);
+ }
+}
+
+export function sprintf(format: string, ...args: unknown[]): string {
+ const printf = new Printf(format, ...args);
+ return printf.doPrintf();
+}
+
+export function printf(format: string, ...args: unknown[]): void {
+ const s = sprintf(format, ...args);
+ Deno.stdout.writeSync(new TextEncoder().encode(s));
+}