edifact-primitives 0.30.4

Shared EDIFACT primitive types — zero dependencies
Documentation
//! Release-character escaping of component values.
//!
//! Escaping works on `char`s, not bytes. The delimiters are ASCII, so a
//! multi-byte UTF-8 character can never be one of them; reading a value byte
//! by byte and pushing each byte back as a `char` re-encodes every byte of
//! such a character on its own (`ä`, `C3 A4`, becomes `ä`).

use crate::EdifactDelimiters;

impl EdifactDelimiters {
    /// Whether `c` is one of the characters a value must escape: the element
    /// and component separators, the segment terminator and the release
    /// character itself.
    fn must_escape(&self, c: char) -> bool {
        c.is_ascii()
            && [self.element, self.component, self.segment, self.release].contains(&(c as u8))
    }

    /// Append `value` to `out`, putting the release character before every
    /// delimiter in it (`a+b` → `a?+b`).
    pub fn escape_into(&self, value: &str, out: &mut String) {
        if !value.chars().any(|c| self.must_escape(c)) {
            out.push_str(value);
            return;
        }
        for c in value.chars() {
            if self.must_escape(c) {
                out.push(self.release as char);
            }
            out.push(c);
        }
    }

    /// `value` with its release characters removed (`a?+b` → `a+b`): each one
    /// stands for the character after it. A trailing release character with
    /// nothing after it is kept.
    pub fn unescape(&self, value: &str) -> String {
        let release = self.release as char;
        if !value.contains(release) {
            return value.to_string();
        }
        let mut out = String::with_capacity(value.len());
        let mut chars = value.chars().peekable();
        while let Some(c) = chars.next() {
            if c == release {
                if let Some(next) = chars.next() {
                    out.push(next);
                    continue;
                }
            }
            out.push(c);
        }
        out
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    fn escaped(value: &str) -> String {
        let mut out = String::new();
        EdifactDelimiters::default().escape_into(value, &mut out);
        out
    }

    #[test]
    fn escape_keeps_multibyte_characters_whole() {
        assert_eq!(
            escaped("Zählpunkt? unbekannt + Müller"),
            "Zählpunkt?? unbekannt ?+ Müller"
        );
    }

    #[test]
    fn escape_leaves_a_plain_value_alone() {
        assert_eq!(escaped("Müller"), "Müller");
    }

    #[test]
    fn escape_marks_every_delimiter() {
        assert_eq!(escaped("a+b:c'd?e"), "a?+b?:c?'d??e");
    }

    #[test]
    fn unescape_keeps_multibyte_characters_whole() {
        let d = EdifactDelimiters::default();
        assert_eq!(
            d.unescape("Zählpunkt?? unbekannt ?+ Müller"),
            "Zählpunkt? unbekannt + Müller"
        );
        assert_eq!(d.unescape("?ä"), "ä");
    }

    #[test]
    fn unescape_inverts_escape() {
        let d = EdifactDelimiters::default();
        for value in ["", "x", "a+b:c'd?e", "Grüße ?+ € 'ß'", "?"] {
            assert_eq!(d.unescape(&escaped(value)), value, "{value:?}");
        }
    }

    #[test]
    fn unescape_keeps_a_trailing_release_character() {
        assert_eq!(EdifactDelimiters::default().unescape("abc?"), "abc?");
    }
}