mirror of
https://github.com/iib0011/omni-tools.git
synced 2025-12-29 16:16:02 +00:00
refactor: tools folder inside pages
This commit is contained in:
3
src/pages/tools/image/index.ts
Normal file
3
src/pages/tools/image/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import { pngTools } from './png';
|
||||
|
||||
export const imageTools = [...pngTools];
|
||||
@@ -0,0 +1,43 @@
|
||||
import { expect, test } from '@playwright/test';
|
||||
import { Buffer } from 'buffer';
|
||||
import path from 'path';
|
||||
import Jimp from 'jimp';
|
||||
import { convertHexToRGBA } from '../../../../../utils/color';
|
||||
|
||||
test.describe('Change colors in png', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto('/png/change-colors-in-png');
|
||||
});
|
||||
|
||||
test('should change pixel color', async ({ page }) => {
|
||||
// Upload image
|
||||
const fileInput = page.locator('input[type="file"]');
|
||||
const imagePath = path.join(__dirname, 'test.png');
|
||||
await fileInput?.setInputFiles(imagePath);
|
||||
|
||||
await page.getByTestId('from-color-input').fill('#FF0000');
|
||||
const toColor = '#0000FF';
|
||||
await page.getByTestId('to-color-input').fill(toColor);
|
||||
|
||||
// Click on download
|
||||
const downloadPromise = page.waitForEvent('download');
|
||||
await page.getByText('Save as').click();
|
||||
|
||||
// Intercept and read downloaded PNG
|
||||
const download = await downloadPromise;
|
||||
const downloadStream = await download.createReadStream();
|
||||
|
||||
const chunks = [];
|
||||
for await (const chunk of downloadStream) {
|
||||
chunks.push(chunk);
|
||||
}
|
||||
const fileContent = Buffer.concat(chunks);
|
||||
|
||||
expect(fileContent.length).toBeGreaterThan(0);
|
||||
|
||||
// Check that the first pixel is transparent
|
||||
const image = await Jimp.read(fileContent);
|
||||
const color = image.getPixelColor(0, 0);
|
||||
expect(color).toBe(convertHexToRGBA(toColor));
|
||||
});
|
||||
});
|
||||
140
src/pages/tools/image/png/change-colors-in-png/index.tsx
Normal file
140
src/pages/tools/image/png/change-colors-in-png/index.tsx
Normal file
@@ -0,0 +1,140 @@
|
||||
import { Box } from '@mui/material';
|
||||
import React, { useState } from 'react';
|
||||
import * as Yup from 'yup';
|
||||
import ToolFileInput from '@components/input/ToolFileInput';
|
||||
import ToolFileResult from '@components/result/ToolFileResult';
|
||||
import ToolOptions from '@components/options/ToolOptions';
|
||||
import ColorSelector from '@components/options/ColorSelector';
|
||||
import Color from 'color';
|
||||
import TextFieldWithDesc from 'components/options/TextFieldWithDesc';
|
||||
import ToolInputAndResult from '@components/ToolInputAndResult';
|
||||
import { areColorsSimilar } from 'utils/color';
|
||||
|
||||
const initialValues = {
|
||||
fromColor: 'white',
|
||||
toColor: 'black',
|
||||
similarity: '10'
|
||||
};
|
||||
const validationSchema = Yup.object({
|
||||
// splitSeparator: Yup.string().required('The separator is required')
|
||||
});
|
||||
export default function ChangeColorsInPng() {
|
||||
const [input, setInput] = useState<File | null>(null);
|
||||
const [result, setResult] = useState<File | null>(null);
|
||||
|
||||
const compute = (optionsValues: typeof initialValues, input: any) => {
|
||||
if (!input) return;
|
||||
const { fromColor, toColor, similarity } = optionsValues;
|
||||
let fromRgb: [number, number, number];
|
||||
let toRgb: [number, number, number];
|
||||
try {
|
||||
//@ts-ignore
|
||||
fromRgb = Color(fromColor).rgb().array();
|
||||
//@ts-ignore
|
||||
toRgb = Color(toColor).rgb().array();
|
||||
} catch (err) {
|
||||
return;
|
||||
}
|
||||
const processImage = async (
|
||||
file: File,
|
||||
fromColor: [number, number, number],
|
||||
toColor: [number, number, number],
|
||||
similarity: number
|
||||
) => {
|
||||
const canvas = document.createElement('canvas');
|
||||
const ctx = canvas.getContext('2d');
|
||||
if (ctx == null) return;
|
||||
const img = new Image();
|
||||
|
||||
img.src = URL.createObjectURL(file);
|
||||
await img.decode();
|
||||
|
||||
canvas.width = img.width;
|
||||
canvas.height = img.height;
|
||||
ctx.drawImage(img, 0, 0);
|
||||
|
||||
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
|
||||
const data: Uint8ClampedArray = imageData.data;
|
||||
|
||||
for (let i = 0; i < data.length; i += 4) {
|
||||
const currentColor: [number, number, number] = [
|
||||
data[i],
|
||||
data[i + 1],
|
||||
data[i + 2]
|
||||
];
|
||||
if (areColorsSimilar(currentColor, fromColor, similarity)) {
|
||||
data[i] = toColor[0]; // Red
|
||||
data[i + 1] = toColor[1]; // Green
|
||||
data[i + 2] = toColor[2]; // Blue
|
||||
}
|
||||
}
|
||||
|
||||
ctx.putImageData(imageData, 0, 0);
|
||||
|
||||
canvas.toBlob((blob) => {
|
||||
if (blob) {
|
||||
const newFile = new File([blob], file.name, {
|
||||
type: 'image/png'
|
||||
});
|
||||
setResult(newFile);
|
||||
}
|
||||
}, 'image/png');
|
||||
};
|
||||
|
||||
processImage(input, fromRgb, toRgb, Number(similarity));
|
||||
};
|
||||
return (
|
||||
<Box>
|
||||
<ToolInputAndResult
|
||||
input={
|
||||
<ToolFileInput
|
||||
value={input}
|
||||
onChange={setInput}
|
||||
accept={['image/png']}
|
||||
title={'Input PNG'}
|
||||
/>
|
||||
}
|
||||
result={
|
||||
<ToolFileResult
|
||||
title={'Output PNG with new colors'}
|
||||
value={result}
|
||||
extension={'png'}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<ToolOptions
|
||||
compute={compute}
|
||||
getGroups={({ values, updateField }) => [
|
||||
{
|
||||
title: 'From color and to color',
|
||||
component: (
|
||||
<Box>
|
||||
<ColorSelector
|
||||
value={values.fromColor}
|
||||
onColorChange={(val) => updateField('fromColor', val)}
|
||||
description={'Replace this color (from color)'}
|
||||
inputProps={{ 'data-testid': 'from-color-input' }}
|
||||
/>
|
||||
<ColorSelector
|
||||
value={values.toColor}
|
||||
onColorChange={(val) => updateField('toColor', val)}
|
||||
description={'With this color (to color)'}
|
||||
inputProps={{ 'data-testid': 'to-color-input' }}
|
||||
/>
|
||||
<TextFieldWithDesc
|
||||
value={values.similarity}
|
||||
onOwnChange={(val) => updateField('similarity', val)}
|
||||
description={
|
||||
'Match this % of similar colors of the from color. For example, 10% white will match white and a little bit of gray.'
|
||||
}
|
||||
/>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
]}
|
||||
initialValues={initialValues}
|
||||
input={input}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
14
src/pages/tools/image/png/change-colors-in-png/meta.ts
Normal file
14
src/pages/tools/image/png/change-colors-in-png/meta.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { defineTool } from '@tools/defineTool';
|
||||
import { lazy } from 'react';
|
||||
import image from '@assets/image.png';
|
||||
|
||||
export const tool = defineTool('png', {
|
||||
name: 'Change colors in png',
|
||||
path: 'change-colors-in-png',
|
||||
image,
|
||||
description:
|
||||
"World's simplest online Portable Network Graphics (PNG) color changer. Just import your PNG image in the editor on the left, select which colors to change, and you'll instantly get a new PNG with the new colors on the right. Free, quick, and very powerful. Import a PNG – replace its colors.",
|
||||
shortDescription: 'Quickly swap colors in a PNG image',
|
||||
keywords: ['change', 'colors', 'in', 'png'],
|
||||
component: lazy(() => import('./index'))
|
||||
});
|
||||
BIN
src/pages/tools/image/png/change-colors-in-png/test.png
Normal file
BIN
src/pages/tools/image/png/change-colors-in-png/test.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.6 KiB |
@@ -0,0 +1,72 @@
|
||||
import { expect, test } from '@playwright/test';
|
||||
import { Buffer } from 'buffer';
|
||||
import path from 'path';
|
||||
import Jimp from 'jimp';
|
||||
|
||||
test.describe('Convert JPG to PNG tool', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto('/png/convert-jgp-to-png');
|
||||
});
|
||||
|
||||
test('should convert jpg to png', async ({ page }) => {
|
||||
// Upload image
|
||||
const fileInput = page.locator('input[type="file"]');
|
||||
const imagePath = path.join(__dirname, 'test.jpg');
|
||||
await fileInput?.setInputFiles(imagePath);
|
||||
|
||||
// Click on download
|
||||
const downloadPromise = page.waitForEvent('download');
|
||||
await page.getByText('Save as').click();
|
||||
|
||||
// Intercept and read downloaded PNG
|
||||
const download = await downloadPromise;
|
||||
const downloadStream = await download.createReadStream();
|
||||
|
||||
const chunks = [];
|
||||
for await (const chunk of downloadStream) {
|
||||
chunks.push(chunk);
|
||||
}
|
||||
const fileContent = Buffer.concat(chunks);
|
||||
|
||||
expect(fileContent.length).toBeGreaterThan(0);
|
||||
|
||||
// Check that the first pixel is 0x808080ff
|
||||
const image = await Jimp.read(fileContent);
|
||||
const color = image.getPixelColor(0, 0);
|
||||
expect(color).toBe(0x808080ff);
|
||||
});
|
||||
|
||||
test('should apply transparency before converting jpg to png', async ({
|
||||
page
|
||||
}) => {
|
||||
// Upload image
|
||||
const fileInput = page.locator('input[type="file"]');
|
||||
const imagePath = path.join(__dirname, 'test.jpg');
|
||||
await fileInput?.setInputFiles(imagePath);
|
||||
|
||||
// Enable transparency on color 0x808080
|
||||
await page.getByLabel('Enable PNG Transparency').check();
|
||||
await page.getByTestId('color-input').fill('#808080');
|
||||
|
||||
// Click on download
|
||||
const downloadPromise = page.waitForEvent('download');
|
||||
await page.getByText('Save as').click();
|
||||
|
||||
// Intercept and read downloaded PNG
|
||||
const download = await downloadPromise;
|
||||
const downloadStream = await download.createReadStream();
|
||||
|
||||
const chunks = [];
|
||||
for await (const chunk of downloadStream) {
|
||||
chunks.push(chunk);
|
||||
}
|
||||
const fileContent = Buffer.concat(chunks);
|
||||
|
||||
expect(fileContent.length).toBeGreaterThan(0);
|
||||
|
||||
// Check that the first pixel is transparent
|
||||
const image = await Jimp.read(fileContent);
|
||||
const color = image.getPixelColor(0, 0);
|
||||
expect(color).toBe(0);
|
||||
});
|
||||
});
|
||||
154
src/pages/tools/image/png/convert-jgp-to-png/index.tsx
Normal file
154
src/pages/tools/image/png/convert-jgp-to-png/index.tsx
Normal file
@@ -0,0 +1,154 @@
|
||||
import { Box } from '@mui/material';
|
||||
import ToolInputAndResult from 'components/ToolInputAndResult';
|
||||
import ToolFileInput from 'components/input/ToolFileInput';
|
||||
import CheckboxWithDesc from 'components/options/CheckboxWithDesc';
|
||||
import ColorSelector from 'components/options/ColorSelector';
|
||||
import TextFieldWithDesc from 'components/options/TextFieldWithDesc';
|
||||
import ToolOptions from 'components/options/ToolOptions';
|
||||
import ToolFileResult from 'components/result/ToolFileResult';
|
||||
import Color from 'color';
|
||||
import React, { useState } from 'react';
|
||||
import * as Yup from 'yup';
|
||||
import { areColorsSimilar } from 'utils/color';
|
||||
|
||||
const initialValues = {
|
||||
enableTransparency: false,
|
||||
color: 'white',
|
||||
similarity: '10'
|
||||
};
|
||||
const validationSchema = Yup.object({
|
||||
// splitSeparator: Yup.string().required('The separator is required')
|
||||
});
|
||||
export default function ConvertJgpToPng() {
|
||||
const [input, setInput] = useState<File | null>(null);
|
||||
const [result, setResult] = useState<File | null>(null);
|
||||
|
||||
const compute = async (
|
||||
optionsValues: typeof initialValues,
|
||||
input: any
|
||||
): Promise<void> => {
|
||||
if (!input) return;
|
||||
|
||||
const processImage = async (
|
||||
file: File,
|
||||
transparencyTransform?: {
|
||||
color: [number, number, number];
|
||||
similarity: number;
|
||||
}
|
||||
) => {
|
||||
const canvas = document.createElement('canvas');
|
||||
const ctx = canvas.getContext('2d');
|
||||
if (ctx == null) return;
|
||||
const img = new Image();
|
||||
|
||||
img.src = URL.createObjectURL(file);
|
||||
await img.decode();
|
||||
|
||||
canvas.width = img.width;
|
||||
canvas.height = img.height;
|
||||
ctx.drawImage(img, 0, 0);
|
||||
|
||||
if (transparencyTransform) {
|
||||
const { color, similarity } = transparencyTransform;
|
||||
|
||||
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
|
||||
const data: Uint8ClampedArray = imageData.data;
|
||||
|
||||
for (let i = 0; i < data.length; i += 4) {
|
||||
const currentColor: [number, number, number] = [
|
||||
data[i],
|
||||
data[i + 1],
|
||||
data[i + 2]
|
||||
];
|
||||
if (areColorsSimilar(currentColor, color, similarity)) {
|
||||
data[i + 3] = 0;
|
||||
}
|
||||
}
|
||||
|
||||
ctx.putImageData(imageData, 0, 0);
|
||||
}
|
||||
|
||||
canvas.toBlob((blob) => {
|
||||
if (blob) {
|
||||
const newFile = new File([blob], file.name, {
|
||||
type: 'image/png'
|
||||
});
|
||||
setResult(newFile);
|
||||
}
|
||||
}, 'image/png');
|
||||
};
|
||||
|
||||
if (optionsValues.enableTransparency) {
|
||||
let rgb: [number, number, number];
|
||||
try {
|
||||
//@ts-ignore
|
||||
rgb = Color(optionsValues.color).rgb().array();
|
||||
} catch (err) {
|
||||
return;
|
||||
}
|
||||
|
||||
processImage(input, {
|
||||
color: rgb,
|
||||
similarity: Number(optionsValues.similarity)
|
||||
});
|
||||
} else {
|
||||
processImage(input);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<ToolInputAndResult
|
||||
input={
|
||||
<ToolFileInput
|
||||
value={input}
|
||||
onChange={setInput}
|
||||
accept={['image/jpeg']}
|
||||
title={'Input JPG'}
|
||||
/>
|
||||
}
|
||||
result={
|
||||
<ToolFileResult
|
||||
title={'Output PNG'}
|
||||
value={result}
|
||||
extension={'png'}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<ToolOptions
|
||||
compute={compute}
|
||||
getGroups={({ values, updateField }) => [
|
||||
{
|
||||
title: 'PNG Transparency Color',
|
||||
component: (
|
||||
<Box>
|
||||
<CheckboxWithDesc
|
||||
key="enableTransparency"
|
||||
title="Enable PNG Transparency"
|
||||
checked={!!values.enableTransparency}
|
||||
onChange={(value) => updateField('enableTransparency', value)}
|
||||
description="Make the color below transparent."
|
||||
/>
|
||||
<ColorSelector
|
||||
value={values.color}
|
||||
onColorChange={(val) => updateField('color', val)}
|
||||
description={'With this color (to color)'}
|
||||
inputProps={{ 'data-testid': 'color-input' }}
|
||||
/>
|
||||
<TextFieldWithDesc
|
||||
value={values.similarity}
|
||||
onOwnChange={(val) => updateField('similarity', val)}
|
||||
description={
|
||||
'Match this % of similar. For example, 10% white will match white and a little bit of gray.'
|
||||
}
|
||||
/>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
]}
|
||||
initialValues={initialValues}
|
||||
input={input}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
14
src/pages/tools/image/png/convert-jgp-to-png/meta.ts
Normal file
14
src/pages/tools/image/png/convert-jgp-to-png/meta.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { defineTool } from '@tools/defineTool';
|
||||
import { lazy } from 'react';
|
||||
import image from '@assets/image.png';
|
||||
|
||||
export const tool = defineTool('png', {
|
||||
name: 'Convert JPG to PNG',
|
||||
path: 'convert-jgp-to-png',
|
||||
image,
|
||||
description:
|
||||
'Quickly convert your JPG images to PNG. Just import your PNG image in the editor on the left',
|
||||
shortDescription: 'Quickly convert your JPG images to PNG',
|
||||
keywords: ['convert', 'jgp', 'png'],
|
||||
component: lazy(() => import('./index'))
|
||||
});
|
||||
BIN
src/pages/tools/image/png/convert-jgp-to-png/test.jpg
Normal file
BIN
src/pages/tools/image/png/convert-jgp-to-png/test.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
@@ -0,0 +1,40 @@
|
||||
import { expect, test } from '@playwright/test';
|
||||
import { Buffer } from 'buffer';
|
||||
import path from 'path';
|
||||
import Jimp from 'jimp';
|
||||
|
||||
test.describe('Create transparent PNG', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto('/png/create-transparent');
|
||||
});
|
||||
|
||||
test('should make png color transparent', async ({ page }) => {
|
||||
// Upload image
|
||||
const fileInput = page.locator('input[type="file"]');
|
||||
const imagePath = path.join(__dirname, 'test.png');
|
||||
await fileInput?.setInputFiles(imagePath);
|
||||
|
||||
await page.getByTestId('color-input').fill('#FF0000');
|
||||
|
||||
// Click on download
|
||||
const downloadPromise = page.waitForEvent('download');
|
||||
await page.getByText('Save as').click();
|
||||
|
||||
// Intercept and read downloaded PNG
|
||||
const download = await downloadPromise;
|
||||
const downloadStream = await download.createReadStream();
|
||||
|
||||
const chunks = [];
|
||||
for await (const chunk of downloadStream) {
|
||||
chunks.push(chunk);
|
||||
}
|
||||
const fileContent = Buffer.concat(chunks);
|
||||
|
||||
expect(fileContent.length).toBeGreaterThan(0);
|
||||
|
||||
// Check that the first pixel is transparent
|
||||
const image = await Jimp.read(fileContent);
|
||||
const color = image.getPixelColor(0, 0);
|
||||
expect(color).toBe(0);
|
||||
});
|
||||
});
|
||||
127
src/pages/tools/image/png/create-transparent/index.tsx
Normal file
127
src/pages/tools/image/png/create-transparent/index.tsx
Normal file
@@ -0,0 +1,127 @@
|
||||
import { Box } from '@mui/material';
|
||||
import React, { useState } from 'react';
|
||||
import * as Yup from 'yup';
|
||||
import ToolFileInput from '@components/input/ToolFileInput';
|
||||
import ToolFileResult from '@components/result/ToolFileResult';
|
||||
import ToolOptions from '@components/options/ToolOptions';
|
||||
import ColorSelector from '@components/options/ColorSelector';
|
||||
import Color from 'color';
|
||||
import TextFieldWithDesc from 'components/options/TextFieldWithDesc';
|
||||
import ToolInputAndResult from '@components/ToolInputAndResult';
|
||||
import { areColorsSimilar } from 'utils/color';
|
||||
|
||||
const initialValues = {
|
||||
fromColor: 'white',
|
||||
similarity: '10'
|
||||
};
|
||||
const validationSchema = Yup.object({
|
||||
// splitSeparator: Yup.string().required('The separator is required')
|
||||
});
|
||||
export default function ChangeColorsInPng() {
|
||||
const [input, setInput] = useState<File | null>(null);
|
||||
const [result, setResult] = useState<File | null>(null);
|
||||
|
||||
const compute = (optionsValues: typeof initialValues, input: any) => {
|
||||
if (!input) return;
|
||||
const { fromColor, similarity } = optionsValues;
|
||||
|
||||
let fromRgb: [number, number, number];
|
||||
try {
|
||||
//@ts-ignore
|
||||
fromRgb = Color(fromColor).rgb().array();
|
||||
} catch (err) {
|
||||
return;
|
||||
}
|
||||
const processImage = async (
|
||||
file: File,
|
||||
fromColor: [number, number, number],
|
||||
similarity: number
|
||||
) => {
|
||||
const canvas = document.createElement('canvas');
|
||||
const ctx = canvas.getContext('2d');
|
||||
if (ctx == null) return;
|
||||
const img = new Image();
|
||||
|
||||
img.src = URL.createObjectURL(file);
|
||||
await img.decode();
|
||||
|
||||
canvas.width = img.width;
|
||||
canvas.height = img.height;
|
||||
ctx.drawImage(img, 0, 0);
|
||||
|
||||
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
|
||||
const data: Uint8ClampedArray = imageData.data;
|
||||
|
||||
for (let i = 0; i < data.length; i += 4) {
|
||||
const currentColor: [number, number, number] = [
|
||||
data[i],
|
||||
data[i + 1],
|
||||
data[i + 2]
|
||||
];
|
||||
if (areColorsSimilar(currentColor, fromColor, similarity)) {
|
||||
data[i + 3] = 0; // Set alpha to 0 (transparent)
|
||||
}
|
||||
}
|
||||
|
||||
ctx.putImageData(imageData, 0, 0);
|
||||
|
||||
canvas.toBlob((blob) => {
|
||||
if (blob) {
|
||||
const newFile = new File([blob], file.name, { type: 'image/png' });
|
||||
setResult(newFile);
|
||||
}
|
||||
}, 'image/png');
|
||||
};
|
||||
|
||||
processImage(input, fromRgb, Number(similarity));
|
||||
};
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<ToolInputAndResult
|
||||
input={
|
||||
<ToolFileInput
|
||||
value={input}
|
||||
onChange={setInput}
|
||||
accept={['image/png']}
|
||||
title={'Input PNG'}
|
||||
/>
|
||||
}
|
||||
result={
|
||||
<ToolFileResult
|
||||
title={'Transparent PNG'}
|
||||
value={result}
|
||||
extension={'png'}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<ToolOptions
|
||||
compute={compute}
|
||||
getGroups={({ values, updateField }) => [
|
||||
{
|
||||
title: 'From color and similarity',
|
||||
component: (
|
||||
<Box>
|
||||
<ColorSelector
|
||||
value={values.fromColor}
|
||||
onColorChange={(val) => updateField('fromColor', val)}
|
||||
description={'Replace this color (from color)'}
|
||||
inputProps={{ 'data-testid': 'color-input' }}
|
||||
/>
|
||||
<TextFieldWithDesc
|
||||
value={values.similarity}
|
||||
onOwnChange={(val) => updateField('similarity', val)}
|
||||
description={
|
||||
'Match this % of similar colors of the from color. For example, 10% white will match white and a little bit of gray.'
|
||||
}
|
||||
/>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
]}
|
||||
initialValues={initialValues}
|
||||
input={input}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
14
src/pages/tools/image/png/create-transparent/meta.ts
Normal file
14
src/pages/tools/image/png/create-transparent/meta.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { defineTool } from '@tools/defineTool';
|
||||
import { lazy } from 'react';
|
||||
import image from '@assets/image.png';
|
||||
|
||||
export const tool = defineTool('png', {
|
||||
name: 'Create transparent PNG',
|
||||
path: 'create-transparent',
|
||||
image,
|
||||
shortDescription: 'Quickly make a PNG image transparent',
|
||||
description:
|
||||
"World's simplest online Portable Network Graphics transparency maker. Just import your PNG image in the editor on the left and you will instantly get a transparent PNG on the right. Free, quick, and very powerful. Import a PNG – get a transparent PNG.",
|
||||
keywords: ['create', 'transparent'],
|
||||
component: lazy(() => import('./index'))
|
||||
});
|
||||
BIN
src/pages/tools/image/png/create-transparent/test.png
Normal file
BIN
src/pages/tools/image/png/create-transparent/test.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.6 KiB |
9
src/pages/tools/image/png/index.ts
Normal file
9
src/pages/tools/image/png/index.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { tool as convertJgpToPng } from './convert-jgp-to-png/meta';
|
||||
import { tool as pngCreateTransparent } from './create-transparent/meta';
|
||||
import { tool as changeColorsInPng } from './change-colors-in-png/meta';
|
||||
|
||||
export const pngTools = [
|
||||
changeColorsInPng,
|
||||
pngCreateTransparent,
|
||||
convertJgpToPng
|
||||
];
|
||||
66
src/pages/tools/list/duplicate/duplicate.service.test.ts
Normal file
66
src/pages/tools/list/duplicate/duplicate.service.test.ts
Normal file
@@ -0,0 +1,66 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { duplicateList } from './service';
|
||||
|
||||
describe('duplicateList function', () => {
|
||||
it('should duplicate elements correctly with symbol split', () => {
|
||||
const input = 'Hello World';
|
||||
const result = duplicateList('symbol', ' ', ' ', input, true, false, 2);
|
||||
expect(result).toBe('Hello World Hello World');
|
||||
});
|
||||
|
||||
it('should duplicate elements correctly with regex split', () => {
|
||||
const input = 'Hello||World';
|
||||
const result = duplicateList('regex', '\\|\\|', ' ', input, true, false, 2);
|
||||
expect(result).toBe('Hello World Hello World');
|
||||
});
|
||||
|
||||
it('should handle fractional duplication', () => {
|
||||
const input = 'Hello World';
|
||||
const result = duplicateList('symbol', ' ', ' ', input, true, false, 1.5);
|
||||
expect(result).toBe('Hello World Hello');
|
||||
});
|
||||
|
||||
it('should handle reverse option correctly', () => {
|
||||
const input = 'Hello World';
|
||||
const result = duplicateList('symbol', ' ', ' ', input, true, true, 2);
|
||||
expect(result).toBe('Hello World World Hello');
|
||||
});
|
||||
|
||||
it('should handle concatenate option correctly', () => {
|
||||
const input = 'Hello World';
|
||||
const result = duplicateList('symbol', ' ', ' ', input, false, false, 2);
|
||||
expect(result).toBe('Hello Hello World World');
|
||||
});
|
||||
|
||||
it('should handle interweaving option correctly', () => {
|
||||
const input = 'Hello World';
|
||||
const result = duplicateList('symbol', ' ', ' ', input, false, false, 2);
|
||||
expect(result).toBe('Hello Hello World World');
|
||||
});
|
||||
|
||||
it('should throw an error for negative copies', () => {
|
||||
expect(() =>
|
||||
duplicateList('symbol', ' ', ' ', 'Hello World', true, false, -1)
|
||||
).toThrow('Number of copies cannot be negative');
|
||||
});
|
||||
|
||||
it('should handle interweaving option correctly 2', () => {
|
||||
const input = "je m'appelle king";
|
||||
const result = duplicateList('symbol', ' ', ', ', input, false, true, 2.1);
|
||||
expect(result).toBe("je, king, m'appelle, m'appelle, king, je");
|
||||
});
|
||||
|
||||
it('should handle interweaving option correctly 3', () => {
|
||||
const input = "je m'appelle king";
|
||||
const result = duplicateList('symbol', ' ', ', ', input, false, true, 1);
|
||||
expect(result).toBe("je, m'appelle, king");
|
||||
});
|
||||
|
||||
it('should handle interweaving option correctly 3', () => {
|
||||
const input = "je m'appelle king";
|
||||
const result = duplicateList('symbol', ' ', ', ', input, true, true, 2.7);
|
||||
expect(result).toBe(
|
||||
"je, m'appelle, king, king, m'appelle, je, king, m'appelle"
|
||||
);
|
||||
});
|
||||
});
|
||||
11
src/pages/tools/list/duplicate/index.tsx
Normal file
11
src/pages/tools/list/duplicate/index.tsx
Normal file
@@ -0,0 +1,11 @@
|
||||
import { Box } from '@mui/material';
|
||||
import React from 'react';
|
||||
import * as Yup from 'yup';
|
||||
|
||||
const initialValues = {};
|
||||
const validationSchema = Yup.object({
|
||||
// splitSeparator: Yup.string().required('The separator is required')
|
||||
});
|
||||
export default function Duplicate() {
|
||||
return <Box>Lorem ipsum</Box>;
|
||||
}
|
||||
13
src/pages/tools/list/duplicate/meta.ts
Normal file
13
src/pages/tools/list/duplicate/meta.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { defineTool } from '@tools/defineTool';
|
||||
import { lazy } from 'react';
|
||||
// import image from '@assets/text.png';
|
||||
|
||||
export const tool = defineTool('list', {
|
||||
name: 'Duplicate',
|
||||
path: 'duplicate',
|
||||
// image,
|
||||
description: '',
|
||||
shortDescription: '',
|
||||
keywords: ['duplicate'],
|
||||
component: lazy(() => import('./index'))
|
||||
});
|
||||
81
src/pages/tools/list/duplicate/service.ts
Normal file
81
src/pages/tools/list/duplicate/service.ts
Normal file
@@ -0,0 +1,81 @@
|
||||
export type SplitOperatorType = 'symbol' | 'regex';
|
||||
|
||||
function interweave(array1: string[], array2: string[]) {
|
||||
const result: string[] = [];
|
||||
const maxLength = Math.max(array1.length, array2.length);
|
||||
|
||||
for (let i = 0; i < maxLength; i++) {
|
||||
if (i < array1.length) result.push(array1[i]);
|
||||
if (i < array2.length) result.push(array2[i]);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
function duplicate(
|
||||
input: string[],
|
||||
concatenate: boolean,
|
||||
reverse: boolean,
|
||||
copy?: number
|
||||
) {
|
||||
if (copy) {
|
||||
if (copy > 0) {
|
||||
let result: string[] = [];
|
||||
let toAdd: string[] = [];
|
||||
let WholePart: string[] = [];
|
||||
let fractionalPart: string[] = [];
|
||||
const whole = Math.floor(copy);
|
||||
const fractional = copy - whole;
|
||||
if (!reverse) {
|
||||
WholePart = concatenate
|
||||
? Array(whole).fill(input).flat()
|
||||
: Array(whole - 1)
|
||||
.fill(input)
|
||||
.flat();
|
||||
fractionalPart = input.slice(0, Math.floor(input.length * fractional));
|
||||
toAdd = WholePart.concat(fractionalPart);
|
||||
result = concatenate
|
||||
? WholePart.concat(fractionalPart)
|
||||
: interweave(input, toAdd);
|
||||
} else {
|
||||
WholePart = Array(whole - 1)
|
||||
.fill(input)
|
||||
.flat()
|
||||
.reverse();
|
||||
fractionalPart = input
|
||||
.slice()
|
||||
.reverse()
|
||||
.slice(0, Math.floor(input.length * fractional));
|
||||
toAdd = WholePart.concat(fractionalPart);
|
||||
result = concatenate ? input.concat(toAdd) : interweave(input, toAdd);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
throw new Error('Number of copies cannot be negative');
|
||||
}
|
||||
throw new Error('Number of copies must be a valid number');
|
||||
}
|
||||
|
||||
export function duplicateList(
|
||||
splitOperatorType: SplitOperatorType,
|
||||
splitSeparator: string,
|
||||
joinSeparator: string,
|
||||
input: string,
|
||||
concatenate: boolean,
|
||||
reverse: boolean,
|
||||
copy?: number
|
||||
): string {
|
||||
let array: string[];
|
||||
let result: string[];
|
||||
switch (splitOperatorType) {
|
||||
case 'symbol':
|
||||
array = input.split(splitSeparator);
|
||||
break;
|
||||
case 'regex':
|
||||
array = input
|
||||
.split(new RegExp(splitSeparator))
|
||||
.filter((item) => item !== '');
|
||||
break;
|
||||
}
|
||||
result = duplicate(array, concatenate, reverse, copy);
|
||||
return result.join(joinSeparator);
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { TopItemsList } from './service';
|
||||
|
||||
describe('TopItemsList function', () => {
|
||||
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 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 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');
|
||||
});
|
||||
});
|
||||
167
src/pages/tools/list/find-most-popular/index.tsx
Normal file
167
src/pages/tools/list/find-most-popular/index.tsx
Normal file
@@ -0,0 +1,167 @@
|
||||
import { Box } from '@mui/material';
|
||||
import React, { useState } from 'react';
|
||||
import ToolTextInput from '@components/input/ToolTextInput';
|
||||
import ToolTextResult from '@components/result/ToolTextResult';
|
||||
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.'
|
||||
}
|
||||
];
|
||||
|
||||
export default function FindMostPopular() {
|
||||
const [input, setInput] = useState<string>('');
|
||||
const [result, setResult] = useState<string>('');
|
||||
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
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<ToolInputAndResult
|
||||
input={
|
||||
<ToolTextInput
|
||||
title={'Input list'}
|
||||
value={input}
|
||||
onChange={setInput}
|
||||
/>
|
||||
}
|
||||
result={<ToolTextResult title={'Most popular items'} value={result} />}
|
||||
/>
|
||||
<ToolOptions
|
||||
compute={compute}
|
||||
getGroups={({ values, updateField }) => [
|
||||
{
|
||||
title: 'How to Extract List Items?',
|
||||
component: (
|
||||
<Box>
|
||||
{splitOperators.map(({ title, description, type }) => (
|
||||
<SimpleRadio
|
||||
key={type}
|
||||
onClick={() => updateField('splitSeparatorType', type)}
|
||||
title={title}
|
||||
description={description}
|
||||
checked={values.splitSeparatorType === type}
|
||||
/>
|
||||
))}
|
||||
<TextFieldWithDesc
|
||||
description={'Set a delimiting symbol or regular expression.'}
|
||||
value={values.splitSeparator}
|
||||
onOwnChange={(val) => updateField('splitSeparator', val)}
|
||||
/>
|
||||
</Box>
|
||||
)
|
||||
},
|
||||
{
|
||||
title: 'Item comparison',
|
||||
component: (
|
||||
<Box>
|
||||
<CheckboxWithDesc
|
||||
title={'Remove empty items'}
|
||||
description={'Ignore empty items from comparison.'}
|
||||
checked={values.deleteEmptyItems}
|
||||
onChange={(value) => updateField('deleteEmptyItems', value)}
|
||||
/>
|
||||
<CheckboxWithDesc
|
||||
title={'Trim top list items'}
|
||||
description={
|
||||
'Remove leading and trailing spaces before comparing items'
|
||||
}
|
||||
checked={values.trimItems}
|
||||
onChange={(value) => updateField('trimItems', value)}
|
||||
/>
|
||||
<CheckboxWithDesc
|
||||
title={'Ignore Item Case'}
|
||||
description={'Compare all list items in lowercase.'}
|
||||
checked={values.ignoreItemCase}
|
||||
onChange={(value) => updateField('ignoreItemCase', value)}
|
||||
/>
|
||||
</Box>
|
||||
)
|
||||
},
|
||||
{
|
||||
title: 'Top item output format',
|
||||
component: (
|
||||
<Box>
|
||||
<SelectWithDesc
|
||||
selected={values.displayFormat}
|
||||
options={[
|
||||
{ label: 'Show item percentage', value: 'percentage' },
|
||||
{ label: 'Show item count', value: 'count' },
|
||||
{ label: 'Show item total', value: 'total' }
|
||||
]}
|
||||
onChange={(value) => updateField('displayFormat', value)}
|
||||
description={'How to display the most popular list items?'}
|
||||
/>
|
||||
<SelectWithDesc
|
||||
selected={values.sortingMethod}
|
||||
options={[
|
||||
{ label: 'Sort Alphabetically', value: 'alphabetic' },
|
||||
{ label: 'Sort by count', value: 'count' }
|
||||
]}
|
||||
onChange={(value) => updateField('sortingMethod', value)}
|
||||
description={'Select a sorting method.'}
|
||||
/>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
]}
|
||||
initialValues={initialValues}
|
||||
input={input}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
13
src/pages/tools/list/find-most-popular/meta.ts
Normal file
13
src/pages/tools/list/find-most-popular/meta.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { defineTool } from '@tools/defineTool';
|
||||
import { lazy } from 'react';
|
||||
// import image from '@assets/text.png';
|
||||
|
||||
export const tool = defineTool('list', {
|
||||
name: 'Find most popular',
|
||||
path: 'find-most-popular',
|
||||
// image,
|
||||
description: '',
|
||||
shortDescription: '',
|
||||
keywords: ['find', 'most', 'popular'],
|
||||
component: lazy(() => import('./index'))
|
||||
});
|
||||
114
src/pages/tools/list/find-most-popular/service.ts
Normal file
114
src/pages/tools/list/find-most-popular/service.ts
Normal file
@@ -0,0 +1,114 @@
|
||||
export type SplitOperatorType = 'symbol' | 'regex';
|
||||
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
|
||||
): { [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;
|
||||
}
|
||||
|
||||
// Function that sorts the dict created with dictMaker based on the chosen sorting method
|
||||
function dictSorter(
|
||||
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);
|
||||
}
|
||||
|
||||
// Function that prepares the output of dictSorter based on the chosen display format
|
||||
function displayFormater(
|
||||
dict: { [key: string]: number },
|
||||
displayFormat: DisplayFormat
|
||||
): string[] {
|
||||
const 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;
|
||||
}
|
||||
|
||||
export function TopItemsList(
|
||||
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;
|
||||
}
|
||||
|
||||
// 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 !== '');
|
||||
}
|
||||
|
||||
// Transform the array into dict
|
||||
const unsortedDict = dictMaker(array, ignoreItemCase);
|
||||
|
||||
// Sort the list if required
|
||||
const sortedDict = dictSorter(unsortedDict, sortingMethod);
|
||||
|
||||
// Format the output with desired format
|
||||
const formattedOutput = displayFormater(sortedDict, displayFormat);
|
||||
|
||||
return formattedOutput.join('\n');
|
||||
}
|
||||
110
src/pages/tools/list/find-unique/find-unique.service.test.ts
Normal file
110
src/pages/tools/list/find-unique/find-unique.service.test.ts
Normal file
@@ -0,0 +1,110 @@
|
||||
import { describe, expect } from 'vitest';
|
||||
|
||||
import { findUniqueCompute } from './service';
|
||||
|
||||
describe('TopItemsList Function', () => {
|
||||
test('should return unique items ignoring case sensitivity', () => {
|
||||
const input = 'apple,banana,Apple,orange,Banana,apple';
|
||||
const result = findUniqueCompute(
|
||||
'symbol',
|
||||
',',
|
||||
'\n',
|
||||
input,
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
true
|
||||
);
|
||||
expect(result).toBe('orange');
|
||||
});
|
||||
|
||||
test('should return unique items considering case sensitivity', () => {
|
||||
const input = 'apple,banana,Apple,orange,Banana,apple';
|
||||
const result = findUniqueCompute(
|
||||
'symbol',
|
||||
',',
|
||||
'\n',
|
||||
input,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true
|
||||
);
|
||||
expect(result).toBe('banana\nApple\norange\nBanana');
|
||||
});
|
||||
|
||||
test('should return all unique items ignoring case sensitivity', () => {
|
||||
const input = 'apple,banana,Apple,orange,Banana,apple';
|
||||
const result = findUniqueCompute(
|
||||
'symbol',
|
||||
',',
|
||||
'\n',
|
||||
input,
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
false
|
||||
);
|
||||
expect(result).toBe('apple\nbanana\norange');
|
||||
});
|
||||
|
||||
test('should return all unique items considering case sensitivity', () => {
|
||||
const input = 'apple,banana,Apple,orange,Banana,apple';
|
||||
const result = findUniqueCompute(
|
||||
'symbol',
|
||||
',',
|
||||
'\n',
|
||||
input,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
false
|
||||
);
|
||||
expect(result).toBe('apple\nbanana\nApple\norange\nBanana');
|
||||
});
|
||||
|
||||
test('should handle empty items deletion', () => {
|
||||
const input = 'apple,,banana, ,orange';
|
||||
const result = findUniqueCompute(
|
||||
'symbol',
|
||||
',',
|
||||
'\n',
|
||||
input,
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
false
|
||||
);
|
||||
expect(result).toBe('apple\nbanana\norange');
|
||||
});
|
||||
|
||||
test('should handle trimming items', () => {
|
||||
const input = ' apple , banana , orange ';
|
||||
const result = findUniqueCompute(
|
||||
'symbol',
|
||||
',',
|
||||
'\n',
|
||||
input,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false
|
||||
);
|
||||
expect(result).toBe(' apple \n banana \n orange ');
|
||||
});
|
||||
|
||||
test('should handle regex split', () => {
|
||||
const input = 'apple banana orange';
|
||||
const result = findUniqueCompute(
|
||||
'regex',
|
||||
'\\s+',
|
||||
'\n',
|
||||
input,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false
|
||||
);
|
||||
expect(result).toBe('apple\nbanana\norange');
|
||||
});
|
||||
});
|
||||
158
src/pages/tools/list/find-unique/index.tsx
Normal file
158
src/pages/tools/list/find-unique/index.tsx
Normal file
@@ -0,0 +1,158 @@
|
||||
import { Box } from '@mui/material';
|
||||
import React, { useState } from 'react';
|
||||
import ToolTextInput from '@components/input/ToolTextInput';
|
||||
import ToolTextResult from '@components/result/ToolTextResult';
|
||||
import ToolOptions from '@components/options/ToolOptions';
|
||||
import { findUniqueCompute, 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';
|
||||
|
||||
const initialValues = {
|
||||
splitOperatorType: 'symbol' as SplitOperatorType,
|
||||
splitSeparator: ',',
|
||||
joinSeparator: '\\n',
|
||||
deleteEmptyItems: true,
|
||||
caseSensitive: false,
|
||||
trimItems: true,
|
||||
absolutelyUnique: 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.'
|
||||
}
|
||||
];
|
||||
|
||||
export default function FindUnique() {
|
||||
const [input, setInput] = useState<string>('');
|
||||
const [result, setResult] = useState<string>('');
|
||||
const compute = (optionsValues: typeof initialValues, input: any) => {
|
||||
const {
|
||||
splitOperatorType,
|
||||
splitSeparator,
|
||||
joinSeparator,
|
||||
deleteEmptyItems,
|
||||
trimItems,
|
||||
caseSensitive,
|
||||
absolutelyUnique
|
||||
} = optionsValues;
|
||||
|
||||
setResult(
|
||||
findUniqueCompute(
|
||||
splitOperatorType,
|
||||
splitSeparator,
|
||||
joinSeparator,
|
||||
input,
|
||||
deleteEmptyItems,
|
||||
trimItems,
|
||||
caseSensitive,
|
||||
absolutelyUnique
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<ToolInputAndResult
|
||||
input={
|
||||
<ToolTextInput
|
||||
title={'Input list'}
|
||||
value={input}
|
||||
onChange={setInput}
|
||||
/>
|
||||
}
|
||||
result={<ToolTextResult title={'Unique items'} value={result} />}
|
||||
/>
|
||||
<ToolOptions
|
||||
compute={compute}
|
||||
getGroups={({ values, updateField }) => [
|
||||
{
|
||||
title: 'Input List Delimiter',
|
||||
component: (
|
||||
<Box>
|
||||
{splitOperators.map(({ title, description, type }) => (
|
||||
<SimpleRadio
|
||||
key={type}
|
||||
onClick={() => updateField('splitOperatorType', type)}
|
||||
title={title}
|
||||
description={description}
|
||||
checked={values.splitOperatorType === type}
|
||||
/>
|
||||
))}
|
||||
<TextFieldWithDesc
|
||||
description={'Set a delimiting symbol or regular expression.'}
|
||||
value={values.splitSeparator}
|
||||
onOwnChange={(val) => updateField('splitSeparator', val)}
|
||||
/>
|
||||
</Box>
|
||||
)
|
||||
},
|
||||
{
|
||||
title: 'Output List Delimiter',
|
||||
component: (
|
||||
<Box>
|
||||
<TextFieldWithDesc
|
||||
value={values.joinSeparator}
|
||||
onOwnChange={(value) => updateField('joinSeparator', value)}
|
||||
/>
|
||||
<CheckboxWithDesc
|
||||
title={'Trim top list items'}
|
||||
description={
|
||||
'Remove leading and trailing spaces before comparing items'
|
||||
}
|
||||
checked={values.trimItems}
|
||||
onChange={(value) => updateField('trimItems', value)}
|
||||
/>
|
||||
<CheckboxWithDesc
|
||||
title={'Skip empty items'}
|
||||
description={
|
||||
"Don't include the empty list items in the output."
|
||||
}
|
||||
checked={values.deleteEmptyItems}
|
||||
onChange={(value) => updateField('deleteEmptyItems', value)}
|
||||
/>
|
||||
</Box>
|
||||
)
|
||||
},
|
||||
{
|
||||
title: 'Unique Item Options',
|
||||
component: (
|
||||
<Box>
|
||||
<CheckboxWithDesc
|
||||
title={'Find Absolutely Unique Items'}
|
||||
description={
|
||||
'Display only those items of the list that exist in a single copy.'
|
||||
}
|
||||
checked={values.absolutelyUnique}
|
||||
onChange={(value) => updateField('absolutelyUnique', value)}
|
||||
/>
|
||||
<CheckboxWithDesc
|
||||
title={'Case Sensitive Items'}
|
||||
description={
|
||||
'Output items with different case as unique elements in the list.'
|
||||
}
|
||||
checked={values.caseSensitive}
|
||||
onChange={(value) => updateField('caseSensitive', value)}
|
||||
/>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
]}
|
||||
initialValues={initialValues}
|
||||
input={input}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
13
src/pages/tools/list/find-unique/meta.ts
Normal file
13
src/pages/tools/list/find-unique/meta.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { defineTool } from '@tools/defineTool';
|
||||
import { lazy } from 'react';
|
||||
// import image from '@assets/text.png';
|
||||
|
||||
export const tool = defineTool('list', {
|
||||
name: 'Find unique',
|
||||
path: 'find-unique',
|
||||
// image,
|
||||
description: '',
|
||||
shortDescription: '',
|
||||
keywords: ['find', 'unique'],
|
||||
component: lazy(() => import('./index'))
|
||||
});
|
||||
64
src/pages/tools/list/find-unique/service.ts
Normal file
64
src/pages/tools/list/find-unique/service.ts
Normal file
@@ -0,0 +1,64 @@
|
||||
export type SplitOperatorType = 'symbol' | 'regex';
|
||||
|
||||
// Function that builds the unique items array handling caseSensitive and absolutelyUnique options
|
||||
function uniqueListBuilder(
|
||||
array: string[],
|
||||
caseSensitive: boolean,
|
||||
absolutelyUnique: boolean
|
||||
): string[] {
|
||||
const dict: { [key: string]: number } = {};
|
||||
for (const item of array) {
|
||||
const key = caseSensitive ? item : item.toLowerCase();
|
||||
dict[key] = (dict[key] || 0) + 1;
|
||||
}
|
||||
if (absolutelyUnique) {
|
||||
for (const [key, value] of Object.entries(dict)) {
|
||||
if (value > 1) {
|
||||
delete dict[key];
|
||||
}
|
||||
}
|
||||
}
|
||||
return Object.keys(dict);
|
||||
}
|
||||
|
||||
export function findUniqueCompute(
|
||||
splitOperatorType: SplitOperatorType,
|
||||
splitSeparator: string,
|
||||
joinSeparator: string = '\n',
|
||||
input: string,
|
||||
deleteEmptyItems: boolean,
|
||||
trimItems: boolean,
|
||||
caseSensitive: boolean,
|
||||
absolutelyUnique: 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;
|
||||
}
|
||||
|
||||
// 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 !== '');
|
||||
}
|
||||
|
||||
// Format the output with desired format
|
||||
const uniqueListItems = uniqueListBuilder(
|
||||
array,
|
||||
caseSensitive,
|
||||
absolutelyUnique
|
||||
);
|
||||
|
||||
return uniqueListItems.join(joinSeparator);
|
||||
}
|
||||
99
src/pages/tools/list/group/group.service.test.ts
Normal file
99
src/pages/tools/list/group/group.service.test.ts
Normal file
@@ -0,0 +1,99 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import { groupList, SplitOperatorType } from './service';
|
||||
|
||||
describe('groupList', () => {
|
||||
it('splits by symbol, groups, pads, and formats correctly', () => {
|
||||
const input = 'a,b,c,d,e,f,g,h,i,j';
|
||||
const splitOperatorType: SplitOperatorType = 'symbol';
|
||||
const splitSeparator = ',';
|
||||
const groupNumber = 3;
|
||||
const itemSeparator = '-';
|
||||
const leftWrap = '[';
|
||||
const rightWrap = ']';
|
||||
const groupSeparator = ' | ';
|
||||
const deleteEmptyItems = false;
|
||||
const padNonFullGroup = true;
|
||||
const paddingChar = 'x';
|
||||
|
||||
const expectedOutput = '[a-b-c] | [d-e-f] | [g-h-i] | [j-x-x]';
|
||||
|
||||
const result = groupList(
|
||||
splitOperatorType,
|
||||
splitSeparator,
|
||||
input,
|
||||
groupNumber,
|
||||
itemSeparator,
|
||||
leftWrap,
|
||||
rightWrap,
|
||||
groupSeparator,
|
||||
deleteEmptyItems,
|
||||
padNonFullGroup,
|
||||
paddingChar
|
||||
);
|
||||
|
||||
expect(result).toBe(expectedOutput);
|
||||
});
|
||||
|
||||
it('handles regex split, no padding, and formats correctly', () => {
|
||||
const input = 'a1b2c3d4e5f6g7h8i9j';
|
||||
const splitOperatorType: SplitOperatorType = 'regex';
|
||||
const splitSeparator = '\\d';
|
||||
const groupNumber = 4;
|
||||
const itemSeparator = ',';
|
||||
const leftWrap = '(';
|
||||
const rightWrap = ')';
|
||||
const groupSeparator = ' / ';
|
||||
const deleteEmptyItems = true;
|
||||
const padNonFullGroup = false;
|
||||
|
||||
const expectedOutput = '(a,b,c,d) / (e,f,g,h) / (i,j)';
|
||||
|
||||
const result = groupList(
|
||||
splitOperatorType,
|
||||
splitSeparator,
|
||||
input,
|
||||
groupNumber,
|
||||
itemSeparator,
|
||||
leftWrap,
|
||||
rightWrap,
|
||||
groupSeparator,
|
||||
deleteEmptyItems,
|
||||
padNonFullGroup
|
||||
);
|
||||
|
||||
expect(result).toBe(expectedOutput);
|
||||
});
|
||||
|
||||
it('handles empty items removal and padd the last group with a z', () => {
|
||||
const input = 'a,,b,,c,,d,,e,,';
|
||||
const splitOperatorType: SplitOperatorType = 'symbol';
|
||||
const splitSeparator = ',';
|
||||
const groupNumber = 2;
|
||||
const itemSeparator = ':';
|
||||
const leftWrap = '<';
|
||||
const rightWrap = '>';
|
||||
const groupSeparator = ' & ';
|
||||
const deleteEmptyItems = true;
|
||||
const padNonFullGroup = true;
|
||||
const paddingChar = 'z';
|
||||
|
||||
const expectedOutput = '<a:b> & <c:d> & <e:z>';
|
||||
|
||||
const result = groupList(
|
||||
splitOperatorType,
|
||||
splitSeparator,
|
||||
input,
|
||||
groupNumber,
|
||||
itemSeparator,
|
||||
leftWrap,
|
||||
rightWrap,
|
||||
groupSeparator,
|
||||
deleteEmptyItems,
|
||||
padNonFullGroup,
|
||||
paddingChar
|
||||
);
|
||||
|
||||
expect(result).toBe(expectedOutput);
|
||||
});
|
||||
});
|
||||
183
src/pages/tools/list/group/index.tsx
Normal file
183
src/pages/tools/list/group/index.tsx
Normal file
@@ -0,0 +1,183 @@
|
||||
import { Box } from '@mui/material';
|
||||
import React, { useState } from 'react';
|
||||
import ToolTextInput from '@components/input/ToolTextInput';
|
||||
import ToolTextResult from '@components/result/ToolTextResult';
|
||||
import ToolOptions from '@components/options/ToolOptions';
|
||||
import { groupList, 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 { formatNumber } from '../../../../utils/number';
|
||||
|
||||
const initialValues = {
|
||||
splitOperatorType: 'symbol' as SplitOperatorType,
|
||||
splitSeparator: ',',
|
||||
groupNumber: 2,
|
||||
itemSeparator: ',',
|
||||
leftWrap: '[',
|
||||
rightWrap: ']',
|
||||
groupSeparator: '\\n',
|
||||
deleteEmptyItems: true,
|
||||
padNonFullGroup: false,
|
||||
paddingChar: '...'
|
||||
};
|
||||
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.'
|
||||
}
|
||||
];
|
||||
|
||||
export default function FindUnique() {
|
||||
const [input, setInput] = useState<string>('');
|
||||
const [result, setResult] = useState<string>('');
|
||||
const compute = (optionsValues: typeof initialValues, input: any) => {
|
||||
const {
|
||||
splitOperatorType,
|
||||
splitSeparator,
|
||||
groupNumber,
|
||||
itemSeparator,
|
||||
leftWrap,
|
||||
rightWrap,
|
||||
groupSeparator,
|
||||
deleteEmptyItems,
|
||||
padNonFullGroup,
|
||||
paddingChar
|
||||
} = optionsValues;
|
||||
|
||||
setResult(
|
||||
groupList(
|
||||
splitOperatorType,
|
||||
splitSeparator,
|
||||
input,
|
||||
groupNumber,
|
||||
itemSeparator,
|
||||
leftWrap,
|
||||
rightWrap,
|
||||
groupSeparator,
|
||||
deleteEmptyItems,
|
||||
padNonFullGroup,
|
||||
paddingChar
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<ToolInputAndResult
|
||||
input={
|
||||
<ToolTextInput
|
||||
title={'Input list'}
|
||||
value={input}
|
||||
onChange={setInput}
|
||||
/>
|
||||
}
|
||||
result={<ToolTextResult title={'Grouped items'} value={result} />}
|
||||
/>
|
||||
<ToolOptions
|
||||
compute={compute}
|
||||
getGroups={({ values, updateField }) => [
|
||||
{
|
||||
title: 'Input Item Separator',
|
||||
component: (
|
||||
<Box>
|
||||
{splitOperators.map(({ title, description, type }) => (
|
||||
<SimpleRadio
|
||||
key={type}
|
||||
onClick={() => updateField('splitOperatorType', type)}
|
||||
title={title}
|
||||
description={description}
|
||||
checked={values.splitOperatorType === type}
|
||||
/>
|
||||
))}
|
||||
<TextFieldWithDesc
|
||||
description={'Set a delimiting symbol or regular expression.'}
|
||||
value={values.splitSeparator}
|
||||
onOwnChange={(val) => updateField('splitSeparator', val)}
|
||||
/>
|
||||
</Box>
|
||||
)
|
||||
},
|
||||
{
|
||||
title: 'Group Size and Separators',
|
||||
component: (
|
||||
<Box>
|
||||
<TextFieldWithDesc
|
||||
value={values.groupNumber}
|
||||
description={'Number of items in a group'}
|
||||
type={'number'}
|
||||
onOwnChange={(value) =>
|
||||
updateField('groupNumber', formatNumber(value, 1))
|
||||
}
|
||||
/>
|
||||
<TextFieldWithDesc
|
||||
value={values.itemSeparator}
|
||||
description={'Item separator character'}
|
||||
onOwnChange={(value) => updateField('itemSeparator', value)}
|
||||
/>
|
||||
<TextFieldWithDesc
|
||||
value={values.groupSeparator}
|
||||
description={'Group separator character'}
|
||||
onOwnChange={(value) => updateField('groupSeparator', value)}
|
||||
/>
|
||||
<TextFieldWithDesc
|
||||
value={values.leftWrap}
|
||||
description={"Group's left wrap symbol."}
|
||||
onOwnChange={(value) => updateField('leftWrap', value)}
|
||||
/>
|
||||
<TextFieldWithDesc
|
||||
value={values.rightWrap}
|
||||
description={"Group's right wrap symbol."}
|
||||
onOwnChange={(value) => updateField('rightWrap', value)}
|
||||
/>
|
||||
</Box>
|
||||
)
|
||||
},
|
||||
{
|
||||
title: 'Empty Items and Padding',
|
||||
component: (
|
||||
<Box>
|
||||
<CheckboxWithDesc
|
||||
title={'Delete Empty Items'}
|
||||
description={
|
||||
"Ignore empty items and don't include them in the groups."
|
||||
}
|
||||
checked={values.deleteEmptyItems}
|
||||
onChange={(value) => updateField('deleteEmptyItems', value)}
|
||||
/>
|
||||
<CheckboxWithDesc
|
||||
title={'Pad Non-full Groups'}
|
||||
description={
|
||||
'Fill non-full groups with a custom item (enter below).'
|
||||
}
|
||||
checked={values.padNonFullGroup}
|
||||
onChange={(value) => updateField('padNonFullGroup', value)}
|
||||
/>
|
||||
<TextFieldWithDesc
|
||||
value={values.paddingChar}
|
||||
description={
|
||||
'Use this character or item to pad non-full groups.'
|
||||
}
|
||||
onOwnChange={(value) => updateField('paddingChar', value)}
|
||||
/>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
]}
|
||||
initialValues={initialValues}
|
||||
input={input}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
13
src/pages/tools/list/group/meta.ts
Normal file
13
src/pages/tools/list/group/meta.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { defineTool } from '@tools/defineTool';
|
||||
import { lazy } from 'react';
|
||||
// import image from '@assets/text.png';
|
||||
|
||||
export const tool = defineTool('list', {
|
||||
name: 'Group',
|
||||
path: 'group',
|
||||
// image,
|
||||
description: '',
|
||||
shortDescription: '',
|
||||
keywords: ['group'],
|
||||
component: lazy(() => import('./index'))
|
||||
});
|
||||
94
src/pages/tools/list/group/service.ts
Normal file
94
src/pages/tools/list/group/service.ts
Normal file
@@ -0,0 +1,94 @@
|
||||
export type SplitOperatorType = 'symbol' | 'regex';
|
||||
|
||||
// function that split the array into an array of subarray of desired length
|
||||
function groupMaker(array: string[], groupNumber: number): string[][] {
|
||||
const result: string[][] = [];
|
||||
for (let i = 0; i < array.length; i += groupNumber) {
|
||||
result.push(array.slice(i, i + groupNumber));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
// function use to handle the case paddingNonFullGroup is enable
|
||||
function groupFiller(
|
||||
array: string[][],
|
||||
groupNumber: number,
|
||||
padNonFullGroup: boolean,
|
||||
paddingChar: string = ''
|
||||
): string[][] {
|
||||
if (padNonFullGroup) {
|
||||
const lastSubArray: string[] = array[array.length - 1];
|
||||
if (lastSubArray.length < groupNumber) {
|
||||
for (let i = lastSubArray.length; i < groupNumber; i++) {
|
||||
lastSubArray.push(paddingChar);
|
||||
}
|
||||
}
|
||||
array[array.length - 1] = lastSubArray;
|
||||
}
|
||||
return array;
|
||||
}
|
||||
|
||||
// function that join with the item separator and wrap with left and right each subArray of the Array
|
||||
function groupJoinerAndWrapper(
|
||||
array: string[][],
|
||||
itemSeparator: string = '',
|
||||
leftWrap: string = '',
|
||||
rightWrap: string = ''
|
||||
): string[] {
|
||||
return array.map((subArray) => {
|
||||
return leftWrap + subArray.join(itemSeparator) + rightWrap;
|
||||
});
|
||||
}
|
||||
|
||||
export function groupList(
|
||||
splitOperatorType: SplitOperatorType,
|
||||
splitSeparator: string,
|
||||
input: string,
|
||||
groupNumber: number,
|
||||
itemSeparator: string = '',
|
||||
leftWrap: string = '',
|
||||
rightWrap: string = '',
|
||||
groupSeparator: string,
|
||||
deleteEmptyItems: boolean,
|
||||
padNonFullGroup: boolean,
|
||||
paddingChar: string = ''
|
||||
): string {
|
||||
let array: string[];
|
||||
let splitedArray: string[][];
|
||||
let fullSplitedArray: string[][];
|
||||
let result: string[];
|
||||
switch (splitOperatorType) {
|
||||
case 'symbol':
|
||||
array = input.split(splitSeparator);
|
||||
break;
|
||||
case 'regex':
|
||||
array = input.split(new RegExp(splitSeparator));
|
||||
break;
|
||||
}
|
||||
// delete empty items after intial split
|
||||
if (deleteEmptyItems) {
|
||||
array = array.filter((item) => item !== '');
|
||||
}
|
||||
|
||||
// split the input into an array of subArray with the desired length
|
||||
splitedArray = groupMaker(array, groupNumber);
|
||||
|
||||
// fill the last subArray is PadNonFullGroup is enabled
|
||||
fullSplitedArray = groupFiller(
|
||||
splitedArray,
|
||||
groupNumber,
|
||||
padNonFullGroup,
|
||||
paddingChar
|
||||
);
|
||||
|
||||
// get the list of formated subArray with the item separator and left and right wrapper
|
||||
result = groupJoinerAndWrapper(
|
||||
fullSplitedArray,
|
||||
itemSeparator,
|
||||
leftWrap,
|
||||
rightWrap
|
||||
);
|
||||
|
||||
// finnaly join the group separator before returning
|
||||
return result.join(groupSeparator);
|
||||
}
|
||||
25
src/pages/tools/list/index.ts
Normal file
25
src/pages/tools/list/index.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import { tool as listDuplicate } from './duplicate/meta';
|
||||
import { tool as listUnwrap } from './unwrap/meta';
|
||||
import { tool as listReverse } from './reverse/meta';
|
||||
import { tool as listFindUnique } from './find-unique/meta';
|
||||
import { tool as listFindMostPopular } from './find-most-popular/meta';
|
||||
import { tool as listGroup } from './group/meta';
|
||||
import { tool as listWrap } from './wrap/meta';
|
||||
import { tool as listRotate } from './rotate/meta';
|
||||
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,
|
||||
listUnwrap,
|
||||
listReverse,
|
||||
listFindUnique,
|
||||
listFindMostPopular,
|
||||
listGroup,
|
||||
listWrap,
|
||||
listRotate,
|
||||
listShuffle,
|
||||
listTruncate,
|
||||
listDuplicate
|
||||
];
|
||||
105
src/pages/tools/list/reverse/index.tsx
Normal file
105
src/pages/tools/list/reverse/index.tsx
Normal file
@@ -0,0 +1,105 @@
|
||||
import { Box } from '@mui/material';
|
||||
import React, { useState } from 'react';
|
||||
import ToolTextInput from '@components/input/ToolTextInput';
|
||||
import ToolTextResult from '@components/result/ToolTextResult';
|
||||
import ToolOptions from '@components/options/ToolOptions';
|
||||
import { reverseList, SplitOperatorType } from './service';
|
||||
import ToolInputAndResult from '@components/ToolInputAndResult';
|
||||
import SimpleRadio from '@components/options/SimpleRadio';
|
||||
import TextFieldWithDesc from '@components/options/TextFieldWithDesc';
|
||||
|
||||
const initialValues = {
|
||||
splitOperatorType: 'symbol' as SplitOperatorType,
|
||||
splitSeparator: ',',
|
||||
joinSeparator: '\\n'
|
||||
};
|
||||
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.'
|
||||
}
|
||||
];
|
||||
|
||||
export default function Reverse() {
|
||||
const [input, setInput] = useState<string>('');
|
||||
const [result, setResult] = useState<string>('');
|
||||
const compute = (optionsValues: typeof initialValues, input: any) => {
|
||||
const { splitOperatorType, splitSeparator, joinSeparator } = optionsValues;
|
||||
|
||||
setResult(
|
||||
reverseList(splitOperatorType, splitSeparator, joinSeparator, input)
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<ToolInputAndResult
|
||||
input={
|
||||
<ToolTextInput
|
||||
title={'Input list'}
|
||||
value={input}
|
||||
onChange={setInput}
|
||||
/>
|
||||
}
|
||||
result={<ToolTextResult title={'Reversed list'} value={result} />}
|
||||
/>
|
||||
<ToolOptions
|
||||
compute={compute}
|
||||
getGroups={({ values, updateField }) => [
|
||||
{
|
||||
title: 'Splitter Mode',
|
||||
component: (
|
||||
<Box>
|
||||
{splitOperators.map(({ title, description, type }) => (
|
||||
<SimpleRadio
|
||||
key={type}
|
||||
onClick={() => updateField('splitOperatorType', type)}
|
||||
title={title}
|
||||
description={description}
|
||||
checked={values.splitOperatorType === type}
|
||||
/>
|
||||
))}
|
||||
</Box>
|
||||
)
|
||||
},
|
||||
{
|
||||
title: 'Item Separator',
|
||||
component: (
|
||||
<Box>
|
||||
<TextFieldWithDesc
|
||||
description={'Set a delimiting symbol or regular expression.'}
|
||||
value={values.splitSeparator}
|
||||
onOwnChange={(val) => updateField('splitSeparator', val)}
|
||||
/>
|
||||
</Box>
|
||||
)
|
||||
},
|
||||
{
|
||||
title: 'Output List Options',
|
||||
component: (
|
||||
<Box>
|
||||
<TextFieldWithDesc
|
||||
description={'Output list item separator.'}
|
||||
value={values.joinSeparator}
|
||||
onOwnChange={(val) => updateField('joinSeparator', val)}
|
||||
/>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
]}
|
||||
initialValues={initialValues}
|
||||
input={input}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
13
src/pages/tools/list/reverse/meta.ts
Normal file
13
src/pages/tools/list/reverse/meta.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { defineTool } from '@tools/defineTool';
|
||||
import { lazy } from 'react';
|
||||
// import image from '@assets/text.png';
|
||||
|
||||
export const tool = defineTool('list', {
|
||||
name: 'Reverse',
|
||||
path: 'reverse',
|
||||
// image,
|
||||
description: '',
|
||||
shortDescription: '',
|
||||
keywords: ['reverse'],
|
||||
component: lazy(() => import('./index'))
|
||||
});
|
||||
28
src/pages/tools/list/reverse/reverse.service.test.ts
Normal file
28
src/pages/tools/list/reverse/reverse.service.test.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import { describe, expect } from 'vitest';
|
||||
import { reverseList } from './service';
|
||||
|
||||
describe('reverseList Function', () => {
|
||||
test('should reverse items split by symbol', () => {
|
||||
const input = 'apple,banana,orange';
|
||||
const result = reverseList('symbol', ',', '\n', input);
|
||||
expect(result).toBe('orange\nbanana\napple');
|
||||
});
|
||||
|
||||
test('should reverse items split by regex', () => {
|
||||
const input = 'apple banana orange';
|
||||
const result = reverseList('regex', '\\s+', '\n', input);
|
||||
expect(result).toBe('orange\nbanana\napple');
|
||||
});
|
||||
|
||||
test('should handle empty input', () => {
|
||||
const input = '';
|
||||
const result = reverseList('symbol', ',', '\n', input);
|
||||
expect(result).toBe('');
|
||||
});
|
||||
|
||||
test('should handle join separator', () => {
|
||||
const input = 'apple,banana,orange';
|
||||
const result = reverseList('symbol', ',', ', ', input);
|
||||
expect(result).toBe('orange, banana, apple');
|
||||
});
|
||||
});
|
||||
23
src/pages/tools/list/reverse/service.ts
Normal file
23
src/pages/tools/list/reverse/service.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
export type SplitOperatorType = 'symbol' | 'regex';
|
||||
|
||||
export function reverseList(
|
||||
splitOperatorType: SplitOperatorType,
|
||||
splitSeparator: string,
|
||||
joinSeparator: string = '\n',
|
||||
input: string
|
||||
): 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;
|
||||
}
|
||||
|
||||
const reversedList = array.reverse();
|
||||
return reversedList.join(joinSeparator);
|
||||
}
|
||||
153
src/pages/tools/list/rotate/index.tsx
Normal file
153
src/pages/tools/list/rotate/index.tsx
Normal file
@@ -0,0 +1,153 @@
|
||||
import { Box } from '@mui/material';
|
||||
import React, { useState } from 'react';
|
||||
import ToolTextInput from '@components/input/ToolTextInput';
|
||||
import ToolTextResult from '@components/result/ToolTextResult';
|
||||
import ToolOptions from '@components/options/ToolOptions';
|
||||
import { rotateList, SplitOperatorType } from './service';
|
||||
import ToolInputAndResult from '@components/ToolInputAndResult';
|
||||
import SimpleRadio from '@components/options/SimpleRadio';
|
||||
import TextFieldWithDesc from '@components/options/TextFieldWithDesc';
|
||||
import { formatNumber } from '../../../../utils/number';
|
||||
|
||||
const initialValues = {
|
||||
splitOperatorType: 'symbol' as SplitOperatorType,
|
||||
input: '',
|
||||
splitSeparator: ',',
|
||||
joinSeparator: ',',
|
||||
right: true,
|
||||
step: 1
|
||||
};
|
||||
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 rotationDirections: {
|
||||
title: string;
|
||||
description: string;
|
||||
value: boolean;
|
||||
}[] = [
|
||||
{
|
||||
title: 'Rotate forward',
|
||||
description:
|
||||
'Rotate list items to the right. (Down if a vertical column list.)',
|
||||
value: true
|
||||
},
|
||||
{
|
||||
title: 'Rotate backward',
|
||||
description:
|
||||
'Rotate list items to the left. (Up if a vertical column list.)',
|
||||
value: false
|
||||
}
|
||||
];
|
||||
|
||||
export default function Rotate() {
|
||||
const [input, setInput] = useState<string>('');
|
||||
const [result, setResult] = useState<string>('');
|
||||
const compute = (optionsValues: typeof initialValues, input: any) => {
|
||||
const { splitOperatorType, splitSeparator, joinSeparator, right, step } =
|
||||
optionsValues;
|
||||
|
||||
setResult(
|
||||
rotateList(
|
||||
splitOperatorType,
|
||||
input,
|
||||
splitSeparator,
|
||||
joinSeparator,
|
||||
right,
|
||||
step
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<ToolInputAndResult
|
||||
input={
|
||||
<ToolTextInput
|
||||
title={'Input list'}
|
||||
value={input}
|
||||
onChange={setInput}
|
||||
/>
|
||||
}
|
||||
result={<ToolTextResult title={'Rotated list'} value={result} />}
|
||||
/>
|
||||
<ToolOptions
|
||||
compute={compute}
|
||||
getGroups={({ values, updateField }) => [
|
||||
{
|
||||
title: 'Item split mode',
|
||||
component: (
|
||||
<Box>
|
||||
{splitOperators.map(({ title, description, type }) => (
|
||||
<SimpleRadio
|
||||
key={type}
|
||||
onClick={() => updateField('splitOperatorType', type)}
|
||||
title={title}
|
||||
description={description}
|
||||
checked={values.splitOperatorType === type}
|
||||
/>
|
||||
))}
|
||||
<TextFieldWithDesc
|
||||
description={'Set a delimiting symbol or regular expression.'}
|
||||
value={values.splitSeparator}
|
||||
onOwnChange={(val) => updateField('splitSeparator', val)}
|
||||
/>
|
||||
</Box>
|
||||
)
|
||||
},
|
||||
{
|
||||
title: 'Rotation Direction and Count',
|
||||
component: (
|
||||
<Box>
|
||||
{rotationDirections.map(({ title, description, value }) => (
|
||||
<SimpleRadio
|
||||
key={`${value}`}
|
||||
onClick={() => updateField('right', value)}
|
||||
title={title}
|
||||
description={description}
|
||||
checked={values.right === value}
|
||||
/>
|
||||
))}
|
||||
<TextFieldWithDesc
|
||||
description={'Number of items to rotate'}
|
||||
value={values.step}
|
||||
onOwnChange={(val) =>
|
||||
updateField('step', formatNumber(val, 1))
|
||||
}
|
||||
/>
|
||||
</Box>
|
||||
)
|
||||
},
|
||||
{
|
||||
title: 'Rotated List Joining Symbol',
|
||||
component: (
|
||||
<Box>
|
||||
<TextFieldWithDesc
|
||||
value={values.joinSeparator}
|
||||
onOwnChange={(value) => updateField('joinSeparator', value)}
|
||||
description={
|
||||
'Enter the character that goes between items in the rotated list.'
|
||||
}
|
||||
/>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
]}
|
||||
initialValues={initialValues}
|
||||
input={input}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
13
src/pages/tools/list/rotate/meta.ts
Normal file
13
src/pages/tools/list/rotate/meta.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { defineTool } from '@tools/defineTool';
|
||||
import { lazy } from 'react';
|
||||
// import image from '@assets/text.png';
|
||||
|
||||
export const tool = defineTool('list', {
|
||||
name: 'Rotate',
|
||||
path: 'rotate',
|
||||
// image,
|
||||
description: '',
|
||||
shortDescription: '',
|
||||
keywords: ['rotate'],
|
||||
component: lazy(() => import('./index'))
|
||||
});
|
||||
105
src/pages/tools/list/rotate/rotate.service.test.ts
Normal file
105
src/pages/tools/list/rotate/rotate.service.test.ts
Normal file
@@ -0,0 +1,105 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { rotateList, SplitOperatorType } from './service';
|
||||
|
||||
describe('rotate function', () => {
|
||||
it('should rotate right side if right is set to true', () => {
|
||||
const input: string = 'apple, pineaple, lemon, orange, mango';
|
||||
const splitOperatorType: SplitOperatorType = 'symbol';
|
||||
const splitSeparator = ', ';
|
||||
const joinSeparator = ' ';
|
||||
const step = 1;
|
||||
const right = true;
|
||||
|
||||
const result = rotateList(
|
||||
splitOperatorType,
|
||||
input,
|
||||
splitSeparator,
|
||||
joinSeparator,
|
||||
right,
|
||||
|
||||
step
|
||||
);
|
||||
|
||||
expect(result).toBe('mango apple pineaple lemon orange');
|
||||
});
|
||||
|
||||
it('should rotate left side if right is set to true', () => {
|
||||
const input: string = 'apple, pineaple, lemon, orange, mango';
|
||||
const splitOperatorType: SplitOperatorType = 'symbol';
|
||||
const splitSeparator = ', ';
|
||||
const joinSeparator = ' ';
|
||||
const step = 1;
|
||||
const right = false;
|
||||
|
||||
const result = rotateList(
|
||||
splitOperatorType,
|
||||
input,
|
||||
splitSeparator,
|
||||
joinSeparator,
|
||||
right,
|
||||
|
||||
step
|
||||
);
|
||||
|
||||
expect(result).toBe('pineaple lemon orange mango apple');
|
||||
});
|
||||
|
||||
it('should rotate left side with 2 step if right is set to true', () => {
|
||||
const input: string = 'apple, pineaple, lemon, orange, mango';
|
||||
const splitOperatorType: SplitOperatorType = 'symbol';
|
||||
const splitSeparator = ', ';
|
||||
const joinSeparator = ' ';
|
||||
const step = 2;
|
||||
const right = false;
|
||||
|
||||
const result = rotateList(
|
||||
splitOperatorType,
|
||||
input,
|
||||
splitSeparator,
|
||||
joinSeparator,
|
||||
right,
|
||||
|
||||
step
|
||||
);
|
||||
|
||||
expect(result).toBe('lemon orange mango apple pineaple');
|
||||
});
|
||||
|
||||
it('should raise an error if step is negative', () => {
|
||||
const input: string = 'apple, pineaple, lemon, orange, mango';
|
||||
const splitOperatorType: SplitOperatorType = 'symbol';
|
||||
const splitSeparator = ', ';
|
||||
const joinSeparator = ' ';
|
||||
const step = -2;
|
||||
const right = false;
|
||||
|
||||
expect(() => {
|
||||
rotateList(
|
||||
splitOperatorType,
|
||||
input,
|
||||
splitSeparator,
|
||||
joinSeparator,
|
||||
right,
|
||||
step
|
||||
);
|
||||
}).toThrowError('Rotation step must be greater than zero.');
|
||||
});
|
||||
|
||||
it('should raise an error if step is undefined', () => {
|
||||
const input: string = 'apple, pineaple, lemon, orange, mango';
|
||||
const splitOperatorType: SplitOperatorType = 'symbol';
|
||||
const splitSeparator = ', ';
|
||||
const joinSeparator = ' ';
|
||||
const right = false;
|
||||
|
||||
expect(() => {
|
||||
rotateList(
|
||||
splitOperatorType,
|
||||
input,
|
||||
splitSeparator,
|
||||
joinSeparator,
|
||||
right
|
||||
);
|
||||
}).toThrowError('Rotation step contains non-digits.');
|
||||
});
|
||||
});
|
||||
48
src/pages/tools/list/rotate/service.ts
Normal file
48
src/pages/tools/list/rotate/service.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
export type SplitOperatorType = 'symbol' | 'regex';
|
||||
|
||||
function rotateArray(array: string[], step: number, right: boolean): string[] {
|
||||
const length = array.length;
|
||||
|
||||
// Normalize the step to be within the bounds of the array length
|
||||
const normalizedPositions = ((step % length) + length) % length;
|
||||
|
||||
if (right) {
|
||||
// Rotate right
|
||||
return array
|
||||
.slice(-normalizedPositions)
|
||||
.concat(array.slice(0, -normalizedPositions));
|
||||
} else {
|
||||
// Rotate left
|
||||
return array
|
||||
.slice(normalizedPositions)
|
||||
.concat(array.slice(0, normalizedPositions));
|
||||
}
|
||||
}
|
||||
|
||||
export function rotateList(
|
||||
splitOperatorType: SplitOperatorType,
|
||||
input: string,
|
||||
splitSeparator: string,
|
||||
joinSeparator: string,
|
||||
right: boolean,
|
||||
step?: number
|
||||
): string {
|
||||
let array: string[];
|
||||
let rotatedArray: string[];
|
||||
switch (splitOperatorType) {
|
||||
case 'symbol':
|
||||
array = input.split(splitSeparator);
|
||||
break;
|
||||
case 'regex':
|
||||
array = input.split(new RegExp(splitSeparator));
|
||||
break;
|
||||
}
|
||||
if (step !== undefined) {
|
||||
if (step <= 0) {
|
||||
throw new Error('Rotation step must be greater than zero.');
|
||||
}
|
||||
rotatedArray = rotateArray(array, step, right);
|
||||
return rotatedArray.join(joinSeparator);
|
||||
}
|
||||
throw new Error('Rotation step contains non-digits.');
|
||||
}
|
||||
119
src/pages/tools/list/shuffle/index.tsx
Normal file
119
src/pages/tools/list/shuffle/index.tsx
Normal file
@@ -0,0 +1,119 @@
|
||||
import { Box } from '@mui/material';
|
||||
import React, { useState } from 'react';
|
||||
import ToolTextInput from '@components/input/ToolTextInput';
|
||||
import ToolTextResult from '@components/result/ToolTextResult';
|
||||
import ToolOptions from '@components/options/ToolOptions';
|
||||
import { shuffleList, SplitOperatorType } from './service';
|
||||
import ToolInputAndResult from '@components/ToolInputAndResult';
|
||||
import SimpleRadio from '@components/options/SimpleRadio';
|
||||
import TextFieldWithDesc from '@components/options/TextFieldWithDesc';
|
||||
import { isNumber } from '../../../../utils/string';
|
||||
|
||||
const initialValues = {
|
||||
splitOperatorType: 'symbol' as SplitOperatorType,
|
||||
splitSeparator: ',',
|
||||
joinSeparator: ',',
|
||||
length: ''
|
||||
};
|
||||
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.'
|
||||
}
|
||||
];
|
||||
|
||||
export default function Shuffle() {
|
||||
const [input, setInput] = useState<string>('');
|
||||
const [result, setResult] = useState<string>('');
|
||||
const compute = (optionsValues: typeof initialValues, input: any) => {
|
||||
const { splitOperatorType, splitSeparator, joinSeparator, length } =
|
||||
optionsValues;
|
||||
|
||||
setResult(
|
||||
shuffleList(
|
||||
splitOperatorType,
|
||||
input,
|
||||
splitSeparator,
|
||||
joinSeparator,
|
||||
isNumber(length) ? Number(length) : undefined
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<ToolInputAndResult
|
||||
input={
|
||||
<ToolTextInput
|
||||
title={'Input list'}
|
||||
value={input}
|
||||
onChange={setInput}
|
||||
/>
|
||||
}
|
||||
result={<ToolTextResult title={'Shuffled list'} value={result} />}
|
||||
/>
|
||||
<ToolOptions
|
||||
compute={compute}
|
||||
getGroups={({ values, updateField }) => [
|
||||
{
|
||||
title: 'Input list separator',
|
||||
component: (
|
||||
<Box>
|
||||
{splitOperators.map(({ title, description, type }) => (
|
||||
<SimpleRadio
|
||||
key={type}
|
||||
onClick={() => updateField('splitOperatorType', type)}
|
||||
title={title}
|
||||
description={description}
|
||||
checked={values.splitOperatorType === type}
|
||||
/>
|
||||
))}
|
||||
<TextFieldWithDesc
|
||||
description={'Set a delimiting symbol or regular expression.'}
|
||||
value={values.splitSeparator}
|
||||
onOwnChange={(val) => updateField('splitSeparator', val)}
|
||||
/>
|
||||
</Box>
|
||||
)
|
||||
},
|
||||
{
|
||||
title: 'Shuffled List Length',
|
||||
component: (
|
||||
<Box>
|
||||
<TextFieldWithDesc
|
||||
description={'Output this many random items'}
|
||||
value={values.length}
|
||||
onOwnChange={(val) => updateField('length', val)}
|
||||
/>
|
||||
</Box>
|
||||
)
|
||||
},
|
||||
{
|
||||
title: 'Shuffled List Separator',
|
||||
component: (
|
||||
<Box>
|
||||
<TextFieldWithDesc
|
||||
value={values.joinSeparator}
|
||||
onOwnChange={(value) => updateField('joinSeparator', value)}
|
||||
description={'Use this separator in the randomized list.'}
|
||||
/>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
]}
|
||||
initialValues={initialValues}
|
||||
input={input}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
13
src/pages/tools/list/shuffle/meta.ts
Normal file
13
src/pages/tools/list/shuffle/meta.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { defineTool } from '@tools/defineTool';
|
||||
import { lazy } from 'react';
|
||||
// import image from '@assets/text.png';
|
||||
|
||||
export const tool = defineTool('list', {
|
||||
name: 'Shuffle',
|
||||
path: 'shuffle',
|
||||
// image,
|
||||
description: '',
|
||||
shortDescription: '',
|
||||
keywords: ['shuffle'],
|
||||
component: lazy(() => import('./index'))
|
||||
});
|
||||
38
src/pages/tools/list/shuffle/service.ts
Normal file
38
src/pages/tools/list/shuffle/service.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
export type SplitOperatorType = 'symbol' | 'regex';
|
||||
|
||||
// function that randomize the array
|
||||
function shuffleArray(array: string[]): string[] {
|
||||
const shuffledArray = array.slice(); // Create a copy of the array
|
||||
for (let i = shuffledArray.length - 1; i > 0; i--) {
|
||||
const j = Math.floor(Math.random() * (i + 1));
|
||||
[shuffledArray[i], shuffledArray[j]] = [shuffledArray[j], shuffledArray[i]];
|
||||
}
|
||||
return shuffledArray;
|
||||
}
|
||||
|
||||
export function shuffleList(
|
||||
splitOperatorType: SplitOperatorType,
|
||||
input: string,
|
||||
splitSeparator: string,
|
||||
joinSeparator: string,
|
||||
length?: number // "?" is to handle the case the user let the input blank
|
||||
): string {
|
||||
let array: string[];
|
||||
let shuffledArray: string[];
|
||||
switch (splitOperatorType) {
|
||||
case 'symbol':
|
||||
array = input.split(splitSeparator);
|
||||
break;
|
||||
case 'regex':
|
||||
array = input.split(new RegExp(splitSeparator));
|
||||
break;
|
||||
}
|
||||
shuffledArray = shuffleArray(array);
|
||||
if (length !== undefined) {
|
||||
if (length <= 0) {
|
||||
throw new Error('Length value must be a positive number.');
|
||||
}
|
||||
return shuffledArray.slice(0, length).join(joinSeparator);
|
||||
}
|
||||
return shuffledArray.join(joinSeparator);
|
||||
}
|
||||
89
src/pages/tools/list/shuffle/shuffle.service.test.ts
Normal file
89
src/pages/tools/list/shuffle/shuffle.service.test.ts
Normal file
@@ -0,0 +1,89 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { shuffleList, SplitOperatorType } from './service';
|
||||
|
||||
describe('shuffle function', () => {
|
||||
it('should be a 4 length list if no length value defined ', () => {
|
||||
const input: string = 'apple, pineaple, lemon, orange';
|
||||
const splitOperatorType: SplitOperatorType = 'symbol';
|
||||
const splitSeparator = ', ';
|
||||
const joinSeparator = ' ';
|
||||
|
||||
const result = shuffleList(
|
||||
splitOperatorType,
|
||||
input,
|
||||
splitSeparator,
|
||||
joinSeparator
|
||||
);
|
||||
expect(result.split(joinSeparator).length).toBe(4);
|
||||
});
|
||||
|
||||
it('should be a 2 length list if length value is set to 2', () => {
|
||||
const input: string = 'apple, pineaple, lemon, orange';
|
||||
const splitOperatorType: SplitOperatorType = 'symbol';
|
||||
const splitSeparator = ', ';
|
||||
const joinSeparator = ' ';
|
||||
const length = 2;
|
||||
|
||||
const result = shuffleList(
|
||||
splitOperatorType,
|
||||
input,
|
||||
splitSeparator,
|
||||
joinSeparator,
|
||||
length
|
||||
);
|
||||
console.log(result);
|
||||
expect(result.split(joinSeparator).length).toBe(2);
|
||||
});
|
||||
|
||||
it('should be a 4 length list if length value is set to 99', () => {
|
||||
const input: string = 'apple, pineaple, lemon, orange';
|
||||
const splitOperatorType: SplitOperatorType = 'symbol';
|
||||
const splitSeparator = ', ';
|
||||
const joinSeparator = ' ';
|
||||
const length = 99;
|
||||
|
||||
const result = shuffleList(
|
||||
splitOperatorType,
|
||||
input,
|
||||
splitSeparator,
|
||||
joinSeparator,
|
||||
length
|
||||
);
|
||||
console.log(result);
|
||||
expect(result.split(joinSeparator).length).toBe(4);
|
||||
});
|
||||
|
||||
it('should include a random element if length value is undefined', () => {
|
||||
const input: string = 'apple, pineaple, lemon, orange';
|
||||
const splitOperatorType: SplitOperatorType = 'symbol';
|
||||
const splitSeparator = ', ';
|
||||
const joinSeparator = ' ';
|
||||
|
||||
const result = shuffleList(
|
||||
splitOperatorType,
|
||||
input,
|
||||
splitSeparator,
|
||||
joinSeparator,
|
||||
length
|
||||
);
|
||||
console.log(result);
|
||||
expect(result.split(joinSeparator)).toContain('apple');
|
||||
});
|
||||
|
||||
it('should return empty string if input is empty', () => {
|
||||
const input: string = '';
|
||||
const splitOperatorType: SplitOperatorType = 'symbol';
|
||||
const splitSeparator = ', ';
|
||||
const joinSeparator = ' ';
|
||||
|
||||
const result = shuffleList(
|
||||
splitOperatorType,
|
||||
input,
|
||||
splitSeparator,
|
||||
joinSeparator,
|
||||
length
|
||||
);
|
||||
console.log(result);
|
||||
expect(result).toBe('');
|
||||
});
|
||||
});
|
||||
165
src/pages/tools/list/sort/index.tsx
Normal file
165
src/pages/tools/list/sort/index.tsx
Normal file
@@ -0,0 +1,165 @@
|
||||
import { Box } from '@mui/material';
|
||||
import React, { useState } from 'react';
|
||||
import ToolTextInput from '@components/input/ToolTextInput';
|
||||
import ToolTextResult from '@components/result/ToolTextResult';
|
||||
import ToolOptions from '@components/options/ToolOptions';
|
||||
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,
|
||||
sortingMethod: 'alphabetic' as SortingMethod,
|
||||
increasing: true,
|
||||
splitSeparator: ',',
|
||||
joinSeparator: ',',
|
||||
removeDuplicated: false,
|
||||
caseSensitive: 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.'
|
||||
}
|
||||
];
|
||||
|
||||
export default function SplitText() {
|
||||
const [input, setInput] = useState<string>('');
|
||||
const [result, setResult] = useState<string>('');
|
||||
const compute = (optionsValues: typeof initialValues, input: any) => {
|
||||
const {
|
||||
splitSeparatorType,
|
||||
joinSeparator,
|
||||
splitSeparator,
|
||||
increasing,
|
||||
caseSensitive,
|
||||
removeDuplicated,
|
||||
sortingMethod
|
||||
} = optionsValues;
|
||||
|
||||
setResult(
|
||||
Sort(
|
||||
sortingMethod,
|
||||
splitSeparatorType,
|
||||
input,
|
||||
increasing,
|
||||
splitSeparator,
|
||||
joinSeparator,
|
||||
removeDuplicated,
|
||||
caseSensitive
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<ToolInputAndResult
|
||||
input={
|
||||
<ToolTextInput
|
||||
title={'Input list'}
|
||||
value={input}
|
||||
onChange={setInput}
|
||||
/>
|
||||
}
|
||||
result={<ToolTextResult title={'Sorted list'} value={result} />}
|
||||
/>
|
||||
<ToolOptions
|
||||
compute={compute}
|
||||
getGroups={({ values, updateField }) => [
|
||||
{
|
||||
title: 'Input item separator',
|
||||
component: (
|
||||
<Box>
|
||||
{splitOperators.map(({ title, description, type }) => (
|
||||
<SimpleRadio
|
||||
key={type}
|
||||
onClick={() => updateField('splitSeparatorType', type)}
|
||||
title={title}
|
||||
description={description}
|
||||
checked={values.splitSeparatorType === type}
|
||||
/>
|
||||
))}
|
||||
<TextFieldWithDesc
|
||||
description={'Set a delimiting symbol or regular expression.'}
|
||||
value={values.splitSeparator}
|
||||
onOwnChange={(val) => updateField('splitSeparator', val)}
|
||||
/>
|
||||
</Box>
|
||||
)
|
||||
},
|
||||
{
|
||||
title: 'Sort method',
|
||||
component: (
|
||||
<Box>
|
||||
<SelectWithDesc
|
||||
selected={values.sortingMethod}
|
||||
options={[
|
||||
{ label: 'Sort Alphabetically', value: 'alphabetic' },
|
||||
{ label: 'Sort Numerically', value: 'numeric' },
|
||||
{ label: 'Sort by Length', value: 'length' }
|
||||
]}
|
||||
onChange={(value) => updateField('sortingMethod', value)}
|
||||
description={'Select a sorting method.'}
|
||||
/>
|
||||
<SelectWithDesc
|
||||
selected={values.increasing}
|
||||
options={[
|
||||
{ label: 'Increasing order', value: true },
|
||||
{ label: 'Decreasing order', value: false }
|
||||
]}
|
||||
onChange={(value) => {
|
||||
updateField('increasing', value);
|
||||
}}
|
||||
description={'Select a sorting order.'}
|
||||
/>
|
||||
<CheckboxWithDesc
|
||||
title={'Case Sensitive Sort'}
|
||||
description={
|
||||
'Sort uppercase and lowercase items separately. Capital letters precede lowercase letters in an ascending list. (Works only in alphabetical sorting mode.)'
|
||||
}
|
||||
checked={values.caseSensitive}
|
||||
onChange={(val) => updateField('caseSensitive', val)}
|
||||
/>
|
||||
</Box>
|
||||
)
|
||||
},
|
||||
{
|
||||
title: 'Sorted item properties',
|
||||
component: (
|
||||
<Box>
|
||||
<TextFieldWithDesc
|
||||
description={
|
||||
'Use this symbol as a joiner between items in a sorted list.'
|
||||
}
|
||||
value={values.joinSeparator}
|
||||
onOwnChange={(val) => updateField('joinSeparator', val)}
|
||||
/>
|
||||
<CheckboxWithDesc
|
||||
title={'Remove duplicates'}
|
||||
description={'Delete duplicate list items.'}
|
||||
checked={values.removeDuplicated}
|
||||
onChange={(val) => updateField('removeDuplicated', val)}
|
||||
/>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
]}
|
||||
initialValues={initialValues}
|
||||
input={input}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
14
src/pages/tools/list/sort/meta.ts
Normal file
14
src/pages/tools/list/sort/meta.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { defineTool } from '@tools/defineTool';
|
||||
import { lazy } from 'react';
|
||||
// import image from '@assets/text.png';
|
||||
|
||||
export const tool = defineTool('list', {
|
||||
name: 'Sort',
|
||||
path: 'sort',
|
||||
// image,
|
||||
description:
|
||||
'This is a super simple browser-based application that sorts items in a list and arranges them in increasing or decreasing order. You can sort the items alphabetically, numerically, or by their length. You can also remove duplicate and empty items, as well as trim individual items that have whitespace around them. You can use any separator character to separate the input list items or alternatively use a regular expression to separate them. Additionally, you can create a new delimiter for the sorted output list.',
|
||||
shortDescription: 'Quickly sort a list',
|
||||
keywords: ['sort'],
|
||||
component: lazy(() => import('./index'))
|
||||
});
|
||||
123
src/pages/tools/list/sort/service.ts
Normal file
123
src/pages/tools/list/sort/service.ts
Normal file
@@ -0,0 +1,123 @@
|
||||
import { isNumber } from 'utils/string';
|
||||
|
||||
export type SortingMethod = 'numeric' | 'alphabetic' | 'length';
|
||||
export type SplitOperatorType = 'symbol' | 'regex';
|
||||
|
||||
// utils function that choose the way of numeric sorting mixed types of array
|
||||
function customNumericSort(a: string, b: string, increasing: boolean): number {
|
||||
const formattedA = isNumber(a) ? Number(a) : a;
|
||||
const formattedB = isNumber(b) ? Number(b) : b;
|
||||
if (typeof formattedA === 'number' && typeof formattedB === 'number') {
|
||||
return increasing ? formattedA - formattedB : formattedB - formattedA;
|
||||
} else if (typeof formattedA === 'string' && typeof formattedB === 'string') {
|
||||
return formattedA.localeCompare(formattedB); // Lexicographical comparison for strings
|
||||
} else if (typeof formattedA === 'number' && typeof formattedB === 'string') {
|
||||
return -1; // Numbers before strings
|
||||
} else {
|
||||
return 1; // Strings after numbers
|
||||
}
|
||||
}
|
||||
|
||||
export function numericSort(
|
||||
array: string[], // array we build after parsing the input
|
||||
increasing: boolean,
|
||||
joinSeparator: string,
|
||||
removeDuplicated: boolean // the value if the checkbox has been selected 1 else 0
|
||||
) {
|
||||
array.sort((a, b) => customNumericSort(a, b, increasing));
|
||||
if (removeDuplicated) {
|
||||
array = array.filter((item, index) => array.indexOf(item) === index);
|
||||
}
|
||||
return array.join(joinSeparator);
|
||||
}
|
||||
|
||||
// utils function that choose the way of numeric sorting mixed types of array
|
||||
function customLengthSort(a: string, b: string, increasing: boolean): number {
|
||||
return increasing ? a.length - b.length : b.length - a.length;
|
||||
}
|
||||
|
||||
export function lengthSort(
|
||||
array: string[], // array we build after parsing the input
|
||||
increasing: boolean, // select value has to be increasing for increasing order and decreasing for decreasing order
|
||||
joinSeparator: string,
|
||||
removeDuplicated: boolean // the value if the checkbox has been selected 1 else 0
|
||||
) {
|
||||
array.sort((a, b) => customLengthSort(a, b, increasing));
|
||||
if (removeDuplicated) {
|
||||
array = array.filter((item, index) => array.indexOf(item) === index);
|
||||
}
|
||||
return array.join(joinSeparator);
|
||||
}
|
||||
|
||||
// Utils function that chooses the way of alphabetic sorting mixed types of array
|
||||
function customAlphabeticSort(
|
||||
a: string,
|
||||
b: string,
|
||||
caseSensitive: boolean
|
||||
): number {
|
||||
if (!caseSensitive) {
|
||||
// Case-insensitive comparison
|
||||
return a.toLowerCase().localeCompare(b.toLowerCase());
|
||||
} else {
|
||||
// Case-sensitive comparison
|
||||
return a.charCodeAt(0) - b.charCodeAt(0);
|
||||
}
|
||||
}
|
||||
|
||||
export function alphabeticSort(
|
||||
array: string[], // array we build after parsing the input
|
||||
increasing: boolean, // select value has to be "increasing" for increasing order and "decreasing" for decreasing order
|
||||
joinSeparator: string,
|
||||
removeDuplicated: boolean, // the value if the checkbox has been selected 1 else 0
|
||||
caseSensitive: boolean // the value if the checkbox has been selected 1 else 0
|
||||
) {
|
||||
array.sort((a, b) => customAlphabeticSort(a, b, caseSensitive));
|
||||
if (!increasing) {
|
||||
array.reverse();
|
||||
}
|
||||
if (removeDuplicated) {
|
||||
array = array.filter((item, index) => array.indexOf(item) === index);
|
||||
}
|
||||
return array.join(joinSeparator);
|
||||
}
|
||||
|
||||
// main function
|
||||
export function Sort(
|
||||
sortingMethod: SortingMethod,
|
||||
splitOperatorType: SplitOperatorType,
|
||||
input: string,
|
||||
increasing: boolean,
|
||||
splitSeparator: string,
|
||||
joinSeparator: string,
|
||||
removeDuplicated: boolean,
|
||||
caseSensitive: boolean
|
||||
) {
|
||||
let array: string[];
|
||||
switch (splitOperatorType) {
|
||||
case 'symbol':
|
||||
array = input.split(splitSeparator);
|
||||
break;
|
||||
case 'regex':
|
||||
array = input.split(new RegExp(splitSeparator));
|
||||
break;
|
||||
}
|
||||
let result: string;
|
||||
switch (sortingMethod) {
|
||||
case 'numeric':
|
||||
result = numericSort(array, increasing, joinSeparator, removeDuplicated);
|
||||
break;
|
||||
case 'length':
|
||||
result = lengthSort(array, increasing, joinSeparator, removeDuplicated);
|
||||
break;
|
||||
case 'alphabetic':
|
||||
result = alphabeticSort(
|
||||
array,
|
||||
increasing,
|
||||
joinSeparator,
|
||||
removeDuplicated,
|
||||
caseSensitive
|
||||
);
|
||||
break;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
352
src/pages/tools/list/sort/sort.service.test.ts
Normal file
352
src/pages/tools/list/sort/sort.service.test.ts
Normal file
@@ -0,0 +1,352 @@
|
||||
// Import necessary modules and functions
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import {
|
||||
alphabeticSort,
|
||||
lengthSort,
|
||||
numericSort,
|
||||
Sort,
|
||||
SortingMethod,
|
||||
SplitOperatorType
|
||||
} from './service';
|
||||
|
||||
// Define test cases for the numericSort function
|
||||
describe('numericSort function', () => {
|
||||
it('should sort a list in increasing order with comma separator not removeduplicated elements', () => {
|
||||
const array: string[] = ['9', '8', '7', '4', '2', '2', '5'];
|
||||
const increasing: boolean = true;
|
||||
const separator = ', ';
|
||||
const removeDuplicated: boolean = false;
|
||||
|
||||
const result = numericSort(array, increasing, separator, removeDuplicated);
|
||||
expect(result).toBe('2, 2, 4, 5, 7, 8, 9');
|
||||
});
|
||||
|
||||
it('should sort a list in decreasing order with " - " separator and remove duplicated elements', () => {
|
||||
const array: string[] = ['2', '4', '4', '9', '6', '6', '7'];
|
||||
const increasing: boolean = false;
|
||||
const separator = ' - ';
|
||||
const removeDuplicated: boolean = true;
|
||||
|
||||
const result = numericSort(array, increasing, separator, removeDuplicated);
|
||||
expect(result).toBe('9 - 7 - 6 - 4 - 2');
|
||||
});
|
||||
|
||||
it('should sort a list with numbers and characters and remove duplicated elements', () => {
|
||||
const array: string[] = ['d', 'd', 'n', 'p', 'h', 'h', '6', '9', '7', '5'];
|
||||
const increasing: boolean = true;
|
||||
const separator = ' ';
|
||||
const removeDuplicated: boolean = true;
|
||||
|
||||
const result = numericSort(array, increasing, separator, removeDuplicated);
|
||||
expect(result).toBe('5 6 7 9 d h n p');
|
||||
});
|
||||
|
||||
// Define test cases for the lengthSort function
|
||||
describe('lengthSort function', () => {
|
||||
it('should sort a list of number by length in increasing order with comma separator ', () => {
|
||||
const array: string[] = ['415689521', '3', '126', '12', '1523'];
|
||||
const increasing: boolean = true;
|
||||
const separator = ', ';
|
||||
const removeDuplicated: boolean = false;
|
||||
|
||||
const result = lengthSort(array, increasing, separator, removeDuplicated);
|
||||
expect(result).toBe('3, 12, 126, 1523, 415689521');
|
||||
});
|
||||
|
||||
it('should sort a list of number by length in increasing order and remove duplicated elements ', () => {
|
||||
const array: string[] = [
|
||||
'415689521',
|
||||
'3',
|
||||
'3',
|
||||
'126',
|
||||
'12',
|
||||
'12',
|
||||
'1523'
|
||||
];
|
||||
const increasing: boolean = true;
|
||||
const separator = ', ';
|
||||
const removeDuplicated: boolean = true;
|
||||
|
||||
const result = lengthSort(array, increasing, separator, removeDuplicated);
|
||||
expect(result).toBe('3, 12, 126, 1523, 415689521');
|
||||
});
|
||||
|
||||
it('should sort a mixed array by length in increasing order ', () => {
|
||||
const array: string[] = [
|
||||
'ddd',
|
||||
'd',
|
||||
'nfg',
|
||||
'p',
|
||||
'h',
|
||||
'h',
|
||||
'6555',
|
||||
'9',
|
||||
'7',
|
||||
'5556'
|
||||
];
|
||||
const increasing: boolean = true;
|
||||
const separator = ' ';
|
||||
const removeDuplicated: boolean = true;
|
||||
|
||||
const result = lengthSort(array, increasing, separator, removeDuplicated);
|
||||
expect(result).toBe('d p h 9 7 ddd nfg 6555 5556');
|
||||
});
|
||||
});
|
||||
|
||||
// Define test cases for the alphabeticSort function
|
||||
describe('alphabeticSort function', () => {
|
||||
// NON CASE SENSITIVE TEST
|
||||
it('should sort a list of string in increasing order with comma separator ', () => {
|
||||
const array: string[] = ['apple', 'pineaple', 'lemon', 'orange'];
|
||||
const increasing: boolean = true;
|
||||
const separator = ', ';
|
||||
const removeDuplicated: boolean = false;
|
||||
const caseSensitive: boolean = false;
|
||||
|
||||
const result = alphabeticSort(
|
||||
array,
|
||||
increasing,
|
||||
separator,
|
||||
removeDuplicated,
|
||||
caseSensitive
|
||||
);
|
||||
expect(result).toBe('apple, lemon, orange, pineaple');
|
||||
});
|
||||
|
||||
it('should sort a list of string in decreasing order with comma separator ', () => {
|
||||
const array: string[] = ['apple', 'pineaple', 'lemon', 'orange'];
|
||||
const increasing: boolean = false;
|
||||
const separator = ', ';
|
||||
const removeDuplicated: boolean = false;
|
||||
const caseSensitive: boolean = false;
|
||||
|
||||
const result = alphabeticSort(
|
||||
array,
|
||||
increasing,
|
||||
separator,
|
||||
removeDuplicated,
|
||||
caseSensitive
|
||||
);
|
||||
expect(result).toBe('pineaple, orange, lemon, apple');
|
||||
});
|
||||
|
||||
it('should sort a list of string and symbols (uppercase and lower) in increasing order with comma separator ', () => {
|
||||
const array: string[] = [
|
||||
'Apple',
|
||||
'pineaple',
|
||||
'lemon',
|
||||
'Orange',
|
||||
'1',
|
||||
'9',
|
||||
'@',
|
||||
'+'
|
||||
];
|
||||
const increasing: boolean = true;
|
||||
const separator = ' ';
|
||||
const removeDuplicated: boolean = true;
|
||||
const caseSensitive: boolean = false;
|
||||
|
||||
const result = alphabeticSort(
|
||||
array,
|
||||
increasing,
|
||||
separator,
|
||||
removeDuplicated,
|
||||
caseSensitive
|
||||
);
|
||||
expect(result).toBe('@ + 1 9 Apple lemon Orange pineaple');
|
||||
});
|
||||
|
||||
it('should sort a list of string and symbols (uppercase and lower) in decreasing order with comma separator ', () => {
|
||||
const array: string[] = [
|
||||
'Apple',
|
||||
'pineaple',
|
||||
'lemon',
|
||||
'Orange',
|
||||
'1',
|
||||
'9',
|
||||
'@',
|
||||
'+'
|
||||
];
|
||||
const increasing: boolean = false;
|
||||
const separator = ' ';
|
||||
const removeDuplicated: boolean = true;
|
||||
const caseSensitive: boolean = false;
|
||||
|
||||
const result = alphabeticSort(
|
||||
array,
|
||||
increasing,
|
||||
separator,
|
||||
removeDuplicated,
|
||||
caseSensitive
|
||||
);
|
||||
expect(result).toBe('pineaple Orange lemon Apple 9 1 + @');
|
||||
});
|
||||
|
||||
// CASE SENSITIVE TEST
|
||||
it('should sort a list of string (uppercase) in decreasing order with comma separator ', () => {
|
||||
const array: string[] = ['Apple', 'Pineaple', 'Lemon', 'Orange'];
|
||||
const increasing: boolean = false;
|
||||
const separator = ' ';
|
||||
const removeDuplicated: boolean = false;
|
||||
const caseSensitive: boolean = true;
|
||||
|
||||
const result = alphabeticSort(
|
||||
array,
|
||||
increasing,
|
||||
separator,
|
||||
removeDuplicated,
|
||||
caseSensitive
|
||||
);
|
||||
expect(result).toBe('Pineaple Orange Lemon Apple');
|
||||
});
|
||||
|
||||
it('should sort a list of string (uppercase and lowercase) in increasing order with comma separator ', () => {
|
||||
const array: string[] = [
|
||||
'Apple',
|
||||
'pineaple',
|
||||
'lemon',
|
||||
'Orange',
|
||||
'1',
|
||||
'9'
|
||||
];
|
||||
const increasing: boolean = true;
|
||||
const separator = ' ';
|
||||
const removeDuplicated: boolean = true;
|
||||
const caseSensitive: boolean = true;
|
||||
|
||||
const result = alphabeticSort(
|
||||
array,
|
||||
increasing,
|
||||
separator,
|
||||
removeDuplicated,
|
||||
caseSensitive
|
||||
);
|
||||
expect(result).toBe('1 9 Apple Orange lemon pineaple');
|
||||
});
|
||||
|
||||
it('should sort a list of string (uppercase and lower) in decreasing order with comma separator ', () => {
|
||||
const array: string[] = [
|
||||
'Apple',
|
||||
'pineaple',
|
||||
'lemon',
|
||||
'Orange',
|
||||
'1',
|
||||
'9'
|
||||
];
|
||||
const increasing: boolean = false;
|
||||
const separator = ' ';
|
||||
const removeDuplicated: boolean = true;
|
||||
const caseSensitive: boolean = true;
|
||||
|
||||
const result = alphabeticSort(
|
||||
array,
|
||||
increasing,
|
||||
separator,
|
||||
removeDuplicated,
|
||||
caseSensitive
|
||||
);
|
||||
expect(result).toBe('pineaple lemon Orange Apple 9 1');
|
||||
});
|
||||
|
||||
it('should sort a list of string and symbols (uppercase and lower) in decreasing order with comma separator ', () => {
|
||||
const array: string[] = [
|
||||
'Apple',
|
||||
'pineaple',
|
||||
'lemon',
|
||||
'Orange',
|
||||
'1',
|
||||
'9',
|
||||
'@',
|
||||
'+'
|
||||
];
|
||||
const increasing: boolean = true;
|
||||
const separator = ' ';
|
||||
const removeDuplicated: boolean = true;
|
||||
const caseSensitive: boolean = true;
|
||||
|
||||
const result = alphabeticSort(
|
||||
array,
|
||||
increasing,
|
||||
separator,
|
||||
removeDuplicated,
|
||||
caseSensitive
|
||||
);
|
||||
expect(result).toBe('+ 1 9 @ Apple Orange lemon pineaple');
|
||||
});
|
||||
|
||||
it('should sort a list of string and symbols (uppercase and lower) in decreasing order with comma separator ', () => {
|
||||
const array: string[] = [
|
||||
'Apple',
|
||||
'pineaple',
|
||||
'lemon',
|
||||
'Orange',
|
||||
'1',
|
||||
'9',
|
||||
'@',
|
||||
'+'
|
||||
];
|
||||
const increasing: boolean = false;
|
||||
const separator = ' ';
|
||||
const removeDuplicated: boolean = true;
|
||||
const caseSensitive: boolean = true;
|
||||
|
||||
const result = alphabeticSort(
|
||||
array,
|
||||
increasing,
|
||||
separator,
|
||||
removeDuplicated,
|
||||
caseSensitive
|
||||
);
|
||||
expect(result).toBe('pineaple lemon Orange Apple @ 9 1 +');
|
||||
});
|
||||
});
|
||||
|
||||
// Define test cases for the lengthSort function
|
||||
describe('main function', () => {
|
||||
it('should do everything alph', () => {
|
||||
const sortingMethod: SortingMethod = 'alphabetic';
|
||||
const splitOperatorType: SplitOperatorType = 'symbol';
|
||||
const input: string = 'Apple pineaple lemon Orange 1 9 @ +';
|
||||
const increasing: boolean = true;
|
||||
const splitSeparator: string = ' ';
|
||||
const joinSeparator: string = ' ';
|
||||
const removeDuplicated: boolean = true;
|
||||
const caseSensitive: boolean = true;
|
||||
|
||||
const result = Sort(
|
||||
sortingMethod,
|
||||
splitOperatorType,
|
||||
input,
|
||||
increasing,
|
||||
splitSeparator,
|
||||
joinSeparator,
|
||||
removeDuplicated,
|
||||
caseSensitive
|
||||
);
|
||||
expect(result).toBe('+ 1 9 @ Apple Orange lemon pineaple');
|
||||
});
|
||||
|
||||
it('should do everything numeric', () => {
|
||||
const sortingMethod: SortingMethod = 'numeric';
|
||||
const splitOperatorType: SplitOperatorType = 'symbol';
|
||||
const input: string = '1 6 9 4 6 7 3 5 8';
|
||||
const increasing: boolean = true;
|
||||
const splitSeparator: string = ' ';
|
||||
const joinSeparator: string = ' ';
|
||||
const removeDuplicated: boolean = true;
|
||||
const caseSensitive: boolean = true;
|
||||
|
||||
const result = Sort(
|
||||
sortingMethod,
|
||||
splitOperatorType,
|
||||
input,
|
||||
increasing,
|
||||
splitSeparator,
|
||||
joinSeparator,
|
||||
removeDuplicated,
|
||||
caseSensitive
|
||||
);
|
||||
expect(result).toBe('1 3 4 5 6 7 8 9');
|
||||
});
|
||||
});
|
||||
});
|
||||
11
src/pages/tools/list/truncate/index.tsx
Normal file
11
src/pages/tools/list/truncate/index.tsx
Normal file
@@ -0,0 +1,11 @@
|
||||
import { Box } from '@mui/material';
|
||||
import React from 'react';
|
||||
import * as Yup from 'yup';
|
||||
|
||||
const initialValues = {};
|
||||
const validationSchema = Yup.object({
|
||||
// splitSeparator: Yup.string().required('The separator is required')
|
||||
});
|
||||
export default function Truncate() {
|
||||
return <Box>Lorem ipsum</Box>;
|
||||
}
|
||||
13
src/pages/tools/list/truncate/meta.ts
Normal file
13
src/pages/tools/list/truncate/meta.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { defineTool } from '@tools/defineTool';
|
||||
import { lazy } from 'react';
|
||||
// import image from '@assets/text.png';
|
||||
|
||||
export const tool = defineTool('list', {
|
||||
name: 'Truncate',
|
||||
path: 'truncate',
|
||||
// image,
|
||||
description: '',
|
||||
shortDescription: '',
|
||||
keywords: ['truncate'],
|
||||
component: lazy(() => import('./index'))
|
||||
});
|
||||
31
src/pages/tools/list/truncate/service.ts
Normal file
31
src/pages/tools/list/truncate/service.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
export type SplitOperatorType = 'symbol' | 'regex';
|
||||
|
||||
export function truncateList(
|
||||
splitOperatorType: SplitOperatorType,
|
||||
input: string,
|
||||
splitSeparator: string,
|
||||
joinSeparator: string,
|
||||
end: boolean,
|
||||
length?: number
|
||||
): string {
|
||||
let array: string[];
|
||||
let truncatedArray: string[];
|
||||
switch (splitOperatorType) {
|
||||
case 'symbol':
|
||||
array = input.split(splitSeparator);
|
||||
break;
|
||||
case 'regex':
|
||||
array = input.split(new RegExp(splitSeparator));
|
||||
break;
|
||||
}
|
||||
if (length !== undefined) {
|
||||
if (length < 0) {
|
||||
throw new Error('Length value must be a positive number.');
|
||||
}
|
||||
truncatedArray = end
|
||||
? array.slice(0, length)
|
||||
: array.slice(array.length - length, array.length);
|
||||
return truncatedArray.join(joinSeparator);
|
||||
}
|
||||
throw new Error("Length value isn't a value number.");
|
||||
}
|
||||
183
src/pages/tools/list/truncate/truncate.service.test.ts
Normal file
183
src/pages/tools/list/truncate/truncate.service.test.ts
Normal file
@@ -0,0 +1,183 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import { SplitOperatorType, truncateList } from './service';
|
||||
|
||||
describe('truncate function', () => {
|
||||
it('should remove at the end (one element) if end is set to true', () => {
|
||||
const input: string = 'apple, pineaple, lemon, orange';
|
||||
const splitOperatorType: SplitOperatorType = 'symbol';
|
||||
const splitSeparator = ', ';
|
||||
const joinSeparator = ' ';
|
||||
const end = true;
|
||||
const length = 3;
|
||||
|
||||
const result = truncateList(
|
||||
splitOperatorType,
|
||||
input,
|
||||
splitSeparator,
|
||||
joinSeparator,
|
||||
end,
|
||||
length
|
||||
);
|
||||
|
||||
expect(result).toBe('apple pineaple lemon');
|
||||
});
|
||||
|
||||
it('should return 3 elements from the start if end is set to true', () => {
|
||||
const input: string = 'apple, pineaple, lemon, orange, mango';
|
||||
const splitOperatorType: SplitOperatorType = 'symbol';
|
||||
const splitSeparator = ', ';
|
||||
const joinSeparator = ' ';
|
||||
const end = true;
|
||||
const length = 3;
|
||||
|
||||
const result = truncateList(
|
||||
splitOperatorType,
|
||||
input,
|
||||
splitSeparator,
|
||||
joinSeparator,
|
||||
end,
|
||||
length
|
||||
);
|
||||
|
||||
expect(result).toBe('apple pineaple lemon');
|
||||
});
|
||||
|
||||
it('should return 3 elements from the start if end is set to true', () => {
|
||||
const input: string = 'apple, pineaple, lemon, orange, mango';
|
||||
const splitOperatorType: SplitOperatorType = 'symbol';
|
||||
const splitSeparator = ', ';
|
||||
const joinSeparator = ' ';
|
||||
const end = true;
|
||||
const length = 3;
|
||||
|
||||
const result = truncateList(
|
||||
splitOperatorType,
|
||||
input,
|
||||
splitSeparator,
|
||||
joinSeparator,
|
||||
end,
|
||||
length
|
||||
);
|
||||
|
||||
expect(result).toBe('apple pineaple lemon');
|
||||
});
|
||||
|
||||
it('should return 3 elements from the end if end is set to true', () => {
|
||||
const input: string = 'apple, pineaple, lemon, orange, mango';
|
||||
const splitOperatorType: SplitOperatorType = 'symbol';
|
||||
const splitSeparator = ', ';
|
||||
const joinSeparator = ' ';
|
||||
const end = false;
|
||||
const length = 3;
|
||||
|
||||
const result = truncateList(
|
||||
splitOperatorType,
|
||||
input,
|
||||
splitSeparator,
|
||||
joinSeparator,
|
||||
end,
|
||||
length
|
||||
);
|
||||
|
||||
expect(result).toBe('lemon orange mango');
|
||||
});
|
||||
|
||||
it('should return a void string if length is set to 0', () => {
|
||||
const input: string = 'apple, pineaple, lemon, orange, mango';
|
||||
const splitOperatorType: SplitOperatorType = 'symbol';
|
||||
const splitSeparator = ', ';
|
||||
const joinSeparator = ' ';
|
||||
const end = false;
|
||||
const length = 0;
|
||||
|
||||
const result = truncateList(
|
||||
splitOperatorType,
|
||||
input,
|
||||
splitSeparator,
|
||||
joinSeparator,
|
||||
end,
|
||||
length
|
||||
);
|
||||
|
||||
expect(result).toBe('');
|
||||
});
|
||||
|
||||
it('should return an element (first) string if length is set to 1 and end is set to true', () => {
|
||||
const input: string = 'apple, pineaple, lemon, orange, mango';
|
||||
const splitOperatorType: SplitOperatorType = 'symbol';
|
||||
const splitSeparator = ', ';
|
||||
const joinSeparator = ' ';
|
||||
const end = true;
|
||||
const length = 1;
|
||||
|
||||
const result = truncateList(
|
||||
splitOperatorType,
|
||||
input,
|
||||
splitSeparator,
|
||||
joinSeparator,
|
||||
end,
|
||||
length
|
||||
);
|
||||
|
||||
expect(result).toBe('apple');
|
||||
});
|
||||
|
||||
it('should return an element (last) string if length is set to 1 and end is set to false', () => {
|
||||
const input: string = 'apple, pineaple, lemon, orange, mango';
|
||||
const splitOperatorType: SplitOperatorType = 'symbol';
|
||||
const splitSeparator = ', ';
|
||||
const joinSeparator = ' ';
|
||||
const end = false;
|
||||
const length = 1;
|
||||
|
||||
const result = truncateList(
|
||||
splitOperatorType,
|
||||
input,
|
||||
splitSeparator,
|
||||
joinSeparator,
|
||||
end,
|
||||
length
|
||||
);
|
||||
|
||||
expect(result).toBe('mango');
|
||||
});
|
||||
|
||||
it('should throw an error if the length value is negative', () => {
|
||||
const input: string = 'apple, pineaple, lemon, orange, mango';
|
||||
const splitOperatorType: SplitOperatorType = 'symbol';
|
||||
const splitSeparator = ', ';
|
||||
const joinSeparator = ' ';
|
||||
const end = false;
|
||||
const length = -5;
|
||||
|
||||
expect(() => {
|
||||
truncateList(
|
||||
splitOperatorType,
|
||||
input,
|
||||
splitSeparator,
|
||||
joinSeparator,
|
||||
end,
|
||||
length
|
||||
);
|
||||
}).toThrow('Length value must be a positive number.');
|
||||
});
|
||||
|
||||
it('should throw an error if the length value is left blank', () => {
|
||||
const input: string = 'apple, pineaple, lemon, orange, mango';
|
||||
const splitOperatorType: SplitOperatorType = 'symbol';
|
||||
const splitSeparator = ', ';
|
||||
const joinSeparator = ' ';
|
||||
const end = false;
|
||||
|
||||
expect(() => {
|
||||
truncateList(
|
||||
splitOperatorType,
|
||||
input,
|
||||
splitSeparator,
|
||||
joinSeparator,
|
||||
end
|
||||
);
|
||||
}).toThrow("Length value isn't a value number.");
|
||||
});
|
||||
});
|
||||
11
src/pages/tools/list/unwrap/index.tsx
Normal file
11
src/pages/tools/list/unwrap/index.tsx
Normal file
@@ -0,0 +1,11 @@
|
||||
import { Box } from '@mui/material';
|
||||
import React from 'react';
|
||||
import * as Yup from 'yup';
|
||||
|
||||
const initialValues = {};
|
||||
const validationSchema = Yup.object({
|
||||
// splitSeparator: Yup.string().required('The separator is required')
|
||||
});
|
||||
export default function Unwrap() {
|
||||
return <Box>Lorem ipsum</Box>;
|
||||
}
|
||||
13
src/pages/tools/list/unwrap/meta.ts
Normal file
13
src/pages/tools/list/unwrap/meta.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { defineTool } from '@tools/defineTool';
|
||||
import { lazy } from 'react';
|
||||
// import image from '@assets/text.png';
|
||||
|
||||
export const tool = defineTool('list', {
|
||||
name: 'Unwrap',
|
||||
path: 'unwrap',
|
||||
// image,
|
||||
description: '',
|
||||
shortDescription: '',
|
||||
keywords: ['unwrap'],
|
||||
component: lazy(() => import('./index'))
|
||||
});
|
||||
69
src/pages/tools/list/unwrap/service.ts
Normal file
69
src/pages/tools/list/unwrap/service.ts
Normal file
@@ -0,0 +1,69 @@
|
||||
export type SplitOperatorType = 'symbol' | 'regex';
|
||||
|
||||
function leftUnwrap(
|
||||
row: string,
|
||||
left: string = '',
|
||||
multiLevel: boolean
|
||||
): string {
|
||||
if (left === '') return row; // Prevent infinite loop if left is an empty string
|
||||
while (row.startsWith(left)) {
|
||||
row = row.slice(left.length);
|
||||
if (!multiLevel) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return row;
|
||||
}
|
||||
|
||||
function rightUnwrap(
|
||||
row: string,
|
||||
right: string = '',
|
||||
multiLevel: boolean
|
||||
): string {
|
||||
if (right === '') return row; // Prevent infinite loop if right is an empty string
|
||||
while (row.endsWith(right)) {
|
||||
row = row.slice(0, row.length - right.length);
|
||||
if (!multiLevel) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return row;
|
||||
}
|
||||
|
||||
export function unwrapList(
|
||||
splitOperatorType: SplitOperatorType,
|
||||
input: string,
|
||||
splitSeparator: string,
|
||||
joinSeparator: string,
|
||||
deleteEmptyItems: boolean,
|
||||
multiLevel: boolean,
|
||||
trimItems: boolean,
|
||||
left: string = '',
|
||||
right: string = ''
|
||||
): string {
|
||||
let array: string[];
|
||||
let unwrappedArray: string[] = [];
|
||||
switch (splitOperatorType) {
|
||||
case 'symbol':
|
||||
array = input.split(splitSeparator);
|
||||
break;
|
||||
case 'regex':
|
||||
array = input.split(new RegExp(splitSeparator));
|
||||
break;
|
||||
}
|
||||
if (deleteEmptyItems) {
|
||||
array = array.filter(Boolean);
|
||||
}
|
||||
|
||||
// for each element of array unwrap left side then right side and push the result to a final array
|
||||
for (let row of array) {
|
||||
row = leftUnwrap(row, left, multiLevel);
|
||||
row = rightUnwrap(row, right, multiLevel);
|
||||
unwrappedArray.push(row);
|
||||
}
|
||||
// trim items if needed
|
||||
if (trimItems) {
|
||||
unwrappedArray = unwrappedArray.map((item) => item.trim());
|
||||
}
|
||||
return unwrappedArray.join(joinSeparator);
|
||||
}
|
||||
170
src/pages/tools/list/unwrap/unwrap.service.test.ts
Normal file
170
src/pages/tools/list/unwrap/unwrap.service.test.ts
Normal file
@@ -0,0 +1,170 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { unwrapList } from './service';
|
||||
|
||||
describe('unwrapList function', () => {
|
||||
it('should unwrap elements correctly with symbol split', () => {
|
||||
const input = '##Hello##\n##World##';
|
||||
const result = unwrapList(
|
||||
'symbol',
|
||||
input,
|
||||
'\n',
|
||||
' ',
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
'#',
|
||||
'#'
|
||||
);
|
||||
expect(result).toBe('Hello World');
|
||||
});
|
||||
|
||||
it('should unwrap elements correctly with regex split', () => {
|
||||
const input = '##Hello##||##World##';
|
||||
const result = unwrapList(
|
||||
'regex',
|
||||
input,
|
||||
'\\|\\|',
|
||||
' ',
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
'#',
|
||||
'#'
|
||||
);
|
||||
expect(result).toBe('Hello World');
|
||||
});
|
||||
|
||||
it('should handle multiple levels of unwrapping', () => {
|
||||
const input = '###Hello###';
|
||||
const result = unwrapList(
|
||||
'symbol',
|
||||
input,
|
||||
'\n',
|
||||
' ',
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
'#',
|
||||
'#'
|
||||
);
|
||||
expect(result).toBe('Hello');
|
||||
});
|
||||
|
||||
it('should handle single level of unwrapping', () => {
|
||||
const input = '###Hello###';
|
||||
const result = unwrapList(
|
||||
'symbol',
|
||||
input,
|
||||
'\n',
|
||||
' ',
|
||||
true,
|
||||
false,
|
||||
true,
|
||||
'#',
|
||||
'#'
|
||||
);
|
||||
expect(result).toBe('##Hello##');
|
||||
});
|
||||
|
||||
it('should delete empty items', () => {
|
||||
const input = '##Hello##\n\n##World##';
|
||||
const result = unwrapList(
|
||||
'symbol',
|
||||
input,
|
||||
'\n',
|
||||
' ',
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
'#',
|
||||
'#'
|
||||
);
|
||||
expect(result).toBe('Hello World');
|
||||
});
|
||||
|
||||
it('should keep empty items if deleteEmptyItems is false', () => {
|
||||
const input = '##Hello##\n\n##World##';
|
||||
const result = unwrapList(
|
||||
'symbol',
|
||||
input,
|
||||
'\n',
|
||||
' ',
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
'#',
|
||||
'#'
|
||||
);
|
||||
expect(result).toBe('Hello World');
|
||||
});
|
||||
|
||||
it('should trim items', () => {
|
||||
const input = '## Hello ##\n## World ##';
|
||||
const result = unwrapList(
|
||||
'symbol',
|
||||
input,
|
||||
'\n',
|
||||
' ',
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
'#',
|
||||
'#'
|
||||
);
|
||||
expect(result).toBe('Hello World');
|
||||
});
|
||||
|
||||
it('should handle no left or right unwrapping', () => {
|
||||
const input = 'Hello\nWorld';
|
||||
const result = unwrapList('symbol', input, '\n', ' ', true, true, true);
|
||||
expect(result).toBe('Hello World');
|
||||
});
|
||||
|
||||
it('should handle mixed levels of unwrapping', () => {
|
||||
const input = '###Hello##\n#World###';
|
||||
const result = unwrapList(
|
||||
'symbol',
|
||||
input,
|
||||
'\n',
|
||||
' ',
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
'#',
|
||||
'#'
|
||||
);
|
||||
expect(result).toBe('Hello World');
|
||||
});
|
||||
|
||||
it('should handle complex regex split', () => {
|
||||
const input = '##Hello##||###World###||####Test####';
|
||||
const result = unwrapList(
|
||||
'regex',
|
||||
input,
|
||||
'\\|\\|',
|
||||
' ',
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
'#',
|
||||
'#'
|
||||
);
|
||||
expect(result).toBe('Hello World Test');
|
||||
});
|
||||
|
||||
it('should handle different joinSeparator', () => {
|
||||
const input = '##Hello##\n##World##';
|
||||
const result = unwrapList(
|
||||
'symbol',
|
||||
input,
|
||||
'\n',
|
||||
'-',
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
'#',
|
||||
'#'
|
||||
);
|
||||
expect(result).toBe('Hello-World');
|
||||
});
|
||||
});
|
||||
11
src/pages/tools/list/wrap/index.tsx
Normal file
11
src/pages/tools/list/wrap/index.tsx
Normal file
@@ -0,0 +1,11 @@
|
||||
import { Box } from '@mui/material';
|
||||
import React from 'react';
|
||||
import * as Yup from 'yup';
|
||||
|
||||
const initialValues = {};
|
||||
const validationSchema = Yup.object({
|
||||
// splitSeparator: Yup.string().required('The separator is required')
|
||||
});
|
||||
export default function Wrap() {
|
||||
return <Box>Lorem ipsum</Box>;
|
||||
}
|
||||
13
src/pages/tools/list/wrap/meta.ts
Normal file
13
src/pages/tools/list/wrap/meta.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { defineTool } from '@tools/defineTool';
|
||||
import { lazy } from 'react';
|
||||
// import image from '@assets/text.png';
|
||||
|
||||
export const tool = defineTool('list', {
|
||||
name: 'Wrap',
|
||||
path: 'wrap',
|
||||
// image,
|
||||
description: '',
|
||||
shortDescription: '',
|
||||
keywords: ['wrap'],
|
||||
component: lazy(() => import('./index'))
|
||||
});
|
||||
31
src/pages/tools/list/wrap/service.ts
Normal file
31
src/pages/tools/list/wrap/service.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
export type SplitOperatorType = 'symbol' | 'regex';
|
||||
|
||||
function wrap(array: string[], left: string, right: string): string[] {
|
||||
return array.map((element) => left + element + right);
|
||||
}
|
||||
|
||||
export function wrapList(
|
||||
splitOperatorType: SplitOperatorType,
|
||||
input: string,
|
||||
splitSeparator: string,
|
||||
joinSeparator: string,
|
||||
deleteEmptyItems: boolean,
|
||||
left: string = '',
|
||||
right: string = ''
|
||||
): string {
|
||||
let array: string[];
|
||||
let wrappedArray: string[];
|
||||
switch (splitOperatorType) {
|
||||
case 'symbol':
|
||||
array = input.split(splitSeparator);
|
||||
break;
|
||||
case 'regex':
|
||||
array = input.split(new RegExp(splitSeparator));
|
||||
break;
|
||||
}
|
||||
if (deleteEmptyItems) {
|
||||
array = array.filter(Boolean);
|
||||
}
|
||||
wrappedArray = wrap(array, left, right);
|
||||
return wrappedArray.join(joinSeparator);
|
||||
}
|
||||
132
src/pages/tools/list/wrap/wrap.service.test.ts
Normal file
132
src/pages/tools/list/wrap/wrap.service.test.ts
Normal file
@@ -0,0 +1,132 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { SplitOperatorType, wrapList } from './service';
|
||||
|
||||
describe('wrap function', () => {
|
||||
it('should return the same input if no left and right are blanked', () => {
|
||||
const input: string = 'apple, pineaple, lemon, orange, mango';
|
||||
const splitOperatorType: SplitOperatorType = 'symbol';
|
||||
const splitSeparator = ', ';
|
||||
const joinSeparator = ', ';
|
||||
const deleteEmptyItems = false;
|
||||
|
||||
const result = wrapList(
|
||||
splitOperatorType,
|
||||
input,
|
||||
splitSeparator,
|
||||
joinSeparator,
|
||||
deleteEmptyItems
|
||||
);
|
||||
|
||||
expect(result).toBe('apple, pineaple, lemon, orange, mango');
|
||||
});
|
||||
|
||||
it('should append to left if defined', () => {
|
||||
const input: string = 'apple, pineaple, lemon, orange, mango';
|
||||
const splitOperatorType: SplitOperatorType = 'symbol';
|
||||
const splitSeparator = ', ';
|
||||
const joinSeparator = ', ';
|
||||
const left = 'the ';
|
||||
const deleteEmptyItems = false;
|
||||
|
||||
const result = wrapList(
|
||||
splitOperatorType,
|
||||
input,
|
||||
splitSeparator,
|
||||
joinSeparator,
|
||||
deleteEmptyItems,
|
||||
left
|
||||
);
|
||||
|
||||
expect(result).toBe(
|
||||
'the apple, the pineaple, the lemon, the orange, the mango'
|
||||
);
|
||||
});
|
||||
|
||||
it('should append to right if defined', () => {
|
||||
const input: string = 'apple, pineaple, lemon, orange, mango';
|
||||
const splitOperatorType: SplitOperatorType = 'symbol';
|
||||
const splitSeparator = ', ';
|
||||
const joinSeparator = ', ';
|
||||
const left = '';
|
||||
const right = 'z';
|
||||
const deleteEmptyItems = false;
|
||||
|
||||
const result = wrapList(
|
||||
splitOperatorType,
|
||||
input,
|
||||
splitSeparator,
|
||||
joinSeparator,
|
||||
deleteEmptyItems,
|
||||
left,
|
||||
right
|
||||
);
|
||||
|
||||
expect(result).toBe('applez, pineaplez, lemonz, orangez, mangoz');
|
||||
});
|
||||
|
||||
it('should append to both side if both defined', () => {
|
||||
const input: string = 'apple, pineaple, lemon, orange, mango';
|
||||
const splitOperatorType: SplitOperatorType = 'symbol';
|
||||
const splitSeparator = ', ';
|
||||
const joinSeparator = ', ';
|
||||
const deleteEmptyItems = false;
|
||||
const left = 'K';
|
||||
const right = 'z';
|
||||
|
||||
const result = wrapList(
|
||||
splitOperatorType,
|
||||
input,
|
||||
splitSeparator,
|
||||
joinSeparator,
|
||||
deleteEmptyItems,
|
||||
left,
|
||||
right
|
||||
);
|
||||
|
||||
expect(result).toBe('Kapplez, Kpineaplez, Klemonz, Korangez, Kmangoz');
|
||||
});
|
||||
|
||||
it('should append to both side if both defined and not delete empty items', () => {
|
||||
const input: string = 'apple, pineaple, lemon, orange, mango, ';
|
||||
const splitOperatorType: SplitOperatorType = 'symbol';
|
||||
const splitSeparator = ', ';
|
||||
const joinSeparator = ', ';
|
||||
const deleteEmptyItems = false;
|
||||
const left = 'K';
|
||||
const right = 'z';
|
||||
|
||||
const result = wrapList(
|
||||
splitOperatorType,
|
||||
input,
|
||||
splitSeparator,
|
||||
joinSeparator,
|
||||
deleteEmptyItems,
|
||||
left,
|
||||
right
|
||||
);
|
||||
|
||||
expect(result).toBe('Kapplez, Kpineaplez, Klemonz, Korangez, Kmangoz, Kz');
|
||||
});
|
||||
|
||||
it('should append to both side if both defined and delete empty items', () => {
|
||||
const input: string = 'apple, pineaple, lemon, , orange, mango';
|
||||
const splitOperatorType: SplitOperatorType = 'symbol';
|
||||
const splitSeparator = ', ';
|
||||
const joinSeparator = ', ';
|
||||
const deleteEmptyItems = true;
|
||||
const left = 'K';
|
||||
const right = 'z';
|
||||
|
||||
const result = wrapList(
|
||||
splitOperatorType,
|
||||
input,
|
||||
splitSeparator,
|
||||
joinSeparator,
|
||||
deleteEmptyItems,
|
||||
left,
|
||||
right
|
||||
);
|
||||
|
||||
expect(result).toBe('Kapplez, Kpineaplez, Klemonz, Korangez, Kmangoz');
|
||||
});
|
||||
});
|
||||
76
src/pages/tools/number/generate/generate.service.test.ts
Normal file
76
src/pages/tools/number/generate/generate.service.test.ts
Normal file
@@ -0,0 +1,76 @@
|
||||
// Import necessary modules and functions
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { listOfIntegers } from './service';
|
||||
|
||||
// Define test cases for the listOfIntegers function
|
||||
describe('listOfIntegers function', () => {
|
||||
it('should generate a list of integers with comma separator', () => {
|
||||
const initialValue = 1;
|
||||
const step = 2;
|
||||
const count = 5;
|
||||
const separator = ', ';
|
||||
|
||||
const result = listOfIntegers(initialValue, count, step, separator);
|
||||
expect(result).toBe('1, 3, 5, 7, 9');
|
||||
});
|
||||
|
||||
it('should generate a list of integers with dash separator', () => {
|
||||
const initialValue = 0;
|
||||
const step = 3;
|
||||
const count = 4;
|
||||
const separator = ' - ';
|
||||
|
||||
const result = listOfIntegers(initialValue, count, step, separator);
|
||||
expect(result).toBe('0 - 3 - 6 - 9');
|
||||
});
|
||||
|
||||
it('should handle negative initial value and step', () => {
|
||||
const initialValue = -10;
|
||||
const step = -2;
|
||||
const count = 5;
|
||||
const separator = ' ';
|
||||
|
||||
const result = listOfIntegers(initialValue, count, step, separator);
|
||||
expect(result).toBe('-10 -12 -14 -16 -18');
|
||||
});
|
||||
|
||||
it('should handle negative initial value and positive step', () => {
|
||||
const initialValue = -10;
|
||||
const step = 2;
|
||||
const count = 5;
|
||||
const separator = ' ';
|
||||
|
||||
const result = listOfIntegers(initialValue, count, step, separator);
|
||||
expect(result).toBe('-10 -8 -6 -4 -2');
|
||||
});
|
||||
|
||||
it('should float value', () => {
|
||||
const initialValue = -10;
|
||||
const step = 2.5;
|
||||
const count = 5;
|
||||
const separator = ' ';
|
||||
|
||||
const result = listOfIntegers(initialValue, count, step, separator);
|
||||
expect(result).toBe('-10 -7.5 -5 -2.5 0');
|
||||
});
|
||||
|
||||
it('should generate a constant sequence if the step is 0', () => {
|
||||
const initialValue = 1;
|
||||
const step = 0;
|
||||
const count = 5;
|
||||
const separator = ' ';
|
||||
|
||||
const result = listOfIntegers(initialValue, count, step, separator);
|
||||
expect(result).toBe('1 1 1 1 1');
|
||||
});
|
||||
|
||||
it('should generate a constant sequence if the step is 0', () => {
|
||||
const initialValue = 1;
|
||||
const step = 0;
|
||||
const count = 5;
|
||||
const separator = ' ';
|
||||
|
||||
const result = listOfIntegers(initialValue, count, step, separator);
|
||||
expect(result).toBe('1 1 1 1 1');
|
||||
});
|
||||
});
|
||||
79
src/pages/tools/number/generate/index.tsx
Normal file
79
src/pages/tools/number/generate/index.tsx
Normal file
@@ -0,0 +1,79 @@
|
||||
import { Box } from '@mui/material';
|
||||
import React, { useState } from 'react';
|
||||
import ToolTextResult from '@components/result/ToolTextResult';
|
||||
import ToolOptions from '@components/options/ToolOptions';
|
||||
import { listOfIntegers } from './service';
|
||||
import ToolInputAndResult from '@components/ToolInputAndResult';
|
||||
import TextFieldWithDesc from '@components/options/TextFieldWithDesc';
|
||||
|
||||
const initialValues = {
|
||||
firstValue: '1',
|
||||
numberOfNumbers: '10',
|
||||
step: '1',
|
||||
separator: '\\n'
|
||||
};
|
||||
export default function SplitText() {
|
||||
const [result, setResult] = useState<string>('');
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<ToolInputAndResult
|
||||
result={<ToolTextResult title={'Total'} value={result} />}
|
||||
/>
|
||||
<ToolOptions
|
||||
getGroups={({ values, updateField }) => [
|
||||
{
|
||||
title: 'Arithmetic sequence option',
|
||||
component: (
|
||||
<Box>
|
||||
<TextFieldWithDesc
|
||||
description={'Start sequence from this number.'}
|
||||
value={values.firstValue}
|
||||
onOwnChange={(val) => updateField('firstValue', val)}
|
||||
type={'number'}
|
||||
/>
|
||||
<TextFieldWithDesc
|
||||
description={'Increase each element by this amount'}
|
||||
value={values.step}
|
||||
onOwnChange={(val) => updateField('step', val)}
|
||||
type={'number'}
|
||||
/>
|
||||
<TextFieldWithDesc
|
||||
description={'Number of elements in sequence.'}
|
||||
value={values.numberOfNumbers}
|
||||
onOwnChange={(val) => updateField('numberOfNumbers', val)}
|
||||
type={'number'}
|
||||
/>
|
||||
</Box>
|
||||
)
|
||||
},
|
||||
{
|
||||
title: 'Separator',
|
||||
component: (
|
||||
<TextFieldWithDesc
|
||||
description={
|
||||
'Separate elements in the arithmetic sequence by this character.'
|
||||
}
|
||||
value={values.separator}
|
||||
onOwnChange={(val) => updateField('separator', val)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
]}
|
||||
compute={(optionsValues) => {
|
||||
const { firstValue, numberOfNumbers, separator, step } =
|
||||
optionsValues;
|
||||
setResult(
|
||||
listOfIntegers(
|
||||
Number(firstValue),
|
||||
Number(numberOfNumbers),
|
||||
Number(step),
|
||||
separator
|
||||
)
|
||||
);
|
||||
}}
|
||||
initialValues={initialValues}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
14
src/pages/tools/number/generate/meta.ts
Normal file
14
src/pages/tools/number/generate/meta.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { defineTool } from '@tools/defineTool';
|
||||
import { lazy } from 'react';
|
||||
// import image from '@assets/text.png';
|
||||
|
||||
export const tool = defineTool('number', {
|
||||
name: 'Generate numbers',
|
||||
path: 'generate',
|
||||
shortDescription: 'Quickly calculate a list of integers in your browser',
|
||||
// image,
|
||||
description:
|
||||
'Quickly calculate a list of integers in your browser. To get your list, just specify the first integer, change value and total count in the options below, and this utility will generate that many integers',
|
||||
keywords: ['generate'],
|
||||
component: lazy(() => import('./index'))
|
||||
});
|
||||
13
src/pages/tools/number/generate/service.ts
Normal file
13
src/pages/tools/number/generate/service.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
export function listOfIntegers(
|
||||
first_value: number,
|
||||
number_of_numbers: number,
|
||||
step: number,
|
||||
separator: string
|
||||
) {
|
||||
const result: number[] = [];
|
||||
for (let i: number = 0; i < number_of_numbers; i++) {
|
||||
const value: number = first_value + i * step;
|
||||
result.push(value);
|
||||
}
|
||||
return result.join(separator);
|
||||
}
|
||||
4
src/pages/tools/number/index.ts
Normal file
4
src/pages/tools/number/index.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
import { tool as numberSum } from './sum/meta';
|
||||
import { tool as numberGenerate } from './generate/meta';
|
||||
|
||||
export const numberTools = [numberSum, numberGenerate];
|
||||
113
src/pages/tools/number/sum/index.tsx
Normal file
113
src/pages/tools/number/sum/index.tsx
Normal file
@@ -0,0 +1,113 @@
|
||||
import { Box } from '@mui/material';
|
||||
import React, { useState } from 'react';
|
||||
import ToolTextInput from '@components/input/ToolTextInput';
|
||||
import ToolTextResult from '@components/result/ToolTextResult';
|
||||
import ToolOptions from '@components/options/ToolOptions';
|
||||
import { compute, NumberExtractionType } from './service';
|
||||
import RadioWithTextField from '@components/options/RadioWithTextField';
|
||||
import SimpleRadio from '@components/options/SimpleRadio';
|
||||
import CheckboxWithDesc from '@components/options/CheckboxWithDesc';
|
||||
import ToolInputAndResult from '@components/ToolInputAndResult';
|
||||
|
||||
const initialValues = {
|
||||
extractionType: 'smart' as NumberExtractionType,
|
||||
separator: '\\n',
|
||||
printRunningSum: false
|
||||
};
|
||||
const extractionTypes: {
|
||||
title: string;
|
||||
description: string;
|
||||
type: NumberExtractionType;
|
||||
withTextField: boolean;
|
||||
textValueAccessor?: keyof typeof initialValues;
|
||||
}[] = [
|
||||
{
|
||||
title: 'Smart sum',
|
||||
description: 'Auto detect numbers in the input.',
|
||||
type: 'smart',
|
||||
withTextField: false
|
||||
},
|
||||
{
|
||||
title: 'Number Delimiter',
|
||||
type: 'delimiter',
|
||||
description:
|
||||
'Input SeparatorCustomize the number separator here. (By default a line break.)',
|
||||
withTextField: true,
|
||||
textValueAccessor: 'separator'
|
||||
}
|
||||
];
|
||||
|
||||
export default function SplitText() {
|
||||
const [input, setInput] = useState<string>('');
|
||||
const [result, setResult] = useState<string>('');
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<ToolInputAndResult
|
||||
input={<ToolTextInput value={input} onChange={setInput} />}
|
||||
result={<ToolTextResult title={'Total'} value={result} />}
|
||||
/>
|
||||
<ToolOptions
|
||||
getGroups={({ values, updateField }) => [
|
||||
{
|
||||
title: 'Number extraction',
|
||||
component: extractionTypes.map(
|
||||
({
|
||||
title,
|
||||
description,
|
||||
type,
|
||||
withTextField,
|
||||
textValueAccessor
|
||||
}) =>
|
||||
withTextField ? (
|
||||
<RadioWithTextField
|
||||
key={type}
|
||||
checked={type === values.extractionType}
|
||||
title={title}
|
||||
fieldName={'extractionType'}
|
||||
description={description}
|
||||
value={
|
||||
textValueAccessor
|
||||
? values[textValueAccessor].toString()
|
||||
: ''
|
||||
}
|
||||
onRadioClick={() => updateField('extractionType', type)}
|
||||
onTextChange={(val) =>
|
||||
textValueAccessor
|
||||
? updateField(textValueAccessor, val)
|
||||
: null
|
||||
}
|
||||
/>
|
||||
) : (
|
||||
<SimpleRadio
|
||||
key={title}
|
||||
onClick={() => updateField('extractionType', type)}
|
||||
checked={values.extractionType === type}
|
||||
description={description}
|
||||
title={title}
|
||||
/>
|
||||
)
|
||||
)
|
||||
},
|
||||
{
|
||||
title: 'Running Sum',
|
||||
component: (
|
||||
<CheckboxWithDesc
|
||||
title={'Print Running Sum'}
|
||||
description={"Display the sum as it's calculated step by step."}
|
||||
checked={values.printRunningSum}
|
||||
onChange={(value) => updateField('printRunningSum', value)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
]}
|
||||
compute={(optionsValues, input) => {
|
||||
const { extractionType, printRunningSum, separator } = optionsValues;
|
||||
setResult(compute(input, extractionType, printRunningSum, separator));
|
||||
}}
|
||||
initialValues={initialValues}
|
||||
input={input}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
14
src/pages/tools/number/sum/meta.ts
Normal file
14
src/pages/tools/number/sum/meta.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { defineTool } from '@tools/defineTool';
|
||||
import { lazy } from 'react';
|
||||
// import image from '@assets/text.png';
|
||||
|
||||
export const tool = defineTool('number', {
|
||||
name: 'Number Sum Calculator',
|
||||
path: 'sum',
|
||||
// image,
|
||||
description:
|
||||
'Quickly calculate the sum of numbers in your browser. To get your sum, just enter your list of numbers in the input field, adjust the separator between the numbers in the options below, and this utility will add up all these numbers.',
|
||||
shortDescription: 'Quickly sum numbers',
|
||||
keywords: ['sum'],
|
||||
component: lazy(() => import('./index'))
|
||||
});
|
||||
37
src/pages/tools/number/sum/service.ts
Normal file
37
src/pages/tools/number/sum/service.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
export type NumberExtractionType = 'smart' | 'delimiter';
|
||||
|
||||
function getAllNumbers(text: string): number[] {
|
||||
const regex = /\d+/g;
|
||||
const matches = text.match(regex);
|
||||
return matches ? matches.map(Number) : [];
|
||||
}
|
||||
|
||||
export const compute = (
|
||||
input: string,
|
||||
extractionType: NumberExtractionType,
|
||||
printRunningSum: boolean,
|
||||
separator: string
|
||||
): string => {
|
||||
let numbers: number[] = [];
|
||||
if (extractionType === 'smart') {
|
||||
numbers = getAllNumbers(input);
|
||||
} else {
|
||||
const parts = input.split(separator);
|
||||
// Filter out and convert parts that are numbers
|
||||
numbers = parts
|
||||
.filter((part) => !isNaN(Number(part)) && part.trim() !== '')
|
||||
.map(Number);
|
||||
}
|
||||
if (printRunningSum) {
|
||||
let result: string = '';
|
||||
let sum: number = 0;
|
||||
for (const i of numbers) {
|
||||
sum = sum + i;
|
||||
result = result + sum + '\n';
|
||||
}
|
||||
return result;
|
||||
} else
|
||||
return numbers
|
||||
.reduce((previousValue, currentValue) => previousValue + currentValue, 0)
|
||||
.toString();
|
||||
};
|
||||
64
src/pages/tools/number/sum/sum.service.test.ts
Normal file
64
src/pages/tools/number/sum/sum.service.test.ts
Normal file
@@ -0,0 +1,64 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { compute } from './service';
|
||||
|
||||
describe('compute function', () => {
|
||||
it('should correctly sum numbers in smart extraction mode', () => {
|
||||
const input = 'The 2 cats have 4 and 7 kittens';
|
||||
const result = compute(input, 'smart', false, ',');
|
||||
expect(result).toBe('13');
|
||||
});
|
||||
|
||||
it('should correctly sum numbers with custom delimiter', () => {
|
||||
const input = '2,4,7';
|
||||
const result = compute(input, 'delimiter', false, ',');
|
||||
expect(result).toBe('13');
|
||||
});
|
||||
|
||||
it('should return running sum in smart extraction mode', () => {
|
||||
const input = 'The 2 cats have 4 and 7 kittens';
|
||||
const result = compute(input, 'smart', true, ',');
|
||||
expect(result).toBe('2\n6\n13\n');
|
||||
});
|
||||
|
||||
it('should return running sum with custom delimiter', () => {
|
||||
const input = '2,4,7';
|
||||
const result = compute(input, 'delimiter', true, ',');
|
||||
expect(result).toBe('2\n6\n13\n');
|
||||
});
|
||||
|
||||
it('should handle empty input gracefully in smart mode', () => {
|
||||
const input = '';
|
||||
const result = compute(input, 'smart', false, ',');
|
||||
expect(result).toBe('0');
|
||||
});
|
||||
|
||||
it('should handle empty input gracefully in delimiter mode', () => {
|
||||
const input = '';
|
||||
const result = compute(input, 'delimiter', false, ',');
|
||||
expect(result).toBe('0');
|
||||
});
|
||||
|
||||
it('should handle input with no numbers in smart mode', () => {
|
||||
const input = 'There are no numbers here';
|
||||
const result = compute(input, 'smart', false, ',');
|
||||
expect(result).toBe('0');
|
||||
});
|
||||
|
||||
it('should handle input with no numbers in delimiter mode', () => {
|
||||
const input = 'a,b,c';
|
||||
const result = compute(input, 'delimiter', false, ',');
|
||||
expect(result).toBe('0');
|
||||
});
|
||||
|
||||
it('should ignore non-numeric parts in delimiter mode', () => {
|
||||
const input = '2,a,4,b,7';
|
||||
const result = compute(input, 'delimiter', false, ',');
|
||||
expect(result).toBe('13');
|
||||
});
|
||||
|
||||
it('should handle different separators', () => {
|
||||
const input = '2;4;7';
|
||||
const result = compute(input, 'delimiter', false, ';');
|
||||
expect(result).toBe('13');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,66 @@
|
||||
import { describe, expect } from 'vitest';
|
||||
import { createPalindrome, createPalindromeList } from './service';
|
||||
|
||||
describe('createPalindrome', () => {
|
||||
test('should create palindrome by reversing the entire string', () => {
|
||||
const input = 'hello';
|
||||
const result = createPalindrome(input, true);
|
||||
expect(result).toBe('helloolleh');
|
||||
});
|
||||
|
||||
test('should create palindrome by reversing the string excluding the last character', () => {
|
||||
const input = 'hello';
|
||||
const result = createPalindrome(input, false);
|
||||
expect(result).toBe('hellolleh');
|
||||
});
|
||||
|
||||
test('should return an empty string if input is empty', () => {
|
||||
const input = '';
|
||||
const result = createPalindrome(input, true);
|
||||
expect(result).toBe('');
|
||||
});
|
||||
});
|
||||
|
||||
describe('createPalindromeList', () => {
|
||||
test('should create palindrome for single-line input', () => {
|
||||
const input = 'hello';
|
||||
const result = createPalindromeList(input, true, false);
|
||||
expect(result).toBe('helloolleh');
|
||||
});
|
||||
|
||||
test('should create palindrome for single-line input considering trailing spaces', () => {
|
||||
const input = 'hello ';
|
||||
const result = createPalindromeList(input, true, false);
|
||||
expect(result).toBe('hello olleh');
|
||||
});
|
||||
|
||||
test('should create palindrome for single-line input ignoring trailing spaces if lastChar is set to false', () => {
|
||||
const input = 'hello ';
|
||||
const result = createPalindromeList(input, true, false);
|
||||
expect(result).toBe('hello olleh');
|
||||
});
|
||||
|
||||
test('should create palindrome for multi-line input', () => {
|
||||
const input = 'hello\nworld';
|
||||
const result = createPalindromeList(input, true, true);
|
||||
expect(result).toBe('helloolleh\nworlddlrow');
|
||||
});
|
||||
|
||||
test('should create palindrome for no multi-line input', () => {
|
||||
const input = 'hello\nworld\n';
|
||||
const result = createPalindromeList(input, true, false);
|
||||
expect(result).toBe('hello\nworld\n\ndlrow\nolleh');
|
||||
});
|
||||
|
||||
test('should handle multi-line input with lastChar set to false', () => {
|
||||
const input = 'hello\nworld';
|
||||
const result = createPalindromeList(input, false, true);
|
||||
expect(result).toBe('hellolleh\nworldlrow');
|
||||
});
|
||||
|
||||
test('should return an empty string if input is empty', () => {
|
||||
const input = '';
|
||||
const result = createPalindromeList(input, true, false);
|
||||
expect(result).toBe('');
|
||||
});
|
||||
});
|
||||
11
src/pages/tools/string/create-palindrome/index.tsx
Normal file
11
src/pages/tools/string/create-palindrome/index.tsx
Normal file
@@ -0,0 +1,11 @@
|
||||
import { Box } from '@mui/material';
|
||||
import React from 'react';
|
||||
import * as Yup from 'yup';
|
||||
|
||||
const initialValues = {};
|
||||
const validationSchema = Yup.object({
|
||||
// splitSeparator: Yup.string().required('The separator is required')
|
||||
});
|
||||
export default function CreatePalindrome() {
|
||||
return <Box>Lorem ipsum</Box>;
|
||||
}
|
||||
13
src/pages/tools/string/create-palindrome/meta.ts
Normal file
13
src/pages/tools/string/create-palindrome/meta.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { defineTool } from '@tools/defineTool';
|
||||
import { lazy } from 'react';
|
||||
// import image from '@assets/text.png';
|
||||
|
||||
export const tool = defineTool('string', {
|
||||
name: 'Create palindrome',
|
||||
path: 'create-palindrome',
|
||||
// image,
|
||||
description: '',
|
||||
shortDescription: '',
|
||||
keywords: ['create', 'palindrome'],
|
||||
component: lazy(() => import('./index'))
|
||||
});
|
||||
36
src/pages/tools/string/create-palindrome/service.ts
Normal file
36
src/pages/tools/string/create-palindrome/service.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import { reverseString } from 'utils/string';
|
||||
|
||||
export function createPalindrome(
|
||||
input: string,
|
||||
lastChar: boolean // only checkbox is need here to handle it [instead of two combo boxes]
|
||||
) {
|
||||
if (!input) return '';
|
||||
let result: string;
|
||||
let reversedString: string;
|
||||
|
||||
// reverse the whole input if lastChar enabled
|
||||
reversedString = lastChar
|
||||
? reverseString(input)
|
||||
: reverseString(input.slice(0, -1));
|
||||
result = input.concat(reversedString);
|
||||
return result;
|
||||
}
|
||||
|
||||
export function createPalindromeList(
|
||||
input: string,
|
||||
lastChar: boolean,
|
||||
multiLine: boolean
|
||||
): string {
|
||||
if (!input) return '';
|
||||
let array: string[];
|
||||
const result: string[] = [];
|
||||
|
||||
if (!multiLine) return createPalindrome(input, lastChar);
|
||||
else {
|
||||
array = input.split('\n');
|
||||
for (const word of array) {
|
||||
result.push(createPalindrome(word, lastChar));
|
||||
}
|
||||
}
|
||||
return result.join('\n');
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { extractSubstring } from './service';
|
||||
|
||||
describe('extractSubstring', () => {
|
||||
it('should extract a substring from single-line input', () => {
|
||||
const input = 'hello world';
|
||||
const result = extractSubstring(input, 1, 4, false, false);
|
||||
expect(result).toBe('hell');
|
||||
});
|
||||
|
||||
it('should extract and reverse a substring from single-line input', () => {
|
||||
const input = 'hello world';
|
||||
const result = extractSubstring(input, 1, 5, false, true);
|
||||
expect(result).toBe('olleh');
|
||||
});
|
||||
|
||||
it('should extract substrings from multi-line input', () => {
|
||||
const input = 'hello\nworld';
|
||||
const result = extractSubstring(input, 1, 5, true, false);
|
||||
expect(result).toBe('hello\nworld');
|
||||
});
|
||||
|
||||
it('should extract and reverse substrings from multi-line input', () => {
|
||||
const input = 'hello\nworld';
|
||||
const result = extractSubstring(input, 1, 4, true, true);
|
||||
expect(result).toBe('lleh\nlrow');
|
||||
});
|
||||
|
||||
it('should handle empty input', () => {
|
||||
const input = '';
|
||||
const result = extractSubstring(input, 1, 5, false, false);
|
||||
expect(result).toBe('');
|
||||
});
|
||||
|
||||
it('should handle start and length out of bounds', () => {
|
||||
const input = 'hello';
|
||||
const result = extractSubstring(input, 10, 5, false, false);
|
||||
expect(result).toBe('');
|
||||
});
|
||||
|
||||
it('should handle negative start and length', () => {
|
||||
expect(() => extractSubstring('hello', -1, 5, false, false)).toThrow(
|
||||
'Start index must be greater than zero.'
|
||||
);
|
||||
expect(() => extractSubstring('hello', 1, -5, false, false)).toThrow(
|
||||
'Length value must be greater than or equal to zero.'
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle zero length', () => {
|
||||
const input = 'hello';
|
||||
const result = extractSubstring(input, 1, 0, false, false);
|
||||
expect(result).toBe('');
|
||||
});
|
||||
|
||||
it('should work', () => {
|
||||
const input = 'je me nomme king\n22 est mon chiffre';
|
||||
const result = extractSubstring(input, 12, 7, true, false);
|
||||
expect(result).toBe(' king\nchiffre');
|
||||
});
|
||||
});
|
||||
11
src/pages/tools/string/extract-substring/index.tsx
Normal file
11
src/pages/tools/string/extract-substring/index.tsx
Normal file
@@ -0,0 +1,11 @@
|
||||
import { Box } from '@mui/material';
|
||||
import React from 'react';
|
||||
import * as Yup from 'yup';
|
||||
|
||||
const initialValues = {};
|
||||
const validationSchema = Yup.object({
|
||||
// splitSeparator: Yup.string().required('The separator is required')
|
||||
});
|
||||
export default function ExtractSubstring() {
|
||||
return <Box>Lorem ipsum</Box>;
|
||||
}
|
||||
13
src/pages/tools/string/extract-substring/meta.ts
Normal file
13
src/pages/tools/string/extract-substring/meta.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { defineTool } from '@tools/defineTool';
|
||||
import { lazy } from 'react';
|
||||
// import image from '@assets/text.png';
|
||||
|
||||
export const tool = defineTool('string', {
|
||||
name: 'Extract substring',
|
||||
path: 'extract-substring',
|
||||
// image,
|
||||
description: '',
|
||||
shortDescription: '',
|
||||
keywords: ['extract', 'substring'],
|
||||
component: lazy(() => import('./index'))
|
||||
});
|
||||
36
src/pages/tools/string/extract-substring/service.ts
Normal file
36
src/pages/tools/string/extract-substring/service.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import { reverseString } from 'utils/string';
|
||||
|
||||
export function extractSubstring(
|
||||
input: string,
|
||||
start: number,
|
||||
length: number,
|
||||
multiLine: boolean,
|
||||
reverse: boolean
|
||||
): string {
|
||||
if (!input) return '';
|
||||
// edge Cases
|
||||
if (start <= 0) throw new Error('Start index must be greater than zero.');
|
||||
if (length < 0)
|
||||
throw new Error('Length value must be greater than or equal to zero.');
|
||||
if (length === 0) return '';
|
||||
|
||||
let array: string[];
|
||||
let result: string[] = [];
|
||||
|
||||
const extract = (str: string, start: number, length: number): string => {
|
||||
const end = start - 1 + length;
|
||||
if (start - 1 >= str.length) return '';
|
||||
return str.substring(start - 1, Math.min(end, str.length));
|
||||
};
|
||||
|
||||
if (!multiLine) {
|
||||
result.push(extract(input, start, length));
|
||||
} else {
|
||||
array = input.split('\n');
|
||||
for (const word of array) {
|
||||
result.push(extract(word, start, length));
|
||||
}
|
||||
}
|
||||
result = reverse ? result.map((word) => reverseString(word)) : result;
|
||||
return result.join('\n');
|
||||
}
|
||||
21
src/pages/tools/string/index.ts
Normal file
21
src/pages/tools/string/index.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import { tool as stringReverse } from './reverse/meta';
|
||||
import { tool as stringRandomizeCase } from './randomize-case/meta';
|
||||
import { tool as stringUppercase } from './uppercase/meta';
|
||||
import { tool as stringExtractSubstring } from './extract-substring/meta';
|
||||
import { tool as stringCreatePalindrome } from './create-palindrome/meta';
|
||||
import { tool as stringPalindrome } from './palindrome/meta';
|
||||
import { tool as stringToMorse } from './to-morse/meta';
|
||||
import { tool as stringSplit } from './split/meta';
|
||||
import { tool as stringJoin } from './join/meta';
|
||||
|
||||
export const stringTools = [
|
||||
stringSplit,
|
||||
stringJoin,
|
||||
stringToMorse,
|
||||
stringReverse,
|
||||
stringRandomizeCase,
|
||||
stringUppercase,
|
||||
stringExtractSubstring,
|
||||
stringCreatePalindrome,
|
||||
stringPalindrome
|
||||
];
|
||||
190
src/pages/tools/string/join/index.tsx
Normal file
190
src/pages/tools/string/join/index.tsx
Normal file
@@ -0,0 +1,190 @@
|
||||
import { Box } from '@mui/material';
|
||||
import React, { useState } from 'react';
|
||||
import * as Yup from 'yup';
|
||||
import ToolTextInput from '@components/input/ToolTextInput';
|
||||
import ToolTextResult from '@components/result/ToolTextResult';
|
||||
import ToolOptions from '@components/options/ToolOptions';
|
||||
import { mergeText } from './service';
|
||||
import TextFieldWithDesc from '@components/options/TextFieldWithDesc';
|
||||
import CheckboxWithDesc from '@components/options/CheckboxWithDesc';
|
||||
import ToolInputAndResult from '@components/ToolInputAndResult';
|
||||
|
||||
import ToolInfo from '@components/ToolInfo';
|
||||
import Separator from '@components/Separator';
|
||||
import Examples from '@components/examples/Examples';
|
||||
|
||||
const initialValues = {
|
||||
joinCharacter: '',
|
||||
deleteBlank: true,
|
||||
deleteTrailing: true
|
||||
};
|
||||
|
||||
const validationSchema = Yup.object().shape({
|
||||
joinCharacter: Yup.string().required('Join character is required'),
|
||||
deleteBlank: Yup.boolean().required('Delete blank is required'),
|
||||
deleteTrailing: Yup.boolean().required('Delete trailing is required')
|
||||
});
|
||||
|
||||
const mergeOptions = {
|
||||
placeholder: 'Join Character',
|
||||
description:
|
||||
'Symbol that connects broken\n' + 'pieces of text. (Space by default.)\n',
|
||||
accessor: 'joinCharacter' as keyof typeof initialValues
|
||||
};
|
||||
|
||||
const blankTrailingOptions: {
|
||||
title: string;
|
||||
description: string;
|
||||
accessor: keyof typeof initialValues;
|
||||
}[] = [
|
||||
{
|
||||
title: 'Delete Blank Lines',
|
||||
description: "Delete lines that don't have\n text symbols.\n",
|
||||
accessor: 'deleteBlank'
|
||||
},
|
||||
{
|
||||
title: 'Delete Trailing Spaces',
|
||||
description: 'Remove spaces and tabs at\n the end of the lines.\n',
|
||||
accessor: 'deleteTrailing'
|
||||
}
|
||||
];
|
||||
|
||||
const exampleCards = [
|
||||
{
|
||||
title: 'Merge a To-Do List',
|
||||
description:
|
||||
"In this example, we merge a bullet point list into one sentence, separating each item by the word 'and'. We also remove all empty lines and trailing spaces. If we didn't remove the empty lines, then they'd be joined with the separator word, making the separator word appear multiple times. If we didn't remove the trailing tabs and spaces, then they'd create extra spacing in the joined text and it wouldn't look nice.",
|
||||
sampleText: `clean the house
|
||||
|
||||
go shopping
|
||||
feed the cat
|
||||
|
||||
make dinner
|
||||
build a rocket ship and fly away`,
|
||||
sampleResult: `clean the house and go shopping and feed the cat and make dinner and build a rocket ship and fly away`,
|
||||
requiredOptions: {
|
||||
joinCharacter: 'and',
|
||||
deleteBlankLines: true,
|
||||
deleteTrailingSpaces: true
|
||||
}
|
||||
},
|
||||
{
|
||||
title: 'Comma Separated List',
|
||||
description:
|
||||
'This example joins a column of words into a comma separated list of words.',
|
||||
sampleText: `computer
|
||||
memory
|
||||
processor
|
||||
mouse
|
||||
keyboard`,
|
||||
sampleResult: `computer, memory, processor, mouse, keyboard`,
|
||||
requiredOptions: {
|
||||
joinCharacter: ',',
|
||||
deleteBlankLines: false,
|
||||
deleteTrailingSpaces: false
|
||||
}
|
||||
},
|
||||
{
|
||||
title: 'Vertical Word to Horizontal',
|
||||
description:
|
||||
'This example rotates words from a vertical position to horizontal. An empty separator is used for this purpose.',
|
||||
sampleText: `T
|
||||
e
|
||||
x
|
||||
t
|
||||
a
|
||||
b
|
||||
u
|
||||
l
|
||||
o
|
||||
u
|
||||
s
|
||||
!`,
|
||||
sampleResult: `Textabulous!`,
|
||||
requiredOptions: {
|
||||
joinCharacter: '',
|
||||
deleteBlankLines: false,
|
||||
deleteTrailingSpaces: false
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
export default function JoinText() {
|
||||
const [input, setInput] = useState<string>('');
|
||||
const [result, setResult] = useState<string>('');
|
||||
|
||||
const compute = (optionsValues: typeof initialValues, input: any) => {
|
||||
const { joinCharacter, deleteBlank, deleteTrailing } = optionsValues;
|
||||
setResult(mergeText(input, deleteBlank, deleteTrailing, joinCharacter));
|
||||
};
|
||||
|
||||
function changeInputResult(input: string, result: string) {
|
||||
setInput(input);
|
||||
setResult(result);
|
||||
|
||||
const toolsElement = document.getElementById('tool');
|
||||
if (toolsElement) {
|
||||
toolsElement.scrollIntoView({ behavior: 'smooth' });
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<ToolInputAndResult
|
||||
input={
|
||||
<ToolTextInput
|
||||
title={'Text Pieces'}
|
||||
value={input}
|
||||
onChange={setInput}
|
||||
/>
|
||||
}
|
||||
result={<ToolTextResult title={'Joined Text'} value={result} />}
|
||||
/>
|
||||
<ToolOptions
|
||||
compute={compute}
|
||||
getGroups={({ values, updateField }) => [
|
||||
{
|
||||
title: 'Text Merged Options',
|
||||
component: (
|
||||
<TextFieldWithDesc
|
||||
placeholder={mergeOptions.placeholder}
|
||||
value={values['joinCharacter']}
|
||||
onOwnChange={(value) =>
|
||||
updateField(mergeOptions.accessor, value)
|
||||
}
|
||||
description={mergeOptions.description}
|
||||
/>
|
||||
)
|
||||
},
|
||||
{
|
||||
title: 'Blank Lines and Trailing Spaces',
|
||||
component: blankTrailingOptions.map((option) => (
|
||||
<CheckboxWithDesc
|
||||
key={option.accessor}
|
||||
title={option.title}
|
||||
checked={!!values[option.accessor]}
|
||||
onChange={(value) => updateField(option.accessor, value)}
|
||||
description={option.description}
|
||||
/>
|
||||
))
|
||||
}
|
||||
]}
|
||||
initialValues={initialValues}
|
||||
input={input}
|
||||
/>
|
||||
<ToolInfo
|
||||
title="What Is a Text Joiner?"
|
||||
description="With this tool you can join parts of the text together. It takes a list of text values, separated by newlines, and merges them together. You can set the character that will be placed between the parts of the combined text. Also, you can ignore all empty lines and remove spaces and tabs at the end of all lines. Textabulous!"
|
||||
/>
|
||||
<Separator backgroundColor="#5581b5" margin="50px" />
|
||||
<Examples
|
||||
title="Text Joiner Examples"
|
||||
subtitle="Click to try!"
|
||||
exampleCards={exampleCards.map((card) => ({
|
||||
...card,
|
||||
changeInputResult
|
||||
}))}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
14
src/pages/tools/string/join/meta.ts
Normal file
14
src/pages/tools/string/join/meta.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { defineTool } from '@tools/defineTool';
|
||||
import { lazy } from 'react';
|
||||
import image from '@assets/text.png';
|
||||
|
||||
export const tool = defineTool('string', {
|
||||
path: 'join',
|
||||
name: 'Text Joiner',
|
||||
image,
|
||||
description:
|
||||
"World's Simplest Text Tool World's simplest browser-based utility for joining text. Load your text in the input form on the left and you'll automatically get merged text on the right. Powerful, free, and fast. Load text – get joined lines",
|
||||
shortDescription: 'Quickly merge texts',
|
||||
keywords: ['text', 'join'],
|
||||
component: lazy(() => import('./index'))
|
||||
});
|
||||
17
src/pages/tools/string/join/service.ts
Normal file
17
src/pages/tools/string/join/service.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
export function mergeText(
|
||||
text: string,
|
||||
deleteBlankLines: boolean = true,
|
||||
deleteTrailingSpaces: boolean = true,
|
||||
joinCharacter: string = ''
|
||||
): string {
|
||||
let processedLines: string[] = text.split('\n');
|
||||
if (deleteTrailingSpaces) {
|
||||
processedLines = processedLines.map((line) => line.trimEnd());
|
||||
}
|
||||
|
||||
if (deleteBlankLines) {
|
||||
processedLines = processedLines.filter((line) => line.trim());
|
||||
}
|
||||
|
||||
return processedLines.join(joinCharacter);
|
||||
}
|
||||
18
src/pages/tools/string/join/string-join.e2e.spec.ts
Normal file
18
src/pages/tools/string/join/string-join.e2e.spec.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { expect, test } from '@playwright/test';
|
||||
|
||||
test.describe('JoinText Component', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto('/string/join');
|
||||
});
|
||||
|
||||
test('should merge text pieces with specified join character', async ({
|
||||
page
|
||||
}) => {
|
||||
// Input the text pieces
|
||||
await page.getByTestId('text-input').fill('1\n2');
|
||||
|
||||
const result = await page.getByTestId('text-result').inputValue();
|
||||
|
||||
expect(result).toBe('12');
|
||||
});
|
||||
});
|
||||
64
src/pages/tools/string/join/string-join.service.test.ts
Normal file
64
src/pages/tools/string/join/string-join.service.test.ts
Normal file
@@ -0,0 +1,64 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { mergeText } from './service';
|
||||
|
||||
describe('mergeText', () => {
|
||||
it('should merge lines with default settings (delete blank lines, delete trailing spaces, join with empty string)', () => {
|
||||
const input = 'line1 \n \nline2\nline3 \n\nline4';
|
||||
const expected = 'line1line2line3line4';
|
||||
expect(mergeText(input)).toBe(expected);
|
||||
});
|
||||
|
||||
it('should merge lines and preserve blank lines when deleteBlankLines is false', () => {
|
||||
const input = 'line1 \n \nline2\nline3 \n\nline4';
|
||||
const expected = 'line1line2line3line4';
|
||||
expect(mergeText(input, false, true, '')).toBe(expected);
|
||||
});
|
||||
|
||||
it('should merge lines and preserve trailing spaces when deleteTrailingSpaces is false', () => {
|
||||
const input = 'line1 \n \nline2\nline3 \n\nline4';
|
||||
const expected = 'line1 line2line3 line4';
|
||||
expect(mergeText(input, true, false)).toBe(expected);
|
||||
});
|
||||
|
||||
it('should merge lines, preserve blank lines and trailing spaces when both deleteBlankLines and deleteTrailingSpaces are false', () => {
|
||||
const input = 'line1 \n \nline2\nline3 \n\nline4';
|
||||
const expected = 'line1 line2line3 line4';
|
||||
expect(mergeText(input, false, false)).toBe(expected);
|
||||
});
|
||||
|
||||
it('should merge lines with a specified joinCharacter', () => {
|
||||
const input = 'line1 \n \nline2\nline3 \n\nline4';
|
||||
const expected = 'line1 line2 line3 line4';
|
||||
expect(mergeText(input, true, true, ' ')).toBe(expected);
|
||||
});
|
||||
|
||||
it('should handle empty input', () => {
|
||||
const input = '';
|
||||
const expected = '';
|
||||
expect(mergeText(input)).toBe(expected);
|
||||
});
|
||||
|
||||
it('should handle input with only blank lines', () => {
|
||||
const input = ' \n \n\n';
|
||||
const expected = '';
|
||||
expect(mergeText(input)).toBe(expected);
|
||||
});
|
||||
|
||||
it('should handle input with only trailing spaces', () => {
|
||||
const input = 'line1 \nline2 \nline3 ';
|
||||
const expected = 'line1line2line3';
|
||||
expect(mergeText(input)).toBe(expected);
|
||||
});
|
||||
|
||||
it('should handle single line input', () => {
|
||||
const input = 'single line';
|
||||
const expected = 'single line';
|
||||
expect(mergeText(input)).toBe(expected);
|
||||
});
|
||||
|
||||
it('should join lines with new line character when joinCharacter is set to "\\n"', () => {
|
||||
const input = 'line1 \n \nline2\nline3 \n\nline4';
|
||||
const expected = 'line1\nline2\nline3\nline4';
|
||||
expect(mergeText(input, true, true, '\n')).toBe(expected);
|
||||
});
|
||||
});
|
||||
11
src/pages/tools/string/palindrome/index.tsx
Normal file
11
src/pages/tools/string/palindrome/index.tsx
Normal file
@@ -0,0 +1,11 @@
|
||||
import { Box } from '@mui/material';
|
||||
import React from 'react';
|
||||
import * as Yup from 'yup';
|
||||
|
||||
const initialValues = {};
|
||||
const validationSchema = Yup.object({
|
||||
// splitSeparator: Yup.string().required('The separator is required')
|
||||
});
|
||||
export default function Palindrome() {
|
||||
return <Box>Lorem ipsum</Box>;
|
||||
}
|
||||
13
src/pages/tools/string/palindrome/meta.ts
Normal file
13
src/pages/tools/string/palindrome/meta.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { defineTool } from '@tools/defineTool';
|
||||
import { lazy } from 'react';
|
||||
// import image from '@assets/text.png';
|
||||
|
||||
export const tool = defineTool('string', {
|
||||
name: 'Palindrome',
|
||||
path: 'palindrome',
|
||||
// image,
|
||||
description: '',
|
||||
shortDescription: '',
|
||||
keywords: ['palindrome'],
|
||||
component: lazy(() => import('./index'))
|
||||
});
|
||||
60
src/pages/tools/string/palindrome/palindrome.service.test.ts
Normal file
60
src/pages/tools/string/palindrome/palindrome.service.test.ts
Normal file
@@ -0,0 +1,60 @@
|
||||
import { describe, expect } from 'vitest';
|
||||
import { palindromeList } from './service';
|
||||
|
||||
describe('palindromeList', () => {
|
||||
test('should return true for single character words', () => {
|
||||
const input = 'a|b|c';
|
||||
const separator = '|';
|
||||
const result = palindromeList('symbol', input, separator);
|
||||
expect(result).toBe('true|true|true');
|
||||
});
|
||||
|
||||
test('should return false for non-palindromes', () => {
|
||||
const input = 'hello|world';
|
||||
const separator = '|';
|
||||
const result = palindromeList('symbol', input, separator);
|
||||
expect(result).toBe('false|false');
|
||||
});
|
||||
|
||||
test('should split using regex', () => {
|
||||
const input = 'racecar,abba,hello';
|
||||
const separator = ',';
|
||||
const result = palindromeList('regex', input, separator);
|
||||
expect(result).toBe('true,true,false');
|
||||
});
|
||||
|
||||
test('should return empty string for empty input', () => {
|
||||
const input = '';
|
||||
const separator = '|';
|
||||
const result = palindromeList('symbol', input, separator);
|
||||
expect(result).toBe('');
|
||||
});
|
||||
|
||||
test('should split using custom separator', () => {
|
||||
const input = 'racecar;abba;hello';
|
||||
const separator = ';';
|
||||
const result = palindromeList('symbol', input, separator);
|
||||
expect(result).toBe('true;true;false');
|
||||
});
|
||||
|
||||
test('should handle leading and trailing spaces', () => {
|
||||
const input = ' racecar | abba | hello ';
|
||||
const separator = '|';
|
||||
const result = palindromeList('symbol', input, separator);
|
||||
expect(result).toBe('true|true|false');
|
||||
});
|
||||
|
||||
test('should handle multilines checking with trimming', () => {
|
||||
const input = ' racecar \n abba \n hello ';
|
||||
const separator = '\n';
|
||||
const result = palindromeList('symbol', input, separator);
|
||||
expect(result).toBe('true\ntrue\nfalse');
|
||||
});
|
||||
|
||||
test('should handle empty strings in input', () => {
|
||||
const input = 'racecar||hello';
|
||||
const separator = '|';
|
||||
const result = palindromeList('symbol', input, separator);
|
||||
expect(result).toBe('true|true|false');
|
||||
});
|
||||
});
|
||||
41
src/pages/tools/string/palindrome/service.ts
Normal file
41
src/pages/tools/string/palindrome/service.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
export type SplitOperatorType = 'symbol' | 'regex';
|
||||
|
||||
function isPalindrome(word: string, left: number, right: number): boolean {
|
||||
if (left >= right) return true;
|
||||
if (word[left] !== word[right]) return false;
|
||||
|
||||
return isPalindrome(word, left + 1, right - 1);
|
||||
}
|
||||
|
||||
// check each word of the input and add the palindrome status in an array
|
||||
function checkPalindromes(array: string[]): boolean[] {
|
||||
const status: boolean[] = [];
|
||||
for (const word of array) {
|
||||
const palindromeStatus = isPalindrome(word, 0, word.length - 1);
|
||||
status.push(palindromeStatus);
|
||||
}
|
||||
return status;
|
||||
}
|
||||
|
||||
export function palindromeList(
|
||||
splitOperatorType: SplitOperatorType,
|
||||
input: string,
|
||||
separator: string // the splitting separator will be the joining separator for visual satisfaction
|
||||
): string {
|
||||
if (!input) return '';
|
||||
let array: string[];
|
||||
switch (splitOperatorType) {
|
||||
case 'symbol':
|
||||
array = input.split(separator);
|
||||
break;
|
||||
case 'regex':
|
||||
array = input.split(new RegExp(separator));
|
||||
break;
|
||||
}
|
||||
// trim all items to focus on the word and not biasing the result due to spaces (leading and trailing)
|
||||
array = array.map((item) => item.trim());
|
||||
|
||||
const statusArray = checkPalindromes(array);
|
||||
|
||||
return statusArray.map((status) => status.toString()).join(separator);
|
||||
}
|
||||
11
src/pages/tools/string/randomize-case/index.tsx
Normal file
11
src/pages/tools/string/randomize-case/index.tsx
Normal file
@@ -0,0 +1,11 @@
|
||||
import { Box } from '@mui/material';
|
||||
import React from 'react';
|
||||
import * as Yup from 'yup';
|
||||
|
||||
const initialValues = {};
|
||||
const validationSchema = Yup.object({
|
||||
// splitSeparator: Yup.string().required('The separator is required')
|
||||
});
|
||||
export default function RandomizeCase() {
|
||||
return <Box>Lorem ipsum</Box>;
|
||||
}
|
||||
13
src/pages/tools/string/randomize-case/meta.ts
Normal file
13
src/pages/tools/string/randomize-case/meta.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { defineTool } from '@tools/defineTool';
|
||||
import { lazy } from 'react';
|
||||
// import image from '@assets/text.png';
|
||||
|
||||
export const tool = defineTool('string', {
|
||||
name: 'Randomize case',
|
||||
path: 'randomize-case',
|
||||
// image,
|
||||
description: '',
|
||||
shortDescription: '',
|
||||
keywords: ['randomize', 'case'],
|
||||
component: lazy(() => import('./index'))
|
||||
});
|
||||
@@ -0,0 +1,50 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { randomizeCase } from './service';
|
||||
|
||||
describe('randomizeCase', () => {
|
||||
it('should randomize the case of each character in the string', () => {
|
||||
const input = 'hello world';
|
||||
const result = randomizeCase(input);
|
||||
|
||||
// Ensure the output length is the same
|
||||
expect(result).toHaveLength(input.length);
|
||||
|
||||
// Ensure each character in the input string appears in the result
|
||||
for (let i = 0; i < input.length; i++) {
|
||||
const inputChar = input[i];
|
||||
const resultChar = result[i];
|
||||
|
||||
if (/[a-zA-Z]/.test(inputChar)) {
|
||||
expect([inputChar.toLowerCase(), inputChar.toUpperCase()]).toContain(
|
||||
resultChar
|
||||
);
|
||||
} else {
|
||||
expect(inputChar).toBe(resultChar);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it('should handle an empty string', () => {
|
||||
const input = '';
|
||||
const result = randomizeCase(input);
|
||||
expect(result).toBe('');
|
||||
});
|
||||
|
||||
it('should handle a string with numbers and symbols', () => {
|
||||
const input = '123 hello! @world';
|
||||
const result = randomizeCase(input);
|
||||
|
||||
// Ensure the output length is the same
|
||||
expect(result).toHaveLength(input.length);
|
||||
|
||||
// Ensure numbers and symbols remain unchanged
|
||||
for (let i = 0; i < input.length; i++) {
|
||||
const inputChar = input[i];
|
||||
const resultChar = result[i];
|
||||
|
||||
if (!/[a-zA-Z]/.test(inputChar)) {
|
||||
expect(inputChar).toBe(resultChar);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
8
src/pages/tools/string/randomize-case/service.ts
Normal file
8
src/pages/tools/string/randomize-case/service.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
export function randomizeCase(input: string): string {
|
||||
return input
|
||||
.split('')
|
||||
.map((char) =>
|
||||
Math.random() < 0.5 ? char.toLowerCase() : char.toUpperCase()
|
||||
)
|
||||
.join('');
|
||||
}
|
||||
11
src/pages/tools/string/reverse/index.tsx
Normal file
11
src/pages/tools/string/reverse/index.tsx
Normal file
@@ -0,0 +1,11 @@
|
||||
import { Box } from '@mui/material';
|
||||
import React from 'react';
|
||||
import * as Yup from 'yup';
|
||||
|
||||
const initialValues = {};
|
||||
const validationSchema = Yup.object({
|
||||
// splitSeparator: Yup.string().required('The separator is required')
|
||||
});
|
||||
export default function Reverse() {
|
||||
return <Box>Lorem ipsum</Box>;
|
||||
}
|
||||
13
src/pages/tools/string/reverse/meta.ts
Normal file
13
src/pages/tools/string/reverse/meta.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { defineTool } from '@tools/defineTool';
|
||||
import { lazy } from 'react';
|
||||
// import image from '@assets/text.png';
|
||||
|
||||
export const tool = defineTool('string', {
|
||||
name: 'Reverse',
|
||||
path: 'reverse',
|
||||
// image,
|
||||
description: '',
|
||||
shortDescription: '',
|
||||
keywords: ['reverse'],
|
||||
component: lazy(() => import('./index'))
|
||||
});
|
||||
52
src/pages/tools/string/reverse/reverse.service.test.ts
Normal file
52
src/pages/tools/string/reverse/reverse.service.test.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { stringReverser } from './service';
|
||||
|
||||
describe('stringReverser', () => {
|
||||
it('should reverse a single-line string', () => {
|
||||
const input = 'hello world';
|
||||
const result = stringReverser(input, false, false, false);
|
||||
expect(result).toBe('dlrow olleh');
|
||||
});
|
||||
|
||||
it('should reverse each line in a multi-line string', () => {
|
||||
const input = 'hello\nworld';
|
||||
const result = stringReverser(input, true, false, false);
|
||||
expect(result).toBe('olleh\ndlrow');
|
||||
});
|
||||
|
||||
it('should remove empty items if emptyItems is true', () => {
|
||||
const input = 'hello\n\nworld';
|
||||
const result = stringReverser(input, true, true, false);
|
||||
expect(result).toBe('olleh\ndlrow');
|
||||
});
|
||||
|
||||
it('should trim each line if trim is true', () => {
|
||||
const input = ' hello \n world ';
|
||||
const result = stringReverser(input, true, false, true);
|
||||
expect(result).toBe('olleh\ndlrow');
|
||||
});
|
||||
|
||||
it('should handle empty input', () => {
|
||||
const input = '';
|
||||
const result = stringReverser(input, false, false, false);
|
||||
expect(result).toBe('');
|
||||
});
|
||||
|
||||
it('should handle a single line with emptyItems and trim', () => {
|
||||
const input = ' hello world ';
|
||||
const result = stringReverser(input, false, true, true);
|
||||
expect(result).toBe('dlrow olleh');
|
||||
});
|
||||
|
||||
it('should handle a single line with emptyItems and non trim', () => {
|
||||
const input = ' hello world ';
|
||||
const result = stringReverser(input, false, true, false);
|
||||
expect(result).toBe(' dlrow olleh ');
|
||||
});
|
||||
|
||||
it('should handle a multi line with emptyItems and non trim', () => {
|
||||
const input = ' hello\n\n\n\nworld ';
|
||||
const result = stringReverser(input, true, true, false);
|
||||
expect(result).toBe('olleh \n dlrow');
|
||||
});
|
||||
});
|
||||
30
src/pages/tools/string/reverse/service.ts
Normal file
30
src/pages/tools/string/reverse/service.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import { reverseString } from 'utils/string';
|
||||
|
||||
export function stringReverser(
|
||||
input: string,
|
||||
multiLine: boolean,
|
||||
emptyItems: boolean,
|
||||
trim: boolean
|
||||
) {
|
||||
let array: string[] = [];
|
||||
let result: string[] = [];
|
||||
|
||||
// split the input in multiLine mode
|
||||
if (multiLine) {
|
||||
array = input.split('\n');
|
||||
} else {
|
||||
array.push(input);
|
||||
}
|
||||
|
||||
// handle empty items
|
||||
if (emptyItems) {
|
||||
array = array.filter(Boolean);
|
||||
}
|
||||
// Handle trim
|
||||
if (trim) {
|
||||
array = array.map((line) => line.trim());
|
||||
}
|
||||
|
||||
result = array.map((element) => reverseString(element));
|
||||
return result.join('\n');
|
||||
}
|
||||
148
src/pages/tools/string/split/index.tsx
Normal file
148
src/pages/tools/string/split/index.tsx
Normal file
@@ -0,0 +1,148 @@
|
||||
import { Box } from '@mui/material';
|
||||
import React, { useState } from 'react';
|
||||
import ToolTextInput from '@components/input/ToolTextInput';
|
||||
import ToolTextResult from '@components/result/ToolTextResult';
|
||||
import ToolOptions from '@components/options/ToolOptions';
|
||||
import { compute, SplitOperatorType } from './service';
|
||||
import RadioWithTextField from '@components/options/RadioWithTextField';
|
||||
import TextFieldWithDesc from '@components/options/TextFieldWithDesc';
|
||||
import ToolInputAndResult from '@components/ToolInputAndResult';
|
||||
|
||||
const initialValues = {
|
||||
splitSeparatorType: 'symbol' as SplitOperatorType,
|
||||
symbolValue: ' ',
|
||||
regexValue: '/\\s+/',
|
||||
lengthValue: '16',
|
||||
chunksValue: '4',
|
||||
|
||||
outputSeparator: '\\n',
|
||||
charBeforeChunk: '',
|
||||
charAfterChunk: ''
|
||||
};
|
||||
const splitOperators: {
|
||||
title: string;
|
||||
description: string;
|
||||
type: SplitOperatorType;
|
||||
}[] = [
|
||||
{
|
||||
title: 'Use a Symbol for Splitting',
|
||||
description:
|
||||
'Character that will be used to\n' +
|
||||
'break text into parts.\n' +
|
||||
'(Space by default.)',
|
||||
type: 'symbol'
|
||||
},
|
||||
{
|
||||
title: 'Use a Regex for Splitting',
|
||||
type: 'regex',
|
||||
description:
|
||||
'Regular expression that will be\n' +
|
||||
'used to break text into parts.\n' +
|
||||
'(Multiple spaces by default.)'
|
||||
},
|
||||
{
|
||||
title: 'Use Length for Splitting',
|
||||
description:
|
||||
'Number of symbols that will be\n' + 'put in each output chunk.',
|
||||
type: 'length'
|
||||
},
|
||||
{
|
||||
title: 'Use a Number of Chunks',
|
||||
description: 'Number of chunks of equal\n' + 'length in the output.',
|
||||
type: 'chunks'
|
||||
}
|
||||
];
|
||||
const outputOptions: {
|
||||
description: string;
|
||||
accessor: keyof typeof initialValues;
|
||||
}[] = [
|
||||
{
|
||||
description:
|
||||
'Character that will be put\n' +
|
||||
'between the split chunks.\n' +
|
||||
'(It\'s newline "\\n" by default.)',
|
||||
accessor: 'outputSeparator'
|
||||
},
|
||||
{
|
||||
description: 'Character before each chunk',
|
||||
accessor: 'charBeforeChunk'
|
||||
},
|
||||
{
|
||||
description: 'Character after each chunk',
|
||||
accessor: 'charAfterChunk'
|
||||
}
|
||||
];
|
||||
|
||||
export default function SplitText() {
|
||||
const [input, setInput] = useState<string>('');
|
||||
const [result, setResult] = useState<string>('');
|
||||
// const formRef = useRef<FormikProps<typeof initialValues>>(null);
|
||||
const computeExternal = (optionsValues: typeof initialValues, input: any) => {
|
||||
const {
|
||||
splitSeparatorType,
|
||||
outputSeparator,
|
||||
charBeforeChunk,
|
||||
charAfterChunk,
|
||||
chunksValue,
|
||||
symbolValue,
|
||||
regexValue,
|
||||
lengthValue
|
||||
} = optionsValues;
|
||||
|
||||
setResult(
|
||||
compute(
|
||||
splitSeparatorType,
|
||||
input,
|
||||
symbolValue,
|
||||
regexValue,
|
||||
Number(lengthValue),
|
||||
Number(chunksValue),
|
||||
charBeforeChunk,
|
||||
charAfterChunk,
|
||||
outputSeparator
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<ToolInputAndResult
|
||||
input={<ToolTextInput value={input} onChange={setInput} />}
|
||||
result={<ToolTextResult title={'Text pieces'} value={result} />}
|
||||
/>
|
||||
<ToolOptions
|
||||
compute={computeExternal}
|
||||
getGroups={({ values, updateField }) => [
|
||||
{
|
||||
title: 'Split separator options',
|
||||
component: splitOperators.map(({ title, description, type }) => (
|
||||
<RadioWithTextField
|
||||
key={type}
|
||||
checked={type === values.splitSeparatorType}
|
||||
title={title}
|
||||
fieldName={'splitSeparatorType'}
|
||||
description={description}
|
||||
value={values[`${type}Value`]}
|
||||
onRadioClick={() => updateField('splitSeparatorType', type)}
|
||||
onTextChange={(val) => updateField(`${type}Value`, val)}
|
||||
/>
|
||||
))
|
||||
},
|
||||
{
|
||||
title: 'Output separator options',
|
||||
component: outputOptions.map((option) => (
|
||||
<TextFieldWithDesc
|
||||
key={option.accessor}
|
||||
value={values[option.accessor]}
|
||||
onOwnChange={(value) => updateField(option.accessor, value)}
|
||||
description={option.description}
|
||||
/>
|
||||
))
|
||||
}
|
||||
]}
|
||||
initialValues={initialValues}
|
||||
input={input}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
14
src/pages/tools/string/split/meta.ts
Normal file
14
src/pages/tools/string/split/meta.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { defineTool } from '@tools/defineTool';
|
||||
import { lazy } from 'react';
|
||||
import image from '@assets/text.png';
|
||||
|
||||
export const tool = defineTool('string', {
|
||||
path: 'split',
|
||||
name: 'Text splitter',
|
||||
image,
|
||||
description:
|
||||
"World's simplest browser-based utility for splitting text. Load your text in the input form on the left and you'll automatically get pieces of this text on the right. Powerful, free, and fast. Load text – get chunks.",
|
||||
shortDescription: 'Quickly split a text',
|
||||
keywords: ['text', 'split'],
|
||||
component: lazy(() => import('./index'))
|
||||
});
|
||||
65
src/pages/tools/string/split/service.ts
Normal file
65
src/pages/tools/string/split/service.ts
Normal file
@@ -0,0 +1,65 @@
|
||||
export type SplitOperatorType = 'symbol' | 'regex' | 'length' | 'chunks';
|
||||
|
||||
function splitTextByLength(text: string, length: number) {
|
||||
if (length <= 0) throw new Error('Length must be a positive number');
|
||||
const result: string[] = [];
|
||||
for (let i = 0; i < text.length; i += length) {
|
||||
result.push(text.slice(i, i + length));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function splitIntoChunks(text: string, numChunks: number) {
|
||||
if (numChunks <= 0)
|
||||
throw new Error('Number of chunks must be a positive number');
|
||||
const totalLength = text.length;
|
||||
if (totalLength < numChunks)
|
||||
throw new Error(
|
||||
'Text length must be at least as long as the number of chunks'
|
||||
);
|
||||
|
||||
const chunkSize = Math.ceil(totalLength / numChunks); // Calculate the chunk size, rounding up to handle remainders
|
||||
let result = [];
|
||||
|
||||
for (let i = 0; i < totalLength; i += chunkSize) {
|
||||
result.push(text.slice(i, i + chunkSize));
|
||||
}
|
||||
|
||||
// Ensure the result contains exactly numChunks, adjusting the last chunk if necessary
|
||||
if (result.length > numChunks) {
|
||||
result[numChunks - 1] = result.slice(numChunks - 1).join(''); // Merge any extra chunks into the last chunk
|
||||
result = result.slice(0, numChunks); // Take only the first numChunks chunks
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
export function compute(
|
||||
splitSeparatorType: SplitOperatorType,
|
||||
input: string,
|
||||
symbolValue: string,
|
||||
regexValue: string,
|
||||
lengthValue: number,
|
||||
chunksValue: number,
|
||||
charBeforeChunk: string,
|
||||
charAfterChunk: string,
|
||||
outputSeparator: string
|
||||
) {
|
||||
let splitText;
|
||||
switch (splitSeparatorType) {
|
||||
case 'symbol':
|
||||
splitText = input.split(symbolValue);
|
||||
break;
|
||||
case 'regex':
|
||||
splitText = input.split(new RegExp(regexValue));
|
||||
break;
|
||||
case 'length':
|
||||
splitText = splitTextByLength(input, lengthValue);
|
||||
break;
|
||||
case 'chunks':
|
||||
splitText = splitIntoChunks(input, chunksValue).map(
|
||||
(chunk) => `${charBeforeChunk}${chunk}${charAfterChunk}`
|
||||
);
|
||||
}
|
||||
return splitText.join(outputSeparator);
|
||||
}
|
||||
72
src/pages/tools/string/split/string-split.service.test.ts
Normal file
72
src/pages/tools/string/split/string-split.service.test.ts
Normal file
@@ -0,0 +1,72 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { compute } from './service';
|
||||
|
||||
describe('compute function', () => {
|
||||
it('should split by symbol', () => {
|
||||
const result = compute('symbol', 'hello world', ' ', '', 0, 0, '', '', ',');
|
||||
expect(result).toBe('hello,world');
|
||||
});
|
||||
|
||||
it('should split by regex', () => {
|
||||
const result = compute(
|
||||
'regex',
|
||||
'hello1world2again',
|
||||
'',
|
||||
'\\d',
|
||||
0,
|
||||
0,
|
||||
'',
|
||||
'',
|
||||
','
|
||||
);
|
||||
expect(result).toBe('hello,world,again');
|
||||
});
|
||||
|
||||
it('should split by length', () => {
|
||||
const result = compute('length', 'helloworld', '', '', 3, 0, '', '', ',');
|
||||
expect(result).toBe('hel,low,orl,d');
|
||||
});
|
||||
|
||||
it('should split into chunks', () => {
|
||||
const result = compute(
|
||||
'chunks',
|
||||
'helloworldagain',
|
||||
'',
|
||||
'',
|
||||
0,
|
||||
3,
|
||||
'[',
|
||||
']',
|
||||
','
|
||||
);
|
||||
expect(result).toBe('[hello],[world],[again]');
|
||||
});
|
||||
|
||||
it('should handle empty input', () => {
|
||||
const result = compute('symbol', '', ' ', '', 0, 0, '', '', ',');
|
||||
expect(result).toBe('');
|
||||
});
|
||||
|
||||
it('should handle length greater than text length', () => {
|
||||
const result = compute('length', 'hi', '', '', 5, 0, '', '', ',');
|
||||
expect(result).toBe('hi');
|
||||
});
|
||||
|
||||
it('should handle chunks greater than text length', () => {
|
||||
expect(() => {
|
||||
compute('chunks', 'hi', '', '', 0, 5, '', '', ',');
|
||||
}).toThrow('Text length must be at least as long as the number of chunks');
|
||||
});
|
||||
|
||||
it('should handle invalid length', () => {
|
||||
expect(() => {
|
||||
compute('length', 'hello', '', '', -1, 0, '', '', ',');
|
||||
}).toThrow('Length must be a positive number');
|
||||
});
|
||||
|
||||
it('should handle invalid chunks', () => {
|
||||
expect(() => {
|
||||
compute('chunks', 'hello', '', '', 0, 0, '', '', ',');
|
||||
}).toThrow('Number of chunks must be a positive number');
|
||||
});
|
||||
});
|
||||
63
src/pages/tools/string/to-morse/index.tsx
Normal file
63
src/pages/tools/string/to-morse/index.tsx
Normal file
@@ -0,0 +1,63 @@
|
||||
import { Box } from '@mui/material';
|
||||
import React, { useState } from 'react';
|
||||
import ToolTextInput from '@components/input/ToolTextInput';
|
||||
import ToolTextResult from '@components/result/ToolTextResult';
|
||||
import ToolOptions from '@components/options/ToolOptions';
|
||||
import { compute } from './service';
|
||||
import TextFieldWithDesc from '@components/options/TextFieldWithDesc';
|
||||
import ToolInputAndResult from '@components/ToolInputAndResult';
|
||||
|
||||
const initialValues = {
|
||||
dotSymbol: '.',
|
||||
dashSymbol: '-'
|
||||
};
|
||||
|
||||
export default function ToMorse() {
|
||||
const [input, setInput] = useState<string>('');
|
||||
const [result, setResult] = useState<string>('');
|
||||
// const formRef = useRef<FormikProps<typeof initialValues>>(null);
|
||||
const computeOptions = (optionsValues: typeof initialValues, input: any) => {
|
||||
const { dotSymbol, dashSymbol } = optionsValues;
|
||||
setResult(compute(input, dotSymbol, dashSymbol));
|
||||
};
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<ToolInputAndResult
|
||||
input={<ToolTextInput value={input} onChange={setInput} />}
|
||||
result={<ToolTextResult title={'Morse code'} value={result} />}
|
||||
/>
|
||||
<ToolOptions
|
||||
compute={computeOptions}
|
||||
getGroups={({ values, updateField }) => [
|
||||
{
|
||||
title: 'Short Signal',
|
||||
component: (
|
||||
<TextFieldWithDesc
|
||||
description={
|
||||
'Symbol that will correspond to the dot in Morse code.'
|
||||
}
|
||||
value={values.dotSymbol}
|
||||
onOwnChange={(val) => updateField('dotSymbol', val)}
|
||||
/>
|
||||
)
|
||||
},
|
||||
{
|
||||
title: 'Long Signal',
|
||||
component: (
|
||||
<TextFieldWithDesc
|
||||
description={
|
||||
'Symbol that will correspond to the dash in Morse code.'
|
||||
}
|
||||
value={values.dashSymbol}
|
||||
onOwnChange={(val) => updateField('dashSymbol', val)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
]}
|
||||
initialValues={initialValues}
|
||||
input={input}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
14
src/pages/tools/string/to-morse/meta.ts
Normal file
14
src/pages/tools/string/to-morse/meta.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { defineTool } from '@tools/defineTool';
|
||||
import { lazy } from 'react';
|
||||
// import image from '@assets/text.png';
|
||||
|
||||
export const tool = defineTool('string', {
|
||||
name: 'String To morse',
|
||||
path: 'to-morse',
|
||||
// image,
|
||||
description:
|
||||
"World's simplest browser-based utility for converting text to Morse code. Load your text in the input form on the left and you'll instantly get Morse code in the output area. Powerful, free, and fast. Load text – get Morse code.",
|
||||
shortDescription: 'Quickly encode text to morse',
|
||||
keywords: ['to', 'morse'],
|
||||
component: lazy(() => import('./index'))
|
||||
});
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user