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::{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
32/// Category description for all symbols provided by this module.
33const CATEGORY: &str = "String and character functions";
34
35/// Formats a boolean `b` for display.
36pub fn format_boolean(b: bool) -> &'static str {
37    if b {
38        "TRUE"
39    } else {
40        "FALSE"
41    }
42}
43
44/// Parses a string `s` as a boolean.
45pub 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
56/// Formats a double `d` for display.
57pub 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
65/// Parses a string `s` as a double.
66pub 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
73/// Formats an integer `i` for display.
74pub fn format_integer(i: i32) -> String {
75    if i.is_negative() {
76        i.to_string()
77    } else {
78        format!(" {}", i)
79    }
80}
81
82/// Parses a string `s` as an integer.
83pub 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
90/// The `ASC` function.
91pub struct AscFunction {
92    metadata: CallableMetadata,
93}
94
95impl AscFunction {
96    /// Creates a new instance of the function.
97    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
157/// The `CHR` function.
158pub struct ChrFunction {
159    metadata: CallableMetadata,
160}
161
162impl ChrFunction {
163    /// Creates a new instance of the function.
164    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
213/// The `LEFT` function.
214pub struct LeftFunction {
215    metadata: CallableMetadata,
216}
217
218impl LeftFunction {
219    /// Creates a new instance of the function.
220    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
274/// The `LEN` function.
275pub struct LenFunction {
276    metadata: CallableMetadata,
277}
278
279impl LenFunction {
280    /// Creates a new instance of the function.
281    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
317/// The `LTRIM` function.
318pub struct LtrimFunction {
319    metadata: CallableMetadata,
320}
321
322impl LtrimFunction {
323    /// Creates a new instance of the function.
324    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
356/// The `MID` function.
357pub struct MidFunction {
358    metadata: CallableMetadata,
359}
360
361impl MidFunction {
362    /// Creates a new instance of the function.
363    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
460/// The `RIGHT` function.
461pub struct RightFunction {
462    metadata: CallableMetadata,
463}
464
465impl RightFunction {
466    /// Creates a new instance of the function.
467    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
521/// The `RTRIM` function.
522pub struct RtrimFunction {
523    metadata: CallableMetadata,
524}
525
526impl RtrimFunction {
527    /// Creates a new instance of the function.
528    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
560/// The `STR` function.
561pub struct StrFunction {
562    metadata: CallableMetadata,
563}
564
565impl StrFunction {
566    /// Creates a new instance of the function.
567    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
626/// Adds all symbols provided by this module to the given `machine`.
627pub 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}