dbl-utils
Version:
Utilities for dbl, adba and others projects
296 lines (258 loc) • 10 kB
text/typescript
import chroma from 'chroma-js';
import moment from 'moment';
//----------------------------------Arrays
/**
* Splits an array into chunks of a given size.
* @param {T[]} arr - Array to split into chunks.
* @param {number} chunkSize - Size of each chunk.
* @returns {T[][]} Array of chunked arrays.
*/
export function sliceIntoChunks<T>(arr: T[], chunkSize: number): T[][] {
const res: T[][] = [];
for (let i = 0; i < arr.length; i += chunkSize) {
const chunk = arr.slice(i, i + chunkSize);
res.push(chunk);
}
return res;
}
export type IArrayString = (null | undefined | boolean | string | IArrayString)[];
/**
* Splits and flattens a list of strings based on a separator.
* @param {string[]} arrayStrings - Array of strings to split and flatten.
* @param {string} separator - Separator to use for splitting.
* @returns {string[]} Flattened and splitted array of strings.
*/
export function splitAndFlat(
arrayStrings: IArrayString,
separator: string = ' '
): string[] {
const r: Array<false | string> = arrayStrings.flatMap(s =>
(typeof s === 'string' && s.split(separator)) as Array<false | string>
);
const filtered = r.filter(Boolean) as Array<string>;
return Array.from(new Set(filtered));
}
//-----------------------------------colors
/**
* Generates a random set of colors with an optional format.
* @param count - Number of colors to generate.
* @param options - Object containing optional parameters.
* @param options.format - The format of the output colors ('hex', 'rgb', 'nrgb').
* @param options.bounds - The bounds for color transformation.
* @param options.distribute - Whether the colors should be evenly distributed.
* @returns An array of colors in the specified format.
*/
export function generateRandomColors(
count: number,
{
format = 'hex',
bounds = 1,
distribute = true
}: { format?: 'hex' | 'rgb' | 'nrgb', bounds?: number | number[] | { r: number, g: number, b: number }, distribute?: boolean } = {}
): Array<string | [number, number, number] | number[]> {
const uniqueColors = new Set<chroma.Color>();
if (distribute) {
const step = Math.floor(0xFFFFFF / count);
for (let i = 0; i < count; i++) {
const colorValue = i * step;
const colorHex = '#' + colorValue.toString(16).padStart(6, '0');
const color = chroma(colorHex);
uniqueColors.add(color);
}
} else {
while (uniqueColors.size < count) {
uniqueColors.add(chroma.random());
}
}
const formattedColors = Array.from(uniqueColors).map((color) => {
const [r, g, b] = color.rgb();
const adjustedColor: [number, number, number] = (typeof bounds === 'number' || Array.isArray(bounds))
? color.rgb().map((c: number) => transform(c, bounds)) as [number, number, number]
: [
transform(r, bounds.r),
transform(g, bounds.g),
transform(b, bounds.b)
];
return chroma(...adjustedColor);
});
return formattedColors.map(color => {
if (format === 'hex') {
return color.hex();
} else if (format === 'rgb') {
return color.rgb();
} else if (format === 'nrgb') {
return color.rgb().map((c: number) => c / 255);
}
}) as Array<string | [number, number, number] | number[]>;
}
/**
* Transforms a color value based on the specified limit.
* @param value - The color component value.
* @param limit - The limit for transformation.
* @returns The transformed color component.
*/
function transform(value: number, limit: number | number[]): number {
const [min, max] = Array.isArray(limit) ? limit : typeof limit === 'number' ? [0, limit] : [0, 1];
return ((value / 255) * (max - min) + min) * 255;
}
/**
* Evaluates the similarity of a set of colors.
* @param cls - Array of colors in hex format.
* @returns A similarity score between 0 and 1, where 0 indicates all colors are distinct and 1 indicates all are similar.
*/
export function evaluateColorSimilarity(colors: string[]): number {
let totalDistance = 0;
let pairsCount = 0;
const cls = colors.sort();
for (let i = 0; i < cls.length; i++) {
for (let j = i + 1; j < cls.length; j++) {
totalDistance += chroma.distance(cls[i], cls[j]);
pairsCount++;
}
}
const averageDistance = totalDistance / pairsCount;
const maxDistance = Math.sqrt(Math.pow(255, 2) * 3); // Maximum possible distance in RGB
return 1 - (averageDistance / maxDistance);
}
//-------------------------------------strings
/**
* Normalizes a string by converting it to lowercase and removing diacritical marks.
* @param {string} str - The string to normalize.
* @returns {string} The normalized string.
*/
export function normalize(str: string = ''): string {
return str.toLowerCase()
.normalize('NFD')
.replace(/[\u0300-\u036f]/g, '');
}
/**
* Converts a string to a slug.
* @param {string} str - The string to slugify.
* @returns {string} The slugified string.
*/
export function slugify(str: string = ''): string {
return normalize(str)
.replace(/\s/g, '-')
.replace(/[^a-zA-Z\d\-]+/g, '')
.replace(/-+/g, '-');
}
/**
* Generates a random four-character string consisting of hexadecimal digits.
* @returns {string} The random four-character string.
*/
export function randomS4(): string {
return (((1 + Math.random()) * 0x10000) | 0).toString(16).substring(1);
}
/**
* Generates a random string of the specified length using the provided characters.
*
* @param {number} [length=16] - The length of the generated string. Default is 16.
* @param {string} [characters='ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'] - A string representing the characters to be used. Default includes uppercase, lowercase letters, and digits.
* @returns {string} A random string composed of the specified characters.
*/
export function randomString(
length: number = 16,
characters: string = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'
): string {
let result = '';
const charactersLength = characters.length;
for (let i = 0; i < length; i++) {
const randomIndex = Math.floor(Math.random() * charactersLength);
result += characters.charAt(randomIndex);
}
return result;
}
//------------------------------------------time
/**
* Generates an array of time intervals between two dates.
* @param {Object} options - Options for generating time intervals.
* @param {string|number} options.from - Start date in string format or as a UNIX timestamp.
* @param {string|number} options.to - End date in string format or as a UNIX timestamp.
* @param {number} options.step - The step size in hours for each time interval.
* @param {string} [options.boundary] - Restrict chunks to 'day', 'month', or 'day,month' boundaries.
* @returns {Array} An array of objects representing each time interval.
*/
export function timeChunks(options: { from: string | number, to: string | number, step: number, boundary?: string }): { from: string, to: string, step: number }[] {
const { from, to, step, boundary } = options;
if (!(step > 0)) {
throw new Error('Invalid step ' + step);
}
if (!moment(from).isValid()) {
throw new Error('Invalid date ' + from);
}
if (!moment(to).isValid()) {
throw new Error('Invalid date ' + to);
}
const hoursArray = [];
const fromMoment = Number.isInteger(from) ? moment.unix(from as number) : moment(from);
const toMoment = Number.isInteger(to) ? moment.unix(to as number) : moment(to);
const diffHours = toMoment.diff(fromMoment, 'hours');
for (let i = 0; i < diffHours; i += step) {
let hourMoment = moment(from).add(i, 'hours');
let nextHourMoment = moment(from).add(i + step, 'hours');
if (boundary) {
const boundaries = boundary.split(',');
if (boundaries.includes('day')) {
const endOfDay = moment(hourMoment).endOf('day');
nextHourMoment = nextHourMoment.isAfter(endOfDay) ? endOfDay : nextHourMoment;
}
if (boundaries.includes('month')) {
const endOfMonth = moment(hourMoment).endOf('month');
nextHourMoment = nextHourMoment.isAfter(endOfMonth) ? endOfMonth : nextHourMoment;
}
}
hoursArray.push({
from: hourMoment.format(),
to: nextHourMoment.isAfter(toMoment) ? moment(to).format() : nextHourMoment.format(),
step
});
}
return hoursArray;
}
/**
* Resolves the promise after a given timeout.
* @param {number} timeout - Milliseconds to wait before resolving.
* @returns {Promise<void>} The promise that resolves after the timeout.
*/
export function delay(timeout: number): Promise<void> {
return new Promise(resolve => setTimeout(resolve, timeout));
}
//-------------------------------------------------------- numbers
/**
* Computes the hash value of a string.
* @param {string} string - The string to hash.
* @returns {number} The computed hash value.
*/
export function hash(string: string): number {
let hash = 0, i, chr;
for (i = 0; i < string.length; i++) {
chr = string.charCodeAt(i);
hash = ((hash << 5) - hash) + chr;
hash |= 0; // Convert to 32-bit integer
}
return hash;
}
/**
* Linear Congruential Generator (LCG) class for pseudo-random number generation.
* @class LCG
*/
export class LCG {
private a: number;
private c: number;
private m: number;
private seed: number;
constructor(seed: number) {
this.a = 1664525;
this.c = 1013904223;
this.m = 2 ** 32;
this.seed = seed;
}
/**
* Generates a pseudo-random number between 0 and 1.
* @returns {number} The generated pseudo-random number.
*/
random(): number {
this.seed = (this.a * this.seed + this.c) % this.m;
return this.seed / this.m;
}
}