diff --git a/config.json b/config.json index c17d22f..b25d696 100644 --- a/config.json +++ b/config.json @@ -495,6 +495,14 @@ "prerequisites": [], "difficulty": 6 }, + { + "slug": "book-store", + "name": "Book Store", + "uuid": "5cda9fbb-709f-44c0-bddf-5cf2652eff01", + "practices": [], + "prerequisites": [], + "difficulty": 7 + }, { "slug": "satellite", "name": "Satellite", diff --git a/exercises/practice/book-store/.busted b/exercises/practice/book-store/.busted new file mode 100644 index 0000000..86b84e7 --- /dev/null +++ b/exercises/practice/book-store/.busted @@ -0,0 +1,5 @@ +return { + default = { + ROOT = { '.' } + } +} diff --git a/exercises/practice/book-store/.docs/instructions.md b/exercises/practice/book-store/.docs/instructions.md new file mode 100644 index 0000000..54403f1 --- /dev/null +++ b/exercises/practice/book-store/.docs/instructions.md @@ -0,0 +1,61 @@ +# Instructions + +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 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 (1st, 2nd,3rd, 4th, 5th) +- 1 group of 3 (1st, 2nd, 3rd) + +This would give a total of: + +- 5 books at a 25% discount +- 3 books at a 10% discount + +Resulting in: + +- 5 × (100% - 25%) × $8 = 5 × $6.00 = $30.00, plus +- 3 × (100% - 10%) × $8 = 3 × $7.20 = $21.60 + +Which equals $51.60. + +However, a different way to group these 8 books is: + +- 1 group of 4 books (1st, 2nd, 3rd, 4th) +- 1 group of 4 books (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 × (100% - 20%) × $8 = 4 × $6.40 = $25.60, plus +- 4 × (100% - 20%) × $8 = 4 × $6.40 = $25.60 + +Which equals $51.20. + +And $51.20 is the price with the biggest discount. diff --git a/exercises/practice/book-store/.meta/config.json b/exercises/practice/book-store/.meta/config.json new file mode 100644 index 0000000..4e48588 --- /dev/null +++ b/exercises/practice/book-store/.meta/config.json @@ -0,0 +1,19 @@ +{ + "authors": [ + "glennj" + ], + "files": { + "solution": [ + "book_store.moon" + ], + "test": [ + "book_store_spec.moon" + ], + "example": [ + ".meta/example.moon" + ] + }, + "blurb": "To try and encourage more sales of different books from a popular 5 book series, a bookshop has decided to offer discounts of multiple-book purchases.", + "source": "Inspired by the harry potter kata from Cyber-Dojo.", + "source_url": "https://cyber-dojo.org" +} diff --git a/exercises/practice/book-store/.meta/example.moon b/exercises/practice/book-store/.meta/example.moon new file mode 100644 index 0000000..92e548c --- /dev/null +++ b/exercises/practice/book-store/.meta/example.moon @@ -0,0 +1,65 @@ +import fold from require 'moon' + +BOOK_PRICE = 800 +DISCOUNTED = {[0]: 1.0, 1.0, 0.95, 0.90, 0.80, 0.75} + + +contains = (list, item) -> + for i, elem in ipairs list + return i if elem == item + nil + + +filter = (list, pred) -> + fold {{}, table.unpack list}, (filtered, elem) -> + table.insert filtered, elem if pred elem + filtered + + +group = (basket) -> + bundles = {{}} + for book in *basket + added = false + for bundle in *bundles + if not contains bundle, book + table.insert bundle, book + added = true + break + if not added + table.insert bundles, {book} + + return bundles + + +optimize = (bundles) -> + -- Two bundles of 4 are cheaper than a bundle of 5 plus a bundle of 3. + -- Look for a book in a 5-bundle that can be moved into a 3-bundle. + + bundle5 = filter bundles, (bundle) -> #bundle == 5 + return bundles if #bundle5 == 0 + + bundle3 = filter bundles, (bundle) -> #bundle == 3 + return bundles if #bundle3 == 0 + + b5 = bundle5[1] + b3 = bundle3[1] + + for book in *b5 + idx = contains b3, book + if not idx + table.insert b3, book + table.remove b5, idx + break + + return optimize bundles + + + +total_price = (basket) -> + bundles = optimize group basket + + fold {0, table.unpack bundles}, (price, bundle) -> + price + #bundle * BOOK_PRICE * DISCOUNTED[#bundle] + + +{ total: total_price } diff --git a/exercises/practice/book-store/.meta/spec_generator.moon b/exercises/practice/book-store/.meta/spec_generator.moon new file mode 100644 index 0000000..ffa8ca4 --- /dev/null +++ b/exercises/practice/book-store/.meta/spec_generator.moon @@ -0,0 +1,13 @@ +int_list = (list) -> "{#{table.concat list, ', '}}" + +{ + module_name: 'BookStore', + + generate_test: (case, level) -> + lines = { + "result = BookStore.#{case.property} #{int_list case.input.basket}", + "expected = #{case.expected}", + "assert.are.equal expected, result" + } + table.concat [indent line, level for line in *lines], '\n' +} diff --git a/exercises/practice/book-store/.meta/tests.toml b/exercises/practice/book-store/.meta/tests.toml new file mode 100644 index 0000000..4b7ce98 --- /dev/null +++ b/exercises/practice/book-store/.meta/tests.toml @@ -0,0 +1,64 @@ +# This is an auto-generated file. +# +# Regenerating this file via `configlet sync` will: +# - Recreate every `description` key/value pair +# - Recreate every `reimplements` key/value pair, where they exist in problem-specifications +# - Remove any `include = true` key/value pair (an omitted `include` key implies inclusion) +# - Preserve any other key/value pair +# +# As user-added comments (using the # character) will be removed when this file +# is regenerated, comments can be added via a `comment` key. + +[17146bd5-2e80-4557-ab4c-05632b6b0d01] +description = "Only a single book" + +[cc2de9ac-ff2a-4efd-b7c7-bfe0f43271ce] +description = "Two of the same book" + +[5a86eac0-45d2-46aa-bbf0-266b94393a1a] +description = "Empty basket" + +[158bd19a-3db4-4468-ae85-e0638a688990] +description = "Two different books" + +[f3833f6b-9332-4a1f-ad98-6c3f8e30e163] +description = "Three different books" + +[1951a1db-2fb6-4cd1-a69a-f691b6dd30a2] +description = "Four different books" + +[d70f6682-3019-4c3f-aede-83c6a8c647a3] +description = "Five different books" + +[78cacb57-911a-45f1-be52-2a5bd428c634] +description = "Two groups of four is cheaper than group of five plus group of three" + +[f808b5a4-e01f-4c0d-881f-f7b90d9739da] +description = "Two groups of four is cheaper than groups of five and three" + +[fe96401c-5268-4be2-9d9e-19b76478007c] +description = "Group of four plus group of two is cheaper than two groups of three" + +[68ea9b78-10ad-420e-a766-836a501d3633] +description = "Two each of first four books and one copy each of rest" + +[c0a779d5-a40c-47ae-9828-a340e936b866] +description = "Two copies of each book" + +[18fd86fe-08f1-4b68-969b-392b8af20513] +description = "Three copies of first book and two each of remaining" + +[0b19a24d-e4cf-4ec8-9db2-8899a41af0da] +description = "Three each of first two books and two each of remaining books" + +[bb376344-4fb2-49ab-ab85-e38d8354a58d] +description = "Four groups of four are cheaper than two groups each of five and three" + +[5260ddde-2703-4915-b45a-e54dbbac4303] +description = "Check that groups of four are created properly even when there are more groups of three than groups of five" + +[b0478278-c551-4747-b0fc-7e0be3158b1f] +description = "One group of one and four is cheaper than one group of two and three" + +[cf868453-6484-4ae1-9dfc-f8ee85bbde01] +description = "One group of one and two plus three groups of four is cheaper than one group of each size" diff --git a/exercises/practice/book-store/book_store.moon b/exercises/practice/book-store/book_store.moon new file mode 100644 index 0000000..5dfe72b --- /dev/null +++ b/exercises/practice/book-store/book_store.moon @@ -0,0 +1,4 @@ +{ + total: (basket) -> + error 'Implement me' +} diff --git a/exercises/practice/book-store/book_store_spec.moon b/exercises/practice/book-store/book_store_spec.moon new file mode 100644 index 0000000..4fda14a --- /dev/null +++ b/exercises/practice/book-store/book_store_spec.moon @@ -0,0 +1,92 @@ +BookStore = require 'book_store' + +describe 'book-store', -> + it 'Only a single book', -> + result = BookStore.total {1} + expected = 800 + assert.are.equal expected, result + + pending 'Two of the same book', -> + result = BookStore.total {2, 2} + expected = 1600 + assert.are.equal expected, result + + pending 'Empty basket', -> + result = BookStore.total {} + expected = 0 + assert.are.equal expected, result + + pending 'Two different books', -> + result = BookStore.total {1, 2} + expected = 1520 + assert.are.equal expected, result + + pending 'Three different books', -> + result = BookStore.total {1, 2, 3} + expected = 2160 + assert.are.equal expected, result + + pending 'Four different books', -> + result = BookStore.total {1, 2, 3, 4} + expected = 2560 + assert.are.equal expected, result + + pending 'Five different books', -> + result = BookStore.total {1, 2, 3, 4, 5} + expected = 3000 + assert.are.equal expected, result + + pending 'Two groups of four is cheaper than group of five plus group of three', -> + result = BookStore.total {1, 1, 2, 2, 3, 3, 4, 5} + expected = 5120 + assert.are.equal expected, result + + pending 'Two groups of four is cheaper than groups of five and three', -> + result = BookStore.total {1, 1, 2, 3, 4, 4, 5, 5} + expected = 5120 + assert.are.equal expected, result + + pending 'Group of four plus group of two is cheaper than two groups of three', -> + result = BookStore.total {1, 1, 2, 2, 3, 4} + expected = 4080 + assert.are.equal expected, result + + pending 'Two each of first four books and one copy each of rest', -> + result = BookStore.total {1, 1, 2, 2, 3, 3, 4, 4, 5} + expected = 5560 + assert.are.equal expected, result + + pending 'Two copies of each book', -> + result = BookStore.total {1, 1, 2, 2, 3, 3, 4, 4, 5, 5} + expected = 6000 + assert.are.equal expected, result + + pending 'Three copies of first book and two each of remaining', -> + result = BookStore.total {1, 1, 2, 2, 3, 3, 4, 4, 5, 5, 1} + expected = 6800 + assert.are.equal expected, result + + pending 'Three each of first two books and two each of remaining books', -> + result = BookStore.total {1, 1, 2, 2, 3, 3, 4, 4, 5, 5, 1, 2} + expected = 7520 + assert.are.equal expected, result + + pending 'Four groups of four are cheaper than two groups each of five and three', -> + result = BookStore.total {1, 1, 2, 2, 3, 3, 4, 5, 1, 1, 2, 2, 3, 3, 4, 5} + expected = 10240 + assert.are.equal expected, result + + pending 'Check that groups of four are created properly even when there are more groups of three than groups of five', -> + result = BookStore.total {1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 3, 3, 3, 3, 3, 3, 4, 4, 5, 5} + expected = 14560 + assert.are.equal expected, result + + pending 'One group of one and four is cheaper than one group of two and three', -> + result = BookStore.total {1, 1, 2, 3, 4} + expected = 3360 + assert.are.equal expected, result + + pending 'One group of one and two plus three groups of four is cheaper than one group of each size', -> + result = BookStore.total {1, 2, 2, 3, 3, 3, 4, 4, 4, 4, 5, 5, 5, 5, 5} + expected = 10000 + assert.are.equal expected, result diff --git a/exercises/test_helpers.md b/exercises/test_helpers.md new file mode 100644 index 0000000..d739746 --- /dev/null +++ b/exercises/test_helpers.md @@ -0,0 +1,127 @@ +# Reusable stuff for spec generators + +## Helper functions + +Useful for generating pretty tables mostly. + +- List of ints + ```moonscript + int_list = (list) -> "{#{table.concat list, ', '}}" + ``` + + Used in: all-your-base + +- List of lists of ints + ```moonscript + int_lists = (lists, level) -> + if #lists == 1 + "{#{int_list lists[1]}}" + else + rows = [indent int_list(row) .. ',', level + 1 for row in *lists] + table.insert rows, 1, '{' + table.insert rows, indent '}', level + table.concat rows, '\n' + ``` + + Used in: saddle-points, spiral-matrix + +- List of strings + ```moonscript + list_of_words = (list) -> + "{#{table.concat [quote word for word in *list], ', '}}" + ``` + The `quote` function is defined in bin/generate-spec + + Used in: anagram + +- Indented multi-line list of strings + ```moonscript + instruction_list = (list, level) -> + if #list <= 2 + "{#{table.concat [quote elem for elem in *list], ', '}}" + else + instrs = [indent quote(elem) .. ',', level + 1 for elem in *list] + table.insert instrs, 1, '{' + table.insert instrs, indent('}', level) + table.concat instrs, '\n' + ``` + The `indent` function is defined in bin/generate-spec + + This returns a multi-line string without the first line indented. + The returned string will be added to another string in the test case body, + and then that string (without regard to internal newlines) will later be indented. + + Used in: anagram, twelve-days + +- Key-Value list + + ```moonscript + kv_table = (tbl, level) -> + lines = {'{'} + for k, v in pairs tbl + key = if k\match('^%a%w*$') then k else "[#{quote k}]" + table.insert lines, indent "#{key}: #{v},", level + 1 + table.insert lines, indent '}', level + table.concat lines, '\n' + ``` + + Used in: word-count + +- Show strings with escapes, when you want to keep the `\t`, `\n` in the test case. + + ```moonscript + json = require 'dkjson' + -- and then + "result = Bob.hey #{json.encode case.input.heyBob}", + ``` + ```moonscript + json = require 'dkjson' + json_string = (s) -> json.encode s + ``` + + Used in: bob, matrix + +- Table contains an element + + ```moonscript + table_contains = (list, target) -> + for elem in *list + return true if elem == target + false + ```` + + Used in: forth + +- Wrapped list of ints + + sieve has a test with loads of numbers. + This _should_ work fine on Mac or Linux ("Works For Me™) + + ```moonscript + formatted = (list, level) -> + joined = table.concat list, ', ' + cmd = "echo '#{joined}' | fold -s -w 76" + fh = io.popen cmd, 'r' + return "{#{joined}}" if not fh + + lines = [indent(line\gsub('%s+$', ''), level + 1) for line in fh\lines!] + + result = {fh\close!} + lines = {joined} if not result[1] + + if #lines == 1 + "{#{lines[1]\gsub('^%s+', '')}}" + else + table.insert lines, 1, "{" + table.insert lines, (indent "}", level) + table.concat lines, '\n' + + ```` + + +## Custom assertions + +- dnd-character: `assert.between value, min, max` +- space-age: `assert.approx_equal #{case.expected}, result` +- word-count: `assert.has.same_kv table1, table2`:x +