Skip to main content

rustledger_core/format/
helpers.rs

1//! Shared helper functions for formatting.
2
3use super::format_amount;
4use crate::MetaValue;
5
6/// Format a metadata value.
7pub fn format_meta_value(value: &MetaValue) -> String {
8    match value {
9        MetaValue::String(s) => format!("\"{}\"", escape_string(s)),
10        MetaValue::Account(a) => a.to_string(),
11        MetaValue::Currency(c) => c.to_string(),
12        MetaValue::Tag(t) => format!("#{t}"),
13        MetaValue::Link(l) => format!("^{l}"),
14        MetaValue::Date(d) => d.to_string(),
15        MetaValue::Number(n) => n.to_string(),
16        MetaValue::Amount(a) => format_amount(a),
17        MetaValue::Bool(b) => if *b { "TRUE" } else { "FALSE" }.to_string(),
18        MetaValue::None => String::new(),
19        MetaValue::Int(i) => i.to_string(),
20    }
21}
22
23/// Escape a string for CSV output (RFC-4180 style).
24///
25/// Values containing a comma, double quote, or line feed (`\n` — carriage
26/// returns do NOT trigger quoting, matching the prior copies
27/// byte-for-byte) are wrapped in double quotes with inner quotes doubled;
28/// everything else passes through unchanged.
29///
30/// The single implementation behind every CSV surface (`rledger report
31/// --format csv`, BQL CSV output) — these previously carried byte-identical
32/// private copies.
33#[must_use]
34pub fn escape_csv(s: &str) -> String {
35    if s.contains(',') || s.contains('"') || s.contains('\n') {
36        format!("\"{}\"", s.replace('"', "\"\""))
37    } else {
38        s.to_string()
39    }
40}
41
42/// Escape a string for output (handle quotes and backslashes).
43pub fn escape_string(s: &str) -> String {
44    let mut out = String::with_capacity(s.len());
45    for c in s.chars() {
46        match c {
47            '"' => out.push_str("\\\""),
48            '\\' => out.push_str("\\\\"),
49            '\n' => out.push_str("\\n"),
50            // The parser decodes `\t`/`\r` into literal tab/CR, so re-escape
51            // them here rather than emitting raw control bytes inside quotes
52            // (hostile to terminals/logs, and not round-trippable).
53            '\t' => out.push_str("\\t"),
54            '\r' => out.push_str("\\r"),
55            _ => out.push(c),
56        }
57    }
58    out
59}
60
61#[cfg(test)]
62mod tests {
63    use super::escape_string;
64
65    #[test]
66    fn escapes_quote_backslash_and_controls() {
67        assert_eq!(escape_string("a\"b"), "a\\\"b");
68        assert_eq!(escape_string("a\\b"), "a\\\\b");
69        assert_eq!(escape_string("a\nb"), "a\\nb");
70        // The parser decodes `\t`/`\r` to literal tab/CR; Display must re-escape
71        // them rather than emit raw control bytes inside the quotes.
72        assert_eq!(escape_string("a\tb"), "a\\tb");
73        assert_eq!(escape_string("a\rb"), "a\\rb");
74    }
75
76    #[test]
77    fn leaves_plain_text_untouched() {
78        assert_eq!(escape_string("plain text 123"), "plain text 123");
79    }
80}