Avatar ·

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

Всем привет! Я пишу небольшое приложение на C# (Windows Forms) для ведения логов событий. Мне нужно отображать дату и время каждого события не в абсолютном формате (например, "2023-10-05 14:30:00"), а в относительном — так, как это делается в соцсетях или мессенджерах: "только что", "5 минут назад", "2 часа назад", "вчера", "3 дня назад" и т.д. Я новичок в C# и пока представляю решение только через кучу if-else, но это выглядит ужасно громоздко. Например, у меня есть переменная `DateTime eventTime`, и я хочу получить строку вроде "15 минут назад". Вот мой текущий "код" (точнее, набросок), который я пытался написать: ```csharp DateTime now = DateTime.Now; TimeSpan diff = now - eventTime; if (diff.TotalMinutes < 1) return "только что"; else if (diff.TotalMinutes < 60) return $"{(int)diff.TotalMinutes} минут назад"; else if (diff.TotalHours < 24) return $"{(int)diff.TotalHours} часов назад"; // ... и так далее, но это же некрасиво и не учитывает склонения слов! ``` Проблема в том, что: 1. Код получается очень длинным и трудночитаемым. 2. Я не знаю, как правильно обрабатывать склонения (1 минута, 2 минуты, 5 минут). 3. Непонятно, как быть с "вчера" и "позавчера". 4. Что делать, если прошло больше месяца? Нужно ли показывать "3 месяца назад" или уже полную дату? Подскажите, пожалуйста, элегантное и компактное решение на C# для вычисления относительного времени. Может быть, есть какие-то готовые методы или лучшие практики? Заранее спасибо! (Использую .NET 6, но решение должно быть совместимо с .NET Framework 4.8 тоже). <<>>

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

Ваша интуиция верна — писать кучу if-else не нужно. В C# можно создать компактный и гибкий метод, который учитывает склонения слов на русском языке и правильно обрабатывает граничные случаи (только что, вчера, неделю назад и т.д.). Ниже представлено готовое решение с подробными комментариями.

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

        // Меньше минуты
        if (diff.TotalSeconds < 60)
        {
            return "только что";
        }

        // Меньше часа
        if (diff.TotalMinutes < 60)
        {
            int minutes = (int)diff.TotalMinutes;
            return $"{minutes} {DeclineWord(minutes, new[] { "минуту", "минуты", "минут" })} назад";
        }

        // Меньше суток
        if (diff.TotalHours < 24)
        {
            int hours = (int)diff.TotalHours;
            return $"{hours} {DeclineWord(hours, new[] { "час", "часа", "часов" })} назад";
        }

        // Меньше двух дней — используем "вчера" 
        if (diff.TotalDays < 2)
        {
            return "вчера";
        }

        // Меньше 30 дней
        if (diff.TotalDays < 30)
        {
            int days = (int)diff.TotalDays;
            return $"{days} {DeclineWord(days, new[] { "день", "дня", "дней" })} назад";
        }

        // Меньше 12 месяцев
        if (diff.TotalDays < 365)
        {
            int months = (int)(diff.TotalDays / 30);
            return $"{months} {DeclineWord(months, new[] { "месяц", "месяца", "месяцев" })} назад";
        }

        // Больше года — показываем полную дату 
        return eventTime.ToString("dd MMMM yyyy");
    }

    // Вспомогательный метод для склонения слов (1 минута, 2 минуты, 5 минут)
    private static string DeclineWord(int number, string[] forms)
    {
        // forms[0] — единственное число (1 минута)
        // forms[1] — множественное для 2-4 (2 минуты)
        // forms[2] — множественное для 5+ (5 минут)
        int n = Math.Abs(number) % 100;
        int n1 = n % 10;

        if (n > 10 && n < 20) return forms[2];
        if (n1 > 1 && n1 < 5) return forms[1];
        if (n1 == 1) return forms[0];

        return forms[2];
    }
}

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

DateTime testTime = DateTime.Now.AddMinutes(-5);
string result = TimeHelper.GetRelativeTime(testTime);
Console.WriteLine(result); // Выведет: "5 минут назад"

Почему это решение хорошее?

  • Компактность: весь код помещается в один статический класс.
  • Правильные склонения: метод DeclineWord корректно обрабатывает русские числительные (1 минута, 2 минуты, 5 минут, 21 минута).
  • Граничные случаи: учтены "только что", "вчера", а для дат старше года показывается полная дата.
  • Производительность: нет циклов или сложных вычислений, только простые арифметические операции.
  • Гибкость: вы легко можете изменить пороговые значения (например, заменить 30 дней на 31) или добавить новые варианты (например, "на прошлой неделе").

Важные замечания

  • Метод использует DateTime.Now, что даёт локальное время. Если нужно UTC — замените на DateTime.UtcNow.
  • Для точности в промышленных приложениях рекомендуется использовать DateTimeOffset вместо DateTime, чтобы избежать проблем с часовыми поясами.
  • Вы можете адаптировать метод для английского языка, просто заменив массив форм и строки-константы.
<<>>

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

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

// Установите пакет: Install-Package Humanizer
using Humanizer;
using Humanizer.Localisation;

DateTime eventTime = DateTime.Now.AddHours(-3);
string result = eventTime.Humanize(); // "3 часа назад"

Преимущества Humanizer

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

Пример с настройками

// Только до минут, без секунд
string result = eventTime.Humanize(precision: Humanizer.Localisation.TimeUnit.Minute, culture: new CultureInfo("ru-RU"));

Недостаток: библиотека добавляет зависимость в проект. Если ваше приложение небольшое и не требует других функций Humanizer, то проще написать свой метод, как в первом ответе. Но для крупных проектов с множеством локализаций Humanizer — отличный выбор.