More on testing, etc.

pull/52/head
Dhghomon 4 years ago committed by GitHub
parent a8db542d38
commit 1891f1311e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -9445,3 +9445,423 @@ test tests::one_plus_one_is_two ... FAILED
```
One succeeded! Our `math()` function will only accept good input now.
The next step is to write the actual calculator. This is the interesting part about test-driven development: the actual writing starts much later. First we will put the logic together for the calculator. We want the following:
- All empty spaces should be removed. This is easy with `.filter()`
- The input should turn into a `Vec` with all the inputs. `+` doesn't need to be an input, but when the program sees `+` it should know that the number is done. For example, the input `11+1` should do something like this: 1) See `1`, push it into an empty string. 2) See another 1, push it into the string (it is now "11"). 3) See a `+`, know the number has ended. It will push the string into the vec, then clear the string.
- The program must count the number of `-`. An odd number (1, 3, 5...) will mean subtract, an even number (2, 4, 6...) will mean add. So "1--9" should give 10, not -8.
- The program should remove anything after the last number. `5+5+++++----` is made out of all the characters in `OKAY_CHARACTERS`, but it should turn to `5+5`. This is easy with `.trim_end_matches()`, where you remove anything that matches at the end of a `&str`.
(By the way, `.trim_end_matches()` and `.trim_start_matches()` used to be `trim_right_matches()` and `trim_left_matches()`. But then people noticed that some languages write from right to left (Persian, Hebrew, Arabic, etc.) so right and left would be wrong. You might still see the older names in some code but they are the same thing.)
First we just want to pass all the tests. After we pass the tests, we can "refactor". Refactor means to make the code better, usually through things like structs and enums and methods. Here is our code to make the tests pass:
```rust
const OKAY_CHARACTERS: &str = "1234567890+- ";
fn math(input: &str) -> i32 {
if let false = input.chars().all(|character| OKAY_CHARACTERS.contains(character)) {
panic!("Please only input numbers, +-, or spaces");
}
let input = input.trim_end_matches(|x| "+-".contains(x)).chars().filter(|x| *x != ' ').collect::<String>(); // Remove + and - at the end, and all spaces
let mut result_vec = vec![]; // Results go in here
let mut push_string = String::new(); // This is the string we push in every time. We will keep reusing it in the loop.
for character in input.chars() {
match character {
'+' => {
if !push_string.is_empty() { // If the string is empty, we don't want to push "" into result_vec
result_vec.push(push_string.clone()); // But if it's not empty, it will be a number. Push it into the vec
push_string.clear(); // Then clear the string
}
},
'-' => { // If we get a -,
if push_string.contains('-') || push_string.is_empty() { // check to see if it's empty or has a -
push_string.push(character) // if so, then push it in
} else { // otherwise, it will contain a number
result_vec.push(push_string.clone()); // so push the number into result_vec, clear it and then push -
push_string.clear();
push_string.push(character);
}
},
number => { // number here means "anything else that matches". We selected the name here
if push_string.contains('-') { // We might have some - characters to push in first
result_vec.push(push_string.clone());
push_string.clear();
push_string.push(number);
} else { // But if we don't, that means we can push the number in
push_string.push(number);
}
},
}
}
result_vec.push(push_string); // Push one last time after the loop is over. Don't need to .clone() because we don't use it anymore
let mut total = 0; // Now it's time to do math. Start with a total
let mut adds = true; // true = add, false = subtract
let mut math_iter = result_vec.into_iter();
while let Some(entry) = math_iter.next() { // Iter through the items
if entry.contains('-') { // If it has a - character, check if it's even or odd
if entry.chars().count() % 2 == 1 {
adds = false;
continue; // Go to the next item
} else {
continue;
}
}
if adds == true {
total += entry.parse::<i32>().unwrap(); // If there is no '-', it must be a number. So we are safe to unwrap
} else {
total -= entry.parse::<i32>().unwrap();
adds = true; // After subtracting, reset adds to true.
}
}
total // Finally, return the total
}
/// We'll add a few more tests just to make sure
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn one_plus_one_is_two() {
assert_eq!(math("1 + 1"), 2);
}
#[test]
fn one_minus_two_is_minus_one() {
assert_eq!(math("1 - 2"), -1);
}
#[test]
fn one_minus_minus_one_is_two() {
assert_eq!(math("1 - -1"), 2);
}
#[test]
fn nine_plus_nine_minus_nine_minus_nine_is_zero() {
assert_eq!(math("9+9-9-9"), 0); // This is a new test
}
#[test]
fn eight_minus_nine_plus_nine_is_eight_even_with_characters_on_the_end() {
assert_eq!(math("8 - 9 +9-----+++++"), 8); // This is a new test
}
#[test]
#[should_panic]
fn panics_when_characters_not_right() {
math("7 + seven");
}
}
```
And now the tests pass!
```text
running 6 tests
test tests::one_minus_minus_one_is_two ... ok
test tests::nine_plus_nine_minus_nine_minus_nine_is_zero ... ok
test tests::one_minus_two_is_minus_one ... ok
test tests::eight_minus_nine_plus_nine_is_eight_even_with_characters_on_the_end ... ok
test tests::one_plus_one_is_two ... ok
test tests::panics_when_characters_not_right ... ok
test result: ok. 6 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out
```
You can see that there is a back and forth process in test-driven development. It's something like this:
- First you write all the tests you can think of.
- Then you start writing the code.
- As you write the code, you get ideas for other tests.
- You add the tests, and your tests grow as you go. The more tests you have, the more times your code gets checked.
Of course, tests don't check everything and it is wrong to think that "passing all tests = the code is pefect". But tests are great for when you change your code. If you change your code later on and run the tests, if one of them doesn't work you will know what to fix.
Now we can rewrite (refactor) the code a bit. One good way to start is with clippy. If you installed Rust then you can type `cargo clippy`, and if you're using the Playground then click on `TOOLS` and select Clippy. Clippy will look at your code and give you tips to make it simpler. Our code doesn't have any mistakes, but it could be better.
Clippy tells us two things:
```text
warning: this loop could be written as a `for` loop
--> src/lib.rs:44:5
|
44 | while let Some(entry) = math_iter.next() { // Iter through the items
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: try: `for entry in math_iter`
|
= note: `#[warn(clippy::while_let_on_iterator)]` on by default
= help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#while_let_on_iterator
warning: equality checks against true are unnecessary
--> src/lib.rs:53:12
|
53 | if adds == true {
| ^^^^^^^^^^^^ help: try simplifying it as shown: `adds`
|
= note: `#[warn(clippy::bool_comparison)]` on by default
= help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#bool_comparison
```
This is true: `for entry in math_iter` is much simpler than `while let Some(entry) = math_iter.next()`. And a `for` loop is actually an iterator so we don't have any reason to write `.iter()`. Thanks, clippy! And also we didn't need to make `math_iter`: we can just write `for entry in result_vec`.
And the second point is true too: `if adds == true` can just be `if adds` (because `adds` = `true`).
Now we'll start some real refactoring. Instead of separate variables, we will create a `Calculator` struct. This will have all the variables we used together. We will change two names to make it more clear: `result_vec` will become `results`, and `push_string` will become `current_input` (current means "now"). And so far it only has one method: new.
```rust
// 🚧
#[derive(Clone)]
struct Calculator {
results: Vec<String>,
current_input: String,
total: i32,
adds: bool,
}
impl Calculator {
fn new() -> Self {
Self {
results: vec![],
current_input: String::new(),
total: 0,
adds: true,
}
}
}
```
Now our code is actually a bit longer, but easier to read. For example, `if adds` is now `if calculator.adds`, which is exactly like reading English. It looks like this:
```rust
#[derive(Clone)]
struct Calculator {
results: Vec<String>,
current_input: String,
total: i32,
adds: bool,
}
impl Calculator {
fn new() -> Self {
Self {
results: vec![],
current_input: String::new(),
total: 0,
adds: true,
}
}
}
const OKAY_CHARACTERS: &str = "1234567890+- ";
fn math(input: &str) -> i32 {
if let false = input.chars().all(|character| OKAY_CHARACTERS.contains(character)) {
panic!("Please only input numbers, +-, or spaces");
}
let input = input.trim_end_matches(|x| "+-".contains(x)).chars().filter(|x| *x != ' ').collect::<String>();
let mut calculator = Calculator::new();
for character in input.chars() {
match character {
'+' => {
if !calculator.current_input.is_empty() {
calculator.results.push(calculator.current_input.clone());
calculator.current_input.clear();
}
},
'-' => {
if calculator.current_input.contains('-') || calculator.current_input.is_empty() {
calculator.current_input.push(character)
} else {
calculator.results.push(calculator.current_input.clone());
calculator.current_input.clear();
calculator.current_input.push(character);
}
},
number => {
if calculator.current_input.contains('-') {
calculator.results.push(calculator.current_input.clone());
calculator.current_input.clear();
calculator.current_input.push(number);
} else {
calculator.current_input.push(number);
}
},
}
}
calculator.results.push(calculator.current_input);
for entry in calculator.results {
if entry.contains('-') {
if entry.chars().count() % 2 == 1 {
calculator.adds = false;
continue;
} else {
continue;
}
}
if calculator.adds {
calculator.total += entry.parse::<i32>().unwrap();
} else {
calculator.total -= entry.parse::<i32>().unwrap();
calculator.adds = true;
}
}
calculator.total
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn one_plus_one_is_two() {
assert_eq!(math("1 + 1"), 2);
}
#[test]
fn one_minus_two_is_minus_one() {
assert_eq!(math("1 - 2"), -1);
}
#[test]
fn one_minus_minus_one_is_two() {
assert_eq!(math("1 - -1"), 2);
}
#[test]
fn nine_plus_nine_minus_nine_minus_nine_is_zero() {
assert_eq!(math("9+9-9-9"), 0);
}
#[test]
fn eight_minus_nine_plus_nine_is_eight_even_with_characters_on_the_end() {
assert_eq!(math("8 - 9 +9-----+++++"), 8);
}
#[test]
#[should_panic]
fn panics_when_characters_not_right() {
math("7 + seven");
}
}
```
Finally we add two new methods. One is called `.clear()` and clears the `current_input()`. The other one is called `push_char()` and pushes the input onto `current_input()`. Here is our refactored code:
```rust
#[derive(Clone)]
struct Calculator {
results: Vec<String>,
current_input: String,
total: i32,
adds: bool,
}
impl Calculator {
fn new() -> Self {
Self {
results: vec![],
current_input: String::new(),
total: 0,
adds: true,
}
}
fn clear(&mut self) {
self.current_input.clear();
}
fn push_char(&mut self, character: char) {
self.current_input.push(character);
}
}
const OKAY_CHARACTERS: &str = "1234567890+- ";
fn math(input: &str) -> i32 {
if let false = input.chars().all(|character| OKAY_CHARACTERS.contains(character)) {
panic!("Please only input numbers, +-, or spaces");
}
let input = input.trim_end_matches(|x| "+-".contains(x)).chars().filter(|x| *x != ' ').collect::<String>();
let mut calculator = Calculator::new();
for character in input.chars() {
match character {
'+' => {
if !calculator.current_input.is_empty() {
calculator.results.push(calculator.current_input.clone());
calculator.clear();
}
},
'-' => {
if calculator.current_input.contains('-') || calculator.current_input.is_empty() {
calculator.push_char(character)
} else {
calculator.results.push(calculator.current_input.clone());
calculator.clear();
calculator.push_char(character);
}
},
number => {
if calculator.current_input.contains('-') {
calculator.results.push(calculator.current_input.clone());
calculator.clear();
calculator.push_char(number);
} else {
calculator.push_char(number);
}
},
}
}
calculator.results.push(calculator.current_input);
for entry in calculator.results {
if entry.contains('-') {
if entry.chars().count() % 2 == 1 {
calculator.adds = false;
continue;
} else {
continue;
}
}
if calculator.adds {
calculator.total += entry.parse::<i32>().unwrap();
} else {
calculator.total -= entry.parse::<i32>().unwrap();
calculator.adds = true;
}
}
calculator.total
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn one_plus_one_is_two() {
assert_eq!(math("1 + 1"), 2);
}
#[test]
fn one_minus_two_is_minus_one() {
assert_eq!(math("1 - 2"), -1);
}
#[test]
fn one_minus_minus_one_is_two() {
assert_eq!(math("1 - -1"), 2);
}
#[test]
fn nine_plus_nine_minus_nine_minus_nine_is_zero() {
assert_eq!(math("9+9-9-9"), 0);
}
#[test]
fn eight_minus_nine_plus_nine_is_eight_even_with_characters_on_the_end() {
assert_eq!(math("8 - 9 +9-----+++++"), 8);
}
#[test]
#[should_panic]
fn panics_when_characters_not_right() {
math("7 + seven");
}
}
```
This is probably good enough for now. We could write more methods but lines like `calculator.results.push(calculator.current_input.clone());` are already very clear. Refactoring is best when you can still read the code well after you are done. You don't want to just refactor to make the code short: `calc.clr()` is much worse than `calculator.clear()`, for example.

Loading…
Cancel
Save