1use endbasic_core::{
20 AnyValueSyntax, ArgSep, ArgSepSyntax, CallError, CallResult, Callable, CallableMetadata,
21 CallableMetadataBuilder, ExprType, RequiredValueSyntax, Scope, SingularArgSyntax, VarArgTag,
22};
23use std::borrow::Cow;
24use std::cmp::min;
25use std::convert::TryFrom;
26use std::rc::Rc;
27
28use crate::MachineBuilder;
29
30const CATEGORY: &str = "String and character functions";
32
33pub fn format_boolean(b: bool) -> &'static str {
35 if b { "TRUE" } else { "FALSE" }
36}
37
38pub fn parse_boolean(s: &str) -> Result<bool, String> {
40 let raw = s.to_uppercase();
41 if raw == "TRUE" || raw == "YES" || raw == "Y" {
42 Ok(true)
43 } else if raw == "FALSE" || raw == "NO" || raw == "N" {
44 Ok(false)
45 } else {
46 Err(format!("Invalid boolean literal {}", s))
47 }
48}
49
50pub fn format_double(d: f64) -> String {
52 if !d.is_nan() && d.is_sign_negative() { d.to_string() } else { format!(" {}", d) }
53}
54
55pub fn parse_double(s: &str) -> Result<f64, String> {
57 match s.parse::<f64>() {
58 Ok(d) => Ok(d),
59 Err(_) => Err(format!("Invalid double-precision floating point literal {}", s)),
60 }
61}
62
63pub fn format_integer(i: i32) -> String {
65 if i.is_negative() { i.to_string() } else { format!(" {}", i) }
66}
67
68pub fn parse_integer(s: &str) -> Result<i32, String> {
70 match s.parse::<i32>() {
71 Ok(d) => Ok(d),
72 Err(_) => Err(format!("Invalid integer literal {}", s)),
73 }
74}
75
76pub struct AscFunction {
78 metadata: Rc<CallableMetadata>,
79}
80
81impl AscFunction {
82 pub fn new() -> Rc<Self> {
84 Rc::from(Self {
85 metadata: CallableMetadataBuilder::new("ASC")
86 .with_return_type(ExprType::Integer)
87 .with_syntax(&[(
88 &[SingularArgSyntax::RequiredValue(
89 RequiredValueSyntax { name: Cow::Borrowed("char"), vtype: ExprType::Text },
90 ArgSepSyntax::End,
91 )],
92 None,
93 )])
94 .with_category(CATEGORY)
95 .with_description(
96 "Returns the UTF character code of the input character.
97The input char$ argument is a string that must be 1-character long.
98This is called ASC for historical reasons but supports more than just ASCII characters in this \
99implementation of BASIC.
100See CHR$() for the inverse of this function.",
101 )
102 .build(),
103 })
104 }
105}
106
107impl Callable for AscFunction {
108 fn metadata(&self) -> Rc<CallableMetadata> {
109 self.metadata.clone()
110 }
111
112 fn exec(&self, scope: Scope<'_>) -> CallResult<()> {
113 debug_assert_eq!(1, scope.nargs());
114 let s = scope.get_string(0);
115
116 let mut chars = s.chars();
117 let ch = match chars.next() {
118 Some(ch) => ch,
119 None => {
120 return Err(CallError::Syntax(
121 scope.get_pos(0),
122 format!("Input string \"{}\" must be 1-character long", s),
123 ));
124 }
125 };
126 if chars.next().is_some() {
127 return Err(CallError::Syntax(
128 scope.get_pos(0),
129 format!("Input string \"{}\" must be 1-character long", s),
130 ));
131 }
132 let ch = if cfg!(debug_assertions) {
133 i32::try_from(ch as u32).expect("Unicode code points end at U+10FFFF")
134 } else {
135 ch as i32
136 };
137
138 scope.return_integer(ch)
139 }
140}
141
142pub struct ChrFunction {
144 metadata: Rc<CallableMetadata>,
145}
146
147impl ChrFunction {
148 pub fn new() -> Rc<Self> {
150 Rc::from(Self {
151 metadata: CallableMetadataBuilder::new("CHR")
152 .with_return_type(ExprType::Text)
153 .with_syntax(&[(
154 &[SingularArgSyntax::RequiredValue(
155 RequiredValueSyntax {
156 name: Cow::Borrowed("code"),
157 vtype: ExprType::Integer,
158 },
159 ArgSepSyntax::End,
160 )],
161 None,
162 )])
163 .with_category(CATEGORY)
164 .with_description(
165 "Returns the UTF character that corresponds to the given code.
166See ASC%() for the inverse of this function.",
167 )
168 .build(),
169 })
170 }
171}
172
173impl Callable for ChrFunction {
174 fn metadata(&self) -> Rc<CallableMetadata> {
175 self.metadata.clone()
176 }
177
178 fn exec(&self, scope: Scope<'_>) -> CallResult<()> {
179 debug_assert_eq!(1, scope.nargs());
180 let i = scope.get_integer(0);
181
182 if i < 0 {
183 return Err(CallError::Syntax(
184 scope.get_pos(0),
185 format!("Character code {} must be positive", i),
186 ));
187 }
188 let code = i as u32;
189
190 match char::from_u32(code) {
191 Some(ch) => scope.return_string(format!("{}", ch)),
192 None => {
193 Err(CallError::Syntax(scope.get_pos(0), format!("Invalid character code {}", code)))
194 }
195 }
196 }
197}
198
199pub struct LeftFunction {
201 metadata: Rc<CallableMetadata>,
202}
203
204impl LeftFunction {
205 pub fn new() -> Rc<Self> {
207 Rc::from(Self {
208 metadata: CallableMetadataBuilder::new("LEFT")
209 .with_return_type(ExprType::Text)
210 .with_syntax(&[(
211 &[
212 SingularArgSyntax::RequiredValue(
213 RequiredValueSyntax {
214 name: Cow::Borrowed("expr"),
215 vtype: ExprType::Text,
216 },
217 ArgSepSyntax::Exactly(ArgSep::Long),
218 ),
219 SingularArgSyntax::RequiredValue(
220 RequiredValueSyntax {
221 name: Cow::Borrowed("n"),
222 vtype: ExprType::Integer,
223 },
224 ArgSepSyntax::End,
225 ),
226 ],
227 None,
228 )])
229 .with_category(CATEGORY)
230 .with_description(
231 "Returns a given number of characters from the left side of a string.
232If n% is 0, returns an empty string.
233If n% is greater than or equal to the number of characters in expr$, returns expr$.",
234 )
235 .build(),
236 })
237 }
238}
239
240impl Callable for LeftFunction {
241 fn metadata(&self) -> Rc<CallableMetadata> {
242 self.metadata.clone()
243 }
244
245 fn exec(&self, scope: Scope<'_>) -> CallResult<()> {
246 debug_assert_eq!(2, scope.nargs());
247 let s = scope.get_string(0).to_owned();
248 let n = scope.get_integer(1);
249
250 if n < 0 {
251 Err(CallError::Syntax(scope.get_pos(1), "n% cannot be negative".to_owned()))
252 } else {
253 let n = min(s.len(), n as usize);
254 scope.return_string(s[..n].to_owned())
255 }
256 }
257}
258
259pub struct LenFunction {
261 metadata: Rc<CallableMetadata>,
262}
263
264impl LenFunction {
265 pub fn new() -> Rc<Self> {
267 Rc::from(Self {
268 metadata: CallableMetadataBuilder::new("LEN")
269 .with_return_type(ExprType::Integer)
270 .with_syntax(&[(
271 &[SingularArgSyntax::RequiredValue(
272 RequiredValueSyntax { name: Cow::Borrowed("expr"), vtype: ExprType::Text },
273 ArgSepSyntax::End,
274 )],
275 None,
276 )])
277 .with_category(CATEGORY)
278 .with_description("Returns the length of the string in expr$.")
279 .build(),
280 })
281 }
282}
283
284impl Callable for LenFunction {
285 fn metadata(&self) -> Rc<CallableMetadata> {
286 self.metadata.clone()
287 }
288
289 fn exec(&self, scope: Scope<'_>) -> CallResult<()> {
290 debug_assert_eq!(1, scope.nargs());
291 let s = scope.get_string(0);
292
293 let len =
294 i32::try_from(s.len()).map_err(|_| CallError::Eval("String too long".to_owned()))?;
295 scope.return_integer(len)
296 }
297}
298
299pub struct LtrimFunction {
301 metadata: Rc<CallableMetadata>,
302}
303
304impl LtrimFunction {
305 pub fn new() -> Rc<Self> {
307 Rc::from(Self {
308 metadata: CallableMetadataBuilder::new("LTRIM")
309 .with_return_type(ExprType::Text)
310 .with_syntax(&[(
311 &[SingularArgSyntax::RequiredValue(
312 RequiredValueSyntax { name: Cow::Borrowed("expr"), vtype: ExprType::Text },
313 ArgSepSyntax::End,
314 )],
315 None,
316 )])
317 .with_category(CATEGORY)
318 .with_description("Returns a copy of a string with leading whitespace removed.")
319 .build(),
320 })
321 }
322}
323
324impl Callable for LtrimFunction {
325 fn metadata(&self) -> Rc<CallableMetadata> {
326 self.metadata.clone()
327 }
328
329 fn exec(&self, scope: Scope<'_>) -> CallResult<()> {
330 debug_assert_eq!(1, scope.nargs());
331 let s = scope.get_string(0).to_owned();
332
333 scope.return_string(s.trim_start().to_owned())
334 }
335}
336
337pub struct MidFunction {
339 metadata: Rc<CallableMetadata>,
340}
341
342impl MidFunction {
343 pub fn new() -> Rc<Self> {
345 Rc::from(Self {
346 metadata: CallableMetadataBuilder::new("MID")
347 .with_return_type(ExprType::Text)
348 .with_syntax(&[
349 (
350 &[
351 SingularArgSyntax::RequiredValue(
352 RequiredValueSyntax {
353 name: Cow::Borrowed("expr"),
354 vtype: ExprType::Text,
355 },
356 ArgSepSyntax::Exactly(ArgSep::Long),
357 ),
358 SingularArgSyntax::RequiredValue(
359 RequiredValueSyntax {
360 name: Cow::Borrowed("start"),
361 vtype: ExprType::Integer,
362 },
363 ArgSepSyntax::End,
364 ),
365 ],
366 None,
367 ),
368 (
369 &[
370 SingularArgSyntax::RequiredValue(
371 RequiredValueSyntax {
372 name: Cow::Borrowed("expr"),
373 vtype: ExprType::Text,
374 },
375 ArgSepSyntax::Exactly(ArgSep::Long),
376 ),
377 SingularArgSyntax::RequiredValue(
378 RequiredValueSyntax {
379 name: Cow::Borrowed("start"),
380 vtype: ExprType::Integer,
381 },
382 ArgSepSyntax::Exactly(ArgSep::Long),
383 ),
384 SingularArgSyntax::RequiredValue(
385 RequiredValueSyntax {
386 name: Cow::Borrowed("length"),
387 vtype: ExprType::Integer,
388 },
389 ArgSepSyntax::End,
390 ),
391 ],
392 None,
393 ),
394 ])
395 .with_category(CATEGORY)
396 .with_description(
397 "Returns a portion of a string.
398start% indicates the starting position of the substring to extract and it is 1-indexed.
399length% indicates the number of characters to extract and, if not specified, defaults to extracting
400until the end of the string.",
401 )
402 .build(),
403 })
404 }
405}
406
407impl Callable for MidFunction {
408 fn metadata(&self) -> Rc<CallableMetadata> {
409 self.metadata.clone()
410 }
411
412 fn exec(&self, scope: Scope<'_>) -> CallResult<()> {
413 debug_assert!((2..=3).contains(&scope.nargs()));
414 let s = scope.get_string(0).to_owned();
415 let start = scope.get_integer(1);
416 let lengtharg = if scope.nargs() == 3 { Some(scope.get_integer(2)) } else { None };
417
418 if start < 0 {
419 return Err(CallError::Syntax(
420 scope.get_pos(1),
421 "start% cannot be negative".to_owned(),
422 ));
423 }
424 let start = min(s.len(), start as usize);
425
426 let end = if let Some(length) = lengtharg {
427 if length < 0 {
428 return Err(CallError::Syntax(
429 scope.get_pos(2),
430 "length% cannot be negative".to_owned(),
431 ));
432 }
433 min(start + (length as usize), s.len())
434 } else {
435 s.len()
436 };
437
438 scope.return_string(s[start..end].to_owned())
439 }
440}
441
442pub struct RightFunction {
444 metadata: Rc<CallableMetadata>,
445}
446
447impl RightFunction {
448 pub fn new() -> Rc<Self> {
450 Rc::from(Self {
451 metadata: CallableMetadataBuilder::new("RIGHT")
452 .with_return_type(ExprType::Text)
453 .with_syntax(&[(
454 &[
455 SingularArgSyntax::RequiredValue(
456 RequiredValueSyntax {
457 name: Cow::Borrowed("expr"),
458 vtype: ExprType::Text,
459 },
460 ArgSepSyntax::Exactly(ArgSep::Long),
461 ),
462 SingularArgSyntax::RequiredValue(
463 RequiredValueSyntax {
464 name: Cow::Borrowed("n"),
465 vtype: ExprType::Integer,
466 },
467 ArgSepSyntax::End,
468 ),
469 ],
470 None,
471 )])
472 .with_category(CATEGORY)
473 .with_description(
474 "Returns a given number of characters from the right side of a string.
475If n% is 0, returns an empty string.
476If n% is greater than or equal to the number of characters in expr$, returns expr$.",
477 )
478 .build(),
479 })
480 }
481}
482
483impl Callable for RightFunction {
484 fn metadata(&self) -> Rc<CallableMetadata> {
485 self.metadata.clone()
486 }
487
488 fn exec(&self, scope: Scope<'_>) -> CallResult<()> {
489 debug_assert_eq!(2, scope.nargs());
490 let s = scope.get_string(0).to_owned();
491 let n = scope.get_integer(1);
492
493 if n < 0 {
494 Err(CallError::Syntax(scope.get_pos(1), "n% cannot be negative".to_owned()))
495 } else {
496 let n = min(s.len(), n as usize);
497 scope.return_string(s[s.len() - n..].to_owned())
498 }
499 }
500}
501
502pub struct RtrimFunction {
504 metadata: Rc<CallableMetadata>,
505}
506
507impl RtrimFunction {
508 pub fn new() -> Rc<Self> {
510 Rc::from(Self {
511 metadata: CallableMetadataBuilder::new("RTRIM")
512 .with_return_type(ExprType::Text)
513 .with_syntax(&[(
514 &[SingularArgSyntax::RequiredValue(
515 RequiredValueSyntax { name: Cow::Borrowed("expr"), vtype: ExprType::Text },
516 ArgSepSyntax::End,
517 )],
518 None,
519 )])
520 .with_category(CATEGORY)
521 .with_description("Returns a copy of a string with trailing whitespace removed.")
522 .build(),
523 })
524 }
525}
526
527impl Callable for RtrimFunction {
528 fn metadata(&self) -> Rc<CallableMetadata> {
529 self.metadata.clone()
530 }
531
532 fn exec(&self, scope: Scope<'_>) -> CallResult<()> {
533 debug_assert_eq!(1, scope.nargs());
534 let s = scope.get_string(0).to_owned();
535
536 scope.return_string(s.trim_end().to_owned())
537 }
538}
539
540pub struct StrFunction {
542 metadata: Rc<CallableMetadata>,
543}
544
545impl StrFunction {
546 pub fn new() -> Rc<Self> {
548 Rc::from(Self {
549 metadata: CallableMetadataBuilder::new("STR")
550 .with_return_type(ExprType::Text)
551 .with_syntax(&[(
552 &[SingularArgSyntax::AnyValue(
553 AnyValueSyntax { name: Cow::Borrowed("expr"), allow_missing: false },
554 ArgSepSyntax::End,
555 )],
556 None,
557 )])
558 .with_category(CATEGORY)
559 .with_description(
560 "Formats a scalar value as a string.
561If expr evaluates to a string, this returns the string unmodified.
562If expr evaluates to a boolean, this returns the strings FALSE or TRUE.
563If expr evaluates to a number, this returns a string with the textual representation of the \
564number. If the number does NOT have a negative sign, the resulting string has a single space \
565in front of it.
566To obtain a clean representation of expr as a string without any artificial whitespace characters \
567in it, do LTRIM$(STR$(expr)).",
568 )
569 .build(),
570 })
571 }
572}
573
574impl Callable for StrFunction {
575 fn metadata(&self) -> Rc<CallableMetadata> {
576 self.metadata.clone()
577 }
578
579 fn exec(&self, scope: Scope<'_>) -> CallResult<()> {
580 debug_assert_eq!(2, scope.nargs());
581 match scope.get_type(0) {
582 VarArgTag::Immediate(_, ExprType::Boolean) => {
583 let b = scope.get_boolean(1);
584 scope.return_string(format_boolean(b).to_owned())
585 }
586
587 VarArgTag::Immediate(_, ExprType::Double) => {
588 let d = scope.get_double(1);
589 scope.return_string(format_double(d))
590 }
591
592 VarArgTag::Immediate(_, ExprType::Integer) => {
593 let i = scope.get_integer(1);
594 scope.return_string(format_integer(i))
595 }
596
597 VarArgTag::Immediate(_, ExprType::Text) => {
598 let s = scope.get_string(1).to_owned();
599 scope.return_string(s)
600 }
601
602 VarArgTag::Missing(..) | VarArgTag::Pointer(..) => unreachable!(),
603 }
604 }
605}
606
607pub fn add_all(machine: &mut MachineBuilder) {
609 machine.add_callable(AscFunction::new());
610 machine.add_callable(ChrFunction::new());
611 machine.add_callable(LeftFunction::new());
612 machine.add_callable(LenFunction::new());
613 machine.add_callable(LtrimFunction::new());
614 machine.add_callable(MidFunction::new());
615 machine.add_callable(RightFunction::new());
616 machine.add_callable(RtrimFunction::new());
617 machine.add_callable(StrFunction::new());
618}
619
620#[cfg(test)]
621mod tests {
622 use super::*;
623 use crate::testutils::*;
624
625 #[test]
626 fn test_value_parse_boolean() {
627 for s in &["true", "TrUe", "TRUE", "yes", "Yes", "y", "Y"] {
628 assert!(parse_boolean(s).unwrap());
629 }
630
631 for s in &["false", "FaLsE", "FALSE", "no", "No", "n", "N"] {
632 assert!(!parse_boolean(s).unwrap());
633 }
634
635 for s in &["ye", "0", "1", " true"] {
636 assert_eq!(
637 format!("Invalid boolean literal {}", s),
638 format!("{}", parse_boolean(s).unwrap_err())
639 );
640 }
641 }
642
643 #[test]
644 fn test_value_parse_double() {
645 assert_eq!(10.0, parse_double("10").unwrap());
646 assert_eq!(0.0, parse_double("0").unwrap());
647 assert_eq!(-21.0, parse_double("-21").unwrap());
648 assert_eq!(1.0, parse_double("1.0").unwrap());
649 assert_eq!(0.01, parse_double(".01").unwrap());
650
651 assert_eq!(
652 123456789012345680000000000000.0,
653 parse_double("123456789012345678901234567890.1").unwrap()
654 );
655
656 assert_eq!(1.1234567890123457, parse_double("1.123456789012345678901234567890").unwrap());
657
658 assert_eq!(
659 "Invalid double-precision floating point literal ",
660 format!("{}", parse_double("").unwrap_err())
661 );
662 assert_eq!(
663 "Invalid double-precision floating point literal - 3.0",
664 format!("{}", parse_double("- 3.0").unwrap_err())
665 );
666 assert_eq!(
667 "Invalid double-precision floating point literal 34ab3.1",
668 format!("{}", parse_double("34ab3.1").unwrap_err())
669 );
670 }
671
672 #[test]
673 fn test_value_parse_integer() {
674 assert_eq!(10, parse_integer("10").unwrap());
675 assert_eq!(0, parse_integer("0").unwrap());
676 assert_eq!(-21, parse_integer("-21").unwrap());
677
678 assert_eq!("Invalid integer literal ", format!("{}", parse_integer("").unwrap_err()));
679 assert_eq!("Invalid integer literal - 3", format!("{}", parse_integer("- 3").unwrap_err()));
680 assert_eq!(
681 "Invalid integer literal 34ab3",
682 format!("{}", parse_integer("34ab3").unwrap_err())
683 );
684 }
685
686 #[test]
687 fn test_asc() {
688 check_expr_ok('a' as i32, r#"ASC("a")"#);
689 check_expr_ok(' ' as i32, r#"ASC(" ")"#);
690 check_expr_ok('오' as i32, r#"ASC("오")"#);
691
692 check_expr_ok_with_vars('a' as i32, r#"ASC(s)"#, [("s", "a".into())]);
693
694 check_expr_compilation_error("1:10: ASC expected char$", r#"ASC()"#);
695 check_expr_compilation_error("1:14: Expected STRING but found INTEGER", r#"ASC(3)"#);
696 check_expr_compilation_error("1:10: ASC expected char$", r#"ASC("a", 1)"#);
697 check_expr_error("1:14: Input string \"\" must be 1-character long", r#"ASC("")"#);
698 check_expr_error("1:14: Input string \"ab\" must be 1-character long", r#"ASC("ab")"#);
699 }
700
701 #[test]
702 fn test_chr() {
703 check_expr_ok("a", r#"CHR(97)"#);
704 check_expr_ok("c", r#"CHR(98.6)"#);
705 check_expr_ok(" ", r#"CHR(32)"#);
706 check_expr_ok("오", r#"CHR(50724)"#);
707
708 check_expr_ok_with_vars(" ", r#"CHR(i)"#, [("i", 32.into())]);
709
710 check_expr_compilation_error("1:10: CHR expected code%", r#"CHR()"#);
711 check_expr_compilation_error("1:14: BOOLEAN is not a number", r#"CHR(FALSE)"#);
712 check_expr_compilation_error("1:10: CHR expected code%", r#"CHR("a", 1)"#);
713 check_expr_error("1:14: Character code -1 must be positive", r#"CHR(-1)"#);
714 check_expr_error("1:14: Invalid character code 55296", r#"CHR(55296)"#);
715 }
716
717 #[test]
718 fn test_asc_chr_integration() {
719 check_expr_ok("a", r#"CHR(ASC("a"))"#);
720 check_expr_ok('a' as i32, r#"ASC(CHR(97))"#);
721 }
722
723 #[test]
724 fn test_left() {
725 check_expr_ok("", r#"LEFT("", 0)"#);
726 check_expr_ok("abc", r#"LEFT("abcdef", 3)"#);
727 check_expr_ok("abcd", r#"LEFT("abcdef", 4)"#);
728 check_expr_ok("abcdef", r#"LEFT("abcdef", 6)"#);
729 check_expr_ok("abcdef", r#"LEFT("abcdef", 10)"#);
730
731 check_expr_ok_with_vars("abc", r#"LEFT(s, i)"#, [("s", "abcdef".into()), ("i", 3.into())]);
732
733 check_expr_compilation_error("1:10: LEFT expected expr$, n%", r#"LEFT()"#);
734 check_expr_compilation_error("1:10: LEFT expected expr$, n%", r#"LEFT("", 1, 2)"#);
735 check_expr_compilation_error("1:15: Expected STRING but found INTEGER", r#"LEFT(1, 2)"#);
736 check_expr_compilation_error("1:19: STRING is not a number", r#"LEFT("", "")"#);
737 check_expr_error("1:25: n% cannot be negative", r#"LEFT("abcdef", -5)"#);
738 }
739
740 #[test]
741 fn test_len() {
742 check_expr_ok(0, r#"LEN("")"#);
743 check_expr_ok(1, r#"LEN(" ")"#);
744 check_expr_ok(5, r#"LEN("abcde")"#);
745
746 check_expr_ok_with_vars(4, r#"LEN(s)"#, [("s", "1234".into())]);
747
748 check_expr_compilation_error("1:10: LEN expected expr$", r#"LEN()"#);
749 check_expr_compilation_error("1:14: Expected STRING but found INTEGER", r#"LEN(3)"#);
750 check_expr_compilation_error("1:10: LEN expected expr$", r#"LEN(" ", 1)"#);
751 }
752
753 #[test]
754 fn test_ltrim() {
755 check_expr_ok("", r#"LTRIM("")"#);
756 check_expr_ok("", r#"LTRIM(" ")"#);
757 check_expr_ok("", "LTRIM(\"\t\t\")");
758 check_expr_ok("foo \t ", "LTRIM(\" \t foo \t \")");
759
760 check_expr_ok_with_vars("foo ", r#"LTRIM(s)"#, [("s", " foo ".into())]);
761
762 check_expr_compilation_error("1:10: LTRIM expected expr$", r#"LTRIM()"#);
763 check_expr_compilation_error("1:16: Expected STRING but found INTEGER", r#"LTRIM(3)"#);
764 check_expr_compilation_error("1:10: LTRIM expected expr$", r#"LTRIM(" ", 1)"#);
765 }
766
767 #[test]
768 fn test_mid() {
769 check_expr_ok("", r#"MID("", 0, 0)"#);
770 check_expr_ok("", r#"MID("basic", 0, 0)"#);
771 check_expr_ok("", r#"MID("basic", 1, 0)"#);
772 check_expr_ok("a", r#"MID("basic", 1, 1)"#);
773 check_expr_ok("as", r#"MID("basic", 1, 2)"#);
774 check_expr_ok("asic", r#"MID("basic", 1, 4)"#);
775 check_expr_ok("asi", r#"MID("basic", 0.8, 3.2)"#);
776 check_expr_ok("asic", r#"MID("basic", 1, 10)"#);
777 check_expr_ok("asic", r#"MID("basic", 1)"#);
778 check_expr_ok("", r#"MID("basic", 100, 10)"#);
779
780 check_expr_ok_with_vars(
781 "asic",
782 r#"MID(s, i, j)"#,
783 [("s", "basic".into()), ("i", 1.into()), ("j", 4.into())],
784 );
785
786 check_expr_compilation_error(
787 "1:10: MID expected <expr$, start%> | <expr$, start%, length%>",
788 r#"MID()"#,
789 );
790 check_expr_compilation_error(
791 "1:10: MID expected <expr$, start%> | <expr$, start%, length%>",
792 r#"MID(3)"#,
793 );
794 check_expr_compilation_error(
795 "1:10: MID expected <expr$, start%> | <expr$, start%, length%>",
796 r#"MID(" ", 1, 1, 10)"#,
797 );
798 check_expr_compilation_error("1:19: STRING is not a number", r#"MID(" ", "1", 2)"#);
799 check_expr_compilation_error("1:22: STRING is not a number", r#"MID(" ", 1, "2")"#);
800 check_expr_error("1:24: start% cannot be negative", r#"MID("abcdef", -5, 10)"#);
801 check_expr_error("1:27: length% cannot be negative", r#"MID("abcdef", 3, -5)"#);
802 }
803
804 #[test]
805 fn test_right() {
806 check_expr_ok("", r#"RIGHT("", 0)"#);
807 check_expr_ok("def", r#"RIGHT("abcdef", 3)"#);
808 check_expr_ok("cdef", r#"RIGHT("abcdef", 4.2)"#);
809 check_expr_ok("abcdef", r#"RIGHT("abcdef", 6)"#);
810 check_expr_ok("abcdef", r#"RIGHT("abcdef", 10)"#);
811
812 check_expr_ok_with_vars("def", r#"RIGHT(s, i)"#, [("s", "abcdef".into()), ("i", 3.into())]);
813
814 check_expr_compilation_error("1:10: RIGHT expected expr$, n%", r#"RIGHT()"#);
815 check_expr_compilation_error("1:10: RIGHT expected expr$, n%", r#"RIGHT("", 1, 2)"#);
816 check_expr_compilation_error("1:16: Expected STRING but found INTEGER", r#"RIGHT(1, 2)"#);
817 check_expr_compilation_error("1:20: STRING is not a number", r#"RIGHT("", "")"#);
818 check_expr_error("1:26: n% cannot be negative", r#"RIGHT("abcdef", -5)"#);
819 }
820
821 #[test]
822 fn test_rtrim() {
823 check_expr_ok("", r#"RTRIM("")"#);
824 check_expr_ok("", r#"RTRIM(" ")"#);
825 check_expr_ok("", "RTRIM(\"\t\t\")");
826 check_expr_ok(" \t foo", "RTRIM(\" \t foo \t \")");
827
828 check_expr_ok_with_vars(" foo", r#"RTRIM(s)"#, [("s", " foo ".into())]);
829
830 check_expr_compilation_error("1:10: RTRIM expected expr$", r#"RTRIM()"#);
831 check_expr_compilation_error("1:16: Expected STRING but found INTEGER", r#"RTRIM(3)"#);
832 check_expr_compilation_error("1:10: RTRIM expected expr$", r#"RTRIM(" ", 1)"#);
833 }
834
835 #[test]
836 fn test_str() {
837 check_expr_ok("FALSE", r#"STR(FALSE)"#);
838 check_expr_ok("TRUE", r#"STR(true)"#);
839
840 check_expr_ok(" 0", r#"STR(0)"#);
841 check_expr_ok(" 1", r#"STR(1)"#);
842 check_expr_ok("-1", r#"STR(-1)"#);
843
844 check_expr_ok(" 0.5", r#"STR(0.5)"#);
845 check_expr_ok(" 1.5", r#"STR(1.5)"#);
846 check_expr_ok("-1.5", r#"STR(-1.5)"#);
847
848 check_expr_ok("", r#"STR("")"#);
849 check_expr_ok(" \t ", "STR(\" \t \")");
850 check_expr_ok("foo bar", r#"STR("foo bar")"#);
851
852 check_expr_ok_with_vars(" 1", r#"STR(i)"#, [("i", 1.into())]);
853
854 check_expr_compilation_error("1:10: STR expected expr", r#"STR()"#);
855 check_expr_compilation_error("1:10: STR expected expr", r#"STR(" ", 1)"#);
856 }
857
858 #[test]
859 fn test_str_with_ltrim() {
860 check_expr_ok("0", r#"LTRIM(STR(0))"#);
861 check_expr_ok("-1", r#"LTRIM(STR(-1))"#);
862 check_expr_ok("100", r#"LTRIM$(STR$(100))"#);
863 }
864}