Skip to main content

openpql_range_parser/ast/
span.rs

1use super::{
2    Error, LalrError, Loc, RangeCard, RankConst, RankInt, ResultE, SuitConst, TermElem, ToString,
3};
4
5/// One card in a span endpoint.
6#[derive(Copy, Clone, PartialEq, Eq, Debug, derive_more::Display)]
7pub enum SpanElem {
8    /// Concrete rank with concrete suit.
9    #[display("{_0}{_1}")]
10    CC(RankConst, SuitConst),
11    /// Concrete rank with any suit.
12    #[display("{_0}")]
13    CA(RankConst),
14}
15
16impl SpanElem {
17    /// Returns the rank.
18    pub const fn rank(self) -> RankConst {
19        match self {
20            Self::CC(r, _) | Self::CA(r) => r,
21        }
22    }
23
24    /// Returns the suit, or `None` when unspecified.
25    pub const fn suit(self) -> Option<SuitConst> {
26        match self {
27            Self::CC(_, s) => Some(s),
28            Self::CA(_) => None,
29        }
30    }
31}
32
33impl TryFrom<(Loc, Loc, RangeCard)> for SpanElem {
34    type Error = LalrError<'static>;
35
36    fn try_from((l, r, c): (Loc, Loc, RangeCard)) -> Result<Self, Self::Error> {
37        match c {
38            RangeCard::CC(r, s) => Ok(Self::CC(r, s)),
39            RangeCard::CA(r) => Ok(Self::CA(r)),
40            _ => Err(Error::InvalidSpan((l, r)).into()),
41        }
42    }
43}
44
45impl TryFrom<(Loc, Loc, TermElem)> for SpanElem {
46    type Error = LalrError<'static>;
47
48    fn try_from((l, r, e): (Loc, Loc, TermElem)) -> Result<Self, Self::Error> {
49        match e {
50            TermElem::Card(c) => Self::try_from((l, r, c)),
51            _ => Err(Error::InvalidSpan((l, r)).into()),
52        }
53    }
54}
55
56/// Rank span across one or two endpoints.
57#[derive(Clone, PartialEq, Eq, Debug, derive_more::Display)]
58pub enum Span {
59    /// Open-ended downward span like `AK-`.
60    #[display("{}-", to_str(_0))]
61    Down(Vec<SpanElem>),
62    /// Open-ended upward span like `AK+`.
63    #[display("{}+", to_str(_0))]
64    Up(Vec<SpanElem>),
65    /// Bounded span like `AK-QJ` from top to bottom.
66    #[display("{}-{}", to_str(_0), to_str(_1))]
67    To(Vec<SpanElem>, Vec<SpanElem>),
68}
69
70fn to_str(elems: &[SpanElem]) -> String {
71    elems.iter().map(ToString::to_string).collect::<String>()
72}
73
74#[inline]
75fn to_span_elems<'i, T>(l: Loc, r: Loc, cs: Vec<T>) -> ResultE<'i, Vec<SpanElem>>
76where
77    SpanElem: TryFrom<(Loc, Loc, T), Error = LalrError<'i>>,
78{
79    cs.into_iter()
80        .map(|c| SpanElem::try_from((l, r, c)))
81        .collect()
82}
83
84#[inline]
85fn ensure_same_format<'i>(l: Loc, r: Loc, v1: &[SpanElem], v2: &[SpanElem]) -> ResultE<'i, ()> {
86    #[inline]
87    const fn is_same_distance(v1: &[SpanElem], v2: &[SpanElem], i: usize, j: usize) -> bool {
88        v1[j].rank() as RankInt - v1[i].rank() as RankInt
89            == v2[j].rank() as RankInt - v2[i].rank() as RankInt
90    }
91
92    let len = v1.len();
93
94    if v2.len() != len {
95        return Err(Error::NumberOfRanksMismatchInSpan((l, r)).into());
96    }
97
98    for i in 0..len {
99        if i < len - 1 && !is_same_distance(v1, v2, i, i + 1) {
100            return Err(Error::RankDistanceMismatchInSpan((l, r)).into());
101        }
102        if v1[i].suit() != v2[i].suit() {
103            return Err(Error::SuitMismatchInSpan((l, r)).into());
104        }
105    }
106
107    Ok(())
108}
109
110impl<'i> Span {
111    #[allow(clippy::needless_pass_by_value)]
112    pub(crate) fn spandown<T>(l: Loc, cs: Vec<T>, r: Loc) -> ResultE<'i, Self>
113    where
114        SpanElem: TryFrom<(Loc, Loc, T), Error = LalrError<'i>>,
115    {
116        Ok(Self::Down(to_span_elems(l, r, cs)?))
117    }
118
119    #[allow(clippy::needless_pass_by_value)]
120    pub(crate) fn spanup<T>(l: Loc, cs: Vec<T>, r: Loc) -> ResultE<'i, Self>
121    where
122        SpanElem: TryFrom<(Loc, Loc, T), Error = LalrError<'i>>,
123    {
124        Ok(Self::Up(to_span_elems(l, r, cs)?))
125    }
126
127    #[allow(clippy::needless_pass_by_value)]
128    pub(crate) fn spanto<T>(l: Loc, top: Vec<T>, btm: Vec<T>, r: Loc) -> ResultE<'i, Self>
129    where
130        SpanElem: TryFrom<(Loc, Loc, T), Error = LalrError<'i>>,
131    {
132        let t = to_span_elems(l, r, top)?;
133        let b = to_span_elems(l, r, btm)?;
134
135        ensure_same_format(l, r, &t, &b).map(|()| {
136            if t[0].rank() > b[0].rank() {
137                Self::To(t, b)
138            } else {
139                Self::To(b, t)
140            }
141        })
142    }
143}
144
145#[cfg(test)]
146#[cfg_attr(coverage_nightly, coverage(off))]
147mod tests {
148    use super::*;
149    use crate::*;
150
151    fn assert_span(src: &str, expected: &str) {
152        let span = parse_span(src).unwrap();
153
154        assert_eq!(span.to_string(), expected, "{span} != {expected}");
155    }
156
157    const fn valid_card(c: RangeCard) -> bool {
158        matches!(c, RangeCard::CC(_, _) | RangeCard::CA(_))
159    }
160
161    #[quickcheck]
162    fn test_spandown(c1: RangeCard, c2: RangeCard) -> TestResult {
163        if !valid_card(c1) || !valid_card(c2) {
164            return TestResult::discard();
165        }
166
167        let src = format!("{c1}{c2}-");
168        assert_span(&src, &src);
169
170        TestResult::passed()
171    }
172
173    #[quickcheck]
174    fn test_spanup(c1: RangeCard, c2: RangeCard) -> TestResult {
175        if !valid_card(c1) || !valid_card(c2) {
176            return TestResult::discard();
177        }
178
179        let src = format!("{c1}{c2}+");
180        assert_span(&src, &src);
181
182        TestResult::passed()
183    }
184
185    #[quickcheck]
186    fn test_spanto(
187        a1: RankConst,
188        a2: RankConst,
189        b1: RankConst,
190        b2: RankConst,
191        s: SuitConst,
192    ) -> TestResult {
193        if (a1 as RankInt - a2 as RankInt) != (b1 as RankInt - b2 as RankInt) {
194            return TestResult::discard();
195        }
196
197        let src = format!("{a1}{b1}{s}-{a2}{b2}{s}");
198
199        let expected = if a1 > a2 {
200            src.clone()
201        } else {
202            format!("{a2}{b2}{s}-{a1}{b1}{s}")
203        };
204
205        assert_span(&src, &expected);
206
207        TestResult::passed()
208    }
209
210    #[test]
211    fn test_spanto_fixed() {
212        assert_span("AsK-JsT", "AsK-JsT");
213        assert_span("T-A", "A-T");
214        assert_span("A-T", "A-T");
215        assert_span("TT-AA", "AA-TT");
216        assert_span("AA-TT", "AA-TT");
217    }
218
219    #[test]
220    fn test_span_error() {
221        assert_err(
222            parse_span("A-AK"),
223            Error::NumberOfRanksMismatchInSpan((0, 4)),
224        );
225
226        assert_err(
227            parse_span("AA-QT"),
228            Error::RankDistanceMismatchInSpan((0, 5)),
229        );
230
231        assert_err(parse_span("As-K"), Error::SuitMismatchInSpan((0, 4)));
232    }
233
234    #[test]
235    fn test_span_error_invalid() {
236        assert_err(parse_span("B-    "), Error::InvalidSpan((0, 2)));
237        assert_err(parse_span("B+    "), Error::InvalidSpan((0, 2)));
238        assert_err(parse_span("B-A   "), Error::InvalidSpan((0, 3)));
239        assert_err(parse_span("A-B   "), Error::InvalidSpan((0, 3)));
240        assert_err(parse_span("Bs-   "), Error::InvalidSpan((0, 3)));
241        assert_err(parse_span("*w-   "), Error::InvalidSpan((0, 3)));
242        assert_err(parse_span("Aw-   "), Error::InvalidSpan((0, 3)));
243        assert_err(parse_span("Bw-   "), Error::InvalidSpan((0, 3)));
244        assert_err(parse_span("*-    "), Error::InvalidSpan((0, 2)));
245    }
246}