feat: add split by separator + order by numeric + no sort

Fix #764 #1279 #1090
Small screen UI Fix
This commit is contained in:
ShareVB 2024-09-21 16:18:05 +02:00
parent 327ff11a59
commit 7bafd4790e
7 changed files with 168 additions and 72 deletions

2
components.d.ts vendored
View file

@ -132,6 +132,7 @@ declare module '@vue/runtime-core' {
NCode: typeof import('naive-ui')['NCode'] NCode: typeof import('naive-ui')['NCode']
NCollapseTransition: typeof import('naive-ui')['NCollapseTransition'] NCollapseTransition: typeof import('naive-ui')['NCollapseTransition']
NConfigProvider: typeof import('naive-ui')['NConfigProvider'] NConfigProvider: typeof import('naive-ui')['NConfigProvider']
NDivider: typeof import('naive-ui')['NDivider']
NEllipsis: typeof import('naive-ui')['NEllipsis'] NEllipsis: typeof import('naive-ui')['NEllipsis']
NForm: typeof import('naive-ui')['NForm'] NForm: typeof import('naive-ui')['NForm']
NFormItem: typeof import('naive-ui')['NFormItem'] NFormItem: typeof import('naive-ui')['NFormItem']
@ -144,6 +145,7 @@ declare module '@vue/runtime-core' {
NMenu: typeof import('naive-ui')['NMenu'] NMenu: typeof import('naive-ui')['NMenu']
NScrollbar: typeof import('naive-ui')['NScrollbar'] NScrollbar: typeof import('naive-ui')['NScrollbar']
NSlider: typeof import('naive-ui')['NSlider'] NSlider: typeof import('naive-ui')['NSlider']
NSpace: typeof import('naive-ui')['NSpace']
NSwitch: typeof import('naive-ui')['NSwitch'] NSwitch: typeof import('naive-ui')['NSwitch']
NumeronymGenerator: typeof import('./src/tools/numeronym-generator/numeronym-generator.vue')['default'] 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'] OtpCodeGeneratorAndValidator: typeof import('./src/tools/otp-code-generator-and-validator/otp-code-generator-and-validator.vue')['default']

View file

@ -6,19 +6,11 @@ describe('list-converter', () => {
describe('convert', () => { describe('convert', () => {
it('should convert a given list', () => { it('should convert a given list', () => {
const options: ConvertOptions = { const options: ConvertOptions = {
separator: ', ', itemsSeparator: ', ',
trimItems: true, trimItems: true,
removeDuplicates: true, removeDuplicates: true,
itemPrefix: '"', itemPrefix: '"',
itemSuffix: '"', itemSuffix: '"',
removeItemPrefix: '',
removeItemSuffix: '',
listPrefix: '',
listSuffix: '',
reverseList: false,
sortList: null,
lowerCase: false,
keepLineBreaks: false,
}; };
const input = ` const input = `
1 1
@ -33,38 +25,21 @@ describe('list-converter', () => {
it('should return an empty value for an empty input', () => { it('should return an empty value for an empty input', () => {
const options: ConvertOptions = { const options: ConvertOptions = {
separator: ', ', itemsSeparator: ', ',
trimItems: true, trimItems: true,
removeDuplicates: true, removeDuplicates: true,
itemPrefix: '',
itemSuffix: '',
removeItemPrefix: '',
removeItemSuffix: '',
listPrefix: '',
listSuffix: '',
reverseList: false,
sortList: null,
lowerCase: false,
keepLineBreaks: false,
}; };
expect(convert('', options)).toEqual(''); expect(convert('', options)).toEqual('');
}); });
it('should keep line breaks', () => { it('should keep line breaks', () => {
const options: ConvertOptions = { const options: ConvertOptions = {
separator: '',
trimItems: true, trimItems: true,
itemPrefix: '<li>', itemPrefix: '<li>',
itemSuffix: '</li>', itemSuffix: '</li>',
removeItemPrefix: '',
removeItemSuffix: '',
listPrefix: '<ul>', listPrefix: '<ul>',
listSuffix: '</ul>', listSuffix: '</ul>',
keepLineBreaks: true, keepLineBreaks: true,
lowerCase: false,
removeDuplicates: false,
reverseList: false,
sortList: null,
}; };
const input = ` const input = `
1 1
@ -81,30 +56,61 @@ describe('list-converter', () => {
it('should remove prefix and suffix', () => { it('should remove prefix and suffix', () => {
const options: ConvertOptions = { const options: ConvertOptions = {
separator: '',
trimItems: true, trimItems: true,
itemPrefix: '', removeItemPrefix: '<li>',
itemSuffix: '', removeItemSuffix: '</li>',
removeItemPrefix: '\<li\>',
removeItemSuffix: '\</li\>',
listPrefix: '',
listSuffix: '',
keepLineBreaks: true, keepLineBreaks: true,
lowerCase: false,
removeDuplicates: false,
reverseList: false,
sortList: null,
}; };
const input = ` const input = `
<li>1</li> <li>1</li>
<li>2</li> <li>2</li>
<li>3</li> <li>3</li>
`; `;
const expected = ` const expected = `1
1
2 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 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); expect(convert(input, options)).toEqual(expected);
}); });
}); });

View file

@ -4,7 +4,7 @@ import { byOrder } from '@/utils/array';
export { convert }; 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) => return (value: T) =>
condition ? fn(value) : value; condition ? fn(value) : value;
} }
@ -12,18 +12,20 @@ function whenever<T, R>(condition: boolean, fn: (value: T) => R) {
function convert(list: string, options: ConvertOptions): string { function convert(list: string, options: ConvertOptions): string {
const lineBreak = options.keepLineBreaks ? '\n' : ''; const lineBreak = options.keepLineBreaks ? '\n' : '';
const splitSep = options.splitBySeparator ? `${options.splitBySeparator}|` : '';
const splitRegExp = new RegExp(`(?:${splitSep}\\n)`, 'g');
return _.chain(list) return _.chain(list)
.thru(whenever(options.lowerCase, text => text.toLowerCase())) .thru(whenever(options.lowerCase, text => text.toLowerCase()))
.split('\n') .split(splitRegExp)
.thru(whenever(options.removeDuplicates, _.uniq)) .thru(whenever(options.removeDuplicates, _.uniq))
.thru(whenever(options.reverseList, _.reverse)) .thru(whenever(options.reverseList, _.reverse))
.thru(whenever(!_.isNull(options.sortList), parts => parts.sort(byOrder({ order: options.sortList }))))
.map(whenever(options.trimItems, _.trim)) .map(whenever(options.trimItems, _.trim))
.thru(whenever(!!options.sortList, parts => parts.sort(byOrder({ order: options.sortList }))))
.without('') .without('')
.map(p => options.removeItemPrefix ? p.replace(new RegExp(`^${options.removeItemPrefix}`, 'g'), '') : p) .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.removeItemSuffix ? p.replace(new RegExp(`${options.removeItemSuffix}$`, 'g'), '') : p)
.map(p => options.itemPrefix + p + options.itemSuffix) .map(p => (options.itemPrefix || '') + p + (options.itemSuffix || ''))
.join(options.separator + lineBreak) .join((options.itemsSeparator || '') + lineBreak)
.thru(text => [options.listPrefix, text, options.listSuffix].join(lineBreak)) .thru(text => [options.listPrefix, text, options.listSuffix].filter(l => l).join(lineBreak))
.value(); .value();
} }

View file

@ -1,17 +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 { export interface ConvertOptions {
lowerCase: boolean lowerCase?: boolean
trimItems: boolean trimItems?: boolean
itemPrefix: string itemPrefix?: string
itemSuffix: string itemSuffix?: string
removeItemPrefix: string removeItemPrefix?: string
removeItemSuffix: string removeItemSuffix?: string
listPrefix: string listPrefix?: string
listSuffix: string listSuffix?: string
reverseList: boolean reverseList?: boolean
sortList: SortOrder sortList?: SortOrder
removeDuplicates: boolean removeDuplicates?: boolean
separator: string itemsSeparator?: string
keepLineBreaks: boolean splitBySeparator?: string
keepLineBreaks?: boolean
} }

View file

@ -4,15 +4,41 @@ import { convert } from './list-converter.models';
import type { ConvertOptions } from './list-converter.types'; import type { ConvertOptions } from './list-converter.types';
const sortOrderOptions = [ const sortOrderOptions = [
{
label: 'No Sort',
value: null,
},
{ {
label: 'Sort ascending', label: 'Sort ascending',
value: 'asc', value: 'asc',
disabled: false,
}, },
{ {
label: 'Sort descending', label: 'Sort descending',
value: 'desc', 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',
}, },
]; ];
@ -29,7 +55,8 @@ const conversionConfig = useStorage<ConvertOptions>('list-converter:conversionCo
listSuffix: '', listSuffix: '',
reverseList: false, reverseList: false,
sortList: null, sortList: null,
separator: ', ', itemsSeparator: ', ',
splitBySeparator: '',
}); });
function transformer(value: string) { function transformer(value: string) {
@ -41,7 +68,7 @@ function transformer(value: string) {
<div style="flex: 0 0 100%"> <div style="flex: 0 0 100%">
<div style="margin: 0 auto; max-width: 600px"> <div style="margin: 0 auto; max-width: 600px">
<c-card> <c-card>
<div flex> <n-space>
<div> <div>
<n-form-item label="Trim list items" label-placement="left" label-width="150" :show-feedback="false" mb-2> <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" /> <n-switch v-model:value="conversionConfig.trimItems" />
@ -62,7 +89,7 @@ function transformer(value: string) {
<n-switch v-model:value="conversionConfig.keepLineBreaks" /> <n-switch v-model:value="conversionConfig.keepLineBreaks" />
</n-form-item> </n-form-item>
</div> </div>
<div flex-1> <div>
<c-select <c-select
v-model:value="conversionConfig.sortList" v-model:value="conversionConfig.sortList"
label="Sort list" label="Sort list"
@ -78,13 +105,23 @@ function transformer(value: string) {
/> />
<c-input-text <c-input-text
v-model:value="conversionConfig.separator" v-model:value="conversionConfig.itemsSeparator"
label="Separator" label="Items Separator"
label-position="left" label-position="left"
label-width="120px" label-width="120px"
label-align="right" label-align="right"
mb-2 mb-2
placeholder="," 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="Separator for splitting"
/> />
<n-form-item label="Unwrap item" label-placement="left" label-width="120" :show-feedback="false" mb-2> <n-form-item label="Unwrap item" label-placement="left" label-width="120" :show-feedback="false" mb-2>
@ -125,7 +162,7 @@ function transformer(value: string) {
/> />
</n-form-item> </n-form-item>
</div> </div>
</div> </n-space>
</c-card> </c-card>
</div> </div>
</div> </div>

25
src/utils/array.test.ts Normal file
View file

@ -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']);
});
});
});

View file

@ -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 (a: string, b: string) => {
return order === 'asc' ? a.localeCompare(b) : b.localeCompare(a); return order === 'asc' ? a.localeCompare(b) : b.localeCompare(a);
}; };