diff --git a/src/custom-sort/custom-sort-types.ts b/src/custom-sort/custom-sort-types.ts index df30449..8a65ab0 100644 --- a/src/custom-sort/custom-sort-types.ts +++ b/src/custom-sort/custom-sort-types.ts @@ -10,11 +10,11 @@ export enum CustomSortGroupType { export enum CustomSortOrder { alphabetical = 1, // = 1 to allow: if (customSortOrder) { ... alphabeticalReverse, - byModifiedTime, + byModifiedTime, // New to old byModifiedTimeAdvanced, - byModifiedTimeReverse, + byModifiedTimeReverse, // Old to new byModifiedTimeReverseAdvanced, - byCreatedTime, + byCreatedTime, // New to old byCreatedTimeAdvanced, byCreatedTimeReverse, byCreatedTimeReverseAdvanced, 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 3800adc..69578be 100644 --- a/src/custom-sort/custom-sort.ts +++ b/src/custom-sort/custom-sort.ts @@ -8,12 +8,13 @@ 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 @@ -28,10 +29,10 @@ let Sorters: { [key in CustomSortOrder]: SorterFn } = { [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.ctime - b.ctime), - [CustomSortOrder.byCreatedTimeAdvanced]: (a: FolderItemForSorting, b: FolderItemForSorting) => a.ctime - b.ctime, - [CustomSortOrder.byCreatedTimeReverse]: (a: FolderItemForSorting, b: FolderItemForSorting) => (a.isFolder && b.isFolder) ? Collator(a.sortString, b.sortString) : (b.ctime - a.ctime), - [CustomSortOrder.byCreatedTimeReverseAdvanced]: (a: FolderItemForSorting, b: FolderItemForSorting) => b.ctime - a.ctime, + [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), @@ -175,7 +176,8 @@ export const determineSortingGroup = function (entry: TFile | TFolder, spec: Cus isFolder: aFolder, folder: aFolder ? (entry as TFolder) : undefined, path: entry.path, - ctime: aFile ? entryAsTFile.stat.ctime : DEFAULT_FOLDER_CTIME, + ctimeNewest: aFile ? entryAsTFile.stat.ctime : DEFAULT_FOLDER_CTIME, + ctimeOldest: aFile ? entryAsTFile.stat.ctime : DEFAULT_FOLDER_CTIME, mtime: aFile ? entryAsTFile.stat.mtime : DEFAULT_FOLDER_MTIME } } @@ -195,25 +197,44 @@ export const sortOrderNeedsFolderDates = (order: CustomSortOrder | undefined, se // Syntax sugar for readability export type ModifiedTime = number -export type CreatedTime = number +export type CreatedTimeNewest = number +export type CreatedTimeOldest = number -export const determineDatesForFolder = (folder: TFolder): [ModifiedTime, CreatedTime] => { +export const determineDatesForFolder = (folder: TFolder, now: number): [ModifiedTime, CreatedTimeNewest, CreatedTimeOldest] => { let mtimeOfFolder: ModifiedTime = DEFAULT_FOLDER_MTIME - let ctimeOfFolder: CreatedTime = DEFAULT_FOLDER_CTIME + 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 > ctimeOfFolder) { - ctimeOfFolder = file.stat.ctime + if (file.stat.ctime > ctimeNewestOfFolder) { + ctimeNewestOfFolder = file.stat.ctime + } + if (file.stat.ctime < ctimeOldestOfFolder) { + ctimeOldestOfFolder = file.stat.ctime } } }) - return [mtimeOfFolder, ctimeOfFolder] + 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 @@ -235,17 +256,7 @@ export const folderSort = function (sortingSpec: CustomSortSpec, order: string[] }) // Finally, for advanced sorting by modified date, for some of the folders the modified date has to be determined - folderItems.forEach((item) => { - const groupIdx: number | undefined = item.groupIdx - if (groupIdx !== undefined) { - const groupOrder: CustomSortOrder | undefined = sortingSpec.groups[groupIdx].order - if (sortOrderNeedsFolderDates(groupOrder)) { - if (item.folder) { - [item.mtime, item.ctime] = determineDatesForFolder(item.folder) - } - } - } - }) + determineFolderDatesIfNeeded(folderItems, sortingSpec, sortingGroupsCardinality) folderItems.sort(function (itA: FolderItemForSorting, itB: FolderItemForSorting) { return compareTwoItems(itA, itB, sortingSpec);