Rust Tutorial

Letters to Numbers in Rust — Complete Code Guide

Whether you're building a cipher tool, solving puzzles, or processing text data, converting letters to numbers is a common task. Use our free letters to numbers converterfor instant results, or follow this guide to implement it yourself in Rust. Rust's zero-cost abstractions, strong type system, and byte-level control make it an excellent choice for text encoding — you get safety without sacrificing speed.

Basic Letter-to-Number Conversion

Rust represents characters as Unicode scalar values with the char type, but for ASCII letters the underlying byte values follow a predictable pattern. Uppercase A is 0x41 (65) and lowercase a is 0x61 (97). To get the alphabet position (A=1, Z=26), subtract the base and add one.

The idiomatic way to do this in Rust uses byte literals (b'a', b'A') and a cast:

/// Convert a single letter to its alphabet position (A=1, Z=26).
/// Returns None for non-alphabetic characters.
fn letter_to_number(c: char) -> Option<u32> {
    if c.is_ascii_uppercase() {
        Some((c as u8 - b'A' + 1) as u32)
    } else if c.is_ascii_lowercase() {
        Some((c as u8 - b'a' + 1) as u32)
    } else {
        None
    }
}

fn main() {
    assert_eq!(letter_to_number('A'), Some(1));
    assert_eq!(letter_to_number('Z'), Some(26));
    assert_eq!(letter_to_number('m'), Some(13));
    assert_eq!(letter_to_number('5'), None);
    assert_eq!(letter_to_number(' '), None);
    println!("All assertions passed!");
}

A more compact version normalizes to lowercase first using to_ascii_lowercase():

fn letter_to_number(c: char) -> Option<u32> {
    if c.is_ascii_alphabetic() {
        Some((c.to_ascii_lowercase() as u8 - b'a' + 1) as u32)
    } else {
        None
    }
}

Both versions return Option<u32>, which forces callers to handle the case where the input is not a letter. This is idiomatic Rust — no exceptions, no sentinel values, just explicit types.

Converting Entire Strings

Real-world use cases rarely involve single characters. You typically need to convert an entire string of text into a vector of numbers. Rust's iterator chains make this concise and efficient.

Basic Iterator Approach

/// Convert a string to a Vec of alphabet positions,
/// skipping non-alphabetic characters.
fn text_to_numbers(text: &str) -> Vec<u32> {
    text.chars()
        .filter_map(|c| {
            if c.is_ascii_alphabetic() {
                Some((c.to_ascii_lowercase() as u8 - b'a' + 1) as u32)
            } else {
                None
            }
        })
        .collect()
}

fn main() {
    let nums = text_to_numbers("Hello World");
    assert_eq!(nums, vec![8, 5, 12, 12, 15, 23, 15, 18, 12, 4]);

    // Format as dash-separated string
    let encoded: String = nums.iter()
        .map(|n| n.to_string())
        .collect::<Vec<_>>()
        .join("-");
    assert_eq!(encoded, "8-5-12-12-15-23-15-18-12-4");
}

Preserving Word Boundaries

For cipher applications you often want to keep word structure visible in the output. Splitting on whitespace and encoding each word separately achieves this:

fn encode_with_words(text: &str, sep: &str, word_sep: &str) -> String {
    text.split_whitespace()
        .map(|word| {
            word.chars()
                .filter_map(|c| {
                    if c.is_ascii_alphabetic() {
                        Some(
                            ((c.to_ascii_lowercase() as u8 - b'a' + 1) as u32)
                                .to_string(),
                        )
                    } else {
                        None
                    }
                })
                .collect::<Vec<_>>()
                .join(sep)
        })
        .collect::<Vec<_>>()
        .join(word_sep)
}

fn main() {
    let result = encode_with_words("Hello World", "-", " / ");
    assert_eq!(result, "8-5-12-12-15 / 23-15-18-12-4");

    let result2 = encode_with_words("Rust is fast", ",", " | ");
    assert_eq!(result2, "18,21,19,20 | 9,19 | 6,1,19,20");
}

Multiple Encoding Methods in Rust

The A1Z26 system (a=1, z=26) is the most common letter-to-number mapping, but several other encodings are useful depending on your application. Here is how to implement each one in Rust:

A1Z26 — Alphabet Position

fn to_a1z26(c: char) -> Option<u32> {
    if c.is_ascii_alphabetic() {
        Some((c.to_ascii_lowercase() as u8 - b'a' + 1) as u32)
    } else {
        None
    }
}
// 'a' → 1, 'z' → 26

ASCII Decimal

fn to_ascii_decimal(c: char) -> u32 {
    c as u32
}
// 'a' → 97, 'A' → 65, '0' → 48

See our ASCII converter for an interactive tool.

Zero-Indexed (A0Z25)

fn to_a0z25(c: char) -> Option<u32> {
    if c.is_ascii_alphabetic() {
        Some((c.to_ascii_lowercase() as u8 - b'a') as u32)
    } else {
        None
    }
}
// 'a' → 0, 'z' → 25

Hexadecimal

fn to_hex(c: char) -> String {
    format!("{:02X}", c as u8)
}
// 'A' → "41", 'a' → "61", 'Z' → "5A"

Binary

fn to_binary(c: char) -> String {
    format!("{:08b}", c as u8)
}
// 'A' → "01000001", 'a' → "01100001"

Try our binary converter for quick binary encoding.

All Encodings at Once

fn all_encodings(c: char) {
    println!("Character: '{}'", c);
    if let Some(pos) = to_a1z26(c) {
        println!("  A1Z26:   {}", pos);
    }
    println!("  ASCII:   {}", c as u32);
    if let Some(idx) = to_a0z25(c) {
        println!("  A0Z25:   {}", idx);
    }
    println!("  Hex:     {}", format!("{:02X}", c as u8));
    println!("  Binary:  {}", format!("{:08b}", c as u8));
}

// all_encodings('R');
// Character: 'R'
//   A1Z26:   18
//   ASCII:   82
//   A0Z25:   17
//   Hex:     52
//   Binary:  01010010

Reverse Conversion — Numbers to Letters

Decoding — converting numbers back to letters — is equally straightforward. Rust provides char::from for safe byte-to-char conversion and char::from_u32 for Unicode code points. See our numbers to letters converter for a ready-made tool.

Single Number to Letter

/// Convert an A1Z26 number (1-26) to a lowercase letter.
fn number_to_letter(n: u32) -> Option<char> {
    if (1..=26).contains(&n) {
        Some(char::from(b'a' + (n as u8) - 1))
    } else {
        None
    }
}

fn main() {
    assert_eq!(number_to_letter(1), Some('a'));
    assert_eq!(number_to_letter(26), Some('z'));
    assert_eq!(number_to_letter(13), Some('m'));
    assert_eq!(number_to_letter(0), None);
    assert_eq!(number_to_letter(27), None);
}

Batch Decode from Vec

fn numbers_to_text(nums: &[u32]) -> String {
    nums.iter()
        .filter_map(|&n| number_to_letter(n))
        .collect()
}

fn main() {
    let decoded = numbers_to_text(&[18, 21, 19, 20]);
    assert_eq!(decoded, "rust");

    let decoded2 = numbers_to_text(&[8, 5, 12, 12, 15]);
    assert_eq!(decoded2, "hello");
}

Decode a Dash-Separated String

fn decode_a1z26(encoded: &str, sep: &str, word_sep: &str) -> String {
    encoded
        .split(word_sep)
        .map(|word| {
            word.split(sep)
                .filter_map(|s| s.trim().parse::<u32>().ok())
                .filter_map(|n| number_to_letter(n))
                .collect::<String>()
        })
        .collect::<Vec<_>>()
        .join(" ")
}

fn main() {
    let text = decode_a1z26("8-5-12-12-15 / 23-15-18-12-4", "-", " / ");
    assert_eq!(text, "hello world");
}

Building a Complete CLI Tool

Let's put everything together into a proper Cargo project with argument parsing using the clap crate. This gives you a production-ready command-line tool.

Project Structure

letter-numbers/
├── Cargo.toml
└── src/
    └── main.rs

Cargo.toml

[package]
name = "letter-numbers"
version = "0.1.0"
edition = "2021"

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

main.rs

use clap::{Parser, Subcommand};

#[derive(Parser)]
#[command(name = "letter-numbers")]
#[command(about = "Convert between letters and A1Z26 numbers")]
struct Cli {
    #[command(subcommand)]
    command: Commands,
}

#[derive(Subcommand)]
enum Commands {
    /// Encode text to numbers
    Encode {
        /// The text to encode
        text: String,
        /// Separator between numbers
        #[arg(short, long, default_value = " ")]
        sep: String,
    },
    /// Decode numbers back to text
    Decode {
        /// Dash-separated numbers (e.g., "8-5-12-12-15")
        numbers: String,
        /// Separator between numbers
        #[arg(short, long, default_value = "-")]
        sep: String,
    },
}

fn letter_to_number(c: char) -> Option<u32> {
    if c.is_ascii_alphabetic() {
        Some((c.to_ascii_lowercase() as u8 - b'a' + 1) as u32)
    } else {
        None
    }
}

fn number_to_letter(n: u32) -> Option<char> {
    if (1..=26).contains(&n) {
        Some(char::from(b'a' + (n as u8) - 1))
    } else {
        None
    }
}

fn main() {
    let cli = Cli::parse();

    match cli.command {
        Commands::Encode { text, sep } => {
            let encoded: String = text
                .chars()
                .filter_map(|c| letter_to_number(c).map(|n| n.to_string()))
                .collect::<Vec<_>>()
                .join(&sep);
            println!("{}", encoded);
        }
        Commands::Decode { numbers, sep } => {
            let decoded: String = numbers
                .split(&*sep)
                .filter_map(|s| s.trim().parse::<u32>().ok())
                .filter_map(|n| number_to_letter(n))
                .collect();
            println!("{}", decoded);
        }
    }
}

Usage Examples

$ cargo run -- encode "hello"
8 5 12 12 15

$ cargo run -- encode "hello world" --sep "-"
8-5-12-12-15-23-15-18-12-4

$ cargo run -- decode "8-5-12-12-15"
hello

$ cargo run -- decode "18 21 19 20" --sep " "
rust

Performance Considerations

Rust excels at text processing because you can operate at the byte level without garbage collection overhead. Here are the key performance techniques:

.bytes() vs .chars() — The ASCII Fast Path

When you know your input is ASCII (which it is for English letters), use .bytes() instead of .chars(). The .chars() iterator must decode UTF-8 multi-byte sequences, while .bytes() yields raw u8 values with zero decoding overhead:

// Slower: decodes UTF-8 on each iteration
fn encode_chars(text: &str) -> Vec<u32> {
    text.chars()
        .filter_map(|c| {
            if c.is_ascii_alphabetic() {
                Some((c.to_ascii_lowercase() as u8 - b'a' + 1) as u32)
            } else {
                None
            }
        })
        .collect()
}

// Faster: operates directly on bytes
fn encode_bytes(text: &str) -> Vec<u32> {
    text.bytes()
        .filter(|b| b.is_ascii_alphabetic())
        .map(|b| (b.to_ascii_lowercase() - b'a' + 1) as u32)
        .collect()
}

// Both produce identical results for ASCII input
fn main() {
    let text = "Hello World";
    assert_eq!(encode_chars(text), encode_bytes(text));
}

SIMD and Compiler Optimizations

Rust's is_ascii_alphabetic() compiles down to two comparisons that the CPU can evaluate in a single branch. When processing large buffers, the LLVM backend can auto-vectorize simple byte-level operations using SIMD instructions. Writing your conversion as a tight loop over &[u8] gives the optimizer the best chance to vectorize:

fn encode_slice(bytes: &[u8], out: &mut Vec<u32>) {
    out.clear();
    out.reserve(bytes.len());
    for &b in bytes {
        if b.is_ascii_alphabetic() {
            out.push((b.to_ascii_lowercase() - b'a' + 1) as u32);
        }
    }
}

Why Rust Is 10-100x Faster Than Python

For letter-to-number conversion, Rust operates directly on bytes with no heap allocation per character, no reference counting, and no interpreter overhead. A simple benchmark encoding one million characters typically shows:

  • Rust (.bytes() path): ~0.5 ms for 1M characters
  • Python (list comprehension): ~50 ms for 1M characters
  • Python (numpy vectorized): ~5 ms for 1M characters

The difference grows larger with more complex encodings. If you need to process gigabytes of text, Rust is the clear choice.

Benchmarking with criterion

// benches/convert.rs
use criterion::{black_box, criterion_group, criterion_main, Criterion};

fn encode_bytes(text: &str) -> Vec<u32> {
    text.bytes()
        .filter(|b| b.is_ascii_alphabetic())
        .map(|b| (b.to_ascii_lowercase() - b'a' + 1) as u32)
        .collect()
}

fn bench_encode(c: &mut Criterion) {
    let text = "Hello World ".repeat(100_000);
    c.bench_function("encode_1M_chars", |b| {
        b.iter(|| encode_bytes(black_box(&text)))
    });
}

criterion_group!(benches, bench_encode);
criterion_main!(benches);

Testing Your Converter

Rust's built-in test framework makes it easy to write comprehensive tests. Place tests in a #[cfg(test)] module at the bottom of your source file:

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_single_letters() {
        assert_eq!(letter_to_number('a'), Some(1));
        assert_eq!(letter_to_number('z'), Some(26));
        assert_eq!(letter_to_number('A'), Some(1));
        assert_eq!(letter_to_number('Z'), Some(26));
        assert_eq!(letter_to_number('m'), Some(13));
        assert_eq!(letter_to_number('M'), Some(13));
    }

    #[test]
    fn test_non_alpha() {
        assert_eq!(letter_to_number('0'), None);
        assert_eq!(letter_to_number(' '), None);
        assert_eq!(letter_to_number('!'), None);
        assert_eq!(letter_to_number('@'), None);
    }

    #[test]
    fn test_full_string() {
        assert_eq!(
            text_to_numbers("Hello"),
            vec![8, 5, 12, 12, 15]
        );
    }

    #[test]
    fn test_empty_string() {
        assert_eq!(text_to_numbers(""), vec![]);
    }

    #[test]
    fn test_mixed_content() {
        assert_eq!(
            text_to_numbers("abc 123 xyz!"),
            vec![1, 2, 3, 24, 25, 26]
        );
    }

    #[test]
    fn test_roundtrip() {
        let original = "rust";
        let numbers = text_to_numbers(original);
        let decoded = numbers_to_text(&numbers);
        assert_eq!(decoded, original);
    }

    #[test]
    fn test_number_to_letter_bounds() {
        assert_eq!(number_to_letter(0), None);
        assert_eq!(number_to_letter(1), Some('a'));
        assert_eq!(number_to_letter(26), Some('z'));
        assert_eq!(number_to_letter(27), None);
        assert_eq!(number_to_letter(100), None);
    }

    #[test]
    fn test_all_letters() {
        for (i, c) in ('a'..='z').enumerate() {
            assert_eq!(letter_to_number(c), Some((i + 1) as u32));
        }
    }
}

Property-Based Testing with proptest

For deeper coverage, the proptest crate generates random inputs to find edge cases your manual tests might miss:

// Add to Cargo.toml:
// [dev-dependencies]
// proptest = "1"

#[cfg(test)]
mod proptests {
    use super::*;
    use proptest::prelude::*;

    proptest! {
        #[test]
        fn roundtrip_single_letter(c in 'a'..='z') {
            let num = letter_to_number(c).unwrap();
            let back = number_to_letter(num).unwrap();
            prop_assert_eq!(c, back);
        }

        #[test]
        fn roundtrip_string(s in "[a-z]{1,100}") {
            let nums = text_to_numbers(&s);
            let decoded = numbers_to_text(&nums);
            prop_assert_eq!(s, decoded);
        }

        #[test]
        fn position_in_range(c in 'a'..='z') {
            let num = letter_to_number(c).unwrap();
            prop_assert!(num >= 1 && num <= 26);
        }

        #[test]
        fn non_alpha_returns_none(c in "[^a-zA-Z]") {
            let ch = c.chars().next().unwrap();
            prop_assert_eq!(letter_to_number(ch), None);
        }
    }
}

Run all tests with cargo test. The property tests will execute hundreds of random cases by default, catching edge cases that hard-coded tests miss.

No coding required: Convert text instantly with our free online A1Z26 Converter — runs entirely in your browser.

Frequently Asked Questions

How do I convert a letter to a number in Rust?

Use char arithmetic: (c as u8 - b'a' + 1) as u32. This subtracts the ASCII value of 'a' (97) from the character's byte value and adds 1, giving you the A1Z26 position where a=1 and z=26. Always check c.is_ascii_alphabetic() first and use to_ascii_lowercase() to handle both cases. For raw ASCII values instead, simply cast with c as u32.

Does Rust have a built-in letter-to-number function?

No, Rust's standard library does not include a dedicated letter-to-position function. It provides character methods like is_alphabetic(), is_ascii_alphabetic(), to_ascii_lowercase(), and to_ascii_uppercase() for classification and case conversion. The actual position calculation is a one-line arithmetic expression: (c.to_ascii_lowercase() as u8 - b'a' + 1) as u32. This design follows Rust's philosophy of providing composable primitives rather than specialized functions.

How do I handle Unicode letters in Rust?

Rust's char type is a 4-byte Unicode scalar value, so it handles Unicode natively. For ASCII English letters (a-z, A-Z), byte arithmetic with as u8 works perfectly. For accented characters or non-Latin scripts, the byte cast will give wrong results. Use c as u32 to get the full Unicode code point, or bring in the unicode-segmentation crate for grapheme-aware processing. For A1Z26 specifically, stripping diacritics first with a normalization crate is the typical approach.

What is the fastest way to convert text to numbers in Rust?

Use .bytes() iterator chains for ASCII-only text. The .bytes() method yields raw u8 values without UTF-8 decoding overhead, while .chars() must validate and decode multi-byte sequences. The optimal pattern is: text.bytes().filter(|b| b.is_ascii_alphabetic()).map(|b| (b.to_ascii_lowercase() - b'a' + 1) as u32).collect(). For maximum throughput on large inputs, pre-allocate the output vector with Vec::with_capacity() and process the input as a byte slice in a tight loop, which allows LLVM to apply SIMD auto-vectorization.

Written by Jack Shi. AlphaCoder tools process every conversion locally in your browser.