Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(List Converter): remove prefix/suffix capability, split, sort #1302

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion components.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -141,8 +141,10 @@ declare module '@vue/runtime-core' {
NLayout: typeof import('naive-ui')['NLayout']
NLayoutSider: typeof import('naive-ui')['NLayoutSider']
NMenu: typeof import('naive-ui')['NMenu']
NScrollbar: typeof import('naive-ui')['NScrollbar']
NSlider: typeof import('naive-ui')['NSlider']
NSpace: typeof import('naive-ui')['NSpace']
NTable: typeof import('naive-ui')['NTable']
NSwitch: typeof import('naive-ui')['NSwitch']
NumeronymGenerator: typeof import('./src/tools/numeronym-generator/numeronym-generator.vue')['default']
OtpCodeGeneratorAndValidator: typeof import('./src/tools/otp-code-generator-and-validator/otp-code-generator-and-validator.vue')['default']
PasswordStrengthAnalyser: typeof import('./src/tools/password-strength-analyser/password-strength-analyser.vue')['default']
Expand Down
83 changes: 62 additions & 21 deletions src/tools/list-converter/list-converter.models.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,11 @@ describe('list-converter', () => {
describe('convert', () => {
it('should convert a given list', () => {
const options: ConvertOptions = {
separator: ', ',
itemsSeparator: ', ',
trimItems: true,
removeDuplicates: true,
itemPrefix: '"',
itemSuffix: '"',
listPrefix: '',
listSuffix: '',
reverseList: false,
sortList: null,
lowerCase: false,
keepLineBreaks: false,
};
const input = `
1
Expand All @@ -31,34 +25,21 @@ describe('list-converter', () => {

it('should return an empty value for an empty input', () => {
const options: ConvertOptions = {
separator: ', ',
itemsSeparator: ', ',
trimItems: true,
removeDuplicates: true,
itemPrefix: '',
itemSuffix: '',
listPrefix: '',
listSuffix: '',
reverseList: false,
sortList: null,
lowerCase: false,
keepLineBreaks: false,
};
expect(convert('', options)).toEqual('');
});

it('should keep line breaks', () => {
const options: ConvertOptions = {
separator: '',
trimItems: true,
itemPrefix: '<li>',
itemSuffix: '</li>',
listPrefix: '<ul>',
listSuffix: '</ul>',
keepLineBreaks: true,
lowerCase: false,
removeDuplicates: false,
reverseList: false,
sortList: null,
};
const input = `
1
Expand All @@ -72,5 +53,65 @@ describe('list-converter', () => {
</ul>`;
expect(convert(input, options)).toEqual(expected);
});

it('should remove prefix and suffix', () => {
const options: ConvertOptions = {
trimItems: true,
removeItemPrefix: '<li>',
removeItemSuffix: '</li>',
keepLineBreaks: true,
};
const input = `
<li>1</li>
<li>2</li>
<li>3</li>
`;
const expected = `1
2
3`;
expect(convert(input, options)).toEqual(expected);
});

it('should split by separator', () => {
const options: ConvertOptions = {
trimItems: true,
keepLineBreaks: true,
splitBySeparator: ',',
};
const input = '1,2,3';
const expected = `1
2
3`;
expect(convert(input, options)).toEqual(expected);
});

it('should sort by asc-num', () => {
const options: ConvertOptions = {
trimItems: true,
keepLineBreaks: true,
sortList: 'asc-num',
};
const input = `3
20
1`;
const expected = `1
3
20`;
expect(convert(input, options)).toEqual(expected);
});
it('should sort by desc', () => {
const options: ConvertOptions = {
trimItems: true,
keepLineBreaks: true,
sortList: 'desc',
};
const input = `1
20
3`;
const expected = `3
20
1`;
expect(convert(input, options)).toEqual(expected);
});
});
});
16 changes: 10 additions & 6 deletions src/tools/list-converter/list-converter.models.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,24 +4,28 @@ import { byOrder } from '@/utils/array';

export { convert };

function whenever<T, R>(condition: boolean, fn: (value: T) => R) {
function whenever<T, R>(condition: boolean | undefined, fn: (value: T) => R) {
return (value: T) =>
condition ? fn(value) : value;
}

function convert(list: string, options: ConvertOptions): string {
const lineBreak = options.keepLineBreaks ? '\n' : '';

const splitSep = options.splitBySeparator ? `${options.splitBySeparator}|` : '';
const splitRegExp = new RegExp(`(?:${splitSep}\\n)`, 'g');
return _.chain(list)
.thru(whenever(options.lowerCase, text => text.toLowerCase()))
.split('\n')
.split(splitRegExp)
.thru(whenever(options.removeDuplicates, _.uniq))
.thru(whenever(options.reverseList, _.reverse))
.thru(whenever(!_.isNull(options.sortList), parts => parts.sort(byOrder({ order: options.sortList }))))
.map(whenever(options.trimItems, _.trim))
.thru(whenever(!!options.sortList, parts => parts.sort(byOrder({ order: options.sortList }))))
.without('')
.map(p => options.itemPrefix + p + options.itemSuffix)
.join(options.separator + lineBreak)
.thru(text => [options.listPrefix, text, options.listSuffix].join(lineBreak))
.map(p => options.removeItemPrefix ? p.replace(new RegExp(`^${options.removeItemPrefix}`, 'g'), '') : p)
.map(p => options.removeItemSuffix ? p.replace(new RegExp(`${options.removeItemSuffix}$`, 'g'), '') : p)
.map(p => (options.itemPrefix || '') + p + (options.itemSuffix || ''))
.join((options.itemsSeparator || '') + lineBreak)
.thru(text => [options.listPrefix, text, options.listSuffix].filter(l => l).join(lineBreak))
.value();
}
27 changes: 15 additions & 12 deletions src/tools/list-converter/list-converter.types.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,18 @@
export type SortOrder = 'asc' | 'desc' | null;
export type SortOrder = null | 'asc' | 'desc' | 'asc-num' | 'desc-num' | 'asc-bin' | 'desc-bin' | 'asc-upper' | 'desc-upper';

export interface ConvertOptions {
lowerCase: boolean
trimItems: boolean
itemPrefix: string
itemSuffix: string
listPrefix: string
listSuffix: string
reverseList: boolean
sortList: SortOrder
removeDuplicates: boolean
separator: string
keepLineBreaks: boolean
lowerCase?: boolean
trimItems?: boolean
itemPrefix?: string
itemSuffix?: string
removeItemPrefix?: string
removeItemSuffix?: string
listPrefix?: string
listSuffix?: string
reverseList?: boolean
sortList?: SortOrder
removeDuplicates?: boolean
itemsSeparator?: string
splitBySeparator?: string
keepLineBreaks?: boolean
}
70 changes: 61 additions & 9 deletions src/tools/list-converter/list-converter.vue
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,41 @@ import { convert } from './list-converter.models';
import type { ConvertOptions } from './list-converter.types';

const sortOrderOptions = [
{
label: 'No Sort',
value: null,
},
{
label: 'Sort ascending',
value: 'asc',
disabled: false,
},
{
label: 'Sort descending',
value: 'desc',
disabled: false,
},
{
label: 'Sort asc (Numeric)',
value: 'asc-num',
},
{
label: 'Sort desc (Numeric)',
value: 'desc-num',
},
{
label: 'Sort asc (Upper)',
value: 'asc-upper',
},
{
label: 'Sort desc (Upper)',
value: 'desc-upper',
},
{
label: 'Sort asc (Binary)',
value: 'asc-bin',
},
{
label: 'Sort desc (Binary)',
value: 'desc-bin',
},
];

Expand All @@ -23,11 +49,14 @@ const conversionConfig = useStorage<ConvertOptions>('list-converter:conversionCo
keepLineBreaks: false,
itemPrefix: '',
itemSuffix: '',
removeItemPrefix: '',
removeItemSuffix: '',
listPrefix: '',
listSuffix: '',
reverseList: false,
sortList: null,
separator: ', ',
itemsSeparator: ', ',
splitBySeparator: '',
});

function transformer(value: string) {
Expand All @@ -39,7 +68,7 @@ function transformer(value: string) {
<div style="flex: 0 0 100%">
<div style="margin: 0 auto; max-width: 600px">
<c-card>
<div flex>
<n-space>
<div>
<n-form-item label="Trim list items" label-placement="left" label-width="150" :show-feedback="false" mb-2>
<n-switch v-model:value="conversionConfig.trimItems" />
Expand All @@ -60,7 +89,7 @@ function transformer(value: string) {
<n-switch v-model:value="conversionConfig.keepLineBreaks" />
</n-form-item>
</div>
<div flex-1>
<div>
<c-select
v-model:value="conversionConfig.sortList"
label="Sort list"
Expand All @@ -76,15 +105,38 @@ function transformer(value: string) {
/>

<c-input-text
v-model:value="conversionConfig.separator"
label="Separator"
v-model:value="conversionConfig.itemsSeparator"
label="Items Separator"
label-position="left"
label-width="120px"
label-align="right"
mb-2
placeholder="Items separator"
/>

<c-input-text
v-model:value="conversionConfig.splitBySeparator"
label="Split Separator"
label-position="left"
label-width="120px"
label-align="right"
mb-2
placeholder=","
placeholder="Separator for splitting"
/>

<n-form-item label="Unwrap item" label-placement="left" label-width="120" :show-feedback="false" mb-2>
<c-input-text
v-model:value="conversionConfig.removeItemPrefix"
placeholder="Remove item prefix regex"
test-id="removeItemPrefix"
/>
<c-input-text
v-model:value="conversionConfig.removeItemSuffix"
placeholder="Remove item suffix regex"
test-id="removeItemSuffix"
/>
</n-form-item>

<n-form-item label="Wrap item" label-placement="left" label-width="120" :show-feedback="false" mb-2>
<c-input-text
v-model:value="conversionConfig.itemPrefix"
Expand All @@ -110,7 +162,7 @@ function transformer(value: string) {
/>
</n-form-item>
</div>
</div>
</n-space>
</c-card>
</div>
</div>
Expand Down
25 changes: 25 additions & 0 deletions src/utils/array.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { describe, expect, it } from 'vitest';
import { type SortOrder, byOrder } from './array';

describe('array utils', () => {
describe('byOrder', () => {
it('should sort correctly', () => {
const sortBy = (array: string[], order: SortOrder) => {
return array.sort(byOrder({ order }));
};

const strings = ['a', 'A', 'b', 'B', 'á', '1', '2', '10', '一', '阿'];

expect(sortBy(strings, null)).to.eql(strings);
expect(sortBy(strings, undefined)).to.eql(strings);
expect(sortBy(strings, 'asc')).to.eql(['1', '10', '2', 'a', 'A', 'á', 'b', 'B', '一', '阿']);
expect(sortBy(strings, 'asc-num')).to.eql(['1', '2', '10', 'a', 'A', 'á', 'b', 'B', '一', '阿']);
expect(sortBy(strings, 'asc-bin')).to.eql(['1', '10', '2', 'A', 'B', 'a', 'b', 'á', '一', '阿']);
expect(sortBy(strings, 'asc-upper')).to.eql(['1', '10', '2', 'A', 'a', 'á', 'B', 'b', '一', '阿']);
expect(sortBy(strings, 'desc')).to.eql(['阿', '一', 'B', 'b', 'á', 'A', 'a', '2', '10', '1']);
expect(sortBy(strings, 'desc-num')).to.eql(['阿', '一', 'B', 'b', 'á', 'A', 'a', '10', '2', '1']);
expect(sortBy(strings, 'desc-bin')).to.eql(['阿', '一', 'á', 'b', 'a', 'B', 'A', '2', '10', '1']);
expect(sortBy(strings, 'desc-upper')).to.eql(['阿', '一', 'b', 'B', 'á', 'a', 'A', '2', '10', '1']);
});
});
});
27 changes: 25 additions & 2 deletions src/utils/array.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,29 @@
export { byOrder };
export type SortOrder = 'asc' | 'desc' | 'asc-num' | 'desc-num' | 'asc-bin' | 'desc-bin' | 'asc-upper' | 'desc-upper' | null | undefined;

export function byOrder({ order }: { order: SortOrder }) {
if (order === 'asc-bin' || order === 'desc-bin') {
return (a: string, b: string) => {
const compare = a > b ? 1 : (a < b ? -1 : 0); // NOSONAR
return order === 'asc-bin' ? compare : -compare;
};
}
if (order === 'asc-num' || order === 'desc-num') {
return (a: string, b: string) => {
const compare = a.localeCompare(b, undefined, {
numeric: true,
});
return order === 'asc-num' ? compare : -compare;
};
}
if (order === 'asc-upper' || order === 'desc-upper') {
return (a: string, b: string) => {
const compare = a.localeCompare(b, undefined, {
caseFirst: 'upper',
});
return order === 'asc-upper' ? compare : -compare;
};
}

function byOrder({ order }: { order: 'asc' | 'desc' | null | undefined }) {
return (a: string, b: string) => {
return order === 'asc' ? a.localeCompare(b) : b.localeCompare(a);
};
Expand Down
Loading