1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
use crate::prelude::*;

/// Loosely ordered from oldest to newest.
/// newest > oldest
#[derive(Debug, Clone, Copy, PartialEq, Eq, EnumDiscriminants, PartialOrd, Ord)]
pub enum IPadVariant {
	Air {
		screen_size: MaybeScreenSize,
		generation: Generation,
	},
	Mini {
		generation: Generation,
	},
	Plain {
		generation: Generation,
	},
	Pro {
		size: ScreenSize,
		generation: Generation,
		/// For lossless parsing
		size_before_generation: bool,
	},
}

impl NomFromStr for IPadVariant {
	#[tracing::instrument(level = "trace")]
	fn nom_from_str(input: &str) -> IResult<&str, Self> {
		let (remaining, discriminate) = preceded(
			ws(tag("iPad")),
			cut(alt((
				value(IPadVariantDiscriminants::Mini, ws(tag("mini"))),
				value(IPadVariantDiscriminants::Air, ws(tag("Air"))),
				value(IPadVariantDiscriminants::Pro, ws(tag("Pro"))),
				success(IPadVariantDiscriminants::Plain),
			))),
		)(input)?;

		#[cfg(test)]
		trace!(
			?discriminate,
			remaining,
			input,
			"Discriminant found for parsing [IPadVariant]"
		);

		match discriminate {
			IPadVariantDiscriminants::Air => {
				let (remaining, screen_size) = MaybeScreenSize::nom_from_str(remaining)?;
				let (remaining, generation) = Generation::nom_from_str(remaining)?;
				Ok((
					remaining,
					IPadVariant::Air {
						screen_size,
						generation,
					},
				))
			}
			IPadVariantDiscriminants::Mini => {
				let (remaining, generation) = Generation::nom_from_str(remaining)?;
				Ok((remaining, IPadVariant::Mini { generation }))
			}
			IPadVariantDiscriminants::Plain => {
				let (remaining, generation) = Generation::nom_from_str(remaining)?;
				Ok((remaining, IPadVariant::Plain { generation }))
			}
			IPadVariantDiscriminants::Pro => {
				let (remaining, (size_before_generation, (size, generation))) = alt((
					map(
						pair(ws(ScreenSize::nom_from_str), ws(Generation::nom_from_str)),
						|v| (true, v),
					),
					map(
						pair(ws(Generation::nom_from_str), ws(ScreenSize::nom_from_str)),
						|(gen, ss)| (false, (ss, gen)),
					),
				))(remaining)?;
				Ok((
					remaining,
					IPadVariant::Pro {
						size,
						generation,
						size_before_generation,
					},
				))
			}
		}
	}
}

impl Display for IPadVariant {
	fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
		match self {
			IPadVariant::Pro {
				size,
				generation,
				size_before_generation: true,
			} => write!(f, "iPad Pro {} {}", size, generation),
			IPadVariant::Pro {
				size,
				generation,
				size_before_generation: false,
			} => write!(f, "iPad Pro {} {}", generation, size,),
			IPadVariant::Mini { generation } => write!(f, "iPad mini {}", generation),
			IPadVariant::Air {
				screen_size,
				generation,
			} => write!(f, "iPad Air{} {}", screen_size, generation),
			IPadVariant::Plain { generation } => write!(f, "iPad {}", generation),
		}
	}
}

#[cfg(test)]
mod test {
	use crate::shared::assert_nom_parses;

	use super::*;

	#[test]
	fn ipad_ordering() {
		let old = IPadVariant::Plain {
			generation: Generation::testing_num(NonZeroU8::new(1).unwrap()),
		};
		let new = IPadVariant::Pro {
			size: ScreenSize::long_brackets(12.9),
			generation: Generation::testing_num(NonZeroU8::new(2).unwrap()),
			size_before_generation: false,
		};
		assert!(new > old);
	}

	#[test]
	fn hard_coded_parsing() {
		let examples = [
			// "iPad Air (5th generation)",
			// "iPad (10th generation)",
			// "iPad mini (6th generation)",
			// "iPad Pro (11-inch) (4th generation)",
			// "iPad Pro (12.9-inch) (6th generation)",
			// "iPad Air 11-inch (M2)",
			// "iPad Air 13-inch (M2)",
			// "iPad Pro 11-inch (M4)",
			"iPad Pro 13-inch (M4)",
		];
		assert_nom_parses::<IPadVariant>(examples, |_| true)
	}
}