/* ** ruby.js -- Ruby-style object extensions for JavaScript ** Copyright (c) 2006 Florian Gross ** Copyright (c) 2007 Ralf S. Engelschall ** Licensed under GPL ** ** 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)); }; })();