<commit_before><?php
/**
 * Library of Utility Functions in the Spirit of Underscore.js
 *
 * This source file is subject to the MIT license that is bundled
 * with this package in the file LICENSE.txt.
 *
 * @package    Underscore.php
 * @author     <NAME> <<EMAIL>>
 * @copyright  Copyright (c) <NAME>
 * @license    MIT License
 */

namespace
{
    function _c($collection) {
        return Underscore\chain($collection);
    }
}

/** @namespace */
namespace Underscore
{
    /**
     * Converts the list/array to a collection object
     *
     * @param  object|array $collection
     * @return Collection
     */
    function chain($collection)
    {
        return new Chain($collection);
    }

    class Chain implements \ArrayAccess, \IteratorAggregate, \Countable
    {
        /** @var array */
        protected $value;

        function __construct($value = array())
        {
            $this->value = toArray($value);
        }

        /**
         * Forwards calls to the underscore functions and passes the value
         * of the wrapped object as first argument
         *
         * @param  string $method
         * @param  array  $args
         * @return Chain
         */
        function __call($method, array $args)
        {
            array_unshift($args, $this->value);
            $this->value = call_user_func_array(__NAMESPACE__ . '\\' . $method, $args);
            return $this;
        }

        /**
         * Calls the callback with the value as argument without interrupting the chain
         *
         * @param  callback $callback
         * @return Chain
         */
        function tap($callback)
        {
            call_user_func($callback, $this->value);
            return $this;
        }

        function range($start, $stop, $step = 1)
        {
            $this->value = range($start, $stop, $step);
            return $this;
        }

        function shift()
        {
            return array_shift($this->value);
        }

        function unshift($value)
        {
            array_unshift($this->value, $value);
            return $this;
        }

        function pop()
        {
            return array_pop($this->value);
        }

        function push($value)
        {
            $this->value[] = $value;
            return $this;
        }

        function value()
        {
            return $this->value;
        }

        /**
         * Implement Countable
         *
         * @return int
         */
        function count()
        {
            return count($this->value);
        }

        /**
         * Implement IteratorAggregate
         */
        function getIterator()
        {
            return new ArrayIterator($this->value);
        }

        /*
         * Implement ArrayAccess
         */

        function offsetGet($offset)
        {
            return $this->value[$offset];
        }

        function offsetSet($offset, $value)
        {
            $this->value[$offset] = $value;
        }

        function offsetExists($offset)
        {
            return isset($this->value[$offset]);
        }

        function offsetUnset($offset)
        {
            unset($this->value[$offset]);
        }
    }

    /**
     * Small base class for dynamically extendable classes
     */
    abstract class Mixable
    {
        /** @var array Mixed in methods */
        private $mixins = array();

        /**
         * Check if the object responds to the given method
         *
         * @param string $method
         */
        function _respondsTo($method)
        {
            return is_callable(array($this, $method)) or array_key_exists($method, $this->mixins);
        }

        /**
         * Add dynamic methods
         */
        function _extend(array $spec)
        {
            foreach ($spec as $method => $callable) {
                $this->_def($method, $callable);
            }
            return $this;
        }

        function _def($method, $body)
        {
            if (!is_callable($body)) {
                throw new \InvalidArgumentException(sprintf(
                    "Dynamic methods must be callable, %s given", gettype($body)
                ));
            }

            $this->mixins[$method] = $body;
            return $this;
        }

        /**
         * Call the dynamic methods
         */
        function __call($method, array $args)
        {
            if (empty($this->mixins[$method])) {
                throw new \BadMethodCallException("Call to undefined method $method");
            }
            array_unshift($args, $this);
            return call_user_func_array($this->mixins[$method], $args);
        }
    }

    /**
     * Converts the given collection to an array
     *
     * @param  mixed Either a plain object or something Traversable
     * @return array
     */
    function toArray($collection)
    {
        if (is_array($collection)) {
            return $collection;
        }

        if ($collection instanceof \Traversable) {
            $array = iterator_to_array($collection);

        } else if (is_object($collection)) {
            $array = (array) $collection;
        }
        return $array;
    }

    function each($list, $iterator)
    {
        foreach ($list as $key => $value) {
            call_user_func($iterator, $value, $key);
        }
    }

    function map($list, $iterator)
    {
        $return = array();

        foreach ($list as $key => $value) {
            $return[$key] = call_user_func($iterator, $value, $key);
        }
        return $return;
    }

    function reduce($list, $iterator, $memo)
    {
        $list = toArray($list);
        return array_reduce($list, $iterator, $memo);
    }

    function detect($list, $iterator)
    {
        foreach ($list as $key => $value) {
            if (true === (bool) call_user_func($iterator, $value, $key)) {
                return $value;
            }
        }
        return false;
    }

    function select($list, $iterator)
    {
        $return = array();

        foreach ($list as $key => $value) {
            if (true === (bool) call_user_func($iterator, $value, $key)) {
                $return[$key] = $value;
            }
        }
        return $return;
    }

    function reject($list, $iterator)
    {
        $return = array();

        foreach ($list as $key => $value) {
            if (true !== (bool) call_user_func($iterator, $value, $key)) {
                $return[$key] = $value;
            }
        }
        return $return;
    }

    function all($list, $iterator)
    {
        $valid = true;

        foreach ($list as $key => $value) {
            $valid = (bool) call_user_func($iterator, $value, $key);
        }
        return $valid;
    }

    function any($list, $iterator)
    {
        return detect($list, $iterator) ? true : false;
    }

    function includes($list, $value)
    {
        if (is_array($list)) {
            return indexOf($list, $value) ? true : false;
        }
        foreach ($list as $value) {
            if ($v === $value) {
                return true;
            }
        }
        return false;
    }

    function invoke($list, $method/*, $arg,... */)
    {
        $args = array_slice(func_get_args(), 2);

        foreach ($list as $object) {
            if (is_callable($object, $method)) {
                call_user_func_array(array($object, $method), $args);
            }
        }
        return $list;
    }

    function pluck($list, $property)
    {
        $values = array();
        foreach ($list as $object) {
            if (empty($object->{$property})) {
                $value = null;
            } else {
                $value = $object->{$property};
            }
            $values[] = $value;
        }
        return $value;
    }

    function size($list)
    {
        if ($list instanceof \Traversable and !$list instanceof \Countable) {
            return iterator_count($list);
        }
        return count($list);
    }

    /**
     * Splits the string on spaces and returns the parts
     *
     * @param  string $string
     * @return array
     */
    function words($string)
    {
        return explode(" ", (string) $string);
    }

    /**
     * Deletes the given key from the array and returns his value
     *
     * @param  mixed $key   Key to search for
     * @param  array $array
     * @return mixed Value of the given key, NULL if key was not found in array
     */
    function deleteKey($key, &$array)
    {
        if (!isset($array[$key])) {
            return null;
        }
        $value = $array[$key];
        unset($array[$key]);
        return $value;
    }

    /**
     * Searches the given value in the array, unsets the found offset
     * and returns the value
     *
     * @param  mixed $value Value to search for
     * @param  array $array
     * @return mixed The value or NULL if the value was not found
     */
    function delete($value, &$array)
    {
        $offset = array_search($value, (array) $array);
        if (false === $offset) {
            return null;
        }
        unset($array[$offset]);
        return $value;
    }

    /**
     * Returns the array without all falsy values
     *
     * @param  array $array
     * @return array
     */
    function compact($array)
    {
        return array_filter(toArray($array));
    }

    function flatten($array)
    {
    }

    /**
     * Returns a copy of the array with all occurences of $value removed
     *
     * @param  array $array
     * @param  mixed $value
     * @return array
     */
    function without($array, $value)
    {
        $return = array();

        foreach ($array as $key => $v) {
            if ($value !== $v) {
                $return[$key] = $v;
            }
        }
        return $return;
    }

    /**
     * Returns a duplicate free version of the array
     *
     * @param array $array
     * @return array
     */
    function uniq($array)
    {
        return array_unique(toArray($array));
    }

    // TODO: Implement Algorithm
    function zip(/* $array,... */)
    {
        $arrays = func_get_args();
        $return = array();

    }

    /**
     * Searches the value in the array and returns its position
     *
     * @param  array $array
     * @param  mixed $value
     * @return mixed Index of the element or -1 if it was not found
     */
    function indexOf($array, $value)
    {
        $index = array_search($value, $array);
        return $index ?: -1;
    }

    /**
     * Returns the intersection of all given arrays
     *
     * @see \array_intersect
     * @return array
     */
    function intersect(/* $array,... */)
    {
        $arrays = func_get_args();

        foreach ($arrays as &$array) {
            $array = toArray($array);
        }

        return call_user_func_array("array_intersect", $arrays);
    }

    /**
     * Returns the first element of the array
     *
     * @return mixed
     */
    function first($array)
    {
        return array_shift(toArray($array));
    }

    /**
     * Returns the last element of the array
     *
     * @return mixed
     */
    function last($array)
    {
        return array_pop(toArray($array));
    }

    /*
     * Function functions
     */

    /**
     * Calls a supplied callback {n} times
     *
     * @param int $number
     * @param callback $callback
     * @param mixed $arg,...
     */
    function times($number, $callback/*, $arg,... */)
    {
        $args = array_slice(func_get_args(), 2);

        for ($i = 0; $i < $number; $i++) {
            call_user_func_array($callback, $args);
        }
    }

    /**
     * Returns an identity function
     *
     * An identity function is a function which returns it's passed argument unmodified,
     * which is useful for default loop callbacks
     *
     * @return callback
     */
    function identity()
    {
        return function($k) { return $k; };
    }

    /**
     * Wrap a function in another function and avoid a recursion by passing
     * the wrapped function as argument to the wrapper
     *
     * @param  callback $fn      The function to wrap
     * @param  callback $wrapper A wrapper function, receives the wrapped function as
     *                           first argument and the arguments passed to the wrapped
     *                           function as subsequent arguments
     * @return Closure
     */
    function wrap($fn, $wrapper)
    {
        // Unify calling of the wrapped function
        if (is_array($fn)) {
            $original = function() use ($fn) {
                return call_user_func_array($fn, func_get_args());
            };
        } else {
            $original = $fn;
        }

        $wrapped = function() use ($original, $wrapper) {
            $args = func_get_args();
            array_unshift($args, $original);
            return call_user_func_array($wrapper, $args);
        };

        return $wrapped;
    }

    /**
     * Caches the result of calls with the same arguments
     *
     * @param  callback $fn
     * @return Closure
     */
    function memoize($fn, $hashFunction = null)
    {
        return function() use ($fn) {
            static $results = array();

            $args = func_get_args();

            $hash = empty($hashFunction)
                ? md5(join($args, ",")) : call_user_func($hashFunction, $args);

            if (empty($results[$hash])) {
                $results[$hash] = call_user_func_array($fn, $args);
            }
            return $results[$hash];
        };
    }

    /**
     * Prefills the arguments of a given function
     *
     * @param  callback $fn        Function to curry
     * @param  mixed    $value,... Arguments for currying the function
     * @return Closure
     */
    function curry($fn/*, $arg,... */)
    {
        $curry = array_slice(func_get_args(), 1);

        return function() use ($fn, $curry) {
            $args = array_merge($curry, func_get_args());
            return call_user_func_array($fn, $args);
        };
    }

    /**
     * Composes multiple callback functions into one by passing each function's
     * return value as argument into the next function. The arguments passed to
     * the composed function get passed to the first (most inner) function.
     *
     * @param  callback $fn,... Functions to compose
     * @return Closure
     */
    function compose(/* $fn,... */)
    {
        $fns = func_get_args();

        return function() use ($fns) {
            $input = func_get_args();
            foreach ($fns as $fn) {
                $returnValue = call_user_func_array($fn, $input);
                $input = array($returnValue);
            }
            return $returnValue;
        };
    }

    /**
     * Calls the Setter Methods in the given object context for every key
     * in the supplied options. The Name of the Setter Method must be camelCased
     * and the key in the $options Array must have underscores
     * e.g. for the key "file_name" the Setter's name is "setFileName".
     *
     * @throws InvalidArgumentException If no object is given as context
     *
     * @param  object $context The object context in which the Setters get called
     * @param  array  $options Array containing key => value pairs
     * @param  array  $settableOptions Optional list of fields which are settable
     *                                on the object
     * @return bool  true if some options have been set in the context, false if no
     *               options were set
     */
    function setOptions($context, array $options, array $defaults = array())
    {
        if (!is_object($context)) {
	        throw new \InvalidArgumentException("Context for setting options is not an Object");
        }
        if (!$options) {
	        return false;
        }

        if ($defaults) {
	        $options = array_merge($defaults, $options);
        }

        foreach ($options as $key => $value) {
	        $setterName = "set" . camelize($key);

	        if   (!is_callable(array($context, $setterName))) continue;
	        else $context->{$setterName}($value);
        }
        return true;
    }

    /**
     * Camelizes a dash or underscore separated string
     *
     * @param  string $string
     * @param  bool   $pascalCase By default the first letter is uppercase
     * @return string
     */
    function camelize($string, $pascalCase = true)
    {
        $string = str_replace(array("-", "_"), " ", $string);
        $string = ucwords($string);
        $string = str_replace(" ", null, $string);

        if (!$pascalCase) {
            return lcfirst($string);
        }
        return $string;
    }
}

<commit_msg>Mixin::_extend now copies instance methods<commit_after><?php
/**
 * Library of Utility Functions in the Spirit of Underscore.js
 *
 * This source file is subject to the MIT license that is bundled
 * with this package in the file LICENSE.txt.
 *
 * @package    Underscore.php
 * @author     <NAME> <<EMAIL>>
 * @copyright  Copyright (c) <NAME>
 * @license    MIT License
 */

namespace
{
    /**
     * Utility function for creating chains
     */
    function _c($collection) {
        return Underscore\chain($collection);
    }
}

/** @namespace */
namespace Underscore
{
    /**
     * Converts the list/array to a collection object
     *
     * @param  object|array $collection
     * @return Collection
     */
    function chain($collection)
    {
        return new Chain($collection);
    }

    class Chain implements \ArrayAccess, \IteratorAggregate, \Countable
    {
        /** @var array */
        protected $value;

        function __construct($value = array())
        {
            $this->value = toArray($value);
        }

        /**
         * Forwards calls to the underscore functions and passes the value
         * of the wrapped object as first argument
         *
         * @param  string $method
         * @param  array  $args
         * @return Chain
         */
        function __call($method, array $args)
        {
            array_unshift($args, $this->value);
            $this->value = call_user_func_array(__NAMESPACE__ . '\\' . $method, $args);
            return $this;
        }

        /**
         * Calls the callback with the value as argument without interrupting the chain
         *
         * @param  callback $callback
         * @return Chain
         */
        function tap($callback)
        {
            call_user_func($callback, $this->value);
            return $this;
        }

        function range($start, $stop, $step = 1)
        {
            $this->value = range($start, $stop, $step);
            return $this;
        }

        function shift()
        {
            return array_shift($this->value);
        }

        function unshift($value)
        {
            array_unshift($this->value, $value);
            return $this;
        }

        function pop()
        {
            return array_pop($this->value);
        }

        function push($value)
        {
            $this->value[] = $value;
            return $this;
        }

        function value()
        {
            return $this->value;
        }

        /**
         * Implement Countable
         *
         * @return int
         */
        function count()
        {
            return count($this->value);
        }

        /**
         * Implement IteratorAggregate
         */
        function getIterator()
        {
            return new ArrayIterator($this->value);
        }

        /*
         * Implement ArrayAccess
         */

        function offsetGet($offset)
        {
            return $this->value[$offset];
        }

        function offsetSet($offset, $value)
        {
            $this->value[$offset] = $value;
        }

        function offsetExists($offset)
        {
            return isset($this->value[$offset]);
        }

        function offsetUnset($offset)
        {
            unset($this->value[$offset]);
        }
    }

    /**
     * Small base class for dynamically extendable classes
     */
    abstract class Mixable
    {
        /** @var array Mixed in methods */
        private $mixins = array();

        /**
         * Check if the object responds to the given method
         *
         * @param string $method
         */
        function _respondsTo($method)
        {
            return is_callable(array($this, $method)) or array_key_exists($method, $this->mixins);
        }

        /**
         * Add dynamic methods
         */
        function _extend($spec)
        {   
            if (is_string($spec) and class_exists($spec)) {
                $spec = new $spec;
            }
            
            /**
             * Copy public instance methods
             */
            if (is_object($spec)) {
                $methods = get_class_methods($spec);
                
                foreach ($methods as $method) {
                    $this->_def($method, array($spec, $method));
                }
            }
        
            foreach ($spec as $method => $callable) {
                try {
                    $this->_def($method, $callable);
                } catch (\InvalidArgumentException $e) {
                    continue;
                }
            }
            return $this;
        }

        function _def($method, $body)
        {
            if (!is_callable($body)) {
                throw new \InvalidArgumentException(sprintf(
                    "Dynamic methods must be callable, %s given", gettype($body)
                ));
            }

            $this->mixins[$method] = $body;
            return $this;
        }

        /**
         * Call the dynamic methods
         */
        function __call($method, array $args)
        {
            if (empty($this->mixins[$method])) {
                throw new \BadMethodCallException("Call to undefined method $method");
            }
            array_unshift($args, $this);
            return call_user_func_array($this->mixins[$method], $args);
        }
    }

    /**
     * Converts the given collection to an array
     *
     * @param  mixed Either a plain object or something Traversable
     * @return array
     */
    function toArray($collection)
    {
        if (is_array($collection)) {
            return $collection;
        }

        if ($collection instanceof \Traversable) {
            $array = iterator_to_array($collection);

        } else if (is_object($collection)) {
            $array = (array) $collection;
        }
        return $array;
    }

    function each($list, $iterator)
    {
        foreach ($list as $key => $value) {
            call_user_func($iterator, $value, $key);
        }
    }

    function map($list, $iterator)
    {
        $return = array();

        foreach ($list as $key => $value) {
            $return[$key] = call_user_func($iterator, $value, $key);
        }
        return $return;
    }

    function reduce($list, $iterator, $memo)
    {
        $list = toArray($list);
        return array_reduce($list, $iterator, $memo);
    }

    function detect($list, $iterator)
    {
        foreach ($list as $key => $value) {
            if (true === (bool) call_user_func($iterator, $value, $key)) {
                return $value;
            }
        }
        return false;
    }

    function select($list, $iterator)
    {
        $return = array();

        foreach ($list as $key => $value) {
            if (true === (bool) call_user_func($iterator, $value, $key)) {
                $return[$key] = $value;
            }
        }
        return $return;
    }

    function reject($list, $iterator)
    {
        $return = array();

        foreach ($list as $key => $value) {
            if (true !== (bool) call_user_func($iterator, $value, $key)) {
                $return[$key] = $value;
            }
        }
        return $return;
    }

    function all($list, $iterator)
    {
        $valid = true;

        foreach ($list as $key => $value) {
            $valid = (bool) call_user_func($iterator, $value, $key);
        }
        return $valid;
    }

    function any($list, $iterator)
    {
        return detect($list, $iterator) ? true : false;
    }

    function includes($list, $value)
    {
        if (is_array($list)) {
            return indexOf($list, $value) ? true : false;
        }
        foreach ($list as $value) {
            if ($v === $value) {
                return true;
            }
        }
        return false;
    }

    function invoke($list, $method/*, $arg,... */)
    {
        $args = array_slice(func_get_args(), 2);

        foreach ($list as $object) {
            if (is_callable($object, $method)) {
                call_user_func_array(array($object, $method), $args);
            }
        }
        return $list;
    }

    function pluck($list, $property)
    {
        $values = array();
        foreach ($list as $object) {
            if (empty($object->{$property})) {
                $value = null;
            } else {
                $value = $object->{$property};
            }
            $values[] = $value;
        }
        return $value;
    }

    function size($list)
    {
        if ($list instanceof \Traversable and !$list instanceof \Countable) {
            return iterator_count($list);
        }
        return count($list);
    }

    /**
     * Splits the string on spaces and returns the parts
     *
     * @param  string $string
     * @return array
     */
    function words($string)
    {
        return explode(" ", (string) $string);
    }

    /**
     * Deletes the given key from the array and returns his value
     *
     * @param  mixed $key   Key to search for
     * @param  array $array
     * @return mixed Value of the given key, NULL if key was not found in array
     */
    function deleteKey($key, &$array)
    {
        if (!isset($array[$key])) {
            return null;
        }
        $value = $array[$key];
        unset($array[$key]);
        return $value;
    }

    /**
     * Searches the given value in the array, unsets the found offset
     * and returns the value
     *
     * @param  mixed $value Value to search for
     * @param  array $array
     * @return mixed The value or NULL if the value was not found
     */
    function delete($value, &$array)
    {
        $offset = array_search($value, (array) $array);
        if (false === $offset) {
            return null;
        }
        unset($array[$offset]);
        return $value;
    }

    /**
     * Returns the array without all falsy values
     *
     * @param  array $array
     * @return array
     */
    function compact($array)
    {
        return array_filter(toArray($array));
    }

    function flatten($array)
    {
    }

    /**
     * Returns a copy of the array with all occurences of $value removed
     *
     * @param  array $array
     * @param  mixed $value
     * @return array
     */
    function without($array, $value)
    {
        $return = array();

        foreach ($array as $key => $v) {
            if ($value !== $v) {
                $return[$key] = $v;
            }
        }
        return $return;
    }

    /**
     * Returns a duplicate free version of the array
     *
     * @param array $array
     * @return array
     */
    function uniq($array)
    {
        return array_unique(toArray($array));
    }

    // TODO: Implement Algorithm
    function zip(/* $array,... */)
    {
        $arrays = func_get_args();
        $return = array();

    }

    /**
     * Searches the value in the array and returns its position
     *
     * @param  array $array
     * @param  mixed $value
     * @return mixed Index of the element or -1 if it was not found
     */
    function indexOf($array, $value)
    {
        $index = array_search($value, $array);
        return $index ?: -1;
    }

    /**
     * Returns the intersection of all given arrays
     *
     * @see \array_intersect
     * @return array
     */
    function intersect(/* $array,... */)
    {
        $arrays = func_get_args();

        foreach ($arrays as &$array) {
            $array = toArray($array);
        }

        return call_user_func_array("array_intersect", $arrays);
    }

    /**
     * Returns the first element of the array
     *
     * @return mixed
     */
    function first($array)
    {
        return array_shift(toArray($array));
    }

    /**
     * Returns the last element of the array
     *
     * @return mixed
     */
    function last($array)
    {
        return array_pop(toArray($array));
    }

    /*
     * Function functions
     */

    /**
     * Calls a supplied callback {n} times
     *
     * @param int $number
     * @param callback $callback
     * @param mixed $arg,...
     */
    function times($number, $callback/*, $arg,... */)
    {
        $args = array_slice(func_get_args(), 2);

        for ($i = 0; $i < $number; $i++) {
            call_user_func_array($callback, $args);
        }
    }

    /**
     * Returns an identity function
     *
     * An identity function is a function which returns it's passed argument unmodified,
     * which is useful for default loop callbacks
     *
     * @return callback
     */
    function identity()
    {
        return function($k) { return $k; };
    }

    /**
     * Wrap a function in another function and avoid a recursion by passing
     * the wrapped function as argument to the wrapper
     *
     * @param  callback $fn      The function to wrap
     * @param  callback $wrapper A wrapper function, receives the wrapped function as
     *                           first argument and the arguments passed to the wrapped
     *                           function as subsequent arguments
     * @return Closure
     */
    function wrap($fn, $wrapper)
    {
        // Unify calling of the wrapped function
        if (is_array($fn)) {
            $original = function() use ($fn) {
                return call_user_func_array($fn, func_get_args());
            };
        } else {
            $original = $fn;
        }

        $wrapped = function() use ($original, $wrapper) {
            $args = func_get_args();
            array_unshift($args, $original);
            return call_user_func_array($wrapper, $args);
        };

        return $wrapped;
    }

    /**
     * Caches the result of calls with the same arguments
     *
     * @param  callback $fn
     * @return Closure
     */
    function memoize($fn, $hashFunction = null)
    {
        return function() use ($fn) {
            static $results = array();

            $args = func_get_args();

            $hash = empty($hashFunction)
                ? md5(join($args, ",")) : call_user_func($hashFunction, $args);

            if (empty($results[$hash])) {
                $results[$hash] = call_user_func_array($fn, $args);
            }
            return $results[$hash];
        };
    }

    /**
     * Prefills the arguments of a given function
     *
     * @param  callback $fn        Function to curry
     * @param  mixed    $value,... Arguments for currying the function
     * @return Closure
     */
    function curry($fn/*, $arg,... */)
    {
        $curry = array_slice(func_get_args(), 1);

        return function() use ($fn, $curry) {
            $args = array_merge($curry, func_get_args());
            return call_user_func_array($fn, $args);
        };
    }

    /**
     * Composes multiple callback functions into one by passing each function's
     * return value as argument into the next function. The arguments passed to
     * the composed function get passed to the first (most inner) function.
     *
     * @param  callback $fn,... Functions to compose
     * @return Closure
     */
    function compose(/* $fn,... */)
    {
        $fns = func_get_args();

        return function() use ($fns) {
            $input = func_get_args();
            foreach ($fns as $fn) {
                $returnValue = call_user_func_array($fn, $input);
                $input = array($returnValue);
            }
            return $returnValue;
        };
    }

    /**
     * Calls the Setter Methods in the given object context for every key
     * in the supplied options. The Name of the Setter Method must be camelCased
     * and the key in the $options Array must have underscores
     * e.g. for the key "file_name" the Setter's name is "setFileName".
     *
     * @throws InvalidArgumentException If no object is given as context
     *
     * @param  object $context The object context in which the Setters get called
     * @param  array  $options Array containing key => value pairs
     * @param  array  $settableOptions Optional list of fields which are settable
     *                                on the object
     * @return bool  true if some options have been set in the context, false if no
     *               options were set
     */
    function setOptions($context, array $options, array $defaults = array())
    {
        if (!is_object($context)) {
	        throw new \InvalidArgumentException("Context for setting options is not an Object");
        }
        if (!$options) {
	        return false;
        }

        if ($defaults) {
	        $options = array_merge($defaults, $options);
        }

        foreach ($options as $key => $value) {
	        $setterName = "set" . camelize($key);

	        if   (!is_callable(array($context, $setterName))) continue;
	        else $context->{$setterName}($value);
        }
        return true;
    }

    /**
     * Camelizes a dash or underscore separated string
     *
     * @param  string $string
     * @param  bool   $pascalCase By default the first letter is uppercase
     * @return string
     */
    function camelize($string, $pascalCase = true)
    {
        $string = str_replace(array("-", "_"), " ", $string);
        $string = ucwords($string);
        $string = str_replace(" ", null, $string);

        if (!$pascalCase) {
            return lcfirst($string);
        }
        return $string;
    }
}


{'fraction_non_alphanumeric': 0.11969292976647426, 'fraction_numerical': 0.00046880951683319174, 'mean_word_length': 1.5997867154174283, 'pattern_counts': {'":': 0, '<': 15, '<?xml version=': 0, '>': 80, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 6, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '2828921', 'n_tokens_mistral': 9567, 'n_tokens_neox': 9267, 'n_words': 2655}