binary search date range
This commit is contained in:
38
Cargo.lock
generated
38
Cargo.lock
generated
@@ -661,6 +661,19 @@ dependencies = [
|
|||||||
"libc",
|
"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]]
|
[[package]]
|
||||||
name = "clipboard-win"
|
name = "clipboard-win"
|
||||||
version = "5.4.1"
|
version = "5.4.1"
|
||||||
@@ -1520,6 +1533,30 @@ version = "0.2.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "dfa686283ad6dd069f105e5ab091b04c62850d3e4cf5d67debad1933f55023df"
|
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]]
|
[[package]]
|
||||||
name = "icu_collections"
|
name = "icu_collections"
|
||||||
version = "2.1.1"
|
version = "2.1.1"
|
||||||
@@ -2671,6 +2708,7 @@ dependencies = [
|
|||||||
name = "rlogg"
|
name = "rlogg"
|
||||||
version = "0.2.0"
|
version = "0.2.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"chrono",
|
||||||
"eframe",
|
"eframe",
|
||||||
"rayon",
|
"rayon",
|
||||||
"regex",
|
"regex",
|
||||||
|
|||||||
@@ -17,3 +17,4 @@ regex = "1.11"
|
|||||||
serde = { version = "1.0", features = ["derive"] }
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
serde_json = "1.0"
|
serde_json = "1.0"
|
||||||
rayon = "1.10"
|
rayon = "1.10"
|
||||||
|
chrono = "0.4"
|
||||||
|
|||||||
@@ -12,6 +12,18 @@ pub struct AppConfig {
|
|||||||
pub last_search_query: String,
|
pub last_search_query: String,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub highlight_rules: Vec<HighlightRule>,
|
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 {
|
impl AppConfig {
|
||||||
|
|||||||
40
src/main.rs
40
src/main.rs
@@ -67,9 +67,10 @@ impl LogViewerApp {
|
|||||||
case_sensitive: config.case_sensitive,
|
case_sensitive: config.case_sensitive,
|
||||||
use_regex: config.use_regex,
|
use_regex: config.use_regex,
|
||||||
history: config.search_history,
|
history: config.search_history,
|
||||||
date_range_enabled: false,
|
date_range_enabled: config.date_range_enabled,
|
||||||
date_from: String::new(),
|
date_format: config.date_format,
|
||||||
date_to: String::new(),
|
date_from: config.date_from,
|
||||||
|
date_to: config.date_to,
|
||||||
},
|
},
|
||||||
indexing_state: IndexingState::new(),
|
indexing_state: IndexingState::new(),
|
||||||
search_state: SearchState::new(),
|
search_state: SearchState::new(),
|
||||||
@@ -93,6 +94,10 @@ impl LogViewerApp {
|
|||||||
use_regex: self.search_panel_state.use_regex,
|
use_regex: self.search_panel_state.use_regex,
|
||||||
last_search_query: self.search_panel_state.query.clone(),
|
last_search_query: self.search_panel_state.query.clone(),
|
||||||
highlight_rules: self.highlight_manager.rules.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();
|
config.save();
|
||||||
}
|
}
|
||||||
@@ -110,11 +115,40 @@ impl LogViewerApp {
|
|||||||
|
|
||||||
fn handle_search(&mut self) {
|
fn handle_search(&mut self) {
|
||||||
if let Some(tab) = self.active_tab() {
|
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 {
|
let params = SearchParams {
|
||||||
query: self.search_panel_state.query.clone(),
|
query: self.search_panel_state.query.clone(),
|
||||||
case_sensitive: self.search_panel_state.case_sensitive,
|
case_sensitive: self.search_panel_state.case_sensitive,
|
||||||
use_regex: self.search_panel_state.use_regex,
|
use_regex: self.search_panel_state.use_regex,
|
||||||
date_range_enabled: self.search_panel_state.date_range_enabled,
|
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_from: self.search_panel_state.date_from.clone(),
|
||||||
date_to: self.search_panel_state.date_to.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::path::Path;
|
||||||
use std::sync::{Arc, Mutex};
|
use std::sync::{Arc, Mutex};
|
||||||
use std::thread;
|
use std::thread;
|
||||||
|
use chrono::NaiveDateTime;
|
||||||
|
|
||||||
use crate::line_index::LineIndex;
|
use crate::line_index::LineIndex;
|
||||||
use crate::types::FilteredLine;
|
use crate::types::FilteredLine;
|
||||||
@@ -44,6 +45,7 @@ pub struct SearchParams {
|
|||||||
pub case_sensitive: bool,
|
pub case_sensitive: bool,
|
||||||
pub use_regex: bool,
|
pub use_regex: bool,
|
||||||
pub date_range_enabled: bool,
|
pub date_range_enabled: bool,
|
||||||
|
pub date_format: String,
|
||||||
pub date_from: String,
|
pub date_from: String,
|
||||||
pub date_to: 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(
|
fn find_date_range(
|
||||||
params: &SearchParams,
|
params: &SearchParams,
|
||||||
line_index: &LineIndex,
|
line_index: &LineIndex,
|
||||||
@@ -111,35 +214,23 @@ fn find_date_range(
|
|||||||
return None;
|
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 file = File::open(file_path).ok()?;
|
||||||
let mut file_handle = BufReader::new(file);
|
let mut file_handle = BufReader::new(file);
|
||||||
|
|
||||||
let mut start_line = None;
|
// Binary search for first line >= date_from
|
||||||
let mut end_line = None;
|
let start_line = binary_search_date(line_index, &mut file_handle, &date_from, ¶ms.date_format, true)?;
|
||||||
|
|
||||||
// Find first line containing date_from text
|
// Binary search for last line <= date_to
|
||||||
for line_num in 0..line_index.total_lines {
|
let end_line = binary_search_date(line_index, &mut file_handle, &date_to, ¶ms.date_format, false)?;
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Find last line containing date_to text
|
if start_line < end_line {
|
||||||
for line_num in (0..line_index.total_lines).rev() {
|
Some((start_line, end_line + 1)) // +1 to include the end line
|
||||||
if let Some(content) = line_index.read_line(&mut file_handle, line_num) {
|
} else {
|
||||||
if content.contains(¶ms.date_to) {
|
None
|
||||||
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,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ pub struct SearchPanelState {
|
|||||||
pub use_regex: bool,
|
pub use_regex: bool,
|
||||||
pub history: Vec<String>,
|
pub history: Vec<String>,
|
||||||
pub date_range_enabled: bool,
|
pub date_range_enabled: bool,
|
||||||
|
pub date_format: String,
|
||||||
pub date_from: String,
|
pub date_from: String,
|
||||||
pub date_to: String,
|
pub date_to: String,
|
||||||
}
|
}
|
||||||
@@ -110,6 +111,16 @@ pub fn render_search_panel(
|
|||||||
// Date range fields (show when date_range_enabled is true)
|
// Date range fields (show when date_range_enabled is true)
|
||||||
if state.date_range_enabled {
|
if state.date_range_enabled {
|
||||||
ui.add_space(6.0);
|
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.horizontal(|ui| {
|
||||||
ui.label("From:");
|
ui.label("From:");
|
||||||
ui.add(
|
ui.add(
|
||||||
|
|||||||
Reference in New Issue
Block a user