///|
/// Structured representation of DE90 Original Data Elements.
pub(all) struct OriginalDataElements {
original_mti : String
original_stan : String
original_transmission_datetime : String
acquiring_institution_id : String
forwarding_institution_id : String
} derive(Eq, Debug)
///|
/// Parse the fixed 42-digit DE90 layout.
pub fn parse_original_data(
value : String,
) -> Result[OriginalDataElements, IsoError] {
if value.length() != 42 {
return Err(InvalidOriginalData("DE90 must contain 42 digits"))
}
match validate_numeric_text(90, value) {
Err(_) =>
return Err(InvalidOriginalData("DE90 contains a non-decimal character"))
Ok(_) => ()
}
let original_mti = value[0:4].to_owned()
match parse_mti(original_mti) {
Err(_) => return Err(InvalidOriginalData("invalid original MTI"))
Ok(_) => ()
}
let transmission = value[10:20].to_owned()
match validate_transmission_datetime(transmission) {
Err(_) =>
return Err(InvalidOriginalData("invalid original transmission date/time"))
Ok(_) => ()
}
Ok({
original_mti,
original_stan: value[4:10].to_owned(),
original_transmission_datetime: transmission,
acquiring_institution_id: value[20:31].to_owned(),
forwarding_institution_id: value[31:42].to_owned(),
})
}
///|
/// Validate MMDDhhmmss without assigning a year or timezone.
pub fn validate_transmission_datetime(value : String) -> Result[Unit, IsoError] {
if value.length() != 10 {
return Err(InvalidDateTime(value))
}
match validate_numeric_text(7, value) {
Err(_) => return Err(InvalidDateTime(value))
Ok(_) => ()
}
let month = decimal_pair(value, 0)
let day = decimal_pair(value, 2)
let hour = decimal_pair(value, 4)
let minute = decimal_pair(value, 6)
let second = decimal_pair(value, 8)
if month < 1 ||
month > 12 ||
day < 1 ||
day > 31 ||
hour > 23 ||
minute > 59 ||
second > 59 {
return Err(InvalidDateTime(value))
}
let maximum_day = days_in_month_without_year(month)
if day > maximum_day {
return Err(InvalidDateTime(value))
}
Ok(())
}
///|
/// Parse two decimal digits at a known-valid offset.
fn decimal_pair(value : String, offset : Int) -> Int {
(value[offset].to_int() - 48) * 10 + value[offset + 1].to_int() - 48
}
///|
/// Conservative month length; February accepts day 29.
fn days_in_month_without_year(month : Int) -> Int {
match month {
2 => 29
4 | 6 | 9 | 11 => 30
_ => 31
}
}
///|
/// Normalize an institution identifier to eleven decimal digits.
fn normalize_institution_id(value : String) -> Result[String, IsoError] {
if value.length() > 11 {
return Err(InvalidOriginalData("institution identifier exceeds 11 digits"))
}
match validate_numeric_text(90, value) {
Err(_) => Err(InvalidOriginalData("institution identifier is not numeric"))
Ok(_) => Ok(left_pad(value, 11, '0'))
}
}
///|
/// Build DE90 from individual original-message identifiers.
pub fn original_data(
original_mti : String,
original_stan : String,
original_transmission_datetime : String,
acquiring_institution_id : String,
forwarding_institution_id : String,
) -> Result[OriginalDataElements, IsoError] {
match parse_mti(original_mti) {
Err(_) => return Err(InvalidOriginalData("invalid original MTI"))
Ok(_) => ()
}
if original_stan.length() != 6 {
return Err(InvalidOriginalData("STAN must contain six digits"))
}
match validate_numeric_text(11, original_stan) {
Err(_) => return Err(InvalidOriginalData("STAN is not numeric"))
Ok(_) => ()
}
match validate_transmission_datetime(original_transmission_datetime) {
Err(_) => return Err(InvalidOriginalData("invalid transmission date/time"))
Ok(_) => ()
}
let acquiring = match normalize_institution_id(acquiring_institution_id) {
Ok(value) => value
Err(error) => return Err(error)
}
let forwarding = match normalize_institution_id(forwarding_institution_id) {
Ok(value) => value
Err(error) => return Err(error)
}
Ok({
original_mti,
original_stan,
original_transmission_datetime,
acquiring_institution_id: acquiring,
forwarding_institution_id: forwarding,
})
}
///|
/// Render the 42-digit wire representation.
pub fn OriginalDataElements::to_string(self : OriginalDataElements) -> String {
self.original_mti +
self.original_stan +
self.original_transmission_datetime +
self.acquiring_institution_id +
self.forwarding_institution_id
}
///|
/// Build DE90 from fields available in an original message.
pub fn original_data_from_message(
message : IsoMessage,
) -> Result[OriginalDataElements, IsoError] {
let stan = match message.field(11) {
Some(value) => value
None => return Err(MissingField(11))
}
let transmission = match message.field(7) {
Some(value) => value
None => return Err(MissingField(7))
}
let acquiring = message.field(32).unwrap_or("0")
let forwarding = message.field(33).unwrap_or("0")
original_data(message.mti, stan, transmission, acquiring, forwarding)
}
///|
/// Check that DE90 refers to the supplied original message.
pub fn OriginalDataElements::matches(
self : OriginalDataElements,
message : IsoMessage,
) -> Bool {
if self.original_mti != message.mti {
return false
}
if message.field(11) != Some(self.original_stan) {
return false
}
if message.field(7) != Some(self.original_transmission_datetime) {
return false
}
let acquiring = message.field(32).unwrap_or("0")
let forwarding = message.field(33).unwrap_or("0")
match
(normalize_institution_id(acquiring), normalize_institution_id(forwarding)) {
(Ok(a), Ok(f)) =>
a == self.acquiring_institution_id && f == self.forwarding_institution_id
_ => false
}
}
///|
/// Attach a rendered DE90 to a reversal or advice message.
pub fn attach_original_data(
target : IsoMessage,
original : IsoMessage,
) -> Result[Unit, IsoError] {
let data = match original_data_from_message(original) {
Ok(value) => value
Err(error) => return Err(error)
}
target.set_field(90, data.to_string())
}