1use super::swift_utils::{parse_date_yymmdd, parse_exact_length, parse_numeric, parse_time_hhmm};
6use crate::errors::ParseError;
7use crate::traits::SwiftField;
8use chrono::{NaiveDate, NaiveTime};
9use serde::{Deserialize, Serialize};
10
11mod time_format {
13 use chrono::{NaiveTime, Timelike};
14 use serde::{Deserialize, Deserializer, Serializer};
15
16 pub fn serialize<S>(time: &NaiveTime, serializer: S) -> Result<S::Ok, S::Error>
17 where
18 S: Serializer,
19 {
20 let s = format!("{:02}{:02}", time.hour(), time.minute());
21 serializer.serialize_str(&s)
22 }
23
24 pub fn deserialize<'de, D>(deserializer: D) -> Result<NaiveTime, D::Error>
25 where
26 D: Deserializer<'de>,
27 {
28 let s = String::deserialize(deserializer)?;
29 if s.len() != 4 {
30 return Err(serde::de::Error::custom("Time must be 4 digits (HHMM)"));
31 }
32 let hours: u32 = s[0..2].parse().map_err(serde::de::Error::custom)?;
33 let minutes: u32 = s[2..4].parse().map_err(serde::de::Error::custom)?;
34
35 NaiveTime::from_hms_opt(hours, minutes, 0)
36 .ok_or_else(|| serde::de::Error::custom(format!("Invalid time: {}:{}", hours, minutes)))
37 }
38}
39
40mod date_format {
42 use chrono::NaiveDate;
43 use serde::{Deserialize, Deserializer, Serializer};
44
45 pub fn serialize<S>(date: &NaiveDate, serializer: S) -> Result<S::Ok, S::Error>
46 where
47 S: Serializer,
48 {
49 let s = date.format("%y%m%d").to_string();
50 serializer.serialize_str(&s)
51 }
52
53 pub fn deserialize<'de, D>(deserializer: D) -> Result<NaiveDate, D::Error>
54 where
55 D: Deserializer<'de>,
56 {
57 let s = String::deserialize(deserializer)?;
58 if s.len() != 6 {
59 return Err(serde::de::Error::custom("Date must be 6 digits (YYMMDD)"));
60 }
61
62 let year: i32 = s[0..2].parse::<i32>().map_err(serde::de::Error::custom)?;
63 let year = if year >= 80 { 1900 + year } else { 2000 + year };
64 let month: u32 = s[2..4].parse().map_err(serde::de::Error::custom)?;
65 let day: u32 = s[4..6].parse().map_err(serde::de::Error::custom)?;
66
67 NaiveDate::from_ymd_opt(year, month, day).ok_or_else(|| {
68 serde::de::Error::custom(format!("Invalid date: {}/{}/{}", year, month, day))
69 })
70 }
71}
72
73#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
85pub struct Field13C {
86 pub code: String,
88
89 #[serde(with = "time_format")]
91 pub time: NaiveTime,
92
93 pub sign: char,
95
96 pub offset: String,
98}
99
100impl SwiftField for Field13C {
101 fn parse(input: &str) -> crate::Result<Self>
102 where
103 Self: Sized,
104 {
105 if input.len() < 10 {
107 return Err(ParseError::InvalidFormat {
109 message: format!(
110 "Field 13C must be at least 10 characters, found {}",
111 input.len()
112 ),
113 });
114 }
115
116 if !input.starts_with('/') {
118 return Err(ParseError::InvalidFormat {
119 message: "Field 13C code must start with '/'".to_string(),
120 });
121 }
122
123 let end_slash = input[1..].find('/').ok_or(ParseError::InvalidFormat {
124 message: "Field 13C code must be enclosed in slashes".to_string(),
125 })? + 1;
126
127 if end_slash < 2 {
128 return Err(ParseError::InvalidFormat {
129 message: "Field 13C code cannot be empty".to_string(),
130 });
131 }
132
133 let code = input[1..end_slash].to_string();
134
135 const VALID_CODES: &[&str] = &["SNDTIME", "CLSTIME", "RNCTIME", "REJTIME", "CUTTIME"];
137 if !VALID_CODES.contains(&code.as_str()) {
138 return Err(ParseError::InvalidFormat {
139 message: format!(
140 "Field 13C code must be one of {:?}, found {}",
141 VALID_CODES, code
142 ),
143 });
144 }
145
146 let remaining = &input[end_slash + 1..];
147 if remaining.len() != 9 {
148 return Err(ParseError::InvalidFormat {
150 message: format!(
151 "Field 13C after code must be exactly 9 characters, found {}",
152 remaining.len()
153 ),
154 });
155 }
156
157 let time_str = &remaining[0..4];
159 parse_numeric(time_str, "Field 13C time")?;
160 let time = parse_time_hhmm(time_str)?;
161
162 let sign_char = remaining.chars().nth(4).unwrap();
164 if sign_char != '+' && sign_char != '-' {
165 return Err(ParseError::InvalidFormat {
166 message: format!(
167 "Field 13C UTC offset sign must be '+' or '-', found '{}'",
168 sign_char
169 ),
170 });
171 }
172
173 let offset = parse_exact_length(&remaining[5..9], 4, "Field 13C offset")?;
175 parse_numeric(&offset, "Field 13C offset")?;
176
177 let offset_hours: u32 = offset[0..2].parse().unwrap();
179 let offset_minutes: u32 = offset[2..4].parse().unwrap();
180 if offset_hours > 14 || offset_minutes > 59 {
181 return Err(ParseError::InvalidFormat {
182 message: format!(
183 "Field 13C offset must be valid time offset, found {}:{}",
184 offset_hours, offset_minutes
185 ),
186 });
187 }
188
189 Ok(Field13C {
190 code,
191 time,
192 sign: sign_char,
193 offset,
194 })
195 }
196
197 fn to_swift_string(&self) -> String {
198 format!(
199 ":13C:/{}/{}{}{}",
200 self.code,
201 self.time.format("%H%M"),
202 self.sign,
203 self.offset
204 )
205 }
206}
207
208#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
219pub struct Field13D {
220 #[serde(with = "date_format")]
222 pub date: NaiveDate,
223
224 #[serde(with = "time_format")]
226 pub time: NaiveTime,
227
228 pub offset_sign: char,
230
231 pub offset: String,
233}
234
235impl SwiftField for Field13D {
236 fn parse(input: &str) -> crate::Result<Self>
237 where
238 Self: Sized,
239 {
240 if input.len() != 15 {
242 return Err(ParseError::InvalidFormat {
243 message: format!(
244 "Field 13D must be exactly 15 characters, found {}",
245 input.len()
246 ),
247 });
248 }
249
250 let date = parse_date_yymmdd(&input[0..6])?;
252
253 let time_str = &input[6..10];
255 parse_numeric(time_str, "Field 13D time")?;
256 let time = parse_time_hhmm(time_str)?;
257
258 let offset_sign = input.chars().nth(10).unwrap();
260 if offset_sign != '+' && offset_sign != '-' {
261 return Err(ParseError::InvalidFormat {
262 message: format!(
263 "Field 13D UTC offset sign must be '+' or '-', found '{}'",
264 offset_sign
265 ),
266 });
267 }
268
269 let offset = parse_exact_length(&input[11..15], 4, "Field 13D offset")?;
271 parse_numeric(&offset, "Field 13D offset")?;
272
273 let offset_hours: u32 = offset[0..2].parse().unwrap();
275 let offset_minutes: u32 = offset[2..4].parse().unwrap();
276 if offset_hours > 14 || offset_minutes > 59 {
277 return Err(ParseError::InvalidFormat {
278 message: format!(
279 "Field 13D offset must be valid time offset, found {}:{}",
280 offset_hours, offset_minutes
281 ),
282 });
283 }
284
285 Ok(Field13D {
286 date,
287 time,
288 offset_sign,
289 offset,
290 })
291 }
292
293 fn to_swift_string(&self) -> String {
294 format!(
295 ":13D:{}{}{}{}",
296 self.date.format("%y%m%d"),
297 self.time.format("%H%M"),
298 self.offset_sign,
299 self.offset
300 )
301 }
302}
303
304#[cfg(test)]
305mod tests {
306 use super::*;
307
308 #[test]
309 fn test_field13c_valid() {
310 let field = Field13C::parse("/SNDTIME/1230+0100").unwrap();
311 assert_eq!(field.code, "SNDTIME");
312 assert_eq!(field.time.format("%H%M").to_string(), "1230");
313 assert_eq!(field.sign, '+');
314 assert_eq!(field.offset, "0100");
315 assert_eq!(field.to_swift_string(), ":13C:/SNDTIME/1230+0100");
316
317 let field = Field13C::parse("/CLSTIME/0900-0500").unwrap();
318 assert_eq!(field.code, "CLSTIME");
319 assert_eq!(field.time.format("%H%M").to_string(), "0900");
320 assert_eq!(field.sign, '-');
321 assert_eq!(field.offset, "0500");
322 }
323
324 #[test]
325 fn test_field13c_invalid() {
326 assert!(Field13C::parse("SNDTIME1230+0100").is_err());
328
329 assert!(Field13C::parse("/BADCODE/1230+0100").is_err());
331
332 assert!(Field13C::parse("/SNDTIME/2500+0100").is_err());
334
335 assert!(Field13C::parse("/SNDTIME/1230*0100").is_err());
337
338 assert!(Field13C::parse("/SNDTIME/1230+2500").is_err());
340
341 assert!(Field13C::parse("/SNDTIME/1230+01").is_err());
343 }
344
345 #[test]
346 fn test_field13d_valid() {
347 let field = Field13D::parse("2407191230+0100").unwrap();
348 assert_eq!(field.date.format("%y%m%d").to_string(), "240719");
349 assert_eq!(field.time.format("%H%M").to_string(), "1230");
350 assert_eq!(field.offset_sign, '+');
351 assert_eq!(field.offset, "0100");
352 assert_eq!(field.to_swift_string(), ":13D:2407191230+0100");
353
354 let field = Field13D::parse("2412310000-0800").unwrap();
355 assert_eq!(field.date.format("%y%m%d").to_string(), "241231");
356 assert_eq!(field.time.format("%H%M").to_string(), "0000");
357 assert_eq!(field.offset_sign, '-');
358 assert_eq!(field.offset, "0800");
359 }
360
361 #[test]
362 fn test_field13d_invalid() {
363 assert!(Field13D::parse("2407191230+01").is_err());
365 assert!(Field13D::parse("2407191230+010000").is_err());
366
367 assert!(Field13D::parse("9913321230+0100").is_err());
369
370 assert!(Field13D::parse("2407192500+0100").is_err());
372
373 assert!(Field13D::parse("2407191230*0100").is_err());
375
376 assert!(Field13D::parse("2407191230+2500").is_err());
378 }
379}