mirror of
https://github.com/iib0011/omni-tools.git
synced 2025-12-29 16:16:02 +00:00
Merge remote-tracking branch 'origin/main' into truncate
# Conflicts: # src/pages/tools/string/index.ts
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));
|
||||
});
|
||||
});
|
||||
149
src/pages/tools/image/png/change-colors-in-png/index.tsx
Normal file
149
src/pages/tools/image/png/change-colors-in-png/index.tsx
Normal file
@@ -0,0 +1,149 @@
|
||||
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, { GetGroupsType } 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';
|
||||
import ToolContent from '@components/ToolContent';
|
||||
import { ToolComponentProps } from '@tools/defineTool';
|
||||
|
||||
const initialValues = {
|
||||
fromColor: 'white',
|
||||
toColor: 'black',
|
||||
similarity: '10'
|
||||
};
|
||||
const validationSchema = Yup.object({
|
||||
// splitSeparator: Yup.string().required('The separator is required')
|
||||
});
|
||||
export default function ChangeColorsInPng({ title }: ToolComponentProps) {
|
||||
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));
|
||||
};
|
||||
const getGroups: GetGroupsType<typeof initialValues> = ({
|
||||
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>
|
||||
)
|
||||
}
|
||||
];
|
||||
return (
|
||||
<ToolContent
|
||||
title={title}
|
||||
initialValues={initialValues}
|
||||
getGroups={getGroups}
|
||||
compute={compute}
|
||||
input={input}
|
||||
validationSchema={validationSchema}
|
||||
inputComponent={
|
||||
<ToolFileInput
|
||||
value={input}
|
||||
onChange={setInput}
|
||||
accept={['image/png']}
|
||||
title={'Input PNG'}
|
||||
/>
|
||||
}
|
||||
resultComponent={
|
||||
<ToolFileResult
|
||||
title={'Transparent PNG'}
|
||||
value={result}
|
||||
extension={'png'}
|
||||
/>
|
||||
}
|
||||
toolInfo={{
|
||||
title: 'Make Colors Transparent',
|
||||
description:
|
||||
'This tool allows you to make specific colors in a PNG image transparent. You can select the color to replace and adjust the similarity threshold to include similar colors.'
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
13
src/pages/tools/image/png/change-colors-in-png/meta.ts
Normal file
13
src/pages/tools/image/png/change-colors-in-png/meta.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { defineTool } from '@tools/defineTool';
|
||||
import { lazy } from 'react';
|
||||
|
||||
export const tool = defineTool('png', {
|
||||
name: 'Change colors in png',
|
||||
path: 'change-colors-in-png',
|
||||
icon: 'cil:color-fill',
|
||||
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 |
113
src/pages/tools/image/png/compress-png/index.tsx
Normal file
113
src/pages/tools/image/png/compress-png/index.tsx
Normal file
@@ -0,0 +1,113 @@
|
||||
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 TextFieldWithDesc from 'components/options/TextFieldWithDesc';
|
||||
import ToolInputAndResult from '@components/ToolInputAndResult';
|
||||
import imageCompression from 'browser-image-compression';
|
||||
import Typography from '@mui/material/Typography';
|
||||
|
||||
const initialValues = {
|
||||
rate: '50'
|
||||
};
|
||||
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 [originalSize, setOriginalSize] = useState<number | null>(null); // Store original file size
|
||||
const [compressedSize, setCompressedSize] = useState<number | null>(null); // Store compressed file size
|
||||
|
||||
const compressImage = async (file: File, rate: number) => {
|
||||
if (!file) return;
|
||||
|
||||
// Set original file size
|
||||
setOriginalSize(file.size);
|
||||
|
||||
const options = {
|
||||
maxSizeMB: 1, // Maximum size in MB
|
||||
maxWidthOrHeight: 1024, // Maximum width or height
|
||||
quality: rate / 100, // Convert percentage to decimal (e.g., 50% becomes 0.5)
|
||||
useWebWorker: true
|
||||
};
|
||||
|
||||
try {
|
||||
const compressedFile = await imageCompression(file, options);
|
||||
setResult(compressedFile);
|
||||
setCompressedSize(compressedFile.size); // Set compressed file size
|
||||
} catch (error) {
|
||||
console.error('Error during compression:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const compute = (optionsValues: typeof initialValues, input: any) => {
|
||||
if (!input) return;
|
||||
|
||||
const { rate } = optionsValues;
|
||||
compressImage(input, Number(rate)); // Pass the rate as a number
|
||||
};
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<ToolInputAndResult
|
||||
input={
|
||||
<ToolFileInput
|
||||
value={input}
|
||||
onChange={setInput}
|
||||
accept={['image/png']}
|
||||
title={'Input PNG'}
|
||||
/>
|
||||
}
|
||||
result={
|
||||
<ToolFileResult
|
||||
title={'Compressed PNG'}
|
||||
value={result}
|
||||
extension={'png'}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<ToolOptions
|
||||
compute={compute}
|
||||
getGroups={({ values, updateField }) => [
|
||||
{
|
||||
title: 'Compression options',
|
||||
component: (
|
||||
<Box>
|
||||
<TextFieldWithDesc
|
||||
value={values.rate}
|
||||
onOwnChange={(val) => updateField('rate', val)}
|
||||
description={'Compression rate (1-100)'}
|
||||
/>
|
||||
</Box>
|
||||
)
|
||||
},
|
||||
{
|
||||
title: 'File sizes',
|
||||
component: (
|
||||
<Box>
|
||||
<Box>
|
||||
{originalSize !== null && (
|
||||
<Typography>
|
||||
Original Size: {(originalSize / 1024).toFixed(2)} KB
|
||||
</Typography>
|
||||
)}
|
||||
{compressedSize !== null && (
|
||||
<Typography>
|
||||
Compressed Size: {(compressedSize / 1024).toFixed(2)} KB
|
||||
</Typography>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
]}
|
||||
initialValues={initialValues}
|
||||
input={input}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
14
src/pages/tools/image/png/compress-png/meta.ts
Normal file
14
src/pages/tools/image/png/compress-png/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('png', {
|
||||
name: 'Compress png',
|
||||
path: 'compress-png',
|
||||
icon: 'material-symbols-light:compress',
|
||||
description:
|
||||
'This is a program that compresses PNG pictures. As soon as you paste your PNG picture in the input area, the program will compress it and show the result in the output area. In the options, you can adjust the compression level, as well as find the old and new picture file sizes.',
|
||||
shortDescription: 'Quicly compress a PNG',
|
||||
keywords: ['compress', 'png'],
|
||||
component: lazy(() => import('./index'))
|
||||
});
|
||||
0
src/pages/tools/image/png/compress-png/service.ts
Normal file
0
src/pages/tools/image/png/compress-png/service.ts
Normal file
@@ -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>
|
||||
);
|
||||
}
|
||||
13
src/pages/tools/image/png/convert-jgp-to-png/meta.ts
Normal file
13
src/pages/tools/image/png/convert-jgp-to-png/meta.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { defineTool } from '@tools/defineTool';
|
||||
import { lazy } from 'react';
|
||||
|
||||
export const tool = defineTool('png', {
|
||||
name: 'Convert JPG to PNG',
|
||||
path: 'convert-jgp-to-png',
|
||||
icon: 'ph:file-jpg-thin',
|
||||
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>
|
||||
);
|
||||
}
|
||||
13
src/pages/tools/image/png/create-transparent/meta.ts
Normal file
13
src/pages/tools/image/png/create-transparent/meta.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { defineTool } from '@tools/defineTool';
|
||||
import { lazy } from 'react';
|
||||
|
||||
export const tool = defineTool('png', {
|
||||
name: 'Create transparent PNG',
|
||||
path: 'create-transparent',
|
||||
icon: 'mdi:circle-transparent',
|
||||
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 |
11
src/pages/tools/image/png/index.ts
Normal file
11
src/pages/tools/image/png/index.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { tool as pngCompressPng } from './compress-png/meta';
|
||||
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 = [
|
||||
pngCompressPng,
|
||||
pngCreateTransparent,
|
||||
changeColorsInPng,
|
||||
convertJgpToPng
|
||||
];
|
||||
3
src/pages/tools/json/index.ts
Normal file
3
src/pages/tools/json/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import { tool as jsonPrettify } from './prettify/meta';
|
||||
|
||||
export const jsonTools = [jsonPrettify];
|
||||
190
src/pages/tools/json/prettify/index.tsx
Normal file
190
src/pages/tools/json/prettify/index.tsx
Normal file
@@ -0,0 +1,190 @@
|
||||
import { Box } from '@mui/material';
|
||||
import React, { useRef, useState } from 'react';
|
||||
import ToolTextInput from '@components/input/ToolTextInput';
|
||||
import ToolTextResult from '@components/result/ToolTextResult';
|
||||
import ToolOptions, { GetGroupsType } from '@components/options/ToolOptions';
|
||||
import { beautifyJson } from './service';
|
||||
import ToolInputAndResult from '@components/ToolInputAndResult';
|
||||
|
||||
import ToolInfo from '@components/ToolInfo';
|
||||
import Separator from '@components/Separator';
|
||||
import ToolExamples, {
|
||||
CardExampleType
|
||||
} from '@components/examples/ToolExamples';
|
||||
import { FormikProps } from 'formik';
|
||||
import { ToolComponentProps } from '@tools/defineTool';
|
||||
import RadioWithTextField from '@components/options/RadioWithTextField';
|
||||
import SimpleRadio from '@components/options/SimpleRadio';
|
||||
import { isNumber } from '../../../../utils/string';
|
||||
|
||||
type InitialValuesType = {
|
||||
indentationType: 'tab' | 'space';
|
||||
spacesCount: number;
|
||||
};
|
||||
|
||||
const initialValues: InitialValuesType = {
|
||||
indentationType: 'space',
|
||||
spacesCount: 2
|
||||
};
|
||||
|
||||
const exampleCards: CardExampleType<InitialValuesType>[] = [
|
||||
{
|
||||
title: 'Beautify an Ugly JSON Array',
|
||||
description:
|
||||
'In this example, we prettify an ugly JSON array. The input data is a one-dimensional array of numbers [1,2,3] but they are all over the place. This array gets cleaned up and transformed into a more readable format where each element is on a new line with an appropriate indentation using four spaces.',
|
||||
sampleText: `[
|
||||
1,
|
||||
2,3
|
||||
]`,
|
||||
sampleResult: `[
|
||||
1,
|
||||
2,
|
||||
3
|
||||
]`,
|
||||
sampleOptions: {
|
||||
indentationType: 'space',
|
||||
spacesCount: 4
|
||||
}
|
||||
},
|
||||
{
|
||||
title: 'Prettify a Complex JSON Object',
|
||||
description:
|
||||
'In this example, we prettify a complex JSON data structure consisting of arrays and objects. The input data is a minified JSON object with multiple data structure depth levels. To make it neat and readable, we add two spaces for indentation to each depth level, making the JSON structure clear and easy to understand.',
|
||||
sampleText: `{"names":["jack","john","alex"],"hobbies":{"jack":["programming","rock climbing"],"john":["running","racing"],"alex":["dancing","fencing"]}}`,
|
||||
sampleResult: `{
|
||||
"names": [
|
||||
"jack",
|
||||
"john",
|
||||
"alex"
|
||||
],
|
||||
"hobbies": {
|
||||
"jack": [
|
||||
"programming",
|
||||
"rock climbing"
|
||||
],
|
||||
"john": [
|
||||
"running",
|
||||
"racing"
|
||||
],
|
||||
"alex": [
|
||||
"dancing",
|
||||
"fencing"
|
||||
]
|
||||
}
|
||||
}`,
|
||||
sampleOptions: {
|
||||
indentationType: 'space',
|
||||
spacesCount: 2
|
||||
}
|
||||
},
|
||||
{
|
||||
title: 'Beautify a JSON with Excessive Whitespace',
|
||||
description:
|
||||
"In this example, we show how the JSON prettify tool can handle code with excessive whitespace. The input file has many leading and trailing spaces as well as spaces within the objects. The excessive whitespace makes the file bulky and hard to read and leads to a bad impression of the programmer who wrote it. The program removes all these unnecessary spaces and creates a proper data hierarchy that's easy to work with by adding indentation via tabs.",
|
||||
sampleText: `
|
||||
{
|
||||
"name": "The Name of the Wind",
|
||||
"author" : "Patrick Rothfuss",
|
||||
"genre" : "Fantasy",
|
||||
"published" : 2007,
|
||||
"rating" : {
|
||||
"average" : 4.6,
|
||||
"goodreads" : 4.58,
|
||||
"amazon" : 4.4
|
||||
},
|
||||
"is_fiction" : true
|
||||
}
|
||||
|
||||
|
||||
`,
|
||||
sampleResult: `{
|
||||
\t"name": "The Name of the Wind",
|
||||
\t"author": "Patrick Rothfuss",
|
||||
\t"genre": "Fantasy",
|
||||
\t"published": 2007,
|
||||
\t"rating": {
|
||||
\t\t"average": 4.6,
|
||||
\t\t"goodreads": 4.58,
|
||||
\t\t"amazon": 4.4
|
||||
\t},
|
||||
\t"is_fiction": true
|
||||
}`,
|
||||
sampleOptions: {
|
||||
indentationType: 'tab',
|
||||
spacesCount: 0
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
export default function PrettifyJson({ title }: ToolComponentProps) {
|
||||
const [input, setInput] = useState<string>('');
|
||||
const [result, setResult] = useState<string>('');
|
||||
const formRef = useRef<FormikProps<InitialValuesType>>(null);
|
||||
const compute = (optionsValues: InitialValuesType, input: any) => {
|
||||
const { indentationType, spacesCount } = optionsValues;
|
||||
if (input) setResult(beautifyJson(input, indentationType, spacesCount));
|
||||
};
|
||||
|
||||
const getGroups: GetGroupsType<InitialValuesType> = ({
|
||||
values,
|
||||
updateField
|
||||
}) => [
|
||||
{
|
||||
title: 'Indentation',
|
||||
component: (
|
||||
<Box>
|
||||
<RadioWithTextField
|
||||
checked={values.indentationType === 'space'}
|
||||
title={'Use Spaces'}
|
||||
fieldName={'indentationType'}
|
||||
description={'Indent output with spaces'}
|
||||
value={values.spacesCount.toString()}
|
||||
onRadioClick={() => updateField('indentationType', 'space')}
|
||||
onTextChange={(val) =>
|
||||
isNumber(val) ? updateField('spacesCount', Number(val)) : null
|
||||
}
|
||||
/>
|
||||
<SimpleRadio
|
||||
onClick={() => updateField('indentationType', 'tab')}
|
||||
checked={values.indentationType === 'tab'}
|
||||
description={'Indent output with tabs.'}
|
||||
title={'Use Tabs'}
|
||||
/>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
];
|
||||
return (
|
||||
<Box>
|
||||
<ToolInputAndResult
|
||||
input={
|
||||
<ToolTextInput
|
||||
title={'Input JSON'}
|
||||
value={input}
|
||||
onChange={setInput}
|
||||
/>
|
||||
}
|
||||
result={<ToolTextResult title={'Pretty JSON'} value={result} />}
|
||||
/>
|
||||
<ToolOptions
|
||||
formRef={formRef}
|
||||
compute={compute}
|
||||
getGroups={getGroups}
|
||||
initialValues={initialValues}
|
||||
input={input}
|
||||
/>
|
||||
<ToolInfo
|
||||
title="What Is a JSON Prettifier?"
|
||||
description="This tool adds consistent formatting to the data in JavaScript Object Notation (JSON) format. This transformation makes the JSON code more readable, making it easier to understand and edit. The program parses the JSON data structure into tokens and then reformats them by adding indentation and line breaks. If the data is hierarchial, then it adds indentation at the beginning of lines to visually show the depth of the JSON and adds newlines to break long single-line JSON arrays into multiple shorter, more readable ones. Additionally, this utility can remove unnecessary spaces and tabs from your JSON code (especially leading and trailing whitespaces), making it more compact. You can choose the line indentation method in the options: indent with spaces or indent with tabs. When using spaces, you can also specify how many spaces to use for each indentation level (usually 2 or 4 spaces). "
|
||||
/>
|
||||
<Separator backgroundColor="#5581b5" margin="50px" />
|
||||
<ToolExamples
|
||||
title={title}
|
||||
exampleCards={exampleCards}
|
||||
getGroups={getGroups}
|
||||
formRef={formRef}
|
||||
setInput={setInput}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
13
src/pages/tools/json/prettify/meta.ts
Normal file
13
src/pages/tools/json/prettify/meta.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { defineTool } from '@tools/defineTool';
|
||||
import { lazy } from 'react';
|
||||
|
||||
export const tool = defineTool('json', {
|
||||
name: 'Prettify JSON',
|
||||
path: 'prettify',
|
||||
icon: 'lets-icons:json-light',
|
||||
description:
|
||||
"Just load your JSON in the input field and it will automatically get prettified. In the tool options, you can choose whether to use spaces or tabs for indentation and if you're using spaces, you can specify the number of spaces to add per indentation level.",
|
||||
shortDescription: 'Quickly beautify a JSON data structure.',
|
||||
keywords: ['prettify'],
|
||||
component: lazy(() => import('./index'))
|
||||
});
|
||||
16
src/pages/tools/json/prettify/service.ts
Normal file
16
src/pages/tools/json/prettify/service.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
export const beautifyJson = (
|
||||
text: string,
|
||||
indentationType: 'tab' | 'space',
|
||||
spacesCount: number
|
||||
) => {
|
||||
let parsedJson;
|
||||
try {
|
||||
parsedJson = JSON.parse(text);
|
||||
} catch (e) {
|
||||
throw new Error('Invalid JSON string');
|
||||
}
|
||||
|
||||
const indent = indentationType === 'tab' ? '\t' : spacesCount;
|
||||
|
||||
return JSON.stringify(parsedJson, null, indent);
|
||||
};
|
||||
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',
|
||||
icon: '',
|
||||
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',
|
||||
icon: 'material-symbols-light:query-stats',
|
||||
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',
|
||||
icon: 'mynaui:one',
|
||||
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',
|
||||
icon: 'pajamas:group',
|
||||
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
|
||||
];
|
||||
192
src/pages/tools/list/reverse/index.tsx
Normal file
192
src/pages/tools/list/reverse/index.tsx
Normal file
@@ -0,0 +1,192 @@
|
||||
import { Box } from '@mui/material';
|
||||
import React, { useState } from 'react';
|
||||
import ToolTextInput from '@components/input/ToolTextInput';
|
||||
import ToolTextResult from '@components/result/ToolTextResult';
|
||||
import { GetGroupsType } from '@components/options/ToolOptions';
|
||||
import { reverseList, SplitOperatorType } from './service';
|
||||
import SimpleRadio from '@components/options/SimpleRadio';
|
||||
import TextFieldWithDesc from '@components/options/TextFieldWithDesc';
|
||||
import { CardExampleType } from '@components/examples/ToolExamples';
|
||||
import { ToolComponentProps } from '@tools/defineTool';
|
||||
import ToolContent from '@components/ToolContent';
|
||||
|
||||
const initialValues = {
|
||||
splitOperatorType: 'symbol' as SplitOperatorType,
|
||||
splitSeparator: ',',
|
||||
joinSeparator: '\\n'
|
||||
};
|
||||
type InitialValuesType = typeof initialValues;
|
||||
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 exampleCards: CardExampleType<InitialValuesType>[] = [
|
||||
{
|
||||
title: 'Reverse a List of Digits',
|
||||
description:
|
||||
'In this example, we load a list of digits in the input. The digits are separated by a mix of dot, comma, and semicolon characters, so we use the regular expression split mode and enter a regular expression that matches all these characters as the input item separator. In the output, we get a reversed list of digits that all use the semicolon as a separator.',
|
||||
sampleText: `2, 9, 6; 3; 7. 4. 4. 2, 1; 4, 8. 4; 4. 8, 2, 5; 1; 7; 7. 0`,
|
||||
sampleResult: `0; 7; 7; 1; 5; 2; 8; 4; 4; 8; 4; 1; 2; 4; 4; 7; 3; 6; 9; 2`,
|
||||
sampleOptions: {
|
||||
splitOperatorType: 'regex',
|
||||
splitSeparator: '[;,.]\\s*',
|
||||
joinSeparator: '; '
|
||||
}
|
||||
},
|
||||
{
|
||||
title: 'Reverse a Column of Words',
|
||||
description:
|
||||
'This example reverses a column of twenty three-syllable nouns and prints all the words from the bottom to top. To separate the list items, it uses the \n character as input item separator, which means that each item is on its own line..',
|
||||
sampleText: `argument
|
||||
pollution
|
||||
emphasis
|
||||
vehicle
|
||||
family
|
||||
property
|
||||
preference
|
||||
studio
|
||||
suggestion
|
||||
accident
|
||||
analyst
|
||||
permission
|
||||
reaction
|
||||
promotion
|
||||
quantity
|
||||
inspection
|
||||
chemistry
|
||||
conclusion
|
||||
confusion
|
||||
memory`,
|
||||
sampleResult: `memory
|
||||
confusion
|
||||
conclusion
|
||||
chemistry
|
||||
inspection
|
||||
quantity
|
||||
promotion
|
||||
reaction
|
||||
permission
|
||||
analyst
|
||||
accident
|
||||
suggestion
|
||||
studio
|
||||
preference
|
||||
property
|
||||
family
|
||||
vehicle
|
||||
emphasis
|
||||
pollution
|
||||
argument`,
|
||||
sampleOptions: {
|
||||
splitOperatorType: 'symbol',
|
||||
splitSeparator: '\\n',
|
||||
joinSeparator: '\\n'
|
||||
}
|
||||
},
|
||||
{
|
||||
title: 'Reverse a Random List',
|
||||
description:
|
||||
'In this example, the list elements are random cities, zip codes, and weather conditions. To reverse list elements, we first need to identify them and separate them apart. The input list incorrectly uses the dash symbol to separate the elements but the output list fixes this and uses commas.',
|
||||
sampleText: `Hamburg-21334-Dhaka-Sunny-Managua-Rainy-Chongqing-95123-Oakland`,
|
||||
sampleResult: `Oakland, 95123, Chongqing, Rainy, Managua, Sunny, Dhaka, 21334, Hamburg`,
|
||||
sampleOptions: {
|
||||
splitOperatorType: 'symbol',
|
||||
splitSeparator: '-',
|
||||
joinSeparator: ', '
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
export default function Reverse({ title }: ToolComponentProps) {
|
||||
const [input, setInput] = useState<string>('');
|
||||
const [result, setResult] = useState<string>('');
|
||||
|
||||
const getGroups: GetGroupsType<InitialValuesType> = ({
|
||||
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>
|
||||
)
|
||||
}
|
||||
];
|
||||
const compute = (optionsValues: typeof initialValues, input: any) => {
|
||||
const { splitOperatorType, splitSeparator, joinSeparator } = optionsValues;
|
||||
|
||||
setResult(
|
||||
reverseList(splitOperatorType, splitSeparator, joinSeparator, input)
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<ToolContent
|
||||
title={title}
|
||||
initialValues={initialValues}
|
||||
getGroups={getGroups}
|
||||
compute={compute}
|
||||
input={input}
|
||||
setInput={setInput}
|
||||
inputComponent={
|
||||
<ToolTextInput title={'Input list'} value={input} onChange={setInput} />
|
||||
}
|
||||
resultComponent={
|
||||
<ToolTextResult title={'Reversed list'} value={result} />
|
||||
}
|
||||
toolInfo={{
|
||||
title: 'What Is a List Reverser?',
|
||||
description:
|
||||
'With this utility, you can reverse the order of items in a list. The utility first splits the input list into individual items and then iterates through them from the last item to the first item, printing each item to the output during the iteration. The input list may contain anything that can be represented as textual data, which includes digits, numbers, strings, words, sentences, etc. The input item separator can also be a regular expression. For example, the regex /[;,]/ will allow you to use items that are either comma- or semicolon-separated. The input and output list items delimiters can be customized in the options. By default, both input and output lists are comma-separated. Listabulous!'
|
||||
}}
|
||||
exampleCards={exampleCards}
|
||||
/>
|
||||
);
|
||||
}
|
||||
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',
|
||||
icon: 'proicons:reverse',
|
||||
description: 'This is a super simple browser-based application prints all list items in reverse. The input items can be separated by any symbol and you can also change the separator of the reversed list items.',
|
||||
shortDescription: 'Quickly reverse a list',
|
||||
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',
|
||||
icon: 'material-symbols-light:rotate-right',
|
||||
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',
|
||||
icon: 'material-symbols-light:shuffle',
|
||||
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',
|
||||
icon: 'basil:sort-outline',
|
||||
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',
|
||||
icon: '',
|
||||
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',
|
||||
icon: 'mdi:unwrap',
|
||||
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',
|
||||
icon: '',
|
||||
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',
|
||||
icon: 'lsicon:number-filled',
|
||||
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];
|
||||
207
src/pages/tools/number/sum/index.tsx
Normal file
207
src/pages/tools/number/sum/index.tsx
Normal file
@@ -0,0 +1,207 @@
|
||||
import { Box } from '@mui/material';
|
||||
import React, { useRef, useState } from 'react';
|
||||
import ToolTextInput from '@components/input/ToolTextInput';
|
||||
import ToolTextResult from '@components/result/ToolTextResult';
|
||||
import ToolOptions, { GetGroupsType } 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';
|
||||
import ToolExamples, {
|
||||
CardExampleType
|
||||
} from '@components/examples/ToolExamples';
|
||||
import ToolInfo from '@components/ToolInfo';
|
||||
import Separator from '@components/Separator';
|
||||
import { ToolComponentProps } from '@tools/defineTool';
|
||||
import { FormikProps } from 'formik';
|
||||
|
||||
const initialValues = {
|
||||
extractionType: 'smart' as NumberExtractionType,
|
||||
separator: '\\n',
|
||||
printRunningSum: false
|
||||
};
|
||||
type InitialValuesType = typeof initialValues;
|
||||
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'
|
||||
}
|
||||
];
|
||||
|
||||
const exampleCards: CardExampleType<InitialValuesType>[] = [
|
||||
{
|
||||
title: 'Sum of Ten Positive Numbers',
|
||||
description:
|
||||
'In this example, we calculate the sum of ten positive integers. These integers are listed as a column and their total sum equals 19494.',
|
||||
sampleText: `0
|
||||
1
|
||||
20
|
||||
33
|
||||
400
|
||||
505
|
||||
660
|
||||
777
|
||||
8008
|
||||
9090`,
|
||||
sampleResult: `19494`,
|
||||
sampleOptions: {
|
||||
extractionType: 'delimiter',
|
||||
separator: '\\n',
|
||||
printRunningSum: false
|
||||
}
|
||||
},
|
||||
{
|
||||
title: 'Count Trees in the Park',
|
||||
description:
|
||||
'This example reverses a column of twenty three-syllable nouns and prints all the words from the bottom to top. To separate the list items, it uses the \n character as input item separator, which means that each item is on its own line..',
|
||||
sampleText: `This year gardeners have planted 20 red maples, 35 sweetgum, 13 quaking aspen, and 7 white oaks in the central park of the city.`,
|
||||
sampleResult: `75`,
|
||||
sampleOptions: {
|
||||
extractionType: 'smart',
|
||||
separator: '\\n',
|
||||
printRunningSum: false
|
||||
}
|
||||
},
|
||||
{
|
||||
title: 'Sum of Integers and Decimals',
|
||||
description:
|
||||
'In this example, we add together ninety different values – positive numbers, negative numbers, integers and decimal fractions. We set the input separator to a comma and after adding all of them together, we get 0 as output.',
|
||||
sampleText: `1, 2, 3, 4, 5, 6, 7, 8, 9, -1.1, -2.1, -3.1, -4.1, -5.1, -6.1, -7.1, -8.1, -9.1, 10, 20, 30, 40, 50, 60, 70, 80, 90, -10.2, -20.2, -30.2, -40.2, -50.2, -60.2, -70.2, -80.2, -90.2, 100, 200, 300, 400, 500, 600, 700, 800, 900, -100.3, -200.3, -300.3, -400.3, -500.3, -600.3, -700.3, -800.3, -900.3, 1000, 2000, 3000, 4000, 5000, 6000, 7000, 8000, 9000, -1000.4, -2000.4, -3000.4, -4000.4, -5000.4, -6000.4, -7000.4, -8000.4, -9000.4, 10001, 20001, 30001, 40001, 50001, 60001, 70001, 80001, 90001, -10000, -20000, -30000, -40000, -50000, -60000, -70000, -80000, -90000`,
|
||||
sampleResult: `0`,
|
||||
sampleOptions: {
|
||||
extractionType: 'delimiter',
|
||||
separator: ', ',
|
||||
printRunningSum: false
|
||||
}
|
||||
},
|
||||
{
|
||||
title: 'Running Sum of Numbers',
|
||||
description:
|
||||
'In this example, we calculate the sum of all ten digits and enable the option "Print Running Sum". We get the intermediate values of the sum in the process of addition. Thus, we have the following sequence in the output: 0, 1 (0 + 1), 3 (0 + 1 + 2), 6 (0 + 1 + 2 + 3), 10 (0 + 1 + 2 + 3 + 4), and so on.',
|
||||
sampleText: `0
|
||||
1
|
||||
2
|
||||
3
|
||||
4
|
||||
5
|
||||
6
|
||||
7
|
||||
8
|
||||
9`,
|
||||
sampleResult: `0
|
||||
1
|
||||
3
|
||||
6
|
||||
10
|
||||
15
|
||||
21
|
||||
28
|
||||
36
|
||||
45`,
|
||||
sampleOptions: {
|
||||
extractionType: 'delimiter',
|
||||
separator: '\\n',
|
||||
printRunningSum: true
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
export default function SumNumbers({ title }: ToolComponentProps) {
|
||||
const [input, setInput] = useState<string>('');
|
||||
const [result, setResult] = useState<string>('');
|
||||
const formRef = useRef<FormikProps<typeof initialValues>>(null);
|
||||
|
||||
const getGroups: GetGroupsType<InitialValuesType> = ({
|
||||
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)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
];
|
||||
return (
|
||||
<Box>
|
||||
<ToolInputAndResult
|
||||
input={<ToolTextInput value={input} onChange={setInput} />}
|
||||
result={<ToolTextResult title={'Total'} value={result} />}
|
||||
/>
|
||||
<ToolOptions
|
||||
formRef={formRef}
|
||||
getGroups={getGroups}
|
||||
compute={(optionsValues, input) => {
|
||||
const { extractionType, printRunningSum, separator } = optionsValues;
|
||||
setResult(compute(input, extractionType, printRunningSum, separator));
|
||||
}}
|
||||
initialValues={initialValues}
|
||||
input={input}
|
||||
/>
|
||||
<ToolInfo
|
||||
title="What Is a Number Sum Calculator?"
|
||||
description="This is an online browser-based utility for calculating the sum of a bunch of numbers. You can enter the numbers separated by a comma, space, or any other character, including the line break. You can also simply paste a fragment of textual data that contains numerical values that you want to sum up and the utility will extract them and find their sum."
|
||||
/>
|
||||
<Separator backgroundColor="#5581b5" margin="50px" />
|
||||
<ToolExamples
|
||||
title={title}
|
||||
exampleCards={exampleCards}
|
||||
getGroups={getGroups}
|
||||
formRef={formRef}
|
||||
setInput={setInput}
|
||||
/>
|
||||
</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',
|
||||
icon: 'fluent:autosum-20-regular',
|
||||
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',
|
||||
icon: '',
|
||||
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',
|
||||
icon: '',
|
||||
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');
|
||||
}
|
||||
30
src/pages/tools/string/index.ts
Normal file
30
src/pages/tools/string/index.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import { tool as stringRemoveDuplicateLines } from './remove-duplicate-lines/meta';
|
||||
import { tool as stringRotate } from './rotate/meta';
|
||||
import { tool as stringQuote } from './quote/meta';
|
||||
import { tool as stringRot13 } from './rot13/meta';
|
||||
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';
|
||||
import { tool as stringReplace } from './text-replacer/meta';
|
||||
import { tool as stringRepeat } from './repeat/meta';
|
||||
|
||||
export const stringTools = [
|
||||
stringSplit,
|
||||
stringJoin,
|
||||
stringRemoveDuplicateLines,
|
||||
stringToMorse,
|
||||
stringReplace,
|
||||
stringRepeat
|
||||
// stringReverse,
|
||||
// stringRandomizeCase,
|
||||
// stringUppercase,
|
||||
// stringExtractSubstring,
|
||||
// stringCreatePalindrome,
|
||||
// stringPalindrome
|
||||
];
|
||||
186
src/pages/tools/string/join/index.tsx
Normal file
186
src/pages/tools/string/join/index.tsx
Normal file
@@ -0,0 +1,186 @@
|
||||
import { Box } from '@mui/material';
|
||||
import React, { useRef, useState } from 'react';
|
||||
import * as Yup from 'yup';
|
||||
import ToolTextInput from '@components/input/ToolTextInput';
|
||||
import ToolTextResult from '@components/result/ToolTextResult';
|
||||
import ToolOptions, { GetGroupsType } 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 ToolExamples, {
|
||||
CardExampleType
|
||||
} from '@components/examples/ToolExamples';
|
||||
import { FormikProps } from 'formik';
|
||||
import { ToolComponentProps } from '@tools/defineTool';
|
||||
|
||||
const initialValues = {
|
||||
joinCharacter: '',
|
||||
deleteBlank: true,
|
||||
deleteTrailing: true
|
||||
};
|
||||
type InitialValuesType = typeof initialValues;
|
||||
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 InitialValuesType
|
||||
};
|
||||
|
||||
const blankTrailingOptions: {
|
||||
title: string;
|
||||
description: string;
|
||||
accessor: keyof InitialValuesType;
|
||||
}[] = [
|
||||
{
|
||||
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: CardExampleType<InitialValuesType>[] = [
|
||||
{
|
||||
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`,
|
||||
sampleOptions: {
|
||||
joinCharacter: 'and',
|
||||
deleteBlank: true,
|
||||
deleteTrailing: 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`,
|
||||
sampleOptions: {
|
||||
joinCharacter: ',',
|
||||
deleteBlank: false,
|
||||
deleteTrailing: 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!`,
|
||||
sampleOptions: {
|
||||
joinCharacter: '',
|
||||
deleteBlank: false,
|
||||
deleteTrailing: false
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
export default function JoinText({ title }: ToolComponentProps) {
|
||||
const [input, setInput] = useState<string>('');
|
||||
const [result, setResult] = useState<string>('');
|
||||
const formRef = useRef<FormikProps<InitialValuesType>>(null);
|
||||
const compute = (optionsValues: InitialValuesType, input: any) => {
|
||||
const { joinCharacter, deleteBlank, deleteTrailing } = optionsValues;
|
||||
setResult(mergeText(input, deleteBlank, deleteTrailing, joinCharacter));
|
||||
};
|
||||
|
||||
const getGroups: GetGroupsType<InitialValuesType> = ({
|
||||
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}
|
||||
/>
|
||||
))
|
||||
}
|
||||
];
|
||||
return (
|
||||
<Box>
|
||||
<ToolInputAndResult
|
||||
input={
|
||||
<ToolTextInput
|
||||
title={'Text Pieces'}
|
||||
value={input}
|
||||
onChange={setInput}
|
||||
/>
|
||||
}
|
||||
result={<ToolTextResult title={'Joined Text'} value={result} />}
|
||||
/>
|
||||
<ToolOptions
|
||||
formRef={formRef}
|
||||
compute={compute}
|
||||
getGroups={getGroups}
|
||||
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" />
|
||||
<ToolExamples
|
||||
title={title}
|
||||
exampleCards={exampleCards}
|
||||
getGroups={getGroups}
|
||||
formRef={formRef}
|
||||
setInput={setInput}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
13
src/pages/tools/string/join/meta.ts
Normal file
13
src/pages/tools/string/join/meta.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { defineTool } from '@tools/defineTool';
|
||||
import { lazy } from 'react';
|
||||
|
||||
export const tool = defineTool('string', {
|
||||
path: 'join',
|
||||
name: 'Text Joiner',
|
||||
icon: 'tabler:arrows-join',
|
||||
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',
|
||||
icon: '',
|
||||
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',
|
||||
icon: '',
|
||||
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('');
|
||||
}
|
||||
260
src/pages/tools/string/remove-duplicate-lines/index.tsx
Normal file
260
src/pages/tools/string/remove-duplicate-lines/index.tsx
Normal file
@@ -0,0 +1,260 @@
|
||||
import { Box } from '@mui/material';
|
||||
import React, { useRef, useState } from 'react';
|
||||
import ToolTextInput from '@components/input/ToolTextInput';
|
||||
import ToolTextResult from '@components/result/ToolTextResult';
|
||||
import ToolOptions, { GetGroupsType } from '@components/options/ToolOptions';
|
||||
import SimpleRadio from '@components/options/SimpleRadio';
|
||||
import CheckboxWithDesc from '@components/options/CheckboxWithDesc';
|
||||
import ToolInputAndResult from '@components/ToolInputAndResult';
|
||||
import ToolExamples, {
|
||||
CardExampleType
|
||||
} from '@components/examples/ToolExamples';
|
||||
import { ToolComponentProps } from '@tools/defineTool';
|
||||
import { FormikProps } from 'formik';
|
||||
import removeDuplicateLines, {
|
||||
DuplicateRemovalMode,
|
||||
DuplicateRemoverOptions,
|
||||
NewlineOption
|
||||
} from './service';
|
||||
|
||||
// Initial values for our form
|
||||
const initialValues: DuplicateRemoverOptions = {
|
||||
mode: 'all',
|
||||
newlines: 'filter',
|
||||
sortLines: false,
|
||||
trimTextLines: false
|
||||
};
|
||||
|
||||
// Operation mode options
|
||||
const operationModes = [
|
||||
{
|
||||
title: 'Remove All Duplicate Lines',
|
||||
description:
|
||||
'If this option is selected, then all repeated lines across entire text are removed, starting from the second occurrence.',
|
||||
value: 'all' as DuplicateRemovalMode
|
||||
},
|
||||
{
|
||||
title: 'Remove Consecutive Duplicate Lines',
|
||||
description:
|
||||
'If this option is selected, then only consecutive repeated lines are removed.',
|
||||
value: 'consecutive' as DuplicateRemovalMode
|
||||
},
|
||||
{
|
||||
title: 'Leave Absolutely Unique Text Lines',
|
||||
description:
|
||||
'If this option is selected, then all lines that appear more than once are removed.',
|
||||
value: 'unique' as DuplicateRemovalMode
|
||||
}
|
||||
];
|
||||
|
||||
// Newlines options
|
||||
const newlineOptions = [
|
||||
{
|
||||
title: 'Preserve All Newlines',
|
||||
description: 'Leave all empty lines in the output.',
|
||||
value: 'preserve' as NewlineOption
|
||||
},
|
||||
{
|
||||
title: 'Filter All Newlines',
|
||||
description: 'Process newlines as regular lines.',
|
||||
value: 'filter' as NewlineOption
|
||||
},
|
||||
{
|
||||
title: 'Delete All Newlines',
|
||||
description: 'Before filtering uniques, remove all newlines.',
|
||||
value: 'delete' as NewlineOption
|
||||
}
|
||||
];
|
||||
|
||||
// Example cards for demonstration
|
||||
const exampleCards: CardExampleType<typeof initialValues>[] = [
|
||||
{
|
||||
title: 'Remove Duplicate Items from List',
|
||||
description:
|
||||
'Removes duplicate items from a shopping list, keeping only the first occurrence of each item.',
|
||||
sampleText: `Apples
|
||||
Bananas
|
||||
Milk
|
||||
Eggs
|
||||
Bread
|
||||
Milk
|
||||
Cheese
|
||||
Apples
|
||||
Yogurt`,
|
||||
sampleResult: `Apples
|
||||
Bananas
|
||||
Milk
|
||||
Eggs
|
||||
Bread
|
||||
Cheese
|
||||
Yogurt`,
|
||||
sampleOptions: {
|
||||
...initialValues,
|
||||
mode: 'all',
|
||||
newlines: 'filter',
|
||||
sortLines: false,
|
||||
trimTextLines: false
|
||||
}
|
||||
},
|
||||
{
|
||||
title: 'Clean Consecutive Duplicates',
|
||||
description:
|
||||
'Removes consecutive duplicates from log entries, which often happen when a system repeatedly logs the same error.',
|
||||
sampleText: `[INFO] Application started
|
||||
[ERROR] Connection failed
|
||||
[ERROR] Connection failed
|
||||
[ERROR] Connection failed
|
||||
[INFO] Retrying connection
|
||||
[ERROR] Authentication error
|
||||
[ERROR] Authentication error
|
||||
[INFO] Connection established`,
|
||||
sampleResult: `[INFO] Application started
|
||||
[ERROR] Connection failed
|
||||
[INFO] Retrying connection
|
||||
[ERROR] Authentication error
|
||||
[INFO] Connection established`,
|
||||
sampleOptions: {
|
||||
...initialValues,
|
||||
mode: 'consecutive',
|
||||
newlines: 'filter',
|
||||
sortLines: false,
|
||||
trimTextLines: false
|
||||
}
|
||||
},
|
||||
{
|
||||
title: 'Extract Unique Entries Only',
|
||||
description:
|
||||
'Filters a list to keep only entries that appear exactly once, removing any duplicated items entirely.',
|
||||
sampleText: `Red
|
||||
Blue
|
||||
Green
|
||||
Blue
|
||||
Yellow
|
||||
Purple
|
||||
Red
|
||||
Orange`,
|
||||
sampleResult: `Green
|
||||
Yellow
|
||||
Purple
|
||||
Orange`,
|
||||
sampleOptions: {
|
||||
...initialValues,
|
||||
mode: 'unique',
|
||||
newlines: 'filter',
|
||||
sortLines: false,
|
||||
trimTextLines: false
|
||||
}
|
||||
},
|
||||
{
|
||||
title: 'Sort and Clean Data',
|
||||
description:
|
||||
'Removes duplicate items from a list, trims whitespace, and sorts the results alphabetically.',
|
||||
sampleText: ` Apple
|
||||
Banana
|
||||
Cherry
|
||||
Apple
|
||||
Banana
|
||||
Dragonfruit
|
||||
Elderberry `,
|
||||
sampleResult: `Apple
|
||||
Banana
|
||||
Cherry
|
||||
Dragonfruit
|
||||
Elderberry`,
|
||||
sampleOptions: {
|
||||
...initialValues,
|
||||
mode: 'all',
|
||||
newlines: 'filter',
|
||||
sortLines: true,
|
||||
trimTextLines: true
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
export default function RemoveDuplicateLines({ title }: ToolComponentProps) {
|
||||
const [input, setInput] = useState<string>('');
|
||||
const [result, setResult] = useState<string>('');
|
||||
const formRef = useRef<FormikProps<typeof initialValues>>(null);
|
||||
|
||||
const computeExternal = (
|
||||
optionsValues: typeof initialValues,
|
||||
inputText: string
|
||||
) => {
|
||||
setResult(removeDuplicateLines(inputText, optionsValues));
|
||||
};
|
||||
|
||||
const getGroups: GetGroupsType<typeof initialValues> = ({
|
||||
values,
|
||||
updateField
|
||||
}) => [
|
||||
{
|
||||
title: 'Operation Mode',
|
||||
component: operationModes.map(({ title, description, value }) => (
|
||||
<SimpleRadio
|
||||
key={value}
|
||||
checked={value === values.mode}
|
||||
title={title}
|
||||
description={description}
|
||||
onClick={() => updateField('mode', value)}
|
||||
/>
|
||||
))
|
||||
},
|
||||
{
|
||||
title: 'Newlines, Tabs and Spaces',
|
||||
component: [
|
||||
...newlineOptions.map(({ title, description, value }) => (
|
||||
<SimpleRadio
|
||||
key={value}
|
||||
checked={value === values.newlines}
|
||||
title={title}
|
||||
description={description}
|
||||
onClick={() => updateField('newlines', value)}
|
||||
/>
|
||||
)),
|
||||
<CheckboxWithDesc
|
||||
key="trimTextLines"
|
||||
checked={values.trimTextLines}
|
||||
title="Trim Text Lines"
|
||||
description="Before filtering uniques, remove tabs and spaces from the beginning and end of all lines."
|
||||
onChange={(checked) => updateField('trimTextLines', checked)}
|
||||
/>
|
||||
]
|
||||
},
|
||||
{
|
||||
title: 'Sort Lines',
|
||||
component: [
|
||||
<CheckboxWithDesc
|
||||
key="sortLines"
|
||||
checked={values.sortLines}
|
||||
title="Sort the Output Lines"
|
||||
description="After removing the duplicates, sort the unique lines."
|
||||
onChange={(checked) => updateField('sortLines', checked)}
|
||||
/>
|
||||
]
|
||||
}
|
||||
];
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<ToolInputAndResult
|
||||
input={<ToolTextInput value={input} onChange={setInput} />}
|
||||
result={
|
||||
<ToolTextResult title={'Text without duplicates'} value={result} />
|
||||
}
|
||||
/>
|
||||
<ToolOptions
|
||||
compute={computeExternal}
|
||||
getGroups={getGroups}
|
||||
initialValues={initialValues}
|
||||
input={input}
|
||||
/>
|
||||
<ToolExamples
|
||||
title={title}
|
||||
exampleCards={exampleCards}
|
||||
getGroups={getGroups}
|
||||
formRef={formRef}
|
||||
setInput={setInput}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
13
src/pages/tools/string/remove-duplicate-lines/meta.ts
Normal file
13
src/pages/tools/string/remove-duplicate-lines/meta.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { defineTool } from '@tools/defineTool';
|
||||
import { lazy } from 'react';
|
||||
|
||||
export const tool = defineTool('string', {
|
||||
name: 'Remove duplicate lines',
|
||||
path: 'remove-duplicate-lines',
|
||||
icon: 'pepicons-print:duplicate-off',
|
||||
description:
|
||||
"Load your text in the input form on the left and you'll instantly get text with no duplicate lines in the output area. Powerful, free, and fast. Load text lines – get unique text lines",
|
||||
shortDescription: 'Quickly delete all repeated lines from text',
|
||||
keywords: ['remove', 'duplicate', 'lines'],
|
||||
component: lazy(() => import('./index'))
|
||||
});
|
||||
@@ -0,0 +1,200 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import removeDuplicateLines, { DuplicateRemoverOptions } from './service';
|
||||
|
||||
describe('removeDuplicateLines function', () => {
|
||||
// Test for 'all' duplicate removal mode
|
||||
describe('mode: all', () => {
|
||||
it('should remove all duplicates keeping first occurrence', () => {
|
||||
const input = 'line1\nline2\nline1\nline3\nline2';
|
||||
const options: DuplicateRemoverOptions = {
|
||||
mode: 'all',
|
||||
newlines: 'filter',
|
||||
sortLines: false,
|
||||
trimTextLines: false
|
||||
};
|
||||
const result = removeDuplicateLines(input, options);
|
||||
expect(result).toBe('line1\nline2\nline3');
|
||||
});
|
||||
|
||||
it('should handle case-sensitive duplicates correctly', () => {
|
||||
const input = 'Line1\nline1\nLine2\nline2';
|
||||
const options: DuplicateRemoverOptions = {
|
||||
mode: 'all',
|
||||
newlines: 'filter',
|
||||
sortLines: false,
|
||||
trimTextLines: false
|
||||
};
|
||||
const result = removeDuplicateLines(input, options);
|
||||
expect(result).toBe('Line1\nline1\nLine2\nline2');
|
||||
});
|
||||
});
|
||||
|
||||
// Test for 'consecutive' duplicate removal mode
|
||||
describe('mode: consecutive', () => {
|
||||
it('should remove only consecutive duplicates', () => {
|
||||
const input = 'line1\nline1\nline2\nline3\nline3\nline1';
|
||||
const options: DuplicateRemoverOptions = {
|
||||
mode: 'consecutive',
|
||||
newlines: 'filter',
|
||||
sortLines: false,
|
||||
trimTextLines: false
|
||||
};
|
||||
const result = removeDuplicateLines(input, options);
|
||||
expect(result).toBe('line1\nline2\nline3\nline1');
|
||||
});
|
||||
});
|
||||
|
||||
// Test for 'unique' duplicate removal mode
|
||||
describe('mode: unique', () => {
|
||||
it('should keep only lines that appear exactly once', () => {
|
||||
const input = 'line1\nline2\nline1\nline3\nline4\nline4';
|
||||
const options: DuplicateRemoverOptions = {
|
||||
mode: 'unique',
|
||||
newlines: 'filter',
|
||||
sortLines: false,
|
||||
trimTextLines: false
|
||||
};
|
||||
const result = removeDuplicateLines(input, options);
|
||||
expect(result).toBe('line2\nline3');
|
||||
});
|
||||
});
|
||||
|
||||
// Test for newlines handling
|
||||
describe('newlines option', () => {
|
||||
it('should filter newlines when newlines is set to filter', () => {
|
||||
const input = 'line1\n\nline2\n\n\nline3';
|
||||
const options: DuplicateRemoverOptions = {
|
||||
mode: 'all',
|
||||
newlines: 'filter',
|
||||
sortLines: false,
|
||||
trimTextLines: false
|
||||
};
|
||||
const result = removeDuplicateLines(input, options);
|
||||
expect(result).toBe('line1\n\nline2\nline3');
|
||||
});
|
||||
|
||||
it('should delete newlines when newlines is set to delete', () => {
|
||||
const input = 'line1\n\nline2\n\n\nline3';
|
||||
const options: DuplicateRemoverOptions = {
|
||||
mode: 'all',
|
||||
newlines: 'delete',
|
||||
sortLines: false,
|
||||
trimTextLines: false
|
||||
};
|
||||
const result = removeDuplicateLines(input, options);
|
||||
expect(result).toBe('line1\nline2\nline3');
|
||||
});
|
||||
|
||||
it('should preserve newlines when newlines is set to preserve', () => {
|
||||
const input = 'line1\n\nline2\n\nline2\nline3';
|
||||
const options: DuplicateRemoverOptions = {
|
||||
mode: 'all',
|
||||
newlines: 'preserve',
|
||||
sortLines: false,
|
||||
trimTextLines: false
|
||||
};
|
||||
const result = removeDuplicateLines(input, options);
|
||||
// This test needs careful consideration of the expected behavior
|
||||
expect(result).not.toContain('line2\nline2');
|
||||
expect(result).toContain('line1');
|
||||
expect(result).toContain('line2');
|
||||
expect(result).toContain('line3');
|
||||
});
|
||||
});
|
||||
|
||||
// Test for sorting
|
||||
describe('sortLines option', () => {
|
||||
it('should sort lines when sortLines is true', () => {
|
||||
const input = 'line3\nline1\nline2';
|
||||
const options: DuplicateRemoverOptions = {
|
||||
mode: 'all',
|
||||
newlines: 'filter',
|
||||
sortLines: true,
|
||||
trimTextLines: false
|
||||
};
|
||||
const result = removeDuplicateLines(input, options);
|
||||
expect(result).toBe('line1\nline2\nline3');
|
||||
});
|
||||
});
|
||||
|
||||
// Test for trimming
|
||||
describe('trimTextLines option', () => {
|
||||
it('should trim lines when trimTextLines is true', () => {
|
||||
const input = ' line1 \n line2 \nline3';
|
||||
const options: DuplicateRemoverOptions = {
|
||||
mode: 'all',
|
||||
newlines: 'filter',
|
||||
sortLines: false,
|
||||
trimTextLines: true
|
||||
};
|
||||
const result = removeDuplicateLines(input, options);
|
||||
expect(result).toBe('line1\nline2\nline3');
|
||||
});
|
||||
|
||||
it('should consider trimmed lines as duplicates', () => {
|
||||
const input = ' line1 \nline1\n line2\nline2 ';
|
||||
const options: DuplicateRemoverOptions = {
|
||||
mode: 'all',
|
||||
newlines: 'filter',
|
||||
sortLines: false,
|
||||
trimTextLines: true
|
||||
};
|
||||
const result = removeDuplicateLines(input, options);
|
||||
expect(result).toBe('line1\nline2');
|
||||
});
|
||||
});
|
||||
|
||||
// Combined scenarios
|
||||
describe('combined options', () => {
|
||||
it('should handle all options together correctly', () => {
|
||||
const input = ' line3 \nline1\n\nline3\nline2\nline1';
|
||||
const options: DuplicateRemoverOptions = {
|
||||
mode: 'all',
|
||||
newlines: 'delete',
|
||||
sortLines: true,
|
||||
trimTextLines: true
|
||||
};
|
||||
const result = removeDuplicateLines(input, options);
|
||||
expect(result).toBe('line1\nline2\nline3');
|
||||
});
|
||||
});
|
||||
|
||||
// Edge cases
|
||||
describe('edge cases', () => {
|
||||
it('should handle empty input', () => {
|
||||
const input = '';
|
||||
const options: DuplicateRemoverOptions = {
|
||||
mode: 'all',
|
||||
newlines: 'filter',
|
||||
sortLines: false,
|
||||
trimTextLines: false
|
||||
};
|
||||
const result = removeDuplicateLines(input, options);
|
||||
expect(result).toBe('');
|
||||
});
|
||||
|
||||
it('should handle input with only newlines', () => {
|
||||
const input = '\n\n\n';
|
||||
const options: DuplicateRemoverOptions = {
|
||||
mode: 'all',
|
||||
newlines: 'filter',
|
||||
sortLines: false,
|
||||
trimTextLines: false
|
||||
};
|
||||
const result = removeDuplicateLines(input, options);
|
||||
expect(result).toBe('');
|
||||
});
|
||||
|
||||
it('should handle input with only whitespace', () => {
|
||||
const input = ' \n \n ';
|
||||
const options: DuplicateRemoverOptions = {
|
||||
mode: 'all',
|
||||
newlines: 'filter',
|
||||
sortLines: false,
|
||||
trimTextLines: true
|
||||
};
|
||||
const result = removeDuplicateLines(input, options);
|
||||
expect(result).toBe('');
|
||||
});
|
||||
});
|
||||
});
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user