diff --git a/components.d.ts b/components.d.ts index 3e65c3cc..6098f64d 100644 --- a/components.d.ts +++ b/components.d.ts @@ -116,6 +116,7 @@ declare module '@vue/runtime-core' { JsonViewer: typeof import('./src/tools/json-viewer/json-viewer.vue')['default'] JwtParser: typeof import('./src/tools/jwt-parser/jwt-parser.vue')['default'] KeycodeInfo: typeof import('./src/tools/keycode-info/keycode-info.vue')['default'] + ListComparer: typeof import('./src/tools/list-comparer/list-comparer.vue')['default'] ListConverter: typeof import('./src/tools/list-converter/list-converter.vue')['default'] LocaleSelector: typeof import('./src/modules/i18n/components/locale-selector.vue')['default'] LoremIpsumGenerator: typeof import('./src/tools/lorem-ipsum-generator/lorem-ipsum-generator.vue')['default'] @@ -138,11 +139,15 @@ declare module '@vue/runtime-core' { NH1: typeof import('naive-ui')['NH1'] NH3: typeof import('naive-ui')['NH3'] NIcon: typeof import('naive-ui')['NIcon'] + NInput: typeof import('naive-ui')['NInput'] + NInputNumber: typeof import('naive-ui')['NInputNumber'] NLayout: typeof import('naive-ui')['NLayout'] NLayoutSider: typeof import('naive-ui')['NLayoutSider'] NMenu: typeof import('naive-ui')['NMenu'] + NScrollbar: typeof import('naive-ui')['NScrollbar'] + NSlider: typeof import('naive-ui')['NSlider'] NSpace: typeof import('naive-ui')['NSpace'] - NTable: typeof import('naive-ui')['NTable'] + NSwitch: typeof import('naive-ui')['NSwitch'] NumeronymGenerator: typeof import('./src/tools/numeronym-generator/numeronym-generator.vue')['default'] OtpCodeGeneratorAndValidator: typeof import('./src/tools/otp-code-generator-and-validator/otp-code-generator-and-validator.vue')['default'] PasswordStrengthAnalyser: typeof import('./src/tools/password-strength-analyser/password-strength-analyser.vue')['default'] diff --git a/package.json b/package.json index 5c991cff..a0e671f5 100644 --- a/package.json +++ b/package.json @@ -44,6 +44,7 @@ "@tiptap/pm": "2.1.6", "@tiptap/starter-kit": "2.1.6", "@tiptap/vue-3": "2.0.3", + "@types/arr-diff": "^4.0.3", "@types/figlet": "^1.5.8", "@types/markdown-it": "^13.0.7", "@vicons/material": "^0.12.0", @@ -51,6 +52,7 @@ "@vueuse/core": "^10.3.0", "@vueuse/head": "^1.0.0", "@vueuse/router": "^10.0.0", + "arr-diff": "^4.0.0", "bcryptjs": "^2.4.3", "change-case": "^4.1.2", "colord": "^2.9.3", @@ -63,6 +65,7 @@ "dompurify": "^3.0.6", "email-normalizer": "^1.0.0", "emojilib": "^3.0.10", + "fast_array_intersect": "^1.1.0", "figlet": "^1.7.0", "figue": "^1.2.0", "fuse.js": "^6.6.2", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3798ae17..09708a5c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -29,6 +29,9 @@ dependencies: '@tiptap/vue-3': specifier: 2.0.3 version: 2.0.3(@tiptap/core@2.1.12)(@tiptap/pm@2.1.6)(vue@3.3.4) + '@types/arr-diff': + specifier: ^4.0.3 + version: 4.0.3 '@types/figlet': specifier: ^1.5.8 version: 1.5.8 @@ -50,6 +53,9 @@ dependencies: '@vueuse/router': specifier: ^10.0.0 version: 10.0.0(vue-router@4.1.6)(vue@3.3.4) + arr-diff: + specifier: ^4.0.0 + version: 4.0.0 bcryptjs: specifier: ^2.4.3 version: 2.4.3 @@ -86,6 +92,9 @@ dependencies: emojilib: specifier: ^3.0.10 version: 3.0.10 + fast_array_intersect: + specifier: ^1.1.0 + version: 1.1.0 figlet: specifier: ^1.7.0 version: 1.7.0 @@ -2933,6 +2942,10 @@ packages: resolution: {integrity: sha512-yhxwIlFVSVcMym3O31HoMnRXpoenmpIxcj4Yoes2DUpe+xCJnA7ECQP1Vw889V0jTt/2nzvpLQ/UuMYCd3JPIg==} dev: true + /@types/arr-diff@4.0.3: + resolution: {integrity: sha512-oIBe7qtc48Q1JPNuqSIUYCNMQxYzTkQAEw07b0NEZpwKbUeOswzex3qfvXCLrZDIQ9t3ucIxH6JqFmjRTPbDEg==} + dev: false + /@types/bcryptjs@2.4.2: resolution: {integrity: sha512-LiMQ6EOPob/4yUL66SZzu6Yh77cbzJFYll+ZfaPiPPFswtIlA/Fs1MzdKYA7JApHU49zQTbJGX3PDmCpIdDBRQ==} dev: true @@ -3412,7 +3425,7 @@ packages: dependencies: '@unhead/dom': 0.5.1 '@unhead/schema': 0.5.1 - '@vueuse/shared': 11.0.3(vue@3.3.4) + '@vueuse/shared': 11.1.0(vue@3.3.4) unhead: 0.5.1 vue: 3.3.4 transitivePeerDependencies: @@ -4054,8 +4067,8 @@ packages: - vue dev: false - /@vueuse/shared@11.0.3(vue@3.3.4): - resolution: {integrity: sha512-0rY2m6HS5t27n/Vp5cTDsKTlNnimCqsbh/fmT2LgE+aaU42EMfXo8+bNX91W9I7DDmxfuACXMmrd7d79JxkqWA==} + /@vueuse/shared@11.1.0(vue@3.3.4): + resolution: {integrity: sha512-YUtIpY122q7osj+zsNMFAfMTubGz0sn5QzE5gPzAIiCmtt2ha3uQUY1+JPyL4gRCTsLPX82Y9brNbo/aqlA91w==} dependencies: vue-demi: 0.14.10(vue@3.3.4) transitivePeerDependencies: @@ -4171,6 +4184,11 @@ packages: /argparse@2.0.1: resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + /arr-diff@4.0.0: + resolution: {integrity: sha512-YVIQ82gZPGBebQV/a8dar4AitzCQs0jjXwMPZllpXMaGjXPYVUawSxQrRsjhjupyVxEvbHgUmIhKVlND+j02kA==} + engines: {node: '>=0.10.0'} + dev: false + /array-buffer-byte-length@1.0.0: resolution: {integrity: sha512-LPuwb2P+NrQw3XhxGc36+XSvuBPopovXYTR9Ew++Du9Yb/bx5AzBfrIsBoj0EZUifjQU+sHL21sseZ3jerWO/A==} dependencies: @@ -5653,6 +5671,10 @@ packages: resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} dev: true + /fast_array_intersect@1.1.0: + resolution: {integrity: sha512-/DCilZlUdz2XyNDF+ASs0PwY+RKG9Y4Silp/gbS72Cvbg4oibc778xcecg+pnNyiNHYgh/TApsiDTjpdniyShw==} + dev: false + /fastq@1.15.0: resolution: {integrity: sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw==} dependencies: diff --git a/src/tools/index.ts b/src/tools/index.ts index 388cfaf4..8edc6c48 100644 --- a/src/tools/index.ts +++ b/src/tools/index.ts @@ -29,6 +29,7 @@ import { tool as tomlToJson } from './toml-to-json'; import { tool as jsonToCsv } from './json-to-csv'; import { tool as cameraRecorder } from './camera-recorder'; import { tool as listConverter } from './list-converter'; +import { tool as listComparer } from './list-comparer'; import { tool as phoneParserAndFormatter } from './phone-parser-and-formatter'; import { tool as jsonDiff } from './json-diff'; import { tool as ipv4RangeExpander } from './ipv4-range-expander'; @@ -111,6 +112,7 @@ export const toolsByCategory: ToolCategory[] = [ jsonToYaml, jsonToToml, listConverter, + listComparer, tomlToJson, tomlToYaml, xmlToJson, diff --git a/src/tools/list-comparer/index.ts b/src/tools/list-comparer/index.ts new file mode 100644 index 00000000..88262c14 --- /dev/null +++ b/src/tools/list-comparer/index.ts @@ -0,0 +1,12 @@ +import { List } from '@vicons/tabler'; +import { defineTool } from '../tool'; + +export const tool = defineTool({ + name: 'Lists Comparer', + path: '/list-comparer', + description: 'Compare two list items', + keywords: ['list', 'comparer'], + component: () => import('./list-comparer.vue'), + icon: List, + createdAt: new Date('2024-08-15'), +}); diff --git a/src/tools/list-comparer/list-comparer.service.test.ts b/src/tools/list-comparer/list-comparer.service.test.ts new file mode 100644 index 00000000..98affcf7 --- /dev/null +++ b/src/tools/list-comparer/list-comparer.service.test.ts @@ -0,0 +1,97 @@ +import { describe, expect, it } from 'vitest'; +import { compareLists } from './list-comparer.service'; + +describe('list-comparer', () => { + describe('compareLists', () => { + it('return correct comparaison', () => { + expect(compareLists({ + list1: '1\n 2\n3\n4\n5\n4\n7\nA', + list2: '1\n2\n3\n4\n6\n4\n7\na', + trimItems: true, + ignoreCase: true, + })).to.deep.eq({ + list1Not2: [ + '5', + ], + list2Not1: [ + '6', + ], + same: [ + '1', + '2', + '3', + '4', + '7', + 'a', + ], + }); + + expect(compareLists({ + list1: '1\n 2\n3\n4\n5\n4\n7\nA', + list2: '1\n2\n3\n4\n6\n4\n7\na', + trimItems: false, + ignoreCase: false, + })).to.deep.eq({ + list1Not2: [ + ' 2', + '5', + 'A', + ], + list2Not1: [ + '2', + '6', + 'a', + ], + same: [ + '1', + '3', + '4', + '7', + ], + }); + + expect(compareLists({ + list1: '1, 2,3,4,5\n4,7,A,A', + list2: '1\n2\n3\n4\n6\n4\n7\na', + trimItems: false, + ignoreCase: false, + separator: ',', + })).to.deep.eq({ + list1Not2: [ + ' 2', + '5', + 'A', + ], + list2Not1: [ + '2', + '6', + 'a', + ], + same: [ + '1', + '3', + '4', + '7', + ], + }); + + expect(compareLists({ + list1: '10\n20\n20\n30', + list2: '30\n20\n40', + trimItems: false, + ignoreCase: false, + })).to.deep.eq({ + list1Not2: [ + '10', + ], + list2Not1: [ + '40', + ], + same: [ + '30', + '20', + ], + }); + }); + }); +}); diff --git a/src/tools/list-comparer/list-comparer.service.ts b/src/tools/list-comparer/list-comparer.service.ts new file mode 100644 index 00000000..c0ca4a45 --- /dev/null +++ b/src/tools/list-comparer/list-comparer.service.ts @@ -0,0 +1,36 @@ +import _ from 'lodash'; +import intersect from 'fast_array_intersect'; +import diff from 'arr-diff'; + +export function compareLists({ + list1, + list2, + ignoreCase = false, + trimItems = true, + separator = '', +}: { + list1: string + list2: string + separator?: string + ignoreCase?: boolean + trimItems?: boolean +}) { + const splitSep = separator ? `${separator}|` : ''; + const splitRegExp = new RegExp(`(?:${splitSep}\\n)`, 'g'); + + const prepareList = (list: string) => + _.chain(list ?? '') + .thru(text => ignoreCase ? text.toLowerCase() : text) + .split(splitRegExp) + .map(text => trimItems ? text.trim() : text) + .value(); + + const list1Arr = prepareList(list1); + const list2Arr = prepareList(list2); + + return { + same: [...new Set(intersect([list1Arr, list2Arr]))], + list2Not1: [...new Set(diff(list2Arr, list1Arr))], + list1Not2: [...new Set(diff(list1Arr, list2Arr))], + }; +} diff --git a/src/tools/list-comparer/list-comparer.vue b/src/tools/list-comparer/list-comparer.vue new file mode 100644 index 00000000..94caa5a4 --- /dev/null +++ b/src/tools/list-comparer/list-comparer.vue @@ -0,0 +1,82 @@ + + +