-
Notifications
You must be signed in to change notification settings - Fork 544
Add exercise book-store #390
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
coriolinus
merged 3 commits into
exercism:master
from
coriolinus:add-exercise-book-store
Nov 11, 2017
Merged
Changes from 2 commits
Commits
Show all changes
3 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries | ||
# More information here http://doc.crates.io/guide.html#cargotoml-vs-cargolock | ||
Cargo.lock |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
[package] | ||
name = "book_store" | ||
version = "1.0.1" | ||
authors = ["Peter Goodspeed-Niklaus <[email protected]>"] | ||
|
||
[dependencies] | ||
noisy_float = "0.1.2" |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
[package] | ||
name = "book_store" | ||
version = "1.0.1" | ||
authors = ["Peter Goodspeed-Niklaus <[email protected]>"] | ||
|
||
[dependencies] |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,107 @@ | ||
# Book Store | ||
|
||
To try and encourage more sales of different books from a popular 5 book | ||
series, a bookshop has decided to offer discounts on multiple book purchases. | ||
|
||
One copy of any of the five books costs $8. | ||
|
||
If, however, you buy two different books, you get a 5% | ||
discount on those two books. | ||
|
||
If you buy 3 different books, you get a 10% discount. | ||
|
||
If you buy 4 different books, you get a 20% discount. | ||
|
||
If you buy all 5, you get a 25% discount. | ||
|
||
Note: that if you buy four books, of which 3 are | ||
different titles, you get a 10% discount on the 3 that | ||
form part of a set, but the fourth book still costs $8. | ||
|
||
Your mission is to write a piece of code to calculate the | ||
price of any conceivable shopping basket (containing only | ||
books of the same series), giving as big a discount as | ||
possible. | ||
|
||
For example, how much does this basket of books cost? | ||
|
||
- 2 copies of the first book | ||
- 2 copies of the second book | ||
- 2 copies of the third book | ||
- 1 copy of the fourth book | ||
- 1 copy of the fifth book | ||
|
||
One way of grouping these 8 books is: | ||
|
||
- 1 group of 5 --> 25% discount (1st,2nd,3rd,4th,5th) | ||
- +1 group of 3 --> 10% discount (1st,2nd,3rd) | ||
|
||
This would give a total of: | ||
|
||
- 5 books at a 25% discount | ||
- +3 books at a 10% discount | ||
|
||
Resulting in: | ||
|
||
- 5 x (8 - 2.00) == 5 x 6.00 == $30.00 | ||
- +3 x (8 - 0.80) == 3 x 7.20 == $21.60 | ||
|
||
For a total of $51.60 | ||
|
||
However, a different way to group these 8 books is: | ||
|
||
- 1 group of 4 books --> 20% discount (1st,2nd,3rd,4th) | ||
- +1 group of 4 books --> 20% discount (1st,2nd,3rd,5th) | ||
|
||
This would give a total of: | ||
|
||
- 4 books at a 20% discount | ||
- +4 books at a 20% discount | ||
|
||
Resulting in: | ||
|
||
- 4 x (8 - 1.60) == 4 x 6.40 == $25.60 | ||
- +4 x (8 - 1.60) == 4 x 6.40 == $25.60 | ||
|
||
For a total of $51.20 | ||
|
||
And $51.20 is the price with the biggest discount. | ||
|
||
## Rust Installation | ||
|
||
Refer to the [exercism help page][help-page] for Rust installation and learning | ||
resources. | ||
|
||
## Writing the Code | ||
|
||
Execute the tests with: | ||
|
||
```bash | ||
$ cargo test | ||
``` | ||
|
||
All but the first test have been ignored. After you get the first test to | ||
pass, remove the ignore flag (`#[ignore]`) from the next test and get the tests | ||
to pass again. The test file is located in the `tests` directory. You can | ||
also remove the ignore flag from all the tests to get them to run all at once | ||
if you wish. | ||
|
||
Make sure to read the [Modules](https://doc.rust-lang.org/book/second-edition/ch07-00-modules.html) chapter if you | ||
haven't already, it will help you with organizing your files. | ||
|
||
## Feedback, Issues, Pull Requests | ||
|
||
The [exercism/rust](https://github.com/exercism/rust) repository on GitHub is the home for all of the Rust exercises. If you have feedback about an exercise, or want to help implement new exercises, head over there and create an issue. Members of the [rust track team](https://github.com/orgs/exercism/teams/rust) are happy to help! | ||
|
||
If you want to know more about Exercism, take a look at the [contribution guide](https://github.com/exercism/docs/blob/master/contributing-to-language-tracks/README.md). | ||
|
||
[help-page]: http://exercism.io/languages/rust | ||
[modules]: https://doc.rust-lang.org/book/second-edition/ch07-00-modules.html | ||
[cargo]: https://doc.rust-lang.org/book/second-edition/ch14-00-more-about-cargo.html | ||
|
||
## Source | ||
|
||
Inspired by the harry potter kata from Cyber-Dojo. [http://cyber-dojo.org](http://cyber-dojo.org) | ||
|
||
## Submitting Incomplete Solutions | ||
It's possible to submit an incomplete solution so you can see how others have completed the exercise. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,189 @@ | ||
use std::cmp::Ordering; | ||
use std::collections::{BTreeSet, HashSet}; | ||
use std::collections::hash_map::DefaultHasher; | ||
use std::hash::{Hash, Hasher}; | ||
use std::mem; | ||
use std::cell::RefCell; | ||
|
||
extern crate noisy_float; | ||
use noisy_float::prelude::*; | ||
|
||
type Book = usize; | ||
type GroupedBasket = Vec<Group>; | ||
type Price = f64; | ||
const BOOK_PRICE: Price = 8.0; | ||
|
||
#[derive(Debug, Clone, PartialEq, Eq)] | ||
struct Group(RefCell<BTreeSet<Book>>); | ||
|
||
impl Group { | ||
fn new() -> Group { | ||
Group(RefCell::new(BTreeSet::new())) | ||
} | ||
|
||
fn new_containing(book: Book) -> Group { | ||
let g = Group::new(); | ||
g.0.borrow_mut().insert(book); | ||
g | ||
} | ||
|
||
fn price(&self) -> Price { | ||
(self.0.borrow().len() as Price) * BOOK_PRICE * | ||
match self.0.borrow().len() { | ||
2 => 0.95, | ||
3 => 0.90, | ||
4 => 0.80, | ||
5 => 0.75, | ||
_ => 1.0, | ||
} | ||
} | ||
} | ||
|
||
|
||
impl Ord for Group { | ||
// we want to order groups first by qty contained DESC, then by lowest value ASC | ||
fn cmp(&self, other: &Group) -> Ordering { | ||
match other.0.borrow().len().cmp(&self.0.borrow().len()) { | ||
Ordering::Equal => { | ||
if self.0.borrow().len() == 0 { | ||
Ordering::Equal | ||
} else { | ||
self.0.borrow().iter().next().unwrap().cmp( | ||
other | ||
.0 | ||
.borrow() | ||
.iter() | ||
.next() | ||
.unwrap(), | ||
) | ||
} | ||
} | ||
otherwise => otherwise, | ||
} | ||
} | ||
} | ||
|
||
impl PartialOrd for Group { | ||
fn partial_cmp(&self, other: &Group) -> Option<Ordering> { | ||
Some(self.cmp(other)) | ||
} | ||
} | ||
|
||
impl Hash for Group { | ||
fn hash<H: Hasher>(&self, hasher: &mut H) { | ||
self.0.borrow().hash(hasher); | ||
} | ||
} | ||
|
||
fn basket_price(basket: &GroupedBasket) -> Price { | ||
basket.iter().map(|g| g.price()).sum() | ||
} | ||
|
||
/// Compute the hash of a GroupedBasket | ||
/// | ||
/// Note that we don't actually care at all about the _values_ within | ||
/// the groups, only their lengths. Therefore, let's hash not the actual | ||
/// GB but its lengths. | ||
fn hash_of(basket: &GroupedBasket) -> u64 { | ||
let lengths = basket | ||
.iter() | ||
.map(|g| g.0.borrow().len()) | ||
.collect::<Vec<_>>(); | ||
let mut hasher = DefaultHasher::new(); | ||
lengths.hash(&mut hasher); | ||
hasher.finish() | ||
} | ||
|
||
pub fn lowest_price(books: &[Book]) -> Price { | ||
DecomposeGroups::new(books) | ||
.map(|gb| r64(basket_price(&gb))) | ||
.min() | ||
.map(|r| r.raw()) | ||
.unwrap_or(0.0) | ||
} | ||
|
||
struct DecomposeGroups { | ||
prev_states: HashSet<u64>, | ||
next: Option<GroupedBasket>, | ||
} | ||
|
||
impl Iterator for DecomposeGroups { | ||
type Item = GroupedBasket; | ||
fn next(&mut self) -> Option<Self::Item> { | ||
// our goal here: produce a stream of valid groups, differentiated by their | ||
// counts, from most compact to most dispersed. | ||
// | ||
// Algorithm: | ||
// - Start with the most compact groups possible | ||
// - If the number of groups == 0 or the max population of any group == 1, return None | ||
// - For every item in the most populous group: | ||
// - Try removing it and adding it to a smaller group. | ||
// - Can any smaller group accept it? if yes, move it there and return | ||
// - If it cannot be added to any smaller group, try the next item from this set | ||
// - If no item from the most populous group can be added to any smaller group, | ||
// then move the last item from the most populous group into a new group, alone, | ||
// and return | ||
println!("DecomposeGroups.next() --> {:?}", self.next); | ||
let return_value = self.next.clone(); | ||
if let Some(groups) = mem::replace(&mut self.next, None) { | ||
if !(groups.is_empty() || groups.iter().all(|g| g.0.borrow().len() == 1)) { | ||
let mut hypothetical; | ||
for mpg_book in groups[0].0.borrow().iter() { | ||
for (idx, other_group) in groups[1..].iter().enumerate() { | ||
if !other_group.0.borrow().contains(mpg_book) { | ||
hypothetical = groups.clone(); | ||
hypothetical[0].0.borrow_mut().remove(mpg_book); | ||
hypothetical[1 + idx].0.borrow_mut().insert(*mpg_book); | ||
hypothetical.sort(); | ||
let hypothetical_hash = hash_of(&hypothetical); | ||
if !self.prev_states.contains(&hypothetical_hash) { | ||
self.prev_states.insert(hypothetical_hash); | ||
mem::replace(&mut self.next, Some(hypothetical)); | ||
return return_value; | ||
} | ||
} | ||
} | ||
} | ||
// we've gone through all the items of the most populous group, | ||
// and none of them can be added to any other existing group. | ||
// We need to create a new group; | ||
let book = { | ||
let backing_bt = groups[0].0.borrow(); | ||
let mut book_iter = backing_bt.iter(); | ||
book_iter.next().unwrap().clone() | ||
}; | ||
hypothetical = groups.clone(); | ||
hypothetical[0].0.borrow_mut().remove(&book); | ||
hypothetical.push(Group::new_containing(book)); | ||
hypothetical.sort(); | ||
self.prev_states.insert(hash_of(&hypothetical)); | ||
mem::replace(&mut self.next, Some(hypothetical)); | ||
} | ||
} | ||
return_value | ||
} | ||
} | ||
|
||
impl DecomposeGroups { | ||
fn new(books: &[Book]) -> DecomposeGroups { | ||
let mut book_groups = GroupedBasket::new(); | ||
'nextbook: for book in books { | ||
for idx in 0..book_groups.len() { | ||
if !book_groups[idx].0.borrow().contains(&book) { | ||
book_groups[idx].0.borrow_mut().insert(*book); | ||
continue 'nextbook; | ||
} | ||
} | ||
// if we're here, we still haven't found a place for the book. | ||
// better add it to a new group | ||
book_groups.push(Group::new_containing(*book)); | ||
} | ||
book_groups.sort(); | ||
println!("DecomposeGroups::new() --> {:?}", book_groups); | ||
|
||
DecomposeGroups { | ||
next: Some(book_groups), | ||
prev_states: HashSet::new(), | ||
} | ||
} | ||
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
pub fn lowest_price(_: &[usize]) -> f64 { | ||
unimplemented!() | ||
} |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
still need this?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No, but it does get captured by the test runner, so I didn't bother to take it out. That said, I don't object to removing it.