mathypad_core/expression/
parser.rs1use super::chumsky_parser::parse_expression_chumsky;
4use super::tokens::Token;
5use crate::units::parse_unit;
6
7pub fn parse_line_reference(text: &str) -> Option<usize> {
9 let text_lower = text.to_lowercase();
10 if let Some(number_part) = text_lower.strip_prefix("line") {
11 if let Ok(line_num) = number_part.parse::<usize>() {
12 if line_num > 0 {
13 return Some(line_num - 1); }
15 }
16 }
17 None
18}
19
20pub fn extract_line_references(text: &str) -> Vec<(usize, usize, usize)> {
23 let mut references = Vec::new();
24 let text_lower = text.to_lowercase();
25 let mut search_start = 0;
26
27 while let Some(line_pos) = text_lower[search_start..].find("line") {
28 let absolute_pos = search_start + line_pos;
29
30 let is_word_start = absolute_pos == 0
32 || !text_lower
33 .chars()
34 .nth(absolute_pos - 1)
35 .unwrap_or(' ')
36 .is_ascii_alphanumeric();
37
38 if is_word_start {
39 let remaining = &text_lower[absolute_pos + 4..]; let mut num_end = 0;
43 for ch in remaining.chars() {
44 if ch.is_ascii_digit() {
45 num_end += 1;
46 } else {
47 break;
48 }
49 }
50
51 if num_end > 0 {
52 let is_word_end = absolute_pos + 4 + num_end >= text_lower.len()
54 || !text_lower
55 .chars()
56 .nth(absolute_pos + 4 + num_end)
57 .unwrap_or(' ')
58 .is_ascii_alphanumeric();
59
60 if is_word_end {
61 let original_remaining = &text[absolute_pos + 4..];
63 if let Ok(line_num) = original_remaining[..num_end].parse::<usize>() {
64 if line_num > 0 {
65 let start_pos = absolute_pos;
66 let end_pos = absolute_pos + 4 + num_end; references.push((start_pos, end_pos, line_num - 1)); }
69 }
70 }
71 }
72 }
73
74 search_start = absolute_pos + 4; }
76
77 references
78}
79
80pub fn update_line_references_in_text(text: &str, threshold: usize, offset: i32) -> String {
84 let references = extract_line_references(text);
85
86 if references.is_empty() {
87 return text.to_string();
88 }
89
90 let mut result = text.to_string();
91
92 for (start_pos, end_pos, line_num) in references.into_iter().rev() {
94 if offset > 0 {
95 if line_num >= threshold {
98 let new_ref = format!("line{}", line_num + 2); result.replace_range(start_pos..end_pos, &new_ref);
100 }
101 } else {
102 if line_num == threshold {
104 result.replace_range(start_pos..end_pos, "INVALID_REF");
106 } else if line_num > threshold {
107 let new_ref = format!("line{}", line_num); result.replace_range(start_pos..end_pos, &new_ref);
110 }
111 }
113 }
114
115 result
116}
117
118pub fn tokenize_with_units(expr: &str) -> Option<Vec<Token>> {
120 match parse_expression_chumsky(expr) {
122 Ok(tokens) if tokens.is_empty() => None, Ok(tokens) => Some(tokens), Err(_) => None, }
126}
127
128pub fn is_valid_mathematical_expression(tokens: &[Token]) -> bool {
130 if tokens.is_empty() {
131 return false;
132 }
133
134 let mut has_number_or_value = false;
136 let mut consecutive_operators = 0;
137 let mut consecutive_values = 0;
138
139 for (i, token) in tokens.iter().enumerate() {
140 match token {
141 Token::Number(_)
142 | Token::NumberWithUnit(_, _)
143 | Token::LineReference(_)
144 | Token::Variable(_) => {
145 has_number_or_value = true;
146 consecutive_values += 1;
147 consecutive_operators = 0;
148
149 if consecutive_values > 1 {
151 if i >= 2
153 && matches!(tokens[i - 1], Token::Assign)
154 && matches!(tokens[i - 2], Token::Variable(_))
155 {
156 consecutive_values = 1; } else {
158 return false;
159 }
160 }
161 }
162 Token::Plus | Token::Minus | Token::Multiply | Token::Divide | Token::Power => {
163 consecutive_operators += 1;
164 consecutive_values = 0;
165
166 if consecutive_operators > 1 && !matches!(token, Token::Minus) {
168 return false;
169 }
170 }
171 Token::LeftParen | Token::RightParen => {
172 consecutive_operators = 0;
173 consecutive_values = 0;
174 }
175 Token::To | Token::In | Token::Of => {
176 consecutive_operators = 0;
178 consecutive_values = 0;
179 }
180 Token::Assign => {
181 if i == 0 || !matches!(tokens[i - 1], Token::Variable(_)) {
183 return false;
184 }
185 consecutive_operators = 0;
186 consecutive_values = 0;
187 }
188 Token::Function(_) => {
189 consecutive_operators = 0;
191 consecutive_values = 0;
192 }
193 }
194 }
195
196 has_number_or_value
198}
199
200pub fn is_valid_math_expression(expr: &str) -> bool {
202 let expr = expr.trim();
203 if expr.is_empty() {
204 return false;
205 }
206
207 let mut has_number = false;
208 let mut paren_count = 0;
210 let mut prev_was_operator = true; let chars: Vec<char> = expr.chars().collect();
213 let mut i = 0;
214
215 while i < chars.len() {
216 let ch = chars[i];
217 match ch {
218 ' ' => {
219 i += 1;
220 continue;
221 }
222 '0'..='9' => {
223 has_number = true;
224 prev_was_operator = false;
225 while i < chars.len()
227 && (chars[i].is_ascii_digit() || chars[i] == '.' || chars[i] == ',')
228 {
229 i += 1;
230 }
231
232 while i < chars.len() && chars[i] == ' ' {
234 i += 1;
235 }
236
237 if i < chars.len() && chars[i].is_ascii_alphabetic() {
239 let unit_start = i;
240 while i < chars.len() && (chars[i].is_ascii_alphabetic() || chars[i] == '/') {
241 i += 1;
242 }
243
244 let unit_str: String = chars[unit_start..i].iter().collect();
245 if parse_unit(&unit_str).is_none()
246 && unit_str.to_lowercase() != "to"
247 && unit_str.to_lowercase() != "in"
248 && parse_line_reference(&unit_str).is_none()
249 {
250 i = unit_start;
252 }
253 }
254 continue;
255 }
256 '.' => {
257 if prev_was_operator {
258 return false; }
260 i += 1;
261 }
262 '+' | '-' | '*' | '/' => {
263 if prev_was_operator && ch != '-' {
264 return false; }
266 prev_was_operator = true;
267 i += 1;
268 }
269 '(' => {
270 paren_count += 1;
271 prev_was_operator = true;
272 i += 1;
273 }
274 ')' => {
275 paren_count -= 1;
276 if paren_count < 0 {
277 return false;
278 }
279 prev_was_operator = false;
280 i += 1;
281 }
282 _ => {
283 if ch.is_ascii_alphabetic() {
284 let unit_start = i;
285 while i < chars.len()
287 && (chars[i].is_ascii_alphabetic()
288 || chars[i].is_ascii_digit()
289 || chars[i] == '/')
290 {
291 i += 1;
292 }
293
294 let word: String = chars[unit_start..i].iter().collect();
295 if word.to_lowercase() == "to" || word.to_lowercase() == "in" {
296 prev_was_operator = true;
297 } else if parse_line_reference(&word).is_some() {
298 has_number = true;
300 prev_was_operator = false;
301 } else if parse_unit(&word).is_some() {
302 prev_was_operator = false;
304 } else {
305 break;
308 }
309 } else {
310 break;
312 }
313 }
314 }
315 }
316
317 paren_count == 0 && has_number && !prev_was_operator
319}
320
321#[cfg(test)]
322mod parser_tests {
323 use super::*;
324
325 #[test]
326 fn test_parse_line_reference() {
327 assert_eq!(parse_line_reference("line1"), Some(0));
329 assert_eq!(parse_line_reference("line2"), Some(1));
330 assert_eq!(parse_line_reference("line10"), Some(9));
331 assert_eq!(parse_line_reference("line999"), Some(998));
332
333 assert_eq!(parse_line_reference("LINE1"), Some(0));
335 assert_eq!(parse_line_reference("Line2"), Some(1));
336 assert_eq!(parse_line_reference("LiNe3"), Some(2));
337
338 assert_eq!(parse_line_reference("line0"), None); assert_eq!(parse_line_reference("line"), None); assert_eq!(parse_line_reference("line-1"), None); assert_eq!(parse_line_reference("linea"), None); assert_eq!(parse_line_reference("notline1"), None); assert_eq!(parse_line_reference(""), None); assert_eq!(parse_line_reference("1line"), None); }
347
348 #[test]
349 fn test_tokenize_with_units_basic() {
350 let tokens = tokenize_with_units("42").unwrap();
352 assert_eq!(tokens.len(), 1);
353 assert!(matches!(tokens[0], Token::Number(42.0)));
354
355 let tokens = tokenize_with_units("5 GiB").unwrap();
357 assert_eq!(tokens.len(), 1);
358 assert!(matches!(tokens[0], Token::NumberWithUnit(5.0, _)));
359
360 let tokens = tokenize_with_units("2 + 3").unwrap();
362 assert_eq!(tokens.len(), 3);
363 assert!(matches!(tokens[0], Token::Number(2.0)));
364 assert!(matches!(tokens[1], Token::Plus));
365 assert!(matches!(tokens[2], Token::Number(3.0)));
366 }
367
368 #[test]
369 fn test_tokenize_with_units_invalid() {
370 let result = tokenize_with_units("invalid text");
372 assert!(result.is_some()); assert!(tokenize_with_units("1 + 2)").is_none());
376 assert!(tokenize_with_units("1 invalidunit").is_some()); let result = tokenize_with_units("");
381 assert!(result.is_none());
382 }
383
384 #[test]
385 fn test_is_valid_math_expression() {
386 assert!(is_valid_math_expression("42"));
388 assert!(is_valid_math_expression("2 + 3"));
389 assert!(is_valid_math_expression("(1 + 2) * 3"));
390 assert!(is_valid_math_expression("5 GiB + 10 MiB"));
391 assert!(is_valid_math_expression("line1 * 2"));
392 assert!(is_valid_math_expression("1 TiB to GiB"));
393 assert!(is_valid_math_expression("24 MiB * 32 in KiB"));
394
395 assert!(!is_valid_math_expression(""));
397 assert!(!is_valid_math_expression("invalid text"));
398 assert!(!is_valid_math_expression("1 +"));
399 assert!(!is_valid_math_expression("+ 2"));
400 assert!(!is_valid_math_expression("1 + + 2"));
401 assert!(!is_valid_math_expression("(1 + 2"));
402 assert!(!is_valid_math_expression("1 + 2)"));
403
404 assert!(is_valid_math_expression("0"));
410 assert!(is_valid_math_expression("-5")); assert!(is_valid_math_expression("1.5"));
412 assert!(is_valid_math_expression("1,000"));
413 assert!(is_valid_math_expression("1,000,000.50"));
414 }
415
416 #[test]
417 fn test_is_valid_math_expression_units() {
418 assert!(is_valid_math_expression("5GiB")); assert!(is_valid_math_expression("5 GiB")); assert!(is_valid_math_expression("10.5 MB/s")); assert!(is_valid_math_expression("100 QPS")); assert!(is_valid_math_expression("1 hour")); assert!(is_valid_math_expression("8 bit")); assert!(is_valid_math_expression("1 GiB to MiB"));
428 assert!(is_valid_math_expression("24 MiB * 32 in KiB"));
429 assert!(is_valid_math_expression("100 QPS to req/min"));
430
431 assert!(is_valid_math_expression("1 gib TO mib"));
433 assert!(is_valid_math_expression("1 GIB to MIB"));
434 }
435
436 #[test]
437 fn test_is_valid_math_expression_operators() {
438 assert!(is_valid_math_expression("1 + 2"));
440 assert!(is_valid_math_expression("5 - 3"));
441 assert!(is_valid_math_expression("4 * 6"));
442 assert!(is_valid_math_expression("8 / 2"));
443
444 assert!(is_valid_math_expression("1 + 2 - 3"));
446 assert!(is_valid_math_expression("2 * 3 + 4"));
447 assert!(is_valid_math_expression("10 / 2 - 1"));
448
449 assert!(is_valid_math_expression("(1 + 2) * 3"));
451 assert!(is_valid_math_expression("1 + (2 * 3)"));
452 assert!(is_valid_math_expression("((1 + 2) * 3) - 4"));
453
454 assert!(!is_valid_math_expression("1 + * 2"));
456 assert!(!is_valid_math_expression("* 1 + 2"));
457 assert!(!is_valid_math_expression("1 + 2 *"));
458 }
459
460 #[test]
461 fn test_is_valid_math_expression_line_references() {
462 assert!(is_valid_math_expression("line1"));
464 assert!(is_valid_math_expression("line10"));
465 assert!(is_valid_math_expression("line1 + line2"));
466 assert!(is_valid_math_expression("line1 * 2"));
467 assert!(is_valid_math_expression("(line1 + line2) / 2"));
468
469 assert!(is_valid_math_expression("line1 + 5 GiB"));
471 assert!(is_valid_math_expression("line1 to MiB"));
472 assert!(is_valid_math_expression("line1 + line2 in KiB"));
473
474 assert!(is_valid_math_expression("LINE1"));
476 assert!(is_valid_math_expression("Line2"));
477 assert!(is_valid_math_expression("LiNe3 + LiNe4"));
478 }
479
480 #[test]
481 fn test_whitespace_handling() {
482 assert!(is_valid_math_expression(" 1 + 2 "));
484 assert!(is_valid_math_expression("1 + 2"));
485 assert!(is_valid_math_expression("1\t+\t2"));
486 assert!(is_valid_math_expression("1+2")); assert!(is_valid_math_expression("5 GiB"));
490 assert!(is_valid_math_expression("5GiB"));
491
492 assert!(is_valid_math_expression("1 GiB to MiB"));
494 assert!(is_valid_math_expression("1 GiB to MiB"));
495 }
496
497 #[test]
498 fn test_extract_line_references() {
499 assert_eq!(extract_line_references("line1 + 5"), vec![(0, 5, 0)]);
501 assert_eq!(extract_line_references("10 + line2"), vec![(5, 10, 1)]);
502 assert_eq!(
503 extract_line_references("line1 + line2 * line3"),
504 vec![(0, 5, 0), (8, 13, 1), (16, 21, 2)]
505 );
506
507 assert_eq!(
509 extract_line_references("Line1 + Line2"),
510 vec![(0, 5, 0), (8, 13, 1)]
511 );
512 assert_eq!(
513 extract_line_references("LINE1 + line2"),
514 vec![(0, 5, 0), (8, 13, 1)]
515 );
516
517 assert_eq!(
519 extract_line_references("(line1 + line2) * 2 to GiB"),
520 vec![(1, 6, 0), (9, 14, 1)]
521 );
522
523 assert_eq!(
525 extract_line_references("line10 + line123"),
526 vec![(0, 6, 9), (9, 16, 122)]
527 );
528
529 assert_eq!(extract_line_references("5 + 3 * 2"), vec![]);
531 assert_eq!(extract_line_references("hello world"), vec![]);
532
533 assert_eq!(extract_line_references("line0"), vec![]); assert_eq!(extract_line_references("line"), vec![]); assert_eq!(extract_line_references("myline1"), vec![]); assert_eq!(
540 extract_line_references("result: line1 + 2"),
541 vec![(8, 13, 0)]
542 );
543 }
544
545 #[test]
546 fn test_update_line_references_insertion() {
547 assert_eq!(
549 update_line_references_in_text("line1 + line2", 0, 1),
550 "line2 + line3"
551 );
552 assert_eq!(
553 update_line_references_in_text("line3 + 5", 0, 1),
554 "line4 + 5"
555 );
556
557 assert_eq!(
559 update_line_references_in_text("line1 + line3", 2, 1),
560 "line1 + line4"
561 );
562 assert_eq!(
563 update_line_references_in_text("line1 + line2 + line3", 2, 1),
564 "line1 + line2 + line4"
565 );
566
567 assert_eq!(
569 update_line_references_in_text("line1 + line2", 5, 1),
570 "line1 + line2"
571 );
572
573 assert_eq!(update_line_references_in_text("5 + 3", 1, 1), "5 + 3");
575
576 assert_eq!(
578 update_line_references_in_text("(line2 + line4) * 2 to GiB", 3, 1),
579 "(line2 + line5) * 2 to GiB"
580 );
581
582 assert_eq!(
586 update_line_references_in_text("line1 + 1", 1, 1),
587 "line1 + 1"
588 );
589 }
590
591 #[test]
592 fn test_update_line_references_deletion() {
593 assert_eq!(
595 update_line_references_in_text("line1 + line2 + line3", 0, -1),
596 "INVALID_REF + line1 + line2"
597 );
598
599 assert_eq!(
601 update_line_references_in_text("line1 + line2 + line3", 1, -1),
602 "line1 + INVALID_REF + line2"
603 );
604
605 assert_eq!(
607 update_line_references_in_text("line1 + line2 + line3", 2, -1),
608 "line1 + line2 + INVALID_REF"
609 );
610
611 assert_eq!(
613 update_line_references_in_text("line1 + line5", 3, -1),
614 "line1 + line4"
615 );
616
617 assert_eq!(update_line_references_in_text("5 + 3", 1, -1), "5 + 3");
619
620 assert_eq!(
622 update_line_references_in_text("line1 + line3 + line5", 2, -1),
623 "line1 + INVALID_REF + line4"
624 );
625
626 assert_eq!(
628 update_line_references_in_text("line2 + 1", 0, -1),
629 "line1 + 1"
630 );
631 }
632
633 #[test]
634 fn test_update_line_references_edge_cases() {
635 assert_eq!(
637 update_line_references_in_text("line2 + line2 * line2", 1, -1),
638 "INVALID_REF + INVALID_REF * INVALID_REF"
639 );
640
641 assert_eq!(
643 update_line_references_in_text("line100 + line200", 150, 1),
644 "line100 + line201"
645 );
646
647 assert_eq!(
649 update_line_references_in_text("Result: Line1 + LINE2", 1, -1),
650 "Result: Line1 + INVALID_REF"
651 );
652
653 assert_eq!(
655 update_line_references_in_text("Memory usage: line3 * 1024 bytes", 2, 1),
656 "Memory usage: line4 * 1024 bytes"
657 );
658 }
659}