Skip to content

Commit

Permalink
#178 - explicit support for telling if the Www date should be same, e…
Browse files Browse the repository at this point in the history
…arlier than the first day of the week or later than the last day of the week

- syntax W1 W1- W1+
  • Loading branch information
SebastianMC committed Jan 14, 2025
1 parent 2204982 commit f7c69b1
Show file tree
Hide file tree
Showing 5 changed files with 66 additions and 19 deletions.
42 changes: 31 additions & 11 deletions src/custom-sort/matchers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -62,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}`
}
}

Expand Down Expand Up @@ -108,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}`
}
}

Expand All @@ -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}`
}
}

Expand All @@ -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
Expand All @@ -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
Expand All @@ -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}`
}
}

Expand Down
16 changes: 16 additions & 0 deletions src/test/unit/matchers.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
})
})
4 changes: 2 additions & 2 deletions src/test/unit/sorting-spec-processor.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}, {
Expand Down
6 changes: 5 additions & 1 deletion src/test/unit/week-of-year.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'))
})
})
17 changes: 12 additions & 5 deletions src/utils/week-of-year.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 = {
Expand Down

0 comments on commit f7c69b1

Please sign in to comment.