diff --git a/README.md b/README.md index 0c07eeb..6733b47 100644 --- a/README.md +++ b/README.md @@ -355,12 +355,13 @@ sorting-spec: | the result is: -![Book - Roman compond suffixes](./docs/svg/roman-suffix.svg) +![Book - Roman compound suffixes](./docs/svg/roman-suffix.svg) ### Example 12: Apply same sorting to all folders in the vault -Apply the alphabetical sorting to all folders in the Vault. The alphabetical sorting treats the folders and files equally -(which is different from the standard Obsidian sort, which groups folders in the top of File Explorer) +Apply the same advanced modified date sorting to all folders in the Vault. The advanced modified sorting treats the folders + and files equally (which is different from the standard Obsidian sort, which groups folders in the top of File Explorer) + The modified date for a folder is derived from its newest direct child file (if any), otherwise a folder is considered old This involves the wildcard suffix syntax `*` which means _apply the sorting rule to the specified folder and all of its subfolders, including descendants. In other words, this is imposing a deep inheritance @@ -371,7 +372,7 @@ Applying the wildcard suffix to root folder path `/*` actually means _apply the --- sorting-spec: | target-folder: /* - < a-z + > advanced modified --- ``` diff --git a/docs/syntax-reference.md b/docs/syntax-reference.md index da2dd93..6f465fc 100644 --- a/docs/syntax-reference.md +++ b/docs/syntax-reference.md @@ -1,3 +1,45 @@ -Yet to be filled with content ;-) +> Document is partial, creation in progress +> Please refer to [README.md](../../README.md) for usage examples +> Check [manual.md](), maybe that file has already some content? -Check [manual.md](), maybe that file has already some content? +### Supported sorting methods + +#### At folder level only + +- `sorting: standard` - gives back the control on order of items in hands of standard Obsidian mechanisms (UI driven). + Typical (and intended) use: exclude a folder (or folders subtree) from a custom sorting resulting from wilcard-based target folder rule + +#### At folder and group level + +- `< a-z` - alphabetical +- `> a-z` - alphabetical reverse, aka alphabetical descending, 'z' goes before 'a' +- `< modified` - by modified time, the long untouched item goes first (modified time of folder is assumed the beginning of the world, so folders go first and alphabetical) +- `> modified` - by modified time reverse, the most recently modified item goes first (modified time of folder is assumed the beginning of the world, so folders land in the bottom and alphabetical) +- `< created` - by created time, the oldest item goes first (modified time of folder is assumed the beginning of the world, so folders go first and alphabetical) +- `> created` - by created time reverse, the newest item goes first (modified time of folder is assumed the beginning of the world, so folders land in the bottom and alphabetical) +- `< advanced modified` - by modified time, the long untouched item goes first. For folders, their modification date is derived from the most recently modified direct child file. + For extremely large vaults use with caution, as the sorting needs to scan all files inside a folder to determine the folder's modified date +- `> advanced modified` - by modified time reverse, the most recently modified item goes first. For folders, their modification date is derived from the most recently modified direct child file. + For extremely large vaults use with caution, as the sorting needs to scan all files inside a folder to determine the folder's modified date +- `< advanced created` - by created time, the oldest item goes first. For folders, their creation date is derived from the oldest direct child file. + For extremely large vaults use with caution, as the sorting needs to scan all files inside a folder to determine the folder's created date +- `> advanced created` - by created time reverse, the newest item goes first. For folders, their creation date is derived from the newest direct child file. + For extremely large vaults use with caution, as the sorting needs to scan all files inside a folder to determine the folder's created date + +#### At group level only (aka secondary sorting rule) + +> Only applicable in edge cases based on numerical symbols, when the regex-based match is equal for more than one item + and need to apply a secondary order on same matches. + +- `< a-z, created` +- `> a-z, created` +- `< a-z, created desc` +- `> a-z, created desc` +- `< a-z, modified` +- `> a-z, modified` +- `< a-z, modified desc` +- `> a-z, modified desc` +- `< a-z, advanced modified` +- `> a-z, advanced modified` +- `< a-z, advanced modified desc` +- `> a-z, advanced modified desc` diff --git a/src/custom-sort/custom-sort-types.ts b/src/custom-sort/custom-sort-types.ts index ed79c19..8a65ab0 100644 --- a/src/custom-sort/custom-sort-types.ts +++ b/src/custom-sort/custom-sort-types.ts @@ -10,10 +10,14 @@ export enum CustomSortGroupType { export enum CustomSortOrder { alphabetical = 1, // = 1 to allow: if (customSortOrder) { ... alphabeticalReverse, - byModifiedTime, - byModifiedTimeReverse, - byCreatedTime, + byModifiedTime, // New to old + byModifiedTimeAdvanced, + byModifiedTimeReverse, // Old to new + byModifiedTimeReverseAdvanced, + byCreatedTime, // New to old + byCreatedTimeAdvanced, byCreatedTimeReverse, + byCreatedTimeReverseAdvanced, standardObsidian, // Let the folder sorting be in hands of Obsidian, whatever user selected in the UI default = alphabetical } diff --git a/src/custom-sort/custom-sort.spec.ts b/src/custom-sort/custom-sort.spec.ts index e70c65f..e0f1d88 100644 --- a/src/custom-sort/custom-sort.spec.ts +++ b/src/custom-sort/custom-sort.spec.ts @@ -1,6 +1,11 @@ import {TFile, TFolder, Vault} from 'obsidian'; -import {determineSortingGroup} from './custom-sort'; -import {CustomSortGroupType, CustomSortSpec} from './custom-sort-types'; +import { + DEFAULT_FOLDER_CTIME, + determineFolderDatesIfNeeded, + determineSortingGroup, + FolderItemForSorting +} from './custom-sort'; +import {CustomSortGroupType, CustomSortOrder, CustomSortSpec} from './custom-sort-types'; import {CompoundDashNumberNormalizerFn, CompoundDotRomanNumberNormalizerFn} from "./sorting-spec-processor"; const mockTFile = (basename: string, ext: string, size?: number, ctime?: number, mtime?: number): TFile => { @@ -19,7 +24,31 @@ const mockTFile = (basename: string, ext: string, size?: number, ctime?: number, } } +const mockTFolder = (name: string, children?: Array, parent?: TFolder): TFolder => { + return { + isRoot(): boolean { return name === '/' }, + vault: {} as Vault, // To satisfy TS typechecking + path: `/${name}`, + name: name, + parent: parent ?? ({} as TFolder), // To satisfy TS typechecking + children: children ?? [] + } +} + const MOCK_TIMESTAMP: number = 1656417542418 +const TIMESTAMP_OLDEST: number = MOCK_TIMESTAMP +const TIMESTAMP_NEWEST: number = MOCK_TIMESTAMP + 1000 +const TIMESTAMP_INBETWEEN: number = MOCK_TIMESTAMP + 500 + +const mockTFolderWithChildren = (name: string): TFolder => { + const child1: TFolder = mockTFolder('Section A') + const child2: TFolder = mockTFolder('Section B') + const child3: TFile = mockTFile('Child file 1 created as oldest, modified recently', 'md', 100, TIMESTAMP_OLDEST, TIMESTAMP_NEWEST) + const child4: TFile = mockTFile('Child file 2 created as newest, not modified at all', 'md', 100, TIMESTAMP_NEWEST, TIMESTAMP_NEWEST) + const child5: TFile = mockTFile('Child file 3 created inbetween, modified inbetween', 'md', 100, TIMESTAMP_INBETWEEN, TIMESTAMP_INBETWEEN) + + return mockTFolder('Mock parent folder', [child1, child2, child3, child4, child5]) +} describe('determineSortingGroup', () => { describe('CustomSortGroupType.ExactHeadAndTail', () => { @@ -43,7 +72,8 @@ describe('determineSortingGroup', () => { groupIdx: 0, isFolder: false, sortString: "References.md", - ctime: MOCK_TIMESTAMP + 222, + ctimeNewest: MOCK_TIMESTAMP + 222, + ctimeOldest: MOCK_TIMESTAMP + 222, mtime: MOCK_TIMESTAMP + 333, path: 'Some parent folder/References.md' }); @@ -68,7 +98,8 @@ describe('determineSortingGroup', () => { groupIdx: 1, // This indicates the last+1 idx isFolder: false, sortString: "References.md", - ctime: MOCK_TIMESTAMP + 555, + ctimeNewest: MOCK_TIMESTAMP + 555, + ctimeOldest: MOCK_TIMESTAMP + 555, mtime: MOCK_TIMESTAMP + 666, path: 'Some parent folder/References.md' }); @@ -96,7 +127,8 @@ describe('determineSortingGroup', () => { groupIdx: 1, // This indicates the last+1 idx isFolder: false, sortString: "Part123:-icle.md", - ctime: MOCK_TIMESTAMP + 555, + ctimeNewest: MOCK_TIMESTAMP + 555, + ctimeOldest: MOCK_TIMESTAMP + 555, mtime: MOCK_TIMESTAMP + 666, path: 'Some parent folder/Part123:-icle.md' }); @@ -125,7 +157,8 @@ describe('determineSortingGroup', () => { isFolder: false, sortString: "00000123////Part123:-icle.md", matchGroup: '00000123//', - ctime: MOCK_TIMESTAMP + 555, + ctimeNewest: MOCK_TIMESTAMP + 555, + ctimeOldest: MOCK_TIMESTAMP + 555, mtime: MOCK_TIMESTAMP + 666, path: 'Some parent folder/Part123:-icle.md' }); @@ -153,7 +186,8 @@ describe('determineSortingGroup', () => { groupIdx: 1, // This indicates the last+1 idx isFolder: false, sortString: "Part:123-icle.md", - ctime: MOCK_TIMESTAMP + 555, + ctimeNewest: MOCK_TIMESTAMP + 555, + ctimeOldest: MOCK_TIMESTAMP + 555, mtime: MOCK_TIMESTAMP + 666, path: 'Some parent folder/Part:123-icle.md' }); @@ -182,7 +216,8 @@ describe('determineSortingGroup', () => { isFolder: false, sortString: "00000123////Part:123-icle.md", matchGroup: '00000123//', - ctime: MOCK_TIMESTAMP + 555, + ctimeNewest: MOCK_TIMESTAMP + 555, + ctimeOldest: MOCK_TIMESTAMP + 555, mtime: MOCK_TIMESTAMP + 666, path: 'Some parent folder/Part:123-icle.md' }); @@ -208,7 +243,8 @@ describe('determineSortingGroup', () => { groupIdx: 0, isFolder: false, sortString: "References.md", - ctime: MOCK_TIMESTAMP + 222, + ctimeNewest: MOCK_TIMESTAMP + 222, + ctimeOldest: MOCK_TIMESTAMP + 222, mtime: MOCK_TIMESTAMP + 333, path: 'Some parent folder/References.md' }); @@ -236,7 +272,8 @@ describe('determineSortingGroup', () => { isFolder: false, sortString: '00000001|00000030|00000006|00001900////Reference i.xxx.vi.mcm.md', matchGroup: "00000001|00000030|00000006|00001900//", - ctime: MOCK_TIMESTAMP + 222, + ctimeNewest: MOCK_TIMESTAMP + 222, + ctimeOldest: MOCK_TIMESTAMP + 222, mtime: MOCK_TIMESTAMP + 333, path: 'Some parent folder/Reference i.xxx.vi.mcm.md' }); @@ -260,10 +297,83 @@ describe('determineSortingGroup', () => { groupIdx: 1, // This indicates the last+1 idx isFolder: false, sortString: "References.md", - ctime: MOCK_TIMESTAMP + 222, + ctimeNewest: MOCK_TIMESTAMP + 222, + ctimeOldest: MOCK_TIMESTAMP + 222, mtime: MOCK_TIMESTAMP + 333, path: 'Some parent folder/References.md' }); }) }) }) + +describe('determineFolderDatesIfNeeded', () => { + it('should not be triggered if not needed - sorting method does not require it', () => { + // given + const folder: TFolder = mockTFolderWithChildren('Test folder 1') + const OUTSIDERS_GROUP_IDX = 0 + const sortSpec: CustomSortSpec = { + targetFoldersPaths: ['/'], + groups: [{ + type: CustomSortGroupType.Outsiders, + order: CustomSortOrder.alphabetical + }], + outsidersGroupIdx: OUTSIDERS_GROUP_IDX + } + const cardinality = {[OUTSIDERS_GROUP_IDX]: 10} // Group 0 contains 10 items + + // when + const result: FolderItemForSorting = determineSortingGroup(folder, sortSpec) + determineFolderDatesIfNeeded([result], sortSpec, cardinality) + + // then + expect(result.ctimeOldest).toEqual(DEFAULT_FOLDER_CTIME) + expect(result.ctimeNewest).toEqual(DEFAULT_FOLDER_CTIME) + expect(result.mtime).toEqual(DEFAULT_FOLDER_CTIME) + }) + it('should not be triggered if not needed - the folder is an only item', () => { + // given + const folder: TFolder = mockTFolderWithChildren('Test folder 1') + const OUTSIDERS_GROUP_IDX = 0 + const sortSpec: CustomSortSpec = { + targetFoldersPaths: ['/'], + groups: [{ + type: CustomSortGroupType.Outsiders, + order: CustomSortOrder.byModifiedTimeAdvanced + }], + outsidersGroupIdx: OUTSIDERS_GROUP_IDX + } + const cardinality = {[OUTSIDERS_GROUP_IDX]: 1} // Group 0 contains the folder alone + + // when + const result: FolderItemForSorting = determineSortingGroup(folder, sortSpec) + determineFolderDatesIfNeeded([result], sortSpec, cardinality) + + // then + expect(result.ctimeOldest).toEqual(DEFAULT_FOLDER_CTIME) + expect(result.ctimeNewest).toEqual(DEFAULT_FOLDER_CTIME) + expect(result.mtime).toEqual(DEFAULT_FOLDER_CTIME) + }) + it('should correctly determine dates, if triggered', () => { + // given + const folder: TFolder = mockTFolderWithChildren('Test folder 1') + const OUTSIDERS_GROUP_IDX = 0 + const sortSpec: CustomSortSpec = { + targetFoldersPaths: ['/'], + groups: [{ + type: CustomSortGroupType.Outsiders, + order: CustomSortOrder.byCreatedTimeReverseAdvanced + }], + outsidersGroupIdx: OUTSIDERS_GROUP_IDX + } + const cardinality = {[OUTSIDERS_GROUP_IDX]: 10} // Group 0 contains 10 items + + // when + const result: FolderItemForSorting = determineSortingGroup(folder, sortSpec) + determineFolderDatesIfNeeded([result], sortSpec, cardinality) + + // then + expect(result.ctimeOldest).toEqual(TIMESTAMP_OLDEST) + expect(result.ctimeNewest).toEqual(TIMESTAMP_NEWEST) + expect(result.mtime).toEqual(TIMESTAMP_NEWEST) + }) +}) diff --git a/src/custom-sort/custom-sort.ts b/src/custom-sort/custom-sort.ts index 460de86..69578be 100644 --- a/src/custom-sort/custom-sort.ts +++ b/src/custom-sort/custom-sort.ts @@ -1,4 +1,4 @@ -import {requireApiVersion, TFile, TFolder} from 'obsidian'; +import {requireApiVersion, TAbstractFile, TFile, TFolder} from 'obsidian'; import {CustomSortGroup, CustomSortGroupType, CustomSortOrder, CustomSortSpec} from "./custom-sort-types"; import {isDefined} from "../utils/utils"; @@ -8,14 +8,16 @@ let Collator = new Intl.Collator(undefined, { numeric: true, }).compare; -interface FolderItemForSorting { +export interface FolderItemForSorting { path: string groupIdx?: number // the index itself represents order for groups sortString: string // fragment (or full name) to be used for sorting matchGroup?: string // advanced - used for secondary sorting rule, to recognize 'same regex match' - ctime: number + ctimeOldest: number // for a file, both ctime values are the same. For folder they can be different: + ctimeNewest: number // ctimeOldest = ctime of oldest child file, ctimeNewest = ctime of newest child file mtime: number isFolder: boolean + folder?: TFolder } type SorterFn = (a: FolderItemForSorting, b: FolderItemForSorting) => number @@ -23,10 +25,14 @@ type SorterFn = (a: FolderItemForSorting, b: FolderItemForSorting) => number let Sorters: { [key in CustomSortOrder]: SorterFn } = { [CustomSortOrder.alphabetical]: (a: FolderItemForSorting, b: FolderItemForSorting) => Collator(a.sortString, b.sortString), [CustomSortOrder.alphabeticalReverse]: (a: FolderItemForSorting, b: FolderItemForSorting) => Collator(b.sortString, a.sortString), - [CustomSortOrder.byModifiedTime]: (a: FolderItemForSorting, b: FolderItemForSorting) => a.mtime - b.mtime, - [CustomSortOrder.byModifiedTimeReverse]: (a: FolderItemForSorting, b: FolderItemForSorting) => b.mtime - a.mtime, - [CustomSortOrder.byCreatedTime]: (a: FolderItemForSorting, b: FolderItemForSorting) => a.ctime - b.ctime, - [CustomSortOrder.byCreatedTimeReverse]: (a: FolderItemForSorting, b: FolderItemForSorting) => b.ctime - a.ctime, + [CustomSortOrder.byModifiedTime]: (a: FolderItemForSorting, b: FolderItemForSorting) => (a.isFolder && b.isFolder) ? Collator(a.sortString, b.sortString) : (a.mtime - b.mtime), + [CustomSortOrder.byModifiedTimeAdvanced]: (a: FolderItemForSorting, b: FolderItemForSorting) => a.mtime - b.mtime, + [CustomSortOrder.byModifiedTimeReverse]: (a: FolderItemForSorting, b: FolderItemForSorting) => (a.isFolder && b.isFolder) ? Collator(a.sortString, b.sortString) : (b.mtime - a.mtime), + [CustomSortOrder.byModifiedTimeReverseAdvanced]: (a: FolderItemForSorting, b: FolderItemForSorting) => b.mtime - a.mtime, + [CustomSortOrder.byCreatedTime]: (a: FolderItemForSorting, b: FolderItemForSorting) => (a.isFolder && b.isFolder) ? Collator(a.sortString, b.sortString) : (a.ctimeNewest - b.ctimeNewest), + [CustomSortOrder.byCreatedTimeAdvanced]: (a: FolderItemForSorting, b: FolderItemForSorting) => a.ctimeNewest - b.ctimeNewest, + [CustomSortOrder.byCreatedTimeReverse]: (a: FolderItemForSorting, b: FolderItemForSorting) => (a.isFolder && b.isFolder) ? Collator(a.sortString, b.sortString) : (b.ctimeOldest - a.ctimeOldest), + [CustomSortOrder.byCreatedTimeReverseAdvanced]: (a: FolderItemForSorting, b: FolderItemForSorting) => b.ctimeOldest - a.ctimeOldest, // This is a fallback entry which should not be used - the plugin code should refrain from custom sorting at all [CustomSortOrder.standardObsidian]: (a: FolderItemForSorting, b: FolderItemForSorting) => Collator(a.sortString, b.sortString), @@ -52,11 +58,14 @@ function compareTwoItems(itA: FolderItemForSorting, itB: FolderItemForSorting, s } } -const isFolder = (entry: TFile | TFolder) => { +const isFolder = (entry: TAbstractFile) => { // The plain obvious 'entry instanceof TFolder' doesn't work inside Jest unit tests, hence a workaround below return !!((entry as any).isRoot); } +export const DEFAULT_FOLDER_MTIME: number = 0 +export const DEFAULT_FOLDER_CTIME: number = 0 + export const determineSortingGroup = function (entry: TFile | TFolder, spec: CustomSortSpec): FolderItemForSorting { let groupIdx: number let determined: boolean = false @@ -165,14 +174,71 @@ export const determineSortingGroup = function (entry: TFile | TFolder, spec: Cus sortString: matchedGroup ? (matchedGroup + '//' + entry.name) : entry.name, matchGroup: matchedGroup ?? undefined, isFolder: aFolder, + folder: aFolder ? (entry as TFolder) : undefined, path: entry.path, - ctime: aFile ? entryAsTFile.stat.ctime : 0, - mtime: aFile ? entryAsTFile.stat.mtime : 0 + ctimeNewest: aFile ? entryAsTFile.stat.ctime : DEFAULT_FOLDER_CTIME, + ctimeOldest: aFile ? entryAsTFile.stat.ctime : DEFAULT_FOLDER_CTIME, + mtime: aFile ? entryAsTFile.stat.mtime : DEFAULT_FOLDER_MTIME } } +const SortOrderRequiringFolderDate = new Set([ + CustomSortOrder.byModifiedTimeAdvanced, + CustomSortOrder.byModifiedTimeReverseAdvanced, + CustomSortOrder.byCreatedTimeAdvanced, + CustomSortOrder.byCreatedTimeReverseAdvanced +]) + +export const sortOrderNeedsFolderDates = (order: CustomSortOrder | undefined, secondary?: CustomSortOrder): boolean => { + // The CustomSortOrder.standardObsidian used as default because it doesn't require date on folders + return SortOrderRequiringFolderDate.has(order ?? CustomSortOrder.standardObsidian) + || SortOrderRequiringFolderDate.has(secondary ?? CustomSortOrder.standardObsidian) +} + +// Syntax sugar for readability +export type ModifiedTime = number +export type CreatedTimeNewest = number +export type CreatedTimeOldest = number + +export const determineDatesForFolder = (folder: TFolder, now: number): [ModifiedTime, CreatedTimeNewest, CreatedTimeOldest] => { + let mtimeOfFolder: ModifiedTime = DEFAULT_FOLDER_MTIME + let ctimeNewestOfFolder: CreatedTimeNewest = DEFAULT_FOLDER_CTIME + let ctimeOldestOfFolder: CreatedTimeOldest = now + folder.children.forEach((item) => { + if (!isFolder(item)) { + const file: TFile = item as TFile + if (file.stat.mtime > mtimeOfFolder) { + mtimeOfFolder = file.stat.mtime + } + if (file.stat.ctime > ctimeNewestOfFolder) { + ctimeNewestOfFolder = file.stat.ctime + } + if (file.stat.ctime < ctimeOldestOfFolder) { + ctimeOldestOfFolder = file.stat.ctime + } + } + }) + return [mtimeOfFolder, ctimeNewestOfFolder, ctimeOldestOfFolder] +} + +export const determineFolderDatesIfNeeded = (folderItems: Array, sortingSpec: CustomSortSpec, sortingGroupsCardinality: {[key: number]: number} = {}) => { + const Now: number = Date.now() + folderItems.forEach((item) => { + const groupIdx: number | undefined = item.groupIdx + if (groupIdx !== undefined && sortingGroupsCardinality[groupIdx] > 1) { + const groupOrder: CustomSortOrder | undefined = sortingSpec.groups[groupIdx].order + if (sortOrderNeedsFolderDates(groupOrder)) { + if (item.folder) { + [item.mtime, item.ctimeNewest, item.ctimeOldest] = determineDatesForFolder(item.folder, Now) + } + } + } + }) +} + export const folderSort = function (sortingSpec: CustomSortSpec, order: string[]) { let fileExplorer = this.fileExplorer + const sortingGroupsCardinality: {[key: number]: number} = {} const folderItems: Array = (sortingSpec.itemsToHide ? this.file.children.filter((entry: TFile | TFolder) => { @@ -180,9 +246,17 @@ export const folderSort = function (sortingSpec: CustomSortSpec, order: string[] }) : this.file.children) - .map((entry: TFile | TFolder) => - determineSortingGroup(entry, sortingSpec) - ) + .map((entry: TFile | TFolder) => { + const itemForSorting: FolderItemForSorting = determineSortingGroup(entry, sortingSpec) + const groupIdx: number | undefined = itemForSorting.groupIdx + if (groupIdx !== undefined) { + sortingGroupsCardinality[groupIdx] = 1 + (sortingGroupsCardinality[groupIdx] ?? 0) + } + return itemForSorting + }) + + // Finally, for advanced sorting by modified date, for some of the folders the modified date has to be determined + determineFolderDatesIfNeeded(folderItems, sortingSpec, sortingGroupsCardinality) folderItems.sort(function (itA: FolderItemForSorting, itB: FolderItemForSorting) { return compareTwoItems(itA, itB, sortingSpec); diff --git a/src/custom-sort/sorting-spec-processor.spec.ts b/src/custom-sort/sorting-spec-processor.spec.ts index d680158..288d0b7 100644 --- a/src/custom-sort/sorting-spec-processor.spec.ts +++ b/src/custom-sort/sorting-spec-processor.spec.ts @@ -31,7 +31,7 @@ target-folder: tricky folder target-folder: / /: Con... / - > modified + > advanced modified /: < modified /: Ref... @@ -78,7 +78,7 @@ target-folder: tricky folder target-folder: / /:files Con... /folders - > modified + > advanced modified /:files < modified /:files Ref... @@ -163,7 +163,7 @@ const expectedSortSpecsExampleA: { [key: string]: CustomSortSpec } = { type: CustomSortGroupType.ExactPrefix }, { foldersOnly: true, - order: CustomSortOrder.byModifiedTimeReverse, + order: CustomSortOrder.byModifiedTimeReverseAdvanced, type: CustomSortGroupType.Outsiders }, { filesOnly: true, @@ -865,6 +865,102 @@ describe('SortingSpecProcessor path wildcard priorities', () => { }) }) +const txtInputAdvancedFolderDateSortingMethods: string = ` +target-folder: A +< advanced modified +target-folder: B +> advanced modified +target-folder: C +< advanced created +target-folder: D +> advanced created +target-folder: AA +/folders + < advanced modified +/:files + > advanced modified +/folders Archive... + < advanced created +/:files Archive... + > advanced created +` + +const expectedSortSpecForAdvancedFolderDateSortingMethods: { [key: string]: CustomSortSpec } = { + 'A': { + defaultOrder: CustomSortOrder.byModifiedTimeAdvanced, + groups: [{ + order: CustomSortOrder.byModifiedTimeAdvanced, + type: CustomSortGroupType.Outsiders + }], + outsidersGroupIdx: 0, + targetFoldersPaths: ['A'] + }, + 'B': { + defaultOrder: CustomSortOrder.byModifiedTimeReverseAdvanced, + groups: [{ + order: CustomSortOrder.byModifiedTimeReverseAdvanced, + type: CustomSortGroupType.Outsiders + }], + outsidersGroupIdx: 0, + targetFoldersPaths: ['B'] + }, + 'C': { + defaultOrder: CustomSortOrder.byCreatedTimeAdvanced, + groups: [{ + order: CustomSortOrder.byCreatedTimeAdvanced, + type: CustomSortGroupType.Outsiders + }], + outsidersGroupIdx: 0, + targetFoldersPaths: ['C'] + }, + 'D': { + defaultOrder: CustomSortOrder.byCreatedTimeReverseAdvanced, + groups: [{ + order: CustomSortOrder.byCreatedTimeReverseAdvanced, + type: CustomSortGroupType.Outsiders + }], + outsidersGroupIdx: 0, + targetFoldersPaths: ['D'] + }, + 'AA': { + groups: [{ + foldersOnly: true, + order: CustomSortOrder.byModifiedTimeAdvanced, + type: CustomSortGroupType.Outsiders + }, { + filesOnly: true, + order: CustomSortOrder.byModifiedTimeReverseAdvanced, + type: CustomSortGroupType.Outsiders + }, { + exactPrefix: 'Archive', + foldersOnly: true, + order: CustomSortOrder.byCreatedTimeAdvanced, + type: 3 + }, { + exactPrefix: 'Archive', + filesOnly: true, + order: CustomSortOrder.byCreatedTimeReverseAdvanced, + type: 3 + }], + outsidersFilesGroupIdx: 1, + outsidersFoldersGroupIdx: 0, + targetFoldersPaths: ['AA'] + } +} + +describe('SortingSpecProcessor advanced folder-date based sorting methods', () => { + let processor: SortingSpecProcessor; + beforeEach(() => { + processor = new SortingSpecProcessor(); + }); + it('should not raise error for multiple spec for the same path and choose correct spec, case A', () => { + const inputTxtArr: Array = txtInputAdvancedFolderDateSortingMethods.split('\n') + const result = processor.parseSortSpecFromText(inputTxtArr, 'mock-folder', 'custom-name-note.md') + expect(result?.sortSpecByPath).toEqual(expectedSortSpecForAdvancedFolderDateSortingMethods) + expect(result?.sortSpecByWildcard).toBeUndefined() + }) +}) + const errorsLogger = jest.fn(); const ERR_PREFIX = 'Sorting specification problem:' diff --git a/src/custom-sort/sorting-spec-processor.ts b/src/custom-sort/sorting-spec-processor.ts index 3f0050d..e281af1 100644 --- a/src/custom-sort/sorting-spec-processor.ts +++ b/src/custom-sort/sorting-spec-processor.ts @@ -86,6 +86,8 @@ const OrderLiterals: { [key: string]: CustomSortOrderAscDescPair } = { 'a-z': {asc: CustomSortOrder.alphabetical, desc: CustomSortOrder.alphabeticalReverse}, 'created': {asc: CustomSortOrder.byCreatedTime, desc: CustomSortOrder.byCreatedTimeReverse}, 'modified': {asc: CustomSortOrder.byModifiedTime, desc: CustomSortOrder.byModifiedTimeReverse}, + 'advanced modified': {asc: CustomSortOrder.byModifiedTimeAdvanced, desc: CustomSortOrder.byModifiedTimeReverseAdvanced}, + 'advanced created': {asc: CustomSortOrder.byCreatedTimeAdvanced, desc: CustomSortOrder.byCreatedTimeReverseAdvanced}, // Advanced, for edge cases of secondary sorting, when if regexp match is the same, override the alphabetical sorting by full name 'a-z, created': { @@ -107,6 +109,26 @@ const OrderLiterals: { [key: string]: CustomSortOrderAscDescPair } = { asc: CustomSortOrder.alphabetical, desc: CustomSortOrder.alphabeticalReverse, secondary: CustomSortOrder.byModifiedTimeReverse + }, + 'a-z, advanced created': { + asc: CustomSortOrder.alphabetical, + desc: CustomSortOrder.alphabeticalReverse, + secondary: CustomSortOrder.byCreatedTimeAdvanced + }, + 'a-z, advanced created desc': { + asc: CustomSortOrder.alphabetical, + desc: CustomSortOrder.alphabeticalReverse, + secondary: CustomSortOrder.byCreatedTimeReverseAdvanced + }, + 'a-z, advanced modified': { + asc: CustomSortOrder.alphabetical, + desc: CustomSortOrder.alphabeticalReverse, + secondary: CustomSortOrder.byModifiedTimeAdvanced + }, + 'a-z, advanced modified desc': { + asc: CustomSortOrder.alphabetical, + desc: CustomSortOrder.alphabeticalReverse, + secondary: CustomSortOrder.byModifiedTimeReverseAdvanced } } @@ -399,7 +421,7 @@ export class SortingSpecProcessor { lineIdx++ this.currentEntryLine = entryLine this.currentEntryLineIdx = lineIdx - this.currentSortingSpecContainerFilePath = `${folderPath}/${sortingSpecFileName}` + this.currentSortingSpecContainerFilePath = `${folderPath === '/' ? '' : folderPath}/${sortingSpecFileName}` this.problemAlreadyReportedForCurrentLine = false const trimmedEntryLine: string = entryLine.trim()