mirror of
https://github.com/CorentinTh/it-tools.git
synced 2025-04-22 07:46:15 -04:00
feat: add split by separator + order by numeric + no sort
Fix #764 #1279 #1090 Small screen UI Fix
This commit is contained in:
parent
327ff11a59
commit
7bafd4790e
7 changed files with 168 additions and 72 deletions
2
components.d.ts
vendored
2
components.d.ts
vendored
|
@ -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']
|
||||||
|
|
|
@ -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);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -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();
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
25
src/utils/array.test.ts
Normal 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']);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -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);
|
||||||
};
|
};
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue