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