Skip to main content
9 of 9
added 108 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];

#[derive(Debug)]
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}"),
        }
    }
}

#[derive(Debug)]
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];

#[derive(Debug)]
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}"),
        }
    }
}

#[derive(Debug)]
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))
    }
}

You can even optimize the storage space of the Isbn further by just storing its numerical value, which maxes out at 999999999 and thus fits into a u32:

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];

#[derive(Debug)]
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}"),
        }
    }
}

#[derive(Debug)]
pub struct Isbn(u32);

impl Isbn {
    #[must_use]
    pub fn digits(&self) -> String {
        self.0.to_string()
    }

    #[must_use]
    pub fn checksum(&self) -> u8 {
        let dot_product: u32 = self
            .digits()
            .chars()
            .map(|c| c.to_digit(10).unwrap_or_else(|| unreachable!()))
            .zip(FACTORS)
            .map(|(x, y)| x * u32::from(y))
            .sum();

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

        0
    }

    fn from_digits(digits: &[u8]) -> Result<Self, Error> {
        if digits.len() != 9 {
            return Err(Error::InvalidLength(digits.len()));
        }

        Ok(Self(
            digits
                .iter()
                .rev()
                .enumerate()
                .map(|(exponent, digit)| {
                    u32::from(*digit)
                        * 10u32.pow(u32::try_from(exponent).unwrap_or_else(|_| unreachable!()))
                })
                .sum(),
        ))
    }
}

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

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

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));
            }
        }

        Self::from_digits(digits.as_slice())
    }
}

Here I implemented from_digits() as a private method of Isbn and not via a TryFrom<&[u8]> implementation, since it would need additional checks to guarantee that each digit is in {0..=9} if it was exposed publicly where this prerequisite is not guaranteed.