Skip to main content

openpql_range_parser/ast/
span.rs

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