Merge pull request #190 from SebastianMC/178-week-numbers-date-regex-patterns
#178 - week-number based date extraction patterns for titles, incl. Www, Www- and Www+ specs #191 - Explicit support for the common date formats of `yyyy-mm-dd` and `yyyy-dd-mm`
This commit is contained in:
		
						commit
						c200c2eda0
					
				| 
						 | 
				
			
			@ -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
 | 
			
		||||
| 
						 | 
				
			
			@ -6,15 +10,26 @@ 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}-[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 DOT_SEPARATOR = '.'
 | 
			
		||||
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 = '.' // ASCII 46
 | 
			
		||||
export const DASH_SEPARATOR = '-'
 | 
			
		||||
 | 
			
		||||
const SLASH_SEPARATOR = '/' // ASCII 47
 | 
			
		||||
const SLASH_SEPARATOR = '/' // ASCII 47, right before ASCII 48 = '0'
 | 
			
		||||
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 = GT_SEPARATOR
 | 
			
		||||
 | 
			
		||||
export const DEFAULT_NORMALIZATION_PLACES = 8;  // Fixed width of a normalized number (with leading zeros)
 | 
			
		||||
 | 
			
		||||
// Property escapes:
 | 
			
		||||
| 
						 | 
				
			
			@ -51,9 +66,9 @@ export function getNormalizedNumber(s: string = '', separator?: string, places?:
 | 
			
		|||
	// guarantees correct order (/ = ASCII 47, | = ASCII 124)
 | 
			
		||||
	if (separator) {
 | 
			
		||||
		const components: Array<string> = 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}`
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -97,9 +112,9 @@ export function getNormalizedRomanNumber(s: string, separator?: string, places?:
 | 
			
		|||
	// guarantees correct order (/ = ASCII 47, | = ASCII 124)
 | 
			
		||||
	if (separator) {
 | 
			
		||||
		const components: Array<string> = 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}`
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -117,9 +132,76 @@ 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}`
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
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)
 | 
			
		||||
 | 
			
		||||
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})([-+]?)/
 | 
			
		||||
 | 
			
		||||
// 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
 | 
			
		||||
 | 
			
		||||
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
 | 
			
		||||
		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 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
 | 
			
		||||
			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)}` +
 | 
			
		||||
			`${separator}${SLASH_SEPARATOR}`
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
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)
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -19,9 +19,19 @@ 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_dd_mm_NormalizerFn,
 | 
			
		||||
	getNormalizedDate_yyyy_mm_dd_NormalizerFn,
 | 
			
		||||
	getNormalizedDate_yyyy_Www_mm_dd_NormalizerFn,
 | 
			
		||||
	getNormalizedDate_yyyy_Www_NormalizerFn,
 | 
			
		||||
	getNormalizedDate_yyyy_WwwISO_NormalizerFn,
 | 
			
		||||
	getNormalizedNumber,
 | 
			
		||||
	getNormalizedRomanNumber,
 | 
			
		||||
	NumberRegexStr,
 | 
			
		||||
| 
						 | 
				
			
			@ -36,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
 | 
			
		||||
| 
						 | 
				
			
			@ -352,8 +359,13 @@ 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)]'
 | 
			
		||||
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'
 | 
			
		||||
| 
						 | 
				
			
			@ -373,8 +385,13 @@ const sortingSymbolsArr: Array<string> = [
 | 
			
		|||
	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_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')
 | 
			
		||||
| 
						 | 
				
			
			@ -442,8 +459,13 @@ 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)
 | 
			
		||||
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)
 | 
			
		||||
| 
						 | 
				
			
			@ -455,8 +477,13 @@ export enum AdvancedRegexType {
 | 
			
		|||
	CompoundDashRomanNumber,
 | 
			
		||||
	WordInASCII,
 | 
			
		||||
	WordInAnyLanguage,
 | 
			
		||||
	Date_yyyy_mm_dd,
 | 
			
		||||
	Date_yyyy_dd_mm,
 | 
			
		||||
	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 } = {
 | 
			
		||||
| 
						 | 
				
			
			@ -501,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,
 | 
			
		||||
| 
						 | 
				
			
			@ -510,6 +547,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_WwwISO_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
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,5 +1,5 @@
 | 
			
		|||
import {
 | 
			
		||||
    TAbstractFile,
 | 
			
		||||
    TAbstractFile, TFile,
 | 
			
		||||
    TFolder,
 | 
			
		||||
    Vault
 | 
			
		||||
} from "obsidian";
 | 
			
		||||
| 
						 | 
				
			
			@ -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,
 | 
			
		||||
| 
						 | 
				
			
			@ -21,17 +25,20 @@ import {
 | 
			
		|||
    mockTFolderWithDateNamedChildren,
 | 
			
		||||
    TIMESTAMP_DEEP_NEWEST,
 | 
			
		||||
    TIMESTAMP_DEEP_OLDEST,
 | 
			
		||||
    mockTFolderWithDateWeekNamedChildrenForISOvsUSweekNumberingTest,
 | 
			
		||||
    mockTFolderWithDateWeekNamedChildren, mockTFile, mockTFolder,
 | 
			
		||||
} from "../mocks";
 | 
			
		||||
import {
 | 
			
		||||
    SortingSpecProcessor
 | 
			
		||||
} 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'
 | 
			
		||||
| 
						 | 
				
			
			@ -58,6 +65,224 @@ describe('sortFolderItems', () => {
 | 
			
		|||
            'AAA Jan-01-2012'
 | 
			
		||||
        ])
 | 
			
		||||
    })
 | 
			
		||||
    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)]
 | 
			
		||||
     < 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<TAbstractFile> = 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 and folder 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<TAbstractFile> = 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 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<TAbstractFile> = 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-mm-dd]
 | 
			
		||||
   /+ ... dd-mm \\[yyyy-dd-mm]
 | 
			
		||||
   /+ ... \\[yyyy-mm-dd]
 | 
			
		||||
   /+ ... \\[Mmm-dd-yyyy]
 | 
			
		||||
   /+ \\[dd-Mmm-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 = 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 = {}
 | 
			
		||||
 | 
			
		||||
        // when
 | 
			
		||||
        const result: Array<TAbstractFile> = 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',
 | 
			
		||||
            "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"
 | 
			
		||||
        ])
 | 
			
		||||
    })
 | 
			
		||||
    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<TAbstractFile> = 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",
 | 
			
		||||
        ])
 | 
			
		||||
    })
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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])
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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,41 @@ 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)
 | 
			
		||||
	})
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
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)
 | 
			
		||||
	})
 | 
			
		||||
})
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -7,6 +7,11 @@ 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,
 | 
			
		||||
	detectSortingSymbols,
 | 
			
		||||
	escapeRegexUnsafeCharacters,
 | 
			
		||||
	extractSortingSymbol,
 | 
			
		||||
| 
						 | 
				
			
			@ -367,6 +372,23 @@ 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]
 | 
			
		||||
\\[yyyy-mm-dd] plain spec 1
 | 
			
		||||
\\[yyyy-dd-mm] plain spec 2
 | 
			
		||||
`
 | 
			
		||||
 | 
			
		||||
const expectedSortSpecsExampleSortingSymbols: { [key: string]: CustomSortSpec } = {
 | 
			
		||||
	"mock-folder": {
 | 
			
		||||
		groups: [{
 | 
			
		||||
| 
						 | 
				
			
			@ -427,26 +449,44 @@ 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[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[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,
 | 
			
		||||
				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: 9
 | 
			
		||||
		outsidersGroupIdx: 14
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
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 +560,6 @@ const expectedSortSpecsExampleMDataExtractors2: { [key: string]: CustomSortSpec
 | 
			
		|||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
describe('SortingSpecProcessor', () => {
 | 
			
		||||
	let processor: SortingSpecProcessor;
 | 
			
		||||
	beforeEach(() => {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -0,0 +1,55 @@
 | 
			
		|||
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 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'))
 | 
			
		||||
    })
 | 
			
		||||
})
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,63 @@
 | 
			
		|||
 | 
			
		||||
// 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, 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
 | 
			
		||||
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),
 | 
			
		||||
        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, sunday?: boolean): Date => {
 | 
			
		||||
    const WEEK_OF_MILIS = DAYS_IN_WEEK * DAY_OF_MILIS
 | 
			
		||||
    const dataOfMondayIn1stWeekOfYear = (MondaysCache[year] ??= calculateMondayDateIn1stWeekOfYear(year))
 | 
			
		||||
    const mondayOfTheRequestedWeek =
 | 
			
		||||
        (useISO ? dataOfMondayIn1stWeekOfYear.mondayDateOf1stWeekISO : dataOfMondayIn1stWeekOfYear.mondayDateOf1stWeekUS)
 | 
			
		||||
        + (weekNumber-1)*WEEK_OF_MILIS
 | 
			
		||||
 | 
			
		||||
    const sundayOfTheRequestedWeek =
 | 
			
		||||
        (useISO ? dataOfMondayIn1stWeekOfYear.sundayDateOf1stWeekISO : dataOfMondayIn1stWeekOfYear.sundayDateOf1stWeekUS)
 | 
			
		||||
        + (weekNumber-1)*WEEK_OF_MILIS
 | 
			
		||||
 | 
			
		||||
    return new Date(sunday ? sundayOfTheRequestedWeek : mondayOfTheRequestedWeek)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const _unitTests = {
 | 
			
		||||
    calculateMondayDateIn2stWeekOfYear: calculateMondayDateIn1stWeekOfYear
 | 
			
		||||
}
 | 
			
		||||
		Loading…
	
		Reference in New Issue