Skip to main content

endbasic_std/
strings.rs

1// EndBASIC
2// Copyright 2020 Julio Merino
3//
4// Licensed under the Apache License, Version 2.0 (the "License"); you may not
5// use this file except in compliance with the License.  You may obtain a copy
6// of the License at:
7//
8//     http://www.apache.org/licenses/LICENSE-2.0
9//
10// Unless required by applicable law or agreed to in writing, software
11// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
13// License for the specific language governing permissions and limitations
14// under the License.
15
16//! String functions for EndBASIC.
17
18use 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
30/// Category description for all symbols provided by this module.
31const CATEGORY: &str = "String and character functions";
32
33/// Formats a boolean `b` for display.
34pub fn format_boolean(b: bool) -> &'static str {
35    if b { "TRUE" } else { "FALSE" }
36}
37
38/// Parses a string `s` as a boolean.
39pub 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
50/// Formats a double `d` for display.
51pub fn format_double(d: f64) -> String {
52    if !d.is_nan() && d.is_sign_negative() { d.to_string() } else { format!(" {}", d) }
53}
54
55/// Parses a string `s` as a double.
56pub 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
63/// Formats an integer `i` for display.
64pub fn format_integer(i: i32) -> String {
65    if i.is_negative() { i.to_string() } else { format!(" {}", i) }
66}
67
68/// Parses a string `s` as an integer.
69pub 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
76/// The `ASC` function.
77pub struct AscFunction {
78    metadata: CallableMetadata,
79}
80
81impl AscFunction {
82    /// Creates a new instance of the function.
83    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
143/// The `CHR` function.
144pub struct ChrFunction {
145    metadata: CallableMetadata,
146}
147
148impl ChrFunction {
149    /// Creates a new instance of the function.
150    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
196/// The `LEFT` function.
197pub struct LeftFunction {
198    metadata: CallableMetadata,
199}
200
201impl LeftFunction {
202    /// Creates a new instance of the function.
203    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
257/// The `LEN` function.
258pub struct LenFunction {
259    metadata: CallableMetadata,
260}
261
262impl LenFunction {
263    /// Creates a new instance of the function.
264    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
300/// The `LTRIM` function.
301pub struct LtrimFunction {
302    metadata: CallableMetadata,
303}
304
305impl LtrimFunction {
306    /// Creates a new instance of the function.
307    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
339/// The `MID` function.
340pub struct MidFunction {
341    metadata: CallableMetadata,
342}
343
344impl MidFunction {
345    /// Creates a new instance of the function.
346    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
440/// The `RIGHT` function.
441pub struct RightFunction {
442    metadata: CallableMetadata,
443}
444
445impl RightFunction {
446    /// Creates a new instance of the function.
447    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
501/// The `RTRIM` function.
502pub struct RtrimFunction {
503    metadata: CallableMetadata,
504}
505
506impl RtrimFunction {
507    /// Creates a new instance of the function.
508    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
540/// The `STR` function.
541pub struct StrFunction {
542    metadata: CallableMetadata,
543}
544
545impl StrFunction {
546    /// Creates a new instance of the function.
547    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
606/// Adds all symbols provided by this module to the given `machine`.
607pub 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}