装備の照合結果をすべて説明する

RustBeginner
オンラインで実践に進む

はじめに

信頼できる個体一覧は、装備の確認の出発点にすぎません。チームには種類ごとの説明が必要です。正常な個体はいくつか、何が破損しているか、何が足りず、何が計画を超えているかを示します。計画外の物品や必要数ゼロは、入力を無効にはしません。

このラボでは、その照合ロジックを実装し、異なる 2 組の実際の入力ファイルと照らして報告を読みます。独立した環境であり、前のラボの状態パーサーと個体収集処理は提供済みです。前の VM のファイルは不要です。前のラボの個体と矛盾に関する規則に加え、Rust の構造体、列挙型、HashMap、Result、ファイル読み込み、ループを理解していることを前提とします。著者の見積もりは読解と通常の修正を含め 45 分で、初心者による実測時間ではありません。

すべての種類と個体を照合する

このステップでは、関係する種類を特定し、完全な個体集合を集め、不足数と余剰数を計算する 3 回の編集で照合を作ります。Terminal を開きます。このシェルの検索パスに Cargo を追加し、独立して準備されたプロジェクトに入ります。新しいターミナルでは export を再実行し、場所が分からなくなったら cd を繰り返します。

export PATH=/home/labex/.cargo/bin:$PATH
cd /home/labex/project/field-kit

まず、すでに動く正常な結果を調べます。このラボでは状態パーサーと個体収集関数が提供済みです。

cat data/plan.tsv
cargo run --quiet -- assets data/scans.tsv

表にはスキャン 7 行ではなく実物の ID が 6 つあります。a、b、c は good の light-a 個体です。d は damaged の light-b、e と f は計画外の種類 other の good と damaged の個体です。表示名 Flashlight は異なる 2 つの計画上の種類 ID に使われています。計画には、スキャンのないロープが 2 本必要、スキャンのない予備品は必要数ゼロという指定もあります。完全なレポートに何を説明させるか考えるとき、これらの違いを保ってください。

編集するのは src/readiness.rs だけで、公開関数名と引数・戻り値の型を維持します。他のソース、Cargo.toml、rust-toolchain.toml、元の data/ ファイルすべては変更しません。プロジェクトディレクトリに別の実験用入力を作ります。コマンドインターフェースは実際に指定したパスを読み、サンプルへ置き換えたりしません。

TSV の限定ルールは同じです。計画の正確な 3 フィールドのヘッダーは type_id、name、required、スキャンは asset_id、type_id、condition で、実際のタブで区切ります。ID は ASCII 小文字で始まり、後続は小文字、数字、- です。計画の名称は空ではない表示可能な ASCII 文字列で、空白を含められます。数量は 0~999 の値を持つ十進数字で、先頭のゼロを許可します。表示名は重複できても、計画の種類 ID は重複できません。状態は正確に good か damaged です。LF/CRLF、末行の改行省略、後続の完全な空行を受け付けます。フィールド前後の空白は除かず、空白だけの行は空行ではなく、単独の復帰文字は改行ではありません。これは合成テキスト記録であり、実物の検査ではありません。

提供された型と未完成の関数を読みます。

cat src/lib.rs
cat src/readiness.rs

Need は計画の種類 ID、名称、必要数を持ちます。Scan は個体 ID、種類 ID、状態、診断用の行を持ちます。reconcile が受け取る &[Need] と &[Scan] は変更してはいけない借用スライスです。個体はすでに形式検証と個体収集を済ませ、各実物の ID は 1 回だけ現れます。返す Vec<TypeSummary> には計画とスキャンの種類の和集合、good/damaged の完全な ID 集合、必要数、不足数、余剰数が必要です。提供済みの整形処理が表示名を付け、行と ID リストをソートし、各個体の planned または unplanned の関係を表示します。

最初の編集:関係する種類を維持する

スキャン個体のない計画を考えます。必要数が正なら依然として不足し、必要数ゼロなら不足はゼロです。保護された原本を空にせず、別の空スキャン入力を作ります。printf は書式文字列を表示し、\t をタブ、\n を改行として解釈します。単一引用符がバックスラッシュを printf 用に残し、> がこの名前の実験ファイルを書きます。ヘッダーだけのファイルです。

printf 'asset_id\ttype_id\tcondition\n' > empty-scans.tsv
cat empty-scans.tsv

コードを変える前に、この入力で残す種類を予測してください。light-a、light-b、rope、zero で、必要数は 2、1、2、0 です。この入力にはスキャンだけにある種類はありません。それでも一般的な関数は両方のスライスから種類を見つけ、後で実際のスキャンから other を加えられる必要があります。

ソースだけの復元点を作ります。mktemp -d は 6 個の X を一意な接尾辞に置き換えて新しいディレクトリを作り、前のバックアップを再利用しません。$() は表示されたパスをセッション変数に格納します。&& はディレクトリ作成に成功した場合だけ cp -p を実行します。コピーはソースのメタデータを保ち、変数の引用符はパスをひとまとまりにします。どちらかが失敗したら、エラーを読み、編集前にバックアップを作成してください。

rs03_before_types=$(mktemp -d .edit-before-types.XXXXXX) && cp -p src/readiness.rs "$rs03_before_types/readiness.rs"

nano で既存ファイルを開きます。-c はカーソル位置を表示します。

nano -c src/readiness.rs

nano の ^ は Ctrl を意味します。Ctrl+W を押し、pub fn reconcile を検索して Enter を押します。関数の開始と終了の括弧を読みます。矢印と Backspace/Delete で本体と未使用の引数名だけを編集します。型を保ち、_needs と _assets を needs と assets に変えます。提供済みの use crate::{Need, Scan, TypeSummary}; を残してください。この段階ではまだ Condition を使いません。

データの関係からこの最初の段階を組み立てます。空の types vector を作り、計画上の各種類 ID を加えます。次に個体をたどり、types.contains(&asset.type_id) が false の場合だけ種類 ID を加えます。計画の種類 ID はすでに一意です。.clone() は借用した記録から所有する種類 ID の文字列をコピーし、結果が所有できるようにします。

次に空の summaries vector を作り、集めた各種類 ID をたどります。必要数をゼロから始め、計画内でその種類を探します。usize::from(need.required) は計画の u16 値を vector の長さで使うカウント型に変換します。今は good_ids と damaged_ids を空 vector にします。使用可能な個体をまだ数えていない間は missing に required を代入し、surplus をゼロにします。これらの変数で TypeSummary を追加し、summaries を関数の最後の式にします。type_id, required のような短いフィールド記法は、同名の変数を使います。まだ個体の分類は実装していません。

Ctrl+O、Enter で現在のファイル名を確認し、Ctrl+X で終了します。実行前に保存した関数を読みます。

cat src/readiness.rs

cargo run はプログラムをコンパイルして実行し、--quiet は Cargo の進行表示を隠し、-- は続く引数をプログラムに渡します。限定されたスキャンなしの場合を試します。

cargo run --quiet -- check data/plan.tsv empty-scans.tsv

計画の 4 種類、good 個体ゼロ、空の ID 集合、不足数 2、1、2、0、余剰数ゼロになるはずです。ASSET ヘッダーの後に個体データ行があってはいけません。この最初の段階が正しいのは、この空スキャンの観察に対してだけです。空でない個体リストの完全な実装ではありません。 終了状態ゼロを、すべての照合ルールが完成した証拠にしないでください。

2 回目の編集:状態ごとの完全な集合を保つ

実際のスキャンの完全な集合を予測します。light-a の good ID は a,b,c、light-b の damaged ID は d、other の good ID は e、damaged ID は f です。rope と zero は個体がなくても要約が必要です。優先する「余分な」個体を選ぶのではなく、各集合を完全に保ちます。

動作する種類の和集合の段階を保存します。

rs03_before_sets=$(mktemp -d .edit-before-sets.XXXXXX) && cp -p src/readiness.rs "$rs03_before_sets/readiness.rs"
nano -c src/readiness.rs

先頭の crate の import を、これから使う列挙型も含むように変えます。

use crate::{Condition, Need, Scan, TypeSummary};

これは明示的なソース編集です。元の開始コードは Condition を取り込んでいません。crate はこのライブラリーを指し、use はその名前をこのモジュールに取り込みます。最初に使う時点で import を加えると、前の段階での未使用 import 警告も避けられます。

pub fn reconcile を検索します。種類の和集合と必要数のロジックは残します。ID を追加するため、good_ids と damaged_ids を変更可能にします。各種類のループ内ですべての個体をたどり、現在の type_id に一致するものを選んで状態を match します。Good の場合は個体 ID のコピーを good vector に、Damaged なら damaged vector に加えます。両方とも有効な入力です。破損した個体や計画外の個体を削除しないでください。不足数と余剰数の一時的な 2 つの代入は、次の編集まで変えません。

保存して終了し、実際のスキャンで実行します。

cargo run --quiet -- check data/plan.tsv data/scans.tsv

種類 ID、必要数、good 数、完全な ID 集合、ASSET 行を読みます。other も含めて予測と一致するはずです。不足数と余剰数の列はまだ一時的で、一般に正しい結果ではありません。 たとえば light-a では good ID が 3 つあっても、一時的な不足数はまだ 2 と表示され得ます。これが完成したレポートではない理由を説明してください。次にこの 2 計算を直します。この中間の表が最終チェックに合格するとは考えないでください。

3 回目の編集:good だけで不足数と余剰数を計算する

需要を満たすのは一意な good 個体だけです。light-a は必要数 2、good 3 なので不足 0、余剰 1 です。light-b の damaged 個体は必要数を満たさず、不足 1、余剰 0 です。rope は不足 2、zero は 0/0 のままです。計画外の other は必要数ゼロで good 個体が 1 つなので余剰 1 です。その damaged ID は残しますが、余剰には加えません。

正しい集合をすでに集める段階を保存します。

rs03_before_totals=$(mktemp -d .edit-before-totals.XXXXXX) && cp -p src/readiness.rs "$rs03_before_totals/readiness.rs"
nano -c src/readiness.rs

pub fn reconcile を検索し、一時的な不足数と余剰数の式だけを置き換えます。vector の len() が個数を返します。saturating_sub はゼロ未満にならずに減算し、符号なし数のアンダーフローを避けます。規則は missing = max(required - good, 0) と surplus = max(good - required, 0) です。生のスキャン数や damaged ID 数ではなく、必ず good_ids.len() に適用してください。種類の和集合と完全な vector は維持します。Ctrl+O、Enter、Ctrl+X で保存して終了します。

cargo run --quiet -- check data/plan.tsv data/scans.tsv
TYPE	type_id	name	required	good	missing	surplus	good_ids	damaged_ids
TYPE	light-a	Flashlight	2	3	0	1	a,b,c	-
TYPE	light-b	Flashlight	1	0	1	0	-	d
TYPE	other	-	0	1	0	1	e	f
TYPE	rope	Rope	2	0	2	0	-	-
TYPE	zero	Spare	0	0	0	0	-	-
ASSET	asset_id	type_id	condition	plan
ASSET	a	light-a	good	planned
ASSET	b	light-a	good	planned
ASSET	c	light-a	good	planned
ASSET	d	light-b	damaged	planned
ASSET	e	other	good	unplanned
ASSET	f	other	damaged	unplanned

今はどの数量もたどれます。light-b は damaged ID の d を保ったまま不足し、other は両 ID を保ちますが余剰を作るのは e だけです。a の反復スキャンは別の実物を生んでいません。完成した関数で、前の空ソースのチェックを繰り返します。

cargo run --quiet -- check data/plan.tsv empty-scans.tsv

計画の 4 種類とスキャンゼロでの不足数は、今も正しくなければなりません。この時点で初めて、このステップの完全な照合チェックの合格を期待できます。提供済み整形処理が表示行と ID リストをソートするので、内部の収集順序は同等の別順でも有効です。

プロジェクトをリセットせずソースの編集を直す

Cargo のエラーはファイルと行を示します。そのファイルを開き直し、Ctrl+W で関数を探して示された編集を直します。誤変更をまだ保存していなければ、Ctrl+X、保存の質問に N で破棄できます。それ以前の保存済み編集は元に戻りません。

保存済みのミスから回復する場合だけ、やり直したい段階の復元点を選びます。.edit-before-types.* は開始コード、.edit-before-sets.* は種類の和集合の段階、.edit-before-totals.* は最終計算前の完全な ID 集合を含みます。新しい Terminal では PATH と cd を再実行し、実際のバックアップ名を列挙します。

ls -d .edit-before-*

-d は中身ではなくディレクトリ名を列挙し、* は生成された接尾辞に一致します。次を実行し、シェルが待つ間に表示された正確なディレクトリ名を 1 つ貼り付け、Enter を押します。read は選択をこのセッション変数に格納します。

read rs03_restore

コピーする前に選んだソースを確認します。

cat "$rs03_restore/readiness.rs"

パスや段階が違えば選択と確認を繰り返します。一致するバックアップがなければ、パスを推測せず参照コードを使って関数を直してください。意図した段階と確認した後、次のコマンドで許可された readiness のソースだけを意図的に置き換えます。

cp -p "$rs03_restore/readiness.rs" src/readiness.rs

その後、対象の編集とテストをやり直します。ソースの復元点は入力を変えません。誤ったアルゴリズムに合わせて元の計画を変更しないでください。次の参照コードは、試した後で特定の部分を比較するためのもので、標準的にファイル全体を置き換えるためのものではありません。

比較と回復用の完全な照合の参照コード
use crate::{Condition, Need, Scan, TypeSummary};

pub fn reconcile(needs: &[Need], assets: &[Scan]) -> Vec<TypeSummary> {
    let mut types = Vec::new();
    for need in needs {
        types.push(need.type_id.clone());
    }
    for asset in assets {
        if !types.contains(&asset.type_id) {
            types.push(asset.type_id.clone());
        }
    }
    let mut summaries = Vec::new();
    for type_id in types {
        let mut required = 0;
        for need in needs {
            if need.type_id == type_id {
                required = usize::from(need.required);
            }
        }
        let mut good_ids = Vec::new();
        let mut damaged_ids = Vec::new();
        for asset in assets {
            if asset.type_id == type_id {
                match asset.condition {
                    Condition::Good => good_ids.push(asset.asset_id.clone()),
                    Condition::Damaged => damaged_ids.push(asset.asset_id.clone()),
                }
            }
        }
        let missing = required.saturating_sub(good_ids.len());
        let surplus = good_ids.len().saturating_sub(required);
        summaries.push(TypeSummary {
            type_id, required, good_ids, damaged_ids, missing, surplus,
        });
    }
    summaries
}

別の入力元から部分的な充足を説明する

このステップでは、異なる入力パスで部分的な不足と、破損個体しかない計画外の種類を調べます。引き続き Terminal の /home/labex/project/field-kit で作業します。元の data/ ファイルをすべて維持し、代わりに次の別の試行ファイル 2 つを作ります。

次の短い入力ブロックは here-document を使います。cat が後続の行を入力として受け、> が指定の実験ファイルに書き、<<'TSV' が TSV だけの行までのリテラル入力を始めます。引用した区切りは内容のシェル展開を防ぎます。両方の区切り行をコピーし、実際のタブを保ってください。これは小さなテスト入力で、Rust モジュールの置き換えではありません。Water filter の空白は 1 つのフィールド内にあり、フィールドを分けるのはタブです。

cat > trial-plan.tsv <<'TSV'
type_id	name	required
lamp	Lamp	2
filter	Water filter	5
zero	Spare	0
TSV
cat > trial-scans.tsv <<'TSV'
asset_id	type_id	condition
l-one	lamp	good
l-two	lamp	damaged
f-one	filter	good
f-two	filter	good
u-one	unplanned	damaged
TSV

実行前に、電灯の不足数、フィルターの不足数、破損個体だけの計画外の種類が残るかを予測します。次に実際のこれらのファイルを指定します。

cargo run --quiet -- check trial-plan.tsv trial-scans.tsv
TYPE	type_id	name	required	good	missing	surplus	good_ids	damaged_ids
TYPE	filter	Water filter	5	2	3	0	f-one,f-two	-
TYPE	lamp	Lamp	2	1	1	0	l-one	l-two
TYPE	unplanned	-	0	0	0	0	-	u-one
TYPE	zero	Spare	0	0	0	0	-	-
ASSET	asset_id	type_id	condition	plan
ASSET	f-one	filter	good	planned
ASSET	f-two	filter	good	planned
ASSET	l-one	lamp	good	planned
ASSET	l-two	lamp	damaged	planned
ASSET	u-one	unplanned	damaged	unplanned

レポートを解釈する前に、スキャン数ではなく good 個体から電灯とフィルターの不足を予測してください。電灯はスキャン 2 行でも不足 1 です。good のフィルター 2 個は必要数 5 のうち 2 個しか満たさず、不足 3 です。破損した計画外の個体は good ゼロ、余剰ゼロで表示に残ります。試行のスキャンに一度も出ない zero も残ります。

元のデータや最初の試行スキャンを編集せず、制御した変更を 1 つ試します。cp -p はコピーしたファイルのメタデータを保ってコピーを作ります。別の新しいファイル名を使ってください。前の試行ですでに使った場合はその作業を残すか、未使用の別名を選び、後続のコマンドでも一貫して使います。

cp -p trial-scans.tsv trial-scans-one-repair.tsv
nano -c trial-scans-one-repair.tsv

Ctrl+W で l-two を検索し、Enter を押します。その行だけで矢印と Backspace/Delete を使い、状態語 damaged を good に置き換えます。個体 ID、種類 ID、ヘッダー、実際のタブ区切りは維持します。Ctrl+O、Enter で保存し、Ctrl+X で終了します。これは別の実験への意図的な編集で、保護された証拠の修復ではありません。

実行前に、影響するレポートの 2 行を予測します。TYPE lamp は good ID が 2 つ、damaged ID なし、不足ゼロ、余剰ゼロになるはずです。ASSET l-two は planned のまま状態が good に変わります。他の種類行と個体行はすべて不変です。フィルターの必要数も、破損した計画外個体も変えていません。

cargo run --quiet -- check trial-plan.tsv trial-scans-one-repair.tsv

その 2 行を読み、他の行も一致したままか確認します。他にも変わったら cat でコピーを調べ、意図せず変えたフィールドを直します。偶然の入力編集に合わせるだけのためにアルゴリズムを変えないでください。変更していない最初の試行ソースに戻ります。

cargo run --quiet -- check trial-plan.tsv trial-scans.tsv

l-two が damaged で、電灯の不足が 1 に戻るはずです。レポートが異なるのは実際に指定した入力が異なるためで、どちらのソースも黙って置き換えられてはいません。同じ完成した照合実装で両方を説明する必要があります。

入力エラーを不足レポートにしたり、黙って元の例へ代替したりしてはいけません。次の誤ったファイル名は意図的で、非ゼロを返すはずです。

cargo run --quiet -- check trial-plans.tsv trial-scans.tsv

存在しない trial-plans.tsv をエラーが示します。複数形のつづりを直し、再実行します。

cargo run --quiet -- check trial-plan.tsv trial-scans.tsv

意図した形式エラーには、既存の未知状態のテストデータを使います。このコマンドも非ゼロになり、data/bad-status.tsv と行番号を示すはずです。

cargo run --quiet -- check trial-plan.tsv data/bad-status.tsv

broken は不正な語ですが、damaged は需要を満たさない有効な状態です。有効な試行スキャンを再選択して回復します。

cargo run --quiet -- check trial-plan.tsv trial-scans.tsv

最後に元の入力に戻ります。

cargo run --quiet -- check data/plan.tsv data/scans.tsv

元のレポートが戻るのは、プログラムがキャッシュしたレポートや固定の準備完了フラグではなく実際のパスを読むからです。元と試行の両ソースは無傷で残ります。どれも cat で調べられます。試行ファイルを誤編集したら、上の完全な作成ブロックを再実行して回復します。保護原本はコードのエラーを修復する場所ではありません。

まとめ

計画と一意の個体記録から、種類ごとの完全な照合を実装しました。正常な装備品だけが必要数を満たし、部分的な充足では不足が残り、破損品と余剰品も説明に残りました。別の実際の入力パスの組で試し、通常のエラーを処理し、すべての入力元を保ちました。チャレンジでは、別の旅行用装備について、個体の収集と照合を組み合わせます。