Skip to content

Commit 975f6ee

Browse files
committed
#178 - week-number based date extraction patterns for titles
1 parent f9c9c0b commit 975f6ee

File tree

7 files changed

+377
-18
lines changed

7 files changed

+377
-18
lines changed

src/custom-sort/matchers.ts

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
import {
2+
getDateForWeekOfYear
3+
} from "../utils/week-of-year";
4+
15
export const RomanNumberRegexStr: string = ' *([MDCLXVI]+)'; // Roman number
26
export const CompoundRomanNumberDotRegexStr: string = ' *([MDCLXVI]+(?:\\.[MDCLXVI]+)*)';// Compound Roman number with dot as separator
37
export const CompoundRomanNumberDashRegexStr: string = ' *([MDCLXVI]+(?:-[MDCLXVI]+)*)'; // Compound Roman number with dash as separator
@@ -9,6 +13,9 @@ export const CompoundNumberDashRegexStr: string = ' *(\\d+(?:-\\d+)*)'; // Compo
913
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
1014
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
1115

16+
export const Date_yyyy_Www_mm_dd_RegexStr: string = ' *(\\d{4}-W\\d{1,2} \\(\\d{2}-\\d{2}\\))'
17+
export const Date_yyyy_Www_RegexStr: string = ' *(\\d{4}-W\\d{1,2})'
18+
1219
export const DOT_SEPARATOR = '.'
1320
export const DASH_SEPARATOR = '-'
1421

@@ -123,3 +130,52 @@ export function getNormalizedDate_NormalizerFn_for(separator: string, dayIdx: nu
123130

124131
export const getNormalizedDate_dd_Mmm_yyyy_NormalizerFn = getNormalizedDate_NormalizerFn_for('-', 0, 1, 2, MONTHS)
125132
export const getNormalizedDate_Mmm_dd_yyyy_NormalizerFn = getNormalizedDate_NormalizerFn_for('-', 1, 0, 2, MONTHS)
133+
134+
const DateExtractor_yyyy_Www_mm_dd_Regex = /(\d{4})-W(\d{1,2}) \((\d{2})-(\d{2})\)/
135+
const DateExtractor_yyyy_Www_Regex = /(\d{4})-W(\d{1,2})/
136+
137+
// Matching groups
138+
const YEAR_IDX = 1
139+
const WEEK_IDX = 2
140+
const MONTH_IDX = 3
141+
const DAY_IDX = 4
142+
143+
const DECEMBER = 12
144+
const JANUARY = 1
145+
146+
export function getNormalizedDate_NormalizerFn_yyyy_Www_mm_dd(consumeWeek: boolean, weeksISO?: boolean) {
147+
return (s: string): string | null => {
148+
// Assumption - the regex date matched against input s, no extensive defensive coding needed
149+
const matches = consumeWeek ? DateExtractor_yyyy_Www_Regex.exec(s) : DateExtractor_yyyy_Www_mm_dd_Regex.exec(s)
150+
const yearStr = matches![YEAR_IDX]
151+
let yearNumber = Number.parseInt(yearStr,10)
152+
let monthNumber: number
153+
let dayNumber: number
154+
if (consumeWeek) {
155+
const weekNumberStr = matches![WEEK_IDX]
156+
const weekNumber = Number.parseInt(weekNumberStr, 10)
157+
const dateForWeek = getDateForWeekOfYear(yearNumber, weekNumber, weeksISO)
158+
monthNumber = dateForWeek.getMonth()+1 // 1 - 12
159+
dayNumber = dateForWeek.getDate() // 1 - 31
160+
// Be careful with edge dates, which can belong to previous or next year
161+
if (weekNumber === 1) {
162+
if (monthNumber === DECEMBER) {
163+
yearNumber--
164+
}
165+
}
166+
if (weekNumber >= 50) {
167+
if (monthNumber === JANUARY) {
168+
yearNumber++
169+
}
170+
}
171+
} else { // ignore week
172+
monthNumber = Number.parseInt(matches![MONTH_IDX],10)
173+
dayNumber = Number.parseInt(matches![DAY_IDX], 10)
174+
}
175+
return `${prependWithZeros(`${yearNumber}`, YEAR_POSITIONS)}-${prependWithZeros(`${monthNumber}`, MONTH_POSITIONS)}-${prependWithZeros(`${dayNumber}`, DAY_POSITIONS)}//`
176+
}
177+
}
178+
179+
export const getNormalizedDate_yyyy_Www_mm_dd_NormalizerFn = getNormalizedDate_NormalizerFn_yyyy_Www_mm_dd(false)
180+
export const getNormalizedDate_yyyy_WwwISO_NormalizerFn = getNormalizedDate_NormalizerFn_yyyy_Www_mm_dd(true, true)
181+
export const getNormalizedDate_yyyy_Www_NormalizerFn = getNormalizedDate_NormalizerFn_yyyy_Www_mm_dd(true, false)

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

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,14 @@ import {
1919
DASH_SEPARATOR,
2020
Date_dd_Mmm_yyyy_RegexStr,
2121
Date_Mmm_dd_yyyy_RegexStr,
22+
Date_yyyy_Www_mm_dd_RegexStr,
23+
Date_yyyy_Www_RegexStr,
2224
DOT_SEPARATOR,
2325
getNormalizedDate_dd_Mmm_yyyy_NormalizerFn,
2426
getNormalizedDate_Mmm_dd_yyyy_NormalizerFn,
27+
getNormalizedDate_yyyy_Www_mm_dd_NormalizerFn,
28+
getNormalizedDate_yyyy_WwwISO_NormalizerFn,
29+
getNormalizedDate_yyyy_Www_NormalizerFn,
2530
getNormalizedNumber,
2631
getNormalizedRomanNumber,
2732
NumberRegexStr,
@@ -354,6 +359,9 @@ const InlineRegexSymbol_0_to_3: string = '\\[0-3]'
354359

355360
const Date_dd_Mmm_yyyy_RegexSymbol: string = '\\[dd-Mmm-yyyy]'
356361
const Date_Mmm_dd_yyyy_RegexSymbol: string = '\\[Mmm-dd-yyyy]'
362+
const Date_yyyy_Www_mm_dd_RegexSymbol: string = '\\[yyyy-Www (mm-dd)]'
363+
const Date_yyyy_Www_RegexSymbol: string = '\\[yyyy-Www]'
364+
const Date_yyyy_WwwISO_RegexSymbol: string = '\\[yyyy-WwwISO]'
357365

358366
const InlineRegexSymbol_CapitalLetter: string = '\\C'
359367
const InlineRegexSymbol_LowercaseLetter: string = '\\l'
@@ -374,7 +382,10 @@ const sortingSymbolsArr: Array<string> = [
374382
escapeRegexUnsafeCharacters(WordInASCIIRegexSymbol),
375383
escapeRegexUnsafeCharacters(WordInAnyLanguageRegexSymbol),
376384
escapeRegexUnsafeCharacters(Date_dd_Mmm_yyyy_RegexSymbol),
377-
escapeRegexUnsafeCharacters(Date_Mmm_dd_yyyy_RegexSymbol)
385+
escapeRegexUnsafeCharacters(Date_Mmm_dd_yyyy_RegexSymbol),
386+
escapeRegexUnsafeCharacters(Date_yyyy_Www_mm_dd_RegexSymbol),
387+
escapeRegexUnsafeCharacters(Date_yyyy_WwwISO_RegexSymbol),
388+
escapeRegexUnsafeCharacters(Date_yyyy_Www_RegexSymbol),
378389
]
379390

380391
const sortingSymbolsRegex = new RegExp(sortingSymbolsArr.join('|'), 'gi')
@@ -444,6 +455,9 @@ export const CompoundDotNumberNormalizerFn: NormalizerFn = (s: string) => getNor
444455
export const CompoundDashNumberNormalizerFn: NormalizerFn = (s: string) => getNormalizedNumber(s, DASH_SEPARATOR)
445456
export const Date_dd_Mmm_yyyy_NormalizerFn: NormalizerFn = (s: string) => getNormalizedDate_dd_Mmm_yyyy_NormalizerFn(s)
446457
export const Date_Mmm_dd_yyyy_NormalizerFn: NormalizerFn = (s: string) => getNormalizedDate_Mmm_dd_yyyy_NormalizerFn(s)
458+
export const Date_yyyy_Www_mm_dd_NormalizerFn: NormalizerFn = (s: string) => getNormalizedDate_yyyy_Www_mm_dd_NormalizerFn(s)
459+
export const Date_yyyy_WwwISO_NormalizerFn: NormalizerFn = (s: string) => getNormalizedDate_yyyy_WwwISO_NormalizerFn(s)
460+
export const Date_yyyy_Www_NormalizerFn: NormalizerFn = (s: string) => getNormalizedDate_yyyy_Www_NormalizerFn(s)
447461

448462
export enum AdvancedRegexType {
449463
None, // to allow if (advancedRegex)
@@ -456,7 +470,10 @@ export enum AdvancedRegexType {
456470
WordInASCII,
457471
WordInAnyLanguage,
458472
Date_dd_Mmm_yyyy,
459-
Date_Mmm_dd_yyyy
473+
Date_Mmm_dd_yyyy,
474+
Date_yyyy_Www_mm_dd_yyyy,
475+
Date_yyyy_WwwISO,
476+
Date_yyyy_Www
460477
}
461478

462479
const sortingSymbolToRegexpStr: { [key: string]: RegExpSpecStr } = {
@@ -510,6 +527,21 @@ const sortingSymbolToRegexpStr: { [key: string]: RegExpSpecStr } = {
510527
regexpStr: Date_Mmm_dd_yyyy_RegexStr,
511528
normalizerFn: Date_Mmm_dd_yyyy_NormalizerFn,
512529
advancedRegexType: AdvancedRegexType.Date_Mmm_dd_yyyy
530+
},
531+
[Date_yyyy_Www_mm_dd_RegexSymbol]: { // Intentionally retain character case
532+
regexpStr: Date_yyyy_Www_mm_dd_RegexStr,
533+
normalizerFn: Date_yyyy_Www_mm_dd_NormalizerFn,
534+
advancedRegexType: AdvancedRegexType.Date_yyyy_Www_mm_dd_yyyy
535+
},
536+
[Date_yyyy_WwwISO_RegexSymbol]: { // Intentionally retain character case
537+
regexpStr: Date_yyyy_Www_RegexStr,
538+
normalizerFn: Date_yyyy_WwwISO_NormalizerFn,
539+
advancedRegexType: AdvancedRegexType.Date_yyyy_WwwISO
540+
},
541+
[Date_yyyy_Www_RegexSymbol]: { // Intentionally retain character case
542+
regexpStr: Date_yyyy_Www_RegexStr,
543+
normalizerFn: Date_yyyy_Www_NormalizerFn,
544+
advancedRegexType: AdvancedRegexType.Date_yyyy_Www
513545
}
514546
}
515547

src/test/int/dates-in-names.int.test.ts

Lines changed: 113 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import {
2-
TAbstractFile,
2+
TAbstractFile, TFile,
33
TFolder,
44
Vault
55
} from "obsidian";
@@ -21,6 +21,8 @@ import {
2121
mockTFolderWithDateNamedChildren,
2222
TIMESTAMP_DEEP_NEWEST,
2323
TIMESTAMP_DEEP_OLDEST,
24+
mockTFolderWithDateWeekNamedChildrenForISOvsUSweekNumberingTest,
25+
mockTFolderWithDateWeekNamedChildren, mockTFile, mockTFolder,
2426
} from "../mocks";
2527
import {
2628
SortingSpecProcessor
@@ -58,6 +60,116 @@ describe('sortFolderItems', () => {
5860
'AAA Jan-01-2012'
5961
])
6062
})
63+
it('should correctly handle yyyy-Www (mm-dd) pattern in file names', () => {
64+
// given
65+
const processor: SortingSpecProcessor = new SortingSpecProcessor()
66+
const sortSpecTxt =
67+
` ... \\[yyyy-Www (mm-dd)]
68+
< a-z
69+
------
70+
`
71+
const PARENT_PATH = 'parent/folder/path'
72+
const sortSpecsCollection = processor.parseSortSpecFromText(
73+
sortSpecTxt.split('\n'),
74+
PARENT_PATH,
75+
'file name with the sorting, irrelevant here'
76+
)
77+
78+
const folder: TFolder = mockTFolderWithDateWeekNamedChildren(PARENT_PATH)
79+
const sortSpec: CustomSortSpec = sortSpecsCollection?.sortSpecByPath![PARENT_PATH]!
80+
81+
const ctx: ProcessingContext = {}
82+
83+
// when
84+
const result: Array<TAbstractFile> = sortFolderItems(folder, folder.children, sortSpec, ctx, OS_alphabetical)
85+
86+
// then
87+
const orderedNames = result.map(f => f.name)
88+
expect(orderedNames).toEqual([
89+
"GHI 2021-W1 (01-04)",
90+
"DEF 2021-W9 (03-01).md",
91+
"ABC 2021-W13 (03-29)",
92+
"MNO 2021-W45 (11-08).md",
93+
"JKL 2021-W52 (12-27).md",
94+
"------.md"
95+
])
96+
})
97+
it('should correctly handle yyyy-WwwISO pattern in file names', () => {
98+
// given
99+
const processor: SortingSpecProcessor = new SortingSpecProcessor()
100+
const sortSpecTxt =
101+
` /+ ... \\[yyyy-Www (mm-dd)]
102+
/+ ... \\[yyyy-WwwISO]
103+
< a-z
104+
`
105+
const PARENT_PATH = 'parent/folder/path'
106+
const sortSpecsCollection = processor.parseSortSpecFromText(
107+
sortSpecTxt.split('\n'),
108+
PARENT_PATH,
109+
'file name with the sorting, irrelevant here'
110+
)
111+
112+
const folder: TFolder = mockTFolderWithDateWeekNamedChildrenForISOvsUSweekNumberingTest(PARENT_PATH)
113+
const sortSpec: CustomSortSpec = sortSpecsCollection?.sortSpecByPath![PARENT_PATH]!
114+
115+
const ctx: ProcessingContext = {}
116+
117+
// when
118+
const result: Array<TAbstractFile> = sortFolderItems(folder, folder.children, sortSpec, ctx, OS_alphabetical)
119+
120+
// then
121+
// ISO standard of weeks numbering
122+
const orderedNames = result.map(f => f.name)
123+
expect(orderedNames).toEqual([
124+
'E 2021-W1 (01-01)',
125+
'F ISO:2021-01-04 US:2020-12-28 2021-W1',
126+
'A 2021-W10 (03-05).md',
127+
'B ISO:2021-03-08 US:2021-03-01 2021-W10',
128+
'C 2021-W51 (12-17).md',
129+
'D ISO:2021-12-20 US:2021-12-13 2021-W51.md',
130+
'FFF2 ISO:2021-12-27 US:2021-12-20 2021-W52.md',
131+
'FFF1 ISO:2022-01-03 US:2021-12-27 2021-W53.md',
132+
"------.md"
133+
])
134+
})
135+
it('should correctly handle yyyy-Www pattern in file names', () => {
136+
// given
137+
const processor: SortingSpecProcessor = new SortingSpecProcessor()
138+
const sortSpecTxt =
139+
` /+ ... \\[yyyy-Www (mm-dd)]
140+
/+ ... \\[yyyy-Www]
141+
> a-z
142+
`
143+
const PARENT_PATH = 'parent/folder/path'
144+
const sortSpecsCollection = processor.parseSortSpecFromText(
145+
sortSpecTxt.split('\n'),
146+
PARENT_PATH,
147+
'file name with the sorting, irrelevant here'
148+
)
149+
150+
const folder: TFolder = mockTFolderWithDateWeekNamedChildrenForISOvsUSweekNumberingTest(PARENT_PATH)
151+
const sortSpec: CustomSortSpec = sortSpecsCollection?.sortSpecByPath![PARENT_PATH]!
152+
153+
const ctx: ProcessingContext = {}
154+
155+
// when
156+
const result: Array<TAbstractFile> = sortFolderItems(folder, folder.children, sortSpec, ctx, OS_alphabetical)
157+
158+
// then
159+
// U.S. standard of weeks numbering
160+
const orderedNames = result.map(f => f.name)
161+
expect(orderedNames).toEqual([
162+
'FFF1 ISO:2022-01-03 US:2021-12-27 2021-W53.md',
163+
'FFF2 ISO:2021-12-27 US:2021-12-20 2021-W52.md',
164+
'C 2021-W51 (12-17).md',
165+
'D ISO:2021-12-20 US:2021-12-13 2021-W51.md',
166+
'A 2021-W10 (03-05).md',
167+
'B ISO:2021-03-08 US:2021-03-01 2021-W10',
168+
'E 2021-W1 (01-01)',
169+
'F ISO:2021-01-04 US:2020-12-28 2021-W1',
170+
"------.md"
171+
])
172+
})
61173
})
62174

63175

src/test/mocks.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,3 +65,33 @@ export const mockTFolderWithDateNamedChildren = (name: string): TFolder => {
6565

6666
return mockTFolder(name, [child1, child2, child3, child4])
6767
}
68+
69+
export const mockTFolderWithDateWeekNamedChildren = (name: string): TFolder => {
70+
// Assume ISO week numbers
71+
const child0: TFile = mockTFile('------', 'md')
72+
const child1: TFolder = mockTFolder('ABC 2021-W13 (03-29)')
73+
const child2: TFile = mockTFile('DEF 2021-W9 (03-01)', 'md')
74+
const child3: TFolder = mockTFolder('GHI 2021-W1 (01-04)')
75+
const child4: TFile = mockTFile('JKL 2021-W52 (12-27)', 'md')
76+
const child5: TFile = mockTFile('MNO 2021-W45 (11-08)', 'md')
77+
78+
return mockTFolder(name, [child0, child1, child2, child3, child4, child5])
79+
}
80+
81+
export const mockTFolderWithDateWeekNamedChildrenForISOvsUSweekNumberingTest = (name: string): TFolder => {
82+
// Tricky to test handling of both ISO and U.S. weeks numbering.
83+
// Sample year with different week numbers in ISO vs. U.S. is 2021 with 1st Jan on Fri, ISO != U.S.
84+
// Plain files and folder names to match both week-only and week+date syntax
85+
// Their relative ordering depends on week numbering
86+
const child0: TFile = mockTFile('------', 'md')
87+
const child1: TFile = mockTFile('A 2021-W10 (03-05)', 'md') // Tue date, (ISO) week number invalid, ignored
88+
const child2: TFolder = mockTFolder('B ISO:2021-03-08 US:2021-03-01 2021-W10')
89+
const child3: TFile = mockTFile('C 2021-W51 (12-17)', 'md') // Tue date, (ISO) week number invalid, ignored
90+
const child4: TFile = mockTFile('D ISO:2021-12-20 US:2021-12-13 2021-W51', 'md')
91+
const child5: TFolder = mockTFolder('E 2021-W1 (01-01)') // Tue date, to (ISO) week number invalid, ignored
92+
const child6: TFolder = mockTFolder('F ISO:2021-01-04 US:2020-12-28 2021-W1')
93+
const child7: TFile = mockTFile('FFF2 ISO:2021-12-27 US:2021-12-20 2021-W52', 'md')
94+
const child8: TFile = mockTFile('FFF1 ISO:2022-01-03 US:2021-12-27 2021-W53', 'md') // Invalid week, should fall to next year
95+
96+
return mockTFolder(name, [child0, child1, child2, child3, child4, child5, child6, child7, child8])
97+
}

0 commit comments

Comments
 (0)