1use 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
30const CATEGORY: &str = "String and character functions";
32
33pub fn format_boolean(b: bool) -> &'static str {
35 if b { "TRUE" } else { "FALSE" }
36}
37
38pub fn parse_boolean(s: &str) -> Result<bool, String> {
40 let raw = s.to_uppercase();
41 if raw == "TRUE" || raw == "YES" || raw == "Y" {
42 Ok(true)
43 } else if raw == "FALSE" || raw == "NO" || raw == "N" {
44 Ok(false)
45 } else {
46 Err(format!("Invalid boolean literal {}", s))
47 }
48}
49
50pub fn format_double(d: f64) -> String {
52 if !d.is_nan() && d.is_sign_negative() { d.to_string() } else { format!(" {}", d) }
53}
54
55pub fn parse_double(s: &str) -> Result<f64, String> {
57 match s.parse::<f64>() {
58 Ok(d) => Ok(d),
59 Err(_) => Err(format!("Invalid double-precision floating point literal {}", s)),
60 }
61}
62
63pub fn format_integer(i: i32) -> String {
65 if i.is_negative() { i.to_string() } else { format!(" {}", i) }
66}
67
68pub fn parse_integer(s: &str) -> Result<i32, String> {
70 match s.parse::<i32>() {
71 Ok(d) => Ok(d),
72 Err(_) => Err(format!("Invalid integer literal {}", s)),
73 }
74}
75
76fn 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
81fn byte_index_to_char_index(s: &str, byte_index: usize) -> usize {
83 s[..byte_index].chars().count()
84}
85
86fn char_len(s: &str) -> usize {
88 s.chars().count()
89}
90
91fn 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
98fn 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
145pub struct AscFunction {
147 metadata: Rc<CallableMetadata>,
148}
149
150impl AscFunction {
151 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
211pub struct ChrFunction {
213 metadata: Rc<CallableMetadata>,
214}
215
216impl ChrFunction {
217 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
268pub struct InstrFunction {
270 metadata: Rc<CallableMetadata>,
271}
272
273impl InstrFunction {
274 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
375pub struct LcaseFunction {
377 metadata: Rc<CallableMetadata>,
378}
379
380impl LcaseFunction {
381 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
415pub struct LeftFunction {
417 metadata: Rc<CallableMetadata>,
418}
419
420impl LeftFunction {
421 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
475pub struct LenFunction {
477 metadata: Rc<CallableMetadata>,
478}
479
480impl LenFunction {
481 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
515pub struct LtrimFunction {
517 metadata: Rc<CallableMetadata>,
518}
519
520impl LtrimFunction {
521 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
553pub struct MidFunction {
555 metadata: Rc<CallableMetadata>,
556}
557
558impl MidFunction {
559 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
659pub struct RightFunction {
661 metadata: Rc<CallableMetadata>,
662}
663
664impl RightFunction {
665 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
720pub struct RtrimFunction {
722 metadata: Rc<CallableMetadata>,
723}
724
725impl RtrimFunction {
726 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
758pub struct SpaceFunction {
760 metadata: Rc<CallableMetadata>,
761}
762
763impl SpaceFunction {
764 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
803pub struct StrFunction {
805 metadata: Rc<CallableMetadata>,
806}
807
808impl StrFunction {
809 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
870pub struct TrimFunction {
872 metadata: Rc<CallableMetadata>,
873}
874
875impl TrimFunction {
876 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
910pub struct UcaseFunction {
912 metadata: Rc<CallableMetadata>,
913}
914
915impl UcaseFunction {
916 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
950pub struct ValFunction {
952 metadata: Rc<CallableMetadata>,
953}
954
955impl ValFunction {
956 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
994pub 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}