Паттерн композиции позволяет собирать сложное поведение из простых, независимых частей. В контексте timeago.js это означает создание составных функций форматирования из небольших специализированных функций.
import { format } from 'timeago.js';
type Formatter = (date: Date | string | number, locale: string) => string;
// Функция compose: применяет трансформации последовательно
function compose<T>(...fns: ((arg: T) => T)[]): (arg: T) => T {
return (arg: T) => fns.reduceRight((acc, fn) => fn(acc), arg);
}
// pipe — то же, но слева направо
function pipe<T>(...fns: ((arg: T) => T)[]): (arg: T) => T {
return (arg: T) => fns.reduce((acc, fn) => fn(acc), arg);
}
type DateContext = {
date: Date | string | number | null;
locale: string;
result: string;
};
// Отдельные шаги форматирования
const validateDate = (ctx: DateContext): DateContext => {
if (ctx.date == null) {
return { ...ctx, result: '', date: null };
}
const d = new Date(ctx.date as any);
if (isNaN(d.getTime())) {
return { ...ctx, result: '', date: null };
}
return { ...ctx, date: d };
};
const applyFormat = (ctx: DateContext): DateContext => {
if (!ctx.date) return ctx;
return { ...ctx, result: format(ctx.date as Date, ctx.locale) };
};
const capitalize = (ctx: DateContext): DateContext => ({
...ctx,
result: ctx.result
? ctx.result.charAt(0).toUpperCase() + ctx.result.slice(1)
: ctx.result,
});
const addFallback = (fallback: string) =>
(ctx: DateContext): DateContext => ({
...ctx,
result: ctx.result || fallback,
});
const formatWithFallback = (date: unknown, locale = 'ru', fallback = 'давно'): string => {
const initial: DateContext = { date: date as any, locale, result: '' };
const process = pipe(
validateDate,
applyFormat,
capitalize,
addFallback(fallback)
);
return process(initial).result;
};
type FormatterConfig = {
locale: string;
capitalize: boolean;
fallback: string;
};
function createFormatter(config: FormatterConfig): (date: unknown) => string {
const steps: Array<(ctx: DateContext) => DateContext> = [
validateDate,
applyFormat,
];
if (config.capitalize) steps.push(capitalize);
steps.push(addFallback(config.fallback));
return (date: unknown): string => {
const ctx: DateContext = { date: date as any, locale: config.locale, result: '' };
const process = pipe(...steps);
return process(ctx).result;
};
}
// Создание специализированных форматтеров
const ruFormatter = createFormatter({
locale: 'ru',
capitalize: true,
fallback: 'Давно',
});
const deFormatter = createFormatter({
locale: 'de',
capitalize: false,
fallback: 'Vor langer Zeit',
});
// Higher-order функции — оборачивают форматтер дополнительной логикой
type FormatFn = (date: unknown, locale?: string) => string;
function withLogging(fn: FormatFn): FormatFn {
return (date, locale = 'ru') => {
const result = fn(date, locale);
console.debug(`[format] ${date} → "${result}"`);
return result;
};
}
function withCache(fn: FormatFn, ttl = 30_000): FormatFn {
const cache = new Map<string, { value: string; ts: number }>();
return (date, locale = 'ru') => {
const key = `${date}:${locale}`;
const hit = cache.get(key);
if (hit && Date.now() - hit.ts < ttl) return hit.value;
const value = fn(date, locale);
cache.set(key, { value, ts: Date.now() });
return value;
};
}
function withFallback(fn: FormatFn, fallback: string): FormatFn {
return (date, locale = 'ru') => {
try {
return fn(date, locale) || fallback;
} catch {
return fallback;
}
};
}
// Сборка
const smartFormat = withCache(
withLogging(
withFallback(
(date, locale) => format(new Date(date as any), locale!),
'давно'
)
),
60_000
);
// Базовый хук форматирования
function useFormat(date: Date | string, locale = 'ru'): string {
return useMemo(() => format(new Date(date), locale), [date, locale]);
}
// Хук с автообновлением
function useAutoFormat(date: Date | string, locale = 'ru'): string {
const [value, setValue] = useState(() => format(new Date(date), locale));
useEffect(() => {
const id = setInterval(() => setValue(format(new Date(date), locale)), 30_000);
return () => clearInterval(id);
}, [date, locale]);
return value;
}
// Хук с валидацией
function useSafeFormat(date: unknown, locale = 'ru'): string {
return useMemo(() => {
if (!date) return '';
const d = new Date(date as any);
return isNaN(d.getTime()) ? '' : format(d, locale);
}, [date, locale]);
}
// Составной хук
function useRichTimeAgo(date: unknown, locale = 'ru'): {
text: string;
isValid: boolean;
date: Date | null;
} {
const isValid = useMemo(() => {
if (!date) return false;
return !isNaN(new Date(date as any).getTime());
}, [date]);
const parsedDate = useMemo(
() => (isValid ? new Date(date as any) : null),
[date, isValid]
);
const text = useAutoFormat(parsedDate ?? new Date(), locale);
return { text: isValid ? text : '', isValid, date: parsedDate };
}
| Выгода | Описание |
|---|---|
| Тестируемость | Каждый шаг тестируется независимо |
| Переиспользование | Шаги используются в разных комбинациях |
| Явность | Цепочка шагов читается как список требований |
| Расширяемость | Новый шаг добавляется без изменения существующих |