介绍
可靠的资产清单只是装备核对的起点。小组需要每种装备的解释:有多少完好资产、哪些损坏、缺少什么,以及什么超出计划。计划外物品或零需求量都不会使输入变成非法数据。
在本实验中,你将实现这些核对逻辑,并根据两组不同的实际输入文件阅读结果。这是一个独立环境,已经提供前一个实验中的状态解析器和身份收集器,不需要其 VM 中的文件。你应已掌握前一实验的身份、冲突规则,以及 Rust 结构体、枚举、HashMap、Result、文件读取和循环。作者估计需要 45 分钟,包含阅读与普通纠错;这并非经过初学者实测的完成时间。
核对每种类型与每件资产
这一步分三次编辑构建核对逻辑:确定相关类型、收集完整的资产集合,最后计算缺少量和多余量。打开 Terminal。将 Cargo 加入当前 shell 的搜索路径,然后进入本单元独立准备的项目。在新终端中需要重新执行 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
表中有六个实物 ID,而不是七条扫描记录。a、b、c 是状态完好的 light-a 资产。d 是损坏的 light-b;e 和 f 分别是计划外类型 other 的完好和损坏资产。显示名称 Flashlight 对应两个不同的计划内类型 ID。计划还要求两根绳子,但没有相关扫描;备用品的需求为零,也没有扫描。在判断完整报告必须解释什么时,要保留这些区别。
只编辑 src/readiness.rs,保留其公开函数名以及参数和返回值类型。其他源文件、Cargo.toml、rust-toolchain.toml 和所有原始 data/ 文件都保持不变。你将在项目目录中创建单独的试验输入。命令接口读取你指定的真实路径,不会替换成样例。
受控 TSV 格式契约保持不变:计划的三字段表头必须精确为 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 只出现一次。返回的 Vec<TypeSummary> 必须包含计划内类型与扫描中类型的并集、完整的完好/损坏 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 用唯一后缀替换六个 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 向量,追加每个计划内类型 ID。然后遍历资产,只有当 types.contains(&asset.type_id) 为假时才追加类型 ID。计划中的类型 ID 已保证唯一。.clone() 从借用的记录中复制一个拥有所有权的类型 ID 字符串,让结果可以拥有它。
接着创建空的 summaries 向量,遍历收集到的每个类型 ID。先将需求数量设为零,再在计划中查找该类型。usize::from(need.required) 将计划的 u16 值转换为向量长度使用的计数类型。暂时使用空的 good_ids 和 damaged_ids 向量。在尚未统计可用资产时,将 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
应看到这四种计划内类型,完好资产数为零,ID 集合为空,缺少量分别为 2、1、2、0,多余量均为零。ASSET 表头之后不应有资产数据行。第一阶段只对这次空扫描观察正确,还不是适用于非空资产列表的完整实现。 退出状态为零不代表所有核对规则都已完成。
第二次编辑:保留完整的状态集合
现在预测真实扫描的完整集合:light-a 的完好 ID 为 a,b,c;light-b 的损坏 ID 为 d;other 的完好 ID 为 e,损坏 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 导入修改为包含现在要使用的枚举:
use crate::{Condition, Need, Scan, TypeSummary};
这是一项明确的源码编辑:原始起始代码没有导入 Condition。crate 指当前库;use 将其中的名称引入当前模块。在首次使用时再加入导入,也能避免前一阶段出现未使用导入的警告。
搜索 pub fn reconcile。保留类型并集和需求数量逻辑。将 good_ids、damaged_ids 改为可变,因为接下来要追加 ID。在每个类型的循环中遍历全部资产,选择当前 type_id 对应的资产,并匹配状态。对于 Good 变体,将资产 ID 的克隆追加到完好向量;对于 Damaged,追加到损坏向量。这两个变体都是合法输入。不要删除损坏或计划外资产。暂时保留缺少量和多余量的两处临时赋值,下一次编辑再修改。
保存、退出,然后使用实际扫描运行:
cargo run --quiet -- check data/plan.tsv data/scans.tsv
阅读类型 ID、需求数量、完好数量、完整 ID 集合和 ASSET 行。它们应符合预测,并包含 other。缺少量和多余量列仍是临时值,不能作为通用结果。 例如,虽然 light-a 已有三个完好 ID,临时缺少量仍可能显示为 2。解释为什么这还不能算完成的报告。下一步将修正这两项计算;不要声称这张中间表已经通过最终检查。
第三次编辑:只根据完好资产计算短缺与多余量
只有完好且唯一的资产才能满足需求。对于 light-a,需求为 2、完好数为 3,意味着缺少 0、多余 1。对于 light-b,损坏资产不能满足需求:缺少 1、多余 0。rope 缺少 2。zero 保持 0/0。计划外的 other 需求为零,且有一件完好资产,因此多余量为 1;损坏 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,只替换临时的缺少量和多余量表达式。向量的 len() 返回元素数量。saturating_sub 执行减法且结果不会低于零,避免无符号整数下溢。规则是 missing = max(required - good, 0) 和 surplus = max(good - required, 0);将其应用于 good_ids.len(),绝不能使用原始扫描数或损坏 ID 数。保留类型并集和完整向量。按 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 保留损坏 ID d,但仍有短缺;other 保留两个 ID,但只有 e 产生多余量。a 的重复扫描从未产生另一件实物资产。使用完成后的函数重复之前的空来源检查:
cargo run --quiet -- check data/plan.tsv empty-scans.tsv
其中四种计划内类型及无扫描时的短缺量仍须正确。只有到了现在,这一步的完整核对检查才应通过。内部收集顺序可以采用等效实现,因为已有的格式化器会对显示行和 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 列出目录名称而不是目录内容,* 匹配生成的后缀。运行以下命令;shell 等待输入时,粘贴输出中的一个准确目录名并按 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/ 文件,改为创建下面两个单独的试验文件。
下面的短输入块使用 here-document。cat 接收后续各行作为输入,> 写入具名试验文件,<<'TSV' 开始按字面接收内容,直到遇到只包含 TSV 的一行。带引号的分隔标记会阻止 shell 展开内容。复制起止两行并保留真实制表符。这些是小型测试输入,不是 Rust 模块的替代品。名称 Water filter 在一个字段内部包含空格;字段之间用制表符分隔。
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
解读报告前,根据完好资产而非扫描数,预测灯具和滤水器的短缺量。虽然灯具有两条扫描行,仍缺少一件。两个完好的滤水器只能满足五件需求中的两件,因此还缺三件。损坏的计划外资产仍然可见,完好数和多余量都为零。即使试验扫描中没有提到 zero,它也仍然出现。
做一次受控修改,不编辑原始数据,也不编辑第一份试验扫描。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 退出。这是有意编辑独立试验,不是在修补受保护的证据。
运行前预测受影响的两行:TYPE lamp 现在应有两个完好 ID,没有损坏 ID,缺少量和多余量均为零。ASSET l-two 的状态应变为 good,但仍属于计划内。其他所有类型行和资产行都应保持不变;滤水器需求及损坏的计划外资产没有变化。
cargo run --quiet -- check trial-plan.tsv trial-scans-one-repair.tsv
阅读这两行,确认其他行仍一致。如果还有其他变化,用 cat 查看副本,纠正误改字段;不要为了匹配意外输入修改而改算法。回到未修改的第一份试验来源:
cargo run --quiet -- check trial-plan.tsv trial-scans.tsv
灯具缺少量应恢复为一,l-two 为损坏状态。两份报告不同,是因为指定的真实输入不同;任何来源都没有被静默替换。同一个已完成的核对实现必须能解释这两种情况。
输入错误不能变成短缺报告,也不能静默退回原始样例。下面故意使用错误文件名,应返回非零状态:
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 查看任一文件;若误改试验文件,重新执行上面创建它的完整代码块即可恢复。不要通过修改受保护的原始文件来修复代码错误。
总结
你根据计划和唯一资产记录,实现了完整的类型核对。只有完好资产满足需求;部分满足仍然留下缺口,损坏和富余装备都得到了清楚说明。你还用另一组实际输入路径测试,处理了普通错误,并保留所有源文件。挑战将要求你为另一套出行装备组合身份收集和核对逻辑。



