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[];
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."); } } 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(); 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; break; case "#": flags.sharp = true; break; case " ": flags.space = true; break; case "0": 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; } } break; case State.POSITIONAL: if (c === "*") { const worp = this.flags.precision === -1 ? WorP.WIDTH : WorP.PRECISION; this.handleWidthOrPrecisionRef(worp); this.state = State.PERCENT; break; } else { this.handleVerb(); return; } default: throw new Error(`Should not be here ${this.state}, library bug!`); } } }
handleWidthOrPrecisionRef(wOrP: WorP): void { if (this.argNum >= this.args.length) { 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 ".": this.flags.precision = 0; this.state = State.PRECISION; break; case "*": this.handleWidthOrPrecisionRef(WorP.WIDTH); break; default: { const val = parseInt(c); if (isNaN(val)) { this.i--; this.state = State.PERCENT; return; } flags.width = flags.width == -1 ? 0 : flags.width; flags.width *= 10; flags.width += val; } } break; case State.PRECISION: { if (c === "*") { this.handleWidthOrPrecisionRef(WorP.PRECISION); break; } const val = parseInt(c); if (isNaN(val)) { this.i--; this.state = State.PERCENT; return; } flags.precision *= 10; flags.precision += val; break; } default: throw new Error("can't be here. bug."); } } }
handlePositional(): void { if (this.format[this.i] !== "[") { 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)) { 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 { 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; } } else if (this.args.length <= this.argNum) { this.buf += `%!(MISSING '${verb}')`; } else { const arg = this.args[this.argNum]; this.haveSeen[this.argNum] = true; if (this.flags.lessthan) { this.buf += this.handleLessThan(); } else { this.buf += this._handleVerb(arg); } } this.argNum++; this.state = State.PASSTHROUGH; }
_handleVerb(arg: any): string { switch (this.verb) { case "t": return this.pad(arg.toString()); case "b": return this.fmtNumber(arg as number, 2); case "c": return this.fmtNumberCodePoint(arg as number); case "d": return this.fmtNumber(arg as number, 10); case "o": return this.fmtNumber(arg as number, 8); case "x": return this.fmtHex(arg); case "X": return this.fmtHex(arg, true); case "e": return this.fmtFloatE(arg as number); case "E": return this.fmtFloatE(arg as number, true); case "f": case "F": return this.fmtFloatF(arg as number); case "g": return this.fmtFloatG(arg as number); case "G": return this.fmtFloatG(arg as number, true); case "s": return this.fmtString(arg as string); case "T": return this.fmtString(typeof arg); case "v": return this.fmtV(arg); case "j": return this.fmtJ(arg); 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) { 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) { 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: prefix += num.startsWith("0") ? "" : "0"; break; case 16: prefix += "0x"; break; default: throw new Error("cannot handle base: " + radix); } } 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 { s = UNICODE_REPLACEMENT_CHARACTER; } return this.pad(s); }
fmtFloatSpecial(n: number): string {
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; let tmp = parseInt(fractional.substr(0, precision + 2)) / 10; tmp = Math.round(tmp); fractional = Math.floor(tmp).toString(); fractional = fractional.substr(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]; 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; }
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); } } 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; }
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 { switch (typeof val) { case "number": return this.fmtNumber(val as number, 16, upper); 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" : " "; } const c = (val.charCodeAt(i) & 0xff).toString(16); hex += c.length === 1 ? `0${c}` : c; } if (upper) { hex = hex.toUpperCase(); } return this.pad(hex); } default: throw new Error( "currently only number and string are implemented for hex", ); } }
fmtV(val: Record<string, unknown>): 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));}