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"
bit as u64
inis_bit_one
looks strange. Can you simply writebit
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 '20 at 8:29