From cc3f33cdb2bb1c40eee78732169d2c133f62209b Mon Sep 17 00:00:00 2001 From: Kir_Antipov Date: Tue, 24 Jan 2023 11:56:32 +0000 Subject: [PATCH] Made a module focused on conversion --- src/utils/convert.ts | 527 +++++++++++++++++++++++++++++++ tests/unit/utils/convert.spec.ts | 509 +++++++++++++++++++++++++++++ 2 files changed, 1036 insertions(+) create mode 100644 src/utils/convert.ts create mode 100644 tests/unit/utils/convert.spec.ts diff --git a/src/utils/convert.ts b/src/utils/convert.ts new file mode 100644 index 0000000..cfee667 --- /dev/null +++ b/src/utils/convert.ts @@ -0,0 +1,527 @@ +import { stringEquals } from "@/utils/string-utils"; +import { TypeOfResult, NamedType } from "@/utils/types/type-of"; +import { $i } from "@/utils/collections/iterable"; +import { getAllNames } from "@/utils/reflection/object-reflector"; + +/** + * Represents a function that converts a value to some target type. + * + * @template TInput - The input data type. + * @template UTarget - The target data type. + * @template ROutput - The output data type. + */ +export interface Converter { + /** + * Converts a value to some target type. + * + * @param value - The value to convert. + * @param target - The target type of the conversion. + * + * @returns The converted value. + */ + (value: TInput, target: UTarget): ROutput; +} + +/** + * Returns whether the given `obj` is `null`, `undefined`, or `NaN`. + * + * @param obj - The object to check. + * + * @returns `true` if the `obj` is `null`, `undefined`, or `NaN`; otherwise, `false`. + */ +function isInvalid(obj: unknown): boolean { + return obj === null || obj === undefined || typeof obj === "number" && isNaN(obj); +} + +/** + * Always returns `undefined`, ignoring the input value. + * + * @param _obj - The input value to ignore. + * + * @returns `undefined`. + */ +export function toUndefined(_obj: unknown): undefined { + return undefined; +} + +/** + * Converts the given `obj` to a string. + * + * @param obj - The object to convert. + * + * @returns The string representation of `obj`, or `undefined` if the input is `null`, `undefined`, or `NaN`. + */ +export function toString(obj: unknown): string { + return isInvalid(obj) ? undefined : String(obj); +} + +/** + * Converts an input value to a boolean value. + * + * @param obj - The object to convert. + * + * @returns The converted boolean value, or `undefined` if the input value cannot be converted to boolean. + */ +export function toBoolean(obj: unknown): boolean { + if (isInvalid(obj)) { + return undefined; + } + + switch (typeof obj) { + case "boolean": + return !!obj; + + case "number": + return obj !== 0; + + case "string": + if (stringEquals("true", obj, { ignoreCase: true })) { + return true; + } + if (stringEquals("false", obj, { ignoreCase: true })) { + return false; + } + break; + } + return undefined; +} + +/** + * Converts an input value to a number type. + * + * @param obj - The input value to be converted. + * @param parser - A function to parse the input value. + * + * @returns The converted number value, or `undefined` if the input value cannot be converted to a number type. + */ +function toNumber(obj: unknown, parser: (value: string | number) => number): number { + if (isInvalid(obj)) { + return undefined; + } + + switch (typeof obj) { + case "number": + return parser(obj); + + case "boolean": + return obj ? 1 : 0; + + case "string": + const parsedNumber = parser(obj); + return isNaN(parsedNumber) ? undefined : parsedNumber; + + case "object": + if (obj instanceof Date && !isNaN(obj.getTime())) { + return obj.getTime(); + } + break; + } + return undefined; +} + +/** + * Converts an input value to an integer number. + * + * @param obj - The input value to be converted. + * + * @returns The converted integer number value, or `undefined` if the input value cannot be converted to an integer number type. + */ +export function toInteger(obj: unknown): number { + return toNumber(obj, parseInt); +} + +/** + * Converts an input value to a floating-point number. + * + * @param obj - The input value to be converted. + * + * @returns The converted floating-point number value, or `undefined` if the input value cannot be converted to a floating-point number type. + */ +export function toFloat(obj: unknown): number { + return toNumber(obj, parseFloat); +} + +/** + * Converts a value to a {@link Date}. + * + * @param obj - The value to convert. + * + * @returns The converted {@link Date}, or `undefined` if the value is invalid. + */ +export function toDate(obj: unknown): Date { + if (isInvalid(obj)) { + return undefined; + } + + switch (typeof obj) { + case "object": + if (obj instanceof Date && !isNaN(obj.getTime())) { + return obj; + } + break; + + case "string": + case "number": + const date = new Date(obj); + return isNaN(date.getTime()) ? undefined : date; + } + return undefined; +} + +/** + * The regular expression used to parse a string representation of a regex into its pattern and flags parts. + */ +const REGEX_PARSER_REGEX = /\/(?.*)\/(?[a-z]*)/; + +/** + * Converts a value to a {@link RegExp}. + * + * @param obj - The value to convert. + * + * @returns A {@link RegExp} representing the given `obj`, or `undefined` if the input is invalid or cannot be converted to a regex. + */ +export function toRegExp(obj: unknown): RegExp | undefined { + if (obj instanceof RegExp) { + return obj; + } + + if (typeof obj !== "string") { + return undefined; + } + + const match = obj.match(REGEX_PARSER_REGEX); + if (!match) { + return undefined; + } + + try { + return new RegExp(match.groups.pattern, match.groups.flags); + } catch { + return undefined; + } +} + +/** + * A type alias for `globalThis`, the global object in a runtime environment. + */ +type GlobalThis = typeof globalThis; + +/** + * A constructor function that creates instances of a given type. + */ +type Constructor = new (...args: unknown[]) => T; + +/** + * Represents the return type of a constructor function. + * + * @template T - The constructor function to extract the return type from. + */ +type ConstructorReturnType = T extends Constructor ? U : never; + +/** + * Represents a member of the `globalThis` object that can be constructed. + * + * @template T - The name of the member of the `globalThis` object to check for constructibility. + */ +type ConstructibleGlobalThisMember = GlobalThis[T] extends Constructor ? T : never; + +/** + * The prefixes that indicate a method is a conversion method. + */ +const CONVERT_METHOD_PREFIXES = ["convert", "from"] as const; + +/** + * A function that converts an unknown value to a typed value of a given type. + */ +type Convert = (obj: unknown) => T; + +/** + * Obtains the type of a method on a class or object that performs a conversion using a {@link Convert} function. + */ +type ConvertMethod = { + [K in keyof T]: T[K] extends Convert + ? K extends `${typeof CONVERT_METHOD_PREFIXES[number]}${string}` + ? T[K] + : never + : never; +}[keyof T]; + +/** + * A type that is convertible if it has at least one method that performs a conversion using a {@link Convert} function. + */ +type Convertible = ConvertMethod extends never ? never : T; + +/** + * Represents a member of the `globalThis` object that is "convertible" based on whether it has at least one method that performs conversion using a {@link Convert} function. + * + * @template T - The name of the member of the `globalThis` object to check for convertibility. + */ +type ConvertibleGlobalThisMember = ConvertMethod extends never ? never : T; + +/** + * The prefixes that indicate a method is a parsing method. + */ +const PARSE_METHOD_PREFIXES = ["parse"] as const; + +/** + * A function that parses a string and returns a value of a given type. + */ +type Parse = (pattern: string) => T; + +/** + * Obtains the type of a method on a class or object that performs parsing using a {@link Parse} function. + */ +type ParseMethod = { + [K in keyof T]: T[K] extends Parse + ? K extends `${typeof PARSE_METHOD_PREFIXES[number]}${string}` + ? T[K] + : never + : never; +}[keyof T]; + +/** + * Represents a type `T` which is considered "parsable" if it has at least one method that performs parsing using a {@link Parse} function. + */ +type Parsable = ParseMethod extends never ? never : T; + +/** + * Represents a member of the `globalThis` object that is "parsable" based on whether it has at least one method that performs parsing using a {@link Parse} function. + * + * @template T - The name of the member of the `globalThis` object to check for parsability. + */ +type ParsableGlobalThisMember = ParseMethod extends never ? never : T; + +/** + * Retrieves a `Converter` function from the given object, if one is defined. + * + * @param obj - The object to retrieve the `Converter` function from. + * @returns A `Converter` function that can convert an unknown value to the target type `T`, or `undefined` if none was found. + */ +function getConverter(obj: unknown): Convert | undefined { + // Attempt to retrieve a `Converter` function from the object using the conversion method prefixes. + const converter = getParseLikeFunction(obj, CONVERT_METHOD_PREFIXES) as Convert; + + // If a `Converter` function was found, return it. + if (converter) { + return converter; + } + + // Otherwise, attempt to retrieve a `Parser` function from the object and create a `Converter` function that uses it. + const parser = getParser(obj); + if (parser) { + return x => typeof x === "string" ? parser(x) : undefined; + } + + // If neither a `Converter` nor a `Parser` function was found, return undefined. + return undefined; +} + +/** + * Retrieves a `Parser` function from the given object, if one is defined. + * + * @param obj - The object to retrieve the `Parser` function from. + * @returns A `Parser` function that can parse a string to the target type `T`, or `undefined` if none was found. + */ +function getParser(obj: unknown): Parse | undefined { + // Attempt to retrieve a `Parser` function from the object using the parsing method prefixes. + return getParseLikeFunction(obj, PARSE_METHOD_PREFIXES) as Parse; +} + +/** + * Attempts to retrieve a parsing method from the given object using the specified prefixes. + * + * @param obj - The object to retrieve the method from. + * @param prefixes - The list of method name prefixes to search for. + * + * @returns The first matching parse-like function that was found, or `undefined` if none were found. + */ +function getParseLikeFunction(obj: unknown, prefixes: readonly string[]): (obj: unknown) => unknown { + // If the object is invalid, return undefined. + if (isInvalid(obj)) { + return undefined; + } + + // Find all method names on the object that start with one of the specified prefixes. + const propertyNames = getAllNames(obj); + const parseMethodNames = $i(propertyNames).filter(x => typeof obj[x] === "function" && prefixes.some(p => x.startsWith(p))); + + // Determine the first parse-like method name by sorting them based on prefix precedence and taking the first result. + const firstParseMethodName = $i(parseMethodNames).min( + (a, b) => prefixes.findIndex(p => a.startsWith(p)) - prefixes.findIndex(p => b.startsWith(p)) + ); + + // If no parse-like method names were found, return undefined. + if (!firstParseMethodName) { + return undefined; + } + + // Return a function that invokes the first parse-like method with the specified input. + return x => obj[firstParseMethodName](x); +} + +/** + * Map of known constructors and their corresponding converters. + */ +const KNOWN_CONSTRUCTORS = new Map, Convert>([ + [String, toString], + [Number, toFloat], + [Boolean, toBoolean], + [Date, toDate], + [RegExp, toRegExp], +]); + +/** + * Map of known types and their corresponding converters. + */ +const KNOWN_TYPES = new Map>([ + ["string", toString], + ["number", toFloat], + ["boolean", toBoolean], + ["undefined", toUndefined], +]); + +/** + * Converts a given object to the target type. + * + * @template T - The type of the returned value. + * @param obj - Value to be converted. + * @param type - Name of the type to convert the value to. + * + * @returns The converted value, or `undefined` if the conversion fails. + */ +export function toType(obj: unknown, type: T): NamedType; + +/** + * Converts a given object to the target type. + * + * @template T - A key of the `globalThis` object that represents the type to convert to, which must be a constructible type. + * + * @param obj - The unknown value to convert. + * @param type - The name of the constructor function corresponding to the target type `T`. + * + * @returns The converted value, or `undefined` if the conversion fails. + */ +export function toType(obj: unknown, type: T & ConstructibleGlobalThisMember): ConstructorReturnType; + +/** + * Converts a given object to the target type. + * + * @template T - A key of the `globalThis` object that represents the type to convert to, which must have at least one method with a signature that matches the {@link ConvertMethod} type. + * + * @param obj - The unknown value to convert. + * @param type - The name of the convertible method corresponding to the target type `T`. + * + * @returns The converted value, or `undefined` if the conversion fails. + */ +export function toType(obj: unknown, type: T & ConvertibleGlobalThisMember): ReturnType>; + +/** + * Parses the given string as the target type. + * + * @template T - A key of the `globalThis` object that represents the type to convert to, which must have at least one method with a signature that matches the {@link ParseMethod} type. + * + * @param obj - The string to parse and convert. + * @param type - The name of the parsing method corresponding to the target type `T`. + * + * @returns The parsed value, or `undefined` if the parsing fails. + */ +export function toType(obj: string, type: T & ParsableGlobalThisMember): ReturnType>; + +/** + * Converts a given object to the target type. + * + * @template T - The type of the returned value. + * @param obj - Value to be converted. + * @param convertible - Convertible type to convert the value to. + * + * @returns The converted value, or `undefined` if the conversion fails. + */ +export function toType(obj: unknown, convertible: T & Convertible): ReturnType>; + +/** + * Parses the given string as the target type. + * + * @template T - The type of the returned value. + * @param s - String to be parsed. + * @param parsable - Parsable type to convert the value to. + * + * @returns The parsed value, or `undefined` if the parsing fails. + */ +export function toType(s: string, parsable: T & Parsable): ReturnType>; + +/** + * Converts a given object to the target type. + * + * @template T - The type of the returned value. + * @param obj - Value to be converted. + * @param constructor - Constructor of the type to convert the value to. + * + * @returns The converted value, or `undefined` if the conversion fails. + */ +export function toType(obj: unknown, constructor: Constructor): T; + +/** + * A function that converts an unknown object to an unknown target type. + * + * @template T - The type to convert the value to. + * @param obj - The value to convert. + * @param target - The type or constructor function to use for the conversion. + * + * @returns The converted value of type `T`, or `undefined` if the conversion fails. + */ +export function toType(obj: unknown, target: unknown): T; + +/** + * Converts an object to the specified target type. + * + * @param obj - The object to convert. + * @param target - The target type to convert to. + * + * @returns An object of the specified target type, or `undefined` if the conversion failed. + */ +export function toType(obj: unknown, target: unknown): unknown { + // If the input object is invalid, return undefined. + if (isInvalid(obj)) { + return undefined; + } + + if (typeof target === "string") { + // If the target is a string representing a known type, use the corresponding conversion function. + const knownConverter = KNOWN_TYPES.get(target as TypeOfResult); + if (knownConverter) { + return knownConverter(obj); + } + + // If the target is a key of the `globalThis` object, convert the input to its type. + const globalThisMember = globalThis[target]; + if (globalThisMember) { + return toType(obj, globalThisMember); + } + + return undefined; + } + + // If the target is a known constructor function, use its corresponding conversion function. + if (typeof target === "function" && KNOWN_CONSTRUCTORS.has(target as Constructor)) { + const knownConverter = KNOWN_CONSTRUCTORS.get(target as Constructor); + return knownConverter(obj); + } + + try { + // Attempt to retrieve a converter function from the target type. + const converter = getConverter(target); + + // If the converter function was found, use it to convert the input object. + if (converter !== undefined) { + const converted = converter(obj); + return isInvalid(converted) ? undefined : converted; + } + + // If no converter function was found, assume that target is a constructor, + // since we've exhausted every over possibility. + return new (target as Constructor)(obj); + } catch { + // If an error occurs during conversion, return undefined. + return undefined; + } +} diff --git a/tests/unit/utils/convert.spec.ts b/tests/unit/utils/convert.spec.ts new file mode 100644 index 0000000..11f865b --- /dev/null +++ b/tests/unit/utils/convert.spec.ts @@ -0,0 +1,509 @@ +import { + toUndefined, + toString, + toBoolean, + toInteger, + toFloat, + toDate, + toRegExp, + toType, +} from "@/utils/convert"; + +describe("toUndefined", () => { + test("always returns undefined", () => { + expect(toUndefined("something")).toBeUndefined(); + expect(toUndefined(123)).toBeUndefined(); + expect(toUndefined(null)).toBeUndefined(); + expect(toUndefined(undefined)).toBeUndefined(); + }); +}); + +describe("toString", () => { + test("returns strings as is", () => { + expect(toString("test")).toBe("test"); + }); + + test("converts valid values to strings", () => { + expect(toString(123)).toBe("123"); + expect(toString(true)).toBe("true"); + }); + + test("returns undefined for invalid values", () => { + expect(toString(null)).toBeUndefined(); + expect(toString(undefined)).toBeUndefined(); + expect(toString(NaN)).toBeUndefined(); + }); +}); + +describe("toBoolean", () => { + test("returns booleans as is", () => { + expect(toBoolean(false)).toBe(false); + expect(toBoolean(true)).toBe(true); + }); + + test("converts 'true' and 'false' strings to booleans ignoring their case", () => { + expect(toBoolean("true")).toBe(true); + expect(toBoolean("false")).toBe(false); + + expect(toBoolean("TRUE")).toBe(true); + expect(toBoolean("FALSE")).toBe(false); + + expect(toBoolean("True")).toBe(true); + expect(toBoolean("False")).toBe(false); + }); + + test("converts numbers to booleans", () => { + expect(toBoolean(0)).toBe(false); + expect(toBoolean(1)).toBe(true); + expect(toBoolean(2)).toBe(true); + }); + + test("returns undefined for invalid values", () => { + expect(toBoolean("invalid")).toBeUndefined(); + expect(toBoolean(null)).toBeUndefined(); + expect(toBoolean(undefined)).toBeUndefined(); + expect(toBoolean(NaN)).toBeUndefined(); + }); +}); + +describe("toInteger", () => { + test("returns integers as is", () => { + expect(toInteger(-1)).toBe(-1); + expect(toInteger(0)).toBe(0); + expect(toInteger(123)).toBe(123); + }); + + test("converts floats to integers", () => { + expect(toInteger(123.456)).toBe(123); + expect(toInteger(true)).toBe(1); + expect(toInteger(false)).toBe(0); + }); + + test("converts dates to integers", () => { + const date = new Date(); + + expect(toInteger(date)).toBe(date.getTime()); + }); + + test("converts valid strings to integers", () => { + expect(toInteger("-1")).toBe(-1); + expect(toInteger("-1.23")).toBe(-1); + expect(toInteger("123")).toBe(123); + expect(toInteger("123.456")).toBe(123); + }); + + test("converts booleans to integers", () => { + expect(toInteger(true)).toBe(1); + expect(toInteger(false)).toBe(0); + }); + + test("returns undefined for invalid values", () => { + expect(toInteger("invalid")).toBeUndefined(); + expect(toInteger(null)).toBeUndefined(); + expect(toInteger(undefined)).toBeUndefined(); + expect(toInteger(NaN)).toBeUndefined(); + }); +}); + +describe("toFloat", () => { + test("returns numbers as is", () => { + expect(toFloat(-1)).toBe(-1); + expect(toFloat(0)).toBe(0); + expect(toFloat(123)).toBe(123); + expect(toFloat(123.456)).toBe(123.456); + }); + + test("converts dates to floats", () => { + const date = new Date(); + + expect(toFloat(date)).toBe(date.valueOf()); + }); + + test("converts valid strings to floats", () => { + expect(toFloat("-1")).toBe(-1); + expect(toFloat("-1.23")).toBe(-1.23); + expect(toFloat("123")).toBe(123); + expect(toFloat("123.456")).toBe(123.456); + }); + + test("converts booleans to floats", () => { + expect(toFloat(true)).toBe(1); + expect(toFloat(false)).toBe(0); + }); + + test("returns undefined for invalid values", () => { + expect(toFloat("invalid")).toBeUndefined(); + expect(toFloat(null)).toBeUndefined(); + expect(toFloat(undefined)).toBeUndefined(); + expect(toFloat(NaN)).toBeUndefined(); + }); +}); + +describe("toDate", () => { + test("returns dates as is", () => { + const date = new Date("1856-07-10T12:34:56.000Z"); + + expect(toDate(date)).toEqual(date); + }); + + test("converts valid values to Date objects", () => { + const date = new Date("1856-07-10T12:34:56.000Z"); + + expect(toDate(date.toISOString())).toEqual(date); + expect(toDate(date.getTime())).toEqual(date); + }); + + test("returns undefined for invalid values", () => { + expect(toDate(new Date("not a date"))).toBeUndefined(); + expect(toDate("invalid")).toBeUndefined(); + expect(toDate(null)).toBeUndefined(); + expect(toDate(undefined)).toBeUndefined(); + expect(toDate(NaN)).toBeUndefined(); + }); +}); + +describe("toRegExp", () => { + test("returns RegExps as is", () => { + const regex = /test/i; + + expect(toRegExp(regex)).toEqual(regex); + }); + + test("converts valid string values to RegExp objects", () => { + const regex = /test/i; + + expect(toRegExp(regex.toString())).toEqual(regex); + }); + + test("returns undefined for invalid values", () => { + expect(toDate("invalid")).toBeUndefined(); + expect(toDate("/invalid")).toBeUndefined(); + expect(toDate(null)).toBeUndefined(); + expect(toDate(undefined)).toBeUndefined(); + expect(toDate(NaN)).toBeUndefined(); + }); +}); + +describe("toType", () => { + describe("via 'typeof' type name", () => { + test("converts an object to a string", () => { + expect(toType("string", "string")).toBe("string"); + expect(toType(123, "string")).toBe("123"); + expect(toType(true, "string")).toBe("true"); + expect(toType(false, "string")).toBe("false"); + expect(toType(new Date("1856-07-10T12:34:56.000Z"), "string")).toBe(new Date("1856-07-10T12:34:56.000Z").toString()); + expect(toType(/\d/, "string")).toBe(String(/\d/)); + }); + + test("converts an object to a number", () => { + expect(toType(123, "number")).toBe(123); + expect(toType(123.456, "number")).toBe(123.456); + expect(toType("123", "number")).toBe(123); + expect(toType("123.456", "number")).toBe(123.456); + expect(toType(false, "number")).toBe(0); + expect(toType(true, "number")).toBe(1); + expect(toType(new Date("1856-07-10T12:34:56.000Z"), "number")).toBe(new Date("1856-07-10T12:34:56.000Z").getTime()); + }); + + test("converts an object to a boolean", () => { + expect(toType(true, "boolean")).toBe(true); + expect(toType(false, "boolean")).toBe(false); + expect(toType(1, "boolean")).toBe(true); + expect(toType(0, "boolean")).toBe(false); + expect(toType(123.456, "boolean")).toBe(true); + expect(toType("true", "boolean")).toBe(true); + expect(toType("false", "boolean")).toBe(false); + expect(toType("TRUE", "boolean")).toBe(true); + expect(toType("FALSE", "boolean")).toBe(false); + expect(toType("True", "boolean")).toBe(true); + expect(toType("False", "boolean")).toBe(false); + }); + + test("converts an object to undefined", () => { + expect(toType(true, "undefined")).toBeUndefined(); + expect(toType(false, "undefined")).toBeUndefined(); + expect(toType("string", "undefined")).toBeUndefined(); + expect(toType({}, "undefined")).toBeUndefined(); + expect(toType(0, "undefined")).toBeUndefined(); + expect(toType(1, "undefined")).toBeUndefined(); + expect(toType(new Date(), "undefined")).toBeUndefined(); + expect(toType(/\d/, "undefined")).toBeUndefined(); + }); + + test("returns undefined when conversion is not possible", () => { + expect(toType(undefined, "string")).toBeUndefined(); + expect(toType(undefined, "number")).toBeUndefined(); + expect(toType(undefined, "boolean")).toBeUndefined(); + expect(toType(null, "string")).toBeUndefined(); + expect(toType(null, "number")).toBeUndefined(); + expect(toType(null, "boolean")).toBeUndefined(); + expect(toType(NaN, "string")).toBeUndefined(); + expect(toType(NaN, "number")).toBeUndefined(); + expect(toType(NaN, "boolean")).toBeUndefined(); + + expect(toType("abc", "number")).toBeUndefined(); + expect(toType("abc", "boolean")).toBeUndefined(); + expect(toType("123", "date")).toBeUndefined(); + expect(toType("123", "void")).toBeUndefined(); + }); + }); + + describe("via constructor", () => { + test("converts an object to a string", () => { + expect(toType("string", String)).toBe("string"); + expect(toType(123, String)).toBe("123"); + expect(toType(true, String)).toBe("true"); + expect(toType(false, String)).toBe("false"); + expect(toType(new Date("1856-07-10T12:34:56.000Z"), String)).toBe(new Date("1856-07-10T12:34:56.000Z").toString()); + expect(toType(/\d/, String)).toBe(String(/\d/)); + }); + + test("converts an object to a number", () => { + expect(toType(123, Number)).toBe(123); + expect(toType(123.456, Number)).toBe(123.456); + expect(toType("123", Number)).toBe(123); + expect(toType("123.456", Number)).toBe(123.456); + expect(toType(false, Number)).toBe(0); + expect(toType(true, Number)).toBe(1); + expect(toType(new Date("1856-07-10T12:34:56.000Z"), Number)).toBe(new Date("1856-07-10T12:34:56.000Z").getTime()); + }); + + test("converts an object to a boolean", () => { + expect(toType(true, Boolean)).toBe(true); + expect(toType(false, Boolean)).toBe(false); + expect(toType(1, Boolean)).toBe(true); + expect(toType(0, Boolean)).toBe(false); + expect(toType(123.456, Boolean)).toBe(true); + expect(toType("true", Boolean)).toBe(true); + expect(toType("false", Boolean)).toBe(false); + expect(toType("TRUE", Boolean)).toBe(true); + expect(toType("FALSE", Boolean)).toBe(false); + expect(toType("True", Boolean)).toBe(true); + expect(toType("False", Boolean)).toBe(false); + }); + + test("converts an object to a Date", () => { + const date = new Date("1856-07-10T12:34:56.000Z"); + + expect(toType(date, Date)).toEqual(date); + expect(toType(date.toISOString(), Date)).toEqual(date); + expect(toType(date.getTime(), Date)).toEqual(date); + }); + + test("converts an object to a RegExp", () => { + const regex = /^a+$/im; + + expect(toType(regex, RegExp)).toEqual(regex); + expect(toType(String(regex), RegExp)).toEqual(regex); + }); + + test("returns undefined when conversion is not possible", () => { + expect(toType(undefined, String)).toBeUndefined(); + expect(toType(undefined, Number)).toBeUndefined(); + expect(toType(undefined, Boolean)).toBeUndefined(); + expect(toType(undefined, Date)).toBeUndefined(); + expect(toType(undefined, RegExp)).toBeUndefined(); + expect(toType(null, String)).toBeUndefined(); + expect(toType(null, Number)).toBeUndefined(); + expect(toType(null, Boolean)).toBeUndefined(); + expect(toType(null, Date)).toBeUndefined(); + expect(toType(null, RegExp)).toBeUndefined(); + expect(toType(NaN, String)).toBeUndefined(); + expect(toType(NaN, Number)).toBeUndefined(); + expect(toType(NaN, Boolean)).toBeUndefined(); + expect(toType(NaN, Date)).toBeUndefined(); + expect(toType(NaN, RegExp)).toBeUndefined(); + + expect(toType("abc", Number)).toBeUndefined(); + expect(toType("abc", Boolean)).toBeUndefined(); + expect(toType("abc", Date)).toBeUndefined(); + expect(toType("123", RegExp)).toBeUndefined(); + expect(toType("123", undefined)).toBeUndefined(); + }); + }); + + describe("via globalThis type name", () => { + test("converts an object to a string", () => { + expect(toType("string", "String")).toBe("string"); + expect(toType(123, "String")).toBe("123"); + expect(toType(true, "String")).toBe("true"); + expect(toType(false, "String")).toBe("false"); + expect(toType(new Date("1856-07-10T12:34:56.000Z"), "String")).toBe(new Date("1856-07-10T12:34:56.000Z").toString()); + expect(toType(/\d/, "String")).toBe(String(/\d/)); + }); + + test("converts an object to a number", () => { + expect(toType(123, "Number")).toBe(123); + expect(toType(123.456, "Number")).toBe(123.456); + expect(toType("123", "Number")).toBe(123); + expect(toType("123.456", "Number")).toBe(123.456); + expect(toType(false, "Number")).toBe(0); + expect(toType(true, "Number")).toBe(1); + expect(toType(new Date("1856-07-10T12:34:56.000Z"), "Number")).toBe(new Date("1856-07-10T12:34:56.000Z").getTime()); + }); + + test("converts an object to a boolean", () => { + expect(toType(true, "Boolean")).toBe(true); + expect(toType(false, "Boolean")).toBe(false); + expect(toType(1, "Boolean")).toBe(true); + expect(toType(0, "Boolean")).toBe(false); + expect(toType(123.456, "Boolean")).toBe(true); + expect(toType("true", "Boolean")).toBe(true); + expect(toType("false", "Boolean")).toBe(false); + expect(toType("TRUE", "Boolean")).toBe(true); + expect(toType("FALSE", "Boolean")).toBe(false); + expect(toType("True", "Boolean")).toBe(true); + expect(toType("False", "Boolean")).toBe(false); + }); + + test("converts an object to a Date", () => { + const date = new Date("1856-07-10T12:34:56.000Z"); + + expect(toType(date, "Date")).toEqual(date); + expect(toType(date.toISOString(), "Date")).toEqual(date); + expect(toType(date.getTime(), "Date")).toEqual(date); + }); + + test("converts an object to a RegExp", () => { + const regex = /^a+$/im; + + expect(toType(regex, "RegExp")).toEqual(regex); + expect(toType(String(regex), "RegExp")).toEqual(regex); + }); + + test("returns undefined when conversion is not possible", () => { + expect(toType(undefined, "String")).toBeUndefined(); + expect(toType(undefined, "Number")).toBeUndefined(); + expect(toType(undefined, "Boolean")).toBeUndefined(); + expect(toType(undefined, "Date")).toBeUndefined(); + expect(toType(undefined, "RegExp")).toBeUndefined(); + expect(toType(null, "String")).toBeUndefined(); + expect(toType(null, "Number")).toBeUndefined(); + expect(toType(null, "Boolean")).toBeUndefined(); + expect(toType(null, "Date")).toBeUndefined(); + expect(toType(null, "RegExp")).toBeUndefined(); + expect(toType(NaN, "String")).toBeUndefined(); + expect(toType(NaN, "Number")).toBeUndefined(); + expect(toType(NaN, "Boolean")).toBeUndefined(); + expect(toType(NaN, "Date")).toBeUndefined(); + expect(toType(NaN, "RegExp")).toBeUndefined(); + + expect(toType("abc", "Number")).toBeUndefined(); + expect(toType("abc", "Boolean")).toBeUndefined(); + expect(toType("abc", "Date")).toBeUndefined(); + expect(toType("123", "RegExp")).toBeUndefined(); + expect(toType("123", "NotAType")).toBeUndefined(); + }); + }); + + describe("from convertible object", () => { + test("converts a value via the standard 'convert' function", () => { + const convertible = { + convert: jest.fn().mockImplementation(o => String(o)), + }; + + expect(toType(123, convertible)).toBe("123"); + expect(convertible.convert).toBeCalledTimes(1); + expect(convertible.convert).toBeCalledWith(123); + }); + + test("converts a value via a first function that start with 'convert'", () => { + const convertible = { + convertObjectToNumber: jest.fn().mockImplementation(o => String(o)), + }; + + expect(toType(123, convertible)).toBe("123"); + expect(convertible.convertObjectToNumber).toBeCalledTimes(1); + expect(convertible.convertObjectToNumber).toBeCalledWith(123); + }); + + test("returns undefined when conversion is not possible", () => { + expect(toType(123, {})).toBeUndefined(); + expect(toType(123, { notConvertFunction: () => 42 })).toBeUndefined(); + }); + + test("returns undefined instead of throwing", () => { + const throwingConvertible = { + convert: jest.fn().mockImplementation(() => { + throw new Error("Conversion is impossible"); + }), + }; + const anotherThrowingConvertible = { + convertNothing: jest.fn().mockImplementation(() => { + throw new Error("Conversion is impossible"); + }), + }; + + expect(toType(123, throwingConvertible)).toBeUndefined(); + expect(throwingConvertible.convert).toHaveBeenCalledTimes(1); + expect(throwingConvertible.convert).toHaveBeenCalledWith(123); + + expect(toType(123, anotherThrowingConvertible)).toBeUndefined(); + expect(anotherThrowingConvertible.convertNothing).toHaveBeenCalledTimes(1); + expect(anotherThrowingConvertible.convertNothing).toHaveBeenCalledWith(123); + }); + }); + + describe("from parsable object", () => { + test("parses a value via the standard 'parse' function", () => { + const parsable = { + parse: jest.fn().mockImplementation(s => +s), + }; + + expect(toType("123", parsable)).toBe(123); + expect(parsable.parse).toBeCalledTimes(1); + expect(parsable.parse).toBeCalledWith("123"); + }); + + test("parses a value via a first function that start with 'parse'", () => { + const parsable = { + parseStringToNumber: jest.fn().mockImplementation(s => +s), + }; + + expect(toType("123", parsable)).toBe(123); + expect(parsable.parseStringToNumber).toBeCalledTimes(1); + expect(parsable.parseStringToNumber).toBeCalledWith("123"); + }); + + test("returns undefined when the input value is not a string", () => { + const parsable = { + parse: jest.fn().mockImplementation(s => +s), + }; + const anotherParsable = { + parseStringToNumber: jest.fn().mockImplementation(s => +s), + }; + + expect(toType(123, parsable)).toBeUndefined(); + expect(parsable.parse).not.toBeCalled(); + + expect(toType(123, anotherParsable)).toBeUndefined(); + expect(anotherParsable.parseStringToNumber).not.toBeCalled(); + }); + + test("returns undefined when conversion is not possible", () => { + expect(toType("123", {})).toBeUndefined(); + expect(toType("123", { notParseFunction: () => 42 })).toBeUndefined(); + }); + + test("returns undefined instead of throwing", () => { + const throwingParsable = { + parse: jest.fn().mockImplementation(() => { + throw new Error("Parsing is impossible"); + }), + }; + const anotherThrowingParsable = { + parseNothing: jest.fn().mockImplementation(() => { + throw new Error("Parsing is impossible"); + }), + }; + + expect(toType("123", throwingParsable)).toBeUndefined(); + expect(throwingParsable.parse).toHaveBeenCalledTimes(1); + expect(throwingParsable.parse).toHaveBeenCalledWith("123"); + + expect(toType("123", anotherThrowingParsable)).toBeUndefined(); + expect(anotherThrowingParsable.parseNothing).toHaveBeenCalledTimes(1); + expect(anotherThrowingParsable.parseNothing).toHaveBeenCalledWith("123"); + }); + }); +});