Skip to content

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
merged 3 commits into from
Nov 11, 2017
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
12 changes: 12 additions & 0 deletions config.json
Original file line number Diff line number Diff line change
Expand Up @@ -760,6 +760,18 @@
"self mut"
]
},
{
"uuid": "a78ed17a-b2c0-485c-814b-e13ccd1f4153",
"slug": "book-store",
"core": false,
"unlocked_by": null,
"difficulty": 7,
"topics": [
"algorithms",
"groups",
"set theory"
]
},
{
"uuid": "704aab91-b83a-4e64-8c21-fb0be5076289",
"slug": "ocr-numbers",
Expand Down
3 changes: 3 additions & 0 deletions exercises/book-store/.gitignore
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
7 changes: 7 additions & 0 deletions exercises/book-store/Cargo-example.toml
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"
6 changes: 6 additions & 0 deletions exercises/book-store/Cargo.toml
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]
107 changes: 107 additions & 0 deletions exercises/book-store/README.md
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.
187 changes: 187 additions & 0 deletions exercises/book-store/example.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
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
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();

DecomposeGroups {
next: Some(book_groups),
prev_states: HashSet::new(),
}
}
}
3 changes: 3 additions & 0 deletions exercises/book-store/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
pub fn lowest_price(_: &[usize]) -> f64 {
unimplemented!()
}
Loading