4
\$\begingroup\$

This code is similar to Underscore. I've added in some functions to fill in different use cases.

For example, one can use someKey to iterate through localStorage and sessionStorage.

Underscore does not have a good way to loop through localStorage / sessionStorage as it is incorrectly detected as an array like object, i.e. it has a length property but does not have indices.

/***************************************************************************************************
UTILITY

This is a small and efficient utility library.  There is additional coverage,
consistent ordering, consistent naming conventions, increased input validation, increased structure,
and fewer function branches compared to underscore.

*/

/*jslint
    browser: true,
    forin: true,
    plusplus: true,
    eqeq: true,
    ass: true
*/

(function (global, undef) {
    "use strict";

    // holds (Pub)lic properties for the package
    var Pub = {},

        // holds (Priv)ate properties for the package
        Priv = {},

        // native prototype methods
        nativeSlice = Array.prototype.slice,
        nativeSome = Array.prototype.some,
        nativeToString = Object.prototype.toString;

    // handles global variable management
    Pub.noWar = (function () {

        // Priv.g holds the single user-defined global variable
        Priv.g = '$A';
        Priv.previous = global[Priv.g];
        Pub.pack = {
            utility: true
        };
        return function () {
            var temp = global[Priv.g];
            global[Priv.g] = Priv.previous;
            return temp;
        };
    }());

    // returns type in a capitalized string form
    // typeof is only accurate for function, string, number, boolean, and
    // undefined.  null and array are both reported as objects
    // also typeof does not detect "boxed" values such as `new Number(1)`
    Pub.getType = function (obj) {
        return nativeToString.call(obj).slice(8, -1);
    };

    Pub.isType = function (type, obj) {
        return Pub.getType(obj) === type;
    };

    Pub.isGone = function (obj) {
        return obj == null;
    };

    // detects null, undefined, NaN, '', "", 0, -0, false
    Pub.isFalsy = function (obj) {
        return !obj;
    };

    Pub.hasLength = function (obj) {
        if (obj == null) {
            return false;
        }
        return obj.length === +obj.length;
    };

    // *underscore calls this, isObject
    Pub.isObjectWritable  = function (obj) {
        return Object(obj) === obj;
    };

    // *breaks naming convention for compatibility with underscore
    Pub.isObjectLiteral = function (obj) {
        return nativeToString.call(obj) === '[object Object]';
    };


    //compare to underscore
    //   - on a func truthy match returns true and on no match returns false
    //   - on func/obj validation fail returns false
    //   - does not insert identity function on func validation fail
    Pub.someKey = function (obj, func, con) {
        var key;
        if (typeof func !== 'function') {
            return false;
        }
        for (key in obj) {
            if (obj.hasOwnProperty(key)) {
                if (func.call(con, obj[key], key, obj)) {
                    return true;
                }
            }
        }
        return false;
    };

    Pub.someIndex = function (arr, func, con) {
        var ind,
            len;

        // validation - prevent type errors
        if ((arr == null) || (arr.length !== +arr.length) || (typeof func !== 'function')) {
            return false;
        }

        // delegate to native some()
        if (nativeSome && arr.some === nativeSome) {
            return arr.some(func, con);
        }
        for (ind = 0, len = arr.length; ind < len; ind++) {
            if (func.call(con, arr[ind], ind, arr)) {
                return true;
            }
        }
        return false;
    };

    Pub.someString = function (str, func, con) {
        if (typeof str !== 'string') {
            return false;
        }
        return Pub.someIndex(str.split(/\s+/), func, con);
    };

    Pub.morph = function (obj, func) {
        if (typeof func !== 'function') {
            return false;
        }
        Pub.someKey(obj, function (val, key) {
            obj[key] = func(val);
        });
        return obj;
    };

    // near direct copy from underscore
    Pub.lacks = function (obj) {
        Pub.someIndex(nativeSlice.call(arguments, 1), function (val) {
            var lacks;
            if (val) {
                for (lacks in val) {
                    if (obj[lacks] === undef) {
                        obj[lacks] = val[lacks];
                    }
                }
            }
        });
        return obj;
    };

    // shallow clone
    Pub.cloneFlat = function (obj) {
        if (!Pub.isObjectWritable(obj)) {
            return obj;
        }
        return Pub.isType('Array', obj) ? obj.slice() : Pub.extendFlat({}, obj);
    };

    // extends non-prototype properties from obj2 on to obj1
    // with out any over writing.  does not extend up the prototype chain.
    Pub.extendSafe = function (obj1, obj2) {
        var key;
        for (key in obj2) {
            if (obj2.hasOwnProperty(key)) {
                if (obj1.hasOwnProperty(key)) {
                    throw new Error("naming collision: " + key);
                }
                obj1[key] = obj2[key];
            }
        }
        return obj1;
    };

    // does not extend up the prototype chain like underscore
    Pub.extendFlat = function (obj) {
        Pub.someIndex(nativeSlice.call(arguments, 1), function (object) {
            var key;
            if (object) {
                for (key in object) {
                    if (object.hasOwnProperty(key)) {
                        obj[key] = object[key];
                    }
                }
            }
        });
        return obj;
    };

    // for adding underscore and a suffix
    Pub.addU = function (str, suf) {
        if (typeof str !== 'string' || typeof suf !== 'string') {
            return false;
        }
        return str + '_' + suf;
    };

    // for removing the last underscore and suffix
    Pub.removeU = function (str) {
        var res;
        if (typeof str !== 'string') {
            return false;
        }
        res = str.lastIndexOf("_");
        if (res !== -1) {
            return str.slice(0, res);
        }
        return false;
    };

    // first key that for / in will iterate through
    Pub.firstKey = function (obj) {
        var prop;
        for (prop in obj) {
            if (obj.hasOwnProperty(prop)) {
                return prop;
            }
        }
        return false;
    };

    Pub.testKeys = function (keys, pattern, func) {
        var test = '',
            key;
        for (key in keys) {
            test += key;
        }
        if (test === pattern) {
            func();
        }
    };

    // runtTest("test_foo", [input1, input2], function(arr){//write test here});
    Pub.runTest = (function () {
        var tests = {};
        return function (name, arr, func) {
            tests[name] = func.apply(this, arr);
        };
    }());

    Pub.prettyTime = function (post_time) {
        var NORMALIZE = 1000,   // 1000 milliseconds in a second
            MINUTE = 60,        // 60 seconds in a minute
            HOUR = 3600,        // 3600 seconds in an hour
            DAY = 43200,        // 43,200 seconds in a day

        // server time is in seconds while browser time is in milliseconds
            current_time = Math.round(Date.now() / NORMALIZE),
            rounded_time,
            elapsed_time,
            string = '';

        // synch factor actually exeeds transit time in some cases
        // post_time originates on the server as a unix time stamp
        // and current_time we calculate above
        if (current_time < post_time) {
            current_time = post_time;
        }
        elapsed_time = (current_time - post_time);
        if (elapsed_time === 0) {
            string = ' just a second ago';

        // 0 to 1 minute ago
        } else if ((elapsed_time > 0) && (elapsed_time < MINUTE)) {
            string = (elapsed_time === 1) ? 'one second ago' :
                    (elapsed_time + ' seconds ago');

        // 1 minute to 1 hour ago
        } else if ((elapsed_time >= MINUTE) && (elapsed_time < HOUR)) {
            rounded_time = Math.floor(elapsed_time / MINUTE);
            string = (rounded_time === 1) ? 'one minute ago' :
                    (rounded_time + ' minutes ago');

        // 1 hour to to 1 day ago
        } else if ((elapsed_time >= HOUR) && (elapsed_time < DAY)) {
            rounded_time = Math.floor(elapsed_time / HOUR);
            string = (rounded_time === 1) ? 'one hour ago' :
                    (rounded_time + ' hours ago');

        // more than 1 day ago
        } else if ((elapsed_time >= DAY)) {
            rounded_time = new Date(post_time * NORMALIZE);
            string = 'on ' + rounded_time.toLocaleDateString();
        }
        return string;
    };

    // Used for primitives saved to localStorage and sessionStorage
    Pub.unStringify = function (string) {

        // Booleans first
        if (string === "true") {
            return true;
        }
        if (string === "false") {
            return false;
        }
    };

    // for the arc library
    global[Priv.g] = Pub.extendSafe(global[Priv.g] || {}, Pub);

    // for underscore mixins
    global.ArcUtility = Pub;

}(this));
\$\endgroup\$
6
  • 4
    \$\begingroup\$ This question appears to be off-topic because it is about advertising your library, and not about how to code it better \$\endgroup\$
    – Malachi
    Oct 1, 2014 at 15:07
  • 2
    \$\begingroup\$ Welcome to CR! I voted to leave this post open, but I think it might be a good idea to edit your post with more context, fix typos and make it clearer that you're looking for a code review (wink-wink), not a usability review. \$\endgroup\$ Oct 1, 2014 at 15:45
  • \$\begingroup\$ You should probably limit the code you post to that which you have coded yourself that you want reviewed as well. \$\endgroup\$
    – Malachi
    Oct 1, 2014 at 15:47
  • 1
    \$\begingroup\$ Also it's better to write explanations and context outside of code blocks - are these comments in your actual code? \$\endgroup\$ Oct 1, 2014 at 15:49
  • 1
    \$\begingroup\$ I fixed the formatting a bit. The code does not have an API yet as nobody uses it. Once there is use I could create an API. It is a utiliy library like underscore, it does the same thing underscore does but fills in "gaps". Underscore was abstracted from a single project. This was abstracted out from my project, so its core is a bit different. I just want it reviewed as a javascript utility library or as an underscore mixin. It works both ways. \$\endgroup\$
    – user35776
    Oct 1, 2014 at 17:00

1 Answer 1

4
\$\begingroup\$

Quick review, in no particular order. I haven't gone line by line, I just scrolled around and noted what I saw.

Firstly, the good stuff:

  1. Consistent style (almost, see #8 below)
  2. "use strict";
  3. jslint-checked

Then, the not-so-good stuff

  1. Naming convention may be internally consistent (not that I can really tell), but your library is not, in my opinion, quite large enough to get away with it. The first function I come across is called noWar. This seems to be (the documentation is lacking) your library's implementation of the common noConflict function - so why not call it that? Right now it's just a cutesy name for a function that should aim for external consistency.
    It's almost ironic that a function that exists to play nice with other code goes out of its way to do things differently.

  2. isGone and isFalsy should be defined as negations of calling isHere and isTruthy respectively. Or vice-versa. The whole point is that those functions report the opposite of their counterparts; don't write separate logic for either one, even when that logic is pretty straightforward.

  3. Similar to the above, hasLength should also use isGone in its internal check. Don't repeat logic. And someIndex should be calling hasLength and isType in its checks. If your library doesn't trust its own functions, why should anyone?

  4. These two comments confuse me:

    // *underscore calls this, isObject
    ...
    // *breaks naming convention for compatibility with underscore
    

    So... in one case, you use your own name for something, underscore be damned, but in the very next function, you "break naming convention" (though I don't see how) specifically to match underscore. Huh?

  5. Naming in general: I just don't get many of these names, and with spotty documentation, I'm often left wondering. For instance testKeys. I can read the code, but I have no idea what I'd ever use it for.

  6. Speaking of testKeys: Why does it take a callback when it's not asynchronous? It can just return a boolean. How can I be sure it'll do what I expect? Objects are unordered, so if the keys object contains the keys foo and bar, and my pattern string is "foobar", the string that's being tested might still be "barfoo". And it doesn't use hasOwnProperty like everything else.

  7. runTest doesn't appear to let anyone access its internal tests object, nor does it return the result of the test being run. So you run a test, and... what? You always get undefined back, and tests is forever a "private closure".

  8. prettyTime. Nice - unless you want another language than English, of course. The entire function just doesn't seem germane to whatever else the library is doing.
    Furthermore it looks like a copy-paste job, since all the variables are snake_cased, while the rest of you code is camelCased. My bad, the style is consistent with the other functions (see comments). I would add, though, that distinguishing between "functions" and "variables" is sort of pointless in JS. Functions are variables. So I'm not sure the distinction makes sense.

  9. addU and removeU. These functions are just overly specific (and complex). Besides, removeU will fail if the original suffix contains an underscore already: removeU(addU("test", "my_suffix")) will return "test_my". So it'll only work for some kinds of strings, meaning it's probably built to work for your strings in your project. In another context, you or anyone else might have to do their own utility methods anyway, so as a utility library function there's not much value here, I'm afraid.

  10. firstKey. Again: Objects do not guarantee any ordering of their keys, so the function doesn't really do anything useful. At worst it misinforms the caller. It's entirely dependent on the vagaries of the runtime. To quote the spec: "The mechanics and order of enumerating the properties [...] is not specified." (emphasis added)

In all, this doesn't seem like a generic library (like underscore). It does have some generally applicable functions, sure, but it's also got some pretty context-specific and sometimes mystifying functions.

\$\endgroup\$
4
  • \$\begingroup\$ @www.arcmarks.com I don't see where the mixing-in is happening, and I don't see you relying on underscore anywhere, while you also seem to be functionally duplicating some things that underscore does do already, so it looks like an independent library. Esp. since you said your code is "similar to underscore", it seemed more like an alternative than a mixin. So I was confused \$\endgroup\$
    – Flambino
    Oct 2, 2014 at 12:00
  • \$\begingroup\$ @www.arcmarks.com (8) yes, I see that now. It's just that there more local variables in prettyTime, so it caught my eye there. So, granted, it's consistent. \$\endgroup\$
    – Flambino
    Oct 2, 2014 at 12:07
  • 4
    \$\begingroup\$ As always, a bang up job. @www.arcmarks.com I hope you will not delete your account again, we loose reps when you do that.. \$\endgroup\$
    – konijn
    Oct 2, 2014 at 12:13
  • \$\begingroup\$ @www.arcmarks.com Please don't treat the comments as a commit log :) It's cool that you've gotten some ideas for refactoring your code, but it'd be simpler to post a new question with the updated code, and let others have a go at it. \$\endgroup\$
    – Flambino
    Oct 2, 2014 at 14:41

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy