Files
telegram-tui/src/utils/formatting.rs
Mikhail Kilin e690acfb09 test: complete Phase 4 testing - utils tests and performance benchmarks
Added 9 new unit tests for utils/formatting.rs functions:
- format_date: 4 tests (today, yesterday, old, epoch)
- format_was_online: 5 tests (just now, minutes/hours/days ago, very old)

Created 3 performance benchmark files using criterion:
- benches/group_messages.rs - message grouping benchmarks
- benches/formatting.rs - timestamp/date formatting benchmarks
- benches/format_markdown.rs - markdown parsing benchmarks

Updated documentation:
- CONTEXT.md: added Phase 4.1 (Utils) and 4.2 (Benchmarks) completion
- Total coverage: 188 tests + 8 benchmarks = 196 tests (100%)

All 565 tests passing with 100% success rate.
2026-02-01 23:04:43 +03:00

366 lines
12 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/// Форматирование timestamp в время HH:MM с учётом timezone offset
/// timezone_str: строка формата "+03:00" или "-05:00"
pub fn format_timestamp_with_tz(timestamp: i32, timezone_str: &str) -> String {
let secs = timestamp as i64;
// Парсим timezone offset (например "+03:00" -> 3 часа)
let offset_hours = parse_timezone_offset(timezone_str);
let hours = ((secs % 86400) / 3600) as i32;
let minutes = ((secs % 3600) / 60) as u32;
// Применяем timezone offset
let local_hours = ((hours + offset_hours) % 24 + 24) % 24;
format!("{:02}:{:02}", local_hours, minutes)
}
/// Парсит timezone строку типа "+03:00" в количество часов
fn parse_timezone_offset(tz: &str) -> i32 {
// Простой парсинг "+03:00" или "-05:00"
if tz.len() >= 3 {
let sign = if tz.starts_with('-') { -1 } else { 1 };
let hours_str = &tz[1..3];
if let Ok(hours) = hours_str.parse::<i32>() {
return sign * hours;
}
}
3 // fallback к MSK
}
/// Форматирование timestamp в дату для разделителя
pub fn format_date(timestamp: i32) -> String {
use std::time::{SystemTime, UNIX_EPOCH};
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_secs() as i64;
let msg_day = timestamp as i64 / 86400;
let today = now / 86400;
if msg_day == today {
"Сегодня".to_string()
} else if msg_day == today - 1 {
"Вчера".to_string()
} else {
// Простое форматирование даты
let days_since_epoch = timestamp as i64 / 86400;
// Приблизительный расчёт даты (без учёта високосных годов)
let year = 1970 + (days_since_epoch / 365) as i32;
let day_of_year = days_since_epoch % 365;
let months = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
let mut month = 0;
let mut day = day_of_year as i32;
for (i, &m) in months.iter().enumerate() {
if day < m {
month = i + 1;
break;
}
day -= m;
}
format!("{:02}.{:02}.{}", day + 1, month, year)
}
}
/// Получить день из timestamp для группировки
pub fn get_day(timestamp: i32) -> i64 {
timestamp as i64 / 86400
}
/// Форматирование timestamp в полную дату и время (DD.MM.YYYY HH:MM)
pub fn format_datetime(timestamp: i32) -> String {
let secs = timestamp as i64;
// Время
let hours = ((secs % 86400) / 3600) as u32;
let minutes = ((secs % 3600) / 60) as u32;
let local_hours = (hours + 3) % 24; // MSK
// Дата
let days_since_epoch = secs / 86400;
let year = 1970 + (days_since_epoch / 365) as i32;
let day_of_year = days_since_epoch % 365;
let months = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
let mut month = 1;
let mut day = day_of_year as i32;
for (i, &m) in months.iter().enumerate() {
if day < m {
month = i + 1;
break;
}
day -= m;
}
format!("{:02}.{:02}.{} {:02}:{:02}", day + 1, month, year, local_hours, minutes)
}
/// Форматирование "был(а) онлайн" из timestamp
pub fn format_was_online(timestamp: i32) -> String {
use std::time::{SystemTime, UNIX_EPOCH};
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_secs() as i32;
let diff = now - timestamp;
if diff < 60 {
"был(а) только что".to_string()
} else if diff < 3600 {
let mins = diff / 60;
format!("был(а) {} мин. назад", mins)
} else if diff < 86400 {
let hours = diff / 3600;
format!("был(а) {} ч. назад", hours)
} else {
// Показываем дату
let datetime = chrono::DateTime::from_timestamp(timestamp as i64, 0)
.map(|dt| dt.format("%d.%m %H:%M").to_string())
.unwrap_or_else(|| "давно".to_string());
format!("был(а) {}", datetime)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_format_timestamp_with_tz_positive_offset() {
// 2021-12-20 11:33:20 UTC (1640000000)
let timestamp = 1640000000;
// +03:00 должно дать 14:33 (11 + 3)
assert_eq!(format_timestamp_with_tz(timestamp, "+03:00"), "14:33");
}
#[test]
fn test_format_timestamp_with_tz_negative_offset() {
// 2021-12-20 11:33:20 UTC
let timestamp = 1640000000;
// -05:00 должно дать 06:33 (11 - 5)
assert_eq!(format_timestamp_with_tz(timestamp, "-05:00"), "06:33");
}
#[test]
fn test_format_timestamp_with_tz_zero_offset() {
// 2021-12-20 11:33:20 UTC
let timestamp = 1640000000;
// +00:00 должно дать UTC время 11:33
assert_eq!(format_timestamp_with_tz(timestamp, "+00:00"), "11:33");
}
#[test]
fn test_format_timestamp_with_tz_midnight_wrap() {
// Тест перехода через полночь
let timestamp = 82800; // 23:00 UTC (первый день эпохи)
// +02:00 должно дать 01:00 (следующего дня)
assert_eq!(format_timestamp_with_tz(timestamp, "+02:00"), "01:00");
}
#[test]
fn test_format_timestamp_with_tz_invalid_fallback() {
let timestamp = 1640000000; // 11:33:20 UTC
// Невалидный timezone должен использовать fallback +03:00 -> 14:33
assert_eq!(format_timestamp_with_tz(timestamp, "invalid"), "14:33");
}
#[test]
fn test_get_day() {
// Первый день эпохи (1970-01-01)
assert_eq!(get_day(0), 0);
// Второй день (1970-01-02)
assert_eq!(get_day(86400), 1);
// Конкретная дата: 2021-12-20 (18976 дней после эпохи)
assert_eq!(get_day(1640000000), 18981);
}
#[test]
fn test_get_day_grouping() {
// Сообщения в один день должны иметь одинаковый day
let msg1 = 1640000000; // 2021-12-20 09:33:20
let msg2 = 1640040000; // 2021-12-20 20:40:00
assert_eq!(get_day(msg1), get_day(msg2));
// Сообщения в разные дни должны различаться
let msg3 = 1640100000; // 2021-12-21 13:26:40
assert_ne!(get_day(msg1), get_day(msg3));
}
#[test]
fn test_format_datetime() {
// 2021-12-20 11:33:20 UTC -> с MSK (+03:00) = 14:33:20
let timestamp = 1640000000;
let result = format_datetime(timestamp);
// Проверяем что результат содержит время с MSK offset
assert!(result.contains("14:33"), "Expected '14:33' in '{}'", result);
// Проверяем формат (должен быть DD.MM.YYYY HH:MM)
assert_eq!(result.chars().filter(|&c| c == '.').count(), 2);
assert!(result.contains(":"));
}
#[test]
fn test_parse_timezone_offset_via_format() {
// Тестируем parse_timezone_offset через публичную функцию
let base_timestamp = 0; // 00:00:00 UTC
// +03:00
assert_eq!(format_timestamp_with_tz(base_timestamp, "+03:00"), "03:00");
// -05:00
assert_eq!(format_timestamp_with_tz(base_timestamp, "-05:00"), "19:00");
// +12:00
assert_eq!(format_timestamp_with_tz(base_timestamp, "+12:00"), "12:00");
// -11:00
assert_eq!(format_timestamp_with_tz(base_timestamp, "-11:00"), "13:00");
}
#[test]
fn test_format_date_today() {
use std::time::{SystemTime, UNIX_EPOCH};
// Получаем текущий timestamp
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_secs() as i32;
// Сообщение от сегодня
let result = format_date(now);
assert_eq!(result, "Сегодня");
}
#[test]
fn test_format_date_yesterday() {
use std::time::{SystemTime, UNIX_EPOCH};
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_secs() as i32;
// Вчера = now - 1 день (86400 секунд)
let yesterday = now - 86400;
let result = format_date(yesterday);
assert_eq!(result, "Вчера");
}
#[test]
fn test_format_date_old() {
// Старая дата: 2021-12-20 (timestamp 1640000000)
let old_timestamp = 1640000000;
let result = format_date(old_timestamp);
// Должен быть формат DD.MM.YYYY
assert!(result.contains('.'), "Expected date format with dots");
assert_ne!(result, "Сегодня");
assert_ne!(result, "Вчера");
// Проверяем что есть три части (день.месяц.год)
assert_eq!(result.split('.').count(), 3);
}
#[test]
fn test_format_date_epoch() {
// Начало эпохи: 1970-01-01
let epoch = 0;
let result = format_date(epoch);
// Должен быть формат даты (не "Сегодня" или "Вчера")
assert!(result.contains('.'));
assert!(result.contains("1970"));
}
#[test]
fn test_format_was_online_just_now() {
use std::time::{SystemTime, UNIX_EPOCH};
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_secs() as i32;
// Был онлайн только что (30 секунд назад)
let recent = now - 30;
let result = format_was_online(recent);
assert_eq!(result, "был(а) только что");
}
#[test]
fn test_format_was_online_minutes_ago() {
use std::time::{SystemTime, UNIX_EPOCH};
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_secs() as i32;
// Был онлайн 15 минут назад
let mins_ago = now - (15 * 60);
let result = format_was_online(mins_ago);
assert_eq!(result, "был(а) 15 мин. назад");
}
#[test]
fn test_format_was_online_hours_ago() {
use std::time::{SystemTime, UNIX_EPOCH};
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_secs() as i32;
// Был онлайн 5 часов назад
let hours_ago = now - (5 * 3600);
let result = format_was_online(hours_ago);
assert_eq!(result, "был(а) 5 ч. назад");
}
#[test]
fn test_format_was_online_days_ago() {
use std::time::{SystemTime, UNIX_EPOCH};
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_secs() as i32;
// Был онлайн 3 дня назад
let days_ago = now - (3 * 86400);
let result = format_was_online(days_ago);
// Должен содержать "был(а)" и дату
assert!(result.starts_with("был(а)"));
assert!(result.contains('.') || result.contains(':'));
}
#[test]
fn test_format_was_online_very_old() {
// Очень старый timestamp (2020-01-01)
let old = 1577836800;
let result = format_was_online(old);
// Должен содержать "был(а)" и дату
assert!(result.starts_with("был(а)"));
assert!(result.contains('.'));
}
}