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