Skip to main content

endbasic_std/
strings.rs

1// EndBASIC
2// Copyright 2020 Julio Merino
3//
4// This program is free software: you can redistribute it and/or modify
5// it under the terms of the GNU Affero General Public License as published by
6// the Free Software Foundation, either version 3 of the License, or
7// (at your option) any later version.
8//
9// This program is distributed in the hope that it will be useful,
10// but WITHOUT ANY WARRANTY; without even the implied warranty of
11// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12// GNU Affero General Public License for more details.
13//
14// You should have received a copy of the GNU Affero General Public License
15// along with this program.  If not, see <https://www.gnu.org/licenses/>.
16
17//! String functions for EndBASIC.
18
19use 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
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) -> 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) -> 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) -> 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: Rc<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
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
142/// The `CHR` function.
143pub struct ChrFunction {
144    metadata: Rc<CallableMetadata>,
145}
146
147impl ChrFunction {
148    /// Creates a new instance of the function.
149    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
199/// The `LEFT` function.
200pub struct LeftFunction {
201    metadata: Rc<CallableMetadata>,
202}
203
204impl LeftFunction {
205    /// Creates a new instance of the function.
206    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
259/// The `LEN` function.
260pub struct LenFunction {
261    metadata: Rc<CallableMetadata>,
262}
263
264impl LenFunction {
265    /// Creates a new instance of the function.
266    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
299/// The `LTRIM` function.
300pub struct LtrimFunction {
301    metadata: Rc<CallableMetadata>,
302}
303
304impl LtrimFunction {
305    /// Creates a new instance of the function.
306    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
337/// The `MID` function.
338pub struct MidFunction {
339    metadata: Rc<CallableMetadata>,
340}
341
342impl MidFunction {
343    /// Creates a new instance of the function.
344    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
442/// The `RIGHT` function.
443pub struct RightFunction {
444    metadata: Rc<CallableMetadata>,
445}
446
447impl RightFunction {
448    /// Creates a new instance of the function.
449    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
502/// The `RTRIM` function.
503pub struct RtrimFunction {
504    metadata: Rc<CallableMetadata>,
505}
506
507impl RtrimFunction {
508    /// Creates a new instance of the function.
509    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
540/// The `STR` function.
541pub struct StrFunction {
542    metadata: Rc<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
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
607/// Adds all symbols provided by this module to the given `machine`.
608pub 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}