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/// Returns the byte offset of the given character position within `s`.
77fn char_index_to_byte_index(s: &str, char_index: usize) -> usize {
78    s.char_indices().nth(char_index).map(|(i, _)| i).unwrap_or(s.len())
79}
80
81/// Returns the number of characters before the given byte offset within `s`.
82fn byte_index_to_char_index(s: &str, byte_index: usize) -> usize {
83    s[..byte_index].chars().count()
84}
85
86/// Returns the number of characters in `s`.
87fn char_len(s: &str) -> usize {
88    s.chars().count()
89}
90
91/// Returns the substring of `s` within the given character range.
92fn slice_by_char_range(s: &str, start: usize, end: usize) -> &str {
93    let start = char_index_to_byte_index(s, start);
94    let end = char_index_to_byte_index(s, end);
95    &s[start..end]
96}
97
98/// Parses the numeric prefix in `s` according to BASIC's `VAL` rules.
99fn parse_numeric_prefix(s: &str) -> f64 {
100    let s = s.trim_start();
101    let bytes = s.as_bytes();
102
103    let mut i = 0;
104    if let Some(b'+' | b'-') = bytes.first().copied() {
105        i += 1;
106    }
107
108    let int_start = i;
109    while i < bytes.len() && bytes[i].is_ascii_digit() {
110        i += 1;
111    }
112    let have_int = i > int_start;
113
114    if i < bytes.len() && bytes[i] == b'.' {
115        i += 1;
116        let frac_start = i;
117        while i < bytes.len() && bytes[i].is_ascii_digit() {
118            i += 1;
119        }
120        if !have_int && i == frac_start {
121            return 0.0;
122        }
123    } else if !have_int {
124        return 0.0;
125    }
126
127    let mut end = i;
128    if i < bytes.len() && matches!(bytes[i], b'e' | b'E') {
129        let mut j = i + 1;
130        if j < bytes.len() && matches!(bytes[j], b'+' | b'-') {
131            j += 1;
132        }
133        let exp_start = j;
134        while j < bytes.len() && bytes[j].is_ascii_digit() {
135            j += 1;
136        }
137        if j > exp_start {
138            end = j;
139        }
140    }
141
142    s[..end].parse().expect("numeric prefix must parse")
143}
144
145/// The `ASC` function.
146pub struct AscFunction {
147    metadata: Rc<CallableMetadata>,
148}
149
150impl AscFunction {
151    /// Creates a new instance of the function.
152    pub fn new() -> Rc<Self> {
153        Rc::from(Self {
154            metadata: CallableMetadataBuilder::new("ASC")
155                .with_return_type(ExprType::Integer)
156                .with_syntax(&[(
157                    &[SingularArgSyntax::RequiredValue(
158                        RequiredValueSyntax { name: Cow::Borrowed("char"), vtype: ExprType::Text },
159                        ArgSepSyntax::End,
160                    )],
161                    None,
162                )])
163                .with_category(CATEGORY)
164                .with_description(
165                    "Returns the UTF character code of the input character.
166The input char$ argument is a string that must be 1-character long.
167This is called ASC for historical reasons but supports more than just ASCII characters in this \
168implementation of BASIC.
169See CHR$() for the inverse of this function.",
170                )
171                .build(),
172        })
173    }
174}
175
176impl Callable for AscFunction {
177    fn metadata(&self) -> Rc<CallableMetadata> {
178        self.metadata.clone()
179    }
180
181    fn exec(&self, scope: Scope<'_>) -> CallResult<()> {
182        debug_assert_eq!(1, scope.nargs());
183        let s = scope.get_string(0);
184
185        let mut chars = s.chars();
186        let ch = match chars.next() {
187            Some(ch) => ch,
188            None => {
189                return Err(CallError::Syntax(
190                    scope.get_pos(0),
191                    format!("Input string \"{}\" must be 1-character long", s),
192                ));
193            }
194        };
195        if chars.next().is_some() {
196            return Err(CallError::Syntax(
197                scope.get_pos(0),
198                format!("Input string \"{}\" must be 1-character long", s),
199            ));
200        }
201        let ch = if cfg!(debug_assertions) {
202            i32::try_from(ch as u32).expect("Unicode code points end at U+10FFFF")
203        } else {
204            ch as i32
205        };
206
207        scope.return_integer(ch)
208    }
209}
210
211/// The `CHR` function.
212pub struct ChrFunction {
213    metadata: Rc<CallableMetadata>,
214}
215
216impl ChrFunction {
217    /// Creates a new instance of the function.
218    pub fn new() -> Rc<Self> {
219        Rc::from(Self {
220            metadata: CallableMetadataBuilder::new("CHR")
221                .with_return_type(ExprType::Text)
222                .with_syntax(&[(
223                    &[SingularArgSyntax::RequiredValue(
224                        RequiredValueSyntax {
225                            name: Cow::Borrowed("code"),
226                            vtype: ExprType::Integer,
227                        },
228                        ArgSepSyntax::End,
229                    )],
230                    None,
231                )])
232                .with_category(CATEGORY)
233                .with_description(
234                    "Returns the UTF character that corresponds to the given code.
235See ASC%() for the inverse of this function.",
236                )
237                .build(),
238        })
239    }
240}
241
242impl Callable for ChrFunction {
243    fn metadata(&self) -> Rc<CallableMetadata> {
244        self.metadata.clone()
245    }
246
247    fn exec(&self, scope: Scope<'_>) -> CallResult<()> {
248        debug_assert_eq!(1, scope.nargs());
249        let i = scope.get_integer(0);
250
251        if i < 0 {
252            return Err(CallError::Syntax(
253                scope.get_pos(0),
254                format!("Character code {} must be positive", i),
255            ));
256        }
257        let code = i as u32;
258
259        match char::from_u32(code) {
260            Some(ch) => scope.return_string(format!("{}", ch)),
261            None => {
262                Err(CallError::Syntax(scope.get_pos(0), format!("Invalid character code {}", code)))
263            }
264        }
265    }
266}
267
268/// The `INSTR` function.
269pub struct InstrFunction {
270    metadata: Rc<CallableMetadata>,
271}
272
273impl InstrFunction {
274    /// Creates a new instance of the function.
275    pub fn new() -> Rc<Self> {
276        Rc::from(Self {
277            metadata: CallableMetadataBuilder::new("INSTR")
278                .with_return_type(ExprType::Integer)
279                .with_syntax(&[
280                    (
281                        &[
282                            SingularArgSyntax::RequiredValue(
283                                RequiredValueSyntax {
284                                    name: Cow::Borrowed("expr"),
285                                    vtype: ExprType::Text,
286                                },
287                                ArgSepSyntax::Exactly(ArgSep::Long),
288                            ),
289                            SingularArgSyntax::RequiredValue(
290                                RequiredValueSyntax {
291                                    name: Cow::Borrowed("search"),
292                                    vtype: ExprType::Text,
293                                },
294                                ArgSepSyntax::End,
295                            ),
296                        ],
297                        None,
298                    ),
299                    (
300                        &[
301                            SingularArgSyntax::RequiredValue(
302                                RequiredValueSyntax {
303                                    name: Cow::Borrowed("start"),
304                                    vtype: ExprType::Integer,
305                                },
306                                ArgSepSyntax::Exactly(ArgSep::Long),
307                            ),
308                            SingularArgSyntax::RequiredValue(
309                                RequiredValueSyntax {
310                                    name: Cow::Borrowed("expr"),
311                                    vtype: ExprType::Text,
312                                },
313                                ArgSepSyntax::Exactly(ArgSep::Long),
314                            ),
315                            SingularArgSyntax::RequiredValue(
316                                RequiredValueSyntax {
317                                    name: Cow::Borrowed("search"),
318                                    vtype: ExprType::Text,
319                                },
320                                ArgSepSyntax::End,
321                            ),
322                        ],
323                        None,
324                    ),
325                ])
326                .with_category(CATEGORY)
327                .with_description(
328                    "Searches for a substring within another string.
329Returns the 1-indexed position of search$ within expr$, or 0 if not found.
330If start% is given, the search begins at that 1-indexed character position.",
331                )
332                .build(),
333        })
334    }
335}
336
337impl Callable for InstrFunction {
338    fn metadata(&self) -> Rc<CallableMetadata> {
339        self.metadata.clone()
340    }
341
342    fn exec(&self, scope: Scope<'_>) -> CallResult<()> {
343        debug_assert!((2..=3).contains(&scope.nargs()));
344
345        let (start, sarg, searcharg) =
346            if scope.nargs() == 2 { (1, 0, 1) } else { (scope.get_integer(0), 1, 2) };
347        let s = scope.get_string(sarg);
348        let search = scope.get_string(searcharg);
349
350        if start <= 0 {
351            return Err(CallError::Syntax(scope.get_pos(0), "start% must be positive".to_owned()));
352        }
353        let start = (start - 1) as usize;
354
355        let slen = s.chars().count();
356        if start > slen {
357            return scope.return_integer(0);
358        }
359        if search.is_empty() {
360            return scope.return_integer((start + 1) as i32);
361        }
362
363        let start_byte = char_index_to_byte_index(s, start);
364        let position = match s[start_byte..].find(search) {
365            Some(offset) => {
366                let byte_index = start_byte + offset;
367                (byte_index_to_char_index(s, byte_index) + 1) as i32
368            }
369            None => 0,
370        };
371        scope.return_integer(position)
372    }
373}
374
375/// The `LCASE` function.
376pub struct LcaseFunction {
377    metadata: Rc<CallableMetadata>,
378}
379
380impl LcaseFunction {
381    /// Creates a new instance of the function.
382    pub fn new() -> Rc<Self> {
383        Rc::from(Self {
384            metadata: CallableMetadataBuilder::new("LCASE")
385                .with_return_type(ExprType::Text)
386                .with_syntax(&[(
387                    &[SingularArgSyntax::RequiredValue(
388                        RequiredValueSyntax { name: Cow::Borrowed("expr"), vtype: ExprType::Text },
389                        ArgSepSyntax::End,
390                    )],
391                    None,
392                )])
393                .with_category(CATEGORY)
394                .with_description(
395                    "Returns a copy of a string with all letters converted to lowercase.",
396                )
397                .build(),
398        })
399    }
400}
401
402impl Callable for LcaseFunction {
403    fn metadata(&self) -> Rc<CallableMetadata> {
404        self.metadata.clone()
405    }
406
407    fn exec(&self, scope: Scope<'_>) -> CallResult<()> {
408        debug_assert_eq!(1, scope.nargs());
409        let s = scope.get_string(0).to_owned();
410
411        scope.return_string(s.to_lowercase())
412    }
413}
414
415/// The `LEFT` function.
416pub struct LeftFunction {
417    metadata: Rc<CallableMetadata>,
418}
419
420impl LeftFunction {
421    /// Creates a new instance of the function.
422    pub fn new() -> Rc<Self> {
423        Rc::from(Self {
424            metadata: CallableMetadataBuilder::new("LEFT")
425                .with_return_type(ExprType::Text)
426                .with_syntax(&[(
427                    &[
428                        SingularArgSyntax::RequiredValue(
429                            RequiredValueSyntax {
430                                name: Cow::Borrowed("expr"),
431                                vtype: ExprType::Text,
432                            },
433                            ArgSepSyntax::Exactly(ArgSep::Long),
434                        ),
435                        SingularArgSyntax::RequiredValue(
436                            RequiredValueSyntax {
437                                name: Cow::Borrowed("n"),
438                                vtype: ExprType::Integer,
439                            },
440                            ArgSepSyntax::End,
441                        ),
442                    ],
443                    None,
444                )])
445                .with_category(CATEGORY)
446                .with_description(
447                    "Returns a given number of characters from the left side of a string.
448If n% is 0, returns an empty string.
449If n% is greater than or equal to the number of characters in expr$, returns expr$.",
450                )
451                .build(),
452        })
453    }
454}
455
456impl Callable for LeftFunction {
457    fn metadata(&self) -> Rc<CallableMetadata> {
458        self.metadata.clone()
459    }
460
461    fn exec(&self, scope: Scope<'_>) -> CallResult<()> {
462        debug_assert_eq!(2, scope.nargs());
463        let s = scope.get_string(0).to_owned();
464        let n = scope.get_integer(1);
465
466        if n < 0 {
467            Err(CallError::Syntax(scope.get_pos(1), "n% cannot be negative".to_owned()))
468        } else {
469            let n = min(char_len(&s), n as usize);
470            scope.return_string(slice_by_char_range(&s, 0, n).to_owned())
471        }
472    }
473}
474
475/// The `LEN` function.
476pub struct LenFunction {
477    metadata: Rc<CallableMetadata>,
478}
479
480impl LenFunction {
481    /// Creates a new instance of the function.
482    pub fn new() -> Rc<Self> {
483        Rc::from(Self {
484            metadata: CallableMetadataBuilder::new("LEN")
485                .with_return_type(ExprType::Integer)
486                .with_syntax(&[(
487                    &[SingularArgSyntax::RequiredValue(
488                        RequiredValueSyntax { name: Cow::Borrowed("expr"), vtype: ExprType::Text },
489                        ArgSepSyntax::End,
490                    )],
491                    None,
492                )])
493                .with_category(CATEGORY)
494                .with_description("Returns the length of the string in expr$.")
495                .build(),
496        })
497    }
498}
499
500impl Callable for LenFunction {
501    fn metadata(&self) -> Rc<CallableMetadata> {
502        self.metadata.clone()
503    }
504
505    fn exec(&self, scope: Scope<'_>) -> CallResult<()> {
506        debug_assert_eq!(1, scope.nargs());
507        let s = scope.get_string(0);
508
509        let len = i32::try_from(char_len(s))
510            .map_err(|_| CallError::Eval("String too long".to_owned()))?;
511        scope.return_integer(len)
512    }
513}
514
515/// The `LTRIM` function.
516pub struct LtrimFunction {
517    metadata: Rc<CallableMetadata>,
518}
519
520impl LtrimFunction {
521    /// Creates a new instance of the function.
522    pub fn new() -> Rc<Self> {
523        Rc::from(Self {
524            metadata: CallableMetadataBuilder::new("LTRIM")
525                .with_return_type(ExprType::Text)
526                .with_syntax(&[(
527                    &[SingularArgSyntax::RequiredValue(
528                        RequiredValueSyntax { name: Cow::Borrowed("expr"), vtype: ExprType::Text },
529                        ArgSepSyntax::End,
530                    )],
531                    None,
532                )])
533                .with_category(CATEGORY)
534                .with_description("Returns a copy of a string with leading whitespace removed.")
535                .build(),
536        })
537    }
538}
539
540impl Callable for LtrimFunction {
541    fn metadata(&self) -> Rc<CallableMetadata> {
542        self.metadata.clone()
543    }
544
545    fn exec(&self, scope: Scope<'_>) -> CallResult<()> {
546        debug_assert_eq!(1, scope.nargs());
547        let s = scope.get_string(0).to_owned();
548
549        scope.return_string(s.trim_start().to_owned())
550    }
551}
552
553/// The `MID` function.
554pub struct MidFunction {
555    metadata: Rc<CallableMetadata>,
556}
557
558impl MidFunction {
559    /// Creates a new instance of the function.
560    pub fn new() -> Rc<Self> {
561        Rc::from(Self {
562            metadata: CallableMetadataBuilder::new("MID")
563                .with_return_type(ExprType::Text)
564                .with_syntax(&[
565                    (
566                        &[
567                            SingularArgSyntax::RequiredValue(
568                                RequiredValueSyntax {
569                                    name: Cow::Borrowed("expr"),
570                                    vtype: ExprType::Text,
571                                },
572                                ArgSepSyntax::Exactly(ArgSep::Long),
573                            ),
574                            SingularArgSyntax::RequiredValue(
575                                RequiredValueSyntax {
576                                    name: Cow::Borrowed("start"),
577                                    vtype: ExprType::Integer,
578                                },
579                                ArgSepSyntax::End,
580                            ),
581                        ],
582                        None,
583                    ),
584                    (
585                        &[
586                            SingularArgSyntax::RequiredValue(
587                                RequiredValueSyntax {
588                                    name: Cow::Borrowed("expr"),
589                                    vtype: ExprType::Text,
590                                },
591                                ArgSepSyntax::Exactly(ArgSep::Long),
592                            ),
593                            SingularArgSyntax::RequiredValue(
594                                RequiredValueSyntax {
595                                    name: Cow::Borrowed("start"),
596                                    vtype: ExprType::Integer,
597                                },
598                                ArgSepSyntax::Exactly(ArgSep::Long),
599                            ),
600                            SingularArgSyntax::RequiredValue(
601                                RequiredValueSyntax {
602                                    name: Cow::Borrowed("length"),
603                                    vtype: ExprType::Integer,
604                                },
605                                ArgSepSyntax::End,
606                            ),
607                        ],
608                        None,
609                    ),
610                ])
611                .with_category(CATEGORY)
612                .with_description(
613                    "Returns a portion of a string.
614start% indicates the starting position of the substring to extract and it is 1-indexed.
615length% indicates the number of characters to extract and, if not specified, defaults to extracting
616until the end of the string.",
617                )
618                .build(),
619        })
620    }
621}
622
623impl Callable for MidFunction {
624    fn metadata(&self) -> Rc<CallableMetadata> {
625        self.metadata.clone()
626    }
627
628    fn exec(&self, scope: Scope<'_>) -> CallResult<()> {
629        debug_assert!((2..=3).contains(&scope.nargs()));
630        let s = scope.get_string(0).to_owned();
631        let start = scope.get_integer(1);
632        let lengtharg = if scope.nargs() == 3 { Some(scope.get_integer(2)) } else { None };
633
634        if start < 0 {
635            return Err(CallError::Syntax(
636                scope.get_pos(1),
637                "start% cannot be negative".to_owned(),
638            ));
639        }
640        let slen = char_len(&s);
641        let start = min(slen, start as usize);
642
643        let end = if let Some(length) = lengtharg {
644            if length < 0 {
645                return Err(CallError::Syntax(
646                    scope.get_pos(2),
647                    "length% cannot be negative".to_owned(),
648                ));
649            }
650            min(start + (length as usize), slen)
651        } else {
652            slen
653        };
654
655        scope.return_string(slice_by_char_range(&s, start, end).to_owned())
656    }
657}
658
659/// The `RIGHT` function.
660pub struct RightFunction {
661    metadata: Rc<CallableMetadata>,
662}
663
664impl RightFunction {
665    /// Creates a new instance of the function.
666    pub fn new() -> Rc<Self> {
667        Rc::from(Self {
668            metadata: CallableMetadataBuilder::new("RIGHT")
669                .with_return_type(ExprType::Text)
670                .with_syntax(&[(
671                    &[
672                        SingularArgSyntax::RequiredValue(
673                            RequiredValueSyntax {
674                                name: Cow::Borrowed("expr"),
675                                vtype: ExprType::Text,
676                            },
677                            ArgSepSyntax::Exactly(ArgSep::Long),
678                        ),
679                        SingularArgSyntax::RequiredValue(
680                            RequiredValueSyntax {
681                                name: Cow::Borrowed("n"),
682                                vtype: ExprType::Integer,
683                            },
684                            ArgSepSyntax::End,
685                        ),
686                    ],
687                    None,
688                )])
689                .with_category(CATEGORY)
690                .with_description(
691                    "Returns a given number of characters from the right side of a string.
692If n% is 0, returns an empty string.
693If n% is greater than or equal to the number of characters in expr$, returns expr$.",
694                )
695                .build(),
696        })
697    }
698}
699
700impl Callable for RightFunction {
701    fn metadata(&self) -> Rc<CallableMetadata> {
702        self.metadata.clone()
703    }
704
705    fn exec(&self, scope: Scope<'_>) -> CallResult<()> {
706        debug_assert_eq!(2, scope.nargs());
707        let s = scope.get_string(0).to_owned();
708        let n = scope.get_integer(1);
709
710        if n < 0 {
711            Err(CallError::Syntax(scope.get_pos(1), "n% cannot be negative".to_owned()))
712        } else {
713            let slen = char_len(&s);
714            let n = min(slen, n as usize);
715            scope.return_string(slice_by_char_range(&s, slen - n, slen).to_owned())
716        }
717    }
718}
719
720/// The `RTRIM` function.
721pub struct RtrimFunction {
722    metadata: Rc<CallableMetadata>,
723}
724
725impl RtrimFunction {
726    /// Creates a new instance of the function.
727    pub fn new() -> Rc<Self> {
728        Rc::from(Self {
729            metadata: CallableMetadataBuilder::new("RTRIM")
730                .with_return_type(ExprType::Text)
731                .with_syntax(&[(
732                    &[SingularArgSyntax::RequiredValue(
733                        RequiredValueSyntax { name: Cow::Borrowed("expr"), vtype: ExprType::Text },
734                        ArgSepSyntax::End,
735                    )],
736                    None,
737                )])
738                .with_category(CATEGORY)
739                .with_description("Returns a copy of a string with trailing whitespace removed.")
740                .build(),
741        })
742    }
743}
744
745impl Callable for RtrimFunction {
746    fn metadata(&self) -> Rc<CallableMetadata> {
747        self.metadata.clone()
748    }
749
750    fn exec(&self, scope: Scope<'_>) -> CallResult<()> {
751        debug_assert_eq!(1, scope.nargs());
752        let s = scope.get_string(0).to_owned();
753
754        scope.return_string(s.trim_end().to_owned())
755    }
756}
757
758/// The `SPACE` function.
759pub struct SpaceFunction {
760    metadata: Rc<CallableMetadata>,
761}
762
763impl SpaceFunction {
764    /// Creates a new instance of the function.
765    pub fn new() -> Rc<Self> {
766        Rc::from(Self {
767            metadata: CallableMetadataBuilder::new("SPACE")
768                .with_return_type(ExprType::Text)
769                .with_syntax(&[(
770                    &[SingularArgSyntax::RequiredValue(
771                        RequiredValueSyntax { name: Cow::Borrowed("n"), vtype: ExprType::Integer },
772                        ArgSepSyntax::End,
773                    )],
774                    None,
775                )])
776                .with_category(CATEGORY)
777                .with_description(
778                    "Returns a string that contains n% space characters.
779If n% is 0, returns an empty string.",
780                )
781                .build(),
782        })
783    }
784}
785
786impl Callable for SpaceFunction {
787    fn metadata(&self) -> Rc<CallableMetadata> {
788        self.metadata.clone()
789    }
790
791    fn exec(&self, scope: Scope<'_>) -> CallResult<()> {
792        debug_assert_eq!(1, scope.nargs());
793        let n = scope.get_integer(0);
794
795        if n < 0 {
796            Err(CallError::Syntax(scope.get_pos(0), "n% cannot be negative".to_owned()))
797        } else {
798            scope.return_string(" ".repeat(n as usize))
799        }
800    }
801}
802
803/// The `STR` function.
804pub struct StrFunction {
805    metadata: Rc<CallableMetadata>,
806}
807
808impl StrFunction {
809    /// Creates a new instance of the function.
810    pub fn new() -> Rc<Self> {
811        Rc::from(Self {
812            metadata: CallableMetadataBuilder::new("STR")
813                .with_return_type(ExprType::Text)
814                .with_syntax(&[(
815                    &[SingularArgSyntax::AnyValue(
816                        AnyValueSyntax { name: Cow::Borrowed("expr"), allow_missing: false },
817                        ArgSepSyntax::End,
818                    )],
819                    None,
820                )])
821                .with_category(CATEGORY)
822                .with_description(
823                    "Formats a scalar value as a string.
824If expr evaluates to a string, this returns the string unmodified.
825If expr evaluates to a boolean, this returns the strings FALSE or TRUE.
826If expr evaluates to a number, this returns a string with the textual representation of the \
827number.  If the number does NOT have a negative sign, the resulting string has a single space \
828in front of it.
829To obtain a clean representation of expr as a string without any artificial whitespace characters \
830in it, do LTRIM$(STR$(expr)).",
831                )
832                .build(),
833        })
834    }
835}
836
837impl Callable for StrFunction {
838    fn metadata(&self) -> Rc<CallableMetadata> {
839        self.metadata.clone()
840    }
841
842    fn exec(&self, scope: Scope<'_>) -> CallResult<()> {
843        debug_assert_eq!(2, scope.nargs());
844        match scope.get_type(0) {
845            VarArgTag::Immediate(_, ExprType::Boolean) => {
846                let b = scope.get_boolean(1);
847                scope.return_string(format_boolean(b).to_owned())
848            }
849
850            VarArgTag::Immediate(_, ExprType::Double) => {
851                let d = scope.get_double(1);
852                scope.return_string(format_double(d))
853            }
854
855            VarArgTag::Immediate(_, ExprType::Integer) => {
856                let i = scope.get_integer(1);
857                scope.return_string(format_integer(i))
858            }
859
860            VarArgTag::Immediate(_, ExprType::Text) => {
861                let s = scope.get_string(1).to_owned();
862                scope.return_string(s)
863            }
864
865            VarArgTag::Missing(..) | VarArgTag::Pointer(..) => unreachable!(),
866        }
867    }
868}
869
870/// The `TRIM` function.
871pub struct TrimFunction {
872    metadata: Rc<CallableMetadata>,
873}
874
875impl TrimFunction {
876    /// Creates a new instance of the function.
877    pub fn new() -> Rc<Self> {
878        Rc::from(Self {
879            metadata: CallableMetadataBuilder::new("TRIM")
880                .with_return_type(ExprType::Text)
881                .with_syntax(&[(
882                    &[SingularArgSyntax::RequiredValue(
883                        RequiredValueSyntax { name: Cow::Borrowed("expr"), vtype: ExprType::Text },
884                        ArgSepSyntax::End,
885                    )],
886                    None,
887                )])
888                .with_category(CATEGORY)
889                .with_description(
890                    "Returns a copy of a string with leading and trailing whitespace removed.",
891                )
892                .build(),
893        })
894    }
895}
896
897impl Callable for TrimFunction {
898    fn metadata(&self) -> Rc<CallableMetadata> {
899        self.metadata.clone()
900    }
901
902    fn exec(&self, scope: Scope<'_>) -> CallResult<()> {
903        debug_assert_eq!(1, scope.nargs());
904        let s = scope.get_string(0).to_owned();
905
906        scope.return_string(s.trim().to_owned())
907    }
908}
909
910/// The `UCASE` function.
911pub struct UcaseFunction {
912    metadata: Rc<CallableMetadata>,
913}
914
915impl UcaseFunction {
916    /// Creates a new instance of the function.
917    pub fn new() -> Rc<Self> {
918        Rc::from(Self {
919            metadata: CallableMetadataBuilder::new("UCASE")
920                .with_return_type(ExprType::Text)
921                .with_syntax(&[(
922                    &[SingularArgSyntax::RequiredValue(
923                        RequiredValueSyntax { name: Cow::Borrowed("expr"), vtype: ExprType::Text },
924                        ArgSepSyntax::End,
925                    )],
926                    None,
927                )])
928                .with_category(CATEGORY)
929                .with_description(
930                    "Returns a copy of a string with all letters converted to uppercase.",
931                )
932                .build(),
933        })
934    }
935}
936
937impl Callable for UcaseFunction {
938    fn metadata(&self) -> Rc<CallableMetadata> {
939        self.metadata.clone()
940    }
941
942    fn exec(&self, scope: Scope<'_>) -> CallResult<()> {
943        debug_assert_eq!(1, scope.nargs());
944        let s = scope.get_string(0).to_owned();
945
946        scope.return_string(s.to_uppercase())
947    }
948}
949
950/// The `VAL` function.
951pub struct ValFunction {
952    metadata: Rc<CallableMetadata>,
953}
954
955impl ValFunction {
956    /// Creates a new instance of the function.
957    pub fn new() -> Rc<Self> {
958        Rc::from(Self {
959            metadata: CallableMetadataBuilder::new("VAL")
960                .with_return_type(ExprType::Double)
961                .with_syntax(&[(
962                    &[SingularArgSyntax::RequiredValue(
963                        RequiredValueSyntax { name: Cow::Borrowed("expr"), vtype: ExprType::Text },
964                        ArgSepSyntax::End,
965                    )],
966                    None,
967                )])
968                .with_category(CATEGORY)
969                .with_description(
970                    "Parses a string as a number.
971Returns the numeric prefix of expr$.  Leading whitespace is ignored, and if expr$ does not start \
972with a number, this returns 0.  Stops processing (without an error) at the first non-numeric \
973character.",
974                )
975                .build(),
976        })
977    }
978}
979
980impl Callable for ValFunction {
981    fn metadata(&self) -> Rc<CallableMetadata> {
982        self.metadata.clone()
983    }
984
985    fn exec(&self, scope: Scope<'_>) -> CallResult<()> {
986        debug_assert_eq!(1, scope.nargs());
987        let s = scope.get_string(0);
988
989        let value = parse_numeric_prefix(s);
990        scope.return_double(value)
991    }
992}
993
994/// Adds all symbols provided by this module to the given `machine`.
995pub fn add_all(machine: &mut MachineBuilder) {
996    machine.add_callable(AscFunction::new());
997    machine.add_callable(ChrFunction::new());
998    machine.add_callable(InstrFunction::new());
999    machine.add_callable(LcaseFunction::new());
1000    machine.add_callable(LeftFunction::new());
1001    machine.add_callable(LenFunction::new());
1002    machine.add_callable(LtrimFunction::new());
1003    machine.add_callable(MidFunction::new());
1004    machine.add_callable(RightFunction::new());
1005    machine.add_callable(RtrimFunction::new());
1006    machine.add_callable(SpaceFunction::new());
1007    machine.add_callable(StrFunction::new());
1008    machine.add_callable(TrimFunction::new());
1009    machine.add_callable(UcaseFunction::new());
1010    machine.add_callable(ValFunction::new());
1011}
1012
1013#[cfg(test)]
1014mod tests {
1015    use super::*;
1016    use crate::testutils::*;
1017
1018    #[test]
1019    fn test_value_parse_boolean() {
1020        for s in &["true", "TrUe", "TRUE", "yes", "Yes", "y", "Y"] {
1021            assert!(parse_boolean(s).unwrap());
1022        }
1023
1024        for s in &["false", "FaLsE", "FALSE", "no", "No", "n", "N"] {
1025            assert!(!parse_boolean(s).unwrap());
1026        }
1027
1028        for s in &["ye", "0", "1", " true"] {
1029            assert_eq!(
1030                format!("Invalid boolean literal {}", s),
1031                format!("{}", parse_boolean(s).unwrap_err())
1032            );
1033        }
1034    }
1035
1036    #[test]
1037    fn test_value_parse_double() {
1038        assert_eq!(10.0, parse_double("10").unwrap());
1039        assert_eq!(0.0, parse_double("0").unwrap());
1040        assert_eq!(-21.0, parse_double("-21").unwrap());
1041        assert_eq!(1.0, parse_double("1.0").unwrap());
1042        assert_eq!(0.01, parse_double(".01").unwrap());
1043
1044        assert_eq!(
1045            123456789012345680000000000000.0,
1046            parse_double("123456789012345678901234567890.1").unwrap()
1047        );
1048
1049        assert_eq!(1.1234567890123457, parse_double("1.123456789012345678901234567890").unwrap());
1050
1051        assert_eq!(
1052            "Invalid double-precision floating point literal ",
1053            format!("{}", parse_double("").unwrap_err())
1054        );
1055        assert_eq!(
1056            "Invalid double-precision floating point literal - 3.0",
1057            format!("{}", parse_double("- 3.0").unwrap_err())
1058        );
1059        assert_eq!(
1060            "Invalid double-precision floating point literal 34ab3.1",
1061            format!("{}", parse_double("34ab3.1").unwrap_err())
1062        );
1063    }
1064
1065    #[test]
1066    fn test_value_parse_integer() {
1067        assert_eq!(10, parse_integer("10").unwrap());
1068        assert_eq!(0, parse_integer("0").unwrap());
1069        assert_eq!(-21, parse_integer("-21").unwrap());
1070
1071        assert_eq!("Invalid integer literal ", format!("{}", parse_integer("").unwrap_err()));
1072        assert_eq!("Invalid integer literal - 3", format!("{}", parse_integer("- 3").unwrap_err()));
1073        assert_eq!(
1074            "Invalid integer literal 34ab3",
1075            format!("{}", parse_integer("34ab3").unwrap_err())
1076        );
1077    }
1078
1079    #[test]
1080    fn test_asc() {
1081        check_expr_ok('a' as i32, r#"ASC("a")"#);
1082        check_expr_ok(' ' as i32, r#"ASC(" ")"#);
1083        check_expr_ok('오' as i32, r#"ASC("오")"#);
1084
1085        check_expr_ok_with_vars('a' as i32, r#"ASC(s)"#, [("s", "a".into())]);
1086
1087        check_expr_compilation_error("1:10: ASC expected char$", r#"ASC()"#);
1088        check_expr_compilation_error("1:14: Expected STRING but found INTEGER", r#"ASC(3)"#);
1089        check_expr_compilation_error("1:10: ASC expected char$", r#"ASC("a", 1)"#);
1090        check_expr_error("1:14: Input string \"\" must be 1-character long", r#"ASC("")"#);
1091        check_expr_error("1:14: Input string \"ab\" must be 1-character long", r#"ASC("ab")"#);
1092    }
1093
1094    #[test]
1095    fn test_chr() {
1096        check_expr_ok("a", r#"CHR(97)"#);
1097        check_expr_ok("c", r#"CHR(98.6)"#);
1098        check_expr_ok(" ", r#"CHR(32)"#);
1099        check_expr_ok("오", r#"CHR(50724)"#);
1100
1101        check_expr_ok_with_vars(" ", r#"CHR(i)"#, [("i", 32.into())]);
1102
1103        check_expr_compilation_error("1:10: CHR expected code%", r#"CHR()"#);
1104        check_expr_compilation_error("1:14: BOOLEAN is not a number", r#"CHR(FALSE)"#);
1105        check_expr_compilation_error("1:10: CHR expected code%", r#"CHR("a", 1)"#);
1106        check_expr_error("1:14: Character code -1 must be positive", r#"CHR(-1)"#);
1107        check_expr_error("1:14: Invalid character code 55296", r#"CHR(55296)"#);
1108    }
1109
1110    #[test]
1111    fn test_asc_chr_integration() {
1112        check_expr_ok("a", r#"CHR(ASC("a"))"#);
1113        check_expr_ok('a' as i32, r#"ASC(CHR(97))"#);
1114    }
1115
1116    #[test]
1117    fn test_instr() {
1118        check_expr_ok(0, r#"INSTR("", "a")"#);
1119        check_expr_ok(1, r#"INSTR("basic", "b")"#);
1120        check_expr_ok(3, r#"INSTR("basic", "si")"#);
1121        check_expr_ok(0, r#"INSTR("basic", "z")"#);
1122        check_expr_ok(4, r#"INSTR(4, "banana", "an")"#);
1123        check_expr_ok(2, r#"INSTR("banana", "an")"#);
1124        check_expr_ok(2, r#"INSTR("한글테스트", "글")"#);
1125        check_expr_ok(3, r#"INSTR(3, "basic", "")"#);
1126
1127        check_expr_ok_with_vars(
1128            3,
1129            r#"INSTR(i, s, t)"#,
1130            [("i", 2.into()), ("s", "banana".into()), ("t", "na".into())],
1131        );
1132
1133        check_expr_compilation_error(
1134            "1:10: INSTR expected <expr$, search$> | <start%, expr$, search$>",
1135            r#"INSTR()"#,
1136        );
1137        check_expr_compilation_error(
1138            "1:10: INSTR expected <expr$, search$> | <start%, expr$, search$>",
1139            r#"INSTR("a")"#,
1140        );
1141        check_expr_compilation_error(
1142            "1:10: INSTR expected <expr$, search$> | <start%, expr$, search$>",
1143            r#"INSTR("a", "b", "c", "d")"#,
1144        );
1145        check_expr_error("1:16: start% must be positive", r#"INSTR(0, "abc", "a")"#);
1146    }
1147
1148    #[test]
1149    fn test_lcase() {
1150        check_expr_ok("", r#"LCASE("")"#);
1151        check_expr_ok("basic", r#"LCASE("BaSiC")"#);
1152        check_expr_ok("anos", r#"LCASE("ANOS")"#);
1153
1154        check_expr_ok_with_vars("hello", r#"LCASE(s)"#, [("s", "HELLO".into())]);
1155
1156        check_expr_compilation_error("1:10: LCASE expected expr$", r#"LCASE()"#);
1157        check_expr_compilation_error("1:16: Expected STRING but found INTEGER", r#"LCASE(3)"#);
1158        check_expr_compilation_error("1:10: LCASE expected expr$", r#"LCASE(" ", 1)"#);
1159    }
1160
1161    #[test]
1162    fn test_left() {
1163        check_expr_ok("", r#"LEFT("", 0)"#);
1164        check_expr_ok("abc", r#"LEFT("abcdef", 3)"#);
1165        check_expr_ok("abcd", r#"LEFT("abcdef", 4)"#);
1166        check_expr_ok("abcdef", r#"LEFT("abcdef", 6)"#);
1167        check_expr_ok("abcdef", r#"LEFT("abcdef", 10)"#);
1168        check_expr_ok("한", r#"LEFT("한글", 1)"#);
1169
1170        check_expr_ok_with_vars("abc", r#"LEFT(s, i)"#, [("s", "abcdef".into()), ("i", 3.into())]);
1171
1172        check_expr_compilation_error("1:10: LEFT expected expr$, n%", r#"LEFT()"#);
1173        check_expr_compilation_error("1:10: LEFT expected expr$, n%", r#"LEFT("", 1, 2)"#);
1174        check_expr_compilation_error("1:15: Expected STRING but found INTEGER", r#"LEFT(1, 2)"#);
1175        check_expr_compilation_error("1:19: STRING is not a number", r#"LEFT("", "")"#);
1176        check_expr_error("1:25: n% cannot be negative", r#"LEFT("abcdef", -5)"#);
1177    }
1178
1179    #[test]
1180    fn test_len() {
1181        check_expr_ok(0, r#"LEN("")"#);
1182        check_expr_ok(1, r#"LEN(" ")"#);
1183        check_expr_ok(5, r#"LEN("abcde")"#);
1184        check_expr_ok(2, r#"LEN("한글")"#);
1185
1186        check_expr_ok_with_vars(4, r#"LEN(s)"#, [("s", "1234".into())]);
1187
1188        check_expr_compilation_error("1:10: LEN expected expr$", r#"LEN()"#);
1189        check_expr_compilation_error("1:14: Expected STRING but found INTEGER", r#"LEN(3)"#);
1190        check_expr_compilation_error("1:10: LEN expected expr$", r#"LEN(" ", 1)"#);
1191    }
1192
1193    #[test]
1194    fn test_ltrim() {
1195        check_expr_ok("", r#"LTRIM("")"#);
1196        check_expr_ok("", r#"LTRIM("  ")"#);
1197        check_expr_ok("", "LTRIM(\"\t\t\")");
1198        check_expr_ok("foo \t ", "LTRIM(\" \t foo \t \")");
1199
1200        check_expr_ok_with_vars("foo ", r#"LTRIM(s)"#, [("s", " foo ".into())]);
1201
1202        check_expr_compilation_error("1:10: LTRIM expected expr$", r#"LTRIM()"#);
1203        check_expr_compilation_error("1:16: Expected STRING but found INTEGER", r#"LTRIM(3)"#);
1204        check_expr_compilation_error("1:10: LTRIM expected expr$", r#"LTRIM(" ", 1)"#);
1205    }
1206
1207    #[test]
1208    fn test_mid() {
1209        check_expr_ok("", r#"MID("", 0, 0)"#);
1210        check_expr_ok("", r#"MID("basic", 0, 0)"#);
1211        check_expr_ok("", r#"MID("basic", 1, 0)"#);
1212        check_expr_ok("a", r#"MID("basic", 1, 1)"#);
1213        check_expr_ok("as", r#"MID("basic", 1, 2)"#);
1214        check_expr_ok("asic", r#"MID("basic", 1, 4)"#);
1215        check_expr_ok("asi", r#"MID("basic", 0.8, 3.2)"#);
1216        check_expr_ok("asic", r#"MID("basic", 1, 10)"#);
1217        check_expr_ok("asic", r#"MID("basic", 1)"#);
1218        check_expr_ok("", r#"MID("basic", 100, 10)"#);
1219        check_expr_ok("글", r#"MID("한글", 1, 1)"#);
1220        check_expr_ok("글", r#"MID("한글", 1)"#);
1221
1222        check_expr_ok_with_vars(
1223            "asic",
1224            r#"MID(s, i, j)"#,
1225            [("s", "basic".into()), ("i", 1.into()), ("j", 4.into())],
1226        );
1227
1228        check_expr_compilation_error(
1229            "1:10: MID expected <expr$, start%> | <expr$, start%, length%>",
1230            r#"MID()"#,
1231        );
1232        check_expr_compilation_error(
1233            "1:10: MID expected <expr$, start%> | <expr$, start%, length%>",
1234            r#"MID(3)"#,
1235        );
1236        check_expr_compilation_error(
1237            "1:10: MID expected <expr$, start%> | <expr$, start%, length%>",
1238            r#"MID(" ", 1, 1, 10)"#,
1239        );
1240        check_expr_compilation_error("1:19: STRING is not a number", r#"MID(" ", "1", 2)"#);
1241        check_expr_compilation_error("1:22: STRING is not a number", r#"MID(" ", 1, "2")"#);
1242        check_expr_error("1:24: start% cannot be negative", r#"MID("abcdef", -5, 10)"#);
1243        check_expr_error("1:27: length% cannot be negative", r#"MID("abcdef", 3, -5)"#);
1244    }
1245
1246    #[test]
1247    fn test_right() {
1248        check_expr_ok("", r#"RIGHT("", 0)"#);
1249        check_expr_ok("def", r#"RIGHT("abcdef", 3)"#);
1250        check_expr_ok("cdef", r#"RIGHT("abcdef", 4.2)"#);
1251        check_expr_ok("abcdef", r#"RIGHT("abcdef", 6)"#);
1252        check_expr_ok("abcdef", r#"RIGHT("abcdef", 10)"#);
1253        check_expr_ok("글", r#"RIGHT("한글", 1)"#);
1254
1255        check_expr_ok_with_vars("def", r#"RIGHT(s, i)"#, [("s", "abcdef".into()), ("i", 3.into())]);
1256
1257        check_expr_compilation_error("1:10: RIGHT expected expr$, n%", r#"RIGHT()"#);
1258        check_expr_compilation_error("1:10: RIGHT expected expr$, n%", r#"RIGHT("", 1, 2)"#);
1259        check_expr_compilation_error("1:16: Expected STRING but found INTEGER", r#"RIGHT(1, 2)"#);
1260        check_expr_compilation_error("1:20: STRING is not a number", r#"RIGHT("", "")"#);
1261        check_expr_error("1:26: n% cannot be negative", r#"RIGHT("abcdef", -5)"#);
1262    }
1263
1264    #[test]
1265    fn test_rtrim() {
1266        check_expr_ok("", r#"RTRIM("")"#);
1267        check_expr_ok("", r#"RTRIM("  ")"#);
1268        check_expr_ok("", "RTRIM(\"\t\t\")");
1269        check_expr_ok(" \t foo", "RTRIM(\" \t foo \t \")");
1270
1271        check_expr_ok_with_vars(" foo", r#"RTRIM(s)"#, [("s", " foo ".into())]);
1272
1273        check_expr_compilation_error("1:10: RTRIM expected expr$", r#"RTRIM()"#);
1274        check_expr_compilation_error("1:16: Expected STRING but found INTEGER", r#"RTRIM(3)"#);
1275        check_expr_compilation_error("1:10: RTRIM expected expr$", r#"RTRIM(" ", 1)"#);
1276    }
1277
1278    #[test]
1279    fn test_space() {
1280        check_expr_ok("", r#"SPACE(0)"#);
1281        check_expr_ok("   ", r#"SPACE(3)"#);
1282        check_expr_ok("     ", r#"SPACE(4.8)"#);
1283
1284        check_expr_ok_with_vars("  ", r#"SPACE(i)"#, [("i", 2.into())]);
1285
1286        check_expr_compilation_error("1:10: SPACE expected n%", r#"SPACE()"#);
1287        check_expr_compilation_error("1:16: BOOLEAN is not a number", r#"SPACE(FALSE)"#);
1288        check_expr_compilation_error("1:10: SPACE expected n%", r#"SPACE(1, 2)"#);
1289        check_expr_error("1:16: n% cannot be negative", r#"SPACE(-1)"#);
1290    }
1291
1292    #[test]
1293    fn test_str() {
1294        check_expr_ok("FALSE", r#"STR(FALSE)"#);
1295        check_expr_ok("TRUE", r#"STR(true)"#);
1296
1297        check_expr_ok(" 0", r#"STR(0)"#);
1298        check_expr_ok(" 1", r#"STR(1)"#);
1299        check_expr_ok("-1", r#"STR(-1)"#);
1300
1301        check_expr_ok(" 0.5", r#"STR(0.5)"#);
1302        check_expr_ok(" 1.5", r#"STR(1.5)"#);
1303        check_expr_ok("-1.5", r#"STR(-1.5)"#);
1304
1305        check_expr_ok("", r#"STR("")"#);
1306        check_expr_ok(" \t ", "STR(\" \t \")");
1307        check_expr_ok("foo bar", r#"STR("foo bar")"#);
1308
1309        check_expr_ok_with_vars(" 1", r#"STR(i)"#, [("i", 1.into())]);
1310
1311        check_expr_compilation_error("1:10: STR expected expr", r#"STR()"#);
1312        check_expr_compilation_error("1:10: STR expected expr", r#"STR(" ", 1)"#);
1313    }
1314
1315    #[test]
1316    fn test_str_with_ltrim() {
1317        check_expr_ok("0", r#"LTRIM(STR(0))"#);
1318        check_expr_ok("-1", r#"LTRIM(STR(-1))"#);
1319        check_expr_ok("100", r#"LTRIM$(STR$(100))"#);
1320    }
1321
1322    #[test]
1323    fn test_trim() {
1324        check_expr_ok("", r#"TRIM("")"#);
1325        check_expr_ok("", r#"TRIM("  ")"#);
1326        check_expr_ok("foo", "TRIM(\"\t foo \t\")");
1327
1328        check_expr_ok_with_vars("foo", r#"TRIM(s)"#, [("s", " foo ".into())]);
1329
1330        check_expr_compilation_error("1:10: TRIM expected expr$", r#"TRIM()"#);
1331        check_expr_compilation_error("1:15: Expected STRING but found INTEGER", r#"TRIM(3)"#);
1332        check_expr_compilation_error("1:10: TRIM expected expr$", r#"TRIM(" ", 1)"#);
1333    }
1334
1335    #[test]
1336    fn test_ucase() {
1337        check_expr_ok("", r#"UCASE("")"#);
1338        check_expr_ok("BASIC", r#"UCASE("BaSiC")"#);
1339        check_expr_ok("AOS", r#"UCASE("aos")"#);
1340
1341        check_expr_ok_with_vars("HELLO", r#"UCASE(s)"#, [("s", "hello".into())]);
1342
1343        check_expr_compilation_error("1:10: UCASE expected expr$", r#"UCASE()"#);
1344        check_expr_compilation_error("1:16: Expected STRING but found INTEGER", r#"UCASE(3)"#);
1345        check_expr_compilation_error("1:10: UCASE expected expr$", r#"UCASE(" ", 1)"#);
1346    }
1347
1348    #[test]
1349    fn test_val() {
1350        check_expr_ok(0.0, r#"VAL("")"#);
1351        check_expr_ok(0.0, r#"VAL("foo")"#);
1352        check_expr_ok(10.0, r#"VAL("10")"#);
1353        check_expr_ok(-21.5, r#"VAL("  -21.5xyz")"#);
1354        check_expr_ok(0.01, r#"VAL(".01")"#);
1355        check_expr_ok(1200.0, r#"VAL("12e2z")"#);
1356        check_expr_ok(12.0, r#"VAL("12e")"#);
1357
1358        check_expr_ok_with_vars(3.5, r#"VAL(s)"#, [("s", "3.5ms".into())]);
1359
1360        check_expr_compilation_error("1:10: VAL expected expr$", r#"VAL()"#);
1361        check_expr_compilation_error("1:14: Expected STRING but found INTEGER", r#"VAL(3)"#);
1362        check_expr_compilation_error("1:10: VAL expected expr$", r#"VAL(" ", 1)"#);
1363    }
1364}