Typescript helpers (tshelpers)

Date: 2025-03-31

Iterables / Arrays

type IterableKind<T> = Iterable<T> | T[];

export function ensureArray<T>(data?: IterableKind<T>): T[] {
    if (!data) return [];
    if (Array.isArray(data)) return data;
    if (typeof data === "string") return [data];
    if (data instanceof Map) return [];
    return Array.from(data);
}

export function firstItem<T>(data?: IterableKind<T>): T | undefined {
    return ensureArray(data)[0];
};

export function arraySum<T>(arr: IterableKind<T>, fn: (arg: T) => number): number {
    return ensureArray(arr).reduce((p, c) => p + fn(c), 0);
}

export function arrayMin<T>(arr: IterableKind<T>, fn: (arg: T) => number): number | undefined {
    const values = ensureArray(arr).map(fn);
    return values.length ? Math.min.apply(Math, values) : undefined;
}

export function arrayMax<T>(arr: IterableKind<T>, fn: (arg: T) => number): number | undefined {
    const values = ensureArray(arr).map(fn);
    return values.length ? Math.max.apply(Math, values) : undefined;
}
import { getISOWeek, getISOWeekYear } from "date-fns";
import { IYearWeek } from "../domain/general/IYearWeek";

export const isDebugMode = `${window.location.origin}`.includes("localhost");

const eq = (a: string, b: string) => a === b;

const strEq = (a?: string, b?: string) => String(a).trim().toLowerCase() === String(b).trim().toLowerCase();

const remove = (arr: any[], item: any) => {
    const i = arr.indexOf(item);
    if (i >= 0) { arr.splice(i, 1); }
};
const isset = (x: any) => ![undefined, null].includes(x);

const getString = (x: any) => isset(x) ? `${x}` : "";

const getFirstItem = (obj: any) => {
    if (isset(obj)) {
        return Array.from(obj)[0];
    }
    return null;
};


export type Dictionary<T> = { [id: string]: T };

const htmlEncode = (source: string) => {
    let i = 0;
    let ch: string;
    let peek = "";
    const result: string[] = [];
    let line: string[] = [];
    // Stash the next character and advance the pointer
    const next = () => {
        peek = source.charAt(i);
        i += 1;
    };
    // Start a new "line" of output, to be joined later by <br />
    const endline = () => {
        result.push(line.join(""));
        line = [];
    };
    // Push a character or its entity onto the current line
    const push = () => {
        if (ch !== "\r" && ch !== "\n" && (ch < " " || ch > "~")) {
            line.push(`&#${ch.charCodeAt(0)};`);
        } else {
            line.push(ch);
        }
    };
    next();
    while (i <= source.length) {
        ch = peek;
        next();
        switch (ch) {
            case "<":
                line.push("<");
                break;
            case ">":
                line.push(">");
                break;
            case "&":
                line.push("&");
                break;
            case "\"":
                line.push(""");
                break;
            case "'":
                line.push("'");
                break;
            default:
                push();
        }
    }
    endline();
    return result.join("<br />");
};

const unique = (arr: any[], valFn: (item: any) => any) => {
    const values: { [key: string]: any } = {};
    return arr.filter(item => {
        const val = String(valFn(item));
        return val in values ? false : values[val] = true;
    });
};

const propertiesEqual = (a: any, b: any, properties: string[]) => a && b && properties.every((p: any) => a[p] === b[p]);

export function arraysEqual(arr1: any[], arr2: any[]) {
    return arr1.length === arr2.length && arr1.every((val, index) => val === arr2[index]);
}
export function arraysEqualUnordered(arr1: any[], arr2: any[]) {
    return JSON.stringify([...arr1].sort()) === JSON.stringify([...arr2].sort());
}


const dateAdd = (date: Date, ms: number) => new Date(date.getTime() + ms);
const getToday = () => {
    const d = new Date();
    d.setHours(23, 59, 59);
    return d;
};

export interface IPageInfo { from?: Date; to?: Date; page: number; }
type Pager = (page: number) => IPageInfo;

const historyPager = (msRange: number): Pager => {
    const today = getToday();
    return (page: number) => {
        const from = dateAdd(today, -msRange * (page + 1));
        from.setHours(0, 0, 0);
        let to: Date | undefined = dateAdd(today, -msRange * page);
        if (page < 1) {
            to = undefined;
        }
        return {
            from,
            to,
            page
        };
    };
};

// only skips sunday
const nextWorkDay = function (date: Date): Date {
    const tomorrow = new Date(date.setDate(date.getDate() + 1));
    return tomorrow.getDay() % 7 ? tomorrow : nextWorkDay(tomorrow);
};

// only skips sunday
const lastWorkday = function (date: Date): Date {
    const yesterday = new Date(date.setDate(date.getDate() - 1));
    return yesterday.getDay() % 7 ? yesterday : lastWorkday(yesterday);
};

const isFileImage = (fileName: string) => {
    const extensions = [".JPG", ".JPEG", ".JPE", ".BMP", ".GIF", ".PNG"];
    return extensions.some(x => fileName.toUpperCase().includes(x));
};

const isValidDate = (d: any) => d instanceof Date && !isNaN(d.getTime());

const getNumber = (x: any) => Number(String(x).replace(/([^\d])/, "").replace(/^0+/, ""));

const isNumberKey = (event: any) => {
    const charCode = (event.which) ? event.which : event.keyCode;
    if (charCode !== 46 && charCode > 31 && (charCode < 48 || charCode > 57)) {
        return false;
    }
    return true;
};

const escapeHtml = (unsafe: any): string => {
    return String(unsafe)
        .replace(/&/g, "&")
        .replace(/</g, "<")
        .replace(/>/g, ">")
        .replace(/"/g, """)
        .replace(/'/g, "'");
};

const onChange = (objToWatch: any, fn: any) => {
    const handler = {
        set(target: any, property: any, value: any) {
            fn();
            return Reflect.set(target, property, value);
        },
        deleteProperty(target: any, property: any) {
            fn();
            return Reflect.deleteProperty(target, property);
        }
    }; return new Proxy(objToWatch, handler);
};

function debounced(delay: number, fn: (...args: Array<any>) => any) {
    let timerId: any;
    return function (...args: Array<any>) {
        if (timerId) {
            clearTimeout(timerId);
        }
        timerId = setTimeout(() => {
            fn(...args);
            timerId = null;
        }, delay);
    };
}

const displayTextAreaProperly = (str: string): string => {
    if (str) {
        return str.replace(/\n/g, "<br>");
    }
    return "";
};

const createCounter = () => {
    let c = 0;
    return () => { c += 1; return c; };
};

const getCircularReplacer = () => {
    const seen = new WeakSet();
    return (_key: any, value: any) => {
        if (typeof value === "object" && value !== null) {
            if (seen.has(value)) {
                return;
            }
            seen.add(value);
        }
        return value;
    };
};
const toJson = (x: any) => JSON.stringify(x, getCircularReplacer());
const parseJson = (x: any) => {
    if (typeof x !== "string") { return undefined; }
    try {
        return JSON.parse(x);
    } catch {
        return undefined;
    }
};
const getLocalStorage = <T>(key: string): T => parseJson(localStorage.getItem(key)) as T;
const setLocalStorage = (key: string, state: any): any => localStorage.setItem(key, toJson(state));

export const getSessionStorage = <T>(key: string): T => parseJson(sessionStorage.getItem(key)) as T;
export const setSessionStorage = (key: string, state: any): any => sessionStorage.setItem(key, toJson(state));

const c = encodeURIComponent;
const cv = (v: any) => v && v.toISOString ? v.toISOString() : v;
const objToQueryString = (obj: any) => Object.keys(obj).map(k => `${c(k)}=${c(cv(obj[k]))}`).join("&");

function b64toBlob(b64Data: string, contentType: string, sliceSize?: number): Blob {
    contentType = contentType || "";
    sliceSize = sliceSize || 512;
    const byteCharacters = atob(b64Data);
    const byteArrays = [];

    for (let offset = 0; offset < byteCharacters.length; offset += sliceSize) {
        const slice = byteCharacters.slice(offset, offset + sliceSize);
        const byteNumbers = new Array(slice.length);
        for (let i = 0; i < slice.length; i++) {
            byteNumbers[i] = slice.charCodeAt(i);
        }
        const byteArray = new Uint8Array(byteNumbers);
        byteArrays.push(byteArray);
    }
    return new Blob(byteArrays, { type: contentType });
}

function imagedataToBlob(imagedata: ImageData): Promise<Blob> {
    return new Promise((resolve, reject) => {
        try {
            const canvas = document.createElement("canvas");
            const ctx = canvas.getContext("2d");
            if (!ctx) { reject(); return; }
            canvas.width = imagedata.width;
            canvas.height = imagedata.height;
            ctx.putImageData(imagedata, 0, 0);
            canvas.toBlob((blob) => {
                if (!blob) { reject(); return; }
                resolve(blob);
            }, "image/jpeg", 0.95);
        } catch (error) {
            reject(error);
        }
    });
}

const arrRemove = (arr: Array<any>, item: any) => {
    const i = arr.indexOf(item);
    if (i >= 0) { arr.splice(i, 1); }
};

const filterUnique = () => {
    const keys = new Set();
    return (key: string) => !keys.has(key) ? keys.add(key) || true : false;
};


const isSet = (x: any) => x != null; // works for [null, undefined], [ false, 0, Nan ] are true
const isNumber = (x: any) => typeof x === "number";
const isString = (x: any) => typeof x === "string";
const isIterable = (value: any) => Symbol.iterator in Object(value);

function* iterator(iterable: Array<any>) {
    for (const item of iterable) { yield item; }
}
function* zipIterables(...iterables: Array<any>) {
    const iterators = iterables.map(x => iterator(x));

    while (true) {
        const stats = [];
        const results = [];
        for (const iterable of iterators) {
            const result = iterable.next();
            stats.push(result.done);
            results.push(result.value);
        }
        if (stats.every((stat) => stat === true)) {
            break;
        }
        yield results;
    }
}

const stringComparer = (locales?: any, options?: any) => {
    if (!locales) { locales = "en"; }
    if (!options) { options = { numeric: true, sensitivity: "base" }; }
    const collator = new Intl.Collator(locales, options);
    return (a: string, b: string) => collator.compare(a, b);
};
const comparer = stringComparer();
function compare(a: any, b: any, reverse: boolean): number {
    const [x, y] = reverse ? [b, a] : [a, b];
    if (!isSet(x) && !isSet(y)) { return 0; }
    if (!isSet(x) && isSet(y)) { return -1; }
    if (!isSet(y) && isSet(x)) { return 1; }
    if (isNumber(x) && isNumber(y)) { return x - y; }
    if (isString(x) && isString(y)) { return comparer(x, y); }
    if (isIterable(x) && isIterable(y)) {
        for (const d of Array.from(zipIterables(x, y))) {
            const c = compare(d[0], d[1], reverse);
            if (c !== 0) { return c; }
        }
        return 0;
    }
    return comparer(String(x), String(y));
}
export function orderBy<T>(array: T[], fn: (x: T) => any, reverse?: boolean) {
    const result = Array.from(array);
    result.sort((a, b) => compare(fn(a), fn(b), reverse || false));
    return result;
}

export function getDate(input: any): Date | undefined {
    if (typeof input === "object" && input instanceof Date) { return new Date(input); }
    if (typeof input === "string") { return new Date(input); }
    return undefined;
}

export function numberFormat(value: number) {
    if (typeof (value) !== "number" || Number.isNaN(value) || !Number.isFinite(value)) return "";
    const currencyFormatter = new Intl.NumberFormat("nl-NL");
    return currencyFormatter.format(value);
}

export function numberFormatRounded(value?: number, decimals: number = 0, unit: string = "") {
    if (typeof (value) !== "number" || Number.isNaN(value) || !Number.isFinite(value)) return "";
    const currencyFormatter = new Intl.NumberFormat("nl-NL");
    return currencyFormatter.format(round(value, decimals) ?? 0) + (unit ? ` ${unit}` : "");
}

export function numberFormatRoundedWithoutSeperator(value?: number, decimals: number = 0, unit: string = "") {
    if (typeof (value) !== "number" || Number.isNaN(value) || !Number.isFinite(value)) return "";
    // Use fixed-point notation to avoid thousand separators
    const roundedValue = (value ?? 0).toFixed(decimals);
    return roundedValue + (unit ? ` ${unit}` : "");
}

export const normalizeKeysToLowercase = (obj: any) => {
    return Object.keys(obj).reduce((acc, key) => {
        const lowercaseKey = key.charAt(0).toLowerCase() + key.slice(1);
        acc[lowercaseKey] = obj[key];
        return acc;
    }, {} as any);
};

export function currencyFormat(value: number | undefined, currencyISO: string = "EUR"): string {
    if (value === null || value === undefined) return "";
    if (typeof (value) !== "number" || Number.isNaN(value) || !Number.isFinite(value)) return "";
    const currencyFormatter = new Intl.NumberFormat("nl-NL", { style: "currency", currency: currencyISO, minimumFractionDigits: 2, maximumFractionDigits: 6 });
    return currencyFormatter.format(value);
}

export function round(value: number | undefined, precision: number): number | undefined {
    if (value === null || value === undefined) return undefined;
    value = Number(value) || 0.0;
    const multiplier = Math.pow(10, precision || 0);
    return Math.round(value * multiplier) / multiplier;
}

export function getNumberFromString(value: any): number | undefined {
    if (value === null || value === undefined || value === "") return undefined;
    const s = String(value).replace(/[^\d.,]/, "");
    const parts = s.split(/[.,]/);
    if (parts.length > 1)
        parts.splice(parts.length - 1, 0, "."); // insert at index
    const j = parts.join("");
    return parseFloat(j) || 0.0;
}

interface IGroup<U> {
    group: string;
    data: U;
}
export function groupBy<T, U>(array: T[], fn: (item: T) => IGroup<U>) {
    const groups: { [key: string]: U[] } = {};
    array.forEach((item) => {
        const result = fn(item);
        const group = JSON.stringify(result.group);
        groups[group] = groups[group] || [];
        groups[group].push(result.data);
    });
    return Object.keys(groups).map((group) => groups[group]);
};

export const getDateXDaysAgo = (numOfDays: number, date = new Date()) => {
    const daysAgo = new Date(date.getTime());
    daysAgo.setDate(date.getDate() - numOfDays);
    return daysAgo;
}

const nameof = <T>(name: Extract<keyof T, string>): string => name;

export function propertyOf<T>(name: keyof T) { return name; }

export function pushRange(arr: any[], itemsToPush: any[]) {
    Array.prototype.push.apply(arr, itemsToPush);
}

export interface IHaveParentId {
    id: any;
    parentId?: any;
}

interface IHasChilds {
    children: any[];
}

export function createDataTree<T extends IHasChilds>(dataset: IHaveParentId[]) {
    const hashTable = new Map<any, any>();
    dataset.forEach(aData => hashTable.set(aData.id, { ...aData }));
    const dataTree: T[] = [];
    dataset.forEach(aData => {
        if (aData.parentId) {
            const parent = hashTable.get(aData.parentId);
            parent.children ??= [];
            parent.children.push(hashTable.get(aData.id));
        }
        else dataTree.push(hashTable.get(aData.id))
    });

    for (const v of hashTable.values()) {
        if (v.children && v.children.length < 1)
            delete v.children;
    }
    return dataTree;
};


const toBase64 = (file: Blob) => new Promise((resolve, reject) => {
    const reader = new FileReader();
    reader.readAsDataURL(file);
    reader.onload = () => resolve(reader.result);
    reader.onerror = error => reject(error);
});

function* rangeGen(from: number, to: number) {
    let i = from;
    while (i <= to) {
        yield i;
        i++;
    }
}

export function range(from: number, to: number) {
    return Array.from(rangeGen(from, to));
}

export function sumArray<T>(arr: T[], fn: (arg: T) => number): number {
    return arr.reduce((p, c) => p + fn(c), 0);
}

export function getCurrentWeek() {
    return { year: getISOWeekYear(new Date()), week: getISOWeek(new Date()) };
}
export function getWeeksBack(weekCount = 4) {
    const now = new Date();
    now.setDate(now.getDate() - 7 * weekCount);
    return { year: getISOWeekYear(now), week: getISOWeek(now) };
}

export function SetCurrentWeekStyle(yw: IYearWeek): React.CSSProperties {
    const currentWeek = getCurrentWeek();
    return getCurrentWeekStyle(weeksEqual(yw, currentWeek));
}

export function getCurrentWeekStyle(isCurrentWeek: boolean): React.CSSProperties {
    if (!isCurrentWeek) return {};
    return {
        fontWeight: "bold",
        color: "#5D3FD3"
    };
}

export function isHttpSucces(status?: number) {
    return !!status && status >= 200 && status < 400;
}

export function bytesToKBMB(bytes?: number) {
    if (!bytes) return;
    if (bytes < 1024) {
        return bytes + " bytes";
    } else if (bytes < 1024 * 1024) {
        return (bytes / 1024).toFixed(2) + " KB";
    } else {
        return (bytes / (1024 * 1024)).toFixed(2) + " MB";
    }
}

export function GetYearWeekFromKey(yearWeekString: string) {
    const parts = yearWeekString.split("-");
    const year = parseInt(parts[0], 10);
    const week = parseInt(parts[1], 10);

    const yearWeek = { key: yearWeekString, year: year, week: week } as IYearWeek
    return yearWeek;
}

export function truncateString(text: string, maxLength: number): string {
    text = String(text);
    if (text.length <= maxLength) {
        return text;
    }
    return text.slice(0, maxLength - 3) + "...";
}
export function limitArraySize<T>(array: T[], maxSize: number): T[] {
    return array.slice(0, maxSize);
}


export function IsNullOrEmpty(text: string) {
    if (text === undefined || text === null || text === "")
        return true;
    return false;
}
export const weeksEqual = (a: IYearWeek | undefined, b: IYearWeek | undefined) => getWeekKey(a) === getWeekKey(b);
export const getWeekKey = (yw: IYearWeek | undefined) => {
    return `${yw?.year}-` + `${yw?.week}`.padStart(2, "0");
}
export const getWeekKeyDefault = (yw: IYearWeek | undefined) => {
    if (!yw) return undefined;
    return `${yw?.year}-` + `${yw?.week}`.padStart(2, "0");
}

export function isSubset(array1: string[], array2: string[]): boolean {
    const set2 = new Set(array2);
    return array1.every(element => set2.has(element));
}


export const weekAsInt = (yw: IYearWeek) => (yw.year * 100) + yw.week;

export const joinClasses = (...args: any[]) => Array.from(args).filter(x => !!x).join(" ");

export function clearTextSelection() {
    const w = window as any;
    const d = document as any;
    if (w.getSelection) {
        if (w.getSelection().empty) {  // Chrome
            w.getSelection().empty();
        } else if (w.getSelection().removeAllRanges) {  // Firefox
            w.getSelection().removeAllRanges();
        }
    } else if (d.selection) {  // IE?
        d.selection.empty();
    }
}

export function delay(ms: number) {
    return new Promise((r) => setTimeout(r, ms));
}

export function deserializeState<T>(state: string | null, defValue: T): T {
    if (!state) return defValue;
    try {
        return JSON.parse(atob(state));
    } catch (error) {
        return defValue;
    }
}

export function serializeState<T>(state: T): string {
    return btoa(JSON.stringify(state));
}


// Debounce function to limit the rate at which a function can fire.
export function debounce<T extends (...args: any[]) => void>(func: T, wait: number): (...args: Parameters<T>) => void {
    let timeout: ReturnType<typeof setTimeout> | null;
    return function (...args: Parameters<T>) {
        const later = () => {
            if (timeout !== null) {
                clearTimeout(timeout);
            }
            func(...args);
        };
        if (timeout !== null) {
            clearTimeout(timeout);
        }
        timeout = setTimeout(later, wait);
    };
}

export function ensureArray(b: any): any[] {
    if (Array.isArray(b))
        return b;
    if (!b) return [];
    return [b];
}

export function ensureTypedArray<T>(b?: T[]): T[] {
    if (Array.isArray(b))
        return b;
    if (!b) return [];
    return [b];
}



export { arrRemove, b64toBlob, createCounter, dateAdd, debounced, displayTextAreaProperly, eq, escapeHtml, filterUnique, getFirstItem, getLocalStorage, getNumber, getString, getToday, historyPager, htmlEncode, imagedataToBlob, isFileImage, isNumberKey, isset, isValidDate, lastWorkday, nameof, nextWorkDay, objToQueryString, onChange, propertiesEqual, remove, setLocalStorage, strEq, toBase64, unique };


import moment from "moment";

const minimumValue = (value: number, min: number) => value < min ? min : value;

const isLast = (arr: Array<any>, i: number) => arr.length - 1 === i;

const nextItemInArray = (arr: Array<any>, i: number) => arr[minimumValue(i + 1, arr.length - 1)];

const unique = (arr: Array<any>) => Array.from(new Set([...arr]));

export function isValidNumber(x: any) {
    return typeof (x) === "number" && Number.isFinite(x);
}

export const filterUnique = () => {
    const keys = new Set();
    return (key: string) => !keys.has(key) ? keys.add(key) || true : false;
};

const getFileExtension = (str: string) => {
    const splitted = str.split(".");
    return splitted[splitted.length - 1].toLowerCase();
};

const toBase64 = (file: Blob) => new Promise((resolve, reject) => {
    const reader = new FileReader();
    reader.readAsDataURL(file);
    reader.onload = () => resolve(reader.result);
    reader.onerror = error => reject(error);
});

const b64toBlob = (base64: string) => fetch(base64).then(res => res.blob());

const asyncForEach = async (array: Array<any>, callback: Function) => {
    for (let index = 0; index < array.length; index++) {
        await callback(array[index], index, array);
    }
};

function escapeHtml(unsafe: any): string {
    return String(unsafe)
        .replace(/&/g, "&")
        .replace(/</g, "<")
        .replace(/>/g, ">")
        .replace(/"/g, """)
        .replace(/'/g, "'");
}

const any = (arr: Array<any>) => arr?.length > 0;

const stringComparer = (locales?: any, options?: any) => {
    if (!locales) locales = "en";
    if (!options) options = { numeric: true, sensitivity: "base" };
    const collator = new Intl.Collator(locales, options);
    return (a: string, b: string) => collator.compare(a, b);
};

const stringCompare = stringComparer();

export interface IFilterItem {
    text: string;
    value: any;
}

function getFilter<T>(values: T[keyof T][], fn: (x: T[keyof T]) => IFilterItem) {
    return Array.from(new Set(values)).map(fn);
}

function escapeRegExp(s: string) {
    return String(s).replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); // $& means the whole matched string
}

function getPropertyValue<T, K extends keyof T>(o: T, propertyName: K): T[K] {
    return o[propertyName];// o[propertyName] is of type T[K]
};

const createQuery = (data: any) => {
    const c = encodeURIComponent;
    const cv = (v: any) => v && v.toISOString ? v.toISOString() : v;
    return "?" + Object.keys(data).map(k => `${c(k)}=${c(cv(data[k]))}`).join("&");
};

const stringIsNullOrEmpty = (str: any) => {
    if (typeof str !== "string") return false;
    if (str.trim().length < 1) return false;
    return true;
};

const getCircularReplacer = () => {
    const seen = new WeakSet();
    return (key: any, value: any) => {
        if (typeof value === "object" && value !== null) {
            if (seen.has(value)) {
                return;
            }
            seen.add(value);
        }
        return value;
    };
};

const createGuid = (): string => {
    let d = new Date().getTime();
    const uuid = "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, function (c) {
        const r = (d + Math.random() * 16) % 16 | 0;
        d = Math.floor(d / 16);
        // eslint-disable-next-line no-mixed-operators
        return (c === "x" ? r : (r & 0x3 | 0x8)).toString(16);
    });
    return uuid;
};

const toJson = (x: any) => JSON.stringify(x, getCircularReplacer());

const parseJson = (x: any) => {
    if (typeof x !== "string") return undefined;
    try {
        return JSON.parse(x);
    } catch {
        return undefined;
    }
};

const getLocalStorage = <T>(key: string): T => parseJson(localStorage.getItem(key)) as T;
const setLocalStorage = (key: string, state: any): any => localStorage.setItem(key, toJson(state));

const getLocationStateKey = (path: string) => ("state-" + String(path)).toLowerCase();
const getLocationState = <T>(path: string): T => parseJson(sessionStorage.getItem(getLocationStateKey(path))) as T;
const setLocationState = (path: string, state: any): any => sessionStorage.setItem(getLocationStateKey(path), toJson(state));


export const toSortableDate = (x?: Date) => `${x?.toISOString()}`;
const formatDate = (value: Date) => value ? moment(value).format("DD-MM-yyyy") : "";
const formatDateTime = (value: Date) => value ? moment(value).format("DD-MM-yyyy HH:mm") : "";

export function classes(...args: string[]) { return Array.from(args).filter(x => x).join(" "); }

export const limit = (v: number, min: number, max: number): number => Math.max(min, Math.min(max, Number(v) || 0));

interface IGroup<U> {
    group: string;
    data: U;
}
export function groupBy<T, U>(array: T[], fn: (item: T) => IGroup<U>) {
    const groups: { [key: string]: U[] } = {};
    array.forEach((item) => {
        const result = fn(item);
        const group = JSON.stringify(result.group);
        groups[group] = groups[group] || [];
        groups[group].push(result.data);
    });
    return Object.keys(groups).map((group) => groups[group]);
};

export function round2Dec(num: number) {
    return Math.round(num * 100) / 100;
}

function addPostfixIfNumber(input: string, postFix: string) {
    if (!isNumeric(input)) {
        return input;
    }
    return `${input}${postFix}`;
}

function isNumeric(str: any) {
    return !isNaN(str) && !isNaN(parseFloat(str));
}

export { addPostfixIfNumber, any, asyncForEach, b64toBlob, createGuid, createQuery, escapeHtml, escapeRegExp, formatDate, formatDateTime, getFileExtension, getFilter, getLocalStorage, getLocationState, getPropertyValue, isLast, minimumValue, nextItemInArray, parseJson, setLocalStorage, setLocationState, stringCompare, stringComparer, stringIsNullOrEmpty, toBase64, toJson, unique };
// --
/* eslint-disable @typescript-eslint/no-explicit-any */
// import { getISOWeek, getISOWeekYear } from "date-fns";
// import { IYearWeek } from "../domain/general/IYearWeek";


const eq = (a: string, b: string) => a === b;

const strEq = (a?: string, b?: string) => String(a).trim().toLowerCase() === String(b).trim().toLowerCase();

const remove = (arr: any[], item: any) => {
    const i = arr.indexOf(item);
    if (i >= 0) { arr.splice(i, 1); }
};
const isset = (x: any) => ![undefined, null].includes(x);

const getString = (x: any) => isset(x) ? `${x}` : "";

const getFirstItem = (obj: any) => {
    if (isset(obj)) {
        return Array.from(obj)[0];
    }
    return null;
};

export const roundNumberUpBy = (n: number, roundBy: number) => Math.ceil(n * 1.0 / roundBy) * roundBy; // Only add up
export const roundNumberBy = (n: number, roundBy: number) => Math.round(n * 1.0 / roundBy) * roundBy;
export const roundNumber = (n: number) => roundNumberBy(n, 1);

export function isDigitsOnly(str: string): boolean {
    return /^\d+$/.test(str);
}

export function getArray<T>(data?: T[]) {
    return Array.from(data || []);
}

export type Dictionary<T> = { [id: string]: T };

const htmlEncode = (source: string) => {
    let i = 0;
    let ch: string;
    let peek = "";
    const result: string[] = [];
    let line: string[] = [];
    // Stash the next character and advance the pointer
    const next = () => {
        peek = source.charAt(i);
        i += 1;
    };
    // Start a new "line" of output, to be joined later by <br />
    const endline = () => {
        result.push(line.join(""));
        line = [];
    };
    // Push a character or its entity onto the current line
    const push = () => {
        if (ch !== "\r" && ch !== "\n" && (ch < " " || ch > "~")) {
            line.push(`&#${ch.charCodeAt(0)};`);
        } else {
            line.push(ch);
        }
    };
    next();
    while (i <= source.length) {
        ch = peek;
        next();
        switch (ch) {
            case "<":
                line.push("<");
                break;
            case ">":
                line.push(">");
                break;
            case "&":
                line.push("&");
                break;
            case "\"":
                line.push(""");
                break;
            case "'":
                line.push("'");
                break;
            default:
                push();
        }
    }
    endline();
    return result.join("<br />");
};

const unique = (arr: any[], valFn: (item: any) => any) => {
    const values: { [key: string]: any } = {};
    return arr.filter(item => {
        const val = String(valFn(item));
        return val in values ? false : values[val] = true;
    });
};

const propertiesEqual = (a: any, b: any, properties: string[]) => a && b && properties.every((p: any) => a[p] === b[p]);

const dateAdd = (date: Date, ms: number) => new Date(date.getTime() + ms);
const getToday = () => {
    const d = new Date();
    d.setHours(23, 59, 59);
    return d;
};

export interface IPageInfo { from?: Date; to?: Date; page: number; }
type Pager = (page: number) => IPageInfo;

const historyPager = (msRange: number): Pager => {
    const today = getToday();
    return (page: number) => {
        const from = dateAdd(today, -msRange * (page + 1));
        from.setHours(0, 0, 0);
        let to: Date | undefined = dateAdd(today, -msRange * page);
        if (page < 1) {
            to = undefined;
        }
        return {
            from,
            to,
            page
        };
    };
};

// only skips sunday
const nextWorkDay = function (date: Date): Date {
    const tomorrow = new Date(date.setDate(date.getDate() + 1));
    return tomorrow.getDay() % 7 ? tomorrow : nextWorkDay(tomorrow);
};

// only skips sunday
const lastWorkday = function (date: Date): Date {
    const yesterday = new Date(date.setDate(date.getDate() - 1));
    return yesterday.getDay() % 7 ? yesterday : lastWorkday(yesterday);
};

const isFileImage = (fileName: string) => {
    const extensions = [".JPG", ".JPEG", ".JPE", ".BMP", ".GIF", ".PNG"];
    return extensions.some(x => fileName.toUpperCase().includes(x));
};

const isValidDate = (d: any) => d instanceof Date && !isNaN(d.getTime());

function getNumber(x: any) {
    const value = Number(String(x).replace(/([^\d])/, "").replace(/^0+/, ""));
    if (Number.isFinite(value)) return value;
    return undefined;
}

const isNumberKey = (event: any) => {
    const charCode = (event.which) ? event.which : event.keyCode;
    if (charCode !== 46 && charCode > 31 && (charCode < 48 || charCode > 57)) {
        return false;
    }
    return true;
};

const escapeHtml = (unsafe: any): string => {
    return String(unsafe)
        .replace(/&/g, "&")
        .replace(/</g, "<")
        .replace(/>/g, ">")
        .replace(/"/g, """)
        .replace(/'/g, "'");
};

const onChange = (objToWatch: any, fn: any) => {
    const handler = {
        set(target: any, property: any, value: any) {
            fn();
            return Reflect.set(target, property, value);
        },
        deleteProperty(target: any, property: any) {
            fn();
            return Reflect.deleteProperty(target, property);
        }
    }; return new Proxy(objToWatch, handler);
};

function debounced(delay: number, fn: (...args: Array<any>) => any) {
    let timerId: any;
    return function (...args: Array<any>) {
        if (timerId) {
            clearTimeout(timerId);
        }
        timerId = setTimeout(() => {
            fn(...args);
            timerId = null;
        }, delay);
    };
}

const displayTextAreaProperly = (str: string): string => {
    if (str) {
        return str.replace(/\n/g, "<br>");
    }
    return "";
};

const createCounter = () => {
    let c = 0;
    return () => { c += 1; return c; };
};

const getCircularReplacer = () => {
    const seen = new WeakSet();
    return (_key: any, value: any) => {
        if (typeof value === "object" && value !== null) {
            if (seen.has(value)) {
                return;
            }
            seen.add(value);
        }
        return value;
    };
};

export function getDateString(date: Date) {
    const day = String(date.getDate()).padStart(2, '0');
    const month = String(date.getMonth() + 1).padStart(2, '0');
    const year = date.getFullYear();
    return `${day}-${month}-${year}`;
}

export function jsonCopy<T>(x: T): T { return parseJson(toJson(x)) }
const toJson = (x: any) => JSON.stringify(x, getCircularReplacer());
const parseJson = (x: any) => {
    if (typeof x !== "string") { return undefined; }
    try {
        return JSON.parse(x);
    } catch {
        return undefined;
    }
};
export const getLocalStorage = <T>(key: string): T => parseJson(localStorage.getItem(key)) as T;
export const setLocalStorage = (key: string, state: any): any => localStorage.setItem(key, toJson(state));
export const removeLocalStorage = (key: string): any => localStorage.removeItem(key);

export const getSessionStorage = <T>(key: string): T => parseJson(sessionStorage.getItem(key)) as T;
export const setSessionStorage = (key: string, state: any): any => sessionStorage.setItem(key, toJson(state));

const c = encodeURIComponent;
const cv = (v: any) => v && v.toISOString ? v.toISOString() : v;
const objToQueryString = (obj: any) => Object.keys(obj).map(k => `${c(k)}=${c(cv(obj[k]))}`).join("&");

function b64toBlob(b64Data: string, contentType: string, sliceSize?: number): Blob {
    contentType = contentType || "";
    sliceSize = sliceSize || 512;
    const byteCharacters = atob(b64Data);
    const byteArrays = [];

    for (let offset = 0; offset < byteCharacters.length; offset += sliceSize) {
        const slice = byteCharacters.slice(offset, offset + sliceSize);
        const byteNumbers = new Array(slice.length);
        for (let i = 0; i < slice.length; i++) {
            byteNumbers[i] = slice.charCodeAt(i);
        }
        const byteArray = new Uint8Array(byteNumbers);
        byteArrays.push(byteArray);
    }
    return new Blob(byteArrays, { type: contentType });
}

function imagedataToBlob(imagedata: ImageData): Promise<Blob> {
    return new Promise((resolve, reject) => {
        try {
            const canvas = document.createElement("canvas");
            const ctx = canvas.getContext("2d");
            if (!ctx) { reject(); return; }
            canvas.width = imagedata.width;
            canvas.height = imagedata.height;
            ctx.putImageData(imagedata, 0, 0);
            canvas.toBlob((blob) => {
                if (!blob) { reject(); return; }
                resolve(blob);
            }, "image/jpeg", 0.95);
        } catch (error) {
            reject(error);
        }
    });
}

const arrRemove = (arr: Array<any>, item: any) => {
    const i = arr.indexOf(item);
    if (i >= 0) { arr.splice(i, 1); }
};

const filterUnique = () => {
    const keys = new Set();
    return (key: string) => !keys.has(key) ? keys.add(key) || true : false;
};


const isSet = (x: any) => x != null; // works for [null, undefined], [ false, 0, Nan ] are true
export const isNumber = (x: any) => typeof x === "number" && !Number.isNaN(x) && Number.isFinite(x);
const isString = (x: any) => typeof x === "string";
const isIterable = (value: any) => Symbol.iterator in Object(value);

function* iterator(iterable: Array<any>) {
    for (const item of iterable) { yield item; }
}
function* zipIterables(...iterables: Array<any>) {
    const iterators = iterables.map(x => iterator(x));

    while (true) {
        const stats = [];
        const results = [];
        for (const iterable of iterators) {
            const result = iterable.next();
            stats.push(result.done);
            results.push(result.value);
        }
        if (stats.every((stat) => stat === true)) {
            break;
        }
        yield results;
    }
}

const stringComparer = (locales?: any, options?: any) => {
    if (!locales) { locales = "en"; }
    if (!options) { options = { numeric: true, sensitivity: "base" }; }
    const collator = new Intl.Collator(locales, options);
    return (a: string, b: string) => collator.compare(a, b);
};
const comparer = stringComparer();
function compare(a: any, b: any, reverse: boolean): number {
    const [x, y] = reverse ? [b, a] : [a, b];
    if (!isSet(x) && !isSet(y)) { return 0; }
    if (!isSet(x) && isSet(y)) { return -1; }
    if (!isSet(y) && isSet(x)) { return 1; }
    if (isNumber(x) && isNumber(y)) { return x - y; }
    if (isString(x) && isString(y)) { return comparer(x, y); }
    if (isIterable(x) && isIterable(y)) {
        for (const d of Array.from(zipIterables(x, y))) {
            const c = compare(d[0], d[1], reverse);
            if (c !== 0) { return c; }
        }
        return 0;
    }
    return comparer(String(x), String(y));
}
export function orderBy<T>(array: T[], fn: (x: T) => any, reverse?: boolean) {
    const result = Array.from(array);
    result.sort((a, b) => compare(fn(a), fn(b), reverse || false));
    return result;
}

export function getDate(input: any): Date | undefined {
    if (typeof input === "object" && input instanceof Date) { return new Date(input); }
    if (typeof input === "string") { return new Date(input); }
    return undefined;
}

export function numberFormat(value?: number, unit: string = "", showZero = false) {
    if (typeof (value) !== "number" || Number.isNaN(value) || !Number.isFinite(value)) return "";
    if (!showZero && value === 0) return "";
    const currencyFormatter = new Intl.NumberFormat("nl-NL");
    return currencyFormatter.format(value) + (unit ? ` ${unit}` : "");
}

export function numberFormatRounded(value?: number, decimals: number = 0, unit: string = "", showZero = false) {
    if (typeof (value) !== "number" || Number.isNaN(value) || !Number.isFinite(value)) return "";
    if (!showZero && value === 0) return "";
    const roundedValue = round(value || 0, decimals) ?? 0;
    const isInteger = roundedValue % 1 === 0;
    const currencyFormatter = new Intl.NumberFormat("nl-NL", {
        minimumFractionDigits: isInteger ? 0 : decimals,
        maximumFractionDigits: decimals
    });
    return currencyFormatter.format(roundedValue) + (unit ? ` ${unit}` : "");
}



export function currencyFormat(value: number | undefined, currencyISO: string = "EUR"): string {
    if (value === null || value === undefined) return "";
    if (typeof (value) !== "number" || Number.isNaN(value) || !Number.isFinite(value)) return "";
    const currencyFormatter = new Intl.NumberFormat("nl-NL", { style: "currency", currency: currencyISO, minimumFractionDigits: 2, maximumFractionDigits: 6 });
    return currencyFormatter.format(value);
}

export function round(value: number | undefined, precision: number): number | undefined {
    if (value === null || value === undefined) return undefined;
    value = Number(value) || 0.0;
    const multiplier = Math.pow(10, precision || 0);
    return Math.round(value * multiplier) / multiplier;
}

export function getNumberFromString(value: any): number | undefined {
    if (value === null || value === undefined || value === "") return undefined;
    const s = String(value).replace(/[^\d.,]/, "");
    const parts = s.split(/[.,]/);
    if (parts.length > 1)
        parts.splice(parts.length - 1, 0, "."); // insert at index
    const j = parts.join("");
    return parseFloat(j) || 0.0;
}

export const doubleIsEqualTo = (a: number, b: number, maxDiff: number = Number.EPSILON) => Math.abs(b - a) <= maxDiff;
interface IGroup<U> {
    group: string;
    data: U;
}
export function groupBy<T, U>(array: T[], fn: (item: T) => IGroup<U>) {
    const groups: { [key: string]: U[] } = {};
    array.forEach((item) => {
        const result = fn(item);
        const group = JSON.stringify(result.group);
        groups[group] = groups[group] || [];
        groups[group].push(result.data);
    });
    return Object.keys(groups).map((group) => groups[group]);
}

export const getDateXDaysAgo = (numOfDays: number, date = new Date()) => {
    const daysAgo = new Date(date.getTime());
    daysAgo.setDate(date.getDate() - numOfDays);
    return daysAgo;
}

const nameof = <T>(name: Extract<keyof T, string>): string => name;

export function propertyOf<T>(name: keyof T) { return name; }

export function pushRange(arr: any[], itemsToPush: any[]) {
    Array.prototype.push.apply(arr, itemsToPush);
}

export interface IHaveParentId {
    id: any;
    parentId?: any;
}

interface IHasChilds {
    children: any[];
}

export function createDataTree<T extends IHasChilds>(dataset: IHaveParentId[]) {
    const hashTable = Object.create(null);
    dataset.forEach(aData => hashTable[aData.id] = { ...aData });
    const dataTree: T[] = [];
    dataset.forEach(aData => {
        if (aData.parentId) {
            const parent = hashTable[aData.parentId];
            parent.children ??= [];
            parent.children.push(hashTable[aData.id]);
        }
        else dataTree.push(hashTable[aData.id])
    });
    return dataTree;
}


const toBase64 = (file: Blob) => new Promise((resolve, reject) => {
    const reader = new FileReader();
    reader.readAsDataURL(file);
    reader.onload = () => resolve(reader.result);
    reader.onerror = error => reject(error);
});

function* rangeGen(from: number, to: number) {
    let i = from;
    while (i <= to) {
        yield i;
        i++;
    }
}

export function range(from: number, to: number) {
    return Array.from(rangeGen(from, to));
}

export function sumArray<T>(arr: T[], fn: (arg: T) => number): number {
    return arr.reduce((p, c) => p + fn(c), 0);
}

export function isHttpSucces(status?: number) {
    return !!status && status >= 200 && status < 400;
}

export function bytesToKBMB(bytes?: number) {
    if (!bytes) return;
    if (bytes < 1024) {
        return bytes + " bytes";
    } else if (bytes < 1024 * 1024) {
        return (bytes / 1024).toFixed(2) + " KB";
    } else {
        return (bytes / (1024 * 1024)).toFixed(2) + " MB";
    }
}

export function IsNullOrEmpty(text: string) {
    if (text === undefined || text === null || text === "")
        return true;
    return false;
}

export async function delay(ms: number) {
    return new Promise((resolve) => setTimeout(resolve, ms));
}

export interface IPathPart {
    name: string;
    path: string;
    isRootPath: boolean;
}

export function splitStringOnBackslashWithPath(str: string, rootPath: string): IPathPart[] {
    const splitted = str.split("\\");
    const response: IPathPart[] = [];
    const prevPath: string[] = [];
    for (const item of splitted) {
        prevPath.push(item);
        const path = prevPath.join("\\");
        response.push({
            name: item,
            path: path,
            isRootPath: rootPath.includes(path)
        })
    }
    return response;
}

export function getDateFromNumber(numberDate: number): string {
    const dateStr = numberDate.toString();

    const year = parseInt(dateStr.substring(0, 4), 10);
    const month = parseInt(dateStr.substring(4, 6), 10);
    const day = parseInt(dateStr.substring(6, 8), 10);

    const date = new Date(year, month - 1, day); // month is 0-indexed in Date

    const options: Intl.DateTimeFormatOptions = { day: 'numeric', month: 'short', year: 'numeric' };
    return new Intl.DateTimeFormat('en-GB', options).format(date);
}

export const classes = (...args: any[]) => Array.from(args).map(x => String(x).trim()).filter(x => !!x).join(" ");

export { arrRemove, b64toBlob, createCounter, dateAdd, debounced, displayTextAreaProperly, eq, escapeHtml, filterUnique, getFirstItem, getNumber, getString, getToday, historyPager, htmlEncode, imagedataToBlob, isFileImage, isNumberKey, isset, isValidDate, lastWorkday, nameof, nextWorkDay, objToQueryString, onChange, propertiesEqual, remove, strEq, toBase64, unique };

93920cookie-checkTypescript helpers (tshelpers)