diff --git a/.github/workflows/ci-test.yml b/.github/workflows/ci-test.yml index bcb5b07..8644915 100755 --- a/.github/workflows/ci-test.yml +++ b/.github/workflows/ci-test.yml @@ -12,49 +12,27 @@ jobs: strategy: matrix: - node_version: [18.x, 20.x, 22.x] + deno-version: [1.44.4] steps: - - uses: actions/checkout@v4 - - - name: setup Node.js v${{ matrix.node_version }} - uses: actions/setup-node@v4 - with: - node-version: ${{ matrix.node_version }} - - - name: setup timezone - uses: zcong1993/setup-timezone@master - with: - timezone: ${{ secrets.TZ }} - - - name: run npm scripts - run: | - npm install - npm run lint - npm run test - - - name: Coveralls Parallel - uses: coverallsapp/github-action@v2 - with: - flag-name: run-${{ join(matrix.*, '-') }} - parallel: true - github-token: ${{ secrets.GITHUB_TOKEN }} - - - name: cache node modules - uses: actions/cache@v4 - with: - path: ~/.npm - key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }} - restore-keys: | - ${{ runner.os }}-node- - - finish: - needs: test - if: ${{ always() }} - runs-on: ubuntu-latest - steps: - - name: Coveralls Finished - uses: coverallsapp/github-action@v2 - with: - parallel-finished: true - carryforward: "run-18.x,run-20.x,run-22.x" + - name: Git Checkout Deno Module + uses: actions/checkout@v4 + - name: Use Deno Version ${{ matrix.deno-version }} + uses: denoland/setup-deno@v1 + with: + deno-version: ${{ matrix.deno-version }} + - name: format check + run: deno fmt --check mod.ts utils/* scripts/* tests/* + - name: run linter + run: deno lint mod.ts utils/* scripts/* tests/* + - name: run test + run: deno test --allow-all --coverage=cov/ + + - name: Generate coverage report + run: deno coverage --lcov cov > cov.lcov + + - name: Upload coverage to Coveralls.io + uses: coverallsapp/github-action@v2 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + path-to-lcov: cov.lcov diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index a77d776..102df5a 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -1,14 +1,3 @@ -# For most projects, this workflow file will not need changing; you simply need -# to commit it to your repository. -# -# You may wish to alter this file to override the set of languages analyzed, -# or to provide custom queries or build logic. -# -# ******** NOTE ******** -# We have attempted to detect the languages in your repository. Please check -# the `language` matrix defined below to confirm you have the correct set of -# supported CodeQL languages. -# name: "CodeQL" on: diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 0000000..b1c0ff2 --- /dev/null +++ b/.github/workflows/publish.yml @@ -0,0 +1,16 @@ +name: publish + +on: + push: + branches: + - main + +jobs: + publish: + runs-on: ubuntu-latest + permissions: + contents: read + id-token: write # The OIDC ID token is used for authentication with JSR. + steps: + - uses: actions/checkout@v4 + - run: npx jsr publish diff --git a/.gitignore b/.gitignore index 47be9c0..4b39c21 100755 --- a/.gitignore +++ b/.gitignore @@ -15,3 +15,8 @@ yarn.lock coverage.lcov package-lock.json pnpm-lock.yaml +deno.lock + +npm +cov +cov.lcov diff --git a/.npmignore b/.npmignore deleted file mode 100644 index 7f1eb85..0000000 --- a/.npmignore +++ /dev/null @@ -1,8 +0,0 @@ -node_modules -coverage -.github -pnpm-lock.yaml -examples -build.js -build.test.js -reset.js diff --git a/README.md b/README.md index c0ba8c8..11ac483 100755 --- a/README.md +++ b/README.md @@ -1,524 +1,604 @@ -BellaJS -======== - -Lightweight util for handling data type, string... in your Node.js and browser apps. - -[![NPM](https://badge.fury.io/js/bellajs.svg)](https://badge.fury.io/js/bellajs) -![CodeQL](https://github.com/ndaidong/bellajs/workflows/CodeQL/badge.svg) -![CI test](https://github.com/ndaidong/bellajs/workflows/ci-test/badge.svg) -[![Coverage Status](https://coveralls.io/repos/github/ndaidong/bellajs/badge.svg)](https://coveralls.io/github/ndaidong/bellajs) - -# Contents - -* [Setup](#setup) -* [APIs](#apis) - * [DataType detection](#datatype-detection) - * [String manipulation](#string-manipulation) - * [Data handling](#data-handling): [`clone`](#cloneanything-val), [`copies`](#copiesobject-source-object-target-boolean-requirematching-array-excepts) - * [Array utils](#array-utils): [`pick`](#pickarray-arr--number-count--1), [`sort`](#sortarray-arr--function-compare), [`sortBy`](#sortbyarray-arr-number-order-string-property), [`shuffle`](#shufflearray-arr), [`unique`](#uniquearray-arr) - * [Functional utils](#functional-utils): [`curry`](#curryfn), [`compose`](#composef1-f2-fn), [`pipe`](#pipef1-f2-fn), [`maybe`](#maybeanything-val) - * [Date utils](#date-utils): [`formatDateString`](#formatdatestringdate--timestamp--string-locale--object-options), [`formatTimeAgo`](#formattimeagodate--timestamp--string-locale--string-justnow) - * [Random utils](#random-utils): [`randint`](#randintnumber-min--number-max), [`genid`](#genidnumber-length--string-prefix) - -* [Test](#test) - -* [License](#license) - -## Install & Usage - -### Node.js - -```bash -npm i bellajs - -# pnpm -pnpm i bellajs - -# yarn -yarn add bellajs -``` - -### Deno - -```ts -import { genid } from 'https://esm.sh/bellajs' - -console.log(genid()) -``` - -### Browser - -```html - -``` - -## APIs - -### DataType detection - -- `.isArray(Anything val)` -- `.isBoolean(Anything val)` -- `.isDate(Anything val)` -- `.isElement(Anything val)` -- `.isEmail(Anything val)` -- `.isEmpty(Anything val)` -- `.isFunction(Anything val)` -- `.isInteger(Anything val)` -- `.isLetter(Anything val)` -- `.isNil(Anything val)` -- `.isNull(Anything val)` -- `.isNumber(Anything val)` -- `.isObject(Anything val)` -- `.isString(Anything val)` -- `.isUndefined(Anything val)` - -### String manipulation - -- `.ucfirst(String s)` -- `.ucwords(String s)` -- `.escapeHTML(String s)` -- `.unescapeHTML(String s)` -- `.slugify(String s)` -- `.stripTags(String s)` -- `.stripAccent(String s)` -- `.truncate(String s, Number limit)` -- `.replaceAll(String s, String|Array search, String|Array replace)` - - -### Data handling - -#### `clone(Anything val)` - -Make a deep copy of a variable. - -```js -import { clone } from 'bellajs' - -const b = [ - 1, 5, 0, 'a', -10, '-10', '', - { - a: 1, - b: 'Awesome' - } -] - -const cb = clone(b) -console.log(cb) -``` - -*cb* now has the same values as *b*, while the properties are standalone, not reference. So that: - -```js -cb[7].a = 2 -cb[7].b = 'Noop' - -console.log(b[7]) -``` - -What you get is still: - -```js -{ - a: 1, - b: 'Awesome' -} -``` - -#### `copies(Object source, Object target[[, Boolean requireMatching], Array excepts])` - -Copy the properties from *source* to *target*. - -- *requireMatching*: if true, BellaJS only copies the properties that are already exist in *target*. -- *excepts*: array of the properties properties in *source* that you don't want to copy. - -After this action, target will be modified. - -```js -import { copies } from 'bellajs' - -const a = { - name: 'Toto', - age: 30, - level: 8, - nationality: { - name: 'America' - } -} -const b = { - level: 4, - IQ: 140, - epouse: { - name: 'Alice', - age: 27 - }, - nationality: { - long: '18123.123123.12312', - lat: '98984771.134231.1234' - } -} - -copies(a, b) -console.log(b) -``` - -Output: - -```js -{ - level: 8, - IQ: 140, - epouse: { - name: 'Alice', - age: 27 - }, - nationality: { - long: '18123.123123.12312', - lat: '98984771.134231.1234', - name: 'America' - }, - name: 'Toto', - age: 30 -} -``` - -### Array utils - -#### `pick(Array arr [, Number count = 1])` - -Randomly choose N elements from array. - -```js -import { pick } from 'bellajs' - -const arr = [1, 3, 8, 2, 5, 7] -pick(arr, 2) // --> [3, 5] -pick(arr, 2) // --> [8, 1] -pick(arr) // --> [3] -pick(arr) // --> [7] -``` - - -#### `sort(Array arr [, Function compare])` - -Sort the array using a function. - -```js -import { sort } from 'bellajs' - -const fn = (a, b) => { - return a < b ? 1 : a > b ? -1 : 0 -} - -sort([3, 1, 5, 2], fn) // => [ 1, 2, 3, 5 ] -``` - -#### `sortBy(Array arr, Number order, String property)` - -Sort the array by specific property and direction. - -```js -import { sortBy } from 'bellajs' - -const players = [ - { - name: 'Jerome Nash', - age: 24 - }, - { - name: 'Jackson Valdez', - age: 21 - }, - { - name: 'Benjamin Cole', - age: 23 - }, - { - name: 'Manuel Delgado', - age: 33 - }, - { - name: 'Caleb McKinney', - age: 28 - } -] - -const result = sortBy(players, -1, 'age') -console.log(result) -``` - -#### `shuffle(Array arr)` - -Shuffle the positions of elements in an array. - -```js -import { shuffle } from 'bellajs' - -shuffle([1, 3, 8, 2, 5, 7]) -``` - -#### `unique(Array arr)` - -Remove all duplicate elements from an array. - -```js -import { unique } from 'bellajs' - -unique([1, 2, 3, 2, 3, 1, 5]) // => [ 1, 2, 3, 5 ] -``` - -### Functional utils - -#### `curry(fn)` - -Make a curried function. - -```js -import { curry } from 'bellajs' - -const sum = curry((a, b, c) => { - return a + b + c -}) - -sum(3)(2)(1) // => 6 -sum(1)(2)(3) // => 6 -sum(1, 2)(3) // => 6 -sum(1)(2, 3) // => 6 -sum(1, 2, 3) // => 6 -``` - -#### `compose(f1, f2, ...fN)` - -Performs right-to-left function composition. - -```js -import { compose } from 'bellajs' - -const f1 = (name) => { - return `f1 ${name}` -} -const f2 = (name) => { - return `f2 ${name}` -} -const f3 = (name) => { - return `f3 ${name}` -} - -const addF = compose(f1, f2, f3) - -addF('Hello') // => 'f1 f2 f3 Hello' - -const add1 = (num) => { - return num + 1 -} - -const mult2 = (num) => { - return num * 2 -} - -const add1AndMult2 = compose(add1, mult2) -add1AndMult2(3) // => 7 -// because multiple to 2 first, then add 1 late => 3 * 2 + 1 -``` - - -#### `pipe(f1, f2, ...fN)` - -Performs left-to-right function composition. - -```js -import { pipe } from 'bellajs' - -const f1 = (name) => { - return `f1 ${name}` -} -const f2 = (name) => { - return `f2 ${name}` -} -const f3 = (name) => { - return `f3 ${name}` -} - -const addF = pipe(f1, f2, f3) - -addF('Hello') // => 'f3 f2 f1 Hello' - -const add1 = (num) => { - return num + 1 -} - -const mult2 = (num) => { - return num * 2 -} - -const add1AndMult2 = pipe(add1, mult2) -add1AndMult2(3) // => 8 -// because add 1 first, then multiple to 2 late => (3 + 1) * 2 -``` - -#### `maybe(Anything val)` - -Return a static variant of `Maybe` monad. - -```js -import { maybe } from 'bellajs' - -const plus5 = x => x + 5 -const minus2 = x => x - 2 -const isNumber = x => Number(x) === x -const toString = x => 'The value is ' + String(x) -const getDefault = () => 'This is default value' - -maybe(5) - .map(plus5) - .map(minus2) - .value() // 8 - -maybe('noop') - .map(plus5) - .map(minus2) - .value() // null - -maybe(5) - .if(isNumber) - .map(plus5) - .map(minus2) - .else(getDefault) - .map(toString) - .value() // 'The value is 8' - -maybe() - .if(isNumber) - .map(plus5) - .map(minus2) - .map(toString) - .value() // null - -maybe() - .if(isNumber) - .map(plus5) - .map(minus2) - .else(getDefault) - .map(toString) - .value() // 'This is default value' -``` - -### Date utils - -#### `formatDateString(Date | Timestamp [, String locale [, Object options]])` - -```js -import { - formatDateString -} from 'bellajs' - -const today = new Date() - -formatDateString(today) // => Jan 3, 2022, 8:34:28 PM GMT+7 - -// custom format -formatDateString(today, { - dateStyle: 'short', - timeStyle: 'short', - hour12: true -}) // => 1/3/22, 8:34 PM - -// custom locale -formatDateString(today, 'zh') // => 2022年1月3日 GMT+7 下午8:34:28 - -// custom lang and format -formatDateString(today, 'zh', { - dateStyle: 'short', - timeStyle: 'long', - hour12: true -}) // => 2022/1/3 GMT+7 下午8:34:28 - -formatDateString(today, 'vi') // => 20:34:28 GMT+7, 3 thg 1, 2022 -formatDateString(today, 'vi', { - dateStyle: 'full', - timeStyle: 'full' -}) // => 20:34:28 Giờ Đông Dương Thứ Hai, 3 tháng 1, 2022 -``` - - -#### `formatTimeAgo(Date | Timestamp [, String locale [, String justnow]])` - -```js -import { - formatTimeAgo -} from 'bellajs' - -const today = new Date() - -const yesterday = today.setDate(today.getDate() - 1) -formatTimeAgo(yesterday) // => 1 day ago - -const current = new Date() -const aLittleWhile = current.setHours(current.getHours() - 3) -formatTimeAgo(aLittleWhile) // => 3 hours ago - -// change locale -formatTimeAgo(aLittleWhile, 'zh') // => 3小时前 -formatTimeAgo(aLittleWhile, 'vi') // => 3 giờ trước -``` - -The last param `justnow` can be used to display a custom 'just now' message, when the distance is lesser than 1s. - -```js -const now = new Date() -const aJiff = now.setTime(now.getTime() - 100) -formatTimeAgo(aJiff) // => 'just now' -formatTimeAgo(aJiff, 'fr', 'à l\'instant') // => à l'instant -formatTimeAgo(aJiff, 'ja', 'すこし前') // => すこし前 -``` - -These two functions based on recent features of built-in object `Intl`. - -Please refer the following resources for more info: - -- [Intl.DateTimeFormat() constructor](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat/DateTimeFormat) -- [Intl.RelativeTimeFormat](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/RelativeTimeFormat) -- [Intl.Locale](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/Locale) - - -### Random utils - -#### `randint([Number min [, Number max]])` - -Returns a number between `min` and `max` - -```js -import { randint } from 'bellajs' - -randint() // => a random integer -randint(1, 5) // => a random integer between 3 and 5, including 1 and 5 -``` - -#### `genid([Number length [, String prefix]])` - -Create random ID string. - -```js -import { genid } from 'bellajs' - -genid() // => random 32 chars -genid(16) // => random 16 chars -genid(5) // => random 5 chars -genid(5, 'X_') // => X_{random 3 chars} -``` - -## Test - -```bash -git clone https://github.com/ndaidong/bellajs.git -cd bellajs -npm install -npm test -``` - -# License - -The MIT License (MIT) +# BellaJS + +Lightweight util for handling data type, string... in your Node.js and browser +apps. + +![CodeQL](https://github.com/ndaidong/bellajs/workflows/CodeQL/badge.svg) +[![CI test](https://github.com/ndaidong/bellajs/workflows/ci-test/badge.svg)](https://github.com/ndaidong/bellajs/actions) +[![Coverage Status](https://coveralls.io/repos/github/ndaidong/bellajs/badge.svg)](https://coveralls.io/github/ndaidong/bellajs) +[![NPM](https://img.shields.io/npm/v/%40ndaidong%2Fbellajs?color=32bb24)](https://www.npmjs.com/package/@ndaidong/bellajs) +[![JSR](https://jsr.io/badges/@ndaidong/bellajs?color=32bb24)](https://jsr.io/@ndaidong/bellajs) + +## Contents + +- [Setup](#setup) +- [APIs](#apis) + - [DataType detection](#datatype-detection) + - [String manipulation](#string-manipulation) + - [Data handling](#data-handling): [`clone`](#cloneanything-val), + [`copies`](#copiesobject-source-object-target-boolean-requirematching-array-excepts) + - [Array utils](#array-utils): [`pick`](#pickarray-arr--number-count--1), + [`sort`](#sortarray-arr--function-compare), + [`sortBy`](#sortbyarray-arr-number-order-string-property), + [`shuffle`](#shufflearray-arr), [`unique`](#uniquearray-arr) + - [Functional utils](#functional-utils): [`curry`](#curryfn), + [`compose`](#composef1-f2-fn), [`pipe`](#pipef1-f2-fn), + [`maybe`](#maybeanything-val) + - [Date utils](#date-utils): + [`formatDateString`](#formatdatestringdate--timestamp--string-locale--object-options), + [`formatTimeAgo`](#formattimeagodate--timestamp--string-locale--string-justnow) + - [Random utils](#random-utils): [`randint`](#randintnumber-min--number-max), + [`genid`](#genidnumber-length--string-prefix) + +- [Development](#development) + +- [License](#license) + +## Setup & Usage + +### Deno + +https://jsr.io/@ndaidong/bellajs + +```ts +import { genid } from "@ndaidong/bellajs"; + +for (let i = 0; i < 5; i++) { + console.log(genid()); +} +``` + +You can use JSR packages without an install step using `jsr:` specifiers: + +```ts +import { genid } from "jsr:@ndaidong/bellajs"; + +for (let i = 0; i < 5; i++) { + console.log(genid()); +} +``` + +You can also use `npm:` specifiers as before: + +```ts +import { genid } from "npm:@ndaidong/bellajs"; + +for (let i = 0; i < 5; i++) { + console.log(genid()); +} +``` + +Or import from esm.sh + +```ts +import { genid } from "https://esm.sh/@ndaidong/bellajs"; + +for (let i = 0; i < 5; i++) { + console.log(genid()); +} +``` + +### Node.js & Bun + +https://www.npmjs.com/package/@ndaidong/bellajs + +```bash +npm i @ndaidong/bellajs +# pnpm +pnpm i @ndaidong/bellajs +# yarn +yarn add @ndaidong/bellajs +# bun +bun add @ndaidong/bellajs +``` + +```js +import { genid } from "@ndaidong/bellajs"; + +for (let i = 0; i < 5; i++) { + console.log(genid()); +} +``` + +You can also use CJS style: + +```js +const { genid } = require("@ndaidong/bellajs"); + +for (let i = 0; i < 5; i++) { + console.log(genid()); +} +``` + +### Browsers: + +```html + +``` + +## APIs + +### DataType detection + +- `.isArray(Anything val)` +- `.isBoolean(Anything val)` +- `.isDate(Anything val)` +- `.isEmail(Anything val)` +- `.isEmpty(Anything val)` +- `.isFunction(Anything val)` +- `.isInteger(Anything val)` +- `.isLetter(Anything val)` +- `.isNil(Anything val)` +- `.isNull(Anything val)` +- `.isNumber(Anything val)` +- `.isObject(Anything val)` +- `.isString(Anything val)` +- `.isUndefined(Anything val)` + +### String manipulation + +- `.ucfirst(String s)` +- `.ucwords(String s)` +- `.escapeHTML(String s)` +- `.unescapeHTML(String s)` +- `.slugify(String s)` +- `.stripTags(String s)` +- `.stripAccent(String s)` +- `.truncate(String s, Number limit)` +- `.replaceAll(String s, String|Array search, String|Array replace)` + +### Data handling + +#### `clone(Anything val)` + +Make a deep copy of a variable. + +```js +import { clone } from "@ndaidong/bellajs"; + +const b = [ + 1, + 5, + 0, + "a", + -10, + "-10", + "", + { + a: 1, + b: "Awesome", + }, +]; + +const cb = clone(b); +console.log(cb); +``` + +_cb_ now has the same values as _b_, while the properties are standalone, not +reference. So that: + +```js +cb[7].a = 2; +cb[7].b = "Noop"; + +console.log(b[7]); +``` + +What you get is still: + +```js +{ + a: 1, + b: 'Awesome' +} +``` + +#### `copies(Object source, Object target[[, Boolean requireMatching], Array excepts])` + +Copy the properties from _source_ to _target_. + +- _requireMatching_: if true, BellaJS only copies the properties that are + already exist in _target_. +- _excepts_: array of the properties properties in _source_ that you don't want + to copy. + +After this action, target will be modified. + +```js +import { copies } from "@ndaidong/bellajs"; + +const a = { + name: "Toto", + age: 30, + level: 8, + nationality: { + name: "America", + }, +}; +const b = { + level: 4, + IQ: 140, + epouse: { + name: "Alice", + age: 27, + }, + nationality: { + long: "18123.123123.12312", + lat: "98984771.134231.1234", + }, +}; + +copies(a, b); +console.log(b); +``` + +Output: + +```js +{ + level: 8, + IQ: 140, + epouse: { + name: 'Alice', + age: 27 + }, + nationality: { + long: '18123.123123.12312', + lat: '98984771.134231.1234', + name: 'America' + }, + name: 'Toto', + age: 30 +} +``` + +### Array utils + +#### `pick(Array arr [, Number count = 1])` + +Randomly choose N elements from array. + +```js +import { pick } from "@ndaidong/bellajs"; + +const arr = [1, 3, 8, 2, 5, 7]; +pick(arr, 2); // --> [3, 5] +pick(arr, 2); // --> [8, 1] +pick(arr); // --> [3] +pick(arr); // --> [7] +``` + +#### `sort(Array arr [, Function compare])` + +Sort the array using a function. + +```js +import { sort } from "@ndaidong/bellajs"; + +const fn = (a, b) => { + return a < b ? 1 : a > b ? -1 : 0; +}; + +sort([3, 1, 5, 2], fn); // => [ 1, 2, 3, 5 ] +``` + +#### `sortBy(Array arr, Number order, String property)` + +Sort the array by specific property and direction. + +```js +import { sortBy } from "@ndaidong/bellajs"; + +const players = [ + { + name: "Jerome Nash", + age: 24, + }, + { + name: "Jackson Valdez", + age: 21, + }, + { + name: "Benjamin Cole", + age: 23, + }, + { + name: "Manuel Delgado", + age: 33, + }, + { + name: "Caleb McKinney", + age: 28, + }, +]; + +const result = sortBy(players, -1, "age"); +console.log(result); +``` + +#### `shuffle(Array arr)` + +Shuffle the positions of elements in an array. + +```js +import { shuffle } from "@ndaidong/bellajs"; + +shuffle([1, 3, 8, 2, 5, 7]); +``` + +#### `unique(Array arr)` + +Remove all duplicate elements from an array. + +```js +import { unique } from "@ndaidong/bellajs"; + +unique([1, 2, 3, 2, 3, 1, 5]); // => [ 1, 2, 3, 5 ] +``` + +### Functional utils + +#### `curry(fn)` + +Make a curried function. + +```js +import { curry } from "@ndaidong/bellajs"; + +const sum = curry((a, b, c) => { + return a + b + c; +}); + +sum(3)(2)(1); // => 6 +sum(1)(2)(3); // => 6 +sum(1, 2)(3); // => 6 +sum(1)(2, 3); // => 6 +sum(1, 2, 3); // => 6 +``` + +#### `compose(f1, f2, ...fN)` + +Performs right-to-left function composition. + +```js +import { compose } from "@ndaidong/bellajs"; + +const f1 = (name) => { + return `f1 ${name}`; +}; +const f2 = (name) => { + return `f2 ${name}`; +}; +const f3 = (name) => { + return `f3 ${name}`; +}; + +const addF = compose(f1, f2, f3); + +addF("Hello"); // => 'f1 f2 f3 Hello' + +const add1 = (num) => { + return num + 1; +}; + +const mult2 = (num) => { + return num * 2; +}; + +const add1AndMult2 = compose(add1, mult2); +add1AndMult2(3); // => 7 +// because multiple to 2 first, then add 1 late => 3 * 2 + 1 +``` + +#### `pipe(f1, f2, ...fN)` + +Performs left-to-right function composition. + +```js +import { pipe } from "@ndaidong/bellajs"; + +const f1 = (name) => { + return `f1 ${name}`; +}; +const f2 = (name) => { + return `f2 ${name}`; +}; +const f3 = (name) => { + return `f3 ${name}`; +}; + +const addF = pipe(f1, f2, f3); + +addF("Hello"); // => 'f3 f2 f1 Hello' + +const add1 = (num) => { + return num + 1; +}; + +const mult2 = (num) => { + return num * 2; +}; + +const add1AndMult2 = pipe(add1, mult2); +add1AndMult2(3); // => 8 +// because add 1 first, then multiple to 2 late => (3 + 1) * 2 +``` + +#### `maybe(Anything val)` + +Return a static variant of `Maybe` monad. + +```js +import { maybe } from "@ndaidong/bellajs"; + +const plus5 = (x) => x + 5; +const minus2 = (x) => x - 2; +const isNumber = (x) => Number(x) === x; +const toString = (x) => "The value is " + String(x); +const getDefault = () => "This is default value"; + +maybe(5) + .map(plus5) + .map(minus2) + .value(); // 8 + +maybe("noop") + .map(plus5) + .map(minus2) + .value(); // null + +maybe(5) + .if(isNumber) + .map(plus5) + .map(minus2) + .else(getDefault) + .map(toString) + .value(); // 'The value is 8' + +maybe() + .if(isNumber) + .map(plus5) + .map(minus2) + .map(toString) + .value(); // null + +maybe() + .if(isNumber) + .map(plus5) + .map(minus2) + .else(getDefault) + .map(toString) + .value(); // 'This is default value' +``` + +### Date utils + +#### `formatDateString(Date | Timestamp [, String locale [, Object options]])` + +```js +import { formatDateString } from "@ndaidong/bellajs"; + +const today = new Date(); + +formatDateString(today); // => Jan 3, 2022, 8:34:28 PM GMT+7 + +// custom format +formatDateString(today, { + dateStyle: "short", + timeStyle: "short", + hour12: true, +}); // => 1/3/22, 8:34 PM + +// custom locale +formatDateString(today, "zh"); // => 2022年1月3日 GMT+7 下午8:34:28 + +// custom lang and format +formatDateString(today, "zh", { + dateStyle: "short", + timeStyle: "long", + hour12: true, +}); // => 2022/1/3 GMT+7 下午8:34:28 + +formatDateString(today, "vi"); // => 20:34:28 GMT+7, 3 thg 1, 2022 +formatDateString(today, "vi", { + dateStyle: "full", + timeStyle: "full", +}); // => 20:34:28 Giờ Đông Dương Thứ Hai, 3 tháng 1, 2022 +``` + +#### `formatTimeAgo(Date | Timestamp [, String locale [, String justnow]])` + +```js +import { formatTimeAgo } from "@ndaidong/bellajs"; + +const today = new Date(); + +const yesterday = today.setDate(today.getDate() - 1); +formatTimeAgo(yesterday); // => 1 day ago + +const current = new Date(); +const aLittleWhile = current.setHours(current.getHours() - 3); +formatTimeAgo(aLittleWhile); // => 3 hours ago + +// change locale +formatTimeAgo(aLittleWhile, "zh"); // => 3小时前 +formatTimeAgo(aLittleWhile, "vi"); // => 3 giờ trước +``` + +The last param `justnow` can be used to display a custom 'just now' message, +when the distance is lesser than 1s. + +```js +const now = new Date(); +const aJiff = now.setTime(now.getTime() - 100); +formatTimeAgo(aJiff); // => 'just now' +formatTimeAgo(aJiff, "fr", "à l'instant"); // => à l'instant +formatTimeAgo(aJiff, "ja", "すこし前"); // => すこし前 +``` + +These two functions based on recent features of built-in object `Intl`. + +Please refer the following resources for more info: + +- [Intl.DateTimeFormat() constructor](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat/DateTimeFormat) +- [Intl.RelativeTimeFormat](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/RelativeTimeFormat) +- [Intl.Locale](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/Locale) + +### Random utils + +#### `randint([Number min [, Number max]])` + +Returns a number between `min` and `max` + +```js +import { randint } from "@ndaidong/bellajs"; + +randint(); // => a random integer +randint(1, 5); // => a random integer between 3 and 5, including 1 and 5 +``` + +#### `genid([Number length [, String prefix]])` + +Create random ID string. + +```js +import { genid } from "@ndaidong/bellajs"; + +genid(); // => random 32 chars +genid(16); // => random 16 chars +genid(5); // => random 5 chars +genid(5, "X_"); // => X_{random 3 chars} +``` + +## Development + +Since v12.x.x, we switched to [Deno](https://docs.deno.com/runtime/manual/) +platform, and use [DNT](https://github.com/denoland/dnt) to build Node.js +packages. + +```bash +git clone https://github.com/ndaidong/bellajs.git +cd bellajs + +# test +deno test + +# build npm packages +deno task build + +cd npm +node test_runner.js +``` + +## License + +The MIT License (MIT) + +--- diff --git a/deno.json b/deno.json new file mode 100644 index 0000000..ef529f3 --- /dev/null +++ b/deno.json @@ -0,0 +1,33 @@ +{ + "name": "@ndaidong/bellajs", + "version": "12.0.0-rc1", + "description": "A useful helper for any javascript program", + "homepage": "https://github.com/ndaidong/bellajs", + "repository": { + "type": "git", + "url": "git+https://github.com/ndaidong/bellajs.git" + }, + "author": "@ndaidong", + "license": "MIT", + "tasks": { + "build": "deno run -A ./scripts/build_npm.ts" + }, + "imports": { + "assert": "https://deno.land/std@0.224.0/assert/mod.ts", + "@deno/dnt": "jsr:@deno/dnt@^0.41.2" + }, + "exports": "./mod.ts", + "test": { + "include": ["tests"], + "exclude": [] + }, + "publish": { + "include": [ + "LICENSE", + "README.md", + "mod.ts", + "utils/*.ts", + "tests/*.ts" + ] + } +} diff --git a/eslint.config.js b/eslint.config.js deleted file mode 100644 index 1bef40a..0000000 --- a/eslint.config.js +++ /dev/null @@ -1,133 +0,0 @@ -// eslint.config.js - -import eslintjs from '@eslint/js' -import globals from 'globals' - -const ignores = [ - 'node_modules/*', - 'dist/*', -] - -export default [ - { - ignores, - ...eslintjs.configs.recommended, - }, - { - languageOptions: { - ecmaVersion: 'latest', - sourceType: 'module', - globals: { - ...globals.node, - ...globals.browser, - ...globals.jest, - Intl: 'readonly', - }, - }, - ignores, - rules: { - 'arrow-spacing': ['error', { 'before': true, 'after': true }], - 'block-spacing': ['error', 'always'], - 'brace-style': ['error', '1tbs', { 'allowSingleLine': true }], - 'camelcase': ['error', { - 'allow': ['^UNSAFE_'], - 'properties': 'never', - 'ignoreGlobals': true, - }], - 'comma-dangle': ['error', { - 'arrays': 'always-multiline', - 'objects': 'always-multiline', - 'imports': 'never', - 'exports': 'never', - 'functions': 'never', - }], - 'comma-spacing': ['error', { 'before': false, 'after': true }], - 'eol-last': 'error', - 'eqeqeq': ['error', 'always', { 'null': 'ignore' }], - 'func-call-spacing': ['error', 'never'], - 'indent': [ - 'error', - 2, - { - 'MemberExpression': 1, - 'FunctionDeclaration': { - 'body': 1, - 'parameters': 2, - }, - 'SwitchCase': 1, - 'ignoredNodes': ['TemplateLiteral > *'], - }, - ], - 'key-spacing': ['error', { 'beforeColon': false, 'afterColon': true }], - 'keyword-spacing': ['error', { 'before': true, 'after': true }], - 'lines-between-class-members': ['error', 'always', { 'exceptAfterSingleLine': true }], - 'max-len': [ - 'error', - { - 'code': 120, - 'ignoreTrailingComments': true, - 'ignoreComments': true, - 'ignoreUrls': true, - }, - ], - 'max-lines': [ - 'error', - { - 'max': 450, - 'skipBlankLines': true, - 'skipComments': false, - }, - ], - 'max-lines-per-function': [ - 'error', - { - 'max': 150, - 'skipBlankLines': true, - }, - ], - 'max-params': ['error', 4], - 'no-array-constructor': 'error', - 'no-mixed-spaces-and-tabs': 'error', - 'no-multi-spaces': 'error', - 'no-multi-str': 'error', - 'no-multiple-empty-lines': [ - 'error', - { - 'max': 1, - 'maxEOF': 0, - }, - ], - 'no-restricted-syntax': [ - 'error', - 'WithStatement', - 'BinaryExpression[operator=\'in\']', - ], - 'no-trailing-spaces': 'error', - 'no-use-before-define': [ - 'error', - { - 'functions': true, - 'classes': true, - 'variables': false, - }, - ], - 'no-var': 'warn', - 'object-curly-spacing': ['error', 'always'], - 'padded-blocks': [ - 'error', - { - 'blocks': 'never', - 'switches': 'never', - 'classes': 'never', - }, - ], - 'quotes': ['error', 'single'], - 'space-before-blocks': ['error', 'always'], - 'space-before-function-paren': ['error', 'always'], - 'space-infix-ops': 'error', - 'space-unary-ops': ['error', { 'words': true, 'nonwords': false }], - 'space-in-parens': ['error', 'never'], - 'semi': ['error', 'never'], - }, - }, -] diff --git a/mod.ts b/mod.ts new file mode 100644 index 0000000..0a10b0b --- /dev/null +++ b/mod.ts @@ -0,0 +1,97 @@ +// mod.ts + +import { + hasProperty, + isArray, + isDate, + isObject, + isString, +} from "./utils/detection.ts"; + +export const clone = (val: any): any => { + return structuredClone(val); +}; + +export const copies = ( + source: object, + dest: object, + matched: boolean = false, + excepts: string[] = [], +): object => { + const xdest = clone(dest) + for (const k in source) { + if (excepts.length > 0 && excepts.includes(k)) { + continue; + } + if (!matched || (matched && hasProperty(dest, k))) { + const oa: any = source[k as keyof typeof source]; + const ob: any = dest[k as keyof typeof dest]; + if ((isObject(ob) && isObject(oa)) || (isArray(ob) && isArray(oa))) { + xdest[k] = copies(oa, dest[k as keyof typeof dest], matched, excepts); + } else { + xdest[k] = clone(oa); + } + } + } + return xdest; +}; + +export const unique = (arr: any[] = []): any[] => { + return [...new Set(arr)]; +}; + +const fnSort = (a: any, b: any): number => { + return a > b ? 1 : (a < b ? -1 : 0); +}; + +export const sort = ( + arr: any[] = [], + sorting: ((a: any, b: any) => number) | null = null, +): any[] => { + const tmp: any[] = [...arr]; + const fn: (a: any, b: any) => number = sorting || fnSort; + tmp.sort(fn); + return tmp; +}; + +export const sortBy = ( + arr: any[] = [], + order: number = 1, + key: string = "", +): any[] => { + if (!isString(key) || !hasProperty(arr[0], key)) { + return arr; + } + return sort(arr, (m, n) => { + return m[key] > n[key] ? order : (m[key] < n[key] ? (-1 * order) : 0); + }); +}; + +export const shuffle = (arr: any[] = []): any[] => { + const input: any[] = [...arr]; + const output: any[] = []; + let inputLen: number = input.length; + while (inputLen > 0) { + const index: number = Math.floor(Math.random() * inputLen); + output.push(input.splice(index, 1)[0]); + inputLen--; + } + return output; +}; + +export const pick = (arr: any[] = [], count: number = 1): any[] => { + const a: any[] = shuffle(arr); + const mc: number = Math.max(1, count); + const c: number = Math.min(mc, a.length - 1); + return a.splice(0, c); +}; + +export * from "./utils/detection.ts"; +export * from "./utils/string.ts"; +export * from "./utils/random.ts"; +export * from "./utils/date.ts"; + +export * from "./utils/curry.ts"; +export * from "./utils/compose.ts"; +export * from "./utils/pipe.ts"; +export * from "./utils/maybe.ts"; diff --git a/package.json b/package.json deleted file mode 100755 index 5f23078..0000000 --- a/package.json +++ /dev/null @@ -1,36 +0,0 @@ -{ - "version": "11.2.0", - "name": "bellajs", - "description": "A useful helper for any javascript program", - "homepage": "https://www.npmjs.com/package/bellajs", - "repository": { - "type": "git", - "url": "https://github.com/ndaidong/bellajs" - }, - "author": "@ndaidong", - "main": "./src/main.js", - "type": "module", - "engines": { - "node": ">= 18.4" - }, - "scripts": { - "lint": "eslint .", - "lint:fix": "eslint --fix .", - "pretest": "npm run lint", - "test": "NODE_ENV=test NODE_OPTIONS=--experimental-vm-modules jest --verbose --coverage=true --env=jsdom", - "reset": "node reset" - }, - "devDependencies": { - "eslint": "^9.2.0", - "globals": "^15.2.0", - "jest": "^29.7.0", - "jest-environment-jsdom": "^29.7.0" - }, - "keywords": [ - "detection", - "manipulation", - "templating", - "utilities" - ], - "license": "MIT" -} diff --git a/reset.js b/reset.js deleted file mode 100755 index 8e31507..0000000 --- a/reset.js +++ /dev/null @@ -1,37 +0,0 @@ -/** - * reset.js - * @ndaidong -**/ - -import { - existsSync, - unlinkSync -} from 'fs' - -import { execSync } from 'child_process' - -const dirs = [ - 'dist', - 'docs', - '.nyc_output', - 'coverage', - 'node_modules', - '.nuxt', -] - -const files = [ - 'yarn.lock', - 'pnpm-lock.yaml', - 'package-lock.json', - 'coverage.lcov', -] - -dirs.forEach((d) => { - execSync(`rm -rf ${d}`) -}) - -files.forEach((f) => { - if (existsSync(f)) { - unlinkSync(f) - } -}) diff --git a/scripts/build_npm.ts b/scripts/build_npm.ts new file mode 100644 index 0000000..2e969c6 --- /dev/null +++ b/scripts/build_npm.ts @@ -0,0 +1,32 @@ +// scripts/build_npm.ts +import { build, emptyDir } from "@deno/dnt"; + +import pkg from "../deno.json" with { type: "json" }; + +const outputDir = "./npm"; + +await emptyDir(outputDir); + +await build({ + importMap: "deno.json", + entryPoints: ["./mod.ts"], + outDir: outputDir, + shims: { + deno: true, + }, + package: { + name: pkg.name, + version: pkg.version, + description: pkg.description, + author: pkg.author, + repository: pkg.repository, + bugs: { + url: `${pkg.homepage}/issues`, + }, + license: pkg.license, + }, + postBuild() { + Deno.copyFileSync("LICENSE", "npm/LICENSE"); + Deno.copyFileSync("README.md", "npm/README.md"); + }, +}); diff --git a/src/main.js b/src/main.js deleted file mode 100755 index f400bf3..0000000 --- a/src/main.js +++ /dev/null @@ -1,128 +0,0 @@ -/** - * bellajs - * @ndaidong -**/ - -import { - isObject, - isArray, - isDate, - isString, - hasProperty -} from './utils/detection.js' - -export const clone = (val, history = null) => { - const stack = history || new Set() - - if (stack.has(val)) { - return val - } - - stack.add(val) - - if (isDate(val)) { - return new Date(val.valueOf()) - } - - const copyObject = (o) => { - const oo = Object.create({}) - for (const k in o) { - if (hasProperty(o, k)) { - oo[k] = clone(o[k], stack) - } - } - return oo - } - - const copyArray = (a) => { - return [...a].map((e) => { - if (isArray(e)) { - return copyArray(e) - } else if (isObject(e)) { - return copyObject(e) - } - return clone(e, stack) - }) - } - - if (isArray(val)) { - return copyArray(val) - } - - if (isObject(val)) { - return copyObject(val) - } - - return val -} - -export const copies = (source, dest, matched = false, excepts = []) => { - for (const k in source) { - if (excepts.length > 0 && excepts.includes(k)) { - continue - } - if (!matched || (matched && hasProperty(dest, k))) { - const oa = source[k] - const ob = dest[k] - if ((isObject(ob) && isObject(oa)) || (isArray(ob) && isArray(oa))) { - dest[k] = copies(oa, dest[k], matched, excepts) - } else { - dest[k] = clone(oa) - } - } - } - return dest -} - -export const unique = (arr = []) => { - return [...new Set(arr)] -} - -const fnSort = (a, b) => { - return a > b ? 1 : (a < b ? -1 : 0) -} - -export const sort = (arr = [], sorting = null) => { - const tmp = [...arr] - const fn = sorting || fnSort - tmp.sort(fn) - return tmp -} - -export const sortBy = (arr = [], order = 1, key = '') => { - if (!isString(key) || !hasProperty(arr[0], key)) { - return arr - } - return sort(arr, (m, n) => { - return m[key] > n[key] ? order : (m[key] < n[key] ? (-1 * order) : 0) - }) -} - -export const shuffle = (arr = []) => { - const input = [...arr] - const output = [] - let inputLen = input.length - while (inputLen > 0) { - const index = Math.floor(Math.random() * inputLen) - output.push(input.splice(index, 1)[0]) - inputLen-- - } - return output -} - -export const pick = (arr = [], count = 1) => { - const a = shuffle(arr) - const mc = Math.max(1, count) - const c = Math.min(mc, a.length - 1) - return a.splice(0, c) -} - -export * from './utils/detection.js' -export * from './utils/string.js' -export * from './utils/random.js' -export * from './utils/date.js' - -export * from './utils/curry.js' -export * from './utils/compose.js' -export * from './utils/pipe.js' -export * from './utils/maybe.js' diff --git a/src/main.test.js b/src/main.test.js deleted file mode 100755 index 6de4927..0000000 --- a/src/main.test.js +++ /dev/null @@ -1,192 +0,0 @@ -// main.test - -/* eslint-env jest */ - -import { - hasProperty, - clone, - copies, - unique, - sort, - sortBy, - pick -} from './main.js' - -describe('test .clone() method:', () => { - test(' check if .clone(object) works correctly', () => { - const x = { - level: 4, - IQ: 140, - epouse: { - name: 'Alice', - age: 27, - }, - birthday: new Date(), - a: 0, - clone: false, - reg: /^\w+@\s([a-z])$/gi, - } - const y = clone(x) - Object.keys(x).forEach((k) => { - expect(hasProperty(y, k)).toBeTruthy() - }) - Object.keys(x.epouse).forEach((k) => { - expect(hasProperty(y.epouse, k)).toBeTruthy() - expect(y.epouse[k]).toEqual(x.epouse[k]) - }) - - // check immutability - y.epouse.age = 25 - expect(y.epouse.age).toEqual(25) - expect(x.epouse.age).toEqual(27) - }) - - test(' check if .clone(array) works correctly', () => { - const x = [ - 1, - 5, - 0, - 'a', - -10, - '-10', - '', - { - a: 1, - b: 'Awesome', - }, - [ - 5, - 6, - 8, - { - name: 'Lys', - age: 11, - }, - ], - ] - const y = clone(x) - expect(y).toHaveLength(x.length) - for (let i = 0; i < x.length; i++) { - expect(x[i]).toEqual(y[i]) - } - - // check immutability - y[8][3].age = 10 - expect(y[8][3].age).toEqual(10) - expect(x[8][3].age).toEqual(11) - }) -}) - -describe('test .copies() method:', () => { - test(' check if .copies(source, dest) works correctly', () => { - const source = { - name: 'Toto', - age: 30, - level: 8, - nationality: { - name: 'America', - }, - groups: [ - 'admin', - 'accountant', - ], - } - const dest = { - level: 4, - IQ: 140, - epouse: { - name: 'Alice', - age: 27, - }, - nationality: { - name: 'Congo', - long: '18123.123123.12312', - lat: '98984771.134231.1234', - }, - groups: [ - 'finance', - 'manager', - ], - } - copies(source, dest) - Object.keys(source).forEach((k) => { - expect(hasProperty(dest, k)).toBeTruthy() - }) - expect(dest.nationality.name).toEqual(source.nationality.name) - }) - - test(' check if .copies(source, dest, matched, excepts) works correctly', () => { - const source = { - name: 'Kiwi', - age: 16, - gender: 'male', - } - const dest = { - name: 'Aline', - age: 20, - } - copies(source, dest, true, ['age']) - expect(hasProperty(dest, 'gender')).toBeFalsy() - expect(dest.name).toEqual(source.name) - expect(dest.age === source.age).toBeFalsy() - }) -}) - -describe('test .unique() method:', () => { - test(' check if .unique(array) works correctly', () => { - const arr = [1, 1, 2, 2, 3, 4, 5, 5, 6, 3, 5, 4] - const uniqArr = unique(arr) - expect(uniqArr).toHaveLength(6) - }) -}) - -describe('test .sort() method:', () => { - test(' check if .sort(array) works correctly', () => { - const arr = [6, 4, 8, 2] - const sortedArr = sort(arr) - expect(sortedArr.join('')).toEqual('2468') - }) -}) - -describe('test .sortBy() method:', () => { - test(' check if .sortBy(array) works correctly', () => { - const arr = [ - { age: 5, name: 'E' }, - { age: 9, name: 'B' }, - { age: 3, name: 'A' }, - { age: 12, name: 'D' }, - { age: 7, name: 'C' }, - ] - const sortedByAge = [ - { age: 3, name: 'A' }, - { age: 5, name: 'E' }, - { age: 7, name: 'C' }, - { age: 9, name: 'B' }, - { age: 12, name: 'D' }, - ] - const sortedArr = sortBy(arr, 1, 'age') - expect(JSON.stringify(sortedArr) === JSON.stringify(sortedByAge)).toBeTruthy() - - const sortedByNonStringKey = sortBy(arr, 1, 99) - expect(JSON.stringify(sortedByNonStringKey) === JSON.stringify(arr)).toBeTruthy() - - const sortedByNonExistKey = sortBy(arr, 1, 'balance') - expect(JSON.stringify(sortedByNonExistKey) === JSON.stringify(arr)).toBeTruthy() - }) -}) - -describe('test .pick() method:', () => { - test(' check if .pick(array) works correctly', () => { - const str = 'abcdefghijklmnopqrstuvwxyz' - const arr = str.split('') - const uniqChar = pick(arr)[0] - expect(str.includes(uniqChar)).toBeTruthy() - }) - - test(' check if .pick(array, count) works correctly', () => { - const str = 'abcdefghijklmnopqrstuvwxyz' - const arr = str.split('') - const picked = pick(arr, 10) - expect(picked).toHaveLength(10) - }) -}) diff --git a/src/utils/compose.js b/src/utils/compose.js deleted file mode 100755 index c061193..0000000 --- a/src/utils/compose.js +++ /dev/null @@ -1,5 +0,0 @@ -// utils / compose - -export const compose = (...fns) => { - return fns.reduce((f, g) => (x) => f(g(x))) -} diff --git a/src/utils/compose.test.js b/src/utils/compose.test.js deleted file mode 100755 index 642e1a2..0000000 --- a/src/utils/compose.test.js +++ /dev/null @@ -1,44 +0,0 @@ -// compose.test - -/* eslint-env jest */ - -import { - compose -} from './compose.js' - -describe('test .compose() method:', () => { - const f1 = (name) => { - return `f1 ${name}` - } - const f2 = (name) => { - return `f2 ${name}` - } - const f3 = (name) => { - return `f3 ${name}` - } - - const addDashes = compose(f1, f2, f3) - - const add3 = (num) => { - return num + 3 - } - - const mul6 = (num) => { - return num * 6 - } - - const div2 = (num) => { - return num / 2 - } - - const sub5 = (num) => { - return num - 5 - } - - const calculate = compose(sub5, div2, mul6, add3) - - test(' check if .compose() works correctly', () => { - expect(addDashes('Alice')).toEqual('f1 f2 f3 Alice') - expect(calculate(5)).toEqual(19) - }) -}) diff --git a/src/utils/curry.js b/src/utils/curry.js deleted file mode 100755 index b4f42db..0000000 --- a/src/utils/curry.js +++ /dev/null @@ -1,14 +0,0 @@ -// utils / curry - -export const curry = (fn) => { - const totalArguments = fn.length - const next = (argumentLength, rest) => { - if (argumentLength > 0) { - return (...args) => { - return next(argumentLength - args.length, [...rest, ...args]) - } - } - return fn(...rest) - } - return next(totalArguments, []) -} diff --git a/src/utils/curry.test.js b/src/utils/curry.test.js deleted file mode 100755 index d6612fc..0000000 --- a/src/utils/curry.test.js +++ /dev/null @@ -1,20 +0,0 @@ -// curry.test - -/* eslint-env jest */ - -import { - curry -} from './curry.js' - -describe('test .curry() method:', () => { - const sum = curry((a, b, c) => { - return a + b + c - }) - test(' check if .curry() works correctly', () => { - expect(sum(3)(2)(1)).toEqual(6) - expect(sum(1)(2)(3)).toEqual(6) - expect(sum(1, 2)(3)).toEqual(6) - expect(sum(1)(2, 3)).toEqual(6) - expect(sum(1, 2, 3)).toEqual(6) - }) -}) diff --git a/src/utils/date.test.js b/src/utils/date.test.js deleted file mode 100755 index 7881189..0000000 --- a/src/utils/date.test.js +++ /dev/null @@ -1,62 +0,0 @@ -// date.test - -/* eslint-env jest */ - -import { jest } from '@jest/globals' - -import { - formatDateString, - formatTimeAgo -} from './date.js' - -describe('test .formatDateString() method', () => { - const d = new Date() - - test(' check .formatDateString() with default options', () => { - const result = formatDateString(d) - const reg = /^\w+\s\d+,\s+\d{4},\s\d+:\d+:\d+\s(AM|PM)\s(GMT)\+\d+$/ - expect(result.match(reg) !== null).toBeTruthy() - }) - - test(' check .formatDateString() with custom options', () => { - const result = formatDateString(d, { - dateStyle: 'full', - timeStyle: 'medium', - hour12: true, - }) - const reg = /^\w+,\s\w+\s\d+,\s+\d{4}\sat\s\d+:\d+:\d+\s(AM|PM)$/ - expect(result.match(reg) !== null).toBeTruthy() - }) - - test(' check .formatDateString() with custom language and options', () => { - const result = formatDateString(d, 'en', { - dateStyle: 'full', - timeStyle: 'medium', - hour12: true, - }) - const reg = /^\w+,\s\w+\s\d+,\s+\d{4}\sat\s\d+:\d+:\d+\s(AM|PM)$/ - expect(result.match(reg) !== null).toBeTruthy() - }) -}) - -describe('test .formatTimeAgo() method:', () => { - jest.useFakeTimers() - jest.spyOn(global, 'setTimeout') - - const d = new Date() - - test(' check if .formatTimeAgo() return "just now"', () => { - const result = formatTimeAgo(d) - expect(result === 'just now').toBeTruthy() - const justnowCustomMessage = formatTimeAgo(d, 'vi', 'vừa mới xong') - expect(justnowCustomMessage === 'vừa mới xong').toBeTruthy() - }) - - test(' check .formatTimeAgo() after 5s', () => { - setTimeout(() => { - const result = formatTimeAgo(d) - expect(result === '5 seconds ago').toBeTruthy() - }, 5000) - jest.advanceTimersByTime(5000) - }) -}) diff --git a/src/utils/detection.js b/src/utils/detection.js deleted file mode 100755 index 80c019d..0000000 --- a/src/utils/detection.js +++ /dev/null @@ -1,77 +0,0 @@ -// utils / detection - -const ob2Str = (val) => { - return {}.toString.call(val) -} - -export const isInteger = (val) => { - return Number.isInteger(val) -} - -export const isArray = (val) => { - return Array.isArray(val) -} - -export const isString = (val) => { - return String(val) === val -} - -export const isNumber = (val) => { - return Number(val) === val -} - -export const isBoolean = (val) => { - return Boolean(val) === val -} - -export const isNull = (val) => { - return ob2Str(val) === '[object Null]' -} - -export const isUndefined = (val) => { - return ob2Str(val) === '[object Undefined]' -} - -export const isNil = (val) => { - return isUndefined(val) || isNull(val) -} - -export const isFunction = (val) => { - return ob2Str(val) === '[object Function]' -} - -export const isObject = (val) => { - return ob2Str(val) === '[object Object]' && !isArray(val) -} - -export const isDate = (val) => { - return val instanceof Date && !isNaN(val.valueOf()) -} - -export const isElement = (v) => { - return ob2Str(v).match(/^\[object HTML\w*Element]$/) !== null -} - -export const isLetter = (val) => { - const re = /^[a-z]+$/i - return isString(val) && re.test(val) -} - -export const isEmail = (val) => { - const re = /^([\w-]+(?:\.[\w-]+)*)@((?:[\w-]+\.)*\w[\w-]{0,66})\.([a-z]{2,6}(?:\.[a-z]{2})?)$/i - return isString(val) && re.test(val) -} - -export const isEmpty = (val) => { - return !val || isNil(val) || - (isString(val) && val === '') || - (isArray(val) && val.length === 0) || - (isObject(val) && Object.keys(val).length === 0) -} - -export const hasProperty = (ob, k) => { - if (!ob || !k) { - return false - } - return Object.prototype.hasOwnProperty.call(ob, k) -} diff --git a/src/utils/detection.test.js b/src/utils/detection.test.js deleted file mode 100755 index e7e030a..0000000 --- a/src/utils/detection.test.js +++ /dev/null @@ -1,290 +0,0 @@ -// detection.test - -/* eslint-env jest */ - -import { - isInteger, - isArray, - isString, - isNumber, - isBoolean, - isNull, - isUndefined, - isNil, - isFunction, - isObject, - isDate, - isElement, - isLetter, - isEmail, - isEmpty, - hasProperty -} from './detection.js' - -describe('test .isInteger() method:', () => { - const positives = [1, 1000, 9999, 0, -3] - positives.forEach((val) => { - test(`test .isInteger(${val}) --> true`, () => { - expect(isInteger(val)).toBe(true) - }) - }) - - const negatives = [1.5, -3.2, '', undefined] - negatives.forEach((val) => { - test(`test .isInteger(${val}) --> false`, () => { - expect(isInteger(val)).toBe(false) - }) - }) -}) - -describe('test .isArray() method:', () => { - const positives = [[], [1, 2, 3]] - positives.forEach((val) => { - test(`test .isArray(${val}) --> true`, () => { - expect(isArray(val)).toBe(true) - }) - }) - - const negatives = [1.5, '', undefined] - negatives.forEach((val) => { - test(`test .isArray(${val}) --> false`, () => { - expect(isArray(val)).toBe(false) - }) - }) -}) - -describe('test .isString() method:', () => { - const positives = ['', 'abc xyz', '10000'] - positives.forEach((val) => { - test(`test .isString(${val}) --> true`, () => { - expect(isString(val)).toBe(true) - }) - }) - - const negatives = [{}, 30, [], 1.5, null, undefined] - negatives.forEach((val) => { - test(`test .isString(${val}) --> false`, () => { - expect(isString(val)).toBe(false) - }) - }) -}) - -describe('test .isNumber() method:', () => { - const positives = [1, 1.5, 0, 9999, -2] - positives.forEach((val) => { - test(`test .isNumber(${val}) --> true`, () => { - expect(isNumber(val)).toBe(true) - }) - }) - - const negatives = [{}, [], '', null, undefined] - negatives.forEach((val) => { - test(`test .isNumber(${val}) --> false`, () => { - expect(isNumber(val)).toBe(false) - }) - }) -}) - -describe('test .isBoolean() method:', () => { - const positives = [true, false, 3 !== 2, 3 === 2] - positives.forEach((val) => { - test(`test .isBoolean(${val}) --> true`, () => { - expect(isBoolean(val)).toBe(true) - }) - }) - - const negatives = [{}, [], '', 1, 0, null, undefined] - negatives.forEach((val) => { - test(`test .isBoolean(${val}) --> false`, () => { - expect(isBoolean(val)).toBe(false) - }) - }) -}) - -describe('test .isNull() method:', () => { - const positives = [null] - positives.forEach((val) => { - test(`test .isNull(${val}) --> true`, () => { - expect(isNull(val)).toBe(true) - }) - }) - - const negatives = [{}, [], '', 0, undefined] - negatives.forEach((val) => { - test(`test .isNull(${val}) --> false`, () => { - expect(isNull(val)).toBe(false) - }) - }) -}) - -describe('test .isUndefined() method:', () => { - let v - const positives = [undefined, v] - positives.forEach((val) => { - test(`test .isUndefined(${val}) --> true`, () => { - expect(isUndefined(val)).toBe(true) - }) - }) - - const negatives = [{}, [], '', 0, null] - negatives.forEach((val) => { - test(`test .isUndefined(${val}) --> false`, () => { - expect(isUndefined(val)).toBe(false) - }) - }) -}) - -describe('test .isNil() method:', () => { - let v - const positives = [undefined, v, null] - positives.forEach((val) => { - test(`test .isNil(${val}) --> true`, () => { - expect(isNil(val)).toBe(true) - }) - }) - - const negatives = [{}, [], '', 0] - negatives.forEach((val) => { - test(`test .isNil(${val}) --> false`, () => { - expect(isNil(val)).toBe(false) - }) - }) -}) - -describe('test .isFunction() method:', () => { - const positives = [function () {}, () => {}] - positives.forEach((val) => { - test(`test .isFunction(${val}) --> true`, () => { - expect(isFunction(val)).toBe(true) - }) - }) - - const negatives = [{}, [], '', 0, null] - negatives.forEach((val) => { - test(`test .isFunction(${val}) --> false`, () => { - expect(isFunction(val)).toBe(false) - }) - }) -}) - -describe('test .isObject() method:', () => { - const ob = new Object() // eslint-disable-line - const positives = [{}, ob, Object.create({})] - positives.forEach((val) => { - test(`test .isObject(${val}) --> true`, () => { - expect(isObject(val)).toBe(true) - }) - }) - - const negatives = [17, [], '', 0, null, () => {}, true] - negatives.forEach((val) => { - test(`test .isObject(${val}) --> false`, () => { - expect(isObject(val)).toBe(false) - }) - }) -}) - -describe('test .isDate() method:', () => { - const dt = new Date() - const positives = [dt] - positives.forEach((val) => { - test(`test .isDate(${val}) --> true`, () => { - expect(isDate(val)).toBe(true) - }) - }) - - const negatives = [17, [], '', 0, null, () => {}, true, {}, dt.toUTCString()] - negatives.forEach((val) => { - test(`test .isDate(${val}) --> false`, () => { - expect(isDate(val)).toBe(false) - }) - }) -}) - -describe('test .isElement() method:', () => { - const el = document.createElement('DIV') - const positives = [el] - positives.forEach((val) => { - test(`test .isElement(${val}) --> true`, () => { - expect(isElement(val)).toBe(true) - }) - }) - - const negatives = [17, [], '', 0, null, () => {}, true, {}] - negatives.forEach((val) => { - test(`test .isElement(${val}) --> false`, () => { - expect(isElement(val)).toBe(false) - }) - }) -}) - -describe('test .isLetter() method:', () => { - const positives = ['a', 'A', 'sigma'] - positives.forEach((val) => { - test(`test .isLetter(${val}) --> true`, () => { - expect(isLetter(val)).toBe(true) - }) - }) - - const negatives = [{}, [], '', 0, undefined, 'a23b'] - negatives.forEach((val) => { - test(`test .isLetter(${val}) --> false`, () => { - expect(isLetter(val)).toBe(false) - }) - }) -}) - -describe('test .isEmail() method:', () => { - const positives = ['admin@pwshub.com', 'abc@qtest.com'] - positives.forEach((val) => { - test(`test .isEmail(${val}) --> true`, () => { - expect(isEmail(val)).toBe(true) - }) - }) - - const negatives = [{}, [], '', 0, undefined, 'a23b@qtest@com'] - negatives.forEach((val) => { - test(`test .isEmail(${val}) --> false`, () => { - expect(isEmail(val)).toBe(false) - }) - }) -}) - -describe('test .isEmpty() method:', () => { - const positives = ['', 0, {}, [], undefined, null] - positives.forEach((val) => { - test(`test .isEmpty(${val}) --> true`, () => { - expect(isEmpty(val)).toBe(true) - }) - }) - - const negatives = [{ a: 1 }, '12', 9, [7, 1]] - negatives.forEach((val) => { - test(`test .isEmpty(${val}) --> false`, () => { - expect(isEmpty(val)).toBe(false) - }) - }) -}) - -describe('test .hasProperty() method:', () => { - const obj = { - name: 'alice', - age: 17, - } - const positives = ['name', 'age'] - positives.forEach((val) => { - test(`test .hasProperty(${val}) --> true`, () => { - expect(hasProperty(obj, val)).toBe(true) - }) - }) - - const negatives = [{ a: 1 }, 'email', 9, '__proto__'] - negatives.forEach((val) => { - test(`test .hasProperty(${val}) --> false`, () => { - expect(hasProperty(obj, val)).toBe(false) - }) - }) - test('test .hasProperty(null) --> false', () => { - expect(hasProperty(null)).toBe(false) - }) -}) diff --git a/src/utils/maybe.js b/src/utils/maybe.js deleted file mode 100755 index 203e0d0..0000000 --- a/src/utils/maybe.js +++ /dev/null @@ -1,33 +0,0 @@ -// utils / maybe - -import { - defineProp -} from './defineProp.js' - -export const maybe = (val) => { - const __val = val - const isNil = () => { - return __val === null || __val === undefined - } - const value = () => { - return __val - } - const getElse = (fn) => { - return maybe(__val || fn()) - } - const filter = (fn) => { - return maybe(fn(__val) === true ? __val : null) - } - const map = (fn) => { - return maybe(isNil() ? null : fn(__val)) - } - const output = Object.create({}) - defineProp(output, '__value__', __val, { enumerable: true }) - defineProp(output, '__type__', 'Maybe', { enumerable: true }) - defineProp(output, 'isNil', isNil) - defineProp(output, 'value', value) - defineProp(output, 'map', map) - defineProp(output, 'if', filter) - defineProp(output, 'else', getElse) - return output -} diff --git a/src/utils/maybe.test.js b/src/utils/maybe.test.js deleted file mode 100755 index 5c721eb..0000000 --- a/src/utils/maybe.test.js +++ /dev/null @@ -1,35 +0,0 @@ -// maybe.test - -/* eslint-env jest */ - -import { - maybe -} from './maybe.js' - -describe('test .maybe() method:', () => { - const plus5 = (x) => x + 5 - const minus2 = (x) => x - 2 - const isNumber = (x) => Number(x) === x - const toString = (x) => 'The value is ' + String(x) - const getDefault = () => 'This is default value' - - test(' check if .maybe() works correctly', () => { - const x1 = maybe(5) - .if(isNumber) - .map(plus5) - .map(minus2) - .map(toString) - .else(getDefault) - .value() - expect(x1).toEqual('The value is 8') - - const x2 = maybe('nothing') - .if(isNumber) - .map(plus5) - .map(minus2) - .map(toString) - .else(getDefault) - .value() - expect(x2).toEqual('This is default value') - }) -}) diff --git a/src/utils/pipe.js b/src/utils/pipe.js deleted file mode 100755 index 3ee2b54..0000000 --- a/src/utils/pipe.js +++ /dev/null @@ -1,5 +0,0 @@ -// utils / pipe - -export const pipe = (...fns) => { - return fns.reduce((f, g) => (x) => g(f(x))) -} diff --git a/src/utils/pipe.test.js b/src/utils/pipe.test.js deleted file mode 100755 index 9ce5904..0000000 --- a/src/utils/pipe.test.js +++ /dev/null @@ -1,44 +0,0 @@ -// pipe.test - -/* eslint-env jest */ - -import { - pipe -} from './pipe.js' - -describe('test .pipe() method:', () => { - const f1 = (name) => { - return `f1 ${name}` - } - const f2 = (name) => { - return `f2 ${name}` - } - const f3 = (name) => { - return `f3 ${name}` - } - - const addDashes = pipe(f1, f2, f3) - - const add3 = (num) => { - return num + 3 - } - - const mul6 = (num) => { - return num * 6 - } - - const div2 = (num) => { - return num / 2 - } - - const sub5 = (num) => { - return num - 5 - } - - const calculate = pipe(add3, mul6, div2, sub5) - - test(' check if .compose() works correctly', () => { - expect(addDashes('Alice')).toEqual('f3 f2 f1 Alice') - expect(calculate(5)).toEqual(19) - }) -}) diff --git a/src/utils/random.js b/src/utils/random.js deleted file mode 100755 index fc7cbf1..0000000 --- a/src/utils/random.js +++ /dev/null @@ -1,22 +0,0 @@ -// utils / random - -const crypto = globalThis.crypto - -export const genid = (len = 32, prefix = '') => { - let s = prefix - const nums = crypto.getRandomValues(new Uint32Array(len)) - for (let i = 0; i < nums.length; i++) { - const n = nums[i].toString(36) - const r = Math.random() - const c = n.charAt(Math.floor(r * n.length)) - s += (r > 0.3 && r < 0.7) ? c.toUpperCase() : c - } - return s.substring(0, len) -} - -export const randint = (min = 0, max = 1e6) => { - const byteArray = new Uint8Array(1) - crypto.getRandomValues(byteArray) - const floatNum = '0.' + byteArray[0].toString() - return Math.floor(floatNum * (max - min + 1)) + min -} diff --git a/src/utils/random.test.js b/src/utils/random.test.js deleted file mode 100755 index aca423a..0000000 --- a/src/utils/random.test.js +++ /dev/null @@ -1,55 +0,0 @@ -// random.test - -/* eslint-env jest */ - -import { randint, genid } from './random.js' - -describe('test .randint() method:', () => { - const randArr = [] - while (randArr.length < 20) { - randArr.push(randint()) - } - test(`test .randint() after ${randArr.length} times`, () => { - expect(randArr).toHaveLength(20) - const uniqVal = Array.from(new Set(randArr)) - expect(uniqVal.length).toBeGreaterThan(10) - }) - - test('test .randint() with same min/max', () => { - const q = randint(10, 10) - expect(q).toEqual(10) - }) - - const min = 50 - const max = 80 - test(`test .randint() between ${min} - ${max}`, () => { - for (let i = 0; i < 100; i++) { - const q = randint(min, max) - expect(q).toBeGreaterThanOrEqual(min) - expect(q).toBeLessThanOrEqual(max) - } - }) -}) - -describe('test .genid() method:', () => { - test('check .genid() default param', () => { - const actual = genid() - expect(actual).toHaveLength(32) - }) - - test('check .genid(512)', () => { - const actual = genid(512) - expect(actual).toHaveLength(512) - }) - - const len = 100 - const ids = [] - while (ids.length < len) { - ids.push(genid()) - } - const uniques = Array.from(new Set(ids)) - test('check .genid() always return unique string', () => { - expect(ids).toHaveLength(len) - expect(uniques).toHaveLength(ids.length) - }) -}) diff --git a/src/utils/string.js b/src/utils/string.js deleted file mode 100755 index 0438690..0000000 --- a/src/utils/string.js +++ /dev/null @@ -1,143 +0,0 @@ -// utils / string - -import { - isArray, - isString, - isNumber, - hasProperty -} from './detection.js' - -const toString = (input) => { - const s = isNumber(input) ? String(input) : input - if (!isString(s)) { - throw new Error('InvalidInput: String required.') - } - return s -} - -export const truncate = (s, len = 140) => { - const txt = toString(s) - const txtlen = txt.length - if (txtlen <= len) { - return txt - } - const subtxt = txt.substring(0, len).trim() - const subtxtArr = subtxt.split(' ') - const subtxtLen = subtxtArr.length - if (subtxtLen > 1) { - subtxtArr.pop() - return subtxtArr.map(word => word.trim()).join(' ') + '...' - } - return subtxt.substring(0, len - 3) + '...' -} - -export const stripTags = (s) => { - return toString(s).replace(/(<([^>]+)>)/ig, '').trim() -} - -export const escapeHTML = (s) => { - return toString(s) - .replace(/&/g, '&') - .replace(//g, '>') - .replace(/"/g, '"') -} - -export const unescapeHTML = (s) => { - return toString(s) - .replace(/"/g, '"') - .replace(/</g, '<') - .replace(/>/g, '>') - .replace(/&/g, '&') -} - -export const ucfirst = (s) => { - const x = toString(s).toLowerCase() - return x.length > 1 ? x.charAt(0).toUpperCase() + x.slice(1) : x.toUpperCase() -} - -export const ucwords = (s) => { - return toString(s).split(' ').map((w) => { - return ucfirst(w) - }).join(' ') -} - -export const replaceAll = (s, alpha, beta) => { - let x = toString(s) - const a = isNumber(alpha) ? String(alpha) : alpha - const b = isNumber(beta) ? String(beta) : beta - - if (isString(a) && isString(b)) { - const aa = x.split(a) - x = aa.join(b) - } else if (isArray(a) && isString(b)) { - a.forEach((v) => { - x = replaceAll(x, v, b) - }) - } else if (isArray(a) && isArray(b) && a.length === b.length) { - const k = a.length - if (k > 0) { - for (let i = 0; i < k; i++) { - const aaa = a[i] - const bb = b[i] - x = replaceAll(x, aaa, bb) - } - } - } - return x -} - -const getCharMap = () => { - const lmap = { - a: 'á|à|ả|ã|ạ|ă|ắ|ặ|ằ|ẳ|ẵ|â|ấ|ầ|ẩ|ẫ|ậ|ä|æ', - c: 'ç', - d: 'đ|ð', - e: 'é|è|ẻ|ẽ|ẹ|ê|ế|ề|ể|ễ|ệ|ë', - i: 'í|ì|ỉ|ĩ|ị|ï|î', - n: 'ñ', - o: 'ó|ò|ỏ|õ|ọ|ô|ố|ồ|ổ|ỗ|ộ|ơ|ớ|ờ|ở|ỡ|ợ|ö|ø', - s: 'ß', - u: 'ú|ù|ủ|ũ|ụ|ư|ứ|ừ|ử|ữ|ự|û', - y: 'ý|ỳ|ỷ|ỹ|ỵ|ÿ', - } - - const map = { - ...lmap, - } - Object.keys(lmap).forEach((k) => { - const K = k.toUpperCase() - map[K] = lmap[k].toUpperCase() - }) - - return map -} - -export const stripAccent = (s) => { - let x = toString(s) - - const updateS = (ai, key) => { - x = replaceAll(x, ai, key) - } - - const map = getCharMap() - for (const key in map) { - if (hasProperty(map, key)) { - const a = map[key].split('|') - a.forEach((item) => { - return updateS(item, key) - }) - } - } - return x -} - -export const slugify = (s, delimiter = '-') => { - return stripAccent(s) - .normalize('NFKD') - .replace(/[\u0300-\u036f]/g, '') - .trim() - .toLowerCase() - .replace(/[^a-z0-9 -]/g, '') - .replace(/\s+/g, delimiter) - .replace(new RegExp(`${delimiter}+`, 'g'), delimiter) -} diff --git a/src/utils/string.test.js b/src/utils/string.test.js deleted file mode 100755 index 27202a0..0000000 --- a/src/utils/string.test.js +++ /dev/null @@ -1,254 +0,0 @@ -// string.test - -/* eslint-env jest */ - -import { - truncate, - stripTags, - escapeHTML, - unescapeHTML, - ucfirst, - ucwords, - replaceAll, - stripAccent, - slugify -} from './string.js' - -describe('test .truncate() method:', () => { - const inputs = [ - { - text: 'If a property is non-configurable, its writable attribute can only be changed to false.', - limit: 60, - expectation: 'If a property is non-configurable, its writable attribute...', - }, - { - text: 'this string is less than limit', - limit: 100, - expectation: 'this string is less than limit', - }, - { - text: 'uyyiyirwqyiyiyrihklhkjhskdjfhkahfiusayiyfiudyiyqwiyriuqyiouroiuyi', - limit: 20, - expectation: 'uyyiyirwqyiyiyrih...', - }, - ] - - inputs.forEach(({ text, limit, expectation }, k) => { - test(` check .truncate(text, ${k})`, () => { - const actual = truncate(text, limit) - expect(actual).toEqual(expectation) - }) - }) -}) - -describe('test .stripTags() method:', () => { - const inputs = [ - { - text: 'Hello world', - expectation: 'Hello world', - }, - ] - - inputs.forEach(({ text, expectation }, k) => { - test(` check .stripTags(text, ${k})`, () => { - const actual = stripTags(text) - expect(actual).toEqual(expectation) - }) - }) - - test(' check .stripTags(non-text)', () => { - expect(() => { - stripTags({}) - }).toThrow() - }) -}) - -describe('test .escapeHTML() method:', () => { - const inputs = [ - { - text: 'Hello world', - expectation: '<a>Hello <b>world</b></a>', - }, - ] - - inputs.forEach(({ text, expectation }) => { - test(` check .escapeHTML(${text})`, () => { - const actual = escapeHTML(text) - expect(actual).toEqual(expectation) - }) - }) -}) - -describe('test .unescapeHTML() method:', () => { - const inputs = [ - { - text: '<a>Hello <b>world</b></a>', - expectation: 'Hello world', - }, - ] - - inputs.forEach(({ text, expectation }) => { - test(` check .unescapeHTML(${text})`, () => { - const actual = unescapeHTML(text) - expect(actual).toEqual(expectation) - }) - }) -}) - -describe('test .ucfirst() method:', () => { - const inputs = [ - { - text: 'HElLo wOrLd', - expectation: 'Hello world', - }, - { - text: 'h', - expectation: 'H', - }, - ] - - inputs.forEach(({ text, expectation }) => { - test(` check .ucfirst(${text})`, () => { - const actual = ucfirst(text) - expect(actual).toEqual(expectation) - }) - }) -}) - -describe('test .ucwords() method:', () => { - const inputs = [ - { - text: 'HElLo wOrLd', - expectation: 'Hello World', - }, - ] - - inputs.forEach(({ text, expectation }) => { - test(` check .ucwords(${text})`, () => { - const actual = ucwords(text) - expect(actual).toEqual(expectation) - }) - }) -}) - -describe('test .replaceAll() method:', () => { - const inputs = [ - { - input: { - a: 'Hello world', - b: 'l', - c: '2', - }, - expectation: 'He22o wor2d', - }, - { - input: { - a: 'Hello world', - b: 'l', - c: 2, - }, - expectation: 'He22o wor2d', - }, - { - input: { - a: 798078967, - b: 7, - c: 1, - }, - expectation: '198018961', - }, - { - input: { - a: 'Hello world', - b: ['l', 'o'], - c: ['2', '0'], - }, - expectation: 'He220 w0r2d', - }, - { - input: { - a: 'Hello world', - b: ['l', 'o'], - c: '2', - }, - expectation: 'He222 w2r2d', - }, - { - input: { - a: 'Hello world', - b: ['l'], - c: ['2', '0'], - }, - expectation: 'Hello world', - }, - { - input: { - a: 'Hello world', - b: 'l', - }, - expectation: 'Hello world', - }, - { - input: { - a: 'Hello world', - }, - expectation: 'Hello world', - }, - { - input: { - a: 10000, - }, - expectation: '10000', - }, - { - input: { - a: 0, - }, - expectation: '0', - }, - ] - - inputs.forEach(({ input, expectation }) => { - const { a, b, c } = input - test(` check .replaceAll(${a}, ${b}, ${c})`, () => { - const actual = replaceAll(a, b, c) - expect(actual).toEqual(expectation) - }) - }) -}) - -describe('test .stripAccent() method:', () => { - const inputs = [ - { - text: 'Sur l\'année 2015 - ủ Ù ỹ Ỹ', - expectation: 'Sur l\'annee 2015 - u U y Y', - }, - ] - - inputs.forEach(({ text, expectation }) => { - test(` check .stripAccent(${text})`, () => { - const actual = stripAccent(text) - expect(actual).toEqual(expectation) - }) - }) -}) - -describe('test .slugify() method:', () => { - const inputs = [ - { - text: 'Sur l\'année 2015', - expectation: 'sur-lannee-2015', - }, - { - text: 'Nghị luận tác phẩm "Đường kách mệnh" của Hồ Chí Minh?!', - expectation: 'nghi-luan-tac-pham-duong-kach-menh-cua-ho-chi-minh', - }, - ] - - inputs.forEach(({ text, expectation }) => { - test(` check .slugify(${text})`, () => { - const actual = slugify(text) - expect(actual).toEqual(expectation) - }) - }) -}) diff --git a/tests/compose_test.ts b/tests/compose_test.ts new file mode 100644 index 0000000..2cfa02f --- /dev/null +++ b/tests/compose_test.ts @@ -0,0 +1,37 @@ +import { assertEquals } from "assert"; + +import { compose } from "../utils/compose.ts"; + +Deno.test("check if .compose() works correctly", () => { + const f1 = (name: string): string => { + return `f1 ${name}`; + }; + const f2 = (name: string): string => { + return `f2 ${name}`; + }; + const f3 = (name: string): string => { + return `f3 ${name}`; + }; + + const addDashes = compose(f1, f2, f3); + assertEquals(addDashes("Alice"), "f1 f2 f3 Alice"); + + const add3 = (num: number): number => { + return num + 3; + }; + + const mul6 = (num: number): number => { + return num * 6; + }; + + const div2 = (num: number): number => { + return num / 2; + }; + + const sub5 = (num: number): number => { + return num - 5; + }; + + const calculate = compose(sub5, div2, mul6, add3); + assertEquals(calculate(5), 19); +}); diff --git a/tests/curry_test.ts b/tests/curry_test.ts new file mode 100644 index 0000000..e21255b --- /dev/null +++ b/tests/curry_test.ts @@ -0,0 +1,14 @@ +import { assertEquals } from "assert"; + +import { curry } from "../utils/curry.ts"; + +Deno.test("check if .curry() works correctly", () => { + const sum = curry((a: number, b: number, c: number): number => { + return a + b + c; + }); + assertEquals(sum(3)(2)(1), 6); + assertEquals(sum(1)(2)(3), 6); + assertEquals(sum(1, 2)(3), 6); + assertEquals(sum(1)(2, 3), 6); + assertEquals(sum(1, 2, 3), 6); +}); diff --git a/tests/date_test.ts b/tests/date_test.ts new file mode 100644 index 0000000..bb5bf1a --- /dev/null +++ b/tests/date_test.ts @@ -0,0 +1,61 @@ +import { assertEquals } from "assert"; + +import { + formatDateString, + formatTimeAgo +} from "../utils/date.ts"; + +Deno.test("check if .formatDateString() works correctly", async (t) => { + const d = new Date(); + + await t.step(' check .formatDateString() with default options', () => { + const result = formatDateString(d) + const reg = /^\w+\s\d+,\s+\d{4},\s\d+:\d+:\d+\s(AM|PM)\s(GMT)\+\d+$/ + assertEquals(result.match(reg) !== null, true) + }); + + await t.step(' check .formatDateString() with custom options', () => { + const result = formatDateString(d, { + dateStyle: 'full', + timeStyle: 'medium', + hour12: true, + }) + const reg = /^\w+,\s\w+\s\d+,\s+\d{4}\sat\s\d+:\d+:\d+\s(AM|PM)$/ + assertEquals(result.match(reg) !== null, true) + }); + + await t.step(' check .formatDateString() with custom language and options', () => { + const result = formatDateString(d, 'en', { + dateStyle: 'full', + timeStyle: 'medium', + hour12: true, + }) + const reg = /^\w+,\s\w+\s\d+,\s+\d{4}\sat\s\d+:\d+:\d+\s(AM|PM)$/ + assertEquals(result.match(reg) !== null, true) + }); +}); + +Deno.test("check if .formatTimeAgo() works correctly", async (t) => { + const d = new Date(); + + await t.step(' check if .formatTimeAgo() return "just now"', () => { + const result = formatTimeAgo(d) + assertEquals(result, 'just now') + const justnowCustomMessage = formatTimeAgo(d, 'vi', 'vừa mới xong') + assertEquals(justnowCustomMessage, 'vừa mới xong') + }); + + await t.step(' check if .formatTimeAgo() after 5s', () => { + const t = d.getSeconds() + d.setSeconds(t - 5) + const result = formatTimeAgo(d) + assertEquals(result, '5 seconds ago') + }); + + await t.step(' check if .formatTimeAgo() after 2 days', () => { + const t = d.getDate() + d.setDate(t - 2) + const result = formatTimeAgo(d) + assertEquals(result, '2 days ago') + }); +}); diff --git a/tests/detection_test.ts b/tests/detection_test.ts new file mode 100644 index 0000000..29f8aea --- /dev/null +++ b/tests/detection_test.ts @@ -0,0 +1,250 @@ +import { assertEquals } from "assert"; + +import { + isNumber, + isInteger, + isArray, + isString, + isBoolean, + isNull, + isUndefined, + isNil, + isFunction, + isObject, + isDate, + isEmail, + isEmpty, + hasProperty +} from "../utils/detection.ts"; + +Deno.test("check if .isNumber() works correctly", async (t) => { + const positives = [1, 1.5, 0, 9999, -2] + for (const val of positives) { + await t.step(`test .isNumber(${val}) --> true`, () => { + assertEquals(isNumber(val), true) + }) + } + + const negatives = [{}, [], '', null, undefined] + for (const val of negatives) { + await t.step(`test .isNumber(${val}) --> true`, () => { + assertEquals(isNumber(val), false) + }) + } +}); + +Deno.test("check if .isInteger() works correctly", async (t) => { + const positives = [1, 1000, 9999, 0, -3] + for (const val of positives) { + await t.step(`test .isInteger(${val}) --> true`, () => { + assertEquals(isInteger(val), true) + }) + } + + const negatives = [1.5, -3.2, '', undefined] + for (const val of negatives) { + await t.step(`test .isInteger(${val}) --> true`, () => { + assertEquals(isInteger(val), false) + }) + } +}); + +Deno.test("check if .isArray() works correctly", async (t) => { + const positives = [[], [1, 2, 3], new Array(), Array.from(new Set())] + for (const val of positives) { + await t.step(`test .isArray(${val}) --> true`, () => { + assertEquals(isArray(val), true) + }) + } + + const negatives = [1.5, '', undefined] + for (const val of negatives) { + await t.step(`test .isArray(${val}) --> true`, () => { + assertEquals(isArray(val), false) + }) + } +}); + +Deno.test("check if .isString() works correctly", async (t) => { + const positives = ['', 'abc xyz', '10000'] + for (const val of positives) { + await t.step(`test .isString(${val}) --> true`, () => { + assertEquals(isString(val), true) + }) + } + + const negatives = [{}, 30, [], 1.5, null, undefined] + for (const val of negatives) { + await t.step(`test .isString(${val}) --> true`, () => { + assertEquals(isString(val), false) + }) + } +}); + +Deno.test("check if .isBoolean() works correctly", async (t) => { + const positives = [true, false] + for (const val of positives) { + await t.step(`test .isBoolean(${val}) --> true`, () => { + assertEquals(isBoolean(val), true) + }) + } + + const negatives = [{}, [], '', 1, 0, null, undefined] + for (const val of negatives) { + await t.step(`test .isBoolean(${val}) --> true`, () => { + assertEquals(isBoolean(val), false) + }) + } +}); + +Deno.test("check if .isNull() works correctly", async (t) => { + const positives = [null] + for (const val of positives) { + await t.step(`test .isNull(${val}) --> true`, () => { + assertEquals(isNull(val), true) + }) + } + + const negatives = [{}, [], '', 0, undefined] + for (const val of negatives) { + await t.step(`test .isNull(${val}) --> true`, () => { + assertEquals(isNull(val), false) + }) + } +}); + +Deno.test("check if .isUndefined() works correctly", async (t) => { + let v + const positives = [undefined, v] + for (const val of positives) { + await t.step(`test .isUndefined(${val}) --> true`, () => { + assertEquals(isUndefined(val), true) + }) + } + + const negatives = [{}, [], '', 0, null] + for (const val of negatives) { + await t.step(`test .isUndefined(${val}) --> true`, () => { + assertEquals(isUndefined(val), false) + }) + } +}); + +Deno.test("check if .isNil() works correctly", async (t) => { + let v + const positives = [undefined, v, null] + for (const val of positives) { + await t.step(`test .isNil(${val}) --> true`, () => { + assertEquals(isNil(val), true) + }) + } + + const negatives = [{}, [], '', 0] + for (const val of negatives) { + await t.step(`test .isNil(${val}) --> true`, () => { + assertEquals(isNil(val), false) + }) + } +}); + +Deno.test("check if .isFunction() works correctly", async (t) => { + const positives = [function () {}, () => {}] + for (const val of positives) { + await t.step(`test .isFunction(${val}) --> true`, () => { + assertEquals(isFunction(val), true) + }) + } + + const negatives = [{}, [], '', 0, null] + for (const val of negatives) { + await t.step(`test .isFunction(${val}) --> true`, () => { + assertEquals(isFunction(val), false) + }) + } +}); + +Deno.test("check if .isObject() works correctly", async (t) => { + const ob = new Object() + const positives = [{}, ob, Object.create({})] + for (const val of positives) { + await t.step(`test .isObject(${val}) --> true`, () => { + assertEquals(isObject(val), true) + }) + } + + const negatives = [17, [], '', 0, null, () => {}, true] + for (const val of negatives) { + await t.step(`test .isObject(${val}) --> true`, () => { + assertEquals(isObject(val), false) + }) + } +}); + +Deno.test("check if .isDate() works correctly", async (t) => { + const dt = new Date() + const positives = [dt] + for (const val of positives) { + await t.step(`test .isDate(${val}) --> true`, () => { + assertEquals(isDate(val), true) + }) + } + + const negatives = [17, [], '', 0, null, () => {}, true, {}, dt.toUTCString()] + for (const val of negatives) { + await t.step(`test .isDate(${val}) --> true`, () => { + assertEquals(isDate(val), false) + }) + } +}); + +Deno.test("check if .isEmail() works correctly", async (t) => { + const positives = ['admin@pwshub.com', 'abc@qtest.com'] + for (const val of positives) { + await t.step(`test .isEmail(${val}) --> true`, () => { + assertEquals(isEmail(val), true) + }) + } + + const negatives = [{}, [], '', 0, undefined, 'a23b@qtest@com'] + for (const val of negatives) { + await t.step(`test .isEmail(${val}) --> true`, () => { + assertEquals(isEmail(val), false) + }) + } +}); + +Deno.test("check if .isEmpty() works correctly", async (t) => { + const positives = ['', 0, {}, [], undefined, null] + for (const val of positives) { + await t.step(`test .isEmpty(${val}) --> true`, () => { + assertEquals(isEmpty(val), true) + }) + } + + const negatives = [{ a: 1 }, '12', 9, [7, 1]] + for (const val of negatives) { + await t.step(`test .isEmpty(${val}) --> true`, () => { + assertEquals(isEmpty(val), false) + }) + } +}); + +Deno.test("check if .hasProperty() works correctly", async (t) => { + const obj = { + name: 'alice', + age: 17, + } + const positives = ['name', 'age'] + for (const val of positives) { + await t.step(`test .hasProperty(${val}) --> true`, () => { + assertEquals(hasProperty(obj, val), true) + }) + } + + const negatives = [{ a: 1 }, 'email', 9, '__proto__'] + for (const val of negatives) { + await t.step(`test .hasProperty(${val}) --> true`, () => { + assertEquals(hasProperty(obj, val), false) + }) + } +}); diff --git a/tests/maybe_test.ts b/tests/maybe_test.ts new file mode 100644 index 0000000..f9b2ab8 --- /dev/null +++ b/tests/maybe_test.ts @@ -0,0 +1,29 @@ +import { assertEquals } from "assert"; + +import { maybe } from "../utils/maybe.ts"; + +Deno.test("check if .maybe() works correctly", () => { + const plus5 = (x: number): number => x + 5; + const minus2 = (x: number): number => x - 2; + const isNumber = (x: any): boolean => Number(x) === x; + const toString = (x: string): string => "The value is " + String(x); + const getDefault = (): string => "This is default value"; + + const x1 = maybe(5) + .if(isNumber) + .map(plus5) + .map(minus2) + .map(toString) + .else(getDefault) + .value(); + assertEquals(x1, "The value is 8"); + + const x2 = maybe("nothing") + .if(isNumber) + .map(plus5) + .map(minus2) + .map(toString) + .else(getDefault) + .value(); + assertEquals(x2, "This is default value"); +}); diff --git a/tests/mod_test.ts b/tests/mod_test.ts new file mode 100644 index 0000000..ef63fd1 --- /dev/null +++ b/tests/mod_test.ts @@ -0,0 +1,151 @@ +import { assertEquals, assertStringIncludes } from "assert"; + +import { + clone, + copies, + unique, + sort, + sortBy, + pick, + hasProperty +} from "../mod.ts"; + +Deno.test("check if .clone() works correctly", async (t) => { + await t.step(`check if .clone(object) works correctly`, () => { + const x: any = { + level: 4, + IQ: 140, + epouse: { + name: 'Alice', + age: 27, + }, + birthday: new Date(), + a: 0, + clone: false, + reg: /^\w+@\s([a-z])$/gi, + } + const y = clone(x) + Object.keys(x).forEach((k) => { + assertEquals(hasProperty(y, k), true) + }) + Object.keys(x.epouse).forEach((k) => { + assertEquals(hasProperty(y.epouse, k), true) + assertEquals(y.epouse[k], x.epouse[k as keyof typeof x.epouse]) + }) + + // check immutability + y.epouse.age = 25 + assertEquals(y.epouse.age, 25) + assertEquals(x.epouse.age, 27) + }) + + await t.step(`check if .clone(array) works correctly`, () => { + const x: any[] = [ + 1, + 5, + 0, + 'a', + -10, + '-10', + '', + { + a: 1, + b: 'Awesome', + }, + [ + 5, + 6, + 8, + { + name: 'Lys', + age: 11, + }, + ], + ] + const y = clone(x) + assertEquals(y.length, x.length) + for (let i = 0; i < x.length; i++) { + assertEquals(x[i], y[i]) + } + + // check immutability + y[8][3].age = 10 + assertEquals(y[8][3].age, 10) + assertEquals(x[8][3].age, 11) + }) +}); + +Deno.test("check if .copies() works correctly", async (t) => { + // await t.step(`check if .copies(source, dest) works correctly`, () => { + // const str = 'abcdefghijklmnopqrstuvwxyz' + // const arr = str.split('') + // const uniqChar = pick(arr)[0] + // assertStringIncludes(str, uniqChar) + // }) + + await t.step(`check if .copies(source, dest, matched, excepts) works correctly`, () => { + const source = { + name: 'Kiwi', + age: 16, + gender: 'male', + } + const dest = { + name: 'Aline', + age: 20, + } + copies(source, dest, true, ['age']) + assertEquals(hasProperty(dest, 'gender'), false) + assertEquals(dest.name, source.name) + assertEquals(dest.age, source.age) + }) +}); + +Deno.test("check if .unique(array) works correctly", () => { + const arr = [1, 1, 2, 2, 3, 4, 5, 5, 6, 3, 5, 4] + const uniqArr = unique(arr) + assertEquals(uniqArr.length, 6) +}); + +Deno.test("check if .sort(array) works correctly", () => { + const arr = [6, 4, 8, 2] + const sortedArr = sort(arr) + assertEquals(sortedArr.join(''), '2468') +}); + +Deno.test("check if .sortBy(array) works correctly", () => { + const arr = [ + { age: 5, name: 'E' }, + { age: 9, name: 'B' }, + { age: 3, name: 'A' }, + { age: 12, name: 'D' }, + { age: 7, name: 'C' }, + ] + const sortedByAge = [ + { age: 3, name: 'A' }, + { age: 5, name: 'E' }, + { age: 7, name: 'C' }, + { age: 9, name: 'B' }, + { age: 12, name: 'D' }, + ] + const sortedArr = sortBy(arr, 1, 'age') + assertEquals(JSON.stringify(sortedArr), JSON.stringify(sortedByAge)) + + const sortedByNonExistKey = sortBy(arr, 1, 'balance') + assertEquals(JSON.stringify(sortedByNonExistKey), JSON.stringify(arr)) +}); + +Deno.test("check if .pick() works correctly", async (t) => { + await t.step(`check if .pick(array) works correctly`, () => { + const str = 'abcdefghijklmnopqrstuvwxyz' + const arr = str.split('') + const uniqChar = pick(arr)[0] + assertStringIncludes(str, uniqChar) + }) + + await t.step(`check if .pick(array, count) works correctly`, () => { + const str = 'abcdefghijklmnopqrstuvwxyz' + const arr = str.split('') + const picked = pick(arr, 10) + assertEquals(picked.length, 10) + }) +}); diff --git a/tests/pipe_test.ts b/tests/pipe_test.ts new file mode 100644 index 0000000..7f89bda --- /dev/null +++ b/tests/pipe_test.ts @@ -0,0 +1,37 @@ +import { assertEquals } from "assert"; + +import { pipe } from "../utils/pipe.ts"; + +Deno.test("check if .pipe() works correctly", () => { + const f1 = (name: string): string => { + return `f1 ${name}`; + }; + const f2 = (name: string): string => { + return `f2 ${name}`; + }; + const f3 = (name: string): string => { + return `f3 ${name}`; + }; + + const addDashes = pipe(f1, f2, f3); + assertEquals(addDashes("Alice"), "f3 f2 f1 Alice"); + + const add3 = (num: number): number => { + return num + 3; + }; + + const mul6 = (num: number): number => { + return num * 6; + }; + + const div2 = (num: number): number => { + return num / 2; + }; + + const sub5 = (num: number): number => { + return num - 5; + }; + + const calculate = pipe(add3, mul6, div2, sub5); + assertEquals(calculate(5), 19); +}); diff --git a/tests/random_test.ts b/tests/random_test.ts new file mode 100644 index 0000000..2e0797b --- /dev/null +++ b/tests/random_test.ts @@ -0,0 +1,60 @@ +import { assertEquals } from "assert"; + +import { genid, randint } from "../utils/random.ts"; + +Deno.test("check if .randint() works correctly", async (t) => { + const randArr: number[] = []; + while (randArr.length < 20) { + randArr.push(randint()); + } + + await t.step(`.randint() after ${randArr.length} times`, async () => { + assertEquals(randArr.length, 20); + const uniqVal = Array.from(new Set(randArr)); + assertEquals(uniqVal.length > 10, true); + }); + + await t.step(".randint() with min = max", async () => { + const q = randint(10, 10); + assertEquals(q, 10); + }); + + const min = 50; + const max = 80; + await t.step(`.randint() between ${min} - ${max}`, async () => { + for (let i = 0; i < 100; i++) { + const q = randint(min, max); + assertEquals(q >= min, true); + assertEquals(q <= max, true); + } + }); +}); + +Deno.test("check if .genid() works correctly", async (t) => { + const randArr: number[] = []; + while (randArr.length < 20) { + randArr.push(randint()); + } + + await t.step(".genid() default param", async () => { + const actual = genid(); + assertEquals(actual.length, 32); + }); + + await t.step(".genid(512) default param", async () => { + const actual = genid(512); + assertEquals(actual.length, 512); + }); + + const len = 100; + const ids = []; + while (ids.length < len) { + ids.push(genid()); + } + const uniques = Array.from(new Set(ids)); + + await t.step(`.genid() always return unique string`, async () => { + assertEquals(ids.length, len); + assertEquals(uniques.length, len); + }); +}); diff --git a/tests/string_test.ts b/tests/string_test.ts new file mode 100644 index 0000000..123a85e --- /dev/null +++ b/tests/string_test.ts @@ -0,0 +1,77 @@ +import { assertEquals } from "assert"; + +import { + truncate, + stripTags, + escapeHTML, + unescapeHTML, + ucfirst, + ucwords, + slugify +} from "../utils/string.ts"; + +Deno.test("check if .truncate() works correctly", async (t) => { + const inputs = [ + { + text: 'If a property is non-configurable, its writable attribute can only be changed to false.', + limit: 60, + expectation: 'If a property is non-configurable, its writable attribute...', + }, + { + text: 'this string is less than limit', + limit: 100, + expectation: 'this string is less than limit', + }, + { + text: 'uyyiyirwqyiyiyrihklhkjhskdjfhkahfiusayiyfiudyiyqwiyriuqyiouroiuyi', + limit: 20, + expectation: 'uyyiyirwqyiyiyrih...', + }, + ] + + let k = 1 + for (const input of inputs) { + const { text, limit, expectation } = input + await t.step(` check .truncate($${k})`, () => { + const actual = truncate(text, limit) + assertEquals(actual, expectation) + }) + k++ + } +}); + +Deno.test("check if .stripTags() works correctly", () => { + const actual = stripTags('Hello world') + assertEquals(actual, 'Hello world') +}); + +Deno.test("check if .escapeHTML() works correctly", () => { + const actual = escapeHTML('Hello world') + assertEquals(actual, '<a>Hello <b>world</b></a>') +}); + +Deno.test("check if .unescapeHTML() works correctly", () => { + const actual = unescapeHTML('<a>Hello <b>world</b></a>') + assertEquals(actual, 'Hello world') +}); + +Deno.test("check if .ucfirst() works correctly", () => { + const actual = ucfirst('HElLo wOrLd') + assertEquals(actual, 'Hello world') +}); + +Deno.test("check if .ucwords() works correctly", () => { + const actual = ucwords('HElLo wOrLd') + assertEquals(actual, 'Hello World') +}); + +Deno.test("check if .slugify() works correctly", () => { + assertEquals( + slugify('Sur l\'année 2015'), + 'sur-lannee-2015' + ) + assertEquals( + slugify('Nghị luận tác phẩm "Đường kách mệnh" của Hồ Chí Minh?!'), + 'nghi-luan-tac-pham-duong-kach-menh-cua-ho-chi-minh' + ) +}); diff --git a/utils/compose.ts b/utils/compose.ts new file mode 100755 index 0000000..9a06893 --- /dev/null +++ b/utils/compose.ts @@ -0,0 +1,5 @@ +// utils / compose + +export const compose = (...fns: ((arg: T) => T)[]): (arg: T) => T => { + return fns.reduce((f, g) => (x) => f(g(x))); +}; diff --git a/utils/curry.ts b/utils/curry.ts new file mode 100755 index 0000000..3bb349f --- /dev/null +++ b/utils/curry.ts @@ -0,0 +1,14 @@ +// utils / curry + +export const curry = any>(fn: T) => { + const totalArguments: number = fn.length; + const next = (argumentLength: number, rest: any[]) => { + if (argumentLength > 0) { + return (...args: any[]) => { + return next(argumentLength - args.length, [...rest, ...args]); + }; + } + return fn(...rest); + }; + return next(totalArguments, []); +}; diff --git a/src/utils/date.js b/utils/date.ts similarity index 51% rename from src/utils/date.js rename to utils/date.ts index 52aa31f..1a53cb6 100755 --- a/src/utils/date.js +++ b/utils/date.ts @@ -1,17 +1,30 @@ // utils / date -import { - isObject -} from './detection.js' +import { isObject } from "./detection.ts"; -const getDateFormat = () => { - return { - dateStyle: 'medium', - timeStyle: 'long', - } +interface DateFormat { + dateStyle: string; + timeStyle: string; +} + +interface TimeConversions { + second: number; + minute: number; + hour: number; + day: number; + week: number; + month: number; + year: number; } -const getTimeConvers = () => { +const getDateFormat = (): DateFormat => { + return { + dateStyle: "medium", + timeStyle: "long", + }; +}; + +const getTimeConvers = (): TimeConversions => { return { second: 1000, minute: 60, @@ -20,10 +33,10 @@ const getTimeConvers = () => { week: 7, month: 4, year: 12, - } -} + }; +}; -const isValidLocal = (hl) => { +const isValidLocal = (hl: string): boolean => { try { const locale = new Intl.Locale(hl) return locale.language !== '' @@ -32,7 +45,7 @@ const isValidLocal = (hl) => { } } -export const formatDateString = (...args) => { +export const formatDateString = (...args: any[]): string => { const input = args[0] const lang = isValidLocal(args[1]) ? args[1] : 'en' const dfmt = getDateFormat() @@ -47,20 +60,20 @@ export const formatDateString = (...args) => { return dtf.format(new Date(input)) } -export const formatTimeAgo = (input, lang = 'en', justnow = 'just now') => { - const t = new Date(input) - let delta = Date.now() - t +export const formatTimeAgo = (input: any, lang: string = 'en', justnow: string = 'just now'): string => { + const t: number = (new Date(input)).getTime() + let delta: number = Date.now() - t const tcv = getTimeConvers() if (delta <= tcv.second) { return justnow } - let unit = 'second' + let unit: any = 'second' for (const key in tcv) { - if (delta < tcv[key]) { + if (delta < tcv[key as keyof typeof tcv]) { break } else { unit = key - delta /= tcv[key] + delta /= tcv[key as keyof typeof tcv] } } delta = Math.floor(delta) diff --git a/src/utils/defineProp.js b/utils/defineProp.ts similarity index 51% rename from src/utils/defineProp.js rename to utils/defineProp.ts index 44c17b5..4ccd1eb 100755 --- a/src/utils/defineProp.js +++ b/utils/defineProp.ts @@ -1,15 +1,21 @@ // utils / defineProp -export const defineProp = (ob, key, val, config = {}) => { +export const defineProp = ( + ob: object, + key: string, + val: any, + config: { writable?: boolean; configurable?: boolean; enumerable?: boolean } = + {}, +): void => { const { writable = false, configurable = false, enumerable = false, - } = config + } = config; Object.defineProperty(ob, key, { value: val, writable, configurable, enumerable, - }) -} + }); +}; diff --git a/utils/detection.ts b/utils/detection.ts new file mode 100755 index 0000000..42b0e7c --- /dev/null +++ b/utils/detection.ts @@ -0,0 +1,69 @@ +// utils / detection + +const ob2Str = (val: any): string => { + return {}.toString.call(val); +}; + +export const isNumber = (val: any): boolean => { + return Number(val) === val; +}; + +export const isInteger = (val: any): boolean => { + return Number.isInteger(val); +}; + +export const isArray = (val: any): boolean => { + return Array.isArray(val); +}; + +export const isString = (val: any): boolean => { + return String(val) === val; +}; + +export const isBoolean = (val: any): boolean => { + return Boolean(val) === val; +}; + +export const isNull = (val: any): boolean => { + return ob2Str(val) === "[object Null]"; +}; + +export const isUndefined = (val: any): boolean => { + return ob2Str(val) === "[object Undefined]"; +}; + +export const isNil = (val: any): boolean => { + return isUndefined(val) || isNull(val); +}; + +export const isFunction = (val: any): boolean => { + return ob2Str(val) === "[object Function]"; +}; + +export const isObject = (val: any): boolean => { + return ob2Str(val) === "[object Object]" && !isArray(val); +}; + +export const isDate = (val: any): boolean => { + return val instanceof Date && !isNaN(val.valueOf()); +}; + +export const isEmail = (val: any): boolean => { + const re = + /^([\w-]+(?:\.[\w-]+)*)@((?:[\w-]+\.)*\w[\w-]{0,66})\.([a-z]{2,6}(?:\.[a-z]{2})?)$/i; + return isString(val) && re.test(val); +}; + +export const isEmpty = (val: any): boolean => { + return !val || isNil(val) || + (isString(val) && val === "") || + (isArray(val) && val.length === 0) || + (isObject(val) && Object.keys(val).length === 0); +}; + +export const hasProperty = (ob: any, k: any): boolean => { + if (!ob || !k) { + return false; + } + return Object.prototype.hasOwnProperty.call(ob, k); +}; diff --git a/utils/maybe.ts b/utils/maybe.ts new file mode 100755 index 0000000..5465f58 --- /dev/null +++ b/utils/maybe.ts @@ -0,0 +1,47 @@ +// utils / maybe + +import { defineProp } from "./defineProp.ts"; + +export const maybe = (val: T) => { + const __val = val; + const isNil = (): boolean => { + return __val === null || __val === undefined; + }; + const value = (): T => { + return __val; + }; + const getElse = (fn: () => T): typeof maybe => { + return maybe(__val || fn()); + }; + const filter = (fn: (val: T) => boolean): typeof maybe => { + return maybe(fn(__val) === true ? __val : null); + }; + const map = (fn: (val: T) => U): typeof maybe => { + return maybe(isNil() ? null : fn(__val)); + }; + const output = Object.create({}); + Object.defineProperty(output, "__value__", { + value: __val, + enumerable: true, + }); + Object.defineProperty(output, "__type__", { + value: "Maybe", + enumerable: true, + }); + Object.defineProperty(output, "isNil", { + value: isNil, + }); + Object.defineProperty(output, "value", { + value: value, + }); + Object.defineProperty(output, "map", { + value: map, + }); + Object.defineProperty(output, "if", { + value: filter, + }); + Object.defineProperty(output, "else", { + value: getElse, + }); + return output; +}; diff --git a/utils/pipe.ts b/utils/pipe.ts new file mode 100755 index 0000000..dcbe084 --- /dev/null +++ b/utils/pipe.ts @@ -0,0 +1,5 @@ +// utils / pipe + +export const pipe = (...fns: ((arg: T) => T)[]): (arg: T) => T => { + return fns.reduce((f, g) => (x) => g(f(x))); +}; diff --git a/utils/random.ts b/utils/random.ts new file mode 100755 index 0000000..b10f26c --- /dev/null +++ b/utils/random.ts @@ -0,0 +1,23 @@ +// utils / random + +const crypto = globalThis.crypto; + +export const genid = (len: number = 32, prefix: string = ""): string => { + let s: string = prefix; + const nums: Uint32Array = new Uint32Array(len); + crypto.getRandomValues(nums); + for (let i: number = 0; i < nums.length; i++) { + const n: string = nums[i].toString(36); + const r: number = Math.random(); + const c: string = n.charAt(Math.floor(r * n.length)); + s += r > 0.3 && r < 0.7 ? c.toUpperCase() : c; + } + return s.substring(0, len); +}; + +export const randint = (min: number = 0, max: number = 1e6): number => { + const byteArray = new Uint8Array(1); + crypto.getRandomValues(byteArray); + const floatNum = Number("0." + byteArray[0].toString()); + return Math.floor(floatNum * (max - min + 1)) + min; +}; diff --git a/utils/string.ts b/utils/string.ts new file mode 100755 index 0000000..45178ed --- /dev/null +++ b/utils/string.ts @@ -0,0 +1,115 @@ +// utils / string + +import { hasProperty, isArray, isNumber, isString } from "./detection.ts"; + +const toString = (input: any): string => { + return !isString(input) ? String(input) : input; +}; + +export const truncate = (s: string, len: number = 140): string => { + const txt = toString(s); + const txtlen = txt.length; + if (txtlen <= len) { + return txt; + } + const subtxt = txt.substring(0, len).trim(); + const subtxtArr = subtxt.split(" "); + const subtxtLen = subtxtArr.length; + if (subtxtLen > 1) { + subtxtArr.pop(); + return subtxtArr.map((word: string) => word.trim()).join(" ") + "..."; + } + return subtxt.substring(0, len - 3) + "..."; +}; + +export const stripTags = (s: string): string => { + return toString(s).replace(/(<([^>]+)>)/ig, "").trim(); +}; + +export const escapeHTML = (s: string): string => { + return toString(s) + .replace(/&/g, "&") + .replace(//g, ">") + .replace(/"/g, """); +}; + +export const unescapeHTML = (s: string): string => { + return toString(s) + .replace(/"/g, '"') + .replace(/</g, "<") + .replace(/>/g, ">") + .replace(/&/g, "&"); +}; + +export const ucfirst = (s: string): string => { + const x = toString(s).toLowerCase(); + return x.length > 1 + ? x.charAt(0).toUpperCase() + x.slice(1) + : x.toUpperCase(); +}; + +export const ucwords = (s: string): string => { + return toString(s).split(" ").map((w: string) => { + return ucfirst(w); + }).join(" "); +}; + +export const replaceAll = (s: string, a: string, b: string): string => { + return toString(s).replaceAll(a, b) +}; + +const getCharMap = (): { [key: string]: string } => { + const lmap: { [key: string]: string } = { + a: "á|à|ả|ã|ạ|ă|ắ|ặ|ằ|ẳ|ẵ|â|ấ|ầ|ẩ|ẫ|ậ|ä|æ", + c: "ç", + d: "đ|ð", + e: "é|è|ẻ|ẽ|ẹ|ê|ế|ề|ể|ễ|ệ|ë", + i: "í|ì|ỉ|ĩ|ị|ï|î", + n: "ñ", + o: "ó|ò|ỏ|õ|ọ|ô|ố|ồ|ổ|ỗ|ộ|ơ|ớ|ờ|ở|ỡ|ợ|ö|ø", + s: "ß", + u: "ú|ù|ủ|ũ|ụ|ư|ứ|ừ|ử|ữ|ự|û", + y: "ý|ỳ|ỷ|ỹ|ỵ|ÿ", + }; + + const map: { [key: string]: string } = { + ...lmap, + }; + Object.keys(lmap).forEach((k) => { + const K = k.toUpperCase(); + map[K] = lmap[k].toUpperCase(); + }); + + return map; +}; + +export const stripAccent = (s: string): string => { + let x = toString(s); + + const updateS = (ai: string, key: string) => { + x = replaceAll(x, ai, key); + }; + + const map = getCharMap(); + for (const key in map) { + if (hasProperty(map, key)) { + const a = map[key].split("|"); + a.forEach((item) => { + return updateS(item, key); + }); + } + } + return x; +}; + +export const slugify = (s: string, delimiter: string = "-"): string => { + return stripAccent(s) + .normalize("NFKD") + .replace(/[\u0300-\u036f]/g, "") + .trim() + .toLowerCase() + .replace(/[^a-z0-9 -]/g, "") + .replace(/\s+/g, delimiter) + .replace(new RegExp(`${delimiter}+`, "g"), delimiter); +};