소개
야외 활동 팀에는 정상 작동하는 손전등 두 개가 필요합니다. 스캔 두 줄은 서로 다른 손전등 두 개, 같은 손전등을 두 번 스캔한 것, 또는 정상 손전등 하나와 손상된 손전등 하나를 나타낼 수 있습니다. 프로그램은 장비가 갖춰졌는지 판단하기 전에 이 차이를 유지해야 합니다.
이 실습에서는 Rust로 상태 파서와 장비 개체 수집 함수를 구현합니다. 서로 다른 실물은 유지하고, 내용이 같은 반복 스캔은 무시하며, 원본 파일을 바꾸지 않고 모순된 기록을 거부합니다. 다음 실습에서는 이 식별 정보를 사용해 장비 유형별 부족이나 초과를 설명합니다. 합성 기록을 사용하며 실제 장비를 검사하는 실습은 아닙니다.
Rust 구조체, 열거형, HashMap, Result, 텍스트 파일 읽기, 기본 for 반복문을 알고 있어야 합니다. 파서, 명령 인터페이스, 출력 형식은 준비되어 있으므로 개체 식별 판단에 집중할 수 있습니다. 저자가 예상한 시간은 읽기와 일반적인 수정까지 포함해 45분이며, 초보자 대상으로 측정한 시간은 아닙니다.
추측하지 않고 상태 읽기
이 단계에서는 실제 스캔 파일 두 개를 살펴보고 사용 가능한 물건을 예측한 뒤, 프로그램의 정상적인 줄 표시를 작동시키는 짧은 상태 판단을 구현합니다. Terminal을 여세요. 준비된 프로젝트는 /home/labex/project/field-kit이며 Rust 1.99.1을 사용합니다.
다음 export는 현재 셸의 명령 검색 경로에 Cargo 디렉터리를 추가합니다. $PATH는 기존 디렉터리를 유지합니다. 현재 Terminal 세션에만 적용되므로 새 셸에서는 다시 실행하세요. 셸 시작 파일을 편집하지 않습니다.
export PATH=/home/labex/.cargo/bin:$PATH
cd로 프로젝트를 선택하세요. 위치를 잃으면 다시 실행합니다. pwd는 현재 디렉터리를 출력하므로 이후 어느 위치의 파일을 편집할지 확인할 수 있습니다.
cd /home/labex/project/field-kit
pwd
/home/labex/project/field-kit이 나와야 합니다. 경로 오타는 VM을 초기화하는 대신 cd로 고치세요. cat은 파일을 변경하지 않고 출력합니다. 실제 입력 두 개를 읽으세요.
cat data/two-good.tsv
cat data/one-damaged.tsv
TSV는 탭으로 구분된 값을 뜻합니다. 각 스캔에는 실제 탭으로 구분된 asset_id, type_id, condition 세 필드가 있습니다. 첫 파일에서는 실물 개체 a와 b가 모두 good 상태입니다. 두 번째 파일에서는 a가 정상이고 b가 damaged 상태입니다. 손으로 결과를 확인하세요. 두 파일 모두 실물 두 개를 나타내지만 사용 가능한 손전등 두 개를 나타내는 것은 첫 파일뿐입니다. good과 damaged는 정확히 허용된 두 상태입니다. 손상 장비는 잘못된 입력 줄이 아니라 유효한 데이터입니다.
프로젝트에는 구조체와 TSV 리더가 있는 src/lib.rs, 명령 인터페이스와 출력 형식이 있는 src/main.rs, 이후 실습을 위한 src/readiness.rs가 제공됩니다. 이 파일들과 Cargo.toml, rust-toolchain.toml, 모든 원본 data/ 파일을 변경하지 마세요. 여기서는 src/condition.rs만, 다음 단계에서는 src/identity.rs를 편집할 수 있으며 공개 이름과 타입을 유지해야 합니다.
상태 모듈을 읽으세요.
cat src/condition.rs
열거형과 표시 메서드는 이미 제공됩니다. impl은 Condition의 메서드를 묶으며 Self는 그 블록 안에서 해당 열거형을 가리킵니다. pub은 리더에서 이 이름들을 사용할 수 있게 합니다. derive는 복사, 디버그 표시, 동등 비교를 제공합니다. parse는 Result<Self, String>을 반환합니다. 상태를 담은 Ok 또는 데이터를 소유하는 오류 설명을 담은 Err입니다. 현재 시작 코드는 모든 단어를 거부하므로 정상 파일 두 개를 아직 구별하지 못합니다.
편집 전에 소스만 되돌릴 수 있는 지점을 만드세요. mktemp -d는 X 여섯 개를 고유한 접미사로 바꿔 새 디렉터리를 만들며 이전 백업을 재사용하지 않습니다. $()는 출력된 경로를 세션 변수에 저장합니다. &&는 디렉터리 생성이 성공한 경우에만 cp -p를 실행하며 이 복사는 소스 메타데이터를 보존합니다. 따옴표는 변수의 경로를 하나로 유지합니다. 어느 작업이든 실패하면 오류를 읽고 백업을 성공적으로 만든 뒤 편집하세요.
rs03_before_condition=$(mktemp -d .edit-before-condition.XXXXXX) && cp -p src/condition.rs "$rs03_before_condition/condition.rs"
nano로 기존 모듈을 여세요. -c는 커서 위치를 표시합니다.
nano -c src/condition.rs
nano의 ^ 표기는 Ctrl을 뜻합니다. Ctrl+W를 누르고 pub fn parse를 입력한 뒤 Enter를 눌러 함수를 찾으세요. 여는 중괄호와 닫는 중괄호를 읽으세요. 화살표와 Backspace/Delete를 사용해 이 메서드의 본문과 사용하지 않는 매개변수 바인딩만 편집하세요. 매개변수 타입을 유지하며 _value를 value로 바꾸세요. 열거형, derive, name 메서드를 보존하세요.
허용된 각 단어를 정확히 처리하는 분기와 나머지 모든 문자열을 처리하는 와일드카드를 가진 match value 표현식을 작성하세요. "good" => Ok(Self::Good), 같은 분기가 문법을 보여 줍니다. 대응하는 Damaged 결과는 직접 만드세요. _는 남은 모든 값을 일치시키며, 상태를 임의로 만들어 내는 대신 Err를 반환해야 합니다. format!은 String을 만들고 {value:?}는 예상 밖 텍스트를 따옴표로 감싸므로 진단에서 공백과 철자를 볼 수 있습니다. 입력은 good 또는 damaged여야 한다는 설명에 사용하세요. match와 함수 중괄호를 닫고 match를 메서드의 결과로 유지하세요.
Ctrl+O, Enter로 src/condition.rs를 확인한 뒤 Ctrl+X로 nano에서 나가세요. 편집기 안에 있는 동안 셸 명령을 입력하지 마세요. 저장된 수정을 읽으세요.
cat src/condition.rs
cargo run은 준비된 프로그램을 컴파일하고 실행합니다. --quiet는 Cargo 진행 메시지를 숨기고 --는 뒤의 인수를 프로그램에 전달합니다. rows는 실제 파일의 모든 줄을 검증하고 표시하며 아직 중복을 제거하지 않습니다.
cargo run --quiet -- rows data/two-good.tsv
asset_id type_id condition
a light-a good
b light-a good
정상적인 줄 표시에서 두 ID와 정상 상태가 모두 보여야 합니다. 다른 원본과 비교하세요.
cargo run --quiet -- rows data/one-damaged.tsv
asset_id type_id condition
a light-a good
b light-a damaged
손으로 한 예측과 같습니다. 여전히 스캔된 물건은 두 개이며 정상인 것은 하나뿐입니다. 별도의 시연 프로그램이 아니라 실제 리더가 사용하는 경로에 실제 판단을 작성했습니다.
제한된 입력 계약
실제 계획도 읽으세요.
cat data/plan.tsv
필드는 type_id, name, required입니다. light-a와 light-b는 모두 Flashlight로 표시되지만 유형 ID는 다릅니다. 이름은 식별자가 아니라 표시용 텍스트입니다. 필요 수량이 양수인데 스캔이 없으면 부족이며, 필요 수량 0에 스캔이 없으면 부족 수량도 0입니다. 제공된 리더는 다음 규칙을 적용합니다.
- ID는 ASCII 소문자로 시작하고 뒤에는 소문자, 숫자,
-만 옵니다. 대소문자를 구분합니다. - 상태는 정확히
good및damaged이며 다른 철자는 허용하지 않습니다. - 이름은 비어 있지 않은 출력 가능한 ASCII이며 공백을 포함합니다. 수량은 0–999 값을 나타내는 십진 숫자이며 앞자리 0을 허용합니다. 계획의 유형 ID는 내용이 같아도 반복할 수 없습니다.
- 정확한 세 필드 헤더가 첫 줄이어야 합니다. LF/CRLF, 마지막 줄 바꿈 생략, 뒤에 오는 완전히 빈 줄은 허용합니다. 공백 문자만 있는 줄은 빈 줄이 아닙니다. 단독 캐리지 리턴은 줄 끝이 아닙니다. 필드 앞뒤 공백을 제거하지 않습니다. ID나 상태의 공백은 오류이며 이름의 공백은 유지됩니다.
여기서 ASCII는 기본 영문자, 숫자, 공백, 문장 부호이며 이름에는 제어 문자를 허용하지 않습니다. 작게 정의한 형식이며 범용 CSV 리더가 아닙니다. 여러분의 상태 메서드가 준비된 리더 안에서 두 상태 판단을 제공합니다.
소스 편집 복구하기
Rust 오류가 나면 Cargo의 파일과 줄을 읽고 그 파일을 다시 열어 해당 함수를 검색한 뒤 수정을 복구하세요. 저장하고 나가서 실패한 명령을 다시 실행하세요. 아직 저장하지 않은 실수는 Ctrl+X를 누른 뒤 저장 질문에서 N으로 버립니다. 이전에 저장한 수정은 되돌리지 않습니다.
저장한 실수를 복구할 때만, setup을 초기화하는 대신 자신의 상태 소스 체크포인트를 복원하세요. 같은 Terminal에는 백업을 가리키는 변수가 남아 있습니다. 새 Terminal에서는 PATH와 cd를 다시 실행한 뒤 이 단계의 실제 디렉터리를 나열하세요.
ls -d .edit-before-condition.*
-d는 내용 대신 디렉터리 이름을 나열하고 *는 생성된 접미사와 일치합니다. 다음 명령을 실행하고 셸이 기다리는 동안 출력된 정확한 디렉터리 이름 하나를 붙여 넣은 뒤 Enter를 누르세요. read는 그 줄을 이 세션의 변수에 저장합니다.
read rs03_before_condition
선택한 파일을 확인하세요. 경로나 단계가 틀리면 선택과 확인을 반복하세요. 백업이 없다면 접미사를 추측하지 말고 참고 구현을 사용해 복구하세요.
cat "$rs03_before_condition/condition.rs"
의도한 편집 전 소스임을 확인한 뒤 다음 명령으로 허용된 상태 파일만 의도적으로 교체합니다.
cp -p "$rs03_before_condition/condition.rs" src/condition.rs
미완성 파서로 돌아가므로 상태 판단을 다시 구현하고 두 rows 명령을 반복하세요. 아래 참고 구현은 시도한 뒤 특정 메서드를 비교하기 위한 것이며 일반 편집 작업으로 모듈 전체를 교체하기 위한 것이 아닙니다.
비교와 복구를 위한 전체 상태 참고 구현
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Condition { Good, Damaged }
impl Condition {
pub fn parse(value: &str) -> Result<Self, String> {
match value {
"good" => Ok(Self::Good),
"damaged" => Ok(Self::Damaged),
_ => Err(format!("condition must be good or damaged, got {value:?}")),
}
}
pub fn name(self) -> &'static str {
match self { Self::Good => "good", Self::Damaged => "damaged" }
}
}
개체 ID로 실물 장비 기록 모으기
이 단계에서는 서로 다른 실물 개체를 유지하고 동일한 반복을 한 번만 세는 식별 정보 수집을 구현합니다. Terminal의 /home/labex/project/field-kit에서 계속하세요. 더 큰 원본을 읽으세요.
cat data/scans.tsv
코딩 전에 고유한 물건을 예측하세요. 개체 a는 같은 유형과 상태로 두 번 나타납니다. a, b, c는 유형 light-a인 서로 다른 손전등 세 개입니다. 개체 d는 계획에서 표시 이름은 같지만 다른 유형인 light-b에 속합니다. e와 f는 계획에 없는 유형 other에 속합니다. 유효한 추가 장비가 사라져서는 안 됩니다. 실물 ID는 여섯 개여야 하며 일곱 개이거나 유형 또는 이름마다 하나여서는 안 됩니다.
기존 수집 함수의 틀을 읽으세요.
cat src/identity.rs
Scan에는 asset_id, type_id, condition 및 진단용 line이 있습니다. 제공된 use crate::Scan은 이 라이브러리의 구조체를 가져옵니다. 함수는 &[Scan]을 빌리므로 호출자의 기록을 바꾸거나 소유권을 가져오면 안 됩니다. Result는 고유 기록의 벡터 또는 유용한 충돌 설명을 반환합니다.
HashMap은 개체 ID마다 기록 하나를 연결할 수 있습니다. 기존 키에 삽입하면 이전 값이 교체되므로 삽입 전에 조회하세요. 새 ID, ID·유형·상태가 동일한 반복, 모순된 반복을 각각 어떻게 처리할지 정하세요. derive로 만든 동등 비교로 Scan 전체를 비교하지 마세요. 내용이 같아도 줄 번호는 자연히 다릅니다. 유형과 상태를 비교하고 두 줄 번호는 진단에만 유지하세요.
편집 전에 이 단계의 소스를 보존하세요.
rs03_before_identity=$(mktemp -d .edit-before-identity.XXXXXX) && cp -p src/identity.rs "$rs03_before_identity/identity.rs"
nano -c src/identity.rs
이 시작 코드에는 HashMap 가져오기가 없습니다. use crate::Scan;을 유지하고 맨 위에 표준 라이브러리 가져오기를 나란히 추가하세요.
use std::collections::HashMap;
std는 Rust 표준 라이브러리이고 crate는 이 프로젝트의 라이브러리입니다. 이름을 가져오면 모든 파일이 아니라 이 모듈에서 사용할 수 있습니다. 동등한 정렬 맵이나 정렬 구현도 허용하며 실제 필요한 이름을 가져오면 됩니다.
Ctrl+W로 pub fn collect_assets를 검색하고 Enter를 누르세요. 슬라이스 타입을 보존하며 _scans를 scans로 바꾸세요. 함수의 판단을 구현하고 다른 정의와 완성된 상태 모듈은 변경하지 마세요.
맵을 사용하는 한 방법은 변경 가능한 HashMap<String, Scan>으로 시작합니다. 빌린 각 스캔에 대해 get(&scan.asset_id)는 이전 기록이 있는 Some 또는 None을 반환합니다. if let Some(previous)는 기록이 있을 때만 들어갑니다. 유형이나 상태 중 하나라도 다르면 전체 개체 ID, 현재 줄, 첫 줄을 담은 Err를 반환하세요. format!으로 이 필드를 데이터를 소유하는 메시지로 결합할 수 있습니다. 내용이 같으면 continue로 이 반복의 삽입을 건너뜁니다.
새 ID에는 복제한 ID와 복제한 기록을 삽입하세요. .clone()은 빌린 입력에서 데이터를 소유하는 사본을 만듭니다. 모든 스캔을 검사한 뒤 저장된 기록을 새 Vec으로 이동하고 그 벡터를 Ok로 반환하세요. Vec::new()는 빈 벡터를 만들고 push는 항목을 추가합니다. for (_, scan) in by_id 같은 반복문은 쌍을 분해하고 _로 키를 무시합니다. HashMap 순회 순서는 정해져 있지 않습니다. 제공된 출력 형식이 개체 ID를 정렬하므로 내부 순서는 고정하지 않아도 됩니다.
Ctrl+O, Enter, Ctrl+X로 저장하고 나가세요. 실행 전에 저장된 함수를 살펴보세요.
cat src/identity.rs
먼저 실제 원시 줄을 비교하세요.
cargo run --quiet -- rows data/scans.tsv
반복된 a가 여전히 있습니다. 이제 완성한 수집 함수를 실행하세요.
cargo run --quiet -- assets data/scans.tsv
asset_id type_id condition
a light-a good
b light-a good
c light-a good
d light-b damaged
e other good
f other damaged
여섯 ID와 유형, 상태를 모두 확인하세요. 동일한 반복만 수집된 화면에서 사라졌으며 입력 파일은 편집하지 않았습니다. 같은 유형의 다른 ID, 손상, 계획 외 장비가 모두 남았습니다. 다음 단계에서는 모순을 검사합니다. 첫 기록이나 마지막 기록을 조용히 선택하지 말고 전체 수집 결과를 거부해야 합니다.
컴파일러 오류가 나면 nano에서 보고된 줄을 고치세요. 원인이 누락된 가져오기라면 그것도 고칩니다. 저장한 실수를 되돌릴 때만, 다음 조건부 복구에서 자신의 수집 함수 편집 전 파일을 사용합니다. 새 Terminal에서는 PATH와 cd를 다시 실행한 뒤 이 단계의 실제 백업 이름을 나열하세요.
ls -d .edit-before-identity.*
read를 실행하고 출력된 정확한 디렉터리 이름 하나를 붙여 넣은 뒤 Enter를 누르세요. 복원 전에 내용을 확인하세요. 맞는 백업이 없으면 경로를 추측하지 말고 참고 구현으로 특정 부분을 복구하세요.
read rs03_before_identity
cat "$rs03_before_identity/identity.rs"
의도한 소스임을 확인한 뒤 허용된 식별 정보 파일만 복원하세요.
cp -p "$rs03_before_identity/identity.rs" src/identity.rs
원래 가져오기를 포함한 초기 수집 함수로 돌아갑니다. 구현에 필요한 가져오기를 다시 추가하고 함수를 완성한 뒤 다시 시험하세요. 성공한 상태 파서가 제거되거나 원시 입력이 변경되지는 않습니다.
비교와 복구를 위한 전체 개체 수집 참고 구현
직접 구현을 시도한 뒤 관련 판단을 비교하고 nano로 해당 부분을 복구하세요. 이후 독립 단원 준비에 사용한 유일한 전체 개체 수집 소스입니다.
use crate::Scan;
use std::collections::HashMap;
pub fn collect_assets(scans: &[Scan]) -> Result<Vec<Scan>, String> {
let mut by_id: HashMap<String, Scan> = HashMap::new();
for scan in scans {
if let Some(previous) = by_id.get(&scan.asset_id) {
if previous.type_id != scan.type_id || previous.condition != scan.condition {
return Err(format!(
"line {}: conflicting asset {} (first at line {})",
scan.line, scan.asset_id, previous.line
));
}
continue;
}
by_id.insert(scan.asset_id.clone(), scan.clone());
}
let mut unique = Vec::new();
for (_, scan) in by_id {
unique.push(scan);
}
Ok(unique)
}
충돌을 거부하고 입력 오류에서 복구하기
이 단계에서는 모순된 증거, 알 수 없는 상태, 잘못된 경로를 구분합니다. 프로젝트 디렉터리의 Terminal에서 계속하세요. 다음 실패는 의도된 것으로, 오류를 출력하고 0이 아닌 종료 상태를 반환해야 합니다. 오류를 없애려고 보존된 예제 입력을 삭제하거나 고치지 마세요.
아래 오류 예제는 참고 구현의 문구입니다. 여러분의 메시지는 다른 표현을 사용해도 됩니다. 충돌은 실제 경로, 전체 개체 ID, 원본의 두 줄을 식별해야 합니다. 알 수 없는 상태는 실제 경로와 줄을 식별하고 거부한 값을 설명해야 합니다. 정확한 문구를 맞추는 것이 목표는 아닙니다.
먼저 모순된 증거를 읽습니다.
cat data/conflict.tsv
개체 a를 good 및 damaged 두 상태 모두로 설명합니다. 수집 함수를 실행하세요.
cargo run --quiet -- assets data/conflict.tsv
data/conflict.tsv: line 3: conflicting asset a (first at line 2)
제공된 명령 인터페이스는 수집 함수의 오류에 실제 입력 경로를 덧붙입니다. 성공 결과 표는 출력하지 않습니다. 마지막 기록을 채택하는 구현은 증거를 숨기고, 파일 순서만 뒤집어도 결과가 달라질 수 있습니다. 동일한 반복은 허용하지만 모순은 이 연습 범위 밖에서 조사해야 합니다.
유효한 입력 원본을 다시 선택해 복구하세요.
cargo run --quiet -- assets data/scans.tsv
이제 알 수 없는 상태를 포함한 예제를 의도적으로 읽습니다.
cargo run --quiet -- rows data/bad-status.tsv
data/bad-status.tsv: line 2: condition must be good or damaged, got "broken"
이는 손상된 개체가 아니라 형식 판단입니다. damaged는 유효한 데이터지만 broken은 이 과정의 제한된 어휘에 없습니다. 유효한 손상 기록을 읽어 복구하세요.
cargo run --quiet -- rows data/one-damaged.tsv
마지막으로 존재하지 않는 단수형 파일 이름을 의도적으로 사용합니다.
cargo run --quiet -- assets data/scan.tsv
오류는 data/scan.tsv를 가리키며 파일을 찾지 못했다고 알려 줍니다. 기존 파일 안의 줄 오류와는 다릅니다. 실제 원본은 그대로 두고 경로를 고치세요.
cargo run --quiet -- assets data/scans.tsv
여섯 개체의 표가 다시 나타나야 합니다. Rust 편집에 오류가 있으면 지정된 소스를 nano로 다시 열어 해당 함수를 복구하세요. 저장한 실수를 되돌리려면 앞 단계에서 배운 명시적인 ls/read/cat 확인 절차로 상태 또는 개체 수집 단계의 체크포인트를 사용하세요. 허용된 그 소스만 복원하고 다시 구현한 뒤 이 마지막 명령을 재실행하세요. 저장하지 않은 nano 수정은 Ctrl+X와 N으로 버릴 수도 있습니다. 어느 경우에도 setup을 다시 실행하거나 보호된 입력을 편집할 필요가 없습니다. 이제 프로그램은 원본 오류와 유효한 반복·손상·추가 기록을 구분합니다. 다음 실습에서는 이 고유한 개체들로 부족 수량을 계산합니다.
요약
두 상태를 가진 열거형 파서를 구현하고 실물 장비의 개별 ID를 기준으로 기록을 모았습니다. 서로 다른 개체는 유지하고, 동일한 반복으로 수량을 늘리지 않았으며, 모순은 증거를 조용히 덮어쓰지 않고 출처를 포함한 오류로 처리했습니다. 원본 파일을 유지하면서 알 수 없는 상태와 잘못된 파일 이름에서도 복구했습니다. 이 신뢰할 수 있는 개체 기록이 유형별 부족과 초과 수량을 설명하는 바탕입니다.



