Skip to content

Commit

Permalink
feat: date utils
Browse files Browse the repository at this point in the history
  • Loading branch information
asabotovich committed Sep 23, 2024
1 parent 142b64d commit 440350f
Show file tree
Hide file tree
Showing 3 changed files with 232 additions and 29 deletions.
49 changes: 20 additions & 29 deletions src/harmony/DatePicker/DatePicker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import {
} from '@taskany/icons';
import classNames from 'classnames';

import { nullable } from '../../utils';
import { nullable, DateRange, DateRangeType, QuarterAlias, QuartersKeys } from '../../utils';
import { Text } from '../Text/Text';
import { Button } from '../Button/Button';
import { Input } from '../Input/Input';
Expand All @@ -17,20 +17,11 @@ import { Badge } from '../Badge/Badge';

import classes from './DatePicker.module.css';

type DatePickerRangeType = 'Year' | 'Quarter' | 'Strict';
type Quarter = 'Q1' | 'Q2' | 'Q3' | 'Q4';
type QuarterAlias = '@prev' | '@current' | '@next';

interface DatePickerRange {
start?: Date;
end: Date;
}

interface DatePickerValue {
range: DatePickerRange;
type?: DatePickerRangeType;
range: DateRange;
type?: DateRangeType;
alias?: QuarterAlias;
shortcutQuarter?: Quarter;
shortcutQuarter?: QuartersKeys;
}

interface DatePickerProps extends Omit<React.HTMLAttributes<HTMLDivElement>, 'onChange'> {
Expand All @@ -46,18 +37,18 @@ interface DatePickerProps extends Omit<React.HTMLAttributes<HTMLDivElement>, 'on
interface DatePickerContextProps {
currentDate: Date;
currentYear: number;
currentQuarter: Quarter;
currentQuarter: QuartersKeys;
currentAlias: QuarterAlias;
selectedYear?: number;
selectedQuarter?: Quarter;
selectedQuarter?: QuartersKeys;
selectedAlias?: QuarterAlias;
selectedDate?: Date;
collapseFields?: Record<'year' | 'quarter' | 'date', boolean>;
setSelectedYear: (year: number) => void;
setSelectedQuarter: (quarter: Quarter) => void;
setSelectedQuarter: (quarter: QuartersKeys) => void;
setSelectedQuarterAlias: (alias?: QuarterAlias) => void;
setStrictDate: (date: string | Date) => void;
setCollapseFields: (mode: DatePickerRangeType) => void;
setCollapseFields: (mode: DateRangeType) => void;
}

interface DatePickerOptionProps {
Expand All @@ -70,10 +61,10 @@ interface DatePickerOptionProps {
interface DatePickerState {
currentDate: Date;
currentYear: number;
currentQuarter: Quarter;
currentQuarter: QuartersKeys;
currentAlias: QuarterAlias;
selectedYear?: number;
selectedQuarter?: Quarter;
selectedQuarter?: QuartersKeys;
selectedAlias?: QuarterAlias;
selectedDate?: Date;
collapseFields: ReturnType<typeof getReadOnlyFields>;
Expand All @@ -88,7 +79,7 @@ type Actions =
}
| {
type: 'set selected quarter';
payload: Quarter | undefined;
payload: QuartersKeys | undefined;
}
| {
type: 'set selected alias';
Expand Down Expand Up @@ -139,9 +130,9 @@ const getDateObject = (now = new Date()) => {
};
};

const getCurrentQuarter = (now = new Date()): Quarter => `Q${Math.floor(now.getMonth() / 3) + 1}` as Quarter;
const getCurrentQuarter = (now = new Date()): QuartersKeys => `Q${Math.floor(now.getMonth() / 3) + 1}` as QuartersKeys;

const getReadOnlyFields = (type: DatePickerRangeType) => {
const getReadOnlyFields = (type: DateRangeType) => {
if (type === 'Strict') {
return { year: true, quarter: true, date: false };
}
Expand All @@ -151,8 +142,8 @@ const getReadOnlyFields = (type: DatePickerRangeType) => {
return { year: false, quarter: true, date: true };
};

const createQuarterRange = (q: Quarter, year?: number): Required<DatePickerRange> => {
const qToM: Record<Quarter, number> = {
const createQuarterRange = (q: QuartersKeys, year?: number): Required<DateRange> => {
const qToM: Record<QuartersKeys, number> = {
Q1: 2,
Q2: 5,
Q3: 8,
Expand All @@ -176,7 +167,7 @@ const createQuarterRange = (q: Quarter, year?: number): Required<DatePickerRange
};
};

const getQuaterByAlias = (alias: QuarterAlias): [Quarter, number] => {
const getQuaterByAlias = (alias: QuarterAlias): [QuartersKeys, number] => {
const currentQuarterRange = createQuarterRange(getCurrentQuarter());

if (alias === '@current') {
Expand Down Expand Up @@ -354,7 +345,7 @@ const datePickerReducer: React.Reducer<DatePickerState, Actions> = (state, actio

const quartersBricks = ['right', 'center', 'center', 'left'] as const;
const aliasesBricks = ['right', 'center', 'left'] as const;
const quarters: Quarter[] = ['Q1', 'Q2', 'Q3', 'Q4'] as const;
const quarters: QuartersKeys[] = ['Q1', 'Q2', 'Q3', 'Q4'] as const;
const aliases: QuarterAlias[] = ['@prev', '@current', '@next'] as const;

const inputNames = {
Expand Down Expand Up @@ -395,7 +386,7 @@ export const DatePicker: React.FC<React.PropsWithChildren<DatePickerProps>> = ({
});
}, []);

const setSelectedQuarter = useCallback((quarter?: Quarter) => {
const setSelectedQuarter = useCallback((quarter?: QuartersKeys) => {
dispatch({
type: 'set selected quarter',
payload: quarter,
Expand All @@ -416,7 +407,7 @@ export const DatePicker: React.FC<React.PropsWithChildren<DatePickerProps>> = ({
});
}, []);

const setCollapseFields = useCallback((mode: DatePickerRangeType) => {
const setCollapseFields = useCallback((mode: DateRangeType) => {
dispatch({
type: 'set collapse fields',
payload: getReadOnlyFields(mode),
Expand Down Expand Up @@ -599,7 +590,7 @@ export const DatePickerQuarter: React.FC<DatePickerQuarterProps> = ({
setCollapseFields,
} = useContext(DatePickerContext);

const handleSelectQuarter = useCallback((q: Quarter) => {
const handleSelectQuarter = useCallback((q: QuartersKeys) => {
setSelectedQuarter(q);
setSelectedQuarterAlias(undefined);
}, []);
Expand Down
211 changes: 211 additions & 0 deletions src/utils/date.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,211 @@
export type DateRangeType = 'Year' | 'Quarter' | 'Strict';

export interface DateRange {
start?: Date;
end: Date;
}

export enum Quarters {
Q1 = 'Q1',
Q2 = 'Q2',
Q3 = 'Q3',
Q4 = 'Q4',
}

export type QuartersKeys = keyof typeof Quarters;

export enum QuartersAliases {
'@prev' = '@prev',
'@current' = '@current',
'@next' = '@next',
}

export type QuarterAlias = keyof typeof QuartersAliases;

const urlDateRangeSeparator = '~';

export const createLocaleDate = (date: Date | string, { locale }: { locale: Intl.LocalesArgument }): string =>
new Intl.DateTimeFormat(locale, {
day: '2-digit',
month: '2-digit',
year: 'numeric',
}).format(new Date(date));

const pad = (num: number) => (num < 10 ? '0' : '') + num;

// Return string in format yyyy-mm-dd from local Date

export const getDateString = (date: Date | string) => {
const parsedDate = new Date(date);

return `${parsedDate.getFullYear()}-${pad(parsedDate.getMonth() + 1)}-${pad(parsedDate.getDate())}`;
};

export const encodeUrlDateRange = ({ start, end }: DateRange): string =>
[start ? getDateString(start) : '', getDateString(end)].join(urlDateRangeSeparator);

export const getYearFromDate = (date: Date | string): number => new Date(date).getFullYear();

export const getQuarterFromDate = (date: Date | string): QuartersKeys =>
`Q${Math.floor(new Date(date).getMonth() / 3 + 1)}` as QuartersKeys;

export const getAvailableYears = (n = 5, currY = new Date().getFullYear()): number[] =>
Array(n)
.fill(0)
.map((_, i) => currY + i);

export const createYearRange = (year: number): DateRange => {
const date = new Date();

date.setFullYear(year);

return {
start: new Date(date.getFullYear(), 0, 1),
end: new Date(date.getFullYear(), 11, 31),
};
};

export const createQuarterRange = (q: QuartersKeys, year?: number): DateRange => {
const qToM = {
[Quarters.Q1]: 2,
[Quarters.Q2]: 5,
[Quarters.Q3]: 8,
[Quarters.Q4]: 11,
};
const abstractDate = new Date();

abstractDate.setMonth(qToM[q], 0);

if (year) {
abstractDate.setFullYear(year);
}

const qEndDate = (date: number) => {
const d = new Date(date);
const quarter = Math.floor(d.getMonth() / 3);
const start = new Date(d.getFullYear(), quarter * 3, 1);

return {
end: new Date(start.getFullYear(), start.getMonth() + 3, 0),
start,
};
};

return qEndDate(+abstractDate);
};

export const createDateRange = (year: number, quarter?: QuartersKeys | null): DateRange => {
if (quarter) {
return createQuarterRange(quarter, year);
}

return createYearRange(year);
};

export const createQuarterRangeFromDate = (value: Date): DateRange => {
return createDateRange(getYearFromDate(value), getQuarterFromDate(value));
};

export const getRelativeQuarterRange = (target: QuartersAliases): DateRange => {
const current = createQuarterRangeFromDate(new Date());

if (target === QuartersAliases['@current']) {
return current;
}

const endOfQuarter = current.end;

if (target === QuartersAliases['@next']) {
endOfQuarter.setMonth(endOfQuarter.getMonth() + 1);

return createQuarterRangeFromDate(endOfQuarter);
}

const startOfQuarter = current.start ?? current.end;

startOfQuarter.setMonth(startOfQuarter.getMonth() - 1);

return createQuarterRangeFromDate(startOfQuarter);
};

export const decodeUrlQuarterAlias = (data: string): QuartersAliases | null => {
if (Object.keys(QuartersAliases).includes(data)) {
return data as QuartersAliases;
}

return null;
};

export const decodeUrlDateRange = (data: string): null | DateRange => {
const alias = decodeUrlQuarterAlias(data);

if (alias) {
return getRelativeQuarterRange(alias);
}

const [start = null, end] = data.split(urlDateRangeSeparator);

if (!end) {
return null;
}

return {
start: start ? new Date(start) : undefined,
end: new Date(end),
};
};

const getMonthDifference = (start: Date, end: Date): number =>
end.getMonth() - start.getMonth() + 12 * (end.getFullYear() - start.getFullYear());

export const getDateTypeFromRange = ({ start, end }: DateRange): DateRangeType => {
if (start && getMonthDifference(start, end) === 11) {
return 'Year';
}

if (start && getMonthDifference(start, end) === 2) {
return 'Quarter';
}

return 'Strict';
};

export const decodeDateRangeFromString = (value?: string) => {
if (!value) {
return undefined;
}

const range = decodeUrlDateRange(value);

if (!range) {
return undefined;
}

return {
range,
type: getDateTypeFromRange(range),
alias: decodeUrlQuarterAlias(value) || undefined,
};
};

export const formateEstimate = (
date: Date | string,
{
locale,
type,
}: {
locale: Intl.LocalesArgument;
type?: DateRangeType;
},
): string => {
const realDate = new Date(date);
if (type === 'Year') {
return String(getYearFromDate(realDate));
}

if (type === 'Quarter') {
return `${getQuarterFromDate(realDate)}/${getYearFromDate(realDate)}`;
}

return createLocaleDate(realDate, { locale });
};
1 change: 1 addition & 0 deletions src/utils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,4 @@ export * from './setRefs';
export * from './translit';
export * from './upload';
export * from './worker';
export * from './date';

0 comments on commit 440350f

Please sign in to comment.