Added test suite
This commit is contained in:
@ -72,10 +72,11 @@ export class ArgParser {
|
||||
extras.push(arg);
|
||||
continue;
|
||||
}
|
||||
const value = argDef.default === false ? true :
|
||||
argDef.default === true ? false :
|
||||
queue.splice(queue.findIndex(q => q[0] != '-'), 1)[0] ||
|
||||
argDef.default;
|
||||
const value = combined[1] != null ? combined [1] :
|
||||
argDef.default === false ? true :
|
||||
argDef.default === true ? false :
|
||||
queue.splice(queue.findIndex(q => q[0] != '-'), 1)[0] ||
|
||||
argDef.default;
|
||||
if(value == null) parsed['_error'].push(`Option missing value: ${argDef.name || combined[0]}`);
|
||||
parsed[argDef.name] = value;
|
||||
} else { // Command
|
||||
|
@ -1,3 +1,4 @@
|
||||
import {ASet} from './aset.ts';
|
||||
import {dotNotation, isEqual} from './objects';
|
||||
|
||||
/**
|
||||
@ -28,10 +29,7 @@ export function addUnique<T>(array: T[], el: T): T[] {
|
||||
* @deprecated Use ASet to perform Set operations on arrays
|
||||
*/
|
||||
export function arrayDiff(a: any[], b: any[]): any[] {
|
||||
return makeUnique([
|
||||
...a.filter(v1 => !b.includes((v2: any) => isEqual(v1, v2))),
|
||||
...b.filter(v1 => !a.includes((v2: any) => isEqual(v1, v2))),
|
||||
]);
|
||||
return new ASet(a).symmetricDifference(new ASet(b));
|
||||
}
|
||||
|
||||
/**
|
||||
|
13
src/cache.ts
13
src/cache.ts
@ -99,18 +99,20 @@ export class Cache<K extends string | number | symbol, T> {
|
||||
/**
|
||||
* Remove all keys from cache
|
||||
*/
|
||||
clear() {
|
||||
clear(): this {
|
||||
this.complete = false;
|
||||
this.store = <any>{};
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete an item from the cache
|
||||
* @param {K} key Item's primary key
|
||||
*/
|
||||
delete(key: K) {
|
||||
delete(key: K): this {
|
||||
delete this.store[key];
|
||||
this.save();
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -126,10 +128,11 @@ export class Cache<K extends string | number | symbol, T> {
|
||||
* Manually expire a cached item
|
||||
* @param {K} key Key to expire
|
||||
*/
|
||||
expire(key: K) {
|
||||
expire(key: K): this {
|
||||
this.complete = false;
|
||||
if(this.options.expiryPolicy == 'keep') (<any>this.store[key])._expired = true;
|
||||
else this.delete(key);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -137,7 +140,7 @@ export class Cache<K extends string | number | symbol, T> {
|
||||
* @param {K} key Key to lookup
|
||||
* @return {T} Cached item
|
||||
*/
|
||||
get(key: K, expired?: boolean): T | null {
|
||||
get(key: K, expired?: boolean): CachedValue<T> | null {
|
||||
const cached = deepCopy<any>(this.store[key] ?? null);
|
||||
if(expired || !cached?._expired) return cached;
|
||||
return null;
|
||||
@ -178,7 +181,7 @@ export class Cache<K extends string | number | symbol, T> {
|
||||
if(ttl) setTimeout(() => {
|
||||
this.expire(key);
|
||||
this.save();
|
||||
}, ttl * 1000);
|
||||
}, (ttl || 0) * 1000);
|
||||
return this;
|
||||
}
|
||||
|
||||
|
@ -3,10 +3,10 @@
|
||||
* @param {string} background Color to compare against
|
||||
* @return {"white" | "black"} Color with the most contrast
|
||||
*/
|
||||
export function blackOrWhite(background: string): 'white' | 'black' {
|
||||
const exploded = background?.match(background.length >= 6 ? /\w\w/g : /\w/g);
|
||||
if(!exploded) return 'black';
|
||||
const [r, g, b] = exploded.map(hex => parseInt(hex, 16));
|
||||
export function contrast(background: string): 'white' | 'black' {
|
||||
const exploded = background?.match(background.length >= 6 ? /[0-9a-fA-F]{2}/g : /[0-9a-fA-F]/g);
|
||||
if(!exploded || exploded?.length < 3) return 'black';
|
||||
const [r, g, b] = exploded.map(hex => parseInt(hex.length == 1 ? `${hex}${hex}` : hex, 16));
|
||||
const luminance = (0.299 * r + 0.587 * g + 0.114 * b) / 255;
|
||||
return luminance > 0.5 ? 'black' : 'white';
|
||||
}
|
||||
|
@ -17,7 +17,7 @@ export class TypedEmitter<T extends TypedEvents = TypedEvents> {
|
||||
|
||||
static off(event: any, listener: TypedListener) {
|
||||
const e = event.toString();
|
||||
this.listeners[e] = (this.listeners[e] || []).filter(l => l === listener);
|
||||
this.listeners[e] = (this.listeners[e] || []).filter(l => l != listener);
|
||||
}
|
||||
|
||||
static on(event: any, listener: TypedListener) {
|
||||
@ -43,7 +43,7 @@ export class TypedEmitter<T extends TypedEvents = TypedEvents> {
|
||||
};
|
||||
|
||||
off<K extends keyof T = string>(event: K, listener: T[K]) {
|
||||
this.listeners[event] = (this.listeners[event] || []).filter(l => l === listener);
|
||||
this.listeners[event] = (this.listeners[event] || []).filter(l => l != listener);
|
||||
}
|
||||
|
||||
on<K extends keyof T = string>(event: K, listener: T[K]) {
|
||||
|
18
src/jwt.ts
18
src/jwt.ts
@ -1,7 +1,23 @@
|
||||
import {JSONAttemptParse} from './objects.ts';
|
||||
|
||||
/**
|
||||
* Decode a JWT payload, this will not check for tampering so be careful
|
||||
* Creates a JSON Web Token (JWT) using the provided payload.
|
||||
*
|
||||
* @param {object} payload The payload to include in the JWT.
|
||||
* @param signature Add a JWT signature
|
||||
* @return {string} The generated JWT string.
|
||||
*/
|
||||
export function createJwt(payload: object, signature = 'unsigned'): string {
|
||||
const header = Buffer.from(JSON.stringify({ alg: "HS256", typ: "JWT" }))
|
||||
.toString('base64url');
|
||||
const body = Buffer.from(JSON.stringify(payload))
|
||||
.toString('base64url');
|
||||
// Signature is irrelevant for decodeJwt
|
||||
return `${header}.${body}.${signature}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Decode a JSON Web Token (JWT) payload, this will not check for tampering so be careful
|
||||
*
|
||||
* @param {string} token JWT to decode
|
||||
* @return {unknown} JWT payload
|
||||
|
35
src/math.ts
35
src/math.ts
@ -7,25 +7,30 @@
|
||||
* ```
|
||||
*
|
||||
* @param {number} num Number to convert
|
||||
* @param maxDen
|
||||
* @return {string} Fraction with remainder
|
||||
*/
|
||||
export function dec2Frac(num: number) {
|
||||
const gcd = (a: number, b: number): number => {
|
||||
if (b < 0.0000001) return a;
|
||||
return gcd(b, ~~(a % b));
|
||||
};
|
||||
|
||||
const len = num.toString().length - 2;
|
||||
let denominator = Math.pow(10, len);
|
||||
let numerator = num * denominator;
|
||||
const divisor = gcd(numerator, denominator);
|
||||
numerator = ~~(numerator / divisor);
|
||||
denominator = ~~(denominator / divisor)
|
||||
const remainder = ~~(numerator / denominator);
|
||||
numerator -= remainder * denominator;
|
||||
return `${remainder ? remainder + ' ' : ''}${~~(numerator)}/${~~(denominator)}`;
|
||||
export function dec2Frac(num: number, maxDen=1000): string {
|
||||
let sign = Math.sign(num);
|
||||
num = Math.abs(num);
|
||||
if (Number.isInteger(num)) return (sign * num) + "";
|
||||
let closest = { n: 0, d: 1, diff: Math.abs(num) };
|
||||
for (let d = 1; d <= maxDen; d++) {
|
||||
let n = Math.round(num * d);
|
||||
let diff = Math.abs(num - n / d);
|
||||
if (diff < closest.diff) {
|
||||
closest = { n, d, diff };
|
||||
if (diff < 1e-8) break; // Close enough
|
||||
}
|
||||
}
|
||||
let integer = Math.floor(closest.n / closest.d);
|
||||
let numerator = closest.n - integer * closest.d;
|
||||
return (sign < 0 ? '-' : '') +
|
||||
(integer ? integer + ' ' : '') +
|
||||
(numerator ? numerator + '/' + closest.d : '');
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Convert fraction to decimal number
|
||||
*
|
||||
|
@ -35,5 +35,14 @@ export function escapeRegex(value: string) {
|
||||
return value.replace(/[.*+?^${}()|\[\]\\]/g, '\\$&');
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a function that listens for events and handles them accordingly.
|
||||
*
|
||||
* @param {PathEvent} event - The event object containing data related to the triggered event.
|
||||
* @param {...any} args - Additional arguments that may be passed to the listener.
|
||||
* @returns {any} The return value of the listener, which can vary based on implementation.
|
||||
*/
|
||||
export type Listener = (event: PathEvent, ...args: any[]) => any;
|
||||
|
||||
/** Represents a function that can be called to unsubscribe from an event, stream, or observer */
|
||||
export type Unsubscribe = () => void;
|
||||
|
@ -14,7 +14,7 @@
|
||||
export function clean<T>(obj: T, undefinedOnly = false): Partial<T> {
|
||||
if(obj == null) throw new Error("Cannot clean a NULL value");
|
||||
if(Array.isArray(obj)) {
|
||||
obj = <any>obj.filter(o => o != null);
|
||||
obj = <any>obj.filter(o => undefinedOnly ? o !== undefined : o != null);
|
||||
} else {
|
||||
Object.entries(obj).forEach(([key, value]) => {
|
||||
if((undefinedOnly && value === undefined) || (!undefinedOnly && value == null)) delete (<any>obj)[key];
|
||||
@ -128,7 +128,6 @@ export function flattenObj(obj: any, parent?: any, result: any = {}) {
|
||||
for(const key of Object.keys(obj)) {
|
||||
const propName = parent ? `${parent}.${key}` : key;
|
||||
if(typeof obj[key] === 'object' && obj[key] != null && !Array.isArray(obj[key])) {
|
||||
console.log(propName, );
|
||||
flattenObj(obj[key], propName, result);
|
||||
} else {
|
||||
result[propName] = obj[key];
|
||||
@ -242,10 +241,12 @@ export function JSONSerialize<T1>(obj: T1): T1 | string {
|
||||
* @return {string} JSON string
|
||||
*/
|
||||
export function JSONSanitize(obj: any, space?: number): string {
|
||||
let cache: any[] = [];
|
||||
const cache: any[] = [];
|
||||
return JSON.stringify(obj, (key, value) => {
|
||||
if(typeof value === 'object' && value !== null)
|
||||
if(!cache.includes(value)) cache.push(value);
|
||||
if (typeof value === 'object' && value !== null) {
|
||||
if (cache.includes(value)) return '[Circular]';
|
||||
cache.push(value);
|
||||
}
|
||||
return value;
|
||||
}, space);
|
||||
}
|
||||
|
@ -120,6 +120,7 @@ export class PathEvent {
|
||||
const l1 = p1.fullPath.length, l2 = p2.fullPath.length;
|
||||
return l1 < l2 ? 1 : (l1 > l2 ? -1 : 0);
|
||||
}).reduce((acc, p) => {
|
||||
if(acc && !acc.fullPath.startsWith(p.fullPath)) return acc;
|
||||
if(p.none) hitNone = true;
|
||||
if(!acc) return p;
|
||||
if(hitNone) return acc;
|
||||
@ -253,8 +254,8 @@ export class PathEventEmitter implements IPathEventEmitter{
|
||||
constructor(public readonly prefix: string = '') { }
|
||||
|
||||
emit(event: Event, ...args: any[]) {
|
||||
const parsed = new PathEvent(`${this.prefix}/${typeof event == 'string' ? event : event.toString()}`);
|
||||
this.listeners.filter(l => PathEvent.has(l[0], event))
|
||||
const parsed = new PathEvent(`${this.prefix}/${new PathEvent(event).toString()}`);
|
||||
this.listeners.filter(l => PathEvent.has(l[0], `${this.prefix}/${event}`))
|
||||
.forEach(async l => l[1](parsed, ...args));
|
||||
};
|
||||
|
||||
@ -264,7 +265,7 @@ export class PathEventEmitter implements IPathEventEmitter{
|
||||
|
||||
on(event: Event | Event[], listener: PathListener): PathUnsubscribe {
|
||||
makeArray(event).forEach(e => this.listeners.push([
|
||||
new PathEvent(`${this.prefix}/${typeof e == 'string' ? event : event.toString()}`),
|
||||
new PathEvent(`${this.prefix}/${new PathEvent(e).toString()}`),
|
||||
listener
|
||||
]));
|
||||
return () => this.off(listener);
|
||||
|
@ -1,5 +1,14 @@
|
||||
import {dotNotation, JSONAttemptParse, JSONSerialize} from './objects.ts';
|
||||
|
||||
/**
|
||||
* Filters an array of objects based on a search term and optional regex checking.
|
||||
*
|
||||
* @param {Array} rows Array of objects to filter
|
||||
* @param {string} search The logic string or regext to filter on
|
||||
* @param {boolean} [regex=false] Treat search expression as regex
|
||||
* @param {Function} [transform=(r) => r] - Transform rows before filtering
|
||||
* @return {Array} The filtered array of objects that matched search
|
||||
*/
|
||||
export function search(rows: any[], search: string, regex?: boolean, transform: Function = (r: any) => r) {
|
||||
if(!rows) return [];
|
||||
return rows.filter(r => {
|
||||
|
@ -23,7 +23,7 @@ export const CHAR_LIST = LETTER_LIST + LETTER_LIST.toLowerCase() + NUMBER_LIST +
|
||||
*/
|
||||
export function camelCase(str?: string) {
|
||||
const text = pascalCase(str);
|
||||
return text[0].toLowerCase() + text.slice(1);
|
||||
return !text ? '' : text[0].toLowerCase() + text.slice(1);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -110,13 +110,14 @@ export function pad(text: any, length: number, char: string = ' ', start = true)
|
||||
* @param {string} str
|
||||
* @return {string}
|
||||
*/
|
||||
export function pascalCase(str?: string) {
|
||||
export function pascalCase(str?: string): string {
|
||||
if(!str) return '';
|
||||
const text = str.replaceAll(/(^[^a-zA-Z]+|[^a-zA-Z0-9-_])/g, '')
|
||||
.replaceAll(/[^a-zA-Z0-9]+(\w?)/g, (...args) => args[1]?.toUpperCase() || '');
|
||||
return text[0].toUpperCase() + text.slice(1);
|
||||
return str.match(/[a-zA-Z0-9]+/g)
|
||||
?.map(word => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
|
||||
.join('') ?? '';
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Generate a random hexadecimal value
|
||||
*
|
||||
|
25
src/types.ts
25
src/types.ts
@ -1,27 +1,4 @@
|
||||
/**
|
||||
* Return keys on a type as an array of strings
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* type Person = {
|
||||
* firstName: string;
|
||||
* lastName: string;
|
||||
* age: number;
|
||||
* }
|
||||
*
|
||||
* const keys = typeKeys<Person>();
|
||||
* console.log(keys); // Output: ["firstName", "lastName", "age"]
|
||||
* ```
|
||||
*
|
||||
* @return {Array<keyof T>} Available keys
|
||||
*/
|
||||
export function typeKeys<T extends object>() {
|
||||
return Object.keys(<T>{}) as Array<keyof T>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark all properties as writable
|
||||
*/
|
||||
/** Mark all properties as writable */
|
||||
export type Writable<T> = {
|
||||
-readonly [P in keyof T]: T[P]
|
||||
};
|
||||
|
Reference in New Issue
Block a user