Skip to content

wordy 1.4.0 #736

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Nov 18, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion exercises/wordy/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
[package]
name = "wordy"
version = "1.1.0"
version = "1.4.0"
16 changes: 16 additions & 0 deletions exercises/wordy/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,14 @@

Parse and evaluate simple math word problems returning the answer as an integer.

## Iteration 0 — Numbers

Problems with no operations simply evaluate to the number given.

> What is 5?

Evaluates to 5.

## Iteration 1 — Addition

Add two numbers together.
Expand Down Expand Up @@ -43,6 +51,14 @@ left-to-right, _ignoring the typical order of operations._

15 (i.e. not 9)

## Iteration 4 — Errors

The parser should reject:

* Unsupported operations ("What is 52 cubed?")
* Non-math questions ("Who is the President of the United States")
* Word problems with invalid syntax ("What is 1 plus plus 2?")

## Bonus — Exponentials

If you'd like, handle exponentials.
Expand Down
100 changes: 44 additions & 56 deletions exercises/wordy/example.rs
Original file line number Diff line number Diff line change
@@ -1,65 +1,53 @@
struct Token<'a> {
value: &'a str,
#[derive(PartialEq)]
enum Token<'a> {
Number(i32),
NonNumber(&'a str),
}

impl <'a> Token<'a> {
fn is_valid(&self) -> bool {
!self.value.is_empty() && (self.is_operand() || self.is_operator())
}

fn is_operand(&self) -> bool {
self.value.chars().all(|c| c.is_numeric() || c == '-')
}

fn is_operator(&self) -> bool {
self.value == "plus"
|| self.value == "minus"
|| self.value == "multiplied"
|| self.value == "divided"
}
fn apply_op<'a, 'b>(num1: i32, words: &'a [Token<'b>]) -> Option<(i32, &'a [Token<'b>])> {
let number_pos = words.iter().position(|w| match w {
Token::Number(_) => true,
Token::NonNumber(_) => false,
})?;
let (op_and_num, remainder) = words.split_at(number_pos + 1);
let (op, num2) = op_and_num.split_at(number_pos);
let num2 = match num2 {
[Token::Number(i)] => i,
_ => unreachable!("We split at a Number above, so num2 is surely a single-element slice w/ a number"),
};
match op {
[Token::NonNumber("plus")] => Some(num1 + num2),
[Token::NonNumber("minus")] => Some(num1 - num2),
[Token::NonNumber("multiplied"), Token::NonNumber("by")] => Some(num1 * num2),
[Token::NonNumber("divided"), Token::NonNumber("by")] => Some(num1 / num2),
_ => None,
}.map(|n| (n, remainder))
}

pub fn answer(c: &str) -> Option<i32> {
let mut t = tokens(c);
let mut result: i32 = 0;
let mut opr = "plus";

if t.len() <= 1 {
None
} else {
while t.len() > 1 {
result = evaluate(result, opr, operand(&t.remove(0)));
opr = operator(&t.remove(0));
let words = c.trim_end_matches('?').split_whitespace().map(|word| {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

note that if there were a test case along the lines of What is 1 + 2??? then that might raise questions as to whether that case is acceptable. This code would accept it.

if let Ok(i) = word.parse::<i32>() {
Token::Number(i)
} else {
Token::NonNumber(word)
}
result = evaluate(result, opr, operand(&t.remove(0)));
Some(result)
}).collect::<Vec<_>>();
if words.len() < 3 {
Copy link
Member Author

@petertseng petertseng Nov 18, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

there is no test case that exercises this condition. I am considering whether to submit What is? as wordy 1.5.0

return None;
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wish I didn't have to do this separately, but words[0..3] will panic if words.len() < 3. Maybe something with iter().take(3), but then I hae to collect as well and it doesn't seem worth it.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think words[..3] might not panic, being an unbounded range. Might be worth looking into, at least.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Interesting. Looks like it still does though. thread 'reject_empty' panicked at 'index 3 out of range for slice of length 0', libcore/slice/mod.rs:1932:5

}
}

fn evaluate(r: i32, operator: &str, operand: i32) -> i32 {
match operator {
"plus" => r + operand,
"minus" => r - operand,
"multiplied" => r * operand,
"divided" => r / operand,
_ => r,
let mut result: i32 = match words[0..3] {
[
Token::NonNumber("What"),
Token::NonNumber("is"),
Token::Number(i),
] => i,
_ => return None,
};
let mut words = words.split_at(3).1;
while !words.is_empty() {
let tmp = apply_op(result, words)?;
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would have liked to write (result, words) = apply_op(result, words)?; or something, but that is not valid. A shame.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

that was pointed out in #665 (comment) . I wonder if I should also consider using that function that takes &mut. Note though that there still remains the possibility that the function will cause an early return None (currently done by the ?), so there's a bit more to it

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, destructuring assignment would be a really nice usability improvement for the language.

result = tmp.0;
words = tmp.1;
}
}

fn operand(t: &Token) -> i32 {
t.value.parse().unwrap()
}

fn operator<'a>(t: &Token<'a>) -> &'a str {
t.value
}

fn tokens<'a>(command: &'a str) -> Vec<Token<'a>> {
command
.split(|c: char| c.is_whitespace() || c == '?')
.map(|w| Token {
value: w,
})
.filter(|t| t.is_valid())
.collect()
Some(result)
}
42 changes: 42 additions & 0 deletions exercises/wordy/tests/wordy.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,13 @@ extern crate wordy;
use wordy::answer;

#[test]
fn just_a_number() {
let command = "What is 5?";
assert_eq!(Some(5), answer(command));
}

#[test]
#[ignore]
fn addition() {
let command = "What is 1 plus 1?";
assert_eq!(Some(2), answer(command));
Expand Down Expand Up @@ -112,3 +119,38 @@ fn non_math_question() {
let command = "Who is the President of the United States?";
assert!(answer(command).is_none());
}

#[test]
#[ignore]
fn reject_incomplete_problem() {
let command = "What is 1 plus?";
assert!(answer(command).is_none());
}

#[test]
#[ignore]
fn reject_two_operations_in_a_row() {
let command = "What is 1 plus plus 2?";
assert!(answer(command).is_none());
}

#[test]
#[ignore]
fn reject_two_numbers_in_a_row() {
let command = "What is 1 plus 2 1?";
assert!(answer(command).is_none());
}

#[test]
#[ignore]
fn reject_postfix_notation() {
let command = "What is 1 2 plus?";
assert!(answer(command).is_none());
}

#[test]
#[ignore]
fn reject_prefix_notation() {
let command = "What is plus 1 2?";
assert!(answer(command).is_none());
}