Skip to content

Commit

Permalink
✨ common covert utils #61
Browse files Browse the repository at this point in the history
  • Loading branch information
trydofor committed Oct 20, 2024
1 parent 56f503c commit 4b0290b
Show file tree
Hide file tree
Showing 3 changed files with 386 additions and 0 deletions.
228 changes: 228 additions & 0 deletions layers/common/test/safe-converter.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,228 @@
import { describe, it, expect } from 'vitest';
import { safeString, safeNumber, safeBigint, safeValues, safeKeys, safeEntries } from '../utils/safe-converter';

describe('safeString', () => {
it('should return defaults if null or undefined', () => {
expect(safeString(null)).toBe('');
expect(safeString(undefined)).toBe('');
});

it('should return the string itself if valOrFun is a string', () => {
expect(safeString('test')).toBe('test');
});

it('should convert number, boolean, and bigint to string', () => {
expect(safeString(123)).toBe('123');
expect(safeString(true)).toBe('true');
expect(safeString(123n)).toBe('123');
});

it('should return result of function if valOrFun is a function', () => {
expect(safeString(() => 'dynamic', 'default')).toBe('dynamic');
});

it('should convert object to JSON string', () => {
expect(safeString({ key: 'value' })).toBe('{"key":"value"}');
expect(safeString({ key: 123 })).toBe('{"key":123}');
expect(safeString({ key: 123n })).toBe('{"key":"123"}');
});
});

describe('safeNumber', () => {
it('should return defaults if null, undefined, or NaN', () => {
expect(safeNumber(null)).toBe(0);
expect(safeNumber(undefined)).toBe(0);
expect(safeNumber(NaN)).toBe(0);
});

it('should return the number itself if valOrFun is a valid number', () => {
expect(safeNumber(123)).toBe(123);
expect(safeNumber(-456)).toBe(-456);
});

it('should convert boolean to number (true -> 1, false -> 0)', () => {
expect(safeNumber(true)).toBe(1);
expect(safeNumber(false)).toBe(0);
});

it('should return result of function if valOrFun is a function', () => {
expect(safeNumber(() => 42, 0)).toBe(42);
});

it('should convert string, bigint, and other types to number', () => {
expect(safeNumber('123')).toBe(123);
expect(safeNumber(123n)).toBe(123);
expect(safeNumber('invalid', 99)).toBe(99); // Invalid string
});
});

describe('safeBigint', () => {
it('should return defaults if null or undefined', () => {
expect(safeBigint(null)).toBe(0n);
expect(safeBigint(undefined)).toBe(0n);
});

it('should return the bigint itself if valOrFun is a bigint', () => {
expect(safeBigint(123n)).toBe(123n);
});

it('should convert number and boolean to bigint', () => {
expect(safeBigint(123)).toBe(123n);
expect(safeBigint(true)).toBe(1n);
expect(safeBigint(false)).toBe(0n);
});

it('should convert string to bigint', () => {
expect(safeBigint('123')).toBe(123n);
expect(safeBigint('9007199254740991123')).toBe(9007199254740991123n);
expect(safeBigint('9007199254740991123n')).toBe(9007199254740991123n);
expect(safeBigint('90071992547409911.23')).toBe(90071992547409911n);
});

it('should return result of function if valOrFun is a function', () => {
expect(safeBigint(() => 42n, 0n)).toBe(42n);
});

it('should return defaults for invalid string', () => {
expect(safeBigint('invalid', 99n)).toBe(99n);
});
});

describe('safeValues', () => {
it('should return defaults if null or undefined', () => {
expect(safeValues(null)).toEqual([]);
expect(safeValues(undefined)).toEqual([]);
});

it('should return the values of an object', () => {
const obj = { a: 1, b: 'test' };
expect(safeValues(obj)).toEqual([1, 'test']);
});

it('should return result of function if valOrFun is a function', () => {
expect(safeValues(() => ({ a: 1, b: 'test' }))).toEqual([1, 'test']);
});
});

describe('safeKeys', () => {
it('should return defaults if null or undefined', () => {
expect(safeKeys(null)).toEqual([]);
expect(safeKeys(undefined)).toEqual([]);
});

it('should return the keys of an object', () => {
const obj = { a: 1, b: 'test', c: true };
expect(safeKeys(obj)).toEqual(['a', 'b', 'c']);
});

it('should return an empty array for an empty object', () => {
expect(safeKeys({})).toEqual([]);
});

it('should return result of function if valOrFun is a function', () => {
const obj = { a: 1, b: 2 };
expect(safeKeys(() => obj)).toEqual(['a', 'b']);
});

it('should return defaults if function returns null or undefined', () => {
const defaults = ['defaultKey'];
expect(safeKeys(() => null, defaults)).toEqual(defaults);
expect(safeKeys(() => undefined, defaults)).toEqual(defaults);
});

it('should handle arrays and return string index keys', () => {
const arr = [10, 20, 30];
expect(safeKeys(arr)).toEqual(['0', '1', '2']);
});

it('should handle nested functions that return objects', () => {
const nestedFunction = () => () => ({ foo: 'bar' });
expect(safeKeys(nestedFunction)).toEqual(['foo']);
});

it('should return an empty array for non-object values (like number, boolean)', () => {
expect(safeKeys(123 as unknown)).toEqual([]); // Non-object value
expect(safeKeys(true as unknown)).toEqual([]); // Non-object value
});

it('should return keys for objects with complex types', () => {
const obj = { a: [1, 2], b: { nested: true }, c: 'test' };
expect(safeKeys(obj)).toEqual(['a', 'b', 'c']);
});
});

describe('safeEntries', () => {
it('should return defaults if null or undefined', () => {
expect(safeEntries(null)).toEqual([]);
expect(safeEntries(undefined)).toEqual([]);
});

it('should return entries of an object', () => {
const obj = { a: 1, b: 'test', c: true };
expect(safeEntries(obj)).toEqual([['a', 1], ['b', 'test'], ['c', true]]);
});

it('should return entries of an empty object', () => {
expect(safeEntries({})).toEqual([]);
});

it('should return result of function if valOrFun is a function', () => {
const obj = { a: 1, b: 2 };
expect(safeEntries(() => obj)).toEqual([['a', 1], ['b', 2]]);
});

it('should return defaults if valOrFun is null and function is provided', () => {
const defaults = [['key', 'value']] as [string, unknown][];
expect(safeEntries(() => null, defaults)).toEqual(defaults);
});

it('should handle non-object values by throwing or returning an empty array', () => {
expect(safeEntries(123 as unknown)).toEqual([]); // Non-object values should return an empty array
});

it('should handle arrays and return index-value pairs', () => {
const arr = [10, 20, 30];
expect(safeEntries(arr)).toEqual([['0', 10], ['1', 20], ['2', 30]]);
});

it('should handle nested functions that return objects', () => {
const nestedFunction = () => () => ({ foo: 'bar' });
expect(safeEntries(nestedFunction)).toEqual([['foo', 'bar']]);
});
});

describe('numberKey;', () => {
const d3 = 0.1 + 0.2;

it('number key of object', () => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const obj = { 1: '1', 0.3: '0.3' } as any;
expect(obj[1]).toBe('1');
expect(obj['1']).toBe('1');
expect(obj[d3]).toBe(undefined);
});

it('number key of Set/Map', () => {
const numSet = new Set<unknown>([1, 0.3]);
const strSet = new Set<unknown>(['1', '0.3']);

expect(numSet.has(1)).toBe(true);
expect(strSet.has('1')).toBe(true);

expect(numSet.has('1')).toBe(false);
expect(numSet.has('0.3')).toBe(false);
expect(strSet.has(1)).toBe(false);
expect(strSet.has(d3)).toBe(false);

const numMap = new Map<unknown, string>([[1, '1'], [0.3, '0.3']]);
const strMap = new Map<unknown, string>([['1', '1'], ['0.3', '0.3']]);

expect(numMap.has(1)).toBe(true);
expect(strMap.has('1')).toBe(true);

expect(numMap.has('1')).toBe(false);
expect(numMap.has('0.3')).toBe(false);
expect(strMap.has(1)).toBe(false);
expect(strMap.has(d3)).toBe(false);
});
});
12 changes: 12 additions & 0 deletions layers/common/types/common.global.d.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,22 @@
declare global {

/**
* - success: response success
* - data: response data
* - message: human i18n message
* - code: business code
*/
interface DataResult<T> {
success: boolean;
data?: T;
message?: string;
code?: string;
}

/**
* 0 if `null` | `undefined` | `false` | `NaN` | `''`
*/
type NumberLike = null | undefined | boolean | number | string | bigint;
}

//
Expand Down
146 changes: 146 additions & 0 deletions layers/common/utils/safe-converter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
/**
* get non-null converted value
*
* @param valOrFun value or function
* @param defaults default value if null/undefined
* @param convert non-null value converter
* @returns
*/

export function safeConvert<S, T>(valOrFun: S, defaults: T, convert: (value: S) => T | null): T {
if (valOrFun == null) return defaults;

switch (typeof valOrFun) {
case 'function':
return safeConvert(valOrFun(), defaults, convert);
default:
return convert(valOrFun) ?? defaults;
}
}

/**
* get non-null string. bigint to string
*
* @param valOrFun value or function
* @param defaults default value if null/undefined
* @returns
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export function safeString(valOrFun: any, defaults: string = ''): string {
return safeConvert(valOrFun, defaults, (value) => {
switch (typeof value) {
case 'string':
return value;
case 'number':
case 'boolean':
case 'bigint':
return String(value);
default:
return JSON.stringify(value, (_, v) => typeof v === 'bigint' ? v.toString() : v);
}
});
}

/**
* get non-null and non-NaN number
*
* @param valOrFun value or function
* @param defaults default value if null/undefined/NaN
* @returns
*/
export function safeNumber(valOrFun: NumberLike | (() => NumberLike), defaults: number = 0): number {
return safeConvert(valOrFun, defaults, (value) => {
switch (typeof value) {
case 'number':
return isNaN(value) ? null : value;
case 'boolean':
return value ? 1 : 0;
default:{
const num = parseInt(String(value)); // Number('') === 0
return isNaN(num) ? null : num;
}
}
});
}

/**
* get non-null bigint
*
* @param valOrFun value or function
* @param defaults default value if null/undefined/NaN
* @returns
*/
export function safeBigint(valOrFun: NumberLike | (() => NumberLike), defaults: bigint = 0n): bigint {
return safeConvert(valOrFun, defaults, (value) => {
switch (typeof value) {
case 'bigint':
return value;
case 'boolean':
return value ? 1n : 0n;
case 'number':{
const num = Math.trunc(value);
return isNaN(num) ? null : BigInt(num);
}
case 'string':{
const num = parseInt(value);
if (Number.isSafeInteger(num)) {
return BigInt(num);
}
else {
const p1 = value.indexOf('.'); // ###.###
if (p1 > 0) return BigInt(value.substring(0, p1));

const n1 = value.substring(value.length - 1); // ####n
if (n1 === 'n' || n1 === 'N') return BigInt(value.substring(0, value.length - 1));

// #.##e+10, 0x###, 0b###, 0o###
// invalid number
try {
return BigInt(value);
}
catch {
return null;
}
}
}
default:
return null;
}
});
}

/**
* get non-null Object.values
*
* @param valOrFun value or function
* @param defaults default value if null/undefined
* @returns
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export function safeValues<T>(valOrFun: any, defaults: T[] = []): T[] {
return safeConvert(valOrFun, defaults, Object.values);
}

/**
* get non-null Object.keys
*
* @param valOrFun value or function
* @param defaults default value if null/undefined
* @returns
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export function safeKeys(valOrFun: any, defaults: string[] = []): string[] {
return safeConvert(valOrFun, defaults, Object.keys);
}

/**
* get non-null Object.entries
*
* @param valOrFun value or function
* @param defaults default value if null/undefined
* @returns
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export function safeEntries<T>(valOrFun: any, defaults: [string, T][] = []): [string, T][] {
return safeConvert(valOrFun, defaults, Object.entries);
}

0 comments on commit 4b0290b

Please sign in to comment.