Merge pull request #310 from iib0011/list-group-fix

Improve clarity for List Group tool (issue #308)
This commit is contained in:
Ibrahima G. Coulibaly
2026-02-14 19:41:42 +01:00
committed by GitHub
5 changed files with 169 additions and 90 deletions
+20 -20
View File
@@ -113,36 +113,36 @@
"trimItemsDescription": "Remove leading and trailing spaces before comparing items.",
"uniqueItemOptions": "Unique Item Options"
},
"group": {
"chunk": {
"deleteEmptyItems": "Delete Empty Items",
"deleteEmptyItemsDescription": "Ignore empty items and don't include them in the groups.",
"description": "World's simplest browser-based utility for grouping list items. Input your list and specify grouping criteria to organize items into logical groups. Perfect for categorizing data, organizing information, or creating structured lists. Supports custom separators and various grouping options.",
"deleteEmptyItemsDescription": "Ignore empty items and exclude them from the result.",
"description": "Split a list into consecutive chunks of a fixed size. This tool separates input items, chunks them sequentially, optionally pads incomplete chunks, and formats the output using custom separators and wrappers.",
"emptyItemsAndPadding": "Empty Items and Padding",
"groupNumberDescription": "Number of items in a group",
"groupSeparatorDescription": "Group separator character",
"groupSizeAndSeparators": "Group Size and Separators",
"groupNumberDescription": "Number of items per group",
"groupSeparatorDescription": "Separator placed between groups",
"groupSizeAndSeparators": "Group Size and Output Format",
"inputItemSeparator": "Input Item Separator",
"inputTitle": "Input list",
"itemSeparatorDescription": "Item separator character",
"leftWrapDescription": "Group's left wrap symbol.",
"padNonFullGroups": "Pad Non-full Groups",
"padNonFullGroupsDescription": "Fill non-full groups with a custom item (enter below).",
"paddingCharDescription": "Use this character or item to pad non-full groups.",
"resultTitle": "Grouped items",
"rightWrapDescription": "Group's right wrap symbol.",
"shortDescription": "Group list items by common properties",
"itemSeparatorDescription": "Separator placed between items inside a group",
"leftWrapDescription": "Symbol added before each group",
"padNonFullGroups": "Pad Incomplete Groups",
"padNonFullGroupsDescription": "Fill the last group if it has fewer items than required.",
"paddingCharDescription": "Character or value used to pad incomplete groups.",
"resultTitle": "Grouped output",
"rightWrapDescription": "Symbol added after each group",
"shortDescription": "Split a list into fixed-size chunks",
"splitOperators": {
"regex": {
"description": "Delimit input list items with a regular expression.",
"title": "Use a Regex for Splitting"
"description": "Split input items using a regular expression.",
"title": "Use Regex Splitting"
},
"symbol": {
"description": "Delimit input list items with a character.",
"title": "Use a Symbol for Splitting"
"description": "Split input items using a single character.",
"title": "Use Symbol Splitting"
}
},
"splitSeparatorDescription": "Set a delimiting symbol or regular expression.",
"title": "Group"
"splitSeparatorDescription": "Delimiter symbol or regular expression used to split input items.",
"title": "Chunk List"
},
"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.",
@@ -1,8 +1,8 @@
import { describe, expect, it } from 'vitest';
import { groupList, SplitOperatorType } from './service';
import { chunkList, SplitOperatorType } from './service';
describe('groupList', () => {
describe('chunkList', () => {
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';
@@ -18,7 +18,7 @@ describe('groupList', () => {
const expectedOutput = '[a-b-c] | [d-e-f] | [g-h-i] | [j-x-x]';
const result = groupList(
const result = chunkList(
splitOperatorType,
splitSeparator,
input,
@@ -49,7 +49,7 @@ describe('groupList', () => {
const expectedOutput = '(a,b,c,d) / (e,f,g,h) / (i,j)';
const result = groupList(
const result = chunkList(
splitOperatorType,
splitSeparator,
input,
@@ -80,7 +80,7 @@ describe('groupList', () => {
const expectedOutput = '<a:b> & <c:d> & <e:z>';
const result = groupList(
const result = chunkList(
splitOperatorType,
splitSeparator,
input,
+112 -33
View File
@@ -2,9 +2,10 @@ import { Box } from '@mui/material';
import React, { useState } from 'react';
import ToolTextInput from '@components/input/ToolTextInput';
import ToolTextResult from '@components/result/ToolTextResult';
import { groupList, SplitOperatorType } from './service';
import { chunkList, SplitOperatorType } from './service';
import SimpleRadio from '@components/options/SimpleRadio';
import TextFieldWithDesc from '@components/options/TextFieldWithDesc';
import { CardExampleType } from '@components/examples/ToolExamples';
import CheckboxWithDesc from '@components/options/CheckboxWithDesc';
import { formatNumber } from '../../../../utils/number';
import ToolContent from '@components/ToolContent';
@@ -23,24 +24,101 @@ const initialValues = {
padNonFullGroup: false,
paddingChar: '...'
};
const splitOperators: {
title: string;
description: string;
type: SplitOperatorType;
}[] = [
type InitialValuesType = typeof initialValues;
const splitOperators: SplitOperatorType[] = ['symbol', 'regex'];
const exampleCards: CardExampleType<InitialValuesType>[] = [
{
title: 'Use a Symbol for Splitting',
description: 'Delimit input list items with a character.',
type: 'symbol'
title: 'Group Hexagon Coordinates',
description:
'In this example, we group the coordinates of a regular hexagon. The input coordinates are given as a space-separated list "x1 y1 x2 y2 x3 y3 …". What we want to do is create vector point pairs such as "(x1, y1); (x2, y2); (x3, y3); …". To do that, we use the space character as the input coordinate separator, and to create vectors, we group them by pairs. We wrap the coordinates in parentheses, put a comma between the x and y group items, and a semicolon between individual groups.',
sampleText: '2.5 9.33 0 5 2.5 0.66 7.5 0.66 10 5 7.5 9.33',
sampleResult: `(2.5, 9.33); (0, 5); (2.5, 0.66); (7.5, 0.66); (10, 5); (7.5, 9.33)`,
sampleOptions: {
splitOperatorType: 'symbol',
splitSeparator: ' ',
groupNumber: 2,
itemSeparator: ', ',
leftWrap: '(',
rightWrap: ')',
groupSeparator: '; ',
deleteEmptyItems: true,
padNonFullGroup: false,
paddingChar: 'x'
}
},
{
title: 'Use a Regex for Splitting',
type: 'regex',
description: 'Delimit input list items with a regular expression.'
title: 'Chunks of Size 3',
description:
'This example demonstrates grouping of list items and creates 9 groups of 3 items. The input list contains all alphabet letters (26 letters, separated by a semicolon) and the output is groups of letter trigrams. As the last group is missing one letter, we enable padding and add the underscore symbol as the padding element.',
sampleText: 'a;b;c;d;e;f;g;h;i;j;k;l;m;n;o;p;q;r;s;t;u;v;w;x;y;z',
sampleResult: `[a, b, c]
[d, e, f]
[g, h, i]
[j, k, l]
[m, n, o]
[p, q, r]
[s, t, u]
[v, w, x]
[y, z, _]`,
sampleOptions: {
splitOperatorType: 'symbol',
splitSeparator: ';',
groupNumber: 3,
itemSeparator: ',',
leftWrap: '[',
rightWrap: ']',
groupSeparator: '\\n',
deleteEmptyItems: false,
padNonFullGroup: true,
paddingChar: '_'
}
},
{
title: 'Convert a List to a TSV',
description:
'In this example, we use our list item grouper to convert a food list to tab-separated values (TSV). As spaces are chaotically used between the items of the input list, we use the item separating regular expression "\\s+" to match them. We create a TSV with three columns (three groups), separate them with a tab character, and put newlines between the groups.',
sampleText: `beef buns
cake corn
crab
dill
fish
kiwi kale
lime meat
mint
milk
pear plum
pate
pork rice
soup
tuna
tart`,
sampleResult: `beef buns cake
corn crab dill
fish kiwi kale
lime meat mint
milk pear plum
pate pork rice
soup tuna tart`,
sampleOptions: {
splitOperatorType: 'regex',
splitSeparator: '\\s+',
groupNumber: 3,
itemSeparator: '\\t',
leftWrap: '',
rightWrap: '',
groupSeparator: '\\n',
deleteEmptyItems: true,
padNonFullGroup: false,
paddingChar: 'x'
}
}
];
export default function FindUnique({ title }: ToolComponentProps) {
export default function ChunkList({ title }: ToolComponentProps) {
const { t } = useTranslation('list');
const [input, setInput] = useState<string>('');
const [result, setResult] = useState<string>('');
@@ -59,7 +137,7 @@ export default function FindUnique({ title }: ToolComponentProps) {
} = optionsValues;
setResult(
groupList(
chunkList(
splitOperatorType,
splitSeparator,
input,
@@ -79,33 +157,34 @@ export default function FindUnique({ title }: ToolComponentProps) {
<ToolContent
title={title}
input={input}
exampleCards={exampleCards}
inputComponent={
<ToolTextInput
title={t('group.inputTitle')}
title={t('chunk.inputTitle')}
value={input}
onChange={setInput}
/>
}
resultComponent={
<ToolTextResult title={t('group.resultTitle')} value={result} />
<ToolTextResult title={t('chunk.resultTitle')} value={result} />
}
initialValues={initialValues}
getGroups={({ values, updateField }) => [
{
title: t('group.inputItemSeparator'),
title: t('chunk.inputItemSeparator'),
component: (
<Box>
{splitOperators.map(({ title, description, type }) => (
{splitOperators.map((type) => (
<SimpleRadio
key={type}
onClick={() => updateField('splitOperatorType', type)}
title={t(`group.splitOperators.${type}.title`)}
description={t(`group.splitOperators.${type}.description`)}
title={t(`chunk.splitOperators.${type}.title`)}
description={t(`chunk.splitOperators.${type}.description`)}
checked={values.splitOperatorType === type}
/>
))}
<TextFieldWithDesc
description={t('group.splitSeparatorDescription')}
description={t('chunk.splitSeparatorDescription')}
value={values.splitSeparator}
onOwnChange={(val) => updateField('splitSeparator', val)}
/>
@@ -113,12 +192,12 @@ export default function FindUnique({ title }: ToolComponentProps) {
)
},
{
title: t('group.groupSizeAndSeparators'),
title: t('chunk.groupSizeAndSeparators'),
component: (
<Box>
<TextFieldWithDesc
value={values.groupNumber}
description={t('group.groupNumberDescription')}
description={t('chunk.groupNumberDescription')}
type={'number'}
onOwnChange={(value) =>
updateField('groupNumber', formatNumber(value, 1))
@@ -126,46 +205,46 @@ export default function FindUnique({ title }: ToolComponentProps) {
/>
<TextFieldWithDesc
value={values.itemSeparator}
description={t('group.itemSeparatorDescription')}
description={t('chunk.itemSeparatorDescription')}
onOwnChange={(value) => updateField('itemSeparator', value)}
/>
<TextFieldWithDesc
value={values.groupSeparator}
description={t('group.groupSeparatorDescription')}
description={t('chunk.groupSeparatorDescription')}
onOwnChange={(value) => updateField('groupSeparator', value)}
/>
<TextFieldWithDesc
value={values.leftWrap}
description={t('group.leftWrapDescription')}
description={t('chunk.leftWrapDescription')}
onOwnChange={(value) => updateField('leftWrap', value)}
/>
<TextFieldWithDesc
value={values.rightWrap}
description={t('group.rightWrapDescription')}
description={t('chunk.rightWrapDescription')}
onOwnChange={(value) => updateField('rightWrap', value)}
/>
</Box>
)
},
{
title: t('group.emptyItemsAndPadding'),
title: t('chunk.emptyItemsAndPadding'),
component: (
<Box>
<CheckboxWithDesc
title={t('group.deleteEmptyItems')}
description={t('group.deleteEmptyItemsDescription')}
title={t('chunk.deleteEmptyItems')}
description={t('chunk.deleteEmptyItemsDescription')}
checked={values.deleteEmptyItems}
onChange={(value) => updateField('deleteEmptyItems', value)}
/>
<CheckboxWithDesc
title={t('group.padNonFullGroups')}
description={t('group.padNonFullGroupsDescription')}
title={t('chunk.padNonFullGroups')}
description={t('chunk.padNonFullGroupsDescription')}
checked={values.padNonFullGroup}
onChange={(value) => updateField('padNonFullGroup', value)}
/>
<TextFieldWithDesc
value={values.paddingChar}
description={t('group.paddingCharDescription')}
description={t('chunk.paddingCharDescription')}
onOwnChange={(value) => updateField('paddingChar', value)}
/>
</Box>
+6 -6
View File
@@ -2,15 +2,15 @@ import { defineTool } from '@tools/defineTool';
import { lazy } from 'react';
export const tool = defineTool('list', {
path: 'group',
icon: 'pajamas:group',
path: 'chunk',
icon: 'mdi:rhombus-split',
keywords: ['group'],
keywords: ['chuck', 'list', 'partition', 'split'],
component: lazy(() => import('./index')),
i18n: {
name: 'list:group.title',
description: 'list:group.description',
shortDescription: 'list:group.shortDescription',
name: 'list:chunk.title',
description: 'list:chunk.description',
shortDescription: 'list:chunk.shortDescription',
userTypes: ['generalUsers', 'developers']
}
});
+26 -26
View File
@@ -1,25 +1,25 @@
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[][] {
function chunkMaker(array: string[], chunkNumber: number): string[][] {
const result: string[][] = [];
for (let i = 0; i < array.length; i += groupNumber) {
result.push(array.slice(i, i + groupNumber));
for (let i = 0; i < array.length; i += chunkNumber) {
result.push(array.slice(i, i + chunkNumber));
}
return result;
}
// function use to handle the case paddingNonFullGroup is enable
function groupFiller(
// function use to handle the case paddingNonFullChunk is enable
function chunkFiller(
array: string[][],
groupNumber: number,
padNonFullGroup: boolean,
chunkNumber: number,
padNonFullChunk: boolean,
paddingChar: string = ''
): string[][] {
if (padNonFullGroup) {
if (padNonFullChunk) {
const lastSubArray: string[] = array[array.length - 1];
if (lastSubArray.length < groupNumber) {
for (let i = lastSubArray.length; i < groupNumber; i++) {
if (lastSubArray.length < chunkNumber) {
for (let i = lastSubArray.length; i < chunkNumber; i++) {
lastSubArray.push(paddingChar);
}
}
@@ -29,7 +29,7 @@ function groupFiller(
}
// function that join with the item separator and wrap with left and right each subArray of the Array
function groupJoinerAndWrapper(
function chunkJoinerAndWrapper(
array: string[][],
itemSeparator: string = '',
leftWrap: string = '',
@@ -40,23 +40,23 @@ function groupJoinerAndWrapper(
});
}
export function groupList(
export function chunkList(
splitOperatorType: SplitOperatorType,
splitSeparator: string,
input: string,
groupNumber: number,
chunkNumber: number,
itemSeparator: string = '',
leftWrap: string = '',
rightWrap: string = '',
groupSeparator: string,
chunkSeparator: string,
deleteEmptyItems: boolean,
padNonFullGroup: boolean,
padNonFullChunk: boolean,
paddingChar: string = ''
): string {
if (!splitSeparator) return '';
let array: string[];
let splitedArray: string[][];
let fullSplitedArray: string[][];
let result: string[];
switch (splitOperatorType) {
case 'symbol':
array = input.split(splitSeparator);
@@ -71,24 +71,24 @@ export function groupList(
}
// split the input into an array of subArray with the desired length
splitedArray = groupMaker(array, groupNumber);
const splitedArray = chunkMaker(array, chunkNumber);
// fill the last subArray is PadNonFullGroup is enabled
fullSplitedArray = groupFiller(
// fill the last subArray is PadNonFullChunk is enabled
const fullSplitedArray = chunkFiller(
splitedArray,
groupNumber,
padNonFullGroup,
chunkNumber,
padNonFullChunk,
paddingChar
);
// get the list of formated subArray with the item separator and left and right wrapper
result = groupJoinerAndWrapper(
const result = chunkJoinerAndWrapper(
fullSplitedArray,
itemSeparator,
leftWrap,
rightWrap
);
// finnaly join the group separator before returning
return result.join(groupSeparator);
// finnaly join the chunk separator before returning
return result.join(chunkSeparator);
}