import {EncodedValue} from "./types"; /** * Interprets an encoded string and returns either the string or null/undefined if not available. * Ignores array inputs (takes just first element in array) * @param input encoded string */ function getEncodedValue( input: EncodedValue, allowEmptyString?: boolean ): string | null | undefined { if (input == null) { return input; } // '' or [] if ( input.length === 0 && (!allowEmptyString || (allowEmptyString && input !== '')) ) { return null; } const str = input instanceof Array ? input[0] : input; if (str == null) { return str; } if (!allowEmptyString && str === '') { return null; } return str; } /** * Interprets an encoded string and return null/undefined or an array with * the encoded string contents * @param input encoded string */ function getEncodedValueArray( input: EncodedValue ): (string | null)[] | null | undefined { if (input == null) { return input; } return input instanceof Array ? input : input === '' ? [] : [input]; } /** * Encodes a date as a string in YYYY-MM-DD format. * * @param {Date} date * @return {String} the encoded date */ export function encodeDate( date: Date | null | undefined ): string | null | undefined { if (date == null) { return date; } const year = date.getFullYear(); const month = date.getMonth() + 1; const day = date.getDate(); return `${year}-${month < 10 ? `0${month}` : month}-${ day < 10 ? `0${day}` : day }`; } /** * Converts a date in the format 'YYYY-mm-dd...' into a proper date, because * new Date() does not do that correctly. The date can be as complete or incomplete * as necessary (aka, '2015', '2015-10', '2015-10-01'). * It will not work for dates that have times included in them. * * If an array is provided, only the first entry is used. * * @param {String} input String date form like '2015-10-01' * @return {Date} parsed date */ export function decodeDate( input: EncodedValue ): Date | null | undefined { const dateString = getEncodedValue(input); if (dateString == null) return dateString; const parts = dateString.split('-') as any; // may only be a year so won't even have a month if (parts[1] != null) { parts[1] -= 1; // Note: months are 0-based } else { // just a year, set the month and day to the first parts[1] = 0; parts[2] = 1; } const decoded = new Date(...(parts as [number, number, number])); if (isNaN(decoded.getTime())) { return null; } return decoded; } /** * Encodes a date as a string in ISO 8601 ("2019-05-28T10:58:40Z") format. * * @param {Date} date * @return {String} the encoded date */ export function encodeDateTime( date: Date | null | undefined ): string | null | undefined { if (date == null) { return date; } return date.toISOString(); } /** * Converts a date in the https://en.wikipedia.org/wiki/ISO_8601 format. * For allowed inputs see specs: * - https://tools.ietf.org/html/rfc2822#page-14 * - http://www.ecma-international.org/ecma-262/5.1/#sec-15.9.1.15 * * If an array is provided, only the first entry is used. * * @param {String} input String date form like '1995-12-17T03:24:00' * @return {Date} parsed date */ export function decodeDateTime( input: EncodedValue ): Date | null | undefined { const dateString = getEncodedValue(input); if (dateString == null) return dateString; const decoded = new Date(dateString); if (isNaN(decoded.getTime())) { return null; } return decoded; } /** * Encodes a boolean as a string. true -> "1", false -> "0". * * @param {Boolean} bool * @return {String} the encoded boolean */ export function encodeBoolean( bool: boolean | null | undefined ): string | null | undefined { if (bool == null) { return bool; } return bool ? '1' : '0'; } /** * Decodes a boolean from a string. "1" -> true, "0" -> false. * Everything else maps to undefined. * * If an array is provided, only the first entry is used. * * @param {String} input the encoded boolean string * @return {Boolean} the boolean value */ export function decodeBoolean( input: EncodedValue ): boolean | null | undefined { const boolStr = getEncodedValue(input); if (boolStr == null) return boolStr; if (boolStr === '1') { return true; } else if (boolStr === '0') { return false; } return null; } /** * Encodes a number as a string. * * @param {Number} num * @return {String} the encoded number */ export function encodeNumber( num: number | null | undefined ): string | null | undefined { if (num == null) { return num; } return String(num); } /** * Decodes a number from a string. If the number is invalid, * it returns undefined. * * If an array is provided, only the first entry is used. * * @param {String} input the encoded number string * @return {Number} the number value */ export function decodeNumber( input: EncodedValue ): number | null | undefined { const numStr = getEncodedValue(input); if (numStr == null) return numStr; if (numStr === '') return null; const result = +numStr; return result; } /** * Encodes a string while safely handling null and undefined values. * * @param {String} str a string to encode * @return {String} the encoded string */ export function encodeString( str: EncodedValue ): string | null | undefined { if (str == null) { return str; } return String(str); } /** * Decodes a string while safely handling null and undefined values. * * If an array is provided, only the first entry is used. * * @param {String} input the encoded string * @return {String} the string value */ export function decodeString( input: EncodedValue ): string | null | undefined { const str = getEncodedValue(input, true); if (str == null) return str; return String(str); } /** * Decodes an enum value while safely handling null and undefined values. * * If an array is provided, only the first entry is used. * * @param {String} input the encoded string * @param {String[]} enumValues allowed enum values * @return {String} the string value from enumValues */ export function decodeEnum( input: EncodedValue, enumValues: T[] ): T | null | undefined { const str = decodeString(input); if (str == null) return str; return enumValues.includes(str as any) ? (str as T) : undefined; } /** * Decodes an enum value from arrays while safely handling null and undefined values. * * @template T * @param {String} input the encoded string * @param {T[]} enumValues allowed enum values * @return {T[]} the string value from enumValues */ export function decodeArrayEnum( input: EncodedValue, enumValues: T[] ): T[] | null | undefined { const arr = decodeArray(input); if (arr == null) return arr; if (!arr.length) return undefined; return arr.every((str) => str != null && enumValues.includes(str as T)) ? (arr as T[]) : undefined; } /** * Decodes an enum value from arrays while safely handling null and undefined values. * * @template T * @param {String} input the encoded string * @param {T[]} enumValues allowed enum values * @param entrySeparator The array as a string with elements joined by the * entry separator * @return {T[]} the string value from enumValues */ export function decodeDelimitedArrayEnum( input: EncodedValue, enumValues: T[], entrySeparator = '_' ): T[] | null | undefined { if (input != null && Array.isArray(input) && !input.length) return undefined; const arr = decodeDelimitedArray(input, entrySeparator); return decodeArrayEnum(arr, enumValues); } /** * Encodes anything as a JSON string. * * @param {Any} any The thing to be encoded * @return {String} The JSON string representation of any */ export function encodeJson( any: any | null | undefined ): string | null | undefined { if (any == null) { return any; } return JSON.stringify(any); } /** * Decodes a JSON string into javascript * * If an array is provided, only the first entry is used. * * @param {String} input The JSON string representation * @return {Any} The javascript representation */ export function decodeJson( input: EncodedValue ): any | null | undefined { const jsonStr = getEncodedValue(input); if (jsonStr == null) return jsonStr; let result = null; try { result = JSON.parse(jsonStr); } catch (e) { /* ignore errors, returning undefined */ } return result; } /** * Encodes an array as a JSON string. * * @param {Array} array The array to be encoded * @return {String[]} The array of strings to be put in the URL * as repeated query parameters */ export function encodeArray( array: (string | null)[] | null | undefined ): (string | null)[] | null | undefined { if (array == null) { return array; } return array; } /** * Decodes an array or singular value and returns it as an array * or undefined if falsy. Filters out undefined values. * * @param {String | Array} input The input value * @return {Array} The javascript representation */ export function decodeArray( input: EncodedValue ): (string | null)[] | null | undefined { const arr = getEncodedValueArray(input); if (arr == null) return arr; return arr; } /** * Encodes a numeric array as a JSON string. * * @param {Array} array The array to be encoded * @return {String[]} The array of strings to be put in the URL * as repeated query parameters */ export function encodeNumericArray( array: (number | null)[] | null | undefined ): (string | null)[] | null | undefined { if (array == null) { return array; } return array.map(String); } /** * Decodes an array or singular value and returns it as an array * or undefined if falsy. Filters out undefined and NaN values. * * @param {String | Array} input The input value * @return {Array} The javascript representation */ export function decodeNumericArray( input: EncodedValue ): (number | null)[] | null | undefined { const arr = decodeArray(input); if (arr == null) return arr; return arr.map((d) => (d === '' || d == null ? null : +d)); } /** * Encodes an array as a delimited string. For example, * ['a', 'b'] -> 'a_b' with entrySeparator='_' * * @param array The array to be encoded * @param entrySeparator The string used to delimit entries * @return The array as a string with elements joined by the * entry separator */ export function encodeDelimitedArray( array: (string | null)[] | null | undefined, entrySeparator = '_' ): string | null | undefined { if (array == null) { return array; } return array.join(entrySeparator); } /** * Decodes a delimited string into javascript array. For example, * 'a_b' -> ['a', 'b'] with entrySeparator='_' * * If an array is provided as input, only the first entry is used. * * @param {String} input The JSON string representation * @param entrySeparator The array as a string with elements joined by the * entry separator * @return {Array} The javascript representation */ export function decodeDelimitedArray( input: EncodedValue, entrySeparator = '_' ): (string | null)[] | null | undefined { const arrayStr = getEncodedValue(input, true); if (arrayStr == null) return arrayStr; if (arrayStr === '') return []; return arrayStr.split(entrySeparator); } /** * Encodes a numeric array as a delimited string. (alias of encodeDelimitedArray) * For example, [1, 2] -> '1_2' with entrySeparator='_' * * @param {Array} array The array to be encoded * @return {String} The JSON string representation of array */ export const encodeDelimitedNumericArray = encodeDelimitedArray as ( array: (number | null)[] | null | undefined, entrySeparator?: string ) => string | null | undefined; /** * Decodes a delimited string into javascript array where all entries are numbers * For example, '1_2' -> [1, 2] with entrySeparator='_' * * If an array is provided as input, only the first entry is used. * * @param {String} jsonStr The JSON string representation * @return {Array} The javascript representation */ export function decodeDelimitedNumericArray( arrayStr: EncodedValue, entrySeparator = '_' ): (number | null)[] | null | undefined { const decoded = decodeDelimitedArray(arrayStr, entrySeparator); if (decoded == null) return decoded; return decoded.map((d) => (d === '' || d == null ? null : +d)); } /** * Encode simple objects as readable strings. Works only for simple, * flat objects where values are numbers, strings. * * For example { foo: bar, boo: baz } -> "foo-bar_boo-baz" * * @param {Object} object The object to encode * @param {String} keyValSeparator="-" The separator between keys and values * @param {String} entrySeparator="_" The separator between entries * @return {String} The encoded object */ export function encodeObject( obj: { [key: string]: string | null | number | undefined } | null | undefined, keyValSeparator = '-', entrySeparator = '_' ): string | null | undefined { if (obj == null) return obj; // null or undefined if (!Object.keys(obj).length) return ''; // {} case return Object.keys(obj) .map((key) => `${key}${keyValSeparator}${obj[key]}`) .join(entrySeparator); } /** * Decodes a simple object to javascript. Currently works only for simple, * flat objects where values are strings. * * For example "foo-bar_boo-baz" -> { foo: bar, boo: baz } * * If an array is provided as input, only the first entry is used. * * @param {String} input The object string to decode * @param {String} keyValSeparator="-" The separator between keys and values * @param {String} entrySeparator="_" The separator between entries * @return {Object} The javascript object */ export function decodeObject( input: EncodedValue, keyValSeparator = '-', entrySeparator = '_' ): { [key: string]: string } | null | undefined { const objStr = getEncodedValue(input, true); if (objStr == null) return objStr; if (objStr === '') return {}; const obj: { [key: string]: string } = {}; const keyValSeparatorRegExp = new RegExp(`${keyValSeparator}(.*)`); objStr.split(entrySeparator).forEach((entryStr) => { const [key, value] = entryStr.split(keyValSeparatorRegExp); obj[key] = value; }); return obj; } /** * Encode simple objects as readable strings. Alias of encodeObject. * * For example { foo: 123, boo: 521 } -> "foo-123_boo-521" * * @param {Object} object The object to encode * @param {String} keyValSeparator="-" The separator between keys and values * @param {String} entrySeparator="_" The separator between entries * @return {String} The encoded object */ export const encodeNumericObject = encodeObject as ( obj: { [key: string]: number | null | undefined } | null | undefined, keyValSeparator?: string, entrySeparator?: string ) => string | null | undefined; /** * Decodes a simple object to javascript where all values are numbers. * Currently works only for simple, flat objects. * * For example "foo-123_boo-521" -> { foo: 123, boo: 521 } * * If an array is provided as input, only the first entry is used. * * @param {String} input The object string to decode * @param {String} keyValSeparator="-" The separator between keys and values * @param {String} entrySeparator="_" The separator between entries * @return {Object} The javascript object */ export function decodeNumericObject( input: EncodedValue, keyValSeparator = '-', entrySeparator = '_' ): { [key: string]: number | null | undefined } | null | undefined { const decoded: { [key: string]: string } | null | undefined = decodeObject( input, keyValSeparator, entrySeparator ); if (decoded == null) return decoded; // convert to numbers const decodedNumberObj: { [key: string]: number | null | undefined } = {}; for (const key of Object.keys(decoded)) { decodedNumberObj[key] = decodeNumber(decoded[key]); } return decodedNumberObj; }