Avatar ·

Как вычислить относительное время (например, "2 часа назад") в C#?

Всем привет! Я начинающий C# разработчик и столкнулся с такой задачей: нужно отображать дату в формате "относительного времени" — как это делают в соцсетях. Вместо "01.03.2024 14:30" должно быть "5 минут назад", "вчера", "3 дня назад" и так далее. Например, у меня есть список комментариев с полем `DateTime CreatedAt`. Я хочу показывать пользователю, сколько времени прошло с момента создания. Но стандартный `DateTime.ToString()` выводит только абсолютную дату. Я попробовал написать простую функцию, но она работает криво — неправильно обрабатывает границы (например, "вчера" вместо "1 день назад"), не учитывает вчерашний день, если прошло меньше 24 часов, и вообще выглядит ужасно: ```csharp public string GetRelativeTime(DateTime dateTime) { var span = DateTime.Now - dateTime; if (span.TotalMinutes < 60) return $"{(int)span.TotalMinutes} минут назад"; if (span.TotalHours < 24) return $"{(int)span.TotalHours} часов назад"; if (span.TotalDays < 2) return "вчера"; return $"{(int)span.TotalDays} дней назад"; } ``` Этот код не учитывает склонения ("1 минут" вместо "1 минута"), путается с "вчера" (если прошло 23 часа, но это был еще сегодня, а не вчера) и не обрабатывает будущие даты. Подскажите, как правильно реализовать вычисление относительного времени на C#? Нужно учесть все нюансы: секунды, минуты, часы, дни, недели, месяцы и годы, а также правильные окончания для русского языка. <<>>

Решение: универсальный метод для вычисления относительного времени

Ваша проблема очень популярна, и на Stack Overflow есть тысячи голосов за этот вопрос. Дело в том, что простое вычитание дат не учитывает культурные особенности (склонения слов) и граничные случаи. Вот правильное и полное решение на C#.

Основная идея: мы вычисляем разницу между текущим временем и переданной датой, а затем подбираем подходящую единицу измерения с учётом правил русского языка.

public static string GetRelativeTime(DateTime dateTime)
{
    var now = DateTime.Now;
    var diff = now - dateTime;

    // Будущие даты
    if (diff.TotalSeconds < 0)
    {
        diff = dateTime - now;
        if (diff.TotalSeconds < 60) return "через несколько секунд";
        if (diff.TotalMinutes < 60) return $"через {FormatNumber((int)diff.TotalMinutes, "минуту", "минуты", "минут")}";
        if (diff.TotalHours < 24) return $"через {FormatNumber((int)diff.TotalHours, "час", "часа", "часов")}";
        if (diff.TotalDays < 2) return "завтра";
        return $"через {(int)diff.TotalDays} дней";
    }

    // Прошлые даты
    if (diff.TotalSeconds < 60)
    {
        return "только что";
    }
    else if (diff.TotalMinutes < 60)
    {
        return $"{FormatNumber((int)diff.TotalMinutes, "минуту", "минуты", "минут")} назад";
    }
    else if (diff.TotalHours < 24)
    {
        return $"{FormatNumber((int)diff.TotalHours, "час", "часа", "часов")} назад";
    }
    else if (diff.TotalDays < 2)
    {
        // Проверяем, был ли этот день вчера (с учётом даты, а не разницы часов)
        if (dateTime.Date == now.Date.AddDays(-1))
            return "вчера";
        else
            return "1 день назад";
    }
    else if (diff.TotalDays < 7)
    {
        return $"{FormatNumber((int)diff.TotalDays, "день", "дня", "дней")} назад";
    }
    else if (diff.TotalDays < 30)
    {
        int weeks = (int)(diff.TotalDays / 7);
        return $"{FormatNumber(weeks, "неделю", "недели", "недель")} назад";
    }
    else if (diff.TotalDays < 365)
    {
        int months = (int)(diff.TotalDays / 30);
        return $"{FormatNumber(months, "месяц", "месяца", "месяцев")} назад";
    }
    else
    {
        int years = (int)(diff.TotalDays / 365);
        return $"{FormatNumber(years, "год", "года", "лет")} назад";
    }
}

// Вспомогательный метод для склонения числительных
private static string FormatNumber(int number, string form1, string form2, string form5)
{
    int n = Math.Abs(number) % 100;
    int n1 = n % 10;

    if (n > 10 && n < 20) return $"{number} {form5}";
    if (n1 > 1 && n1 < 5) return $"{number} {form2}";
    if (n1 == 1) return $"{number} {form1}";

    return $"{number} {form5}";
}

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

  • Будущие даты — обрабатываются отдельно, с префиксом "через" и правильными склонениями.
  • "Только что" — для интервала менее 60 секунд.
  • "Вчера" — строгая проверка через dateTime.Date == now.Date.AddDays(-1), а не по разнице часов. Это важно: если сейчас 2 часа ночи, а событие было в 23:00 предыдущего дня, то прошло всего 3 часа, но это уже "вчера".
  • Недели, месяцы, годы — аппроксимация (30 дней для месяца, 365 для года). Для точности можно использовать dateTime.AddMonths(1), но для UI обычно достаточно приближения.
  • Склонения — метод FormatNumber реализует правила русского языка: 1 минута, 2 минуты, 5 минут, 21 минута и т.д.

Примеры использования

Console.WriteLine(GetRelativeTime(DateTime.Now.AddMinutes(-1)));   // "1 минуту назад"
Console.WriteLine(GetRelativeTime(DateTime.Now.AddHours(-3)));    // "3 часа назад"
Console.WriteLine(GetRelativeTime(DateTime.Now.AddDays(-1)));     // "вчера"
Console.WriteLine(GetRelativeTime(DateTime.Now.AddDays(-7)));     // "1 неделю назад"
Console.WriteLine(GetRelativeTime(DateTime.Now.AddMonths(-2)));   // "2 месяца назад"
Console.WriteLine(GetRelativeTime(DateTime.Now.AddMinutes(5)));   // "через 5 минут"

Этот код можно легко адаптировать под английский или другие языки — достаточно изменить строки в FormatNumber и условия.

<<>>

Альтернативный подход: использование сторонней библиотеки Humanizer

Если вы не хотите писать свою реализацию и поддерживать склонения для разных языков, рекомендую использовать популярную NuGet-библиотеку Humanizer. Она уже содержит готовые методы для относительного времени на многих языках, включая русский.

Установка через NuGet:

Install-Package Humanizer

Пример использования:

using Humanizer;
using Humanizer.Localisation;

DateTime dateTime = DateTime.Now.AddHours(-5);
string relative = dateTime.Humanize(); // по умолчанию английский

// Для русского языка:
string russianRelative = dateTime.Humanize(culture: new System.Globalization.CultureInfo("ru-RU"));
Console.WriteLine(russianRelative); // "5 часов назад"

// Для будущих дат:
DateTime futureDate = DateTime.Now.AddDays(2);
Console.WriteLine(futureDate.Humanize(culture: new CultureInfo("ru-RU"))); // "через 2 дня"

Humanizer также поддерживает точные настройки:

// Максимальная единица — часы (не будет показывать дни)
string precise = dateTime.Humanize(maxUnit: TimeUnit.Hour, culture: new CultureInfo("ru-RU"));

// Минимальная единица — минуты (не будет "только что")
string minPrecise = dateTime.Humanize(minUnit: TimeUnit.Minute, culture: new CultureInfo("ru-RU"));

Плюсы: не нужно писать свою логику склонений, поддерживается множество языков, библиотека активно развивается. Минусы: дополнительная зависимость в проекте, и для очень специфических требований (например, "позавчера") может потребоваться доработка.

Если ваш проект небольшой или вы хотите полностью контролировать поведение — используйте первый вариант. Если нужна быстрая и надёжная поддержка нескольких языков — Humanizer будет лучшим выбором.