はじめに
野外活動チームには、正常に使える懐中電灯が 2 本必要です。2 行のスキャン記録は、異なる 2 本、同じ 1 本を 2 回読み取ったもの、または正常な 1 本と破損した 1 本かもしれません。装備がそろっているかを判断する前に、プログラムはこの違いを保つ必要があります。
このラボでは、Rust で状態パーサーと装備品の収集処理を実装します。異なる実物は残し、同一内容の重複スキャンは無視し、元のファイルを変更せずに矛盾する記録を拒否します。次のラボでは、この個体情報を使って種類ごとの不足や余剰を説明します。扱うのは合成記録であり、実物の点検ではありません。
Rust の構造体、列挙型、HashMap、Result、テキストファイルの読み込み、基本的な for ループの理解を前提とします。パーサー、コマンドインターフェース、出力整形は用意されているため、個体の識別判断に集中できます。読解と通常の修正を含め、著者の見積もりは 45 分です。初心者による実測時間ではありません。
状態を推測せずに読み取る
このステップでは 2 つの実際のスキャンファイルを調べ、使える品物を予測し、通常の行表示を動作させる短い状態判定を実装します。Terminal を開きます。Rust 1.99.1 と、/home/labex/project/field-kit のプロジェクトが準備済みです。
次の 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 はファイルを変えずに表示します。次の 2 つの実際の入力を読みます。
cat data/two-good.tsv
cat data/one-damaged.tsv
TSV はタブ区切りの値です。各スキャンは asset_id、type_id、condition の 3 フィールドを実際のタブで区切ります。最初のファイルでは実物の個体 a と b は両方 good です。2 つ目では a が good、b が damaged です。手で結果を確かめてください。両ファイルとも実物は 2 点ですが、使える電灯が 2 本あるのは最初だけです。good と damaged が許可される正確な 2 状態で、破損した装備も有効なデータであり、不正な入力行ではありません。
プロジェクトには、構造体と 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 を返します。現在の仮実装はすべての語を拒否するため、2 つの正常なファイルをまだ区別できません。
編集前にソースだけの復元点を作ります。mktemp -d は 6 個の 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 と good の状態が現れるはずです。もう一方のソースと比較します。
cargo run --quiet -- rows data/one-damaged.tsv
asset_id type_id condition
a light-a good
b light-a damaged
手で予測したとおり、スキャンされた物は依然として 2 点で、good は 1 点だけです。別のデモプログラムではなく、実際のリーダーの処理経路に本物の判定を書きました。
入力形式の限定ルール
実際の計画も読みます。
cat data/plan.tsv
フィールドは type_id、name、required です。light-a と light-b は両方 Flashlight と表示されますが、異なる種類 ID です。名称は表示テキストであって識別子ではありません。必要数が正でスキャンがなければ不足し、必要数もスキャンもゼロなら不足数はゼロです。提供済みリーダーは次の規則を適用します。
- ID は ASCII 小文字で始まり、後続は小文字、数字、
-だけです。大文字と小文字は区別します。 - 状態は正確に
goodとdamagedで、他のつづりは受け付けません。 - 名称は空ではない表示可能な ASCII 文字列で、空白も含められます。数量は 0~999 の値を持つ十進数字で、先頭のゼロを許可します。計画の種類 ID は内容が同一でも重複できません。
- 最初に正確な 3 フィールドのヘッダーが必要です。LF/CRLF、末行の改行省略、後続の完全な空行を許可します。空白だけの行は空行ではありません。単独の復帰文字は改行ではありません。フィールド前後の空白は除かず、ID や状態の空白はエラーになり、名称の空白は保たれます。
ここでの ASCII は基本的な英字、数字、空白、句読点を意味し、名称に制御文字は含められません。これは小さく定義した形式で、汎用 CSV リーダーではありません。状態メソッドが、この提供済みリーダー内で 2 状態の判定を担います。
ソースの編集から回復する
Rust のエラーでは Cargo が示すファイルと行を読み、そのファイルを開き直して対象関数を検索し、編集を修復します。保存して終了し、失敗したコマンドを再試行します。まだ未保存の誤編集を破棄するには Ctrl+X、保存の質問に N を使います。それ以前の保存済み変更は元に戻りません。
保存済みのミスから回復する場合だけ、setup をリセットせず、自分の状態判定の復元点を戻します。同じ Terminal では変数がまだバックアップを指しています。新しい Terminal では PATH と cd を再実行し、この段階の実際のディレクトリを列挙します。
ls -d .edit-before-condition.*
-d は中身ではなくディレクトリ名を列挙し、* は生成された接尾辞に一致します。次のコマンドを実行し、シェルが待つ間に表示された正確なディレクトリ名を 1 つ貼り付け、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 で装備品の記録を集める
このステップでは、異なる実物を維持し、同一内容の繰り返しは 1 回と数える個体の収集を実装します。引き続き Terminal の /home/labex/project/field-kit で作業します。大きい方のソースを読みます。
cat data/scans.tsv
コードを書く前に一意な品物を予測します。個体 a は同じ種類と状態で 2 回現れます。a、b、c は種類 light-a の別々の電灯 3 本です。d は、計画上の表示名は同じでも異なる種類の light-b に属します。e と f は計画外の種類 other に属し、正当な追加装備を消してはいけません。実物の ID は 6 つで、7 つでも、種類や名称ごとに 1 つでもありません。
既存の収集関数の枠組みを読みます。
cat src/identity.rs
Scan は asset_id、type_id、condition、診断用の line を持ちます。提供済みの use crate::Scan はこのライブラリーからその構造体を取り込みます。関数は &[Scan] を借用するため、呼び出し元の記録を変えたり所有権を奪ったりしてはいけません。Result は一意な記録の vector または役に立つ矛盾の説明を返します。
HashMap なら個体 ID ごとに 1 記録を対応させられます。既存キーへの挿入は前の値を置き換えるため、挿入前に検索します。新しい 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; を残し、先頭で隣に標準ライブラリーの import を加えます。
use std::collections::HashMap;
std は Rust の標準ライブラリー、crate はこのプロジェクトのライブラリーです。import した名前が使えるのはこのモジュールであり、全ファイルではありません。必要な import を使って、同等の順序付き map やソートによる実装にしても構いません。
Ctrl+W で pub fn collect_assets を検索して Enter を押します。スライスの型を保ち、_scans を scans に変えます。関数の判断を実装し、他の定義と完成済みの状態モジュールは変えません。
map を使う方法の 1 つは、変更可能な HashMap<String, Scan> から始めます。借用された各 scan に対して get(&scan.asset_id) は前の記録を伴う Some または None を返します。if let Some(previous) は記録がある場合だけ入ります。種類か状態が違えば、完全な個体 ID、現在の行、最初の行を示す Err を返します。format! でこれらのフィールドを所有するメッセージにまとめられます。内容が同じなら continue でこの反復の挿入を飛ばします。
新しい ID には、複製した ID と記録を挿入します。.clone() は借用入力から所有するコピーを作ります。すべての scan を調べたら保存した記録を新しい Vec に移し、その vector を Ok で返します。Vec::new() は空 vector を作り、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
6 つすべての ID、種類、状態を確認します。同じ反復だけが収集後の表示から消え、入力ファイルは編集していません。同じ種類の異なる ID、破損、計画外装備はすべて残りました。次は矛盾を調べます。最初か最後の記録を黙って選ぶのではなく、収集結果全体を拒否する必要があります。
コンパイラーのエラーでは、nano で報告された行を修復し、import 不足が原因ならそれも直します。保存済みのミスを戻す場合だけ、次の条件付き回復手順で自分の収集前のファイルを使います。新しい Terminal では PATH と cd を再実行し、この段階の実際のバックアップ名を列挙します。
ls -d .edit-before-identity.*
read を実行し、表示された正確なディレクトリ名を 1 つ貼り付け、Enter を押します。復元前に内容を確認してください。一致するバックアップがなければ、パスを推測せず参照コードで特定箇所を直します。
read rs03_before_identity
cat "$rs03_before_identity/identity.rs"
意図したソースと確認してから、許可された個体収集のファイルだけを復元します。
cp -p "$rs03_before_identity/identity.rs" src/identity.rs
元の import を含む最初の収集関数の仮実装に戻ります。実装に必要な import を加え直し、関数を完成させてから再テストします。成功した状態パーサーを消したり、生の入力を変えたりはしません。
比較と回復用の完全な個体収集の参照コード
自分で実装を試した後に対象の判断を比較し、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 で作業します。これからの失敗は意図的で、エラーを表示し、ゼロ以外の終了状態を返すはずです。エラーを消すために保存対象の元のテストデータを削除・修復しないでください。
以下のエラー例は参照実装の文言です。自分のメッセージの言葉は異なっても構いません。矛盾では実際のパス、完全な個体 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
6 個体の表に戻るはずです。Rust の編集に問題があれば、示されたソースを nano で開き直し、対象関数を修復します。保存済みのミスを戻すなら、前のステップで教えた明示的な ls/read/cat の確認手順で状態判定か個体収集の段階の復元点を使います。その許可されたソースだけを復元し、実装をやり直してこの最後のコマンドを再実行してください。未保存の nano の変更なら Ctrl+X と N で破棄できます。いずれも setup の再実行や保護された入力の編集は必要ありません。プログラムは、入力ソースのエラーを、有効な反復・破損・追加の記録と区別できるようになりました。次のラボでは一意な個体を使って不足数を計算します。
まとめ
2 状態の列挙型パーサーを実装し、実物の個体 ID を基準に装備品を収集しました。異なる個体は残し、同一内容の重複で件数を増やさず、矛盾は証拠を黙って置き換えるのではなく、出所の分かるエラーにしました。元のファイルを維持したまま、未知の状態と誤ったファイル名からも復旧しました。この信頼できる個体記録が、種類ごとの不足数と余剰数を説明する基礎になります。



