1use std::any::type_name;
2use std::fmt::Display;
3use std::str::FromStr;
4
5use genawaiter::rc::Gen;
6
7use crate::parse::parse_error::ParseError;
8
9use super::parse_error::{Backing, IntoOwningBacking, StringParseContext};
10
11#[derive(Debug, Copy, PartialEq, Eq)]
12pub struct ParseableStr<'t, B: Backing> {
13 pub position: usize,
14 pub backing: &'t B,
15 pub s: &'t str,
16}
17
18impl<'t, B: Backing> Clone for ParseableStr<'t, B> {
19 fn clone(&self) -> Self {
20 let ParseableStr {
21 position,
22 backing,
23 s,
24 } = self;
25 Self {
26 position: *position,
27 backing: *backing,
28 s,
29 }
30 }
31}
32
33impl<'t, B: Backing> ParseableStr<'t, B> {
34 pub fn new(backing: &'t B) -> Self {
35 Self {
36 position: 0,
37 backing,
38 s: backing.as_ref(),
39 }
40 }
41
42 pub fn position_difference(&self, to: &Self) -> usize {
44 to.position
45 .checked_sub(self.position)
46 .expect("to is after self")
47 }
48
49 pub fn up_to(&self, to: &Self) -> Self {
51 let len = self.position_difference(to);
52 Self {
53 position: self.position,
54 backing: self.backing,
55 s: &self.s[0..len],
56 }
57 }
58
59 pub fn eos(&self) -> ParseableStr<'t, B> {
61 ParseableStr {
62 position: self.position + self.s.len(),
63 backing: self.backing,
64 s: "",
65 }
66 }
67
68 pub fn is_empty(&self) -> bool {
71 self.s.is_empty()
72 }
73
74 pub fn len(&self) -> usize {
75 self.s.len()
76 }
77
78 pub fn first(&self) -> Option<char> {
79 self.s.chars().next()
80 }
81
82 pub fn starts_with(&self, s: &str) -> bool {
84 self.s.starts_with(s)
85 }
86}
87
88impl<'s, B: Backing> IntoOwningBacking<B::Owned> for ParseableStr<'s, B>
89where
90 &'s B: Backing,
91 B::Owned: Backing,
92{
93 type Owning = StringParseContext<B::Owned>;
94
95 fn into_owning_backing(self) -> Self::Owning {
96 StringParseContext {
97 position: self.position,
98 backing: self.backing.to_owned(),
99 }
100 }
101}
102
103impl<'s, B: Backing> From<ParseableStr<'s, B>> for StringParseContext<&'s B>
107where
108 &'s B: Backing,
109{
110 fn from(value: ParseableStr<'s, B>) -> Self {
111 let ParseableStr {
112 position,
113 backing,
114 s: _,
115 } = value;
116 StringParseContext { position, backing }
117 }
118}
119
120impl<'r, 's, B: Backing> From<&'r ParseableStr<'s, B>>
121 for StringParseContext<&'s B>
122where
123 &'s B: Backing,
124{
125 fn from(value: &'r ParseableStr<'s, B>) -> Self {
126 let ParseableStr {
127 position,
128 backing,
129 s: _,
130 } = *value;
131 StringParseContext { position, backing }
132 }
133}
134
135#[derive(Debug, thiserror::Error)]
136#[error("expected {:?}", self.needle)]
137pub struct ExpectedString<'t, B: Backing> {
138 pub needle: &'t str,
139 pub context: ParseableStr<'t, B>,
140}
141
142#[derive(Debug, PartialEq, thiserror::Error)]
143#[error("expected {}", self.desc)]
144pub struct Expected<'t, B: Backing> {
145 pub desc: &'t str,
146 pub context: ParseableStr<'t, B>,
147}
148
149#[derive(Debug, PartialEq, thiserror::Error)]
150#[error("cannot parse as {type_}: {error}")]
151pub struct FromStrError {
152 pub type_: &'static str,
155 pub error: String,
156 pub position: usize,
157}
158
159impl<'t, B: Backing> From<Box<ExpectedString<'t, B>>>
160 for ParseError<StringParseContext<&'t B>>
161where
162 &'t B: Backing,
163{
164 fn from(e: Box<ExpectedString<'t, B>>) -> Self {
165 ParseError {
168 message: e.to_string(),
169 context: e.context.into(),
170 backtrace: Vec::new(),
171 }
172 }
173}
174
175impl<'t, B: Backing> From<Box<Expected<'t, B>>>
176 for ParseError<StringParseContext<&'t B>>
177where
178 &'t B: Backing,
179{
180 fn from(e: Box<Expected<'t, B>>) -> Self {
181 ParseError {
184 message: e.to_string(),
185 context: e.context.into(),
186 backtrace: Vec::new(),
187 }
188 }
189}
190
191#[derive(Debug, thiserror::Error)]
192pub enum ParseFailure {
193 #[error("unexpected end of input")]
194 Eos,
195 #[error("invalid character")]
196 InvalidCharacter,
197}
198impl<'t, B: Backing> From<&'t B> for ParseableStr<'t, B> {
204 fn from(s: &'t B) -> Self {
205 ParseableStr {
206 position: 0,
207 backing: s,
208 s: s.as_ref(),
209 }
210 }
211}
212
213pub trait FromParseableStr<'s, B: Backing + 's>: Sized
214where
215 &'s B: Backing,
216{
217 fn from_parseable_str(
218 s: &ParseableStr<'s, B>,
219 ) -> Result<Self, ParseError<StringParseContext<&'s B>>>;
220}
221
222#[derive(Debug, Clone)]
223pub struct Separator {
224 pub required: bool,
227 pub alternatives: &'static [&'static str],
228}
229
230impl<'t, B: Backing> ParseableStr<'t, B> {
231 pub fn parse<T: FromStr>(&self) -> Result<T, Box<FromStrError>>
233 where
234 T::Err: Display,
235 {
236 self.s.parse().map_err(|e: T::Err| {
237 FromStrError {
238 type_: type_name::<T>(),
239 error: e.to_string(),
240 position: self.position,
241 }
242 .into()
243 })
244 }
245
246 pub fn opt_parse<T: FromStr>(&self) -> Option<T> {
249 self.s.parse().ok()
250 }
251
252 pub fn skip_bytes(&self, num_bytes: usize) -> ParseableStr<'t, B> {
256 let ParseableStr {
257 position,
258 backing,
259 s,
260 } = self;
261 Self {
262 position: position + num_bytes,
263 backing,
264 s: &s[num_bytes..],
265 }
266 }
267
268 pub fn split_at(
271 &self,
272 mid: usize,
273 ) -> (ParseableStr<'t, B>, ParseableStr<'t, B>) {
274 let ParseableStr {
275 position,
276 backing,
277 s,
278 } = *self;
279 (
280 Self {
281 position,
282 backing,
283 s: &s[..mid],
284 },
285 Self {
286 position: position + mid,
287 backing,
288 s: &s[mid..],
289 },
290 )
291 }
292
293 pub fn trim(&self) -> ParseableStr<'t, B> {
295 let s1 = self.s.trim_start();
296 Self {
297 s: s1.trim_end(),
298 backing: self.backing,
299 position: self.position + (self.len() - s1.len()),
300 }
301 }
302
303 pub fn trim_start(&self) -> ParseableStr<'t, B> {
305 let s1 = self.s.trim_start();
306 Self {
307 s: s1,
308 backing: self.backing,
309 position: self.position + (self.len() - s1.len()),
310 }
311 }
312
313 pub fn trim_end(&self) -> ParseableStr<'t, B> {
315 Self {
316 s: self.s.trim_end(),
317 backing: self.backing,
318 position: self.position,
319 }
320 }
321
322 pub fn find_str(&self, needle: &str) -> Option<ParseableStr<'t, B>> {
325 let ParseableStr {
326 position,
327 backing,
328 s,
329 } = self;
330 let offset = s.find(needle)?;
331 Some(ParseableStr {
332 position: position + offset,
333 backing,
334 s: &s[offset..],
335 })
336 }
337
338 pub fn find_str_rest(
342 &self,
343 needle: &str,
344 ) -> Option<(ParseableStr<'t, B>, ParseableStr<'t, B>)> {
345 let ParseableStr {
346 position,
347 backing,
348 s,
349 } = self;
350 let offset = s.find(needle)?;
351 Some((
352 ParseableStr {
353 position: position + offset,
354 backing,
355 s: &s[offset..offset + needle.len()],
356 },
357 ParseableStr {
358 position: position + offset + needle.len(),
359 backing,
360 s: &s[offset + needle.len()..],
361 },
362 ))
363 }
364
365 pub fn after_str(&self, needle: &str) -> Option<ParseableStr<'t, B>> {
368 let ParseableStr {
369 position,
370 backing,
371 s,
372 } = self;
373 let pos = s.find(needle)?;
374 let offset = pos + needle.len();
375 Some(ParseableStr {
376 position: position + offset,
377 backing,
378 s: &s[offset..],
379 })
380 }
381
382 pub fn drop_str(&self, beginning: &str) -> Option<ParseableStr<'t, B>> {
385 let ParseableStr {
386 position,
387 backing,
388 s,
389 } = *self;
390 if s.starts_with(beginning) {
391 Some(ParseableStr {
392 position: position + beginning.len(),
393 backing,
394 s: &s[beginning.len()..],
395 })
396 } else {
397 None
398 }
399 }
400
401 pub fn expect_str(
404 &self,
405 beginning: &'t str,
406 ) -> Result<ParseableStr<'t, B>, Box<ExpectedString<'t, B>>> {
407 let ParseableStr {
408 position,
409 backing,
410 s,
411 } = *self;
412 self.drop_str(beginning).ok_or_else(|| {
413 ExpectedString {
414 needle: beginning,
415 context: ParseableStr {
416 position,
417 backing,
418 s,
419 },
420 }
421 .into()
422 })
423 }
424
425 pub fn expect_str_or_eos(
426 &self,
427 beginning: &'t str,
428 ) -> Result<ParseableStr<'t, B>, Box<ExpectedString<'t, B>>> {
429 if self.is_empty() {
430 return Ok(self.clone());
431 }
432 self.expect_str(beginning)
433 }
434
435 pub fn expect_separator(
439 &self,
440 separator: &Separator,
441 ) -> Result<ParseableStr<'t, B>, Box<ExpectedString<'t, B>>> {
442 let mut last_e = None;
443 for &sep in separator.alternatives {
444 match self.expect_str(sep) {
445 Ok(s) => return Ok(s),
446 Err(e) => last_e = Some(e),
447 }
448 }
449 if separator.required {
450 if let Some(last_e) = last_e {
451 Err(last_e)
453 } else {
454 Ok(self.clone())
455 }
456 } else {
457 Ok(self.clone())
458 }
459 }
460
461 pub fn take_until_str(&self, needle: &str) -> Option<ParseableStr<'t, B>> {
464 let ParseableStr {
465 position,
466 backing,
467 s,
468 } = *self;
469 let pos = s.find(needle)?;
470 Some(ParseableStr {
471 position,
472 backing,
473 s: &s[0..pos],
474 })
475 }
476
477 pub fn split_at_str(
480 &self,
481 needle: &str,
482 ) -> Option<(ParseableStr<'t, B>, ParseableStr<'t, B>)> {
483 let ParseableStr {
484 position,
485 backing,
486 s,
487 } = *self;
488 let pos = s.find(needle)?;
489 Some((
490 ParseableStr {
491 position,
492 backing,
493 s: &s[0..pos],
494 },
495 ParseableStr {
496 position: position + pos + needle.len(),
497 backing,
498 s: &s[pos + needle.len()..],
499 },
500 ))
501 }
502
503 pub fn take_while(
506 &self,
507 mut pred: impl FnMut(char) -> bool,
508 ) -> (ParseableStr<'t, B>, ParseableStr<'t, B>) {
509 let ParseableStr {
510 position,
511 backing,
512 s,
513 } = *self;
514 for (pos, c) in s.char_indices() {
515 if !pred(c) {
516 return (
517 ParseableStr {
518 position,
519 backing,
520 s: &s[0..pos],
521 },
522 ParseableStr {
523 position: position + pos,
524 backing,
525 s: &s[pos..],
526 },
527 );
528 }
529 }
530 (
531 self.clone(),
532 ParseableStr {
533 s: "",
534 position: position + s.len(),
535 backing,
536 },
537 )
538 }
539
540 pub fn expect1_matching(
543 &self,
544 pred: impl FnOnce(char) -> bool,
545 desc: &'t str,
546 ) -> Result<ParseableStr<'t, B>, Box<Expected<'t, B>>> {
547 let ParseableStr {
548 position,
549 backing,
550 s,
551 } = self;
552 let err = || {
553 Err(Expected {
554 desc,
555 context: self.clone(),
556 }
557 .into())
558 };
559 if s.is_empty() {
560 return err();
561 }
562 let mut cs = s.char_indices();
563 let (_, c) = cs.next().unwrap();
564 if !pred(c) {
565 return err();
566 }
567 let (pos, _) = cs.next().unwrap_or_else(|| (s.len(), ' '));
568 Ok(ParseableStr {
569 position: position + pos,
570 backing,
571 s: &s[pos..],
572 })
573 }
574
575 pub fn take_identifier(
576 &self,
577 ) -> Result<(ParseableStr<'t, B>, ParseableStr<'t, B>), Box<Expected<'t, B>>>
578 {
579 let rest = self.expect1_matching(
580 |c| c.is_ascii_lowercase() && (c.is_ascii_alphabetic() || c == '_'),
581 "[a-z_] followed by [a-z0-9_]*",
582 )?;
583 let rest = rest.drop_while(|c| {
584 c.is_ascii_lowercase() && (c.is_ascii_alphanumeric() || c == '_')
585 });
586 let ParseableStr {
587 position,
588 backing,
589 s,
590 } = *self;
591 Ok((
592 ParseableStr {
593 position,
594 backing,
595 s: &s[0..(rest.position - position)],
596 },
597 rest,
598 ))
599 }
600
601 pub fn drop_while(
602 &self,
603 mut pred: impl FnMut(char) -> bool,
604 ) -> ParseableStr<'t, B> {
605 let ParseableStr {
606 position,
607 backing,
608 s,
609 } = self;
610 for (pos, c) in s.char_indices() {
611 if !pred(c) {
612 return ParseableStr {
613 position: position + pos,
614 backing,
615 s: &s[pos..],
616 };
617 }
618 }
619 ParseableStr {
620 position: position + s.len(),
621 backing,
622 s: "",
623 }
624 }
625
626 pub fn take_n_while(
630 &self,
631 n: usize,
632 pred: impl FnMut(char) -> bool,
633 desc: &'t str,
634 ) -> Result<(ParseableStr<'t, B>, ParseableStr<'t, B>), Box<Expected<'t, B>>>
635 {
636 self.take_nrange_while(n, n, pred, desc)
637 }
638
639 pub fn take_nrange_while(
644 &self,
645 n_min: usize,
646 n_max: usize, mut pred: impl FnMut(char) -> bool,
648 desc: &'t str,
649 ) -> Result<(ParseableStr<'t, B>, ParseableStr<'t, B>), Box<Expected<'t, B>>>
650 {
651 let ParseableStr {
652 position,
653 backing,
654 s,
655 } = *self;
656 let mut cs = s.char_indices();
657 let mut failed_pos = None;
658 for i in 0..n_max {
659 if let Some((pos, c)) = cs.next() {
660 if !pred(c) {
661 if i < n_min {
662 return Err(Expected {
668 desc,
669 context: ParseableStr {
670 s,
671 backing,
672 position: position + pos,
673 },
674 }
675 .into());
676 }
677 failed_pos = Some(pos);
678 break;
679 }
680 } else {
681 if i >= n_min {
683 failed_pos = Some(s.len());
684 break;
685 } else {
686 return Err(Expected {
687 desc,
688 context: ParseableStr {
689 s,
690 backing,
691 position: position + s.len(),
692 },
693 }
694 .into());
695 }
696 }
697 }
698 let pos;
699 if let Some(failed_pos) = failed_pos {
700 pos = failed_pos;
701 } else {
702 (pos, _) = cs.next().unwrap_or_else(|| (s.len(), ' '));
703 }
704 Ok((
705 ParseableStr {
706 position,
707 backing,
708 s: &s[0..pos],
709 },
710 ParseableStr {
711 position: position + pos,
712 backing,
713 s: &s[pos..],
714 },
715 ))
716 }
717
718 pub fn drop_whitespace(&self) -> ParseableStr<'t, B> {
719 self.drop_while(|c| c.is_whitespace())
720 }
721
722 pub fn split_str<'r, 'n>(
727 &'r self,
728 separator: &'n str,
729 omit_empty_last_item: bool,
730 ) -> Box<dyn Iterator<Item = ParseableStr<'t, B>> + 'n>
731 where
732 't: 'n,
733 'r: 'n,
734 {
735 Box::new(
736 Gen::new(|co| async move {
737 let mut rest = self.clone();
738 loop {
739 if omit_empty_last_item && rest.is_empty() {
740 break;
741 }
742 if let Some(p2) = rest.find_str(separator) {
743 co.yield_(rest.up_to(&p2)).await;
744 rest = p2.skip_bytes(separator.len());
745 } else {
746 co.yield_(rest).await;
747 break;
748 }
749 }
750 })
751 .into_iter(),
752 )
753 }
754}
755
756#[cfg(test)]
774mod tests {
775 use super::*;
776
777 macro_rules! let_parseable {
780 { $backingvar:ident, $var:ident = $backing:expr } => {
781 let $backingvar: String = $backing.into();
782 let $var = ParseableStr::from(&$backingvar);
783 }
784 }
785 macro_rules! new {
786 { $backingvar:ident, $pos:expr, $string:expr } => {
787 ParseableStr {
788 position: $pos,
789 backing: &$backingvar,
790 s: $string
791 }
792 }
793 }
794
795 #[test]
796 fn t_split_at_str() {
797 let_parseable! { backing, s = "foo -- bar" }
798 assert_eq!(s.split_at_str("---"), None);
799
800 let_parseable! { backing, s = "foo -- bar" }
801 assert_eq!(
802 s.split_at_str("--"),
803 Some((
804 ParseableStr {
805 s: "foo ",
806 backing: &backing,
807 position: 0
808 },
809 ParseableStr {
810 s: " bar",
811 backing: &backing,
812 position: 6
813 },
814 ))
815 );
816 }
817
818 #[test]
819 fn t_split_str() {
820 {
821 let_parseable! { backing, s = "foo - bar- baz - bam" }
822 let r1: Vec<ParseableStr<_>> = s.split_str(" - ", false).collect();
823 assert_eq!(
824 &*r1,
825 &[
826 new!(backing, 0, "foo"),
827 new!(backing, 6, "bar- baz"),
828 new!(backing, 17, " bam")
829 ]
830 );
831 }
832 {
833 let_parseable! { backing, s = "foo - bar- baz - " }
834 let r1: Vec<ParseableStr<_>> = s.split_str(" - ", false).collect();
835 assert_eq!(
836 &*r1,
837 &[
838 new!(backing, 0, "foo"),
839 new!(backing, 6, "bar- baz"),
840 new!(backing, 17, "")
841 ]
842 );
843 }
844 {
845 let_parseable! { backing, s = "foo - bar- baz - " }
846 let r1: Vec<ParseableStr<_>> = s.split_str(" - ", true).collect();
847 assert_eq!(
848 &*r1,
849 &[new!(backing, 0, "foo"), new!(backing, 6, "bar- baz"),]
850 );
851 }
852 {
853 let_parseable! { backing, s = " - " }
854 let r1: Vec<ParseableStr<_>> = s.split_str(" - ", false).collect();
855 assert_eq!(&*r1, &[new!(backing, 0, ""), new!(backing, 3, ""),]);
856 }
857 {
858 let_parseable! { backing, s = " - " }
859 let r1: Vec<ParseableStr<_>> = s.split_str(" - ", true).collect();
860 assert_eq!(&*r1, &[new!(backing, 0, ""),]);
861 }
862 {
863 let_parseable! { backing, s = "" }
864 let r1: Vec<ParseableStr<_>> = s.split_str(" - ", false).collect();
865 assert_eq!(&*r1, &[new!(backing, 0, ""),]);
866 }
867 {
868 let_parseable! { backing, s = "" }
869 let r1: Vec<ParseableStr<_>> = s.split_str(" - ", true).collect();
870 assert_eq!(&*r1, &[]);
871 }
872 }
873
874 #[test]
875 fn t_take_while() {
876 let_parseable! { backing, s = "" }
877 assert_eq!(
878 s.take_while(|c| c.is_alphanumeric()),
879 (new!(backing, 0, ""), new!(backing, 0, ""),)
880 );
881
882 let_parseable! { backing, s = " abc def" }
883 assert_eq!(
884 s.take_while(|c| c.is_alphanumeric()),
885 (new!(backing, 0, ""), new!(backing, 0, " abc def"),)
886 );
887
888 let_parseable! { backing, s = "abc def" }
889 assert_eq!(
890 s.take_while(|c| c.is_alphanumeric()),
891 (new!(backing, 0, "abc"), new!(backing, 3, " def"),)
892 );
893
894 let_parseable! { backing, s = "abcdef" }
895 assert_eq!(
896 s.take_while(|c| c.is_alphanumeric()),
897 (new!(backing, 0, "abcdef"), new!(backing, 6, ""),)
898 );
899 }
900
901 #[test]
902 fn t_take_n_while() {
903 fn t<'s, 't>(
904 s: &'s ParseableStr<'t, String>,
905 n: usize,
906 ) -> Result<
907 (ParseableStr<'t, String>, ParseableStr<'t, String>),
908 Box<Expected<'t, String>>,
909 > {
910 s.take_n_while(
911 n,
912 |c| c.is_ascii_digit(),
913 "digit as part of year number",
914 )
915 }
916
917 fn ok<'s, 't>(
918 backing: &'t String,
919 s0: &'t str,
920 position: usize,
921 s1: &'t str,
922 ) -> Result<
923 (ParseableStr<'t, String>, ParseableStr<'t, String>),
924 Box<Expected<'t, String>>,
925 > {
926 Ok((
927 ParseableStr {
928 position: 0,
929 backing,
930 s: s0,
931 },
932 ParseableStr {
933 position,
934 backing,
935 s: s1,
936 },
937 ))
938 }
939
940 let err = |backing, position, desc, s| {
941 Err(Box::new(Expected {
942 desc,
943 context: ParseableStr {
944 position,
945 backing,
946 s,
947 },
948 }))
949 };
950
951 macro_rules! test {
952 { t($sourcestr:expr, $n:expr), ok($matchstr:expr, $matchpos:expr, $reststr:expr) } => {
953 let_parseable!{ backing, s = $sourcestr }
954 assert_eq!(t(&s, $n), ok(&backing, $matchstr, $matchpos, $reststr));
955 };
956 { t($sourcestr:expr, $n:expr), err($pos:expr, $msg:expr, $matchstr:expr) } => {
957 let_parseable!{ backing, s = $sourcestr }
958 assert_eq!(t(&s, $n), err(&backing, $pos, $msg, $matchstr));
959 }
960 }
961
962 test!(t("2024-10", 4), ok("2024", 4, "-10"));
963 test!(t("2024-10", 3), ok("202", 3, "4-10"));
964 test!(t("2024-10", 0), ok("", 0, "2024-10"));
965 test!(
967 t("2024-10", 5),
968 err(4, "digit as part of year number", "2024-10")
969 );
970 test!(t("2024", 5), err(4, "digit as part of year number", "2024"));
971 test!(t("2024", 4), ok("2024", 4, ""));
972 }
973
974 #[test]
975 fn t_take_nrange_while() {
976 fn t<'s, 't>(
977 s: &ParseableStr<'t, String>,
978 n_min: usize,
979 n_max: usize,
980 ) -> Result<
981 (ParseableStr<'t, String>, ParseableStr<'t, String>),
982 Box<Expected<'t, String>>,
983 > {
984 s.take_nrange_while(
985 n_min,
986 n_max,
987 |c| c.is_ascii_digit(),
988 "digit as part of year number",
989 )
990 }
991
992 fn ok<'s, 't>(
993 backing: &'t String,
994 s0: &'t str,
995 position: usize,
996 s1: &'t str,
997 ) -> Result<
998 (ParseableStr<'t, String>, ParseableStr<'t, String>),
999 Box<Expected<'t, String>>,
1000 > {
1001 Ok((
1002 ParseableStr {
1003 position: 0,
1004 backing,
1005 s: s0,
1006 },
1007 ParseableStr {
1008 position,
1009 backing,
1010 s: s1,
1011 },
1012 ))
1013 }
1014
1015 let err = |backing, position, desc, s| {
1016 Err(Box::new(Expected {
1017 desc,
1018 context: ParseableStr {
1019 position,
1020 backing,
1021 s,
1022 },
1023 }))
1024 };
1025
1026 macro_rules! test {
1027 { t($sourcestr:expr, $n_min:expr, $n_max:expr), ok($matchstr:expr, $matchpos:expr, $reststr:expr) } => {
1028 let_parseable!{ backing, s = $sourcestr }
1029 assert_eq!(t(&s, $n_min, $n_max), ok(&backing, $matchstr, $matchpos, $reststr));
1030 };
1031 { t($sourcestr:expr, $n_min:expr, $n_max:expr), err($pos:expr, $msg:expr, $matchstr:expr) } => {
1032 let_parseable!{ backing, s = $sourcestr }
1033 assert_eq!(t(&s, $n_min, $n_max), err(&backing, $pos, $msg, $matchstr));
1034 }
1035 }
1036
1037 test!(t("2024-10", 3, 4), ok("2024", 4, "-10"));
1038 test!(t("2024-10", 1, 3), ok("202", 3, "4-10"));
1039 test!(t("2024-10", 8, 3), ok("202", 3, "4-10"));
1040 test!(t("2024-10", 3, 0), ok("", 0, "2024-10"));
1041 test!(t("2024-10", 4, 5), ok("2024", 4, "-10"));
1042 test!(
1044 t("2024-10 abc", 5, 6),
1045 err(4, "digit as part of year number", "2024-10 abc")
1046 );
1047 test!(t("2024", 3, 4), ok("2024", 4, ""));
1048 test!(t("2024x", 4, 5), ok("2024", 4, "x"));
1049 test!(t("2024", 4, 5), ok("2024", 4, ""));
1050 }
1051}