export { get_start_time_ts, get_uptime, log, wrap_log } from "./log";
export * from "./misc-path";
import LRU from "lru-cache";
import {
is_array,
is_integer,
is_object,
is_string,
is_date,
is_set,
} from "./type-checking";
export { is_array, is_integer, is_object, is_string, is_date, is_set };
export {
map_limit,
map_max,
map_min,
sum,
is_zero_map,
map_without_undefined_and_null,
map_mutate_out_undefined_and_null,
} from "./maps";
export { done, done1, done2 } from "./done";
export {
cmp,
cmp_Date,
cmp_dayjs,
cmp_moment,
cmp_array,
timestamp_cmp,
field_cmp,
is_different,
is_different_array,
shallowCompare,
all_fields_equal,
} from "./cmp";
export {
server_time,
server_milliseconds_ago,
server_seconds_ago,
server_minutes_ago,
server_hours_ago,
server_days_ago,
server_weeks_ago,
server_months_ago,
milliseconds_before,
seconds_before,
minutes_before,
hours_before,
days_before,
weeks_before,
months_before,
expire_time,
YEAR,
} from "./relative-time";
import sha1 from "sha1";
export { sha1 };
function base16ToBase64(hex) {
return Buffer.from(hex, "hex").toString("base64");
}
export function sha1base64(s) {
return base16ToBase64(sha1(s));
}
import getRandomValues from "get-random-values";
import * as lodash from "lodash";
import * as immutable from "immutable";
export const keys: (any) => string[] = lodash.keys;
import { required, defaults, types } from "./opts";
export { required, defaults, types };
interface SplittedPath {
head: string;
tail: string;
}
export function path_split(path: string): SplittedPath {
const v = path.split("/");
return { head: v.slice(0, -1).join("/"), tail: v[v.length - 1] };
}
export function capitalize(s?: string): string {
if (!s) return "";
return s.charAt(0).toUpperCase() + s.slice(1);
}
export function make_valid_name(s: string): string {
return s.replace(/\W/g, "_").toLowerCase();
}
const filename_extension_re = /(?:\.([^.]+))?$/;
export function filename_extension(filename: string): string {
filename = path_split(filename).tail;
const match = filename_extension_re.exec(filename);
if (!match) {
return "";
}
const ext = match[1];
return ext ? ext : "";
}
export function filename_extension_notilde(filename: string): string {
let ext = filename_extension(filename);
while (ext && ext[ext.length - 1] === "~") {
ext = ext.slice(0, ext.length - 1);
}
return ext;
}
export function separate_file_extension(name: string): {
name: string;
ext: string;
} {
const ext: string = filename_extension(name);
if (ext !== "") {
name = name.slice(0, name.length - ext.length - 1);
}
return { name, ext };
}
export function change_filename_extension(
path: string,
new_ext: string,
): string {
const { name } = separate_file_extension(path);
return `${name}.${new_ext}`;
}
export function normalized_path_join(...parts): string {
const sep = "/";
const replace = new RegExp(sep + "{1,}", "g");
const result: string[] = [];
for (let x of Array.from(parts)) {
if (x != null && `${x}`.length > 0) {
result.push(`${x}`);
}
}
return result.join(sep).replace(replace, sep);
}
export function splitlines(s: string): string[] {
const r = s.match(/[^\r\n]+/g);
return r ? r : [];
}
export function split(s: string): string[] {
const r = s.match(/\S+/g);
if (r) {
return r;
} else {
return [];
}
}
export function merge(dest, ...objs) {
for (const obj of objs) {
for (const k in obj) {
dest[k] = obj[k];
}
}
return dest;
}
export function merge_copy(...objs): object {
return merge({}, ...Array.from(objs));
}
export function copy_with<T>(obj: T, w: string | string[]): Partial<T> {
if (typeof w === "string") {
w = [w];
}
const obj2: any = {};
let key: string;
for (key of w) {
const y = obj[key];
if (y !== undefined) {
obj2[key] = y;
}
}
return obj2;
}
export function copy_without(obj: object, w: string | string[]): object {
if (typeof w === "string") {
w = [w];
}
const r = {};
for (let key in obj) {
const y = obj[key];
if (!Array.from(w).includes(key)) {
r[key] = y;
}
}
return r;
}
import { cloneDeep } from "lodash";
export const deep_copy = cloneDeep;
export function set(v: string[]): { [key: string]: true } {
const s: { [key: string]: true } = {};
for (const x of v) {
s[x] = true;
}
return s;
}
export function copy<T>(obj: T): T {
return lodash.clone(obj);
}
export function startswith(s: any, x: string | string[]): boolean {
if (typeof s != "string") {
return false;
}
if (typeof x === "string") {
return s.startsWith(x);
}
for (const v of x) {
if (s.indexOf(v) === 0) {
return true;
}
}
return false;
}
export function endswith(s: any, t: any): boolean {
if (typeof s != "string" || typeof t != "string") {
return false;
}
return s.endsWith(t);
}
import { v4 as v4uuid } from "uuid";
export const uuid: () => string = v4uuid;
const uuid_regexp = new RegExp(
/[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}/i,
);
export function is_valid_uuid_string(uuid?: any): boolean {
return (
typeof uuid === "string" && uuid.length === 36 && uuid_regexp.test(uuid)
);
}
export function assert_valid_account_id(uuid?: any): void {
if (!is_valid_uuid_string(uuid)) {
throw new Error(`Invalid Account ID: ${uuid}`);
}
}
export const isValidUUID = is_valid_uuid_string;
export function assertValidAccountID(account_id?: any) {
if (!isValidUUID(account_id)) {
throw Error("account_id is invalid");
}
}
export function assert_uuid(uuid: string): void {
if (!is_valid_uuid_string(uuid)) {
throw Error(`invalid uuid='${uuid}'`);
}
}
export function uuidsha1(data: string): string {
const s = sha1(data);
let i = -1;
return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, function (c) {
i += 1;
switch (c) {
case "x":
return s[i];
case "y":
return ((parseInt(`0x${s[i]}`, 16) & 0x3) | 0x8).toString(16);
}
});
}
export function len(obj: object | undefined | null): number {
if (obj == null) {
return 0;
}
return Object.keys(obj).length;
}
export function milliseconds_ago(ms: number): Date {
return new Date(Date.now() - ms);
}
export function seconds_ago(s: number) {
return milliseconds_ago(1000 * s);
}
export function minutes_ago(m: number) {
return seconds_ago(60 * m);
}
export function hours_ago(h: number) {
return minutes_ago(60 * h);
}
export function days_ago(d: number) {
return hours_ago(24 * d);
}
export function weeks_ago(w: number) {
return days_ago(7 * w);
}
export function months_ago(m: number) {
return days_ago(30.5 * m);
}
export function how_long_ago_ms(ts: Date | number): number {
const ts_ms = typeof ts === "number" ? ts : ts.getTime();
return Date.now() - ts_ms;
}
export function how_long_ago_s(ts: Date | number): number {
return how_long_ago_ms(ts) / 1000;
}
export function how_long_ago_m(ts: Date | number): number {
return how_long_ago_s(ts) / 60;
}
export function mswalltime(t?: number): number {
return Date.now() - (t ?? 0);
}
export function walltime(t?: number): number {
return mswalltime() / 1000.0 - (t ?? 0);
}
export function encode_path(path) {
path = encodeURI(path);
return path.replace(/#/g, "%23").replace(/\?/g, "%3F");
}
const reValidEmail = (function () {
const sQtext = "[^\\x0d\\x22\\x5c\\x80-\\xff]";
const sDtext = "[^\\x0d\\x5b-\\x5d\\x80-\\xff]";
const sAtom =
"[^\\x00-\\x20\\x22\\x28\\x29\\x2c\\x2e\\x3a-\\x3c\\x3e\\x40\\x5b-\\x5d\\x7f-\\xff]+";
const sQuotedPair = "\\x5c[\\x00-\\x7f]";
const sDomainLiteral = `\\x5b(${sDtext}|${sQuotedPair})*\\x5d`;
const sQuotedString = `\\x22(${sQtext}|${sQuotedPair})*\\x22`;
const sDomain_ref = sAtom;
const sSubDomain = `(${sDomain_ref}|${sDomainLiteral})`;
const sWord = `(${sAtom}|${sQuotedString})`;
const sDomain = sSubDomain + "(\\x2e" + sSubDomain + ")*";
const sLocalPart = sWord + "(\\x2e" + sWord + ")*";
const sAddrSpec = sLocalPart + "\\x40" + sDomain;
const sValidEmail = `^${sAddrSpec}$`;
return new RegExp(sValidEmail);
})();
export function is_valid_email_address(email: string): boolean {
if (reValidEmail.test(email)) {
return true;
} else {
return false;
}
}
export function assert_valid_email_address(email: string): void {
if (!is_valid_email_address(email)) {
throw Error(`Invalid email address: ${email}`);
}
}
export const to_json = JSON.stringify;
export function plural(
number: number = 0,
singular: string,
plural: string = `${singular}s`,
) {
if (["GB", "G", "MB"].includes(singular)) {
return singular;
}
if (number === 1) {
return singular;
} else {
return plural;
}
}
const ELLIPSIS = "…";
export function trunc<T>(
sArg: T,
max_length = 1024,
ellipsis = ELLIPSIS,
): string | T {
if (sArg == null) {
return sArg;
}
const s = typeof sArg !== "string" ? `${sArg}` : sArg;
if (s.length > max_length) {
if (max_length < 1) {
throw new Error("ValueError: max_length must be >= 1");
}
return s.slice(0, max_length - 1) + ellipsis;
} else {
return s;
}
}
export function trunc_middle<T>(
sArg: T,
max_length = 1024,
ellipsis = ELLIPSIS,
): T | string {
if (sArg == null) {
return sArg;
}
const s = typeof sArg !== "string" ? `${sArg}` : sArg;
if (s.length <= max_length) {
return s;
}
if (max_length < 1) {
throw new Error("ValueError: max_length must be >= 1");
}
const n = Math.floor(max_length / 2);
return (
s.slice(0, n - 1 + (max_length % 2 ? 1 : 0)) +
ellipsis +
s.slice(s.length - n)
);
}
export function trunc_left<T>(
sArg: T,
max_length = 1024,
ellipsis = ELLIPSIS,
): T | string {
if (sArg == null) {
return sArg;
}
const s = typeof sArg !== "string" ? `${sArg}` : sArg;
if (s.length > max_length) {
if (max_length < 1) {
throw new Error("ValueError: max_length must be >= 1");
}
return ellipsis + s.slice(s.length - max_length + 1);
} else {
return s;
}
}
export function getIn(x: any, path: string[], default_value?: any): any {
for (const key of path) {
if (x !== undefined) {
try {
x = x[key];
} catch (err) {
return default_value;
}
} else {
return default_value;
}
}
return x === undefined ? default_value : x;
}
export function replace_all(
s: string,
search: string,
replace: string,
): string {
return s.split(search).join(replace);
}
export function replace_all_function(
s: string,
search: string,
replace_f: (i: number) => string,
): string {
const v = s.split(search);
const w: string[] = [];
for (let i = 0; i < v.length; i++) {
w.push(v[i]);
if (i < v.length - 1) {
w.push(replace_f(i));
}
}
return w.join("");
}
export function path_to_title(path: string): string {
const subtitle = separate_file_extension(path_split(path).tail).name;
return capitalize(replace_all(replace_all(subtitle, "-", " "), "_", " "));
}
export function list_alternatives(names): string {
names = names.map((x) => x.toUpperCase()).toJS();
if (names.length == 1) {
return names[0];
} else if (names.length == 2) {
return `${names[0]} or ${names[1]}`;
}
return names.join(", ");
}
export function to_user_string(x: any): string {
switch (typeof x) {
case "undefined":
return "undefined";
case "number":
case "symbol":
case "boolean":
return x.toString();
case "function":
return x.toString();
case "object":
if (typeof x.toString !== "function") {
return JSON.stringify(x);
}
const a = x.toString();
if (a === "[object Object]") {
return JSON.stringify(x);
} else {
return a;
}
default:
return JSON.stringify(x);
}
}
export function delete_null_fields(obj: object): void {
for (const k in obj) {
if (obj[k] == null) {
delete obj[k];
}
}
}
export function unreachable(x: never) {
const tmp: never = x;
tmp;
}
function get_methods(obj: object): string[] {
let properties = new Set<string>();
let current_obj = obj;
do {
Object.getOwnPropertyNames(current_obj).map((item) => properties.add(item));
} while ((current_obj = Object.getPrototypeOf(current_obj)));
return [...properties.keys()].filter(
(item) => typeof obj[item] === "function",
);
}
export function bind_methods<T extends object>(
obj: T,
method_names: undefined | string[] = undefined,
): T {
if (method_names === undefined) {
method_names = get_methods(obj);
method_names.splice(method_names.indexOf("constructor"), 1);
}
for (const method_name of method_names) {
obj[method_name] = obj[method_name].bind(obj);
}
return obj;
}
export function human_readable_size(
bytes: number | null | undefined,
short = false,
): string {
if (bytes == null) {
return "?";
}
if (bytes < 1000) {
return `${bytes} ${short ? "b" : "bytes"}`;
}
if (bytes < 1000000) {
const b = Math.floor(bytes / 100);
return `${b / 10} KB`;
}
if (bytes < 1000000000) {
const b = Math.floor(bytes / 100000);
return `${b / 10} MB`;
}
const b = Math.floor(bytes / 100000000);
return `${b / 10} GB`;
}
export const re_url =
/(http:\/\/www\.|https:\/\/www\.|http:\/\/|https:\/\/)?[a-z0-9]+([\-\.]{1}[a-z0-9]+)*\.[a-z]{2,5}(:[0-9]{1,5})?(\/.*)?/gi;
export function contains_url(str: string): boolean {
return !!str.toLowerCase().match(re_url);
}
export function hidden_meta_file(path: string, ext: string): string {
const p = path_split(path);
let head: string = p.head;
if (head !== "") {
head += "/";
}
return head + "." + p.tail + "." + ext;
}
export function history_path(path: string): string {
return hidden_meta_file(path, "time-travel");
}
export function meta_file(path: string, ext: string): string {
return hidden_meta_file(path, "sage-" + ext);
}
export function tuple<T extends string[]>(o: T) {
return o;
}
export function aux_file(path: string, ext: string): string {
const s = path_split(path);
s.tail += "." + ext;
if (s.head) {
return s.head + "/." + s.tail;
} else {
return "." + s.tail;
}
}
export function auxFileToOriginal(path: string): string {
const { head, tail } = path_split(path);
const i = tail.lastIndexOf(".");
const filename = tail.slice(1, i);
if (!head) {
return filename;
}
return head + "/" + filename;
}
const BASE58 = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
export function secure_random_token(
length: number = 16,
alphabet: string = BASE58,
): string {
let s = "";
if (length == 0) return s;
if (alphabet.length == 0) {
throw Error("impossible, since alphabet is empty");
}
const v = new Uint8Array(length);
getRandomValues(v);
for (const i of v) {
s += alphabet[i % alphabet.length];
}
return s;
}
export function random_choice(v: any[]): any {
return v[Math.floor(Math.random() * v.length)];
}
export function close(obj: object, omit?: Set<string>): void {
if (omit != null) {
Object.keys(obj).forEach(function (key) {
if (omit.has(key)) return;
if (typeof obj[key] == "function") return;
delete obj[key];
});
} else {
Object.keys(obj).forEach(function (key) {
if (typeof obj[key] == "function") return;
delete obj[key];
});
}
}
export function contains(word: string, sub: string): boolean {
return word.indexOf(sub) !== -1;
}
export function assertDefined<T>(val: T): asserts val is NonNullable<T> {
if (val === undefined || val === null) {
throw new Error(`Expected 'val' to be defined, but received ${val}`);
}
}
export function round1(num: number): number {
return Math.round(num * 10) / 10;
}
export function round2(num: number): number {
return Math.round((num + 0.00001) * 100) / 100;
}
export function round3(num: number): number {
return Math.round((num + 0.000001) * 1000) / 1000;
}
export function round4(num: number): number {
return Math.round((num + 0.0000001) * 10000) / 10000;
}
export function round2up(num: number): number {
const rnd = parseFloat(num.toFixed(2));
if (rnd >= num) {
return rnd;
}
return parseFloat((num + 0.01).toFixed(2));
}
export function round2down(num: number): number {
const rnd = parseFloat(num.toFixed(2));
if (rnd <= num) {
return rnd;
}
return parseFloat((num - 0.01).toFixed(2));
}
export function parse_number_input(
input: any,
round_number: boolean = true,
allow_negative: boolean = false,
): number | undefined {
if (typeof input == "boolean") {
return input ? 1 : 0;
}
if (typeof input == "number") {
if (!isFinite(input)) {
return;
}
if (!allow_negative && input < 0) {
return;
}
return input;
}
if (input == null || !input) return 0;
let val;
const v = `${input}`.split("/");
if (v.length !== 1 && v.length !== 2) {
return undefined;
}
if (v.length === 2) {
val = parseFloat(v[0]) / parseFloat(v[1]);
}
if (v.length === 1) {
val = parseFloat(v[0]);
if (isNaN(val) || v[0].trim() === "") {
return undefined;
}
}
if (round_number) {
val = round2(val);
}
if (isNaN(val) || val === Infinity || (val < 0 && !allow_negative)) {
return undefined;
}
return val;
}
export function coerce_codomain_to_numbers(map: { [k: string]: any }): {
[k: string]: number;
} {
for (const k in map) {
const x = map[k];
if (typeof x === "boolean") {
map[k] = x ? 1 : 0;
} else {
try {
const t = parseFloat(x);
if (isFinite(t)) {
map[k] = t;
} else {
map[k] = 0;
}
} catch (_) {
map[k] = 0;
}
}
}
return map;
}
export function map_sum(
a?: { [k: string]: number },
b?: { [k: string]: number },
): { [k: string]: number } {
if (a == null) {
return coerce_codomain_to_numbers(b ?? {});
}
if (b == null) {
return coerce_codomain_to_numbers(a ?? {});
}
a = coerce_codomain_to_numbers(a);
b = coerce_codomain_to_numbers(b);
const c: { [k: string]: number } = {};
for (const k in a) {
c[k] = (a[k] ?? 0) + (b[k] ?? 0);
}
for (const k in b) {
if (c[k] == null) {
c[k] = b[k] ?? 0;
}
}
return c;
}
export function map_diff(
a?: { [k: string]: number },
b?: { [k: string]: number },
): { [k: string]: number } {
if (b == null) {
return coerce_codomain_to_numbers(a ?? {});
}
b = coerce_codomain_to_numbers(b);
const c: { [k: string]: number } = {};
if (a == null) {
for (const k in b) {
c[k] = -(b[k] ?? 0);
}
return c;
}
a = coerce_codomain_to_numbers(a);
for (const k in a) {
c[k] = (a[k] ?? 0) - (b[k] ?? 0);
}
for (const k in b) {
if (c[k] == null) {
c[k] = -(b[k] ?? 0);
}
}
return c;
}
export function search_split(
search: string,
allowRegexp: boolean = true,
regexpOptions: string = "i",
): (string | RegExp)[] {
search = search.trim();
if (
allowRegexp &&
search.length > 2 &&
search[0] == "/" &&
search[search.length - 1] == "/"
) {
const t = stringOrRegExp(search, regexpOptions);
if (typeof t != "string") {
return [t];
}
}
const terms: (string | RegExp)[] = [];
const v = search.split('"');
const { length } = v;
for (let i = 0; i < v.length; i++) {
let element = v[i];
element = element.trim();
if (element.length == 0) continue;
if (i % 2 === 0 || (i === length - 1 && length % 2 === 0)) {
for (const s of split(element)) {
terms.push(allowRegexp ? stringOrRegExp(s, regexpOptions) : s);
}
} else {
terms.push(
allowRegexp ? stringOrRegExp(element, regexpOptions) : element,
);
}
}
return terms;
}
function stringOrRegExp(s: string, options: string): string | RegExp {
if (s.length < 2 || s[0] != "/" || s[s.length - 1] != "/")
return s.toLowerCase();
try {
return new RegExp(s.slice(1, -1), options);
} catch (_err) {
return s.toLowerCase();
}
}
function isMatch(s: string, x: string | RegExp): boolean {
if (typeof x == "string") {
if (x[0] == "-") {
if (x.length == 1) {
return true;
}
return !isMatch(s, x.slice(1));
}
if (x[0] === "#") {
return s.search(new RegExp(x + "\\b")) != -1;
}
return s.includes(x);
} else {
return x.test?.(s);
}
return false;
}
export function search_match(s: string, v: (string | RegExp)[]): boolean {
if (typeof s != "string" || !is_array(v)) {
return false;
}
s = s.toLowerCase();
const s1 = s.replace(/\\/g, "");
for (let x of v) {
if (!isMatch(s, x) && !isMatch(s1, x)) return false;
}
return true;
}
export let RUNNING_IN_NODE: boolean;
try {
RUNNING_IN_NODE = process?.title == "node";
} catch (_err) {
RUNNING_IN_NODE = false;
}
const SOCKET_DATE_KEY = "DateEpochMS";
function socket_date_replacer(key: string, value: any): any {
const x = this[key];
return x instanceof Date ? { [SOCKET_DATE_KEY]: x.valueOf() } : value;
}
export function to_json_socket(x: any): string {
return JSON.stringify(x, socket_date_replacer);
}
function socket_date_parser(_key: string, value: any): any {
const x = value?.[SOCKET_DATE_KEY];
return x != null && len(value) == 1 ? new Date(x) : value;
}
export function from_json_socket(x: string): any {
try {
return JSON.parse(x, socket_date_parser);
} catch (err) {
console.debug(`from_json: error parsing ${x} (=${to_json(x)}) from JSON`);
throw err;
}
}
export function to_safe_str(x: any): string {
if (typeof x === "string") {
return x;
}
const obj = {};
for (const key in x) {
let value = x[key];
let sanitize = false;
if (
key.indexOf("pass") !== -1 ||
key.indexOf("token") !== -1 ||
key.indexOf("secret") !== -1
) {
sanitize = true;
} else if (typeof value === "string" && value.slice(0, 7) === "sha512$") {
sanitize = true;
}
if (sanitize) {
obj[key] = "(unsafe)";
} else {
if (typeof value === "object") {
value = "[object]";
} else if (typeof value === "string") {
value = trunc(value, 1000);
}
obj[key] = value;
}
}
return JSON.stringify(obj);
}
const reISO =
/^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2}(?:\.\d*))(?:Z|(\+|-)([\d|:]*))?$/;
export function date_parser(_key: string | undefined, value: any) {
if (typeof value === "string" && value.length >= 20 && reISO.exec(value)) {
return ISO_to_Date(value);
} else {
return value;
}
}
export function ISO_to_Date(s: string): Date {
if (s.indexOf("Z") === -1) {
s += "Z";
}
return new Date(s);
}
export function from_json(x: string): any {
try {
return JSON.parse(x, date_parser);
} catch (err) {
console.debug(`from_json: error parsing ${x} (=${to_json(x)}) from JSON`);
throw err;
}
}
export function fix_json_dates(obj: any, date_keys?: "all" | string[]) {
if (date_keys == null) {
return obj;
}
if (is_object(obj)) {
for (let k in obj) {
const v = obj[k];
if (typeof v === "object") {
fix_json_dates(v, date_keys);
} else if (
typeof v === "string" &&
v.length >= 20 &&
reISO.exec(v) &&
(date_keys === "all" || Array.from(date_keys).includes(k))
) {
obj[k] = new Date(v);
}
}
} else if (is_array(obj)) {
for (let i in obj) {
const x = obj[i];
obj[i] = fix_json_dates(x, date_keys);
}
} else if (
typeof obj === "string" &&
obj.length >= 20 &&
reISO.exec(obj) &&
date_keys === "all"
) {
return new Date(obj);
}
return obj;
}
function to_iso(d: Date): string {
return new Date(d.valueOf() - d.getTimezoneOffset() * 60 * 1000)
.toISOString()
.slice(0, -5);
}
export function to_iso_path(d: Date): string {
return to_iso(d).replace("T", "-").replace(/:/g, "");
}
export const has_key: (obj: object, path: string[] | string) => boolean =
lodash.has;
export const values = lodash.values;
export function dict(v: [string, any][]): { [key: string]: any } {
const obj: { [key: string]: any } = {};
for (let a of Array.from(v)) {
if (a.length !== 2) {
throw new Error("ValueError: unexpected length of tuple");
}
obj[a[0]] = a[1];
}
return obj;
}
export function remove(arr: any[], val: any): void {
for (
let i = 0, end = arr.length, asc = 0 <= end;
asc ? i < end : i > end;
asc ? i++ : i--
) {
if (arr[i] === val) {
arr.splice(i, 1);
return;
}
}
throw new Error("ValueError -- item not in array");
}
export const max: (x: any[]) => any = lodash.max;
export const min: (x: any[]) => any = lodash.min;
export function path_to_file(path: string = "", file: string): string {
if (path === "") {
return file;
}
return path + "/" + file;
}
export function original_path(path: string): string {
const s = path_split(path);
if (s.tail[0] != "." || s.tail.indexOf(".sage-") == -1) {
return path;
}
const ext = filename_extension(s.tail);
let x = s.tail.slice(
s.tail[0] === "." ? 1 : 0,
s.tail.length - (ext.length + 1),
);
if (s.head !== "") {
x = s.head + "/" + x;
}
return x;
}
export function lower_email_address(email_address: any): string {
if (email_address == null) {
return "";
}
if (typeof email_address !== "string") {
email_address = JSON.stringify(email_address);
}
return email_address.toLowerCase();
}
export function parse_user_search(query: string): {
string_queries: string[][];
email_queries: string[];
} {
const r = { string_queries: [] as string[][], email_queries: [] as string[] };
if (typeof query !== "string") {
return r;
}
const queries = query
.split("\n")
.map((q1) => q1.split(/,|;/))
.reduce((acc, val) => acc.concat(val), [])
.map((q) => q.trim().toLowerCase());
const email_re = /<(.*)>/;
for (const x of queries) {
if (x) {
if (x.indexOf("@") === -1 || x.startsWith("@")) {
r.string_queries.push(x.split(/\s+/g));
} else {
for (let a of split(x)) {
if (a[0] === "<") {
const match = email_re.exec(a);
a = match != null ? match[1] : a;
}
if (is_valid_email_address(a)) {
r.email_queries.push(a);
}
}
}
}
}
return r;
}
export function delete_trailing_whitespace(s: string): string {
return s.replace(/[^\S\n]+$/gm, "");
}
export function retry_until_success(opts: {
f: Function;
start_delay?: number;
max_delay?: number;
factor?: number;
max_tries?: number;
max_time?: number;
log?: Function;
warn?: Function;
name?: string;
cb?: Function;
}): void {
let start_time;
opts = defaults(opts, {
f: required,
start_delay: 100,
max_delay: 20000,
factor: 1.4,
max_tries: undefined,
max_time: undefined,
log: undefined,
warn: undefined,
name: "",
cb: undefined,
});
let delta = opts.start_delay as number;
let tries = 0;
if (opts.max_time != null) {
start_time = new Date();
}
const g = function () {
tries += 1;
if (opts.log != null) {
if (opts.max_tries != null) {
opts.log(
`retry_until_success(${opts.name}) -- try ${tries}/${opts.max_tries}`,
);
}
if (opts.max_time != null) {
opts.log(
`retry_until_success(${opts.name}) -- try ${tries} (started ${
Date.now() - start_time
}ms ago; will stop before ${opts.max_time}ms max time)`,
);
}
if (opts.max_tries == null && opts.max_time == null) {
opts.log(`retry_until_success(${opts.name}) -- try ${tries}`);
}
}
opts.f(function (err) {
if (err) {
if (err === "not_public") {
opts.cb?.("not_public");
return;
}
if (err && opts.warn != null) {
opts.warn(`retry_until_success(${opts.name}) -- err=${err}`);
}
if (opts.log != null) {
opts.log(`retry_until_success(${opts.name}) -- err=${err}`);
}
if (opts.max_tries != null && opts.max_tries <= tries) {
opts.cb?.(
`maximum tries (=${opts.max_tries}) exceeded - last error ${err}`,
err,
);
return;
}
delta = Math.min(
opts.max_delay as number,
(opts.factor as number) * delta,
);
if (
opts.max_time != null &&
Date.now() - start_time + delta > opts.max_time
) {
opts.cb?.(
`maximum time (=${opts.max_time}ms) exceeded - last error ${err}`,
err,
);
return;
}
return setTimeout(g, delta);
} else {
if (opts.log != null) {
opts.log(`retry_until_success(${opts.name}) -- success`);
}
opts.cb?.();
}
});
};
g();
}
export class StringCharMapping {
private _to_char: { [s: string]: string } = {};
private _next_char: string = "A";
public _to_string: { [s: string]: string } = {};
constructor(opts?) {
let ch, st;
this.find_next_char = this.find_next_char.bind(this);
this.to_string = this.to_string.bind(this);
this.to_array = this.to_array.bind(this);
if (opts == null) {
opts = {};
}
opts = defaults(opts, {
to_char: undefined,
to_string: undefined,
});
if (opts.to_string != null) {
for (ch in opts.to_string) {
st = opts.to_string[ch];
this._to_string[ch] = st;
this._to_char[st] = ch;
}
}
if (opts.to_char != null) {
for (st in opts.to_char) {
ch = opts.to_char[st];
this._to_string[ch] = st;
this._to_char[st] = ch;
}
}
this.find_next_char();
}
private find_next_char(): void {
while (true) {
this._next_char = String.fromCharCode(this._next_char.charCodeAt(0) + 1);
if (this._to_string[this._next_char] == null) {
break;
}
}
}
public to_string(strings: string[]): string {
let t = "";
for (const s of strings) {
const a = this._to_char[s];
if (a != null) {
t += a;
} else {
t += this._next_char;
this._to_char[s] = this._next_char;
this._to_string[this._next_char] = s;
this.find_next_char();
}
}
return t;
}
public to_array(x: string): string[] {
return Array.from(x).map((s) => this.to_string[s]);
}
public _debug_get_to_char() {
return this._to_char;
}
public _debug_get_next_char() {
return this._next_char;
}
}
export const PROJECT_GROUPS: string[] = [
"owner",
"collaborator",
"viewer",
"invited_collaborator",
"invited_viewer",
];
export function parse_bup_timestamp(s: string): Date {
const v = [
s.slice(0, 4),
s.slice(5, 7),
s.slice(8, 10),
s.slice(11, 13),
s.slice(13, 15),
s.slice(15, 17),
"0",
];
return new Date(`${v[1]}/${v[2]}/${v[0]} ${v[3]}:${v[4]}:${v[5]} UTC`);
}
export function hash_string(s: string): number {
if (typeof s != "string") {
return 0;
}
let hash = 0;
if (s.length === 0) {
return hash;
}
const n = s.length;
for (let i = 0; i < n; i++) {
const chr = s.charCodeAt(i);
hash = (hash << 5) - hash + chr;
hash |= 0;
}
return hash;
}
export function parse_hashtags(t?: string): [number, number][] {
const v: [number, number][] = [];
if (typeof t != "string") {
return v;
}
let base = 0;
while (true) {
let i: number = t.indexOf("#");
if (i === -1 || i === t.length - 1) {
return v;
}
base += i + 1;
if (t[i + 1] === "#" || !(i === 0 || t[i - 1].match(/\s/))) {
t = t.slice(i + 1);
continue;
}
t = t.slice(i + 1);
const m = t.match(/\s|[^A-Za-z0-9_\-]/);
if (m && m.index != null) {
i = m.index;
} else {
i = -1;
}
if (i === 0) {
base += i + 1;
t = t.slice(i + 1);
} else {
if (i === -1) {
v.push([base - 1, base + t.length]);
return v;
} else {
v.push([base - 1, base + i]);
base += i + 1;
t = t.slice(i + 1);
}
}
}
}
export function path_is_in_public_paths(
path: string | undefined | null,
paths: string[] | Set<string> | object | undefined | null,
): boolean {
return containing_public_path(path, paths) != null;
}
export function containing_public_path(
path: string | undefined | null,
paths: string[] | Set<string> | object | undefined | null,
): undefined | string {
if (paths == null || path == null) {
return;
}
if (path.indexOf("../") !== -1) {
return;
}
if (is_array(paths) || is_set(paths)) {
for (const p of paths) {
if (p == null) continue;
if (p === "") {
return "";
}
if (path === p) {
return p;
}
if (path.slice(0, p.length + 1) === p + "/") {
return p;
}
}
} else if (is_object(paths)) {
for (const p in paths) {
if (p === "") {
return "";
}
if (path === p) {
return p;
}
if (path.slice(0, p.length + 1) === p + "/") {
return p;
}
}
} else {
throw Error("paths must be undefined, an array, or a map");
}
if (filename_extension(path) === "zip") {
return containing_public_path(path.slice(0, path.length - 4), paths);
}
return undefined;
}
export const is_equal = lodash.isEqual;
export function is_whitespace(s?: string): boolean {
return (s?.trim().length ?? 0) == 0;
}
export function lstrip(s: string): string {
return s.replace(/^\s*/g, "");
}
export function date_to_snapshot_format(
d: Date | undefined | null | number,
): string {
if (d == null) {
d = 0;
}
if (typeof d === "number") {
d = new Date(d);
}
let s = d.toJSON();
s = s.replace("T", "-").replace(/:/g, "");
const i = s.lastIndexOf(".");
return s.slice(0, i);
}
export function stripeDate(d: number): string {
return new Date(d * 1000).toLocaleDateString(undefined, {
year: "numeric",
month: "long",
day: "numeric",
});
}
export function to_money(n: number, d = 2): string {
return n.toFixed(d).replace(/(\d)(?=(\d{3})+\.)/g, "$1,");
}
export function commas(n: number): string {
if (n == null) {
return "";
}
return n.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",");
}
export function currency(n: number, d?: number) {
if (n == 0) {
return `$0.00`;
}
let s = `$${to_money(Math.abs(n) ?? 0, d ?? (Math.abs(n) < 0.0095 ? 3 : 2))}`;
if (n < 0) {
s = `-${s}`;
}
if (d == null || d <= 2) {
return s;
}
const i = s.indexOf(".");
while (s[s.length - 1] == "0" && i <= s.length - (d ?? 2)) {
s = s.slice(0, s.length - 1);
}
return s;
}
export function stripeAmount(
unitPrice: number,
currency: string,
units = 1,
): string {
if (currency !== "usd") {
return `${currency == "eur" ? "€" : ""}${to_money(
(units * unitPrice) / 100,
)} ${currency.toUpperCase()}`;
}
return `$${to_money((units * unitPrice) / 100)} USD`;
}
export function planInterval(
interval: string,
interval_count: number = 1,
): string {
return `${interval_count} ${plural(interval_count, interval)}`;
}
export function get_array_range(arr: any[], value1: any, value2: any): any[] {
let index1 = arr.indexOf(value1);
let index2 = arr.indexOf(value2);
if (index1 > index2) {
[index1, index2] = [index2, index1];
}
return arr.slice(index1, +index2 + 1 || undefined);
}
function seconds2hms_days(
d: number,
h: number,
m: number,
longform: boolean,
): string {
h = h % 24;
const s = h * 60 * 60 + m * 60;
const x = s > 0 ? seconds2hms(s, longform, false) : "";
if (longform) {
return `${d} ${plural(d, "day")} ${x}`.trim();
} else {
return `${d}d${x}`;
}
}
export function seconds2hm(secs: number, longform: boolean = false): string {
return seconds2hms(secs, longform, false);
}
export function seconds2hms(
secs: number,
longform: boolean = false,
show_seconds: boolean = true,
): string {
let s;
if (!longform && secs < 10) {
s = round2(secs % 60);
} else if (!longform && secs < 60) {
s = round1(secs % 60);
} else {
s = Math.round(secs % 60);
}
const m = Math.floor(secs / 60) % 60;
const h = Math.floor(secs / 60 / 60);
const d = Math.floor(secs / 60 / 60 / 24);
if (d > 0) {
return seconds2hms_days(d, h, m, longform);
}
if (h === 0 && m === 0 && show_seconds) {
if (longform) {
return `${s} ${plural(s, "second")}`;
} else {
return `${s}s`;
}
}
if (h > 0) {
if (longform) {
let ret = `${h} ${plural(h, "hour")}`;
if (m > 0) {
ret += ` ${m} ${plural(m, "minute")}`;
}
return ret;
} else {
if (show_seconds) {
return `${h}h${m}m${s}s`;
} else {
return `${h}h${m}m`;
}
}
}
if (m > 0 || !show_seconds) {
if (show_seconds) {
if (longform) {
let ret = `${m} ${plural(m, "minute")}`;
if (s > 0) {
ret += ` ${s} ${plural(s, "second")}`;
}
return ret;
} else {
return `${m}m${s}s`;
}
} else {
if (longform) {
return `${m} ${plural(m, "minute")}`;
} else {
return `${m}m`;
}
}
}
return "";
}
export function range(n: number): number[] {
const v: number[] = [];
for (let i = 0; i < n; i++) {
v.push(i);
}
return v;
}
export function enumerate(v: any[]) {
const w: [number, any][] = [];
let i = 0;
for (let x of Array.from(v)) {
w.push([i, x]);
i += 1;
}
return w;
}
export function to_human_list(arr: any[]): string {
arr = lodash.map(arr, (x) => `${x}`);
if (arr.length > 1) {
return arr.slice(0, -1).join(", ") + " and " + arr.slice(-1);
} else if (arr.length === 1) {
return arr[0].toString();
} else {
return "";
}
}
export function console_init_filename(path: string): string {
const x = path_split(path);
x.tail = `.${x.tail}.init`;
if (x.head === "") {
return x.tail;
}
return [x.head, x.tail].join("/");
}
export function has_null_leaf(obj: object): boolean {
for (const k in obj) {
const v = obj[k];
if (v === null || (typeof v === "object" && has_null_leaf(v))) {
return true;
}
}
return false;
}
export function peer_grading(
students: string[],
N: number = 2,
): { [student_id: string]: string[] } {
if (N <= 0) {
throw Error("Number of peer assigments must be at least 1");
}
if (students.length <= N) {
throw Error(`You need at least ${N + 1} students`);
}
const assignment: { [student_id: string]: string[] } = {};
for (const s of students) {
assignment[s] = [];
}
const s_random = lodash.shuffle(students);
const L = students.length;
for (let i = 0; i < L; i++) {
for (let j = i + 1; j <= i + N; j++) {
assignment[s_random[i]].push(s_random[j % L]);
}
}
for (let k in assignment) {
const v = assignment[k];
assignment[k] = lodash.sortBy(v, (s) => students.indexOf(s));
}
return assignment;
}
export function is_only_downloadable(s: string): boolean {
return s.indexOf("://") !== -1 || startswith(s, "[email protected]");
}
export function ensure_bound(x: number, min: number, max: number): number {
return x < min ? min : x > max ? max : x;
}
export const EDITOR_PREFIX = "editor-";
export function path_to_tab(name: string): string {
return `${EDITOR_PREFIX}${name}`;
}
export function tab_to_path(name?: string): string | undefined {
if (name?.substring(0, 7) === EDITOR_PREFIX) {
return name.substring(7);
}
return;
}
export function suggest_duplicate_filename(name: string): string {
let ext;
({ name, ext } = separate_file_extension(name));
const idx_dash = name.lastIndexOf("-");
const idx_under = name.lastIndexOf("_");
const idx = Math.max(idx_dash, idx_under);
let new_name: string | undefined = undefined;
if (idx > 0) {
const [prefix, ending] = Array.from([
name.slice(0, idx + 1),
name.slice(idx + 1),
]);
const paddedEnding = ending.padStart(ending.length, "0");
const num = parseInt(paddedEnding);
if (!Number.isNaN(num)) {
const newNum = (num + 1).toString().padStart(ending.length, "0");
new_name = `${prefix}${newNum}`;
}
}
if (new_name == null) {
new_name = `${name}-1`;
}
if (ext.length > 0) {
new_name += "." + ext;
}
return new_name;
}
export function top_sort(
DAG: { [node: string]: string[] },
opts: { omit_sources?: boolean } = { omit_sources: false },
): string[] {
const { omit_sources } = opts;
const source_names: string[] = [];
let num_edges = 0;
const graph_nodes = {};
for (const name in DAG) {
const parents = DAG[name];
if (graph_nodes[name] == null) {
graph_nodes[name] = {};
}
const node = graph_nodes[name];
node.name = name;
if (node.children == null) {
node.children = [];
}
node.parent_set = {};
for (const parent_name of parents) {
node.parent_set[parent_name] = true;
if (graph_nodes[parent_name] == null) {
graph_nodes[parent_name] = {};
if (DAG[parent_name] == null) {
source_names.push(parent_name);
}
}
if (graph_nodes[parent_name].children == null) {
graph_nodes[parent_name].children = [];
}
graph_nodes[parent_name].children.push(node);
}
if (parents.length === 0) {
source_names.push(name);
} else {
num_edges += parents.length;
}
}
const path: string[] = [];
const num_sources = source_names.length;
let walked_edges = 0;
while (source_names.length !== 0) {
const curr_name = source_names.shift();
if (curr_name == null) throw Error("BUG -- can't happen");
path.push(curr_name);
for (const child of graph_nodes[curr_name].children) {
delete child.parent_set[curr_name];
walked_edges++;
if (Object.keys(child.parent_set).length === 0) {
source_names.push(child.name);
}
}
}
if (num_sources === 0) {
throw new Error("No sources were detected");
}
if (num_edges !== walked_edges) {
throw new Error("Store has a cycle in its computed values");
}
if (omit_sources) {
return path.slice(num_sources);
} else {
return path;
}
}
export function create_dependency_graph(obj: {
[name: string]: Function & { dependency_names?: string[] };
}): { [name: string]: string[] } {
const DAG = {};
for (const name in obj) {
const written_func = obj[name];
DAG[name] = written_func.dependency_names ?? [];
}
return DAG;
}
export function obj_key_subs(obj: object, subs: { [key: string]: any }): void {
for (const k in obj) {
const v = obj[k];
const s: any = subs[k];
if (typeof s == "string") {
delete obj[k];
obj[s] = v;
}
if (typeof v === "object") {
obj_key_subs(v, subs);
} else if (typeof v === "string") {
const s2: any = subs[v];
if (s2 != null) {
obj[k] = s2;
}
}
}
}
export function sanitize_html_attributes($, node): void {
$.each(node.attributes, function () {
if (this == null) {
return;
}
const attrName = this.name;
const attrValue = this.value;
if (
attrName?.indexOf("on") === 0 ||
attrValue?.indexOf("javascript:") === 0
) {
$(node).removeAttr(attrName);
}
});
}
export const analytics_cookie_name = "CC_ANA";
export function jupyter_language_to_name(lang: string): string {
if (lang === "python") {
return "Python";
} else if (lang === "gap") {
return "GAP";
} else if (lang === "sage" || exports.startswith(lang, "sage-")) {
return "SageMath";
} else {
return capitalize(lang);
}
}
export function closest_kernel_match(
name: string,
kernel_list: immutable.List<immutable.Map<string, any>>,
): immutable.Map<string, any> {
name = name.toLowerCase().replace("matlab", "octave");
name = name === "python" ? "python3" : name;
let bestValue = -1;
let bestMatch: immutable.Map<string, any> | undefined = undefined;
for (let i = 0; i < kernel_list.size; i++) {
const k = kernel_list.get(i);
if (k == null) {
continue;
}
if ((k.getIn(["metadata", "cocalc", "priority"], 0) as number) < 0)
continue;
const kernel_name = k.get("name")?.toLowerCase();
if (!kernel_name) continue;
let v = 0;
for (let j = 0; j < name.length; j++) {
if (name[j] === kernel_name[j]) {
v++;
} else {
break;
}
}
if (
v > bestValue ||
(v === bestValue &&
bestMatch &&
compareVersionStrings(
k.get("name") ?? "",
bestMatch.get("name") ?? "",
) === 1)
) {
bestValue = v;
bestMatch = k;
}
}
if (bestMatch == null) {
return kernel_list.get(0) ?? immutable.Map<string, string>();
}
return bestMatch;
}
function compareVersionStrings(a: string, b: string): -1 | 0 | 1 {
const av: string[] = a.split(/(\d+)/);
const bv: string[] = b.split(/(\d+)/);
for (let i = 0; i < Math.max(av.length, bv.length); i++) {
const l = av[i] ?? "";
const r = bv[i] ?? "";
if (/\d/.test(l) && /\d/.test(r)) {
const vA = parseInt(l);
const vB = parseInt(r);
if (vA > vB) {
return 1;
}
if (vA < vB) {
return -1;
}
} else {
if (l > r) {
return 1;
}
if (l < r) {
return -1;
}
}
}
return 0;
}
export function count(str: string, strsearch: string): number {
let index = -1;
let count = -1;
while (true) {
index = str.indexOf(strsearch, index + 1);
count++;
if (index === -1) {
break;
}
}
return count;
}
export function rpad_html(num: number, width: number, round_fn?: Function) {
num = (round_fn ?? Math.round)(num);
const s = " ";
if (num == 0) return lodash.repeat(s, width - 1) + "0";
if (num < 0) return num;
const str = `${num}`;
const pad = Math.max(0, width - str.length);
return lodash.repeat(s, pad) + str;
}
export function removeNulls(obj) {
if (typeof obj != "object") {
return obj;
}
if (is_array(obj)) {
for (const x of obj) {
removeNulls(x);
}
return obj;
}
const obj2: any = {};
for (const field in obj) {
if (obj[field] != null) {
obj2[field] = removeNulls(obj[field]);
}
}
return obj2;
}
const academicCountry = new RegExp(/\.(ac|edu)\...$/);
export function isAcademic(s?: string): boolean {
if (!s) return false;
const domain = s.split("@")[1];
if (!domain) return false;
if (domain.endsWith(".edu")) return true;
if (academicCountry.test(domain)) return true;
return false;
}
export function test_valid_jsonpatch(patch: any): boolean {
if (!is_array(patch)) {
return false;
}
for (const op of patch) {
if (!is_object(op)) {
return false;
}
if (op["op"] == null) {
return false;
}
if (
!["add", "remove", "replace", "move", "copy", "test"].includes(op["op"])
) {
return false;
}
if (op["path"] == null) {
return false;
}
if (op["from"] != null && typeof op["from"] !== "string") {
return false;
}
}
return true;
}
export function rowBackground({
index,
checked,
}: {
index: number;
checked?: boolean;
}): string {
if (checked) {
if (index % 2 === 0) {
return "#a3d4ff";
} else {
return "#a3d4f0";
}
} else if (index % 2 === 0) {
return "#f4f4f4";
} else {
return "white";
}
}
export function firstLetterUppercase(str: string | undefined) {
if (str == null) return "";
return str.charAt(0).toUpperCase() + str.slice(1);
}
const randomColorCache = new LRU<string, string>({ max: 100 });
export function getRandomColor(
s: string,
opts?: { min?: number; max?: number; diff?: number; seed?: number },
): string {
const diff = opts?.diff ?? 0;
const min = clip(opts?.min ?? 120, 0, 254);
const max = Math.max(min, clip(opts?.max ?? 230, 1, 255));
const seed = opts?.seed ?? 0;
const key = `${s}-${min}-${max}-${diff}-${seed}`;
const cached = randomColorCache.get(key);
if (cached) {
return cached;
}
let iter = 0;
const iterLimit = "z".charCodeAt(0) - "A".charCodeAt(0);
const mod = max - min;
while (true) {
const val = `${seed}-${s}-${String.fromCharCode("A".charCodeAt(0) + iter)}`;
const hash = sha1(val)
.split("")
.reduce((a, b) => ((a << 6) - a + b.charCodeAt(0)) | 0, 0);
const r = (((hash >> 0) & 0xff) % mod) + min;
const g = (((hash >> 8) & 0xff) % mod) + min;
const b = (((hash >> 16) & 0xff) % mod) + min;
iter += 1;
if (iter <= iterLimit && diff) {
const diffVal = Math.abs(r - g) + Math.abs(g - b) + Math.abs(b - r);
if (diffVal < diff) continue;
}
const col = `rgb(${r}, ${g}, ${b})`;
randomColorCache.set(key, col);
return col;
}
}
export function hexColorToRGBA(col: string, opacity?: number): string {
const r = parseInt(col.slice(1, 3), 16);
const g = parseInt(col.slice(3, 5), 16);
const b = parseInt(col.slice(5, 7), 16);
if (opacity && opacity <= 1 && opacity >= 0) {
return `rgba(${r},${g},${b},${opacity})`;
} else {
return `rgb(${r},${g},${b})`;
}
}
export function strictMod(a: number, b: number): number {
return ((a % b) + b) % b;
}
export function clip(val: number, min: number, max: number): number {
return Math.min(Math.max(val, min), max);
}
export function smallIntegerToEnglishWord(val: number): string | number {
if (!Number.isInteger(val)) return val;
switch (val) {
case 0:
return "zero";
case 1:
return "one";
case 2:
return "two";
case 3:
return "three";
case 4:
return "four";
case 5:
return "five";
case 6:
return "six";
case 7:
return "seven";
case 8:
return "eight";
case 9:
return "nine";
case 10:
return "ten";
case 11:
return "eleven";
case 12:
return "twelve";
case 13:
return "thirteen";
case 14:
return "fourteen";
case 15:
return "fifteen";
case 16:
return "sixteen";
case 17:
return "seventeen";
case 18:
return "eighteen";
case 19:
return "nineteen";
case 20:
return "twenty";
}
return val;
}
export function numToOrdinal(val: number): string {
if (!Number.isInteger(val)) return `${val}th`;
const mod100 = val % 100;
if (mod100 >= 11 && mod100 <= 13) {
return `${val}th`;
}
const mod10 = val % 10;
switch (mod10) {
case 1:
return `${val}st`;
case 2:
return `${val}nd`;
case 3:
return `${val}rd`;
default:
return `${val}th`;
}
}
export function hoursToTimeIntervalHuman(num: number): string {
if (num < 24) {
const n = round1(num);
return `${n} ${plural(n, "hour")}`;
} else if (num < 24 * 7) {
const n = round1(num / 24);
return `${n} ${plural(n, "day")}`;
} else {
const n = round1(num / (24 * 7));
return `${n} ${plural(n, "week")}`;
}
}
export function tail(s: string, lines: number) {
if (lines < 1) return "";
let lineCount = 0;
let lastIndex = s.length - 1;
while (lastIndex >= 0 && lineCount < lines) {
lastIndex = s.lastIndexOf("\n", lastIndex);
if (lastIndex === -1) {
return s;
}
lineCount++;
lastIndex--;
}
return s.slice(lastIndex + 2);
}
export function basePathCookieName({
basePath,
name,
}: {
basePath: string;
name: string;
}): string {
return `${basePath.length <= 1 ? "" : encodeURIComponent(basePath)}${name}`;
}
export function isNumericString(str: string): boolean {
if (typeof str != "string") {
return false;
}
return (
!isNaN(str) &&
!isNaN(parseFloat(str))
);
}
export function uint8ArrayToBase64(uint8Array: Uint8Array) {
let binaryString = "";
for (let i = 0; i < uint8Array.length; i++) {
binaryString += String.fromCharCode(uint8Array[i]);
}
return btoa(binaryString);
}