binary search date range
This commit is contained in:
38
Cargo.lock
generated
38
Cargo.lock
generated
@@ -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",
|
||||
|
||||
@@ -17,3 +17,4 @@ regex = "1.11"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
rayon = "1.10"
|
||||
chrono = "0.4"
|
||||
|
||||
@@ -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 {
|
||||
|
||||
40
src/main.rs
40
src/main.rs
@@ -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(),
|
||||
};
|
||||
|
||||
139
src/search.rs
139
src/search.rs
@@ -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(¶ms.date_from, ¶ms.date_format).ok()?;
|
||||
let date_to = NaiveDateTime::parse_from_str(¶ms.date_to, ¶ms.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, ¶ms.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(¶ms.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, ¶ms.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(¶ms.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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user