mirror of
https://github.com/denoland/deno.git
synced 2025-01-05 13:59:01 -05:00
Add fmt modules (printf implementation) (denoland/deno_std#566)
Original: f7b511611c
This commit is contained in:
parent
917b202354
commit
16e134d8a8
4 changed files with 1571 additions and 0 deletions
212
fmt/README.md
Normal file
212
fmt/README.md
Normal file
|
@ -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
|
12
fmt/TODO
Normal file
12
fmt/TODO
Normal file
|
@ -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.
|
677
fmt/sprintf.ts
Normal file
677
fmt/sprintf.ts
Normal file
|
@ -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();
|
||||
}
|
670
fmt/sprintf_test.ts
Normal file
670
fmt/sprintf_test.ts
Normal file
|
@ -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, "<22>"],
|
||||
// TODO surrogate half, doesn't make sense in itself, how
|
||||
// to determine in JS?
|
||||
// ["%c", 0xDC80, "<22>"],
|
||||
["%c", 0x110000, "<22>"],
|
||||
["%c", 0xfffffffff, "<22>"],
|
||||
|
||||
// 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();
|
Loading…
Reference in a new issue