1use super::{
2 Error, LalrError, Loc, RangeCard, RankConst, RankInt, ResultE, SuitConst,
3 TermElem, ToString,
4};
5
6#[derive(Copy, Clone, PartialEq, Eq, Debug, derive_more::Display)]
8pub enum SpanElem {
9 #[display("{_0}{_1}")]
11 CC(RankConst, SuitConst),
12 #[display("{_0}")]
14 CA(RankConst),
15}
16
17impl SpanElem {
18 pub const fn rank(self) -> RankConst {
20 match self {
21 Self::CC(r, _) | Self::CA(r) => r,
22 }
23 }
24
25 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#[derive(Clone, PartialEq, Eq, Debug, derive_more::Display)]
59pub enum Span {
60 #[display("{}-", to_str(_0))]
62 Down(Vec<SpanElem>),
63 #[display("{}+", to_str(_0))]
65 Up(Vec<SpanElem>),
66 #[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}