1use crate::{SwiftField, ValidationError, ValidationResult};
2use serde::{Deserialize, Serialize};
3
4#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
84pub struct Field13C {
85 pub time: String,
95
96 pub utc_offset1: String,
106
107 pub utc_offset2: String,
115}
116
117impl SwiftField for Field13C {
118 fn parse(value: &str) -> Result<Self, crate::ParseError> {
119 let content = if let Some(stripped) = value.strip_prefix(":13C:") {
120 stripped } else if let Some(stripped) = value.strip_prefix("13C:") {
122 stripped } else {
124 value
125 };
126
127 if content.is_empty() {
128 return Err(crate::ParseError::InvalidFieldFormat {
129 field_tag: "13C".to_string(),
130 message: "Field content cannot be empty after removing tag".to_string(),
131 });
132 }
133
134 if !content.starts_with('/') {
137 return Err(crate::ParseError::InvalidFieldFormat {
138 field_tag: "13C".to_string(),
139 message: "Field must start with /".to_string(),
140 });
141 }
142
143 let parts: Vec<&str> = content[1..].split('/').collect(); if parts.len() != 3 {
146 return Err(crate::ParseError::InvalidFieldFormat {
147 field_tag: "13C".to_string(),
148 message: "Format must be /time/offset1/offset2 (3 parts separated by /)"
149 .to_string(),
150 });
151 }
152
153 let time = parts[0].to_string();
154 let utc_offset1 = parts[1].to_string();
155 let utc_offset2 = parts[2].to_string();
156
157 Self::new(time, utc_offset1, utc_offset2)
158 }
159
160 fn to_swift_string(&self) -> String {
161 format!(
162 ":13C:/{}/{}/{}",
163 self.time, self.utc_offset1, self.utc_offset2
164 )
165 }
166
167 fn validate(&self) -> ValidationResult {
168 let mut errors = Vec::new();
169
170 if self.time.len() != 8 {
172 errors.push(ValidationError::LengthValidation {
173 field_tag: "13C".to_string(),
174 expected: "8 characters".to_string(),
175 actual: self.time.len(),
176 });
177 } else {
178 let hours_str = &self.time[0..2];
180 let minutes_str = &self.time[2..4];
181 let seconds_str = &self.time[4..6];
182 let remainder = &self.time[6..8];
183
184 if let Ok(hours) = hours_str.parse::<u32>() {
186 if hours > 23 {
187 errors.push(ValidationError::ValueValidation {
188 field_tag: "13C".to_string(),
189 message: "Hours must be 00-23".to_string(),
190 });
191 }
192 } else {
193 errors.push(ValidationError::FormatValidation {
194 field_tag: "13C".to_string(),
195 message: "Invalid hours in time portion".to_string(),
196 });
197 }
198
199 if let Ok(minutes) = minutes_str.parse::<u32>() {
201 if minutes > 59 {
202 errors.push(ValidationError::ValueValidation {
203 field_tag: "13C".to_string(),
204 message: "Minutes must be 00-59".to_string(),
205 });
206 }
207 } else {
208 errors.push(ValidationError::FormatValidation {
209 field_tag: "13C".to_string(),
210 message: "Invalid minutes in time portion".to_string(),
211 });
212 }
213
214 if let Ok(seconds) = seconds_str.parse::<u32>() {
216 if seconds > 59 {
217 errors.push(ValidationError::ValueValidation {
218 field_tag: "13C".to_string(),
219 message: "Seconds must be 00-59".to_string(),
220 });
221 }
222 } else {
223 errors.push(ValidationError::FormatValidation {
224 field_tag: "13C".to_string(),
225 message: "Invalid seconds in time portion".to_string(),
226 });
227 }
228
229 if !remainder.chars().all(|c| c.is_ascii() && !c.is_control()) {
231 errors.push(ValidationError::FormatValidation {
232 field_tag: "13C".to_string(),
233 message: "Invalid characters in time remainder".to_string(),
234 });
235 }
236 }
237
238 if let Err(e) = Self::validate_utc_offset(&self.utc_offset1, "UTC offset 1") {
240 errors.push(ValidationError::FormatValidation {
241 field_tag: "13C".to_string(),
242 message: format!("UTC offset 1 validation failed: {}", e),
243 });
244 }
245
246 if let Err(e) = Self::validate_utc_offset(&self.utc_offset2, "UTC offset 2") {
247 errors.push(ValidationError::FormatValidation {
248 field_tag: "13C".to_string(),
249 message: format!("UTC offset 2 validation failed: {}", e),
250 });
251 }
252
253 ValidationResult {
254 is_valid: errors.is_empty(),
255 errors,
256 warnings: Vec::new(),
257 }
258 }
259
260 fn format_spec() -> &'static str {
261 "/8c/4!n1!x4!n"
262 }
263}
264
265impl Field13C {
266 pub fn new(
291 time: impl Into<String>,
292 utc_offset1: impl Into<String>,
293 utc_offset2: impl Into<String>,
294 ) -> crate::Result<Self> {
295 let time = time.into().trim().to_string();
296 let utc_offset1 = utc_offset1.into().trim().to_string();
297 let utc_offset2 = utc_offset2.into().trim().to_string();
298
299 if time.len() != 8 {
301 return Err(crate::ParseError::InvalidFieldFormat {
302 field_tag: "13C".to_string(),
303 message: "Time must be exactly 8 characters (HHMMSS+DD)".to_string(),
304 });
305 }
306
307 let hours_str = &time[0..2];
309 let minutes_str = &time[2..4];
310 let seconds_str = &time[4..6];
311 let remainder = &time[6..8];
312
313 let hours: u32 = hours_str
315 .parse()
316 .map_err(|_| crate::ParseError::InvalidFieldFormat {
317 field_tag: "13C".to_string(),
318 message: "Invalid hours in time portion".to_string(),
319 })?;
320 if hours > 23 {
321 return Err(crate::ParseError::InvalidFieldFormat {
322 field_tag: "13C".to_string(),
323 message: "Hours must be 00-23".to_string(),
324 });
325 }
326
327 let minutes: u32 =
329 minutes_str
330 .parse()
331 .map_err(|_| crate::ParseError::InvalidFieldFormat {
332 field_tag: "13C".to_string(),
333 message: "Invalid minutes in time portion".to_string(),
334 })?;
335 if minutes > 59 {
336 return Err(crate::ParseError::InvalidFieldFormat {
337 field_tag: "13C".to_string(),
338 message: "Minutes must be 00-59".to_string(),
339 });
340 }
341
342 let seconds: u32 =
344 seconds_str
345 .parse()
346 .map_err(|_| crate::ParseError::InvalidFieldFormat {
347 field_tag: "13C".to_string(),
348 message: "Invalid seconds in time portion".to_string(),
349 })?;
350 if seconds > 59 {
351 return Err(crate::ParseError::InvalidFieldFormat {
352 field_tag: "13C".to_string(),
353 message: "Seconds must be 00-59".to_string(),
354 });
355 }
356
357 if !remainder.chars().all(|c| c.is_ascii() && !c.is_control()) {
359 return Err(crate::ParseError::InvalidFieldFormat {
360 field_tag: "13C".to_string(),
361 message: "Invalid characters in time remainder".to_string(),
362 });
363 }
364
365 Self::validate_utc_offset(&utc_offset1, "UTC offset 1").map_err(|msg| {
367 crate::ParseError::InvalidFieldFormat {
368 field_tag: "13C".to_string(),
369 message: format!("UTC offset 1 validation failed: {}", msg),
370 }
371 })?;
372
373 Self::validate_utc_offset(&utc_offset2, "UTC offset 2").map_err(|msg| {
374 crate::ParseError::InvalidFieldFormat {
375 field_tag: "13C".to_string(),
376 message: format!("UTC offset 2 validation failed: {}", msg),
377 }
378 })?;
379
380 Ok(Field13C {
381 time,
382 utc_offset1,
383 utc_offset2,
384 })
385 }
386
387 fn validate_utc_offset(offset: &str, context: &str) -> Result<(), String> {
401 if offset.len() != 5 {
402 return Err(format!(
403 "{} must be exactly 5 characters (+HHMM or -HHMM)",
404 context
405 ));
406 }
407
408 let sign = &offset[0..1];
409 let hours_str = &offset[1..3];
410 let minutes_str = &offset[3..5];
411
412 if sign != "+" && sign != "-" {
414 return Err(format!("{} must start with + or -", context));
415 }
416
417 let hours: u32 = hours_str
419 .parse()
420 .map_err(|_| format!("Invalid hours in {}", context))?;
421 if hours > 14 {
422 return Err(format!("Hours in {} must be 00-14", context));
423 }
424
425 let minutes: u32 = minutes_str
427 .parse()
428 .map_err(|_| format!("Invalid minutes in {}", context))?;
429 if minutes > 59 {
430 return Err(format!("Minutes in {} must be 00-59", context));
431 }
432
433 Ok(())
434 }
435
436 pub fn time(&self) -> &str {
450 &self.time
451 }
452
453 pub fn utc_offset1(&self) -> &str {
467 &self.utc_offset1
468 }
469
470 pub fn utc_offset2(&self) -> &str {
484 &self.utc_offset2
485 }
486
487 pub fn hours(&self) -> u32 {
501 self.time[0..2].parse().unwrap_or(0)
502 }
503
504 pub fn minutes(&self) -> u32 {
518 self.time[2..4].parse().unwrap_or(0)
519 }
520
521 pub fn seconds(&self) -> u32 {
535 self.time[4..6].parse().unwrap_or(0)
536 }
537
538 pub fn time_remainder(&self) -> &str {
553 &self.time[6..8]
554 }
555
556 pub fn is_cls_time(&self) -> bool {
564 matches!(self.time_remainder(), "+1" | "+0" | "+C")
566 }
567
568 pub fn is_target_time(&self) -> bool {
576 self.time_remainder() == "+0"
578 && (self.utc_offset1 == "+0100" || self.utc_offset1 == "+0200")
579 }
580
581 pub fn description(&self) -> String {
596 let time_desc = if self.is_target_time() {
597 "TARGET system time"
598 } else if self.is_cls_time() {
599 "CLS Bank cut-off time"
600 } else {
601 "Time indication"
602 };
603
604 format!(
605 "{} at {:02}:{:02}:{:02}{} (UTC{}/UTC{})",
606 time_desc,
607 self.hours(),
608 self.minutes(),
609 self.seconds(),
610 self.time_remainder(),
611 self.utc_offset1,
612 self.utc_offset2
613 )
614 }
615}
616
617impl std::fmt::Display for Field13C {
618 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
619 write!(
620 f,
621 "{:02}:{:02}:{:02}{} {} {}",
622 self.hours(),
623 self.minutes(),
624 self.seconds(),
625 self.time_remainder(),
626 self.utc_offset1,
627 self.utc_offset2
628 )
629 }
630}
631
632#[cfg(test)]
633mod tests {
634 use super::*;
635
636 #[test]
637 fn test_field13c_creation() {
638 let field = Field13C::new("153045+1", "+0100", "-0500").unwrap();
639 assert_eq!(field.time(), "153045+1");
640 assert_eq!(field.utc_offset1(), "+0100");
641 assert_eq!(field.utc_offset2(), "-0500");
642 }
643
644 #[test]
645 fn test_field13c_parse() {
646 let field = Field13C::parse("/123456+0/+0000/+0200").unwrap();
647 assert_eq!(field.time, "123456+0");
648 assert_eq!(field.utc_offset1, "+0000");
649 assert_eq!(field.utc_offset2, "+0200");
650 }
651
652 #[test]
653 fn test_field13c_parse_with_prefix() {
654 let field = Field13C::parse(":13C:/235959+D/+0000/+0530").unwrap();
655 assert_eq!(field.time, "235959+D");
656 assert_eq!(field.utc_offset1, "+0000");
657 assert_eq!(field.utc_offset2, "+0530");
658
659 let field = Field13C::parse("13C:/090000+X/-0300/+0900").unwrap();
660 assert_eq!(field.time, "090000+X");
661 assert_eq!(field.utc_offset1, "-0300");
662 assert_eq!(field.utc_offset2, "+0900");
663 }
664
665 #[test]
666 fn test_field13c_case_normalization() {
667 let field = Field13C::new("120000+d", "+0100", "-0500").unwrap();
669 assert_eq!(field.time, "120000+d");
670 }
671
672 #[test]
673 fn test_field13c_invalid_time_code() {
674 let result = Field13C::new("1234567", "+0100", "-0500"); assert!(result.is_err());
676
677 let result = Field13C::new("123456789", "+0100", "-0500"); assert!(result.is_err());
679
680 let result = Field13C::new("245959+D", "+0100", "-0500"); assert!(result.is_err());
682
683 let result = Field13C::new("236059+D", "+0100", "-0500"); assert!(result.is_err());
685
686 let result = Field13C::new("235960+D", "+0100", "-0500"); assert!(result.is_err());
688 }
689
690 #[test]
691 fn test_field13c_invalid_utc_offset() {
692 let result = Field13C::new("120000+0", "0100", "-0500"); assert!(result.is_err());
694
695 let result = Field13C::new("120000+0", "+25000", "-0500"); assert!(result.is_err());
697
698 let result = Field13C::new("120000+0", "+1500", "-0500"); assert!(result.is_err());
700
701 let result = Field13C::new("120000+0", "+0160", "-0500"); assert!(result.is_err());
703 }
704
705 #[test]
706 fn test_field13c_invalid_format() {
707 let result = Field13C::parse("123456+0/+0100/-0500"); assert!(result.is_err());
709
710 let result = Field13C::parse("/123456+0/+0100"); assert!(result.is_err());
712
713 let result = Field13C::parse("/123456+0/+0100/-0500/extra"); assert!(result.is_err());
715 }
716
717 #[test]
718 fn test_field13c_to_swift_string() {
719 let field = Field13C::new("143725+2", "+0100", "-0800").unwrap();
720 assert_eq!(field.to_swift_string(), ":13C:/143725+2/+0100/-0800");
721 }
722
723 #[test]
724 fn test_field13c_validation() {
725 let field = Field13C::new("090000+0", "+0000", "+0000").unwrap();
726 let result = field.validate();
727 assert!(result.is_valid);
728
729 let invalid_field = Field13C {
730 time: "1234567".to_string(), utc_offset1: "+0100".to_string(),
732 utc_offset2: "-0500".to_string(),
733 };
734 let result = invalid_field.validate();
735 assert!(!result.is_valid);
736 }
737
738 #[test]
739 fn test_field13c_format_spec() {
740 assert_eq!(Field13C::format_spec(), "/8c/4!n1!x4!n");
741 }
742
743 #[test]
744 fn test_field13c_display() {
745 let field = Field13C::new("143725+D", "+0100", "-0800").unwrap();
746 assert_eq!(format!("{}", field), "14:37:25+D +0100 -0800");
747 }
748
749 #[test]
750 fn test_field13c_descriptions() {
751 let field = Field13C::new("235959+X", "+1200", "-0700").unwrap();
752 assert_eq!(field.time(), "235959+X");
753 assert_eq!(field.utc_offset1(), "+1200");
754 assert_eq!(field.utc_offset2(), "-0700");
755 assert_eq!(field.hours(), 23);
756 assert_eq!(field.minutes(), 59);
757 assert_eq!(field.seconds(), 59);
758 assert_eq!(field.time_remainder(), "+X");
759 }
760
761 #[test]
762 fn test_field13c_is_valid_time_code() {
763 let field = Field13C::new("143725+2", "+0100", "-0500").unwrap();
765 assert_eq!(field.hours(), 14);
766 assert_eq!(field.minutes(), 37);
767 assert_eq!(field.seconds(), 25);
768 assert_eq!(field.time_remainder(), "+2");
769 }
770
771 #[test]
772 fn test_field13c_cls_time_detection() {
773 let cls_field1 = Field13C::new("153045+1", "+0100", "-0500").unwrap();
775 assert!(cls_field1.is_cls_time());
776
777 let cls_field2 = Field13C::new("090000+0", "+0000", "+0900").unwrap();
778 assert!(cls_field2.is_cls_time());
779
780 let cls_field3 = Field13C::new("120000+C", "+0200", "-0800").unwrap();
781 assert!(cls_field3.is_cls_time());
782
783 let non_cls_field = Field13C::new("143725+D", "+0100", "-0800").unwrap();
784 assert!(!non_cls_field.is_cls_time());
785 }
786
787 #[test]
788 fn test_field13c_target_time_detection() {
789 let target_field1 = Field13C::new("090000+0", "+0100", "+0900").unwrap();
791 assert!(target_field1.is_target_time());
792
793 let target_field2 = Field13C::new("160000+0", "+0200", "-0500").unwrap();
794 assert!(target_field2.is_target_time());
795
796 let non_target_field1 = Field13C::new("090000+1", "+0100", "+0900").unwrap();
797 assert!(!non_target_field1.is_target_time());
798
799 let non_target_field2 = Field13C::new("090000+0", "+0000", "+0900").unwrap();
800 assert!(!non_target_field2.is_target_time());
801 }
802
803 #[test]
804 fn test_field13c_description_generation() {
805 let cls_field = Field13C::new("153045+1", "+0100", "-0500").unwrap();
807 let description = cls_field.description();
808 assert!(description.contains("CLS Bank cut-off time"));
809 assert!(description.contains("15:30:45+1"));
810 assert!(description.contains("UTC+0100"));
811 assert!(description.contains("UTC-0500"));
812
813 let target_field = Field13C::new("090000+0", "+0100", "+0900").unwrap();
815 let description = target_field.description();
816 assert!(description.contains("TARGET system time"));
817 assert!(description.contains("09:00:00+0"));
818
819 let generic_field = Field13C::new("143725+D", "+0200", "-0800").unwrap();
821 let description = generic_field.description();
822 assert!(description.contains("Time indication"));
823 assert!(description.contains("14:37:25+D"));
824 }
825
826 #[test]
827 fn test_field13c_real_world_examples() {
828 let cls_example = Field13C::new("153045+1", "+0100", "-0500").unwrap();
830 assert_eq!(cls_example.to_swift_string(), ":13C:/153045+1/+0100/-0500");
831 assert!(cls_example.is_cls_time());
832 assert!(!cls_example.is_target_time());
833
834 let target_example = Field13C::new("090000+0", "+0100", "+0900").unwrap();
836 assert_eq!(
837 target_example.to_swift_string(),
838 ":13C:/090000+0/+0100/+0900"
839 );
840 assert!(target_example.is_target_time());
841 assert!(target_example.is_cls_time()); let generic_example = Field13C::new("235959+D", "+0200", "-0800").unwrap();
845 assert_eq!(
846 generic_example.to_swift_string(),
847 ":13C:/235959+D/+0200/-0800"
848 );
849 assert!(!generic_example.is_cls_time());
850 assert!(!generic_example.is_target_time());
851 }
852
853 #[test]
854 fn test_field13c_edge_cases() {
855 let midnight = Field13C::new("000000+0", "+0000", "+0000").unwrap();
857 assert_eq!(midnight.hours(), 0);
858 assert_eq!(midnight.minutes(), 0);
859 assert_eq!(midnight.seconds(), 0);
860
861 let end_of_day = Field13C::new("235959+X", "+1400", "-1200").unwrap();
863 assert_eq!(end_of_day.hours(), 23);
864 assert_eq!(end_of_day.minutes(), 59);
865 assert_eq!(end_of_day.seconds(), 59);
866
867 let extreme_positive = Field13C::new("120000+Z", "+1400", "+1200").unwrap();
869 assert_eq!(extreme_positive.utc_offset1(), "+1400");
870
871 let extreme_negative = Field13C::new("120000+A", "-1200", "-1100").unwrap();
872 assert_eq!(extreme_negative.utc_offset1(), "-1200");
873 }
874
875 #[test]
876 fn test_field13c_serialization() {
877 let field = Field13C::new("153045+1", "+0100", "-0500").unwrap();
878
879 let json = serde_json::to_string(&field).unwrap();
881 let deserialized: Field13C = serde_json::from_str(&json).unwrap();
882
883 assert_eq!(field, deserialized);
884 assert_eq!(field.time(), deserialized.time());
885 assert_eq!(field.utc_offset1(), deserialized.utc_offset1());
886 assert_eq!(field.utc_offset2(), deserialized.utc_offset2());
887 }
888}