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));
};
})();