Skip to content

← Generics and Traits step 10 of 24

Medium Primitives

TryFrom and TryInto: validate a percentage

Validate integer percentages using TryFrom, then format each conversion result.

pub struct Percentage(pub u8);
pub fn convert(vals: Vec<i64>) -> Vec<String>

Implement TryFrom<i64> for Percentage and an error type with two variants: NotAByte(std::num::TryFromIntError) and TooLarge(u8).

Check the byte range before the percentage range

First call u8::try_from(value). If that fails, return NotAByte containing the original conversion error. If it succeeds, accept values through 100 inclusive and return TooLarge for larger bytes.

In convert, use .try_into() for each input. Format a successful percentage as "<value>%", and format an error with .to_string() using these exact messages:

Input range Output
0..=100 The value followed by %, such as "50%".
101..=255 "too large: <value>", such as "too large: 101".
Below 0 or above 255 "not a byte".

Preserve input order and duplicates. Return one string per input, including errors; an empty input returns an empty vector. Do not clamp, wrap or skip invalid values. In particular, 256 produces "not a byte", not "too large: 256".

input  = [0, 100, 101, 255, 256, -1]
output = ["0%", "100%", "too large: 101", "too large: 255",
          "not a byte", "not a byte"]

The conversion enforces 0..=100. The supplied public tuple field does not enforce that invariant by itself: code can still construct Percentage(200) directly. An API that must prevent that would keep its field private and expose checked constructors.

Implement TryFrom to get TryInto

TryFrom provides a standard interface for a conversion that may fail. Its associated Error type identifies the failure returned in Result. Implementing TryFrom<i64> for Percentage also supplies TryInto<Percentage> for i64 through the standard library's blanket implementation. Adding that same TryInto implementation yourself would conflict.

Prefer implementing TryFrom for this task. That is a convention supported by the blanket implementation, not a rule that directly implementing TryInto is always illegal.

The target type sometimes needs to be stated at the call site:

let parsed: Result<Percentage, PercentError> = value.try_into();

Give errors a useful display and source

Implement Display for the two exact messages above. This supplies the formatting used by .to_string().

Also implement std::error::Error. Its source() should return the contained TryFromIntError for NotAByte, and None for TooLarge. The source preserves the underlying cause even though the displayed message is short. Deriving Debug provides the other formatting trait required by Error.

The conversion can start with:

let byte = u8::try_from(value).map_err(PercentError::NotAByte)?;

The enum variant constructor wraps the integer-conversion error. ? then returns that error early, or yields the byte for the second check. This propagation works for other Result-returning functions too; it is not a special capability restricted to conversion traits.

Why the starter's Infallible error is unsuitable

The starter clamps every input into 0..=100, always returns Ok, and declares Infallible as its error. It therefore violates the required behavior for invalid inputs. Clippy also flags its explicit infallible TryFrom implementation.

Infallible has no variants, so a valid value of that type cannot be produced. An empty match can express that an error branch is impossible. Here is a complete example with an actually infallible conversion:

use std::convert::Infallible;

fn unwrap_infallible<T>(result: Result<T, Infallible>) -> T {
    match result {
        Ok(value) => value,
        Err(error) => match error {},
    }
}

fn main() {
    let byte = 7_u8;
    let number: i64 = i64::from(byte);
    assert_eq!(unwrap_infallible(Ok(number)), 7);
}

Use From when the conversion has an appropriate infallible implementation, such as u8 to i64. Use TryFrom when you need a checked conversion, such as i64 to u8. Do not choose solely by type size: conversions also have semantic requirements, such as this task's percentage limit.

A passing submission must compile, pass the tests, and pass clippy -D warnings. The trait and error-source design above is part of the exercise; output checks alone cannot establish every implementation detail.