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}