1use super::pattern::Pattern;
7use crate::records::sch::PinElectricalType;
8
9#[derive(Debug, Clone, Copy, PartialEq, Eq)]
14pub enum FilterOp {
15 Exists,
17 Equals,
19 NotEquals,
21 WordMatch,
23 StartsWith,
25 EndsWith,
27 Contains,
29 GreaterThan,
31 LessThan,
33 GreaterOrEqual,
35 LessOrEqual,
37}
38
39impl FilterOp {
40 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 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#[derive(Debug, Clone)]
77pub enum FilterValue {
78 String(String),
80 Number(f64),
82 Bool(bool),
84 Pattern(Pattern),
86}
87
88impl FilterValue {
89 pub fn string(s: impl Into<String>) -> Self {
91 Self::String(s.into())
92 }
93
94 pub fn number(n: f64) -> Self {
96 Self::Number(n)
97 }
98
99 pub fn bool(b: bool) -> Self {
101 Self::Bool(b)
102 }
103
104 pub fn as_str(&self) -> Option<&str> {
106 match self {
107 Self::String(s) => Some(s),
108 _ => None,
109 }
110 }
111
112 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
122pub 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
246fn 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
263fn 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#[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 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 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 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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
340pub enum ElectricalFilter {
341 Connected,
343 Unconnected,
345 Input,
347 Output,
349 Bidirectional,
351 Power,
353 Ground,
355 Passive,
357 OpenCollector,
359 OpenEmitter,
361 HiZ,
363}
364
365impl ElectricalFilter {
366 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 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 _ => false,
397 }
398 }
399}
400
401#[derive(Debug, Clone, Copy, PartialEq, Eq)]
403pub enum VisibilityFilter {
404 Visible,
406 Hidden,
408}
409
410impl VisibilityFilter {
411 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 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}