inventoresed
Version:
Z-Wave driver written entirely in JavaScript/TypeScript
211 lines (195 loc) • 5.84 kB
text/typescript
import { isArray, isObject } from "alcalzone-shared/typeguards";
import { num2hex } from "./strings";
/** Object.keys, but with `(keyof T)[]` as the return type */
export function keysOf<T>(obj: T): (keyof T)[] {
return Object.keys(obj) as unknown as (keyof T)[];
}
/** Returns a subset of `obj` that contains only the given keys */
export function pick<T extends Record<any, any>, K extends keyof T>(
obj: T,
keys: readonly K[],
): Pick<T, K> {
const ret = {} as Pick<T, K>;
for (const key of keys) {
if (key in obj) ret[key] = obj[key];
}
return ret;
}
/**
* Traverses an object and returns the property identified by the given path. For example, picking from
* ```json
* {
* "foo": {
* "bar": [
* 1, 2, 3
* ]
* }
* ```
* with path `foo.bar.1` will return `2`.
*/
export function pickDeep<T = unknown>(
object: Record<string, any>,
path: string,
): T {
function _pickDeep(obj: Record<string, any>, pathArr: string[]): unknown {
// are we there yet? then return obj
if (!pathArr.length) return obj;
// are we not looking at an object or array? Then bail
if (!isObject(obj) && !isArray(obj)) return undefined;
// go deeper
const propName = pathArr.shift()!;
return _pickDeep(obj[propName], pathArr);
}
return _pickDeep(object, path.split(".")) as T;
}
/** Calls the map function of the given array and flattens the result by one level */
export function flatMap<U, T extends any[]>(
array: T[],
callbackfn: (value: T, index: number, array: T[]) => U[],
): U[] {
const mapped = array.map(callbackfn);
return mapped.reduce((acc, cur) => [...acc, ...cur], [] as U[]);
}
/**
* Returns a human-readable representation of the given enum value.
* If the given value is not found in the enum object, `"unknown (<value-as-hex>)"` is returned.
*
* @param enumeration The enumeration object the value comes from
* @param value The enum value to be pretty-printed
*/
export function getEnumMemberName(enumeration: unknown, value: number): string {
return (enumeration as any)[value] || `unknown (${num2hex(value)})`;
}
/** Skips the first n bytes of a buffer and returns the rest */
export function skipBytes(buf: Buffer, n: number): Buffer {
return Buffer.from(buf.slice(n));
}
/**
* Returns a throttled version of the given function. No matter how often the throttled version is called,
* the underlying function is only called at maximum every `intervalMs` milliseconds.
*/
export function throttle<T extends any[]>(
fn: (...args: T) => void,
intervalMs: number,
trailing: boolean = false,
): (...args: T) => void {
let lastCall = 0;
let timeout: NodeJS.Timeout | undefined;
return (...args: T) => {
const now = Date.now();
if (now >= lastCall + intervalMs) {
// waited long enough, call now
lastCall = now;
fn(...args);
} else if (trailing) {
if (timeout) clearTimeout(timeout);
const delay = lastCall + intervalMs - now;
timeout = setTimeout(() => {
lastCall = now;
fn(...args);
}, delay);
}
};
}
/**
* Merges the user-defined options with the default options
*/
export function mergeDeep(
target: Record<string, any> | undefined,
source: Record<string, any>,
): Record<string, any> {
target = target || {};
for (const [key, value] of Object.entries(source)) {
if (!(key in target)) {
target[key] = value;
} else {
if (typeof value === "object") {
// merge objects
target[key] = mergeDeep(target[key], value);
} else if (typeof target[key] === "undefined") {
// don't override single keys
target[key] = value;
}
}
}
return target;
}
/**
* Creates a deep copy of the given object
*/
export function cloneDeep<T>(source: T): T {
if (isArray(source)) {
return source.map((i) => cloneDeep(i)) as any;
} else if (isObject(source)) {
const target: any = {};
for (const [key, value] of Object.entries(source)) {
target[key] = cloneDeep(value);
}
return target;
} else {
return source;
}
}
/** Pads a firmware version string, so it can be compared with semver */
export function padVersion(version: string): string {
if (version.split(".").length === 3) return version;
return version + ".0";
}
/**
* Using a binary search, this finds the highest discrete value in [rangeMin...rangeMax] where executor returns true, assuming that
* increasing the value will at some point cause the executor to return false.
*/
export async function discreteBinarySearch(
rangeMin: number,
rangeMax: number,
executor: (value: number) => boolean | PromiseLike<boolean>,
): Promise<number | undefined> {
let min = rangeMin;
let max = rangeMax;
while (min < max) {
const mid = min + Math.floor((max - min + 1) / 2);
const result = await executor(mid);
if (result) {
min = mid;
} else {
max = mid - 1;
}
}
if (min === rangeMin) {
// We didn't test this yet
const result = await executor(min);
if (!result) return undefined;
}
return min;
}
/**
* Using a linear search, this finds the highest discrete value in [rangeMin...rangeMax] where executor returns true, assuming that
* increasing the value will at some point cause the executor to return false.
*/
export async function discreteLinearSearch(
rangeMin: number,
rangeMax: number,
executor: (value: number) => boolean | PromiseLike<boolean>,
): Promise<number | undefined> {
for (let val = rangeMin; val <= rangeMax; val++) {
const result = await executor(val);
if (!result) {
// Found the first value where it no longer returns true
if (val === rangeMin) {
// No success at all
break;
} else {
// The previous test was successful
return val - 1;
}
} else {
if (val === rangeMax) {
// Everything was successful
return rangeMax;
}
}
}
}
export function sum(values: number[]): number {
return values.reduce((acc, cur) => acc + cur, 0);
}