Skip to content

Commit

Permalink
minor: added YearWeekCode
Browse files Browse the repository at this point in the history
  • Loading branch information
dereekb committed Sep 29, 2022
1 parent bb981e9 commit 32638e3
Show file tree
Hide file tree
Showing 2 changed files with 249 additions and 0 deletions.
248 changes: 248 additions & 0 deletions packages/date/src/lib/date/date.week.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,248 @@
import { Building, makeValuesGroupMap, MapFunction, Maybe } from '@dereekb/util';
import { isDate, getWeek, getYear, endOfWeek, startOfMonth, endOfMonth, isBefore, addWeeks, startOfWeek, setWeek } from 'date-fns';
import { dateTimezoneUtcNormal, DateTimezoneUtcNormalInstance, DateTimezoneUtcNormalInstanceInput, SYSTEM_DATE_TIMEZONE_UTC_NORMAL_INSTANCE } from './date.timezone';

/**
* A Week/Year number combination used to refer to a specific Week on a specific Year.
*
* 202201 is January 2022
*/
export type YearWeekCode = number;

/**
* Used for default YearWeekCode values
*/
export const UNKNOWN_JOB_YEAR_WEEK = 0;

export type UnknownYearWeekCode = typeof UNKNOWN_JOB_YEAR_WEEK;

/**
* The week in the year. Starts from 1.
*/
export type YearWeekCodeIndex = number;

/**
* Returns the YearWeekCodeIndex for the YearWeekCode.
*
* @param yearWeekCode
* @returns
*/
export function yearWeekCodeIndex(yearWeekCode: YearWeekCode): YearWeekCodeIndex {
return yearWeekCode % 100;
}

/**
* Pair of the YearWeekCodeIndex and the year.
*/
export interface YearWeekCodePair {
week: YearWeekCodeIndex;
year: number;
}

/**
* Returns the YearWeekCodePair for the YearWeekCode.
*
* @param yearWeekCode
* @returns
*/
export function yearWeekCodePair(yearWeekCode: YearWeekCode): YearWeekCodePair {
return {
week: yearWeekCodeIndex(yearWeekCode),
year: Math.floor(yearWeekCode / 100)
};
}

/**
* Used to convert the input to a YearWeekCode.
*/
export type YearWeekCodeFactory = ((dateOrYear: Date | number, inputWeek?: YearWeekCodeIndex) => YearWeekCode) & {
_normal: DateTimezoneUtcNormalInstance;
};

export type YearWeekCodeDateTimezoneInput = DateTimezoneUtcNormalInstanceInput | DateTimezoneUtcNormalInstance;

export interface YearWeekCodeConfig {
/**
* (Optional) Input timezone configuration for a DateTimezoneUtcNormalInstance.
*
* Configured to use the system timezone by default.
*/
timezone?: YearWeekCodeDateTimezoneInput;
}

export function yearWeekCodeDateTimezoneInstance(input: YearWeekCodeDateTimezoneInput): DateTimezoneUtcNormalInstance {
const normal = input ? (input instanceof DateTimezoneUtcNormalInstance ? input : dateTimezoneUtcNormal(input)) : SYSTEM_DATE_TIMEZONE_UTC_NORMAL_INSTANCE;
return normal;
}

/**
* Returns the yearWeekCode for the input Date or Year/Week combo.
*
* The date is expected to be relative to UTC.
*
* @param date
*/
export function yearWeekCode(date: Date): YearWeekCode;
export function yearWeekCode(year: number, week: YearWeekCodeIndex): YearWeekCode;
export function yearWeekCode(dateOrYear: Date | number, inputWeek?: YearWeekCodeIndex): YearWeekCode {
return yearWeekCodeFactory({ timezone: SYSTEM_DATE_TIMEZONE_UTC_NORMAL_INSTANCE })(dateOrYear, inputWeek);
}

/**
* Creates a YearWeekCodeFactory using the optional input config.
*
* @param config
* @returns
*/
export function yearWeekCodeFactory(config?: YearWeekCodeConfig): YearWeekCodeFactory {
const normal = yearWeekCodeDateTimezoneInstance(config?.timezone);

const result: Building<YearWeekCodeFactory> = (dateOrYear: Date | number, inputWeek?: YearWeekCodeIndex) => {
let year: number;
let week: YearWeekCodeIndex;

if (isDate(dateOrYear)) {
const normalDate = normal.systemDateToTargetDate(dateOrYear as Date);
week = getWeek(normalDate);

// check if the date is not in the previous year, in which case we need to add one.
if (week === 1) {
year = getYear(endOfWeek(normalDate)); // the last day is the important one to get the year from.
} else {
year = getYear(normalDate);
}
} else {
year = dateOrYear as number;
week = inputWeek as YearWeekCodeIndex;
}

const encodedYear = year * 100;
return encodedYear + week;
};

result._normal = normal;
return result as YearWeekCodeFactory;
}

/**
* Used for returning an array of YearWeekCode values for a pre-configured date range.
*/
export type YearWeekCodeForCalendarMonthFactory = (date: Date) => YearWeekCode[];

/**
* Returns the yearWeekCodes for the input Date's calendar month.
*
* The date is expected to be relative to UTC.
*
* @param date
*/
export function yearWeekCodeForCalendarMonth(date: Date): YearWeekCode[] {
return yearWeekCodeForCalendarMonthFactory(yearWeekCodeFactory({ timezone: SYSTEM_DATE_TIMEZONE_UTC_NORMAL_INSTANCE }))(date);
}

/**
* Create a YearWeekCodeForMonthFactory.
*
* @param factory
* @returns
*/
export function yearWeekCodeForCalendarMonthFactory(factory: YearWeekCodeFactory = yearWeekCodeFactory()): YearWeekCodeForCalendarMonthFactory {
const { _normal } = factory;

return (date: Date) => {
const normalDate = _normal.systemDateToTargetDate(date as Date);
const start = startOfMonth(endOfWeek(normalDate));
const end = endOfWeek(endOfMonth(start));

const weeks: YearWeekCode[] = [];

let current = start;

while (isBefore(current, end)) {
const week = factory(current);
weeks.push(week);
current = addWeeks(current, 1);
}

return weeks;
};
}

/**
* Used to convert the input to a YearWeekCode.
*/
export type YearWeekCodeDateFactory = (yearWeekCode: YearWeekCode) => Date;

export interface YearWeekCodeDateConfig extends Pick<YearWeekCodeConfig, 'timezone'> {}

/**
* Creates a YearWeekCodeDateFactory using the optional input config.
*
* @param config
* @returns
*/
export function yearWeekCodeDateFactory(config?: YearWeekCodeDateConfig): YearWeekCodeDateFactory {
const normal = yearWeekCodeDateTimezoneInstance(config?.timezone);
return (yearWeekCode: YearWeekCode) => {
const pair = yearWeekCodePair(yearWeekCode);
const date = startOfWeek(setWeek(new Date(Date.UTC(pair.year, 0, 1, 0, 0, 0, 0)), pair.week));
const fixed = normal.targetDateToSystemDate(date);
return fixed;
};
}

/**
*
*/
export interface YearWeekCodeGroup<B> {
readonly items: B[];
readonly week: YearWeekCode;
}

/**
* Used to group the input items into an array of YearWeekCodeGroup values.
*/
export type YearWeekCodeGroupFactory<B> = (items: B[]) => YearWeekCodeGroup<B>[];

/**
* MapFunction that reads the relevant date to use for the YearWeekCode calculation from the input item.
*/
export type YearWeekCodeDateReader<B> = MapFunction<B, Maybe<Date>>;

export interface YearWeekCodeGroupFactoryConfig<B> {
yearWeekCodeFactory?: YearWeekCodeFactory | YearWeekCodeConfig;
dateReader: YearWeekCodeDateReader<B>;
}

/**
* Crates a YearWeekCodeGroupFactory.
*
* @param config
* @returns
*/
export function yearWeekCodeGroupFactory<B>(config: YearWeekCodeGroupFactoryConfig<B>): YearWeekCodeGroupFactory<B> {
const { yearWeekCodeFactory: factoryInput, dateReader } = config;
const readJobWeekYear = typeof factoryInput === 'function' ? factoryInput : yearWeekCodeFactory(factoryInput);

return (items: B[]) => {
const map = makeValuesGroupMap(items, (item: B) => {
let yearWeekCode: Maybe<YearWeekCode>;
const date = dateReader(item);

if (date != null) {
yearWeekCode = readJobWeekYear(date);
}

return yearWeekCode;
});

const groups: YearWeekCodeGroup<B>[] = Array.from(map.entries()).map(([week, items]) => {
return {
week: week || 0,
items
};
});

return groups;
};
}
1 change: 1 addition & 0 deletions packages/date/src/lib/date/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,4 @@ export * from './date.time';
export * from './date.timezone';
export * from './date';
export * from './date.unix';
export * from './date.week';

0 comments on commit 32638e3

Please sign in to comment.