smriti/search/
query_parser.rs1use super::date_parser::{DateParser, DateRange};
4
5#[derive(Debug, Clone)]
7pub enum SearchFilter {
8 Person(String),
9 Location(String),
10 DateRange(DateRange),
11 Text,
12}
13
14#[derive(Debug, Clone, Default)]
16pub struct SearchQuery {
17 pub filters: Vec<SearchFilter>,
18}
19
20impl SearchQuery {
21 pub fn new() -> Self {
22 Self {
23 filters: Vec::new(),
24 }
25 }
26
27 pub fn is_empty(&self) -> bool {
28 self.filters.is_empty()
29 }
30
31 pub fn person(&self) -> Option<&str> {
32 self.filters.iter().find_map(|f| match f {
33 SearchFilter::Person(name) => Some(name.as_str()),
34 _ => None,
35 })
36 }
37
38 pub fn location(&self) -> Option<&str> {
39 self.filters.iter().find_map(|f| match f {
40 SearchFilter::Location(loc) => Some(loc.as_str()),
41 _ => None,
42 })
43 }
44
45 pub fn date_range(&self) -> Option<&DateRange> {
46 self.filters.iter().find_map(|f| match f {
47 SearchFilter::DateRange(range) => Some(range),
48 _ => None,
49 })
50 }
51}
52
53pub struct QueryParser;
55
56impl QueryParser {
57 pub fn parse(input: &str) -> SearchQuery {
58 let input = input.trim();
59 if input.is_empty() {
60 return SearchQuery::new();
61 }
62
63 let mut query = SearchQuery::new();
64 let mut remaining = input.to_string();
65
66 if let Some((before, location)) = Self::extract_in_location(&remaining) {
67 query.filters.push(SearchFilter::Location(location));
68 remaining = before;
69 }
70
71 if let Some((non_date, date_range)) = Self::extract_date(&remaining) {
72 query.filters.push(SearchFilter::DateRange(date_range));
73 remaining = non_date;
74 }
75
76 let remaining = remaining.trim();
77 if !remaining.is_empty() {
78 if Self::looks_like_location(remaining) {
79 query
80 .filters
81 .push(SearchFilter::Location(remaining.to_string()));
82 } else {
83 query
84 .filters
85 .push(SearchFilter::Person(remaining.to_string()));
86 }
87 }
88
89 if query.is_empty() {
90 query.filters.push(SearchFilter::Text);
91 }
92
93 query
94 }
95
96 fn extract_in_location(input: &str) -> Option<(String, String)> {
97 let lower = input.to_lowercase();
98 if let Some(idx) = lower.rfind(" in ") {
99 let before = input[..idx].trim().to_string();
100 let location = input[idx + 4..].trim().to_string();
101 if !location.is_empty() {
102 return Some((before, location));
103 }
104 }
105 None
106 }
107
108 fn extract_date(input: &str) -> Option<(String, DateRange)> {
109 if let Some(range) = DateParser::parse(input) {
110 return Some((String::new(), range));
111 }
112
113 let words: Vec<&str> = input.split_whitespace().collect();
114
115 if words.len() >= 2 {
116 let last_two = format!("{} {}", words[words.len() - 2], words[words.len() - 1]);
117 if let Some(range) = DateParser::parse(&last_two) {
118 let before = words[..words.len() - 2].join(" ");
119 return Some((before, range));
120 }
121 }
122
123 if let Some(last) = words.last() {
124 if let Some(range) = DateParser::parse(last) {
125 let before = words[..words.len() - 1].join(" ");
126 return Some((before, range));
127 }
128 }
129
130 None
131 }
132
133 fn looks_like_location(text: &str) -> bool {
134 let lower = text.to_lowercase();
135 let location_words = [
136 "city",
137 "country",
138 "beach",
139 "mountain",
140 "park",
141 "airport",
142 "station",
143 "hotel",
144 "restaurant",
145 "museum",
146 "temple",
147 "shrine",
148 ];
149
150 if location_words.iter().any(|w| lower.contains(w)) {
151 return true;
152 }
153
154 let known_locations = [
155 "japan",
156 "tokyo",
157 "usa",
158 "new york",
159 "london",
160 "paris",
161 "france",
162 "germany",
163 "berlin",
164 "italy",
165 "rome",
166 "spain",
167 "china",
168 "beijing",
169 "australia",
170 "sydney",
171 "canada",
172 "toronto",
173 "india",
174 "hyderabad",
175 "delhi",
176 ];
177
178 known_locations.iter().any(|loc| lower.contains(loc))
179 }
180}
181
182#[cfg(test)]
183mod tests {
184 use super::*;
185
186 #[test]
187 fn test_parse_person_in_location() {
188 let query = QueryParser::parse("Dad in Tokyo");
189 assert_eq!(query.person(), Some("Dad"));
190 assert_eq!(query.location(), Some("Tokyo"));
191 }
192
193 #[test]
194 fn test_parse_date_only() {
195 let query = QueryParser::parse("March 2019");
196 assert!(query.date_range().is_some());
197 }
198
199 #[test]
200 fn test_parse_person_with_date() {
201 let query = QueryParser::parse("Dad March 2019");
202 assert_eq!(query.person(), Some("Dad"));
203 assert!(query.date_range().is_some());
204 }
205}