diff --git a/src/custom-sort/custom-sort.ts b/src/custom-sort/custom-sort.ts index d320f56..e906929 100644 --- a/src/custom-sort/custom-sort.ts +++ b/src/custom-sort/custom-sort.ts @@ -240,7 +240,7 @@ const SortersForDerivedSecondary: { [key in CustomSortOrder]?: SorterFn } = { }; // OS - Obsidian Sort -const OS_alphabetical = 'alphabetical' +export const OS_alphabetical = 'alphabetical' const OS_alphabeticalReverse = 'alphabeticalReverse' export const OS_byModifiedTime = 'byModifiedTime' export const OS_byModifiedTimeReverse = 'byModifiedTimeReverse' @@ -794,7 +794,7 @@ const folderSortCore = function (sortedFolder: TFolder, sortOrder: string, sorti }; // Returns a sorted copy of the input array, intentionally to keep it intact -export const sortFolderItemsForBookmarking = function (folder: TFolder, items: Array, sortingSpec: CustomSortSpec|null|undefined, ctx: ProcessingContext, uiSortOrder: string): Array { +export const sortFolderItems = function (folder: TFolder, items: Array, sortingSpec: CustomSortSpec|null|undefined, ctx: ProcessingContext, uiSortOrder: string): Array { if (sortingSpec) { const folderItemsByPath: { [key: string]: TAbstractFile } = {} @@ -831,6 +831,9 @@ export const sortFolderItemsForBookmarking = function (folder: TFolder, items: A } }; +// Exported legacy function name for backward compatibility +export const sortFolderItemsForBookmarking = sortFolderItems + export const _unitTests = { fileGoesFirstWhenSameBasenameAsFolder: fileGoesFirstWhenSameBasenameAsFolder, folderGoesFirstWhenSameBasenameAsFolder: folderGoesFirstWhenSameBasenameAsFolder diff --git a/src/custom-sort/matchers.ts b/src/custom-sort/matchers.ts index b92c65c..31504a9 100644 --- a/src/custom-sort/matchers.ts +++ b/src/custom-sort/matchers.ts @@ -1,5 +1,3 @@ -import {toString} from "builtin-modules"; - export const RomanNumberRegexStr: string = ' *([MDCLXVI]+)'; // Roman number export const CompoundRomanNumberDotRegexStr: string = ' *([MDCLXVI]+(?:\\.[MDCLXVI]+)*)';// Compound Roman number with dot as separator export const CompoundRomanNumberDashRegexStr: string = ' *([MDCLXVI]+(?:-[MDCLXVI]+)*)'; // Compound Roman number with dash as separator @@ -9,6 +7,7 @@ export const CompoundNumberDotRegexStr: string = ' *(\\d+(?:\\.\\d+)*)'; // Comp export const CompoundNumberDashRegexStr: string = ' *(\\d+(?:-\\d+)*)'; // Compound number with dash as separator export const Date_dd_Mmm_yyyy_RegexStr: string = ' *([0-3]*[0-9]-(?:Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)-\\d{4})'; // Date like 01-Jan-2020 +export const Date_Mmm_dd_yyyy_RegexStr: string = ' *((?:Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)-[0-3]*[0-9]-\\d{4})'; // Date like Jan-01-2020 export const DOT_SEPARATOR = '.' export const DASH_SEPARATOR = '-' @@ -104,17 +103,23 @@ export function getNormalizedRomanNumber(s: string, separator?: string, places?: } } -const DAY_POSITIONS = '00'.length -const MONTH_POSITIONS = '00'.length -const YEAR_POSITIONS = '0000'.length +export const DAY_POSITIONS = '00'.length +export const MONTH_POSITIONS = '00'.length +export const YEAR_POSITIONS = '0000'.length const MONTHS = ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec'] -export function getNormalizedDate_dd_Mmm_yyyy_NormalizerFn(s: string): string | null { - // Assumption - the regex date matched against input s, no extensive defensive coding needed - const components = s.split('-') - const day = prependWithZeros(components[0], DAY_POSITIONS) - const month = prependWithZeros( `${1 + MONTHS.indexOf(components[1])}`, MONTH_POSITIONS) - const year = prependWithZeros(components[2], YEAR_POSITIONS) - return `${year}-${month}-${day}//` +export function getNormalizedDate_NormalizerFn_for(separator: string, dayIdx: number, monthIdx: number, yearIdx: number, months?: string[]) { + return (s: string): string | null => { + // Assumption - the regex date matched against input s, no extensive defensive coding needed + const components = s.split(separator) + const day = prependWithZeros(components[dayIdx], DAY_POSITIONS) + const monthValue = months ? `${1 + MONTHS.indexOf(components[monthIdx])}` : components[monthIdx] + const month = prependWithZeros(monthValue, MONTH_POSITIONS) + const year = prependWithZeros(components[yearIdx], YEAR_POSITIONS) + return `${year}-${month}-${day}//` + } } + +export const getNormalizedDate_dd_Mmm_yyyy_NormalizerFn = getNormalizedDate_NormalizerFn_for('-', 0, 1, 2, MONTHS) +export const getNormalizedDate_Mmm_dd_yyyy_NormalizerFn = getNormalizedDate_NormalizerFn_for('-', 1, 0, 2, MONTHS) diff --git a/src/custom-sort/sorting-spec-processor.ts b/src/custom-sort/sorting-spec-processor.ts index 537dcc1..f7a4cf7 100644 --- a/src/custom-sort/sorting-spec-processor.ts +++ b/src/custom-sort/sorting-spec-processor.ts @@ -17,8 +17,10 @@ import { CompoundRomanNumberDotRegexStr, DASH_SEPARATOR, Date_dd_Mmm_yyyy_RegexStr, + Date_Mmm_dd_yyyy_RegexStr, DOT_SEPARATOR, getNormalizedDate_dd_Mmm_yyyy_NormalizerFn, + getNormalizedDate_Mmm_dd_yyyy_NormalizerFn, getNormalizedNumber, getNormalizedRomanNumber, NumberRegexStr, @@ -351,6 +353,7 @@ const InlineRegexSymbol_Digit2: string = '\\[0-9]' const InlineRegexSymbol_0_to_3: string = '\\[0-3]' const Date_dd_Mmm_yyyy_RegexSymbol: string = '\\[dd-Mmm-yyyy]' +const Date_Mmm_dd_yyyy_RegexSymbol: string = '\\[Mmm-dd-yyyy]' const InlineRegexSymbol_CapitalLetter: string = '\\C' const InlineRegexSymbol_LowercaseLetter: string = '\\l' @@ -370,7 +373,8 @@ const sortingSymbolsArr: Array = [ escapeRegexUnsafeCharacters(CompoundRomanNumberDashRegexSymbol), escapeRegexUnsafeCharacters(WordInASCIIRegexSymbol), escapeRegexUnsafeCharacters(WordInAnyLanguageRegexSymbol), - escapeRegexUnsafeCharacters(Date_dd_Mmm_yyyy_RegexSymbol) + escapeRegexUnsafeCharacters(Date_dd_Mmm_yyyy_RegexSymbol), + escapeRegexUnsafeCharacters(Date_Mmm_dd_yyyy_RegexSymbol) ] const sortingSymbolsRegex = new RegExp(sortingSymbolsArr.join('|'), 'gi') @@ -439,6 +443,7 @@ export const NumberNormalizerFn: NormalizerFn = (s: string) => getNormalizedNumb export const CompoundDotNumberNormalizerFn: NormalizerFn = (s: string) => getNormalizedNumber(s, DOT_SEPARATOR) export const CompoundDashNumberNormalizerFn: NormalizerFn = (s: string) => getNormalizedNumber(s, DASH_SEPARATOR) export const Date_dd_Mmm_yyyy_NormalizerFn: NormalizerFn = (s: string) => getNormalizedDate_dd_Mmm_yyyy_NormalizerFn(s) +export const Date_Mmm_dd_yyyy_NormalizerFn: NormalizerFn = (s: string) => getNormalizedDate_Mmm_dd_yyyy_NormalizerFn(s) export enum AdvancedRegexType { None, // to allow if (advancedRegex) @@ -450,7 +455,8 @@ export enum AdvancedRegexType { CompoundDashRomanNumber, WordInASCII, WordInAnyLanguage, - Date_dd_Mmm_yyyy + Date_dd_Mmm_yyyy, + Date_Mmm_dd_yyyy } const sortingSymbolToRegexpStr: { [key: string]: RegExpSpecStr } = { @@ -499,6 +505,11 @@ const sortingSymbolToRegexpStr: { [key: string]: RegExpSpecStr } = { regexpStr: Date_dd_Mmm_yyyy_RegexStr, normalizerFn: Date_dd_Mmm_yyyy_NormalizerFn, advancedRegexType: AdvancedRegexType.Date_dd_Mmm_yyyy + }, + [Date_Mmm_dd_yyyy_RegexSymbol]: { // Intentionally retain character case + regexpStr: Date_Mmm_dd_yyyy_RegexStr, + normalizerFn: Date_Mmm_dd_yyyy_NormalizerFn, + advancedRegexType: AdvancedRegexType.Date_Mmm_dd_yyyy } } diff --git a/src/test/int/dates-in-names.int.test.ts b/src/test/int/dates-in-names.int.test.ts new file mode 100644 index 0000000..66487e5 --- /dev/null +++ b/src/test/int/dates-in-names.int.test.ts @@ -0,0 +1,64 @@ +import { + TAbstractFile, + TFolder, + Vault +} from "obsidian"; +import { + DEFAULT_FOLDER_CTIME, + determineFolderDatesIfNeeded, + determineSortingGroup, + FolderItemForSorting, OS_alphabetical, OS_byCreatedTime, ProcessingContext, sortFolderItems +} from "../../custom-sort/custom-sort"; +import { + CustomSortGroupType, + CustomSortOrder, + CustomSortSpec +} from "../../custom-sort/custom-sort-types"; +import { + TIMESTAMP_OLDEST, + TIMESTAMP_NEWEST, + mockTFolderWithChildren, + mockTFolderWithDateNamedChildren, + TIMESTAMP_DEEP_NEWEST, + TIMESTAMP_DEEP_OLDEST, +} from "../mocks"; +import { + SortingSpecProcessor +} from "../../custom-sort/sorting-spec-processor"; + +describe('sortFolderItems', () => { + it('should correctly handle Mmm-dd-yyyy pattern in file names', () => { + // given + const processor: SortingSpecProcessor = new SortingSpecProcessor() + const sortSpecTxt = +` ... \\[Mmm-dd-yyyy] + > a-z +` + const PARENT_PATH = 'parent/folder/path' + const sortSpecsCollection = processor.parseSortSpecFromText( + sortSpecTxt.split('\n'), + PARENT_PATH, + 'file name with the sorting, irrelevant here' + ) + + const folder: TFolder = mockTFolderWithDateNamedChildren(PARENT_PATH) + const sortSpec: CustomSortSpec = sortSpecsCollection?.sortSpecByPath![PARENT_PATH]! + + const ctx: ProcessingContext = {} + + // when + const result: Array = sortFolderItems(folder, folder.children, sortSpec, ctx, OS_alphabetical) + + // then + const orderedNames = result.map(f => f.name) + expect(orderedNames).toEqual([ + 'CCC Feb-28-2025', + 'BBB Dec-23-2024.md', + 'DDD Jul-15-2024.md', + 'AAA Jan-01-2012' + ]) + }) +}) + + + diff --git a/src/test/mocks.ts b/src/test/mocks.ts index 4b86251..a5853ef 100644 --- a/src/test/mocks.ts +++ b/src/test/mocks.ts @@ -3,6 +3,9 @@ import { TFolder, Vault } from "obsidian"; +import { + lastPathComponent +} from "../utils/utils"; export const mockTFile = (basename: string, ext: string, size?: number, ctime?: number, mtime?: number): TFile => { return { @@ -25,7 +28,7 @@ export const mockTFolder = (name: string, children?: Array, paren isRoot(): boolean { return name === '/' }, vault: {} as Vault, // To satisfy TS typechecking path: `${name}`, - name: name, + name: lastPathComponent(name), parent: parent ?? ({} as TFolder), // To satisfy TS typechecking children: children ?? [] } @@ -53,3 +56,12 @@ export const mockTFolderWithChildren = (name: string): TFolder => { return mockTFolder(name, [child1, child2, child3, child4, child5, subfolder1, subfolder2]) } + +export const mockTFolderWithDateNamedChildren = (name: string): TFolder => { + const child1: TFolder = mockTFolder('AAA Jan-01-2012') + const child2: TFile = mockTFile('BBB Dec-23-2024', 'md') + const child3: TFolder = mockTFolder('CCC Feb-28-2025') + const child4: TFile = mockTFile('DDD Jul-15-2024', 'md') + + return mockTFolder(name, [child1, child2, child3, child4]) +} diff --git a/src/test/unit/sorting-spec-processor.spec.ts b/src/test/unit/sorting-spec-processor.spec.ts index 3a7fbd1..c3e66dc 100644 --- a/src/test/unit/sorting-spec-processor.spec.ts +++ b/src/test/unit/sorting-spec-processor.spec.ts @@ -4,7 +4,9 @@ import { CompoundDotNumberNormalizerFn, ConsumedFolderMatchingRegexp, consumeFolderByRegexpExpression, - convertPlainStringToRegex, Date_dd_Mmm_yyyy_NormalizerFn, + convertPlainStringToRegex, + Date_dd_Mmm_yyyy_NormalizerFn, + Date_Mmm_dd_yyyy_NormalizerFn, detectSortingSymbols, escapeRegexUnsafeCharacters, extractSortingSymbol, @@ -410,11 +412,17 @@ const expectedSortSpecsExampleSortingSymbols: { [key: string]: CustomSortSpec } regex: /^ *([0-3]*[0-9]-(?:Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)-\d{4}) for the specific date format of 12\-Apr\-2024$/i, normalizerFn: Date_dd_Mmm_yyyy_NormalizerFn } + }, { + type: CustomSortGroupType.ExactName, + regexPrefix: { + regex: /^ *((?:Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)-[0-3]*[0-9]-\d{4}) for the specific date format of Apr\-01\-2024$/i, + normalizerFn: Date_Mmm_dd_yyyy_NormalizerFn + } }, { type: CustomSortGroupType.Outsiders }], targetFoldersPaths: ['mock-folder'], - outsidersGroupIdx: 8 + outsidersGroupIdx: 9 } } @@ -427,6 +435,7 @@ And this kind of... \\D+plain syntax??? Here goes ASCII word \\a+ \\A+. is for any modern language word \\[dd-Mmm-yyyy] for the specific date format of 12-Apr-2024 +\\[Mmm-dd-yyyy] for the specific date format of Apr-01-2024 ` describe('SortingSpecProcessor', () => {