Automate The Boring Stuff With Rust (PART 2)
The purpose of this blog post series is to help people reading the Rust book and learning Rust in general to sharpen their knowledge through doing.
It’s time to jump into the challenges of Chapter 4. [Read part 1 here]
Prerequisites for this Chapter
- Read “Storing Lists and Values” from the Rust book
Comma Code
Say you have a list value like this:
spam = ['apples', 'bananas', 'tofu', 'cats']
Write a function that takes a list value as an argument and returns a string with all the items separated by a comma and a space, with and inserted before the last item. For example, passing the previous spam list to the function would return ‘apples, bananas, tofu, and cats. But your function should be able to work with any list value passed to it. Be sure to test the case where an empty list [] is passed to your function.
Solutions
import time
start_time = time.time()
spam = [
"apples",
"bananas",
"tofu",
"cats",
"dogs",
"fish",
]
def comma_code(lst):
if len(lst) == 0:
return ""
elif len(lst) == 1:
return lst[0]
else:
# call .join() without the last element then add "and" and the last element
return ", ".join(lst[:-1]) + " and " + lst[-1]
results = comma_code(spam)
end_time = time.time()
print(results)
elapsed_time = end_time - start_time
print(f"Time taken: {elapsed_time:.6f} seconds")
use std::time::Instant;
fn main() {
let start_time = Instant::now();
let spam = vec![
"apples",
"bananas",
"tofu",
"cats",
"dogs",
"fish",
];
fn comma_code(spam: &[&str]) -> String {
match spam.len() {
0 => String::new(),
1 => spam[0].to_string(),
_ => {
// Pre-calculate the capacity
let capacity =
spam.iter().map(|s| s.len()).sum::<usize>() + 2 * (spam.len() - 1) + 5;
let mut result = String::with_capacity(capacity);
// Use iterators to avoid the need for manual indexing
let mut iter = spam.iter();
if let Some(first) = iter.next() {
result.push_str(first);
}
let last = iter.next_back();
for item in iter {
result.push_str(", ");
result.push_str(item);
}
if let Some(last_item) = last {
result.push_str(" and ");
result.push_str(last_item);
}
result
}
}
}
let result = comma_code(&spam);
let elapsed_time = start_time.elapsed();
println!("Result: {}", result);
println!("Time taken: {:.6} seconds", elapsed_time.as_secs_f64());
}
Notes
- There are ways to optimize the Python code further, such as calculating the array capacity. However, this might only have an effect with really large datasets.
- In Rust, it’s standard to optimize code, and no one will frown upon you for declaring the size of your String instead of creating an empty one.
- The Python code is arguably more readable and concise.
Performance Comparison (Average of 3 runs with 290 strings in array/vec)
- Python: 0.000012 seconds
- Rust: 0.000006 seconds
- Rust is 2 times faster
Coin Flip Streaks
Problem Statement
Write a program to:
- Simulate flipping a coin 100 times
- Record ‘H’ for heads and ‘T’ for tails
- Repeat the experiment 10,000 times
- Check each list of flips for a streak of six heads or six tails in a row
- Calculate the percentage of experiments that contain such a streak
Solutions
import random, time
start_time = time.time()
numberOfStreaks = 0
for experimentNumber in range(10000):
# Code that creates a list of 100 'heads' or 'tails' values.
flips = []
for i in range(100):
if random.randint(0, 1):
flips.append("H")
else:
flips.append("T")
# Code that checks if there is a streak of 6 heads or tails in a row.
for i in range(100 - 6):
if (
flips[i]
== flips[i + 1]
== flips[i + 2]
== flips[i + 3]
== flips[i + 4]
== flips[i + 5]
):
numberOfStreaks += 1
break
end_time = time.time()
print("Chance of streak: %s%%" % (numberOfStreaks / 100))
print("Elapsed time: %s seconds" % (end_time - start_time))
use rand::Rng;
use std::time::Instant;
fn main() {
let start_time = Instant::now();
let mut numberOfStreaks = 0;
for _ in 0..10000 {
// Code that creates a list of 100 'heads' or 'tails' values.
let mut flips = Vec::new();
for _ in 0..100 {
let flip = if rand::thread_rng().gen_bool(0.5) {
"H"
} else {
"T"
};
flips.push(flip);
}
// Code that checks if there is a streak of 6 heads or tails in a row.
for i in 0..flips.len() - 6 {
if flips[i..i + 6].iter().all(|&x| x == "H")
|| flips[i..i + 6].iter().all(|&x| x == "T")
{
numberOfStreaks += 1;
break;
}
}
}
let elapsed_time = start_time.elapsed();
println!("Chance of streak: {}%", numberOfStreaks / 100);
println!("Time taken: {:.6} seconds", elapsed_time.as_secs_f64());
}
Performance Comparison (Average of 3 runs)
- Python: 0.266782 seconds
- Rust: 0.032330 seconds
- Rust is 8.25 times faster
Character Picture Grid
Problem Statement
Given a list of lists where each value is a one-character string:
grid = [
['.', '.', '.', '.', '.', '.'],
['.', 'O', 'O', '.', '.', '.'],
['O', 'O', 'O', 'O', '.', '.'],
['O', 'O', 'O', 'O', 'O', '.'],
['.', 'O', 'O', 'O', 'O', 'O'],
['O', 'O', 'O', 'O', 'O', '.'],
['O', 'O', 'O', 'O', '.', '.'],
['.', 'O', 'O', '.', '.', '.'],
['.', '.', '.', '.', '.', '.']
]
Think of grid[x][y] as being the character at the x- and y-coordinates of a “picture” drawn with text characters. The (0, 0) origin is in the upper-left corner, the x-coordinates increase going right, and the y-coordinates increase going down.
Copy the previous grid value and write code that uses it to print the image.
..OO.OO..
.OOOOOOO.
.OOOOOOO.
..OOOOO..
…OOO…
….O….
Solutions
import time
start_time = time.time()
grid = [
[".", ".", ".", ".", ".", "."],
[".", "O", "O", ".", ".", "."],
["O", "O", "O", "O", ".", "."],
["O", "O", "O", "O", "O", "."],
[".", "O", "O", "O", "O", "O"],
["O", "O", "O", "O", "O", "."],
["O", "O", "O", "O", ".", "."],
[".", "O", "O", ".", ".", "."],
[".", ".", ".", ".", ".", "."],
]
width = len(grid[0])
height = len(grid)
result = []
for col in range(width):
for row in grid:
result.append(row[col])
result.append("\n")
result_string = "".join(result)
end_time = time.time()
print(result_string)
elapsed_time = end_time - start_time
print(f"Time taken: {elapsed_time:.6f} seconds")
use std::time::Instant;
fn main() {
let start_time = Instant::now();
let picture_grid = vec![
vec!['.', '.', '.', '.', '.', '.'],
vec!['.', 'O', 'O', '.', '.', '.'],
vec!['O', 'O', 'O', 'O', '.', '.'],
vec!['O', 'O', 'O', 'O', 'O', '.'],
vec!['.', 'O', 'O', 'O', 'O', 'O'],
vec!['O', 'O', 'O', 'O', 'O', '.'],
vec!['O', 'O', 'O', 'O', '.', '.'],
vec!['.', 'O', 'O', '.', '.', '.'],
vec!['.', '.', '.', '.', '.', '.'],
];
let width = picture_grid[0].len();
let height = picture_grid.len();
let mut result = String::with_capacity(width * (height + 1));
for col in 0..width {
for row in &picture_grid {
result.push(row[col]);
}
result.push('\n');
}
let elapsed_time = start_time.elapsed();
print!("{}", result);
println!("Time taken: {:.6} seconds", elapsed_time.as_secs_f64());
}
Note: Something I realized is that sometimes when testing, the print!
macro was included in the function timing by mistake, which slows down the code so much that Python actually was faster when including the print to terminal as part of the benchmarking, read more here.
Performance Comparison
- Python: 0.000010 seconds
- Rust: 0.00000167 seconds
- Rust is approximately 6 times faster
Link to GitHub