245 lines
7.7 KiB
JavaScript
245 lines
7.7 KiB
JavaScript
// stringmap.js
|
|
// MIT licensed, see LICENSE file
|
|
// Copyright (c) 2013 Olov Lassus <olov.lassus@gmail.com>
|
|
|
|
var StringMap = (function() {
|
|
"use strict";
|
|
|
|
// to save us a few characters
|
|
var hasOwnProperty = Object.prototype.hasOwnProperty;
|
|
|
|
var create = (function() {
|
|
function hasOwnEnumerableProps(obj) {
|
|
for (var prop in obj) {
|
|
if (hasOwnProperty.call(obj, prop)) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
// FF <= 3.6:
|
|
// o = {}; o.hasOwnProperty("__proto__" or "__count__" or "__parent__") => true
|
|
// o = {"__proto__": null}; Object.prototype.hasOwnProperty.call(o, "__proto__" or "__count__" or "__parent__") => false
|
|
function hasOwnPollutedProps(obj) {
|
|
return hasOwnProperty.call(obj, "__count__") || hasOwnProperty.call(obj, "__parent__");
|
|
}
|
|
|
|
var useObjectCreate = false;
|
|
if (typeof Object.create === "function") {
|
|
if (!hasOwnEnumerableProps(Object.create(null))) {
|
|
useObjectCreate = true;
|
|
}
|
|
}
|
|
if (useObjectCreate === false) {
|
|
if (hasOwnEnumerableProps({})) {
|
|
throw new Error("StringMap environment error 0, please file a bug at https://github.com/olov/stringmap/issues");
|
|
}
|
|
}
|
|
// no throw yet means we can create objects without own enumerable props (safe-guard against VMs and shims)
|
|
|
|
var o = (useObjectCreate ? Object.create(null) : {});
|
|
var useProtoClear = false;
|
|
if (hasOwnPollutedProps(o)) {
|
|
o.__proto__ = null;
|
|
if (hasOwnEnumerableProps(o) || hasOwnPollutedProps(o)) {
|
|
throw new Error("StringMap environment error 1, please file a bug at https://github.com/olov/stringmap/issues");
|
|
}
|
|
useProtoClear = true;
|
|
}
|
|
// no throw yet means we can create objects without own polluted props (safe-guard against VMs and shims)
|
|
|
|
return function() {
|
|
var o = (useObjectCreate ? Object.create(null) : {});
|
|
if (useProtoClear) {
|
|
o.__proto__ = null;
|
|
}
|
|
return o;
|
|
};
|
|
})();
|
|
|
|
// stringmap ctor
|
|
function stringmap(optional_object) {
|
|
// use with or without new
|
|
if (!(this instanceof stringmap)) {
|
|
return new stringmap(optional_object);
|
|
}
|
|
this.obj = create();
|
|
this.hasProto = false; // false (no __proto__ key) or true (has __proto__ key)
|
|
this.proto = undefined; // value for __proto__ key when hasProto is true, undefined otherwise
|
|
|
|
if (optional_object) {
|
|
this.setMany(optional_object);
|
|
}
|
|
};
|
|
|
|
// primitive methods that deals with data representation
|
|
stringmap.prototype.has = function(key) {
|
|
// The type-check of key in has, get, set and delete is important because otherwise an object
|
|
// {toString: function() { return "__proto__"; }} can avoid the key === "__proto__" test.
|
|
// The alternative to type-checking would be to force string conversion, i.e. key = String(key);
|
|
if (typeof key !== "string") {
|
|
throw new Error("StringMap expected string key");
|
|
}
|
|
return (key === "__proto__" ?
|
|
this.hasProto :
|
|
hasOwnProperty.call(this.obj, key));
|
|
};
|
|
|
|
stringmap.prototype.get = function(key) {
|
|
if (typeof key !== "string") {
|
|
throw new Error("StringMap expected string key");
|
|
}
|
|
return (key === "__proto__" ?
|
|
this.proto :
|
|
(hasOwnProperty.call(this.obj, key) ? this.obj[key] : undefined));
|
|
};
|
|
|
|
stringmap.prototype.set = function(key, value) {
|
|
if (typeof key !== "string") {
|
|
throw new Error("StringMap expected string key");
|
|
}
|
|
if (key === "__proto__") {
|
|
this.hasProto = true;
|
|
this.proto = value;
|
|
} else {
|
|
this.obj[key] = value;
|
|
}
|
|
};
|
|
|
|
stringmap.prototype.remove = function(key) {
|
|
if (typeof key !== "string") {
|
|
throw new Error("StringMap expected string key");
|
|
}
|
|
var didExist = this.has(key);
|
|
if (key === "__proto__") {
|
|
this.hasProto = false;
|
|
this.proto = undefined;
|
|
} else {
|
|
delete this.obj[key];
|
|
}
|
|
return didExist;
|
|
};
|
|
|
|
// alias remove to delete but beware:
|
|
// sm.delete("key"); // OK in ES5 and later
|
|
// sm['delete']("key"); // OK in all ES versions
|
|
// sm.remove("key"); // OK in all ES versions
|
|
stringmap.prototype['delete'] = stringmap.prototype.remove;
|
|
|
|
stringmap.prototype.isEmpty = function() {
|
|
for (var key in this.obj) {
|
|
if (hasOwnProperty.call(this.obj, key)) {
|
|
return false;
|
|
}
|
|
}
|
|
return !this.hasProto;
|
|
};
|
|
|
|
stringmap.prototype.size = function() {
|
|
var len = 0;
|
|
for (var key in this.obj) {
|
|
if (hasOwnProperty.call(this.obj, key)) {
|
|
++len;
|
|
}
|
|
}
|
|
return (this.hasProto ? len + 1 : len);
|
|
};
|
|
|
|
stringmap.prototype.keys = function() {
|
|
var keys = [];
|
|
for (var key in this.obj) {
|
|
if (hasOwnProperty.call(this.obj, key)) {
|
|
keys.push(key);
|
|
}
|
|
}
|
|
if (this.hasProto) {
|
|
keys.push("__proto__");
|
|
}
|
|
return keys;
|
|
};
|
|
|
|
stringmap.prototype.values = function() {
|
|
var values = [];
|
|
for (var key in this.obj) {
|
|
if (hasOwnProperty.call(this.obj, key)) {
|
|
values.push(this.obj[key]);
|
|
}
|
|
}
|
|
if (this.hasProto) {
|
|
values.push(this.proto);
|
|
}
|
|
return values;
|
|
};
|
|
|
|
stringmap.prototype.items = function() {
|
|
var items = [];
|
|
for (var key in this.obj) {
|
|
if (hasOwnProperty.call(this.obj, key)) {
|
|
items.push([key, this.obj[key]]);
|
|
}
|
|
}
|
|
if (this.hasProto) {
|
|
items.push(["__proto__", this.proto]);
|
|
}
|
|
return items;
|
|
};
|
|
|
|
|
|
// methods that rely on the above primitives
|
|
stringmap.prototype.setMany = function(object) {
|
|
if (object === null || (typeof object !== "object" && typeof object !== "function")) {
|
|
throw new Error("StringMap expected Object");
|
|
}
|
|
for (var key in object) {
|
|
if (hasOwnProperty.call(object, key)) {
|
|
this.set(key, object[key]);
|
|
}
|
|
}
|
|
return this;
|
|
};
|
|
|
|
stringmap.prototype.merge = function(other) {
|
|
var keys = other.keys();
|
|
for (var i = 0; i < keys.length; i++) {
|
|
var key = keys[i];
|
|
this.set(key, other.get(key));
|
|
}
|
|
return this;
|
|
};
|
|
|
|
stringmap.prototype.map = function(fn) {
|
|
var keys = this.keys();
|
|
for (var i = 0; i < keys.length; i++) {
|
|
var key = keys[i];
|
|
keys[i] = fn(this.get(key), key); // re-use keys array for results
|
|
}
|
|
return keys;
|
|
};
|
|
|
|
stringmap.prototype.forEach = function(fn) {
|
|
var keys = this.keys();
|
|
for (var i = 0; i < keys.length; i++) {
|
|
var key = keys[i];
|
|
fn(this.get(key), key);
|
|
}
|
|
};
|
|
|
|
stringmap.prototype.clone = function() {
|
|
var other = stringmap();
|
|
return other.merge(this);
|
|
};
|
|
|
|
stringmap.prototype.toString = function() {
|
|
var self = this;
|
|
return "{" + this.keys().map(function(key) {
|
|
return JSON.stringify(key) + ":" + JSON.stringify(self.get(key));
|
|
}).join(",") + "}";
|
|
};
|
|
|
|
return stringmap;
|
|
})();
|
|
|
|
if (typeof module !== "undefined" && typeof module.exports !== "undefined") {
|
|
module.exports = StringMap;
|
|
}
|