2

I'm trying to iterate through an array of digits and count how many times each digit is found in the array.

In ruby it's easy, I just declare a Hash.new(0) and that hash is already been set up for counting from 0 as a value. For example:

arr = [1,0,0,0,1,0,0,1]
counter = Hash.new(0)
arr.each { |num| counter[num] += 1 } # which gives {1=> 3, 0=> 5}

I wanted to do the same thing in JavaScript but let counter = {} gives me { '0': NaN, '1': NaN }.

Do you have any idea how to create that same Hash as an Object in JavaScript?

Aleksei Matiushkin
  • 119,336
  • 10
  • 100
  • 160
Commando
  • 358
  • 3
  • 16
  • 1
    _Sidenote:_ in _Ruby_ you are abusing `each` for reducing. It should be done as `arr.each_with_object(Hash.new{0}) { |num, counter| counter[num] += 1 }`. – Aleksei Matiushkin Aug 25 '19 at 05:07
  • 1
    _Sidenote:_ in _Ruby_ it might be more consise with `[1,0,0,0,1,0,0,1].group_by(&:itself).transform_values(&:count) #⇒ {1=>3, 0=>5}`. – Aleksei Matiushkin Aug 25 '19 at 05:20
  • 1
    You can't do this, JavaScript has no analog for `Hash.new(0)` in Ruby. You need to test whether the key exists before attempting to increment its value. – user229044 Aug 25 '19 at 05:46

3 Answers3

3

ECMAScript does not have default values for missing keys in objects the same way Ruby does for Hashes. You can, however, use dynamic introspective metaprogramming to do something similar, using ECMAScript Proxy objects:

const defaultValue = 42;
const proxyHandler = {
    get: (target, name) => name in target ? target[name] : defaultValue
};
const underlyingObject = {};

const hash = new Proxy(underlyingObject, proxyHandler);

1 in hash
//=> false
1 in underlyingObject
//=> false

hash[1]
//=> 42
underlyingObject[1]
//=> undefined

So, you could do something like this:

arr.reduce(
    (acc, el) => { acc[el]++; return acc }, 
    new Proxy(
        {},
        { get: (target, name) => name in target ? target[name] : 0 }
    )
)
//=> Proxy [ { '0': 5, '1': 3 }, { get: [Function: get] } ]

However, this is still not equivalent to the Ruby version, where the keys of the Hash can be arbitrary objects whereas the property keys in an ECMAScript object can only be Strings and Symbols.

The direct equivalent of a Ruby Hash is an ECMAScript Map.

Unfortunately, ECMAScript Maps don't have default values either. We could use the same trick we used for objects and create a Proxy, but that would be awkward since we would have to intercept accesses to the get method of the Map, then extract the arguments, call has, and so on.

Luckily, Maps are designed to be subclassable:

class DefaultMap extends Map {
    constructor(iterable=undefined, defaultValue=undefined) {
        super(iterable);
        Object.defineProperty(this, "defaultValue", { value: defaultValue });
    }

    get(key) {
        return this.has(key) ? super.get(key) : this.defaultValue;
    }
}

const hash = new DefaultMap(undefined, 42);

hash.has(1)
//=> false

hash.get(1)
//=> 42

This allows us to do something like this:

arr.reduce(
    (acc, el) => acc.set(el, acc.get(el) + 1), 
    new DefaultMap(undefined, 0)
)
//=> DefaultMap [Map] { 1 => 3, 0 => 5 }

Of course, once we start defining our own Map anyway, we might just go the whole way:

class Histogram extends DefaultMap {
    constructor(iterator=undefined) {
        super(undefined, 0);

        if (iterator) {
            for (const el of iterator) {
                this.set(el);
            }
        }
    }

    set(key) {
        super.set(key, this.get(key) + 1)
    }
}

new Histogram(arr)
//=> Histogram [Map] { 1 => 3, 0 => 5 }

This also demonstrates a very important lesson: the choice of data structure can vastly influence the complexity of the algorithm. With the correct choice of data structure (a Histogram), the algorithm completely vanishes, all we do is instantiate the data structure.

Note that the same is true in Ruby also. By choosing the right data structure (there are several implementations of a MultiSet floating around the web), your entire algorithm vanishes and all that is left is:

require 'multiset'

Multiset[*arr]
#=> #<Multiset:#5 0, #3 1>
Jörg W Mittag
  • 363,080
  • 75
  • 446
  • 653
2

In Javascript you do it with Array.reduce

const reducer = (acc, e) => acc.set(e, (acc.get(e) || 0) + 1);
[1, 0, 0, 0, 1, 0, 0, 1].reduce(reducer, new Map())
//⇒ Map(2) {1 => 3, 0 => 5}
Aleksei Matiushkin
  • 119,336
  • 10
  • 100
  • 160
  • That's the "correct" solution in Ruby as well `arr.each_with_object(Hash.new(0)) {|el, acc| acc[el] += 1 }`. The question is how to get a dictionary with default values. – Jörg W Mittag Aug 25 '19 at 14:41
0

You can use Map,

  • initialize hash as Map
  • Loop through array, if key is already present in hash increase it's value by 1 else set it to 1

let arr = [1, 0, 0, 0, 1, 0, 0, 1]
let hash = new Map()

arr.forEach(val => {
  hash.set(val, (hash.get(val) || 0) + 1)
})

console.log([...hash])
Code Maniac
  • 37,143
  • 5
  • 39
  • 60
  • I've done that idea with an if else statement: ` let hash = new Map() arr.forEach(num => { hash[num] ? hash[num]++ : hash[num] = 1 } ) ` . Is there any way in JavaScript to avoid the If else statement and just do the fast counting like in my ruby example in the post? – Commando Aug 25 '19 at 05:02
  • @Commando using `if else` statement doesn't affect performance much, the one i posted uses a `logical OR` – Code Maniac Aug 25 '19 at 05:10
  • Is there any benefit to using `Map`, `set`, and `get` vs the hash literal `{}` with `[]` and `[]=`? – max pleaner Aug 25 '19 at 05:47
  • @maxpleaner: ECMAScript objects only allow strings or symbols as keys. Anything else will be implicitly converted to strings. Depending on how close you want the analogy to Ruby to be, that could be a problem. E.g. in Ruby, I can have `{ 0 => 23, '0' => 42 }`, in ECMAScript, those would be the same key. `Map`s are the ECMAScript equivalent to Ruby `Hash`es. – Jörg W Mittag Aug 25 '19 at 06:00