DEV Community

Javascript Sock Merchant Challenge - Solution 2

Ady Ngom on April 26, 2019

You can find Part 1 here 02:15 Javascript Sock Merchant Challenge - Solut...
Collapse
 
qm3ster profile image
Mihail Malo • Edited

I'd write the provided code as follows:

function stockAndCount( n, arr ) {
  const colors = {}
  for (const color of arr)
    if (colors[color]) colors[color]++
    else colors[color] = 1

  return Object.values(colors).reduce((sum, b) => sum + (b/2|0), 0)
}
Enter fullscreen mode Exit fullscreen mode

Comments:

  1. Why is pairs defined at the top, when it's only used for the final summation? This increases cognitive overhead, since we need to dig deeper to realize it's not being used in the reduce loop.
  2. Why the reduce? We are mutating a single object by reference, and using reduce just forces us to thread the reference through.
  3. Why the explicit boolean coercion? It relies on the same truthiness rules the conditional expression does.
  4. Why a conditional expression? We aren't using the expression return value, we are performing side effects upon the acc object.
  5. Why Object.keys if we only care about values, regardless of the color?
  6. Why parseInt? It's slower and less idiomatic than Math.floor (I used |0 to make it "integer division")
  7. Why the if condition? Even if we did get 0 sometimes, x+=0 is fine to do, and more functional.

My impression of this code is that it's an improved algorithm, but looks to be in the state of an unfinished refactor.

Collapse
 
theodesp profile image
Theofanis Despoudis • Edited

I don't think you even need the last reducer. Just keep a running counter.

function sockMerchant(n, ar) {
    const colors = {}
    let pairs = 0;
    for (const color of ar)
        if (colors[color]) {
            colors[color] = 0;
            pairs += 1;
        } else {
            colors[color] = 1;
        }

    return pairs;
}

Most of the times it reports submillisecond cost with 10000 elements

Call to sockMerchant took 0.22000004537403584 milliseconds.

@Ady can you check this?

Collapse
 
qm3ster profile image
Mihail Malo • Edited

Another hilarious one:

function sockMerchant(n, ar) {
    const colors = {}
    let pairs = 0
    for (const color of ar) pairs += !(colors[color] = !colors[color])

    return pairs
}
Enter fullscreen mode Exit fullscreen mode
Thread Thread
 
johnboy5358 profile image
John

... and just when I thought it could not get better...

Sweet!

Thread Thread
 
adyngom profile image
Ady Ngom

Nice indeed 😀

Thread Thread
 
johnboy5358 profile image
John

MM has come up with some JS wizardry here ... flipping those 1's and 0's ... I would'nt have come up with it myself, but hats off to him; it's a cracker!

I wonder if he plays code golf?

Collapse
 
johnboy5358 profile image
John

An alternative solution:


const a = [10, 20, 20, 10, 10, 30, 50, 10, 20]
// [[10, 10, 10, 10], [20, 20, 20], [30], [50]]
// => 3 complete pairs (& 3 odd socks)

const group = (p,c) =>
  (c in p)
    ? {...p, [c]: p[c] + 1}
    : {...p, [c]: 1}

// Note: ~~ is Math.floor in js.
const sumPairs = (acc, x) => acc + ~~(x/2)

// Note: .map can be used for function composition provided you wrap the input ary in an array => [ary].
// for further info Visit https://egghead.io/lessons/javascript-linear-data-flow-with-container-style-types-box

const completePairs = (n, ary) =>
  [ary]
    .map(a => a.reduce(group, {}))
    .map(Object.values)
    .map(a => a.reduce(sumPairs,0))
    .pop()

console.log(completePairs(9, a))


Collapse
 
adyngom profile image
Ady Ngom

Hello John, I have definitely enjoyed reading your post and have learned a few nice tricks from it. My issue though with the solution will be readability first. It really takes a minute to grasp what is going on here. The second one is that a lot of transforms (map chain) are performed here where they probably could be taken care of by one computation reducing the complexity and improving the performance. Running against a simple benchmark this solution is over 300% slower than the proposed one.
I will host a webinar probably next week that will focus on why this second approach might be more suitable for this type of challenges. It will mean the world to me if you could attend and add to the discussion

Collapse
 
johnboy5358 profile image
John

A somewhat faster solution.


const count = (ofValue, ary) => {
  let i = ary.length
  let c = 0
  while(i--){ if(ary[i] === ofValue) {c++} }
  return c
}

const completePairs = (n, Ary) =>
  Array
    .from(new Set(Ary))
    .reduce((acc, v) => acc + ~~(count(v, Ary) / 2), 0)

My hope is that this is more readable and fast enough to satisfy most users.

Thread Thread
 
adyngom profile image
Ady Ngom

I will take a closer look and since you don’t like to be put on the spot 😀 I think I want to have our discussion transcend the comments section with an article that captures the exchange.
In the meantime I’ll run a benchmark on it but as far as readability this seems easier to digest IMHO.
I have started to write in a more functional way and isolating the arity for flexible composition. I think in the end it comes down to choosing the right tool and style for the right job.

Collapse
 
johnboy5358 profile image
John • Edited

Hello Ady, thanks for the reply.

I recognize that my solution must look a bit strange on first sight. I was reading on MDN recently about the draft proposal for the pipeline operator aka |> to be added to JavaScript(as yet unsupported).

I also had passed sometime on the egghead.io site
re: egghead.io/courses/professor-frisb... and was intrigued how .map is used here to produce a topdown workflow composing(or piping) one function's result to the next function. What a shame that, as you have discovered, JS is not performant in this way, because, I for one, think this is an elegant and clean workflow that I hope, one day, will be worth using.

Of course speed of execution is always an important issue and so for the moment, atleast, we fall back to the roots of JavaScript and use it's for, for of, while, etc loops. Shame, because I really do like a more declarative style of coding and here I was merely experimenting.

Perhaps you will forgive a quick hack to make my solution a tad more readable(see code below) by placing a pipe method on the Array prototype. Sorry, I can't do anything about JS performance, but I think somewhat less than 10,000 socks to sort would be favourable; that sock merchant needs to keep his house in closer order I think!

Incidentally, please excuse me if I decline the webinar participation; it really is not my thing and you may take this reply as my input to that.

Have a great day.


if(!Array.prototype.pipe) Array.prototype.pipe = Array.prototype.map
const reduce = (fn, init) => xs => Array.prototype.reduce.call(xs, fn, init)

const a = [10, 20, 20, 10, 10, 30, 50, 10, 20]
// [[10, 10, 10, 10], [20, 20, 20], [30], [50]]
// => 3 complete pairs (& 3 odd socks)

const group = (p,c) =>
  (c in p)
    ? {...p, [c]: p[c] + 1}
    : {...p, [c]: 1}

// Note: ~~ is Math.floor in js.
const sumPairs = (acc, x) => acc + ~~(x/2)

// Note: .pipe is function composition. Pipes the result of a function to the next.
// Visit https://egghead.io/lessons/javascript-linear-data-flow-with-container-style-types-box
const completePairs = (n, ary) =>
  [ary]
    .pipe(reduce(group, {}))
    .pipe(Object.values)
    .pipe(reduce(sumPairs,0))
    .pop()

console.log(completePairs(9, a))
// => 3

As a final request, I would be interested in what benchmark timings (in milliseconds) you achieved for my soloution vs your solution for a test array of 10,000 integers.

Collapse
 
theodesp profile image
Theofanis Despoudis

Looks complicated. Take a look at my comment.

Collapse
 
qm3ster profile image
Mihail Malo • Edited

My own solution would be:

function stockAndCount( n, arr ) {
  let pairs = 0
  const unpaired = new Set()

  for (const color of arr)
    if (unpaired.has(color)) {
      pairs++
      unpaired.delete(color)
    } else unpaired.add(color)

  return pairs
}

This doesn't iterate again over the colors encountered, and uses the minimal memory required for the task: The running total of pairs and the unpaired colors, encountering which again would be the completion of a pair.

Collapse
 
adyngom profile image
Ady Ngom • Edited

Hello Mihail and thank you for both of your detailed inputs. I have a final episode and webinar coming up to allow us in taking a detail look at why one solution over the other. If you go the Hacker Rank site you will in fact find numbers of way that this one challenge is solved. So it will be pretentious from me to say that this one is the best one of all the possible ones. But when it comes to the first solution provided in episode 1 and this one, this is by far a better solution as far as approach and performance.

I have added the two functions you have provided and run all three against a simple benchmark to see how each performs against a set of 10,000 items. Check the screenshot below:

sock merchant stock and count benchmark

// First solution episode 1
$node sockMerchant.js 
4997
Sort And Count: 74.724ms

// This solution episode 2
$node sockMerchant.js 
4997
Stock And Count: 16.788ms

// Your first solution
$node sockMerchant.js 
4997
Stock And Count 2: 49.022ms

// Your second solution
$node sockMerchant.js 
4997
Stock And Count 3: 77.768ms

I have call and run each of those separately running the setup below:

console time to run benchmarks

I really like the points you raised regarding side effects and type coercion. I will make sure that those are addressed during the webinar.

It will be great if you could join and offer some insights.

Cheers :)

Collapse
 
qm3ster profile image
Mihail Malo

Yup, my "highly scientific" browser benchmark also makes it seem that Set is slower:

function socks_set(arr) {
  let pairs = 0
  const unpaired = new Set()

  for (const color of arr)
  if (unpaired.has(color)) {
      pairs++
      unpaired.delete(color)
    } else unpaired.add(color)

  return pairs
}
function socks_obj(ar) {
  const colors = {}
  let pairs = 0
  for (const color of ar) pairs += !(colors[color] = !colors[color])

  return pairs
}
function* good() {
  let i = 0
  while (i < 100000000) yield i++ >> 1
  return i >> 1
}
function* bad() {
  let i = 0
  while (i < 10000000) yield i++
  return i
}

console.log(socks_set(good()))
console.log(socks_obj(good()))
console.time("good Set")
socks_set(good())
console.timeEnd("good Set")
console.time("good Obj")
socks_obj(good())
console.timeEnd("good Obj")
console.time("good Set")
socks_set(good())
console.timeEnd("good Set")
console.time("good Obj")
socks_obj(good())
console.timeEnd("good Obj")

console.log(socks_set(bad()))
console.log(socks_obj(bad()))
console.time("bad Set")
socks_set(bad())
console.timeEnd("bad Set")
console.time("bad Obj")
socks_obj(bad())
console.timeEnd("bad Obj")
console.time("bad Set")
socks_set(bad())
console.timeEnd("bad Set")
console.time("bad Obj")
socks_obj(bad())
console.timeEnd("bad Obj")