Hi there!
In this article I will try to show how different the solution can be for one not super hard task and also want to show you some tricks (maybe hacky, but it works in JS, not TS ;)) with reduce function.
First, let's take a look on the task and a super long, a bit crazy solution where I've tried to
Initial task (Book store)
As usual - this is a task from lovely and useful exercism.org portal and there is an instruction:
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.
...the rest of instructions are here
The goal - find the better discount for the customer depends on the set of the books he or she is buying.
Looong solution with BookDispenser class
So, in this solution I've tried to focused not on the descent algorithm itself, but mostly on OO approach using BookDispenser
machine
const getDiscountMultiplier = (discountInPercent) => 100 - discountInPercent;
const ONE_COPY_COST = 8;
const TWO_COPIES_DISCOUNT_PERCENT = 5;
const TWO_COPIES_DISCOUNT = getDiscountMultiplier(TWO_COPIES_DISCOUNT_PERCENT);
const TWO_COPIES_COST = 2 * ONE_COPY_COST * TWO_COPIES_DISCOUNT;
const THREE_COPIES_DISCOUNT_PERCENT = 10;
const THREE_COPIES_DISCOUNT = getDiscountMultiplier(THREE_COPIES_DISCOUNT_PERCENT);
const THREE_COPIES_COST = 3 * ONE_COPY_COST * THREE_COPIES_DISCOUNT;
const FOUR_COPIES_DISCOUNT_PERCENT = 20;
const FOUR_COPIES_DISCOUNT = getDiscountMultiplier(FOUR_COPIES_DISCOUNT_PERCENT);
const FOUR_COPIES_COST = 4 * ONE_COPY_COST * FOUR_COPIES_DISCOUNT;
const FIVE_COPIES_DISCOUNT_PERCENT = 25;
const FIVE_COPIES_DISCOUNT = getDiscountMultiplier(FIVE_COPIES_DISCOUNT_PERCENT);
const FIVE_COPIES_COST = 5 * ONE_COPY_COST * FIVE_COPIES_DISCOUNT;
const bestPriceForOneBook = Math.min(1 * ONE_COPY_COST * 100);
const bestPriceForTwoBooks = Math.min(
2 * bestPriceForOneBook,
1 * TWO_COPIES_COST
)
const bestPriceForThreeBooks = Math.min(
3 * bestPriceForOneBook,
1 * bestPriceForTwoBooks + 1 * bestPriceForOneBook,
1 * THREE_COPIES_COST
)
const bestPriceForFourBooks = Math.min(
4 * bestPriceForOneBook,
2 * bestPriceForTwoBooks,
1 * bestPriceForThreeBooks + 1 * bestPriceForOneBook,
1 * FOUR_COPIES_COST
)
const bestPriceForFiveBooks = Math.min(
5 * bestPriceForOneBook,
2 * bestPriceForTwoBooks + 1 * bestPriceForThreeBooks,
1 * bestPriceForThreeBooks + 1 * bestPriceForTwoBooks,
1 * bestPriceForFourBooks + 1 * bestPriceForOneBook,
1 * FIVE_COPIES_COST
)
//TODO: try to make it countable itself
const bestPricePerBookAmount = new Map([
[1, bestPriceForOneBook],
[2, bestPriceForTwoBooks],
[3, bestPriceForThreeBooks],
[4, bestPriceForFourBooks],
[5, bestPriceForFiveBooks],
])
class BookDispenser {
_booksByCount;
_dispenserOrder;
constructor(books) {
this._booksByCount = BookDispenser._getBooksByCount(books);
this._dispenserOrder = BookDispenser._countDispenserOrder(this._booksByCount);
}
_booksLeft() {
let stillBooksLeft = false;
for (let book = 1; book <= 5; book++) {
const booksLeft = this._booksByCount.get(book);
if (booksLeft > 0) {
stillBooksLeft = true;
break;
}
}
return stillBooksLeft;
}
dispense(numberOfBooks) {
let numberOfDispersedBooks = 0;
for (let index = 0; index < this._dispenserOrder.length; index++) {
const bookToDispense = this._dispenserOrder[index];
numberOfDispersedBooks += this.dispenseBook(bookToDispense);
if (numberOfDispersedBooks == numberOfBooks) break;
}
return numberOfDispersedBooks;
}
dispenseBook(bookNumber) {
const booksCount = this._booksByCount.get(bookNumber);
if (booksCount > 0) {
this._booksByCount.set(bookNumber , booksCount - 1);
return 1;
}
return 0;
}
getBatch(booksPerBatch, numberOfDispenses) {
const booksBatches = [];
let dispenses = 0;
let batch;
while ((!numberOfDispenses || dispenses < numberOfDispenses) &&
(batch = this.dispense(booksPerBatch))
) {
dispenses++;
// log({booksPerBatch, batch, dispenses, still: dispenses < numberOfDispenses });
booksBatches.push(batch);
}
return booksBatches;
}
getBatchReduced(booksPerBatch, dispenses = 1) {
const basketVariants = [];
const batch = this.getBatch(booksPerBatch, dispenses);
if (booksPerBatch >= 1) {
const reducedBatch = this.getBatch(
booksPerBatch === 1 ? 1 : booksPerBatch - 1
);
if (reducedBatch.length > 0) {
batch.push(...reducedBatch);
}
}
if (batch.length !== 0) {
basketVariants.push(...batch);
}
return basketVariants;
}
static _getBooksByCount = (books) => {
const booksByCount = new Map();
for (let book = 1; book <= 5; book++) {
const booksInBasket = books.filter(bookInBasket => bookInBasket === book).length;
booksByCount.set(book, booksInBasket);
}
return booksByCount;
}
static _countDispenserOrder(booksByCount) {
const bookEntries = [];
for (let entry of booksByCount.entries()) {
bookEntries.push(entry)
}
return bookEntries
.sort((a, b) => b[1] - a[1] )
.map(({0:a}) => a)
}
}
const cost = (books) => {
if (books.length === 0) return 0;
const basketVariants = [];
const addBatchIfNotAlreadyInVariants = (batch) => {
const isExists = basketVariants.some(basket => {
return ''+basket === ''+batch;
})
if (!isExists) {
basketVariants.push(batch);
}
}
for (let booksBatch = 5; booksBatch >= 1; booksBatch--) {
// TODO: fix dispenses; should be calculated, not constant
for (let dispenses = 1; dispenses < 10; dispenses++) {
const bookDispenser = new BookDispenser(books);
const reducedBatch = bookDispenser.getBatchReduced(booksBatch, dispenses);
addBatchIfNotAlreadyInVariants(reducedBatch);
}
const batch = new BookDispenser(books).getBatch(booksBatch);
addBatchIfNotAlreadyInVariants(batch);
}
log({ basketVariants });
return Math.min(...basketVariants.map(basket => {
return basket.reduce((sum, b) => sum + bestPricePerBookAmount.get(b), 0);
}))
};
const log = value => console.log(value);
Yep, it's a bit of over, I know :)
What maybe here can be pointed is a usage of Maps not objects, the benefit/different of which you can read on mozilla web site. The different...
Super short solution
Based on the great super concise solution from LittleArmstrong!
const cost = (books) => books.reduce((grouped, book) => {
grouped[book-1] = (grouped?.[book-1] ?? 0) + 1; return grouped;
}, []).reduce((grouped, amount) => {
for (let i = 0; i < amount; i++) grouped[i] = (grouped?.[i] ?? 0) + 1; return grouped;
}, []).map(
(amount, _ , group) => (amount === 5 & group.includes(3)) ? (group[group.indexOf(3)] += 1, amount - 1) : amount).map(amount => 800 * amount * (1 - ({2: 0.05, 3: 0.1, 4: 0.2, 5: 0.25}?.[amount] ?? 0))).reduce((acc, cur) => acc += cur, 0);
Yes, this is hardly readable, I know :)
Conclusion
In this example I show you two polar solutions for the same task and of course the truth is somewhere between :)
Notice the usage of reduce
function with the initial value as an accumulator. It's absolutely not trivial but possible.
Pros? The code looks a bit more functional and chained.
Cons? It's just looks hacky, hard to read and not highlighted in the MDN specification.
Please, feel free to provide your solution(s) in the comments or even better on exercism.org site.
Happy programming and refactoring! Let's stay tuned!
Top comments (3)
For better refactoring, you can analyze the code. See all places that can be improved. You can use special tools, such as AppRefactoring or Sourcery AI
Thanks! Good advice. Will take a look on the tools
Hi again!
Is sourcery.ai only for Python for now? Or I did a bad search?