shelving
Version:
Toolkit for using data in JavaScript.
172 lines (171 loc) • 7.17 kB
JavaScript
import { isArray } from "./array.js";
import { requireCurrencyCode } from "./currency.js";
import { isDate, requireDate } from "./date.js";
import { getPercent } from "./number.js";
import { isObject } from "./object.js";
import { isURI, requireURI } from "./uri.js";
import { requireURL } from "./url.js";
/** Format a boolean as "Yes" or "No". */
export function formatBoolean(value) {
return value ? "Yes" : "No";
}
/** Format a number (based on the user's browser language settings). */
export function formatNumber(num, options) {
return Intl.NumberFormat(options?.locale, options).format(num);
}
/** Format a number range (based on the user's browser language settings). */
export function formatRange(from, to, options) {
return Intl.NumberFormat(options?.locale, options).formatRange(from, to);
}
/**
* Format a quantity of a given unit.
*
* - Javascript has built-in support for formatting a number of different units.
* - Unfortunately the list of supported units changes in different browsers.
* - Ideally we want to format units using the built-in formatting so things like translation and internationalisation are covered.
* - But we want provide fallback formatting for unsupported units, and do something _good enough_ job in most cases.
*/
export function formatUnit(num, unit, options) {
// Check if the unit is supported by the browser.
if (Intl.supportedValuesOf("unit").includes(unit))
return Intl.NumberFormat(options?.locale, { ...options, style: "unit", unit }).format(num);
// Otherwise, use the default number format.
const str = Intl.NumberFormat(options?.locale, { ...options, style: "decimal" }).format(num);
const { unitDisplay, abbr = unit, one = unit, many = `${one}s` } = options ?? {};
if (unitDisplay === "long")
return `${str} ${str === "1" ? one : many}`;
return `${str}${unitDisplay === "narrow" ? "" : " "}${abbr}`; // "short" is the default.
}
/** Format a currency amount (based on the user's browser language settings). */
export function formatCurrency(amount, currency, options, caller = formatCurrency) {
return Intl.NumberFormat(options?.locale, {
style: "currency",
...options,
currency: requireCurrencyCode(currency, caller),
}).format(amount);
}
/**
* Format a percentage (combines `getPercent()` and `formatUnit()` for convenience).
* - Defaults to showing no decimal places.
* - Defaults to rounding closer to zero (so that 99.99% is shown as 99%).
* - Javascript's built-in percent formatting works on the `0` zero to `1` range. This uses `getPercent()` which works on `0` to `100` for convenience.
*
* @param numerator Number representing the amount of progress (e.g. `50`).
* @param denumerator The number representing the whole amount (defaults to 100).
*/
export function formatPercent(numerator, denumerator, options) {
return Intl.NumberFormat(options?.locale, {
style: "percent",
maximumFractionDigits: 0,
roundingMode: "floor",
...options,
}).format(getPercent(numerator, denumerator) / 100);
}
/**
* Format an unknown object as a string.
* - Use the custom `.toString()` function if it exists (don't use built in `Object.prototype.toString` because it's useless.
* - Use `.title` or `.name` or `.id` if they exist and are strings.
* - Use `Object` otherwise.
*/
export function formatObject(obj) {
if (typeof obj.toString === "function" && obj.toString !== Object.prototype.toString)
return obj.toString();
const name = obj.name;
if (typeof name === "string")
return name;
const title = obj.title;
if (typeof title === "string")
return title;
const id = obj.id;
if (typeof id === "string")
return id;
return "Object";
}
/** Format an unknown array as a string. */
export function formatArray(arr, options, caller = formatArray) {
return new Intl.ListFormat(undefined, { style: "long", type: "unit", ...options }).format(formatValues(arr, options, caller));
}
/** Format a date in the browser locale. */
export function formatDate(date, options, caller = formatDate) {
return requireDate(date, caller).toLocaleDateString(options?.locale, options);
}
/** Format a time in the browser locale (no seconds by default). */
export function formatTime(time, options, caller = formatTime) {
return requireDate(time, caller).toLocaleTimeString(options?.locale, {
hour: "2-digit",
minute: "2-digit",
second: undefined, // No seconds by default.
...options,
});
}
/** Format a datetime in the browser locale (no seconds by default). */
export function formatDateTime(date, options, caller = formatDateTime) {
return requireDate(date, caller).toLocaleString(options?.locale, {
year: "numeric",
month: "numeric",
day: "numeric",
hour: "2-digit",
minute: "2-digit",
// No seconds by default.
...options,
});
}
/**
* Format a URI as a user-friendly string
* e.g. `mailto:dave@shax.com` → `dave@shax.com`
* e.g. `http://shax.com/test?uid=129483` → `shax.com/test`
*/
export function formatURI(url, caller = formatURI) {
return _formatURI(requireURI(url, caller));
}
function _formatURI({ host, pathname }) {
return `${host}${pathname.endsWith("/") ? pathname.slice(0, -1) : pathname}`;
}
/**
* Format a URI as a string.
* e.g. `http://shax.com/test?uid=129483` → `shax.com/test`
*/
export function formatURL(url, base, caller = formatURL) {
return _formatURI(requireURL(url, base, caller));
}
/**
* Convert any unknown value into a friendly string for user-facing use.
* - Strings return the string.
* - Booleans return `"Yes"` or `"No"`
* - Numbers return formatted number with commas etc (e.g. `formatNumber()`).
* - Dates return formatted datetime (e.g. `formatDateTime()`).
* - Arrays return the array items converted to string (with `toTitle()`), and joined with a comma.
* - Objects return...
* 1. `object.name` if it exists, or
* 2. `object.title` if it exists.
* - Falsy values like `null` and `undefined` return `"None"`
* - Everything else returns `"Unknown"`
*/
export function formatValue(value, options, caller = formatValue) {
if (value === null || value === undefined)
return "None";
if (typeof value === "boolean")
return formatBoolean(value);
if (typeof value === "string")
return value || "None";
if (typeof value === "number")
return formatNumber(value, options);
if (typeof value === "symbol")
return value.description || "Symbol";
if (typeof value === "function")
return "Function";
if (isDate(value))
return formatDateTime(value, options, caller);
if (isArray(value))
return formatArray(value, options, caller);
if (isObject(value))
return formatObject(value);
if (isURI(value))
return formatURI(value, caller);
return "Unknown";
}
/** Format a sequence of values. */
export function* formatValues(values, options, caller = formatValues) {
for (const v of values)
yield formatValue(v, options, caller);
}