diff --git a/CHANGELOG.md b/CHANGELOG.md index d8b2c46..0a0afd3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,65 @@ # deverything +## 1.3.0 + +### Minor Changes + +- randomEmptyValue + +## 1.2.0 + +### Minor Changes + +- Add new helpers +- Remove deprecated + - `pretty` is now `stringify` + - `randomNumericId` is now `incrementalId` + +## 1.1.0 + +### Minor Changes + +- minor fixes and more helpers + +## 1.0.0 + +### Major Changes + +- Breaking changes on args + - percentageChange + - randomInt + - randomPositiveInt + - randomNegativeInt + - randomDate + - randomMaxDate + - checkEnvVars is dropped + - randomPercentage is dropped + - randomPositivePercentage is dropped + +## 0.51.1 + +### Patch Changes + +- setUrlSearchParams fix + +## 0.51.0 + +### Minor Changes + +- startOfDay + +## 0.50.0 + +### Minor Changes + +- dates + +## 0.49.0 + +### Minor Changes + +- isStrictlyBetween + ## 0.48.1 ### Patch Changes diff --git a/README.md b/README.md index ebe5510..1c54e0e 100644 --- a/README.md +++ b/README.md @@ -15,8 +15,8 @@ Contributions always welcome! ### Validators -- `isArray()` -- `isBoolean()` +- `isArray()` +- `isBoolean()` - `isBrowser()` to detect if you are on the browser - `isBuffer()` if it's a buffer - `isClient()` same as isBrowser @@ -26,20 +26,20 @@ Contributions always welcome! - `isEmptyArray()` checks if the array has no items - `isEmptyObject()` checks if the object has no keys - `isFile()` if it's a file -- `isFunction()` +- `isFunction()` - `isJsDate()` if it's a **valid** javascript's Date - - `isFutureDate()` - - `isPastDate()` + - `isFutureDate()` + - `isPastDate()` - `isStringDate()` also checks if the string passed is a **valid** date - `isKey()` is a real key of an object - `isLastIndex()` is the index is the last item of array - `isNotEmptyString()` must have some text, checks for spaces-only - `isNumber()` if the arg is number, and also usable (no infinity) - `isInt()` if it's an integer - - `isEven()` - - `isOdd()` - - `isPositiveInt()` - - `isNegativeInt()` + - `isEven()` + - `isOdd()` + - `isPositiveInt()` + - `isNegativeInt()` - `isNumeric()` if string is representing a number - ⭐ `isObject()` if it's a js plain Object - `isPromise()` if it's a promise @@ -49,15 +49,29 @@ Contributions always welcome! - ⭐ `isSame()` Compare if dates, functions, arrays, objects or anything else are the same - `isServer()` if you are on the server - `isString()` -- `isURL()` +- `isURL()` - `isUUID()` if it's a valid UUID +### Dates + +- `isOver18()` +- `startOfNextMonth()` +- `startOfNextWeek()` +- `startOfThisWeek()` +- `startOfToday()` +- `startOfTomorrow()` + ### Math - `average()` +- `isBetween()` +- `isOutside()` +- `isStrictlyBetween()` - `max()` - `min()` - `multiply()` +- `normaliseArray()` +- `normaliseNumber()` - `percentageChange()` - `sum()` @@ -67,10 +81,11 @@ Contributions always welcome! - `arrayDiff()` get the difference of two arrays - `arrayIntersection()` get the intersection of two arrays - `capitalize()` word => Word -- `cleanSpaces()` trims and turns double spaces into single space - `clamp()` clamp number in a range +- `cleanSpaces()` trims and turns double spaces into single space - `enumKeys()` enum FRUIT { APPLE, PEAR } => ["APPLE", "PEAR"] - `enumValues()` enum FRUIT { APPLE = 1, PEAR = 3 } => [1, 3] +- `filterAlphanumeric()` remove non-alphanumeric characters - `first()` get the first element of an array - `firstKey()` get the first key of an object - `firstValue()` get the first value of an object @@ -87,7 +102,6 @@ Contributions always welcome! - `normalizeNumber()` normalizes between 0 and 1 - `objectDiff()` get the difference between two objects - ⭐ `parseDate()` pass anything Date-Like, and get a JS Date back -- `pretty()` stringify anything, without breaking on circular dependencies - `promiseWithTimeout()` takes a promise, a timeoutMs, and an option error as arguments. Returns a new Promise that either resolves with the value of the input promise or rejects with the provided error or a default error message if the input promise does not resolve or reject within the specified timeoutMs. - `scrambleText()` replace alpha chars with random chars - `seriesAll()` executes promises in series, and returns all results @@ -95,6 +109,7 @@ Contributions always welcome! - `setUrlSearchParams()` set URL search params - `shuffle()` shuffles elements in an array - `sleep()` promise-based sleep +- `stringify()` stringify anything, without breaking on circular dependencies - `toggleArrayValue()` remove/add value in array - `truncate()` truncate text, does not break emojis - `uniqueValues()` gets unique values in an array @@ -102,6 +117,7 @@ Contributions always welcome! ### Formatters - `formatCamelCase()` +- `formatCookies()` { cookie1: "1", cookie2: "2" } => "cookie1=1; cookie2=2" - `formatNumber()` 1000 => "1,000" or "1K" or 0.112 => "11.2%" - `formatPercentage()` 0.11 => "11%" - `formatProgress()` => "[2/10]" @@ -112,8 +128,9 @@ Contributions always welcome! These functions are optimized for low entropy random data generation useful for Unit Testing, Storybook, Pass real validations, Reverse hacking, Penetration testing... -- `randomAddress()` +- `randomAddress()` - `randomAlphaNumericCode()` +- `randomArray()` - ⭐ `randomArrayItem()` now supporting non-uniform distribution - `randomBankAccount()` - `randomBool()` @@ -140,31 +157,28 @@ These functions are optimized for low entropy random data generation useful for - `randomHtmlColorName()` - `randomIBAN()` - `randomInt()` - - `randomPositiveInt()` > 0 - - `randomNegativeInt()` < 0 - - `randomMaxSafeInt()` Range of very BIG integers, which are still safe to use tho + - `randomBigInt()` - `randomMaxInt()` Range within the Maximum integer supported by js + - `randomMaxSafeInt()` Range of very BIG integers, which are still safe to use tho + - `randomNegativeInt()` < 0 + - `randomPositiveInt()` > 0 - `randomIP()` - `randomName()` - `randomFirstName()` - `randomLastName()` - `randomFullName()` - `randomNumericCode()` +- `randomObject()` - `randomParagraph()` - `randomPassword()` - `randomPath()` /path/to/something - `randomPhoneNumber()` - `randomString()` +- `randomSymbol()` - `randomUUID()` lightweight uuid generation, passing UUID validation - `randomValue()` - `randomWord()` -### Checks - -Checks are functions that throw an error, if the validation fails - -- ⭐ `checkEnvVars()` Make sure env vars are set per-environment - ### TypeScript Helpers & Generics - `Coords` diff --git a/package.json b/package.json index f9690f4..8749e4b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "deverything", - "version": "0.48.1", + "version": "1.3.0", "description": "Everything you need for Dev", "main": "./dist/index.js", "module": "./dist/index.mjs", @@ -17,7 +17,7 @@ "lint": "TIMING=1 eslint src --fix", "prepublish": "pnpm test && pnpm build", "test": "jest", - "bump": "pnpm changeset && pnpm changeset version" + "release": "pnpm changeset && pnpm changeset version" }, "repository": { "type": "git", @@ -27,9 +27,10 @@ "checks", "dates", "fake", + "formatters", "generator", "helpers", - "formatters", + "math", "numbers", "random", "testing", @@ -53,5 +54,6 @@ "ts-node": "^10.9.2", "tsup": "^6.7.0", "typescript": "^4.9.5" - } + }, + "packageManager": "pnpm@9.4.0+sha512.f549b8a52c9d2b8536762f99c0722205efc5af913e77835dbccc3b0b0b2ca9e7dc8022b78062c17291c48e88749c70ce88eb5a74f1fa8c4bf5e18bb46c8bd83a" } diff --git a/src/checks/checkEnvVars.test.ts b/src/checks/checkEnvVars.test.ts deleted file mode 100644 index e82d94b..0000000 --- a/src/checks/checkEnvVars.test.ts +++ /dev/null @@ -1,99 +0,0 @@ -import { expect, it, describe } from "@jest/globals"; -import { checkEnvVars } from "./checkEnvVars"; - -describe("checkEnvVars", function () { - describe("SimpleValidation", function () { - it("undefined value", function () { - expect(() => - checkEnvVars({ - MISSING: true, - }) - ).toThrow(); - expect(() => - checkEnvVars({ - MISSING: "always", - }) - ).toThrow(); - - expect(() => - checkEnvVars({ - MISSING: false, - }) - ).not.toThrow(); - expect(() => - checkEnvVars({ - MISSING: "never", - }) - ).not.toThrow(); - }); - - it("string value", function () { - process.env.IS_THERE = "true"; - expect(() => - checkEnvVars({ - IS_THERE: true, - }) - ).not.toThrow(); - - expect(() => - checkEnvVars({ - IS_THERE: false, - }) - ).toThrow(); - }); - - it("empty value", function () { - process.env.IS_THERE_EMPTY = ""; - - // DOES throw because empty string could be result of `VAR=` in .env file - expect(() => - checkEnvVars({ - IS_THERE_EMPTY: true, - }) - ).toThrow(); - - expect(() => - checkEnvVars({ - IS_THERE_EMPTY: false, // TODO, send warning for empty string - }) - ).not.toThrow(); - }); - }); - - describe("AdvancedValidation", function () { - process.env.TEST_VAL = "TEST_VAL"; - - it("simple", function () { - expect(() => - checkEnvVars({ - TEST_VAL: { - TEST: true, - DEV: false, - }, - }) - ).not.toThrow(); - }); - it("direct oneOf", function () { - expect(() => - checkEnvVars({ - TEST_VAL: { oneOf: ["TEST_VAL", "TEST_VALDO"] }, - }) - ).not.toThrow(); - }); - - it("env oneOf", function () { - expect(() => - checkEnvVars({ - TEST_VAL: { - TEST: { - oneOf: ["TEST_VAL", "TEST_VALDO"], - }, - STAGE: { - oneOf: ["doesnt", "matter"], - }, - }, - }) - ).not.toThrow(); - }); - }); -}); diff --git a/src/checks/checkEnvVars.ts b/src/checks/checkEnvVars.ts deleted file mode 100644 index b4db43f..0000000 --- a/src/checks/checkEnvVars.ts +++ /dev/null @@ -1,186 +0,0 @@ -import { isArray } from "../validators/isArray"; -import { isObject } from "../validators/isObject"; - -type Keyword = "should" | "shouldNot" | "always" | "never"; -type SimpleValidationRule = Keyword | boolean; -type EnvValidation = string[]; -type AdvancedValidation = { - oneOf?: string[]; - endsWith?: string; - startsWith?: string; -}; -const advancedValidationKeys: (keyof AdvancedValidation)[] = [ - "oneOf", - "endsWith", - "startsWith", -]; -type WithEnvValidation = - | AdvancedValidation - | { - [env: string]: AdvancedValidation | SimpleValidationRule; - }; -type Config = { - processEnvKey?: string; -}; - -/** - * @param envVarsMap - * @param config - * @example - * checkEnvVars({ - * NEW_API_KEY: true, - * OLD_API: false, - * ONLY_NON_PROD: ["test", "dev"], - * ONLY_PROD: { - * prod: true, - * }, - * APP_ENV: { - * oneOf: ["test", "dev", "prod"], - * }, - * STRIPE_KEY: { - * prod: { - * startsWith: "live_key_", - * endsWith: "_end", - * }, - * }, - * }, { - * processEnvKey: "APP_ENV" // default is "NODE_ENV" - * }) - */ -export const checkEnvVars = ( - envVarsMap: Record< - string, - SimpleValidationRule | EnvValidation | WithEnvValidation - >, - config?: Config -) => { - const processEnvKey = config?.processEnvKey || "NODE_ENV"; - const errors: string[] = []; - const warnings: string[] = []; - - const validateAdvanced = ({ - envVarKey, - envVarValue, - validation, - }: { - envVarKey: string; - envVarValue: string | undefined; - validation: AdvancedValidation; - }) => { - if (validation.oneOf) { - if (envVarValue) { - if (!validation.oneOf.includes(envVarValue)) - errors.push( - `${envVarKey}=${envVarValue} is not allowed, use one of: ${validation.oneOf.join( - ", " - )}` - ); - } else { - errors.push(`${envVarKey} is missing`); - } - } - - if (validation.endsWith) { - if (envVarValue) { - if (!envVarValue?.endsWith(validation.endsWith)) { - errors.push( - `${envVarKey}=${envVarValue} is not allowed, must end in: ${validation.endsWith}` - ); - } - } else { - errors.push(`${envVarKey} is missing`); - } - } - - if (validation.startsWith) { - if (envVarValue) { - if (!envVarValue?.startsWith(validation.startsWith)) { - errors.push( - `${envVarKey}=${envVarValue} is not allowed, must start with: ${validation.startsWith}` - ); - } - } else { - errors.push(`${envVarKey} is missing`); - } - } - }; - - const validateSimple = ({ - envVarKey, - envVarValue, - rule, - }: { - envVarKey: string; - envVarValue: string | undefined; - rule: SimpleValidationRule; - }) => { - switch (rule) { - case "should": - if (!envVarValue) warnings.push(`${envVarKey} should be set`); - break; - case "shouldNot": - if (envVarValue) warnings.push(`${envVarKey} should not be set`); - break; - case "never": - case false: - if (envVarValue) errors.push(`${envVarKey} is not allowed`); - break; - case "always": - case true: - default: // safety net if ts gets ignored - if (!envVarValue) errors.push(`${envVarKey} is missing`); - break; - } - }; - - Object.entries(envVarsMap).forEach(([envVarKey, rule]) => { - const envVarValue = process.env[envVarKey]; - if (isObject(rule)) { - // Direct - Object.entries(rule).forEach(([advancedValidationKey, rule]) => { - if (advancedValidationKeys.includes(advancedValidationKey as any)) - validateAdvanced({ - envVarValue, - validation: { [advancedValidationKey]: rule }, - envVarKey, - }); - }); - - //With Env - Object.entries(rule).forEach(([envName, rule]) => { - if (process.env[processEnvKey] === envName) { - if (isObject(rule)) - validateAdvanced({ - envVarValue, - validation: rule as AdvancedValidation, - envVarKey, - }); - else - validateSimple({ - envVarValue, - rule: rule as SimpleValidationRule, - envVarKey, - }); - } - }); - } else if (isArray(rule)) { - const envNames = rule; - envNames.forEach((envName) => { - if (process.env[processEnvKey] === envName && !envVarValue) - errors.push(`${envVarKey} is missing`); - }); - } else { - validateSimple({ - envVarValue, - rule: rule as SimpleValidationRule, - envVarKey, - }); - } - }); - - if (warnings.length) console.warn(`[WARNING] ` + warnings.join(", ")); - - if (errors.length) { - throw new Error(`[ERROR] ` + errors.join(", ")); - } -}; diff --git a/src/checks/index.ts b/src/checks/index.ts deleted file mode 100644 index 1cd3d99..0000000 --- a/src/checks/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./checkEnvVars"; diff --git a/src/dates/index.ts b/src/dates/index.ts new file mode 100644 index 0000000..0655ab1 --- /dev/null +++ b/src/dates/index.ts @@ -0,0 +1,7 @@ +export * from "./isOver18"; +export * from "./startOfNextMonth"; +export * from "./startOfNextWeek"; +export * from "./startOfThisWeek"; +export * from "./startOfDay"; +export * from "./startOfToday"; +export * from "./startOfTomorrow"; diff --git a/src/dates/isOver18.ts b/src/dates/isOver18.ts new file mode 100644 index 0000000..ed5f8ac --- /dev/null +++ b/src/dates/isOver18.ts @@ -0,0 +1,21 @@ +import { parseDate } from "../helpers"; +import { DateLike } from "../types"; + +export const isOver18 = (birthDate: DateLike) => { + const now = new Date(); + const birth = parseDate(birthDate); + if (!birth) return false; + + const age = now.getFullYear() - birth.getFullYear(); + if (age > 18) return true; + if (age < 18) return false; + if (age === 18) { + if (now.getMonth() > birth.getMonth()) return true; + if (now.getMonth() < birth.getMonth()) return false; + if (now.getMonth() === birth.getMonth()) { + if (now.getDate() >= birth.getDate()) return true; + if (now.getDate() < birth.getDate()) return false; + } + } + return false; +}; diff --git a/src/dates/startOfDay.ts b/src/dates/startOfDay.ts new file mode 100644 index 0000000..663db48 --- /dev/null +++ b/src/dates/startOfDay.ts @@ -0,0 +1,3 @@ +export const startOfDay = (day: Date): Date => { + return new Date(day.getFullYear(), day.getMonth(), day.getDate()); +}; diff --git a/src/dates/startOfNextMonth.ts b/src/dates/startOfNextMonth.ts new file mode 100644 index 0000000..e5277b6 --- /dev/null +++ b/src/dates/startOfNextMonth.ts @@ -0,0 +1,4 @@ +export const startOfNextMonth = () => { + const now = new Date(); + return new Date(now.getFullYear(), now.getMonth() + 1, 1); +}; diff --git a/src/dates/startOfNextWeek.ts b/src/dates/startOfNextWeek.ts new file mode 100644 index 0000000..c3f977d --- /dev/null +++ b/src/dates/startOfNextWeek.ts @@ -0,0 +1,8 @@ +export const startOfNextWeek = () => { + const now = new Date(); + return new Date( + now.getFullYear(), + now.getMonth(), + now.getDate() + 7 - now.getDay() + ); +}; diff --git a/src/dates/startOfThisWeek.ts b/src/dates/startOfThisWeek.ts new file mode 100644 index 0000000..adf2b41 --- /dev/null +++ b/src/dates/startOfThisWeek.ts @@ -0,0 +1,8 @@ +export const startOfThisWeek = () => { + const now = new Date(); + return new Date( + now.getFullYear(), + now.getMonth(), + now.getDate() - now.getDay() + ); +}; diff --git a/src/dates/startOfToday.ts b/src/dates/startOfToday.ts new file mode 100644 index 0000000..5a43a03 --- /dev/null +++ b/src/dates/startOfToday.ts @@ -0,0 +1,6 @@ +import { startOfDay } from "./startOfDay"; + +export const startOfToday = (): Date => { + const now = new Date(); + return startOfDay(now); +}; diff --git a/src/dates/startOfTomorrow.ts b/src/dates/startOfTomorrow.ts new file mode 100644 index 0000000..76077ab --- /dev/null +++ b/src/dates/startOfTomorrow.ts @@ -0,0 +1,4 @@ +export const startOfTomorrow = () => { + const now = new Date(); + return new Date(now.getFullYear(), now.getMonth(), now.getDate() + 1); +}; diff --git a/src/formatters/formatCookies.ts b/src/formatters/formatCookies.ts new file mode 100644 index 0000000..30f065b --- /dev/null +++ b/src/formatters/formatCookies.ts @@ -0,0 +1,12 @@ +import { PlainObject } from "../types/Object"; + +/** + * + * @example formatCookies({}) => "" + * @example formatCookies({ session: "123", _ga: 123 }) => "session=123; _ga=123" + */ +export const formatCookies = (object: PlainObject): string => { + return Object.entries(object) + .map(([key, value]) => `${key}=${value}`) + .join("; "); +}; diff --git a/src/formatters/formatLatin.ts b/src/formatters/formatLatin.ts new file mode 100644 index 0000000..996a4f4 --- /dev/null +++ b/src/formatters/formatLatin.ts @@ -0,0 +1,8 @@ +/** + * + * @deprecated This is still WIP + */ +export const formatLatin = (text: string): string => { + let normalized = text.normalize("NFKD"); + return normalized; +}; diff --git a/src/formatters/formatNumber.test.ts b/src/formatters/formatNumber.test.ts index e42f8d9..248446e 100644 --- a/src/formatters/formatNumber.test.ts +++ b/src/formatters/formatNumber.test.ts @@ -4,22 +4,24 @@ import { randomInt } from "../random/randomInt"; describe("formatNumber", () => { test("should return the same number if test is under a thousand", () => { - const value = randomInt(0, 999); + const value = randomInt({ min: 0, max: 999 }); expect(formatNumber(value, { compact: true })).toBe(`${value}`); }); test("should return a string in compact K notation if value is one thousand or above", () => { - const value = randomInt(1000, 9999); + const value = randomInt({ min: 1000, max: 9999 }); expect(formatNumber(value, { compact: true })).toContain("K"); }); test("should return a string in compact M notation if value is one million or above", () => { - const value = randomInt(1000000, 9999999); + const value = randomInt({ min: 1000000, max: 9999999 }); expect(formatNumber(value, { compact: true })).toContain("M"); }); test("should return a string with thousand separator but no compact notation", () => { - const formattedValue = formatNumber(randomInt(1000000, 9999999)); + const formattedValue = formatNumber( + randomInt({ min: 1000000, max: 9999999 }) + ); expect(formattedValue).not.toContain("M"); expect(formattedValue).toContain(","); }); diff --git a/src/formatters/index.ts b/src/formatters/index.ts index 778759a..2da0b49 100644 --- a/src/formatters/index.ts +++ b/src/formatters/index.ts @@ -1,4 +1,5 @@ export * from "./formatCamelCase"; +export * from "./formatCookies"; export * from "./formatNumber"; export * from "./formatPercentage"; export * from "./formatProgress"; diff --git a/src/helpers/dir.test.ts b/src/helpers/dir.test.ts new file mode 100644 index 0000000..aa86bb0 --- /dev/null +++ b/src/helpers/dir.test.ts @@ -0,0 +1,10 @@ +import { describe, expect, test } from "@jest/globals"; +import { dir } from "./dir"; + +describe("dir", () => { + // dir(randomObject({ circular: true })); + + test("return type", async () => { + expect(dir(undefined)).toBe(undefined); + }); +}); diff --git a/src/helpers/dir.ts b/src/helpers/dir.ts index 1c6476a..9dbfd68 100644 --- a/src/helpers/dir.ts +++ b/src/helpers/dir.ts @@ -1,6 +1,9 @@ /** * Print or log helper that does not break on circular references, and expands nested objects. */ -export const dir = (arg: any, depth = 5): void => { - console.dir(arg, { depth }); +export const dir = ( + arg: any, + { maxDepth = 5 }: { maxDepth?: number } = {} +): void => { + console.dir(arg, { depth: maxDepth }); }; diff --git a/src/helpers/filterAlphanumeric.ts b/src/helpers/filterAlphanumeric.ts new file mode 100644 index 0000000..8d54fba --- /dev/null +++ b/src/helpers/filterAlphanumeric.ts @@ -0,0 +1,7 @@ +/** + * @returns a string with only alphanumeric characters + * @example filterAlphanumeric("!abc()") // returns "abc" + */ +export const filterAlphanumeric = (string: string) => { + return string.replace(/[^a-zA-Z0-9]/g, ""); +}; diff --git a/src/helpers/getUrlSearchParam.test.ts b/src/helpers/getUrlSearchParam.test.ts index bedc864..4703abc 100644 --- a/src/helpers/getUrlSearchParam.test.ts +++ b/src/helpers/getUrlSearchParam.test.ts @@ -2,7 +2,7 @@ import { describe, expect, test } from "@jest/globals"; import { getUrlSearchParam } from "./getUrlSearchParam"; describe("getUrlSearchParam", () => { - test("undefined", async () => { + test("undefined", () => { expect(getUrlSearchParam(undefined, "")).toBeUndefined(); expect(getUrlSearchParam(undefined, "param")).toBeUndefined(); expect(getUrlSearchParam("", "param")).toBeUndefined(); @@ -20,7 +20,13 @@ describe("getUrlSearchParam", () => { expect(getUrlSearchParam("https://www.ciao.com", "param2")).toBeUndefined(); }); - test("found", async () => { + test("does not work with window.location.search", () => { + expect( + getUrlSearchParam("?q=app+config&oq=app+cofig", "q") + ).toBeUndefined(); + }); + + test("found", () => { expect(getUrlSearchParam("https://www.ciao.com/?param=", "param")).toBe(""); expect(getUrlSearchParam("https://www.ciao.com/?param", "param")).toBe(""); expect(getUrlSearchParam("https://www.ciao.com/?param=1", "param")).toBe( diff --git a/src/helpers/index.ts b/src/helpers/index.ts index be1c193..bdfec78 100644 --- a/src/helpers/index.ts +++ b/src/helpers/index.ts @@ -11,6 +11,7 @@ export * from "./cyclicalItem"; export * from "./dir"; export * from "./enumKeys"; export * from "./enumValues"; +export * from "./filterAlphanumeric"; export * from "./first"; export * from "./firstKey"; export * from "./firstValue"; @@ -32,7 +33,6 @@ export * from "./omit"; export * from "./parseDate"; export * from "./pickObjectKeys"; export * from "./pickObjectValues"; -export * from "./pretty"; export * from "./promiseWithTimeout"; export * from "./scrambleText"; export * from "./serialize"; @@ -41,6 +41,7 @@ export * from "./setObjectPath"; export * from "./setUrlSearchParams"; export * from "./shuffle"; export * from "./sleep"; +export * from "./stringify"; export * from "./toggleArrayValue"; export * from "./truncate"; export * from "./uniqueValues"; diff --git a/src/helpers/pretty.test.ts b/src/helpers/pretty.test.ts deleted file mode 100644 index fc6aaec..0000000 --- a/src/helpers/pretty.test.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { describe, expect, test } from "@jest/globals"; -import { pretty } from "./pretty"; - -describe("pretty", () => { - test("mixed", async () => { - expect(pretty()).toBeUndefined(); - expect(pretty("")).toBe(`""`); - expect(pretty(true)).toBe("true"); - expect(pretty(0)).toBe("0"); - }); - test("object", async () => { - expect(pretty({})).toBe("{}"); - expect(pretty({ a: 1 })).toBe(`{\n "a": 1\n}`); - const a = { a: 1 }; - // @ts-ignore - a.b = a; - expect(pretty(a)).toBe(`{\n "a": 1,\n "b": "[Circular ~]"\n}`); - }); -}); diff --git a/src/helpers/seriesAll.test.ts b/src/helpers/seriesAll.test.ts index ca12022..616ffea 100644 --- a/src/helpers/seriesAll.test.ts +++ b/src/helpers/seriesAll.test.ts @@ -3,12 +3,40 @@ import { seriesAll } from "./seriesAll"; import { sleep } from "./sleep"; describe("seriesAll", () => { - const fn1 = () => Promise.resolve(1); - const fn2 = () => sleep(100).then(() => 2); - const fn3 = () => 3; - test("simple", async () => { - expect(seriesAll([fn1, fn2, fn3])).resolves.toEqual([1, 2, 3]); - expect(await seriesAll([() => true])).toStrictEqual([true]); + expect( + await seriesAll([ + Promise.resolve(1), + sleep(1).then(() => 2), + () => Promise.resolve(3), + async () => 5, + async () => { + await sleep(1); + return 6; + }, + async () => { + return sleep(1).then(() => 7); + }, + ]) + ).toStrictEqual([1, 2, 3, 5, 6, 7]); + }); + + test("throw new Error", () => { + expect( + seriesAll([ + () => { + throw new Error("1"); + }, + () => { + throw new Error("2"); + }, + ]) + ).rejects.toThrowError("1"); + }); + + test("Promise.reject", () => { + expect( + seriesAll([Promise.reject("3"), () => Promise.reject("4")]) + ).rejects.toEqual("3"); }); }); diff --git a/src/helpers/seriesAll.ts b/src/helpers/seriesAll.ts index b0c1607..bbf2fe1 100644 --- a/src/helpers/seriesAll.ts +++ b/src/helpers/seriesAll.ts @@ -1,19 +1,30 @@ +import { isFunction, isPromise } from "../validators"; + /** * * @description Run a series of (async) functions in order and return the results * @example * const results = await seriesAll([ - * () => Promise.resolve(1), - * () => sleep(100).then(() => 2), + * Promise.resolve(1), + * sleep(100).then(() => 2), * () => Promise.resolve(3), - * ]); => [1, 2, 3] + * async () => 4, + * ]); => [1, 2, 3, 4] */ -export const seriesAll = async (series: Function[]): Promise => { - const results: T[] = []; +export const seriesAll = async ( + series: (Promise | (() => Promise))[] +): Promise => { + const results = []; for (const fn of series) { - results.push(await fn()); + if (isPromise(fn)) results.push(await fn); + else if (isFunction(fn)) results.push(await fn()); + else throw new Error("seriesAll: invalid type"); } - return results; + + // TODO: "as T[];" fix TS error + // error TS2345: Argument of type '(() => Promise) | Awaited' is not assignable to parameter of type 'T'. + // 'T' could be instantiated with an arbitrary type which could be unrelated to '(() => Promise) | Awaited'. + return results as T[]; }; // TODO: rename to seriesAsync diff --git a/src/helpers/setUrlSearchParams.test.ts b/src/helpers/setUrlSearchParams.test.ts index 2e18bc6..e1c46cf 100644 --- a/src/helpers/setUrlSearchParams.test.ts +++ b/src/helpers/setUrlSearchParams.test.ts @@ -2,8 +2,18 @@ import { expect, describe, test } from "@jest/globals"; import { setUrlSearchParams } from "./setUrlSearchParams"; describe("setUrlSearchParams", () => { + test("no nullish", () => { + expect( + setUrlSearchParams("/signin?token#hash", { n: null, u: undefined, z: 0 }) + ).toBe("/signin?token=&z=0#hash"); + }); + test("relative url", () => { expect(setUrlSearchParams("/signin")).toBe("/signin"); + expect(setUrlSearchParams("/signin/", { ok: true })).toBe( + "/signin/?ok=true" + ); + expect(setUrlSearchParams("/signin/", {})).toBe("/signin/"); expect(setUrlSearchParams("/signin?")).toBe("/signin"); expect(setUrlSearchParams("/signin?in")).toBe("/signin?in"); expect(setUrlSearchParams("/signin?in#sec")).toBe("/signin?in#sec"); diff --git a/src/helpers/setUrlSearchParams.ts b/src/helpers/setUrlSearchParams.ts index e07b45f..567ed36 100644 --- a/src/helpers/setUrlSearchParams.ts +++ b/src/helpers/setUrlSearchParams.ts @@ -1,6 +1,8 @@ +import { Maybe } from "../types"; + export const setUrlSearchParams = ( currentURL: string, - searchParams: Record = {} + searchParams: Record> = {} ) => { const isRelativeUrl = currentURL.startsWith("/"); const url = new URL( @@ -9,6 +11,7 @@ export const setUrlSearchParams = ( ); Object.entries(searchParams).forEach(([paramKey, paramValue]) => { + if (paramValue === null || paramValue === undefined) return; url.searchParams.set(paramKey, paramValue.toString()); }); diff --git a/src/helpers/stringify.test.ts b/src/helpers/stringify.test.ts new file mode 100644 index 0000000..18b2291 --- /dev/null +++ b/src/helpers/stringify.test.ts @@ -0,0 +1,19 @@ +import { describe, expect, test } from "@jest/globals"; +import { stringify } from "./stringify"; + +describe("stringify", () => { + test("mixed", async () => { + expect(stringify()).toBeUndefined(); + expect(stringify("")).toBe(`""`); + expect(stringify(true)).toBe("true"); + expect(stringify(0)).toBe("0"); + }); + test("object", async () => { + expect(stringify({})).toBe("{}"); + expect(stringify({ a: 1 })).toBe(`{\n "a": 1\n}`); + const a = { a: 1 }; + // @ts-ignore + a.b = a; + expect(stringify(a)).toBe(`{\n "a": 1,\n "b": "[Circular ~]"\n}`); + }); +}); diff --git a/src/helpers/pretty.ts b/src/helpers/stringify.ts similarity index 60% rename from src/helpers/pretty.ts rename to src/helpers/stringify.ts index ad7bf19..bb765e9 100644 --- a/src/helpers/pretty.ts +++ b/src/helpers/stringify.ts @@ -1,6 +1,5 @@ import { objectSerializer } from "../_internals/objectSerializer"; -// TODO: deprecate and rename to stringify -export const pretty = (arg?: any) => { +export const stringify = (arg?: any) => { return JSON.stringify(arg, objectSerializer(), 2); }; diff --git a/src/index.ts b/src/index.ts index 742819e..a7e1e51 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,4 +1,4 @@ -export * from "./checks"; +export * from "./dates"; export * from "./formatters"; export * from "./helpers"; export * from "./math"; diff --git a/src/math/index.ts b/src/math/index.ts index dc11e28..2e086cc 100644 --- a/src/math/index.ts +++ b/src/math/index.ts @@ -1,8 +1,11 @@ export * from "./average"; export * from "./isBetween"; +export * from "./isOutside"; +export * from "./isStrictlyBetween"; export * from "./max"; export * from "./min"; export * from "./multiply"; -export * from "./isOutside"; +export * from "./normaliseArray"; +export * from "./normaliseNumber"; export * from "./percentageChange"; export * from "./sum"; diff --git a/src/math/isStrictlyBetween.ts b/src/math/isStrictlyBetween.ts new file mode 100644 index 0000000..4d932eb --- /dev/null +++ b/src/math/isStrictlyBetween.ts @@ -0,0 +1,3 @@ +export const isStrictlyBetween = (value: number, min: number, max: number) => { + return value > min && value < max; +}; diff --git a/src/math/normaliseArray.ts b/src/math/normaliseArray.ts new file mode 100644 index 0000000..beffd9b --- /dev/null +++ b/src/math/normaliseArray.ts @@ -0,0 +1,14 @@ +import { max } from "./max"; +import { min } from "./min"; +import { normaliseNumber } from "./normaliseNumber"; + +/** + * Normalises an array of numbers + * @example normaliseArray([1, 2, 3]) => [0, 0.5, 1] + */ +export const normaliseArray = (values: number[]) => { + const minValue = min(values); + const maxValue = max(values); + + return values.map((value) => normaliseNumber(value, minValue, maxValue)); +}; diff --git a/src/math/normaliseNumber.ts b/src/math/normaliseNumber.ts new file mode 100644 index 0000000..e8938cc --- /dev/null +++ b/src/math/normaliseNumber.ts @@ -0,0 +1,9 @@ +/** + * + * @example normaliseNumber(50, 0, 100) => 0.5 + */ +export const normaliseNumber = ( + value: number, + minValue: number, + maxValue: number +) => (value - minValue) / (maxValue - minValue); diff --git a/src/math/percentageChange.test.ts b/src/math/percentageChange.test.ts index 8771ef5..9015ccc 100644 --- a/src/math/percentageChange.test.ts +++ b/src/math/percentageChange.test.ts @@ -3,29 +3,12 @@ import { percentageChange } from "./percentageChange"; describe("percentageChange", () => { test("simple", async () => { - expect( - percentageChange({ - current: 10, - previous: 12, - }) - ).toBe(-16.67); - expect( - percentageChange({ - current: 0, - previous: 12, - }) - ).toBe(0); - expect( - percentageChange({ - current: 0, - previous: 0, - }) - ).toBe(0); - expect( - percentageChange({ - current: 99, - previous: 0, - }) - ).toBe(0); + expect(percentageChange(-0.1, 0.2)).toBe(0); + expect(percentageChange(0.2, 0.1)).toBe(-0.5); + expect(percentageChange(0.1, 0.2)).toBe(1); + expect(percentageChange(0.3, 0.333)).toBe(0.11); + expect(percentageChange(0, 0.12)).toBe(1); + expect(percentageChange(0, 0)).toBe(0); + expect(percentageChange(0.99, 0)).toBe(-1); }); }); diff --git a/src/math/percentageChange.ts b/src/math/percentageChange.ts index 3b5dfd5..7a300ac 100644 --- a/src/math/percentageChange.ts +++ b/src/math/percentageChange.ts @@ -1,16 +1,14 @@ -import { isPositiveInt } from "../validators"; - -export const percentageChange = ({ - previous, - current, -}: { - previous: number; - current: number; -}): number => { - if (!isPositiveInt(previous) || !isPositiveInt(current)) return 0; +/** + * + * @param previous Positive percentage i.e. 0.1 for 10% + * @param current Positive percentage i.e. 0.2 for 20% + * @returns + */ +export const percentageChange = (previous: number, current: number): number => { + if (previous < 0 || current < 0) return 0; if (current === 0 && previous === 0) return 0; - if (current === 0 && previous !== 0) return -100; - if (current !== 0 && previous === 0) return 100; - const perChange = ((current - previous) * 100) / previous; - return parseFloat(perChange.toFixed(2)); + if (current === 0 && previous !== 0) return -1; + if (current !== 0 && previous === 0) return 1; + const perChange = (current - previous) / previous; + return parseFloat(perChange.toFixed(4)); // 4 decimal places so when formatting to % two decimal places are shown }; diff --git a/src/random/index.ts b/src/random/index.ts index a7958ad..860a46f 100644 --- a/src/random/index.ts +++ b/src/random/index.ts @@ -1,5 +1,6 @@ export * from "./randomAddress"; export * from "./randomAlphaNumericCode"; +export * from "./randomArray"; export * from "./randomArrayItem"; export * from "./randomBankAccount"; export * from "./randomBool"; @@ -9,6 +10,7 @@ export * from "./randomCoords"; export * from "./randomDate"; export * from "./randomEmail"; export * from "./randomEmoji"; +export * from "./randomEmptyValue"; export * from "./randomEnumKey"; export * from "./randomEnumValue"; export * from "./randomFile"; @@ -22,11 +24,13 @@ export * from "./randomInt"; export * from "./randomIP"; export * from "./randomName"; export * from "./randomNumericCode"; -export * from "./randomNumericId"; +export * from "./randomObject"; export * from "./randomParagraph"; export * from "./randomPassword"; export * from "./randomPath"; export * from "./randomPhoneNumber"; export * from "./randomString"; +export * from "./randomSymbol"; export * from "./randomUUID"; +export * from "./randomValue"; export * from "./randomWord"; diff --git a/src/random/randomArray.ts b/src/random/randomArray.ts new file mode 100644 index 0000000..4d8dc9a --- /dev/null +++ b/src/random/randomArray.ts @@ -0,0 +1,7 @@ +import { randomInt } from "./randomInt"; +import { randomValue } from "./randomValue"; +import { array } from "../helpers/array"; + +export const randomArray = () => { + return array(randomInt({ min: 1, max: 5 }), randomValue); +}; diff --git a/src/random/randomArrayItem.ts b/src/random/randomArrayItem.ts index 9a3f987..cb810af 100644 --- a/src/random/randomArrayItem.ts +++ b/src/random/randomArrayItem.ts @@ -22,5 +22,5 @@ export const randomArrayItem = ( return last(array); } - return array[randomInt(0, lastIndex(array))]; + return array[randomInt({ min: 0, max: lastIndex(array) })]; }; diff --git a/src/random/randomBool.ts b/src/random/randomBool.ts index b33f9fe..79d5923 100644 --- a/src/random/randomBool.ts +++ b/src/random/randomBool.ts @@ -1,3 +1,3 @@ -import { randomInt } from "./randomInt"; +import { randomArrayItem } from "./randomArrayItem"; -export const randomBool = () => !!randomInt(0, 1); +export const randomBool = () => randomArrayItem([true, false]); diff --git a/src/random/randomChar.ts b/src/random/randomChar.ts index b7f835b..5c3b25d 100644 --- a/src/random/randomChar.ts +++ b/src/random/randomChar.ts @@ -1,5 +1,5 @@ import { randomInt } from "./randomInt"; export const randomChar = () => { - return String.fromCharCode(randomInt(97, 122)); + return String.fromCharCode(randomInt({ min: 97, max: 122 })); }; diff --git a/src/random/randomDate.test.ts b/src/random/randomDate.test.ts index b1fa530..7a89b80 100644 --- a/src/random/randomDate.test.ts +++ b/src/random/randomDate.test.ts @@ -6,8 +6,18 @@ describe(`randomDate`, () => { expect(randomDate().getTime()).toBeGreaterThan(0); }); it(`args`, () => { - expect(randomDate("2010", "2011").toISOString().substring(0, 3)).toBe( - "201" + expect( + randomDate({ startDate: "2010", endDate: "2011" }) + .toISOString() + .substring(0, 3) + ).toBe("201"); + }); + it(`no start`, () => { + expect(randomDate({ endDate: "2011" }).getFullYear()).toBeLessThan(2011); + }); + it(`no end`, () => { + expect(randomDate({ startDate: "2012" }).getFullYear()).toBeGreaterThan( + 2011 ); }); }); diff --git a/src/random/randomDate.ts b/src/random/randomDate.ts index e54bf6a..c6bc606 100644 --- a/src/random/randomDate.ts +++ b/src/random/randomDate.ts @@ -4,13 +4,19 @@ import { MILLISECONDS_IN_MINUTE, } from "../constants/time"; import { parseDate } from "../helpers/parseDate"; -import { DateLike, DateRange } from "../types"; +import { DateRange } from "../types"; import { isFutureDate, isPastDate } from "../validators"; import { randomInt } from "./randomInt"; -const nowPlusMs = (ms: number) => new Date(new Date().getTime() + ms); +const datePlusDecade = (date: Date = new Date()) => + datePlusMs(date, MILLISECONDS_IN_DECADE); -export const randomDate = (startDate?: DateLike, endDate?: DateLike) => { +const dateMinusDecade = (date: Date = new Date()) => + datePlusMs(date, -MILLISECONDS_IN_DECADE); + +const datePlusMs = (date: Date, ms: number) => new Date(date.getTime() + ms); + +export const randomDate = ({ startDate, endDate }: Partial = {}) => { const parsedStartDate = parseDate(startDate); const parsedEndDate = parseDate(endDate); @@ -19,24 +25,22 @@ export const randomDate = (startDate?: DateLike, endDate?: DateLike) => { } const finalStartDate = - parsedStartDate || - (parsedEndDate - ? new Date(parsedEndDate.getTime() - MILLISECONDS_IN_DECADE) - : nowPlusMs(-MILLISECONDS_IN_DECADE)); + parsedStartDate || // + dateMinusDecade(parsedEndDate); // uses now if undefined const finalEndDate = - parsedEndDate || - (parsedStartDate - ? new Date(parsedStartDate.getTime() + MILLISECONDS_IN_DECADE) - : nowPlusMs(MILLISECONDS_IN_DECADE)); + parsedEndDate || // + datePlusDecade(parsedStartDate); // uses now if undefined - return new Date(randomInt(finalStartDate.getTime(), finalEndDate.getTime())); + return new Date( + randomInt({ min: finalStartDate.getTime(), max: finalEndDate.getTime() }) + ); }; -export const randomMaxDate = (start?: Date, end?: Date) => { - const startDate = start || new Date(-MAX_DATE_MILLISECONDS); - const endDate = end || new Date(MAX_DATE_MILLISECONDS); - return randomDate(startDate, endDate); +export const randomMaxDate = ({ startDate, endDate }: Partial) => { + startDate = startDate || new Date(-MAX_DATE_MILLISECONDS); + endDate = endDate || new Date(MAX_DATE_MILLISECONDS); + return randomDate({ startDate, endDate }); }; export const randomFutureDate = ({ @@ -51,9 +55,9 @@ export const randomFutureDate = ({ } const finalStartDate = - parseDate(startDate) || nowPlusMs(5 * MILLISECONDS_IN_MINUTE); // Add a safe margin in the future (i.e. lagging tests) + parseDate(startDate) || datePlusMs(new Date(), 5 * MILLISECONDS_IN_MINUTE); // Add a safe margin in the future (i.e. lagging tests) - return randomDate(finalStartDate, endDate); + return randomDate({ startDate: finalStartDate, endDate }); }; export const randomPastDate = ({ @@ -68,12 +72,12 @@ export const randomPastDate = ({ } const finalEndDate = parseDate(endDate) || new Date(); - return randomDate(startDate, finalEndDate); + return randomDate({ startDate, endDate: finalEndDate }); }; export const randomDateRange = () => { const startDate = randomDate(); - const endDate = randomDate(startDate); + const endDate = randomDate({ startDate }); return { endDate, diff --git a/src/random/randomEmptyValue.ts b/src/random/randomEmptyValue.ts new file mode 100644 index 0000000..12b6e36 --- /dev/null +++ b/src/random/randomEmptyValue.ts @@ -0,0 +1,5 @@ +import { randomArrayItem } from "./randomArrayItem"; + +export const randomEmptyValue = () => { + return randomArrayItem([undefined, null, NaN, Infinity]); +}; diff --git a/src/random/randomIP.ts b/src/random/randomIP.ts index f618ac6..a30fca5 100644 --- a/src/random/randomIP.ts +++ b/src/random/randomIP.ts @@ -2,5 +2,5 @@ import { array } from "../helpers/array"; import { randomInt } from "./randomInt"; export const randomIP = () => { - return array(4, () => randomInt(0, 255).toString()).join("."); + return array(4, () => randomInt({ min: 0, max: 255 }).toString()).join("."); }; diff --git a/src/random/randomInt.test.ts b/src/random/randomInt.test.ts index b8e45d2..f927ca3 100644 --- a/src/random/randomInt.test.ts +++ b/src/random/randomInt.test.ts @@ -1,5 +1,5 @@ import { describe, expect, test } from "@jest/globals"; -import { randomInt } from "./randomInt"; +import { randomBigInt, randomInt } from "./randomInt"; describe("randomInt", () => { test("no args", async () => { @@ -8,8 +8,12 @@ describe("randomInt", () => { }); test("args", async () => { - expect(randomInt(12, 20)).toBeGreaterThanOrEqual(12); - expect(randomInt(12, 12)).toBeLessThanOrEqual(12); - expect(randomInt(11, 12)).toBeLessThanOrEqual(13); + expect(randomInt({ min: 12, max: 20 })).toBeGreaterThanOrEqual(12); + expect(randomInt({ min: 12, max: 12 })).toBeLessThanOrEqual(12); + expect(randomInt({ min: 11, max: 12 })).toBeLessThanOrEqual(13); + }); + + test("randomBigInt", async () => { + expect(randomBigInt().toString()).not.toContain("n"); }); }); diff --git a/src/random/randomInt.ts b/src/random/randomInt.ts index f3717c2..5b643a5 100644 --- a/src/random/randomInt.ts +++ b/src/random/randomInt.ts @@ -1,35 +1,47 @@ -export const randomInt = (min: number = -100, max: number = 100): number => { +export const randomInt = ({ + min = -100, + max = 100, +}: { + min?: number; + max?: number; +} = {}): number => { min = Math.ceil(min); // in case is a float max = Math.floor(max); // in case is a float return Math.floor(Math.random() * (max - min + 1) + min); }; -export const randomPositiveInt = (max: number = 100): number => - randomInt(1, max); - -export const randomNegativeInt = (min: number = -100): number => - randomInt(min, -1); - -export const randomMaxSafeInt = () => - randomInt(-Number.MAX_SAFE_INTEGER, Number.MAX_SAFE_INTEGER); - -export const randomMaxInt = () => - randomInt(-Number.MAX_VALUE, Number.MAX_VALUE); +export const randomBigInt = (): BigInt => { + return BigInt(randomInt()); +}; -export const randomPercentage = ({ - min, +export const randomPositiveInt = ({ + min = 1, max, }: { min?: number; max?: number; -} = {}) => randomInt(min ?? -100, max ?? 100); +} = {}): number => + randomInt({ + min, + max, + }); -export const randomPositivePercentage = ({ +export const randomNegativeInt = ({ min, - max, + max = -1, }: { min?: number; max?: number; -} = {}) => randomInt(min ?? 1, max ?? 100); +} = {}): number => + randomInt({ + min, + max, + }); + +export const randomMaxSafeInt = () => + randomInt({ min: -Number.MAX_SAFE_INTEGER, max: Number.MAX_SAFE_INTEGER }); + +export const randomMaxInt = () => + randomInt({ min: -Number.MAX_VALUE, max: Number.MAX_VALUE }); -export const randomFormattedPercentage = () => randomPercentage() + "%"; +export const randomFormattedPercentage = () => randomInt() + "%"; diff --git a/src/random/randomNumericCode.ts b/src/random/randomNumericCode.ts index f797466..5313e53 100644 --- a/src/random/randomNumericCode.ts +++ b/src/random/randomNumericCode.ts @@ -15,5 +15,7 @@ export const randomNumericCode = ({ length = 6 }: { length?: number } = {}) => { if (length < 1) throw new Error("randomNumericCode: Length must be greater than 0."); - return array(length, (_, index) => randomInt(!index ? 1 : 0, 9)).join(""); + return array(length, (_, index) => + randomInt({ min: !index ? 1 : 0, max: 9 }) + ).join(""); }; diff --git a/src/random/randomNumericId.ts b/src/random/randomNumericId.ts deleted file mode 100644 index dbff056..0000000 --- a/src/random/randomNumericId.ts +++ /dev/null @@ -1,8 +0,0 @@ -let id = 1; // don't start with 0, to be closer to SQL autoincrement - -/** - * @deprecated use incrementalId() instead, as this one is not random and could cause confusion - */ -export const randomNumericId = () => { - return id++; -}; diff --git a/src/random/randomObject.test.ts b/src/random/randomObject.test.ts index 13de7df..db5af02 100644 --- a/src/random/randomObject.test.ts +++ b/src/random/randomObject.test.ts @@ -3,6 +3,12 @@ import { keysLength } from "../helpers"; import { randomObject } from "./randomObject"; describe(`randomObject`, () => { + // dir( + // randomObject({ + // circular: true, + // }) + // ); + it(`works`, () => { expect(keysLength(randomObject())).toBeGreaterThan(0); }); diff --git a/src/random/randomObject.ts b/src/random/randomObject.ts index 7f10d3b..a062163 100644 --- a/src/random/randomObject.ts +++ b/src/random/randomObject.ts @@ -1,19 +1,46 @@ import { array } from "../helpers"; import { PlainObject } from "../types"; +import { randomArray } from "./randomArray"; +import { randomArrayItem } from "./randomArrayItem"; import { randomInt } from "./randomInt"; import { randomValue } from "./randomValue"; import { randomNoun } from "./randomWord"; -export const randomObject = ({ maxDepth = 5 }: { maxDepth?: number } = {}) => { - const getRandomObject = (depth: number): PlainObject => { +export const randomObject = ({ + maxDepth = 5, + circular = false, +}: { + maxDepth?: number; + circular?: boolean; +} = {}) => { + const getRandomObject = (depth: number = 0): PlainObject => { if (depth >= maxDepth) return {}; - const keys = array(randomInt(1, 5), randomNoun); + const keys = array(randomInt({ min: 1, max: 5 }), randomNoun); return keys.reduce((partial, key) => { - partial[key] = randomValue() || getRandomObject(depth + 1); + const valueOrObject = randomArrayItem(["value", "object", "array"]); + + if (valueOrObject === "object") { + const object = getRandomObject(depth + 1); + partial[key] = object; + if ( + circular && + randomArrayItem([true, false], { weights: [0.2, 0.8] }) + ) { + object.circular = object; + } + } else if (valueOrObject === "array") { + const array = randomArray(); + partial[key] = array; + } else { + const value = randomValue(); + partial[key] = value; + } return partial; }, {} as PlainObject); }; - return getRandomObject(0); + const object = getRandomObject(); + + return object; }; diff --git a/src/random/randomParagraph.ts b/src/random/randomParagraph.ts index f449aba..d145428 100644 --- a/src/random/randomParagraph.ts +++ b/src/random/randomParagraph.ts @@ -20,7 +20,7 @@ export const randomParagraph = ({ maxWords?: number; } = {}) => { return capitalize( - array(randomInt(minWords, maxWords), () => randomWord()) + array(randomInt({ min: minWords, max: maxWords }), () => randomWord()) .join(" ") .slice(0, maxCharacters - 1) + "." ); diff --git a/src/random/randomPassword.ts b/src/random/randomPassword.ts index 1af3c54..87cb25b 100644 --- a/src/random/randomPassword.ts +++ b/src/random/randomPassword.ts @@ -8,6 +8,6 @@ export const randomPassword = ({ maxChars = 32, }: { minChars?: number; maxChars?: number } = {}) => randomString({ length: 1 }).toUpperCase() + // Upper case - randomString({ length: randomInt(minChars, maxChars) - 3 }) + // At least 9 chars + randomString({ length: randomInt({ min: minChars, max: maxChars }) - 3 }) + // At least 9 chars randomArrayItem(SPECIAL_CHARACTERS) + // Special character - randomInt(1, 9); // Number + randomInt({ min: 1, max: 9 }); // Number diff --git a/src/random/randomSymbol.ts b/src/random/randomSymbol.ts new file mode 100644 index 0000000..171a444 --- /dev/null +++ b/src/random/randomSymbol.ts @@ -0,0 +1,5 @@ +import { randomString } from "./randomString"; + +export const randomSymbol = () => { + return Symbol(randomString()); +}; diff --git a/src/random/randomUUID.ts b/src/random/randomUUID.ts index 85c10a5..85d2e3b 100644 --- a/src/random/randomUUID.ts +++ b/src/random/randomUUID.ts @@ -1,4 +1,4 @@ -import { randomNumericId } from "./randomNumericId"; +import { incrementalId } from "../helpers"; /** * This is a light-weight version of the `generateUuid` function @@ -8,7 +8,7 @@ import { randomNumericId } from "./randomNumericId"; * /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i */ export const randomUUID = () => { - const id = randomNumericId().toString().padStart(15, "0"); + const id = incrementalId().toString().padStart(15, "0"); const digit12 = id.substring(0, 12); const digit3 = id.substring(12, 15); return `00000000-0000-1000-8${digit3}-${digit12}`; diff --git a/src/random/randomValue.ts b/src/random/randomValue.ts index df1737a..426b87a 100644 --- a/src/random/randomValue.ts +++ b/src/random/randomValue.ts @@ -1,16 +1,18 @@ import { randomArrayItem } from "./randomArrayItem"; import { randomBool } from "./randomBool"; import { randomDate } from "./randomDate"; -import { randomInt } from "./randomInt"; +import { randomBigInt, randomInt } from "./randomInt"; import { randomString } from "./randomString"; +import { randomSymbol } from "./randomSymbol"; +// This must return true with `isValue` assertion export const randomValue = () => { return randomArrayItem([ randomBool(), randomString(), randomInt(), randomDate(), - undefined, - null, + randomBigInt(), + randomSymbol(), ]); }; diff --git a/src/types/Date.ts b/src/types/Date.ts index b2d1fb0..2f294b9 100644 --- a/src/types/Date.ts +++ b/src/types/Date.ts @@ -1,6 +1,19 @@ export type DateLike = Date | string | number; export type Datey = Date | string; +/** + * @example "2021-01-01T00:00:00.000Z" + */ +export type ISODate = string; +/** + * @example "2021-01-01" + */ +export type ISODay = string; +/** + * @example "America/New_York" + */ +export type Timezone = string; + export type DateRange = { startDate: DateLike; endDate: DateLike; diff --git a/src/types/HashMap.ts b/src/types/HashMap.ts index edc33d3..9314b8d 100644 --- a/src/types/HashMap.ts +++ b/src/types/HashMap.ts @@ -1,7 +1,7 @@ import { PlainKey } from "./Object"; // I don't like the Dict keyword, but it's a possibility... -export type HashMap = Record; +export type HashMap = Record; export type NumberMap = Record; export type StringMap = Record; export type BoolMap = Record; diff --git a/src/validators/isFunction.ts b/src/validators/isFunction.ts index 3221e2b..f485de5 100644 --- a/src/validators/isFunction.ts +++ b/src/validators/isFunction.ts @@ -1,2 +1,7 @@ -export const isFunction = (arg: any): arg is Function => - Object.prototype.toString.call(arg) === "[object Function]"; +/** + * @returns true if the argument can be called like a function -> fn() or await fn() + */ +export const isFunction = (arg: any): arg is Function => { + const type = Object.prototype.toString.call(arg); + return type === "[object Function]" || type === "[object AsyncFunction]"; +};