binary search date range

This commit is contained in:
2025-12-09 20:11:50 +01:00
parent 5ddbc1b8e1
commit d8255bfc02
6 changed files with 214 additions and 27 deletions

38
Cargo.lock generated
View File

@@ -661,6 +661,19 @@ dependencies = [
"libc",
]
[[package]]
name = "chrono"
version = "0.4.42"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "145052bdd345b87320e369255277e3fb5152762ad123a901ef5c262dd38fe8d2"
dependencies = [
"iana-time-zone",
"js-sys",
"num-traits",
"wasm-bindgen",
"windows-link",
]
[[package]]
name = "clipboard-win"
version = "5.4.1"
@@ -1520,6 +1533,30 @@ version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dfa686283ad6dd069f105e5ab091b04c62850d3e4cf5d67debad1933f55023df"
[[package]]
name = "iana-time-zone"
version = "0.1.64"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "33e57f83510bb73707521ebaffa789ec8caf86f9657cad665b092b581d40e9fb"
dependencies = [
"android_system_properties",
"core-foundation-sys",
"iana-time-zone-haiku",
"js-sys",
"log",
"wasm-bindgen",
"windows-core 0.58.0",
]
[[package]]
name = "iana-time-zone-haiku"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f"
dependencies = [
"cc",
]
[[package]]
name = "icu_collections"
version = "2.1.1"
@@ -2671,6 +2708,7 @@ dependencies = [
name = "rlogg"
version = "0.2.0"
dependencies = [
"chrono",
"eframe",
"rayon",
"regex",

View File

@@ -17,3 +17,4 @@ regex = "1.11"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
rayon = "1.10"
chrono = "0.4"

View File

@@ -12,6 +12,18 @@ pub struct AppConfig {
pub last_search_query: String,
#[serde(default)]
pub highlight_rules: Vec<HighlightRule>,
#[serde(default)]
pub date_range_enabled: bool,
#[serde(default = "default_date_format")]
pub date_format: String,
#[serde(default)]
pub date_from: String,
#[serde(default)]
pub date_to: String,
}
fn default_date_format() -> String {
String::from("%Y-%m-%d %H:%M:%S")
}
impl AppConfig {

View File

@@ -67,9 +67,10 @@ impl LogViewerApp {
case_sensitive: config.case_sensitive,
use_regex: config.use_regex,
history: config.search_history,
date_range_enabled: false,
date_from: String::new(),
date_to: String::new(),
date_range_enabled: config.date_range_enabled,
date_format: config.date_format,
date_from: config.date_from,
date_to: config.date_to,
},
indexing_state: IndexingState::new(),
search_state: SearchState::new(),
@@ -93,6 +94,10 @@ impl LogViewerApp {
use_regex: self.search_panel_state.use_regex,
last_search_query: self.search_panel_state.query.clone(),
highlight_rules: self.highlight_manager.rules.clone(),
date_range_enabled: self.search_panel_state.date_range_enabled,
date_format: self.search_panel_state.date_format.clone(),
date_from: self.search_panel_state.date_from.clone(),
date_to: self.search_panel_state.date_to.clone(),
};
config.save();
}
@@ -110,11 +115,40 @@ impl LogViewerApp {
fn handle_search(&mut self) {
if let Some(tab) = self.active_tab() {
// Validate date format if date range is enabled
if self.search_panel_state.date_range_enabled {
use chrono::NaiveDateTime;
// Validate format by trying to parse example dates
let test_date = "2025-01-01 00:00:00";
if NaiveDateTime::parse_from_str(test_date, &self.search_panel_state.date_format).is_err() {
eprintln!("Invalid date format: {}", self.search_panel_state.date_format);
eprintln!("Expected format like: %Y-%m-%d %H:%M:%S");
return;
}
// Validate date_from and date_to can be parsed
if !self.search_panel_state.date_from.is_empty() {
if NaiveDateTime::parse_from_str(&self.search_panel_state.date_from, &self.search_panel_state.date_format).is_err() {
eprintln!("Invalid date_from format. Expected format: {}", self.search_panel_state.date_format);
return;
}
}
if !self.search_panel_state.date_to.is_empty() {
if NaiveDateTime::parse_from_str(&self.search_panel_state.date_to, &self.search_panel_state.date_format).is_err() {
eprintln!("Invalid date_to format. Expected format: {}", self.search_panel_state.date_format);
return;
}
}
}
let params = SearchParams {
query: self.search_panel_state.query.clone(),
case_sensitive: self.search_panel_state.case_sensitive,
use_regex: self.search_panel_state.use_regex,
date_range_enabled: self.search_panel_state.date_range_enabled,
date_format: self.search_panel_state.date_format.clone(),
date_from: self.search_panel_state.date_from.clone(),
date_to: self.search_panel_state.date_to.clone(),
};

View File

@@ -5,6 +5,7 @@ use std::io::BufReader;
use std::path::Path;
use std::sync::{Arc, Mutex};
use std::thread;
use chrono::NaiveDateTime;
use crate::line_index::LineIndex;
use crate::types::FilteredLine;
@@ -44,6 +45,7 @@ pub struct SearchParams {
pub case_sensitive: bool,
pub use_regex: bool,
pub date_range_enabled: bool,
pub date_format: String,
pub date_from: String,
pub date_to: String,
}
@@ -102,6 +104,107 @@ pub fn start_search(
});
}
fn format_to_regex(format: &str) -> Option<String> {
// Convert chrono format to regex pattern
let mut regex = format.to_string();
regex = regex.replace("%Y", r"\d{4}"); // 4-digit year
regex = regex.replace("%m", r"\d{2}"); // 2-digit month
regex = regex.replace("%d", r"\d{2}"); // 2-digit day
regex = regex.replace("%H", r"\d{2}"); // 2-digit hour
regex = regex.replace("%M", r"\d{2}"); // 2-digit minute
regex = regex.replace("%S", r"\d{2}"); // 2-digit second
regex = regex.replace("%I", r"\d{2}"); // 2-digit hour (12h)
regex = regex.replace("%p", r"(AM|PM)"); // AM/PM
Some(regex)
}
fn extract_and_parse_date(content: &str, format: &str) -> Option<NaiveDateTime> {
// Convert format to regex pattern
let pattern = format_to_regex(format)?;
let regex = Regex::new(&pattern).ok()?;
let date_str = regex.captures(content)?.get(0)?.as_str();
// Parse using the provided format
NaiveDateTime::parse_from_str(date_str, format).ok()
}
fn find_line_with_date(
line_index: &LineIndex,
file_handle: &mut BufReader<File>,
start_line: usize,
format: &str,
move_down: bool,
) -> Option<(usize, NaiveDateTime)> {
let max_search = 1000; // Search up to 10 lines
let range: Box<dyn Iterator<Item = usize>> = if move_down {
Box::new(start_line..std::cmp::min(start_line + max_search, line_index.total_lines))
} else {
Box::new((start_line.saturating_sub(max_search)..=start_line).rev())
};
for line_num in range {
if let Some(content) = line_index.read_line(file_handle, line_num) {
if let Some(date) = extract_and_parse_date(&content, format) {
return Some((line_num, date));
}
}
}
None
}
fn binary_search_date(
line_index: &LineIndex,
file_handle: &mut BufReader<File>,
target_date: &NaiveDateTime,
format: &str,
find_first: bool, // true = find first occurrence, false = find last
) -> Option<usize> {
let mut left = 0;
let mut right = line_index.total_lines;
let mut result = None;
while left < right {
let mid = (left + right) / 2;
// Find a line with a date starting from mid, moving down
match find_line_with_date(line_index, file_handle, mid, format, true) {
Some((line_with_date, date)) => {
if find_first {
// Finding first line >= target_date
if date >= *target_date {
result = Some(line_with_date);
right = mid;
} else {
left = mid + 1;
}
} else {
// Finding last line <= target_date
if date <= *target_date {
result = Some(line_with_date);
// Ensure we make progress in the binary search
left = mid + 1;
} else {
right = mid;
}
}
}
None => {
// Can't find a date near mid, try searching in a larger range
// or skip this section
if find_first {
// When finding first, if we can't find a date, try the right half
left = mid + 1;
} else {
// When finding last, if we can't find a date, try the left half
right = mid;
}
}
}
}
result
}
fn find_date_range(
params: &SearchParams,
line_index: &LineIndex,
@@ -111,35 +214,23 @@ fn find_date_range(
return None;
}
// Parse the target dates using the provided format
let date_from = NaiveDateTime::parse_from_str(&params.date_from, &params.date_format).ok()?;
let date_to = NaiveDateTime::parse_from_str(&params.date_to, &params.date_format).ok()?;
let file = File::open(file_path).ok()?;
let mut file_handle = BufReader::new(file);
let mut start_line = None;
let mut end_line = None;
// Binary search for first line >= date_from
let start_line = binary_search_date(line_index, &mut file_handle, &date_from, &params.date_format, true)?;
// Find first line containing date_from text
for line_num in 0..line_index.total_lines {
if let Some(content) = line_index.read_line(&mut file_handle, line_num) {
if content.contains(&params.date_from) {
start_line = Some(line_num);
break;
}
}
}
// Binary search for last line <= date_to
let end_line = binary_search_date(line_index, &mut file_handle, &date_to, &params.date_format, false)?;
// Find last line containing date_to text
for line_num in (0..line_index.total_lines).rev() {
if let Some(content) = line_index.read_line(&mut file_handle, line_num) {
if content.contains(&params.date_to) {
end_line = Some(line_num + 1); // +1 to include this line
break;
}
}
}
match (start_line, end_line) {
(Some(start), Some(end)) if start < end => Some((start, end)),
_ => None,
if start_line < end_line {
Some((start_line, end_line + 1)) // +1 to include the end line
} else {
None
}
}

View File

@@ -9,6 +9,7 @@ pub struct SearchPanelState {
pub use_regex: bool,
pub history: Vec<String>,
pub date_range_enabled: bool,
pub date_format: String,
pub date_from: String,
pub date_to: String,
}
@@ -110,6 +111,16 @@ pub fn render_search_panel(
// Date range fields (show when date_range_enabled is true)
if state.date_range_enabled {
ui.add_space(6.0);
ui.horizontal(|ui| {
ui.label("Format:");
ui.add(
egui::TextEdit::singleline(&mut state.date_format)
.desired_width(200.0)
.hint_text("%Y-%m-%d %H:%M:%S")
);
});
ui.add_space(4.0);
ui.horizontal(|ui| {
ui.label("From:");
ui.add(