1use std::cmp::Ordering;
2use std::fmt;
3use std::num::NonZeroU8;
4use std::str::FromStr;
5
6use crate::version::Version;
7use serde::de::Error as _;
8use serde::{Deserialize, Deserializer, Serialize, Serializer};
9
10#[derive(Clone, Copy, Debug, PartialEq, Eq)]
11pub struct UnityVersion {
12 major: u16,
16 minor: u8,
18 revision: u8,
20 type_: ReleaseType,
27 increment: u8,
29 china_increment: Option<NonZeroU8>,
33}
34
35impl UnityVersion {
36 pub const fn new(
37 major: u16,
38 minor: u8,
39 revision: u8,
40 type_: ReleaseType,
41 increment: u8,
42 ) -> Self {
43 Self {
44 major,
45 minor,
46 revision,
47 type_,
48 increment,
49 china_increment: None,
50 }
51 }
52
53 pub const fn new_f1(major: u16, minor: u8, revision: u8) -> Self {
54 Self {
55 major,
56 minor,
57 revision,
58 type_: ReleaseType::Normal,
59 increment: 1,
60 china_increment: None,
61 }
62 }
63
64 pub const fn new_china(
65 major: u16,
66 minor: u8,
67 revision: u8,
68 increment: u8,
69 china_increment: NonZeroU8,
70 ) -> Self {
71 Self {
72 major,
73 minor,
74 revision,
75 type_: ReleaseType::China,
76 increment,
77 china_increment: Some(china_increment),
78 }
79 }
80
81 pub fn parse(input: &str) -> Option<Self> {
83 let (major, rest) = input.split_once('.')?;
84 let major = u16::from_str(major).ok()?;
85 let (minor, rest) = rest.split_once('.')?;
86 let minor = u8::from_str(minor).ok()?;
87 let revision_delimiter = rest.find(is_release_type_char)?;
88 let revision = &rest[..revision_delimiter];
89 let revision = u8::from_str(revision).ok()?;
90 let type_ = ReleaseType::try_from(rest.as_bytes()[revision_delimiter]).ok()?;
91 let rest = &rest[revision_delimiter + 1..];
92
93 let (increment_part, _rest) = rest.split_once('-').unwrap_or((rest, ""));
94 let (increment, china_increment);
95 if increment_part.contains('c') {
96 let (increment_str, increment_china_str) = increment_part.split_once('c')?;
97 increment = u8::from_str(increment_str).ok()?;
98 china_increment = Some(NonZeroU8::from_str(increment_china_str).ok()?);
99 } else {
100 increment = u8::from_str(increment_part).ok()?;
101 china_increment = None;
102 }
103
104 return Some(Self {
105 major,
106 minor,
107 revision,
108 type_,
109 increment,
110 china_increment,
111 });
112
113 fn is_release_type_char(c: char) -> bool {
114 c == 'a' || c == 'b' || c == 'f' || c == 'c' || c == 'p' || c == 'x'
115 }
116 }
117
118 pub fn parse_no_type_increment(input: &str) -> Option<Self> {
120 let (major, rest) = input.split_once('.')?;
121 let major = u16::from_str(major).ok()?;
122 let (minor, rest) = rest.split_once('.')?;
123 let minor = u8::from_str(minor).ok()?;
124 let revision = rest;
125 let revision = u8::from_str(revision).ok()?;
126
127 Some(Self::new_f1(major, minor, revision))
128 }
129
130 pub fn major(self) -> u16 {
131 self.major
132 }
133
134 pub fn minor(self) -> u8 {
135 self.minor
136 }
137
138 pub fn revision(self) -> u8 {
139 self.revision
140 }
141
142 pub fn type_(self) -> ReleaseType {
143 self.type_
144 }
145
146 pub fn increment(self) -> u8 {
147 self.increment
148 }
149
150 pub fn china_increment(self) -> Option<NonZeroU8> {
151 self.china_increment
152 }
153
154 pub fn as_semver(self) -> Version {
155 Version::new(self.major as u64, self.minor as u64, self.revision as u64)
156 }
157}
158
159impl fmt::Display for UnityVersion {
160 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
161 if let Some(china) = self.china_increment {
162 write!(
163 f,
164 "{maj}.{min}.{rev}{ty}{inc}c{china}",
165 maj = self.major,
166 min = self.minor,
167 rev = self.revision,
168 ty = self.type_,
169 inc = self.increment,
170 china = china.get(),
171 )
172 } else {
173 write!(
174 f,
175 "{maj}.{min}.{rev}{ty}{inc}",
176 maj = self.major,
177 min = self.minor,
178 rev = self.revision,
179 ty = self.type_,
180 inc = self.increment,
181 )
182 }
183 }
184}
185
186impl Serialize for UnityVersion {
187 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
188 where
189 S: Serializer,
190 {
191 serializer.serialize_str(&self.to_string())
192 }
193}
194
195impl<'de> Deserialize<'de> for UnityVersion {
196 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
197 where
198 D: Deserializer<'de>,
199 {
200 UnityVersion::parse(&String::deserialize(deserializer)?)
201 .ok_or_else(|| D::Error::custom("invalid unity version"))
202 }
203}
204
205impl PartialOrd<Self> for UnityVersion {
206 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
207 Some(Ord::cmp(self, other))
208 }
209}
210
211impl Ord for UnityVersion {
212 fn cmp(&self, other: &Self) -> Ordering {
213 (self.major().cmp(&other.major()))
215 .then_with(|| self.minor().cmp(&other.minor()))
216 .then_with(|| self.revision().cmp(&other.revision()))
217 .then_with(|| self.type_().cmp(&other.type_()))
218 .then_with(|| self.increment().cmp(&other.increment()))
219 }
220}
221
222#[derive(Clone, Copy, Debug, Eq)]
223pub enum ReleaseType {
224 Alpha,
225 Beta,
226 Normal,
227 China,
228 Patch,
229 Experimental,
230}
231
232impl PartialEq for ReleaseType {
233 fn eq(&self, other: &Self) -> bool {
234 match (self, other) {
235 (Self::Alpha, Self::Alpha) => true,
236 (Self::Beta, Self::Beta) => true,
237 (Self::Normal, Self::Normal) => true,
238 (Self::China, Self::China) => true,
239 (Self::Patch, Self::Patch) => true,
240 (Self::Experimental, Self::Experimental) => true,
241
242 (Self::Normal, Self::China) => true,
244 (Self::China, Self::Normal) => true,
245 _ => false,
246 }
247 }
248}
249
250impl PartialOrd for ReleaseType {
251 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
252 Some(Ord::cmp(self, other))
253 }
254}
255
256impl Ord for ReleaseType {
257 fn cmp(&self, other: &Self) -> Ordering {
258 use Ordering::*;
259 use ReleaseType::*;
260 match (*self, *other) {
261 (Alpha, Alpha) => Equal,
262 (Alpha, _) => Less,
263 (_, Alpha) => Greater,
264
265 (Beta, Beta) => Equal,
266 (Beta, _) => Less,
267 (_, Beta) => Greater,
268
269 (Normal, Normal) => Equal,
270 (Normal, China) => Equal,
271 (China, Normal) => Equal,
272 (China, China) => Equal,
273 (Normal, _) => Less,
274 (China, _) => Less,
275 (_, Normal) => Greater,
276 (_, China) => Greater,
277
278 (Patch, Patch) => Equal,
279 (Patch, _) => Less,
280 (_, Patch) => Greater,
281
282 (Experimental, Experimental) => Equal,
283 }
284 }
285}
286
287pub struct ReleaseTypeError(());
288
289impl fmt::Display for ReleaseType {
290 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
291 match self {
292 ReleaseType::Alpha => f.write_str("a"),
293 ReleaseType::Beta => f.write_str("b"),
294 ReleaseType::Normal => f.write_str("f"),
295 ReleaseType::China => f.write_str("c"),
296 ReleaseType::Patch => f.write_str("p"),
297 ReleaseType::Experimental => f.write_str("x"),
298 }
299 }
300}
301
302impl TryFrom<u8> for ReleaseType {
303 type Error = ReleaseTypeError;
304
305 fn try_from(value: u8) -> Result<Self, Self::Error> {
306 match value {
307 b'a' => Ok(Self::Alpha),
308 b'b' => Ok(Self::Beta),
309 b'f' => Ok(Self::Normal),
310 b'c' => Ok(Self::China),
311 b'p' => Ok(Self::Patch),
312 b'x' => Ok(Self::Experimental),
313 _ => Err(ReleaseTypeError(())),
314 }
315 }
316}
317
318#[cfg(test)]
319mod tests {
320 use super::*;
321
322 #[test]
323 fn parse_unity_version() {
324 macro_rules! good {
325 ($string: literal, $major: literal, $minor: literal, $revision: literal, $type_: ident, $increment: literal) => {
326 let version = UnityVersion::parse($string).unwrap();
327 assert_eq!(version.major, $major);
328 assert_eq!(version.minor, $minor);
329 assert_eq!(version.revision, $revision);
330 assert!(matches!(version.type_, ReleaseType::$type_));
331 assert_eq!(version.increment, $increment);
332 assert_eq!(version.china_increment, None);
333 };
334 }
335
336 macro_rules! good_cn {
337 ($string: literal, $major: literal, $minor: literal, $revision: literal, $type_: ident, $increment: literal, $china_increment: literal) => {
338 let version = UnityVersion::parse($string).unwrap();
339 assert_eq!(version.major, $major);
340 assert_eq!(version.minor, $minor);
341 assert_eq!(version.revision, $revision);
342 assert!(matches!(version.type_, ReleaseType::$type_));
343 assert_eq!(version.increment, $increment);
344 assert_eq!(
345 version.china_increment,
346 Some(NonZeroU8::new($china_increment).unwrap())
347 );
348 };
349 }
350
351 macro_rules! bad {
352 ($string: literal) => {
353 assert!(UnityVersion::parse($string).is_none());
354 };
355 }
356
357 good!("5.6.6f1", 5, 6, 6, Normal, 1);
358
359 good!("2019.1.0a1", 2019, 1, 0, Alpha, 1);
360 good!("2019.1.0b1", 2019, 1, 0, Beta, 1);
361 good!("2019.4.31f1", 2019, 4, 31, Normal, 1);
362 good!("2023.3.6f1", 2023, 3, 6, Normal, 1);
363 good!("2023.3.6c1", 2023, 3, 6, China, 1);
364 good!("2023.3.6p1", 2023, 3, 6, Patch, 1);
365 good!("2023.3.6x1", 2023, 3, 6, Experimental, 1);
366
367 good!("2019.1.0a1-EXTRA", 2019, 1, 0, Alpha, 1);
368
369 good_cn!("2022.3.22f1c1", 2022, 3, 22, Normal, 1, 1);
370
371 bad!("2022");
372 bad!("2019.0");
373 bad!("5.6.6");
374 bad!("2023.4.6f");
375 }
376
377 #[test]
378 fn ord_version() {
379 macro_rules! test {
380 ($left: literal < $right: literal) => {
381 let left = UnityVersion::parse($left).unwrap();
382 let right = UnityVersion::parse($right).unwrap();
383 assert!(left < right);
384 assert!(right > left);
385 };
386 }
387
388 test!("5.6.5f1" < "5.6.6f1");
389 test!("5.6.6f1" < "5.6.6f2");
390 test!("5.6.6f1" < "2022.1.0f1");
391 test!("2022.1.0a1" < "2022.1.0f1");
392 }
393
394 #[test]
395 fn ord_release_type() {
396 use ReleaseType::*;
397
398 macro_rules! test {
399 ($left: ident $right: ident $ordering: ident) => {
400 assert_eq!($left.cmp(&$right), Ordering::$ordering);
401 };
402 }
403
404 assert!(Alpha < Beta);
405 assert!(Beta < Normal);
406 assert!(Beta < China);
407 assert!(China < Patch);
408 assert!(Patch < Experimental);
409
410 test!(Alpha Alpha Equal);
411 test!(Alpha Beta Less);
412 test!(Alpha Normal Less);
413 test!(Alpha China Less);
414 test!(Alpha Patch Less);
415 test!(Alpha Experimental Less);
416
417 test!(Beta Alpha Greater);
418 test!(Beta Beta Equal);
419 test!(Beta Normal Less);
420 test!(Beta China Less);
421 test!(Beta Patch Less);
422 test!(Beta Experimental Less);
423
424 test!(Normal Alpha Greater);
425 test!(Normal Beta Greater);
426 test!(Normal Normal Equal);
427 test!(Normal China Equal);
428 test!(Normal Patch Less);
429 test!(Normal Experimental Less);
430
431 test!(China Alpha Greater);
432 test!(China Beta Greater);
433 test!(China Normal Equal);
434 test!(China China Equal);
435 test!(China Patch Less);
436 test!(China Experimental Less);
437
438 test!(Patch Alpha Greater);
439 test!(Patch Beta Greater);
440 test!(Patch Normal Greater);
441 test!(Patch China Greater);
442 test!(Patch Patch Equal);
443 test!(Patch Experimental Less);
444
445 test!(Experimental Alpha Greater);
446 test!(Experimental Beta Greater);
447 test!(Experimental Normal Greater);
448 test!(Experimental China Greater);
449 test!(Experimental Patch Greater);
450 test!(Experimental Experimental Equal);
451 }
452
453 #[test]
454 fn eq_release_type() {
455 use ReleaseType::*;
456
457 assert_eq!(Alpha, Alpha);
458 assert_ne!(Alpha, Beta);
459 assert_ne!(Alpha, Normal);
460 assert_ne!(Alpha, China);
461 assert_ne!(Alpha, Patch);
462 assert_ne!(Alpha, Experimental);
463
464 assert_ne!(Beta, Alpha);
465 assert_eq!(Beta, Beta);
466 assert_ne!(Beta, Normal);
467 assert_ne!(Beta, China);
468 assert_ne!(Beta, Patch);
469 assert_ne!(Beta, Experimental);
470
471 assert_ne!(Normal, Alpha);
472 assert_ne!(Normal, Beta);
473 assert_eq!(Normal, Normal);
474 assert_eq!(Normal, China);
475 assert_ne!(Normal, Patch);
476 assert_ne!(Normal, Experimental);
477
478 assert_ne!(China, Alpha);
479 assert_ne!(China, Beta);
480 assert_eq!(China, Normal);
481 assert_eq!(China, China);
482 assert_ne!(China, Patch);
483 assert_ne!(China, Experimental);
484
485 assert_ne!(Patch, Alpha);
486 assert_ne!(Patch, Beta);
487 assert_ne!(Patch, Normal);
488 assert_ne!(Patch, China);
489 assert_eq!(Patch, Patch);
490 assert_ne!(Patch, Experimental);
491
492 assert_ne!(Experimental, Alpha);
493 assert_ne!(Experimental, Beta);
494 assert_ne!(Experimental, Normal);
495 assert_ne!(Experimental, China);
496 assert_ne!(Experimental, Patch);
497 assert_eq!(Experimental, Experimental);
498 }
499}