Skip to content

Commit 2ac5a12

Browse files
authored
Merge pull request #16 from SebastianMC/13-feature-support-modified-date-sort-for-folders
13 feature support modified date sort for folders
2 parents d6802a6 + 4b7849c commit 2ac5a12

File tree

7 files changed

+386
-37
lines changed

7 files changed

+386
-37
lines changed

README.md

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -355,12 +355,13 @@ sorting-spec: |
355355

356356
the result is:
357357

358-
![Book - Roman compond suffixes](./docs/svg/roman-suffix.svg)
358+
![Book - Roman compound suffixes](./docs/svg/roman-suffix.svg)
359359

360360
### Example 12: Apply same sorting to all folders in the vault
361361

362-
Apply the alphabetical sorting to all folders in the Vault. The alphabetical sorting treats the folders and files equally
363-
(which is different from the standard Obsidian sort, which groups folders in the top of File Explorer)
362+
Apply the same advanced modified date sorting to all folders in the Vault. The advanced modified sorting treats the folders
363+
and files equally (which is different from the standard Obsidian sort, which groups folders in the top of File Explorer)
364+
The modified date for a folder is derived from its newest direct child file (if any), otherwise a folder is considered old
364365

365366
This involves the wildcard suffix syntax `*` which means _apply the sorting rule to the specified folder
366367
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
371372
---
372373
sorting-spec: |
373374
target-folder: /*
374-
< a-z
375+
> advanced modified
375376
---
376377
```
377378

docs/syntax-reference.md

Lines changed: 44 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,45 @@
1-
Yet to be filled with content ;-)
1+
> Document is partial, creation in progress
2+
> Please refer to [README.md](../../README.md) for usage examples
3+
> Check [manual.md](), maybe that file has already some content?
24
3-
Check [manual.md](), maybe that file has already some content?
5+
### Supported sorting methods
6+
7+
#### At folder level only
8+
9+
- `sorting: standard` - gives back the control on order of items in hands of standard Obsidian mechanisms (UI driven).
10+
Typical (and intended) use: exclude a folder (or folders subtree) from a custom sorting resulting from wilcard-based target folder rule
11+
12+
#### At folder and group level
13+
14+
- `< a-z` - alphabetical
15+
- `> a-z` - alphabetical reverse, aka alphabetical descending, 'z' goes before 'a'
16+
- `< 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)
17+
- `> 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)
18+
- `< 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)
19+
- `> 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)
20+
- `< 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.
21+
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
22+
- `> 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.
23+
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
24+
- `< advanced created` - by created time, the oldest item goes first. For folders, their creation date is derived from the oldest direct child file.
25+
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
26+
- `> advanced created` - by created time reverse, the newest item goes first. For folders, their creation date is derived from the newest direct child file.
27+
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
28+
29+
#### At group level only (aka secondary sorting rule)
30+
31+
> Only applicable in edge cases based on numerical symbols, when the regex-based match is equal for more than one item
32+
and need to apply a secondary order on same matches.
33+
34+
- `< a-z, created`
35+
- `> a-z, created`
36+
- `< a-z, created desc`
37+
- `> a-z, created desc`
38+
- `< a-z, modified`
39+
- `> a-z, modified`
40+
- `< a-z, modified desc`
41+
- `> a-z, modified desc`
42+
- `< a-z, advanced modified`
43+
- `> a-z, advanced modified`
44+
- `< a-z, advanced modified desc`
45+
- `> a-z, advanced modified desc`

src/custom-sort/custom-sort-types.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,14 @@ export enum CustomSortGroupType {
1010
export enum CustomSortOrder {
1111
alphabetical = 1, // = 1 to allow: if (customSortOrder) { ...
1212
alphabeticalReverse,
13-
byModifiedTime,
14-
byModifiedTimeReverse,
15-
byCreatedTime,
13+
byModifiedTime, // New to old
14+
byModifiedTimeAdvanced,
15+
byModifiedTimeReverse, // Old to new
16+
byModifiedTimeReverseAdvanced,
17+
byCreatedTime, // New to old
18+
byCreatedTimeAdvanced,
1619
byCreatedTimeReverse,
20+
byCreatedTimeReverseAdvanced,
1721
standardObsidian, // Let the folder sorting be in hands of Obsidian, whatever user selected in the UI
1822
default = alphabetical
1923
}

src/custom-sort/custom-sort.spec.ts

Lines changed: 121 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
import {TFile, TFolder, Vault} from 'obsidian';
2-
import {determineSortingGroup} from './custom-sort';
3-
import {CustomSortGroupType, CustomSortSpec} from './custom-sort-types';
2+
import {
3+
DEFAULT_FOLDER_CTIME,
4+
determineFolderDatesIfNeeded,
5+
determineSortingGroup,
6+
FolderItemForSorting
7+
} from './custom-sort';
8+
import {CustomSortGroupType, CustomSortOrder, CustomSortSpec} from './custom-sort-types';
49
import {CompoundDashNumberNormalizerFn, CompoundDotRomanNumberNormalizerFn} from "./sorting-spec-processor";
510

611
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,
1924
}
2025
}
2126

27+
const mockTFolder = (name: string, children?: Array<TFolder|TFile>, parent?: TFolder): TFolder => {
28+
return {
29+
isRoot(): boolean { return name === '/' },
30+
vault: {} as Vault, // To satisfy TS typechecking
31+
path: `/${name}`,
32+
name: name,
33+
parent: parent ?? ({} as TFolder), // To satisfy TS typechecking
34+
children: children ?? []
35+
}
36+
}
37+
2238
const MOCK_TIMESTAMP: number = 1656417542418
39+
const TIMESTAMP_OLDEST: number = MOCK_TIMESTAMP
40+
const TIMESTAMP_NEWEST: number = MOCK_TIMESTAMP + 1000
41+
const TIMESTAMP_INBETWEEN: number = MOCK_TIMESTAMP + 500
42+
43+
const mockTFolderWithChildren = (name: string): TFolder => {
44+
const child1: TFolder = mockTFolder('Section A')
45+
const child2: TFolder = mockTFolder('Section B')
46+
const child3: TFile = mockTFile('Child file 1 created as oldest, modified recently', 'md', 100, TIMESTAMP_OLDEST, TIMESTAMP_NEWEST)
47+
const child4: TFile = mockTFile('Child file 2 created as newest, not modified at all', 'md', 100, TIMESTAMP_NEWEST, TIMESTAMP_NEWEST)
48+
const child5: TFile = mockTFile('Child file 3 created inbetween, modified inbetween', 'md', 100, TIMESTAMP_INBETWEEN, TIMESTAMP_INBETWEEN)
49+
50+
return mockTFolder('Mock parent folder', [child1, child2, child3, child4, child5])
51+
}
2352

2453
describe('determineSortingGroup', () => {
2554
describe('CustomSortGroupType.ExactHeadAndTail', () => {
@@ -43,7 +72,8 @@ describe('determineSortingGroup', () => {
4372
groupIdx: 0,
4473
isFolder: false,
4574
sortString: "References.md",
46-
ctime: MOCK_TIMESTAMP + 222,
75+
ctimeNewest: MOCK_TIMESTAMP + 222,
76+
ctimeOldest: MOCK_TIMESTAMP + 222,
4777
mtime: MOCK_TIMESTAMP + 333,
4878
path: 'Some parent folder/References.md'
4979
});
@@ -68,7 +98,8 @@ describe('determineSortingGroup', () => {
6898
groupIdx: 1, // This indicates the last+1 idx
6999
isFolder: false,
70100
sortString: "References.md",
71-
ctime: MOCK_TIMESTAMP + 555,
101+
ctimeNewest: MOCK_TIMESTAMP + 555,
102+
ctimeOldest: MOCK_TIMESTAMP + 555,
72103
mtime: MOCK_TIMESTAMP + 666,
73104
path: 'Some parent folder/References.md'
74105
});
@@ -96,7 +127,8 @@ describe('determineSortingGroup', () => {
96127
groupIdx: 1, // This indicates the last+1 idx
97128
isFolder: false,
98129
sortString: "Part123:-icle.md",
99-
ctime: MOCK_TIMESTAMP + 555,
130+
ctimeNewest: MOCK_TIMESTAMP + 555,
131+
ctimeOldest: MOCK_TIMESTAMP + 555,
100132
mtime: MOCK_TIMESTAMP + 666,
101133
path: 'Some parent folder/Part123:-icle.md'
102134
});
@@ -125,7 +157,8 @@ describe('determineSortingGroup', () => {
125157
isFolder: false,
126158
sortString: "00000123////Part123:-icle.md",
127159
matchGroup: '00000123//',
128-
ctime: MOCK_TIMESTAMP + 555,
160+
ctimeNewest: MOCK_TIMESTAMP + 555,
161+
ctimeOldest: MOCK_TIMESTAMP + 555,
129162
mtime: MOCK_TIMESTAMP + 666,
130163
path: 'Some parent folder/Part123:-icle.md'
131164
});
@@ -153,7 +186,8 @@ describe('determineSortingGroup', () => {
153186
groupIdx: 1, // This indicates the last+1 idx
154187
isFolder: false,
155188
sortString: "Part:123-icle.md",
156-
ctime: MOCK_TIMESTAMP + 555,
189+
ctimeNewest: MOCK_TIMESTAMP + 555,
190+
ctimeOldest: MOCK_TIMESTAMP + 555,
157191
mtime: MOCK_TIMESTAMP + 666,
158192
path: 'Some parent folder/Part:123-icle.md'
159193
});
@@ -182,7 +216,8 @@ describe('determineSortingGroup', () => {
182216
isFolder: false,
183217
sortString: "00000123////Part:123-icle.md",
184218
matchGroup: '00000123//',
185-
ctime: MOCK_TIMESTAMP + 555,
219+
ctimeNewest: MOCK_TIMESTAMP + 555,
220+
ctimeOldest: MOCK_TIMESTAMP + 555,
186221
mtime: MOCK_TIMESTAMP + 666,
187222
path: 'Some parent folder/Part:123-icle.md'
188223
});
@@ -208,7 +243,8 @@ describe('determineSortingGroup', () => {
208243
groupIdx: 0,
209244
isFolder: false,
210245
sortString: "References.md",
211-
ctime: MOCK_TIMESTAMP + 222,
246+
ctimeNewest: MOCK_TIMESTAMP + 222,
247+
ctimeOldest: MOCK_TIMESTAMP + 222,
212248
mtime: MOCK_TIMESTAMP + 333,
213249
path: 'Some parent folder/References.md'
214250
});
@@ -236,7 +272,8 @@ describe('determineSortingGroup', () => {
236272
isFolder: false,
237273
sortString: '00000001|00000030|00000006|00001900////Reference i.xxx.vi.mcm.md',
238274
matchGroup: "00000001|00000030|00000006|00001900//",
239-
ctime: MOCK_TIMESTAMP + 222,
275+
ctimeNewest: MOCK_TIMESTAMP + 222,
276+
ctimeOldest: MOCK_TIMESTAMP + 222,
240277
mtime: MOCK_TIMESTAMP + 333,
241278
path: 'Some parent folder/Reference i.xxx.vi.mcm.md'
242279
});
@@ -260,10 +297,83 @@ describe('determineSortingGroup', () => {
260297
groupIdx: 1, // This indicates the last+1 idx
261298
isFolder: false,
262299
sortString: "References.md",
263-
ctime: MOCK_TIMESTAMP + 222,
300+
ctimeNewest: MOCK_TIMESTAMP + 222,
301+
ctimeOldest: MOCK_TIMESTAMP + 222,
264302
mtime: MOCK_TIMESTAMP + 333,
265303
path: 'Some parent folder/References.md'
266304
});
267305
})
268306
})
269307
})
308+
309+
describe('determineFolderDatesIfNeeded', () => {
310+
it('should not be triggered if not needed - sorting method does not require it', () => {
311+
// given
312+
const folder: TFolder = mockTFolderWithChildren('Test folder 1')
313+
const OUTSIDERS_GROUP_IDX = 0
314+
const sortSpec: CustomSortSpec = {
315+
targetFoldersPaths: ['/'],
316+
groups: [{
317+
type: CustomSortGroupType.Outsiders,
318+
order: CustomSortOrder.alphabetical
319+
}],
320+
outsidersGroupIdx: OUTSIDERS_GROUP_IDX
321+
}
322+
const cardinality = {[OUTSIDERS_GROUP_IDX]: 10} // Group 0 contains 10 items
323+
324+
// when
325+
const result: FolderItemForSorting = determineSortingGroup(folder, sortSpec)
326+
determineFolderDatesIfNeeded([result], sortSpec, cardinality)
327+
328+
// then
329+
expect(result.ctimeOldest).toEqual(DEFAULT_FOLDER_CTIME)
330+
expect(result.ctimeNewest).toEqual(DEFAULT_FOLDER_CTIME)
331+
expect(result.mtime).toEqual(DEFAULT_FOLDER_CTIME)
332+
})
333+
it('should not be triggered if not needed - the folder is an only item', () => {
334+
// given
335+
const folder: TFolder = mockTFolderWithChildren('Test folder 1')
336+
const OUTSIDERS_GROUP_IDX = 0
337+
const sortSpec: CustomSortSpec = {
338+
targetFoldersPaths: ['/'],
339+
groups: [{
340+
type: CustomSortGroupType.Outsiders,
341+
order: CustomSortOrder.byModifiedTimeAdvanced
342+
}],
343+
outsidersGroupIdx: OUTSIDERS_GROUP_IDX
344+
}
345+
const cardinality = {[OUTSIDERS_GROUP_IDX]: 1} // Group 0 contains the folder alone
346+
347+
// when
348+
const result: FolderItemForSorting = determineSortingGroup(folder, sortSpec)
349+
determineFolderDatesIfNeeded([result], sortSpec, cardinality)
350+
351+
// then
352+
expect(result.ctimeOldest).toEqual(DEFAULT_FOLDER_CTIME)
353+
expect(result.ctimeNewest).toEqual(DEFAULT_FOLDER_CTIME)
354+
expect(result.mtime).toEqual(DEFAULT_FOLDER_CTIME)
355+
})
356+
it('should correctly determine dates, if triggered', () => {
357+
// given
358+
const folder: TFolder = mockTFolderWithChildren('Test folder 1')
359+
const OUTSIDERS_GROUP_IDX = 0
360+
const sortSpec: CustomSortSpec = {
361+
targetFoldersPaths: ['/'],
362+
groups: [{
363+
type: CustomSortGroupType.Outsiders,
364+
order: CustomSortOrder.byCreatedTimeReverseAdvanced
365+
}],
366+
outsidersGroupIdx: OUTSIDERS_GROUP_IDX
367+
}
368+
const cardinality = {[OUTSIDERS_GROUP_IDX]: 10} // Group 0 contains 10 items
369+
370+
// when
371+
const result: FolderItemForSorting = determineSortingGroup(folder, sortSpec)
372+
determineFolderDatesIfNeeded([result], sortSpec, cardinality)
373+
374+
// then
375+
expect(result.ctimeOldest).toEqual(TIMESTAMP_OLDEST)
376+
expect(result.ctimeNewest).toEqual(TIMESTAMP_NEWEST)
377+
expect(result.mtime).toEqual(TIMESTAMP_NEWEST)
378+
})
379+
})

0 commit comments

Comments
 (0)