From 958a9b017c74adccc00113dc660a481b0142a1e1 Mon Sep 17 00:00:00 2001 From: SebastianMC <23032356+SebastianMC@users.noreply.github.com> Date: Thu, 25 Jan 2024 18:10:36 +0100 Subject: [PATCH] #127 - folder and file with the same (base) name advanced sorting support: - the last-resort default fallback sorting method (which was alphabetical) is extended to give preference to files. In other words, if all the specified sorting levels don't sort two items, the file goes first (if the other item is a folder) - added explicit syntax to specify `files-first` or `folders-first` --- src/custom-sort/custom-sort-types.ts | 6 +- src/custom-sort/custom-sort.spec.ts | 38 +++++++++++++ src/custom-sort/custom-sort.ts | 21 ++++++- .../sorting-spec-processor.spec.ts | 55 +++++++++++++++---- src/custom-sort/sorting-spec-processor.ts | 2 + 5 files changed, 109 insertions(+), 13 deletions(-) diff --git a/src/custom-sort/custom-sort-types.ts b/src/custom-sort/custom-sort-types.ts index 45e09ee..688cd4a 100644 --- a/src/custom-sort/custom-sort-types.ts +++ b/src/custom-sort/custom-sort-types.ts @@ -35,7 +35,11 @@ export enum CustomSortOrder { standardObsidian, // whatever user selected in the UI byBookmarkOrder, byBookmarkOrderReverse, - default = alphabetical + fileFirst, + folderFirst, + alphabeticalWithFilesPreferred, // When the (base)names are equal, the file has precedence over a folder + alphabeticalWithFoldersPreferred, // When the (base)names are equal, the file has precedence over a folder + default = alphabeticalWithFilesPreferred } export interface RecognizedOrderValue { diff --git a/src/custom-sort/custom-sort.spec.ts b/src/custom-sort/custom-sort.spec.ts index d0b5154..5b4cb5c 100644 --- a/src/custom-sort/custom-sort.spec.ts +++ b/src/custom-sort/custom-sort.spec.ts @@ -22,6 +22,9 @@ import { sorterByMetadataField, SorterFn } from './custom-sort'; +import { + _unitTests +} from './custom-sort' import { CustomSortGroupType, CustomSortOrder, @@ -2959,3 +2962,38 @@ describe('sorterByFolderCDate', () => { expect(normalizedResultR2).toBe(orderReverseRevParams) }) }) + +describe('fileGoesFirstWhenSameBasenameAsFolder', () => { + const file = 'file' + const folder = 'folder' + it.each([ + // main scenario - file goes first unconditionally before folder with the same name + [0, file, folder, -1, 1], + [0, folder, file, 1, -1], + // Not possible - two folders with the same name - the test only documents the behavior for clarity + [0, folder, folder, 0, 0], + // Realistic yet useless - two files with the same basename, + [0, file, file, 0, 0], + // Obvious cases - text compare returned !== 0, simply pass through + [1, file, file, 1, 1], + [1, file, folder, 1, 1], + [1, folder, file, 1, 1], + [1, folder, folder, 1, 1], + [-1, file, file, -1, -1], + [-1, file, folder, -1, -1], + [-1, folder, file, -1, -1], + [-1, folder, folder, -1, -1], + ])('text compare %s of %s %s gives %s (files first) and %s (folders first)', + (textCompare: number, aIsFolder: string, bIsFolder: string, filePreferredOder: number, folderPreferredOrder: number) => { + // given + const a: Partial = { isFolder: aIsFolder === folder } + const b: Partial = { isFolder: bIsFolder === folder } + + const resultFilePreferred: number = _unitTests.fileGoesFirstWhenSameBasenameAsFolder(textCompare, a as FolderItemForSorting, b as FolderItemForSorting) + const resultFolderPreferred: number = _unitTests.folderGoesFirstWhenSameBasenameAsFolder(textCompare, a as FolderItemForSorting, b as FolderItemForSorting) + + // then + expect(resultFilePreferred).toBe(filePreferredOder) + expect(resultFolderPreferred).toBe(folderPreferredOrder) + }) +}) diff --git a/src/custom-sort/custom-sort.ts b/src/custom-sort/custom-sort.ts index 08de1b0..8432da7 100644 --- a/src/custom-sort/custom-sort.ts +++ b/src/custom-sort/custom-sort.ts @@ -169,8 +169,18 @@ export const sorterByFolderMDate:(reverseOrder?: boolean) => SorterFn = (reverse } } +type FIFS = FolderItemForSorting + +const fileGoesFirstWhenSameBasenameAsFolder = (stringCompareResult: number, a: FIFS, b: FIFS) => + (!!stringCompareResult) ? stringCompareResult : (a.isFolder === b.isFolder ? EQUAL_OR_UNCOMPARABLE : (a.isFolder ? 1 : -1) ); + +const folderGoesFirstWhenSameBasenameAsFolder = (stringCompareResult: number, a: FIFS, b: FIFS) => + (!!stringCompareResult) ? stringCompareResult : (a.isFolder === b.isFolder ? EQUAL_OR_UNCOMPARABLE : (a.isFolder ? -1 : 1) ); + const Sorters: { [key in CustomSortOrder]: SorterFn } = { [CustomSortOrder.alphabetical]: (a: FolderItemForSorting, b: FolderItemForSorting) => CollatorCompare(a.sortString, b.sortString), + [CustomSortOrder.alphabeticalWithFilesPreferred]: (a: FIFS, b: FIFS) => fileGoesFirstWhenSameBasenameAsFolder(CollatorCompare(a.sortString, b.sortString),a,b), + [CustomSortOrder.alphabeticalWithFoldersPreferred]: (a: FIFS, b: FIFS) => fileGoesFirstWhenSameBasenameAsFolder(CollatorCompare(a.sortString, b.sortString),a,b), [CustomSortOrder.alphabeticalWithFileExt]: (a: FolderItemForSorting, b: FolderItemForSorting) => CollatorCompare(a.sortStringWithExt, b.sortStringWithExt), [CustomSortOrder.trueAlphabetical]: (a: FolderItemForSorting, b: FolderItemForSorting) => CollatorTrueAlphabeticalCompare(a.sortString, b.sortString), [CustomSortOrder.trueAlphabeticalWithFileExt]: (a: FolderItemForSorting, b: FolderItemForSorting) => CollatorTrueAlphabeticalCompare(a.sortStringWithExt, b.sortStringWithExt), @@ -192,9 +202,11 @@ const Sorters: { [key in CustomSortOrder]: SorterFn } = { [CustomSortOrder.byMetadataFieldTrueAlphabeticalReverse]: sorterByMetadataField(ReverseOrder, TrueAlphabetical, SortingLevelId.forPrimary), [CustomSortOrder.byBookmarkOrder]: sorterByBookmarkOrder(StraightOrder), [CustomSortOrder.byBookmarkOrderReverse]: sorterByBookmarkOrder(ReverseOrder), + [CustomSortOrder.fileFirst]: (a: FolderItemForSorting, b: FolderItemForSorting) => (a.isFolder && b.isFolder) ? EQUAL_OR_UNCOMPARABLE : (a.isFolder ? 1 : -1), + [CustomSortOrder.folderFirst]: (a: FolderItemForSorting, b: FolderItemForSorting) => (a.isFolder && b.isFolder) ? EQUAL_OR_UNCOMPARABLE : (a.isFolder ? -1 : 0), - // This is a fallback entry which should not be used - the getSorterFor() function below should protect against it - [CustomSortOrder.standardObsidian]: (a: FolderItemForSorting, b: FolderItemForSorting) => CollatorCompare(a.sortString, b.sortString), + // A fallback entry which should not be used - the getSorterFor() function below should protect against it + [CustomSortOrder.standardObsidian]: (a: FIFS, b: FIFS) => CollatorCompare(a.sortString, b.sortString), }; // Some sorters are different when used in primary vs. secondary sorting order @@ -729,3 +741,8 @@ export const sortFolderItemsForBookmarking = function (folder: TFolder, items: A return folderItems } }; + +export const _unitTests = { + fileGoesFirstWhenSameBasenameAsFolder: fileGoesFirstWhenSameBasenameAsFolder, + folderGoesFirstWhenSameBasenameAsFolder: folderGoesFirstWhenSameBasenameAsFolder +} diff --git a/src/custom-sort/sorting-spec-processor.spec.ts b/src/custom-sort/sorting-spec-processor.spec.ts index fde99c8..c0c793f 100644 --- a/src/custom-sort/sorting-spec-processor.spec.ts +++ b/src/custom-sort/sorting-spec-processor.spec.ts @@ -14,16 +14,8 @@ import { RomanNumberNormalizerFn, SortingSpecProcessor } from "./sorting-spec-processor" -import { - CustomSortGroupType, - CustomSortOrder, - CustomSortSpec, - IdentityNormalizerFn -} from "./custom-sort-types"; -import { - FolderMatchingRegexp, - FolderMatchingTreeNode -} from "./folder-matching-rules"; +import {CustomSortGroupType, CustomSortOrder, CustomSortSpec, IdentityNormalizerFn} from "./custom-sort-types"; +import {FolderMatchingRegexp, FolderMatchingTreeNode} from "./folder-matching-rules"; const txtInputExampleA: string = ` order-asc: a-z @@ -527,6 +519,49 @@ describe('SortingSpecProcessor', () => { }) }) +const txtInputFilesOrFoldersPreferred: string = ` +target-folder: AAA +< a-z, files-first +subitems1... + > folders-first, true a-z. +subitems2... + < created, folders-first +` + +const expectedSortSpecForFilesOrFoldersPreferred: { [key: string]: CustomSortSpec } = { + "AAA": { + defaultOrder: CustomSortOrder.alphabetical, + defaultSecondaryOrder: CustomSortOrder.fileFirst, + groups: [{ + exactPrefix: 'subitems1', + order: CustomSortOrder.folderFirst, + secondaryOrder: CustomSortOrder.trueAlphabeticalWithFileExt, + type: CustomSortGroupType.ExactPrefix + },{ + exactPrefix: 'subitems2', + order: CustomSortOrder.byCreatedTime, + secondaryOrder: CustomSortOrder.folderFirst, + type: CustomSortGroupType.ExactPrefix + }, { + type: CustomSortGroupType.Outsiders + }], + outsidersGroupIdx: 2, + targetFoldersPaths: ['AAA'] + } +} + +describe('SortingSpecProcessor', () => { + let processor: SortingSpecProcessor; + beforeEach(() => { + processor = new SortingSpecProcessor(); + }); + it('should recognize the files / folders preferred as a primary and secondary orders', () => { + const inputTxtArr: Array = txtInputFilesOrFoldersPreferred.split('\n') + const result = processor.parseSortSpecFromText(inputTxtArr, 'mock-folder', 'custom-name-note.md') + expect(result?.sortSpecByPath).toEqual(expectedSortSpecForFilesOrFoldersPreferred) + }) +}) + const txtInputTrueAlphabeticalSortAttr: string = ` target-folder: True Alpha < true a-z diff --git a/src/custom-sort/sorting-spec-processor.ts b/src/custom-sort/sorting-spec-processor.ts index 2109e28..19f9846 100644 --- a/src/custom-sort/sorting-spec-processor.ts +++ b/src/custom-sort/sorting-spec-processor.ts @@ -123,6 +123,8 @@ const OrderLiterals: { [key: string]: CustomSortOrderAscDescPair } = { 'standard': {asc: CustomSortOrder.standardObsidian, desc: CustomSortOrder.standardObsidian}, 'ui selected': {asc: CustomSortOrder.standardObsidian, desc: CustomSortOrder.standardObsidian}, 'by-bookmarks-order': {asc: CustomSortOrder.byBookmarkOrder, desc: CustomSortOrder.byBookmarkOrderReverse}, + 'files-first': {asc: CustomSortOrder.fileFirst, desc: CustomSortOrder.fileFirst}, + 'folders-first': {asc: CustomSortOrder.folderFirst, desc: CustomSortOrder.folderFirst} } const OrderByMetadataLexeme: string = 'by-metadata:'