Как вычислить относительное время (например, "2 часа назад") в 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 будет лучшим выбором.