pubmed_client/pubmed/query/
boolean.rs

1//! Boolean logic methods for combining and manipulating search queries
2
3use super::SearchQuery;
4
5impl SearchQuery {
6    /// Combine this query with another using AND logic
7    ///
8    /// # Arguments
9    ///
10    /// * `other` - Another SearchQuery to combine with
11    ///
12    /// # Example
13    ///
14    /// ```
15    /// use pubmed_client::pubmed::SearchQuery;
16    ///
17    /// let query1 = SearchQuery::new().query("covid-19");
18    /// let query2 = SearchQuery::new().query("vaccine");
19    /// let combined = query1.and(query2);
20    /// ```
21    pub fn and(mut self, other: SearchQuery) -> Self {
22        // Combine the queries by wrapping each in parentheses
23        let self_query = self.build();
24        let other_query = other.build();
25
26        if !self_query.is_empty() && !other_query.is_empty() {
27            // Create a new query with the combined result
28            let combined_query = format!("({self_query}) AND ({other_query})");
29            self.terms = vec![combined_query];
30            self.filters = Vec::new();
31        } else if !other_query.is_empty() {
32            self.terms = vec![other_query];
33            self.filters = Vec::new();
34        }
35
36        // Use the higher limit if set
37        if other.limit.is_some() && (self.limit.is_none() || other.limit > self.limit) {
38            self.limit = other.limit;
39        }
40
41        self
42    }
43
44    /// Combine this query with another using OR logic
45    ///
46    /// # Arguments
47    ///
48    /// * `other` - Another SearchQuery to combine with
49    ///
50    /// # Example
51    ///
52    /// ```
53    /// use pubmed_client::pubmed::SearchQuery;
54    ///
55    /// let query1 = SearchQuery::new().query("diabetes");
56    /// let query2 = SearchQuery::new().query("hypertension");
57    /// let combined = query1.or(query2);
58    /// ```
59    pub fn or(mut self, other: SearchQuery) -> Self {
60        // Combine the queries by wrapping each in parentheses
61        let self_query = self.build();
62        let other_query = other.build();
63
64        if !self_query.is_empty() && !other_query.is_empty() {
65            // Create a new query with the combined result
66            let combined_query = format!("({self_query}) OR ({other_query})");
67            self.terms = vec![combined_query];
68            self.filters = Vec::new();
69        } else if !other_query.is_empty() {
70            self.terms = vec![other_query];
71            self.filters = Vec::new();
72        }
73
74        // Use the higher limit if set
75        if other.limit.is_some() && (self.limit.is_none() || other.limit > self.limit) {
76            self.limit = other.limit;
77        }
78
79        self
80    }
81
82    /// Negate this query using NOT logic
83    ///
84    /// # Example
85    ///
86    /// ```
87    /// use pubmed_client::pubmed::SearchQuery;
88    ///
89    /// let query = SearchQuery::new()
90    ///     .query("cancer")
91    ///     .negate();
92    /// ```
93    pub fn negate(mut self) -> Self {
94        let self_query = self.build();
95
96        if !self_query.is_empty() {
97            let negated_query = format!("NOT ({self_query})");
98            self.terms = vec![negated_query];
99            self.filters = Vec::new();
100        }
101
102        self
103    }
104
105    /// Exclude articles matching the given query
106    ///
107    /// # Arguments
108    ///
109    /// * `excluded` - SearchQuery representing articles to exclude
110    ///
111    /// # Example
112    ///
113    /// ```
114    /// use pubmed_client::pubmed::SearchQuery;
115    ///
116    /// let base_query = SearchQuery::new().query("cancer treatment");
117    /// let exclude_query = SearchQuery::new().query("animal studies");
118    /// let filtered = base_query.exclude(exclude_query);
119    /// ```
120    pub fn exclude(mut self, excluded: SearchQuery) -> Self {
121        let self_query = self.build();
122        let excluded_query = excluded.build();
123
124        if !self_query.is_empty() && !excluded_query.is_empty() {
125            let combined_query = format!("({self_query}) NOT ({excluded_query})");
126            self.terms = vec![combined_query];
127            self.filters = Vec::new();
128        }
129
130        self
131    }
132
133    /// Add parentheses around the current query for grouping
134    ///
135    /// # Example
136    ///
137    /// ```
138    /// use pubmed_client::pubmed::SearchQuery;
139    ///
140    /// let query = SearchQuery::new()
141    ///     .query("cancer")
142    ///     .or(SearchQuery::new().query("tumor"))
143    ///     .group();
144    /// ```
145    pub fn group(mut self) -> Self {
146        let self_query = self.build();
147
148        if !self_query.is_empty() {
149            let grouped_query = format!("({self_query})");
150            self.terms = vec![grouped_query];
151            self.filters = Vec::new();
152        }
153
154        self
155    }
156}
157
158#[cfg(test)]
159mod tests {
160    use super::*;
161
162    #[test]
163    fn test_and_operation() {
164        let query1 = SearchQuery::new().query("covid-19");
165        let query2 = SearchQuery::new().query("vaccine");
166        let combined = query1.and(query2);
167        assert_eq!(combined.build(), "(covid-19) AND (vaccine)");
168    }
169
170    #[test]
171    fn test_or_operation() {
172        let query1 = SearchQuery::new().query("diabetes");
173        let query2 = SearchQuery::new().query("hypertension");
174        let combined = query1.or(query2);
175        assert_eq!(combined.build(), "(diabetes) OR (hypertension)");
176    }
177
178    #[test]
179    fn test_negate_operation() {
180        let query = SearchQuery::new().query("cancer").negate();
181        assert_eq!(query.build(), "NOT (cancer)");
182    }
183
184    #[test]
185    fn test_exclude_operation() {
186        let base_query = SearchQuery::new().query("cancer treatment");
187        let exclude_query = SearchQuery::new().query("animal studies");
188        let filtered = base_query.exclude(exclude_query);
189        assert_eq!(filtered.build(), "(cancer treatment) NOT (animal studies)");
190    }
191
192    #[test]
193    fn test_group_operation() {
194        let query = SearchQuery::new().query("cancer").group();
195        assert_eq!(query.build(), "(cancer)");
196    }
197
198    #[test]
199    fn test_complex_boolean_chain() {
200        let ai_query = SearchQuery::new().query("machine learning");
201        let medicine_query = SearchQuery::new().query("medicine");
202        let exclude_query = SearchQuery::new().query("veterinary");
203
204        let final_query = ai_query.and(medicine_query).exclude(exclude_query).group();
205
206        assert_eq!(
207            final_query.build(),
208            "(((machine learning) AND (medicine)) NOT (veterinary))"
209        );
210    }
211
212    #[test]
213    fn test_and_with_empty_queries() {
214        let query1 = SearchQuery::new();
215        let query2 = SearchQuery::new().query("test");
216        let combined = query1.and(query2);
217        assert_eq!(combined.build(), "test");
218    }
219
220    #[test]
221    fn test_or_with_empty_queries() {
222        let query1 = SearchQuery::new().query("test");
223        let query2 = SearchQuery::new();
224        let combined = query1.or(query2);
225        assert_eq!(combined.build(), "test");
226    }
227
228    #[test]
229    fn test_limit_preservation_in_boolean_ops() {
230        let query1 = SearchQuery::new().query("covid").limit(10);
231        let query2 = SearchQuery::new().query("vaccine").limit(50);
232        let combined = query1.and(query2);
233        assert_eq!(combined.get_limit(), 50); // Should use higher limit
234    }
235
236    #[test]
237    fn test_negate_empty_query() {
238        let query = SearchQuery::new().negate();
239        assert_eq!(query.build(), "");
240    }
241
242    #[test]
243    fn test_exclude_empty_base() {
244        let base_query = SearchQuery::new();
245        let exclude_query = SearchQuery::new().query("test");
246        let filtered = base_query.exclude(exclude_query);
247        assert_eq!(filtered.build(), "");
248    }
249
250    #[test]
251    fn test_exclude_empty_excluded() {
252        let base_query = SearchQuery::new().query("test");
253        let exclude_query = SearchQuery::new();
254        let filtered = base_query.exclude(exclude_query);
255        assert_eq!(filtered.build(), "test");
256    }
257
258    #[test]
259    fn test_deep_boolean_nesting() {
260        let q1 = SearchQuery::new().query("a");
261        let q2 = SearchQuery::new().query("b");
262        let q3 = SearchQuery::new().query("c");
263        let q4 = SearchQuery::new().query("d");
264
265        let nested = q1.and(q2).or(q3.and(q4));
266        assert_eq!(nested.build(), "((a) AND (b)) OR ((c) AND (d))");
267    }
268
269    #[test]
270    fn test_group_empty_query() {
271        let query = SearchQuery::new().group();
272        assert_eq!(query.build(), "");
273    }
274}