From 975f6ee26e5d6913029d2c8c61678cc3bb8135f4 Mon Sep 17 00:00:00 2001 From: SebastianMC <23032356+SebastianMC@users.noreply.github.com> Date: Tue, 14 Jan 2025 13:51:34 +0100 Subject: [PATCH 1/6] #178 - week-number based date extraction patterns for titles --- src/custom-sort/matchers.ts | 56 +++++++++ src/custom-sort/sorting-spec-processor.ts | 36 +++++- src/test/int/dates-in-names.int.test.ts | 114 ++++++++++++++++++- src/test/mocks.ts | 30 +++++ src/test/unit/sorting-spec-processor.spec.ts | 52 ++++++--- src/test/unit/week-of-year.spec.ts | 51 +++++++++ src/utils/week-of-year.ts | 56 +++++++++ 7 files changed, 377 insertions(+), 18 deletions(-) create mode 100644 src/test/unit/week-of-year.spec.ts create mode 100644 src/utils/week-of-year.ts diff --git a/src/custom-sort/matchers.ts b/src/custom-sort/matchers.ts index 31504a9fa..b28543a46 100644 --- a/src/custom-sort/matchers.ts +++ b/src/custom-sort/matchers.ts @@ -1,3 +1,7 @@ +import { + getDateForWeekOfYear +} from "../utils/week-of-year"; + 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 +13,9 @@ export const CompoundNumberDashRegexStr: string = ' *(\\d+(?:-\\d+)*)'; // Compo 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 Date_yyyy_Www_mm_dd_RegexStr: string = ' *(\\d{4}-W\\d{1,2} \\(\\d{2}-\\d{2}\\))' +export const Date_yyyy_Www_RegexStr: string = ' *(\\d{4}-W\\d{1,2})' + export const DOT_SEPARATOR = '.' export const DASH_SEPARATOR = '-' @@ -123,3 +130,52 @@ export function getNormalizedDate_NormalizerFn_for(separator: string, dayIdx: nu 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) + +const DateExtractor_yyyy_Www_mm_dd_Regex = /(\d{4})-W(\d{1,2}) \((\d{2})-(\d{2})\)/ +const DateExtractor_yyyy_Www_Regex = /(\d{4})-W(\d{1,2})/ + +// Matching groups +const YEAR_IDX = 1 +const WEEK_IDX = 2 +const MONTH_IDX = 3 +const DAY_IDX = 4 + +const DECEMBER = 12 +const JANUARY = 1 + +export function getNormalizedDate_NormalizerFn_yyyy_Www_mm_dd(consumeWeek: boolean, weeksISO?: boolean) { + return (s: string): string | null => { + // Assumption - the regex date matched against input s, no extensive defensive coding needed + const matches = consumeWeek ? DateExtractor_yyyy_Www_Regex.exec(s) : DateExtractor_yyyy_Www_mm_dd_Regex.exec(s) + const yearStr = matches![YEAR_IDX] + let yearNumber = Number.parseInt(yearStr,10) + let monthNumber: number + let dayNumber: number + if (consumeWeek) { + const weekNumberStr = matches![WEEK_IDX] + const weekNumber = Number.parseInt(weekNumberStr, 10) + const dateForWeek = getDateForWeekOfYear(yearNumber, weekNumber, weeksISO) + monthNumber = dateForWeek.getMonth()+1 // 1 - 12 + dayNumber = dateForWeek.getDate() // 1 - 31 + // Be careful with edge dates, which can belong to previous or next year + if (weekNumber === 1) { + if (monthNumber === DECEMBER) { + yearNumber-- + } + } + if (weekNumber >= 50) { + if (monthNumber === JANUARY) { + yearNumber++ + } + } + } else { // ignore week + monthNumber = Number.parseInt(matches![MONTH_IDX],10) + dayNumber = Number.parseInt(matches![DAY_IDX], 10) + } + return `${prependWithZeros(`${yearNumber}`, YEAR_POSITIONS)}-${prependWithZeros(`${monthNumber}`, MONTH_POSITIONS)}-${prependWithZeros(`${dayNumber}`, DAY_POSITIONS)}//` + } +} + +export const getNormalizedDate_yyyy_Www_mm_dd_NormalizerFn = getNormalizedDate_NormalizerFn_yyyy_Www_mm_dd(false) +export const getNormalizedDate_yyyy_WwwISO_NormalizerFn = getNormalizedDate_NormalizerFn_yyyy_Www_mm_dd(true, true) +export const getNormalizedDate_yyyy_Www_NormalizerFn = getNormalizedDate_NormalizerFn_yyyy_Www_mm_dd(true, false) diff --git a/src/custom-sort/sorting-spec-processor.ts b/src/custom-sort/sorting-spec-processor.ts index 4ffdd10ab..2d9760732 100644 --- a/src/custom-sort/sorting-spec-processor.ts +++ b/src/custom-sort/sorting-spec-processor.ts @@ -19,9 +19,14 @@ import { DASH_SEPARATOR, Date_dd_Mmm_yyyy_RegexStr, Date_Mmm_dd_yyyy_RegexStr, + Date_yyyy_Www_mm_dd_RegexStr, + Date_yyyy_Www_RegexStr, DOT_SEPARATOR, getNormalizedDate_dd_Mmm_yyyy_NormalizerFn, getNormalizedDate_Mmm_dd_yyyy_NormalizerFn, + getNormalizedDate_yyyy_Www_mm_dd_NormalizerFn, + getNormalizedDate_yyyy_WwwISO_NormalizerFn, + getNormalizedDate_yyyy_Www_NormalizerFn, getNormalizedNumber, getNormalizedRomanNumber, NumberRegexStr, @@ -354,6 +359,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 Date_yyyy_Www_mm_dd_RegexSymbol: string = '\\[yyyy-Www (mm-dd)]' +const Date_yyyy_Www_RegexSymbol: string = '\\[yyyy-Www]' +const Date_yyyy_WwwISO_RegexSymbol: string = '\\[yyyy-WwwISO]' const InlineRegexSymbol_CapitalLetter: string = '\\C' const InlineRegexSymbol_LowercaseLetter: string = '\\l' @@ -374,7 +382,10 @@ const sortingSymbolsArr: Array = [ escapeRegexUnsafeCharacters(WordInASCIIRegexSymbol), escapeRegexUnsafeCharacters(WordInAnyLanguageRegexSymbol), escapeRegexUnsafeCharacters(Date_dd_Mmm_yyyy_RegexSymbol), - escapeRegexUnsafeCharacters(Date_Mmm_dd_yyyy_RegexSymbol) + escapeRegexUnsafeCharacters(Date_Mmm_dd_yyyy_RegexSymbol), + escapeRegexUnsafeCharacters(Date_yyyy_Www_mm_dd_RegexSymbol), + escapeRegexUnsafeCharacters(Date_yyyy_WwwISO_RegexSymbol), + escapeRegexUnsafeCharacters(Date_yyyy_Www_RegexSymbol), ] const sortingSymbolsRegex = new RegExp(sortingSymbolsArr.join('|'), 'gi') @@ -444,6 +455,9 @@ export const CompoundDotNumberNormalizerFn: NormalizerFn = (s: string) => getNor 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 const Date_yyyy_Www_mm_dd_NormalizerFn: NormalizerFn = (s: string) => getNormalizedDate_yyyy_Www_mm_dd_NormalizerFn(s) +export const Date_yyyy_WwwISO_NormalizerFn: NormalizerFn = (s: string) => getNormalizedDate_yyyy_WwwISO_NormalizerFn(s) +export const Date_yyyy_Www_NormalizerFn: NormalizerFn = (s: string) => getNormalizedDate_yyyy_Www_NormalizerFn(s) export enum AdvancedRegexType { None, // to allow if (advancedRegex) @@ -456,7 +470,10 @@ export enum AdvancedRegexType { WordInASCII, WordInAnyLanguage, Date_dd_Mmm_yyyy, - Date_Mmm_dd_yyyy + Date_Mmm_dd_yyyy, + Date_yyyy_Www_mm_dd_yyyy, + Date_yyyy_WwwISO, + Date_yyyy_Www } const sortingSymbolToRegexpStr: { [key: string]: RegExpSpecStr } = { @@ -510,6 +527,21 @@ const sortingSymbolToRegexpStr: { [key: string]: RegExpSpecStr } = { regexpStr: Date_Mmm_dd_yyyy_RegexStr, normalizerFn: Date_Mmm_dd_yyyy_NormalizerFn, advancedRegexType: AdvancedRegexType.Date_Mmm_dd_yyyy + }, + [Date_yyyy_Www_mm_dd_RegexSymbol]: { // Intentionally retain character case + regexpStr: Date_yyyy_Www_mm_dd_RegexStr, + normalizerFn: Date_yyyy_Www_mm_dd_NormalizerFn, + advancedRegexType: AdvancedRegexType.Date_yyyy_Www_mm_dd_yyyy + }, + [Date_yyyy_WwwISO_RegexSymbol]: { // Intentionally retain character case + regexpStr: Date_yyyy_Www_RegexStr, + normalizerFn: Date_yyyy_WwwISO_NormalizerFn, + advancedRegexType: AdvancedRegexType.Date_yyyy_WwwISO + }, + [Date_yyyy_Www_RegexSymbol]: { // Intentionally retain character case + regexpStr: Date_yyyy_Www_RegexStr, + normalizerFn: Date_yyyy_Www_NormalizerFn, + advancedRegexType: AdvancedRegexType.Date_yyyy_Www } } diff --git a/src/test/int/dates-in-names.int.test.ts b/src/test/int/dates-in-names.int.test.ts index 66487e5ff..8d4c74081 100644 --- a/src/test/int/dates-in-names.int.test.ts +++ b/src/test/int/dates-in-names.int.test.ts @@ -1,5 +1,5 @@ import { - TAbstractFile, + TAbstractFile, TFile, TFolder, Vault } from "obsidian"; @@ -21,6 +21,8 @@ import { mockTFolderWithDateNamedChildren, TIMESTAMP_DEEP_NEWEST, TIMESTAMP_DEEP_OLDEST, + mockTFolderWithDateWeekNamedChildrenForISOvsUSweekNumberingTest, + mockTFolderWithDateWeekNamedChildren, mockTFile, mockTFolder, } from "../mocks"; import { SortingSpecProcessor @@ -58,6 +60,116 @@ describe('sortFolderItems', () => { 'AAA Jan-01-2012' ]) }) + it('should correctly handle yyyy-Www (mm-dd) pattern in file names', () => { + // given + const processor: SortingSpecProcessor = new SortingSpecProcessor() + const sortSpecTxt = +` ... \\[yyyy-Www (mm-dd)] + < 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 = mockTFolderWithDateWeekNamedChildren(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([ + "GHI 2021-W1 (01-04)", + "DEF 2021-W9 (03-01).md", + "ABC 2021-W13 (03-29)", + "MNO 2021-W45 (11-08).md", + "JKL 2021-W52 (12-27).md", + "------.md" + ]) + }) + it('should correctly handle yyyy-WwwISO pattern in file names', () => { + // given + const processor: SortingSpecProcessor = new SortingSpecProcessor() + const sortSpecTxt = +` /+ ... \\[yyyy-Www (mm-dd)] + /+ ... \\[yyyy-WwwISO] + < 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 = mockTFolderWithDateWeekNamedChildrenForISOvsUSweekNumberingTest(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 + // ISO standard of weeks numbering + const orderedNames = result.map(f => f.name) + expect(orderedNames).toEqual([ + 'E 2021-W1 (01-01)', + 'F ISO:2021-01-04 US:2020-12-28 2021-W1', + 'A 2021-W10 (03-05).md', + 'B ISO:2021-03-08 US:2021-03-01 2021-W10', + 'C 2021-W51 (12-17).md', + 'D ISO:2021-12-20 US:2021-12-13 2021-W51.md', + 'FFF2 ISO:2021-12-27 US:2021-12-20 2021-W52.md', + 'FFF1 ISO:2022-01-03 US:2021-12-27 2021-W53.md', + "------.md" + ]) + }) + it('should correctly handle yyyy-Www pattern in file names', () => { + // given + const processor: SortingSpecProcessor = new SortingSpecProcessor() + const sortSpecTxt = +` /+ ... \\[yyyy-Www (mm-dd)] + /+ ... \\[yyyy-Www] + > 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 = mockTFolderWithDateWeekNamedChildrenForISOvsUSweekNumberingTest(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 + // U.S. standard of weeks numbering + const orderedNames = result.map(f => f.name) + expect(orderedNames).toEqual([ + 'FFF1 ISO:2022-01-03 US:2021-12-27 2021-W53.md', + 'FFF2 ISO:2021-12-27 US:2021-12-20 2021-W52.md', + 'C 2021-W51 (12-17).md', + 'D ISO:2021-12-20 US:2021-12-13 2021-W51.md', + 'A 2021-W10 (03-05).md', + 'B ISO:2021-03-08 US:2021-03-01 2021-W10', + 'E 2021-W1 (01-01)', + 'F ISO:2021-01-04 US:2020-12-28 2021-W1', + "------.md" + ]) + }) }) diff --git a/src/test/mocks.ts b/src/test/mocks.ts index a5853efe2..0f34f389a 100644 --- a/src/test/mocks.ts +++ b/src/test/mocks.ts @@ -65,3 +65,33 @@ export const mockTFolderWithDateNamedChildren = (name: string): TFolder => { return mockTFolder(name, [child1, child2, child3, child4]) } + +export const mockTFolderWithDateWeekNamedChildren = (name: string): TFolder => { + // Assume ISO week numbers + const child0: TFile = mockTFile('------', 'md') + const child1: TFolder = mockTFolder('ABC 2021-W13 (03-29)') + const child2: TFile = mockTFile('DEF 2021-W9 (03-01)', 'md') + const child3: TFolder = mockTFolder('GHI 2021-W1 (01-04)') + const child4: TFile = mockTFile('JKL 2021-W52 (12-27)', 'md') + const child5: TFile = mockTFile('MNO 2021-W45 (11-08)', 'md') + + return mockTFolder(name, [child0, child1, child2, child3, child4, child5]) +} + +export const mockTFolderWithDateWeekNamedChildrenForISOvsUSweekNumberingTest = (name: string): TFolder => { + // Tricky to test handling of both ISO and U.S. weeks numbering. + // Sample year with different week numbers in ISO vs. U.S. is 2021 with 1st Jan on Fri, ISO != U.S. + // Plain files and folder names to match both week-only and week+date syntax + // Their relative ordering depends on week numbering + const child0: TFile = mockTFile('------', 'md') + const child1: TFile = mockTFile('A 2021-W10 (03-05)', 'md') // Tue date, (ISO) week number invalid, ignored + const child2: TFolder = mockTFolder('B ISO:2021-03-08 US:2021-03-01 2021-W10') + const child3: TFile = mockTFile('C 2021-W51 (12-17)', 'md') // Tue date, (ISO) week number invalid, ignored + const child4: TFile = mockTFile('D ISO:2021-12-20 US:2021-12-13 2021-W51', 'md') + const child5: TFolder = mockTFolder('E 2021-W1 (01-01)') // Tue date, to (ISO) week number invalid, ignored + const child6: TFolder = mockTFolder('F ISO:2021-01-04 US:2020-12-28 2021-W1') + const child7: TFile = mockTFile('FFF2 ISO:2021-12-27 US:2021-12-20 2021-W52', 'md') + const child8: TFile = mockTFile('FFF1 ISO:2022-01-03 US:2021-12-27 2021-W53', 'md') // Invalid week, should fall to next year + + return mockTFolder(name, [child0, child1, child2, child3, child4, child5, child6, child7, child8]) +} diff --git a/src/test/unit/sorting-spec-processor.spec.ts b/src/test/unit/sorting-spec-processor.spec.ts index fb358cc64..ec6c0262e 100644 --- a/src/test/unit/sorting-spec-processor.spec.ts +++ b/src/test/unit/sorting-spec-processor.spec.ts @@ -7,6 +7,9 @@ import { convertPlainStringToRegex, Date_dd_Mmm_yyyy_NormalizerFn, Date_Mmm_dd_yyyy_NormalizerFn, + Date_yyyy_Www_mm_dd_NormalizerFn, + Date_yyyy_Www_NormalizerFn, + Date_yyyy_WwwISO_NormalizerFn, detectSortingSymbols, escapeRegexUnsafeCharacters, extractSortingSymbol, @@ -367,6 +370,21 @@ const expectedSortSpecsExampleA: { [key: string]: CustomSortSpec } = { } } +const txtInputExampleSortingSymbols: string = ` +/folders Chapter \\.d+ ... +/:files ...section \\-r+. +% Appendix \\-d+ (attachments) +Plain syntax\\R+ ... works? +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 +\\[yyyy-Www (mm-dd)] Week number ignored +Week number interpreted in ISO standard \\[yyyy-WwwISO] +Week number interpreted in U.S. standard \\[yyyy-Www] +` + const expectedSortSpecsExampleSortingSymbols: { [key: string]: CustomSortSpec } = { "mock-folder": { groups: [{ @@ -427,26 +445,32 @@ const expectedSortSpecsExampleSortingSymbols: { [key: string]: CustomSortSpec } 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.ExactName, + regexPrefix: { + regex: /^ *(\d{4}-W\d{1,2} \(\d{2}-\d{2}\)) Week number ignored$/i, + normalizerFn: Date_yyyy_Www_mm_dd_NormalizerFn + } + }, { + type: CustomSortGroupType.ExactName, + regexPrefix: { + regex: /^Week number interpreted in ISO standard *(\d{4}-W\d{1,2})$/i, + normalizerFn: Date_yyyy_WwwISO_NormalizerFn + } + }, { + type: CustomSortGroupType.ExactName, + regexPrefix: { + regex: /^Week number interpreted in U\.S\. standard *(\d{4}-W\d{1,2})$/i, + normalizerFn: Date_yyyy_Www_NormalizerFn + } }, { type: CustomSortGroupType.Outsiders }], targetFoldersPaths: ['mock-folder'], - outsidersGroupIdx: 9 + outsidersGroupIdx: 12 } } -const txtInputExampleSortingSymbols: string = ` -/folders Chapter \\.d+ ... -/:files ...section \\-r+. -% Appendix \\-d+ (attachments) -Plain syntax\\R+ ... works? -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 -` - // Tricky elements captured: // - Order a-z. for by metadata is transformed to a-z (there is no notion of 'file extension' in metadata values) @@ -520,8 +544,6 @@ const expectedSortSpecsExampleMDataExtractors2: { [key: string]: CustomSortSpec } } - - describe('SortingSpecProcessor', () => { let processor: SortingSpecProcessor; beforeEach(() => { diff --git a/src/test/unit/week-of-year.spec.ts b/src/test/unit/week-of-year.spec.ts new file mode 100644 index 000000000..7a5aaf6d3 --- /dev/null +++ b/src/test/unit/week-of-year.spec.ts @@ -0,0 +1,51 @@ +import {_unitTests, getDateForWeekOfYear} from "../../utils/week-of-year" + +const paramsForWeekOf1stOfJan = [ + [2015,'2014-12-29T00:00:00.000Z','same as U.S.'], // 1st Jan on Thu, ISO = U.S. + [2020,'2019-12-30T00:00:00.000Z','same as U.S.'], // 1st Jan on Wed, ISO = U.S. + [2021,'2020-12-28T00:00:00.000Z','2021-01-04T00:00:00.000Z'], // 1st Jan on Fri, ISO != U.S. + [2022,'2021-12-27T00:00:00.000Z','2022-01-03T00:00:00.000Z'], // 1st Jan on Sat, ISO != U.S. + [2023,'2022-12-26T00:00:00.000Z','2023-01-02T00:00:00.000Z'], // 1st Jan on Sun, ISO != U.S. + [2024,'2024-01-01T00:00:00.000Z','same as U.S.'], // 1st Jan on Mon, ISO = U.S. + [2025,'2024-12-30T00:00:00.000Z','same as U.S.'] // 1st Jan on Wed, ISO = U.S. +] + +const paramsFor10thWeek = [ + [2019,'2019-03-04T00:00:00.000Z','same as U.S.'], + [1999,'1999-03-01T00:00:00.000Z','1999-03-08T00:00:00.000Z'], + [1683,'1683-03-01T00:00:00.000Z','1683-03-08T00:00:00.000Z'], + [1410,'1410-03-05T00:00:00.000Z','same as U.S.'], + [1996,'1996-03-04T00:00:00.000Z','same as U.S.'], + [2023,'2023-02-27T00:00:00.000Z','2023-03-06T00:00:00.000Z'], + [2025,'2025-03-03T00:00:00.000Z','same as U.S.'] +] + +describe('calculateMondayDateIn2stWeekOfYear', () => { + it.each(paramsForWeekOf1stOfJan)('year >%s< should result in %s (U.S.) and %s (ISO)', (year: number, dateOfMondayUS: string, dateOfMondayISO: string) => { + const dateUS = new Date(dateOfMondayUS).getTime() + const dateISO = 'same as U.S.' === dateOfMondayISO ? dateUS : new Date(dateOfMondayISO).getTime() + const mondayData = _unitTests.calculateMondayDateIn2stWeekOfYear(year) + expect(mondayData.mondayDateOf1stWeekUS).toBe(dateUS) + expect(mondayData.mondayDateOf1stWeekISO).toBe(dateISO) + }) +}) + +describe('getDateForWeekOfYear', () => { + it.each(paramsForWeekOf1stOfJan)('For year >%s< 1st week should start on %s (U.S.) and %s (ISO)', (year: number, dateOfMondayUS: string, dateOfMondayISO: string) => { + const dateUS = new Date(dateOfMondayUS) + const dateISO = 'same as U.S.' === dateOfMondayISO ? dateUS : new Date(dateOfMondayISO) + expect(getDateForWeekOfYear(year, 1)).toStrictEqual(dateUS) + expect(getDateForWeekOfYear(year, 1, true)).toStrictEqual(dateISO) + }) + it.each(paramsFor10thWeek)('For year >%s< 10th week should start on %s (U.S.) and %s (ISO)', (year: number, dateOfMondayUS: string, dateOfMondayISO: string) => { + const dateUS = new Date(dateOfMondayUS) + const dateISO = 'same as U.S.' === dateOfMondayISO ? dateUS : new Date(dateOfMondayISO) + expect(getDateForWeekOfYear(year, 10)).toStrictEqual(dateUS) + expect(getDateForWeekOfYear(year, 10, true)).toStrictEqual(dateISO) + }) + it('should correctly handle edge case - a year spanning 54 weeks (leap year staring on Sun)', () => { + // This works in U.S. standard only, where 1st week can start on Sunday + expect(getDateForWeekOfYear(2012,1)).toStrictEqual(new Date('2011-12-26T00:00:00.000Z')) + expect(getDateForWeekOfYear(2012,54)).toStrictEqual(new Date('2012-12-31T00:00:00.000Z')) + }) +}) diff --git a/src/utils/week-of-year.ts b/src/utils/week-of-year.ts new file mode 100644 index 000000000..41eab026f --- /dev/null +++ b/src/utils/week-of-year.ts @@ -0,0 +1,56 @@ + +// Cache of start of years and number of days in the 1st week +interface MondayCache { + year: number // full year, e.g. 2015 + mondayDateOf1stWeekUS: number // U.S. standard, can be in Dec of previous year + mondayDateOf1stWeekISO: number // ISO standard, when the first Thursday of the year determines week numbering +} + +type YEAR = number +const DAY_OF_MILIS = 60*60*24*1000 +const DAYS_IN_WEEK = 7 + +const MondaysCache: { [key: YEAR]: MondayCache } = {} + +const calculateMondayDateIn1stWeekOfYear = (year: number): MondayCache => { + const firstSecondOfYear = new Date(`${year}-01-01T00:00:00.000Z`) + const SUNDAY = 0 + const MONDAY = 1 + const THURSDAY = 4 + const FRIDAY = 5 + const SATURDAY = 6 + + const dayOfWeek = firstSecondOfYear.getDay() + let daysToPrevMonday: number = 0 // For the Monday itself + if (dayOfWeek === SUNDAY) { // Sunday + daysToPrevMonday = DAYS_IN_WEEK - 1 + } else if (dayOfWeek > MONDAY) { // Tue - Sat + daysToPrevMonday = dayOfWeek - MONDAY + } + + // for U.S. the first week is the one with Jan 1st, + // for ISO standard, the first week is the one which contains the 1st Thursday of the year + const useISOoffset = [FRIDAY, SATURDAY, SUNDAY].includes(dayOfWeek) ? DAYS_IN_WEEK : 0 + + return { + year: year, + mondayDateOf1stWeekUS: new Date(firstSecondOfYear).setDate(firstSecondOfYear.getDate() - daysToPrevMonday), + mondayDateOf1stWeekISO: new Date(firstSecondOfYear).setDate(firstSecondOfYear.getDate() - daysToPrevMonday + useISOoffset), + } +} + +// Week number = 1 to 54, U.S. standard by default, can also work in ISO (parameter driven) +export const getDateForWeekOfYear = (year: number, weekNumber: number, useISO?: boolean): Date => { + const WEEK_OF_MILIS = DAYS_IN_WEEK * DAY_OF_MILIS + const dataOfMondayIn1stWeekOfYear = (MondaysCache[year] ??= calculateMondayDateIn1stWeekOfYear(year)) + const mondayOfTheRequestedWeek = new Date( + (useISO ? dataOfMondayIn1stWeekOfYear.mondayDateOf1stWeekISO : dataOfMondayIn1stWeekOfYear.mondayDateOf1stWeekUS) + + (weekNumber-1)*WEEK_OF_MILIS + ) + + return mondayOfTheRequestedWeek +} + +export const _unitTests = { + calculateMondayDateIn2stWeekOfYear: calculateMondayDateIn1stWeekOfYear +} From b142d1951e2300ab91ab10f8edb5c41a839c49c1 Mon Sep 17 00:00:00 2001 From: SebastianMC <23032356+SebastianMC@users.noreply.github.com> Date: Tue, 14 Jan 2025 17:37:02 +0100 Subject: [PATCH 2/6] #178 - week-number based date extraction patterns for titles - more unit tests #191 - added two obvious date formats yyyy-mm-dd and yyyy-dd-mm --- src/custom-sort/matchers.ts | 8 ++- src/custom-sort/sorting-spec-processor.ts | 24 ++++++- src/test/int/dates-in-names.int.test.ts | 84 ++++++++++++++++++++--- src/test/unit/matchers.spec.ts | 29 +++++++- 4 files changed, 132 insertions(+), 13 deletions(-) diff --git a/src/custom-sort/matchers.ts b/src/custom-sort/matchers.ts index b28543a46..7d604d798 100644 --- a/src/custom-sort/matchers.ts +++ b/src/custom-sort/matchers.ts @@ -10,11 +10,15 @@ export const NumberRegexStr: string = ' *(\\d+)'; // Plain number export const CompoundNumberDotRegexStr: string = ' *(\\d+(?:\\.\\d+)*)'; // Compound number with dot as separator export const CompoundNumberDashRegexStr: string = ' *(\\d+(?:-\\d+)*)'; // Compound number with dash as separator +export const Date_yyyy_mm_dd_RegexStr: string = ' *(\\d{4}-\\d{2}-\\d{2})' +export const Date_yyyy_dd_mm_RegexStr: string = Date_yyyy_mm_dd_RegexStr + 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 Date_yyyy_Www_mm_dd_RegexStr: string = ' *(\\d{4}-W\\d{1,2} \\(\\d{2}-\\d{2}\\))' -export const Date_yyyy_Www_RegexStr: string = ' *(\\d{4}-W\\d{1,2})' +export const Date_yyyy_WwwISO_RegexStr: string = ' *(\\d{4}-W\\d{1,2})' +export const Date_yyyy_Www_RegexStr: string = Date_yyyy_WwwISO_RegexStr export const DOT_SEPARATOR = '.' export const DASH_SEPARATOR = '-' @@ -128,6 +132,8 @@ export function getNormalizedDate_NormalizerFn_for(separator: string, dayIdx: nu } } +export const getNormalizedDate_yyyy_mm_dd_NormalizerFn = getNormalizedDate_NormalizerFn_for('-', 2, 1, 0) +export const getNormalizedDate_yyyy_dd_mm_NormalizerFn = getNormalizedDate_NormalizerFn_for('-', 1, 2, 0) 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 2d9760732..04c976ebd 100644 --- a/src/custom-sort/sorting-spec-processor.ts +++ b/src/custom-sort/sorting-spec-processor.ts @@ -24,6 +24,8 @@ import { DOT_SEPARATOR, getNormalizedDate_dd_Mmm_yyyy_NormalizerFn, getNormalizedDate_Mmm_dd_yyyy_NormalizerFn, + getNormalizedDate_yyyy_mm_dd_NormalizerFn, + getNormalizedDate_yyyy_dd_mm_NormalizerFn, getNormalizedDate_yyyy_Www_mm_dd_NormalizerFn, getNormalizedDate_yyyy_WwwISO_NormalizerFn, getNormalizedDate_yyyy_Www_NormalizerFn, @@ -32,7 +34,7 @@ import { NumberRegexStr, RomanNumberRegexStr, WordInAnyLanguageRegexStr, - WordInASCIIRegexStr + WordInASCIIRegexStr, Date_yyyy_WwwISO_RegexStr, Date_yyyy_mm_dd_RegexStr, Date_yyyy_dd_mm_RegexStr } from "./matchers"; import { FolderWildcardMatching, @@ -357,6 +359,8 @@ const InlineRegexSymbol_Digit1: string = '\\d' const InlineRegexSymbol_Digit2: string = '\\[0-9]' const InlineRegexSymbol_0_to_3: string = '\\[0-3]' +const Date_yyyy_mm_dd_RegexSymbol: string = '\\[yyyy-mm-dd]' +const Date_yyyy_dd_mm_RegexSymbol: string = '\\[yyyy-dd-mm]' const Date_dd_Mmm_yyyy_RegexSymbol: string = '\\[dd-Mmm-yyyy]' const Date_Mmm_dd_yyyy_RegexSymbol: string = '\\[Mmm-dd-yyyy]' const Date_yyyy_Www_mm_dd_RegexSymbol: string = '\\[yyyy-Www (mm-dd)]' @@ -381,6 +385,8 @@ const sortingSymbolsArr: Array = [ escapeRegexUnsafeCharacters(CompoundRomanNumberDashRegexSymbol), escapeRegexUnsafeCharacters(WordInASCIIRegexSymbol), escapeRegexUnsafeCharacters(WordInAnyLanguageRegexSymbol), + escapeRegexUnsafeCharacters(Date_yyyy_mm_dd_RegexSymbol), + escapeRegexUnsafeCharacters(Date_yyyy_dd_mm_RegexSymbol), escapeRegexUnsafeCharacters(Date_dd_Mmm_yyyy_RegexSymbol), escapeRegexUnsafeCharacters(Date_Mmm_dd_yyyy_RegexSymbol), escapeRegexUnsafeCharacters(Date_yyyy_Www_mm_dd_RegexSymbol), @@ -453,6 +459,8 @@ export const CompoundDashRomanNumberNormalizerFn: NormalizerFn = (s: string) => export const NumberNormalizerFn: NormalizerFn = (s: string) => getNormalizedNumber(s) export const CompoundDotNumberNormalizerFn: NormalizerFn = (s: string) => getNormalizedNumber(s, DOT_SEPARATOR) export const CompoundDashNumberNormalizerFn: NormalizerFn = (s: string) => getNormalizedNumber(s, DASH_SEPARATOR) +export const Date_yyyy_mm_dd_NormalizerFn: NormalizerFn = (s: string) => getNormalizedDate_yyyy_mm_dd_NormalizerFn(s) +export const Date_yyyy_dd_mm_NormalizerFn: NormalizerFn = (s: string) => getNormalizedDate_yyyy_dd_mm_NormalizerFn(s) 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 const Date_yyyy_Www_mm_dd_NormalizerFn: NormalizerFn = (s: string) => getNormalizedDate_yyyy_Www_mm_dd_NormalizerFn(s) @@ -469,6 +477,8 @@ export enum AdvancedRegexType { CompoundDashRomanNumber, WordInASCII, WordInAnyLanguage, + Date_yyyy_mm_dd, + Date_yyyy_dd_mm, Date_dd_Mmm_yyyy, Date_Mmm_dd_yyyy, Date_yyyy_Www_mm_dd_yyyy, @@ -518,6 +528,16 @@ const sortingSymbolToRegexpStr: { [key: string]: RegExpSpecStr } = { advancedRegexType: AdvancedRegexType.WordInAnyLanguage, unicodeRegex: true }, + [Date_yyyy_mm_dd_RegexSymbol]: { // Intentionally retain character case + regexpStr: Date_yyyy_mm_dd_RegexStr, + normalizerFn: Date_yyyy_mm_dd_NormalizerFn, + advancedRegexType: AdvancedRegexType.Date_yyyy_mm_dd + }, + [Date_yyyy_dd_mm_RegexSymbol]: { // Intentionally retain character case + regexpStr: Date_yyyy_dd_mm_RegexStr, + normalizerFn: Date_yyyy_dd_mm_NormalizerFn, + advancedRegexType: AdvancedRegexType.Date_yyyy_dd_mm + }, [Date_dd_Mmm_yyyy_RegexSymbol]: { // Intentionally retain character case regexpStr: Date_dd_Mmm_yyyy_RegexStr, normalizerFn: Date_dd_Mmm_yyyy_NormalizerFn, @@ -534,7 +554,7 @@ const sortingSymbolToRegexpStr: { [key: string]: RegExpSpecStr } = { advancedRegexType: AdvancedRegexType.Date_yyyy_Www_mm_dd_yyyy }, [Date_yyyy_WwwISO_RegexSymbol]: { // Intentionally retain character case - regexpStr: Date_yyyy_Www_RegexStr, + regexpStr: Date_yyyy_WwwISO_RegexStr, normalizerFn: Date_yyyy_WwwISO_NormalizerFn, advancedRegexType: AdvancedRegexType.Date_yyyy_WwwISO }, diff --git a/src/test/int/dates-in-names.int.test.ts b/src/test/int/dates-in-names.int.test.ts index 8d4c74081..341dd852d 100644 --- a/src/test/int/dates-in-names.int.test.ts +++ b/src/test/int/dates-in-names.int.test.ts @@ -7,7 +7,11 @@ import { DEFAULT_FOLDER_CTIME, determineFolderDatesIfNeeded, determineSortingGroup, - FolderItemForSorting, OS_alphabetical, OS_byCreatedTime, ProcessingContext, sortFolderItems + FolderItemForSorting, + OS_alphabetical, + OS_byCreatedTime, + ProcessingContext, + sortFolderItems } from "../../custom-sort/custom-sort"; import { CustomSortGroupType, @@ -29,11 +33,12 @@ import { } from "../../custom-sort/sorting-spec-processor"; describe('sortFolderItems', () => { - it('should correctly handle Mmm-dd-yyyy pattern in file names', () => { + it('should correctly handle Mmm-dd-yyyy pattern in file and folder names', () => { // given const processor: SortingSpecProcessor = new SortingSpecProcessor() const sortSpecTxt = -` ... \\[Mmm-dd-yyyy] +` + ... \\[Mmm-dd-yyyy] > a-z ` const PARENT_PATH = 'parent/folder/path' @@ -60,11 +65,12 @@ describe('sortFolderItems', () => { 'AAA Jan-01-2012' ]) }) - it('should correctly handle yyyy-Www (mm-dd) pattern in file names', () => { + it('should correctly handle yyyy-Www (mm-dd) pattern in file and folder names', () => { // given const processor: SortingSpecProcessor = new SortingSpecProcessor() const sortSpecTxt = -` ... \\[yyyy-Www (mm-dd)] +` + ... \\[yyyy-Www (mm-dd)] < a-z ------ ` @@ -94,11 +100,12 @@ describe('sortFolderItems', () => { "------.md" ]) }) - it('should correctly handle yyyy-WwwISO pattern in file names', () => { + it('should correctly handle yyyy-WwwISO pattern in file and folder names', () => { // given const processor: SortingSpecProcessor = new SortingSpecProcessor() const sortSpecTxt = -` /+ ... \\[yyyy-Www (mm-dd)] +` + /+ ... \\[yyyy-Www (mm-dd)] /+ ... \\[yyyy-WwwISO] < a-z ` @@ -132,12 +139,58 @@ describe('sortFolderItems', () => { "------.md" ]) }) - it('should correctly handle yyyy-Www pattern in file names', () => { + it('should correctly handle yyyy-Www pattern in file and folder names', () => { + // given + const processor: SortingSpecProcessor = new SortingSpecProcessor() + const sortSpecTxt = +` + /+ ... \\[yyyy-Www (mm-dd)] + /+ ... \\[yyyy-Www] + > a-z + ... \\-d+ +` + 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 = mockTFolderWithDateWeekNamedChildrenForISOvsUSweekNumberingTest(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 + // U.S. standard of weeks numbering + const orderedNames = result.map(f => f.name) + expect(orderedNames).toEqual([ + 'FFF1 ISO:2022-01-03 US:2021-12-27 2021-W53.md', + 'FFF2 ISO:2021-12-27 US:2021-12-20 2021-W52.md', + 'C 2021-W51 (12-17).md', + 'D ISO:2021-12-20 US:2021-12-13 2021-W51.md', + 'A 2021-W10 (03-05).md', + 'B ISO:2021-03-08 US:2021-03-01 2021-W10', + 'E 2021-W1 (01-01)', + 'F ISO:2021-01-04 US:2020-12-28 2021-W1', + "------.md" + ]) + }) + it('should correctly mix for sorting different date formats in file and folder names', () => { // given const processor: SortingSpecProcessor = new SortingSpecProcessor() const sortSpecTxt = -` /+ ... \\[yyyy-Www (mm-dd)] +` + /+ ... \\[yyyy-Www (mm-dd)] /+ ... \\[yyyy-Www] + /+ ... mm-dd \\[yyyy-mm-dd] + /+ ... dd-mm \\[yyyy-dd-mm] + /+ ... \\[yyyy-mm-dd] + /+ ... \\[Mmm-dd-yyyy] + /+ \\[dd-Mmm-yyyy] ... > a-z ` const PARENT_PATH = 'parent/folder/path' @@ -148,6 +201,14 @@ describe('sortFolderItems', () => { ) const folder: TFolder = mockTFolderWithDateWeekNamedChildrenForISOvsUSweekNumberingTest(PARENT_PATH) + folder.children.push(...[ + mockTFile('File 2021-12-14', 'md'), + mockTFile('File mm-dd 2020-12-30', 'md'), // mm-dd + mockTFile('File dd-mm 2020-31-12', 'md'), // dd-mm + mockTFile('File Mar-08-2021', 'md'), + mockTFile('18-Dec-2021 file', 'md'), + ]) + const sortSpec: CustomSortSpec = sortSpecsCollection?.sortSpecByPath![PARENT_PATH]! const ctx: ProcessingContext = {} @@ -161,11 +222,16 @@ describe('sortFolderItems', () => { expect(orderedNames).toEqual([ 'FFF1 ISO:2022-01-03 US:2021-12-27 2021-W53.md', 'FFF2 ISO:2021-12-27 US:2021-12-20 2021-W52.md', + "18-Dec-2021 file.md", 'C 2021-W51 (12-17).md', + "File 2021-12-14.md", 'D ISO:2021-12-20 US:2021-12-13 2021-W51.md', + "File Mar-08-2021.md", 'A 2021-W10 (03-05).md', 'B ISO:2021-03-08 US:2021-03-01 2021-W10', 'E 2021-W1 (01-01)', + "File dd-mm 2020-31-12.md", + "File mm-dd 2020-12-30.md", 'F ISO:2021-01-04 US:2020-12-28 2021-W1', "------.md" ]) diff --git a/src/test/unit/matchers.spec.ts b/src/test/unit/matchers.spec.ts index 7515e37c5..0a4b9862f 100644 --- a/src/test/unit/matchers.spec.ts +++ b/src/test/unit/matchers.spec.ts @@ -10,7 +10,12 @@ import { CompoundRomanNumberDotRegexStr, CompoundRomanNumberDashRegexStr, WordInASCIIRegexStr, - WordInAnyLanguageRegexStr, getNormalizedDate_dd_Mmm_yyyy_NormalizerFn + WordInAnyLanguageRegexStr, + getNormalizedDate_dd_Mmm_yyyy_NormalizerFn, + getNormalizedDate_yyyy_Www_NormalizerFn, + getNormalizedDate_yyyy_Www_mm_dd_NormalizerFn, + getNormalizedDate_yyyy_dd_mm_NormalizerFn, + getNormalizedDate_yyyy_mm_dd_NormalizerFn } from "../../custom-sort/matchers"; describe('Plain numbers regexp', () => { @@ -431,3 +436,25 @@ describe('getNormalizedDate_dd_Mmm_yyyy_NormalizerFn', () => { expect(getNormalizedDate_dd_Mmm_yyyy_NormalizerFn(s)).toBe(out) }) }) + +describe('getNormalizedDate_yyyy_dd_mm_NormalizerFn', () => { + const params = [ + ['2012-13-01', '2012-01-13//', '2012-13-01//'], + ['0001-03-02', '0001-02-03//', '0001-03-02//'], + ['7777-09-1234', '7777-1234-09//', '7777-09-1234//'], + ]; + it.each(params)('>%s< should become %s', (s: string, outForDDMM: string, outForMMDD: string) => { + expect(getNormalizedDate_yyyy_dd_mm_NormalizerFn(s)).toBe(outForDDMM) + expect(getNormalizedDate_yyyy_mm_dd_NormalizerFn(s)).toBe(outForMMDD) + }) +}) + +describe('getNormalizedDate_yyyy_Www_mm_dd_NormalizerFn', () => { + const params = [ + ['2012-W0 (01-13)', '2012-01-13//'], + ['0002-W12 (02-03)', '0002-02-03//'], + ]; + it.each(params)('>%s< should become %s', (s: string, out: string) => { + expect(getNormalizedDate_yyyy_Www_mm_dd_NormalizerFn(s)).toBe(out) + }) +}) From 4eb5d7c120118b0ab9fa56bbcbcc1530f1205ba3 Mon Sep 17 00:00:00 2001 From: SebastianMC <23032356+SebastianMC@users.noreply.github.com> Date: Tue, 14 Jan 2025 17:41:06 +0100 Subject: [PATCH 3/6] Imports optimized in sorting-spec-processor.ts --- src/custom-sort/sorting-spec-processor.ts | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/custom-sort/sorting-spec-processor.ts b/src/custom-sort/sorting-spec-processor.ts index 04c976ebd..0217f3ec3 100644 --- a/src/custom-sort/sorting-spec-processor.ts +++ b/src/custom-sort/sorting-spec-processor.ts @@ -19,22 +19,25 @@ import { DASH_SEPARATOR, Date_dd_Mmm_yyyy_RegexStr, Date_Mmm_dd_yyyy_RegexStr, + Date_yyyy_dd_mm_RegexStr, + Date_yyyy_mm_dd_RegexStr, Date_yyyy_Www_mm_dd_RegexStr, Date_yyyy_Www_RegexStr, + Date_yyyy_WwwISO_RegexStr, DOT_SEPARATOR, getNormalizedDate_dd_Mmm_yyyy_NormalizerFn, getNormalizedDate_Mmm_dd_yyyy_NormalizerFn, - getNormalizedDate_yyyy_mm_dd_NormalizerFn, getNormalizedDate_yyyy_dd_mm_NormalizerFn, + getNormalizedDate_yyyy_mm_dd_NormalizerFn, getNormalizedDate_yyyy_Www_mm_dd_NormalizerFn, - getNormalizedDate_yyyy_WwwISO_NormalizerFn, getNormalizedDate_yyyy_Www_NormalizerFn, + getNormalizedDate_yyyy_WwwISO_NormalizerFn, getNormalizedNumber, getNormalizedRomanNumber, NumberRegexStr, RomanNumberRegexStr, WordInAnyLanguageRegexStr, - WordInASCIIRegexStr, Date_yyyy_WwwISO_RegexStr, Date_yyyy_mm_dd_RegexStr, Date_yyyy_dd_mm_RegexStr + WordInASCIIRegexStr } from "./matchers"; import { FolderWildcardMatching, @@ -43,10 +46,7 @@ import { MATCH_CHILDREN_2_SUFFIX, NO_PRIORITY } from "./folder-matching-rules" -import { - MDataExtractor, - tryParseAsMDataExtractorSpec -} from "./mdata-extractors"; +import {MDataExtractor, tryParseAsMDataExtractorSpec} from "./mdata-extractors"; interface ProcessingContext { folderPath: string From 22049824851c85c5e9451af06b5bf9e73ca27d2e Mon Sep 17 00:00:00 2001 From: SebastianMC <23032356+SebastianMC@users.noreply.github.com> Date: Tue, 14 Jan 2025 17:57:17 +0100 Subject: [PATCH 4/6] #178, #191 - improved regexps for dates matching - e.g. \d{1,2} changes to [0-3]*[0-9] --- src/custom-sort/matchers.ts | 6 ++--- src/test/unit/sorting-spec-processor.spec.ts | 24 ++++++++++++++++---- 2 files changed, 23 insertions(+), 7 deletions(-) diff --git a/src/custom-sort/matchers.ts b/src/custom-sort/matchers.ts index 7d604d798..2cf55cbdd 100644 --- a/src/custom-sort/matchers.ts +++ b/src/custom-sort/matchers.ts @@ -10,14 +10,14 @@ export const NumberRegexStr: string = ' *(\\d+)'; // Plain number export const CompoundNumberDotRegexStr: string = ' *(\\d+(?:\\.\\d+)*)'; // Compound number with dot as separator export const CompoundNumberDashRegexStr: string = ' *(\\d+(?:-\\d+)*)'; // Compound number with dash as separator -export const Date_yyyy_mm_dd_RegexStr: string = ' *(\\d{4}-\\d{2}-\\d{2})' +export const Date_yyyy_mm_dd_RegexStr: string = ' *(\\d{4}-[0-3]*[0-9]-[0-3]*[0-9])' export const Date_yyyy_dd_mm_RegexStr: string = Date_yyyy_mm_dd_RegexStr 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 Date_yyyy_Www_mm_dd_RegexStr: string = ' *(\\d{4}-W\\d{1,2} \\(\\d{2}-\\d{2}\\))' -export const Date_yyyy_WwwISO_RegexStr: string = ' *(\\d{4}-W\\d{1,2})' +export const Date_yyyy_Www_mm_dd_RegexStr: string = ' *(\\d{4}-W[0-5]*[0-9] \\([0-3]*[0-9]-[0-3]*[0-9]\\))' +export const Date_yyyy_WwwISO_RegexStr: string = ' *(\\d{4}-W[0-5]*[0-9])' export const Date_yyyy_Www_RegexStr: string = Date_yyyy_WwwISO_RegexStr export const DOT_SEPARATOR = '.' diff --git a/src/test/unit/sorting-spec-processor.spec.ts b/src/test/unit/sorting-spec-processor.spec.ts index ec6c0262e..a83a4627d 100644 --- a/src/test/unit/sorting-spec-processor.spec.ts +++ b/src/test/unit/sorting-spec-processor.spec.ts @@ -7,6 +7,8 @@ import { convertPlainStringToRegex, Date_dd_Mmm_yyyy_NormalizerFn, Date_Mmm_dd_yyyy_NormalizerFn, + Date_yyyy_dd_mm_NormalizerFn, + Date_yyyy_mm_dd_NormalizerFn, Date_yyyy_Www_mm_dd_NormalizerFn, Date_yyyy_Www_NormalizerFn, Date_yyyy_WwwISO_NormalizerFn, @@ -383,6 +385,8 @@ Here goes ASCII word \\a+ \\[yyyy-Www (mm-dd)] Week number ignored Week number interpreted in ISO standard \\[yyyy-WwwISO] Week number interpreted in U.S. standard \\[yyyy-Www] +\\[yyyy-mm-dd] plain spec 1 +\\[yyyy-dd-mm] plain spec 2 ` const expectedSortSpecsExampleSortingSymbols: { [key: string]: CustomSortSpec } = { @@ -448,26 +452,38 @@ const expectedSortSpecsExampleSortingSymbols: { [key: string]: CustomSortSpec } }, { type: CustomSortGroupType.ExactName, regexPrefix: { - regex: /^ *(\d{4}-W\d{1,2} \(\d{2}-\d{2}\)) Week number ignored$/i, + regex: /^ *(\d{4}-W[0-5]*[0-9] \([0-3]*[0-9]-[0-3]*[0-9]\)) Week number ignored$/i, normalizerFn: Date_yyyy_Www_mm_dd_NormalizerFn } }, { type: CustomSortGroupType.ExactName, regexPrefix: { - regex: /^Week number interpreted in ISO standard *(\d{4}-W\d{1,2})$/i, + regex: /^Week number interpreted in ISO standard *(\d{4}-W[0-5]*[0-9])$/i, normalizerFn: Date_yyyy_WwwISO_NormalizerFn } }, { type: CustomSortGroupType.ExactName, regexPrefix: { - regex: /^Week number interpreted in U\.S\. standard *(\d{4}-W\d{1,2})$/i, + regex: /^Week number interpreted in U\.S\. standard *(\d{4}-W[0-5]*[0-9])$/i, normalizerFn: Date_yyyy_Www_NormalizerFn } + }, { + type: CustomSortGroupType.ExactName, + regexPrefix: { + regex: /^ *(\d{4}-[0-3]*[0-9]-[0-3]*[0-9]) plain spec 1$/i, + normalizerFn: Date_yyyy_mm_dd_NormalizerFn + } + }, { + type: CustomSortGroupType.ExactName, + regexPrefix: { + regex: /^ *(\d{4}-[0-3]*[0-9]-[0-3]*[0-9]) plain spec 2$/i, + normalizerFn: Date_yyyy_dd_mm_NormalizerFn + } }, { type: CustomSortGroupType.Outsiders }], targetFoldersPaths: ['mock-folder'], - outsidersGroupIdx: 12 + outsidersGroupIdx: 14 } } From f7c69b18f91163131a3688d24b5cdf2e226be352 Mon Sep 17 00:00:00 2001 From: SebastianMC <23032356+SebastianMC@users.noreply.github.com> Date: Tue, 14 Jan 2025 20:57:51 +0100 Subject: [PATCH 5/6] #178 - explicit support for telling if the Www date should be same, earlier than the first day of the week or later than the last day of the week - syntax W1 W1- W1+ --- src/custom-sort/matchers.ts | 42 +++++++++++++++----- src/test/unit/matchers.spec.ts | 16 ++++++++ src/test/unit/sorting-spec-processor.spec.ts | 4 +- src/test/unit/week-of-year.spec.ts | 6 ++- src/utils/week-of-year.ts | 17 +++++--- 5 files changed, 66 insertions(+), 19 deletions(-) diff --git a/src/custom-sort/matchers.ts b/src/custom-sort/matchers.ts index 2cf55cbdd..e10ec5639 100644 --- a/src/custom-sort/matchers.ts +++ b/src/custom-sort/matchers.ts @@ -17,15 +17,19 @@ export const Date_dd_Mmm_yyyy_RegexStr: string = ' *([0-3]*[0-9]-(?:Jan|Feb|Mar| 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 Date_yyyy_Www_mm_dd_RegexStr: string = ' *(\\d{4}-W[0-5]*[0-9] \\([0-3]*[0-9]-[0-3]*[0-9]\\))' -export const Date_yyyy_WwwISO_RegexStr: string = ' *(\\d{4}-W[0-5]*[0-9])' +export const Date_yyyy_WwwISO_RegexStr: string = ' *(\\d{4}-W[0-5]*[0-9][-+]?)' export const Date_yyyy_Www_RegexStr: string = Date_yyyy_WwwISO_RegexStr -export const DOT_SEPARATOR = '.' +export const DOT_SEPARATOR = '.' // ASCII 46 export const DASH_SEPARATOR = '-' -const SLASH_SEPARATOR = '/' // ASCII 47 +const SLASH_SEPARATOR = '/' // ASCII 47, right before ASCII 48 = '0' +const COLON_SEPARATOR = ':' // ASCII 58, first non-digit character const PIPE_SEPARATOR = '|' // ASCII 124 +const EARLIER_THAN_SLASH_SEPARATOR = DOT_SEPARATOR +const LATER_THAN_SLASH_SEPARATOR = COLON_SEPARATOR + export const DEFAULT_NORMALIZATION_PLACES = 8; // Fixed width of a normalized number (with leading zeros) // Property escapes: @@ -62,9 +66,9 @@ export function getNormalizedNumber(s: string = '', separator?: string, places?: // guarantees correct order (/ = ASCII 47, | = ASCII 124) if (separator) { const components: Array = s.split(separator).filter(s => s) - return `${components.map((c) => prependWithZeros(c, places ?? DEFAULT_NORMALIZATION_PLACES)).join(PIPE_SEPARATOR)}//` + return `${components.map((c) => prependWithZeros(c, places ?? DEFAULT_NORMALIZATION_PLACES)).join(PIPE_SEPARATOR)}${SLASH_SEPARATOR}${SLASH_SEPARATOR}` } else { - return `${prependWithZeros(s, places ?? DEFAULT_NORMALIZATION_PLACES)}//` + return `${prependWithZeros(s, places ?? DEFAULT_NORMALIZATION_PLACES)}${SLASH_SEPARATOR}${SLASH_SEPARATOR}` } } @@ -108,9 +112,9 @@ export function getNormalizedRomanNumber(s: string, separator?: string, places?: // guarantees correct order (/ = ASCII 47, | = ASCII 124) if (separator) { const components: Array = s.split(separator).filter(s => s) - return `${components.map((c) => prependWithZeros(romanToIntStr(c), places ?? DEFAULT_NORMALIZATION_PLACES)).join(PIPE_SEPARATOR)}//` + return `${components.map((c) => prependWithZeros(romanToIntStr(c), places ?? DEFAULT_NORMALIZATION_PLACES)).join(PIPE_SEPARATOR)}${SLASH_SEPARATOR}${SLASH_SEPARATOR}` } else { - return `${prependWithZeros(romanToIntStr(s), places ?? DEFAULT_NORMALIZATION_PLACES)}//` + return `${prependWithZeros(romanToIntStr(s), places ?? DEFAULT_NORMALIZATION_PLACES)}${SLASH_SEPARATOR}${SLASH_SEPARATOR}` } } @@ -128,7 +132,7 @@ export function getNormalizedDate_NormalizerFn_for(separator: string, dayIdx: nu 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}//` + return `${year}-${month}-${day}${SLASH_SEPARATOR}${SLASH_SEPARATOR}` } } @@ -137,14 +141,18 @@ export const getNormalizedDate_yyyy_dd_mm_NormalizerFn = getNormalizedDate_Norma 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) +const DateExtractor_orderModifier_earlier_than = '-' +const DateExtractor_orderModifier_later_than = '+' + const DateExtractor_yyyy_Www_mm_dd_Regex = /(\d{4})-W(\d{1,2}) \((\d{2})-(\d{2})\)/ -const DateExtractor_yyyy_Www_Regex = /(\d{4})-W(\d{1,2})/ +const DateExtractor_yyyy_Www_Regex = /(\d{4})-W(\d{1,2})([-+]?)/ // Matching groups const YEAR_IDX = 1 const WEEK_IDX = 2 const MONTH_IDX = 3 const DAY_IDX = 4 +const RELATIVE_ORDER_IDX = 3 // For the yyyy-Www only: yyyy-Www> or yyyy-Www< const DECEMBER = 12 const JANUARY = 1 @@ -157,10 +165,19 @@ export function getNormalizedDate_NormalizerFn_yyyy_Www_mm_dd(consumeWeek: boole let yearNumber = Number.parseInt(yearStr,10) let monthNumber: number let dayNumber: number + let separator = SLASH_SEPARATOR // different values enforce relative > < order of same dates + let useLastDayOfWeek: boolean = false if (consumeWeek) { const weekNumberStr = matches![WEEK_IDX] const weekNumber = Number.parseInt(weekNumberStr, 10) - const dateForWeek = getDateForWeekOfYear(yearNumber, weekNumber, weeksISO) + const orderModifier: string|undefined = matches![RELATIVE_ORDER_IDX] + if (orderModifier === DateExtractor_orderModifier_earlier_than) { + separator = EARLIER_THAN_SLASH_SEPARATOR + } else if (orderModifier === DateExtractor_orderModifier_later_than) { + separator = LATER_THAN_SLASH_SEPARATOR // Will also need to adjust the date to the last day of the week + useLastDayOfWeek = true + } + const dateForWeek = getDateForWeekOfYear(yearNumber, weekNumber, weeksISO, useLastDayOfWeek) monthNumber = dateForWeek.getMonth()+1 // 1 - 12 dayNumber = dateForWeek.getDate() // 1 - 31 // Be careful with edge dates, which can belong to previous or next year @@ -178,7 +195,10 @@ export function getNormalizedDate_NormalizerFn_yyyy_Www_mm_dd(consumeWeek: boole monthNumber = Number.parseInt(matches![MONTH_IDX],10) dayNumber = Number.parseInt(matches![DAY_IDX], 10) } - return `${prependWithZeros(`${yearNumber}`, YEAR_POSITIONS)}-${prependWithZeros(`${monthNumber}`, MONTH_POSITIONS)}-${prependWithZeros(`${dayNumber}`, DAY_POSITIONS)}//` + return `${prependWithZeros(`${yearNumber}`, YEAR_POSITIONS)}` + + `-${prependWithZeros(`${monthNumber}`, MONTH_POSITIONS)}` + + `-${prependWithZeros(`${dayNumber}`, DAY_POSITIONS)}` + + `${separator}${SLASH_SEPARATOR}` } } diff --git a/src/test/unit/matchers.spec.ts b/src/test/unit/matchers.spec.ts index 0a4b9862f..339745fb8 100644 --- a/src/test/unit/matchers.spec.ts +++ b/src/test/unit/matchers.spec.ts @@ -458,3 +458,19 @@ describe('getNormalizedDate_yyyy_Www_mm_dd_NormalizerFn', () => { expect(getNormalizedDate_yyyy_Www_mm_dd_NormalizerFn(s)).toBe(out) }) }) + +describe('getNormalizedDate_yyyy_Www_NormalizerFn', () => { + /* ORDER for week numbers vs. dates of 1st day / last day of the week: + W1 - exactly on the first day of 1st week - the actual title then decides about relative order + W1- - before the first day of 1st week, yet after the last day of prev week) + W1+ - after the last day of 1st week, yet before the first day of next week) + */ + const params = [ + ['2012-W1', '2011-12-26//'], + ['2012-W1+', '2012-01-01:/'], + ['2012-W1-', '2011-12-26./'], + ]; + it.each(params)('>%s< should become %s', (s: string, out: string) => { + expect(getNormalizedDate_yyyy_Www_NormalizerFn(s)).toBe(out) + }) +}) diff --git a/src/test/unit/sorting-spec-processor.spec.ts b/src/test/unit/sorting-spec-processor.spec.ts index a83a4627d..891013c94 100644 --- a/src/test/unit/sorting-spec-processor.spec.ts +++ b/src/test/unit/sorting-spec-processor.spec.ts @@ -458,13 +458,13 @@ const expectedSortSpecsExampleSortingSymbols: { [key: string]: CustomSortSpec } }, { type: CustomSortGroupType.ExactName, regexPrefix: { - regex: /^Week number interpreted in ISO standard *(\d{4}-W[0-5]*[0-9])$/i, + regex: /^Week number interpreted in ISO standard *(\d{4}-W[0-5]*[0-9][-+]?)$/i, normalizerFn: Date_yyyy_WwwISO_NormalizerFn } }, { type: CustomSortGroupType.ExactName, regexPrefix: { - regex: /^Week number interpreted in U\.S\. standard *(\d{4}-W[0-5]*[0-9])$/i, + regex: /^Week number interpreted in U\.S\. standard *(\d{4}-W[0-5]*[0-9][-+]?)$/i, normalizerFn: Date_yyyy_Www_NormalizerFn } }, { diff --git a/src/test/unit/week-of-year.spec.ts b/src/test/unit/week-of-year.spec.ts index 7a5aaf6d3..16eba9917 100644 --- a/src/test/unit/week-of-year.spec.ts +++ b/src/test/unit/week-of-year.spec.ts @@ -43,9 +43,13 @@ describe('getDateForWeekOfYear', () => { expect(getDateForWeekOfYear(year, 10)).toStrictEqual(dateUS) expect(getDateForWeekOfYear(year, 10, true)).toStrictEqual(dateISO) }) - it('should correctly handle edge case - a year spanning 54 weeks (leap year staring on Sun)', () => { + it('should correctly handle edge case - a year spanning 54 weeks (leap year starting on Sun)', () => { + const USstandard = false + const SUNDAY = true // This works in U.S. standard only, where 1st week can start on Sunday expect(getDateForWeekOfYear(2012,1)).toStrictEqual(new Date('2011-12-26T00:00:00.000Z')) + expect(getDateForWeekOfYear(2012,1, USstandard, SUNDAY)).toStrictEqual(new Date('2012-01-01T00:00:00.000Z')) expect(getDateForWeekOfYear(2012,54)).toStrictEqual(new Date('2012-12-31T00:00:00.000Z')) + expect(getDateForWeekOfYear(2012,54, USstandard, SUNDAY)).toStrictEqual(new Date('2013-01-06T00:00:00.000Z')) }) }) diff --git a/src/utils/week-of-year.ts b/src/utils/week-of-year.ts index 41eab026f..e93b8df04 100644 --- a/src/utils/week-of-year.ts +++ b/src/utils/week-of-year.ts @@ -2,8 +2,10 @@ // Cache of start of years and number of days in the 1st week interface MondayCache { year: number // full year, e.g. 2015 - mondayDateOf1stWeekUS: number // U.S. standard, can be in Dec of previous year + mondayDateOf1stWeekUS: number // U.S. standard, the 1st of Jan determines the first week, monday can be in Dec of previous year + sundayDateOf1stWeekUS: number mondayDateOf1stWeekISO: number // ISO standard, when the first Thursday of the year determines week numbering + sundayDateOf1stWeekISO: number } type YEAR = number @@ -35,20 +37,25 @@ const calculateMondayDateIn1stWeekOfYear = (year: number): MondayCache => { return { year: year, mondayDateOf1stWeekUS: new Date(firstSecondOfYear).setDate(firstSecondOfYear.getDate() - daysToPrevMonday), + sundayDateOf1stWeekUS: new Date(firstSecondOfYear).setDate(firstSecondOfYear.getDate() - daysToPrevMonday + DAYS_IN_WEEK - 1), mondayDateOf1stWeekISO: new Date(firstSecondOfYear).setDate(firstSecondOfYear.getDate() - daysToPrevMonday + useISOoffset), + sundayDateOf1stWeekISO: new Date(firstSecondOfYear).setDate(firstSecondOfYear.getDate() - daysToPrevMonday + useISOoffset + DAYS_IN_WEEK - 1), } } // Week number = 1 to 54, U.S. standard by default, can also work in ISO (parameter driven) -export const getDateForWeekOfYear = (year: number, weekNumber: number, useISO?: boolean): Date => { +export const getDateForWeekOfYear = (year: number, weekNumber: number, useISO?: boolean, sunday?: boolean): Date => { const WEEK_OF_MILIS = DAYS_IN_WEEK * DAY_OF_MILIS const dataOfMondayIn1stWeekOfYear = (MondaysCache[year] ??= calculateMondayDateIn1stWeekOfYear(year)) - const mondayOfTheRequestedWeek = new Date( + const mondayOfTheRequestedWeek = (useISO ? dataOfMondayIn1stWeekOfYear.mondayDateOf1stWeekISO : dataOfMondayIn1stWeekOfYear.mondayDateOf1stWeekUS) + (weekNumber-1)*WEEK_OF_MILIS - ) - return mondayOfTheRequestedWeek + const sundayOfTheRequestedWeek = + (useISO ? dataOfMondayIn1stWeekOfYear.sundayDateOf1stWeekISO : dataOfMondayIn1stWeekOfYear.sundayDateOf1stWeekUS) + + (weekNumber-1)*WEEK_OF_MILIS + + return new Date(sunday ? sundayOfTheRequestedWeek : mondayOfTheRequestedWeek) } export const _unitTests = { From 6e7b2e1b6a449afbe8ae1f8f48760f8729b45293 Mon Sep 17 00:00:00 2001 From: SebastianMC <23032356+SebastianMC@users.noreply.github.com> Date: Tue, 14 Jan 2025 21:48:52 +0100 Subject: [PATCH 6/6] #178 - explicit support for telling if the Www date should be same, earlier than the first day of the week or later than the last day of the week - syntax W1 W1- W1+ --- src/custom-sort/matchers.ts | 6 ++-- src/test/int/dates-in-names.int.test.ts | 47 +++++++++++++++++++++++++ src/test/unit/matchers.spec.ts | 2 +- 3 files changed, 51 insertions(+), 4 deletions(-) diff --git a/src/custom-sort/matchers.ts b/src/custom-sort/matchers.ts index e10ec5639..a48133ef2 100644 --- a/src/custom-sort/matchers.ts +++ b/src/custom-sort/matchers.ts @@ -24,11 +24,11 @@ export const DOT_SEPARATOR = '.' // ASCII 46 export const DASH_SEPARATOR = '-' const SLASH_SEPARATOR = '/' // ASCII 47, right before ASCII 48 = '0' -const COLON_SEPARATOR = ':' // ASCII 58, first non-digit character +const GT_SEPARATOR = '>' // ASCII 62, alphabetical sorting in Collator puts it after / const PIPE_SEPARATOR = '|' // ASCII 124 const EARLIER_THAN_SLASH_SEPARATOR = DOT_SEPARATOR -const LATER_THAN_SLASH_SEPARATOR = COLON_SEPARATOR +const LATER_THAN_SLASH_SEPARATOR = GT_SEPARATOR export const DEFAULT_NORMALIZATION_PLACES = 8; // Fixed width of a normalized number (with leading zeros) @@ -152,7 +152,7 @@ const YEAR_IDX = 1 const WEEK_IDX = 2 const MONTH_IDX = 3 const DAY_IDX = 4 -const RELATIVE_ORDER_IDX = 3 // For the yyyy-Www only: yyyy-Www> or yyyy-Www< +const RELATIVE_ORDER_IDX = 3 // For the yyyy-Www only: yyyy-Www- or yyyy-Www+ const DECEMBER = 12 const JANUARY = 1 diff --git a/src/test/int/dates-in-names.int.test.ts b/src/test/int/dates-in-names.int.test.ts index 341dd852d..b94a31d84 100644 --- a/src/test/int/dates-in-names.int.test.ts +++ b/src/test/int/dates-in-names.int.test.ts @@ -236,6 +236,53 @@ describe('sortFolderItems', () => { "------.md" ]) }) + it('should correctly order the week number with specifiers', () => { + // given + const processor: SortingSpecProcessor = new SortingSpecProcessor() + const sortSpecTxt = + ` + /+ \\[yyyy-Www] + /+ \\[yyyy-mm-dd] + > 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 = mockTFolder(PARENT_PATH,[ + // ISO and U.S. standard for 2025 give the same week numbers (remark for clarity) + mockTFile('2025-03-09', 'md'), // sunday of W10 + mockTFile('2025-W11-', 'md'), // earlier than monday of W11 + mockTFile('2025-03-10', 'md'), // monday W11 + mockTFile('2025-W11', 'md'), // monday of W11 + mockTFile('2025-03-16', 'md'), // sunday W11 + mockTFile('2025-W11+', 'md'), // later than sunday W11 // expected + mockTFile('2025-03-17', 'md'), // monday of W12 + ]) + + const sortSpec: CustomSortSpec = sortSpecsCollection?.sortSpecByPath![PARENT_PATH]! + + const ctx: ProcessingContext = {} + + // when + const result: Array = sortFolderItems(folder, folder.children, sortSpec, ctx, OS_alphabetical) + + // then + // U.S. standard of weeks numbering + const orderedNames = result.map(f => f.name) + expect(orderedNames).toEqual([ + "2025-03-17.md", + '2025-W11+.md', + "2025-03-16.md", + '2025-W11.md', + "2025-03-10.md", + "2025-W11-.md", + "2025-03-09.md", + ]) + }) }) diff --git a/src/test/unit/matchers.spec.ts b/src/test/unit/matchers.spec.ts index 339745fb8..7ef0ce46c 100644 --- a/src/test/unit/matchers.spec.ts +++ b/src/test/unit/matchers.spec.ts @@ -467,7 +467,7 @@ describe('getNormalizedDate_yyyy_Www_NormalizerFn', () => { */ const params = [ ['2012-W1', '2011-12-26//'], - ['2012-W1+', '2012-01-01:/'], + ['2012-W1+', '2012-01-01>/'], ['2012-W1-', '2011-12-26./'], ]; it.each(params)('>%s< should become %s', (s: string, out: string) => {