mirror of
https://github.com/denoland/std.git
synced 2024-11-22 04:59:05 +00:00
999 lines
29 KiB
TypeScript
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));
|
|
}
|