@sdflc/utils
v1.0.28
Published
A set of utilites used in the @sdflc libraries and related projects. Purpose is to minimize usage of 3rd party libraries.
Readme
@sdflc/utils
A collection of TypeScript utility functions for common tasks including address formatting, array manipulation, color helpers, unit conversions, geolocation math, logging, performance measurement, string processing, and more.
Installation
npm install @sdflc/utilsTable of Contents
- Addresses
- Arrays
- Colors
- Constants
- Converters
- Geo
- Languages
- Logger
- MeasureTool
- Numbers
- Objects
- Strings
- String Helpers
- StrOrderHelpers
- Transformers
- URL Params
Addresses
import { formatAddressShort, formatAddressFull } from '@sdflc/utils';formatAddressShort(address)
Formats an AddressData object into a short display string suitable for place auto-fill. Includes address1, city, and region.
formatAddressShort({ address1: '123 Main St', city: 'Calgary', region: 'AB' });
// => '123 Main St, Calgary, AB'
formatAddressShort(null); // => ''formatAddressFull(address)
Formats an AddressData object into a full display string. Includes address1, city, region, postalCode, and country.
formatAddressFull({
address1: '123 Main St',
city: 'Calgary',
region: 'AB',
postalCode: 'T2P 1J9',
country: 'Canada',
});
// => '123 Main St, Calgary, AB T2P 1J9, Canada'Arrays
import { arrayToObject, arrToUpperCase, arrToLowerCase, arrToChunks } from '@sdflc/utils';arrayToObject(arr, nameKey, valueKey)
Converts an array of objects into a plain object using nameKey for property names and valueKey for values.
const arr = [
{ name: 'status', value: 'active' },
{ name: 'role', value: 'admin' },
];
arrayToObject(arr, 'name', 'value');
// => { status: 'active', role: 'admin' }arrToUpperCase(arr)
Uppercases every string in the array. Null/undefined items become ''.
arrToUpperCase(['hello', 'world']); // => ['HELLO', 'WORLD']arrToLowerCase(arr)
Lowercases every string in the array. Null/undefined items become ''.
arrToLowerCase(['HELLO', 'WORLD']); // => ['hello', 'world']arrToChunks(arr, chunkSize)
Splits an array into sequential chunks of chunkSize. Returns the original array if chunkSize is less than 1 or NaN.
arrToChunks([1, 2, 3, 4, 5], 2); // => [[1, 2], [3, 4], [5]]
arrToChunks([1, 2, 3], 0); // => [1, 2, 3]Colors
import { getRandomHexColor, getMostContrastingColor } from '@sdflc/utils';getRandomHexColor()
Returns a random hex color string in #RRGGBB format, including #000000 and #FFFFFF.
getRandomHexColor(); // => '#a3f2c1'getMostContrastingColor(hex)
Returns either #000000 or #FFFFFF — whichever has the higher WCAG contrast ratio against the given background color. Supports #RGB, #RGBA, #RRGGBB, and #RRGGBBAA formats. Alpha is preserved in the output if present in the input.
getMostContrastingColor('#000000'); // => '#FFFFFF'
getMostContrastingColor('#FFFFFF'); // => '#000000'
getMostContrastingColor('#1a1a2e'); // => '#FFFFFF'
getMostContrastingColor('#000000aa'); // => '#FFFFFFaa'Constants
import {
VALUE_TYPES,
STATUSES,
ACCESS_RIGHTS,
SORT_ORDER,
DISTANCE_UNITS,
VOLUME_UNITS,
CONSUMPTION_UNITS,
} from '@sdflc/utils';VALUE_TYPES
Numeric codes for data types used with convertStringToValue.
| Key | Value |
| ---------- | ----- |
| NUMBER | 100 |
| INTEGER | 101 |
| FLOAT | 102 |
| DECIMAL | 103 |
| CURRENCY | 200 |
| BOOLEAN | 300 |
| STRING | 400 |
| JSON | 500 |
| EMAIL | 1001 |
| PHONE | 1002 |
| URL | 1003 |
STATUSES
Standard record status codes.
| Key | Value |
| ---------- | ----- |
| TEST | 50 |
| ACTIVE | 100 |
| DRAFT | 200 |
| PENDING | 300 |
| BLOCKED | 1000 |
| DISABLED | 5000 |
| REMOVED | 10000 |
ACCESS_RIGHTS
Bitmask values for permission checks.
// Combine with bitwise OR
const rights = ACCESS_RIGHTS.LIST | ACCESS_RIGHTS.GET; // => 3| Key | Value |
| -------- | ----- |
| LIST | 1 |
| GET | 2 |
| CREATE | 4 |
| UPDATE | 8 |
| REMOVE | 16 |
| RUN | 32 |
DISTANCE_UNITS / VOLUME_UNITS / CONSUMPTION_UNITS
Type-safe string constants for unit conversion functions. Always prefer these over raw strings.
DISTANCE_UNITS.KM; // => 'km'
DISTANCE_UNITS.MILES; // => 'mi'
VOLUME_UNITS.LITERS; // => 'l'
VOLUME_UNITS.GALLONS_US; // => 'gal-us'
VOLUME_UNITS.GALLONS_UK; // => 'gal-uk'
VOLUME_UNITS.KWH; // => 'kwh'
VOLUME_UNITS.KG; // => 'kg'
CONSUMPTION_UNITS.L_PER_100KM; // => 'l100km'
CONSUMPTION_UNITS.MPG_US; // => 'mpg-us'
CONSUMPTION_UNITS.KWH_PER_100KM; // => 'kwh100km'
// ... and moreOther constants
import { UUID_EMPTY, UUID_ZERO, ALPHABET, ALPHABET_CODE, EARTH_RADIUS_M } from '@sdflc/utils';Converters
import {
convertStringToValue,
stringifyObject,
parseObject,
toRadians,
metersToKm,
mpsToKmh,
isElectricUnit,
isHydrogenUnit,
isLiquidUnit,
toMetricDistance,
fromMetricDistance,
fromMetricDistanceRounded,
toMetricVolume,
fromMetricVolume,
getConsumptionUnitForFuelType,
deriveConsumptionUnit,
calculateConsumption,
} from '@sdflc/utils';convertStringToValue(value, valueType)
Converts a string to a typed value using a VALUE_TYPES constant.
convertStringToValue('42', VALUE_TYPES.INTEGER); // => 42
convertStringToValue('3.14', VALUE_TYPES.FLOAT); // => 3.14
convertStringToValue('true', VALUE_TYPES.BOOLEAN); // => truestringifyObject(config, defaultObj?)
Safely JSON-stringifies an object. Returns the stringified defaultObj on failure.
stringifyObject({ a: 1 }); // => '{"a":1}'
stringifyObject(circular, { ok: 1 }); // => '{"ok":1}'parseObject(jsonString, defaultObj)
Safely parses a JSON string into an object shaped like defaultObj. Fills in missing keys from defaults and ignores type mismatches.
parseObject('{"name":"Alice"}', { name: '', age: 0 });
// => { name: 'Alice', age: 0 }Unit conversion functions
All return null for null/undefined inputs.
toRadians(180); // => 3.14159...
metersToKm(1500); // => 1.5
mpsToKmh(10); // => 36
toMetricDistance(10, DISTANCE_UNITS.MILES); // => 16.093
fromMetricDistance(16.0934, DISTANCE_UNITS.MILES); // => ~10
toMetricVolume(1, VOLUME_UNITS.GALLONS_US); // => 3.785
fromMetricVolume(3.785, VOLUME_UNITS.GALLONS_US); // => ~1calculateConsumption(distanceKm, volumeOrEnergy, consumptionUnit)
Calculates fuel/energy consumption in the specified unit. Supports liquid, electric, and hydrogen units.
// 10L over 100km = 10 l/100km
calculateConsumption(100, 10, CONSUMPTION_UNITS.L_PER_100KM); // => 10
// 20kWh over 100km = 20 kWh/100km
calculateConsumption(100, 20, CONSUMPTION_UNITS.KWH_PER_100KM); // => 20Geo
import {
buildCoordinatesStr,
extractLatitudeLongitude,
isValidCoordinate,
calcDistance,
areCoordinatesNear,
findNearestLocation,
haversineDistanceM,
formatCoordinates,
createBoundingBox,
isWithinBoundingBox,
calculateBearing,
bearingDelta,
calculateTotalDistance,
} from '@sdflc/utils';buildCoordinatesStr(coords, precision?)
Formats a GeoCoords object as "lat, lng" string. Defaults to 5 decimal places.
buildCoordinatesStr({ latitude: 51.5074, longitude: -0.1278 });
// => '51.50740, -0.12780'extractLatitudeLongitude(coordinatesStr)
Parses a "lat,lng" string into a GeoCoords object with numeric values.
extractLatitudeLongitude('51.5074,-0.1278');
// => { latitude: 51.5074, longitude: -0.1278 }isValidCoordinate(coords)
Returns true if the coordinates are non-null, numeric, and within valid lat/lng ranges.
isValidCoordinate({ latitude: 51.5074, longitude: -0.1278 }); // => true
isValidCoordinate({ latitude: 999, longitude: 0 }); // => falsecalcDistance(args)
Calculates the great-circle distance between two coordinates using the Haversine formula (atan2 variant). Returns distance in kilometers.
calcDistance({
position1: { latitude: 51.5074, longitude: -0.1278 },
position2: { latitude: 48.8566, longitude: 2.3522 },
}); // => ~343 kmhaversineDistanceM(coord1, coord2)
Same as calcDistance but returns distance in meters.
haversineDistanceM({ latitude: 51.5074, longitude: -0.1278 }, { latitude: 48.8566, longitude: 2.3522 }); // => ~343000 metersareCoordinatesNear(coord1, coord2, thresholdM?)
Returns true if two coordinates are within thresholdM meters of each other. Default threshold is DEFAULT_DISTANCE_THRESHOLD_M.
areCoordinatesNear(coordA, coordB, 100); // within 100m?findNearestLocation(target, locations, getCoords)
Finds the nearest item in locations to target. Returns { item, distance } or null.
const nearest = findNearestLocation(userCoords, stores, (store) => store.coords);
// => { item: nearestStore, distance: 250.5 }createBoundingBox(center, radiusM)
Creates a BoundingBox around a coordinate with the given radius in meters.
createBoundingBox({ latitude: 51.5074, longitude: -0.1278 }, 1000);
// => { north: ..., south: ..., east: ..., west: ... }isWithinBoundingBox(coords, box)
Returns true if coords falls within the bounding box.
calculateBearing(p1, p2)
Returns the initial bearing from p1 to p2 in degrees (0–360). Returns null for invalid input.
calculateBearing({ latitude: 0, longitude: 0 }, { latitude: 0, longitude: 10 });
// => ~90 (due East)bearingDelta(bearing1, bearing2)
Returns the absolute angular difference between two bearings (0–180), handling the 0°/360° wraparound.
bearingDelta(10, 350); // => 20calculateTotalDistance(points)
Sums great-circle distances between consecutive points. Returns total in meters.
calculateTotalDistance([london, paris, berlin]); // => total metersLanguages
import { extractLanguages } from '@sdflc/utils';extractLanguages(str)
Parses an Accept-Language HTTP header into an ordered array of language tags.
extractLanguages('en-US,en;q=0.9,ru;q=0.8,fr;q=0.7');
// => ['en-US', 'en', 'ru', 'fr']Logger
import { Logger } from '@sdflc/utils';
import { LoggerLevels } from '@sdflc/utils/interfaces';A configurable logger with level filtering, optional timestamp, request ID, and module name prefixes.
Levels (lowest to highest)
NONE → ERROR → WARNING → LOG → DEBUG
Only messages at or below the current level are emitted.
Basic usage
const logger = new Logger({ level: LoggerLevels.DEBUG });
logger.debug('Fetching user...');
logger.log('User fetched');
logger.warn('Cache miss');
logger.error('Connection failed');Custom handlers
const logger = new Logger({
level: LoggerLevels.LOG,
log: (msg) => myLoggingService.info(msg),
error: (msg) => myLoggingService.error(msg),
});Prefix options
const logger = new Logger({
level: LoggerLevels.DEBUG,
timestamp: true, // prepend timestamp to every line
timestampUtc: true, // use UTC (default: local time)
requestId: 'req-abc123',
module: 'UserService',
});
logger.log('User created');
// => [20250325-143022.123Z] [req-abc123] [UserService] User createdsetLevel(newLevel) / getLevel()
logger.setLevel(LoggerLevels.ERROR); // suppress everything below ERROR
logger.getLevel(); // => LoggerLevels.ERRORMeasureTool
import { MeasureTool } from '@sdflc/utils';A cross-environment performance measurement utility using performance.now() (available in Node.js 16+ and all modern browsers).
Basic measurement
const tool = new MeasureTool();
tool.start();
// ... do work ...
const result = tool.stop('total');
console.log(result.durationMs); // millisecondsCheckpoints
tool.start();
await fetchData();
tool.addCheckpoint('fetch');
await processData();
tool.addCheckpoint('process');
const final = tool.stop('done');
console.log(tool.getCheckpoints());
// => [{ name: 'fetch', durationMs: 120, hrtime: [...] }, ...]measureExecTime(name, fn)
Wraps an async function and returns its result along with timing data.
const { result, durationMs } = await tool.measureExecTime('myQuery', async () => {
return await db.query('SELECT ...');
});threadBlockTime(name?, fn?)
Measures event-loop blocking time using setImmediate. Node.js only.
tool.threadBlockTime('check', ({ name, durationMs }) => {
console.log(`${name} blocked for ${durationMs}ms`);
});Numbers
import { roundNumberValue, roundNumberValues, parseNumericInput } from '@sdflc/utils';roundNumberValue(value, decimals?)
Rounds a number to decimals decimal places (default: 2).
roundNumberValue(2.34567); // => 2.35
roundNumberValue(2.34567, 3); // => 2.346
roundNumberValue(2.5, 0); // => 3roundNumberValues(obj, decimals?)
Recursively rounds all numeric values in an object or array in place. Also returns the mutated value.
const data = { price: 9.9999, tax: 1.2345, label: 'item' };
roundNumberValues(data);
// data => { price: 10, tax: 1.23, label: 'item' }
roundNumberValues([1.111, 2.999]); // => [1.11, 3]parseNumericInput(value)
Normalises a value from a numeric input field into number | null.
parseNumericInput('42'); // => 42
parseNumericInput(''); // => null
parseNumericInput(null); // => null
parseNumericInput('abc'); // => null
parseNumericInput(NaN); // => nullObjects
import { setNullOnEmptyString, onlyPropsOf, compactObject, cloneDeep } from '@sdflc/utils';setNullOnEmptyString(obj)
Recursively replaces all empty string values ('') with null in an object or array.
setNullOnEmptyString({ firstName: 'John', lastName: '' });
// => { firstName: 'John', lastName: null }
setNullOnEmptyString({ user: { name: '', age: 30 } });
// => { user: { name: null, age: 30 } }
setNullOnEmptyString([{ a: '' }, { a: 'keep' }]);
// => [{ a: null }, { a: 'keep' }]onlyPropsOf(source, destinationType)
Creates an instance of destinationType and copies only the properties that exist on that class from source.
class UserDto {
id = 0;
name = '';
}
onlyPropsOf({ id: 1, name: 'Alice', extra: 'ignored' }, UserDto);
// => UserDto { id: 1, name: 'Alice' }compactObject(obj)
Recursively removes all undefined properties from an object or array. null values are preserved.
compactObject({ a: 1, b: undefined, c: { d: undefined, e: 2 } });
// => { a: 1, c: { e: 2 } }cloneDeep(obj)
Creates a deep clone handling Date, undefined, NaN, Infinity, and circular references correctly.
const original = { a: { b: 1 }, d: new Date() };
const clone = cloneDeep(original);
clone.a.b = 99;
original.a.b; // => 1 (unchanged)
clone.d instanceof Date; // => trueStrings
import { camelKeys, camelResponse, pascalCase, pascalCases, buildKey, buildKeys, isIdEmpty, slug } from '@sdflc/utils';camelKeys(result)
Recursively converts all object keys to camelCase. Handles arrays and nested objects. Leaves Date instances and primitives untouched.
camelKeys({ first_name: 'John', last_name: 'Doe' });
// => { firstName: 'John', lastName: 'Doe' }
camelKeys([{ user_id: 1 }, { user_id: 2 }]);
// => [{ userId: 1 }, { userId: 2 }]camelResponse(result)
Converts object keys to camelCase. Returns the value unchanged if it is falsy or has a rows property (raw database result sets).
camelResponse({ user_id: 1 }); // => { userId: 1 }
camelResponse(null); // => nullpascalCase(name)
Converts a string to PascalCase.
pascalCase('hello_world'); // => 'HelloWorld'
pascalCase('hello-world'); // => 'HelloWorld'pascalCases(args)
Recursively converts all keys in an object or array to PascalCase. Supports a mapKey override for specific keys.
pascalCases({ src: { first_name: 'Alice' } });
// => { FirstName: 'Alice' }
pascalCases({ src: { id: 1, name: 'Alice' }, mapKey: { id: 'Id' } });
// => { Id: 1, Name: 'Alice' }buildKey(keys)
Converts a string, number, array, or object to a slug-based key string.
buildKey('hello world'); // => 'hello-world'
buildKey(42); // => '42'
buildKey(['foo', 'bar']); // => 'foo-bar'
buildKey({ a: 1 }); // => '{"a":1}'buildKeys(keys)
Calls buildKey on each item in an array.
buildKeys(['hello world', 42]); // => ['hello-world', '42']isIdEmpty(value)
Returns true if the value represents an empty/unset ID.
isIdEmpty(null); // => true
isIdEmpty(''); // => true
isIdEmpty('0'); // => true
isIdEmpty(0); // => true
isIdEmpty(UUID_EMPTY); // => true
isIdEmpty('abc-123'); // => falseslug(str)
Converts a string to a URL-friendly slug. Normalises Unicode/accented characters to ASCII equivalents.
slug('Hello World!'); // => 'hello-world'
slug('café résumé'); // => 'cafe-resume'
slug('Привет мир'); // => 'privet-mir'String Helpers
import {
doesValueMatchAlphabet,
isLengthBetween,
areStringsEqual,
replaceAt,
insertAt,
randomString,
escapeRegExp,
formatString,
normalizeName,
} from '@sdflc/utils';doesValueMatchAlphabet(value, alphabet)
Returns true if every character in value is present in alphabet.
doesValueMatchAlphabet('ABC123', 'ABCDEFGHJKLMNPQRSTUVWXYZ0123456789'); // => true
doesValueMatchAlphabet('abc', 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'); // => falseisLengthBetween(str, minLen, maxLen)
Returns true if str length is between minLen and maxLen (inclusive).
isLengthBetween('hello', 3, 10); // => true
isLengthBetween('hi', 3, 10); // => falseareStringsEqual(strLeft, strRight)
Returns true if the two strings are equal ignoring case.
areStringsEqual('Hello', 'hello'); // => truereplaceAt(str, index, replacement)
Replaces characters in str starting at index with replacement.
replaceAt('hello world', 6, 'there'); // => 'hello there'insertAt(str, index, insert)
Inserts insert into str at index without removing any characters.
insertAt('helo', 3, 'l'); // => 'hello'randomString(length, alphabet?)
Generates a random string. Not suitable for cryptographic purposes.
randomString(8); // => 'A3KF9Z2M' (from default alphabet)
randomString(6, 'abc'); // => 'bcaabc'escapeRegExp(str)
Escapes all regex special characters so the string can be safely used in new RegExp(...).
escapeRegExp('{{name}}'); // => '\\{\\{name\\}\\}'formatString(str, obj, opt?)
Replaces {{key}} placeholders in a template string with values from obj. Delimiters are configurable. Null/undefined values leave the placeholder unchanged.
formatString('Hello {{name}}!', { name: 'Alice' });
// => 'Hello Alice!'
formatString('Hi ${name}!', { name: 'Bob' }, { leftWrapper: '${', rightWrapper: '}' });
// => 'Hi Bob!'normalizeName(name)
Trims and lowercases a name string. Returns undefined for null/undefined input.
normalizeName(' Alice '); // => 'alice'
normalizeName(null); // => undefinedStrOrderHelpers
import { StrOrderHelpers } from '@sdflc/utils';A counter that operates in any numeric base (default: base-36) using string representations. Useful for generating sortable string order keys.
const order = new StrOrderHelpers({ start: '100', step: '5' });
order.current(); // => '100'
order.increase(); // => '105'
order.increase(); // => '10a'
order.decrease(); // => '105'
order.reset(); // => resets to '100'Arithmetic helpers
order.add('1000', 'bb'); // => '10bb'
order.subtract('10cc', 'cc'); // => '1000'
order.addValue('aa'); // adds to current counter, returns new value
order.subtractValue('a'); // subtracts from current countervalueBetween(valueA, valueB)
Finds the midpoint between two values in the current base. Returns null if no distinct midpoint exists (adjacent or equal values).
order.valueBetween('aaa', 'ccc'); // => 'bbb'
order.valueBetween('100', '104'); // => '102'
order.valueBetween('100', '101'); // => nullConstructor options
| Option | Type | Default | Description |
| ------- | -------- | ------- | ------------------------ |
| start | string | '0' | Initial counter value |
| step | string | '1' | Increment/decrement step |
| base | number | 36 | Numeric base (2–36) |
Transformers
import { buildHierarchy, mapArrayBy, flattenHierarchy, getLowestLevelItems } from '@sdflc/utils';buildHierarchy(arr, idField, parentIdField, nameForChildren)
Converts a flat array into a tree structure. Items with no valid parent are roots; items whose parent isn't in the array are orphans.
const flat = [
{ id: '1', parentId: null, name: 'Root' },
{ id: '2', parentId: '1', name: 'Child' },
];
buildHierarchy(flat, 'id', 'parentId', 'children');
// => {
// tree: [{ id: '1', name: 'Root', children: [{ id: '2', name: 'Child', children: [] }] }],
// orphans: []
// }mapArrayBy(arr, mapBy, opt?)
Maps an array of objects into a lookup object keyed by one or more fields.
const arr = [
{ id: '1', val: 'a' },
{ id: '2', val: 'b' },
];
mapArrayBy(arr, 'id');
// => { '1': { id: '1', val: 'a' }, '2': { id: '2', val: 'b' } }
mapArrayBy(arr, 'id', { multiple: true });
// => { '1': [{ id: '1', val: 'a' }], '2': [{ id: '2', val: 'b' }] }
mapArrayBy(arr, ['id', 'val']);
// => { id: { '1': {...}, '2': {...} }, val: { a: {...}, b: {...} } }flattenHierarchy(arr, nameForChildren, opt?)
Flattens a tree into a depth-first ordered array.
flattenHierarchy(tree, 'children');
// => [root, child1, grandchild1, child2, ...]
// With callback:
flattenHierarchy(tree, 'children', {
onItem: (item) => console.log(item.name),
});getLowestLevelItems(arr, idField, parentIdField, nameForChildren, setRoot?)
Returns only the leaf nodes from a flat array. Optionally attaches a rootId to each leaf.
getLowestLevelItems(flat, 'id', 'parentId', 'children', true);
// => [
// { id: '4', ..., rootId: '1' },
// { id: '5', ..., rootId: '1' },
// ]URL Params
import { buildURLSearchParams } from '@sdflc/utils';buildURLSearchParams(data)
Converts a plain object into a URLSearchParams instance. Supports nested objects (dot notation), arrays (bracket notation), Date values, and mixed structures. Null and undefined values are omitted.
buildURLSearchParams({ name: 'Alice', age: 30 }).toString();
// => 'name=Alice&age=30'
buildURLSearchParams({ tags: ['a', 'b'] }).toString();
// => 'tags[0]=a&tags[1]=b'
buildURLSearchParams({ address: { city: 'Calgary' } }).toString();
// => 'address.city=Calgary'
buildURLSearchParams({
filters: [{ field: 'status', values: ['active', 'pending'] }],
}).toString();
// => 'filters[0].field=status&filters[0].values[0]=active&filters[0].values[1]=pending'
buildURLSearchParams({ createdAt: new Date('2025-01-01T00:00:00Z') }).toString();
// => 'createdAt=2025-01-01T00:00:00.000Z'License
MIT
