2
\$\begingroup\$

I am currently learning Rust. To allow myself to practice, I created the following program that helps the user to practice hamming codes (the goal is for the user to use this program to be able to write the hamming code for a particular code word). I am aware of a very small algorithmic error in the program that appears with a particular length of code word, but I am not interested in fixing that because the goal of the program is to practice Rust. As such, I am interested in feedback relating to my use of Rust to write idiomatic and efficient code (if I didn't care about speed, I would use Java, but it isn't good enough in testing for my applications). Because I am still using Rust, I can't use the features whose syntax I have not yet learned. For example, I can't create my own structs (or any other types for that matter) or use traits on them, even though both would probably be useful in this context. Although feedback on what other features should be used in this context would be useful, I am most interested in feedback about what I did use.

main.rs:

use rand::prelude::*;
use std::collections::HashMap;
use std::io;

fn main() {
    let mut base_codes = create_all_base_codes(4);
    base_codes.shuffle(&mut rand::thread_rng());

    let mut correct = 0;
    for base_code in &base_codes {
        println!("Enter the hamming code for {}.", reverse_string(&base_code));
        let mut guess = String::new();
        io::stdin().read_line(&mut guess).expect("Failed to read guess.");
        let correct_hamming_code = create_hamming_code(&base_code);
        if guess.trim() == correct_hamming_code {
            correct += 1;
            println!("Your guess is correct.");
        } else {
            println!("Your guess is incorrect. The correct hamming code is: {}", reverse_string(&correct_hamming_code));
        }
    }

    println!("You got {}/{} correct, or {}%.", correct, (&base_codes).len(), ((correct*100) as f64)/(base_codes.len() as f64));
}

fn reverse_string(string: &String) -> String{
    string.chars().rev().collect() //I don't really understand what this is doing fully, I got it from stack overflow
}

fn create_hamming_code(base_code: &String) -> String {
    //create parity bit array
    let mut parity_bits: HashMap<u64, u32> = HashMap::new(); //key is the index in hamming_code, value is the number of 1 bits that fall under this index
    let mut hamming_code = Vec::new();

    for char in (&base_code).chars() {
        while is_power_of_two(hamming_code.len() as u64+1) {
            parity_bits.insert((hamming_code.len()+1) as u64, 0);
            hamming_code.push('h');
        }

        hamming_code.push(char);
    }

    //count bits
    let mut index = 1;
    for char in &hamming_code {
        match char {
            '0' => (),
            '1' => {
                for i in 0..64 {
                    if is_bit_one(index, i) {
                        match parity_bits.get_mut(&((1 << i) as u64)) {
                            Some(count) => *count += 1,
                            None => panic!("Invalid data structure.") //is there some way that I can make this check only happen in debug builds, without unsafe{}?
                        }
                    }
                }
            },
            'h' => (),
            _ => panic!("Illegal character in intermediate hamming code: {}", char) //is there some way that I can make this check only happen in debug builds, without unsafe{}?
        }
        index = index + 1;
    }

    //fill in parity bits
    for (index, count) in &parity_bits {
        hamming_code[*index as usize-1] = get_odd_parity_bit(*count);
    }

    let mut hamming_code_string = String::new();
    for c in hamming_code {
        hamming_code_string.push(c);
    }

    hamming_code_string
}

fn get_odd_parity_bit(count: u32) -> char{
    if count % 2 == 0 {
        '0'
    }else{
        '1'
    }
}

fn is_power_of_two(number: u64) -> bool{
    if number == 0 {
        return false;
    }

    let mut one_found_so_far = false;
    for i in 0..64 {
        if is_bit_one(number, i) {
            if one_found_so_far {
                return false;
            } else {
                one_found_so_far = true;
            }
        }
    }

    true
}

#[test]
fn test_is_power_of_two(){
    assert!(!is_power_of_two(0));
    assert!(is_power_of_two(1));
    assert!(is_power_of_two(2));
    assert!(!is_power_of_two(3));
    assert!(is_power_of_two(4));
    assert!(!is_power_of_two(5));
    assert!(!is_power_of_two(6));
    assert!(!is_power_of_two(7));
    assert!(is_power_of_two(8));
    assert!(!is_power_of_two(9));
    assert!(!is_power_of_two(10));
}

fn is_bit_one(number: u64, bit: u8) -> bool{
    debug_assert!(bit < 64); //spending CPU cycles on this in release builds doesn't matter in this case, but it is good practice in a systems language to not waste CPU cycles if not needed
    (number >> bit as u64) & 0x1 == 1
}

#[test]
fn test_is_bit_one(){
    assert_eq!(is_bit_one(0, 0), false);
    assert_eq!(is_bit_one(0, 1), false);
    assert_eq!(is_bit_one(1, 0), true);
    assert_eq!(is_bit_one(1, 1), false);
    assert_eq!(is_bit_one(2, 0), false);
    assert_eq!(is_bit_one(2, 1), true);
    assert_eq!(is_bit_one(3, 0), true);
    assert_eq!(is_bit_one(3, 1), true);
}

fn create_all_base_codes(length: u32) -> Vec<String> {
    match length {
        0 => vec![],
        1 => vec!["0".to_string(), "1".to_string()],
        _ => {
            let mut codes = Vec::new();
            let smaller_vector = create_all_base_codes(length-1);

            for code in smaller_vector {
                codes.push(code.clone() + "0");
                codes.push(code + "1");
            }

            return codes;
        }
    }
}

#[test]
fn test_create_all_base_codes(){
    let codes = create_all_base_codes(3);
    assert_eq!(codes.len(), 8);

    let required_codes = vec!["000", "001", "010", "011", "100", "101", "110", "111"];
    for required_code in &required_codes {
        assert!(codes.contains(&required_code.to_string()));
    }
}

cargo.toml:

[package]
name = "hammingcodequiz"
version = "0.1.0"
authors = [""]
edition = "2018"


[dependencies]
rand = "0.7.3"
\$\endgroup\$
  • 1
    \$\begingroup\$ The bit as u64 in is_bit_one looks strange. Can you simply write bit instead? If not, that's worth a bug report to Rust. The right-hand side of the shift operators should allow any integer type, or at least any unsigned integer type. \$\endgroup\$ – Roland Illig Jan 19 at 8:29
  • 1
    \$\begingroup\$ @RolandIllig It turned out to be a CLion bug. It's official Rust addon complains about it, but rustc takes no issue. I'll report it to Jetbrains. Good catch :) \$\endgroup\$ – john01dav Jan 19 at 8:45
2
\$\begingroup\$
string.chars().rev().collect() 
  • chars() returns an iterator that gives one char (unicode scalar) of the string at a time.
  • rev() reverses an iterator
  • collect() is a generic function that can build various types from an iterator. Types like String implement FromIterator interface to work with it.
let mut parity_bits: HashMap<u64, u32> = HashMap::new()

You generally don't need to specify types. Rust will guess them from the usage. let mut parity_bits = HashMap::new() may be enough.

(&base_code).chars()

You almost never need to borrow a type before calling a method. The . operator does it automatically when necessary.

    let mut hamming_code_string = String::new();
    for c in hamming_code {
        hamming_code_string.push(c);
    }

    hamming_code_string

Can be simplified to hamming_code.into_iter().collect() on the same principle as reversing a string. into_iter() gives owned values instead of references.

| improve this answer | |
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.