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.
366 lines
12 KiB
Rust
366 lines
12 KiB
Rust
/// Форматирование 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('.'));
|
||
}
|
||
}
|