DEV Community

Sergey Korsik
Sergey Korsik

Posted on

Refactoring. From 6 to 180 lines.

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);
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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)

Collapse
 
harryrefact profile image
Makarov

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

Collapse
 
dnim profile image
Sergey Korsik

Thanks! Good advice. Will take a look on the tools

Collapse
 
dnim profile image
Sergey Korsik • Edited

Hi again!
Is sourcery.ai only for Python for now? Or I did a bad search?