Refactoring. From 6 to 180 lines.
Tags: programming, javascript, refactorit, refactoring
Published: 2023-01-07 12:09:31

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](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/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](https://exercism.org/tracks/javascript/exercises/book-store) 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](https://exercism.org/tracks/javascript/exercises/book-store)_
> **_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_ 

```javascript
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](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map#objects_vs._maps). The different... 

### Super short solution

Based on the great super concise solution from [LittleArmstrong](https://exercism.org/tracks/javascript/exercises/book-store/solutions/LittleArmstrong)!

```javascript
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](https://exercism.org/) site. 

Happy programming and refactoring! Let's stay tuned!