Search code examples
rusttexttrimming

How to trim space less than n times?


How to eliminate up to n spaces at the beginning of each line?

For example, when trim 4 space:

  • " 5" ->" 5"
  • " 4" ->"4"
  • " 3" ->"3"
const INPUT:&str = "    4\n  2\n0\n\n      6\n";
const OUTPUT:&str = "4\n2\n0\n\n  6\n";
#[test]
fn main(){
    assert_eq!(&trim_deindent(INPUT,4), OUTPUT)
}

Solution

  • I was about to comment textwrap::dedent, but then I noticed "2", which has less than 4 spaces. So you wanted it to keep removing spaces, if there is any up until 4.

    Just writing a quick solution, it could look something like this:

    Your assert will pass, but note that lines ending in \r\n will be converted to \n, as lines does not provide a way to differentiate between \n and \r\n.

    fn trim_deindent(text: &str, max: usize) -> String {
        let mut new_text = text
            .lines()
            .map(|line| {
                let mut max = max;
                line.chars()
                    // Skip while `c` is a whitespace and at most `max` spaces
                    .skip_while(|c| {
                        if max == 0 {
                            false
                        } else {
                            max -= 1;
                            c.is_whitespace()
                        }
                    })
                    .collect::<String>()
            })
            .collect::<Vec<_>>()
            .join("\n");
    
        // Did the original `text` end with a `\n` then add it again
        if text.ends_with('\n') {
            new_text.push('\n');
        }
    
        new_text
    }
    

    If you want to retain both \n and \r\n then you can go a more complex route of scanning through the string, and thus avoiding using lines.

    fn trim_deindent(text: &str, max: usize) -> String {
        let mut new_text = String::new();
    
        let mut line_start = 0;
        loop {
            let mut max = max;
    
            // Skip `max` spaces
            let after_space = text[line_start..].chars().position(|c| {
                // We can't use `is_whitespace` here, as that will skip past `\n` and `\r` as well
                if (max == 0) || !is_horizontal_whitespace(c) {
                    true
                } else {
                    max -= 1;
                    false
                }
            });
    
            if let Some(after_space) = after_space {
                let after_space = line_start + after_space;
    
                let line = &text[after_space..];
                // Find `\n` or use the line length (if it's the last line)
                let end = line
                    .chars()
                    .position(|c| c == '\n')
                    .unwrap_or_else(|| line.len());
    
                // Push the line (including the line ending) onto `new_text`
                new_text.push_str(&line[..=end]);
    
                line_start = after_space + end + 1;
            } else {
                break;
            }
        }
    
        new_text
    }
    
    #[inline]
    fn is_horizontal_whitespace(c: char) -> bool {
        (c != '\r') && (c != '\n') && c.is_whitespace()
    }