/// Форматирование 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::() { 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('.')); } }