From cae1204f2fbf073afecf1b08aa3303fb9c3ccd01 Mon Sep 17 00:00:00 2001 From: klmhyeonwoo Date: Wed, 25 Feb 2026 16:55:09 +0900 Subject: [PATCH 1/5] feat(utils): expand utility modules and exports --- package.json | 40 ++++++ package/arrayUtil/chunk/index.test.ts | 17 +++ package/arrayUtil/chunk/index.ts | 14 ++ package/arrayUtil/compact/index.test.ts | 12 ++ package/arrayUtil/compact/index.ts | 7 + package/arrayUtil/difference/index.test.ts | 12 ++ package/arrayUtil/difference/index.ts | 4 + package/arrayUtil/first/index.test.ts | 12 ++ package/arrayUtil/first/index.ts | 3 + package/arrayUtil/flatten/index.test.ts | 12 ++ package/arrayUtil/flatten/index.ts | 3 + package/arrayUtil/flattenDeep/index.test.ts | 12 ++ package/arrayUtil/flattenDeep/index.ts | 16 +++ package/arrayUtil/groupBy/index.test.ts | 24 ++++ package/arrayUtil/groupBy/index.ts | 18 +++ package/arrayUtil/index.ts | 20 +++ package/arrayUtil/intersection/index.test.ts | 12 ++ package/arrayUtil/intersection/index.ts | 16 +++ package/arrayUtil/keyBy/index.test.ts | 27 ++++ package/arrayUtil/keyBy/index.ts | 13 ++ package/arrayUtil/last/index.test.ts | 12 ++ package/arrayUtil/last/index.ts | 7 + package/arrayUtil/partition/index.test.ts | 15 ++ package/arrayUtil/partition/index.ts | 18 +++ package/arrayUtil/sample/index.test.ts | 16 +++ package/arrayUtil/sample/index.ts | 8 ++ package/arrayUtil/sampleSize/index.test.ts | 19 +++ package/arrayUtil/sampleSize/index.ts | 10 ++ package/arrayUtil/shuffle/index.test.ts | 23 +++ package/arrayUtil/shuffle/index.ts | 12 ++ package/arrayUtil/sortBy/index.test.ts | 27 ++++ package/arrayUtil/sortBy/index.ts | 33 +++++ package/arrayUtil/union/index.test.ts | 12 ++ package/arrayUtil/union/index.ts | 17 +++ package/arrayUtil/uniqBy/index.test.ts | 21 +++ package/arrayUtil/uniqBy/index.ts | 17 +++ package/arrayUtil/unique/index.test.ts | 12 ++ package/arrayUtil/unique/index.ts | 3 + package/arrayUtil/unzip/index.test.ts | 20 +++ package/arrayUtil/unzip/index.ts | 7 + package/arrayUtil/zip/index.test.ts | 15 ++ package/arrayUtil/zip/index.ts | 7 + package/asyncUtil/eachAsync/index.test.ts | 14 ++ package/asyncUtil/eachAsync/index.ts | 13 ++ package/asyncUtil/filterAsync/index.test.ts | 10 ++ package/asyncUtil/filterAsync/index.ts | 15 ++ package/asyncUtil/index.ts | 6 + package/asyncUtil/mapAsync/index.test.ts | 30 ++++ package/asyncUtil/mapAsync/index.ts | 25 ++++ package/asyncUtil/pLimit/index.test.ts | 32 +++++ package/asyncUtil/pLimit/index.ts | 42 ++++++ package/asyncUtil/parallel/index.test.ts | 14 ++ package/asyncUtil/parallel/index.ts | 5 + package/asyncUtil/series/index.test.ts | 22 +++ package/asyncUtil/series/index.ts | 11 ++ package/collectionUtil/_toEntries.ts | 9 ++ package/collectionUtil/entries/index.test.ts | 15 ++ package/collectionUtil/entries/index.ts | 5 + package/collectionUtil/every/index.test.ts | 12 ++ package/collectionUtil/every/index.ts | 10 ++ package/collectionUtil/filter/index.test.ts | 12 ++ package/collectionUtil/filter/index.ts | 10 ++ package/collectionUtil/find/index.test.ts | 12 ++ package/collectionUtil/find/index.ts | 12 ++ package/collectionUtil/forEach/index.test.ts | 16 +++ package/collectionUtil/forEach/index.ts | 10 ++ package/collectionUtil/includes/index.test.ts | 13 ++ package/collectionUtil/includes/index.ts | 12 ++ package/collectionUtil/index.ts | 12 ++ package/collectionUtil/keys/index.test.ts | 12 ++ package/collectionUtil/keys/index.ts | 9 ++ package/collectionUtil/map/index.test.ts | 15 ++ package/collectionUtil/map/index.ts | 10 ++ package/collectionUtil/reduce/index.test.ts | 12 ++ package/collectionUtil/reduce/index.ts | 12 ++ package/collectionUtil/size/index.test.ts | 15 ++ package/collectionUtil/size/index.ts | 13 ++ package/collectionUtil/some/index.test.ts | 12 ++ package/collectionUtil/some/index.ts | 10 ++ package/collectionUtil/values/index.test.ts | 12 ++ package/collectionUtil/values/index.ts | 9 ++ package/dateUtil/_toDate.ts | 5 + package/dateUtil/addDays/index.test.ts | 9 ++ package/dateUtil/addDays/index.ts | 7 + package/dateUtil/addHours/index.test.ts | 9 ++ package/dateUtil/addHours/index.ts | 7 + .../dateUtil/differenceInDays/index.test.ts | 12 ++ package/dateUtil/differenceInDays/index.ts | 10 ++ package/dateUtil/endOfDay/index.test.ts | 15 ++ package/dateUtil/endOfDay/index.ts | 7 + package/dateUtil/formatDate/index.test.ts | 14 ++ package/dateUtil/formatDate/index.ts | 26 ++++ package/dateUtil/index.ts | 11 ++ package/dateUtil/isAfter/index.test.ts | 8 ++ package/dateUtil/isAfter/index.ts | 5 + package/dateUtil/isBefore/index.test.ts | 8 ++ package/dateUtil/isBefore/index.ts | 5 + package/dateUtil/isSameDay/index.test.ts | 14 ++ package/dateUtil/isSameDay/index.ts | 12 ++ package/dateUtil/startOfDay/index.test.ts | 15 ++ package/dateUtil/startOfDay/index.ts | 7 + package/dateUtil/subDays/index.test.ts | 9 ++ package/dateUtil/subDays/index.ts | 7 + package/dateUtil/subHours/index.test.ts | 9 ++ package/dateUtil/subHours/index.ts | 7 + package/functionUtil/compose/index.test.ts | 13 ++ package/functionUtil/compose/index.ts | 6 + package/functionUtil/identity/index.test.ts | 11 ++ package/functionUtil/identity/index.ts | 3 + package/functionUtil/index.ts | 6 + package/functionUtil/memoize/index.test.ts | 22 +++ package/functionUtil/memoize/index.ts | 31 ++++ package/functionUtil/noop/index.test.ts | 8 ++ package/functionUtil/noop/index.ts | 1 + package/functionUtil/once/index.test.ts | 13 ++ package/functionUtil/once/index.ts | 15 ++ package/functionUtil/pipe/index.test.ts | 13 ++ package/functionUtil/pipe/index.ts | 5 + package/index.ts | 16 +++ package/langUtil/castArray/index.test.ts | 12 ++ package/langUtil/castArray/index.ts | 3 + package/langUtil/defaultTo/index.test.ts | 13 ++ package/langUtil/defaultTo/index.ts | 11 ++ package/langUtil/index.ts | 6 + package/langUtil/isEqual/index.test.ts | 21 +++ package/langUtil/isEqual/index.ts | 134 ++++++++++++++++++ package/langUtil/toBoolean/index.test.ts | 14 ++ package/langUtil/toBoolean/index.ts | 26 ++++ package/langUtil/toNumber/index.test.ts | 12 ++ package/langUtil/toNumber/index.ts | 12 ++ package/langUtil/toString/index.test.ts | 13 ++ package/langUtil/toString/index.ts | 15 ++ package/mathUtil/index.ts | 8 ++ package/mathUtil/max/index.test.ts | 12 ++ package/mathUtil/max/index.ts | 9 ++ package/mathUtil/maxBy/index.test.ts | 10 ++ package/mathUtil/maxBy/index.ts | 22 +++ package/mathUtil/mean/index.test.ts | 12 ++ package/mathUtil/mean/index.ts | 8 ++ package/mathUtil/meanBy/index.test.ts | 8 ++ package/mathUtil/meanBy/index.ts | 12 ++ package/mathUtil/median/index.test.ts | 12 ++ package/mathUtil/median/index.ts | 14 ++ package/mathUtil/min/index.test.ts | 12 ++ package/mathUtil/min/index.ts | 9 ++ package/mathUtil/minBy/index.test.ts | 10 ++ package/mathUtil/minBy/index.ts | 22 +++ package/mathUtil/sumBy/index.test.ts | 8 ++ package/mathUtil/sumBy/index.ts | 8 ++ package/numberUtil/_precision.ts | 17 +++ package/numberUtil/ceil/index.test.ts | 12 ++ package/numberUtil/ceil/index.ts | 5 + package/numberUtil/clamp/index.test.ts | 13 ++ package/numberUtil/clamp/index.ts | 5 + package/numberUtil/floor/index.test.ts | 12 ++ package/numberUtil/floor/index.ts | 5 + package/numberUtil/inRange/index.test.ts | 16 +++ package/numberUtil/inRange/index.ts | 12 ++ package/numberUtil/index.ts | 10 +- package/numberUtil/random/index.test.ts | 28 ++++ package/numberUtil/random/index.ts | 27 ++++ package/numberUtil/round/index.test.ts | 12 ++ package/numberUtil/round/index.ts | 5 + package/objectUtil/deepClone/index.test.ts | 39 +++++ package/objectUtil/deepClone/index.ts | 86 +++++++++++ package/objectUtil/defaults/index.test.ts | 16 +++ package/objectUtil/defaults/index.ts | 16 +++ package/objectUtil/get/index.test.ts | 22 +++ package/objectUtil/get/index.ts | 34 +++++ package/objectUtil/has/index.test.ts | 16 +++ package/objectUtil/has/index.ts | 37 +++++ package/objectUtil/index.ts | 16 ++- package/objectUtil/invert/index.test.ts | 12 ++ package/objectUtil/invert/index.ts | 12 ++ package/objectUtil/mapValues/index.test.ts | 17 +++ package/objectUtil/mapValues/index.ts | 15 ++ package/objectUtil/merge/index.test.ts | 19 +++ package/objectUtil/merge/index.ts | 34 +++++ package/objectUtil/omit/index.test.ts | 18 +++ package/objectUtil/omit/index.ts | 12 ++ package/objectUtil/pick/index.test.ts | 16 +++ package/objectUtil/pick/index.ts | 14 ++ package/objectUtil/set/index.test.ts | 20 +++ package/objectUtil/set/index.ts | 49 +++++++ package/promiseUtil/defer/index.test.ts | 18 +++ package/promiseUtil/defer/index.ts | 17 +++ package/promiseUtil/index.ts | 5 + .../promiseUtil/retryWithDelay/index.test.ts | 27 ++++ package/promiseUtil/retryWithDelay/index.ts | 37 +++++ package/promiseUtil/settle/index.test.ts | 15 ++ package/promiseUtil/settle/index.ts | 23 +++ package/promiseUtil/toResult/index.test.ts | 17 +++ package/promiseUtil/toResult/index.ts | 13 ++ package/promiseUtil/withTimeout/index.test.ts | 25 ++++ package/promiseUtil/withTimeout/index.ts | 36 +++++ package/stringUtil/_wordTokenize.ts | 13 ++ package/stringUtil/camelCase/index.test.ts | 12 ++ package/stringUtil/camelCase/index.ts | 15 ++ package/stringUtil/capitalize/index.test.ts | 12 ++ package/stringUtil/capitalize/index.ts | 7 + package/stringUtil/index.ts | 12 +- package/stringUtil/kebabCase/index.test.ts | 12 ++ package/stringUtil/kebabCase/index.ts | 5 + package/stringUtil/pascalCase/index.test.ts | 12 ++ package/stringUtil/pascalCase/index.ts | 7 + package/stringUtil/snakeCase/index.test.ts | 12 ++ package/stringUtil/snakeCase/index.ts | 5 + package/stringUtil/truncate/index.test.ts | 16 +++ package/stringUtil/truncate/index.ts | 21 +++ package/typeUtil/index.ts | 7 + package/typeUtil/isArray/index.test.ts | 12 ++ package/typeUtil/isArray/index.ts | 3 + package/typeUtil/isBoolean/index.test.ts | 12 ++ package/typeUtil/isBoolean/index.ts | 3 + package/typeUtil/isDate/index.test.ts | 12 ++ package/typeUtil/isDate/index.ts | 3 + package/typeUtil/isFunction/index.test.ts | 12 ++ package/typeUtil/isFunction/index.ts | 5 + package/typeUtil/isNil/index.test.ts | 13 ++ package/typeUtil/isNil/index.ts | 5 + package/typeUtil/isNumber/index.test.ts | 12 ++ package/typeUtil/isNumber/index.ts | 3 + package/typeUtil/isString/index.test.ts | 12 ++ package/typeUtil/isString/index.ts | 3 + 224 files changed, 3256 insertions(+), 8 deletions(-) create mode 100644 package/arrayUtil/chunk/index.test.ts create mode 100644 package/arrayUtil/chunk/index.ts create mode 100644 package/arrayUtil/compact/index.test.ts create mode 100644 package/arrayUtil/compact/index.ts create mode 100644 package/arrayUtil/difference/index.test.ts create mode 100644 package/arrayUtil/difference/index.ts create mode 100644 package/arrayUtil/first/index.test.ts create mode 100644 package/arrayUtil/first/index.ts create mode 100644 package/arrayUtil/flatten/index.test.ts create mode 100644 package/arrayUtil/flatten/index.ts create mode 100644 package/arrayUtil/flattenDeep/index.test.ts create mode 100644 package/arrayUtil/flattenDeep/index.ts create mode 100644 package/arrayUtil/groupBy/index.test.ts create mode 100644 package/arrayUtil/groupBy/index.ts create mode 100644 package/arrayUtil/index.ts create mode 100644 package/arrayUtil/intersection/index.test.ts create mode 100644 package/arrayUtil/intersection/index.ts create mode 100644 package/arrayUtil/keyBy/index.test.ts create mode 100644 package/arrayUtil/keyBy/index.ts create mode 100644 package/arrayUtil/last/index.test.ts create mode 100644 package/arrayUtil/last/index.ts create mode 100644 package/arrayUtil/partition/index.test.ts create mode 100644 package/arrayUtil/partition/index.ts create mode 100644 package/arrayUtil/sample/index.test.ts create mode 100644 package/arrayUtil/sample/index.ts create mode 100644 package/arrayUtil/sampleSize/index.test.ts create mode 100644 package/arrayUtil/sampleSize/index.ts create mode 100644 package/arrayUtil/shuffle/index.test.ts create mode 100644 package/arrayUtil/shuffle/index.ts create mode 100644 package/arrayUtil/sortBy/index.test.ts create mode 100644 package/arrayUtil/sortBy/index.ts create mode 100644 package/arrayUtil/union/index.test.ts create mode 100644 package/arrayUtil/union/index.ts create mode 100644 package/arrayUtil/uniqBy/index.test.ts create mode 100644 package/arrayUtil/uniqBy/index.ts create mode 100644 package/arrayUtil/unique/index.test.ts create mode 100644 package/arrayUtil/unique/index.ts create mode 100644 package/arrayUtil/unzip/index.test.ts create mode 100644 package/arrayUtil/unzip/index.ts create mode 100644 package/arrayUtil/zip/index.test.ts create mode 100644 package/arrayUtil/zip/index.ts create mode 100644 package/asyncUtil/eachAsync/index.test.ts create mode 100644 package/asyncUtil/eachAsync/index.ts create mode 100644 package/asyncUtil/filterAsync/index.test.ts create mode 100644 package/asyncUtil/filterAsync/index.ts create mode 100644 package/asyncUtil/index.ts create mode 100644 package/asyncUtil/mapAsync/index.test.ts create mode 100644 package/asyncUtil/mapAsync/index.ts create mode 100644 package/asyncUtil/pLimit/index.test.ts create mode 100644 package/asyncUtil/pLimit/index.ts create mode 100644 package/asyncUtil/parallel/index.test.ts create mode 100644 package/asyncUtil/parallel/index.ts create mode 100644 package/asyncUtil/series/index.test.ts create mode 100644 package/asyncUtil/series/index.ts create mode 100644 package/collectionUtil/_toEntries.ts create mode 100644 package/collectionUtil/entries/index.test.ts create mode 100644 package/collectionUtil/entries/index.ts create mode 100644 package/collectionUtil/every/index.test.ts create mode 100644 package/collectionUtil/every/index.ts create mode 100644 package/collectionUtil/filter/index.test.ts create mode 100644 package/collectionUtil/filter/index.ts create mode 100644 package/collectionUtil/find/index.test.ts create mode 100644 package/collectionUtil/find/index.ts create mode 100644 package/collectionUtil/forEach/index.test.ts create mode 100644 package/collectionUtil/forEach/index.ts create mode 100644 package/collectionUtil/includes/index.test.ts create mode 100644 package/collectionUtil/includes/index.ts create mode 100644 package/collectionUtil/index.ts create mode 100644 package/collectionUtil/keys/index.test.ts create mode 100644 package/collectionUtil/keys/index.ts create mode 100644 package/collectionUtil/map/index.test.ts create mode 100644 package/collectionUtil/map/index.ts create mode 100644 package/collectionUtil/reduce/index.test.ts create mode 100644 package/collectionUtil/reduce/index.ts create mode 100644 package/collectionUtil/size/index.test.ts create mode 100644 package/collectionUtil/size/index.ts create mode 100644 package/collectionUtil/some/index.test.ts create mode 100644 package/collectionUtil/some/index.ts create mode 100644 package/collectionUtil/values/index.test.ts create mode 100644 package/collectionUtil/values/index.ts create mode 100644 package/dateUtil/_toDate.ts create mode 100644 package/dateUtil/addDays/index.test.ts create mode 100644 package/dateUtil/addDays/index.ts create mode 100644 package/dateUtil/addHours/index.test.ts create mode 100644 package/dateUtil/addHours/index.ts create mode 100644 package/dateUtil/differenceInDays/index.test.ts create mode 100644 package/dateUtil/differenceInDays/index.ts create mode 100644 package/dateUtil/endOfDay/index.test.ts create mode 100644 package/dateUtil/endOfDay/index.ts create mode 100644 package/dateUtil/formatDate/index.test.ts create mode 100644 package/dateUtil/formatDate/index.ts create mode 100644 package/dateUtil/index.ts create mode 100644 package/dateUtil/isAfter/index.test.ts create mode 100644 package/dateUtil/isAfter/index.ts create mode 100644 package/dateUtil/isBefore/index.test.ts create mode 100644 package/dateUtil/isBefore/index.ts create mode 100644 package/dateUtil/isSameDay/index.test.ts create mode 100644 package/dateUtil/isSameDay/index.ts create mode 100644 package/dateUtil/startOfDay/index.test.ts create mode 100644 package/dateUtil/startOfDay/index.ts create mode 100644 package/dateUtil/subDays/index.test.ts create mode 100644 package/dateUtil/subDays/index.ts create mode 100644 package/dateUtil/subHours/index.test.ts create mode 100644 package/dateUtil/subHours/index.ts create mode 100644 package/functionUtil/compose/index.test.ts create mode 100644 package/functionUtil/compose/index.ts create mode 100644 package/functionUtil/identity/index.test.ts create mode 100644 package/functionUtil/identity/index.ts create mode 100644 package/functionUtil/index.ts create mode 100644 package/functionUtil/memoize/index.test.ts create mode 100644 package/functionUtil/memoize/index.ts create mode 100644 package/functionUtil/noop/index.test.ts create mode 100644 package/functionUtil/noop/index.ts create mode 100644 package/functionUtil/once/index.test.ts create mode 100644 package/functionUtil/once/index.ts create mode 100644 package/functionUtil/pipe/index.test.ts create mode 100644 package/functionUtil/pipe/index.ts create mode 100644 package/langUtil/castArray/index.test.ts create mode 100644 package/langUtil/castArray/index.ts create mode 100644 package/langUtil/defaultTo/index.test.ts create mode 100644 package/langUtil/defaultTo/index.ts create mode 100644 package/langUtil/index.ts create mode 100644 package/langUtil/isEqual/index.test.ts create mode 100644 package/langUtil/isEqual/index.ts create mode 100644 package/langUtil/toBoolean/index.test.ts create mode 100644 package/langUtil/toBoolean/index.ts create mode 100644 package/langUtil/toNumber/index.test.ts create mode 100644 package/langUtil/toNumber/index.ts create mode 100644 package/langUtil/toString/index.test.ts create mode 100644 package/langUtil/toString/index.ts create mode 100644 package/mathUtil/index.ts create mode 100644 package/mathUtil/max/index.test.ts create mode 100644 package/mathUtil/max/index.ts create mode 100644 package/mathUtil/maxBy/index.test.ts create mode 100644 package/mathUtil/maxBy/index.ts create mode 100644 package/mathUtil/mean/index.test.ts create mode 100644 package/mathUtil/mean/index.ts create mode 100644 package/mathUtil/meanBy/index.test.ts create mode 100644 package/mathUtil/meanBy/index.ts create mode 100644 package/mathUtil/median/index.test.ts create mode 100644 package/mathUtil/median/index.ts create mode 100644 package/mathUtil/min/index.test.ts create mode 100644 package/mathUtil/min/index.ts create mode 100644 package/mathUtil/minBy/index.test.ts create mode 100644 package/mathUtil/minBy/index.ts create mode 100644 package/mathUtil/sumBy/index.test.ts create mode 100644 package/mathUtil/sumBy/index.ts create mode 100644 package/numberUtil/_precision.ts create mode 100644 package/numberUtil/ceil/index.test.ts create mode 100644 package/numberUtil/ceil/index.ts create mode 100644 package/numberUtil/clamp/index.test.ts create mode 100644 package/numberUtil/clamp/index.ts create mode 100644 package/numberUtil/floor/index.test.ts create mode 100644 package/numberUtil/floor/index.ts create mode 100644 package/numberUtil/inRange/index.test.ts create mode 100644 package/numberUtil/inRange/index.ts create mode 100644 package/numberUtil/random/index.test.ts create mode 100644 package/numberUtil/random/index.ts create mode 100644 package/numberUtil/round/index.test.ts create mode 100644 package/numberUtil/round/index.ts create mode 100644 package/objectUtil/deepClone/index.test.ts create mode 100644 package/objectUtil/deepClone/index.ts create mode 100644 package/objectUtil/defaults/index.test.ts create mode 100644 package/objectUtil/defaults/index.ts create mode 100644 package/objectUtil/get/index.test.ts create mode 100644 package/objectUtil/get/index.ts create mode 100644 package/objectUtil/has/index.test.ts create mode 100644 package/objectUtil/has/index.ts create mode 100644 package/objectUtil/invert/index.test.ts create mode 100644 package/objectUtil/invert/index.ts create mode 100644 package/objectUtil/mapValues/index.test.ts create mode 100644 package/objectUtil/mapValues/index.ts create mode 100644 package/objectUtil/merge/index.test.ts create mode 100644 package/objectUtil/merge/index.ts create mode 100644 package/objectUtil/omit/index.test.ts create mode 100644 package/objectUtil/omit/index.ts create mode 100644 package/objectUtil/pick/index.test.ts create mode 100644 package/objectUtil/pick/index.ts create mode 100644 package/objectUtil/set/index.test.ts create mode 100644 package/objectUtil/set/index.ts create mode 100644 package/promiseUtil/defer/index.test.ts create mode 100644 package/promiseUtil/defer/index.ts create mode 100644 package/promiseUtil/index.ts create mode 100644 package/promiseUtil/retryWithDelay/index.test.ts create mode 100644 package/promiseUtil/retryWithDelay/index.ts create mode 100644 package/promiseUtil/settle/index.test.ts create mode 100644 package/promiseUtil/settle/index.ts create mode 100644 package/promiseUtil/toResult/index.test.ts create mode 100644 package/promiseUtil/toResult/index.ts create mode 100644 package/promiseUtil/withTimeout/index.test.ts create mode 100644 package/promiseUtil/withTimeout/index.ts create mode 100644 package/stringUtil/_wordTokenize.ts create mode 100644 package/stringUtil/camelCase/index.test.ts create mode 100644 package/stringUtil/camelCase/index.ts create mode 100644 package/stringUtil/capitalize/index.test.ts create mode 100644 package/stringUtil/capitalize/index.ts create mode 100644 package/stringUtil/kebabCase/index.test.ts create mode 100644 package/stringUtil/kebabCase/index.ts create mode 100644 package/stringUtil/pascalCase/index.test.ts create mode 100644 package/stringUtil/pascalCase/index.ts create mode 100644 package/stringUtil/snakeCase/index.test.ts create mode 100644 package/stringUtil/snakeCase/index.ts create mode 100644 package/stringUtil/truncate/index.test.ts create mode 100644 package/stringUtil/truncate/index.ts create mode 100644 package/typeUtil/isArray/index.test.ts create mode 100644 package/typeUtil/isArray/index.ts create mode 100644 package/typeUtil/isBoolean/index.test.ts create mode 100644 package/typeUtil/isBoolean/index.ts create mode 100644 package/typeUtil/isDate/index.test.ts create mode 100644 package/typeUtil/isDate/index.ts create mode 100644 package/typeUtil/isFunction/index.test.ts create mode 100644 package/typeUtil/isFunction/index.ts create mode 100644 package/typeUtil/isNil/index.test.ts create mode 100644 package/typeUtil/isNil/index.ts create mode 100644 package/typeUtil/isNumber/index.test.ts create mode 100644 package/typeUtil/isNumber/index.ts create mode 100644 package/typeUtil/isString/index.test.ts create mode 100644 package/typeUtil/isString/index.ts diff --git a/package.json b/package.json index 6776b43..c39f9db 100644 --- a/package.json +++ b/package.json @@ -35,21 +35,56 @@ "require": "./dist/types/stringUtil/index.js", "import": "./dist/types/stringUtil/index.js" }, + "./arrayUtil": { + "types": "./dist/types/arrayUtil/index.d.ts", + "require": "./dist/types/arrayUtil/index.js", + "import": "./dist/types/arrayUtil/index.js" + }, + "./collectionUtil": { + "types": "./dist/types/collectionUtil/index.d.ts", + "require": "./dist/types/collectionUtil/index.js", + "import": "./dist/types/collectionUtil/index.js" + }, "./objectUtil": { "types": "./dist/types/objectUtil/index.d.ts", "require": "./dist/types/objectUtil/index.js", "import": "./dist/types/objectUtil/index.js" }, + "./asyncUtil": { + "types": "./dist/types/asyncUtil/index.d.ts", + "require": "./dist/types/asyncUtil/index.js", + "import": "./dist/types/asyncUtil/index.js" + }, "./cookieUtil": { "types": "./dist/types/cookieUtil/index.d.ts", "require": "./dist/types/cookieUtil/index.js", "import": "./dist/types/cookieUtil/index.js" }, + "./dateUtil": { + "types": "./dist/types/dateUtil/index.d.ts", + "require": "./dist/types/dateUtil/index.js", + "import": "./dist/types/dateUtil/index.js" + }, + "./langUtil": { + "types": "./dist/types/langUtil/index.d.ts", + "require": "./dist/types/langUtil/index.js", + "import": "./dist/types/langUtil/index.js" + }, + "./mathUtil": { + "types": "./dist/types/mathUtil/index.d.ts", + "require": "./dist/types/mathUtil/index.js", + "import": "./dist/types/mathUtil/index.js" + }, "./numberUtil": { "types": "./dist/types/numberUtil/index.d.ts", "require": "./dist/types/numberUtil/index.js", "import": "./dist/types/numberUtil/index.js" }, + "./promiseUtil": { + "types": "./dist/types/promiseUtil/index.d.ts", + "require": "./dist/types/promiseUtil/index.js", + "import": "./dist/types/promiseUtil/index.js" + }, "./validationUtil": { "types": "./dist/types/validationUtil/index.d.ts", "require": "./dist/types/validationUtil/index.js", @@ -60,6 +95,11 @@ "require": "./dist/types/commonUtil/index.js", "import": "./dist/types/commonUtil/index.js" }, + "./functionUtil": { + "types": "./dist/types/functionUtil/index.d.ts", + "require": "./dist/types/functionUtil/index.js", + "import": "./dist/types/functionUtil/index.js" + }, "./searchQueryUtil": { "types": "./dist/types/searchQueryUtil/index.d.ts", "require": "./dist/types/searchQueryUtil/index.js", diff --git a/package/arrayUtil/chunk/index.test.ts b/package/arrayUtil/chunk/index.test.ts new file mode 100644 index 0000000..d4e01d6 --- /dev/null +++ b/package/arrayUtil/chunk/index.test.ts @@ -0,0 +1,17 @@ +import { describe, expect, test } from "vitest"; +import chunk from "."; + +describe("chunk 유틸 함수 테스트", () => { + test("배열을 지정된 크기로 분할한다.", () => { + expect(chunk([1, 2, 3, 4, 5], 2)).toEqual([[1, 2], [3, 4], [5]]); + }); + + test("size가 1보다 작거나 유효하지 않으면 1로 처리한다.", () => { + expect(chunk([1, 2, 3], 0)).toEqual([[1], [2], [3]]); + expect(chunk([1, 2, 3], Number.NaN)).toEqual([[1], [2], [3]]); + }); + + test("빈 배열은 빈 배열을 반환한다.", () => { + expect(chunk([], 3)).toEqual([]); + }); +}); diff --git a/package/arrayUtil/chunk/index.ts b/package/arrayUtil/chunk/index.ts new file mode 100644 index 0000000..93161bf --- /dev/null +++ b/package/arrayUtil/chunk/index.ts @@ -0,0 +1,14 @@ +export default function chunk(array: readonly T[], size: number = 1): T[][] { + if (array.length === 0) { + return []; + } + + const normalizedSize = Number.isFinite(size) ? Math.max(1, Math.floor(size)) : 1; + const result: T[][] = []; + + for (let i = 0; i < array.length; i += normalizedSize) { + result.push(array.slice(i, i + normalizedSize)); + } + + return result; +} diff --git a/package/arrayUtil/compact/index.test.ts b/package/arrayUtil/compact/index.test.ts new file mode 100644 index 0000000..e278287 --- /dev/null +++ b/package/arrayUtil/compact/index.test.ts @@ -0,0 +1,12 @@ +import { describe, expect, test } from "vitest"; +import compact from "."; + +describe("compact 유틸 함수 테스트", () => { + test("falsy 값을 제거한다.", () => { + expect(compact([0, 1, false, 2, "", 3, null, undefined])).toEqual([1, 2, 3]); + }); + + test("truthy 값은 유지한다.", () => { + expect(compact(["a", "b", "c"])).toEqual(["a", "b", "c"]); + }); +}); diff --git a/package/arrayUtil/compact/index.ts b/package/arrayUtil/compact/index.ts new file mode 100644 index 0000000..fe56fde --- /dev/null +++ b/package/arrayUtil/compact/index.ts @@ -0,0 +1,7 @@ +type Falsy = false | 0 | 0n | "" | null | undefined; + +export default function compact( + array: readonly T[] +): Array> { + return array.filter(Boolean) as Array>; +} diff --git a/package/arrayUtil/difference/index.test.ts b/package/arrayUtil/difference/index.test.ts new file mode 100644 index 0000000..e67d675 --- /dev/null +++ b/package/arrayUtil/difference/index.test.ts @@ -0,0 +1,12 @@ +import { describe, expect, test } from "vitest"; +import difference from "."; + +describe("difference 유틸 함수 테스트", () => { + test("두 번째 배열에 없는 값만 반환한다.", () => { + expect(difference([1, 2, 3, 4], [2, 4])).toEqual([1, 3]); + }); + + test("제외 배열이 비어 있으면 원본 복사본을 반환한다.", () => { + expect(difference([1, 2], [])).toEqual([1, 2]); + }); +}); diff --git a/package/arrayUtil/difference/index.ts b/package/arrayUtil/difference/index.ts new file mode 100644 index 0000000..5c50b01 --- /dev/null +++ b/package/arrayUtil/difference/index.ts @@ -0,0 +1,4 @@ +export default function difference(array: readonly T[], values: readonly T[]): T[] { + const excludeSet = new Set(values); + return array.filter((item) => !excludeSet.has(item)); +} diff --git a/package/arrayUtil/first/index.test.ts b/package/arrayUtil/first/index.test.ts new file mode 100644 index 0000000..b14e673 --- /dev/null +++ b/package/arrayUtil/first/index.test.ts @@ -0,0 +1,12 @@ +import { describe, expect, test } from "vitest"; +import first from "."; + +describe("first 유틸 함수 테스트", () => { + test("첫 번째 값을 반환한다.", () => { + expect(first([1, 2, 3])).toBe(1); + }); + + test("빈 배열은 undefined를 반환한다.", () => { + expect(first([])).toBeUndefined(); + }); +}); diff --git a/package/arrayUtil/first/index.ts b/package/arrayUtil/first/index.ts new file mode 100644 index 0000000..d67a7c4 --- /dev/null +++ b/package/arrayUtil/first/index.ts @@ -0,0 +1,3 @@ +export default function first(array: readonly T[]): T | undefined { + return array[0]; +} diff --git a/package/arrayUtil/flatten/index.test.ts b/package/arrayUtil/flatten/index.test.ts new file mode 100644 index 0000000..0d964c7 --- /dev/null +++ b/package/arrayUtil/flatten/index.test.ts @@ -0,0 +1,12 @@ +import { describe, expect, test } from "vitest"; +import flatten from "."; + +describe("flatten 유틸 함수 테스트", () => { + test("1단계 중첩 배열을 평탄화한다.", () => { + expect(flatten([1, [2, 3], [4]])).toEqual([1, 2, 3, 4]); + }); + + test("빈 배열은 빈 배열을 반환한다.", () => { + expect(flatten([])).toEqual([]); + }); +}); diff --git a/package/arrayUtil/flatten/index.ts b/package/arrayUtil/flatten/index.ts new file mode 100644 index 0000000..ea4f5f2 --- /dev/null +++ b/package/arrayUtil/flatten/index.ts @@ -0,0 +1,3 @@ +export default function flatten(array: readonly (T | readonly T[])[]): T[] { + return array.flat(1) as T[]; +} diff --git a/package/arrayUtil/flattenDeep/index.test.ts b/package/arrayUtil/flattenDeep/index.test.ts new file mode 100644 index 0000000..d1d998e --- /dev/null +++ b/package/arrayUtil/flattenDeep/index.test.ts @@ -0,0 +1,12 @@ +import { describe, expect, test } from "vitest"; +import flattenDeep from "."; + +describe("flattenDeep 유틸 함수 테스트", () => { + test("모든 중첩 배열을 평탄화한다.", () => { + expect(flattenDeep([1, [2, [3, [4]]]])).toEqual([1, 2, 3, 4]); + }); + + test("원시 값이 섞여 있어도 동작한다.", () => { + expect(flattenDeep(["a", [1, ["b"]]])).toEqual(["a", 1, "b"]); + }); +}); diff --git a/package/arrayUtil/flattenDeep/index.ts b/package/arrayUtil/flattenDeep/index.ts new file mode 100644 index 0000000..d28dddf --- /dev/null +++ b/package/arrayUtil/flattenDeep/index.ts @@ -0,0 +1,16 @@ +function flatDeep(array: readonly unknown[], result: T[]): T[] { + array.forEach((value) => { + if (Array.isArray(value)) { + flatDeep(value, result); + return; + } + + result.push(value as T); + }); + + return result; +} + +export default function flattenDeep(array: readonly unknown[]): T[] { + return flatDeep(array, []); +} diff --git a/package/arrayUtil/groupBy/index.test.ts b/package/arrayUtil/groupBy/index.test.ts new file mode 100644 index 0000000..819b550 --- /dev/null +++ b/package/arrayUtil/groupBy/index.test.ts @@ -0,0 +1,24 @@ +import { describe, expect, test } from "vitest"; +import groupBy from "."; + +describe("groupBy 유틸 함수 테스트", () => { + test("iteratee 결과 기준으로 그룹화한다.", () => { + const users = [ + { id: 1, role: "admin" }, + { id: 2, role: "user" }, + { id: 3, role: "admin" }, + ]; + + expect(groupBy(users, (user) => user.role)).toEqual({ + admin: [ + { id: 1, role: "admin" }, + { id: 3, role: "admin" }, + ], + user: [{ id: 2, role: "user" }], + }); + }); + + test("빈 배열은 빈 객체를 반환한다.", () => { + expect(groupBy([], (value) => value)).toEqual({}); + }); +}); diff --git a/package/arrayUtil/groupBy/index.ts b/package/arrayUtil/groupBy/index.ts new file mode 100644 index 0000000..186ffc5 --- /dev/null +++ b/package/arrayUtil/groupBy/index.ts @@ -0,0 +1,18 @@ +export default function groupBy( + array: readonly T[], + iteratee: (value: T, index: number, array: readonly T[]) => K +): Record { + const result = {} as Record; + + array.forEach((value, index) => { + const key = iteratee(value, index, array); + + if (!Object.prototype.hasOwnProperty.call(result, key)) { + result[key] = []; + } + + result[key].push(value); + }); + + return result; +} diff --git a/package/arrayUtil/index.ts b/package/arrayUtil/index.ts new file mode 100644 index 0000000..78a8ce7 --- /dev/null +++ b/package/arrayUtil/index.ts @@ -0,0 +1,20 @@ +export { default as chunk } from "./chunk"; +export { default as compact } from "./compact"; +export { default as unique } from "./unique"; +export { default as uniqBy } from "./uniqBy"; +export { default as groupBy } from "./groupBy"; +export { default as keyBy } from "./keyBy"; +export { default as flatten } from "./flatten"; +export { default as flattenDeep } from "./flattenDeep"; +export { default as difference } from "./difference"; +export { default as intersection } from "./intersection"; +export { default as union } from "./union"; +export { default as zip } from "./zip"; +export { default as unzip } from "./unzip"; +export { default as first } from "./first"; +export { default as last } from "./last"; +export { default as shuffle } from "./shuffle"; +export { default as sample } from "./sample"; +export { default as sampleSize } from "./sampleSize"; +export { default as partition } from "./partition"; +export { default as sortBy } from "./sortBy"; diff --git a/package/arrayUtil/intersection/index.test.ts b/package/arrayUtil/intersection/index.test.ts new file mode 100644 index 0000000..8bdd2db --- /dev/null +++ b/package/arrayUtil/intersection/index.test.ts @@ -0,0 +1,12 @@ +import { describe, expect, test } from "vitest"; +import intersection from "."; + +describe("intersection 유틸 함수 테스트", () => { + test("교집합 값을 반환한다.", () => { + expect(intersection([1, 2, 2, 3], [2, 3, 4])).toEqual([2, 3]); + }); + + test("교집합이 없으면 빈 배열을 반환한다.", () => { + expect(intersection([1, 2], [3, 4])).toEqual([]); + }); +}); diff --git a/package/arrayUtil/intersection/index.ts b/package/arrayUtil/intersection/index.ts new file mode 100644 index 0000000..f27df37 --- /dev/null +++ b/package/arrayUtil/intersection/index.ts @@ -0,0 +1,16 @@ +export default function intersection( + first: readonly T[], + second: readonly T[] +): T[] { + const secondSet = new Set(second); + const seen = new Set(); + + return first.filter((value) => { + if (!secondSet.has(value) || seen.has(value)) { + return false; + } + + seen.add(value); + return true; + }); +} diff --git a/package/arrayUtil/keyBy/index.test.ts b/package/arrayUtil/keyBy/index.test.ts new file mode 100644 index 0000000..2b6c379 --- /dev/null +++ b/package/arrayUtil/keyBy/index.test.ts @@ -0,0 +1,27 @@ +import { describe, expect, test } from "vitest"; +import keyBy from "."; + +describe("keyBy 유틸 함수 테스트", () => { + test("iteratee 결과를 key로 객체를 생성한다.", () => { + const users = [ + { id: 1, name: "Kim" }, + { id: 2, name: "Lee" }, + ]; + + expect(keyBy(users, (user) => user.id)).toEqual({ + 1: { id: 1, name: "Kim" }, + 2: { id: 2, name: "Lee" }, + }); + }); + + test("같은 key가 있으면 마지막 값으로 덮어쓴다.", () => { + const users = [ + { id: 1, name: "Kim" }, + { id: 1, name: "Park" }, + ]; + + expect(keyBy(users, (user) => user.id)).toEqual({ + 1: { id: 1, name: "Park" }, + }); + }); +}); diff --git a/package/arrayUtil/keyBy/index.ts b/package/arrayUtil/keyBy/index.ts new file mode 100644 index 0000000..5d9d9df --- /dev/null +++ b/package/arrayUtil/keyBy/index.ts @@ -0,0 +1,13 @@ +export default function keyBy( + array: readonly T[], + iteratee: (value: T, index: number, array: readonly T[]) => K +): Record { + const result = {} as Record; + + array.forEach((value, index) => { + const key = iteratee(value, index, array); + result[key] = value; + }); + + return result; +} diff --git a/package/arrayUtil/last/index.test.ts b/package/arrayUtil/last/index.test.ts new file mode 100644 index 0000000..9001679 --- /dev/null +++ b/package/arrayUtil/last/index.test.ts @@ -0,0 +1,12 @@ +import { describe, expect, test } from "vitest"; +import last from "."; + +describe("last 유틸 함수 테스트", () => { + test("마지막 값을 반환한다.", () => { + expect(last([1, 2, 3])).toBe(3); + }); + + test("빈 배열은 undefined를 반환한다.", () => { + expect(last([])).toBeUndefined(); + }); +}); diff --git a/package/arrayUtil/last/index.ts b/package/arrayUtil/last/index.ts new file mode 100644 index 0000000..2b09ca6 --- /dev/null +++ b/package/arrayUtil/last/index.ts @@ -0,0 +1,7 @@ +export default function last(array: readonly T[]): T | undefined { + if (array.length === 0) { + return undefined; + } + + return array[array.length - 1]; +} diff --git a/package/arrayUtil/partition/index.test.ts b/package/arrayUtil/partition/index.test.ts new file mode 100644 index 0000000..916db60 --- /dev/null +++ b/package/arrayUtil/partition/index.test.ts @@ -0,0 +1,15 @@ +import { describe, expect, test } from "vitest"; +import partition from "."; + +describe("partition 유틸 함수 테스트", () => { + test("조건에 따라 배열을 두 그룹으로 분리한다.", () => { + expect(partition([1, 2, 3, 4], (value) => value % 2 === 0)).toEqual([ + [2, 4], + [1, 3], + ]); + }); + + test("모든 값이 참이면 두 번째 배열은 비어 있다.", () => { + expect(partition([1, 2], () => true)).toEqual([[1, 2], []]); + }); +}); diff --git a/package/arrayUtil/partition/index.ts b/package/arrayUtil/partition/index.ts new file mode 100644 index 0000000..4dfe331 --- /dev/null +++ b/package/arrayUtil/partition/index.ts @@ -0,0 +1,18 @@ +export default function partition( + array: readonly T[], + predicate: (value: T, index: number, array: readonly T[]) => boolean +): [T[], T[]] { + const truthy: T[] = []; + const falsy: T[] = []; + + array.forEach((value, index) => { + if (predicate(value, index, array)) { + truthy.push(value); + return; + } + + falsy.push(value); + }); + + return [truthy, falsy]; +} diff --git a/package/arrayUtil/sample/index.test.ts b/package/arrayUtil/sample/index.test.ts new file mode 100644 index 0000000..c56e469 --- /dev/null +++ b/package/arrayUtil/sample/index.test.ts @@ -0,0 +1,16 @@ +import { describe, expect, test, vi } from "vitest"; +import sample from "."; + +describe("sample 유틸 함수 테스트", () => { + test("랜덤 요소 하나를 반환한다.", () => { + vi.spyOn(Math, "random").mockReturnValue(0.6); + + expect(sample([10, 20, 30])).toBe(20); + + vi.restoreAllMocks(); + }); + + test("빈 배열은 undefined를 반환한다.", () => { + expect(sample([])).toBeUndefined(); + }); +}); diff --git a/package/arrayUtil/sample/index.ts b/package/arrayUtil/sample/index.ts new file mode 100644 index 0000000..7b03add --- /dev/null +++ b/package/arrayUtil/sample/index.ts @@ -0,0 +1,8 @@ +export default function sample(array: readonly T[]): T | undefined { + if (array.length === 0) { + return undefined; + } + + const index = Math.floor(Math.random() * array.length); + return array[index]; +} diff --git a/package/arrayUtil/sampleSize/index.test.ts b/package/arrayUtil/sampleSize/index.test.ts new file mode 100644 index 0000000..0259405 --- /dev/null +++ b/package/arrayUtil/sampleSize/index.test.ts @@ -0,0 +1,19 @@ +import { describe, expect, test, vi } from "vitest"; +import sampleSize from "."; + +describe("sampleSize 유틸 함수 테스트", () => { + test("지정한 개수만큼 랜덤 샘플을 반환한다.", () => { + vi.spyOn(Math, "random") + .mockReturnValueOnce(0.1) + .mockReturnValueOnce(0.5) + .mockReturnValueOnce(0.8); + + expect(sampleSize([1, 2, 3, 4], 2)).toEqual([4, 3]); + + vi.restoreAllMocks(); + }); + + test("size가 음수면 빈 배열을 반환한다.", () => { + expect(sampleSize([1, 2, 3], -1)).toEqual([]); + }); +}); diff --git a/package/arrayUtil/sampleSize/index.ts b/package/arrayUtil/sampleSize/index.ts new file mode 100644 index 0000000..b6ef2a7 --- /dev/null +++ b/package/arrayUtil/sampleSize/index.ts @@ -0,0 +1,10 @@ +import shuffle from "../shuffle"; + +export default function sampleSize(array: readonly T[], size: number = 1): T[] { + if (size <= 0) { + return []; + } + + const normalizedSize = Math.min(array.length, Math.floor(size)); + return shuffle(array).slice(0, normalizedSize); +} diff --git a/package/arrayUtil/shuffle/index.test.ts b/package/arrayUtil/shuffle/index.test.ts new file mode 100644 index 0000000..99e444f --- /dev/null +++ b/package/arrayUtil/shuffle/index.test.ts @@ -0,0 +1,23 @@ +import { describe, expect, test, vi } from "vitest"; +import shuffle from "."; + +describe("shuffle 유틸 함수 테스트", () => { + test("배열 요소를 섞어 반환한다.", () => { + vi.spyOn(Math, "random") + .mockReturnValueOnce(0.1) + .mockReturnValueOnce(0.5) + .mockReturnValueOnce(0.8); + + expect(shuffle([1, 2, 3, 4])).toEqual([4, 3, 2, 1]); + + vi.restoreAllMocks(); + }); + + test("원본 배열은 변경하지 않는다.", () => { + const source = [1, 2, 3]; + + shuffle(source); + + expect(source).toEqual([1, 2, 3]); + }); +}); diff --git a/package/arrayUtil/shuffle/index.ts b/package/arrayUtil/shuffle/index.ts new file mode 100644 index 0000000..5e5aa99 --- /dev/null +++ b/package/arrayUtil/shuffle/index.ts @@ -0,0 +1,12 @@ +export default function shuffle(array: readonly T[]): T[] { + const result = [...array]; + + for (let i = result.length - 1; i > 0; i--) { + const randomIndex = Math.floor(Math.random() * (i + 1)); + const temp = result[i]; + result[i] = result[randomIndex]; + result[randomIndex] = temp; + } + + return result; +} diff --git a/package/arrayUtil/sortBy/index.test.ts b/package/arrayUtil/sortBy/index.test.ts new file mode 100644 index 0000000..65ab891 --- /dev/null +++ b/package/arrayUtil/sortBy/index.test.ts @@ -0,0 +1,27 @@ +import { describe, expect, test } from "vitest"; +import sortBy from "."; + +describe("sortBy 유틸 함수 테스트", () => { + test("기준 값을 기준으로 오름차순 정렬한다.", () => { + const users = [ + { id: 3, name: "Charlie" }, + { id: 1, name: "Alice" }, + { id: 2, name: "Bob" }, + ]; + + expect(sortBy(users, (user) => user.id)).toEqual([ + { id: 1, name: "Alice" }, + { id: 2, name: "Bob" }, + { id: 3, name: "Charlie" }, + ]); + }); + + test("원본 배열은 변경하지 않는다.", () => { + const values = [3, 1, 2]; + + const result = sortBy(values, (value) => value); + + expect(values).toEqual([3, 1, 2]); + expect(result).toEqual([1, 2, 3]); + }); +}); diff --git a/package/arrayUtil/sortBy/index.ts b/package/arrayUtil/sortBy/index.ts new file mode 100644 index 0000000..fe2ed81 --- /dev/null +++ b/package/arrayUtil/sortBy/index.ts @@ -0,0 +1,33 @@ +type SortValue = string | number | bigint | Date; + +function normalizeSortValue(value: SortValue): string | number | bigint { + if (value instanceof Date) { + return value.getTime(); + } + + return value; +} + +export default function sortBy( + array: readonly T[], + iteratee: (value: T, index: number, array: readonly T[]) => SortValue +): T[] { + return array + .map((value, index) => ({ + value, + index, + criteria: normalizeSortValue(iteratee(value, index, array)), + })) + .sort((a, b) => { + if (a.criteria === b.criteria) { + return a.index - b.index; + } + + if (typeof a.criteria === "string" || typeof b.criteria === "string") { + return String(a.criteria).localeCompare(String(b.criteria)); + } + + return a.criteria > b.criteria ? 1 : -1; + }) + .map(({ value }) => value); +} diff --git a/package/arrayUtil/union/index.test.ts b/package/arrayUtil/union/index.test.ts new file mode 100644 index 0000000..8a04c8f --- /dev/null +++ b/package/arrayUtil/union/index.test.ts @@ -0,0 +1,12 @@ +import { describe, expect, test } from "vitest"; +import union from "."; + +describe("union 유틸 함수 테스트", () => { + test("배열들의 합집합을 순서대로 반환한다.", () => { + expect(union([1, 2], [2, 3], [3, 4])).toEqual([1, 2, 3, 4]); + }); + + test("인자가 없으면 빈 배열을 반환한다.", () => { + expect(union()).toEqual([]); + }); +}); diff --git a/package/arrayUtil/union/index.ts b/package/arrayUtil/union/index.ts new file mode 100644 index 0000000..9cd7486 --- /dev/null +++ b/package/arrayUtil/union/index.ts @@ -0,0 +1,17 @@ +export default function union(...arrays: ReadonlyArray): T[] { + const result: T[] = []; + const seen = new Set(); + + arrays.forEach((array) => { + array.forEach((value) => { + if (seen.has(value)) { + return; + } + + seen.add(value); + result.push(value); + }); + }); + + return result; +} diff --git a/package/arrayUtil/uniqBy/index.test.ts b/package/arrayUtil/uniqBy/index.test.ts new file mode 100644 index 0000000..85080b9 --- /dev/null +++ b/package/arrayUtil/uniqBy/index.test.ts @@ -0,0 +1,21 @@ +import { describe, expect, test } from "vitest"; +import uniqBy from "."; + +describe("uniqBy 유틸 함수 테스트", () => { + test("기준 값이 중복되면 첫 번째 값만 유지한다.", () => { + const users = [ + { id: 1, name: "Kim" }, + { id: 1, name: "Park" }, + { id: 2, name: "Lee" }, + ]; + + expect(uniqBy(users, (user) => user.id)).toEqual([ + { id: 1, name: "Kim" }, + { id: 2, name: "Lee" }, + ]); + }); + + test("빈 배열은 빈 배열을 반환한다.", () => { + expect(uniqBy([], (value) => value)).toEqual([]); + }); +}); diff --git a/package/arrayUtil/uniqBy/index.ts b/package/arrayUtil/uniqBy/index.ts new file mode 100644 index 0000000..83986da --- /dev/null +++ b/package/arrayUtil/uniqBy/index.ts @@ -0,0 +1,17 @@ +export default function uniqBy( + array: readonly T[], + iteratee: (value: T, index: number, array: readonly T[]) => K +): T[] { + const seen = new Set(); + + return array.filter((value, index) => { + const key = iteratee(value, index, array); + + if (seen.has(key)) { + return false; + } + + seen.add(key); + return true; + }); +} diff --git a/package/arrayUtil/unique/index.test.ts b/package/arrayUtil/unique/index.test.ts new file mode 100644 index 0000000..f17b209 --- /dev/null +++ b/package/arrayUtil/unique/index.test.ts @@ -0,0 +1,12 @@ +import { describe, expect, test } from "vitest"; +import unique from "."; + +describe("unique 유틸 함수 테스트", () => { + test("중복된 값을 제거한다.", () => { + expect(unique([1, 2, 2, 3, 3, 3])).toEqual([1, 2, 3]); + }); + + test("원래 순서를 유지한다.", () => { + expect(unique(["b", "a", "b", "c", "a"])).toEqual(["b", "a", "c"]); + }); +}); diff --git a/package/arrayUtil/unique/index.ts b/package/arrayUtil/unique/index.ts new file mode 100644 index 0000000..b7674f3 --- /dev/null +++ b/package/arrayUtil/unique/index.ts @@ -0,0 +1,3 @@ +export default function unique(array: readonly T[]): T[] { + return [...new Set(array)]; +} diff --git a/package/arrayUtil/unzip/index.test.ts b/package/arrayUtil/unzip/index.test.ts new file mode 100644 index 0000000..10183c4 --- /dev/null +++ b/package/arrayUtil/unzip/index.test.ts @@ -0,0 +1,20 @@ +import { describe, expect, test } from "vitest"; +import unzip from "."; + +describe("unzip 유틸 함수 테스트", () => { + test("zip된 배열을 원래 형태로 되돌린다.", () => { + expect( + unzip([ + ["a", 1], + ["b", 2], + ]) + ).toEqual([ + ["a", "b"], + [1, 2], + ]); + }); + + test("빈 배열은 빈 배열을 반환한다.", () => { + expect(unzip([])).toEqual([]); + }); +}); diff --git a/package/arrayUtil/unzip/index.ts b/package/arrayUtil/unzip/index.ts new file mode 100644 index 0000000..987b520 --- /dev/null +++ b/package/arrayUtil/unzip/index.ts @@ -0,0 +1,7 @@ +export default function unzip(array: ReadonlyArray): T[][] { + const maxLength = array.reduce((max, row) => Math.max(max, row.length), 0); + + return Array.from({ length: maxLength }, (_, index) => + array.map((row) => row[index]) + ); +} diff --git a/package/arrayUtil/zip/index.test.ts b/package/arrayUtil/zip/index.test.ts new file mode 100644 index 0000000..b3f95c9 --- /dev/null +++ b/package/arrayUtil/zip/index.test.ts @@ -0,0 +1,15 @@ +import { describe, expect, test } from "vitest"; +import zip from "."; + +describe("zip 유틸 함수 테스트", () => { + test("같은 인덱스끼리 묶는다.", () => { + expect(zip(["a", "b"], [1, 2], [true, false])).toEqual([ + ["a", 1, true], + ["b", 2, false], + ]); + }); + + test("길이가 다르면 undefined로 채운다.", () => { + expect(zip([1], [2, 3])).toEqual([[1, 2], [undefined, 3]]); + }); +}); diff --git a/package/arrayUtil/zip/index.ts b/package/arrayUtil/zip/index.ts new file mode 100644 index 0000000..1e735dd --- /dev/null +++ b/package/arrayUtil/zip/index.ts @@ -0,0 +1,7 @@ +export default function zip(...arrays: ReadonlyArray): Array> { + const maxLength = arrays.reduce((max, array) => Math.max(max, array.length), 0); + + return Array.from({ length: maxLength }, (_, index) => + arrays.map((array) => array[index]) + ); +} diff --git a/package/asyncUtil/eachAsync/index.test.ts b/package/asyncUtil/eachAsync/index.test.ts new file mode 100644 index 0000000..f066611 --- /dev/null +++ b/package/asyncUtil/eachAsync/index.test.ts @@ -0,0 +1,14 @@ +import { describe, expect, test } from "vitest"; +import eachAsync from "."; + +describe("async eachAsync 유틸 함수 테스트", () => { + test("모든 요소를 순회한다.", async () => { + const result: number[] = []; + + await eachAsync([1, 2, 3], async (value) => { + result.push(value * 2); + }); + + expect(result).toEqual([2, 4, 6]); + }); +}); diff --git a/package/asyncUtil/eachAsync/index.ts b/package/asyncUtil/eachAsync/index.ts new file mode 100644 index 0000000..1eaf6f4 --- /dev/null +++ b/package/asyncUtil/eachAsync/index.ts @@ -0,0 +1,13 @@ +import mapAsync from "../mapAsync"; + +interface EachAsyncOptions { + concurrency?: number; +} + +export default async function eachAsync( + array: readonly T[], + iteratee: (value: T, index: number, array: readonly T[]) => Promise | void, + options: EachAsyncOptions = {} +): Promise { + await mapAsync(array, iteratee, options); +} diff --git a/package/asyncUtil/filterAsync/index.test.ts b/package/asyncUtil/filterAsync/index.test.ts new file mode 100644 index 0000000..5fcf593 --- /dev/null +++ b/package/asyncUtil/filterAsync/index.test.ts @@ -0,0 +1,10 @@ +import { describe, expect, test } from "vitest"; +import filterAsync from "."; + +describe("async filterAsync 유틸 함수 테스트", () => { + test("비동기 predicate로 필터링한다.", async () => { + const result = await filterAsync([1, 2, 3, 4], async (value) => value % 2 === 0); + + expect(result).toEqual([2, 4]); + }); +}); diff --git a/package/asyncUtil/filterAsync/index.ts b/package/asyncUtil/filterAsync/index.ts new file mode 100644 index 0000000..967e156 --- /dev/null +++ b/package/asyncUtil/filterAsync/index.ts @@ -0,0 +1,15 @@ +import mapAsync from "../mapAsync"; + +interface FilterAsyncOptions { + concurrency?: number; +} + +export default async function filterAsync( + array: readonly T[], + predicate: (value: T, index: number, array: readonly T[]) => Promise | boolean, + options: FilterAsyncOptions = {} +): Promise { + const matches = await mapAsync(array, predicate, options); + + return array.filter((_, index) => matches[index]); +} diff --git a/package/asyncUtil/index.ts b/package/asyncUtil/index.ts new file mode 100644 index 0000000..9caa0b9 --- /dev/null +++ b/package/asyncUtil/index.ts @@ -0,0 +1,6 @@ +export { default as eachAsync } from "./eachAsync"; +export { default as filterAsync } from "./filterAsync"; +export { default as mapAsync } from "./mapAsync"; +export { default as parallel } from "./parallel"; +export { default as pLimit } from "./pLimit"; +export { default as series } from "./series"; diff --git a/package/asyncUtil/mapAsync/index.test.ts b/package/asyncUtil/mapAsync/index.test.ts new file mode 100644 index 0000000..2fee775 --- /dev/null +++ b/package/asyncUtil/mapAsync/index.test.ts @@ -0,0 +1,30 @@ +import { describe, expect, test } from "vitest"; +import mapAsync from "."; + +describe("async mapAsync 유틸 함수 테스트", () => { + test("비동기 iteratee 결과를 매핑한다.", async () => { + const result = await mapAsync([1, 2, 3], async (value) => value * 2); + + expect(result).toEqual([2, 4, 6]); + }); + + test("concurrency 옵션을 적용한다.", async () => { + let running = 0; + let maxRunning = 0; + + const result = await mapAsync( + [1, 2, 3, 4], + async (value) => { + running++; + maxRunning = Math.max(maxRunning, running); + await new Promise((resolve) => setTimeout(resolve, 20)); + running--; + return value; + }, + { concurrency: 2 } + ); + + expect(result).toEqual([1, 2, 3, 4]); + expect(maxRunning).toBeLessThanOrEqual(2); + }); +}); diff --git a/package/asyncUtil/mapAsync/index.ts b/package/asyncUtil/mapAsync/index.ts new file mode 100644 index 0000000..2a1d870 --- /dev/null +++ b/package/asyncUtil/mapAsync/index.ts @@ -0,0 +1,25 @@ +import pLimit from "../pLimit"; + +interface MapAsyncOptions { + concurrency?: number; +} + +export default async function mapAsync( + array: readonly T[], + iteratee: (value: T, index: number, array: readonly T[]) => Promise | R, + options: MapAsyncOptions = {} +): Promise { + const { concurrency = Number.POSITIVE_INFINITY } = options; + + if (!Number.isFinite(concurrency)) { + return Promise.all(array.map((value, index) => iteratee(value, index, array))); + } + + const limit = pLimit(concurrency); + + return Promise.all( + array.map((value, index) => + limit(async () => iteratee(value, index, array)) + ) + ); +} diff --git a/package/asyncUtil/pLimit/index.test.ts b/package/asyncUtil/pLimit/index.test.ts new file mode 100644 index 0000000..ffcecf9 --- /dev/null +++ b/package/asyncUtil/pLimit/index.test.ts @@ -0,0 +1,32 @@ +import { describe, expect, test } from "vitest"; +import pLimit from "."; + +describe("async pLimit 유틸 함수 테스트", () => { + test("동시 실행 수를 제한한다.", async () => { + const limit = pLimit(2); + + let running = 0; + let maxRunning = 0; + + const createTask = (delay: number) => + limit(async () => { + running++; + maxRunning = Math.max(maxRunning, running); + + await new Promise((resolve) => setTimeout(resolve, delay)); + + running--; + return delay; + }); + + const results = await Promise.all([ + createTask(30), + createTask(30), + createTask(30), + createTask(30), + ]); + + expect(results).toEqual([30, 30, 30, 30]); + expect(maxRunning).toBeLessThanOrEqual(2); + }); +}); diff --git a/package/asyncUtil/pLimit/index.ts b/package/asyncUtil/pLimit/index.ts new file mode 100644 index 0000000..726b9d8 --- /dev/null +++ b/package/asyncUtil/pLimit/index.ts @@ -0,0 +1,42 @@ +export type LimitTask = () => Promise; + +export default function pLimit(concurrency: number = 1) { + const normalizedConcurrency = Math.max(1, Math.floor(concurrency)); + + let activeCount = 0; + const queue: Array<() => void> = []; + + const runNext = () => { + if (activeCount >= normalizedConcurrency || queue.length === 0) { + return; + } + + const nextTask = queue.shift(); + + if (!nextTask) { + return; + } + + activeCount++; + nextTask(); + }; + + const schedule = (task: LimitTask): Promise => { + return new Promise((resolve, reject) => { + const runTask = () => { + task() + .then(resolve) + .catch(reject) + .finally(() => { + activeCount--; + runNext(); + }); + }; + + queue.push(runTask); + runNext(); + }); + }; + + return schedule; +} diff --git a/package/asyncUtil/parallel/index.test.ts b/package/asyncUtil/parallel/index.test.ts new file mode 100644 index 0000000..bf5b46e --- /dev/null +++ b/package/asyncUtil/parallel/index.test.ts @@ -0,0 +1,14 @@ +import { describe, expect, test } from "vitest"; +import parallel from "."; + +describe("async parallel 유틸 함수 테스트", () => { + test("작업을 병렬로 실행한다.", async () => { + const result = await parallel([ + async () => 1, + async () => 2, + async () => 3, + ]); + + expect(result).toEqual([1, 2, 3]); + }); +}); diff --git a/package/asyncUtil/parallel/index.ts b/package/asyncUtil/parallel/index.ts new file mode 100644 index 0000000..13b88fd --- /dev/null +++ b/package/asyncUtil/parallel/index.ts @@ -0,0 +1,5 @@ +export default async function parallel( + tasks: ReadonlyArray<() => Promise | T> +): Promise { + return Promise.all(tasks.map((task) => task())); +} diff --git a/package/asyncUtil/series/index.test.ts b/package/asyncUtil/series/index.test.ts new file mode 100644 index 0000000..6dd6509 --- /dev/null +++ b/package/asyncUtil/series/index.test.ts @@ -0,0 +1,22 @@ +import { describe, expect, test } from "vitest"; +import series from "."; + +describe("async series 유틸 함수 테스트", () => { + test("작업을 순차적으로 실행한다.", async () => { + const order: number[] = []; + + const result = await series([ + async () => { + order.push(1); + return 1; + }, + async () => { + order.push(2); + return 2; + }, + ]); + + expect(order).toEqual([1, 2]); + expect(result).toEqual([1, 2]); + }); +}); diff --git a/package/asyncUtil/series/index.ts b/package/asyncUtil/series/index.ts new file mode 100644 index 0000000..d50cc81 --- /dev/null +++ b/package/asyncUtil/series/index.ts @@ -0,0 +1,11 @@ +export default async function series( + tasks: ReadonlyArray<() => Promise | T> +): Promise { + const results: T[] = []; + + for (const task of tasks) { + results.push(await task()); + } + + return results; +} diff --git a/package/collectionUtil/_toEntries.ts b/package/collectionUtil/_toEntries.ts new file mode 100644 index 0000000..1f2eaff --- /dev/null +++ b/package/collectionUtil/_toEntries.ts @@ -0,0 +1,9 @@ +export type Collection = readonly T[] | Record; + +export default function toEntries(collection: Collection): Array<[string, T]> { + if (Array.isArray(collection)) { + return collection.map((value, index) => [String(index), value]); + } + + return Object.entries(collection); +} diff --git a/package/collectionUtil/entries/index.test.ts b/package/collectionUtil/entries/index.test.ts new file mode 100644 index 0000000..7099e97 --- /dev/null +++ b/package/collectionUtil/entries/index.test.ts @@ -0,0 +1,15 @@ +import { describe, expect, test } from "vitest"; +import entries from "."; + +describe("collection entries 유틸 함수 테스트", () => { + test("키-값 쌍 배열을 반환한다.", () => { + expect(entries({ a: 1 })).toEqual([["a", 1]]); + }); + + test("배열은 인덱스 기준 엔트리를 반환한다.", () => { + expect(entries(["x", "y"])).toEqual([ + ["0", "x"], + ["1", "y"], + ]); + }); +}); diff --git a/package/collectionUtil/entries/index.ts b/package/collectionUtil/entries/index.ts new file mode 100644 index 0000000..211f786 --- /dev/null +++ b/package/collectionUtil/entries/index.ts @@ -0,0 +1,5 @@ +import toEntries, { type Collection } from "../_toEntries"; + +export default function entries(collection: Collection): Array<[string, T]> { + return toEntries(collection); +} diff --git a/package/collectionUtil/every/index.test.ts b/package/collectionUtil/every/index.test.ts new file mode 100644 index 0000000..640e232 --- /dev/null +++ b/package/collectionUtil/every/index.test.ts @@ -0,0 +1,12 @@ +import { describe, expect, test } from "vitest"; +import every from "."; + +describe("collection every 유틸 함수 테스트", () => { + test("모든 값이 조건을 만족하면 true다.", () => { + expect(every([2, 4, 6], (value) => value % 2 === 0)).toBe(true); + }); + + test("하나라도 조건을 만족하지 않으면 false다.", () => { + expect(every({ a: 1, b: 2 }, (value) => value > 1)).toBe(false); + }); +}); diff --git a/package/collectionUtil/every/index.ts b/package/collectionUtil/every/index.ts new file mode 100644 index 0000000..c51f15b --- /dev/null +++ b/package/collectionUtil/every/index.ts @@ -0,0 +1,10 @@ +import toEntries, { type Collection } from "../_toEntries"; + +export default function every( + collection: Collection, + predicate: (value: T, key: string, collection: Collection) => boolean +): boolean { + return toEntries(collection).every(([key, value]) => + predicate(value, key, collection) + ); +} diff --git a/package/collectionUtil/filter/index.test.ts b/package/collectionUtil/filter/index.test.ts new file mode 100644 index 0000000..9f52785 --- /dev/null +++ b/package/collectionUtil/filter/index.test.ts @@ -0,0 +1,12 @@ +import { describe, expect, test } from "vitest"; +import filter from "."; + +describe("collection filter 유틸 함수 테스트", () => { + test("조건에 맞는 값만 반환한다.", () => { + expect(filter([1, 2, 3, 4], (value) => value % 2 === 0)).toEqual([2, 4]); + }); + + test("객체에서도 동작한다.", () => { + expect(filter({ a: 1, b: 2, c: 3 }, (value) => value > 1)).toEqual([2, 3]); + }); +}); diff --git a/package/collectionUtil/filter/index.ts b/package/collectionUtil/filter/index.ts new file mode 100644 index 0000000..72423a2 --- /dev/null +++ b/package/collectionUtil/filter/index.ts @@ -0,0 +1,10 @@ +import toEntries, { type Collection } from "../_toEntries"; + +export default function filter( + collection: Collection, + predicate: (value: T, key: string, collection: Collection) => boolean +): T[] { + return toEntries(collection) + .filter(([key, value]) => predicate(value, key, collection)) + .map(([, value]) => value); +} diff --git a/package/collectionUtil/find/index.test.ts b/package/collectionUtil/find/index.test.ts new file mode 100644 index 0000000..9aa5984 --- /dev/null +++ b/package/collectionUtil/find/index.test.ts @@ -0,0 +1,12 @@ +import { describe, expect, test } from "vitest"; +import find from "."; + +describe("collection find 유틸 함수 테스트", () => { + test("조건을 만족하는 첫 값을 반환한다.", () => { + expect(find([1, 2, 3], (value) => value > 1)).toBe(2); + }); + + test("값이 없으면 undefined를 반환한다.", () => { + expect(find({ a: 1 }, (value) => value > 10)).toBeUndefined(); + }); +}); diff --git a/package/collectionUtil/find/index.ts b/package/collectionUtil/find/index.ts new file mode 100644 index 0000000..838fdcb --- /dev/null +++ b/package/collectionUtil/find/index.ts @@ -0,0 +1,12 @@ +import toEntries, { type Collection } from "../_toEntries"; + +export default function find( + collection: Collection, + predicate: (value: T, key: string, collection: Collection) => boolean +): T | undefined { + const entry = toEntries(collection).find(([key, value]) => + predicate(value, key, collection) + ); + + return entry?.[1]; +} diff --git a/package/collectionUtil/forEach/index.test.ts b/package/collectionUtil/forEach/index.test.ts new file mode 100644 index 0000000..5503b30 --- /dev/null +++ b/package/collectionUtil/forEach/index.test.ts @@ -0,0 +1,16 @@ +import { describe, expect, test } from "vitest"; +import forEach from "."; + +describe("collection forEach 유틸 함수 테스트", () => { + test("배열 순회가 가능하다.", () => { + const result: number[] = []; + forEach([1, 2, 3], (value) => result.push(value * 2)); + expect(result).toEqual([2, 4, 6]); + }); + + test("객체 순회가 가능하다.", () => { + const result: string[] = []; + forEach({ a: 1, b: 2 }, (value, key) => result.push(`${key}:${value}`)); + expect(result).toEqual(["a:1", "b:2"]); + }); +}); diff --git a/package/collectionUtil/forEach/index.ts b/package/collectionUtil/forEach/index.ts new file mode 100644 index 0000000..8c783d4 --- /dev/null +++ b/package/collectionUtil/forEach/index.ts @@ -0,0 +1,10 @@ +import toEntries, { type Collection } from "../_toEntries"; + +export default function forEach( + collection: Collection, + iteratee: (value: T, key: string, collection: Collection) => void +): void { + toEntries(collection).forEach(([key, value]) => { + iteratee(value, key, collection); + }); +} diff --git a/package/collectionUtil/includes/index.test.ts b/package/collectionUtil/includes/index.test.ts new file mode 100644 index 0000000..c8e9393 --- /dev/null +++ b/package/collectionUtil/includes/index.test.ts @@ -0,0 +1,13 @@ +import { describe, expect, test } from "vitest"; +import includes from "."; + +describe("collection includes 유틸 함수 테스트", () => { + test("배열/객체 값 포함 여부를 확인한다.", () => { + expect(includes([1, 2, 3], 2)).toBe(true); + expect(includes({ a: "x", b: "y" }, "z")).toBe(false); + }); + + test("문자열 포함 여부를 확인한다.", () => { + expect(includes("hello world", "world")).toBe(true); + }); +}); diff --git a/package/collectionUtil/includes/index.ts b/package/collectionUtil/includes/index.ts new file mode 100644 index 0000000..c7a073a --- /dev/null +++ b/package/collectionUtil/includes/index.ts @@ -0,0 +1,12 @@ +import toEntries, { type Collection } from "../_toEntries"; + +export default function includes( + collection: Collection | string, + target: T | string +): boolean { + if (typeof collection === "string") { + return collection.includes(String(target)); + } + + return toEntries(collection).some(([, value]) => value === target); +} diff --git a/package/collectionUtil/index.ts b/package/collectionUtil/index.ts new file mode 100644 index 0000000..a7d12ad --- /dev/null +++ b/package/collectionUtil/index.ts @@ -0,0 +1,12 @@ +export { default as entries } from "./entries"; +export { default as every } from "./every"; +export { default as filter } from "./filter"; +export { default as find } from "./find"; +export { default as forEach } from "./forEach"; +export { default as includes } from "./includes"; +export { default as keys } from "./keys"; +export { default as map } from "./map"; +export { default as reduce } from "./reduce"; +export { default as size } from "./size"; +export { default as some } from "./some"; +export { default as values } from "./values"; diff --git a/package/collectionUtil/keys/index.test.ts b/package/collectionUtil/keys/index.test.ts new file mode 100644 index 0000000..de9f785 --- /dev/null +++ b/package/collectionUtil/keys/index.test.ts @@ -0,0 +1,12 @@ +import { describe, expect, test } from "vitest"; +import keys from "."; + +describe("collection keys 유틸 함수 테스트", () => { + test("키 배열을 반환한다.", () => { + expect(keys({ a: 1, b: 2 })).toEqual(["a", "b"]); + }); + + test("배열은 인덱스 키를 반환한다.", () => { + expect(keys([10, 20])).toEqual(["0", "1"]); + }); +}); diff --git a/package/collectionUtil/keys/index.ts b/package/collectionUtil/keys/index.ts new file mode 100644 index 0000000..2d8cfbf --- /dev/null +++ b/package/collectionUtil/keys/index.ts @@ -0,0 +1,9 @@ +import { type Collection } from "../_toEntries"; + +export default function keys(collection: Collection): string[] { + if (Array.isArray(collection)) { + return collection.map((_, index) => String(index)); + } + + return Object.keys(collection); +} diff --git a/package/collectionUtil/map/index.test.ts b/package/collectionUtil/map/index.test.ts new file mode 100644 index 0000000..8619a88 --- /dev/null +++ b/package/collectionUtil/map/index.test.ts @@ -0,0 +1,15 @@ +import { describe, expect, test } from "vitest"; +import map from "."; + +describe("collection map 유틸 함수 테스트", () => { + test("배열을 변환한다.", () => { + expect(map([1, 2, 3], (value) => value * 2)).toEqual([2, 4, 6]); + }); + + test("객체를 변환한다.", () => { + expect(map({ a: 1, b: 2 }, (value, key) => `${key}:${value}`)).toEqual([ + "a:1", + "b:2", + ]); + }); +}); diff --git a/package/collectionUtil/map/index.ts b/package/collectionUtil/map/index.ts new file mode 100644 index 0000000..a0d5f6e --- /dev/null +++ b/package/collectionUtil/map/index.ts @@ -0,0 +1,10 @@ +import toEntries, { type Collection } from "../_toEntries"; + +export default function map( + collection: Collection, + iteratee: (value: T, key: string, collection: Collection) => R +): R[] { + return toEntries(collection).map(([key, value]) => + iteratee(value, key, collection) + ); +} diff --git a/package/collectionUtil/reduce/index.test.ts b/package/collectionUtil/reduce/index.test.ts new file mode 100644 index 0000000..f3697cb --- /dev/null +++ b/package/collectionUtil/reduce/index.test.ts @@ -0,0 +1,12 @@ +import { describe, expect, test } from "vitest"; +import reduce from "."; + +describe("collection reduce 유틸 함수 테스트", () => { + test("누적 계산을 수행한다.", () => { + expect(reduce([1, 2, 3], (acc, value) => acc + value, 0)).toBe(6); + }); + + test("객체에서도 누적 계산을 수행한다.", () => { + expect(reduce({ a: 1, b: 2 }, (acc, value, key) => ({ ...acc, [key]: value * 2 }), {} as Record)).toEqual({ a: 2, b: 4 }); + }); +}); diff --git a/package/collectionUtil/reduce/index.ts b/package/collectionUtil/reduce/index.ts new file mode 100644 index 0000000..803b1b5 --- /dev/null +++ b/package/collectionUtil/reduce/index.ts @@ -0,0 +1,12 @@ +import toEntries, { type Collection } from "../_toEntries"; + +export default function reduce( + collection: Collection, + iteratee: (accumulator: R, value: T, key: string, collection: Collection) => R, + initialValue: R +): R { + return toEntries(collection).reduce( + (accumulator, [key, value]) => iteratee(accumulator, value, key, collection), + initialValue + ); +} diff --git a/package/collectionUtil/size/index.test.ts b/package/collectionUtil/size/index.test.ts new file mode 100644 index 0000000..4d1b82f --- /dev/null +++ b/package/collectionUtil/size/index.test.ts @@ -0,0 +1,15 @@ +import { describe, expect, test } from "vitest"; +import size from "."; + +describe("collection size 유틸 함수 테스트", () => { + test("배열/객체/문자열 크기를 반환한다.", () => { + expect(size([1, 2, 3])).toBe(3); + expect(size({ a: 1, b: 2 })).toBe(2); + expect(size("abc")).toBe(3); + }); + + test("Map/Set 크기를 반환한다.", () => { + expect(size(new Map([["a", 1]]))).toBe(1); + expect(size(new Set([1, 2]))).toBe(2); + }); +}); diff --git a/package/collectionUtil/size/index.ts b/package/collectionUtil/size/index.ts new file mode 100644 index 0000000..11b5147 --- /dev/null +++ b/package/collectionUtil/size/index.ts @@ -0,0 +1,13 @@ +import { type Collection } from "../_toEntries"; + +export default function size(collection: Collection | string | Map | Set): number { + if (typeof collection === "string" || Array.isArray(collection)) { + return collection.length; + } + + if (collection instanceof Map || collection instanceof Set) { + return collection.size; + } + + return Object.keys(collection).length; +} diff --git a/package/collectionUtil/some/index.test.ts b/package/collectionUtil/some/index.test.ts new file mode 100644 index 0000000..3edaf1c --- /dev/null +++ b/package/collectionUtil/some/index.test.ts @@ -0,0 +1,12 @@ +import { describe, expect, test } from "vitest"; +import some from "."; + +describe("collection some 유틸 함수 테스트", () => { + test("하나라도 조건을 만족하면 true다.", () => { + expect(some([1, 2, 3], (value) => value === 2)).toBe(true); + }); + + test("조건을 만족하지 않으면 false다.", () => { + expect(some({ a: 1, b: 2 }, (value) => value > 10)).toBe(false); + }); +}); diff --git a/package/collectionUtil/some/index.ts b/package/collectionUtil/some/index.ts new file mode 100644 index 0000000..27d8172 --- /dev/null +++ b/package/collectionUtil/some/index.ts @@ -0,0 +1,10 @@ +import toEntries, { type Collection } from "../_toEntries"; + +export default function some( + collection: Collection, + predicate: (value: T, key: string, collection: Collection) => boolean +): boolean { + return toEntries(collection).some(([key, value]) => + predicate(value, key, collection) + ); +} diff --git a/package/collectionUtil/values/index.test.ts b/package/collectionUtil/values/index.test.ts new file mode 100644 index 0000000..3cd2f0f --- /dev/null +++ b/package/collectionUtil/values/index.test.ts @@ -0,0 +1,12 @@ +import { describe, expect, test } from "vitest"; +import values from "."; + +describe("collection values 유틸 함수 테스트", () => { + test("값 배열을 반환한다.", () => { + expect(values({ a: 1, b: 2 })).toEqual([1, 2]); + }); + + test("배열은 복사본을 반환한다.", () => { + expect(values([1, 2, 3])).toEqual([1, 2, 3]); + }); +}); diff --git a/package/collectionUtil/values/index.ts b/package/collectionUtil/values/index.ts new file mode 100644 index 0000000..ee9fd8b --- /dev/null +++ b/package/collectionUtil/values/index.ts @@ -0,0 +1,9 @@ +import { type Collection } from "../_toEntries"; + +export default function values(collection: Collection): T[] { + if (Array.isArray(collection)) { + return [...collection]; + } + + return Object.values(collection); +} diff --git a/package/dateUtil/_toDate.ts b/package/dateUtil/_toDate.ts new file mode 100644 index 0000000..7fd358a --- /dev/null +++ b/package/dateUtil/_toDate.ts @@ -0,0 +1,5 @@ +export type DateInput = Date | string | number; + +export default function toDate(value: DateInput): Date { + return value instanceof Date ? new Date(value.getTime()) : new Date(value); +} diff --git a/package/dateUtil/addDays/index.test.ts b/package/dateUtil/addDays/index.test.ts new file mode 100644 index 0000000..3bc0f85 --- /dev/null +++ b/package/dateUtil/addDays/index.test.ts @@ -0,0 +1,9 @@ +import { describe, expect, test } from "vitest"; +import addDays from "."; + +describe("date addDays 유틸 함수 테스트", () => { + test("일수를 더한 Date를 반환한다.", () => { + const result = addDays(new Date("2025-01-01T00:00:00.000Z"), 2); + expect(result.toISOString()).toBe("2025-01-03T00:00:00.000Z"); + }); +}); diff --git a/package/dateUtil/addDays/index.ts b/package/dateUtil/addDays/index.ts new file mode 100644 index 0000000..dc23a5d --- /dev/null +++ b/package/dateUtil/addDays/index.ts @@ -0,0 +1,7 @@ +import toDate, { type DateInput } from "../_toDate"; + +export default function addDays(date: DateInput, amount: number): Date { + const result = toDate(date); + result.setDate(result.getDate() + amount); + return result; +} diff --git a/package/dateUtil/addHours/index.test.ts b/package/dateUtil/addHours/index.test.ts new file mode 100644 index 0000000..5a427e0 --- /dev/null +++ b/package/dateUtil/addHours/index.test.ts @@ -0,0 +1,9 @@ +import { describe, expect, test } from "vitest"; +import addHours from "."; + +describe("date addHours 유틸 함수 테스트", () => { + test("시간을 더한 Date를 반환한다.", () => { + const result = addHours(new Date("2025-01-01T00:00:00.000Z"), 5); + expect(result.toISOString()).toBe("2025-01-01T05:00:00.000Z"); + }); +}); diff --git a/package/dateUtil/addHours/index.ts b/package/dateUtil/addHours/index.ts new file mode 100644 index 0000000..91ea7ed --- /dev/null +++ b/package/dateUtil/addHours/index.ts @@ -0,0 +1,7 @@ +import toDate, { type DateInput } from "../_toDate"; + +export default function addHours(date: DateInput, amount: number): Date { + const result = toDate(date); + result.setHours(result.getHours() + amount); + return result; +} diff --git a/package/dateUtil/differenceInDays/index.test.ts b/package/dateUtil/differenceInDays/index.test.ts new file mode 100644 index 0000000..ff51222 --- /dev/null +++ b/package/dateUtil/differenceInDays/index.test.ts @@ -0,0 +1,12 @@ +import { describe, expect, test } from "vitest"; +import differenceInDays from "."; + +describe("date differenceInDays 유틸 함수 테스트", () => { + test("일수 차이를 반환한다.", () => { + expect(differenceInDays("2025-01-03", "2025-01-01")).toBe(2); + }); + + test("역순이면 음수를 반환한다.", () => { + expect(differenceInDays("2025-01-01", "2025-01-03")).toBe(-2); + }); +}); diff --git a/package/dateUtil/differenceInDays/index.ts b/package/dateUtil/differenceInDays/index.ts new file mode 100644 index 0000000..3e2474b --- /dev/null +++ b/package/dateUtil/differenceInDays/index.ts @@ -0,0 +1,10 @@ +import toDate, { type DateInput } from "../_toDate"; + +const MILLISECONDS_IN_DAY = 24 * 60 * 60 * 1000; + +export default function differenceInDays(dateLeft: DateInput, dateRight: DateInput): number { + const left = toDate(dateLeft).getTime(); + const right = toDate(dateRight).getTime(); + + return Math.trunc((left - right) / MILLISECONDS_IN_DAY); +} diff --git a/package/dateUtil/endOfDay/index.test.ts b/package/dateUtil/endOfDay/index.test.ts new file mode 100644 index 0000000..c079f45 --- /dev/null +++ b/package/dateUtil/endOfDay/index.test.ts @@ -0,0 +1,15 @@ +import { describe, expect, test } from "vitest"; +import endOfDay from "."; + +describe("date endOfDay 유틸 함수 테스트", () => { + test("해당 날짜의 종료 시각을 반환한다.", () => { + const result = endOfDay(new Date(2025, 0, 1, 12, 34, 56, 789)); + expect(result.getFullYear()).toBe(2025); + expect(result.getMonth()).toBe(0); + expect(result.getDate()).toBe(1); + expect(result.getHours()).toBe(23); + expect(result.getMinutes()).toBe(59); + expect(result.getSeconds()).toBe(59); + expect(result.getMilliseconds()).toBe(999); + }); +}); diff --git a/package/dateUtil/endOfDay/index.ts b/package/dateUtil/endOfDay/index.ts new file mode 100644 index 0000000..035381b --- /dev/null +++ b/package/dateUtil/endOfDay/index.ts @@ -0,0 +1,7 @@ +import toDate, { type DateInput } from "../_toDate"; + +export default function endOfDay(date: DateInput): Date { + const result = toDate(date); + result.setHours(23, 59, 59, 999); + return result; +} diff --git a/package/dateUtil/formatDate/index.test.ts b/package/dateUtil/formatDate/index.test.ts new file mode 100644 index 0000000..3716adf --- /dev/null +++ b/package/dateUtil/formatDate/index.test.ts @@ -0,0 +1,14 @@ +import { describe, expect, test } from "vitest"; +import formatDate from "."; + +describe("date formatDate 유틸 함수 테스트", () => { + test("기본 포맷으로 날짜를 변환한다.", () => { + expect(formatDate(new Date(2025, 0, 2, 3, 4, 5))).toBe("2025-01-02"); + }); + + test("사용자 지정 포맷을 지원한다.", () => { + expect(formatDate(new Date(2025, 0, 2, 3, 4, 5), "YYYY/MM/DD HH:mm:ss")).toBe( + "2025/01/02 03:04:05" + ); + }); +}); diff --git a/package/dateUtil/formatDate/index.ts b/package/dateUtil/formatDate/index.ts new file mode 100644 index 0000000..5a8a227 --- /dev/null +++ b/package/dateUtil/formatDate/index.ts @@ -0,0 +1,26 @@ +import toDate, { type DateInput } from "../_toDate"; + +function pad(value: number): string { + return String(value).padStart(2, "0"); +} + +export default function formatDate( + date: DateInput, + format: string = "YYYY-MM-DD" +): string { + const value = toDate(date); + + const tokenMap: Record = { + YYYY: String(value.getFullYear()), + MM: pad(value.getMonth() + 1), + DD: pad(value.getDate()), + HH: pad(value.getHours()), + mm: pad(value.getMinutes()), + ss: pad(value.getSeconds()), + }; + + return Object.entries(tokenMap).reduce( + (result, [token, tokenValue]) => result.replaceAll(token, tokenValue), + format + ); +} diff --git a/package/dateUtil/index.ts b/package/dateUtil/index.ts new file mode 100644 index 0000000..ef6562c --- /dev/null +++ b/package/dateUtil/index.ts @@ -0,0 +1,11 @@ +export { default as addDays } from "./addDays"; +export { default as addHours } from "./addHours"; +export { default as differenceInDays } from "./differenceInDays"; +export { default as endOfDay } from "./endOfDay"; +export { default as formatDate } from "./formatDate"; +export { default as isAfter } from "./isAfter"; +export { default as isBefore } from "./isBefore"; +export { default as isSameDay } from "./isSameDay"; +export { default as startOfDay } from "./startOfDay"; +export { default as subDays } from "./subDays"; +export { default as subHours } from "./subHours"; diff --git a/package/dateUtil/isAfter/index.test.ts b/package/dateUtil/isAfter/index.test.ts new file mode 100644 index 0000000..4dce508 --- /dev/null +++ b/package/dateUtil/isAfter/index.test.ts @@ -0,0 +1,8 @@ +import { describe, expect, test } from "vitest"; +import isAfter from "."; + +describe("date isAfter 유틸 함수 테스트", () => { + test("뒤 시각이면 true를 반환한다.", () => { + expect(isAfter("2025-01-03", "2025-01-02")).toBe(true); + }); +}); diff --git a/package/dateUtil/isAfter/index.ts b/package/dateUtil/isAfter/index.ts new file mode 100644 index 0000000..8f2bbbc --- /dev/null +++ b/package/dateUtil/isAfter/index.ts @@ -0,0 +1,5 @@ +import toDate, { type DateInput } from "../_toDate"; + +export default function isAfter(date: DateInput, compareDate: DateInput): boolean { + return toDate(date).getTime() > toDate(compareDate).getTime(); +} diff --git a/package/dateUtil/isBefore/index.test.ts b/package/dateUtil/isBefore/index.test.ts new file mode 100644 index 0000000..d94cb65 --- /dev/null +++ b/package/dateUtil/isBefore/index.test.ts @@ -0,0 +1,8 @@ +import { describe, expect, test } from "vitest"; +import isBefore from "."; + +describe("date isBefore 유틸 함수 테스트", () => { + test("앞선 시각이면 true를 반환한다.", () => { + expect(isBefore("2025-01-01", "2025-01-02")).toBe(true); + }); +}); diff --git a/package/dateUtil/isBefore/index.ts b/package/dateUtil/isBefore/index.ts new file mode 100644 index 0000000..2354326 --- /dev/null +++ b/package/dateUtil/isBefore/index.ts @@ -0,0 +1,5 @@ +import toDate, { type DateInput } from "../_toDate"; + +export default function isBefore(date: DateInput, compareDate: DateInput): boolean { + return toDate(date).getTime() < toDate(compareDate).getTime(); +} diff --git a/package/dateUtil/isSameDay/index.test.ts b/package/dateUtil/isSameDay/index.test.ts new file mode 100644 index 0000000..d8ceda7 --- /dev/null +++ b/package/dateUtil/isSameDay/index.test.ts @@ -0,0 +1,14 @@ +import { describe, expect, test } from "vitest"; +import isSameDay from "."; + +describe("date isSameDay 유틸 함수 테스트", () => { + test("같은 날짜면 true를 반환한다.", () => { + expect( + isSameDay(new Date(2025, 0, 1, 0, 0, 0), new Date(2025, 0, 1, 23, 59, 59)) + ).toBe(true); + }); + + test("다른 날짜면 false를 반환한다.", () => { + expect(isSameDay("2025-01-01", "2025-01-02")).toBe(false); + }); +}); diff --git a/package/dateUtil/isSameDay/index.ts b/package/dateUtil/isSameDay/index.ts new file mode 100644 index 0000000..81f9126 --- /dev/null +++ b/package/dateUtil/isSameDay/index.ts @@ -0,0 +1,12 @@ +import toDate, { type DateInput } from "../_toDate"; + +export default function isSameDay(date: DateInput, compareDate: DateInput): boolean { + const left = toDate(date); + const right = toDate(compareDate); + + return ( + left.getFullYear() === right.getFullYear() && + left.getMonth() === right.getMonth() && + left.getDate() === right.getDate() + ); +} diff --git a/package/dateUtil/startOfDay/index.test.ts b/package/dateUtil/startOfDay/index.test.ts new file mode 100644 index 0000000..6f1fe53 --- /dev/null +++ b/package/dateUtil/startOfDay/index.test.ts @@ -0,0 +1,15 @@ +import { describe, expect, test } from "vitest"; +import startOfDay from "."; + +describe("date startOfDay 유틸 함수 테스트", () => { + test("해당 날짜의 시작 시각을 반환한다.", () => { + const result = startOfDay(new Date(2025, 0, 1, 12, 34, 56, 789)); + expect(result.getFullYear()).toBe(2025); + expect(result.getMonth()).toBe(0); + expect(result.getDate()).toBe(1); + expect(result.getHours()).toBe(0); + expect(result.getMinutes()).toBe(0); + expect(result.getSeconds()).toBe(0); + expect(result.getMilliseconds()).toBe(0); + }); +}); diff --git a/package/dateUtil/startOfDay/index.ts b/package/dateUtil/startOfDay/index.ts new file mode 100644 index 0000000..1b31dfd --- /dev/null +++ b/package/dateUtil/startOfDay/index.ts @@ -0,0 +1,7 @@ +import toDate, { type DateInput } from "../_toDate"; + +export default function startOfDay(date: DateInput): Date { + const result = toDate(date); + result.setHours(0, 0, 0, 0); + return result; +} diff --git a/package/dateUtil/subDays/index.test.ts b/package/dateUtil/subDays/index.test.ts new file mode 100644 index 0000000..7228fb2 --- /dev/null +++ b/package/dateUtil/subDays/index.test.ts @@ -0,0 +1,9 @@ +import { describe, expect, test } from "vitest"; +import subDays from "."; + +describe("date subDays 유틸 함수 테스트", () => { + test("일수를 뺀 Date를 반환한다.", () => { + const result = subDays(new Date("2025-01-03T00:00:00.000Z"), 2); + expect(result.toISOString()).toBe("2025-01-01T00:00:00.000Z"); + }); +}); diff --git a/package/dateUtil/subDays/index.ts b/package/dateUtil/subDays/index.ts new file mode 100644 index 0000000..06bddf3 --- /dev/null +++ b/package/dateUtil/subDays/index.ts @@ -0,0 +1,7 @@ +import toDate, { type DateInput } from "../_toDate"; + +export default function subDays(date: DateInput, amount: number): Date { + const result = toDate(date); + result.setDate(result.getDate() - amount); + return result; +} diff --git a/package/dateUtil/subHours/index.test.ts b/package/dateUtil/subHours/index.test.ts new file mode 100644 index 0000000..8eaf055 --- /dev/null +++ b/package/dateUtil/subHours/index.test.ts @@ -0,0 +1,9 @@ +import { describe, expect, test } from "vitest"; +import subHours from "."; + +describe("date subHours 유틸 함수 테스트", () => { + test("시간을 뺀 Date를 반환한다.", () => { + const result = subHours(new Date("2025-01-01T05:00:00.000Z"), 5); + expect(result.toISOString()).toBe("2025-01-01T00:00:00.000Z"); + }); +}); diff --git a/package/dateUtil/subHours/index.ts b/package/dateUtil/subHours/index.ts new file mode 100644 index 0000000..cf8a9b9 --- /dev/null +++ b/package/dateUtil/subHours/index.ts @@ -0,0 +1,7 @@ +import toDate, { type DateInput } from "../_toDate"; + +export default function subHours(date: DateInput, amount: number): Date { + const result = toDate(date); + result.setHours(result.getHours() - amount); + return result; +} diff --git a/package/functionUtil/compose/index.test.ts b/package/functionUtil/compose/index.test.ts new file mode 100644 index 0000000..be1918a --- /dev/null +++ b/package/functionUtil/compose/index.test.ts @@ -0,0 +1,13 @@ +import { describe, expect, test } from "vitest"; +import compose from "."; + +describe("compose 유틸 함수 테스트", () => { + test("오른쪽에서 왼쪽 순서로 함수를 합성한다.", () => { + const fn = compose( + (value) => Number(value) * 2, + (value) => Number(value) + 1 + ); + + expect(fn(3)).toBe(8); + }); +}); diff --git a/package/functionUtil/compose/index.ts b/package/functionUtil/compose/index.ts new file mode 100644 index 0000000..ed12fd8 --- /dev/null +++ b/package/functionUtil/compose/index.ts @@ -0,0 +1,6 @@ +type UnaryFunction = (value: unknown) => unknown; + +export default function compose(...fns: UnaryFunction[]): UnaryFunction { + return (value: unknown) => + fns.reduceRight((acc, fn) => fn(acc), value); +} diff --git a/package/functionUtil/identity/index.test.ts b/package/functionUtil/identity/index.test.ts new file mode 100644 index 0000000..e06f704 --- /dev/null +++ b/package/functionUtil/identity/index.test.ts @@ -0,0 +1,11 @@ +import { describe, expect, test } from "vitest"; +import identity from "."; + +describe("identity 유틸 함수 테스트", () => { + test("입력값을 그대로 반환한다.", () => { + const obj = { a: 1 }; + + expect(identity(obj)).toBe(obj); + expect(identity(123)).toBe(123); + }); +}); diff --git a/package/functionUtil/identity/index.ts b/package/functionUtil/identity/index.ts new file mode 100644 index 0000000..711dac6 --- /dev/null +++ b/package/functionUtil/identity/index.ts @@ -0,0 +1,3 @@ +export default function identity(value: T): T { + return value; +} diff --git a/package/functionUtil/index.ts b/package/functionUtil/index.ts new file mode 100644 index 0000000..7659cd7 --- /dev/null +++ b/package/functionUtil/index.ts @@ -0,0 +1,6 @@ +export { default as compose } from "./compose"; +export { default as identity } from "./identity"; +export { default as memoize } from "./memoize"; +export { default as noop } from "./noop"; +export { default as once } from "./once"; +export { default as pipe } from "./pipe"; diff --git a/package/functionUtil/memoize/index.test.ts b/package/functionUtil/memoize/index.test.ts new file mode 100644 index 0000000..5fbdd5b --- /dev/null +++ b/package/functionUtil/memoize/index.test.ts @@ -0,0 +1,22 @@ +import { describe, expect, test, vi } from "vitest"; +import memoize from "."; + +describe("memoize 유틸 함수 테스트", () => { + test("같은 키는 캐시된 결과를 반환한다.", () => { + const fn = vi.fn((value: number) => value * 2); + const memoized = memoize(fn); + + expect(memoized(2)).toBe(4); + expect(memoized(2)).toBe(4); + expect(fn).toHaveBeenCalledTimes(1); + }); + + test("resolver를 사용해 커스텀 캐시 키를 만들 수 있다.", () => { + const fn = vi.fn((a: number, b: number) => a + b); + const memoized = memoize(fn, (a, b) => `${a}:${b}`); + + expect(memoized(1, 2)).toBe(3); + expect(memoized(1, 2)).toBe(3); + expect(fn).toHaveBeenCalledTimes(1); + }); +}); diff --git a/package/functionUtil/memoize/index.ts b/package/functionUtil/memoize/index.ts new file mode 100644 index 0000000..217c72a --- /dev/null +++ b/package/functionUtil/memoize/index.ts @@ -0,0 +1,31 @@ +type Memoized unknown, K> = (( + ...args: Parameters +) => ReturnType) & { + cache: Map>; +}; + +export default function memoize< + T extends (...args: unknown[]) => unknown, + K = unknown +>( + fn: T, + resolver?: (...args: Parameters) => K +): Memoized { + const cache = new Map>(); + + const memoized = ((...args: Parameters) => { + const key = resolver ? resolver(...args) : (args[0] as K); + + if (cache.has(key)) { + return cache.get(key) as ReturnType; + } + + const result = fn(...args) as ReturnType; + cache.set(key, result); + return result; + }) as Memoized; + + memoized.cache = cache; + + return memoized; +} diff --git a/package/functionUtil/noop/index.test.ts b/package/functionUtil/noop/index.test.ts new file mode 100644 index 0000000..f550a7e --- /dev/null +++ b/package/functionUtil/noop/index.test.ts @@ -0,0 +1,8 @@ +import { describe, expect, test } from "vitest"; +import noop from "."; + +describe("noop 유틸 함수 테스트", () => { + test("항상 undefined를 반환한다.", () => { + expect(noop(1, 2, 3)).toBeUndefined(); + }); +}); diff --git a/package/functionUtil/noop/index.ts b/package/functionUtil/noop/index.ts new file mode 100644 index 0000000..186b962 --- /dev/null +++ b/package/functionUtil/noop/index.ts @@ -0,0 +1 @@ +export default function noop(..._args: unknown[]): void {} diff --git a/package/functionUtil/once/index.test.ts b/package/functionUtil/once/index.test.ts new file mode 100644 index 0000000..6676385 --- /dev/null +++ b/package/functionUtil/once/index.test.ts @@ -0,0 +1,13 @@ +import { describe, expect, test, vi } from "vitest"; +import once from "."; + +describe("once 유틸 함수 테스트", () => { + test("첫 호출 결과를 캐시하고 이후에는 재실행하지 않는다.", () => { + const fn = vi.fn((value: number) => value * 2); + const wrapped = once(fn); + + expect(wrapped(2)).toBe(4); + expect(wrapped(3)).toBe(4); + expect(fn).toHaveBeenCalledTimes(1); + }); +}); diff --git a/package/functionUtil/once/index.ts b/package/functionUtil/once/index.ts new file mode 100644 index 0000000..be1551f --- /dev/null +++ b/package/functionUtil/once/index.ts @@ -0,0 +1,15 @@ +export default function once unknown>( + fn: T +): (...args: Parameters) => ReturnType { + let called = false; + let result: ReturnType; + + return (...args: Parameters) => { + if (!called) { + called = true; + result = fn(...args) as ReturnType; + } + + return result; + }; +} diff --git a/package/functionUtil/pipe/index.test.ts b/package/functionUtil/pipe/index.test.ts new file mode 100644 index 0000000..7f563cc --- /dev/null +++ b/package/functionUtil/pipe/index.test.ts @@ -0,0 +1,13 @@ +import { describe, expect, test } from "vitest"; +import pipe from "."; + +describe("pipe 유틸 함수 테스트", () => { + test("왼쪽에서 오른쪽 순서로 함수를 합성한다.", () => { + const fn = pipe( + (value) => Number(value) + 1, + (value) => Number(value) * 2 + ); + + expect(fn(3)).toBe(8); + }); +}); diff --git a/package/functionUtil/pipe/index.ts b/package/functionUtil/pipe/index.ts new file mode 100644 index 0000000..edcab24 --- /dev/null +++ b/package/functionUtil/pipe/index.ts @@ -0,0 +1,5 @@ +type UnaryFunction = (value: unknown) => unknown; + +export default function pipe(...fns: UnaryFunction[]): UnaryFunction { + return (value: unknown) => fns.reduce((acc, fn) => fn(acc), value); +} diff --git a/package/index.ts b/package/index.ts index 40a1131..fe9619e 100644 --- a/package/index.ts +++ b/package/index.ts @@ -1,10 +1,18 @@ // 네임스페이스에 대한 익스포트 진행 export * as stringUtil from "./stringUtil"; +export * as arrayUtil from "./arrayUtil"; +export * as collectionUtil from "./collectionUtil"; export * as objectUtil from "./objectUtil"; export * as cookieUtil from "./cookieUtil"; +export * as asyncUtil from "./asyncUtil"; +export * as dateUtil from "./dateUtil"; +export * as langUtil from "./langUtil"; +export * as mathUtil from "./mathUtil"; export * as numberUtil from "./numberUtil"; +export * as promiseUtil from "./promiseUtil"; export * as validationUtil from "./validationUtil"; export * as commonUtil from "./commonUtil"; +export * as functionUtil from "./functionUtil"; export * as searchQueryUtil from "./searchQueryUtil"; export * as typeUtil from "./typeUtil"; export * as formatUtil from "./formatUtil"; @@ -12,11 +20,19 @@ export * as deviceUtil from "./deviceUtil"; // 개별 함수에 대한 익스포트 진행 export * from "./stringUtil"; +export * from "./arrayUtil"; +export * from "./collectionUtil"; export * from "./objectUtil"; export * from "./cookieUtil"; +export * from "./asyncUtil"; +export * from "./dateUtil"; +export * from "./langUtil"; +export * from "./mathUtil"; export * from "./numberUtil"; +export * from "./promiseUtil"; export * from "./validationUtil"; export * from "./commonUtil"; +export * from "./functionUtil"; export * from "./searchQueryUtil"; export * from "./typeUtil"; export * from "./formatUtil"; diff --git a/package/langUtil/castArray/index.test.ts b/package/langUtil/castArray/index.test.ts new file mode 100644 index 0000000..6e95f67 --- /dev/null +++ b/package/langUtil/castArray/index.test.ts @@ -0,0 +1,12 @@ +import { describe, expect, test } from "vitest"; +import castArray from "."; + +describe("lang castArray 유틸 함수 테스트", () => { + test("단일 값을 배열로 감싼다.", () => { + expect(castArray(1)).toEqual([1]); + }); + + test("배열 값은 복사본을 반환한다.", () => { + expect(castArray([1, 2])).toEqual([1, 2]); + }); +}); diff --git a/package/langUtil/castArray/index.ts b/package/langUtil/castArray/index.ts new file mode 100644 index 0000000..8c3fad2 --- /dev/null +++ b/package/langUtil/castArray/index.ts @@ -0,0 +1,3 @@ +export default function castArray(value: T | readonly T[]): T[] { + return Array.isArray(value) ? [...value] : [value as T]; +} diff --git a/package/langUtil/defaultTo/index.test.ts b/package/langUtil/defaultTo/index.test.ts new file mode 100644 index 0000000..cf53bba --- /dev/null +++ b/package/langUtil/defaultTo/index.test.ts @@ -0,0 +1,13 @@ +import { describe, expect, test } from "vitest"; +import defaultTo from "."; + +describe("lang defaultTo 유틸 함수 테스트", () => { + test("nullish 값이면 기본값을 반환한다.", () => { + expect(defaultTo(null, "fallback")).toBe("fallback"); + expect(defaultTo(undefined, "fallback")).toBe("fallback"); + }); + + test("유효한 값이면 원래 값을 반환한다.", () => { + expect(defaultTo("value", "fallback")).toBe("value"); + }); +}); diff --git a/package/langUtil/defaultTo/index.ts b/package/langUtil/defaultTo/index.ts new file mode 100644 index 0000000..a7a72fb --- /dev/null +++ b/package/langUtil/defaultTo/index.ts @@ -0,0 +1,11 @@ +export default function defaultTo(value: T, defaultValue: T): T { + if (value === null || value === undefined) { + return defaultValue; + } + + if (typeof value === "number" && Number.isNaN(value)) { + return defaultValue; + } + + return value; +} diff --git a/package/langUtil/index.ts b/package/langUtil/index.ts new file mode 100644 index 0000000..0d0e367 --- /dev/null +++ b/package/langUtil/index.ts @@ -0,0 +1,6 @@ +export { default as castArray } from "./castArray"; +export { default as defaultTo } from "./defaultTo"; +export { default as isEqual } from "./isEqual"; +export { default as toBoolean } from "./toBoolean"; +export { default as toNumber } from "./toNumber"; +export { default as toString } from "./toString"; diff --git a/package/langUtil/isEqual/index.test.ts b/package/langUtil/isEqual/index.test.ts new file mode 100644 index 0000000..10adc86 --- /dev/null +++ b/package/langUtil/isEqual/index.test.ts @@ -0,0 +1,21 @@ +import { describe, expect, test } from "vitest"; +import isEqual from "."; + +describe("lang isEqual 유틸 함수 테스트", () => { + test("중첩 객체를 깊게 비교한다.", () => { + expect(isEqual({ a: { b: 1 } }, { a: { b: 1 } })).toBe(true); + }); + + test("구조가 다르면 false를 반환한다.", () => { + expect(isEqual({ a: 1 }, { a: 2 })).toBe(false); + }); + + test("순환 참조도 비교한다.", () => { + const left: { self?: unknown } = {}; + const right: { self?: unknown } = {}; + left.self = left; + right.self = right; + + expect(isEqual(left, right)).toBe(true); + }); +}); diff --git a/package/langUtil/isEqual/index.ts b/package/langUtil/isEqual/index.ts new file mode 100644 index 0000000..7f2de84 --- /dev/null +++ b/package/langUtil/isEqual/index.ts @@ -0,0 +1,134 @@ +function isObjectLike(value: unknown): value is Record { + return typeof value === "object" && value !== null; +} + +function equalArrays( + left: readonly unknown[], + right: readonly unknown[], + stack: WeakMap +): boolean { + if (left.length !== right.length) { + return false; + } + + for (let index = 0; index < left.length; index++) { + if (!isEqualInternal(left[index], right[index], stack)) { + return false; + } + } + + return true; +} + +function equalMaps( + left: Map, + right: Map, + stack: WeakMap +): boolean { + if (left.size !== right.size) { + return false; + } + + for (const [key, value] of left) { + if (!right.has(key) || !isEqualInternal(value, right.get(key), stack)) { + return false; + } + } + + return true; +} + +function equalSets( + left: Set, + right: Set, + stack: WeakMap +): boolean { + if (left.size !== right.size) { + return false; + } + + const rightValues = [...right]; + + for (const leftValue of left) { + const matchedIndex = rightValues.findIndex((rightValue) => + isEqualInternal(leftValue, rightValue, stack) + ); + + if (matchedIndex === -1) { + return false; + } + + rightValues.splice(matchedIndex, 1); + } + + return true; +} + +function isEqualInternal( + left: unknown, + right: unknown, + stack: WeakMap +): boolean { + if (Object.is(left, right)) { + return true; + } + + if (!isObjectLike(left) || !isObjectLike(right)) { + return false; + } + + const cached = stack.get(left); + + if (cached && cached === right) { + return true; + } + + stack.set(left, right); + + if (left.constructor !== right.constructor) { + return false; + } + + if (left instanceof Date && right instanceof Date) { + return left.getTime() === right.getTime(); + } + + if (left instanceof RegExp && right instanceof RegExp) { + return left.source === right.source && left.flags === right.flags; + } + + if (Array.isArray(left) && Array.isArray(right)) { + return equalArrays(left, right, stack); + } + + if (left instanceof Map && right instanceof Map) { + return equalMaps(left, right, stack); + } + + if (left instanceof Set && right instanceof Set) { + return equalSets(left, right, stack); + } + + const leftKeys = Reflect.ownKeys(left); + const rightKeys = Reflect.ownKeys(right); + + if (leftKeys.length !== rightKeys.length) { + return false; + } + + for (const key of leftKeys) { + if (!rightKeys.includes(key)) { + return false; + } + + if (!isEqualInternal(left[key], right[key], stack)) { + return false; + } + } + + return true; +} + +export default function isEqual(left: unknown, right: unknown): boolean { + return isEqualInternal(left, right, new WeakMap()); +} diff --git a/package/langUtil/toBoolean/index.test.ts b/package/langUtil/toBoolean/index.test.ts new file mode 100644 index 0000000..1c2d373 --- /dev/null +++ b/package/langUtil/toBoolean/index.test.ts @@ -0,0 +1,14 @@ +import { describe, expect, test } from "vitest"; +import toBoolean from "."; + +describe("lang toBoolean 유틸 함수 테스트", () => { + test("문자열 boolean 값을 변환한다.", () => { + expect(toBoolean("true")).toBe(true); + expect(toBoolean("false")).toBe(false); + }); + + test("숫자 값을 변환한다.", () => { + expect(toBoolean(1)).toBe(true); + expect(toBoolean(0)).toBe(false); + }); +}); diff --git a/package/langUtil/toBoolean/index.ts b/package/langUtil/toBoolean/index.ts new file mode 100644 index 0000000..b1110e3 --- /dev/null +++ b/package/langUtil/toBoolean/index.ts @@ -0,0 +1,26 @@ +const TRUE_SET = new Set(["true", "1", "yes", "y", "on"]); +const FALSE_SET = new Set(["false", "0", "no", "n", "off", ""]); + +export default function toBoolean(value: unknown): boolean { + if (typeof value === "boolean") { + return value; + } + + if (typeof value === "number") { + return value !== 0 && !Number.isNaN(value); + } + + if (typeof value === "string") { + const normalized = value.trim().toLowerCase(); + + if (TRUE_SET.has(normalized)) { + return true; + } + + if (FALSE_SET.has(normalized)) { + return false; + } + } + + return Boolean(value); +} diff --git a/package/langUtil/toNumber/index.test.ts b/package/langUtil/toNumber/index.test.ts new file mode 100644 index 0000000..542710c --- /dev/null +++ b/package/langUtil/toNumber/index.test.ts @@ -0,0 +1,12 @@ +import { describe, expect, test } from "vitest"; +import toNumber from "."; + +describe("lang toNumber 유틸 함수 테스트", () => { + test("숫자로 변환한다.", () => { + expect(toNumber("42")).toBe(42); + }); + + test("변환 실패 시 기본값을 반환한다.", () => { + expect(toNumber("x", 0)).toBe(0); + }); +}); diff --git a/package/langUtil/toNumber/index.ts b/package/langUtil/toNumber/index.ts new file mode 100644 index 0000000..9a89417 --- /dev/null +++ b/package/langUtil/toNumber/index.ts @@ -0,0 +1,12 @@ +export default function toNumber(value: unknown, defaultValue: number = Number.NaN): number { + if (typeof value === "number") { + return Number.isNaN(value) ? defaultValue : value; + } + + if (typeof value === "symbol") { + return defaultValue; + } + + const parsed = Number(value); + return Number.isNaN(parsed) ? defaultValue : parsed; +} diff --git a/package/langUtil/toString/index.test.ts b/package/langUtil/toString/index.test.ts new file mode 100644 index 0000000..c528cef --- /dev/null +++ b/package/langUtil/toString/index.test.ts @@ -0,0 +1,13 @@ +import { describe, expect, test } from "vitest"; +import toString from "."; + +describe("lang toString 유틸 함수 테스트", () => { + test("null/undefined는 빈 문자열로 변환한다.", () => { + expect(toString(null)).toBe(""); + expect(toString(undefined)).toBe(""); + }); + + test("다른 값은 문자열로 변환한다.", () => { + expect(toString(123)).toBe("123"); + }); +}); diff --git a/package/langUtil/toString/index.ts b/package/langUtil/toString/index.ts new file mode 100644 index 0000000..88055e2 --- /dev/null +++ b/package/langUtil/toString/index.ts @@ -0,0 +1,15 @@ +export default function toString(value: unknown): string { + if (value === null || value === undefined) { + return ""; + } + + if (typeof value === "string") { + return value; + } + + if (typeof value === "symbol") { + return value.toString(); + } + + return String(value); +} diff --git a/package/mathUtil/index.ts b/package/mathUtil/index.ts new file mode 100644 index 0000000..9499919 --- /dev/null +++ b/package/mathUtil/index.ts @@ -0,0 +1,8 @@ +export { default as max } from "./max"; +export { default as maxBy } from "./maxBy"; +export { default as mean } from "./mean"; +export { default as meanBy } from "./meanBy"; +export { default as median } from "./median"; +export { default as min } from "./min"; +export { default as minBy } from "./minBy"; +export { default as sumBy } from "./sumBy"; diff --git a/package/mathUtil/max/index.test.ts b/package/mathUtil/max/index.test.ts new file mode 100644 index 0000000..81e3687 --- /dev/null +++ b/package/mathUtil/max/index.test.ts @@ -0,0 +1,12 @@ +import { describe, expect, test } from "vitest"; +import max from "."; + +describe("math max 유틸 함수 테스트", () => { + test("최댓값을 반환한다.", () => { + expect(max([3, 1, 5])).toBe(5); + }); + + test("빈 배열이면 undefined를 반환한다.", () => { + expect(max([])).toBeUndefined(); + }); +}); diff --git a/package/mathUtil/max/index.ts b/package/mathUtil/max/index.ts new file mode 100644 index 0000000..1d2bb56 --- /dev/null +++ b/package/mathUtil/max/index.ts @@ -0,0 +1,9 @@ +export default function max(numbers: readonly number[]): number | undefined { + if (numbers.length === 0) { + return undefined; + } + + return numbers.reduce((accumulator, value) => + value > accumulator ? value : accumulator + ); +} diff --git a/package/mathUtil/maxBy/index.test.ts b/package/mathUtil/maxBy/index.test.ts new file mode 100644 index 0000000..07307b2 --- /dev/null +++ b/package/mathUtil/maxBy/index.test.ts @@ -0,0 +1,10 @@ +import { describe, expect, test } from "vitest"; +import maxBy from "."; + +describe("math maxBy 유틸 함수 테스트", () => { + test("iteratee 기준 최댓값 요소를 반환한다.", () => { + expect(maxBy([{ score: 10 }, { score: 5 }], (item) => item.score)).toEqual({ + score: 10, + }); + }); +}); diff --git a/package/mathUtil/maxBy/index.ts b/package/mathUtil/maxBy/index.ts new file mode 100644 index 0000000..a139054 --- /dev/null +++ b/package/mathUtil/maxBy/index.ts @@ -0,0 +1,22 @@ +export default function maxBy( + array: readonly T[], + iteratee: (value: T, index: number, array: readonly T[]) => number +): T | undefined { + if (array.length === 0) { + return undefined; + } + + let best = array[0]; + let bestScore = iteratee(array[0], 0, array); + + for (let index = 1; index < array.length; index++) { + const score = iteratee(array[index], index, array); + + if (score > bestScore) { + best = array[index]; + bestScore = score; + } + } + + return best; +} diff --git a/package/mathUtil/mean/index.test.ts b/package/mathUtil/mean/index.test.ts new file mode 100644 index 0000000..69a5ce1 --- /dev/null +++ b/package/mathUtil/mean/index.test.ts @@ -0,0 +1,12 @@ +import { describe, expect, test } from "vitest"; +import mean from "."; + +describe("math mean 유틸 함수 테스트", () => { + test("평균값을 반환한다.", () => { + expect(mean([2, 4, 6])).toBe(4); + }); + + test("빈 배열이면 NaN을 반환한다.", () => { + expect(Number.isNaN(mean([]))).toBe(true); + }); +}); diff --git a/package/mathUtil/mean/index.ts b/package/mathUtil/mean/index.ts new file mode 100644 index 0000000..02f18be --- /dev/null +++ b/package/mathUtil/mean/index.ts @@ -0,0 +1,8 @@ +export default function mean(numbers: readonly number[]): number { + if (numbers.length === 0) { + return Number.NaN; + } + + const total = numbers.reduce((accumulator, value) => accumulator + value, 0); + return total / numbers.length; +} diff --git a/package/mathUtil/meanBy/index.test.ts b/package/mathUtil/meanBy/index.test.ts new file mode 100644 index 0000000..945abc9 --- /dev/null +++ b/package/mathUtil/meanBy/index.test.ts @@ -0,0 +1,8 @@ +import { describe, expect, test } from "vitest"; +import meanBy from "."; + +describe("math meanBy 유틸 함수 테스트", () => { + test("iteratee 기준 평균값을 반환한다.", () => { + expect(meanBy([{ v: 3 }, { v: 5 }], (item) => item.v)).toBe(4); + }); +}); diff --git a/package/mathUtil/meanBy/index.ts b/package/mathUtil/meanBy/index.ts new file mode 100644 index 0000000..f175df3 --- /dev/null +++ b/package/mathUtil/meanBy/index.ts @@ -0,0 +1,12 @@ +import sumBy from "../sumBy"; + +export default function meanBy( + array: readonly T[], + iteratee: (value: T, index: number, array: readonly T[]) => number +): number { + if (array.length === 0) { + return Number.NaN; + } + + return sumBy(array, iteratee) / array.length; +} diff --git a/package/mathUtil/median/index.test.ts b/package/mathUtil/median/index.test.ts new file mode 100644 index 0000000..a0426a4 --- /dev/null +++ b/package/mathUtil/median/index.test.ts @@ -0,0 +1,12 @@ +import { describe, expect, test } from "vitest"; +import median from "."; + +describe("math median 유틸 함수 테스트", () => { + test("홀수 길이 배열의 중앙값을 반환한다.", () => { + expect(median([3, 1, 2])).toBe(2); + }); + + test("짝수 길이 배열의 중앙값 평균을 반환한다.", () => { + expect(median([1, 2, 3, 4])).toBe(2.5); + }); +}); diff --git a/package/mathUtil/median/index.ts b/package/mathUtil/median/index.ts new file mode 100644 index 0000000..8e79322 --- /dev/null +++ b/package/mathUtil/median/index.ts @@ -0,0 +1,14 @@ +export default function median(numbers: readonly number[]): number { + if (numbers.length === 0) { + return Number.NaN; + } + + const sorted = [...numbers].sort((a, b) => a - b); + const middle = Math.floor(sorted.length / 2); + + if (sorted.length % 2 !== 0) { + return sorted[middle]; + } + + return (sorted[middle - 1] + sorted[middle]) / 2; +} diff --git a/package/mathUtil/min/index.test.ts b/package/mathUtil/min/index.test.ts new file mode 100644 index 0000000..5162dbf --- /dev/null +++ b/package/mathUtil/min/index.test.ts @@ -0,0 +1,12 @@ +import { describe, expect, test } from "vitest"; +import min from "."; + +describe("math min 유틸 함수 테스트", () => { + test("최솟값을 반환한다.", () => { + expect(min([3, 1, 5])).toBe(1); + }); + + test("빈 배열이면 undefined를 반환한다.", () => { + expect(min([])).toBeUndefined(); + }); +}); diff --git a/package/mathUtil/min/index.ts b/package/mathUtil/min/index.ts new file mode 100644 index 0000000..66f63fa --- /dev/null +++ b/package/mathUtil/min/index.ts @@ -0,0 +1,9 @@ +export default function min(numbers: readonly number[]): number | undefined { + if (numbers.length === 0) { + return undefined; + } + + return numbers.reduce((accumulator, value) => + value < accumulator ? value : accumulator + ); +} diff --git a/package/mathUtil/minBy/index.test.ts b/package/mathUtil/minBy/index.test.ts new file mode 100644 index 0000000..7011db5 --- /dev/null +++ b/package/mathUtil/minBy/index.test.ts @@ -0,0 +1,10 @@ +import { describe, expect, test } from "vitest"; +import minBy from "."; + +describe("math minBy 유틸 함수 테스트", () => { + test("iteratee 기준 최솟값 요소를 반환한다.", () => { + expect(minBy([{ score: 10 }, { score: 5 }], (item) => item.score)).toEqual({ + score: 5, + }); + }); +}); diff --git a/package/mathUtil/minBy/index.ts b/package/mathUtil/minBy/index.ts new file mode 100644 index 0000000..282e438 --- /dev/null +++ b/package/mathUtil/minBy/index.ts @@ -0,0 +1,22 @@ +export default function minBy( + array: readonly T[], + iteratee: (value: T, index: number, array: readonly T[]) => number +): T | undefined { + if (array.length === 0) { + return undefined; + } + + let best = array[0]; + let bestScore = iteratee(array[0], 0, array); + + for (let index = 1; index < array.length; index++) { + const score = iteratee(array[index], index, array); + + if (score < bestScore) { + best = array[index]; + bestScore = score; + } + } + + return best; +} diff --git a/package/mathUtil/sumBy/index.test.ts b/package/mathUtil/sumBy/index.test.ts new file mode 100644 index 0000000..8582c22 --- /dev/null +++ b/package/mathUtil/sumBy/index.test.ts @@ -0,0 +1,8 @@ +import { describe, expect, test } from "vitest"; +import sumBy from "."; + +describe("math sumBy 유틸 함수 테스트", () => { + test("iteratee 결과를 합산한다.", () => { + expect(sumBy([{ score: 10 }, { score: 15 }], (item) => item.score)).toBe(25); + }); +}); diff --git a/package/mathUtil/sumBy/index.ts b/package/mathUtil/sumBy/index.ts new file mode 100644 index 0000000..290f395 --- /dev/null +++ b/package/mathUtil/sumBy/index.ts @@ -0,0 +1,8 @@ +export default function sumBy( + array: readonly T[], + iteratee: (value: T, index: number, array: readonly T[]) => number +): number { + return array.reduce((accumulator, value, index) => { + return accumulator + iteratee(value, index, array); + }, 0); +} diff --git a/package/numberUtil/_precision.ts b/package/numberUtil/_precision.ts new file mode 100644 index 0000000..a96de23 --- /dev/null +++ b/package/numberUtil/_precision.ts @@ -0,0 +1,17 @@ +export default function precisionMath( + value: number, + precision: number, + operator: (value: number) => number +): number { + if (precision === 0) { + return operator(value); + } + + const factor = Math.pow(10, Math.abs(precision)); + + if (precision > 0) { + return operator(value * factor) / factor; + } + + return operator(value / factor) * factor; +} diff --git a/package/numberUtil/ceil/index.test.ts b/package/numberUtil/ceil/index.test.ts new file mode 100644 index 0000000..678730b --- /dev/null +++ b/package/numberUtil/ceil/index.test.ts @@ -0,0 +1,12 @@ +import { describe, expect, test } from "vitest"; +import ceil from "."; + +describe("ceil 유틸 함수 테스트", () => { + test("소수점 올림을 지원한다.", () => { + expect(ceil(3.14159, 2)).toBe(3.15); + }); + + test("음수 precision도 지원한다.", () => { + expect(ceil(1231, -2)).toBe(1300); + }); +}); diff --git a/package/numberUtil/ceil/index.ts b/package/numberUtil/ceil/index.ts new file mode 100644 index 0000000..e9c0cb9 --- /dev/null +++ b/package/numberUtil/ceil/index.ts @@ -0,0 +1,5 @@ +import precisionMath from "../_precision"; + +export default function ceil(value: number, precision: number = 0): number { + return precisionMath(value, precision, Math.ceil); +} diff --git a/package/numberUtil/clamp/index.test.ts b/package/numberUtil/clamp/index.test.ts new file mode 100644 index 0000000..c283951 --- /dev/null +++ b/package/numberUtil/clamp/index.test.ts @@ -0,0 +1,13 @@ +import { describe, expect, test } from "vitest"; +import clamp from "."; + +describe("clamp 유틸 함수 테스트", () => { + test("범위를 벗어난 값을 경계값으로 제한한다.", () => { + expect(clamp(10, 0, 5)).toBe(5); + expect(clamp(-1, 0, 5)).toBe(0); + }); + + test("범위 안의 값은 그대로 반환한다.", () => { + expect(clamp(3, 0, 5)).toBe(3); + }); +}); diff --git a/package/numberUtil/clamp/index.ts b/package/numberUtil/clamp/index.ts new file mode 100644 index 0000000..8fe4bc1 --- /dev/null +++ b/package/numberUtil/clamp/index.ts @@ -0,0 +1,5 @@ +export default function clamp(value: number, lower: number, upper: number): number { + const min = Math.min(lower, upper); + const max = Math.max(lower, upper); + return Math.min(Math.max(value, min), max); +} diff --git a/package/numberUtil/floor/index.test.ts b/package/numberUtil/floor/index.test.ts new file mode 100644 index 0000000..c02f586 --- /dev/null +++ b/package/numberUtil/floor/index.test.ts @@ -0,0 +1,12 @@ +import { describe, expect, test } from "vitest"; +import floor from "."; + +describe("floor 유틸 함수 테스트", () => { + test("소수점 내림을 지원한다.", () => { + expect(floor(3.14159, 2)).toBe(3.14); + }); + + test("음수 precision도 지원한다.", () => { + expect(floor(1299, -2)).toBe(1200); + }); +}); diff --git a/package/numberUtil/floor/index.ts b/package/numberUtil/floor/index.ts new file mode 100644 index 0000000..5434c3e --- /dev/null +++ b/package/numberUtil/floor/index.ts @@ -0,0 +1,5 @@ +import precisionMath from "../_precision"; + +export default function floor(value: number, precision: number = 0): number { + return precisionMath(value, precision, Math.floor); +} diff --git a/package/numberUtil/inRange/index.test.ts b/package/numberUtil/inRange/index.test.ts new file mode 100644 index 0000000..0012681 --- /dev/null +++ b/package/numberUtil/inRange/index.test.ts @@ -0,0 +1,16 @@ +import { describe, expect, test } from "vitest"; +import inRange from "."; + +describe("inRange 유틸 함수 테스트", () => { + test("범위 내부면 true를 반환한다.", () => { + expect(inRange(3, 2, 5)).toBe(true); + }); + + test("상한값은 포함하지 않는다.", () => { + expect(inRange(5, 2, 5)).toBe(false); + }); + + test("end 생략 시 0부터 start까지로 처리한다.", () => { + expect(inRange(2, 5)).toBe(true); + }); +}); diff --git a/package/numberUtil/inRange/index.ts b/package/numberUtil/inRange/index.ts new file mode 100644 index 0000000..b32beb8 --- /dev/null +++ b/package/numberUtil/inRange/index.ts @@ -0,0 +1,12 @@ +export default function inRange( + value: number, + start: number, + end?: number +): boolean { + const rangeStart = end === undefined ? 0 : start; + const rangeEnd = end === undefined ? start : end; + const min = Math.min(rangeStart, rangeEnd); + const max = Math.max(rangeStart, rangeEnd); + + return value >= min && value < max; +} diff --git a/package/numberUtil/index.ts b/package/numberUtil/index.ts index 4ad8b35..a598030 100644 --- a/package/numberUtil/index.ts +++ b/package/numberUtil/index.ts @@ -1,3 +1,9 @@ -export { default as sum } from "./sum"; -export { default as subtract } from "./subtract"; +export { default as ceil } from "./ceil"; +export { default as clamp } from "./clamp"; +export { default as floor } from "./floor"; +export { default as inRange } from "./inRange"; export { default as multiply } from "./multiply"; +export { default as random } from "./random"; +export { default as round } from "./round"; +export { default as subtract } from "./subtract"; +export { default as sum } from "./sum"; diff --git a/package/numberUtil/random/index.test.ts b/package/numberUtil/random/index.test.ts new file mode 100644 index 0000000..dc610ae --- /dev/null +++ b/package/numberUtil/random/index.test.ts @@ -0,0 +1,28 @@ +import { describe, expect, test, vi } from "vitest"; +import random from "."; + +describe("random 유틸 함수 테스트", () => { + test("정수 랜덤 값을 반환한다.", () => { + vi.spyOn(Math, "random").mockReturnValue(0.5); + + expect(random(1, 3)).toBe(2); + + vi.restoreAllMocks(); + }); + + test("floating=true면 실수를 반환한다.", () => { + vi.spyOn(Math, "random").mockReturnValue(0.5); + + expect(random(1, 3, true)).toBe(2); + + vi.restoreAllMocks(); + }); + + test("인자 하나면 0부터 upper 범위로 동작한다.", () => { + vi.spyOn(Math, "random").mockReturnValue(0.5); + + expect(random(4)).toBe(2); + + vi.restoreAllMocks(); + }); +}); diff --git a/package/numberUtil/random/index.ts b/package/numberUtil/random/index.ts new file mode 100644 index 0000000..383a74d --- /dev/null +++ b/package/numberUtil/random/index.ts @@ -0,0 +1,27 @@ +export default function random( + lower: number = 0, + upper: number = 1, + floating: boolean = false +): number { + let min = lower; + let max = upper; + + if (arguments.length === 1) { + min = 0; + max = lower; + } + + if (min > max) { + const temp = min; + min = max; + max = temp; + } + + const shouldFloat = floating || !Number.isInteger(min) || !Number.isInteger(max); + + if (shouldFloat) { + return Math.random() * (max - min) + min; + } + + return Math.floor(Math.random() * (max - min + 1)) + min; +} diff --git a/package/numberUtil/round/index.test.ts b/package/numberUtil/round/index.test.ts new file mode 100644 index 0000000..2cd6458 --- /dev/null +++ b/package/numberUtil/round/index.test.ts @@ -0,0 +1,12 @@ +import { describe, expect, test } from "vitest"; +import round from "."; + +describe("round 유틸 함수 테스트", () => { + test("소수점 자릿수 반올림을 지원한다.", () => { + expect(round(3.14159, 2)).toBe(3.14); + }); + + test("음수 precision도 지원한다.", () => { + expect(round(1234, -2)).toBe(1200); + }); +}); diff --git a/package/numberUtil/round/index.ts b/package/numberUtil/round/index.ts new file mode 100644 index 0000000..08d54ee --- /dev/null +++ b/package/numberUtil/round/index.ts @@ -0,0 +1,5 @@ +import precisionMath from "../_precision"; + +export default function round(value: number, precision: number = 0): number { + return precisionMath(value, precision, Math.round); +} diff --git a/package/objectUtil/deepClone/index.test.ts b/package/objectUtil/deepClone/index.test.ts new file mode 100644 index 0000000..35d0f56 --- /dev/null +++ b/package/objectUtil/deepClone/index.test.ts @@ -0,0 +1,39 @@ +import { describe, expect, test } from "vitest"; +import deepClone from "."; + +describe("deepClone 유틸 함수 테스트", () => { + test("중첩 객체를 깊은 복사한다.", () => { + const source = { user: { name: "Kim" }, list: [1, 2, 3] }; + const cloned = deepClone(source); + + expect(cloned).toEqual(source); + expect(cloned).not.toBe(source); + expect(cloned.user).not.toBe(source.user); + expect(cloned.list).not.toBe(source.list); + }); + + test("Date와 Map 타입을 복사한다.", () => { + const now = new Date(); + const source = { + createdAt: now, + metadata: new Map([["role", "admin"]]), + }; + + const cloned = deepClone(source); + + expect(cloned.createdAt).toEqual(now); + expect(cloned.createdAt).not.toBe(now); + expect(cloned.metadata).toEqual(source.metadata); + expect(cloned.metadata).not.toBe(source.metadata); + }); + + test("순환 참조 객체를 복사할 수 있다.", () => { + const source: { name: string; self?: unknown } = { name: "loop" }; + source.self = source; + + const cloned = deepClone(source); + + expect(cloned).not.toBe(source); + expect(cloned.self).toBe(cloned); + }); +}); diff --git a/package/objectUtil/deepClone/index.ts b/package/objectUtil/deepClone/index.ts new file mode 100644 index 0000000..9a7d021 --- /dev/null +++ b/package/objectUtil/deepClone/index.ts @@ -0,0 +1,86 @@ +function cloneFallback(value: T, seen: WeakMap): T { + if (value === null || typeof value !== "object") { + return value; + } + + if (seen.has(value)) { + return seen.get(value) as T; + } + + if (value instanceof Date) { + return new Date(value.getTime()) as T; + } + + if (value instanceof RegExp) { + return new RegExp(value.source, value.flags) as T; + } + + if (value instanceof Map) { + const clonedMap = new Map(); + seen.set(value, clonedMap); + + value.forEach((mapValue, mapKey) => { + clonedMap.set( + cloneFallback(mapKey, seen), + cloneFallback(mapValue, seen) + ); + }); + + return clonedMap as T; + } + + if (value instanceof Set) { + const clonedSet = new Set(); + seen.set(value, clonedSet); + + value.forEach((item) => { + clonedSet.add(cloneFallback(item, seen)); + }); + + return clonedSet as T; + } + + if (Array.isArray(value)) { + const clonedArray: unknown[] = []; + seen.set(value, clonedArray); + + value.forEach((item, index) => { + clonedArray[index] = cloneFallback(item, seen); + }); + + return clonedArray as T; + } + + const clonedObject = Object.create(Object.getPrototypeOf(value)); + seen.set(value, clonedObject); + + const descriptors = Object.getOwnPropertyDescriptors(value); + + Reflect.ownKeys(descriptors).forEach((key) => { + const descriptor = descriptors[key as keyof typeof descriptors]; + + if (!descriptor) { + return; + } + + if ("value" in descriptor) { + descriptor.value = cloneFallback(descriptor.value, seen); + } + + Object.defineProperty(clonedObject, key, descriptor); + }); + + return clonedObject; +} + +export default function deepClone(value: T): T { + if (typeof globalThis.structuredClone === "function") { + try { + return globalThis.structuredClone(value); + } catch { + return cloneFallback(value, new WeakMap()); + } + } + + return cloneFallback(value, new WeakMap()); +} diff --git a/package/objectUtil/defaults/index.test.ts b/package/objectUtil/defaults/index.test.ts new file mode 100644 index 0000000..58c146b --- /dev/null +++ b/package/objectUtil/defaults/index.test.ts @@ -0,0 +1,16 @@ +import { describe, expect, test } from "vitest"; +import defaults from "."; + +describe("defaults 유틸 함수 테스트", () => { + test("undefined인 값에만 기본값을 채운다.", () => { + expect(defaults({ a: 1, b: undefined }, { b: 2, c: 3 })).toEqual({ + a: 1, + b: 2, + c: 3, + }); + }); + + test("기존 값은 덮어쓰지 않는다.", () => { + expect(defaults({ a: 1 }, { a: 2 })).toEqual({ a: 1 }); + }); +}); diff --git a/package/objectUtil/defaults/index.ts b/package/objectUtil/defaults/index.ts new file mode 100644 index 0000000..f11999d --- /dev/null +++ b/package/objectUtil/defaults/index.ts @@ -0,0 +1,16 @@ +export default function defaults>( + target: T, + ...sources: ReadonlyArray> +): T { + const output = target as Record; + + sources.forEach((source) => { + Reflect.ownKeys(source).forEach((key) => { + if (output[key] === undefined) { + output[key] = source[key as keyof typeof source]; + } + }); + }); + + return target; +} diff --git a/package/objectUtil/get/index.test.ts b/package/objectUtil/get/index.test.ts new file mode 100644 index 0000000..d4a96e7 --- /dev/null +++ b/package/objectUtil/get/index.test.ts @@ -0,0 +1,22 @@ +import { describe, expect, test } from "vitest"; +import get from "."; + +describe("get 유틸 함수 테스트", () => { + test("경로로 중첩 값을 조회한다.", () => { + const data = { user: { profile: { name: "Kim" } } }; + + expect(get(data, "user.profile.name")).toBe("Kim"); + }); + + test("값이 없으면 기본값을 반환한다.", () => { + const data = { user: {} }; + + expect(get(data, "user.profile.name", "guest")).toBe("guest"); + }); + + test("배열 경로도 조회할 수 있다.", () => { + const data = { users: [{ name: "Kim" }] }; + + expect(get(data, "users[0].name")).toBe("Kim"); + }); +}); diff --git a/package/objectUtil/get/index.ts b/package/objectUtil/get/index.ts new file mode 100644 index 0000000..8147981 --- /dev/null +++ b/package/objectUtil/get/index.ts @@ -0,0 +1,34 @@ +type PathSegment = string | number; + +function toPath(path: string | readonly PathSegment[]): PathSegment[] { + if (typeof path !== "string") { + return [...path]; + } + + return path + .replace(/\[(\d+)\]/g, ".$1") + .split(".") + .filter(Boolean) + .map((segment: string) => + /^\d+$/.test(segment) ? Number(segment) : segment + ); +} + +export default function get( + obj: T, + path: string | readonly PathSegment[], + defaultValue?: D +): unknown | D { + const segments = toPath(path); + let current: unknown = obj; + + for (const segment of segments) { + if (current === null || current === undefined) { + return defaultValue as D; + } + + current = (current as Record)[segment]; + } + + return current === undefined ? (defaultValue as D) : current; +} diff --git a/package/objectUtil/has/index.test.ts b/package/objectUtil/has/index.test.ts new file mode 100644 index 0000000..473ca22 --- /dev/null +++ b/package/objectUtil/has/index.test.ts @@ -0,0 +1,16 @@ +import { describe, expect, test } from "vitest"; +import has from "."; + +describe("has 유틸 함수 테스트", () => { + test("경로가 존재하면 true를 반환한다.", () => { + expect(has({ a: { b: 1 } }, "a.b")).toBe(true); + }); + + test("경로가 없으면 false를 반환한다.", () => { + expect(has({ a: { b: 1 } }, "a.c")).toBe(false); + }); + + test("배열 인덱스 경로를 지원한다.", () => { + expect(has({ a: [{ b: 1 }] }, "a[0].b")).toBe(true); + }); +}); diff --git a/package/objectUtil/has/index.ts b/package/objectUtil/has/index.ts new file mode 100644 index 0000000..3f163bc --- /dev/null +++ b/package/objectUtil/has/index.ts @@ -0,0 +1,37 @@ +type PathSegment = string | number; + +function toPath(path: string | readonly PathSegment[]): PathSegment[] { + if (typeof path !== "string") { + return [...path]; + } + + return path + .replace(/\[(\d+)\]/g, ".$1") + .split(".") + .filter(Boolean) + .map((segment: string) => + /^\d+$/.test(segment) ? Number(segment) : segment + ); +} + +export default function has( + obj: unknown, + path: string | readonly PathSegment[] +): boolean { + const segments = toPath(path); + let current = obj; + + for (const segment of segments) { + if ( + current === null || + current === undefined || + !Object.prototype.hasOwnProperty.call(current, segment) + ) { + return false; + } + + current = (current as Record)[segment]; + } + + return true; +} diff --git a/package/objectUtil/index.ts b/package/objectUtil/index.ts index 36b3c9e..84f5c25 100644 --- a/package/objectUtil/index.ts +++ b/package/objectUtil/index.ts @@ -1,3 +1,13 @@ -export { default as clearNullProperties } from "./clearNullProperties"; -export { default as deepFreeze } from "./deepFreeze"; -export { default as removeKey } from "./removeKey"; +export { default as clearNullProperties } from "./clearNullProperties"; +export { default as defaults } from "./defaults"; +export { default as deepClone } from "./deepClone"; +export { default as deepFreeze } from "./deepFreeze"; +export { default as get } from "./get"; +export { default as has } from "./has"; +export { default as invert } from "./invert"; +export { default as mapValues } from "./mapValues"; +export { default as merge } from "./merge"; +export { default as omit } from "./omit"; +export { default as pick } from "./pick"; +export { default as removeKey } from "./removeKey"; +export { default as set } from "./set"; diff --git a/package/objectUtil/invert/index.test.ts b/package/objectUtil/invert/index.test.ts new file mode 100644 index 0000000..5fbe380 --- /dev/null +++ b/package/objectUtil/invert/index.test.ts @@ -0,0 +1,12 @@ +import { describe, expect, test } from "vitest"; +import invert from "."; + +describe("invert 유틸 함수 테스트", () => { + test("key와 value를 뒤집는다.", () => { + expect(invert({ a: "x", b: "y" })).toEqual({ x: "a", y: "b" }); + }); + + test("중복 value가 있으면 마지막 key가 남는다.", () => { + expect(invert({ a: "x", b: "x" })).toEqual({ x: "b" }); + }); +}); diff --git a/package/objectUtil/invert/index.ts b/package/objectUtil/invert/index.ts new file mode 100644 index 0000000..97ba55e --- /dev/null +++ b/package/objectUtil/invert/index.ts @@ -0,0 +1,12 @@ +export default function invert( + obj: Record +): Record { + const result: Record = {}; + + Reflect.ownKeys(obj).forEach((key) => { + const value = obj[key]; + result[String(value)] = String(key); + }); + + return result; +} diff --git a/package/objectUtil/mapValues/index.test.ts b/package/objectUtil/mapValues/index.test.ts new file mode 100644 index 0000000..0542df3 --- /dev/null +++ b/package/objectUtil/mapValues/index.test.ts @@ -0,0 +1,17 @@ +import { describe, expect, test } from "vitest"; +import mapValues from "."; + +describe("mapValues 유틸 함수 테스트", () => { + test("객체의 값을 변환한다.", () => { + expect(mapValues({ a: 1, b: 2 }, (value) => value * 2)).toEqual({ + a: 2, + b: 4, + }); + }); + + test("key를 활용해서 값을 변환할 수 있다.", () => { + expect(mapValues({ a: 1 }, (value, key) => `${String(key)}:${value}`)).toEqual({ + a: "a:1", + }); + }); +}); diff --git a/package/objectUtil/mapValues/index.ts b/package/objectUtil/mapValues/index.ts new file mode 100644 index 0000000..6fc1ae4 --- /dev/null +++ b/package/objectUtil/mapValues/index.ts @@ -0,0 +1,15 @@ +export default function mapValues< + T extends Record, + R +>( + obj: T, + iteratee: (value: T[keyof T], key: keyof T, obj: T) => R +): Record { + const result = {} as Record; + + (Object.keys(obj) as Array).forEach((key) => { + result[key] = iteratee(obj[key], key, obj); + }); + + return result; +} diff --git a/package/objectUtil/merge/index.test.ts b/package/objectUtil/merge/index.test.ts new file mode 100644 index 0000000..d513d35 --- /dev/null +++ b/package/objectUtil/merge/index.test.ts @@ -0,0 +1,19 @@ +import { describe, expect, test } from "vitest"; +import merge from "."; + +describe("merge 유틸 함수 테스트", () => { + test("여러 객체를 깊게 병합한다.", () => { + const target = { a: { b: 1 } }; + + const result = merge(target, { a: { c: 2 } }, { d: 3 }); + + expect(result).toEqual({ a: { b: 1, c: 2 }, d: 3 }); + expect(result).toBe(target); + }); + + test("배열은 새 배열로 덮어쓴다.", () => { + const result = merge({ a: [1, 2] }, { a: [3] }); + + expect(result).toEqual({ a: [3] }); + }); +}); diff --git a/package/objectUtil/merge/index.ts b/package/objectUtil/merge/index.ts new file mode 100644 index 0000000..bb4797e --- /dev/null +++ b/package/objectUtil/merge/index.ts @@ -0,0 +1,34 @@ +import isPlainObject from "../../typeUtil/isPlainObject"; + +function mergeValue(targetValue: unknown, sourceValue: unknown): unknown { + if (Array.isArray(sourceValue)) { + return [...sourceValue]; + } + + if (isPlainObject(sourceValue)) { + const targetBase = isPlainObject(targetValue) + ? (targetValue as Record) + : {}; + + return merge(targetBase, sourceValue as Record); + } + + return sourceValue; +} + +export default function merge>( + target: T, + ...sources: ReadonlyArray> +): T { + const output = target as Record; + + sources.forEach((source) => { + Reflect.ownKeys(source).forEach((key) => { + const sourceValue = source[key as keyof typeof source]; + const targetValue = output[key]; + output[key] = mergeValue(targetValue, sourceValue); + }); + }); + + return target; +} diff --git a/package/objectUtil/omit/index.test.ts b/package/objectUtil/omit/index.test.ts new file mode 100644 index 0000000..3f67d1d --- /dev/null +++ b/package/objectUtil/omit/index.test.ts @@ -0,0 +1,18 @@ +import { describe, expect, test } from "vitest"; +import omit from "."; + +describe("omit 유틸 함수 테스트", () => { + test("지정한 키를 제거한 새 객체를 반환한다.", () => { + const user = { id: 1, name: "Kim", age: 29 }; + + expect(omit(user, ["age"])).toEqual({ id: 1, name: "Kim" }); + }); + + test("원본 객체는 변경하지 않는다.", () => { + const user = { id: 1, name: "Kim", age: 29 }; + + omit(user, ["name"]); + + expect(user).toEqual({ id: 1, name: "Kim", age: 29 }); + }); +}); diff --git a/package/objectUtil/omit/index.ts b/package/objectUtil/omit/index.ts new file mode 100644 index 0000000..ef46a4d --- /dev/null +++ b/package/objectUtil/omit/index.ts @@ -0,0 +1,12 @@ +export default function omit, K extends keyof T>( + obj: T, + keys: readonly K[] +): Omit { + const cloned = { ...obj } as T; + + keys.forEach((key) => { + delete cloned[key]; + }); + + return cloned as Omit; +} diff --git a/package/objectUtil/pick/index.test.ts b/package/objectUtil/pick/index.test.ts new file mode 100644 index 0000000..8c271d5 --- /dev/null +++ b/package/objectUtil/pick/index.test.ts @@ -0,0 +1,16 @@ +import { describe, expect, test } from "vitest"; +import pick from "."; + +describe("pick 유틸 함수 테스트", () => { + test("지정한 키만 포함한 새 객체를 반환한다.", () => { + const user = { id: 1, name: "Kim", age: 29 }; + + expect(pick(user, ["id", "name"])).toEqual({ id: 1, name: "Kim" }); + }); + + test("존재하지 않는 키는 무시한다.", () => { + const user = { id: 1, name: "Kim" }; + + expect(pick(user, ["id", "email" as keyof typeof user])).toEqual({ id: 1 }); + }); +}); diff --git a/package/objectUtil/pick/index.ts b/package/objectUtil/pick/index.ts new file mode 100644 index 0000000..012f002 --- /dev/null +++ b/package/objectUtil/pick/index.ts @@ -0,0 +1,14 @@ +export default function pick, K extends keyof T>( + obj: T, + keys: readonly K[] +): Pick { + const result = {} as Pick; + + keys.forEach((key) => { + if (Object.prototype.hasOwnProperty.call(obj, key)) { + result[key] = obj[key]; + } + }); + + return result; +} diff --git a/package/objectUtil/set/index.test.ts b/package/objectUtil/set/index.test.ts new file mode 100644 index 0000000..70e4213 --- /dev/null +++ b/package/objectUtil/set/index.test.ts @@ -0,0 +1,20 @@ +import { describe, expect, test } from "vitest"; +import set from "."; + +describe("set 유틸 함수 테스트", () => { + test("중첩 경로에 값을 설정한다.", () => { + const target = { a: {} }; + + set(target, "a.b.c", 1); + + expect(target).toEqual({ a: { b: { c: 1 } } }); + }); + + test("배열 경로를 생성하며 값을 설정한다.", () => { + const target = {} as Record; + + set(target, "users[0].name", "Kim"); + + expect(target).toEqual({ users: [{ name: "Kim" }] }); + }); +}); diff --git a/package/objectUtil/set/index.ts b/package/objectUtil/set/index.ts new file mode 100644 index 0000000..b4bccf4 --- /dev/null +++ b/package/objectUtil/set/index.ts @@ -0,0 +1,49 @@ +type PathSegment = string | number; + +function toPath(path: string | readonly PathSegment[]): PathSegment[] { + if (typeof path !== "string") { + return [...path]; + } + + return path + .replace(/\[(\d+)\]/g, ".$1") + .split(".") + .filter(Boolean) + .map((segment: string) => + /^\d+$/.test(segment) ? Number(segment) : segment + ); +} + +export default function set>( + obj: T, + path: string | readonly PathSegment[], + value: unknown +): T { + const segments = toPath(path); + + if (segments.length === 0) { + return obj; + } + + let current: Record = obj; + + for (let i = 0; i < segments.length - 1; i++) { + const segment = segments[i]; + const nextSegment = segments[i + 1]; + const currentValue = current[segment]; + + if ( + currentValue === null || + currentValue === undefined || + typeof currentValue !== "object" + ) { + current[segment] = typeof nextSegment === "number" ? [] : {}; + } + + current = current[segment] as Record; + } + + current[segments[segments.length - 1]] = value; + + return obj; +} diff --git a/package/promiseUtil/defer/index.test.ts b/package/promiseUtil/defer/index.test.ts new file mode 100644 index 0000000..d99832b --- /dev/null +++ b/package/promiseUtil/defer/index.test.ts @@ -0,0 +1,18 @@ +import { describe, expect, test } from "vitest"; +import defer from "."; + +describe("promise defer 유틸 함수 테스트", () => { + test("resolve로 Promise를 완료할 수 있다.", async () => { + const d = defer(); + d.resolve(10); + + await expect(d.promise).resolves.toBe(10); + }); + + test("reject로 Promise를 실패시킬 수 있다.", async () => { + const d = defer(); + d.reject(new Error("fail")); + + await expect(d.promise).rejects.toThrow("fail"); + }); +}); diff --git a/package/promiseUtil/defer/index.ts b/package/promiseUtil/defer/index.ts new file mode 100644 index 0000000..dffc38e --- /dev/null +++ b/package/promiseUtil/defer/index.ts @@ -0,0 +1,17 @@ +export interface Deferred { + promise: Promise; + resolve: (value: T | PromiseLike) => void; + reject: (reason?: unknown) => void; +} + +export default function defer(): Deferred { + let resolve!: (value: T | PromiseLike) => void; + let reject!: (reason?: unknown) => void; + + const promise = new Promise((res, rej) => { + resolve = res; + reject = rej; + }); + + return { promise, resolve, reject }; +} diff --git a/package/promiseUtil/index.ts b/package/promiseUtil/index.ts new file mode 100644 index 0000000..4edcfa5 --- /dev/null +++ b/package/promiseUtil/index.ts @@ -0,0 +1,5 @@ +export { default as defer } from "./defer"; +export { default as retryWithDelay } from "./retryWithDelay"; +export { default as settle } from "./settle"; +export { default as toResult } from "./toResult"; +export { default as withTimeout } from "./withTimeout"; diff --git a/package/promiseUtil/retryWithDelay/index.test.ts b/package/promiseUtil/retryWithDelay/index.test.ts new file mode 100644 index 0000000..f900646 --- /dev/null +++ b/package/promiseUtil/retryWithDelay/index.test.ts @@ -0,0 +1,27 @@ +import { describe, expect, test, vi } from "vitest"; +import retryWithDelay from "."; + +describe("promise retryWithDelay 유틸 함수 테스트", () => { + test("실패 시 재시도 후 성공하면 결과를 반환한다.", async () => { + const fn = vi + .fn() + .mockRejectedValueOnce(new Error("fail 1")) + .mockRejectedValueOnce(new Error("fail 2")) + .mockResolvedValue("ok"); + + const result = await retryWithDelay(fn, { retries: 5, delay: 0 }); + + expect(result).toBe("ok"); + expect(fn).toHaveBeenCalledTimes(3); + }); + + test("재시도 한도 초과 시 에러를 던진다.", async () => { + const fn = vi.fn().mockRejectedValue(new Error("always fail")); + + await expect(retryWithDelay(fn, { retries: 2, delay: 0 })).rejects.toThrow( + "always fail" + ); + + expect(fn).toHaveBeenCalledTimes(2); + }); +}); diff --git a/package/promiseUtil/retryWithDelay/index.ts b/package/promiseUtil/retryWithDelay/index.ts new file mode 100644 index 0000000..a26dfd8 --- /dev/null +++ b/package/promiseUtil/retryWithDelay/index.ts @@ -0,0 +1,37 @@ +interface RetryWithDelayOptions { + retries?: number; + delay?: number; + factor?: number; +} + +function wait(ms: number): Promise { + return new Promise((resolve) => setTimeout(resolve, ms)); +} + +export default async function retryWithDelay( + fn: () => Promise, + options: RetryWithDelayOptions = {} +): Promise { + const { retries = 3, delay = 100, factor = 1 } = options; + + let attempt = 0; + let nextDelay = Math.max(0, delay); + + while (true) { + try { + return await fn(); + } catch (error) { + attempt++; + + if (attempt >= Math.max(1, retries)) { + throw error; + } + + if (nextDelay > 0) { + await wait(nextDelay); + } + + nextDelay *= factor; + } + } +} diff --git a/package/promiseUtil/settle/index.test.ts b/package/promiseUtil/settle/index.test.ts new file mode 100644 index 0000000..ae46bb2 --- /dev/null +++ b/package/promiseUtil/settle/index.test.ts @@ -0,0 +1,15 @@ +import { describe, expect, test } from "vitest"; +import settle from "."; + +describe("promise settle 유틸 함수 테스트", () => { + test("성공/실패를 분리해 반환한다.", async () => { + const result = await settle([ + Promise.resolve(1), + Promise.reject(new Error("fail")), + Promise.resolve(2), + ]); + + expect(result.fulfilled).toEqual([1, 2]); + expect(result.rejected).toHaveLength(1); + }); +}); diff --git a/package/promiseUtil/settle/index.ts b/package/promiseUtil/settle/index.ts new file mode 100644 index 0000000..2fa152a --- /dev/null +++ b/package/promiseUtil/settle/index.ts @@ -0,0 +1,23 @@ +export interface SettledResult { + fulfilled: T[]; + rejected: unknown[]; +} + +export default async function settle( + promises: ReadonlyArray> +): Promise> { + const results = await Promise.allSettled(promises); + + return results.reduce>( + (accumulator, result) => { + if (result.status === "fulfilled") { + accumulator.fulfilled.push(result.value); + } else { + accumulator.rejected.push(result.reason); + } + + return accumulator; + }, + { fulfilled: [], rejected: [] } + ); +} diff --git a/package/promiseUtil/toResult/index.test.ts b/package/promiseUtil/toResult/index.test.ts new file mode 100644 index 0000000..8ef9778 --- /dev/null +++ b/package/promiseUtil/toResult/index.test.ts @@ -0,0 +1,17 @@ +import { describe, expect, test } from "vitest"; +import toResult from "."; + +describe("promise toResult 유틸 함수 테스트", () => { + test("성공 시 data를 반환한다.", async () => { + const result = await toResult(Promise.resolve(10)); + + expect(result).toEqual({ data: 10, error: null }); + }); + + test("실패 시 error를 반환한다.", async () => { + const result = await toResult(Promise.reject(new Error("fail"))); + + expect(result.data).toBeNull(); + expect(result.error).toBeInstanceOf(Error); + }); +}); diff --git a/package/promiseUtil/toResult/index.ts b/package/promiseUtil/toResult/index.ts new file mode 100644 index 0000000..50ab96c --- /dev/null +++ b/package/promiseUtil/toResult/index.ts @@ -0,0 +1,13 @@ +export type PromiseResult = + | { data: T; error: null } + | { data: null; error: unknown }; + +export default async function toResult( + promise: Promise +): Promise> { + try { + return { data: await promise, error: null }; + } catch (error) { + return { data: null, error }; + } +} diff --git a/package/promiseUtil/withTimeout/index.test.ts b/package/promiseUtil/withTimeout/index.test.ts new file mode 100644 index 0000000..64526ec --- /dev/null +++ b/package/promiseUtil/withTimeout/index.test.ts @@ -0,0 +1,25 @@ +import { describe, expect, test } from "vitest"; +import withTimeout from "."; + +describe("promise withTimeout 유틸 함수 테스트", () => { + test("시간 내 완료되면 원래 결과를 반환한다.", async () => { + const result = await withTimeout(Promise.resolve("ok"), 50); + expect(result).toBe("ok"); + }); + + test("시간 초과 시 에러를 던진다.", async () => { + await expect( + withTimeout(new Promise((resolve) => setTimeout(() => resolve("late"), 30)), 5) + ).rejects.toThrow("timed out"); + }); + + test("fallback이 있으면 fallback 결과를 반환한다.", async () => { + const result = await withTimeout( + new Promise((resolve) => setTimeout(() => resolve("late"), 30)), + 5, + { fallback: () => "fallback" } + ); + + expect(result).toBe("fallback"); + }); +}); diff --git a/package/promiseUtil/withTimeout/index.ts b/package/promiseUtil/withTimeout/index.ts new file mode 100644 index 0000000..6d88179 --- /dev/null +++ b/package/promiseUtil/withTimeout/index.ts @@ -0,0 +1,36 @@ +interface WithTimeoutOptions { + message?: string; + fallback?: () => T | Promise; +} + +export default async function withTimeout( + promise: Promise, + ms: number, + options: WithTimeoutOptions = {} +): Promise { + const { message = `Operation timed out after ${ms}ms`, fallback } = options; + + let timer: ReturnType; + + const timeoutPromise = new Promise((resolve, reject) => { + timer = setTimeout(async () => { + if (fallback) { + try { + resolve(await fallback()); + return; + } catch (error) { + reject(error); + return; + } + } + + reject(new Error(message)); + }, Math.max(0, ms)); + }); + + try { + return await Promise.race([promise, timeoutPromise]); + } finally { + clearTimeout(timer!); + } +} diff --git a/package/stringUtil/_wordTokenize.ts b/package/stringUtil/_wordTokenize.ts new file mode 100644 index 0000000..81de0ba --- /dev/null +++ b/package/stringUtil/_wordTokenize.ts @@ -0,0 +1,13 @@ +export default function wordTokenize(text: string): string[] { + if (text.trim() === "") { + return []; + } + + const normalized = text + .replace(/([a-z0-9])([A-Z])/g, "$1 $2") + .replace(/[_\-]+/g, " "); + + return normalized + .match(/[\p{L}\p{N}]+/gu) + ?.map((word) => word.toLowerCase()) ?? []; +} diff --git a/package/stringUtil/camelCase/index.test.ts b/package/stringUtil/camelCase/index.test.ts new file mode 100644 index 0000000..40e47b3 --- /dev/null +++ b/package/stringUtil/camelCase/index.test.ts @@ -0,0 +1,12 @@ +import { describe, expect, test } from "vitest"; +import camelCase from "."; + +describe("camelCase 유틸 함수 테스트", () => { + test("문장을 camelCase로 변환한다.", () => { + expect(camelCase("hello world")).toBe("helloWorld"); + }); + + test("구분자가 섞여 있어도 정상 동작한다.", () => { + expect(camelCase("hello_world-test Value")).toBe("helloWorldTestValue"); + }); +}); diff --git a/package/stringUtil/camelCase/index.ts b/package/stringUtil/camelCase/index.ts new file mode 100644 index 0000000..9d238f7 --- /dev/null +++ b/package/stringUtil/camelCase/index.ts @@ -0,0 +1,15 @@ +import wordTokenize from "../_wordTokenize"; + +export default function camelCase(text: string): string { + const words = wordTokenize(text); + + if (words.length === 0) { + return ""; + } + + return words + .map((word, index) => + index === 0 ? word : word.charAt(0).toUpperCase() + word.slice(1) + ) + .join(""); +} diff --git a/package/stringUtil/capitalize/index.test.ts b/package/stringUtil/capitalize/index.test.ts new file mode 100644 index 0000000..4c6ffb9 --- /dev/null +++ b/package/stringUtil/capitalize/index.test.ts @@ -0,0 +1,12 @@ +import { describe, expect, test } from "vitest"; +import capitalize from "."; + +describe("capitalize 유틸 함수 테스트", () => { + test("첫 글자를 대문자로 변환한다.", () => { + expect(capitalize("hello")).toBe("Hello"); + }); + + test("나머지 글자는 소문자로 변환한다.", () => { + expect(capitalize("hELLO")).toBe("Hello"); + }); +}); diff --git a/package/stringUtil/capitalize/index.ts b/package/stringUtil/capitalize/index.ts new file mode 100644 index 0000000..eb469f5 --- /dev/null +++ b/package/stringUtil/capitalize/index.ts @@ -0,0 +1,7 @@ +export default function capitalize(text: string): string { + if (text.length === 0) { + return ""; + } + + return text.charAt(0).toUpperCase() + text.slice(1).toLowerCase(); +} diff --git a/package/stringUtil/index.ts b/package/stringUtil/index.ts index 2dabd38..a5f3f68 100644 --- a/package/stringUtil/index.ts +++ b/package/stringUtil/index.ts @@ -1,3 +1,9 @@ -export { default as escapeHtml } from "./escapeHtml"; -export { default as unescapeHtml } from "./unescapeHtml"; -export { default as slugify } from "./slugify"; +export { default as camelCase } from "./camelCase"; +export { default as capitalize } from "./capitalize"; +export { default as escapeHtml } from "./escapeHtml"; +export { default as kebabCase } from "./kebabCase"; +export { default as pascalCase } from "./pascalCase"; +export { default as slugify } from "./slugify"; +export { default as snakeCase } from "./snakeCase"; +export { default as truncate } from "./truncate"; +export { default as unescapeHtml } from "./unescapeHtml"; diff --git a/package/stringUtil/kebabCase/index.test.ts b/package/stringUtil/kebabCase/index.test.ts new file mode 100644 index 0000000..5aad5e6 --- /dev/null +++ b/package/stringUtil/kebabCase/index.test.ts @@ -0,0 +1,12 @@ +import { describe, expect, test } from "vitest"; +import kebabCase from "."; + +describe("kebabCase 유틸 함수 테스트", () => { + test("문장을 kebab-case로 변환한다.", () => { + expect(kebabCase("Hello World")).toBe("hello-world"); + }); + + test("camelCase 문자열도 변환한다.", () => { + expect(kebabCase("helloWorld")).toBe("hello-world"); + }); +}); diff --git a/package/stringUtil/kebabCase/index.ts b/package/stringUtil/kebabCase/index.ts new file mode 100644 index 0000000..b3a7cfa --- /dev/null +++ b/package/stringUtil/kebabCase/index.ts @@ -0,0 +1,5 @@ +import wordTokenize from "../_wordTokenize"; + +export default function kebabCase(text: string): string { + return wordTokenize(text).join("-"); +} diff --git a/package/stringUtil/pascalCase/index.test.ts b/package/stringUtil/pascalCase/index.test.ts new file mode 100644 index 0000000..2121de5 --- /dev/null +++ b/package/stringUtil/pascalCase/index.test.ts @@ -0,0 +1,12 @@ +import { describe, expect, test } from "vitest"; +import pascalCase from "."; + +describe("pascalCase 유틸 함수 테스트", () => { + test("문장을 PascalCase로 변환한다.", () => { + expect(pascalCase("hello world")).toBe("HelloWorld"); + }); + + test("기존 구분자를 제거하고 변환한다.", () => { + expect(pascalCase("hello_world-test")).toBe("HelloWorldTest"); + }); +}); diff --git a/package/stringUtil/pascalCase/index.ts b/package/stringUtil/pascalCase/index.ts new file mode 100644 index 0000000..30c4bb6 --- /dev/null +++ b/package/stringUtil/pascalCase/index.ts @@ -0,0 +1,7 @@ +import wordTokenize from "../_wordTokenize"; + +export default function pascalCase(text: string): string { + return wordTokenize(text) + .map((word) => word.charAt(0).toUpperCase() + word.slice(1)) + .join(""); +} diff --git a/package/stringUtil/snakeCase/index.test.ts b/package/stringUtil/snakeCase/index.test.ts new file mode 100644 index 0000000..02ae902 --- /dev/null +++ b/package/stringUtil/snakeCase/index.test.ts @@ -0,0 +1,12 @@ +import { describe, expect, test } from "vitest"; +import snakeCase from "."; + +describe("snakeCase 유틸 함수 테스트", () => { + test("문장을 snake_case로 변환한다.", () => { + expect(snakeCase("Hello World")).toBe("hello_world"); + }); + + test("kebab-case 문자열도 변환한다.", () => { + expect(snakeCase("hello-world")).toBe("hello_world"); + }); +}); diff --git a/package/stringUtil/snakeCase/index.ts b/package/stringUtil/snakeCase/index.ts new file mode 100644 index 0000000..03cfc15 --- /dev/null +++ b/package/stringUtil/snakeCase/index.ts @@ -0,0 +1,5 @@ +import wordTokenize from "../_wordTokenize"; + +export default function snakeCase(text: string): string { + return wordTokenize(text).join("_"); +} diff --git a/package/stringUtil/truncate/index.test.ts b/package/stringUtil/truncate/index.test.ts new file mode 100644 index 0000000..ac9e0c7 --- /dev/null +++ b/package/stringUtil/truncate/index.test.ts @@ -0,0 +1,16 @@ +import { describe, expect, test } from "vitest"; +import truncate from "."; + +describe("truncate 유틸 함수 테스트", () => { + test("기본 길이로 문자열을 자른다.", () => { + expect(truncate("abcdefghijklmnopqrstuvwxyz", { length: 10 })).toBe("abcdefg..."); + }); + + test("길이가 충분하면 원문을 그대로 반환한다.", () => { + expect(truncate("short", { length: 10 })).toBe("short"); + }); + + test("커스텀 omission을 지원한다.", () => { + expect(truncate("abcdefghij", { length: 8, omission: "~~" })).toBe("abcdef~~"); + }); +}); diff --git a/package/stringUtil/truncate/index.ts b/package/stringUtil/truncate/index.ts new file mode 100644 index 0000000..d9c78eb --- /dev/null +++ b/package/stringUtil/truncate/index.ts @@ -0,0 +1,21 @@ +interface TruncateOptions { + length?: number; + omission?: string; +} + +export default function truncate( + text: string, + options: TruncateOptions = {} +): string { + const { length = 30, omission = "..." } = options; + + if (text.length <= length) { + return text; + } + + if (length <= omission.length) { + return omission.slice(0, Math.max(0, length)); + } + + return text.slice(0, length - omission.length) + omission; +} diff --git a/package/typeUtil/index.ts b/package/typeUtil/index.ts index e3d4d93..6dd1269 100644 --- a/package/typeUtil/index.ts +++ b/package/typeUtil/index.ts @@ -1 +1,8 @@ +export { default as isArray } from "./isArray"; +export { default as isBoolean } from "./isBoolean"; +export { default as isDate } from "./isDate"; +export { default as isFunction } from "./isFunction"; +export { default as isNil } from "./isNil"; +export { default as isNumber } from "./isNumber"; export { default as isPlainObject } from "./isPlainObject"; +export { default as isString } from "./isString"; diff --git a/package/typeUtil/isArray/index.test.ts b/package/typeUtil/isArray/index.test.ts new file mode 100644 index 0000000..05c41b4 --- /dev/null +++ b/package/typeUtil/isArray/index.test.ts @@ -0,0 +1,12 @@ +import { describe, expect, test } from "vitest"; +import isArray from "."; + +describe("isArray 유틸 함수 테스트", () => { + test("배열이면 true를 반환한다.", () => { + expect(isArray([1, 2, 3])).toBe(true); + }); + + test("배열이 아니면 false를 반환한다.", () => { + expect(isArray("array")).toBe(false); + }); +}); diff --git a/package/typeUtil/isArray/index.ts b/package/typeUtil/isArray/index.ts new file mode 100644 index 0000000..46dd506 --- /dev/null +++ b/package/typeUtil/isArray/index.ts @@ -0,0 +1,3 @@ +export default function isArray(value: unknown): value is unknown[] { + return Array.isArray(value); +} diff --git a/package/typeUtil/isBoolean/index.test.ts b/package/typeUtil/isBoolean/index.test.ts new file mode 100644 index 0000000..173e8b2 --- /dev/null +++ b/package/typeUtil/isBoolean/index.test.ts @@ -0,0 +1,12 @@ +import { describe, expect, test } from "vitest"; +import isBoolean from "."; + +describe("isBoolean 유틸 함수 테스트", () => { + test("불리언이면 true를 반환한다.", () => { + expect(isBoolean(true)).toBe(true); + }); + + test("불리언이 아니면 false를 반환한다.", () => { + expect(isBoolean(0)).toBe(false); + }); +}); diff --git a/package/typeUtil/isBoolean/index.ts b/package/typeUtil/isBoolean/index.ts new file mode 100644 index 0000000..38b897b --- /dev/null +++ b/package/typeUtil/isBoolean/index.ts @@ -0,0 +1,3 @@ +export default function isBoolean(value: unknown): value is boolean { + return typeof value === "boolean"; +} diff --git a/package/typeUtil/isDate/index.test.ts b/package/typeUtil/isDate/index.test.ts new file mode 100644 index 0000000..a8ae7b5 --- /dev/null +++ b/package/typeUtil/isDate/index.test.ts @@ -0,0 +1,12 @@ +import { describe, expect, test } from "vitest"; +import isDate from "."; + +describe("isDate 유틸 함수 테스트", () => { + test("유효한 Date면 true를 반환한다.", () => { + expect(isDate(new Date())).toBe(true); + }); + + test("Invalid Date는 false를 반환한다.", () => { + expect(isDate(new Date("invalid"))).toBe(false); + }); +}); diff --git a/package/typeUtil/isDate/index.ts b/package/typeUtil/isDate/index.ts new file mode 100644 index 0000000..80c8622 --- /dev/null +++ b/package/typeUtil/isDate/index.ts @@ -0,0 +1,3 @@ +export default function isDate(value: unknown): value is Date { + return value instanceof Date && !Number.isNaN(value.getTime()); +} diff --git a/package/typeUtil/isFunction/index.test.ts b/package/typeUtil/isFunction/index.test.ts new file mode 100644 index 0000000..3d84a8a --- /dev/null +++ b/package/typeUtil/isFunction/index.test.ts @@ -0,0 +1,12 @@ +import { describe, expect, test } from "vitest"; +import isFunction from "."; + +describe("isFunction 유틸 함수 테스트", () => { + test("함수면 true를 반환한다.", () => { + expect(isFunction(() => null)).toBe(true); + }); + + test("함수가 아니면 false를 반환한다.", () => { + expect(isFunction({})).toBe(false); + }); +}); diff --git a/package/typeUtil/isFunction/index.ts b/package/typeUtil/isFunction/index.ts new file mode 100644 index 0000000..1bb231c --- /dev/null +++ b/package/typeUtil/isFunction/index.ts @@ -0,0 +1,5 @@ +export default function isFunction( + value: unknown +): value is (...args: unknown[]) => unknown { + return typeof value === "function"; +} diff --git a/package/typeUtil/isNil/index.test.ts b/package/typeUtil/isNil/index.test.ts new file mode 100644 index 0000000..3b89a23 --- /dev/null +++ b/package/typeUtil/isNil/index.test.ts @@ -0,0 +1,13 @@ +import { describe, expect, test } from "vitest"; +import isNil from "."; + +describe("isNil 유틸 함수 테스트", () => { + test("null과 undefined는 true를 반환한다.", () => { + expect(isNil(null)).toBe(true); + expect(isNil(undefined)).toBe(true); + }); + + test("그 외 값은 false를 반환한다.", () => { + expect(isNil(0)).toBe(false); + }); +}); diff --git a/package/typeUtil/isNil/index.ts b/package/typeUtil/isNil/index.ts new file mode 100644 index 0000000..8fd9dc7 --- /dev/null +++ b/package/typeUtil/isNil/index.ts @@ -0,0 +1,5 @@ +export default function isNil( + value: unknown +): value is null | undefined { + return value === null || value === undefined; +} diff --git a/package/typeUtil/isNumber/index.test.ts b/package/typeUtil/isNumber/index.test.ts new file mode 100644 index 0000000..bf87b73 --- /dev/null +++ b/package/typeUtil/isNumber/index.test.ts @@ -0,0 +1,12 @@ +import { describe, expect, test } from "vitest"; +import isNumber from "."; + +describe("isNumber 유틸 함수 테스트", () => { + test("유효한 숫자면 true를 반환한다.", () => { + expect(isNumber(1)).toBe(true); + }); + + test("NaN은 false를 반환한다.", () => { + expect(isNumber(Number.NaN)).toBe(false); + }); +}); diff --git a/package/typeUtil/isNumber/index.ts b/package/typeUtil/isNumber/index.ts new file mode 100644 index 0000000..f82a5c4 --- /dev/null +++ b/package/typeUtil/isNumber/index.ts @@ -0,0 +1,3 @@ +export default function isNumber(value: unknown): value is number { + return typeof value === "number" && !Number.isNaN(value); +} diff --git a/package/typeUtil/isString/index.test.ts b/package/typeUtil/isString/index.test.ts new file mode 100644 index 0000000..35cdb19 --- /dev/null +++ b/package/typeUtil/isString/index.test.ts @@ -0,0 +1,12 @@ +import { describe, expect, test } from "vitest"; +import isString from "."; + +describe("isString 유틸 함수 테스트", () => { + test("문자열이면 true를 반환한다.", () => { + expect(isString("hello")).toBe(true); + }); + + test("문자열이 아니면 false를 반환한다.", () => { + expect(isString(1)).toBe(false); + }); +}); diff --git a/package/typeUtil/isString/index.ts b/package/typeUtil/isString/index.ts new file mode 100644 index 0000000..d0abd88 --- /dev/null +++ b/package/typeUtil/isString/index.ts @@ -0,0 +1,3 @@ +export default function isString(value: unknown): value is string { + return typeof value === "string"; +} From ec80e221d8c2cdeccfb64cf5081b4fb53893f9aa Mon Sep 17 00:00:00 2001 From: klmhyeonwoo Date: Wed, 25 Feb 2026 16:55:32 +0900 Subject: [PATCH 2/5] chore(benchmark): add micro-benchmark runner --- benchmark/index.mjs | 70 +++++++++++++++++++++++++++++++++++++++++++++ package.json | 1 + 2 files changed, 71 insertions(+) create mode 100644 benchmark/index.mjs diff --git a/benchmark/index.mjs b/benchmark/index.mjs new file mode 100644 index 0000000..4f71ed8 --- /dev/null +++ b/benchmark/index.mjs @@ -0,0 +1,70 @@ +import { performance } from "node:perf_hooks"; +import * as corekit from "../dist/bundle/kr-corekit.js"; + +const { + chunk, + flattenDeep, + get, + merge, + camelCase, + isEqual, + map, + mean, + mapAsync, +} = corekit; + +function run(name, fn, iterations = 20000) { + fn(); + const startedAt = performance.now(); + + for (let index = 0; index < iterations; index++) { + fn(); + } + + const elapsed = performance.now() - startedAt; + const opsPerSec = Math.round((iterations / elapsed) * 1000); + + return { name, iterations, elapsedMs: elapsed.toFixed(2), opsPerSec }; +} + +async function runAsync(name, fn, iterations = 2000) { + await fn(); + const startedAt = performance.now(); + + for (let index = 0; index < iterations; index++) { + await fn(); + } + + const elapsed = performance.now() - startedAt; + const opsPerSec = Math.round((iterations / elapsed) * 1000); + + return { name, iterations, elapsedMs: elapsed.toFixed(2), opsPerSec }; +} + +const sampleArray = Array.from({ length: 1000 }, (_, index) => index); +const nestedArray = [1, [2, [3, [4, [5, [6]]]]]]; +const deepObject = { user: { profile: { address: { city: "Seoul" } } } }; +const mergeTarget = { a: { b: 1 }, c: [1, 2, 3] }; +const mergeSource = { a: { d: 2 }, c: [3, 4], e: true }; + +const syncResults = [ + run("array.chunk", () => chunk(sampleArray, 25)), + run("array.flattenDeep", () => flattenDeep(nestedArray)), + run("object.get", () => get(deepObject, "user.profile.address.city")), + run("object.merge", () => merge({ ...mergeTarget }, mergeSource), 5000), + run("string.camelCase", () => camelCase("kr corekit benchmark sample")), + run("lang.isEqual", () => isEqual({ x: [1, 2] }, { x: [1, 2] }), 10000), + run("collection.map", () => map({ a: 1, b: 2, c: 3 }, (v) => v * 2)), + run("math.mean", () => mean([1, 2, 3, 4, 5])), +]; + +const asyncResults = [ + await runAsync( + "async.mapAsync", + () => mapAsync([1, 2, 3, 4], async (value) => value * 2), + 1000 + ), +]; + +console.log("Benchmark Results"); +console.table([...syncResults, ...asyncResults]); diff --git a/package.json b/package.json index c39f9db..646512a 100644 --- a/package.json +++ b/package.json @@ -123,6 +123,7 @@ }, "scripts": { "build": "tsc && vite build", + "benchmark": "npm run build && node benchmark/index.mjs", "build:analyze": "npm run build && npm run analyze:bundle", "analyze:bundle": "echo 'Bundle Analysis:' && du -h dist/bundle/* && echo '\\nGzipped sizes:' && gzip -c dist/bundle/kr-corekit.js | wc -c | awk '{print \"ESM: \" $1/1024 \" KB\"}' && gzip -c dist/bundle/kr-corekit.cjs | wc -c | awk '{print \"CJS: \" $1/1024 \" KB\"}'", "pack:preview": "npm pack --dry-run", From 8f8318fe3226fd1ffd4dc801a28bc881527057c8 Mon Sep 17 00:00:00 2001 From: klmhyeonwoo Date: Wed, 25 Feb 2026 16:55:42 +0900 Subject: [PATCH 3/5] docs(i18n): add multilingual README variants --- README.ja.md | 98 ++++++++++++++++++ README.ko.md | 98 ++++++++++++++++++ README.md | 270 +++++++++++------------------------------------- README.zh-CN.md | 98 ++++++++++++++++++ 4 files changed, 357 insertions(+), 207 deletions(-) create mode 100644 README.ja.md create mode 100644 README.ko.md create mode 100644 README.zh-CN.md diff --git a/README.ja.md b/README.ja.md new file mode 100644 index 0000000..6519aef --- /dev/null +++ b/README.ja.md @@ -0,0 +1,98 @@ +# kr-corekit + +言語: [English](./README.md) | [한국어](./README.ko.md) | [简体中文](./README.zh-CN.md) | [日本語](./README.ja.md) + +lodash/es-toolkit 風の API を目指した JavaScript/TypeScript ユーティリティライブラリです。 + +## 特徴 + +- 文字列・配列・オブジェクト・非同期・Promise・日付・数値など 130+ 関数 +- Tree-shaking しやすいモジュール export +- TypeScript `.d.ts` 型定義を提供 +- ESM/CJS バンドルを提供 + +## インストール + +```bash +npm install kr-corekit +# or +pnpm add kr-corekit +# or +yarn add kr-corekit +``` + +## クイックスタート + +```ts +import { + stringUtil, + arrayUtil, + objectUtil, + asyncUtil, + promiseUtil, + dateUtil, + mathUtil, + langUtil, +} from "kr-corekit"; + +const id = stringUtil.camelCase("user profile id"); +const rows = arrayUtil.chunk([1, 2, 3, 4, 5], 2); +const city = objectUtil.get({ user: { profile: { city: "Seoul" } } }, "user.profile.city"); + +const mapped = await asyncUtil.mapAsync([1, 2, 3], async (v) => v * 2); +const safe = await promiseUtil.withTimeout(fetch("/api/health"), 1000); + +const tomorrow = dateUtil.addDays(new Date(), 1); +const average = mathUtil.mean([10, 20, 30]); +const enabled = langUtil.toBoolean("yes"); +``` + +## モジュール + +- `stringUtil`: ケース変換、HTML escape/unescape、truncate、slugify +- `arrayUtil`: chunk、flatten、uniq/uniqBy、groupBy、sortBy、集合演算、サンプリング +- `collectionUtil`: 配列/オブジェクト向け map/filter/reduce/find/every/some/includes +- `objectUtil`: get/set/has/merge/defaults/pick/omit/deepClone/deepFreeze +- `numberUtil`: clamp、inRange、random、ceil/floor/round、sum/subtract/multiply +- `mathUtil`: mean/median/min/max/sumBy/minBy/maxBy +- `dateUtil`: 日時加減算、start/end of day、formatDate、日付比較 +- `langUtil`: toBoolean/toNumber/toString/defaultTo/castArray/isEqual +- `asyncUtil`: pLimit、mapAsync、filterAsync、eachAsync、series、parallel +- `promiseUtil`: defer、withTimeout、retryWithDelay、settle、toResult +- 既存モジュール: `commonUtil`, `functionUtil`, `validationUtil`, `formatUtil`, `typeUtil`, `cookieUtil`, `deviceUtil`, `searchQueryUtil` + +## Tree-Shaking インポート + +```ts +import { camelCase } from "kr-corekit/stringUtil"; +import { chunk } from "kr-corekit/arrayUtil"; +import { get } from "kr-corekit/objectUtil"; +import { mapAsync } from "kr-corekit/asyncUtil"; +import { withTimeout } from "kr-corekit/promiseUtil"; +``` + +## ベンチマーク + +```bash +npm run benchmark +``` + +ビルド後にローカルのマイクロベンチマーク [`benchmark/index.mjs`](./benchmark/index.mjs) を実行します。 + +最新ローカルサンプル(2026-02-25): + +```text +array.chunk ~1,065,050 ops/s +object.get ~2,681,055 ops/s +string.camelCase ~1,902,407 ops/s +async.mapAsync ~1,514,005 ops/s +``` + +## 補足 + +- API 全体は `package/*/index.ts` と `dist/types/*/index.d.ts` で確認できます。 +- 関数ドキュメントを拡張する場合は多言語 README を同期してください。 + +## ライセンス + +MIT diff --git a/README.ko.md b/README.ko.md new file mode 100644 index 0000000..dcbbd80 --- /dev/null +++ b/README.ko.md @@ -0,0 +1,98 @@ +# kr-corekit + +언어: [English](./README.md) | [한국어](./README.ko.md) | [简体中文](./README.zh-CN.md) | [日本語](./README.ja.md) + +lodash/es-toolkit 스타일 API를 지향하는 JavaScript/TypeScript 유틸리티 모음입니다. + +## 핵심 특징 + +- 문자열/배열/객체/비동기/Promise/날짜/수학/언어 유틸 포함 130개+ 함수 +- 트리 셰이킹에 유리한 모듈 export 구조 +- TypeScript `.d.ts` 타입 제공 +- ESM/CJS 번들 제공 + +## 설치 + +```bash +npm install kr-corekit +# or +pnpm add kr-corekit +# or +yarn add kr-corekit +``` + +## 빠른 사용 예시 + +```ts +import { + stringUtil, + arrayUtil, + objectUtil, + asyncUtil, + promiseUtil, + dateUtil, + mathUtil, + langUtil, +} from "kr-corekit"; + +const id = stringUtil.camelCase("user profile id"); +const rows = arrayUtil.chunk([1, 2, 3, 4, 5], 2); +const city = objectUtil.get({ user: { profile: { city: "Seoul" } } }, "user.profile.city"); + +const mapped = await asyncUtil.mapAsync([1, 2, 3], async (v) => v * 2); +const safe = await promiseUtil.withTimeout(fetch("/api/health"), 1000); + +const tomorrow = dateUtil.addDays(new Date(), 1); +const average = mathUtil.mean([10, 20, 30]); +const enabled = langUtil.toBoolean("yes"); +``` + +## 모듈 구성 + +- `stringUtil`: 케이스 변환, HTML escape/unescape, truncate, slugify +- `arrayUtil`: chunk, flatten, uniq/uniqBy, groupBy, sortBy, 집합 연산, 샘플링 +- `collectionUtil`: 배열/객체 대상 map/filter/reduce/find/every/some/includes +- `objectUtil`: get/set/has/merge/defaults/pick/omit/deepClone/deepFreeze +- `numberUtil`: clamp, inRange, random, ceil/floor/round, sum/subtract/multiply +- `mathUtil`: mean/median/min/max/sumBy/minBy/maxBy +- `dateUtil`: 일/시간 가감, 하루 시작/끝, formatDate, 날짜 비교 +- `langUtil`: toBoolean/toNumber/toString/defaultTo/castArray/isEqual +- `asyncUtil`: pLimit, mapAsync, filterAsync, eachAsync, series, parallel +- `promiseUtil`: defer, withTimeout, retryWithDelay, settle, toResult +- 기존 모듈: `commonUtil`, `functionUtil`, `validationUtil`, `formatUtil`, `typeUtil`, `cookieUtil`, `deviceUtil`, `searchQueryUtil` + +## 트리 셰이킹 import + +```ts +import { camelCase } from "kr-corekit/stringUtil"; +import { chunk } from "kr-corekit/arrayUtil"; +import { get } from "kr-corekit/objectUtil"; +import { mapAsync } from "kr-corekit/asyncUtil"; +import { withTimeout } from "kr-corekit/promiseUtil"; +``` + +## 벤치마크 + +```bash +npm run benchmark +``` + +빌드 후 [`benchmark/index.mjs`](./benchmark/index.mjs) 기반 로컬 마이크로 벤치마크를 실행합니다. + +최신 로컬 샘플 (2026-02-25): + +```text +array.chunk ~1,065,050 ops/s +object.get ~2,681,055 ops/s +string.camelCase ~1,902,407 ops/s +async.mapAsync ~1,514,005 ops/s +``` + +## 참고 + +- 전체 API는 `package/*/index.ts` 및 `dist/types/*/index.d.ts`에서 확인할 수 있습니다. +- 함수별 문서를 더 확장할 경우 다국어 README 파일을 함께 동기화하세요. + +## 라이선스 + +MIT diff --git a/README.md b/README.md index 5422901..11b4030 100644 --- a/README.md +++ b/README.md @@ -1,14 +1,15 @@ # kr-corekit -A comprehensive collection of TypeScript utility functions for modern web development. +Language: [English](./README.md) | [한국어](./README.ko.md) | [简体中文](./README.zh-CN.md) | [日本語](./README.ja.md) -## Features +A utility toolkit for JavaScript and TypeScript inspired by lodash/es-toolkit style APIs. -- 🛠️ **Comprehensive**: String, object, cookie, number, validation, format, search query, device, type, storage and common utilities -- 📦 **Tree-shakable**: Import only what you need -- 🔒 **Type-safe**: Full TypeScript support with type definitions -- ⚡ **Lightweight**: Minimal dependencies and optimized for performance -- 🧪 **Well-tested**: Extensive test coverage with comprehensive test cases +## Highlights + +- 130+ utility functions across string, array, object, async, promise, date, math, lang and more +- Tree-shake friendly module exports +- TypeScript-first with generated `.d.ts` files +- ESM/CJS bundle output ## Installation @@ -20,223 +21,78 @@ pnpm add kr-corekit yarn add kr-corekit ``` -## Usage - -### Full Import (All utilities) +## Quick Start -```typescript +```ts import { stringUtil, + arrayUtil, objectUtil, - cookieUtil, - numberUtil, - validationUtil, - commonUtil, - formatUtil, - searchQueryUtil, - typeUtil, - deviceUtil, + asyncUtil, + promiseUtil, + dateUtil, + mathUtil, + langUtil, } from "kr-corekit"; -// String utilities -const escaped = stringUtil.escapeHtml("
Hello
"); -const unescaped = stringUtil.unescapeHtml("<div>Hello</div>"); -const slug = stringUtil.slugify("Hello World! 안녕하세요"); // "hello-world-안녕하세요" - -// Object utilities -const cleaned = objectUtil.clearNullProperties({ a: 1, b: null, c: 3 }); -const frozen = objectUtil.deepFreeze({ a: { b: 1 } }); -const withoutKey = objectUtil.removeKey("b", { a: 1, b: 2, c: 3 }); // { a: 1, c: 3 } - -// Number utilities -const total = numberUtil.sum(1, 2, 3, 4, 5); // 15 -const difference = numberUtil.subtract(10, 3); // 7 -const product = numberUtil.multiply(2, 3, 4); // 24 - -// Validation utilities -const isValid = validationUtil.checkEmail("user@example.com"); // true -const isHttpUrl = validationUtil.checkHttpUrl("https://example.com"); // true -const isDomain = validationUtil.checkDomain("example.com"); // true -const isBase64 = validationUtil.checkBase64("U29tZSB2YWxpZCBiYXNlNjQgc3RyaW5n"); // true -const isPasswordValid = validationUtil.checkPassword("Abc123!@#", { - minLength: 8, - requireUppercase: true, - requireLowercase: true, - requireNumber: true, - requireSpecialChar: true, -}); // true - -// Common utilities -const empty = commonUtil.isEmpty(""); // true -const notEmpty = commonUtil.isEmpty("hello"); // false -const nullCheck = commonUtil.isNull(null); // true -const notNull = commonUtil.isNull("hello"); // false -await commonUtil.sleep(1000); // Pauses execution for 1 second -const copied = await commonUtil.copyToClipboard("Hello, World!"); // true if successful -const encoded = commonUtil.encodeBase64("Hello 한글!"); // Base64 encoded string -const decoded = commonUtil.decodeBase64(encoded); // "Hello 한글!" -const debouncedFn = commonUtil.debounce(() => console.log("Called!"), 300); // Debounced function -const throttledFn = commonUtil.throttle(() => console.log("Throttled!"), 300); // Throttled function - -// Storage -commonUtil.storage.set("user", { id: 1, name: "John" }); // Stores object in localStorage -const user = commonUtil.storage.get<{ id: number; name: string }>("user"); // Retrieves typed object -commonUtil.storage.remove("user"); // Removes item from localStorage - -// Retry utilities -const result = await commonUtil.retry(async () => { - const response = await fetch("/api/data"); - if (!response.ok) throw new Error("API failed"); - return response.json(); -}, 3); // Retry up to 3 times - -// More retry examples -const userData = await commonUtil.retry(async () => { - return await fetchUserData(); -}); // Uses default 3 retries - -const fileUpload = await commonUtil.retry(async () => { - return await uploadFile(file); -}, 5); // Custom retry count - -// Search Query utilities -const queryParams = searchQueryUtil.getAllQuery(); // { key: ["value1", "value2"], id: "123" } - -// Type utilities -const isPlain = typeUtil.isPlainObject({}); // true -const isNotPlain = typeUtil.isPlainObject(new Date()); // false - -// Device utilities -const device = deviceUtil.getDevice(); // { isMobile: false, isTablet: false, isDesktop: true, isIOS: false, isAndroid: false } - -// Cookie utilities -cookieUtil.setCookie("theme", "dark"); -const theme = cookieUtil.getCookie("theme"); -// Format utilities -const formattedPhone = formatUtil.formatPhoneNumber("01012345678"); // "010-1234-5678" -``` - -### Tree-Shaking Optimized Import (Recommended) - -For optimal bundle size, import only the functions you need: +const id = stringUtil.camelCase("user profile id"); // userProfileId +const rows = arrayUtil.chunk([1, 2, 3, 4, 5], 2); // [[1,2], [3,4], [5]] +const city = objectUtil.get({ user: { profile: { city: "Seoul" } } }, "user.profile.city"); -```typescript -// Option 1: Import specific functions (best tree-shaking) -import { escapeHtml, unescapeHtml } from "kr-corekit"; -import { sum, multiply } from "kr-corekit"; -import { clearNullProperties, deepFreeze } from "kr-corekit"; +const mapped = await asyncUtil.mapAsync([1, 2, 3], async (v) => v * 2); +const safe = await promiseUtil.withTimeout(fetch("/api/health"), 1000); -// Option 2: Import from specific utility modules (good tree-shaking) -import { escapeHtml } from "kr-corekit/stringUtil"; -import { sum } from "kr-corekit/numberUtil"; -import { clearNullProperties } from "kr-corekit/objectUtil"; -import { storage } from "kr-corekit/commonUtil"; - -// Usage remains the same -const escaped = escapeHtml("
Hello
"); -const total = sum(1, 2, 3, 4, 5); -const cleaned = clearNullProperties({ a: 1, b: null, c: 3 }); -storage.set("data", { key: "value" }); +const tomorrow = dateUtil.addDays(new Date(), 1); +const average = mathUtil.mean([10, 20, 30]); +const enabled = langUtil.toBoolean("yes"); ``` -### Bundle Size Comparison - -- **Full import**: ~8.3KB (2.9KB gzipped) -- **Tree-shaken import**: Only includes functions you use -- **Individual module import**: Further optimized for specific utilities - -## API Reference - -### StringUtil - -- `escapeHtml(str: string): string` - Escapes HTML special characters -- `unescapeHtml(str: string): string` - Unescapes HTML entities -- `slugify(text: string): string` - Converts a string to URL-friendly slug format. Replaces spaces with hyphens, removes special characters, converts to lowercase, and supports Korean characters (e.g., "Hello World! 안녕" → "hello-world-안녕") - -### ObjectUtil - -- `clearNullProperties(obj: object): object` - Removes null/undefined properties -- `deepFreeze(obj: object): object` - Deep freezes an object recursively -- `removeKey(key: string, obj: Record): object` - Returns a new object with the specified key removed - -### NumberUtil - -- `sum(...numbers: number[]): number` - Calculates sum of numbers -- `subtract(...numbers: number[]): number` - Calculates subtraction of numbers -- `multiply(...numbers: number[]): number` - Calculates multiplication of numbers - -### FormatUtil - -- `formatPhoneNumber(phone: string): string` - Formats a phone number string to a standard format (e.g., "010-1234-5678") -- `formatNumberWithCommas(value: number | string | null | undefined): string` - Converts numbers or strings to comma-separated format (e.g., "1,234,567"). Returns empty string for null/undefined values. - -### ValidationUtil - -- `checkEmail(email: string): boolean` - Validates email format -- `checkHttpUrl(url: string): boolean` - Validates HTTP/HTTPS URL format -- `checkDomain(domain: string): boolean` - Validates domain name format -- `checkBase64(value: string): boolean` - Validates whether a string is a valid base64 encoded value -- `checkPassword(password: string, options?: { minLength?: number; maxLength?: number; requireUppercase?: boolean; requireLowercase?: boolean; requireNumber?: boolean; requireSpecialChar?: boolean }): boolean` - Validates password strength and requirements - -### StorageUtil - -- `set(key: string, value: T): void` - Stores a value in localStorage with automatic JSON serialization. Supports objects, arrays, and primitive types. Safe for SSR environments. -- `get(key: string): T | null` - Retrieves a value from localStorage with automatic JSON parsing. Returns null if key doesn't exist or parsing fails. Type-safe with generic support. -- `remove(key: string): void` - Removes a specific item from localStorage. Safe for SSR environments. - -**Features:** - -- 🔒 **SSR Safe**: All methods handle server-side rendering environments gracefully -- 📦 **Type Safe**: Full TypeScript support with generics -- 🛡️ **Error Handling**: Comprehensive error handling with automatic cleanup of corrupted data -- 🔄 **Auto Serialization**: Automatic JSON serialization/deserialization for complex data types - -### CommonUtil - -- `isEmpty(value: unknown): boolean` - Checks if a value is empty (null, undefined, "", 0, [], {}, empty Set/Map, NaN, or invalid Date) -- `isNull(value: unknown): value is null` - Type guard that checks if a value is null and narrows the type -- `sleep(ms: number): Promise` - Pauses execution for a specified number of milliseconds -- `copyToClipboard(text: string): Promise` - Copies text to the user's clipboard. Uses modern Clipboard API with fallback to legacy execCommand method. Returns true if successful, false if failed. -- `encodeBase64(str: string, options?: { convertSpecialChars?: boolean }): string` - Encodes a string to Base64 format with optional special character handling -- `decodeBase64(str: string, options?: { convertSpecialChars?: boolean }): string` - Decodes a Base64 string back to original text with optional special character handling -- `debounce(fn: T, delay?: number): (...args: Parameters) => void` - Creates a debounced function that delays execution until after a specified delay (default 300ms) has passed since its last invocation -- `throttle(fn: T, limit?: number): (...args: Parameters) => void` - Creates a throttled function that only executes at most once per specified time interval (default 300ms), ignoring subsequent calls within the limit -- `storage.set(key: string, value: T): void` - Stores a value in localStorage with automatic JSON serialization. Supports objects, arrays, and primitive types. Safe for SSR environments. -- `storage.get(key: string): T | null` - Retrieves a value from localStorage with automatic JSON parsing. Returns null if key doesn't exist or parsing fails. Type-safe with generic support. -- `storage.remove(key: string): void` - Removes a specific item from localStorage. Safe for SSR environments. - -**Storage Features:** - -- 🔒 **SSR Safe**: All methods handle server-side rendering environments gracefully -- 📦 **Type Safe**: Full TypeScript support with generics -- 🛡️ **Error Handling**: Comprehensive error handling with automatic cleanup of corrupted data -- 🔄 **Auto Serialization**: Automatic JSON serialization/deserialization for complex data types - -### Retry - -- `retry(fn: () => Promise, loop?: number): Promise` - Retries an asynchronous function up to the specified number of times (default 3) if it fails. Automatically re-attempts on error and returns the result of the first successful execution, or throws the last error if all retries fail. - -### SearchQueryUtil - -- `getAllQuery(): Record` - Parses the current URL's query string and returns an object with key-value pairs. Values appear as arrays when the same key is used multiple times. - -### TypeUtil +## Modules + +- `stringUtil`: case conversion, HTML escape/unescape, truncate, slugify +- `arrayUtil`: chunk, flatten, uniq/uniqBy, groupBy, sortBy, set ops, sampling +- `collectionUtil`: map/filter/reduce/find/every/some/includes over arrays/objects +- `objectUtil`: get/set/has/merge/defaults/pick/omit/deepClone/deepFreeze +- `numberUtil`: clamp, inRange, random, ceil/floor/round, sum/subtract/multiply +- `mathUtil`: mean/median/min/max/sumBy/minBy/maxBy +- `dateUtil`: add/sub days/hours, start/end of day, formatDate, date comparisons +- `langUtil`: toBoolean/toNumber/toString/defaultTo/castArray/isEqual +- `asyncUtil`: pLimit, mapAsync, filterAsync, eachAsync, series, parallel +- `promiseUtil`: defer, withTimeout, retryWithDelay, settle, toResult +- plus existing: `commonUtil`, `functionUtil`, `validationUtil`, `formatUtil`, `typeUtil`, `cookieUtil`, `deviceUtil`, `searchQueryUtil` + +## Tree-Shaking Import + +```ts +import { camelCase } from "kr-corekit/stringUtil"; +import { chunk } from "kr-corekit/arrayUtil"; +import { get } from "kr-corekit/objectUtil"; +import { mapAsync } from "kr-corekit/asyncUtil"; +import { withTimeout } from "kr-corekit/promiseUtil"; +``` -- `isPlainObject(value: unknown): boolean` - Checks if a value is a plain object (created by Object literal or Object.create(null)), excluding arrays, dates, and other built-in objects. +## Benchmark -### DeviceUtil +```bash +npm run benchmark +``` -- `getDevice(): DeviceInfo` - Detects the user's device environment. Returns information about device type (mobile/tablet/desktop) and operating system (iOS/Android). Uses navigator.userAgent for detection and provides safe fallback for SSR environments. +This runs a local micro-benchmark script at [`benchmark/index.mjs`](./benchmark/index.mjs) after build. -### CookieUtil +Latest local sample (2026-02-25): -- `setCookie(name: string, value: string, options?: object): void` - Sets a cookie -- `getCookie(name: string): string | null` - Gets a cookie value +```text +array.chunk ~1,065,050 ops/s +object.get ~2,681,055 ops/s +string.camelCase ~1,902,407 ops/s +async.mapAsync ~1,514,005 ops/s +``` -## Contributing +## Notes -Contributions are welcome! Please read our contributing guidelines and submit pull requests to our repository. +- Full API list is available via module entry files under `package/*/index.ts` and generated types under `dist/types/*/index.d.ts`. +- If you want function-level docs/examples expanded further, keep language files in sync when updating. ## License -MIT License - see LICENSE file for details. +MIT diff --git a/README.zh-CN.md b/README.zh-CN.md new file mode 100644 index 0000000..336c8c9 --- /dev/null +++ b/README.zh-CN.md @@ -0,0 +1,98 @@ +# kr-corekit + +语言: [English](./README.md) | [한국어](./README.ko.md) | [简体中文](./README.zh-CN.md) | [日本語](./README.ja.md) + +一个面向 JavaScript/TypeScript 的工具函数库,API 风格参考 lodash/es-toolkit。 + +## 主要特性 + +- 覆盖字符串、数组、对象、异步、Promise、日期、数学、语言等 130+ 函数 +- 适合 Tree-shaking 的模块导出结构 +- 提供 TypeScript `.d.ts` 类型 +- 提供 ESM/CJS 打包产物 + +## 安装 + +```bash +npm install kr-corekit +# or +pnpm add kr-corekit +# or +yarn add kr-corekit +``` + +## 快速示例 + +```ts +import { + stringUtil, + arrayUtil, + objectUtil, + asyncUtil, + promiseUtil, + dateUtil, + mathUtil, + langUtil, +} from "kr-corekit"; + +const id = stringUtil.camelCase("user profile id"); +const rows = arrayUtil.chunk([1, 2, 3, 4, 5], 2); +const city = objectUtil.get({ user: { profile: { city: "Seoul" } } }, "user.profile.city"); + +const mapped = await asyncUtil.mapAsync([1, 2, 3], async (v) => v * 2); +const safe = await promiseUtil.withTimeout(fetch("/api/health"), 1000); + +const tomorrow = dateUtil.addDays(new Date(), 1); +const average = mathUtil.mean([10, 20, 30]); +const enabled = langUtil.toBoolean("yes"); +``` + +## 模块 + +- `stringUtil`: 大小写转换、HTML escape/unescape、truncate、slugify +- `arrayUtil`: chunk、flatten、uniq/uniqBy、groupBy、sortBy、集合运算、抽样 +- `collectionUtil`: 面向数组/对象的 map/filter/reduce/find/every/some/includes +- `objectUtil`: get/set/has/merge/defaults/pick/omit/deepClone/deepFreeze +- `numberUtil`: clamp、inRange、random、ceil/floor/round、sum/subtract/multiply +- `mathUtil`: mean/median/min/max/sumBy/minBy/maxBy +- `dateUtil`: 日期加减、当天起止、formatDate、日期比较 +- `langUtil`: toBoolean/toNumber/toString/defaultTo/castArray/isEqual +- `asyncUtil`: pLimit、mapAsync、filterAsync、eachAsync、series、parallel +- `promiseUtil`: defer、withTimeout、retryWithDelay、settle、toResult +- 其他模块: `commonUtil`, `functionUtil`, `validationUtil`, `formatUtil`, `typeUtil`, `cookieUtil`, `deviceUtil`, `searchQueryUtil` + +## Tree-Shaking 导入 + +```ts +import { camelCase } from "kr-corekit/stringUtil"; +import { chunk } from "kr-corekit/arrayUtil"; +import { get } from "kr-corekit/objectUtil"; +import { mapAsync } from "kr-corekit/asyncUtil"; +import { withTimeout } from "kr-corekit/promiseUtil"; +``` + +## 基准测试 + +```bash +npm run benchmark +``` + +该命令会在构建后执行本地微基准脚本 [`benchmark/index.mjs`](./benchmark/index.mjs)。 + +最新本地样例(2026-02-25): + +```text +array.chunk ~1,065,050 ops/s +object.get ~2,681,055 ops/s +string.camelCase ~1,902,407 ops/s +async.mapAsync ~1,514,005 ops/s +``` + +## 说明 + +- 完整 API 可查看 `package/*/index.ts` 与 `dist/types/*/index.d.ts`。 +- 若继续扩展函数文档,请同步更新多语言 README。 + +## 许可证 + +MIT From a03a0f493d07c1e3b5d0b29763533c1eb4f150a5 Mon Sep 17 00:00:00 2001 From: klmhyeonwoo Date: Wed, 25 Feb 2026 17:17:30 +0900 Subject: [PATCH 4/5] docs(i18n): fix README statement --- README.ja.md | 2 +- README.ko.md | 2 +- README.md | 2 +- README.zh-CN.md | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/README.ja.md b/README.ja.md index 6519aef..b29ab2f 100644 --- a/README.ja.md +++ b/README.ja.md @@ -2,7 +2,7 @@ 言語: [English](./README.md) | [한국어](./README.ko.md) | [简体中文](./README.zh-CN.md) | [日本語](./README.ja.md) -lodash/es-toolkit 風の API を目指した JavaScript/TypeScript ユーティリティライブラリです。 +明確で実用的な API 設計を重視した JavaScript/TypeScript ユーティリティライブラリです。 ## 特徴 diff --git a/README.ko.md b/README.ko.md index dcbbd80..9567877 100644 --- a/README.ko.md +++ b/README.ko.md @@ -2,7 +2,7 @@ 언어: [English](./README.md) | [한국어](./README.ko.md) | [简体中文](./README.zh-CN.md) | [日本語](./README.ja.md) -lodash/es-toolkit 스타일 API를 지향하는 JavaScript/TypeScript 유틸리티 모음입니다. +명확하고 실용적인 API 설계로 만든 JavaScript/TypeScript 유틸리티 모음입니다. ## 핵심 특징 diff --git a/README.md b/README.md index 11b4030..657f7b7 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ Language: [English](./README.md) | [한국어](./README.ko.md) | [简体中文](./README.zh-CN.md) | [日本語](./README.ja.md) -A utility toolkit for JavaScript and TypeScript inspired by lodash/es-toolkit style APIs. +A utility toolkit for JavaScript and TypeScript built with a clear, practical API design. ## Highlights diff --git a/README.zh-CN.md b/README.zh-CN.md index 336c8c9..2fd0891 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -2,7 +2,7 @@ 语言: [English](./README.md) | [한국어](./README.ko.md) | [简体中文](./README.zh-CN.md) | [日本語](./README.ja.md) -一个面向 JavaScript/TypeScript 的工具函数库,API 风格参考 lodash/es-toolkit。 +一个为 JavaScript/TypeScript 打造的工具函数库,强调清晰且实用的 API 设计。 ## 主要特性 From 0b36acb44ba9e22b08378c03f6809f64aca2bc0f Mon Sep 17 00:00:00 2001 From: klmhyeonwoo Date: Wed, 25 Feb 2026 17:24:09 +0900 Subject: [PATCH 5/5] docs(i18n): add utils function example --- README.ja.md | 4 + README.ko.md | 4 + README.md | 4 + README.zh-CN.md | 4 + docs/API_EXAMPLES.md | 495 +++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 511 insertions(+) create mode 100644 docs/API_EXAMPLES.md diff --git a/README.ja.md b/README.ja.md index b29ab2f..8d404c4 100644 --- a/README.ja.md +++ b/README.ja.md @@ -71,6 +71,10 @@ import { mapAsync } from "kr-corekit/asyncUtil"; import { withTimeout } from "kr-corekit/promiseUtil"; ``` +## 全 API サンプル + +- 公開 API の全サンプルは [`docs/API_EXAMPLES.md`](./docs/API_EXAMPLES.md) を参照してください。 + ## ベンチマーク ```bash diff --git a/README.ko.md b/README.ko.md index 9567877..a923851 100644 --- a/README.ko.md +++ b/README.ko.md @@ -71,6 +71,10 @@ import { mapAsync } from "kr-corekit/asyncUtil"; import { withTimeout } from "kr-corekit/promiseUtil"; ``` +## 전체 API 예제 + +- 모든 공개 API 예제는 [`docs/API_EXAMPLES.md`](./docs/API_EXAMPLES.md)에서 확인할 수 있습니다. + ## 벤치마크 ```bash diff --git a/README.md b/README.md index 657f7b7..59dcbea 100644 --- a/README.md +++ b/README.md @@ -71,6 +71,10 @@ import { mapAsync } from "kr-corekit/asyncUtil"; import { withTimeout } from "kr-corekit/promiseUtil"; ``` +## Full API Examples + +- See [`docs/API_EXAMPLES.md`](./docs/API_EXAMPLES.md) for examples of all public APIs. + ## Benchmark ```bash diff --git a/README.zh-CN.md b/README.zh-CN.md index 2fd0891..6821c68 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -71,6 +71,10 @@ import { mapAsync } from "kr-corekit/asyncUtil"; import { withTimeout } from "kr-corekit/promiseUtil"; ``` +## 完整 API 示例 + +- 所有公开 API 的示例请查看 [`docs/API_EXAMPLES.md`](./docs/API_EXAMPLES.md)。 + ## 基准测试 ```bash diff --git a/docs/API_EXAMPLES.md b/docs/API_EXAMPLES.md new file mode 100644 index 0000000..bc8dfe6 --- /dev/null +++ b/docs/API_EXAMPLES.md @@ -0,0 +1,495 @@ +# kr-corekit API Examples (All Public APIs) + +현재 공개된 API(총 134개)에 대한 사용 예제입니다. + +## stringUtil + +```ts +import { + camelCase, + capitalize, + escapeHtml, + kebabCase, + pascalCase, + slugify, + snakeCase, + truncate, + unescapeHtml, +} from "kr-corekit/stringUtil"; + +camelCase("hello world"); // "helloWorld" +capitalize("hELLO"); // "Hello" +escapeHtml("
hello
"); // "<div>hello</div>" +kebabCase("helloWorld"); // "hello-world" +pascalCase("hello world"); // "HelloWorld" +slugify("Hello World 안녕"); // "hello-world-안녕" +snakeCase("helloWorld"); // "hello_world" +truncate("abcdefghijklmnopqrstuvwxyz", { length: 10 }); // "abcdefg..." +unescapeHtml("<div>hello</div>"); // "
hello
" +``` + +## arrayUtil + +```ts +import { + chunk, + compact, + difference, + first, + flatten, + flattenDeep, + groupBy, + intersection, + keyBy, + last, + partition, + sample, + sampleSize, + shuffle, + sortBy, + union, + uniqBy, + unique, + unzip, + zip, +} from "kr-corekit/arrayUtil"; + +chunk([1, 2, 3, 4, 5], 2); // [[1,2], [3,4], [5]] +compact([0, 1, false, 2, "", 3, null, undefined]); // [1,2,3] +difference([1, 2, 3], [2]); // [1,3] +first([10, 20, 30]); // 10 +flatten([1, [2, 3], [4]]); // [1,2,3,4] +flattenDeep([1, [2, [3, [4]]]]); // [1,2,3,4] +groupBy([{ id: 1, role: "admin" }, { id: 2, role: "user" }], (v) => v.role); +intersection([1, 2, 3], [2, 4]); // [2] +keyBy([{ id: 1, name: "Kim" }, { id: 2, name: "Lee" }], (v) => v.id); +last([10, 20, 30]); // 30 +partition([1, 2, 3, 4], (n) => n % 2 === 0); // [[2,4], [1,3]] +sample([1, 2, 3]); // 1 | 2 | 3 +sampleSize([1, 2, 3, 4], 2); // 예: [3,1] +shuffle([1, 2, 3]); // 예: [2,1,3] +sortBy([{ id: 3 }, { id: 1 }, { id: 2 }], (v) => v.id); // [{id:1},{id:2},{id:3}] +union([1, 2], [2, 3], [3, 4]); // [1,2,3,4] +uniqBy([{ id: 1 }, { id: 1 }, { id: 2 }], (v) => v.id); // [{id:1},{id:2}] +unique([1, 2, 2, 3]); // [1,2,3] +unzip([ + ["a", 1], + ["b", 2], +]); // [["a","b"], [1,2]] +zip(["a", "b"], [1, 2], [true, false]); // [["a",1,true], ["b",2,false]] +``` + +## collectionUtil + +```ts +import { + entries, + every, + filter, + find, + forEach, + includes, + keys, + map, + reduce, + size, + some, + values, +} from "kr-corekit/collectionUtil"; + +entries({ a: 1, b: 2 }); // [["a",1], ["b",2]] +every([2, 4, 6], (v) => v % 2 === 0); // true +filter({ a: 1, b: 2, c: 3 }, (v) => v > 1); // [2,3] +find([1, 2, 3], (v) => v > 1); // 2 +forEach({ a: 1, b: 2 }, (value, key) => console.log(key, value)); +includes("hello world", "world"); // true +includes([1, 2, 3], 2); // true +keys({ a: 1, b: 2 }); // ["a", "b"] +map({ a: 1, b: 2 }, (value, key) => `${key}:${value}`); // ["a:1", "b:2"] +reduce([1, 2, 3], (acc, v) => acc + v, 0); // 6 +size(new Set([1, 2, 3])); // 3 +some([1, 2, 3], (v) => v > 2); // true +values({ a: 1, b: 2 }); // [1,2] +``` + +## objectUtil + +```ts +import { + clearNullProperties, + defaults, + deepClone, + deepFreeze, + get, + has, + invert, + mapValues, + merge, + omit, + pick, + removeKey, + set, +} from "kr-corekit/objectUtil"; + +clearNullProperties({ a: 1, b: null, c: undefined, d: { e: null, f: 2 } }); +defaults({ a: 1, b: undefined }, { b: 2, c: 3 }); // {a:1,b:2,c:3} +deepClone({ user: { name: "Kim" }, items: [1, 2, 3] }); +deepFreeze({ config: { enabled: true } }); +get({ user: { profile: [{ city: "Seoul" }] } }, "user.profile[0].city", "N/A"); // "Seoul" +has({ a: { b: 1 } }, "a.b"); // true +invert({ a: "x", b: "y" }); // {x:"a", y:"b"} +mapValues({ a: 1, b: 2 }, (v) => v * 10); // {a:10,b:20} +merge({ a: { b: 1 }, arr: [1, 2] }, { a: { c: 2 }, arr: [3] }); +omit({ a: 1, b: 2, c: 3 }, ["b"]); // {a:1,c:3} +pick({ a: 1, b: 2, c: 3 }, ["a", "c"]); // {a:1,c:3} +removeKey("b", { a: 1, b: 2, c: 3 }); // {a:1,c:3} +set({}, "user.profile[0].city", "Seoul"); // { user: { profile: [{ city: "Seoul" }] } } +``` + +## asyncUtil + +```ts +import { + eachAsync, + filterAsync, + mapAsync, + parallel, + pLimit, + series, +} from "kr-corekit/asyncUtil"; + +await eachAsync([1, 2, 3], async (v) => { + console.log(v); +}); + +await filterAsync([1, 2, 3, 4], async (v) => v % 2 === 0); // [2,4] +await mapAsync([1, 2, 3], async (v) => v * 2, { concurrency: 2 }); // [2,4,6] + +await parallel([ + async () => 1, + async () => 2, + async () => 3, +]); // [1,2,3] + +const limit = pLimit(2); +await Promise.all([ + limit(async () => "A"), + limit(async () => "B"), + limit(async () => "C"), +]); + +await series([ + async () => "step1", + async () => "step2", +]); // ["step1", "step2"] +``` + +## promiseUtil + +```ts +import { + defer, + retryWithDelay, + settle, + toResult, + withTimeout, +} from "kr-corekit/promiseUtil"; + +const d = defer(); +setTimeout(() => d.resolve(10), 100); +await d.promise; // 10 + +let attempt = 0; +await retryWithDelay(async () => { + attempt++; + if (attempt < 3) throw new Error("temporary"); + return "ok"; +}, { retries: 5, delay: 50, factor: 2 }); + +await settle([ + Promise.resolve(1), + Promise.reject(new Error("fail")), + Promise.resolve(2), +]); // { fulfilled: [1,2], rejected: [Error] } + +await toResult(Promise.resolve("ok")); // { data: "ok", error: null } +await toResult(Promise.reject(new Error("fail"))); // { data: null, error: Error } + +await withTimeout( + new Promise((resolve) => setTimeout(() => resolve("late"), 300)), + 100, + { fallback: () => "fallback" } +); // "fallback" +``` + +## dateUtil + +```ts +import { + addDays, + addHours, + differenceInDays, + endOfDay, + formatDate, + isAfter, + isBefore, + isSameDay, + startOfDay, + subDays, + subHours, +} from "kr-corekit/dateUtil"; + +addDays(new Date("2025-01-01"), 7); +addHours(new Date("2025-01-01T00:00:00"), 5); +differenceInDays("2025-01-10", "2025-01-01"); // 9 +endOfDay(new Date()); +formatDate(new Date(), "YYYY/MM/DD HH:mm:ss"); +isAfter("2025-01-02", "2025-01-01"); // true +isBefore("2025-01-01", "2025-01-02"); // true +isSameDay("2025-01-01", "2025-01-01T23:59:59"); // true +startOfDay(new Date()); +subDays(new Date("2025-01-10"), 3); +subHours(new Date("2025-01-01T10:00:00"), 2); +``` + +## langUtil + +```ts +import { + castArray, + defaultTo, + isEqual, + toBoolean, + toNumber, + toString, +} from "kr-corekit/langUtil"; + +castArray(1); // [1] +castArray([1, 2]); // [1,2] +defaultTo(undefined, "fallback"); // "fallback" +isEqual({ a: [1, 2] }, { a: [1, 2] }); // true +toBoolean("yes"); // true +toBoolean("false"); // false +toNumber("42", 0); // 42 +toNumber("x", 0); // 0 +toString(null); // "" +toString(123); // "123" +``` + +## mathUtil + +```ts +import { + max, + maxBy, + mean, + meanBy, + median, + min, + minBy, + sumBy, +} from "kr-corekit/mathUtil"; + +max([1, 5, 3]); // 5 +maxBy([{ score: 10 }, { score: 20 }], (v) => v.score); // { score: 20 } +mean([10, 20, 30]); // 20 +meanBy([{ v: 2 }, { v: 4 }], (x) => x.v); // 3 +median([1, 2, 3, 4]); // 2.5 +min([1, 5, 3]); // 1 +minBy([{ score: 10 }, { score: 20 }], (v) => v.score); // { score: 10 } +sumBy([{ price: 100 }, { price: 200 }], (item) => item.price); // 300 +``` + +## numberUtil + +```ts +import { + ceil, + clamp, + floor, + inRange, + multiply, + random, + round, + subtract, + sum, +} from "kr-corekit/numberUtil"; + +ceil(1.234, 2); // 1.24 +clamp(10, 0, 5); // 5 +floor(1.239, 2); // 1.23 +inRange(3, 1, 5); // true +multiply(2, 3, 4); // 24 +random(1, 10); // 1~10 정수 +round(1.235, 2); // 1.24 +subtract(10, 3, 2); // 5 +sum(1, 2, 3); // 6 +``` + +## commonUtil + +```ts +import { + copyToClipboard, + debounce, + decodeBase64, + encodeBase64, + isEmpty, + isNull, + retry, + sleep, + storage, + throttle, +} from "kr-corekit/commonUtil"; + +isEmpty(""); // true +isNull(null); // true +await sleep(100); +await copyToClipboard("hello"); + +const encoded = encodeBase64("Hello 한글"); +decodeBase64(encoded); // "Hello 한글" + +const debounced = debounce(() => console.log("run"), 300); +const throttled = throttle(() => console.log("run"), 300); + +debounced(); +throttled(); + +await retry(async () => { + return "success"; +}, 3); + +storage.set("user", { id: 1, name: "Kim" }); +storage.get<{ id: number; name: string }>("user"); +storage.remove("user"); +``` + +## functionUtil + +```ts +import { + compose, + identity, + memoize, + noop, + once, + pipe, +} from "kr-corekit/functionUtil"; + +compose( + (v) => Number(v) * 2, + (v) => Number(v) + 1 +)(3); // 8 + +identity({ a: 1 }); // { a: 1 } + +const squareMemo = memoize((n: number) => n * n); +squareMemo(5); // 25 +squareMemo(5); // 캐시 결과 + +noop("ignored", 123); // undefined + +const initialize = once(() => "initialized"); +initialize(); // "initialized" +initialize(); // "initialized" + +pipe( + (v) => Number(v) + 1, + (v) => Number(v) * 2 +)(3); // 8 +``` + +## formatUtil + +```ts +import { + formatNumberWithCommas, + formatPhoneNumber, +} from "kr-corekit/formatUtil"; + +formatPhoneNumber("01012345678"); // "010-1234-5678" +formatNumberWithCommas(1234567); // "1,234,567" +``` + +## typeUtil + +```ts +import { + isArray, + isBoolean, + isDate, + isFunction, + isNil, + isNumber, + isPlainObject, + isString, +} from "kr-corekit/typeUtil"; + +isArray([1, 2]); // true +isBoolean(false); // true +isDate(new Date()); // true +isFunction(() => null); // true +isNil(undefined); // true +isNumber(10); // true +isPlainObject({ a: 1 }); // true +isString("hello"); // true +``` + +## validationUtil + +```ts +import { + checkBase64, + checkDomain, + checkEmail, + checkHttpUrl, + checkPassword, +} from "kr-corekit/validationUtil"; + +checkEmail("user@example.com"); // true +checkHttpUrl("https://example.com"); // true +checkDomain("example.com"); // true +checkBase64("aGVsbG8="); // true +checkPassword("Abc123!@#", { + minLength: 8, + requireUppercase: true, + requireLowercase: true, + requireNumber: true, + requireSpecialChar: true, +}); // true +``` + +## cookieUtil + +```ts +import { getCookie, setCookie } from "kr-corekit/cookieUtil"; + +setCookie("theme", "dark", { path: "/", maxAge: 3600 }); +getCookie("theme"); // "dark" +``` + +## deviceUtil + +```ts +import { getDevice } from "kr-corekit/deviceUtil"; + +getDevice(); +// { isMobile, isTablet, isDesktop, isIOS, isAndroid } +``` + +## searchQueryUtil + +```ts +import { getAllQuery } from "kr-corekit/searchQueryUtil"; + +getAllQuery(); +// 예: { page: "1", tag: ["typescript", "utility"] } +``` + +--- + +참고: + +- 브라우저 환경 의존 API: `copyToClipboard`, `storage`, `getCookie`, `setCookie`, `getAllQuery`, `getDevice` +- 비동기 API는 `await` 또는 `then/catch` 패턴으로 사용하세요.