swift_mt_message/fields/
field13c.rs

1use crate::{SwiftField, ValidationError, ValidationResult};
2use serde::{Deserialize, Serialize};
3
4/// # Field 13C: Time Indication
5///
6/// ## Overview
7/// Field 13C specifies time indication(s) related to the processing of payment instructions
8/// in SWIFT MT messages. This field is used to indicate specific timing requirements or
9/// constraints for transaction processing, particularly in time-sensitive payment scenarios.
10///
11/// ## Format Specification
12/// **Format**: `/8c/4!n1!x4!n`
13/// - **8c**: Time portion (8 characters: HHMMSS + 2 additional characters)
14/// - **4!n1!x4!n**: UTC offset (4 digits + sign + 4 digits, format: ±HHMM)
15/// - **4!n1!x4!n**: Second UTC offset (4 digits + sign + 4 digits, format: ±HHMM)
16///
17/// ## Structure
18/// ```text
19/// /HHMMSS+D/±HHMM/±HHMM
20/// │└─────┘│ │    │ │
21/// │  Time │ │    │ └── Second UTC offset
22/// │       │ │    └──── First UTC offset  
23/// │       │ └───────── Time remainder/indicator
24/// │       └─────────── Time (HHMMSS)
25/// └─────────────────── Field delimiter
26/// ```
27///
28/// ## Time Codes and Indicators
29/// The time portion consists of:
30/// - **HH**: Hours (00-23)
31/// - **MM**: Minutes (00-59)
32/// - **SS**: Seconds (00-59)
33/// - **+D**: Additional indicator (varies by usage context)
34///
35/// Common time indicators include:
36/// - `+0`, `+1`, `+2`, etc.: Numeric indicators
37/// - `+D`: Day indicator
38/// - `+X`: Special processing indicator
39///
40/// ## UTC Offset Format
41/// UTC offsets must follow the format `±HHMM`:
42/// - **±**: Plus (+) or minus (-) sign
43/// - **HH**: Hours offset from UTC (00-14)
44/// - **MM**: Minutes offset (00-59, typically 00 or 30)
45///
46/// ## Usage Context
47/// Field 13C is commonly used in:
48/// - **MT103**: Single Customer Credit Transfer
49/// - **MT202**: General Financial Institution Transfer
50/// - **MT202COV**: Cover for customer credit transfer
51///
52/// ### Business Applications
53/// - **Cut-off times**: Specify latest processing time
54/// - **Value dating**: Indicate when funds should be available
55/// - **Time zone coordination**: Handle cross-border payments
56/// - **STP processing**: Ensure straight-through processing timing
57///
58/// ## Examples
59/// ```text
60/// :13C:/CLSTIME/153045+1/+0100/-0500
61/// └─── CLS Bank cut-off time at 15:30:45+1, CET (+0100), EST (-0500)
62///
63/// :13C:/RNCTIME/090000+0/+0000/+0900  
64/// └─── TARGET receive time at 09:00:00, UTC (+0000), JST (+0900)
65///
66/// :13C:/SNDTIME/235959+D/+0200/-0800
67/// └─── Send time at 23:59:59+D, CEST (+0200), PST (-0800)
68/// ```
69///
70/// ## Validation Rules
71/// 1. **Time format**: Must be exactly 8 characters (HHMMSS + 2 chars)
72/// 2. **Time values**: Hours (00-23), Minutes (00-59), Seconds (00-59)
73/// 3. **UTC offsets**: Must be ±HHMM format with valid ranges
74/// 4. **Structure**: Must have exactly 3 parts separated by '/'
75/// 5. **Leading slash**: Field content must start with '/'
76///
77/// ## Network Validated Rules (SWIFT Standards)
78/// - Time indication must be a valid time expressed as HHMM (Error: T38)
79/// - Sign must be either "+" or "-" (Error: T15)
80/// - Time offset hours must be 00-13, minutes 00-59 (Error: T16)
81/// - Format must comply with SWIFT field specifications
82///
83#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
84pub struct Field13C {
85    /// Time portion (8 characters: HHMMSS + 2 additional characters)
86    ///
87    /// Format: HHMMSS+D where:
88    /// - HH: Hours (00-23)
89    /// - MM: Minutes (00-59)
90    /// - SS: Seconds (00-59)
91    /// - +D: Additional indicator (context-dependent)
92    ///
93    /// Examples: "153045+1", "090000+0", "235959+D"
94    pub time: String,
95
96    /// First UTC offset (format: ±HHMM)
97    ///
98    /// Represents the UTC offset for the first timezone reference.
99    /// Format: ±HHMM where:
100    /// - ±: Plus (+) or minus (-) sign
101    /// - HH: Hours offset from UTC (00-14)
102    /// - MM: Minutes offset (00-59, typically 00 or 30)
103    ///
104    /// Examples: "+0100" (CET), "-0500" (EST), "+0000" (UTC)
105    pub utc_offset1: String,
106
107    /// Second UTC offset (format: ±HHMM)
108    ///
109    /// Represents the UTC offset for the second timezone reference.
110    /// Used for cross-timezone coordination or dual time indication.
111    /// Same format as utc_offset1.
112    ///
113    /// Examples: "-0800" (PST), "+0900" (JST), "+0530" (IST)
114    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 // Remove ":13C:" prefix
121        } else if let Some(stripped) = value.strip_prefix("13C:") {
122            stripped // Remove "13C:" prefix
123        } 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        // Expected format: /8c/4!n1!x4!n
135        // Example: /153045+1/+0100/-0500
136        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(); // Remove leading '/' and split
144
145        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        // Validate time format (8 characters: HHMMSS+DD)
171        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            // Parse time components
179            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            // Validate hours (00-23)
185            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            // Validate minutes (00-59)
200            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            // Validate seconds (00-59)
215            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            // Validate remainder (format: +DD or -DD or similar)
230            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        // Validate UTC offsets
239        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    /// Create a new Field13C with comprehensive validation
267    ///
268    /// # Arguments
269    /// * `time` - Time portion (8 characters: HHMMSS + 2 additional chars)
270    /// * `utc_offset1` - First UTC offset (±HHMM format)
271    /// * `utc_offset2` - Second UTC offset (±HHMM format)
272    ///
273    /// # Examples
274    /// ```rust
275    /// use swift_mt_message::fields::Field13C;
276    ///
277    /// // CLS Bank cut-off time
278    /// let field = Field13C::new("153045+1", "+0100", "-0500").unwrap();
279    ///
280    /// // TARGET processing time
281    /// let field = Field13C::new("090000+0", "+0000", "+0900").unwrap();
282    /// ```
283    ///
284    /// # Errors
285    /// Returns `ParseError` if:
286    /// - Time is not exactly 8 characters
287    /// - Hours, minutes, or seconds are out of valid range
288    /// - UTC offsets are not in ±HHMM format
289    /// - UTC offset values are out of valid range
290    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        // Validate time format (8 characters: HHMMSS+DD)
300        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        // Parse time components
308        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        // Validate hours (00-23)
314        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        // Validate minutes (00-59)
328        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        // Validate seconds (00-59)
343        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        // Validate remainder (format: +DD or -DD or similar)
358        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        // Validate UTC offsets
366        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    /// Validate UTC offset format according to SWIFT standards
388    ///
389    /// Validates that the UTC offset follows the ±HHMM format with realistic values:
390    /// - Sign must be + or -
391    /// - Hours must be 00-14 (covers all real-world timezones)
392    /// - Minutes must be 00-59 (typically 00, 15, 30, or 45)
393    ///
394    /// # Arguments
395    /// * `offset` - The UTC offset string to validate
396    /// * `context` - Description for error messages
397    ///
398    /// # Returns
399    /// `Ok(())` if valid, `Err(String)` with error description if invalid
400    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        // Validate sign
413        if sign != "+" && sign != "-" {
414            return Err(format!("{} must start with + or -", context));
415        }
416
417        // Validate hours (00-14 for UTC offset)
418        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        // Validate minutes (00 or 30 for most timezones, 00-59 allowed)
426        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    /// Get the complete time portion
437    ///
438    /// Returns the full 8-character time string including the time indicator.
439    ///
440    /// # Returns
441    /// The time string in format HHMMSS+D
442    ///
443    /// # Example
444    /// ```rust
445    /// # use swift_mt_message::fields::Field13C;
446    /// let field = Field13C::new("153045+1", "+0100", "-0500").unwrap();
447    /// assert_eq!(field.time(), "153045+1");
448    /// ```
449    pub fn time(&self) -> &str {
450        &self.time
451    }
452
453    /// Get the first UTC offset
454    ///
455    /// Returns the first UTC offset in ±HHMM format.
456    ///
457    /// # Returns
458    /// The first UTC offset string
459    ///
460    /// # Example
461    /// ```rust
462    /// # use swift_mt_message::fields::Field13C;
463    /// let field = Field13C::new("153045+1", "+0100", "-0500").unwrap();
464    /// assert_eq!(field.utc_offset1(), "+0100");
465    /// ```
466    pub fn utc_offset1(&self) -> &str {
467        &self.utc_offset1
468    }
469
470    /// Get the second UTC offset
471    ///
472    /// Returns the second UTC offset in ±HHMM format.
473    ///
474    /// # Returns
475    /// The second UTC offset string
476    ///
477    /// # Example
478    /// ```rust
479    /// # use swift_mt_message::fields::Field13C;
480    /// let field = Field13C::new("153045+1", "+0100", "-0500").unwrap();
481    /// assert_eq!(field.utc_offset2(), "-0500");
482    /// ```
483    pub fn utc_offset2(&self) -> &str {
484        &self.utc_offset2
485    }
486
487    /// Extract hours from the time portion
488    ///
489    /// Parses and returns the hours component (00-23) from the time string.
490    ///
491    /// # Returns
492    /// Hours as u32, or 0 if parsing fails
493    ///
494    /// # Example
495    /// ```rust
496    /// # use swift_mt_message::fields::Field13C;
497    /// let field = Field13C::new("153045+1", "+0100", "-0500").unwrap();
498    /// assert_eq!(field.hours(), 15);
499    /// ```
500    pub fn hours(&self) -> u32 {
501        self.time[0..2].parse().unwrap_or(0)
502    }
503
504    /// Extract minutes from the time portion
505    ///
506    /// Parses and returns the minutes component (00-59) from the time string.
507    ///
508    /// # Returns
509    /// Minutes as u32, or 0 if parsing fails
510    ///
511    /// # Example
512    /// ```rust
513    /// # use swift_mt_message::fields::Field13C;
514    /// let field = Field13C::new("153045+1", "+0100", "-0500").unwrap();
515    /// assert_eq!(field.minutes(), 30);
516    /// ```
517    pub fn minutes(&self) -> u32 {
518        self.time[2..4].parse().unwrap_or(0)
519    }
520
521    /// Extract seconds from the time portion
522    ///
523    /// Parses and returns the seconds component (00-59) from the time string.
524    ///
525    /// # Returns
526    /// Seconds as u32, or 0 if parsing fails
527    ///
528    /// # Example
529    /// ```rust
530    /// # use swift_mt_message::fields::Field13C;
531    /// let field = Field13C::new("153045+1", "+0100", "-0500").unwrap();
532    /// assert_eq!(field.seconds(), 45);
533    /// ```
534    pub fn seconds(&self) -> u32 {
535        self.time[4..6].parse().unwrap_or(0)
536    }
537
538    /// Get the time remainder/indicator
539    ///
540    /// Returns the last 2 characters of the time string, which typically
541    /// contain additional time indicators or processing codes.
542    ///
543    /// # Returns
544    /// The time remainder string (2 characters)
545    ///
546    /// # Example
547    /// ```rust
548    /// # use swift_mt_message::fields::Field13C;
549    /// let field = Field13C::new("153045+1", "+0100", "-0500").unwrap();
550    /// assert_eq!(field.time_remainder(), "+1");
551    /// ```
552    pub fn time_remainder(&self) -> &str {
553        &self.time[6..8]
554    }
555
556    /// Check if this is a CLS Bank time indication
557    ///
558    /// Determines if this field represents a CLS Bank cut-off time
559    /// based on common patterns and indicators.
560    ///
561    /// # Returns
562    /// `true` if this appears to be a CLS time indication
563    pub fn is_cls_time(&self) -> bool {
564        // CLS times often use specific indicators
565        matches!(self.time_remainder(), "+1" | "+0" | "+C")
566    }
567
568    /// Check if this is a TARGET system time indication
569    ///
570    /// Determines if this field represents a TARGET (Trans-European Automated
571    /// Real-time Gross Settlement Express Transfer) system time indication.
572    ///
573    /// # Returns
574    /// `true` if this appears to be a TARGET time indication
575    pub fn is_target_time(&self) -> bool {
576        // TARGET times often use +0 indicator and CET timezone
577        self.time_remainder() == "+0"
578            && (self.utc_offset1 == "+0100" || self.utc_offset1 == "+0200")
579    }
580
581    /// Get a human-readable description of the time indication
582    ///
583    /// Returns a descriptive string explaining what this time indication
584    /// represents based on common SWIFT usage patterns.
585    ///
586    /// # Returns
587    /// A descriptive string
588    ///
589    /// # Example
590    /// ```rust
591    /// # use swift_mt_message::fields::Field13C;
592    /// let field = Field13C::new("153045+1", "+0100", "-0500").unwrap();
593    /// println!("{}", field.description());
594    /// ```
595    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        // Time field is not case-normalized, it's kept as-is
668        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"); // Too short
675        assert!(result.is_err());
676
677        let result = Field13C::new("123456789", "+0100", "-0500"); // Too long
678        assert!(result.is_err());
679
680        let result = Field13C::new("245959+D", "+0100", "-0500"); // Invalid hours
681        assert!(result.is_err());
682
683        let result = Field13C::new("236059+D", "+0100", "-0500"); // Invalid minutes
684        assert!(result.is_err());
685
686        let result = Field13C::new("235960+D", "+0100", "-0500"); // Invalid seconds
687        assert!(result.is_err());
688    }
689
690    #[test]
691    fn test_field13c_invalid_utc_offset() {
692        let result = Field13C::new("120000+0", "0100", "-0500"); // Missing sign
693        assert!(result.is_err());
694
695        let result = Field13C::new("120000+0", "+25000", "-0500"); // Too long
696        assert!(result.is_err());
697
698        let result = Field13C::new("120000+0", "+1500", "-0500"); // Invalid hours
699        assert!(result.is_err());
700
701        let result = Field13C::new("120000+0", "+0160", "-0500"); // Invalid minutes
702        assert!(result.is_err());
703    }
704
705    #[test]
706    fn test_field13c_invalid_format() {
707        let result = Field13C::parse("123456+0/+0100/-0500"); // Missing leading /
708        assert!(result.is_err());
709
710        let result = Field13C::parse("/123456+0/+0100"); // Missing second offset
711        assert!(result.is_err());
712
713        let result = Field13C::parse("/123456+0/+0100/-0500/extra"); // Too many parts
714        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(), // Invalid length
731            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        // Test time parsing methods
764        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        // Test CLS Bank time detection
774        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        // Test TARGET system time detection
790        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        // Test CLS Bank description
806        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        // Test TARGET system description
814        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        // Test generic time indication
820        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        // CLS Bank cut-off time example
829        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        // TARGET system time example
835        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()); // +0 is also a CLS indicator
842
843        // Generic processing time
844        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        // Test midnight
856        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        // Test end of day
862        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        // Test extreme timezone offsets
868        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        // Test JSON serialization
880        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}