Введение: Зачем нам «относительное время»?
Вы когда-нибудь задумывались, почему социальные сети и новостные сайты пишут «2 часа назад» или «вчера», а не «25.04.2025 14:32»? Потому что наш мозг воспринимает относительное время гораздо быстрее. Абсолютная дата требует анализа: «Окей, сегодня 25-е, значит это было…». Относительная дата даёт ответ мгновенно.
Если вы пишете на C# — будь то веб-приложение на ASP.NET, десктопное приложение или Telegram-бот — рано или поздно вам понадобится показывать пользователю время в формате «только что», «3 минуты назад» или «около года назад». На Stack Overflow вопрос «Calculate relative time in C#» набрал тысячи просмотров, а значит — это реальная боль разработчиков.
В этой статье мы разберем самый правильный и гибкий способ вычисления относительного времени на C#. Вы получите готовый код, который можно сразу вставить в проект, поймёте, как он работает, и узнаете, как его кастомизировать под любые нужды.
Проблема: что не так с простым вычитанием дат?
Первое, что приходит в голову — вычесть одну дату из другой и получить TimeSpan. Да, это работает. Но дьявол кроется в деталях:
- Пороговые значения. Когда заканчивается «минута» и начинается «час»? А когда «час» переходит в «день»?
- Футуристические даты. Что, если дата в будущем? Показывать «через 5 минут» или «через 1 день»?
- Читаемость кода. Если вы разбросаете логику по всему проекту, через месяц сами запутаетесь.
Поэтому нам нужен не просто кусок кода, а единый, переиспользуемый метод.
Решение: универсальный метод для расчёта относительного времени
Лучший способ — написать один статический метод, который принимает DateTime (или DateTimeOffset) и возвращает красивую строку. Вот базовая, но уже очень мощная реализация, которую можно найти в обсуждениях на Stack Overflow и адаптировать под себя:
using System;
public static class TimeHelper
{
public static string GetRelativeTime(DateTime dateTime)
{
var now = DateTime.Now;
var utcNow = DateTime.UtcNow;
// Важно: сравниваем в одной временной зоне!
var timeSpan = now - dateTime;
// Случай 1: дата в будущем
if (timeSpan.TotalSeconds < 0)
{
// Вычисляем абсолютное значение для будущего
timeSpan = timeSpan.Duration();
if (timeSpan.TotalMinutes < 1)
return "через несколько секунд";
if (timeSpan.TotalMinutes < 60)
return $"через {timeSpan.Minutes} мин.";
if (timeSpan.TotalHours < 24)
return $"через {timeSpan.Hours} ч.";
if (timeSpan.TotalDays < 30)
return $"через {timeSpan.Days} дн.";
if (timeSpan.TotalDays < 365)
return $"через {timeSpan.Days / 30} мес.";
return $"через {timeSpan.Days / 365} г.";
}
// Случай 2: прошлое
if (timeSpan.TotalMinutes < 1)
return "только что";
if (timeSpan.TotalMinutes < 60)
return $"{timeSpan.Minutes} мин. назад";
if (timeSpan.TotalHours < 24)
return $"{timeSpan.Hours} ч. назад";
if (timeSpan.TotalDays < 7)
return $"{timeSpan.Days} дн. назад";
// Для дат старше недели используем дни
if (timeSpan.TotalDays < 30)
return $"{(int)timeSpan.TotalDays} дн. назад";
if (timeSpan.TotalDays < 365)
{
int months = (int)(timeSpan.TotalDays / 30);
return $"{months} мес. назад";
}
int years = (int)(timeSpan.TotalDays / 365);
return $"{years} г. назад";
}
}
Как это работает?
- Вычисляем разницу. Мы получаем
TimeSpanмежду текущим моментом и переданной датой. - Проверяем будущее. Если разница отрицательна — дата в будущем. Меняем знак и добавляем приставку «через».
- Идём по шкале времени. От секунд к минутам, часам, дням, месяцам и годам. Как только интервал становится меньше следующей единицы — останавливаемся.
- Округляем вниз. Используем целые значения (
timeSpan.Hours,timeSpan.Days), чтобы не было «1.5 часа назад».
Улучшаем код: добавляем поддержку DateTimeOffset и культуру
В современном мире всё больше приложений работают с UTC. DateTimeOffset — лучший выбор для хранения моментов времени, так как он хранит смещение относительно UTC. Давайте адаптируем наш метод:
public static string GetRelativeTime(DateTimeOffset dateTimeOffset)
{
var now = DateTimeOffset.Now;
var timeSpan = now - dateTimeOffset;
// ... (дальше логика идентична предыдущему методу)
// Только используем timeSpan.TotalDays и т.д.
}
Также стоит подумать о локализации. Если ваше приложение на русском — отлично, строки выше подходят. Но если вы хотите сделать мультиязычное приложение, вынесите текстовки в ресурсы или используйте паттерн «Стратегия».
Продвинутый вариант: класс RelativeTime с настройками
Для больших проектов лучше сделать не один метод, а целый класс с настройками. Это позволит не копипастить код, а просто менять параметры. Вот пример такого подхода:
public class RelativeTimeOptions
{
public string JustNow { get; set; } = "только что";
public string MinutesAgo { get; set; } = "мин. назад";
public string HoursAgo { get; set; } = "ч. назад";
public string DaysAgo { get; set; } = "дн. назад";
public string MonthsAgo { get; set; } = "мес. назад";
public string YearsAgo { get; set; } = "г. назад";
// ... аналогично для будущего времени
}
public static class TimeFormatter
{
public static string GetRelativeTime(DateTime dateTime, RelativeTimeOptions options = null)
{
options ??= new RelativeTimeOptions();
// ... логика с использованием options
}
}
Теперь вы можете создать экземпляр RelativeTimeOptions для английского, немецкого или японского языка и передать его в метод.
Частые ошибки и как их избежать
- Игнорирование часовых поясов. Если вы сохранили дату как
DateTime.UtcNow, а сравниваете сDateTime.Now(локальное время), получите дикий сдвиг. Всегда приводите к одному типу: либо всё в UTC, либо всё в лок