Skip to main content
5 of 9
added 2109 characters in body

Don't use &String

in function parameters. It has virtually no use. Instead use &str, since it can be implicitly converted from &String, but not the other way around.

Handle errors

You currently happily unwrap every character.to_digit(10), though this may fail if e.g. letters are provided.

Check for valid input

The input should be a string of exactly 9 digits. Your code happily accepts any amount of digits, though the result of their processing will not constitute a valid ISBN.

Represent the ISBN as a struct

So you can have an object that guarantees the representation of a valid ISBN.

Suggested:

Cargo.toml

[package]
name = "isbn"
version = "0.1.0"
edition = "2021"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
clap = { version = "4.3.8", features = ["derive"] }

src/main.rs

use clap::Parser;
use isbn::Isbn;
use std::str::FromStr;

#[derive(Debug, Parser)]
struct Arguments {
    #[arg(index = 1)]
    digits: String,
}

fn main() {
    let arguments = Arguments::parse();

    match Isbn::from_str(arguments.digits.as_str()) {
        Ok(isbn) => println!("The ISBN is: {isbn}"),
        Err(error) => eprintln!("{error}"),
    }
}

src/lib.rs

use std::fmt::{Display, Formatter};
use std::str::FromStr;

const FACTORS: [u8; 9] = [10, 9, 8, 7, 6, 5, 4, 3, 2];

pub enum Error {
    InvalidDigit(char),
    InvalidLength(usize),
}

impl Display for Error {
    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
        match self {
            Self::InvalidDigit(chr) => write!(f, "Invalid digit: {chr}"),
            Self::InvalidLength(len) => write!(f, "Invalid length: {len}"),
        }
    }
}

pub struct Isbn([u8; 10]);

impl Display for Isbn {
    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
        for digit in self.0 {
            if digit == 10 {
                write!(f, "X")?;
            } else {
                write!(f, "{digit}")?;
            }
        }

        Ok(())
    }
}

impl From<[u8; 9]> for Isbn {
    fn from(value: [u8; 9]) -> Self {
        Self([
            value[0],
            value[1],
            value[2],
            value[3],
            value[4],
            value[5],
            value[6],
            value[7],
            value[8],
            calculate_checksum(&value),
        ])
    }
}

impl FromStr for Isbn {
    type Err = Error;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        let mut digits = Vec::new();

        for c in s.chars() {
            if let Some(digit) = c.to_digit(10).and_then(|digit| u8::try_from(digit).ok()) {
                digits.push(digit);
            } else {
                return Err(Error::InvalidDigit(c));
            }
        }

        let array: [u8; 9] = match digits.try_into() {
            Ok(array) => array,
            Err(error) => return Err(Error::InvalidLength(error.len())),
        };

        Ok(Self::from(array))
    }
}

fn calculate_checksum(digits: &[u8; 9]) -> u8 {
    let dot_product: u32 = digits
        .iter()
        .zip(FACTORS)
        .map(|(&x, y)| u32::from(x) * u32::from(y))
        .sum();

    for checksum in 0..=10u8 {
        if (dot_product + u32::from(checksum)) % 11 == 0 {
            return checksum;
        }
    }

    0
}

Or, alternatively, if you want to only store the first 9 digits of the ISBN and calculate its checksum on the fly:

src/lib.rs

use std::fmt::{Display, Formatter};
use std::str::FromStr;

const FACTORS: [u8; 9] = [10, 9, 8, 7, 6, 5, 4, 3, 2];

pub enum Error {
    InvalidDigit(char),
    InvalidLength(usize),
}

impl Display for Error {
    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
        match self {
            Self::InvalidDigit(chr) => write!(f, "Invalid digit: {chr}"),
            Self::InvalidLength(len) => write!(f, "Invalid length: {len}"),
        }
    }
}

pub struct Isbn([u8; 9]);

impl Isbn {
    #[must_use]
    pub fn checksum(&self) -> u8 {
        let dot_product: u32 = self
            .0
            .iter()
            .zip(FACTORS)
            .map(|(&x, y)| u32::from(x) * u32::from(y))
            .sum();

        for checksum in 0..=10u8 {
            if (dot_product + u32::from(checksum)) % 11 == 0 {
                return checksum;
            }
        }

        0
    }
}

impl Display for Isbn {
    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
        for digit in self.0 {
            write!(f, "{digit}")?;
        }

        match self.checksum() {
            10 => write!(f, "X"),
            checksum => write!(f, "{checksum}"),
        }
    }
}

impl From<[u8; 9]> for Isbn {
    fn from(value: [u8; 9]) -> Self {
        Self(value)
    }
}

impl FromStr for Isbn {
    type Err = Error;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        let mut digits = Vec::new();

        for c in s.chars() {
            if let Some(digit) = c.to_digit(10).and_then(|digit| u8::try_from(digit).ok()) {
                digits.push(digit);
            } else {
                return Err(Error::InvalidDigit(c));
            }
        }

        let array: [u8; 9] = match digits.try_into() {
            Ok(array) => array,
            Err(error) => return Err(Error::InvalidLength(error.len())),
        };

        Ok(Self::from(array))
    }
}
```