diff --git a/.idea/workspace.xml b/.idea/workspace.xml
index 0999c13..9d2dab7 100644
--- a/.idea/workspace.xml
+++ b/.idea/workspace.xml
@@ -4,7 +4,14 @@
-
+
+
+
+
+
+
+
+
@@ -40,44 +47,44 @@
- {
+ "keyToString": {
+ "ASKED_ADD_EXTERNAL_FILES": "true",
+ "ASKED_SHARE_PROJECT_CONFIGURATION_FILES": "true",
+ "Playwright.JoinText Component.executor": "Run",
+ "Playwright.JoinText Component.should merge text pieces with specified join character.executor": "Run",
+ "RunOnceActivity.OpenProjectViewOnStart": "true",
+ "RunOnceActivity.ShowReadmeOnStart": "true",
+ "Vitest.compute function (1).executor": "Run",
+ "Vitest.compute function.executor": "Run",
+ "Vitest.mergeText.executor": "Run",
+ "Vitest.mergeText.should merge lines and preserve blank lines when deleteBlankLines is false.executor": "Run",
+ "Vitest.mergeText.should merge lines, preserve blank lines and trailing spaces when both deleteBlankLines and deleteTrailingSpaces are false.executor": "Run",
+ "git-widget-placeholder": "main",
+ "ignore.virus.scanning.warn.message": "true",
+ "kotlin-language-version-configured": "true",
+ "last_opened_file_path": "C:/Users/HP/IdeaProjects/omni-tools/src/components/options",
+ "node.js.detected.package.eslint": "true",
+ "node.js.detected.package.tslint": "true",
+ "node.js.selected.package.eslint": "(autodetect)",
+ "node.js.selected.package.tslint": "(autodetect)",
+ "nodejs_package_manager_path": "npm",
+ "npm.dev.executor": "Run",
+ "npm.lint.executor": "Run",
+ "npm.prebuild.executor": "Run",
+ "npm.script:create:tool.executor": "Run",
+ "npm.test.executor": "Run",
+ "npm.test:e2e.executor": "Run",
+ "npm.test:e2e:run.executor": "Run",
+ "prettierjs.PrettierConfiguration.Package": "C:\\Users\\HP\\IdeaProjects\\omni-tools\\node_modules\\prettier",
+ "project.structure.last.edited": "Problems",
+ "project.structure.proportion": "0.0",
+ "project.structure.side.proportion": "0.2",
+ "settings.editor.selected.configurable": "settings.typescriptcompiler",
+ "ts.external.directory.path": "C:\\Users\\HP\\IdeaProjects\\omni-tools\\node_modules\\typescript\\lib",
+ "vue.rearranger.settings.migration": "true"
}
-}]]>
+}
+
-
@@ -102,7 +109,7 @@
-
+
@@ -158,11 +165,11 @@
+
-
@@ -198,30 +205,8 @@
-
-
-
- 1719171905785
-
-
-
- 1719171905785
-
-
-
- 1719172147719
-
-
-
- 1719172147719
-
-
-
- 1719184899883
-
-
-
- 1719184899883
+
+
@@ -591,7 +576,31 @@
1719600693979
-
+
+
+ 1719600739052
+
+
+
+ 1719600739052
+
+
+
+ 1720545582958
+
+
+
+ 1720545582958
+
+
+
+ 1720546921899
+
+
+
+ 1720546921899
+
+
@@ -635,9 +644,6 @@
-
-
-
@@ -660,7 +666,10 @@
-
+
+
+
+
diff --git a/src/components/options/SelectWithDesc.tsx b/src/components/options/SelectWithDesc.tsx
new file mode 100644
index 0000000..f0a5a23
--- /dev/null
+++ b/src/components/options/SelectWithDesc.tsx
@@ -0,0 +1,50 @@
+import React from 'react';
+import {
+ Box,
+ MenuItem,
+ Select,
+ SelectChangeEvent,
+ Typography
+} from '@mui/material';
+
+interface Option {
+ label: string;
+ value: T;
+}
+
+const SelectWithDesc = ({
+ selected,
+ options,
+ onChange,
+ description
+}: {
+ selected: T;
+ options: Option[];
+ onChange: (value: T) => void;
+ description: string;
+}) => {
+ const handleChange = (event: SelectChangeEvent) => {
+ const newValue =
+ typeof selected === 'boolean'
+ ? event.target.value === 'true'
+ : event.target.value;
+ onChange(newValue as T);
+ };
+
+ return (
+
+
+
+ {description}
+
+
+ );
+};
+
+export default SelectWithDesc;
diff --git a/src/components/options/ToolOptionGroups.tsx b/src/components/options/ToolOptionGroups.tsx
index 5f1d4a6..3f98bbd 100644
--- a/src/components/options/ToolOptionGroups.tsx
+++ b/src/components/options/ToolOptionGroups.tsx
@@ -15,7 +15,7 @@ export default function ToolOptionGroups({
return (
{groups.map((group) => (
-
+
{group.title}
diff --git a/src/components/result/ToolTextResult.tsx b/src/components/result/ToolTextResult.tsx
index 2a5e596..9baf4cc 100644
--- a/src/components/result/ToolTextResult.tsx
+++ b/src/components/result/ToolTextResult.tsx
@@ -1,8 +1,9 @@
import { Box, TextField } from '@mui/material';
-import React, { useContext } from 'react';
+import React, { useContext, useEffect } from 'react';
import { CustomSnackBarContext } from '../../contexts/CustomSnackBarContext';
import InputHeader from '../InputHeader';
import ResultFooter from './ResultFooter';
+import { replaceSpecialCharacters } from '../../utils/string';
export default function ToolTextResult({
title = 'Result',
@@ -37,7 +38,7 @@ export default function ToolTextResult({
{
- it('should handle sorting alphabetically ignoring case', () => {
- const input = 'Apple,banana,apple,Orange,Banana,apple';
- const result = TopItemsList('symbol', 'alphabetic', 'count', ',', input, false, true, false);
- expect(result).toEqual(
- 'apple: 3\n' +
- 'banana: 2\n' +
- 'orange: 1'
- );
- });
+ it('should handle sorting alphabetically ignoring case', () => {
+ const input = 'Apple,banana,apple,Orange,Banana,apple';
+ const result = TopItemsList(
+ 'symbol',
+ 'alphabetic',
+ 'count',
+ ',',
+ input,
+ false,
+ true,
+ false
+ );
+ expect(result).toEqual('apple: 3\n' + 'banana: 2\n' + 'orange: 1');
+ });
- it('should handle sorting by count and not ignoring case', () => {
- const input = 'apple,banana,apple,orange,banana,apple,Banana';
- const result = TopItemsList('symbol', 'count', 'count', ',', input, false, false, false);
- expect(result).toEqual(
- 'apple: 3\n' +
- 'banana: 2\n' +
- 'orange: 1\n' +
- 'Banana: 1'
- );
- });
+ it('should handle sorting by count and not ignoring case', () => {
+ const input = 'apple,banana,apple,orange,banana,apple,Banana';
+ const result = TopItemsList(
+ 'symbol',
+ 'count',
+ 'count',
+ ',',
+ input,
+ false,
+ false,
+ false
+ );
+ expect(result).toEqual(
+ 'apple: 3\n' + 'banana: 2\n' + 'orange: 1\n' + 'Banana: 1'
+ );
+ });
- it('should handle regex split operator', () => {
- const input = 'apple123banana456apple789orange012banana345apple678';
- const result = TopItemsList('regex', 'count', 'count', '\\d+', input, false, false, false);
- expect(result).toEqual(
- 'apple: 3\n' +
- 'banana: 2\n' +
- 'orange: 1'
- );
- });
+ it('should handle regex split operator', () => {
+ const input = 'apple123banana456apple789orange012banana345apple678';
+ const result = TopItemsList(
+ 'regex',
+ 'count',
+ 'count',
+ '\\d+',
+ input,
+ false,
+ false,
+ false
+ );
+ expect(result).toEqual('apple: 3\n' + 'banana: 2\n' + 'orange: 1');
+ });
- it('should handle percentage display format', () => {
- const input = 'apple,banana,apple,orange,banana,apple';
- const result = TopItemsList('symbol', 'count', 'percentage', ',', input, false, false, false);
- expect(result).toEqual(
- 'apple: 3 (50.00%)\n' +
- 'banana: 2 (33.33%)\n' +
- 'orange: 1 (16.67%)'
- );
- });
+ it('should handle percentage display format', () => {
+ const input = 'apple,banana,apple,orange,banana,apple';
+ const result = TopItemsList(
+ 'symbol',
+ 'count',
+ 'percentage',
+ ',',
+ input,
+ false,
+ false,
+ false
+ );
+ expect(result).toEqual(
+ 'apple: 3 (50.00%)\n' + 'banana: 2 (33.33%)\n' + 'orange: 1 (16.67%)'
+ );
+ });
- it('should handle total display format', () => {
- const input = 'apple,banana,apple,orange,banana,apple';
- const result = TopItemsList('symbol', 'count', 'total', ',', input, false, false, false);
- expect(result).toEqual(
- 'apple: 3 (3 / 6)\n' +
- 'banana: 2 (2 / 6)\n' +
- 'orange: 1 (1 / 6)'
- );
- });
+ it('should handle total display format', () => {
+ const input = 'apple,banana,apple,orange,banana,apple';
+ const result = TopItemsList(
+ 'symbol',
+ 'count',
+ 'total',
+ ',',
+ input,
+ false,
+ false,
+ false
+ );
+ expect(result).toEqual(
+ 'apple: 3 (3 / 6)\n' + 'banana: 2 (2 / 6)\n' + 'orange: 1 (1 / 6)'
+ );
+ });
- it('should handle trimming and ignoring empty items', () => {
- const input = ' apple , banana , apple , orange , banana , apple ';
- const result = TopItemsList('symbol', 'count', 'count', ',', input, true, false, true);
- expect(result).toEqual(
- 'apple: 3\n' +
- 'banana: 2\n' +
- 'orange: 1'
- );
- });
-});
\ No newline at end of file
+ it('should handle trimming and ignoring empty items', () => {
+ const input = ' apple , banana , apple , orange , banana , apple ';
+ const result = TopItemsList(
+ 'symbol',
+ 'count',
+ 'count',
+ ',',
+ input,
+ true,
+ false,
+ true
+ );
+ expect(result).toEqual('apple: 3\n' + 'banana: 2\n' + 'orange: 1');
+ });
+});
diff --git a/src/pages/list/find-most-popular/index.tsx b/src/pages/list/find-most-popular/index.tsx
index 40566cf..6d5da58 100644
--- a/src/pages/list/find-most-popular/index.tsx
+++ b/src/pages/list/find-most-popular/index.tsx
@@ -1,11 +1,172 @@
import { Box } from '@mui/material';
-import React from 'react';
+import React, { useState } from 'react';
+import ToolTextInput from '../../../components/input/ToolTextInput';
+import ToolTextResult from '../../../components/result/ToolTextResult';
import * as Yup from 'yup';
+import ToolOptions from '../../../components/options/ToolOptions';
+import {
+ DisplayFormat,
+ SortingMethod,
+ SplitOperatorType,
+ TopItemsList
+} from './service';
+import ToolInputAndResult from '../../../components/ToolInputAndResult';
+import SimpleRadio from '../../../components/options/SimpleRadio';
+import TextFieldWithDesc from '../../../components/options/TextFieldWithDesc';
+import CheckboxWithDesc from '../../../components/options/CheckboxWithDesc';
+import SelectWithDesc from '../../../components/options/SelectWithDesc';
+
+const initialValues = {
+ splitSeparatorType: 'symbol' as SplitOperatorType,
+ sortingMethod: 'alphabetic' as SortingMethod,
+ displayFormat: 'count' as DisplayFormat,
+ splitSeparator: ',',
+ deleteEmptyItems: false,
+ ignoreItemCase: false,
+ trimItems: false
+};
+const splitOperators: {
+ title: string;
+ description: string;
+ type: SplitOperatorType;
+}[] = [
+ {
+ title: 'Use a Symbol for Splitting',
+ description: 'Delimit input list items with a character.',
+ type: 'symbol'
+ },
+ {
+ title: 'Use a Regex for Splitting',
+ type: 'regex',
+ description: 'Delimit input list items with a regular expression.'
+ }
+];
-const initialValues = {};
-const validationSchema = Yup.object({
- // splitSeparator: Yup.string().required('The separator is required')
-});
export default function FindMostPopular() {
- return Lorem ipsum;
-}
\ No newline at end of file
+ const [input, setInput] = useState('');
+ const [result, setResult] = useState('');
+ const compute = (optionsValues: typeof initialValues, input: any) => {
+ const {
+ splitSeparatorType,
+ splitSeparator,
+ displayFormat,
+ sortingMethod,
+ deleteEmptyItems,
+ ignoreItemCase,
+ trimItems
+ } = optionsValues;
+
+ setResult(
+ TopItemsList(
+ splitSeparatorType,
+ sortingMethod,
+ displayFormat,
+ splitSeparator,
+ input,
+ deleteEmptyItems,
+ ignoreItemCase,
+ trimItems
+ )
+ );
+ };
+ const validationSchema = Yup.object({
+ // splitSeparator: Yup.string().required('The separator is required')
+ });
+
+ return (
+
+
+ }
+ result={}
+ />
+ [
+ {
+ title: 'How to Extract List Items?',
+ component: (
+
+ {splitOperators.map(({ title, description, type }) => (
+ updateField('splitSeparatorType', type)}
+ title={title}
+ description={description}
+ checked={values.splitSeparatorType === type}
+ />
+ ))}
+ updateField('splitSeparator', val)}
+ />
+
+ )
+ },
+ {
+ title: 'Item comparison',
+ component: (
+
+ updateField('deleteEmptyItems', value)}
+ />
+ updateField('trimItems', value)}
+ />
+ updateField('ignoreItemCase', value)}
+ />
+
+ )
+ },
+ {
+ title: 'Top item output format',
+ component: (
+
+ updateField('displayFormat', value)}
+ description={'How to display the most popular list items?'}
+ />
+ updateField('sortingMethod', value)}
+ description={'Select a sorting method.'}
+ />
+
+ )
+ }
+ ]}
+ initialValues={initialValues}
+ input={input}
+ validationSchema={validationSchema}
+ />
+
+ );
+}
diff --git a/src/pages/list/find-most-popular/service.ts b/src/pages/list/find-most-popular/service.ts
index 3c812b9..fd6e6fa 100644
--- a/src/pages/list/find-most-popular/service.ts
+++ b/src/pages/list/find-most-popular/service.ts
@@ -3,105 +3,112 @@ export type DisplayFormat = 'count' | 'percentage' | 'total';
export type SortingMethod = 'count' | 'alphabetic';
// Function that takes the array as arg and returns a dict of element occurrences and handle the ignoreItemCase
-function dictMaker(array: string[],
- ignoreItemCase: boolean
+function dictMaker(
+ array: string[],
+ ignoreItemCase: boolean
): { [key: string]: number } {
- const dict: { [key: string]: number } = {};
- for (const item of array) {
- const key = ignoreItemCase ? item.toLowerCase() : item;
- dict[key] = (dict[key] || 0) + 1;
- }
- return dict;
+ const dict: { [key: string]: number } = {};
+ for (const item of array) {
+ const key = ignoreItemCase ? item.toLowerCase() : item;
+ dict[key] = (dict[key] || 0) + 1;
+ }
+ return dict;
}
// Function that sorts the dict created with dictMaker based on the chosen sorting method
function dictSorter(
- dict: { [key: string]: number },
- sortingMethod: SortingMethod,
+ dict: { [key: string]: number },
+ sortingMethod: SortingMethod
): { [key: string]: number } {
- let sortedArray: [string, number][];
- switch (sortingMethod) {
- case 'count':
- sortedArray = Object.entries(dict).sort(([, countA], [, countB]) => countB - countA);
- break;
- case 'alphabetic':
- sortedArray = Object.entries(dict).sort(([keyA], [keyB]) => {
- return keyA.localeCompare(keyB)
- });
- break;
- default:
- sortedArray = Object.entries(dict);
- break;
- }
- return Object.fromEntries(sortedArray);
+ let sortedArray: [string, number][];
+ switch (sortingMethod) {
+ case 'count':
+ sortedArray = Object.entries(dict).sort(
+ ([, countA], [, countB]) => countB - countA
+ );
+ break;
+ case 'alphabetic':
+ sortedArray = Object.entries(dict).sort(([keyA], [keyB]) => {
+ return keyA.localeCompare(keyB);
+ });
+ break;
+ default:
+ sortedArray = Object.entries(dict);
+ break;
+ }
+ return Object.fromEntries(sortedArray);
}
// Function that prepares the output of dictSorter based on the chosen display format
function displayFormater(
- dict: { [key: string]: number },
- displayFormat: DisplayFormat
+ dict: { [key: string]: number },
+ displayFormat: DisplayFormat
): string[] {
- let formattedOutput: string[] = [];
- const total = Object.values(dict).reduce((acc, val) => acc + val, 0);
+ let formattedOutput: string[] = [];
+ const total = Object.values(dict).reduce((acc, val) => acc + val, 0);
- switch (displayFormat) {
- case 'percentage':
- Object.entries(dict).forEach(([key, value]) => {
- formattedOutput.push(`${key}: ${value} (${((value / total) * 100).toFixed(2)}%)`);
- });
- break;
- case "total":
- Object.entries(dict).forEach(([key, value]) => {
- formattedOutput.push(`${key}: ${value} (${value} / ${total})`);
- });
- break;
- case "count":
- Object.entries(dict).forEach(([key, value]) => {
- formattedOutput.push(`${key}: ${value}`);
- });
- break;
- }
- return formattedOutput;
+ switch (displayFormat) {
+ case 'percentage':
+ Object.entries(dict).forEach(([key, value]) => {
+ formattedOutput.push(
+ `${key}: ${value} (${((value / total) * 100).toFixed(2)}%)`
+ );
+ });
+ break;
+ case 'total':
+ Object.entries(dict).forEach(([key, value]) => {
+ formattedOutput.push(`${key}: ${value} (${value} / ${total})`);
+ });
+ break;
+ case 'count':
+ Object.entries(dict).forEach(([key, value]) => {
+ formattedOutput.push(`${key}: ${value}`);
+ });
+ break;
+ }
+ return formattedOutput;
}
export function TopItemsList(
- splitOperatorType: SplitOperatorType,
- sortingMethod: SortingMethod,
- displayFormat: DisplayFormat,
- splitSeparator: string,
- input: string,
- deleteEmptyItems: boolean,
- ignoreItemCase: boolean,
- trimItems: boolean
+ splitOperatorType: SplitOperatorType,
+ sortingMethod: SortingMethod,
+ displayFormat: DisplayFormat,
+ splitSeparator: string,
+ input: string,
+ deleteEmptyItems: boolean,
+ ignoreItemCase: boolean,
+ trimItems: boolean
): string {
- let array: string[];
- switch (splitOperatorType) {
- case 'symbol':
- array = input.split(splitSeparator);
- break;
- case 'regex':
- array = input.split(new RegExp(splitSeparator)).filter(item => item !== '');
- break;
- }
+ let array: string[];
+ switch (splitOperatorType) {
+ case 'symbol':
+ array = input.split(splitSeparator);
+ break;
+ case 'regex':
+ array = input
+ .split(new RegExp(splitSeparator))
+ .filter((item) => item !== '');
+ break;
+ }
- // Trim items if required
- if (trimItems) {
- array = array.map(item => item.trim());
- }
+ // Trim items if required
+ if (trimItems) {
+ array = array.map((item) => item.trim());
+ }
- // Delete empty items after initial split
- if (deleteEmptyItems) {
- array = array.filter(item => item !== '');
- }
+ // Delete empty items after initial split
+ if (deleteEmptyItems) {
+ array = array.filter((item) => item !== '');
+ }
- // Transform the array into dict
- const unsortedDict = dictMaker(array, ignoreItemCase);
+ // Transform the array into dict
+ const unsortedDict = dictMaker(array, ignoreItemCase);
- // Sort the list if required
- const sortedDict = dictSorter(unsortedDict, sortingMethod);
+ // Sort the list if required
+ const sortedDict = dictSorter(unsortedDict, sortingMethod);
- // Format the output with desired format
- const formattedOutput = displayFormater(sortedDict, displayFormat);
+ // Format the output with desired format
+ const formattedOutput = displayFormater(sortedDict, displayFormat);
- return formattedOutput.join('\n');
+ return formattedOutput.join('\n');
}
diff --git a/src/pages/list/index.ts b/src/pages/list/index.ts
index 79b8491..21fa55b 100644
--- a/src/pages/list/index.ts
+++ b/src/pages/list/index.ts
@@ -10,4 +10,15 @@ import { tool as listTruncate } from './truncate/meta';
import { tool as listShuffle } from './shuffle/meta';
import { tool as listSort } from './sort/meta';
-export const listTools = [listSort];
+export const listTools = [
+ listSort,
+ listUnwrap,
+ listReverse,
+ listFindUnique,
+ listFindMostPopular,
+ listGroup,
+ listWrap,
+ listRotate,
+ listShuffle,
+ listTruncate
+];
diff --git a/src/pages/list/sort/index.tsx b/src/pages/list/sort/index.tsx
index 5223a7e..0e5c063 100644
--- a/src/pages/list/sort/index.tsx
+++ b/src/pages/list/sort/index.tsx
@@ -8,6 +8,8 @@ import { Sort, SortingMethod, SplitOperatorType } from './service';
import ToolInputAndResult from '../../../components/ToolInputAndResult';
import SimpleRadio from '../../../components/options/SimpleRadio';
import TextFieldWithDesc from '../../../components/options/TextFieldWithDesc';
+import CheckboxWithDesc from '../../../components/options/CheckboxWithDesc';
+import SelectWithDesc from '../../../components/options/SelectWithDesc';
const initialValues = {
splitSeparatorType: 'symbol' as SplitOperatorType,
@@ -38,8 +40,7 @@ const splitOperators: {
export default function SplitText() {
const [input, setInput] = useState('');
const [result, setResult] = useState('');
- // const formRef = useRef>(null);
- const computeExternal = (optionsValues: typeof initialValues, input: any) => {
+ const compute = (optionsValues: typeof initialValues, input: any) => {
const {
splitSeparatorType,
joinSeparator,
@@ -80,7 +81,7 @@ export default function SplitText() {
result={}
/>
[
{
title: 'Input item separator',
@@ -105,7 +106,59 @@ export default function SplitText() {
},
{
title: 'Sort method',
- component:
+ component: (
+
+ updateField('sortingMethod', value)}
+ description={'Select a sorting method.'}
+ />
+ {
+ updateField('increasing', value);
+ }}
+ description={'Select a sorting order.'}
+ />
+ updateField('caseSensitive', val)}
+ />
+
+ )
+ },
+ {
+ title: 'Sorted item properties',
+ component: (
+
+ updateField('joinSeparator', val)}
+ />
+ updateField('removeDuplicated', val)}
+ />
+
+ )
}
]}
initialValues={initialValues}
diff --git a/src/utils/string.ts b/src/utils/string.ts
index d2e4e8f..db558eb 100644
--- a/src/utils/string.ts
+++ b/src/utils/string.ts
@@ -6,3 +6,13 @@ export function capitalizeFirstLetter(string: string | undefined) {
export function isNumber(number: any) {
return !isNaN(parseFloat(number)) && isFinite(number);
}
+
+export const replaceSpecialCharacters = (str: string) => {
+ return str
+ .replace(/\\n/g, '\n')
+ .replace(/\\t/g, '\t')
+ .replace(/\\r/g, '\r')
+ .replace(/\\b/g, '\b')
+ .replace(/\\f/g, '\f')
+ .replace(/\\v/g, '\v');
+};