От секунд до годов: вычисляем относительное время в C# правильно

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Введение: Зачем нам «относительное время»?

Вы когда-нибудь задумывались, почему социальные сети и новостные сайты пишут «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} г. назад";
    }
}

Как это работает?

  1. Вычисляем разницу. Мы получаем TimeSpan между текущим моментом и переданной датой.
  2. Проверяем будущее. Если разница отрицательна — дата в будущем. Меняем знак и добавляем приставку «через».
  3. Идём по шкале времени. От секунд к минутам, часам, дням, месяцам и годам. Как только интервал становится меньше следующей единицы — останавливаемся.
  4. Округляем вниз. Используем целые значения (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, либо всё в лок

Блоги

Рекомендаций по книгам