cockpit.js: add new number formatting API style

Add a new simplified style for all of the units-based number formatting
APIs (bytes, bytes/sec, bits/sec).  Keep loose versions of the old APIs
around in `cockpit.d.ts`, but mark them deprecated.

Make sure we test the new API variant from test-format.js.
This commit is contained in:
Allison Karlitskaya 2024-04-18 11:59:29 +02:00
parent a49d9e33de
commit ef5fa2d0ac
4 changed files with 83 additions and 80 deletions

View File

@ -44,75 +44,44 @@ string = cockpit.format_number(number, [precision])
<refsection id="cockpit-format-bytes">
<title>cockpit.format_bytes()</title>
<programlisting>
string = cockpit.format_bytes(number, [factor])
array = cockpit.format_bytes(number, [factor, options])
string = cockpit.format_bytes(number, [options])
</programlisting>
<para>Formats <code>number</code> into a displayable <code>string</code> with a suffix, such as
<emphasis>KB</emphasis> or <emphasis>MB</emphasis>. Returns an <code>array</code> of the
formatted number and the suffix if <code>options.separate</code> is set to <code>true</code>.</para>
<emphasis>KB</emphasis> or <emphasis>MB</emphasis>.</para>
<para>If specifying 1000 or 1024 is specified as a <code>factor</code> then an appropriate suffix
will be chosen. By default the <code>factor</code> is 1000. You can pass a string suffix as a
<code>factor</code> in which case the resulting number will be formatted with the same suffix.</para>
<para>If the <code>number</code> is less than the <code>factor</code> or an unknown factor
was passed in, then the formatted number is returned without a suffix. If <code>options.separate</code>
is true, returns an array of <code>[formatted_number, suffix]</code> or
<code>[formatted_number]</code> if returned without a suffix.</para>
<para>By default, SI units are used. IEC units (1024-based) can be requested by including
<code>base2: true</code> in <code>options</code>.</para>
<para>By default, non-integer numbers will be formatted with 3 digits of precision. This can be changed
with <code>options.precision</code>.</para>
<para>If <code>number</code> is <code>null</code> or <code>undefined</code> an empty string or
an array without a suffix will be returned.</para>
<para>If <code>number</code> is <code>null</code> or <code>undefined</code> an empty string will be
returned.</para>
</refsection>
<refsection id="cockpit-format-bytes-per-sec">
<title>cockpit.format_bytes_per_sec()</title>
<programlisting>
string = cockpit.format_bytes_per_sec(number, [factor])
array = cockpit.format_bytes_per_sec(number, [factor, options])
string = cockpit.format_bytes_per_sec(number, [options])
</programlisting>
<para>Format <code>number</code> of bytes into a displayable speed <code>string</code>.</para>
<para>If specifying 1000 or 1024 is specified as a <code>factor</code> then an appropriate suffix
will be chosen. By default the <code>factor</code> is 1000. You can pass a string suffix as a
<code>factor</code> in which case the resulting number will be formatted with the same suffix.</para>
<para>If the <code>number</code> is less than the <code>factor</code> or an unknown factor
was passed in, then the formatted number is returned without a suffix. If <code>options.separate</code>
is true, returns an array of <code>[formatted_number, suffix]</code> or
<code>[formatted_number]</code> if returned without a suffix.</para>
<para>By default, non-integer numbers will be formatted with 3 digits of precision. This can be changed
with <code>options.precision</code>.</para>
<para>If <code>number</code> is <code>null</code> or <code>undefined</code> an empty string or array
will be returned.</para>
<para>This function is mostly equivalent to <code>cockpit.format_bytes()</code> but the returned
value contains a unit like <emphasis>KB/s</emphasis> or <emphasis>MB/s</emphasis>.</para>
</refsection>
<refsection id="cockpit-format-bits-per-sec">
<title>cockpit.format_bits_per_sec()</title>
<programlisting>
string = cockpit.format_bits_per_sec(number, [factor])
array = cockpit.format_bytes_per_sec(number, [factor, options])
string = cockpit.format_bits_per_sec(number, [options])
</programlisting>
<para>Format <code>number</code> of bits into a displayable speed <code>string</code>.</para>
<para>If specifying 1000 or 1024 is specified as a <code>factor</code> then an appropriate suffix
will be chosen. By default the <code>factor</code> is 1000. You can pass a string suffix as a
<code>factor</code> in which case the resulting number will be formatted with the same suffix.</para>
<para>This function is mostly equivalent to <code>cockpit.format_bytes()</code> but the returned
value contains a unit like <emphasis>kbps</emphasis> or <emphasis>Mbps</emphasis>.</para>
<para>If the <code>number</code> is less than the <code>factor</code> or an unknown factor
was passed in, then the formatted number is returned without a suffix. If <code>options.separate</code>
is true, returns an array of <code>[formatted_number, suffix]</code> or
<code>[formatted_number]</code> if returned without a suffix.</para>
<para>By default, non-integer numbers will be formatted with 3 digits of precision. This can be changed
with <code>options.precision</code>.</para>
<para>If <code>number</code> is <code>null</code> or <code>undefined</code> an empty string or array
will be returned.</para>
<para>This function does not support IEC units. <code>base2</code> may not be passed as part of
<code>options</code>.</para>
</refsection>
<refsection id="cockpit-info">

View File

@ -106,7 +106,18 @@ QUnit.test("format_bytes", function (assert) {
[null, "KB", ""],
];
assert.expect(checks.length * 2 + 2);
for (let i = 0; i < checks.length; i++) {
if (typeof checks[i][1] === 'string') {
// these tests are for backwards compatibility only
continue;
}
const base2 = checks[i][1] == 1024;
assert.strictEqual(cockpit.format_bytes(checks[i][0], { base2 }), checks[i][2],
f`format_bytes(${checks[i][0]}, ${{ base2 }})`);
}
// old API style (deprecated)
for (let i = 0; i < checks.length; i++) {
assert.strictEqual(cockpit.format_bytes(checks[i][0], checks[i][1]), checks[i][2],
f`format_bytes(${checks[i][0]}, ${checks[i][1]})`
@ -146,16 +157,26 @@ QUnit.test("format_bytes_per_sec", function (assert) {
[25555678, "kB/s", { precision: 2 }, "25556 kB/s"],
];
assert.expect(checks.length + 2);
for (let i = 0; i < checks.length; i++) {
if (typeof checks[i][1] === 'string') {
// these tests are for backwards compatibility only
continue;
}
const base2 = checks[i][1] == 1024;
assert.strictEqual(cockpit.format_bytes_per_sec(checks[i][0], { base2, ...checks[i][2] }), checks[i][3],
f`format_bytes_per_sec(${checks[i][0]}, ${{ base2, ...checks[i][2] }})`);
}
// old API style (deprecated)
for (let i = 0; i < checks.length; i++) {
assert.strictEqual(cockpit.format_bytes_per_sec(checks[i][0], checks[i][1], checks[i][2]), checks[i][3],
f`format_bytes_per_sec(${checks[i][0]}, ${checks[i][1]}, ${checks[i][2]})`);
}
// separate unit
// separate unit (very deprecated)
assert.deepEqual(cockpit.format_bytes_per_sec(2555, 1024, { separate: true }),
["2.50", "KiB/s"]);
// backwards compatible API for separate flag
// backwards compatible API for separate flag (oh so very deprecated)
assert.deepEqual(cockpit.format_bytes_per_sec(2555, 1024, true),
["2.50", "KiB/s"]);
});

29
pkg/lib/cockpit.d.ts vendored
View File

@ -193,11 +193,6 @@ declare module 'cockpit' {
/* === String helpers ======================== */
type FormatOptions = {
precision?: number;
separate?: boolean;
};
function message(problem: string | JsonObject): string;
function gettext(message: string): string;
@ -206,11 +201,21 @@ declare module 'cockpit' {
function ngettext(context: string, message1: string, messageN: string, n: number): string;
function format(format_string: string, ...args: unknown[]): string;
function format_number(n: number, precision?: number): string
function format_bytes(n: number, factor?: 1000 | 1024, options?: FormatOptions & { separate?: false }): string;
function format_bytes(n: number, factor: 1000 | 1024, options: FormatOptions & { separate: true }): string[];
function format_bytes_per_sec(n: number, factor?: 1000 | 1024, options?: FormatOptions & { separate?: false }): string;
function format_bytes_per_sec(n: number, factor: 1000 | 1024, options: FormatOptions & { separate: true }): string[];
function format_bits_per_sec(n: number, factor?: 1000 | 1024, options?: FormatOptions & { separate?: false }): string;
function format_bits_per_sec(n: number, factor: 1000 | 1024, options: FormatOptions & { separate: true }): string[];
/* === Number formatting ===================== */
type FormatOptions = {
precision?: number;
base2?: boolean;
};
type MaybeNumber = number | null | undefined;
function format_number(n: MaybeNumber, precision?: number): string
function format_bytes(n: MaybeNumber, options?: FormatOptions): string;
function format_bytes_per_sec(n: MaybeNumber, options?: FormatOptions): string;
function format_bits_per_sec(n: MaybeNumber, options?: FormatOptions & { base2?: false }): string;
/** @deprecated */ function format_bytes(n: MaybeNumber, factor: unknown, options?: object | boolean): string | string[];
/** @deprecated */ function format_bytes_per_sec(n: MaybeNumber, factor: unknown, options?: object | boolean): string | string[];
/** @deprecated */ function format_bits_per_sec(n: MaybeNumber, factor: unknown, options?: object | boolean): string | string[];
}

View File

@ -1483,10 +1483,24 @@ function factory() {
});
};
function format_units(number, suffixes, factor, options) {
// backwards compat: "options" argument position used to be a boolean flag "separate"
if (!is_object(options))
options = { separate: options };
let deprecated_format_warned = false;
function format_units(suffixes, number, second_arg, third_arg) {
let options = second_arg;
let factor = options?.base2 ? 1024 : 1000;
// compat API: we used to accept 'factor' as a separate second arg
if (third_arg || (second_arg && !is_object(second_arg))) {
if (!deprecated_format_warned) {
console.warn(`cockpit.format_{bytes,bits}[_per_sec](..., ${second_arg}, ${third_arg}) is deprecated.`);
deprecated_format_warned = true;
}
factor = second_arg || 1000;
options = third_arg;
// double backwards compat: "options" argument position used to be a boolean flag "separate"
if (!is_object(options))
options = { separate: options };
}
let suffix = null;
@ -1525,7 +1539,7 @@ function factory() {
}
}
const string_representation = cockpit.format_number(number, options.precision);
const string_representation = cockpit.format_number(number, options?.precision);
let ret;
if (string_representation && suffix)
@ -1533,7 +1547,7 @@ function factory() {
else
ret = [string_representation];
if (!options.separate)
if (!options?.separate)
ret = ret.join(" ");
return ret;
@ -1544,10 +1558,8 @@ function factory() {
1024: [null, "KiB", "MiB", "GiB", "TiB", "PiB", "EiB", "ZiB"]
};
cockpit.format_bytes = function format_bytes(number, factor, options) {
if (factor === undefined)
factor = 1000;
return format_units(number, byte_suffixes, factor, options);
cockpit.format_bytes = function format_bytes(number, ...args) {
return format_units(byte_suffixes, number, ...args);
};
const byte_sec_suffixes = {
@ -1555,20 +1567,16 @@ function factory() {
1024: ["B/s", "KiB/s", "MiB/s", "GiB/s", "TiB/s", "PiB/s", "EiB/s", "ZiB/s"]
};
cockpit.format_bytes_per_sec = function format_bytes_per_sec(number, factor, options) {
if (factor === undefined)
factor = 1000;
return format_units(number, byte_sec_suffixes, factor, options);
cockpit.format_bytes_per_sec = function format_bytes_per_sec(number, ...args) {
return format_units(byte_sec_suffixes, number, ...args);
};
const bit_suffixes = {
1000: ["bps", "Kbps", "Mbps", "Gbps", "Tbps", "Pbps", "Ebps", "Zbps"]
};
cockpit.format_bits_per_sec = function format_bits_per_sec(number, factor, options) {
if (factor === undefined)
factor = 1000;
return format_units(number, bit_suffixes, factor, options);
cockpit.format_bits_per_sec = function format_bits_per_sec(number, ...args) {
return format_units(bit_suffixes, number, ...args);
};
/* ---------------------------------------------------------------------