std/fmt/printf.ts
2024-10-24 13:58:50 +09:00

999 lines
29 KiB
TypeScript

// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
/**
* {@linkcode sprintf} and {@linkcode printf} for printing formatted strings to
* stdout.
*
* ```ts
* import { sprintf } from "@std/fmt/printf";
* import { assertEquals } from "@std/assert";
*
* assertEquals(sprintf("%d", 9), "9");
* assertEquals(sprintf("%o", 9), "11");
* assertEquals(sprintf("%f", 4), "4.000000");
* assertEquals(sprintf("%.3f", 0.9999), "1.000");
* ```
*
* This implementation is inspired by POSIX and Golang but does not port
* implementation code.
*
* 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` |
* | `i` | argument as formatted by `Deno.inspect` |
* | `I` | argument as formatted by `Deno.inspect` in compact format |
*
* ## 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 8, 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` | truncate, 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.8f", 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` | use 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 provided and may
* be consumed more than once. E.g.:
*
* sprintf("%[2]s %[1]s", "World", "Hello")
*
* returns "Hello World". The presence 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/
*
* @module
*/
const State = {
PASSTHROUGH: 0,
PERCENT: 1,
POSITIONAL: 2,
PRECISION: 3,
WIDTH: 4,
} as const;
type State = typeof State[keyof typeof State];
const WorP = {
WIDTH: 0,
PRECISION: 1,
} as const;
type WorP = typeof WorP[keyof typeof WorP];
const F = {
sign: 1,
mantissa: 2,
fractional: 3,
esign: 4,
exponent: 5,
} as const;
class Flags {
plus: boolean | undefined;
dash: boolean | undefined;
sharp: boolean | undefined;
space: boolean | undefined;
zero: boolean | undefined;
lessthan: boolean | undefined;
width = -1;
precision = -1;
}
const min = Math.min;
const UNICODE_REPLACEMENT_CHARACTER = "\ufffd";
const DEFAULT_PRECISION = 6;
const FLOAT_REGEXP = /(-?)(\d)\.?(\d*)e([+-])(\d+)/;
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 | undefined;
constructor(format: string, ...args: unknown[]) {
this.format = format;
this.args = args;
this.haveSeen = Array.from({ length: 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 new Error(
`State ${this.state} should be unreachable, please file a bug report against Deno at https://github.com/denoland/std/issues`,
);
}
}
// 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() {
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:
// TODO(bartlomieju): either a verb or * only verb for now
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(
`State ${this.state} should be unreachable, please file a bug report against Deno at https://github.com/denoland/std/issues`,
);
} // switch state
}
}
/**
* Handle width or precision
* @param wOrP
*/
handleWidthOrPrecisionRef(wOrP: WorP) {
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++;
}
/**
* Handle width and precision
* @param flags
*/
handleWidthAndPrecision(flags: Flags) {
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(
`State ${this.state} should be unreachable, please file a bug report against Deno at https://github.com/denoland/std/issues`,
);
} // switch state
}
}
/** Handle positional */
handlePositional() {
if (this.format[this.i] !== "[") {
// sanity only
throw new Error(
"Should be unreachable, please file a bug report against Deno at https://github.com/denoland/std/issues",
);
}
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]!, 10);
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;
}
/** Handle less than */
handleLessThan(): string {
// deno-lint-ignore no-explicit-any
const arg = this.args[this.argNum] as any;
if ((arg || {}).constructor.name !== "Array") {
throw new Error(
`Cannot handle less than '<' flag: 'arg' is not an array`,
);
}
let str = "[ ";
for (let i = 0; i !== arg.length; ++i) {
if (i !== 0) str += ", ";
str += this._handleVerb(arg[i]);
}
return str + " ]";
}
/** Handle verb */
handleVerb() {
const verb = this.format[this.i];
this.verb = verb || this.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;
}
// deno-lint-ignore no-explicit-any
_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);
case "i":
return this.fmtI(arg, false);
case "I":
return this.fmtI(arg, true);
default:
return `%!(BAD VERB '${this.verb}')`;
}
}
/**
* Pad a string
* @param s text to pad
*/
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);
}
/**
* Pad a number
* @param nStr
* @param neg
*/
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;
}
/**
* Format a number
* @param n
* @param radix
* @param upcase
*/
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 the radix ${radix}: only 2, 8, 16 are supported`,
);
}
}
// 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);
}
/**
* Format number with code points
* @param n
*/
fmtNumberCodePoint(n: number): string {
let s = "";
try {
s = String.fromCodePoint(n);
} catch {
s = UNICODE_REPLACEMENT_CHARACTER;
}
return this.pad(s);
}
/**
* Format special float
* @param n
*/
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 "";
}
/**
* Round fraction to precision
* @param fractional
* @param precision
* @returns tuple of fractional and round
*/
roundFractionToPrecision(
fractional: string,
precision: number,
): [string, boolean] {
let round = false;
if (fractional.length > precision) {
fractional = "1" + fractional; // prepend a 1 in case of leading 0
let tmp = parseInt(fractional.slice(0, precision + 2)) / 10;
tmp = Math.round(tmp);
fractional = Math.floor(tmp).toString();
round = fractional[0] === "2";
fractional = fractional.slice(1); // remove extra 1
} else {
while (fractional.length < precision) {
fractional += "0";
}
}
return [fractional, round];
}
/**
* Format float E
* @param n
* @param upcase
*/
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 new Error(
"Should be unreachable, please file a bug report against Deno at https://github.com/denoland/std/issues",
);
}
const precision = this.flags.precision !== -1
? this.flags.precision
: DEFAULT_PRECISION;
const [fractional, rounding] = this.roundFractionToPrecision(
m[F.fractional] ?? "",
precision,
);
let e = m[F.exponent]!;
let esign = m[F.esign]!;
// scientific notation output with exponent padded to minlen 2
let mantissa = parseInt(m[F.mantissa]!);
if (rounding) {
mantissa += 1;
if (10 <= mantissa) {
mantissa = 1;
const r = parseInt(esign + e) + 1;
e = Math.abs(r).toString();
esign = r < 0 ? "-" : "+";
}
}
e = e.length === 1 ? "0" + e : e;
const val = `${mantissa}.${fractional}${upcase ? "E" : "e"}${esign}${e}`;
return this.padNum(val, n < 0);
}
/**
* Format float F
* @param n
*/
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.slice(0, splIdx) + "." + m.slice(splIdx);
}
}
// avoiding sign makes padding easier
const val = expandNumber(Math.abs(n)) as string;
let [dig, fractional] = val.split(".") as [string, string];
const precision = this.flags.precision !== -1
? this.flags.precision
: DEFAULT_PRECISION;
let round = false;
[fractional, round] = this.roundFractionToPrecision(fractional, precision);
if (round) {
dig = (parseInt(dig) + 1).toString();
}
return this.padNum(`${dig}.${fractional}`, n < 0);
}
/**
* Format float G
* @param n
* @param upcase
*/
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 new Error(
"Should be unreachable, please file a bug report against Deno at https://github.com/denoland/std/issues",
);
}
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;
}
/**
* Format string
* @param s
*/
fmtString(s: string): string {
if (this.flags.precision !== -1) {
s = s.slice(0, this.flags.precision);
}
return this.pad(s);
}
/**
* Format hex
* @param val
* @param upper
*/
fmtHex(val: string | number, upper = false): string {
// allow others types ?
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" : " ";
}
// TODO(bartlomieju): 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);
}
default:
throw new Error(
`Cannot format hex, only number and string are supported for hex formatting: ${typeof val} is given`,
);
}
}
/**
* Format value
* @param val
*/
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().slice(0, p);
}
}
/**
* Format JSON
* @param val
*/
fmtJ(val: unknown): string {
return JSON.stringify(val);
}
/**
* Format inspect
* @param val
* @param compact Whether or not the output should be compact.
*/
fmtI(val: unknown, compact: boolean): string {
return Deno.inspect(val, {
colors: !Deno?.noColor,
compact,
depth: Infinity,
iterableLimit: Infinity,
});
}
}
/**
* Converts and formats a variable number of `args` as is specified by `format`.
* `sprintf` returns the formatted string.
*
* See the module documentation for the available format strings.
*
* @example Usage
* ```ts
* import { sprintf } from "@std/fmt/printf";
* import { assertEquals } from "@std/assert";
*
* assertEquals(sprintf("%d", 9), "9");
*
* assertEquals(sprintf("%o", 9), "11");
*
* assertEquals(sprintf("%f", 4), "4.000000");
*
* assertEquals(sprintf("%.3f", 0.9999), "1.000");
* ```
*
* @param format The format string to use
* @param args The arguments to format
* @returns The formatted string
*/
export function sprintf(format: string, ...args: unknown[]): string {
const printf = new Printf(format, ...args);
return printf.doPrintf();
}
/**
* Converts and format a variable number of `args` as is specified by `format`.
* `printf` writes the formatted string to standard output.
*
* See the module documentation for the available format strings.
*
* @example Usage
* ```ts no-assert
* import { printf } from "@std/fmt/printf";
*
* printf("%d", 9); // Prints "9"
*
* printf("%o", 9); // Prints "11"
*
* printf("%f", 4); // Prints "4.000000"
*
* printf("%.3f", 0.9999); // Prints "1.000"
* ```
*
* @param format The format string to use
* @param args The arguments to format
*/
export function printf(format: string, ...args: unknown[]) {
const s = sprintf(format, ...args);
Deno.stdout.writeSync(new TextEncoder().encode(s));
}