13 - Feature request: Sort by modification date, treating folder and files equally

- added unit tests coverage the logic of determining dates for folders
This commit is contained in:
SebastianMC 2022-09-26 19:00:23 +02:00
parent bfd093bbf2
commit 72550fc01d
3 changed files with 159 additions and 38 deletions

View File

@ -10,11 +10,11 @@ export enum CustomSortGroupType {
export enum CustomSortOrder { export enum CustomSortOrder {
alphabetical = 1, // = 1 to allow: if (customSortOrder) { ... alphabetical = 1, // = 1 to allow: if (customSortOrder) { ...
alphabeticalReverse, alphabeticalReverse,
byModifiedTime, byModifiedTime, // New to old
byModifiedTimeAdvanced, byModifiedTimeAdvanced,
byModifiedTimeReverse, byModifiedTimeReverse, // Old to new
byModifiedTimeReverseAdvanced, byModifiedTimeReverseAdvanced,
byCreatedTime, byCreatedTime, // New to old
byCreatedTimeAdvanced, byCreatedTimeAdvanced,
byCreatedTimeReverse, byCreatedTimeReverse,
byCreatedTimeReverseAdvanced, byCreatedTimeReverseAdvanced,

View File

@ -1,6 +1,11 @@
import {TFile, TFolder, Vault} from 'obsidian'; import {TFile, TFolder, Vault} from 'obsidian';
import {determineSortingGroup} from './custom-sort'; import {
import {CustomSortGroupType, CustomSortSpec} from './custom-sort-types'; DEFAULT_FOLDER_CTIME,
determineFolderDatesIfNeeded,
determineSortingGroup,
FolderItemForSorting
} from './custom-sort';
import {CustomSortGroupType, CustomSortOrder, CustomSortSpec} from './custom-sort-types';
import {CompoundDashNumberNormalizerFn, CompoundDotRomanNumberNormalizerFn} from "./sorting-spec-processor"; import {CompoundDashNumberNormalizerFn, CompoundDotRomanNumberNormalizerFn} from "./sorting-spec-processor";
const mockTFile = (basename: string, ext: string, size?: number, ctime?: number, mtime?: number): TFile => { 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<TFolder|TFile>, 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 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('determineSortingGroup', () => {
describe('CustomSortGroupType.ExactHeadAndTail', () => { describe('CustomSortGroupType.ExactHeadAndTail', () => {
@ -43,7 +72,8 @@ describe('determineSortingGroup', () => {
groupIdx: 0, groupIdx: 0,
isFolder: false, isFolder: false,
sortString: "References.md", sortString: "References.md",
ctime: MOCK_TIMESTAMP + 222, ctimeNewest: MOCK_TIMESTAMP + 222,
ctimeOldest: MOCK_TIMESTAMP + 222,
mtime: MOCK_TIMESTAMP + 333, mtime: MOCK_TIMESTAMP + 333,
path: 'Some parent folder/References.md' path: 'Some parent folder/References.md'
}); });
@ -68,7 +98,8 @@ describe('determineSortingGroup', () => {
groupIdx: 1, // This indicates the last+1 idx groupIdx: 1, // This indicates the last+1 idx
isFolder: false, isFolder: false,
sortString: "References.md", sortString: "References.md",
ctime: MOCK_TIMESTAMP + 555, ctimeNewest: MOCK_TIMESTAMP + 555,
ctimeOldest: MOCK_TIMESTAMP + 555,
mtime: MOCK_TIMESTAMP + 666, mtime: MOCK_TIMESTAMP + 666,
path: 'Some parent folder/References.md' path: 'Some parent folder/References.md'
}); });
@ -96,7 +127,8 @@ describe('determineSortingGroup', () => {
groupIdx: 1, // This indicates the last+1 idx groupIdx: 1, // This indicates the last+1 idx
isFolder: false, isFolder: false,
sortString: "Part123:-icle.md", sortString: "Part123:-icle.md",
ctime: MOCK_TIMESTAMP + 555, ctimeNewest: MOCK_TIMESTAMP + 555,
ctimeOldest: MOCK_TIMESTAMP + 555,
mtime: MOCK_TIMESTAMP + 666, mtime: MOCK_TIMESTAMP + 666,
path: 'Some parent folder/Part123:-icle.md' path: 'Some parent folder/Part123:-icle.md'
}); });
@ -125,7 +157,8 @@ describe('determineSortingGroup', () => {
isFolder: false, isFolder: false,
sortString: "00000123////Part123:-icle.md", sortString: "00000123////Part123:-icle.md",
matchGroup: '00000123//', matchGroup: '00000123//',
ctime: MOCK_TIMESTAMP + 555, ctimeNewest: MOCK_TIMESTAMP + 555,
ctimeOldest: MOCK_TIMESTAMP + 555,
mtime: MOCK_TIMESTAMP + 666, mtime: MOCK_TIMESTAMP + 666,
path: 'Some parent folder/Part123:-icle.md' path: 'Some parent folder/Part123:-icle.md'
}); });
@ -153,7 +186,8 @@ describe('determineSortingGroup', () => {
groupIdx: 1, // This indicates the last+1 idx groupIdx: 1, // This indicates the last+1 idx
isFolder: false, isFolder: false,
sortString: "Part:123-icle.md", sortString: "Part:123-icle.md",
ctime: MOCK_TIMESTAMP + 555, ctimeNewest: MOCK_TIMESTAMP + 555,
ctimeOldest: MOCK_TIMESTAMP + 555,
mtime: MOCK_TIMESTAMP + 666, mtime: MOCK_TIMESTAMP + 666,
path: 'Some parent folder/Part:123-icle.md' path: 'Some parent folder/Part:123-icle.md'
}); });
@ -182,7 +216,8 @@ describe('determineSortingGroup', () => {
isFolder: false, isFolder: false,
sortString: "00000123////Part:123-icle.md", sortString: "00000123////Part:123-icle.md",
matchGroup: '00000123//', matchGroup: '00000123//',
ctime: MOCK_TIMESTAMP + 555, ctimeNewest: MOCK_TIMESTAMP + 555,
ctimeOldest: MOCK_TIMESTAMP + 555,
mtime: MOCK_TIMESTAMP + 666, mtime: MOCK_TIMESTAMP + 666,
path: 'Some parent folder/Part:123-icle.md' path: 'Some parent folder/Part:123-icle.md'
}); });
@ -208,7 +243,8 @@ describe('determineSortingGroup', () => {
groupIdx: 0, groupIdx: 0,
isFolder: false, isFolder: false,
sortString: "References.md", sortString: "References.md",
ctime: MOCK_TIMESTAMP + 222, ctimeNewest: MOCK_TIMESTAMP + 222,
ctimeOldest: MOCK_TIMESTAMP + 222,
mtime: MOCK_TIMESTAMP + 333, mtime: MOCK_TIMESTAMP + 333,
path: 'Some parent folder/References.md' path: 'Some parent folder/References.md'
}); });
@ -236,7 +272,8 @@ describe('determineSortingGroup', () => {
isFolder: false, isFolder: false,
sortString: '00000001|00000030|00000006|00001900////Reference i.xxx.vi.mcm.md', sortString: '00000001|00000030|00000006|00001900////Reference i.xxx.vi.mcm.md',
matchGroup: "00000001|00000030|00000006|00001900//", matchGroup: "00000001|00000030|00000006|00001900//",
ctime: MOCK_TIMESTAMP + 222, ctimeNewest: MOCK_TIMESTAMP + 222,
ctimeOldest: MOCK_TIMESTAMP + 222,
mtime: MOCK_TIMESTAMP + 333, mtime: MOCK_TIMESTAMP + 333,
path: 'Some parent folder/Reference i.xxx.vi.mcm.md' path: 'Some parent folder/Reference i.xxx.vi.mcm.md'
}); });
@ -260,10 +297,83 @@ describe('determineSortingGroup', () => {
groupIdx: 1, // This indicates the last+1 idx groupIdx: 1, // This indicates the last+1 idx
isFolder: false, isFolder: false,
sortString: "References.md", sortString: "References.md",
ctime: MOCK_TIMESTAMP + 222, ctimeNewest: MOCK_TIMESTAMP + 222,
ctimeOldest: MOCK_TIMESTAMP + 222,
mtime: MOCK_TIMESTAMP + 333, mtime: MOCK_TIMESTAMP + 333,
path: 'Some parent folder/References.md' 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)
})
})

View File

@ -8,12 +8,13 @@ let Collator = new Intl.Collator(undefined, {
numeric: true, numeric: true,
}).compare; }).compare;
interface FolderItemForSorting { export interface FolderItemForSorting {
path: string path: string
groupIdx?: number // the index itself represents order for groups groupIdx?: number // the index itself represents order for groups
sortString: string // fragment (or full name) to be used for sorting sortString: string // fragment (or full name) to be used for sorting
matchGroup?: string // advanced - used for secondary sorting rule, to recognize 'same regex match' 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 mtime: number
isFolder: boolean isFolder: boolean
folder?: TFolder folder?: TFolder
@ -28,10 +29,10 @@ let Sorters: { [key in CustomSortOrder]: SorterFn } = {
[CustomSortOrder.byModifiedTimeAdvanced]: (a: FolderItemForSorting, b: FolderItemForSorting) => 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.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.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.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.ctime - b.ctime, [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.ctime - a.ctime), [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.ctime - a.ctime, [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 // 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), [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, isFolder: aFolder,
folder: aFolder ? (entry as TFolder) : undefined, folder: aFolder ? (entry as TFolder) : undefined,
path: entry.path, 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 mtime: aFile ? entryAsTFile.stat.mtime : DEFAULT_FOLDER_MTIME
} }
} }
@ -195,25 +197,44 @@ export const sortOrderNeedsFolderDates = (order: CustomSortOrder | undefined, se
// Syntax sugar for readability // Syntax sugar for readability
export type ModifiedTime = number 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 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) => { folder.children.forEach((item) => {
if (!isFolder(item)) { if (!isFolder(item)) {
const file: TFile = item as TFile const file: TFile = item as TFile
if (file.stat.mtime > mtimeOfFolder) { if (file.stat.mtime > mtimeOfFolder) {
mtimeOfFolder = file.stat.mtime mtimeOfFolder = file.stat.mtime
} }
if (file.stat.ctime > ctimeOfFolder) { if (file.stat.ctime > ctimeNewestOfFolder) {
ctimeOfFolder = file.stat.ctime 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<FolderItemForSorting>, 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[]) { export const folderSort = function (sortingSpec: CustomSortSpec, order: string[]) {
let fileExplorer = this.fileExplorer 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 // Finally, for advanced sorting by modified date, for some of the folders the modified date has to be determined
folderItems.forEach((item) => { determineFolderDatesIfNeeded(folderItems, sortingSpec, sortingGroupsCardinality)
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)
}
}
}
})
folderItems.sort(function (itA: FolderItemForSorting, itB: FolderItemForSorting) { folderItems.sort(function (itA: FolderItemForSorting, itB: FolderItemForSorting) {
return compareTwoItems(itA, itB, sortingSpec); return compareTwoItems(itA, itB, sortingSpec);