Skip to main content

altium_format/query/
common.rs

1//! Shared types and utilities for query systems.
2//!
3//! This module contains common definitions used by both the record selector
4//! system and the SchQL system, reducing code duplication.
5
6use super::pattern::Pattern;
7use crate::records::sch::PinElectricalType;
8
9/// Comparison operators for property/attribute filters.
10///
11/// Used by both the record selector `[prop op value]` syntax and
12/// SchQL `[attr op value]` syntax.
13#[derive(Debug, Clone, Copy, PartialEq, Eq)]
14pub enum FilterOp {
15    /// `[attr]` - Has attribute (exists check)
16    Exists,
17    /// `=` - Exact match (case-insensitive for strings)
18    Equals,
19    /// `!=` - Not equal
20    NotEquals,
21    /// `~=` - Word match (matches word in space-separated list)
22    WordMatch,
23    /// `^=` - Starts with
24    StartsWith,
25    /// `$=` - Ends with
26    EndsWith,
27    /// `*=` - Contains substring
28    Contains,
29    /// `>` - Greater than (numeric)
30    GreaterThan,
31    /// `<` - Less than (numeric)
32    LessThan,
33    /// `>=` - Greater or equal (numeric)
34    GreaterOrEqual,
35    /// `<=` - Less or equal (numeric)
36    LessOrEqual,
37}
38
39impl FilterOp {
40    /// Parse operator from string representation.
41    pub fn try_parse(s: &str) -> Option<Self> {
42        match s {
43            "=" => Some(Self::Equals),
44            "!=" => Some(Self::NotEquals),
45            "~=" => Some(Self::WordMatch),
46            "^=" => Some(Self::StartsWith),
47            "$=" => Some(Self::EndsWith),
48            "*=" => Some(Self::Contains),
49            ">" => Some(Self::GreaterThan),
50            "<" => Some(Self::LessThan),
51            ">=" => Some(Self::GreaterOrEqual),
52            "<=" => Some(Self::LessOrEqual),
53            _ => None,
54        }
55    }
56
57    /// Get the string representation of this operator.
58    pub fn as_str(&self) -> &'static str {
59        match self {
60            Self::Exists => "",
61            Self::Equals => "=",
62            Self::NotEquals => "!=",
63            Self::WordMatch => "~=",
64            Self::StartsWith => "^=",
65            Self::EndsWith => "$=",
66            Self::Contains => "*=",
67            Self::GreaterThan => ">",
68            Self::LessThan => "<",
69            Self::GreaterOrEqual => ">=",
70            Self::LessOrEqual => "<=",
71        }
72    }
73}
74
75/// Value types for filter comparisons.
76#[derive(Debug, Clone)]
77pub enum FilterValue {
78    /// String value
79    String(String),
80    /// Numeric value (stored as f64 for flexibility)
81    Number(f64),
82    /// Boolean value
83    Bool(bool),
84    /// Glob pattern for wildcard matching
85    Pattern(Pattern),
86}
87
88impl FilterValue {
89    /// Create a string value.
90    pub fn string(s: impl Into<String>) -> Self {
91        Self::String(s.into())
92    }
93
94    /// Create a numeric value.
95    pub fn number(n: f64) -> Self {
96        Self::Number(n)
97    }
98
99    /// Create a boolean value.
100    pub fn bool(b: bool) -> Self {
101        Self::Bool(b)
102    }
103
104    /// Try to get as string reference.
105    pub fn as_str(&self) -> Option<&str> {
106        match self {
107            Self::String(s) => Some(s),
108            _ => None,
109        }
110    }
111
112    /// Try to get as number.
113    pub fn as_number(&self) -> Option<f64> {
114        match self {
115            Self::Number(n) => Some(*n),
116            Self::String(s) => s.parse().ok(),
117            _ => None,
118        }
119    }
120}
121
122/// Compare a string value against a filter using the specified operator.
123///
124/// This is the shared comparison logic used by both query systems.
125pub fn compare_filter(
126    actual: Option<&str>,
127    op: FilterOp,
128    expected: &FilterValue,
129    case_insensitive: bool,
130) -> bool {
131    match op {
132        FilterOp::Exists => actual.is_some(),
133
134        FilterOp::Equals => {
135            let expected_str = match expected {
136                FilterValue::String(s) => s.as_str(),
137                FilterValue::Number(n) => return compare_numeric(actual, FilterOp::Equals, *n),
138                FilterValue::Bool(b) => return compare_bool(actual, *b),
139                FilterValue::Pattern(p) => return actual.is_some_and(|v| p.matches(v)),
140            };
141            actual.is_some_and(|v| {
142                if case_insensitive {
143                    v.eq_ignore_ascii_case(expected_str)
144                } else {
145                    v == expected_str
146                }
147            })
148        }
149
150        FilterOp::NotEquals => {
151            let expected_str = match expected {
152                FilterValue::String(s) => s.as_str(),
153                FilterValue::Number(n) => return !compare_numeric(actual, FilterOp::Equals, *n),
154                FilterValue::Bool(b) => return !compare_bool(actual, *b),
155                FilterValue::Pattern(p) => return actual.is_none_or(|v| !p.matches(v)),
156            };
157            actual.is_none_or(|v| {
158                if case_insensitive {
159                    !v.eq_ignore_ascii_case(expected_str)
160                } else {
161                    v != expected_str
162                }
163            })
164        }
165
166        FilterOp::WordMatch => {
167            let expected_str = match expected {
168                FilterValue::String(s) => s.as_str(),
169                _ => return false,
170            };
171            actual.is_some_and(|v| {
172                v.split_whitespace().any(|word| {
173                    if case_insensitive {
174                        word.eq_ignore_ascii_case(expected_str)
175                    } else {
176                        word == expected_str
177                    }
178                })
179            })
180        }
181
182        FilterOp::StartsWith => {
183            let expected_str = match expected {
184                FilterValue::String(s) => s.as_str(),
185                _ => return false,
186            };
187            actual.is_some_and(|v| {
188                if case_insensitive {
189                    v.to_lowercase().starts_with(&expected_str.to_lowercase())
190                } else {
191                    v.starts_with(expected_str)
192                }
193            })
194        }
195
196        FilterOp::EndsWith => {
197            let expected_str = match expected {
198                FilterValue::String(s) => s.as_str(),
199                _ => return false,
200            };
201            actual.is_some_and(|v| {
202                if case_insensitive {
203                    v.to_lowercase().ends_with(&expected_str.to_lowercase())
204                } else {
205                    v.ends_with(expected_str)
206                }
207            })
208        }
209
210        FilterOp::Contains => {
211            let expected_str = match expected {
212                FilterValue::String(s) => s.as_str(),
213                _ => return false,
214            };
215            actual.is_some_and(|v| {
216                if case_insensitive {
217                    v.to_lowercase().contains(&expected_str.to_lowercase())
218                } else {
219                    v.contains(expected_str)
220                }
221            })
222        }
223
224        FilterOp::GreaterThan => {
225            let n = expected.as_number().unwrap_or(0.0);
226            compare_numeric(actual, FilterOp::GreaterThan, n)
227        }
228
229        FilterOp::LessThan => {
230            let n = expected.as_number().unwrap_or(0.0);
231            compare_numeric(actual, FilterOp::LessThan, n)
232        }
233
234        FilterOp::GreaterOrEqual => {
235            let n = expected.as_number().unwrap_or(0.0);
236            compare_numeric(actual, FilterOp::GreaterOrEqual, n)
237        }
238
239        FilterOp::LessOrEqual => {
240            let n = expected.as_number().unwrap_or(0.0);
241            compare_numeric(actual, FilterOp::LessOrEqual, n)
242        }
243    }
244}
245
246/// Compare a string value numerically.
247fn compare_numeric(actual: Option<&str>, op: FilterOp, expected: f64) -> bool {
248    let actual_num = actual.and_then(|v| v.parse::<f64>().ok());
249    match actual_num {
250        Some(n) => match op {
251            FilterOp::Equals => (n - expected).abs() < 0.001,
252            FilterOp::NotEquals => (n - expected).abs() >= 0.001,
253            FilterOp::GreaterThan => n > expected,
254            FilterOp::LessThan => n < expected,
255            FilterOp::GreaterOrEqual => n >= expected,
256            FilterOp::LessOrEqual => n <= expected,
257            _ => false,
258        },
259        None => false,
260    }
261}
262
263/// Compare a string value as boolean.
264fn compare_bool(actual: Option<&str>, expected: bool) -> bool {
265    let actual_bool =
266        actual.map(|v| v.eq_ignore_ascii_case("true") || v.eq_ignore_ascii_case("yes") || v == "1");
267    actual_bool == Some(expected)
268}
269
270/// Electrical type for pins (shared between systems).
271#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
272pub enum ElectricalType {
273    Input,
274    Output,
275    InputOutput,
276    Passive,
277    Power,
278    OpenCollector,
279    OpenEmitter,
280    HiZ,
281    Unknown,
282}
283
284impl ElectricalType {
285    /// Parse from string representation.
286    pub fn parse(s: &str) -> Self {
287        match s.to_lowercase().as_str() {
288            "input" | "in" => Self::Input,
289            "output" | "out" => Self::Output,
290            "io" | "inputoutput" | "input/output" | "bidirectional" | "bidir" => Self::InputOutput,
291            "passive" | "pass" => Self::Passive,
292            "power" | "pwr" => Self::Power,
293            "opencollector" | "open collector" | "oc" => Self::OpenCollector,
294            "openemitter" | "open emitter" | "oe" => Self::OpenEmitter,
295            "hiz" | "hi-z" | "high-z" | "tristate" | "tri-state" => Self::HiZ,
296            _ => Self::Unknown,
297        }
298    }
299
300    /// Convert from Altium's PinElectricalType.
301    pub fn from_pin_electrical(pe: PinElectricalType) -> Self {
302        match pe {
303            PinElectricalType::Input => Self::Input,
304            PinElectricalType::Output => Self::Output,
305            PinElectricalType::InputOutput => Self::InputOutput,
306            PinElectricalType::Passive => Self::Passive,
307            PinElectricalType::Power => Self::Power,
308            PinElectricalType::OpenCollector => Self::OpenCollector,
309            PinElectricalType::OpenEmitter => Self::OpenEmitter,
310            PinElectricalType::HiZ => Self::HiZ,
311        }
312    }
313
314    /// Get string representation.
315    pub fn as_str(&self) -> &'static str {
316        match self {
317            Self::Input => "Input",
318            Self::Output => "Output",
319            Self::InputOutput => "Bidirectional",
320            Self::Passive => "Passive",
321            Self::Power => "Power",
322            Self::OpenCollector => "OpenCollector",
323            Self::OpenEmitter => "OpenEmitter",
324            Self::HiZ => "HiZ",
325            Self::Unknown => "Unknown",
326        }
327    }
328}
329
330impl std::fmt::Display for ElectricalType {
331    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
332        write!(f, "{}", self.as_str())
333    }
334}
335
336/// Electrical filter pseudo-selector (shared between systems).
337///
338/// These represent the electrical state filters that can be applied to pins.
339#[derive(Debug, Clone, Copy, PartialEq, Eq)]
340pub enum ElectricalFilter {
341    /// Pin is connected to a net
342    Connected,
343    /// Pin is not connected (floating)
344    Unconnected,
345    /// Input pin
346    Input,
347    /// Output pin
348    Output,
349    /// Bidirectional pin
350    Bidirectional,
351    /// Power pin
352    Power,
353    /// Ground connection
354    Ground,
355    /// Passive pin
356    Passive,
357    /// Open collector pin
358    OpenCollector,
359    /// Open emitter pin
360    OpenEmitter,
361    /// High-impedance pin
362    HiZ,
363}
364
365impl ElectricalFilter {
366    /// Parse from string.
367    pub fn try_parse(s: &str) -> Option<Self> {
368        match s.to_lowercase().as_str() {
369            "connected" | "conn" => Some(Self::Connected),
370            "unconnected" | "unconn" | "floating" | "nc" => Some(Self::Unconnected),
371            "input" | "in" => Some(Self::Input),
372            "output" | "out" => Some(Self::Output),
373            "bidirectional" | "bidir" | "inout" => Some(Self::Bidirectional),
374            "power" | "pwr" => Some(Self::Power),
375            "ground" | "gnd" => Some(Self::Ground),
376            "passive" | "pass" => Some(Self::Passive),
377            "opencollector" | "oc" => Some(Self::OpenCollector),
378            "openemitter" | "oe" => Some(Self::OpenEmitter),
379            "hiz" | "tristate" => Some(Self::HiZ),
380            _ => None,
381        }
382    }
383
384    /// Check if an electrical type matches this filter.
385    pub fn matches_type(&self, electrical_type: ElectricalType) -> bool {
386        match self {
387            Self::Input => electrical_type == ElectricalType::Input,
388            Self::Output => electrical_type == ElectricalType::Output,
389            Self::Bidirectional => electrical_type == ElectricalType::InputOutput,
390            Self::Power => electrical_type == ElectricalType::Power,
391            Self::Passive => electrical_type == ElectricalType::Passive,
392            Self::OpenCollector => electrical_type == ElectricalType::OpenCollector,
393            Self::OpenEmitter => electrical_type == ElectricalType::OpenEmitter,
394            Self::HiZ => electrical_type == ElectricalType::HiZ,
395            // Connected/Unconnected/Ground are not electrical types
396            _ => false,
397        }
398    }
399}
400
401/// Visibility filter (shared between systems).
402#[derive(Debug, Clone, Copy, PartialEq, Eq)]
403pub enum VisibilityFilter {
404    /// Element is visible (not hidden)
405    Visible,
406    /// Element is hidden
407    Hidden,
408}
409
410impl VisibilityFilter {
411    /// Parse from string.
412    pub fn try_parse(s: &str) -> Option<Self> {
413        match s.to_lowercase().as_str() {
414            "visible" => Some(Self::Visible),
415            "hidden" => Some(Self::Hidden),
416            _ => None,
417        }
418    }
419
420    /// Check if a hidden state matches this filter.
421    pub fn matches(&self, is_hidden: bool) -> bool {
422        match self {
423            Self::Visible => !is_hidden,
424            Self::Hidden => is_hidden,
425        }
426    }
427}
428
429#[cfg(test)]
430mod tests {
431    use super::*;
432
433    #[test]
434    fn test_filter_op_try_parse() {
435        assert_eq!(FilterOp::try_parse("="), Some(FilterOp::Equals));
436        assert_eq!(FilterOp::try_parse("!="), Some(FilterOp::NotEquals));
437        assert_eq!(FilterOp::try_parse("^="), Some(FilterOp::StartsWith));
438        assert_eq!(FilterOp::try_parse("$="), Some(FilterOp::EndsWith));
439        assert_eq!(FilterOp::try_parse("*="), Some(FilterOp::Contains));
440        assert_eq!(FilterOp::try_parse(">"), Some(FilterOp::GreaterThan));
441        assert_eq!(FilterOp::try_parse("<"), Some(FilterOp::LessThan));
442        assert_eq!(FilterOp::try_parse(">="), Some(FilterOp::GreaterOrEqual));
443        assert_eq!(FilterOp::try_parse("<="), Some(FilterOp::LessOrEqual));
444        assert_eq!(FilterOp::try_parse("??"), None);
445    }
446
447    #[test]
448    fn test_compare_filter_equals() {
449        let expected = FilterValue::string("hello");
450        assert!(compare_filter(
451            Some("hello"),
452            FilterOp::Equals,
453            &expected,
454            false
455        ));
456        assert!(compare_filter(
457            Some("HELLO"),
458            FilterOp::Equals,
459            &expected,
460            true
461        ));
462        assert!(!compare_filter(
463            Some("HELLO"),
464            FilterOp::Equals,
465            &expected,
466            false
467        ));
468        assert!(!compare_filter(None, FilterOp::Equals, &expected, false));
469    }
470
471    #[test]
472    fn test_compare_filter_exists() {
473        let expected = FilterValue::string("");
474        assert!(compare_filter(
475            Some("anything"),
476            FilterOp::Exists,
477            &expected,
478            false
479        ));
480        assert!(compare_filter(Some(""), FilterOp::Exists, &expected, false));
481        assert!(!compare_filter(None, FilterOp::Exists, &expected, false));
482    }
483
484    #[test]
485    fn test_compare_filter_contains() {
486        let expected = FilterValue::string("test");
487        assert!(compare_filter(
488            Some("this is a test"),
489            FilterOp::Contains,
490            &expected,
491            false
492        ));
493        assert!(compare_filter(
494            Some("TEST value"),
495            FilterOp::Contains,
496            &expected,
497            true
498        ));
499        assert!(!compare_filter(
500            Some("no match"),
501            FilterOp::Contains,
502            &expected,
503            false
504        ));
505    }
506
507    #[test]
508    fn test_compare_filter_numeric() {
509        let expected = FilterValue::number(10.0);
510        assert!(compare_filter(
511            Some("15"),
512            FilterOp::GreaterThan,
513            &expected,
514            false
515        ));
516        assert!(compare_filter(
517            Some("5"),
518            FilterOp::LessThan,
519            &expected,
520            false
521        ));
522        assert!(compare_filter(
523            Some("10"),
524            FilterOp::GreaterOrEqual,
525            &expected,
526            false
527        ));
528        assert!(!compare_filter(
529            Some("5"),
530            FilterOp::GreaterThan,
531            &expected,
532            false
533        ));
534    }
535
536    #[test]
537    fn test_electrical_type_parsing() {
538        assert_eq!(ElectricalType::parse("Input"), ElectricalType::Input);
539        assert_eq!(ElectricalType::parse("output"), ElectricalType::Output);
540        assert_eq!(ElectricalType::parse("BIDIR"), ElectricalType::InputOutput);
541        assert_eq!(ElectricalType::parse("power"), ElectricalType::Power);
542    }
543
544    #[test]
545    fn test_electrical_filter_matches() {
546        assert!(ElectricalFilter::Input.matches_type(ElectricalType::Input));
547        assert!(!ElectricalFilter::Input.matches_type(ElectricalType::Output));
548        assert!(ElectricalFilter::Bidirectional.matches_type(ElectricalType::InputOutput));
549    }
550
551    #[test]
552    fn test_visibility_filter() {
553        assert!(VisibilityFilter::Visible.matches(false));
554        assert!(!VisibilityFilter::Visible.matches(true));
555        assert!(VisibilityFilter::Hidden.matches(true));
556        assert!(!VisibilityFilter::Hidden.matches(false));
557    }
558}