stringmap.js 7.7 KB
// 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;
}