eml_nl/utils/
contest_id.rs1use std::sync::LazyLock;
2
3use regex::Regex;
4use thiserror::Error;
5
6use crate::{EMLError, EMLValueResultExt, utils::StringValueData};
7
8static CONTEST_ID_RE: LazyLock<Regex> = LazyLock::new(|| {
10 Regex::new(r"^([1-9]\d*|geen|alle|M{0,4}(CM|CD|D?C{0,3})(XC|XL|L?X{0,3})(IX|IV|V?I{0,3}))$")
11 .expect("Failed to compile Contest ID regex")
12});
13
14#[derive(Debug, Clone, PartialEq, Eq, Hash)]
16#[repr(transparent)]
17pub struct ContestId(String);
18
19impl ContestId {
20 pub fn new(s: impl AsRef<str>) -> Result<Self, EMLError> {
22 StringValueData::parse_from_str(s.as_ref()).wrap_value_error()
23 }
24
25 pub fn value(&self) -> &str {
27 &self.0
28 }
29
30 pub fn is_geen(&self) -> bool {
32 self.0 == "geen"
33 }
34
35 pub fn is_alle(&self) -> bool {
37 self.0 == "alle"
38 }
39
40 pub fn geen() -> Self {
42 ContestId("geen".to_string())
43 }
44
45 pub fn alle() -> Self {
47 ContestId("alle".to_string())
48 }
49}
50
51#[derive(Debug, Clone, Error)]
53#[error("Invalid contest id: {0}")]
54pub struct InvalidContestIdError(String);
55
56impl StringValueData for ContestId {
57 type Error = InvalidContestIdError;
58
59 fn parse_from_str(s: &str) -> Result<Self, Self::Error>
60 where
61 Self: Sized,
62 {
63 if !s.is_empty() && CONTEST_ID_RE.is_match(s) {
64 Ok(ContestId(s.to_string()))
65 } else {
66 Err(InvalidContestIdError(s.to_string()))
67 }
68 }
69
70 fn to_raw_value(&self) -> String {
71 self.0.clone()
72 }
73}
74
75#[derive(Debug, Clone, PartialEq, Eq, Hash)]
77pub struct ContestIdGeen;
78
79impl ContestIdGeen {
80 pub const GEEN: &str = "geen";
82
83 pub fn new() -> Self {
85 ContestIdGeen
86 }
87
88 pub fn to_contest_id(&self) -> ContestId {
90 ContestId::geen()
91 }
92}
93
94impl Default for ContestIdGeen {
95 fn default() -> Self {
96 ContestIdGeen::new()
97 }
98}
99
100#[derive(Debug, Clone, Error)]
102#[error("Invalid contest id, expected 'geen': {0}")]
103pub struct InvalidContestIdGeenError(String);
104
105impl StringValueData for ContestIdGeen {
106 type Error = InvalidContestIdGeenError;
107
108 fn parse_from_str(s: &str) -> Result<Self, Self::Error>
109 where
110 Self: Sized,
111 {
112 if s == Self::GEEN {
113 Ok(ContestIdGeen)
114 } else {
115 Err(InvalidContestIdGeenError(s.to_string()))
116 }
117 }
118
119 fn to_raw_value(&self) -> String {
120 Self::GEEN.to_string()
121 }
122}
123
124#[cfg(test)]
125mod tests {
126 use super::*;
127
128 #[test]
129 fn test_contest_id_regex_compiles() {
130 LazyLock::force(&CONTEST_ID_RE);
131 }
132
133 #[test]
134 fn test_valid_contest_ids() {
135 let valid_ids = [
136 "1", "12345", "geen", "alle", "III", "IV", "V", "C", "D", "MMMM", "CM",
137 ];
138 for id in valid_ids {
139 assert!(
140 ContestId::new(id).is_ok(),
141 "ContestId should accept valid id: {}",
142 id
143 );
144 }
145 }
146
147 #[test]
148 fn test_invalid_contest_ids() {
149 let invalid_ids = ["", "0", "0123", "abc", "123abc", "-1", "MMMMM", "IC"];
150 for id in invalid_ids {
151 assert!(
152 ContestId::new(id).is_err(),
153 "ContestId should reject invalid id: {}",
154 id
155 );
156 }
157 }
158
159 #[test]
160 fn test_contest_id_types() {
161 let geen = ContestId::geen();
162 assert_eq!(geen.value(), "geen");
163 assert!(geen.is_geen());
164 assert!(!geen.is_alle());
165
166 let alle = ContestId::alle();
167 assert_eq!(alle.value(), "alle");
168 assert!(!alle.is_geen());
169 assert!(alle.is_alle());
170 }
171
172 #[test]
173 fn test_contest_id_geen() {
174 let valid_geen = "geen";
175 let invalid_geen = "alle";
176 assert!(ContestIdGeen::parse_from_str(valid_geen).is_ok());
177 assert!(ContestIdGeen::parse_from_str(invalid_geen).is_err());
178 }
179
180 #[test]
181 fn test_contest_id_geen_to_contest_id() {
182 let geen = ContestIdGeen::new();
183 let contest_id = geen.to_contest_id();
184 assert_eq!(contest_id.value(), "geen");
185 }
186}