openpql_range_parser/ast/
span.rs

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