tot - javascript/ruby.js
Not logged in
[Browse]  [Directory]  [Home]  [Login
[Reports]  [Search]  [Timeline
  [Raw
javascript/ruby.js
/*
**  ruby.js -- Ruby-style object extensions for JavaScript
**  Copyright (c) 2006 Florian Gross <flgr@ccan.de>
**  Copyright (c) 2007 Ralf S. Engelschall <rse@engelschall.com>
**  Licensed under GPL <http://www.gnu.org/licenses/gpl.txt>
**
**  This is derived from the Florian Gross' "ruby.js", version 0.4.0 as
**  of 2005-09-05 from http://flgr.0x42.net/ruby.js/, which implements
**  some of Ruby's standard library functions in JavaScript. It was
**  reformatted and cleaned up (especially semicolons additions to allow
**  code packing) by Ralf S. Engelschall.
**
**  $LastChangedDate: $
**  $LastChangedRevision: $
*/

/*
 *  Notice: This library heavily touches the global JavaScript namespace
 *  and actually doesn't create a custom namespace at all. It is
 *  expected that this library will become obsolete when the next
 *  revision of the ECMA-262 standard is released as there is plans to
 *  offer more functionality as part of the official language's standard
 *  library. Note that this implementation does not exactly mimic all
 *  of Ruby's standard library -- ".each" in general always yields
 *  value,key where key is the index for sequential containers and the
 *  Range implementation is known to have bugs.
 */

(function(){
    /*
     *  "Object" extensions
     */

    Object.prototype.clone = function (deepClone) {
        var result = new this.constructor();
        for (var property in this) {
            if (deepClone && typeof this[property] === 'object')
                result[property] = this[property].clone(deepClone);
            else
                result[property] = this[property];
        }
        return result;
    };

    Object.prototype.extend = function (other) {
        if (!this.mixins)
            this.mixins = [];
        this.mixins.push(other);
        for (var property in other)
            if (!this.hasOwnProperty(property))
                this[property] = other[property];
        return;
    };

    Object.prototype.cmp = function (other) {
        if (this < other)
            return -1;
        if (this > other)
            return +1;
        return 0;
    };

    Object.prototype.valuesAt = function () {
        var obj = this;
        return(arguments.toArray().map(function(index) {
            return obj[index];
        }));
    };

    Object.prototype.toArray = function () {
        if (!this.length)
            throw "Can't convert";
        var result = [];
        for (var i = 0; i < this.length; i++)
            result.push(this[i]);
        return result;
    };

    Object.prototype.hash = function () {
        return this.toSource().hash();
    };

    Object.prototype.instanceOf = function (klass) {
        return (this.constructor == klass);
    };

    Object.prototype.isA =
    Object.prototype.kindOf = function (klass) {
        if (this.instanceOf(klass))
            return true;
        if (this["mixins"] != undefined && this.mixins.includes(klass))
            return true;
        return false;
    };

    Object.prototype.methods = function () {
        var result = [];
        for (var property in this)
            if (typeof this[property] === "function")
                result.push(property);
        return result;
    };

    Object.prototype.respondTo = function (method) {
        return this.methods().includes(method);
    };

    Object.prototype.send = function(method) {
        var rest = arguments.toArray().last(-1);
        if (!this.respondTo(method))
            throw "undefined method";
        return this[method].apply(this, rest);
    };

    Object.prototype.instanceEval = function (code) {
        if (code.isA(Function))
            return code.apply(this);
        return eval(code.toString());
    };

    /*
     *  "Number" extensions
     */

    Number.prototype.times = function (block) {
        for (var i = 0; i < this; i++)
            block(i);
    };

    Number.prototype.upto = function (other, block) {
        for (var i = this; i <= other; i++)
            block(i);
    };

    Number.prototype.downto = function (other, block) {
        for (var i = this; i >= other; i--)
            block(i);
    };

    Number.prototype.towards = function (other, block) {
        var step = this.cmp(other);
        for (var i = this; i !== other - step; i -= step)
            block(i);
    };

    Number.prototype.succ = function () {
        return this + 1;
    };

    Number.prototype.pred = function () {
        return this - 1;
    };

    Number.prototype.chr = function () {
        return String.fromCharCode(this);
    };

    /*
     *  "Enumerable" addition
     */

    var Enumerable = new Object();

    Enumerable.eachWindow = function (window, block) {
        if (!window.isA(Range))
            window = range(0, window);
        var elements = [], pushed = 0;
        this.each(function (item, index) {
            elements.push(item);
            pushed += 1;
            if (pushed % window.rend == 0) {
                var start = [0, window.start - window.rend + pushed].max();
                var end   = [0, window.rend + pushed].max();
                block(elements.fetch(xrange(start, end)), index);
            }
        });
    };

    Enumerable.collect =
    Enumerable.map = function (block) {
        var result = [];
        this.each(function (item, index) {
            result.push(block(item, index));
        });
        return result;
    };

    Enumerable.toArray = Enumerable.entries = function () {
        return this.map(function (item) {
            return(item);
        });
    };

    Enumerable.inject = function (firstArg) {
        var state, block, first = true;
        if (arguments.length == 1)
            block = firstArg;
        else {
            state = firstArg;
            block = arguments[1];
        }
        this.each(function (item, index) {
            if (first && typeof state === "undefined")
                state = item, first = false;
            else
                state = block(state, item, index);
        });
        return state;
    };

    Enumerable.find =
    Enumerable.detect = function (block) {
        var result, done;
        this.each(function (item, index) {
            if (!done && block(item, index)) {
                result = item;
                done = true;
            }
        });
        return result;
    };

    Enumerable.findAll =
    Enumerable.select = function (block) {
        return this.inject([], function (result, item, index) {
            return (block(item, index) ? result.add(item) : result);
        });
    };

    Enumerable.grep = function (obj) {
        return this.findAll(function (item) {
            return obj.test(item);
        });
    };

    Enumerable.reject = function (block) {
        return this.select(function (item, index) {
            return !block(item, index);
        });
    };

    Enumerable.compact = function () {
        return this.select(function (item) {
            return (typeof item !== "undefined")
        });
    };

    Enumerable.nitems = function () {
        return this.compact().length;
    };

    Enumerable.sortBy = function (block) {
        return this.map(function(item, index) {
            return [ block(item, index), item ];
        }).sort(function (a, b) {
            return a[0].cmp(b[0]);
        }).map(function (item) {
            return item[1];
        });
    };

    Enumerable.all = function (block) {
        return (this.findAll(block).length == this.length);
    };

    Enumerable.any = function (block) {
        return (typeof this.find(block) !== "undefined");
    };

    Enumerable.includes = function (obj) {
        return this.any(function (item) {
            return (item === obj);
        });
    };

    Enumerable.index = function (obj) {
        var result;
        this.find(function (item, index) {
            if (obj == item) {
                result = index;
                return true;
            } else
                return false;
        });
        return result;
    };

    Enumerable.uniq = function () {
        return this.inject([], function (result, item) {
            return (result.includes(item) ? result : result.add(item));
        });
    };

    Enumerable.max = function (block) {
        if (!block)
            block = function (a, b) { return a.cmp(b); };
        return this.sort(block).last();
    };

    Enumerable.min = function (block) {
        if (!block) {
            block = function (a, b) { return a.cmp(b); };
        return this.sort(block).first();
    };

    Enumerable.partition = function (block) {
        var positives = [], negatives = [];
        this.each(function (item, index) {
            if (block(item, index))
                positives.push(item);
            else
                negatives.push(item);
        });
        return [positives, negatives];
    };

    Enumerable.zip = function () {
        var ary = arguments.toArray();
        ary.unshift(this);
        return ary.transpose();
    };

    Enumerable.flatten = function (depth) {
        if (depth == undefined)
            depth = -1;
        if (!depth)
            return this;
        return this.inject([], function(result, item) {
            var flatItem = item.respondTo("flatten") ? item.flatten(depth - 1) : [item];
            return result.merge(flatItem);
        });
    };

    /*
     *  "Array" extension
     */

    Array.fromObject = function (obj) {
        if (!obj.length)
            throw "Can't convert";
        var result = [];
        for (var i = 0; i < obj.length; i++)
            result.push(obj[i]);
        return result;
    };

    Array.prototype.transpose = function () {
        var result, length = -1;
        this.each(function (item, index) {
            if (length < 0) { /* first element */
                length = item.length;
                result = Array.withLength(length, function () {
                    return new Array(this.length);
                });
            } else if (length != item.length) {
                throw "Element sizes differ";
            }
            item.each(function (iitem, iindex) {
                result[iindex][index] = iitem;
            });
        });
        return result;
    };

    Array.withLength = function (length, fallback) {
        var result = [null].mul(length);
        result.fill(fallback);
        return result;
    };

    Array.prototype.each = function (block) {
        for (var index = 0; index < this.length; index++) {
            var item = this[index];
            block(item, index);
        }
        return this;
    };

    Array.prototype.extend(Enumerable);

    Array.prototype.isEmpty = function () {
        return (this.length == 0);
    };

    Array.prototype.at =
    Array.prototype.fetch = function (index, length) {
        if (index.isA(Range)) {
            var end = index.rend + (index.rend < 0 ? this.length : 0);
            index = index.start;
            length = end - index + 1;
        }
        if (length == undefined)
            length = 1;
        if (index < 0)
            index += this.length;
        var result = this.slice(index, index + length);
        return (result.length == 1 ? result[0] : result);
    };

    Array.prototype.first = function (amount) {
        if (amount == undefined)
            amount = 1;
        return this.at(xrange(0, amount));
    };

    Array.prototype.last = function (amount) {
        if (amount == undefined)
            amount = 1;
        return this.at(range(-amount, -1));
    };

    Array.prototype.store = function (index) {
        var length = 1, obj;
        arguments = arguments.toArray();
        arguments.shift();
        if (arguments.length == 2)
            length = arguments.shift();
        obj = arguments.shift();
        if (!obj.isA(Array))
            obj = [obj];
        if (index.isA(Range)) {
            var end = index.rend + (index.rend < 0 ? this.length : 0);
            index = index.start;
            length = end - index + 1;
        }
        if (index < 0)
            index += this.length;
        this.replace(this.slice(0, index).merge(obj).merge(this.slice(index + length)));
        return this;
    };

    Array.prototype.insert = function (index) {
        var values = arguments.toArray().last(-1);
        if (index < 0)
            index += this.length + 1;
        return this.store(index, 0, values);
    };

    Array.prototype.update = function (other) {
        var obj = this;
        other.each(function(item) { obj.push(item) });
        return obj;
    };

    Array.prototype.merge = Array.prototype.concat;

    Array.prototype.add = function (item) {
        return this.merge([item]);
    };

    Array.prototype.clear = function () {
        var obj = this;
        this.length.times(function (index) {
            delete obj[index];
        });
        this.length = 0;
    };

    Array.prototype.replace = function (obj) {
        this.clear();
        this.update(obj);
    };

    Array.prototype.mul = function (count) {
        var result = [];
        var obj = this;
        count.times(function() {
            result = result.merge(obj);
        });
        return result;
    };

    Array.prototype.fill = function (value) {
        var old_length = this.length;
        var obj = this;
        this.clear();
        var block;
        if (typeof value !== "function")
            block = function() { return(value) };
        else
            block = value;
        old_length.times(function (i) {
            obj.push(block(i));
        });
    };

    Array.prototype.removeAt = function (targetIndex) {
        var result = this[targetIndex];
        var newArray = this.reject(function (item, index) {
            return (index == targetIndex);
        });
        this.replace(newArray);
        return result;
    };

    Array.prototype.remove = function (obj) {
        this.removeAt(this.index(obj));
    };

    Array.prototype.removeIf = function (block) {
        this.replace(this.reject(block));
    };

    /*
     *  "Range" addition
     */

    function Range (start, end, excludeEnd) {
        this.begin = this.start = start;
        this.end = end;
        this.excludeEnd = excludeEnd;
        this.rend = excludeEnd ? end.pred() : end;
        this.length = this.toArray().length;
    };

    function range (start, end) {
        return (new Range(start, end));
    }
    function xrange(start, end) {
        return (new Range(start, end, true));
    }

    Range.prototype.toString = function () {
        return ("" + this.start + (this.excludeEnd ? "..." : "..") + this.end);
    }

    Range.prototype.each = function (block) {
        var index = 0;
        this.start.towards(this.rend, function (i) {
            return block(i, index++);
        });
    }

    Range.prototype.extend(Enumerable);

    Range.prototype.includes = function (item) {
        return (this.start.cmp(item) == -1 && this.rend.cmp(item) == +1);
    };

    /*
     *  "Hash" addition
     */

    function Hash (defaultBlock) {
        this.defaultBlock = defaultBlock;
        this.keys = [];
        this.values = [];
        this.length = 0;
    }

    Hash.fromArray = function (array) {
        var result = new Hash();
        array.each(function (item) {
            var key = item[0], value = item[1];
            result.store(key, value);
        });
        return result;
    };

    Hash.prototype.at =
    Hash.prototype.fetch = function (key, block) {
        if (this.hasKey(key))
            return this["item_" + key.hash()];
        else if (block)
            return block(key);
        else
            return defaultBlock(key);
    };

    Hash.prototype.store = function (key, value) {
        this.keys.push(key);
        this.values.push(value);
        this.length++;
        return (this["item_" + key.hash()] = value);
    };

    Hash.prototype.toA = function () {
        return this.keys.zip(this.values);
    };

    Hash.prototype.isEmpty = function () {
        return (this.length == 0);
    };

    Hash.prototype.has =
    Hash.prototype.includes =
    Hash.prototype.hasKey = function (key) {
        return hasOwnProperty("item_" + key.hash());
    };

    Hash.prototype.hasValue = function (value) {
        return this.values.includes(value);
    };

    Hash.prototype.each = function (block) {
        this.toA().each(function (pair) {
            return block(pair[1], pair[0]);
        });
    };

    Hash.prototype.extend(Enumerable);

    Hash.prototype.merge = function (other) {
        other.each(function (value, key) {
            this.store(key, value);
        });
    };

    Hash.prototype.remove = function (key) {
        var valueIndex = this.keys.index(key);
        var value = this.values[valueIndex];
        this.keys.remove(key);
        this.values.removeAt(valueIndex);
        delete(this["item_" + key.hash()]);
        this.length--;
        return [key, value];
    };

    Hash.prototype.removeIf = function (block) {
        this.each(function (value, key) {
            if (block(value, key))
                this.remove(key);
        });
    };

    Hash.prototype.shift = function () {
        return this.remove(this.keys[0]);
    };

    Hash.prototype.clear = function () {
        var obj = this;
        this.length.times(function() {obj.shift()});
    };

    Hash.prototype.replace = function (obj) {
        this.clear();
        this.merge(obj);
    };

    Hash.prototype.invert = function () {
        return Hash.fromArray(this.map(function (value, key) {
            return [value, key];
        }));
    };

    Hash.prototype.rehash = function () {
        var result = new Hash(this.defaultBlock);
        this.each(function (value, key) {
             result.store(key, value);
        });
        this.replace(result);
    };

    /*
     *  "MatchData" addition
     */

    function MatchData (matches, str, pos) {
        this.matches = matches, this.string = str;
        this.begin = this.position = pos;
        this.match = matches[0];
        this.captures = matches.slice(1);
        this.end = pos + this.match.length;
        this.length = matches.length;
        this.preMatch = str.substr(0, pos);
        this.postMatch = str.substr(this.end);
    }

    MatchData.prototype.toString = function () {
        return this.match;
    };

    MatchData.prototype.at = function (index) {
        return this.matches.at(index);
    };

    MatchData.prototype.toArray = function () {
        return this.matches;
    };

    /*
     *  "RegExp" extension
     */

    RegExp.prototype.match = function (str) {
        var matches = this.exec(str);
        if (matches) {
            var pos = str.search(this);
            return (new MatchData(matches, str, pos));
        }
    };

    /*
     *  "String" extension
     */

    String.prototype.clone = function () {
        return (new String(this));
    };

    String.prototype.each = function (block) {
        this.split("\n").each(block);
    };

    String.prototype.extend(Enumerable);

    String.prototype.toArray = function () {
        return this.split("\n");
    };

    String.prototype.towards = function (other, block) {
        var item = this;
        while (item.cmp(other) <= 0) {
            block(item);
            item = item.succ();
        }
    };

    String.prototype.hash = function () {
        var result = 0;
        this.split("").each(function (item) {
            result += item.charCodeAt(0);
            result += (result << 10);
            result ^= (result >> 6);
        })
        result += (result << 3);
        result ^= (result >> 11);
        result += (result << 15);
        return result;
    }

    String.prototype.chars = function () {
        return this.split("");
    };

    String.prototype.at =
    String.prototype.fetch = function (index, length) {
        if (index.isA(Range)) {
            var end = index.rend + (index.rend < 0 ? this.length : 0);
            index = index.start;
            length = end - index + 1;
        }
        if (length == undefined)
            length = 1;
        if (index < 0)
            index += this.length;
        return this.substr(index, length);
    };

    String.prototype.store =
    String.prototype.change = function (index) {
        var length = 1, obj;
        arguments = arguments.toArray();
        arguments.shift();
        if (arguments.length == 2)
            length = arguments.shift();
        obj = arguments.shift();
        if (index.isA(Range)) {
            var end = index.rend + (index.rend < 0 ? this.length : 0);
            index = index.start;
            length = end - index + 1;
        }
        if (index < 0)
            index += this.length;
        return (this.substr(0, index) + obj + this.substr(index + length));
    };

    String.prototype.reverse = function () {
        return this.split("").reverse().join("");
    };

    String.prototype.scan = function (pattern) {
        var str = this, result = [], oldPos = -1, match, offset = 0;
        while (match = pattern.match(str)) {
            if (match.end == match.begin)
                throw "Can't have null length matches with scan()";
            var newMatch = new MatchData(match.matches, match.string, match.position + offset);
            result.push(newMatch);
            str = match.postMatch;
            offset += match.toString().length;
        }
        return result;
    };

    String.prototype.sub = function (what, by, global) {
        var block = (typeof by === "function" ? by : function() { return(by) });
        var matches = this.scan(what), result = this, offset = 0;
        if (!global && !by.global)
            matches = matches.slice(0, 1);
        matches.each(function (match) {
            var replacement = block(match);
            offset += replacement.length - match.toString().length;
            result = result.change(match.begin + offset, match.toString().length, replacement);
        });
        return result;
    };

    String.prototype.gsub = function (what, by) {
        return this.sub(what, by, true);
    };

    String.prototype.tr = function (from, to) {
        var map = Hash.fromArray(from.chars().zip(to.chars()));
        return (this.chars().map(function (chr) {
            return (map.includes(chr) ? map.fetch(chr) : chr);
        }).join(""));
    };

    String.prototype.mul = function (other) {
        var result = "", str = this;
        other.times(function() {
            result += str;
        });
        return result;
    };

    String.prototype.isUpcase = function () {
        return (this == this.upcase());
    };

    String.prototype.isDowncase = function () {
        return (this == this.downcase());
    }

    String.prototype.isCapitalized = function () {
        return (   this.fetch(0).isUpcase()
                && this.fetch(range(1, -1)).isDowncase());
    };

    String.prototype.upcase = String.prototype.toUpperCase;

    String.prototype.downcase = String.prototype.toLowerCase;

    String.prototype.capitalize = function () {
        return (this.fetch(0).upcase() + this.fetch(range(1, -1)).downcase());
    };

    String.prototype.swapcase = function () {
        return (this.chars().map(function (chr) {
            if (chr.isUpcase())
                return chr.downcase();
            if (chr.isDowncase())
                return chr.upcase();
            return chr;
        }).join(""));
    };

    String.prototype.ord = function () {
        return this.charCodeAt(0);
    };

    String.prototype.isEmpty = function () {
        return (this.length == 0);
    }

    String.prototype.succ = function () {
        if (this.isEmpty())
            return this;
        /* numerics */
        if (/^\d+$/.test(this))
            return ((Number(this) + 1).toString());
        /* just one character */
        if (this.length == 1) {
            /* letters */
            if (/[A-Za-z]/.test(this)) {
                var lastLetter  = this.isUpcase() ? 'Z' : 'z';
                var firstLetter = this.isUpcase() ? 'A' : 'a';
                return ((this == lastLetter) ? firstLetter.mul(2) : (this.ord() + 1).chr());
            }
            return (this == (-1).chr() ? 0.0.chr().mul(2) : (this.ord() + 1).chr());
        }
        /* multiple characters */
        var result = this;
        for (var index = this.length; --index >= 0; ) {
            var chr = this.at(index);
            if (chr.succ().length == 1 || index == 0)
                return result.change(index, chr.succ());
            result = result.change(index, chr.succ().at(-1));
        }
        return result;
    };

    String.prototype.ljust = function (length, fill) {
        if (!fill)
            fill = " ";
        if (fill.length > 1)
            throw "TODO: Make fills with length > 1 work.";
        return (this + fill.mul(length / fill.length - this.length));
    };

})();