Dictionary

function defaultToString(item) {
  if (item === null) {
    return 'NULL';
  } else if (item === undefined) {
    return 'UNDEFINED';
  } else if (typeof item === 'string' || item instanceof String) {
    return `${item}`;
  }
  return item.toString();
}

class ValuePair {
  constructor (key, value) {
    this.key = key
    this.value = value
  }
  toString () {
    return `[#${this.key}, ${this.value}]`
  }
}

class Dictionary {
  constructor (toStrFn = defaultToString) {
    this.toStrFn = toStrFn
    this.table = {}         // table[key] = {key, value}
  }

  set (key, value) {
    if (key != null && value != null) {
      const tableKey = this.toStrFn(key)
      this.table[tableKey] = new ValuePair(key, value)
      return true
    }
    return false
  }
  remove (key) {
    if (this.hasKey(key)) {
      delete this.table[this.toStrFn(key)]
      return true
    }
    return false
  }
  hasKey (key) {
    return this.table[this.toStrFn(key)] != null
  }
  get (key) {
    const ValuePair = this.table[this.toStrFn(key)]
    return ValuePair == null ? undefined : ValuePair.value
  }
  clear () {
    this.table = {}
  }
  size () {
    return Object.keys(this.table).length
  }
  isEmpty () {
    return this.size() === 0
  }
  keys () {
    return this.keyValues().map(valuePair => valuePair.key)
  }
  values () {
    return this.keyValues().map(valuePair => valuePair.value)
  }
  keyValues () {
    return Object.values(this.table)
  }
  forEach (callbackFn) {
    const valuePairs = this.keyValues()
    for (let i = 0; i < valuePairs.length; i++) {
      const result = callbackFn(valuePairs[i].key, valuePairs[i].value)
      if (result === false) break
    }
  }
  toString () {
    if (this.isEmpty()) return ''
    const valuePairs = this.keyValues()
    let objString = `${valuePairs[0].toString()}`
    for (let i = 1; i < valuePairs.length; i++) {
      objString = `${objString}, ${valuePairs[i].toString()}`
    }
    return objString
  }
}