Browse Source

chore: initial commit

e22m4u 7 months ago
commit
782b0aaaca

+ 9 - 0
.c8rc

@@ -0,0 +1,9 @@
+{
+  "all": true,
+  "include": [
+    "src/**/*.js"
+  ],
+  "exclude": [
+    "src/**/*.spec.js"
+  ]
+}

+ 5 - 0
.commitlintrc

@@ -0,0 +1,5 @@
+{
+  "extends": [
+    "@commitlint/config-conventional"
+  ]
+}

+ 13 - 0
.editorconfig

@@ -0,0 +1,13 @@
+# EditorConfig is awesome: https://EditorConfig.org
+
+# top-most EditorConfig file
+root = true
+
+# Unix-style newlines with a newline ending every file
+[*]
+end_of_line = lf
+insert_final_newline = true
+charset = utf-8
+indent_style = space
+indent_size = 2
+max_line_length = 80

+ 18 - 0
.gitignore

@@ -0,0 +1,18 @@
+# OS
+.DS_Store
+
+# Editor
+.idea
+.vscode
+
+# Npm
+node_modules
+npm-debug.log
+package-lock.json
+
+# Yarn
+.yarn/
+.yarn*
+
+# c8
+coverage

+ 1 - 0
.husky/commit-msg

@@ -0,0 +1 @@
+npx --no -- commitlint --edit $1

+ 6 - 0
.husky/pre-commit

@@ -0,0 +1,6 @@
+npm run lint:fix
+npm run format
+npm run test
+npm run build:cjs
+
+git add -A

+ 4 - 0
.mocharc.cjs

@@ -0,0 +1,4 @@
+module.exports = {
+  extension: ['js'],
+  spec: 'src/**/*.spec.js',
+}

+ 7 - 0
.prettierrc

@@ -0,0 +1,7 @@
+{
+  "bracketSpacing": false,
+  "singleQuote": true,
+  "printWidth": 80,
+  "trailingComma": "all",
+  "arrowParens": "avoid"
+}

+ 21 - 0
LICENSE

@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2023 e22m4u@yandex.ru
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.

+ 125 - 0
README.md

@@ -0,0 +1,125 @@
+## @e22m4u/js-empty-values
+
+Сервис определения пустых значений для JavaScript.
+
+## Установка
+
+```bash
+npm install @e22m4u/js-empty-values
+```
+
+Модуль поддерживает ESM и CommonJS стандарты.
+
+*ESM*
+
+```js
+import {EmptyValuesDefiner} from '@e22m4u/js-empty-values';
+```
+
+*CommonJS*
+
+```js
+const {EmptyValuesDefiner} = require('@e22m4u/js-empty-values');
+```
+
+## Описание
+
+Модуль позволяет задать набор *пустых значений* для каждого типа данных,
+и проверять наличие значения в данном наборе. Ниже приводится список
+предустановленных *пустых значений* для каждого типа, которые можно
+при необходимости изменить.
+
+| константа          | тип         | пустые значения           |
+|--------------------|-------------|---------------------------|
+| `DataType.ANY`     | `'any'`     | `undefined`, `null`       |
+| `DataType.STRING`  | `'string'`  | `undefined`, `null`, `''` |
+| `DataType.NUMBER`  | `'number'`  | `undefined`, `null`, `0`  |
+| `DataType.BOOLEAN` | `'boolean'` | `undefined`, `null`       |
+| `DataType.ARRAY`   | `'array'`   | `undefined`, `null`, `[]` |
+| `DataType.OBJECT`  | `'object'`  | `undefined`, `null`, `{}` |
+
+В первой колонке указаны константы каждого типа, которые могут быть
+использованы вместо названия типа в виде строки (вторая колонка).
+
+## Использование
+
+Модуль экспортирует класс-сервис `EmptyValuesService`, и перед тем
+как использовать его интерфейс, требуется создать экземпляр данного класса.
+
+```js
+import {EmptyValuesService} from '@e22m4u/js-empty-values';
+
+const emptyValues = new EmptyValuesService();
+```
+
+### Проверка полезной нагрузки
+
+Для проверки полезной нагрузки используется метод `isEmpty`, принимающий
+проверяемое значение первым аргументом. Ниже приводится пример проверки
+значений разных типов используя стандартные параметры.
+
+```js
+emptyValues.isEmpty('');           // true
+emptyValues.isEmpty(0);            // true
+emptyValues.isEmpty([]);           // true
+emptyValues.isEmpty({});           // true
+emptyValues.isEmpty(null);         // true
+emptyValues.isEmpty(undefined);    // true
+
+emptyValues.isEmpty('myString');   // false
+emptyValues.isEmpty(10);           // false
+emptyValues.isEmpty(true);         // false
+emptyValues.isEmpty(false);        // false
+emptyValues.isEmpty([1, 2, 3]);    // false
+emptyValues.isEmpty({foo: 'bar'}); // false
+```
+
+Метод `isEmpty` автоматически определяет тип проверяемого значения
+и выполняет поиск в соответствующем наборе *пустых значений*. Стоит
+отметить, что `undefined` и `null` определяются как тип `any`,
+для которого есть собственный набор значений без полезной нагрузки
+(см. [описание](#описание)).
+
+### Проверка с указанием типа
+
+Проверку полезной нагрузки можно ограничить по набору пустых значений
+в рамках определенного типа, указав нужный тип первым, а проверяемое
+значение вторым аргументом метода `isEmptyByType`.
+
+```js
+emptyValues.isEmptyByType(DataType.STRING, '');        // true
+emptyValues.isEmptyByType(DataType.STRING, 0);         // false
+emptyValues.isEmptyByType(DataType.STRING, []);        // false
+emptyValues.isEmptyByType(DataType.STRING, {});        // false
+emptyValues.isEmptyByType(DataType.STRING, null);      // true
+emptyValues.isEmptyByType(DataType.STRING, undefined); // true
+```
+
+Так стандартный набор *пустых значений* для типа `DataType.STRING` содержит
+только `undefined`, `null` и `''`, остальные значения не являются пустыми в данном
+контексте.
+
+### Изменение набора пустых значений
+
+Метод `setEmptyValuesOf` позволяет задать набор пустых значений
+для определенного типа, передав первым аргументом тип, а вторым
+массив значений без полезной нагрузки.
+
+```js
+emptyValues.setEmptyValuesOf(DataType.NUMBER, [-1, 0]);
+
+// проверка значений
+emptyValues.isEmpty(-1); // true
+emptyValues.isEmpty(0);  // true
+emptyValues.isEmpty(1);  // false
+```
+
+## Тесты
+
+```bash
+npm run test
+```
+
+## Лицензия
+
+MIT

+ 16 - 0
build-cjs.js

@@ -0,0 +1,16 @@
+import * as esbuild from 'esbuild';
+import packageJson from './package.json' with {type: 'json'};
+
+await esbuild.build({
+  entryPoints: ['src/index.js'],
+  outfile: 'dist/cjs/index.cjs',
+  format: 'cjs',
+  platform: 'node',
+  target: ['node12'],
+  bundle: true,
+  keepNames: true,
+  external: [
+    ...Object.keys(packageJson.peerDependencies || {}),
+    ...Object.keys(packageJson.dependencies || {}),
+  ],
+});

+ 26 - 0
eslint.config.js

@@ -0,0 +1,26 @@
+import globals from 'globals';
+import eslintJs from '@eslint/js';
+import eslintMochaPlugin from 'eslint-plugin-mocha';
+import eslintPrettierConfig from 'eslint-config-prettier';
+import eslintChaiExpectPlugin from 'eslint-plugin-chai-expect';
+
+export default [{
+  languageOptions: {
+    globals: {
+      ...globals.node,
+      ...globals.es2021,
+      ...globals.mocha,
+    },
+  },
+  plugins: {
+    'mocha': eslintMochaPlugin,
+    'chai-expect': eslintChaiExpectPlugin,
+  },
+  rules: {
+    ...eslintJs.configs.recommended.rules,
+    ...eslintPrettierConfig.rules,
+    ...eslintMochaPlugin.configs.flat.recommended.rules,
+    ...eslintChaiExpectPlugin.configs['recommended-flat'].rules,
+  },
+  files: ['src/**/*.js'],
+}];

+ 61 - 0
package.json

@@ -0,0 +1,61 @@
+{
+  "name": "@e22m4u/js-empty-values",
+  "version": "0.0.1",
+  "description": "Сервис определения пустых значений для JavaScript",
+  "author": "e22m4u <e22m4u@yandex.ru>",
+  "license": "MIT",
+  "keywords": [
+    "null",
+    "empty",
+    "value",
+    "undefined"
+  ],
+  "homepage": "https://github.com/e22m4u/js-empty-values",
+  "repository": {
+    "type": "git",
+    "url": "git+https://github.com/e22m4u/js-empty-values.git"
+  },
+  "type": "module",
+  "types": "./src/index.d.ts",
+  "module": "./src/index.js",
+  "main": "./dist/cjs/index.cjs",
+  "exports": {
+    "types": "./src/index.d.ts",
+    "import": "./src/index.js",
+    "require": "./dist/cjs/index.cjs"
+  },
+  "engines": {
+    "node": ">=12"
+  },
+  "scripts": {
+    "lint": "tsc && eslint ./src",
+    "lint:fix": "tsc && eslint ./src --fix",
+    "format": "prettier --write \"./src/**/*.js\"",
+    "test": "npm run lint && c8 --reporter=text-summary mocha",
+    "test:coverage": "npm run lint && c8 --reporter=text mocha",
+    "build:cjs": "rimraf ./dist/cjs && node --no-warnings=ExperimentalWarning build-cjs.js",
+    "prepare": "husky"
+  },
+  "dependencies": {
+    "@e22m4u/js-format": "~0.1.0",
+    "@e22m4u/js-service": "~0.2.0"
+  },
+  "devDependencies": {
+    "@commitlint/cli": "~19.8.0",
+    "@commitlint/config-conventional": "~19.8.0",
+    "@eslint/js": "~9.25.1",
+    "c8": "~10.1.3",
+    "chai": "~5.2.0",
+    "esbuild": "~0.25.3",
+    "eslint": "~9.25.1",
+    "eslint-config-prettier": "~10.1.2",
+    "eslint-plugin-chai-expect": "~3.1.0",
+    "eslint-plugin-mocha": "~10.5.0",
+    "globals": "~16.0.0",
+    "husky": "~9.1.7",
+    "mocha": "~11.1.0",
+    "prettier": "~3.5.3",
+    "rimraf": "~6.0.1",
+    "typescript": "~5.8.3"
+  }
+}

+ 11 - 0
src/data-type.d.ts

@@ -0,0 +1,11 @@
+/**
+ * Data type.
+ */
+export declare enum DataType {
+  ANY = 'any',
+  STRING = 'string',
+  NUMBER = 'number',
+  BOOLEAN = 'boolean',
+  ARRAY = 'array',
+  OBJECT = 'object',
+}

+ 11 - 0
src/data-type.js

@@ -0,0 +1,11 @@
+/**
+ * Data type.
+ */
+export const DataType = {
+  ANY: 'any',
+  STRING: 'string',
+  NUMBER: 'number',
+  BOOLEAN: 'boolean',
+  ARRAY: 'array',
+  OBJECT: 'object',
+};

+ 30 - 0
src/empty-values-service.d.ts

@@ -0,0 +1,30 @@
+import {DataType} from './data-type.js';
+import {Service} from '@e22m4u/js-service';
+
+/**
+ * Empty values service.
+ */
+export class EmptyValuesService extends Service {
+  /**
+   * Set empty values of.
+   *
+   * @param dataType
+   * @param emptyValues
+   */
+  setEmptyValuesOf(dataType: DataType, emptyValues: unknown[]): this;
+
+  /**
+   * Is empty.
+   *
+   * @param value
+   */
+  isEmpty(value: unknown): boolean;
+
+  /**
+   * Is empty for type.
+   *
+   * @param dataType
+   * @param value
+   */
+  isEmptyByType(dataType: DataType, value: unknown): boolean;
+}

+ 79 - 0
src/empty-values-service.js

@@ -0,0 +1,79 @@
+import {DataType} from './data-type.js';
+import {Errorf} from '@e22m4u/js-format';
+import {Service} from '@e22m4u/js-service';
+import {isDeepEqual} from './utils/index.js';
+import {getDataTypeOf} from './utils/index.js';
+
+/**
+ * Empty values service.
+ */
+export class EmptyValuesService extends Service {
+  /**
+   * Empty values map.
+   *
+   * @type {Map<string, *[]>}
+   */
+  _emptyValuesMap = new Map([
+    [DataType.ANY, [undefined, null]],
+    [DataType.STRING, [undefined, null, '']],
+    [DataType.NUMBER, [undefined, null, 0]],
+    [DataType.BOOLEAN, [undefined, null]],
+    [DataType.ARRAY, [undefined, null, []]],
+    [DataType.OBJECT, [undefined, null, {}]],
+  ]);
+
+  /**
+   * Set empty values of data type.
+   *
+   * @param {string} dataType
+   * @param {*[]} emptyValues
+   * @returns {EmptyValuesService}
+   */
+  setEmptyValuesOf(dataType, emptyValues) {
+    if (!Object.values(DataType).includes(dataType))
+      throw new Errorf(
+        'The argument "dataType" of the EmptyValuesService.setEmptyValuesOf ' +
+          'must be one of data types: %l, but %v given.',
+        Object.values(DataType),
+        dataType,
+      );
+    if (!Array.isArray(emptyValues))
+      throw new Errorf(
+        'The argument "emptyValues" of the EmptyValuesService.setEmptyValuesOf ' +
+          'must be an Array, but %v given.',
+        emptyValues,
+      );
+    this._emptyValuesMap.set(dataType, emptyValues);
+    return this;
+  }
+
+  /**
+   * Is empty.
+   *
+   * @param {*} value
+   * @returns {boolean}
+   */
+  isEmpty(value) {
+    const dataType = getDataTypeOf(value);
+    return this._emptyValuesMap.get(dataType)
+      .some(v => isDeepEqual(v, value));
+  }
+
+  /**
+   * Is empty for type.
+   *
+   * @param {string} dataType
+   * @param {*} value
+   * @returns {boolean}
+   */
+  isEmptyByType(dataType, value) {
+    if (!Object.values(DataType).includes(dataType))
+      throw new Errorf(
+        'The argument "dataType" of EmptyValuesService.isEmptyByType ' +
+          'must be one of data types: %l, but %v given.',
+        Object.values(DataType),
+        dataType,
+      );
+    return this._emptyValuesMap.get(dataType).some(v => isDeepEqual(v, value));
+  }
+}

+ 114 - 0
src/empty-values-service.spec.js

@@ -0,0 +1,114 @@
+import {expect} from 'chai';
+import {DataType} from './data-type.js';
+import {format} from '@e22m4u/js-format';
+import {EmptyValuesService} from './empty-values-service.js';
+
+const getEmptyValues = (definer, dataType) => {
+  return definer['_emptyValuesMap'].get(dataType);
+};
+
+describe('EmptyValuesService', function () {
+  describe('_emptyValuesMap', function () {
+    it('has default values', function () {
+      const S = new EmptyValuesService();
+      expect(Array.from(S['_emptyValuesMap'])).to.be.eql([
+        [DataType.ANY, [undefined, null]],
+        [DataType.STRING, [undefined, null, '']],
+        [DataType.NUMBER, [undefined, null, 0]],
+        [DataType.BOOLEAN, [undefined, null]],
+        [DataType.ARRAY, [undefined, null, []]],
+        [DataType.OBJECT, [undefined, null, {}]],
+      ]);
+    });
+  });
+
+  describe('setEmptyValuesOf', function () {
+    it('requires the parameter "dataType" to be a DataType enum', function () {
+      const S = new EmptyValuesService();
+      const throwable = v => () => S.setEmptyValuesOf(v, []);
+      const error = v =>
+        format(
+          'The argument "dataType" of the EmptyValuesService.setEmptyValuesOf ' +
+            'must be one of data types: %l, but %s given.',
+          Object.values(DataType),
+          v,
+        );
+      expect(throwable('str')).to.throw(error('"str"'));
+      expect(throwable('')).to.throw(error('""'));
+      expect(throwable(10)).to.throw(error('10'));
+      expect(throwable(0)).to.throw(error('0'));
+      expect(throwable(true)).to.throw(error('true'));
+      expect(throwable(false)).to.throw(error('false'));
+      expect(throwable(undefined)).to.throw(error('undefined'));
+      expect(throwable(null)).to.throw(error('null'));
+      expect(throwable({})).to.throw(error('Object'));
+      expect(throwable([])).to.throw(error('Array'));
+      throwable(DataType.ANY)();
+    });
+
+    it('requires the parameter "emptyValues" to be an Array', function () {
+      const S = new EmptyValuesService();
+      const throwable = v => () => S.setEmptyValuesOf(DataType.ANY, v);
+      const error = v =>
+        format(
+          'The argument "emptyValues" of the EmptyValuesService.setEmptyValuesOf ' +
+            'must be an Array, but %s given.',
+          v,
+        );
+      expect(throwable('str')).to.throw(error('"str"'));
+      expect(throwable('')).to.throw(error('""'));
+      expect(throwable(10)).to.throw(error('10'));
+      expect(throwable(0)).to.throw(error('0'));
+      expect(throwable(true)).to.throw(error('true'));
+      expect(throwable(false)).to.throw(error('false'));
+      expect(throwable(undefined)).to.throw(error('undefined'));
+      expect(throwable(null)).to.throw(error('null'));
+      expect(throwable({})).to.throw(error('Object'));
+      throwable([])();
+      throwable([1, 2, 3])();
+    });
+
+    it('overrides default values of the given data type', function () {
+      const S = new EmptyValuesService();
+      expect(getEmptyValues(S, DataType.ANY)).eql([undefined, null]);
+      S.setEmptyValuesOf(DataType.ANY, [1, 2, 3]);
+      expect(getEmptyValues(S, DataType.ANY)).eql([1, 2, 3]);
+    });
+  });
+
+  describe('isEmpty', function () {
+    it('checks if the given value is empty by its type', function () {
+      const S = new EmptyValuesService();
+      S.setEmptyValuesOf(DataType.ANY, [null]);
+      expect(S.isEmpty(null)).to.be.true;
+      expect(S.isEmpty(undefined)).to.be.false;
+      S.setEmptyValuesOf(DataType.STRING, ['10']);
+      expect(S.isEmpty('10')).to.be.true;
+      expect(S.isEmpty('11')).to.be.false;
+      S.setEmptyValuesOf(DataType.NUMBER, [10]);
+      expect(S.isEmpty(10)).to.be.true;
+      expect(S.isEmpty(11)).to.be.false;
+      S.setEmptyValuesOf(DataType.BOOLEAN, [true]);
+      expect(S.isEmpty(true)).to.be.true;
+      expect(S.isEmpty(false)).to.be.false;
+      S.setEmptyValuesOf(DataType.ARRAY, [[1, 2, 3]]);
+      expect(S.isEmpty([1, 2, 3])).to.be.true;
+      expect(S.isEmpty([3, 2, 1])).to.be.false;
+      S.setEmptyValuesOf(DataType.OBJECT, [{foo: 'bar'}]);
+      expect(S.isEmpty({foo: 'bar'})).to.be.true;
+      expect(S.isEmpty({bar: 'foo'})).to.be.false;
+    });
+  });
+
+  describe('isEmptyByType', function () {
+    it('returns true if the given value exists in the given type', function () {
+      const S = new EmptyValuesService();
+      S.setEmptyValuesOf(DataType.ANY, []);
+      expect(S.isEmptyByType(DataType.ANY, 'foo')).to.be.false;
+      S.setEmptyValuesOf(DataType.ANY, ['bar']);
+      expect(S.isEmptyByType(DataType.ANY, 'foo')).to.be.false;
+      S.setEmptyValuesOf(DataType.ANY, ['bar', 'foo']);
+      expect(S.isEmptyByType(DataType.ANY, 'foo')).to.be.true;
+    });
+  });
+});

+ 1 - 0
src/index.d.ts

@@ -0,0 +1 @@
+export * from './empty-values-service.js';

+ 1 - 0
src/index.js

@@ -0,0 +1 @@
+export * from './empty-values-service.js';

+ 8 - 0
src/utils/get-data-type-of.d.ts

@@ -0,0 +1,8 @@
+import {DataType} from '../data-type.js';
+
+/**
+ * Get data type of.
+ *
+ * @param value
+ */
+export declare function getDataTypeOf(value: unknown): DataType;

+ 22 - 0
src/utils/get-data-type-of.js

@@ -0,0 +1,22 @@
+import {DataType} from '../data-type.js';
+
+/**
+ * Get data type of.
+ *
+ * @param {*} value
+ * @returns {string}
+ */
+export function getDataTypeOf(value) {
+  if (typeof value === 'string') {
+    return DataType.STRING;
+  } else if (typeof value === 'number') {
+    return DataType.NUMBER;
+  } else if (typeof value === 'boolean') {
+    return DataType.BOOLEAN;
+  } else if (Array.isArray(value)) {
+    return DataType.ARRAY;
+  } else if (typeof value === 'object' && value !== null) {
+    return DataType.OBJECT;
+  }
+  return DataType.ANY;
+}

+ 54 - 0
src/utils/get-data-type-of.spec.js

@@ -0,0 +1,54 @@
+import {expect} from 'chai';
+import {DataType} from '../data-type.js';
+import {getDataTypeOf} from './get-data-type-of.js';
+
+describe('getDataTypeOf', function () {
+  it('returns DataType.ANY for undefined and null values', function () {
+    const res1 = getDataTypeOf(undefined);
+    const res2 = getDataTypeOf(null);
+    expect(res1).to.be.eq(DataType.ANY);
+    expect(res2).to.be.eq(DataType.ANY);
+  });
+
+  it('returns DataType.STRING for a string value', function () {
+    const res1 = getDataTypeOf('value');
+    const res2 = getDataTypeOf('');
+    expect(res1).to.be.eq(DataType.STRING);
+    expect(res2).to.be.eq(DataType.STRING);
+  });
+
+  it('returns DataType.NUMBER for an integer value', function () {
+    const res1 = getDataTypeOf(10);
+    const res2 = getDataTypeOf(-10);
+    expect(res1).to.be.eq(DataType.NUMBER);
+    expect(res2).to.be.eq(DataType.NUMBER);
+  });
+
+  it('returns DataType.NUMBER for a float value', function () {
+    const res1 = getDataTypeOf(10.5);
+    const res2 = getDataTypeOf(-10.5);
+    expect(res1).to.be.eq(DataType.NUMBER);
+    expect(res2).to.be.eq(DataType.NUMBER);
+  });
+
+  it('returns DataType.BOOLEAN for a boolean value', function () {
+    const res1 = getDataTypeOf(true);
+    const res2 = getDataTypeOf(false);
+    expect(res1).to.be.eq(DataType.BOOLEAN);
+    expect(res2).to.be.eq(DataType.BOOLEAN);
+  });
+
+  it('returns DataType.ARRAY for an array value', function () {
+    const res1 = getDataTypeOf([1, 2, 3]);
+    const res2 = getDataTypeOf([]);
+    expect(res1).to.be.eq(DataType.ARRAY);
+    expect(res2).to.be.eq(DataType.ARRAY);
+  });
+
+  it('returns DataType.OBJECT for an object value', function () {
+    const res1 = getDataTypeOf({foo: 'bar'});
+    const res2 = getDataTypeOf({});
+    expect(res1).to.be.eq(DataType.OBJECT);
+    expect(res2).to.be.eq(DataType.OBJECT);
+  });
+});

+ 2 - 0
src/utils/index.d.ts

@@ -0,0 +1,2 @@
+export * from './is-deep-equal.js';
+export * from './get-data-type-of.js';

+ 2 - 0
src/utils/index.js

@@ -0,0 +1,2 @@
+export * from './is-deep-equal.js';
+export * from './get-data-type-of.js';

+ 10 - 0
src/utils/is-deep-equal.d.ts

@@ -0,0 +1,10 @@
+/**
+ * Is deep equal.
+ *
+ * @param firstValue
+ * @param secondValue
+ */
+export declare function isDeepEqual(
+  firstValue: unknown,
+  secondValue: unknown,
+): boolean;

+ 71 - 0
src/utils/is-deep-equal.js

@@ -0,0 +1,71 @@
+/**
+ * Is deep equal.
+ * https://github.com/pinglu85/BFEdevSolutions/blob/main/Coding-Problems/69.implement-deep-equal-isEqual.md
+ *
+ * @param {*} firstValue
+ * @param {*} secondValue
+ * @returns {boolean}
+ */
+export function isDeepEqual(firstValue, secondValue) {
+  const cached = new WeakMap();
+  const compare = (a, b) => {
+    // Check if one of the two inputs is primitive by using typeof
+    // operator; since the typeof primitive null is object, check
+    // if one of the inputs is equal to null. If one of the two
+    // inputs is primitive, then I can compare them by reference.
+    if (a === null || b === null) return a === b;
+    if (typeof a !== 'object' || typeof b !== 'object') return a === b;
+    // Check if the data type of the two inputs are the same,
+    // both are arrays or objects. If they are not, return false.
+    const dataTypeA = Array.isArray(a) ? 'array' : 'object';
+    const dataTypeB = Array.isArray(b) ? 'array' : 'object';
+    if (dataTypeA !== dataTypeB) return false;
+    // Use Object.keys and Object.getOwnPropertySymbols to get
+    // all of enumerable and not-inherited properties of the two
+    // inputs. Compare their size respectively, if one of them
+    // is not equal, return false.
+    const keysA = Object.keys(a);
+    const keysB = Object.keys(b);
+    if (keysA.length !== keysB.length) return false;
+    const symbolsA = Object.getOwnPropertySymbols(a);
+    const symbolsB = Object.getOwnPropertySymbols(b);
+    if (symbolsA.length !== symbolsB.length) return false;
+    // To handle the circular reference, initialize a WeakMap
+    // that is going to keep track of the objects or arrays
+    // that have been seen, in which each key is an object
+    // or an array and each value is a set of objects or arrays,
+    // that have been compared to that object or array.
+    let setForA = cached.get(a);
+    if (setForA == null) {
+      setForA = new Set();
+      cached.set(a, setForA);
+    } else if (setForA.has(b)) {
+      return true;
+    }
+    setForA.add(b);
+    let setForB = cached.get(b);
+    if (setForB == null) {
+      setForB = new Set();
+      cached.set(b, setForB);
+    } else if (setForB.has(a)) {
+      return true;
+    }
+    setForB.add(a);
+    // Compare the property names and the values. Loop through
+    // all the properties of the first input data, check if
+    // the property name also exist in the second input data,
+    // if not, return false; otherwise recursively compare
+    // the property value.
+    const propertyNamesA = [...keysA, ...symbolsA];
+    for (const propertyNameA of propertyNamesA) {
+      if (!Object.prototype.hasOwnProperty.call(b, propertyNameA)) return false;
+      const propertyValueA = a[propertyNameA];
+      const propertyValueB = b[propertyNameA];
+      if (!compare(propertyValueA, propertyValueB)) return false;
+    }
+    // If we get out of the loop without
+    // returning false, return true.
+    return true;
+  };
+  return compare(firstValue, secondValue);
+}

+ 238 - 0
src/utils/is-deep-equal.spec.js

@@ -0,0 +1,238 @@
+import {expect} from 'chai';
+import {isDeepEqual} from './is-deep-equal.js';
+
+const check = (a, b, expected) => {
+  expect(isDeepEqual(a, b)).to.be.eq(expected);
+  expect(isDeepEqual(b, a)).to.be.eq(expected);
+};
+
+describe('isDeepEqual', function () {
+  describe('string', function () {
+    it('a non-empty string', function () {
+      check('str', 'str', true);
+      check('str', '', false);
+      check('str', 10, false);
+      check('str', 0, false);
+      check('str', -10, false);
+      check('str', true, false);
+      check('str', false, false);
+      check('str', undefined, false);
+      check('str', null, false);
+      check('str', {foo: 'bar'}, false);
+      check('str', {}, false);
+      check('str', [1, 2, 3], false);
+      check('str', [], false);
+    });
+
+    it('an empty string', function () {
+      check('', 'str', false);
+      check('', '', true);
+      check('', 10, false);
+      check('', 0, false);
+      check('', -10, false);
+      check('', true, false);
+      check('', false, false);
+      check('', undefined, false);
+      check('', null, false);
+      check('', {foo: 'bar'}, false);
+      check('', {}, false);
+      check('', [1, 2, 3], false);
+      check('', [], false);
+    });
+  });
+
+  describe('number', function () {
+    it('a positive number', function () {
+      check(10, 'str', false);
+      check(10, '', false);
+      check(10, 10, true);
+      check(10, 0, false);
+      check(10, -10, false);
+      check(10, true, false);
+      check(10, false, false);
+      check(10, undefined, false);
+      check(10, null, false);
+      check(10, {foo: 'bar'}, false);
+      check(10, {}, false);
+      check(10, [1, 2, 3], false);
+      check(10, [], false);
+    });
+
+    it('zero', function () {
+      check(0, 'str', false);
+      check(0, '', false);
+      check(0, 10, false);
+      check(0, 0, true);
+      check(0, -10, false);
+      check(0, true, false);
+      check(0, false, false);
+      check(0, undefined, false);
+      check(0, null, false);
+      check(0, {foo: 'bar'}, false);
+      check(0, {}, false);
+      check(0, [1, 2, 3], false);
+      check(0, [], false);
+    });
+
+    it('a negative number', function () {
+      check(-10, 'str', false);
+      check(-10, '', false);
+      check(-10, 10, false);
+      check(-10, 0, false);
+      check(-10, -10, true);
+      check(-10, true, false);
+      check(-10, false, false);
+      check(-10, undefined, false);
+      check(-10, null, false);
+      check(-10, {foo: 'bar'}, false);
+      check(-10, {}, false);
+      check(-10, [1, 2, 3], false);
+      check(-10, [], false);
+    });
+  });
+
+  describe('boolean', function () {
+    it('true', function () {
+      check(true, 'str', false);
+      check(true, '', false);
+      check(true, 10, false);
+      check(true, 0, false);
+      check(true, -10, false);
+      check(true, true, true);
+      check(true, false, false);
+      check(true, undefined, false);
+      check(true, null, false);
+      check(true, {foo: 'bar'}, false);
+      check(true, {}, false);
+      check(true, [1, 2, 3], false);
+      check(true, [], false);
+    });
+
+    it('false', function () {
+      check(false, 'str', false);
+      check(false, '', false);
+      check(false, 10, false);
+      check(false, 0, false);
+      check(false, -10, false);
+      check(false, true, false);
+      check(false, false, true);
+      check(false, undefined, false);
+      check(false, null, false);
+      check(false, {foo: 'bar'}, false);
+      check(false, {}, false);
+      check(false, [1, 2, 3], false);
+      check(false, [], false);
+    });
+  });
+
+  describe('array', function () {
+    it('an array of numbers', function () {
+      check([1, 2, 3], 'str', false);
+      check([1, 2, 3], '', false);
+      check([1, 2, 3], 10, false);
+      check([1, 2, 3], 0, false);
+      check([1, 2, 3], -10, false);
+      check([1, 2, 3], true, false);
+      check([1, 2, 3], false, false);
+      check([1, 2, 3], undefined, false);
+      check([1, 2, 3], null, false);
+      check([1, 2, 3], {foo: 'bar'}, false);
+      check([1, 2, 3], {}, false);
+      check([1, 2, 3], [1, 2, 3], true);
+      check([1, 2, 3], [], false);
+    });
+
+    it('an empty array', function () {
+      check([], 'str', false);
+      check([], '', false);
+      check([], 10, false);
+      check([], 0, false);
+      check([], -10, false);
+      check([], true, false);
+      check([], false, false);
+      check([], undefined, false);
+      check([], null, false);
+      check([], {foo: 'bar'}, false);
+      check([], {}, false);
+      check([], [1, 2, 3], false);
+      check([], [], true);
+    });
+  });
+
+  describe('object', function () {
+    it('string key and string value', function () {
+      check({foo: 'bar'}, 'str', false);
+      check({foo: 'bar'}, '', false);
+      check({foo: 'bar'}, 10, false);
+      check({foo: 'bar'}, 0, false);
+      check({foo: 'bar'}, -10, false);
+      check({foo: 'bar'}, true, false);
+      check({foo: 'bar'}, false, false);
+      check({foo: 'bar'}, undefined, false);
+      check({foo: 'bar'}, null, false);
+      check({foo: 'bar'}, {foo: 'bar'}, true);
+      check({foo: 'bar'}, {}, false);
+      check({foo: 'bar'}, [1, 2, 3], false);
+      check({foo: 'bar'}, [], false);
+    });
+
+    it('an empty object', function () {
+      check({}, 'str', false);
+      check({}, '', false);
+      check({}, 10, false);
+      check({}, 0, false);
+      check({}, -10, false);
+      check({}, true, false);
+      check({}, false, false);
+      check({}, undefined, false);
+      check({}, null, false);
+      check({}, {foo: 'bar'}, false);
+      check({}, {}, true);
+      check({}, [1, 2, 3], false);
+      check({}, [], false);
+    });
+
+    it('null', function () {
+      check(null, 'str', false);
+      check(null, '', false);
+      check(null, 10, false);
+      check(null, 0, false);
+      check(null, -10, false);
+      check(null, true, false);
+      check(null, false, false);
+      check(null, undefined, false);
+      check(null, null, true);
+      check(null, {foo: 'bar'}, false);
+      check(null, {}, false);
+      check(null, [1, 2, 3], false);
+      check(null, [], false);
+    });
+
+    it('circular reference to itself', function () {
+      const a = {foo: 'bar'};
+      const b = {baz: 'qux'};
+      const c = {foo: 'bar'};
+      a.itself = a;
+      b.itself = b;
+      c.itself = c;
+      check(a, b, false);
+      check(a, c, true);
+    });
+  });
+
+  it('undefined', function () {
+    check(undefined, 'str', false);
+    check(undefined, '', false);
+    check(undefined, 10, false);
+    check(undefined, 0, false);
+    check(undefined, -10, false);
+    check(undefined, true, false);
+    check(undefined, false, false);
+    check(undefined, undefined, true);
+    check(undefined, null, false);
+    check(undefined, {foo: 'bar'}, false);
+    check(undefined, {}, false);
+    check(undefined, [1, 2, 3], false);
+    check(undefined, [], false);
+  });
+});

+ 9 - 0
tsconfig.json

@@ -0,0 +1,9 @@
+{
+  "compilerOptions": {
+    "rootDir": "src",
+    "noEmit": true,
+    "target": "es2022",
+    "module": "NodeNext",
+    "moduleResolution": "NodeNext"
+  }
+}