95 lines
3.7 KiB
Rust
95 lines
3.7 KiB
Rust
//! Line checksums for the two-line element format.
|
||
//!
|
||
//! Each line ends with a single digit that is the checksum of the preceding
|
||
//! columns. The checksum is the sum of the decimal digits on the line, where
|
||
//! a minus sign contributes one and every other character contributes
|
||
//! nothing, reduced modulo ten.
|
||
//!
|
||
//! The checksum digit itself is the last character of the line and is not
|
||
//! part of the sum.
|
||
|
||
use crate::field::{ElementError, ElementErrorKind, Result};
|
||
|
||
/// Returns the checksum of the first 68 columns of a line.
|
||
///
|
||
/// The input may include the trailing checksum digit, which is ignored
|
||
/// because only the first 68 columns contribute to the sum. A line shorter
|
||
/// than 68 characters contributes only the columns it has.
|
||
///
|
||
/// A minus sign contributes one, a decimal digit contributes its value, and
|
||
/// every other character contributes nothing.
|
||
///
|
||
/// 返回某行前 68 列的校验和。
|
||
///
|
||
/// 输入可以包含末尾的校验和数字,该位被忽略,因为只有前 68 列参与求和。短于
|
||
/// 68 字符的行只贡献其实际具有的列。
|
||
///
|
||
/// 负号贡献 1,十进制数字贡献其数值,其余字符不贡献。
|
||
pub fn line_checksum(line: &str) -> u32 {
|
||
line.chars()
|
||
.take(68)
|
||
.map(|character| match character {
|
||
'-' => 1,
|
||
'0'..='9' => character as u32 - '0' as u32,
|
||
_ => 0,
|
||
})
|
||
.sum::<u32>()
|
||
% 10
|
||
}
|
||
|
||
/// Verifies that the last character of a line is its checksum.
|
||
///
|
||
/// The line must be at least 69 characters long, because the checksum
|
||
/// occupies column 69. The expected value is compared against
|
||
/// [`line_checksum`] of the same line.
|
||
///
|
||
/// `line_number` is the one based number used in any error that is
|
||
/// returned, so that a caller parsing a multi-line input can report which
|
||
/// line failed.
|
||
///
|
||
/// 校验某行末位字符是否为其校验和。
|
||
///
|
||
/// 该行至少须有 69 个字符,因为校验和位于第 69 列。期望值与同一行的
|
||
/// [`line_checksum`] 比较。
|
||
///
|
||
/// `line_number` 是返回错误时使用的从一开始的行号,使解析多行输入的调用方
|
||
/// 能指出是哪一行失败。
|
||
pub fn verify_line_checksum(line: &str, line_number: usize) -> Result<()> {
|
||
let checksum_columns = (69, 69);
|
||
// The line must contain at least 69 characters. Counting characters
|
||
// rather than bytes is deliberate: the format is ASCII, but a caller
|
||
// may hand in text with a non-ASCII character by mistake, and counting
|
||
// characters gives a clearer failure than slicing through one.
|
||
// 该行至少须含 69 个字符。刻意按字符而非字节计数:格式虽为 ASCII,但
|
||
// 调用方可能误传含非 ASCII 字符的文本,按字符计数给出的失败信息比
|
||
// 从多字节字符中间切片更清晰。
|
||
let characters: Vec<char> = line.chars().collect();
|
||
if characters.len() < 69 {
|
||
return Err(ElementError::new(
|
||
ElementErrorKind::TooShort,
|
||
line_number,
|
||
Some(checksum_columns),
|
||
)
|
||
.with_detail(format!("found {} characters, need 69", characters.len())));
|
||
}
|
||
let expected = characters[68];
|
||
let Some(expected) = expected.to_digit(10) else {
|
||
return Err(ElementError::new(
|
||
ElementErrorKind::InvalidField,
|
||
line_number,
|
||
Some(checksum_columns),
|
||
)
|
||
.with_detail(format!("checksum character is {expected:?}, not a digit")));
|
||
};
|
||
let actual = line_checksum(line);
|
||
if actual != expected {
|
||
return Err(ElementError::new(
|
||
ElementErrorKind::Checksum,
|
||
line_number,
|
||
Some(checksum_columns),
|
||
)
|
||
.with_detail(format!("computed {actual}, line states {expected}")));
|
||
}
|
||
Ok(())
|
||
}
|