yan_log/unit.rs
1use std::fs;
2use std::io;
3use std::path::PathBuf;
4use std::time::SystemTime;
5
6/// 将 u32 数字格式化为2位数字的字节切片
7/// - 将输入的数字格式化为固定2位宽度的字符串表示,不足两位时前面补零。
8/// - 对于0-9的数字使用预定义的字节字面量以获得最佳性能。
9///
10/// # 参数
11/// - `i`: 要格式化的无符号32位整数
12/// - `buf`: 用于存储格式化结果的缓冲区,必须至少10字节长度
13///
14/// # 返回值
15/// - `&[u8]`: 指向格式化结果的字节切片引用,长度为2
16///
17/// # 性能优化
18/// - 对于0-9的数字直接返回预定义的字节字面量,避免函数调用
19/// - 对于10及以上的数字使用高效的算法
20/// - 使用内联优化减少函数调用开销
21/// # 示例
22/// ```rust,ignore
23/// let mut buf = [0u8; 3];
24/// let result = format_u8_as_padded_2_digits(5, &mut buf);
25/// assert_eq!(result, b"05");
26///
27/// let result2 = format_u8_as_padded_2_digits(15, &mut buf);
28/// assert_eq!(result2, b"15");
29/// ```
30#[inline]
31pub(crate) fn format_u8_as_padded_2_digits(i: u8, buf: &mut [u8; 3]) -> &[u8] {
32 match i {
33 0 => b"00",
34 1 => b"01",
35 2 => b"02",
36 3 => b"03",
37 4 => b"04",
38 5 => b"05",
39 6 => b"06",
40 7 => b"07",
41 8 => b"08",
42 9 => b"09",
43 _ => proc_tools_core::utils_core::impl_to_ascii::itoa_buf_u8(buf, i),
44 }
45}
46
47/// 将 u32 数字格式化为3位数字的字节切片
48/// - 将输入的数字格式化为固定3位宽度的字符串表示,不足三位时前面补零。
49/// - 针对不同范围的数字使用不同的优化策略。
50///
51/// # 参数
52/// - `i`: 要格式化的无符号32位整数
53/// - `buf`: 用于存储格式化结果的缓冲区,必须至少10字节长度
54///
55/// # 返回值
56/// - `&[u8]`: 指向格式化结果的字节切片引用,长度为3
57///
58/// # 算法说明
59/// - 0-9: 手动填充两个前导零
60/// - 10-99: 手动填充一个前导零
61/// - 100+: 使用高效的算法
62///
63/// # 性能优化
64/// - 使用内联(always)确保关键路径的性能
65/// - 对小数字进行手动处理避免函数调用
66/// - 缓冲区预初始化为'0'字符减少赋值操作
67///
68/// # 示例
69/// ```rust,ignore
70/// let mut buf = [b'0'; 5];
71/// let result = format_u16_as_padded_3_digits(5, &mut buf);
72/// assert_eq!(result, b"005");
73///
74/// let result2 = format_u16_as_padded_3_digits(42, &mut buf);
75/// assert_eq!(result2, b"042");
76///
77/// let result3 = format_u16_as_padded_3_digits(123, &mut buf);
78/// assert_eq!(result3, b"123");
79/// ```
80#[inline(always)]
81pub(crate) fn format_u16_as_padded_3_digits(i: u16, buf: &mut [u8; 5]) -> &[u8] {
82 if i < 10 {
83 buf[0] = b'0';
84 buf[1] = b'0';
85 buf[2] = b'0' + i as u8;
86 &buf[0..3]
87 } else if i < 100 {
88 buf[0] = b'0';
89 buf[1] = b'0' + (i / 10) as u8; // 十位
90 buf[2] = b'0' + (i % 10) as u8; // 个位
91 &buf[0..3]
92 } else {
93 proc_tools_core::utils_core::impl_to_ascii::itoa_buf_u16(buf, i)
94 }
95}
96
97/// 将毫秒级时间戳转换为日期时间组件
98/// - 将自 Unix 纪元(1970-01-01 00:00:00 UTC)以来的毫秒数转换为对应的
99/// - 使用 UTC/GMT (世界协调时间),如果要用北京时间的时区,需手动为时间戳加 28_800_000(8小时)
100/// - 年、月、日、时、分、秒和毫秒组件。使用简单的计算避免昂贵的日期时间库调用。
101///
102/// # 参数
103/// - `timestamp`: 毫秒级 Unix 时间戳
104///
105/// # 返回值
106/// - `(u32, u8, u8, u8, u8, u8, u16)`: 日期时间组件的元组,包含:
107/// - `u32`: 年份(如 2023)
108/// - `u8`: 月份(1-12)
109/// - `u8`: 日期(1-31)
110/// - `u8`: 小时(0-23)
111/// - `u8`: 分钟(0-59)
112/// - `u8`: 秒数(0-59)
113/// - `u16`: 毫秒数(0-999)
114///
115/// # 算法特点
116/// - 使用 400 年、100 年、4 年周期进行高效年份计算
117/// - 时间复杂度为 O(1),最多进行 3 次年份调整和 12 次月份调整
118/// - 正确处理闰年规则:能被4整除但不能被100整除,或能被400整除
119///
120/// # 注意事项
121/// - 输入时间戳应为毫秒级(Unix 时间戳 × 1000)
122/// - 返回的月份和日期从1开始(1月=1,1日=1)
123/// - 算法假设格里高利历法,适用于 1970 年之后的日期
124///
125/// # 示例
126/// ```rust,ignore
127/// let timestamp = 1698242456123; // 世界协调时间:2023-10-25 14:00:56.123
128/// let (year, month, day, hour, minute, second, millis) = timestamp_to_datetime(timestamp);
129/// assert_eq!(year, 2023);
130/// assert_eq!(month, 10);
131/// assert_eq!(day, 25);
132/// assert_eq!(hour, 14);
133/// assert_eq!(minute, 0);
134/// assert_eq!(second, 56);
135/// assert_eq!(millis, 123);
136///
137/// //北京时间需要在原时间戳上加28_800_000
138/// let mut timestamp = 1698242456123; // 世界协调时间:2023-10-25 14:00:56.123
139/// timestamp += 28_800_000; // 北京时间:2023-10-25 22:00:56.123
140/// let (year, month, day, hour, minute, second, millis) = timestamp_to_datetime(timestamp);
141/// assert_eq!(hour, 22);
142/// ```
143///
144/// # 性能
145/// - 使用整数运算,避免浮点数计算
146/// - 循环次数有上限(年份最多3次,月份最多12次)
147/// - 适合高性能场景,如日志处理、时间序列数据分析
148#[inline(always)]
149pub fn timestamp_ms_to_datetime(timestamp: u128) -> (u32, u8, u8, u8, u8, u8, u16) {
150 // 毫秒时间戳 -> 秒 + 毫秒
151 let total_seconds = timestamp / 1000;
152 let milliseconds = timestamp % 1000;
153
154 // 计算天数和当天的秒数
155 let days = total_seconds / 86400;
156 let seconds_in_day = total_seconds % 86400;
157
158 // 计算时分秒
159 let hours = (seconds_in_day / 3600) as u8;
160 let minutes = ((seconds_in_day % 3600) / 60) as u8;
161 let seconds = (seconds_in_day % 60) as u8;
162
163 // 精确年份计算(400年/100年/4年周期)
164 let n400 = days / 146097; // 400年周期天数: 146097
165 let mut year = 1970 + n400 * 400;
166 let mut remaining_days = days % 146097;
167
168 let n100 = remaining_days / 36524; // 100年周期天数: 36524
169 year += n100 * 100;
170 remaining_days %= 36524;
171
172 let n4 = remaining_days / 1461; // 4年周期天数: 1461
173 year += n4 * 4;
174 remaining_days %= 1461;
175
176 // 剩余天数最多处理3次(O(1)时间)
177 for _ in 0..3 {
178 let is_leap = (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0);
179 if remaining_days >= 366 && is_leap {
180 remaining_days -= 366;
181 year += 1;
182 } else if remaining_days >= 365 {
183 remaining_days -= 365;
184 year += 1;
185 } else {
186 break;
187 }
188 }
189
190 // 计算月份和日期(最多12次循环,常数时间)
191 let mut month = 1;
192 let mut days_in_month = 31;
193 while remaining_days >= days_in_month {
194 remaining_days -= days_in_month;
195 month += 1;
196 days_in_month = match month {
197 2 => {
198 if (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0) {
199 29
200 } else {
201 28
202 }
203 }
204 4 | 6 | 9 | 11 => 30,
205 _ => 31,
206 };
207 }
208
209 let day = (remaining_days + 1) as u8;
210
211 (
212 year as u32,
213 month,
214 day,
215 hours,
216 minutes,
217 seconds,
218 milliseconds as u16,
219 )
220}
221
222/// 根据日志文件命名规则,查询目录下所有匹配的文件路径,并按修改时间从新到旧排序
223///
224/// # 参数
225/// - `dir_path`: 日志目录路径
226/// - `pattern`: 文件命名模式(如 "app_%Y-%m-%d.log")
227///
228/// # 返回
229/// - `Ok(Vec<PathBuf>)`: 匹配文件的路径列表(按修改时间新到旧排序)
230/// - `Err(std::io::Error)`: 目录读取或文件操作错误
231#[inline]
232pub(crate) fn find_log_files(dir_path: &str, pattern: &str) -> io::Result<Vec<PathBuf>> {
233 let fragments = parse_pattern(pattern);
234 let mut files: Vec<(PathBuf, SystemTime)> = Vec::new();
235
236 for entry in fs::read_dir(dir_path)? {
237 let entry = entry?;
238 let path = entry.path();
239 if !path.is_file() || path.file_name().is_none() {
240 continue;
241 }
242
243 let file_name_lossy = path.file_name().unwrap().to_string_lossy();
244
245 if !matches_pattern_bytes(&file_name_lossy, &fragments) {
246 continue;
247 }
248
249 let mod_time = fs::metadata(&path)?.modified()?;
250 files.push((path, mod_time));
251 }
252
253 files.sort_by(|a, b| b.1.cmp(&a.1));
254 Ok(files.into_iter().map(|(path, _)| path).collect())
255}
256
257/// 解析日志文件名模式字符串
258/// - 将模式字符串解析为片段序列,用于后续的文件名匹配和生成。
259/// - 支持占位符语法:`%Y`(四位数年份), `%m`(月份), `%d`(日期), `%H`(小时), `%M`(分钟), `%S`(秒), `%i`(任意数字)
260///
261/// # 参数
262/// - `pattern`: 模式字符串,可以包含固定文本和占位符
263///
264/// # 返回值
265/// - `Vec<Fragment>`: 解析后的片段序列,用于文件名模式匹配
266///
267/// # 处理流程
268/// 1. 逐字节扫描模式字符串
269/// 2. 遇到 `%` 字符时检查后续字符是否为有效占位符
270/// 3. 将固定文本和占位符分别存储为不同的片段类型
271/// 4. 支持UTF-8字符的安全处理
272///
273/// # 示例
274/// ```
275/// use log_processor::parse_pattern;
276///
277/// let fragments = parse_pattern("%Y-%m-%d %H:%M:%S-%i.log");
278/// // 解析结果包含固定文本和日期时间占位符
279/// ```
280///
281/// # 注意事项
282/// - 不支持嵌套或复杂的占位符语法
283/// - 未识别的占位符会被忽略
284/// - 模式字符串必须是有效的UTF-8编码
285#[inline(always)]
286fn parse_pattern(pattern: &str) -> Vec<Fragment> {
287 let mut fragments: Vec<Fragment> = Vec::new();
288 let mut i: usize = 0;
289 let mut poi: usize = 0;
290 let bytes = pattern.as_bytes();
291 while i < pattern.len() {
292 if bytes[i] == b'%' {
293 if i + 1 == pattern.len() {
294 break;
295 }
296 let next_byte = bytes[i + 1];
297 let char_len = utf8_char_len(next_byte);
298 if char_len == 1 {
299 fragments.push(Fragment::Fixed(Box::from(&pattern[poi..i])));
300 if next_byte == b'Y' {
301 fragments.push(Fragment::PlaceholderFourDigits);
302 } else if next_byte == b'm'
303 || next_byte == b'd'
304 || next_byte == b'H'
305 || next_byte == b'M'
306 || next_byte == b'S'
307 {
308 fragments.push(Fragment::PlaceholderTwoDigits);
309 } else if next_byte == b'i' {
310 fragments.push(Fragment::PlaceholderAnyDigits);
311 } else {
312 i += 1;
313 fragments.pop();
314 continue;
315 }
316 i += 2;
317 poi = i;
318 } else {
319 // UTF-8字符处理,确保不会越界
320 let actual_len = char_len.min(bytes.len() - i);
321 i += actual_len;
322 }
323 } else {
324 i += 1;
325 }
326 }
327 if poi < pattern.len() {
328 fragments.push(Fragment::Fixed(Box::from(&pattern[poi..pattern.len()])));
329 }
330 fragments
331}
332
333/// 文件名模式片段枚举
334enum Fragment {
335 // 四位数数字占位符
336 PlaceholderFourDigits,
337 // 两位数数字占位符
338 PlaceholderTwoDigits,
339 // 任意位数数字占位符
340 PlaceholderAnyDigits,
341 // 固定字符串
342 Fixed(Box<str>),
343}
344
345/// 计算 UTF-8 字符的字节长度
346///
347/// # 参数
348/// - `next_byte`: UTF-8字符的首字节
349///
350/// # 返回值
351/// - `usize`: UTF-8字符的字节长度 (1-4)
352///
353/// # UTF-8编码规则
354/// - 单字节: 0xxxxxxx
355/// - 双字节: 110xxxxx
356/// - 三字节: 1110xxxx
357/// - 四字节: 11110xxx
358///
359/// # 示例
360/// ```rust`ignore
361/// assert_eq!(utf8_char_len(b'a'), 1); // ASCII字符
362/// assert_eq!(utf8_char_len(0xC3), 2); // 双字节UTF-8
363/// assert_eq!(utf8_char_len(0xE2), 3); // 三字节UTF-8
364/// ```
365#[inline(always)]
366fn utf8_char_len(next_byte: u8) -> usize {
367 if next_byte & 0b1110_0000 == 0b1100_0000 {
368 2
369 } else if next_byte & 0b1111_0000 == 0b1110_0000 {
370 3
371 } else if next_byte & 0b1111_1000 == 0b1111_0000 {
372 4
373 } else {
374 1 // 单字节ASCII字符或无效UTF-8
375 }
376}
377
378/// 检查文件名是否匹配片段模式
379/// - 验证给定的文件名是否符合预解析的片段模式结构。
380/// - 逐个匹配片段,确保文件名在结构和内容上符合预期格式。
381///
382/// # 参数
383/// - `file_name`: 要检查的文件名字符串
384/// - `fragments`: 预解析的模式片段序列
385///
386/// # 返回值
387/// - `bool`: 文件名是否匹配模式
388/// - `true`: 文件名完全匹配所有片段
389/// - `false`: 文件名不符合模式要求
390///
391/// # 匹配规则
392/// - `Fixed`: 精确匹配固定字符串
393/// - `PlaceholderTwoDigits`: 匹配连续两个 `ASCII` 数字
394/// - `PlaceholderFourDigits`: 匹配连续四个 `ASCII` 数字
395/// - `PlaceholderAnyDigits`: 匹配连续 1-39 个 `ASCII` 数字
396#[inline(always)]
397fn matches_pattern_bytes(file_name: &str, fragments: &[Fragment]) -> bool {
398 let mut pos = 0;
399 let file_name_bytes = file_name.as_bytes();
400 for fragment in fragments {
401 match fragment {
402 Fragment::Fixed(s) => {
403 if !file_name_bytes[pos..pos + s.len()].starts_with(s.as_bytes()) {
404 return false;
405 }
406 pos += s.len();
407 }
408 Fragment::PlaceholderTwoDigits => {
409 if !file_name_bytes[pos].is_ascii_digit() {
410 return false;
411 }
412 pos += 1;
413 if !file_name_bytes[pos].is_ascii_digit() {
414 return false;
415 }
416 pos += 1;
417 }
418 Fragment::PlaceholderFourDigits => {
419 for ch in &file_name_bytes[pos..pos + 4] {
420 if !ch.is_ascii_digit() {
421 return false;
422 }
423 }
424 pos += 4;
425 }
426 Fragment::PlaceholderAnyDigits => {
427 let remain_str = &file_name_bytes[pos..];
428 let remain_len = remain_str.len().min(39);
429 let mut any_digits = 0;
430 for i in &file_name_bytes[pos..pos + remain_len] {
431 if !i.is_ascii_digit() {
432 break;
433 }
434 any_digits += 1;
435 }
436 if any_digits == 0 {
437 return false;
438 }
439 pos += any_digits;
440 }
441 }
442 }
443 pos == file_name_bytes.len()
444}