From 327ff11a595e736d820e26770fda2abd42c2d2e5 Mon Sep 17 00:00:00 2001 From: ShareVB Date: Sat, 21 Sep 2024 14:55:23 +0200 Subject: [PATCH 1/2] feat(List Converter): add remove prefix/suffix capability Fix #702 --- .../list-converter.models.test.ts | 35 +++++++++++++++++++ .../list-converter/list-converter.models.ts | 2 ++ .../list-converter/list-converter.types.ts | 2 ++ src/tools/list-converter/list-converter.vue | 15 ++++++++ 4 files changed, 54 insertions(+) diff --git a/src/tools/list-converter/list-converter.models.test.ts b/src/tools/list-converter/list-converter.models.test.ts index abbc43c2..d6c87773 100644 --- a/src/tools/list-converter/list-converter.models.test.ts +++ b/src/tools/list-converter/list-converter.models.test.ts @@ -11,6 +11,8 @@ describe('list-converter', () => { removeDuplicates: true, itemPrefix: '"', itemSuffix: '"', + removeItemPrefix: '', + removeItemSuffix: '', listPrefix: '', listSuffix: '', reverseList: false, @@ -36,6 +38,8 @@ describe('list-converter', () => { removeDuplicates: true, itemPrefix: '', itemSuffix: '', + removeItemPrefix: '', + removeItemSuffix: '', listPrefix: '', listSuffix: '', reverseList: false, @@ -52,6 +56,8 @@ describe('list-converter', () => { trimItems: true, itemPrefix: '
  • ', itemSuffix: '
  • ', + removeItemPrefix: '', + removeItemSuffix: '', listPrefix: '', keepLineBreaks: true, @@ -72,5 +78,34 @@ describe('list-converter', () => { `; expect(convert(input, options)).toEqual(expected); }); + + it('should remove prefix and suffix', () => { + const options: ConvertOptions = { + separator: '', + trimItems: true, + itemPrefix: '', + itemSuffix: '', + removeItemPrefix: '\', + removeItemSuffix: '\', + listPrefix: '', + listSuffix: '', + keepLineBreaks: true, + lowerCase: false, + removeDuplicates: false, + reverseList: false, + sortList: null, + }; + const input = ` +
  • 1
  • +
  • 2
  • +
  • 3
  • + `; + const expected = ` +1 +2 +3 +`; + expect(convert(input, options)).toEqual(expected); + }); }); }); diff --git a/src/tools/list-converter/list-converter.models.ts b/src/tools/list-converter/list-converter.models.ts index 548baa28..5c047293 100644 --- a/src/tools/list-converter/list-converter.models.ts +++ b/src/tools/list-converter/list-converter.models.ts @@ -20,6 +20,8 @@ function convert(list: string, options: ConvertOptions): string { .thru(whenever(!_.isNull(options.sortList), parts => parts.sort(byOrder({ order: options.sortList })))) .map(whenever(options.trimItems, _.trim)) .without('') + .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.separator + lineBreak) .thru(text => [options.listPrefix, text, options.listSuffix].join(lineBreak)) diff --git a/src/tools/list-converter/list-converter.types.ts b/src/tools/list-converter/list-converter.types.ts index 3f9ea3f8..448f1a5a 100644 --- a/src/tools/list-converter/list-converter.types.ts +++ b/src/tools/list-converter/list-converter.types.ts @@ -5,6 +5,8 @@ export interface ConvertOptions { trimItems: boolean itemPrefix: string itemSuffix: string + removeItemPrefix: string + removeItemSuffix: string listPrefix: string listSuffix: string reverseList: boolean diff --git a/src/tools/list-converter/list-converter.vue b/src/tools/list-converter/list-converter.vue index 19dd30e5..58c58188 100644 --- a/src/tools/list-converter/list-converter.vue +++ b/src/tools/list-converter/list-converter.vue @@ -23,6 +23,8 @@ const conversionConfig = useStorage('list-converter:conversionCo keepLineBreaks: false, itemPrefix: '', itemSuffix: '', + removeItemPrefix: '', + removeItemSuffix: '', listPrefix: '', listSuffix: '', reverseList: false, @@ -85,6 +87,19 @@ function transformer(value: string) { placeholder="," /> + + + + + Date: Sat, 21 Sep 2024 16:18:05 +0200 Subject: [PATCH 2/2] feat: add split by separator + order by numeric + no sort Fix #764 #1279 #1090 Small screen UI Fix --- components.d.ts | 2 + .../list-converter.models.test.ts | 88 ++++++++++--------- .../list-converter/list-converter.models.ts | 14 +-- .../list-converter/list-converter.types.ts | 29 +++--- src/tools/list-converter/list-converter.vue | 55 ++++++++++-- src/utils/array.test.ts | 25 ++++++ src/utils/array.ts | 27 +++++- 7 files changed, 168 insertions(+), 72 deletions(-) create mode 100644 src/utils/array.test.ts diff --git a/components.d.ts b/components.d.ts index 89f41f80..f1a03072 100644 --- a/components.d.ts +++ b/components.d.ts @@ -132,6 +132,7 @@ declare module '@vue/runtime-core' { NCode: typeof import('naive-ui')['NCode'] NCollapseTransition: typeof import('naive-ui')['NCollapseTransition'] NConfigProvider: typeof import('naive-ui')['NConfigProvider'] + NDivider: typeof import('naive-ui')['NDivider'] NEllipsis: typeof import('naive-ui')['NEllipsis'] NForm: typeof import('naive-ui')['NForm'] NFormItem: typeof import('naive-ui')['NFormItem'] @@ -144,6 +145,7 @@ declare module '@vue/runtime-core' { NMenu: typeof import('naive-ui')['NMenu'] NScrollbar: typeof import('naive-ui')['NScrollbar'] NSlider: typeof import('naive-ui')['NSlider'] + NSpace: typeof import('naive-ui')['NSpace'] 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'] diff --git a/src/tools/list-converter/list-converter.models.test.ts b/src/tools/list-converter/list-converter.models.test.ts index d6c87773..6229c5c7 100644 --- a/src/tools/list-converter/list-converter.models.test.ts +++ b/src/tools/list-converter/list-converter.models.test.ts @@ -6,19 +6,11 @@ describe('list-converter', () => { describe('convert', () => { it('should convert a given list', () => { const options: ConvertOptions = { - separator: ', ', + itemsSeparator: ', ', trimItems: true, removeDuplicates: true, itemPrefix: '"', itemSuffix: '"', - removeItemPrefix: '', - removeItemSuffix: '', - listPrefix: '', - listSuffix: '', - reverseList: false, - sortList: null, - lowerCase: false, - keepLineBreaks: false, }; const input = ` 1 @@ -33,38 +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: '', - removeItemPrefix: '', - removeItemSuffix: '', - 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: '
  • ', itemSuffix: '
  • ', - removeItemPrefix: '', - removeItemSuffix: '', listPrefix: '
      ', listSuffix: '
    ', keepLineBreaks: true, - lowerCase: false, - removeDuplicates: false, - reverseList: false, - sortList: null, }; const input = ` 1 @@ -81,30 +56,61 @@ describe('list-converter', () => { it('should remove prefix and suffix', () => { const options: ConvertOptions = { - separator: '', trimItems: true, - itemPrefix: '', - itemSuffix: '', - removeItemPrefix: '\', - removeItemSuffix: '\', - listPrefix: '', - listSuffix: '', + removeItemPrefix: '
  • ', + removeItemSuffix: '
  • ', keepLineBreaks: true, - lowerCase: false, - removeDuplicates: false, - reverseList: false, - sortList: null, }; const input = `
  • 1
  • 2
  • 3
  • `; - const expected = ` -1 + 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); }); }); diff --git a/src/tools/list-converter/list-converter.models.ts b/src/tools/list-converter/list-converter.models.ts index 5c047293..e76c9ed4 100644 --- a/src/tools/list-converter/list-converter.models.ts +++ b/src/tools/list-converter/list-converter.models.ts @@ -4,7 +4,7 @@ import { byOrder } from '@/utils/array'; export { convert }; -function whenever(condition: boolean, fn: (value: T) => R) { +function whenever(condition: boolean | undefined, fn: (value: T) => R) { return (value: T) => condition ? fn(value) : value; } @@ -12,18 +12,20 @@ function whenever(condition: boolean, fn: (value: T) => R) { 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.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.separator + lineBreak) - .thru(text => [options.listPrefix, text, options.listSuffix].join(lineBreak)) + .map(p => (options.itemPrefix || '') + p + (options.itemSuffix || '')) + .join((options.itemsSeparator || '') + lineBreak) + .thru(text => [options.listPrefix, text, options.listSuffix].filter(l => l).join(lineBreak)) .value(); } diff --git a/src/tools/list-converter/list-converter.types.ts b/src/tools/list-converter/list-converter.types.ts index 448f1a5a..7487f1e7 100644 --- a/src/tools/list-converter/list-converter.types.ts +++ b/src/tools/list-converter/list-converter.types.ts @@ -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 { - lowerCase: boolean - trimItems: boolean - itemPrefix: string - itemSuffix: string - removeItemPrefix: string - removeItemSuffix: 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 } diff --git a/src/tools/list-converter/list-converter.vue b/src/tools/list-converter/list-converter.vue index 58c58188..8e27f304 100644 --- a/src/tools/list-converter/list-converter.vue +++ b/src/tools/list-converter/list-converter.vue @@ -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', }, ]; @@ -29,7 +55,8 @@ const conversionConfig = useStorage('list-converter:conversionCo listSuffix: '', reverseList: false, sortList: null, - separator: ', ', + itemsSeparator: ', ', + splitBySeparator: '', }); function transformer(value: string) { @@ -41,7 +68,7 @@ function transformer(value: string) {
    -
    +
    @@ -62,7 +89,7 @@ function transformer(value: string) {
    -
    +
    + + @@ -125,7 +162,7 @@ function transformer(value: string) { />
    -
    +
    diff --git a/src/utils/array.test.ts b/src/utils/array.test.ts new file mode 100644 index 00000000..a521c2fd --- /dev/null +++ b/src/utils/array.test.ts @@ -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']); + }); + }); +}); diff --git a/src/utils/array.ts b/src/utils/array.ts index 15b3506d..e9a875c8 100644 --- a/src/utils/array.ts +++ b/src/utils/array.ts @@ -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); };