Optional Skip

About

CSV를 읽어서 line, cell 별로 분리해서 출력하는 프로그램을 만들고 있다가 다음과 같은 상황을 마주하였다.

다음 코드는 CSV 문자열 스트림을 읽어서 출력하는 코드인데, parse_csv_document는 두 번째 인자로 header bool 값을 받아 선택적으로 CSV 테이블 헤더를 스킵할 수 있는 기능을 갖고 있다.

use std::io::{BufRead, Result};

fn parse_csv_document(src: impl BufRead, header: bool) -> Result<Vec<Vec<String>>> {
    let mut lines = src.lines();

    if !header {
        // ?
    }

    lines
        .map(|line| {
            println!("line: {:?}", line);
            line.map(|line| {
                line.split(",")
                    .map(|entry| String::from(entry.trim()))
                    .collect()
            })
        })
        .collect()
}

const CSV_STRING: &str = "KO-KR,KO-KR Alphabet,EN Alphabet,
기역,ㄱ,a
니은,ㄴ,b
디귿,ㄷ,c
";

fn main() {
    let res = parse_csv_document(CSV_STRING.as_bytes(), false);

    println!("{:?}", res);
}

이런 상황에서 if header 조건 안에 어떤 코드를 넣어야 할까?

1. lines.next()

가장 간단한 방법이다. next()는 mutable하게 iterable를 받아서(&mut) 다음 line으로 넘어가도록 하는 메소드이다.

if !header {
    lines.next();
}

하지만 한 번만 실행된다는 것이 마음에 들지는 않았다.

2. lines.skip(N)

사실 이게 될 거라고 생각했다. 그런데 사실 skip()은 기존의 lines를 mutate 하지 않고, N만큼 스킵한 새로운 iterator를 반환한다.

그리고 move가 일어나기도 해서 해당 코드 이후로 lines는 사용할 수 없다.

if !header {
  // note: `skip` takes ownership of the receiver `self`, which moves `lines`
}

참고로 이것도 안된다. 타입 에러가 발생하기 때문이다.

let mut given_lines = src.lines();

let lines;
if header {
    lines = given_lines;
} else {
    lines = given_lines.skip(1);
    // Type mismatch [E0308] expected `Lines<impl BufRead+Sized>`,
    // but found `Skip<Lines<impl BufRead+Sized>>`
}

물론 해결하는 데 가장 쉬운 방법은 명백히 next()이다. 한 줄만 스킵하면 되니까. 하지만 여러 개를 스킵할 땐 어떻게 해야할까?

3. advance_by()

advance_by()는 현재 상황에 가장 맞는 메소드이다.

기존의 iterator를 수정하고 move가 일어나지 않기 때문이다.

lines.advance_by(3);

근데 단점은 작성 시점인 2023.9 기준으로 아직 Nightly에만 존재한다는 것이다.

따라서 직접 함수를 만들어서 진행이 필요하다.

fn advance_by(iter: &mut impl Iterator, n: usize) -> Result<()> {
    for _ in 0..n {
        iter.next();
    }

    Ok(())
}

// ...
fn parse_csv_document(src: impl BufRead, header: bool) -> Result<Vec<Vec<String>>> {
    // ...
    if !header {
        advance_by(&mut lines, 1)?;
    }
    // ...
}

단순하지만 loop을 돌려 next()를 실행해주는 방법이다.

이렇게 하면 next()를 동적으로 주어진 횟수만큼 실행하여 skip(N)을 따라할 수 있다.

물론 immutable하게 실행하고 싶다면 skip과 map을 잘 조합해주면 되겠다.

Referernce

Last updated