Maddy is a functional object operations library. It provides various higher-order functions typical of a functional programming library, as well as specialized methods for determining deep equality and recursively serializing objects. All recursive methods fully support cyclic structures.
Maddy is compatible with web browsers, CommonJS environments, JavaScript engines, and asynchronous script loaders. It also normalizes infamous bugs present in older environments.
The project is hosted on GitHub, along with the unit tests and annotated source code. You can use the GitHub issue tracker to submit bug reports and discuss feature requests, or send tweets to @kitcambridge.
The current version is 0.3.0.
Development Version | Download (26 KB, commented and uncompressed) |
Production Version | Download (2.3 KB, compressed using Closure Compiler) |
Utility Methods | noConflict, curry |
Generic Methods | isPropertyOf, getClassOf, isEqual, stringify, isEmpty, keys, extend |
Functional Methods | forEach, map, fold, some, select, invoke, all, reject, max, min, partition, groupBy |
Maddy.noConflict()
Restores the original value of the global Maddy
variable and returns a reference to the Maddy object. noConflict
is not available in CommonJS environments, and cannot be invoked more than once.
var Associative = Maddy.noConflict();
typeof Maddy;
// => The type of the original value of the `Maddy` variable.
// `noConflict` can only be invoked once.
typeof Associative.noConflict;
// => "undefined"
typeof Associative.map;
// => "function"
Maddy.curry(function, [*arguments])
Partially applies a function, returning a function that, when invoked, calls the original function with the arguments prepended to any additional arguments.
curry
can be invoked on itself (i.e., Maddy.curry(Maddy.curry, ...)
) to produce a function that, when called, will return a curried function for further partial application. curry
can also be used with other higher-order functions, providing a convenient way to invoke a single callback function on multiple objects.
function convert(ratio, symbol, input) {
return (input * ratio).toFixed(1) + " " + symbol;
}
var pounds = Maddy.curry(convert, 2.2, "lbs.");
pounds(4);
// => "8.8 lbs."
var pints = Maddy.curry(convert, 1.98);
pints("U.S. pints", 2.4);
// => "4.8 U.S. pints"
var meta = Maddy.curry(Maddy.curry, convert);
var kilometers = meta(1.62, "km");
kilometers(34);
// => "55.1 km"
var callback = Maddy.curry(Maddy.forEach, function (key, value) {
console.log(key + ": " + value);
});
callback({"jdalton": "John-David", "mathias": "Mathias"});
// => Prints `"jdalton: John-David" and "mathias: Mathias"`
callback({"kitcam": "Kit", "maddy": "Maddy"});
// => Prints `"kitcam: Kit" and "maddy: Maddy"`
Maddy.isPropertyOf(object, property)
Determines if property is a direct property of the specified object.
function Person(name, age) {
this.name = name;
this.age = age;
}
Person.prototype.say = function (message) {
return this.name + ": " + message;
};
var kit = new Person("Kit", 18);
Maddy.isPropertyOf(kit, "name"); // => true
Maddy.isPropertyOf(kit, "age"); // => true
Maddy.isPropertyOf(kit, "say"); // => false
Maddy.getClassOf(object)
Returns the internal [[Class]]
name of the provided object. To account for cross-environment inconsistencies, only the following [[Class]]
names are supported: Function
, Array
, Date
, RegExp
, String
, Boolean
, and Number
.
Note that Null
and Undefined
are not [[Class]]
names, but are implemented for parity with Object::toString
.
Maddy.getClassOf(null); // => "Null"
Maddy.getClassOf(); // => "Undefined"
Maddy.getClassOf("Maddy"); // => "String"
Maddy.getClassOf(new String("Maddy")); // => "String"
Maddy.getClassOf(5); // => "Number"
Maddy.getClassOf(new Boolean(true)); // => "Boolean"
Maddy.getClassOf({}); // => "Object"
Maddy.getClassOf([]); // => "Array"
Maddy.getClassOf(function () {}); // => "Function"
Maddy.getClassOf(/(?:)/); // => "RegExp"
Maddy.getClassOf(new Date); // => "Date"
Maddy.isEqual([left, right])
Recursively compares two objects (left
and right
) using the following algorithm:
left === right
) are automatically equivalent.0
is not equal to -0
.[[Class]]
names are not equivalent.null
, undefined
, and functions are compared by identity."5"
is equivalent to new String("5")
.NaN
is equivalent to NaN
.new Date("Maddy")
) are not equivalent.Array(10)
) are supported.Note: JScript 5, implemented in Internet Explorer 6-8, treats undefined
elements in arrays as elisions. Thus, in Internet Explorer, sparse arrays (Array(3)
) are equivalent to dense arrays containing undefined
values ([undefined, undefined, undefined]
).
Maddy.isEqual(0, 0); // => true
Maddy.isEqual(0, -0); // => false
Maddy.isEqual("", 0); // => false
Maddy.isEqual(false, ""); // => false
Maddy.isEqual(null, undefined); // => false
Maddy.isEqual("Kit", new String("Kit")); // => true
Maddy.isEqual(new String("Kit"), new String("Kit")); // => true
Maddy.isEqual(new Number(3), new Number(3)); // => true
Maddy.isEqual(NaN, NaN); // => true
Maddy.isEqual(new Date(2009, 4, 9), new Date(2009, 4, 9)); // => true
Maddy.isEqual(new Date("Kit"), new Date("Kit")); // => false;
Maddy.isEqual(/(?:)/gim, /(?:)/gim); // => true
Maddy.isEqual(/(?:)/gi, /(?:)/gm); // => false
Maddy.isEqual(/Kit/gm, /Maddy/gm); // => false
var elements = [], equivalent = [];
elements.push(elements, 1, new String("Kit"), new Number(3), elements);
equivalent.push(equivalent, new Number(1), new String("Kit"), 3, equivalent);
Maddy.isEqual(elements, equivalent); // => true
elements = {};
equivalent = {};
elements.elements = elements;
equivalent.elements = equivalent;
Maddy.isEqual(elements, equivalent); // => true
Maddy.stringify(object)
Returns a debug-oriented representation of an object, similar to JSON.stringify
. Objects are serialized according to a superset of the JSON encoding algorithm:
null
are represented as such.Infinity
, NaN
, invalid dates, and all other non-JSON values are serialized as null
.YYYY-MM-DDTHH:mm:ss.sssZ
.Note that custom toJSON
methods are ignored.
Maddy.stringify([1, 2, 3, "Kit", {"name": "Maddy"}, NaN, Infinity]);
// => "[1, 2, 3, \"Kit\", {\"name\": \"Maddy\"}, null, null]"
Maddy.stringify("Hello\tthis\bis\\nice");
// => "\"Hello\tthis\bis\\nice\""
Maddy.stringify(/(?:)/);
// => "{\"source\": \"(?:)\", \"global\": false, \"ignoreCase\": false, \"multiline\": false}"
Maddy.stringify({"kitcam": [new String("Kit Cambridge"), new Number(18)]});
// => "{\"kitcam\": [\"Kit Cambridge\", 18]}"
Maddy.stringify(new Date(Date.UTC(1994, 06, 03)));
// => "\"1994-07-03T00:00:00.000Z\""
Maddy.stringify();
// => "null"
Maddy.stringify({"value": new Boolean(true), "toJSON": function () {
return this.value.valueOf();
}});
// => "{\"value\": true, \"toJSON\": null}"
var structure = [];
structure.push(structure, 2, structure, 4, structure);
Maddy.stringify(structure);
// => "[null, 2, null 4, null]"
Maddy.isEmpty(object)
Checks if the provided object is empty using the following algorithm:
null
, undefined
NaN
, Infinity
, and dates with invalid millisecond values are empty.length
of zero.isEmpty
does not distinguish between primitives and their corresponding object wrappers.
Maddy.isEmpty(null); // => true
Maddy.isEmpty(); // => true
Maddy.isEmpty(Infinity); // => true
Maddy.isEmpty(NaN); // => true
Maddy.isEmpty(new Date("Invalid Date")); // => true
Maddy.isEmpty({}); // => true
Maddy.isEmpty([1, 2, 3]); // => false
Maddy.isEmpty(""); // => true
Maddy.isEmpty(/(?:)/); // => false
Maddy.keys(object)
Returns a lexicographically-sorted array of an object’s property names.
Maddy.keys({"first": 1, "second": 2, "third": 3});
// => ["first", "second", "third"]
Maddy.keys({"name": "Kit", "age": 18, developer: true, hobbies: ["running", "programming"]});
// => ["age", "developer", "hobbies", "name"]
Maddy.extend(destination, *sources)
Extends a destination object with the properties of one or more sources.
Maddy.extend({"name": "Kit"}, {"age": 18}, {"hobbies": ["running", "programming"]});
// => {"name": "Kit", "age": 18, "hobbies": ["running", "programming"]}
Note: These methods should not be used with arrays or index-based collections, as they are exceedingly inefficient for this purpose. Consider Underscore or Functional instead.
Maddy.forEach(callback, [context], object)
Iterates over an object, invoking a callback function for each iteration. The iteration algorithm is normalized to account for cross-environment inconsistencies, including the JScript [[DontEnum]]
bug, the Safari 2 property shadowing bug, and inconsistent enumeration of the prototype
and constructor
properties.
The callback is bound to the optional context object, if one is specified, and accepts three arguments: (key, value, object)
.
Maddy.forEach(function (pseudonym, name, object) {
console.log(pseudonym + ": " + name);
}, {"jdalton": "John-David Dalton", "kitcam": "Kit Cambridge"});
// => Prints `"jdalton: John-David Dalton"` and `"kitcam: Kit Cambridge"`.
Maddy.map(callback, [context], object)
Returns an object containing the results of invoking a callback function on each object member.
Maddy.map(function (pseudonym, data) {
return [data.name, data.age];
}, {
"jdalton": {"name": "John-David", "age": 29},
"kitcam": {"name": "Kit Cambridge", "age": 18}
});
// => {"jdalton": ["John-David", 29], "kitcam": ["Kit Cambridge", 18]}
Maddy.fold(callback, [context], memo, object)
Reduces an object to a single value by successively invoking the callback function on each member. memo specifies the initial state of the reduction; each nth invocation of callback should return the value of memo to be used in the n+1th invocation.
The callback function accepts four arguments: (memo, key, value, object)
.
Maddy.fold(function (memo, name, occupation) {
return memo.push(name, occupation), memo;
}, [], {"kitcam": "programmer", "maddy": "runner"}).sort();
// => ["kitcam", "maddy", "programmer", "runner"]
Maddy.reduce(function (memo, name, year) {
return memo[year] = name, memo;
}, {}, {"Perl": 1987, "Haskell": 1990, "JavaScript": 1996});
// => {1987: "Perl", 1990: "Haskell", 1996: "JavaScript"}
Maddy.inject(function (memo, name, age) {
return memo + age;
}, 0, {"jdalton": 29, "kitcam": 18, "maddy": 17});
// => 64
Maddy.some(callback, [context], object)
Determines if the callback returns true
for at least one object member.
Maddy.some(function (name, year) {
return name > "Lua" && year > 1990;
}, {"Haskell": 1990, "JavaScript": 1996, "Python": 1991});
// => true
Maddy.select(callback, [context], object)
Returns an object containing all object members for which the callback returns true
. reject
is the opposite of this method.
Maddy.select(function (name, year) {
return year > 2007;
}, {"Functional": 2007, "Underscore": 2009, "Maddy": 2011});
// => {"Underscore": 2009, "Maddy": 2011}
Maddy.findAll(function (name, year) {
return name > "Sencha" && year > 2007;
}, {"Prototype": 2005, "MooTools": 2006, "Underscore": 2009});
// => {"Underscore": 2009}
Maddy.invoke(object, method, [*arguments])
Invokes a method with optional arguments on every object member value. invoke
does not directly mutate the original object, but the object may be mutated by the method.
Maddy.invoke({1990: "**Haskell**", 1993: "_*Ruby*_"}, "slice", 2, -2);
// => {1990: "Haskell", 1993: "Ruby"}
// Note that the original object may be indirectly mutated by the invoked method.
var developers = {"mathias": ["Mathias Bynens", 23], "kitcam": ["Kit Cambridge", 18]};
Maddy.send(developers, "shift");
// => {"mathias": "Mathias Bynens", "kitcam": "Kit Cambridge"}
developers;
// => {"mathias": [23], "kitcam": [18]}
Maddy.all(callback, [context], object)
Determines whether the callback returns true
for all object members.
Maddy.all(function (name, year) {
return name < "Visual Basic";
}, {"Haskell": 1990, "JavaScript": 1996, "Python": 1991});
// => true
Maddy.reject(callback, [context], object)
Returns an object containing all object members for which the callback returns false
. select
is the opposite of this method.
Maddy.reject(function (name, year) {
return year > 2010;
}, {"Backbone": 2010, "Underscore": 2009, "Maddy": 2011});
// => {"Backbone": 2010, "Underscore": 2009}
Maddy.max(callback, [context], object)
Returns the key of the maximum object member-based computation. Members are compared by invoking the callback on each member and comparing successive return values.
Maddy.max(function (pseudonym, data) {
return data.age;
}, {"jdalton": {"name": "John-David", "age": 29}, "mathias": {"name": "Mathias", "age": 23}});
// => "jdalton"
Maddy.min(callback, [context], object)
Returns the key of the minimum object member-based computation.
Maddy.min(function (pseudonym, data) {
return data.age;
}, {"mathias": {"name": "Mathias", "age": 23}, "kitcam": {"name": "Kit", "age": 18}});
// => "kitcam"
Maddy.partition(callback, [context], object)
Separates an object’s members into two groups: those for which the callback returns true
, and those for which it returns false
. partition
is preferred to invoking both select
and reject
on the same object.
Maddy.partition(function (pseudonym, name) {
return name.length > 5;
}, {"jdalton": "John-David", "mathias": "Mathias", "kitcam": "Kit", "maddy": "Maddy"});
// => [{"jdalton": "John-David", "mathias": "Mathias"}, {"kitcam": "Kit", "maddy": "Maddy"}]
Maddy.groupBy(callback, [context], object)
Groups an object’s members using the criteria specified by the callback.
Maddy.groupBy(function (name, dish) {
return dish;
}, {
"John-David": "lasagna",
"Mathias": "sushi",
"Maddy": "lasagna",
"Kit": "soup",
"Sam": "cinnamon rolls",
"Joe": "sushi",
"Mark": "sushi"
});
/* => {
"lasagna": {"John-David": "lasagna", "Maddy": "lasagna"},
"sushi": {"Mathias": "sushi", "Joe": "sushi", "Mark": "sushi"},
"soup": {"Kit": "soup"},
"cinnamon rolls": {"Sam": "cinnamon rolls"}
} */