In part 1, we put together a simple single-file console application and demonstrated some basic functionality.
Part 1 - Basic Conversion and Display Functionality
For the next pass, it would make sense to enable unit-testing and encapsulate the conversion details. I'm still getting my head wrapped around the fact that there are no classes in Rust... the struct, separate "impl" section for methods and Traits are interesting however.
Code
Base Converter Struct
I had ChatGPT analyze my first conversion to this struct-based approach. It recommended creating a new struct output instead of using mutable, but this works for now.
In general, parse_and_convert
is called to specify the text input from the user via console, and the various conversions are stored in variables ready to be displayed.
In this form, we can add unit-tests to verify the conversions.
// Define the BaseConverter struct
pub struct BaseConverter {
pub int64: i64,
pub int32: i32,
pub int16: i16,
pub int8: i8,
pub uint64: u64,
pub uint32: u32,
pub uint16: u16,
pub uint8: u8,
pub float_value: f32,
pub double_value: f64,
pub little_endian_bytes: [u8; 8],
pub little_endian_hex_string: String,
pub big_endian_bytes: [u8; 8],
pub big_endian_hex_string: String,
}
impl Default for BaseConverter {
fn default() -> BaseConverter {
BaseConverter {
int64: 0,
int32: 0,
int16: 0,
int8: 0,
uint64: 0,
uint32: 0,
uint16: 0,
uint8: 0,
float_value: 0.0,
double_value: 0.0,
little_endian_bytes: Default::default(),
little_endian_hex_string: Default::default(),
big_endian_bytes: Default::default(),
big_endian_hex_string: Default::default(),
}
}
}
// Implement methods for BaseConverter
impl BaseConverter {
// Method to calculate the area of the rectangle
pub fn parse_and_convert(&mut self, arg: &str) -> () {
use regex::Regex; // Add the import statement here
if let Ok(i) = arg.parse::<i64>() {
self.convert_and_print_bytes(&i.to_le_bytes());
} else if let Ok(u) = arg.parse::<u64>() {
self.convert_and_print_bytes(&u.to_le_bytes());
} else if let Ok(f) = arg.parse::<f64>() {
self.convert_and_print_bytes(&f.to_le_bytes());
} else if arg.ends_with('f') || arg.ends_with('F') {
self.parse_float(arg);
} else {
let pattern = r"^(0x|<0x|>0x|0X|<0X|>0X|>|<)?.*?[hH]?$";
let re = Regex::new(pattern).unwrap();
if re.is_match(arg) {
self.parse_hex(arg);
} else {
println!("Argument didn't match any expected format.");
}
}
}
fn convert_and_print_float(&mut self, float_value: f32) {
// Convert to byte array in little-endian format
let float_bytes = float_value.to_le_bytes();
let int32 = i32::from_le_bytes(float_bytes);
let int64: i64 = int32 as i64; // Convert i32 to i64
let int64_bytes = int64.to_le_bytes();
self.convert_and_print_bytes(&int64_bytes);
}
fn hex_to_byte_array(&self, hex_str: &str, is_big_endian: bool) -> [u8; 8] {
let mut bytes = Vec::new();
// Convert hex string to bytes
for chunk in hex_str.as_bytes().chunks(2) {
let hex = std::str::from_utf8(chunk).expect("Invalid UTF-8 sequence");
if let Ok(byte) = u8::from_str_radix(hex, 16) {
bytes.push(byte);
}
}
// If input is big-endian, reverse it for little-endian systems (most systems are little-endian)
if is_big_endian {
bytes.reverse();
}
// Initialize a fixed size array with zeros
let mut fixed_bytes: [u8; 8] = [0; 8];
// Copy the bytes into the fixed size array, truncating or padding as necessary
for (i, &byte) in bytes.iter().enumerate().take(8) {
fixed_bytes[i] = byte;
}
fixed_bytes
}
fn parse_float(&mut self, arg: &str) {
let float_arg = &arg[..arg.len() - 1]; // Remove the 'f' suffix
match float_arg.parse::<f32>() {
Ok(f) => self.convert_and_print_float(f),
Err(_) => println!("Failed to parse as float."),
}
}
fn parse_hex(&mut self, arg: &str) {
let (big_endian, hex_value) = self.parse_hex_string(arg);
let bytes = self.hex_to_byte_array(&hex_value, big_endian);
self.convert_and_print_bytes(&bytes);
}
fn parse_hex_string(&mut self, input: &str) -> (bool, String) {
// Determine the boolean value based on the starting character
let starts_with = match input.chars().next() {
Some('>') => true,
Some('<') => false,
_ => true,
};
// Remove '>' or '<' from the start if present
let trimmed_start = if input.starts_with('>') || input.starts_with('<') {
&input[1..]
} else {
input
};
// Remove "0x" or "0X" prefix if present
let trimmed_prefix = if trimmed_start.to_lowercase().starts_with("0x") {
&trimmed_start[2..]
} else {
trimmed_start
};
// Remove "h" or "H" suffix if present
let hex_value = if trimmed_prefix.to_lowercase().ends_with('h') {
&trimmed_prefix[..trimmed_prefix.len() - 1]
} else {
trimmed_prefix
};
(starts_with, hex_value.to_string())
}
fn convert_and_print_bytes(&mut self, bytes: &[u8; 8]) {
// Convert to various integer types
self.int64 = i64::from_le_bytes(bytes[0..8].try_into().unwrap());
self.int32 = i32::from_le_bytes(bytes[0..4].try_into().unwrap());
self.int16 = i16::from_le_bytes(bytes[0..2].try_into().unwrap());
self.int8 = bytes[0] as i8; // Direct cast, since i8 and u8 have the same memory representation
self.uint64 = u64::from_le_bytes(bytes[0..8].try_into().unwrap());
self.uint32 = u32::from_le_bytes(bytes[0..4].try_into().unwrap());
self.uint16 = u16::from_le_bytes(bytes[0..2].try_into().unwrap());
self.uint8 = bytes[0]; // Direct use, as u8 is the type of bytes' elements
// Convert to floating-point types
self.float_value = f32::from_le_bytes(bytes[0..4].try_into().unwrap());
self.double_value = f64::from_le_bytes(bytes[0..8].try_into().unwrap());
self.little_endian_bytes = self.uint64.to_le_bytes();
self.little_endian_hex_string = self
.little_endian_bytes
.iter()
.map(|byte| format!("{:02X}", byte))
.collect();
self.big_endian_bytes = self.uint64.to_be_bytes();
self.big_endian_hex_string = self
.big_endian_bytes
.iter()
.map(|byte| format!("{:02X}", byte))
.collect();
}
pub fn print_values(&self) {
// Prefix with "0x" to denote hexadecimal
println!(" Hex (le): 0x{}", self.little_endian_hex_string);
println!(" Hex (be): 0x{}", self.big_endian_hex_string);
println!("Integer (i64): {}", self.int64);
println!(" (i32): {}", self.int32);
println!(" (i16): {}", self.int16);
println!(" (i8): {}", self.int8);
println!(" (u64): {}", self.uint64);
println!(" (u32): {}", self.uint32);
println!(" (u16): {}", self.uint16);
println!(" (u8): {}", self.uint8);
println!(" Float (f32): {:.2e}", self.float_value);
println!(" Double (f64): {:.2e}", self.double_value);
}
}
main.rs
It took me a while to get the unit-tests and module references right. I ended up watching a YouTube to understand the basic example. By default, when creating a lib, they auto create the scaffolding for unit tests within the library, but you can move the tests to another module, like shown.
mod base_converter;
use std::env;
#[cfg(test)]
mod tests;
fn main() {
let args: Vec<String> = env::args().collect();
if args.len() > 1 {
let arg = &args[1];
let mut converter = base_converter::BaseConverter::default();
converter.parse_and_convert(arg);
converter.print_values();
} else {
println!("Please provide an argument.");
}
}
tests/baseconv.rs
There is a parameterized crate similar to google-test as shown below.
use crate::base_converter::BaseConverter;
// The module where our tests will reside
#[cfg(test)]
mod tests {
// Bring the outer scope (Rectangle struct) into the scope of this module
use super::*;
// Import the BaseConverter struct
// Define a test case
#[test]
fn converter_test1() {
// Create an instance of BaseConverter
let mut converter = BaseConverter::default();
// Assert that the area method returns the expected result
converter.parse_and_convert("0x3f9df3b6");
assert_eq!(converter.float_value, 1.234);
}
use parameterized::parameterized;
#[parameterized(input = {
"0x3f9df3b6", "0xc0add2f2", "0xc59c4800"
}, expected = {
1.234, -5.432, -5001.0
})]
fn test_convert_to_float(input: &str, expected: f32) {
let mut converter = BaseConverter::default();
// Assert that the area method returns the expected result
converter.parse_and_convert(&input);
assert_eq!(converter.float_value, expected);
}
}
Test Output
* Executing task: C:\Users\ericj\.cargo\bin\cargo.exe test --package basec --bin basec -- tests::baseconv::tests --nocapture
Compiling parameterized v1.1.0
Compiling basec v0.1.0 (C:\Projects\rust\rust-baseconv)
Finished test [unoptimized + debuginfo] target(s) in 0.86s
Running unittests src\main.rs (target\debug\deps\basec-3294768479281e61.exe)
running 4 tests
test tests::baseconv::tests::converter_test1 ... ok
test tests::baseconv::tests::test_convert_to_float::case_0 ... ok
test tests::baseconv::tests::test_convert_to_float::case_2 ... ok
test tests::baseconv::tests::test_convert_to_float::case_1 ... ok
test result: ok. 4 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
* Terminal will be reused by tasks, press any key to close it.