Skip to content

Instantly share code, notes, and snippets.

@jhyland87
Created November 23, 2016 16:45
Show Gist options
  • Save jhyland87/5447fa2d025c2da67c5435637bfa6254 to your computer and use it in GitHub Desktop.
Save jhyland87/5447fa2d025c2da67c5435637bfa6254 to your computer and use it in GitHub Desktop.
'use strict'
/**
* @title Lodash Mixins aka flat-line
* @description Extra useful Lodash mixins
* @requires lodash, crypto, ./data.js
*
* Note: A few of the mixins were originally from phpjs.org methods, and were modified to use some of the lodash methods,
* and to work as a mixin with the other methods. Also, they may have been optimized a bit, as they may have originally
* been created some time ago. The methods that were originally from phpjs.org are: utf8Encode, utf8Decode and sha1.
* Authors of borrowed functions are noted inside the functions themselves
*
* @author Justin Hyland (Mostly)
* @url https://github.com/SASSET/flat-line
* @see https://github.com/SASSET/flat-line
* @version 0.1.0
* @todo Split all functions into separate .js files; which can all be loaded by loading the index
*/
const Util = require('util')
/**
* @module _
*/
const _ = require('lodash')
// Get a fresh copy of lodash, since implementing mixins in the instance
// being used to add the mixins, doesn't work very well
const __ = _.runInContext()
const _m = _.runInContext()
// Used for makeHash
const crypto = require('crypto')
// Functions and storage for internal use only
const _internals = {
alternator: {
i: 0,
params: null
},
uncountable: require('./data').uncountable,
censored: require('./data').censored,
htmlEntities: {
'&': '&',
'<': '&lt;',
'>': '&gt;',
'"': '&quot;',
"'": '&#039;'
}
}
function generateSlug(){
// https://confluence.atlassian.com/bitbucket/what-is-a-slug-224395839.html
}
/**
* Partition Key Generator - Mimics the Jira Project key generation
*
* Rules:
* - First character must be a letter
* - All letters must be from teh Modern Roman Alphabet, and in upper case format
* - Only letters, numbers or the underscore character can be used
*
* @todo Make an option to grab the first uppercase alpha characters from the words, instead of just the first alpha
* @todo Add the ability to require a minimum length
* @todo Add the ability to require a minimum amount of alpha characters in the key (EG: If the minimum is 3, then 'str99' is ok, but 'st100' would fail )
* @example // Generate a key thats 3 characters or less, provided an array of existing keys to validate a unique result
* _.generateKey( 'Foo Bar Baz', 3, [ 'FBB', 'FB1', 'FB2' ] )
* // => FB3
*
* @example // Generate a key thats 5 characters or less, using a function to validate the unique key
* _.generateKey( '1st Project: Foo Bar Baz', 5, key => _.indexOf( [ 'PFBB', 'PFBB1', 'PFBB2'], key ) === -1 )
* // => PFBB3
*/
function generateKey( cfgOrName, maxLength, unique, condenseDupChars ){
var debug = false
var _l = {
out: function( type, args ){
if ( debug ){
console[ type ].apply( null, args )
}
},
l: function(){ _l.out( 'log', arguments ) },
d: function(){ _l.out( 'debug', arguments ) },
w: function(){ _l.out( 'warn', arguments ) },
e: function(){ _l.out( 'error', arguments ) },
}
// Functions to validate the parameter values
var paramValidators = {
/**
* Name - Needs to contain some alpha characters (_.isString would pass a value containing only numbers
* or special characters)
*/
name: name => !( _.isEmpty( name ) || ! _.isFunction( name.match ) || ! name.match( /[a-zA-Z]/ ) ),
/**
* Length - Needs to be a positive numeric value (_.isNumber or _.isInteger fails for numerical strings)
*/
maxLength: leng => ( parseInt( leng ) == leng && leng > 0 ),
/**
* Unique value - Needs to be an array (of existing values), a function (which accepts a string and
* verifies its unique) or false (disabling unique validations)
*/
unique: uniq => ( _.isArray( uniq ) || _.isFunction( uniq ) || uniq === false ),
/**
* Condense Duplicates - Boolean only
*/
condense: cond => ( _.isBoolean( cond ) )
}
// Set default values in the initial cfg object
var cfg = {
name : null,
length : 5,
unique : null,
condense: false,
regex : {
/**
* To
*/
segment: '[a-zA-Z0-9-]+'
}
}
var valInfo = val => `Value Type: ${Object.prototype.toString.call( val )}; Value (JSON): ${JSON.stringify(val)}`
// If the first parameter is an object, then its expected to hold all the values
if ( _.isObject( arguments[0] ) ){
// Iterate over the items in the first parameter, processing each value as a config item, based on the key
_.forEach( arguments[0], ( value, key ) => {
//paramInfo = `Key: ${key}; Value Type: ${Object.prototype.toString.call( value )}; Value (JSON): ${JSON.stringify(value)}`
//console.debug( '> KEY: %s; VALUE: %s', key, JSON.stringify(value))
// Use a switch statement to accommodate aliases for the setting names
switch( _.toLower( key ) ){
case 'name':
case 'title':
if ( ! paramValidators.name( value ) ){
//throw new Error( `Invalid value provided for the name/title (Key: ${key}; ${valInfo(value)})` )
return
}
cfg.name = value
break
case 'maxlength':
case 'max':
case 'length':
case 'limit':
if ( ! paramValidators.maxLength( value ) ){
//throw new Error( `Invalid value provided for the maximum length (Key: ${key}; ${valInfo(value)})` )
return
}
cfg.length = parseInt( value )
break
case 'unique':
case 'distinct':
if ( ! paramValidators.unique( value ) ){
//throw new Error( `Invalid value provided for the unique validator (Key: ${key}; ${valInfo(value)})` )
return
}
cfg.unique = value
break
case 'condensedupchars':
case 'condenseduplicatechars':
case 'condenseduplicates':
case 'condenseduplicate':
case 'condensedups':
case 'condense':
case 'condensedup':
if ( ! paramValidators.condense( value ) ){
//throw new Error( `Invalid value provided for the duplicate character condenser (Key: ${key}; ${valInfo(value)})` )
return
}
cfg.condense = !!value
break
default:
console.warn( `Unknown setting found in the object provided - Key: ${key}; ${valInfo(value)}` )
break
}
})
}
// If its not an object, but there were parameters provided, then process the other parameters
else if ( ! _.isEmpty( arguments ) ){
// Name ------------------------------
if ( ! paramValidators.name( cfgOrName ) ){
_l.e( 'Invalid value provided for the name/title (%s)', valInfo( cfgOrName ) )
return
}
cfg.name = cfgOrName
// Max Length -------------------------
if ( ! _.isUndefined( maxLength ) ){
if ( ! paramValidators.maxLength( maxLength ) ){
_l.w( 'Invalid value provided for the maximum length (%s)', valInfo( cfgOrName ) )
}
cfg.length = parseInt( maxLength )
}
// Unique Validator -------------------
if ( ! _.isUndefined( unique ) ){
if ( ! paramValidators.unique( unique ) ){
_l.w( 'Invalid value provided for the unique validator (%s)', valInfo( cfgOrName ) )
}
cfg.unique = unique
}
// Condense Duplicate Chars -----------
if ( ! _.isUndefined( condenseDupChars ) ){
if ( ! paramValidators.condense( condenseDupChars ) ){
_l.w( 'Invalid value provided for the duplicate character condenser (%s)', valInfo( cfgOrName ) )
}
cfg.condense = !!condenseDupChars
}
}
//
else {
//throw new Error( 'Invalid or undefined values provided' )
return undefined
}
// Convert the unique config value to an executable function
cfg.isUnique = ( uniqueCfg => {
console.log('[cfg.unique IIFE] PARAM uniqueCfg:',uniqueCfg)
// If its already a function, just return it
if ( _.isFunction( uniqueCfg ) ){
return uniqueCfg
}
// If its an array, return a function that verifies it doesn't contain the param
if ( _.isArray( uniqueCfg ) ){
return v => _.indexOf( uniqueCfg, v ) === -1
}
// For anything else, return a function that always returns true, effectively disabling unique validation
return () => true
})( cfg.unique )
// Convert the regex string to a real regex object
cfg.segRegex = new RegExp( cfg.regex.segment, 'g' )
/**
* Takes a string and returns a version of said string:
* - Converted to uppercase
* - Starting with an alpha character (first alpha character found in the string)
* - Any non-alphanumeric or underscore characters removed
* @see https://confluence.atlassian.com/adminjiraserver071/changing-the-project-key-format-802592378.html
*/
let _sanitizeStr = function( _pName ){
_pName = _.toUpper( _pName )
// Strip any non-alpha characters from the beginning
_pName = _pName.replace(/^[^A-Z]*/g, "")
// Remove anything thats not alpha-numeric or an underscore
_pName = _pName.replace(/[^A-Z0-9_]+/g, " ")
_pName = _.trim( _pName )
if ( ! _pName ){
return false
}
return _pName
}
// Internal function to shorten a string by taking one or more characters off of the end
let _shortenStr = function( _str, _len ) {
if ( ! _str || _len == 0 ) return
_str = _.toString( _str )
if ( _.isUndefined( _len ) ){
_len = 1
}
else if ( parseInt( _len ) != _len ){
_l.w( 'Unable to use the value "%s" as the shortening length - value is not an integer', _len )
_len = 1
}
else {
_len = parseInt( _len )
}
if ( ! _str || ! _.isString( _str ) || ! _str.hasOwnProperty('length') || _str.length < 1 || (_str.length - _len) < 1 ) {
_l.w( 'Unable to shorten the string - either due to an invalid value, or the resulting value is zero length (String: "%s"; Trim Length: ")', _str, _len )
return
}
return _str.substring( 0, _str.length - _len )
}
// Function to check if the key is unique or not (by using isUniqueOrList, either as an array of existing keys, or a function to check)
let _makeUnique = function( _key ){
let i = 0,
// Store the original key as it is before any changes
origBaseKey = _key,
// Base key is the portion of the key getting modified, without the numerical values
baseKey = _key,
// The modified base key and the numerical incremented digits concatenated together
uniqueKey = _key,
// When the value of uniqueKey is verified to be unique (by cfg.unique), this is set to true
isUnique = false,
// misc
m, result
do {
if ( baseKey.length < i.toString().length ){
return
}
// Only append the numerical value if its non-zero
if ( i !== 0 ){
uniqueKey = baseKey + i.toString()
// If the concatenated key and numerical value is greater than the key size, then trim the difference
// off of the key
if ( uniqueKey.length > cfg.length ){
var was = uniqueKey
baseKey = _shortenStr( baseKey, baseKey.length - (baseKey.length - i.toString().length) )
if ( ! baseKey ){
return
}
uniqueKey = baseKey + i.toString()
}
}
isUnique = cfg.isUnique( uniqueKey )
if ( uniqueKey.length > cfg.length ){
_l.w( 'Invalid value provided for the maximum length (%s)', valInfo( cfgOrName ) )
}
i += 1
}
while ( isUnique !== true )
return uniqueKey
}
let key = ''
let origName = cfg.name
let name = cfg.name
name = _sanitizeStr( name )
if ( ! name ){
console.error( 'Failed to generate key for the partition name "%s" - The sanitized version of the name was empty' )
return
}
let nameSegments = _.words( name, cfg.segRegex )
// https://lodash.com/docs/4.16.6#words
// If there arent enough 'words' in the partition name to generate a decent key, then just use the first characters of the partition name itself
if ( nameSegments.length < cfg.length ){
key = name.substr( 0, cfg.length )
}
else {
let char
// These two are only used if cfg.condense is enabled
let dupCount = 0
let lastChar
_.forEach( nameSegments, s => {
char = _.toUpper( s.charAt(0) )
if ( s.match( /[a-zA-Z]/ ) ){
}
else {
}
// If duplicate character condensing is enabled, then keep track of the duplicates
// (changing a key that would be FOOOD to F03D)
if ( cfg.condense === true ){
// If the current char is the same as the last, then increment the dup count
if ( char === lastChar && char.match( /[A-Z]/ ) ){
// If this is the first detected duplicate, then increment twice, since were now on the 2nd character
if ( dupCount === 0 ){
dupCount++
}
dupCount++
return
}
// If its not the same, but we just ended a duplicate character count, then add the duplicate count
if ( dupCount > 0 ) {
key += dupCount.toString()
dupCount = 0
}
lastChar = char
}
key += char
})
if ( key.length > cfg.length ){
key = key.substring( 0, cfg.length )
}
}
if ( ! _.size( key ) ){
return
}
key = _makeUnique( key )
return key
}
/**
* Encodes an ISO-8859-1 string to UTF-8, this is meant to provide the same functionality
* as the PHP utf8_encode function.
*
* @name module:_.utf8Encode
* @function module:_.utf8Encode
* @memberof module:_
* @param {string} str Standard ISO-8859-1 encoded string
* @returns {string} UTF-8 encoded version of the str param value
* @example _.utf8Encode('Hello World')
* // => Hello World
*/
function utf8Encode ( str ) {
if ( _.isNull( str ) || _.isUndefined( str ) || str === '' )
return str
if( ! _.isString( str ) && ! _.isNumber( str ))
throw new Error( `Illegal value type given to utf8Encode, expected a ISO-8859-1 encoded string, but received a ${typeof str}` )
const string = (str + '') // .replace(/\r\n/g, "\n").replace(/\r/g, "\n");
let utftext = '',
stringl = 0,
start, end
start = end = 0
stringl = _.size( string )
for (let n = 0; n < stringl; n++) {
let c1 = string.charCodeAt(n)
let enc = null
if (c1 < 128) {
end++;
} else if (c1 > 127 && c1 < 2048) {
enc = String.fromCharCode(
(c1 >> 6) | 192, (c1 & 63) | 128
);
} else if ((c1 & 0xF800) != 0xD800) {
enc = String.fromCharCode(
(c1 >> 12) | 224, ((c1 >> 6) & 63) | 128, (c1 & 63) | 128
)
} else { // surrogate pairs
if ((c1 & 0xFC00) != 0xD800)
throw new RangeError('Unmatched trail surrogate at ' + n)
var c2 = string.charCodeAt(++n)
if ((c2 & 0xFC00) != 0xDC00)
throw new RangeError('Unmatched lead surrogate at ' + (n - 1))
c1 = ((c1 & 0x3FF) << 10) + (c2 & 0x3FF) + 0x10000
enc = String.fromCharCode(
(c1 >> 18) | 240, ((c1 >> 12) & 63) | 128, ((c1 >> 6) & 63) | 128, (c1 & 63) | 128
)
}
if ( ! _.isNull( enc )) {
if (end > start)
utftext += string.slice(start, end)
utftext += enc
start = end = n + 1
}
}
if (end > start)
utftext += string.slice(start, stringl)
return utftext
}
/**
* Decodes a UTF-8 encoded string to the standard ISO-8859-1, this is meant to provide the same functionality
* as the PHP utf8_decode function.
*
* @name module:_.utf8Decode
* @function module:_.utf8Decode
* @memberof module:_
* @param {string} str UTF-8 encoded string
* @returns {string} ISO-8859-1 decoded string
* @example _.utf8Decode('Hello World')
* // => Hello World
*/
function utf8Decode ( str ) {
if ( _.isNull( str ) || _.isUndefined( str ) || str === '' )
return str
//if( ! _.isString( str ) && ! _.isNumber( str ))
//throw new Error( `Illegal value type given to utf8Decode, expected a UTF-8 encoded string, but received a ${typeof str}` )
let tmp_arr = [],
i = 0,
ac = 0,
c1 = 0,
c2 = 0,
c3 = 0,
c4 = 0
str += ''
while ( i < _.size( str ) ) {
c1 = str.charCodeAt(i);
if (c1 <= 191) {
tmp_arr[ac++] = String.fromCharCode(c1)
i++
} else if (c1 <= 223) {
c2 = str.charCodeAt(i + 1)
tmp_arr[ac++] = String.fromCharCode(((c1 & 31) << 6) | (c2 & 63))
i += 2
} else if (c1 <= 239) {
// http://en.wikipedia.org/wiki/UTF-8#Codepage_layout
c2 = str.charCodeAt(i + 1)
c3 = str.charCodeAt(i + 2)
tmp_arr[ac++] = String.fromCharCode(((c1 & 15) << 12) | ((c2 & 63) << 6) | (c3 & 63))
i += 3
} else {
c2 = str.charCodeAt(i + 1)
c3 = str.charCodeAt(i + 2)
c4 = str.charCodeAt(i + 3)
c1 = ((c1 & 7) << 18) | ((c2 & 63) << 12) | ((c3 & 63) << 6) | (c4 & 63)
c1 -= 0x10000
tmp_arr[ac++] = String.fromCharCode(0xD800 | ((c1 >> 10) & 0x3FF))
tmp_arr[ac++] = String.fromCharCode(0xDC00 | (c1 & 0x3FF))
i += 4
}
}
return tmp_arr.join('')
}
/**
* Alternate through the parameters provided, returning the next one in line every time.
*
* Instructions:
* - Calling alternator() with the SAME parameters will return the next param each time
* - Calling alternator() with NEW parameters will re-initialize the rotation, and return
* the first new parameter listed
* - Calling alternator() with NO parameters will reset the rotation to null, and return nothing
*
* @name module:_.alternator
* @function module:_.alternator
* @memberof module:_
* @var {array} parameters Parameters to rotate through
* @returns {Mixed} Whatever array element is next in line, or nothing when resetting
* @todo Create unit tests
* @example
* for(i = 0; i< 6; i++)
* _.alternator('a','b','c')
* // returns (incrementally) : a, b, c, a, b, c
*/
function alternator() {
// If no params are set, just reset everything, return nothing
if( ! arguments ){
//console.log('# A')
_internals.alternator.i = 0
_internals.alternator.params = null
}
// If this is the first time passing params, OR the params md5sum has changed
// (meaning new params), then reset the alternator with the new params
if( _internals.alternator.params === null || md5( JSON.stringify( arguments ) ) !== _internals.alternator.params ){
//console.log('# B')
_internals.alternator.i = 0
_internals.alternator.params = md5( JSON.stringify( arguments ) )
return arguments[ _internals.alternator.i ++ ]
}
// Just calling alternator again with the same params as last time..
if( _internals.alternator.i === arguments.length ){
_internals.alternator.i = 0
}
return arguments[ _internals.alternator.i ++ ]
}
/**
* Retrieve the md5sum value for a specific string.
*
* This source was taken from the PHP.js project, I take no credit for this code
*
* @name module:_.md5
* @function module:_.md5
* @memberof module:_
* @author Not me (Justin Hyland)
* @see http://phpjs.org/functions/md5/
* @param {string} str String to hash
* @returns {string} 32 character MD5 sum
* @todo Create unit tests
* @example md5('Hello World') === 'b10a8db164e0754105b7a99be72e3fe5'
*/
function md5( str ) {
// discuss at: http://phpjs.org/functions/md5/
// original by: Webtoolkit.info (http://www.webtoolkit.info/)
// improved by: Michael White (http://getsprink.com)
// improved by: Jack
// improved by: Kevin van Zonneveld (http://kevin.vanzonneveld.net)
// input by: Brett Zamir (http://brett-zamir.me)
// bugfixed by: Kevin van Zonneveld (http://kevin.vanzonneveld.net)
// depends on: utf8_encode
// example 1: md5('Kevin van Zonneveld');
// returns 1: '6e658d4bfcb59cc13f96c14450ac40b9'
var xl;
var rotateLeft = function(lValue, iShiftBits) {
return (lValue << iShiftBits) | (lValue >>> (32 - iShiftBits));
};
var addUnsigned = function(lX, lY) {
var lX4, lY4, lX8, lY8, lResult;
lX8 = (lX & 0x80000000);
lY8 = (lY & 0x80000000);
lX4 = (lX & 0x40000000);
lY4 = (lY & 0x40000000);
lResult = (lX & 0x3FFFFFFF) + (lY & 0x3FFFFFFF);
if (lX4 & lY4) {
return (lResult ^ 0x80000000 ^ lX8 ^ lY8);
}
if (lX4 | lY4) {
if (lResult & 0x40000000) {
return (lResult ^ 0xC0000000 ^ lX8 ^ lY8);
} else {
return (lResult ^ 0x40000000 ^ lX8 ^ lY8);
}
} else {
return (lResult ^ lX8 ^ lY8);
}
};
var _F = function(x, y, z) {
return (x & y) | ((~x) & z);
};
var _G = function(x, y, z) {
return (x & z) | (y & (~z));
};
var _H = function(x, y, z) {
return (x ^ y ^ z);
};
var _I = function(x, y, z) {
return (y ^ (x | (~z)));
};
var _FF = function(a, b, c, d, x, s, ac) {
a = addUnsigned(a, addUnsigned(addUnsigned(_F(b, c, d), x), ac));
return addUnsigned(rotateLeft(a, s), b);
};
var _GG = function(a, b, c, d, x, s, ac) {
a = addUnsigned(a, addUnsigned(addUnsigned(_G(b, c, d), x), ac));
return addUnsigned(rotateLeft(a, s), b);
};
var _HH = function(a, b, c, d, x, s, ac) {
a = addUnsigned(a, addUnsigned(addUnsigned(_H(b, c, d), x), ac));
return addUnsigned(rotateLeft(a, s), b);
};
var _II = function(a, b, c, d, x, s, ac) {
a = addUnsigned(a, addUnsigned(addUnsigned(_I(b, c, d), x), ac));
return addUnsigned(rotateLeft(a, s), b);
};
var convertToWordArray = function(str) {
var lWordCount;
var lMessageLength = str.length;
var lNumberOfWords_temp1 = lMessageLength + 8;
var lNumberOfWords_temp2 = (lNumberOfWords_temp1 - (lNumberOfWords_temp1 % 64)) / 64;
var lNumberOfWords = (lNumberOfWords_temp2 + 1) * 16;
var lWordArray = new Array(lNumberOfWords - 1);
var lBytePosition = 0;
var lByteCount = 0;
while (lByteCount < lMessageLength) {
lWordCount = (lByteCount - (lByteCount % 4)) / 4;
lBytePosition = (lByteCount % 4) * 8;
lWordArray[lWordCount] = (lWordArray[lWordCount] | (str.charCodeAt(lByteCount) << lBytePosition));
lByteCount++;
}
lWordCount = (lByteCount - (lByteCount % 4)) / 4;
lBytePosition = (lByteCount % 4) * 8;
lWordArray[lWordCount] = lWordArray[lWordCount] | (0x80 << lBytePosition);
lWordArray[lNumberOfWords - 2] = lMessageLength << 3;
lWordArray[lNumberOfWords - 1] = lMessageLength >>> 29;
return lWordArray;
};
var wordToHex = function(lValue) {
var wordToHexValue = '',
wordToHexValue_temp = '',
lByte, lCount;
for (lCount = 0; lCount <= 3; lCount++) {
lByte = (lValue >>> (lCount * 8)) & 255;
wordToHexValue_temp = '0' + lByte.toString(16);
wordToHexValue = wordToHexValue + wordToHexValue_temp.substr(wordToHexValue_temp.length - 2, 2);
}
return wordToHexValue;
};
var x = [],
k, AA, BB, CC, DD, a, b, c, d, S11 = 7,
S12 = 12,
S13 = 17,
S14 = 22,
S21 = 5,
S22 = 9,
S23 = 14,
S24 = 20,
S31 = 4,
S32 = 11,
S33 = 16,
S34 = 23,
S41 = 6,
S42 = 10,
S43 = 15,
S44 = 21;
str = utf8Decode(str);
x = convertToWordArray(str);
a = 0x67452301;
b = 0xEFCDAB89;
c = 0x98BADCFE;
d = 0x10325476;
xl = x.length;
for (k = 0; k < xl; k += 16) {
AA = a;
BB = b;
CC = c;
DD = d;
a = _FF(a, b, c, d, x[k + 0], S11, 0xD76AA478);
d = _FF(d, a, b, c, x[k + 1], S12, 0xE8C7B756);
c = _FF(c, d, a, b, x[k + 2], S13, 0x242070DB);
b = _FF(b, c, d, a, x[k + 3], S14, 0xC1BDCEEE);
a = _FF(a, b, c, d, x[k + 4], S11, 0xF57C0FAF);
d = _FF(d, a, b, c, x[k + 5], S12, 0x4787C62A);
c = _FF(c, d, a, b, x[k + 6], S13, 0xA8304613);
b = _FF(b, c, d, a, x[k + 7], S14, 0xFD469501);
a = _FF(a, b, c, d, x[k + 8], S11, 0x698098D8);
d = _FF(d, a, b, c, x[k + 9], S12, 0x8B44F7AF);
c = _FF(c, d, a, b, x[k + 10], S13, 0xFFFF5BB1);
b = _FF(b, c, d, a, x[k + 11], S14, 0x895CD7BE);
a = _FF(a, b, c, d, x[k + 12], S11, 0x6B901122);
d = _FF(d, a, b, c, x[k + 13], S12, 0xFD987193);
c = _FF(c, d, a, b, x[k + 14], S13, 0xA679438E);
b = _FF(b, c, d, a, x[k + 15], S14, 0x49B40821);
a = _GG(a, b, c, d, x[k + 1], S21, 0xF61E2562);
d = _GG(d, a, b, c, x[k + 6], S22, 0xC040B340);
c = _GG(c, d, a, b, x[k + 11], S23, 0x265E5A51);
b = _GG(b, c, d, a, x[k + 0], S24, 0xE9B6C7AA);
a = _GG(a, b, c, d, x[k + 5], S21, 0xD62F105D);
d = _GG(d, a, b, c, x[k + 10], S22, 0x2441453);
c = _GG(c, d, a, b, x[k + 15], S23, 0xD8A1E681);
b = _GG(b, c, d, a, x[k + 4], S24, 0xE7D3FBC8);
a = _GG(a, b, c, d, x[k + 9], S21, 0x21E1CDE6);
d = _GG(d, a, b, c, x[k + 14], S22, 0xC33707D6);
c = _GG(c, d, a, b, x[k + 3], S23, 0xF4D50D87);
b = _GG(b, c, d, a, x[k + 8], S24, 0x455A14ED);
a = _GG(a, b, c, d, x[k + 13], S21, 0xA9E3E905);
d = _GG(d, a, b, c, x[k + 2], S22, 0xFCEFA3F8);
c = _GG(c, d, a, b, x[k + 7], S23, 0x676F02D9);
b = _GG(b, c, d, a, x[k + 12], S24, 0x8D2A4C8A);
a = _HH(a, b, c, d, x[k + 5], S31, 0xFFFA3942);
d = _HH(d, a, b, c, x[k + 8], S32, 0x8771F681);
c = _HH(c, d, a, b, x[k + 11], S33, 0x6D9D6122);
b = _HH(b, c, d, a, x[k + 14], S34, 0xFDE5380C);
a = _HH(a, b, c, d, x[k + 1], S31, 0xA4BEEA44);
d = _HH(d, a, b, c, x[k + 4], S32, 0x4BDECFA9);
c = _HH(c, d, a, b, x[k + 7], S33, 0xF6BB4B60);
b = _HH(b, c, d, a, x[k + 10], S34, 0xBEBFBC70);
a = _HH(a, b, c, d, x[k + 13], S31, 0x289B7EC6);
d = _HH(d, a, b, c, x[k + 0], S32, 0xEAA127FA);
c = _HH(c, d, a, b, x[k + 3], S33, 0xD4EF3085);
b = _HH(b, c, d, a, x[k + 6], S34, 0x4881D05);
a = _HH(a, b, c, d, x[k + 9], S31, 0xD9D4D039);
d = _HH(d, a, b, c, x[k + 12], S32, 0xE6DB99E5);
c = _HH(c, d, a, b, x[k + 15], S33, 0x1FA27CF8);
b = _HH(b, c, d, a, x[k + 2], S34, 0xC4AC5665);
a = _II(a, b, c, d, x[k + 0], S41, 0xF4292244);
d = _II(d, a, b, c, x[k + 7], S42, 0x432AFF97);
c = _II(c, d, a, b, x[k + 14], S43, 0xAB9423A7);
b = _II(b, c, d, a, x[k + 5], S44, 0xFC93A039);
a = _II(a, b, c, d, x[k + 12], S41, 0x655B59C3);
d = _II(d, a, b, c, x[k + 3], S42, 0x8F0CCC92);
c = _II(c, d, a, b, x[k + 10], S43, 0xFFEFF47D);
b = _II(b, c, d, a, x[k + 1], S44, 0x85845DD1);
a = _II(a, b, c, d, x[k + 8], S41, 0x6FA87E4F);
d = _II(d, a, b, c, x[k + 15], S42, 0xFE2CE6E0);
c = _II(c, d, a, b, x[k + 6], S43, 0xA3014314);
b = _II(b, c, d, a, x[k + 13], S44, 0x4E0811A1);
a = _II(a, b, c, d, x[k + 4], S41, 0xF7537E82);
d = _II(d, a, b, c, x[k + 11], S42, 0xBD3AF235);
c = _II(c, d, a, b, x[k + 2], S43, 0x2AD7D2BB);
b = _II(b, c, d, a, x[k + 9], S44, 0xEB86D391);
a = addUnsigned(a, AA);
b = addUnsigned(b, BB);
c = addUnsigned(c, CC);
d = addUnsigned(d, DD);
}
var temp = wordToHex(a) + wordToHex(b) + wordToHex(c) + wordToHex(d);
return temp.toLowerCase();
}
/**
* Iterate through an array of absolute file paths, removing the common paths from each element. This is useful
* for when you don't need to have the entire absolute path in the name.
*
* @function module:_.stripCommonRoot
* @name module:_.stripCommonRoot
* @memberof module:_
* @param {array} pathArray Array of paths..
* @returns {array} Modified version of the provided array
*
* @example
* Gizmo.stripCommonRoot([
* '/home/jdoe/app/lib/helpers/mongoose-helper.js',
* '/home/jdoe/app/dev/file-foo.js',
* '/home/jdoe/app/dev/some-file.js'
* ]).join(', ')
* // => /lib/helpers/mongoose-helper.js, /dev/file-foo.js, /dev/some-file.js
*/
function stripCommonRoot( pathArray ){
if( ! _.isArray( pathArray ) ){
Log.debug( `Expected an array - received ${_.typeof(pathArray)}` )
return false
}
if( ! _.size( pathArray ) ){
return []
}
if( ! _.every( pathArray, _.isString ) ){
Log.debug( `Expected an array of strings - received ${_.typeof(pathArray)}` )
return false
}
let pathsSplit = _.map( pathArray, p => _.split( p, '/') )
let currentVal = null
let result = null
let isFirstSame = p => {
if( currentVal === null ){
currentVal = p[0]
return true
}
return p[0] == currentVal
}
// Iterate over the segments of the path, checking if all of the values for this segment in every path is the same.
do {
if( _.every( pathsSplit, isFirstSame ) ){
pathsSplit = _.map( pathsSplit, p => _.drop( p ) )
currentVal = null
}
// When we reach a segment thats not the same in every path, then set the `result` variable, which will abort
// the do/while loop
else {
result = _.map( pathsSplit, p => '/' + _.join( p, '/' ) )
}
}
while ( result === null )
return result
}
/**
* Iterate through an array of absolute file paths, removing the common paths from each absolute path. The shortened
* filenames are returned in an array, while the common path
*
* @function module:_.sumPaths
* @alias module:_.summarizePaths
* @memberof module:_
* @param {array} pathArray Array of paths..
* @returns {Object} pathObj Object containing the common absolute path, and an array of files (with
* paths relative to the common absolute path)
* @returns {string} pathObj.path The absolute path up to the last common folder that all files share
* @returns {array} pathObj.files Array of filenames, paths starting where {pathObj.path} left off
*
* @example
* _.sumPaths.summarizePaths([
* '/home/jdoe/app/lib/helpers/mongoose-helper.js',
* '/home/jdoe/app/dev/file-foo.js',
* '/home/jdoe/app/dev/some-file.js'
* ])
* // => { path: '/home/jdoe/app',
* files: [
* '/lib/helpers/mongoose-helper.js', '/dev/file-foo.js', '/dev/some-file.js'
* ]
* }
*/
function sumPaths( pathArray ){
if( ! _.isArray( pathArray ) ){
Log.debug( `Expected an array - received ${_.typeof(pathArray)}` )
return false
}
if( ! _.size( pathArray ) ){
return []
}
if( ! _.every( pathArray, _.isString ) ){
Log.debug( `Expected an array of strings - received ${_.typeof(pathArray)}` )
return false
}
let pathsSplit = _.map( pathArray, p => _.split( p, '/') )
let currentVal = null
let result = null
let commonRoot = []
let isFirstSame = p => {
// If the 'currentVal' is null, then this is the first iteration, so set it to this
// value, which will be compared with all the others
if( currentVal === null ){
currentVal = p[0]
return true
}
// Not the first iteration, compare it to the value
return p[0] == currentVal
}
// Iterate over the segments of the path, checking if all of the values for this segment in every path is the same.
do {
// Check if the [0] element in each array is the same..
if( _.every( pathsSplit, isFirstSame ) ){
// .. If they are all the same, then drop the first item from each path,
pathsSplit = _.map( pathsSplit, _.drop )
// Add the path segment to the common root array/path
commonRoot.push( currentVal )
// Reset the currentVal
currentVal = null
}
// When we reach a segment thats not the same in every path, then set the `result` variable, which will abort
// the do/while loop
else {
result = _.map( pathsSplit, p => '/' + _.join( p, '/' ) )
}
}
while ( result === null )
return {
path : commonRoot.join('/'),
files: result
}
}
/**
* Retrieve the types of values in an array or an object.
*
* @name module:_.valTypes
* @alias module:_.valueTypes
* @function module:_.valTypes
* @memberof module:_
* @param {(Object|array)} collection Array or object (collection of data).
* @param {?function=} filter Filter the collection using a simple function
* @returns {array} Array of types of values in the collection
*
* @example // Example showing how duplicate value types only display the value type once
* _.valTypes([
* 1, 'Str', false, [], null, new Array(), undefined, {},
* new Date(), function(){}, (s => `This is a ${s}`)('str')
* ]).join(', ').join(', ')
* // => number, string, boolean, array, null, undefined, object, date, function
*
* @example // Using Gizmo.valueTypes to verify all parameters are string types
* function onlyAcceptsStringParams( foo, bar, baz, bang ){
* var invalidParamTypes = Gizmo.valTypes( arguments, f => ! _.isString(f) )
* if( invalidParamTypes.length > 0 )
* throw new Error( 'Expected ll parameters to be strings - received invalid type(s): ' + invalidParamTypes.join(', ') )
* }
*/
function valTypes( collection, filter ){
if( _.isObject( collection ) ){
collection = _.values( collection )
}
else if( ! _.isArray( collection ) ){
throw new Error( `Helper function valTypes expected an object or array for the collection value - received type: ${_.type(collection)}` )
}
var chain = _.chain( collection )
if( _.isFunction( filter ) ){
chain = chain.filter( filter )
}
return chain
//.map( v => _.lowerCase( _.typeof( v ) ) )
.map( _.typeof )
.map( _.lowerCase )
.uniq()
.value()
}
/**
* Calculate the sha1 hash of a specific string. This is the equivalent of PHP's sha1()
* function.
*
* @name module:_.sha1
* @function module:_.sha1
* @memberof module:_
* @param {string} str String to calculate hash for
* @returns {string} SHA1 hash
* @example
* _.sha1('test')
* // => a94a8fe5ccb19ba61c4c0873d391e987982fbbd3
*/
function sha1 ( str ) {
const rotate_left = ( n, s ) => (n << s) | (n >>> (32 - s))
/*var lsb_hex = function (val) { // Not in use; needed?
var str="";
var i;
var vh;
var vl;
for ( i=0; i<=6; i+=2 ) {
vh = (val>>>(i*4+4))&0x0f;
vl = (val>>>(i*4))&0x0f;
str += vh.toString(16) + vl.toString(16);
}
return str;
};*/
const cvt_hex = val => {
let str = ''
let i
let v
for (i = 7; i >= 0; i--) {
str += ((val >>> (i * 4)) & 0x0f).toString(16)
}
return str
}
let blockstart
let i, j
const W = new Array(80)
let H0 = 0x67452301
let H1 = 0xEFCDAB89
let H2 = 0x98BADCFE
let H3 = 0x10325476
let H4 = 0xC3D2E1F0
let A, B, C, D, E
let temp
str = utf8Encode(str)
const str_len = _.size( str )
const word_array = []
for (i = 0; i < str_len - 3; i += 4) {
j = str.charCodeAt(i) << 24 | str.charCodeAt(i + 1) << 16 | str.charCodeAt(i + 2) << 8 | str.charCodeAt(i + 3)
word_array.push(j)
}
switch (str_len % 4) {
case 0:
i = 0x080000000
break;
case 1:
i = str.charCodeAt(str_len - 1) << 24 | 0x0800000
break;
case 2:
i = str.charCodeAt(str_len - 2) << 24 | str.charCodeAt(str_len - 1) << 16 | 0x08000
break;
case 3:
i = str.charCodeAt(str_len - 3) << 24 | str.charCodeAt(str_len - 2) << 16 | str.charCodeAt(str_len - 1) <<
8 | 0x80
break;
}
word_array.push(i)
while ((_.size( word_array ) % 16) != 14) {
word_array.push(0)
}
word_array.push(str_len >>> 29)
word_array.push((str_len << 3) & 0x0ffffffff)
for (blockstart = 0; blockstart < _.size( word_array ); blockstart += 16) {
for (i = 0; i < 16; i++) {
W[i] = word_array[blockstart + i]
}
for (i = 16; i <= 79; i++) {
W[i] = rotate_left(W[i - 3] ^ W[i - 8] ^ W[i - 14] ^ W[i - 16], 1)
}
A = H0
B = H1
C = H2
D = H3
E = H4
for (i = 0; i <= 19; i++) {
temp = (rotate_left(A, 5) + ((B & C) | (~B & D)) + E + W[i] + 0x5A827999) & 0x0ffffffff
E = D
D = C
C = rotate_left(B, 30)
B = A
A = temp
}
for (i = 20; i <= 39; i++) {
temp = (rotate_left(A, 5) + (B ^ C ^ D) + E + W[i] + 0x6ED9EBA1) & 0x0ffffffff
E = D
D = C
C = rotate_left(B, 30)
B = A
A = temp
}
for (i = 40; i <= 59; i++) {
temp = (rotate_left(A, 5) + ((B & C) | (B & D) | (C & D)) + E + W[i] + 0x8F1BBCDC) & 0x0ffffffff
E = D
D = C
C = rotate_left(B, 30)
B = A
A = temp
}
for (i = 60; i <= 79; i++) {
temp = (rotate_left(A, 5) + (B ^ C ^ D) + E + W[i] + 0xCA62C1D6) & 0x0ffffffff
E = D
D = C
C = rotate_left(B, 30)
B = A
A = temp
}
H0 = (H0 + A) & 0x0ffffffff
H1 = (H1 + B) & 0x0ffffffff
H2 = (H2 + C) & 0x0ffffffff
H3 = (H3 + D) & 0x0ffffffff
H4 = (H4 + E) & 0x0ffffffff
}
return ( cvt_hex(H0) + cvt_hex(H1) + cvt_hex(H2) + cvt_hex(H3) + cvt_hex(H4) ).toLowerCase()
}
/**
* Generate a hash of a given string, using the provided salt
*
* @name module:_.makeHash
* @function module:_.makeHash
* @memberof module:_
* @param {string} str String to hash
* @param {string} salt Salt to use for hash
* @returns {string} base64 encoded hash
* @example
* _.makeHash('superSecretPassword','secret-salt')
* // => ebA3UZET3LDQWzl <cut> TUnV5oRxAvOLsA==
*/
function makeHash ( str, salt ) {
if( ! _.isString( str ) || ! _.isString( salt ))
throw new Error('_.hash() requires two string parameters, a string to hash and a salt')
const h = crypto.createHash('sha512')
h.update(str)
h.update(salt)
return h.digest('base64')
}
/**
* Return a randomly generated string - at a specific length
*
* @name module:_.randStr
* @function module:_.randStr
* @memberof module:_
* @param {number} length Length of the desored string (Default: 20)
* @returns {string}
* @todo Add the ability to specify the 'possible' string characters
* @example
* _.randStr( 15 )
* // => gyC8Q9MABoEjGK6
*/
function randStr ( length ) {
length = length || 20
if( ! isNumeric( length ))
throw new Error('_.randStr needs a numeric value')
let result = ''
const possible = [
'ABCDEFGHIJKLMNOPQRSTUVWXYZ',
'abcdefghijklmnopqrstuvwxyz',
'0123456789',
'$./'
//'`~!@#$%^&*()-_=+[{]}\\|\'";:/?.>,<'
].join('')
for( let i=0; i < Number( length ); i++ )
result += possible.charAt(Math.floor(Math.random() * possible.length))
return result
}
/**
* Substitute specific characters within a string with a specified replacement.
* Replacement positions are specified by either a single (numeric) value, or an
* array of numeric values
*
* @name module:_.replaceAt
* @function module:_.replaceAt
* @memberof module:_
* @param {string} str String to process
* @param {(number|array)} indexndex Location(s) to be substituted
* @param {string} character Character to substitute replacements with
* @returns {string} Parsed/modified version of the provided string
* @todo Allow the character parameter to be an array, and use the alternator method to iterate through them while substituting the replacements
* @todo Allow the index to be a range
* @example
* _.replaceAt( 'baz', 2, 'r')
* // => bar
* _.replaceAt( 'bad-word', [1,2,5,6], '*')
* // => b**-w**d
* _.replaceAt( 'Hello World', [6,7,8,9,10] )
* // => Hello ?????
*/
function replaceAt ( str, index, character ) {
character = character || '?'
if( _.isArray( index ) ){
return __( str )
.map((s, i) => {
if( _.indexOf(index, i ) === -1 ){
return s
}
return character
})
.value()
.join('')
}
return str.substr(0, index) + character + str.substr(index+character.length)
}
/**
* Return items true type by grabbing the 2nd string content from Object.prototype.toString.call, as opposed to the
* less-specific 'typeof'
*
* @name module:_.getType
* @function module:_.getType
* @memberof module:_
* @param {*} item Item to retrieve type for
* @returns {string} Type of variable
* @example
* _.type([])
* // => array
* _.type({})
* // => object
* _.type(() => {})
* // => function
*/
function getType ( item ) {
const objType = Object.prototype.toString.call( item )
const match = objType.match( /^\[object\s(.*)\]$/ )
return match[1].toLowerCase()
}
/**
* This performs a series of replacements in a string, using the items within
* an object/array. Just a quicker/easier way than chaining .replace() over
* and over again. The replacements can be an array of arrays, an array of objects,
* or an object
*
* @name module:_.multiReplace
* @function module:_.multiReplace
* @memberof module:_
* @param {string} str String to be parsed/returned
* @param {(object|array)} replacements Replacements, with original string as the key, and replacement as
* the value
* @param {string} modifiers Regex modifiers to use for search (EG: i for case-insensitivity)
* 'g' (global) is included by default
* @returns {string} Parsed and modified version of the provided string
* @example
* _.multiReplace( 'test', { t: 'T'} )
* // => TesT
* _.multiReplace( 'foo', { FOO: 'bar'}, 'i' )
* // => bar
* _.multiReplace( 'Windows XP', [{ windows: 'Linux'}, {xp: 'RHEL'}], 'i' )
* // => Linux RHEL
*/
function multiReplace ( str, replacements, modifiers ) {
if( ! str || ! _.isString(str) ){
return str
}
if( ! replacements ){
return str
}
// Replacements need to be an object, or an array with two values (which is verified later)
if( ! _.isPlainObject( replacements ) && ! _.isArray( replacements ) ){
throw new Error(`Replacements need to be an array or plain object, you gave us a ${getType(str)}`)
}
// Since we later expect for the replacements to be an object, check if its
// an array, if so, reconstruct it into an object
if( _.isArray( replacements ) ) {
const replacementsObj = {}
// Loop through each replacement, checking the values, making sure both a search/replace is present
_.forEach( replacements, r => {
// If its an array, then it needs atleast two values in it
if( _.isArray(r)) {
if( _.isUndefined( r[0] ) || _.isUndefined( r[1] ) ) {
throw new Error( 'Replacement structure illegal - Array of unfulfilled array' )
}
else {
replacementsObj[ r[ 0 ] ] = r[ 1 ]
}
}
// If its an object, use hte key/val
else if(_.isPlainObject(r)) {
replacementsObj[ Object.keys(r)[0]] = r[Object.keys(r)[0]]
}
// Shouldnt ever really get here, but I guess im just paranoid
else {
throw new Error(`Replacement structure illegal - Array of non-array and non-object`)
}
} )
replacements = replacementsObj
}
// Execute the replacements!
_.forEach( replacements, ( r, f ) => {
str = str.replace( new RegExp( f, `g${modifiers || ''}` ), r )
})
return str
}
/**
* Swap the keys and values of a simple plain object
*
* @name module:_.swap
* @function module:_.swap
* @memberof module:_
* @param {object} obj Object to swap values for
* @returns {object} Returns a version of the original object with the keys and values switched (wherever possible)
* @example
* _.swap({a:'b', c:'d'})
* // => {b:'a', d:'c'}
*/
function swap ( obj ) {
if( ! _.isPlainObject( obj ) ){
throw new Error(`Only plain objects can be swapped, you gave us a ${getType(obj)}`)
}
const result = {}
_.forEach(obj, ( v, k ) => {
result[v] = k
})
return result
}
/**
* Return a new array containing only the unique objects inside the provided
* array. Unlike _.uniq, this will check _every_ key/value in the array
*
* @name module:_.uniqObjs
* @function module:_.uniqObjs
* @memberof module:_
* @param {array} arr Array of structurally identical objects
* @param {object} arr[] All values in the provided array need to be objects
* @returns {array}
* @example
* // Remove any duplicate objects
* const objs = [ { x: 1, y: 2 }, { a: 1, b: 2 }, { x: 1, y: 2 }]
* console.log( _( objs ).uniqObjs().value() )
* console.log( _.uniqObjs( objs ) )
* // => [ { x: 1, y: 2 }, { a: 1, b: 2 } ]
*/
function uniqObjs ( arr ) {
// Make sure that the arr parameter is a defined & populated array of objects
if( ! _.isArray( arr ) || ! arr.length || ! _.isObject( arr[0] ) ){
return false
}
const uniqs = []
// Filter out the duplicate objects within the array by checking if
// the stringified object value already exist in the temporary uniqs
// array (while adding them to the variable)
return _.filter( arr, ( obj ) => {
// Use _.sortObj to sort the contents of the object by the keys, since stringify
// will use the current order (which means identical objects in different orders
// will be seen as discrepancies)
if( _.indexOf( uniqs, JSON.stringify( sortObj( obj ) ) ) === -1 ){
uniqs.push( JSON.stringify( sortObj( obj ) ) )
return true
}
return false
})
}
/**
* Check if the provided number is a float or integer value. This just tacks
* a 2nd check onto lodashes isNumber, which uses a lenient comparative operator
* to check if the value of Number is the same as the provided number
*
* @name module:_.isNumeric
* @function module:_.isNumeric
* @memberof module:_
* @param {(string|integer|number)} num Number to check
* @returns {boolean}
* @example
* _.isNumber( 123 )
* _.isNumber( '123' )
* _.isNumber( 1.2 )
* _.isNumber( '1.2' )
* // => true
*
* _.isNumber( 'foo' )
* _.isNumber( [] )
* _.isNumber( {} )
* // => false
*/
function isNumeric ( num ) {
return _.isNumber( num ) || Number( num ) == num
}
/**
* Validate a string against an RFC822 compliant pattern
*
* @name module:_.isEmail
* @function module:_.isEmail
* @memberof module:_
* @param {string} email Email address to validate against pattern
* @returns {boolean}
* @example
* _.isEmail( '[email protected]' )
* // => true
*
* _.isEmail( '[email protected]' )
* _.isEmail( 'jinux.com' )
* _.isEmail( null )
* // => false
*/
function isEmail ( email ) {
// Must be a string!
if( ! _.isString( email ) ){
return false
}
// Verify the length (using min/max standards)
if( email.length < 4 || email.length > 255 ){
return false
}
// Only RFC822 compliant pattern that would work with JS
return /^([^\x00-\x20\x22\x28\x29\x2c\x2e\x3a-\x3c\x3e\x40\x5b-\x5d\x7f-\xff]+|\x22([^\x0d\x22\x5c\x80-\xff]|\x5c[\x00-\x7f])*\x22)(\x2e([^\x00-\x20\x22\x28\x29\x2c\x2e\x3a-\x3c\x3e\x40\x5b-\x5d\x7f-\xff]+|\x22([^\x0d\x22\x5c\x80-\xff]|\x5c[\x00-\x7f])*\x22))*\x40([^\x00-\x20\x22\x28\x29\x2c\x2e\x3a-\x3c\x3e\x40\x5b-\x5d\x7f-\xff]+|\x5b([^\x0d\x5b-\x5d\x80-\xff]|\x5c[\x00-\x7f])*\x5d)(\x2e([^\x00-\x20\x22\x28\x29\x2c\x2e\x3a-\x3c\x3e\x40\x5b-\x5d\x7f-\xff]+|\x5b([^\x0d\x5b-\x5d\x80-\xff]|\x5c[\x00-\x7f])*\x5d))*$/.test( email )
}
/**
* Check if two values match each other. Basically sorts the object and
* source, then passes it off to _.isMatch, (Since objects/arrays with
* same values in different orders would be considered discrepancies
*
* @name module:_.sortMatch
* @function module:_.sortMatch
* @memberof module:_
* @param {*} object Item A to match to B
* @param {*} source Item B to match to A
* @param {function=} customizer Function to cuztomize the object and src (Just handed of to _.isMatch)
* @returns {boolean}
* @example
* _.sortMatch( [1,2,3], [3,2,1] )
* // => true
* _.sortMatch( [1,2,'3'], [3,2,1] )
* // => false
*/
function sortMatch ( object, source, customizer ) {
if( _.isUndefined( object ) || _.isUndefined( source ) ){
throw new Error('Must define two same-type values to sort and match')
}
if( getType( object ) !== getType( source ) ){
return false
}
if( _.isPlainObject( object )) {
object = sortObj( object )
source = sortObj( source )
}
else if( _.isArray( object )) {
object = object.sort()
source = source.sort()
}
else {
throw new Error('test')
}
return _.isMatch( object, source, customizer )
}
/**
* Just a boolean comparison tool, Allows you to specify other true-type
* variables, as well as convert the value to lower case (Since the string
* representations of the boolean values are lower). Also compares integer
* values
*
* @name module:_.bool
* @function module:_.bool
* @memberof module:_
* @param {(string|boolean|integer)} value Value to compare
* @param {(array|string)} trues Any other custom 'true' type variables, an attempt is made
* to convert any value to an array
* @param {boolean} [lower=false] Process the values after toLowerCase() is called
* @returns Boolean casted version of the provided value
* @example
* _.bool( true ) === true
* _.bool( 'true' ) === true
* _.bool( 1 ) === true
* _.bool( 'foo', [ 'foo', 'bar' ] ) === true
* _.bool( '1' ) === true
* _.bool( 'false' ) === false
* _.bool( false ) === false
* _.bool( 0 ) === false
* _.bool( '0' ) === false
* _.bool( 'foo', [ 'bar', 'baz' ] ) === false
*/
function bool ( value, trues, lower ) {
if( _.isUndefined( trues ) ){
trues = []
}
else if(_.isString( trues )){
trues = [ trues ]
}
else if( ! _.isArray( trues )){
throw new Error( `Illegal additional true types, must be string or array, received: ${getType(trues)}`)
}
trues = _.union( [ 1, '1', true, 'true' ], trues )
return _.indexOf( trues, !!lower === true ? value.toLowerCase() : value ) !== -1
}
/**
* Ensure a specific string ends with a certain character
*
* @name module:_.endWith
* @function module:_.endWith
* @memberof module:_
* @param {string} str String to parse and modify (if needed)
* @param {string} endChar String to check for on the ending, and possibly append
* @returns {string} The string returned will be either the exact same string provided, or ${str + endChar} if
* the original string doesn't end with the endChar character
* @example
* _.endWith('/User/john.doe/Documents', '/')
* // => /User/john.doe/Documents/
* _.endWith('Something else.', '.')
* // => Something else.
*/
function endWith ( str, endChar ) {
return _.endsWith( str, endChar )
? str
: str + endChar
}
/**
* Ensure a specific string DOESN'T end with a certain character
*
* @name module:_.dontEndWith
* @function module:_.dontEndWith
* @memberof module:_
* @todo Should be able to replace an ending str like // with /
* @param {string} str String to parse and modify (if needed)
* @param {string} endChar String to check for on the ending, and possibly remove
* @returns {string} The string returned will be either the exact same string provided, or a version of the
* original string with the value of endChar removed from the end
* @example
* _.dontEndWith('/v1/resource/name/', '/')
* // => /v1/resource/name
*/
function dontEndWith ( str, endChar ) {
return _.endsWith( str, endChar )
? str.replace( new RegExp( endChar+'$'), '')
: str
}
/**
* Ensure a specific string starts with a certain character
*
* @name module:_.startWith
* @function module:_.startWith
* @memberof module:_
* @param {string} str String to parse and modify (if needed)
* @param {string} startChar String to check for on the beginning, and possibly append
* @returns {string} The string returned will be either the exact same string provided, or ${startChar + str} if
* the original string doesn't begin with the startChar character
* @example
* _.startWith('Documents/', '~/')
* // => ~/Documents/
* _.startWith('Something else.', '.')
* // => Something else.
* _( 'Using startsWith and endsWith together' )
* .startWith('(')
* .endWith(')')
* .value()
* // => (Using startsWith and endsWith together)
*/
function startWith ( str, startChar ) {
return _.startsWith( str, startChar )
? str
: startChar + str
}
/**
* Ensure a specific string DOESN'T start with a certain character
*
* @name module:_.dontStartWith
* @function module:_.dontStartWith
* @memberof module:_
* @todo Should be able to replace an starting str like // with /
* @param {string} str String to parse and modify (if needed)
* @param {string} startChar String to check for on the beginning, and possibly remove
* @returns {string} The string returned will be either the exact same string provided, or a version of the
* original string with the value of startChar removed from the beginning
* @example
* _.dontStartWith('.unhide-me', '.')
* // => unhide-me
*/
function dontStartWith ( str, startChar ) {
return _.startsWith( str, startChar )
? str.replace( new RegExp( '^'+startChar ), '')
: str
}
/**
* Convert any new-line characters to HTML Line breaks, which can optionally be specified,
* but defaults to just </br>. The replaced characters consists of \r\n, \n\r, \n and \r.
*
* @name module:_.nl2br
* @function module:_.nl2br
* @memberof module:_
* @param {string} str String to process and replace any new lines for
* @param {string} [br='</br>'] HTML Break (</br> by default)
* @returns {string} Modified version of ${str}, with all new-line characters replaced with an HTML line break
* @todo Another parameter to optionally trim the string before line breaks to get rid of first/last
* @todo Another parameter to keep the \n on the end of the newly added </br> tag
* @example
* _.nl2br("One\r\nTwo\n\rThree\nFour\rFive")
* // => One</br>Two</br>Three</br>Four</br>Five
*/
function nl2br ( str, br ) {
return str.split(/\r\n|\n\r|\n|\r/).join( br || '</br>' )
}
/**
* Complete opposite of the _.nl2br - This replaces any HTML Line breaks with the line return character,
* which can optionally be specified, but defaults to just \r\n. The HTML break replaced is </br>, <br>,
* </BR> or <BR>
*
* @name module:_.br2nl
* @function module:_.br2nl
* @memberof module:_
* @param {string} str String to process and replace any HTML line breaks for
* @param {string} [nl='\r\n'] New line character (\r\n by default)
* @returns {string} Modified version of ${str}, with all HTML line breaks replaced with new-line characters
* @todo Another parameter to optionally trim the string before line breaks to get rid of first/last
* @todo Another parameter to keep the \</br> tag on the end of the newly added \n
* @example
* _.nl2br("One<br>Two</br>Three</BR>Four<BR>Five")
* // => One\r\nTwo\r\nThree\r\nFour\r\nFive
*/
function br2nl ( str, nl ) {
return str.split(/<\/?br>/i).join( nl || "\r\n" )
}
/**
* Censor any common profanity words by replacing it with a specified word, or masking all or
* some of the characters with a single specified character. The words are kept in the separate
* data.js file, and base64 encrypted, as to not store a huge list of profanity on any users
* computer. The list of words is actually a list that was downloaded from a TeamSpeak related
* website of words to ban:
* http://addons.teamspeak.com/directory/addon/miscellaneous-tools/TXT-English-badwords-bans-and-list.html
* Note: This only supports the English language, the dirty version
*
* @name module:_.censor
* @function module:_.censor
* @memberof module:_
* @param {string} word Word to censor and parse
* @param {string} [masker='*'] Single character or full single word
* @param {string} [maskType='partial'] The masking 'type', can be:
* full Entire word
* single Single character
* firstlast First and last letters
* middle All BUT first and last
* partial Majority of letters (55% after first letter)
* @returns {string} Parsed and censored version of the provided word
* @example
* _.censor('damn')
* // => d**n
*/
function censor ( word, masker, maskType ) {
if( ! word ){
return word
}
masker = masker ||'*'
maskType = maskType || 'partial'
const censored = _internals.censored
const encWord = new Buffer( word ).toString( 'base64' )
// Lets hope this is a God fearing christian without a potty mouth
if( _.indexOf( censored, encWord ) === -1 ){
return word
}
// Return the masker to default if it's not a string
if( ! masker || ! _.isString( masker ) ){
masker = '*'
}
// If just a single character was given for the masker, then we can use the maskType
if( masker.length <= 1 ){
switch ( maskType ) {
case 'full':
return _.repeat( masker, word.length )
break
case 'single':
return replaceAt( word, 2, masker )
break
case 'firstlast':
return replaceAt( word, [0, word.length-1], masker )
break
case 'middle':
const middles = _( word ).map(( s, i ) => i).drop().dropRight().value();
return replaceAt( word, middles, masker )
break
default: // Partial
const replaceNum = Math.floor(( 55 / 100 ) * word.length)
const range = _.range(1, replaceNum+1)
return replaceAt( word, range, masker )
break
}
}
// If we were given a phrase as the mask, then just replace the entire word with that
return masker
}
/**
* Generate a salted hash of a specified password string - Similar to PHPs
* password_hash function, which returns a string with the hash AND the salt,
* making it easier to store in a database, and easier to verify
*
* @name module:_.passwordHash
* @function module:_.passwordHash
* @memberof module:_
* @param {string} password Password to hash
* @returns {string} 109 character password hash (salt is first 20 characters)
* @note Every password hash is generated by using a salt value that is randomly generated every time, this means
* that the resulting hash will be different every time it executes, even if the passwords are the same
* @example
* const pwd1 = _.passwordHash('SomePass')
* // => LIE9OKy0g$eNB <cut> XFMcfx78L5SuZZivA==
* const pwd2 = _.passwordHash('SomePass')
* pwd1 === pwd2
* // => false
*/
function passwordHash ( password ) {
if( ! password ){
throw new Error('No password was given to hash')
}
if( ! _.isString( password ) ){
throw new Error('Must provide a STRING as a password')
}
// Generate the salt
// THIS MUST NOT CHANGE! If this value is not the same as what
// passwordVerify expects, no hash will be validated
const salt = randStr( 20 )
// Return the salted hash with the salt prepended to it
return salt + makeHash( password, salt )
}
/**
* Verify a password against a password hash generated by _.passwordHash
*
* @name module:_.passwordVerify
* @function module:_.passwordVerify
* @memberof module:_
* @param {string} password Password to verify
* @param {string} passwdHash String generated by _.passwordHash
* @returns {boolean} TRUE if the result of a hash generated with the
* same password and the salt found in passwordHash,
* matches the hash inside passwordHash
* @example
* const hashA = _.passwordHash( 'secret' )
* _.passwordVerify( 'secret', hashA )
* // => true
*/
function passwordVerify ( password, passwdHash ) {
if( ! password || ! passwdHash ){
throw new Error('Need to provide both a password and a hash to verify')
}
if( ! _.isString( password ) || ! _.isString( passwdHash ) ){
throw new Error('Password and hash both need to be strings')
}
// If the hash isn't even the proper length, don't bother checking
if( passwdHash.length !== 108 ){
return false
}
// Get the salt from the password hash - first 20 chars
const salt = passwdHash.substr( 0, 20 )
// Get the password hash, everything after the first 20 chars
const hash = passwdHash.substr( 20 )
// Check the hash against a hash generated with the same data
return makeHash( password, salt ) === hash
}
/**
* Return a copy of the object with the content sorted by the keys
*
* @name module:_.sortObj
* @function module:_.sortObj
* @memberof module:_
* @param {object} obj Object to sort by keys
* @param {function} comparator Function to compare/sort the elements
* @returns {object}
* @example
* const obj = {b: 3, c: 2, a: 1}
* console.log( _.sortObj( obj ) )
* console.log( _( obj ).sortObj().value() )
*
* // => {a: 1, b: 3, c: 2}
*
* _.sortObj( obj, ( value, key ) => value )
* // => {a: 1, c: 2, b: 3}
*/
function sortObj( obj, comparator ) {
// Make sure we were given an object...
if( ! _.isObject( obj ) ){
throw new Error(`_.sortObj expects an object obj is: ${getType(obj)}`)
}
// If comparator is provided, then it needs to be a function, if it isn't
// a function, then throw an error
if( ! _.isUndefined( comparator ) && ! _.isFunction( comparator ) ){
throw new Error(`_.sortObj expects the comparator to be a function (if defined), but received a: ${getType(comparator)}`)
}
// Create an array of the object keys, sorted either alpha/numeric
// by default, or using the comparator if defined
const keys = _.sortBy( _.keys( obj ), key => {
return _.isFunction( comparator )
? comparator( obj[ key ], key )
: key
})
// Return a newly created object which uses the keys in the array
// created above, and grabs the associated data from the object
// provided
return _.zipObject( keys, _.map( keys, key => {
return obj[ key ]
}))
}
/**
* Validate that an array, or objects in an array, or elements within the
* objects in an array are all unique
*
* @name module:_.isUniq
* @function module:_.isUniq
* @memberof module:_
* @param {array} collection Single level array or array of objects
* @param {string=} element If `collection` is an array of objects, and we are to check that a specific
* element in those objects is unique, then this should be the name of the element
* in the object
* @returns {boolean}
* @example
* _.isUniq( [ 1, 2, 3, 2 ] )
* // => false
* _.isUniq( [ {a: 1}, {a: 2}, {a: 1} ] )
* // => false
* _.isUniq( [ {a: 1, b: 2}, {a: 2, b: 5}, {a: 1, b: 2} ], 'b')
* // => false
*/
function isUniq ( collection, element ) {
if( ! _.isArray( collection ) ){
throw new Error( 'Collection needs to be an array, you provided a ' + getTypeof( collection ) )
}
if( collection.length === 0 ){
return true
}
// If this is an array of objects, then handle it differently than if its just an array
if( _.isObject( collection[0] ) ) {
// If no specific element is provided, then uniq the entire object
if ( _.isUndefined( element ) ) {
return uniqObjs( collection ).length === collection.length
}
// If an element was provided, then check that just that element is unique
return _.uniqBy( collection, element ).length === collection.length
}
// Here, we can just unique the array and verify the length
return _.uniq( collection ).length === collection.length
}
/**
* Remove items from object, mutating the original object by removing specified element(s),
* and returning a new object of said element(s)
* This is basically the same as lodashes _.remove method, except this works for Objects,
* not arrays.
*
* @name module:_.removeObj
* @function module:_.removeObj
* @memberof module:_
* @param {object} obj Object (to mutate)
* @param {(array|string)} del Element(s) to remove from obj
* @returns {object} Object of items removed from obj param
* @note This will mutate the original object, removing the `del` element(s)
* @todo Need to add some sanity checking, some more logic, etc etc
* @todo This should be able to take a function for the del
* @example
* var testObj = { first: 'John', last: 'Doe', middle: 'w', age: 26, height: 75 }
* testObj = _.removeObj( testObj, 'height')
* // => { first: 'John', last: 'Doe', middle: 'w', age: 26 }
* testObj = _.removeObj( testObj, [ 'age','middle' ])
* // => { first: 'John', last: 'Doe' }
*/
function removeObj ( obj, del ) {
const picked = _.pick( obj, del )
if(_.isArray(del)){
_.forEach(del, d => {
_.unset(obj, d)
})
}
else {
_.unset(obj, del)
}
return picked
}
/**
* Escape a string, making it safe to use in a MySQL query. Based off of PHPs
* mysql_real_escape_string
*
* @name module:_.mysqlEscape
* @function module:_.mysqlEscape
* @memberof module:_
* @param {string} content String to use in the MySQL query
* @returns {string} Safe version of the content string parameter
* @note ALPHA PHASE - Under Construction
* @example
* _.mysqlEscape( "Justin\\\\'s Boots" )
* // => "Justin\\'s Boots"
*/
function mysqlEscape ( content ) {
const replacements = [
[ "\\", "\\\\" ],
[ "\'", "\\\'" ],
[ "\"", "\\\"" ],
[ "\n", "\\\n" ],
[ "\r", "\\\r" ],
[ "\x00", "\\\x00" ],
[ "\x1a", "\\\x1a" ]
]
const map = {
"\\": "\\\\",
"\'": "\\\'",
"\"": "\\\"",
"\n": "\\\n",
"\r": "\\\r",
"\x00": "\\\x00",
"\x1a": "\\\x1a"
}
return replace( content, map )
/*return content
.replace("\\", "\\\\")
.replace("\'", "\\\'")
.replace("\"", "\\\"")
.replace("\n", "\\\n")
.replace("\r", "\\\r")
.replace("\x00", "\\\x00")
.replace("\x1a", "\\\x1a")
*/
}
/**
* Check if a specified string is in snake_case format
*
* @name module:_.isSnake
* @function module:_.isSnake
* @memberof module:_
* @param {string} str String to inspect
* @returns {boolean} Returns True if the string is in case snake, False otherwise.
* @note ALPHA PHASE - Under Construction
* @example
* _.isSnake( _.snakeCase('Foo Bar') )
* // => true
* _.isSnake( _.camelCase('Foo Bar') )
* // => false
*/
function isSnake ( str ) {
return str === _.snakeCase( str )
}
/**
* Check if a specified string is in camelCase format
*
* @name module:_.isCamel
* @function module:_.isCamel
* @memberof module:_
* @param {string} str String to inspect
* @returns {boolean}
* @note ALPHA PHASE - Under Construction
* @example
* _.isSnake( _.snakeCase('Foo Bar') )
* // => true
* _.isSnake( _.camelCase('Foo Bar') )
* // => false
*/
function isCamel ( str ) {
return str === _.camelCase( str )
}
/**
* Check if a specified string is in kebab-case format
*
* @name module:_.isKebab
* @function module:_.isKebab
* @memberof module:_
* @param {string} str String to inspect
* @returns {boolean}
* @note ALPHA PHASE - Under Construction
* @example
* _.isKebab( _.kebabCase('Foo Bar') )
* // => true
* _.isKebab( _.camelCase('Foo Bar') )
* // => false
*/
function isKebab ( str ) {
return str === _.kebabCase( str )
}
/**
* Check if a specified string is in Start Case format
*
* @name module:_.isStart
* @function module:_.isStart
* @memberof module:_
* @param {string} str String to inspect
* @returns {boolean}
* @note ALPHA PHASE - Under Construction
* @example
* _.isSnake( _.snakeCase('Foo Bar') )
* // => true
* _.isSnake( _.camelCase('Foo Bar') )
* // => false
*/
function isStart ( str ) {
return str === _.startCase( str )
}
/**
* Check if a specified string is in lower case format
*
* @name module:_.isLower
* @function module:_.isLower
* @memberof module:_
* @param {string} str String to inspect
* @returns {boolean}
* @example
* _.isLower( _.lowerCase('Foo Bar') )
* // => true
* _.isLower( _.upperCase('Foo Bar') )
* // => false
*/
function isLower ( str ) {
return str === _.lowerCase( str )
}
/**
* Check if a specified string is in UPPER CASE format
*
* @name module:_.isUpper
* @function module:_.isUpper
* @memberof module:_
* @param {string} str String to inspect
* @returns {boolean}
* @note ALPHA PHASE - Under Construction
* @example
* _.isUpper( _.upperCase('Foo Bar') )
* // => true
* _.isUpper( _.lowerCase('Foo Bar') )
* // => false
*/
function isUpper ( str ) {
return str === _.upperCase( str )
}
/**
* Retrieve the case type of a specified string
*
* @name module:_.getCase
* @function module:_.getCase
* @memberof module:_
* @param {string} str String to inspect
* @returns {(string|undefined)} Will return one of: snake, camel, kebab, start, lower, upper or undefined if none
* @note ALPHA PHASE - Under Construction, needs a serious re-write
* @example
* var str = 'Hello World..'
* _.each()
*/
function getCase ( str ) {
if( isUpper( str ) ){
return 'upper'
}
if( isSnake( str ) ){
return 'snake'
}
if( isCamel( str ) ){
return 'camel'
}
if( isKebab( str ) ){
return 'kebab'
}
if( isStart( str ) ){
return 'start'
}
if( isLower( str ) ){
return 'lower'
}
return undefined
}
/**
* Verify a string is in a specified format.
*
* @name module:_.isCase
* @function module:_.isCase
* @memberof module:_
* @param {string} theCase The case to validate
* @param {string} str String to inspect
* @returns {boolean}
* @note ALPHA PHASE - Under Construction
* @example
* _.isCase( 'snake', _.snakeCase( 'Hello World' ) )
* // => true
* _.isCase( 'kebab', _.snakeCase( 'Hello World' ) )
* // => false
*/
function isCase ( theCase, str ) {
switch( theCase ){
case 'snake':
return _.snakeCase( str ) === str
break
case 'camel':
return _.camelCase( str ) === str
break
case 'kebab':
return _.kebabCase( str ) === str
break
case 'start':
return _.startCase( str ) === str
break
case 'lower':
return _.lowerCase( str ) === str
break
case 'upper':
return _.upperCase( str ) === str
break
default:
return false
break
}
}
/**
* Verify that a collection (string, array or object) has all listed values, basically
* just an array-friendly version of _.includes
*
* @name module:_.includesAll
* @function module:_.includesAll
* @memberof module:_
* @param {(array|object|string)} collection The collection to search
* @param {mixed} values The value or values to search for
* @param {number} fromIndex The index to search from.
* @returns {boolean} Returns `true` based on the result of _.includes
* @example
* _.includesAll( [1,2,3], [1,3] )
* // => true
* _.includesAll( [1,2,3], [1,2], 2 )
* // => false
* _.includesAll( {user: 'fred', age: 40 }, ['fred', 40] )
* // => true
* _.includesAll( 'abcdef', ['a','d] )
* // => true
*/
function includesAll ( collection, values, fromIndex ) {
// Make sure we were given an array as the collection
if( ! _.isArray( collection ) && ! _.isObject( collection ) && ! _.isString( collection ) ){
throw new Error( '_.includesAll: Expecting an array, string or object as the collection' )
}
if( _.isUndefined( values ) || _.isNull( values ) ){
throw new Error( '_.includesAll: Need a value to check for' )
}
// Default this to 0
fromIndex = _.isNumber( fromIndex ) ? Number( fromIndex ) : 0
// If were given an array, then iterate through the collection
if( _.isArray( values ) ){
return _.every( values, v => _.includes( collection, v, fromIndex ) )
}
// If we are NOT given an array for the values, then just hand everything down to the
// _.includes, according to the documentation, it can accept "anything" as the value
// (But it doesn't work as expected when given an array), hence this function
return _.includes( collection, values, fromIndex )
}
/**
* Return the maximum value of all arguments passed. This is the same thing as _.max,
* only instead of an array, it takes all the arguments
*
* @name module:_.maxOf
* @function module:_.maxOf
* @memberof module:_
* @var {array} arguments Pulls the arguments provided
* @todo Create unit tests
* @returns {number} Maximum value, retrieved by _.max()
* @example
* _.maxOf( 1, 20, 'a', ['test'], 1000 )
* // => 1000
*/
function maxOf() {
return _.max( _.chain( arguments ).map( n => Number( n ) ).value() )
}
/**
* Return the minimum value of all arguments passed. This is the same thing as _.min,
* only instead of an array, it takes all the arguments
*
* @name module:_.minOf
* @function module:_.minOf
* @memberof module:_
* @var {array} arguments Pulls the arguments provided
* @todo Create unit tests
* @returns {number} Minimum value, retrieved by _.min()
* @example
* _.minOf( 1, 20, 'a', ['test'], 1000 )
* // => 1
*/
function minOf() {
return _.min( _.chain( arguments ).map(n => Number( n ) ).value() )
}
/**
* Use the Levenshtein formula to calculate the distance in the similarities
* of two separate strings, which can be anywhere from 0 (strings are identical)
* to the length of the longer string provided (100% different). The higher the
* distance, the more different the strings are, but the distance can only be
* as high as high as the number of characters in the longer string
*
* @name module:_.levenshtein
* @function module:_.levenshtein
* @memberof module:_
* @param {(string|number)} strA String A
* @param {(string|number)} strB String .... Yep, B
* @returns {number} Levenshtein distance value
* @note ALPHA PHASE - Under Construction
* @todo Create unit tests
* @example
* levenshtein( 'foo','foo' )
* // => 0
* levenshtein( 'foo','bar' )
* // => 3
*/
function levenshtein ( strA, strB ) {
// Make sure we were given Strings or Numbers, and nothing else
if( (_.isString( strA ) && ! _.isNumber( strA ) ) || (_.isString( strB ) && ! _.isNumber( strB ) ) ){
throw new Error( 'Need to provide two strings or numbers to differentiate')
}
const cost = []
const n = strA.length
const m = strB.length
let i
let j
if ( n === 0 || m === 0 ){
return
}
for ( i = 0; i <= n; i++ ) {
_.set( cost, `[${i}][0]`, i )
}
for ( j = 0; j <= m; j++ ) {
cost[0][j] = j
}
//console.log('cost',cost)
for ( i = 1; i <= n; i++ ) {
let x = strA.charAt(i - 1)
for ( j = 1; j <= m; j++ ) {
let y = strB.charAt(j - 1)
if ( x == y ) {
cost[i][j] = cost[i - 1][j - 1]
}
else {
cost[i][j] = 1 + minOf( cost[i - 1][j - 1], cost[i][j - 1], cost[i - 1][j] )
}
}
}
return cost[n][m]
}
/**
* String Difference Distance (In percentages). This basically returns
* the Levenshtein value as a percentage
*
* @name module:_.strDist
* @function module:_.strDist
* @memberof module:_
* @param {(string|number)} strA String A
* @param {(string|number)} strB String .... Yep, B
* @returns {number} Levenshtein distance percentage (WITHOUT the % on the end)
* @todo Create unit tests
* @example
* strDist( 'foo','foo' )
* // => 0
* strDist( 'foo','bar' )
* // => 100
* strDist( 'something', 'somewhere' )
* // => 44.44
*/
function strDist( strA, strB ) {
const distance = levenshtein( strA, strB )
if( distance === false ){
return false
}
return Number( (distance * 100) / maxOf( strA.length, strB.length ))
}
function isCountable( noun ){
return ! _.includes( _internals.uncountable, noun )
}
/**
* Return the plural version of a string
*
* @name module:_.plural
* @function module:_.plural
* @memberof module:_
* @param {string} str Singular format of a noun
* @returns {string} Plural version of same noun
* @todo Create unit tests
* @example
* _.plural( 'apple' )
* // => apples
* _.plural( 'toy' )
* // => toys
* _.plural( 'fly' )
* // => flies
*/
function plural( str ){
if (str.lastChar() === 'y') {
if ( (str.charAt(str.length - 2)).isVowel() ) {
// If the y has a vowel before it (i.e. toys), then you just add the s.
return str + 's';
}
// If a this ends in y with a consonant before it (fly), you drop the y and add -ies to make it plural.
return str.slice(0, -1) + 'ies';
}
if ( str.substring( str.length - 2) === 'us') {
// ends in us -> i, needs to preceede the generic 's' rule
return str.slice(0, -2) + 'i';
}
if (['ch', 'sh'].indexOf( str.substring( str.length - 2)) !== -1 || ['x','s'].indexOf(str.lastChar()) !== -1) {
// If a this ends in ch, sh, x, s, you add -es to make it plural.
return str + 'es';
}
// anything else, just add s
return str + 's';
}
/**
* Merge multiple objects together without mutating the original object
* This basically just hands everything off to _.merge, just adds an empty object to the beginning, so
* _.merge( {}, ObjsA, ObjsB )
* would be the same as
* _.mergeObjs( ObjsA, ObjsB )
*
* @name module:_.mergeObjs
* @function module:_.mergeObjs
* @memberof module:_
* @param {...object} [sources] The source objects
* @returns {object} Newly merged object
* @example
* _.mergeObjs( { a: 1 }, { b: 2 }, { c: 3 } )
* // => { a: 1, b: 2, c: 3 }
*/
function mergeObjs( sources ){
return _.merge.apply( this, _.flatten( [ {}, arguments || [] ]))
}
/* Not sure what this is for.. forgot.
function matches( source ) {
return function(obj) {
return _.some(_.toPairs(source), function(keyValue) {
return obj[keyValue[0]] === keyValue[1];
});
};
}
*/
/**
* Ensures the item is an instance of the exception specified by type
*
* @name module:_.setException
* @function module:_.setException
* @memberof module:_
* @param {Mixed} item Item/Error/Whatever
* @param {Mixed} [type=Error] Exception type (Default: Error)
* @returns {Mixed} Returns an instance of Error, or whatevers specified by item
* @example
* let err = 'Error Str'
* // => Error Str
* err = _.setException( err )
* // => [Error: Error Str]
* err = _.setException( err )
* // => [Error: Error Str]
* // Notice no matter how many times its used, Error is not nested, as opposed to setting new Error( err )
*/
function setException( item, type ){
if( _.isUndefined( type ) ){
type = Error
}
return item instanceof type
? item
: new type( item )
}
/**
* Pulls a sample from an array - Useful for when iterating over an array (manually), and having to remove the previous
* iterations
*
* @name module:_.pullSample
* @function module:_.pullSample
* @memberof module:_
* @note This method mutates the array, just as _.pull does
* @param {array} arr Array to sample
* @returns {Mixed} Whatever element was sampled from the array
* @example
* var data = [ 100, 200 ]
* _.pullSample( data )
* // => 200
* _.pullSample( data )
* // => 100
* _.pullSample( data )
* // => []
*/
function pullSample( arr ){
if( _.isUndefined( arr ) ){
return undefined
}
if( ! _.isArray( arr ) ){
throw new Error( `Expected to sample an array - received a ${getTypeof( arr )}` )
}
if( _.isEmpty( arr ) ){
return undefined
}
const sample = _.sample( arr )
_.pull( arr, sample )
return sample
}
/**
* Pulls an array of samples from an array - Basically the same thing as _.pullSample, except this samples multiple
* elements, with the amount specified by the size parameter
*
* @name module:_.pullSampleSize
* @function module:_.pullSampleSize
* @memberof module:_
* @note This method mutates the array, just as _.pull does
* @param {array} arr Array to sample
* @param {number} size Amount of elements to sample/remove from arr
* @returns {array} Array of one or more elements from arr
* @example
* var data = [ 100, 200, 300, 400 ]
* _.pullSampleSize( data, 2 ) // [ 100, 200 ]
* _.pullSampleSize( data, 2 ) // [ 300, 400 ]
* _.pullSampleSize( data, 2 ) // [ ]
* data // []
*/
function pullSampleSize( arr, size ){
if( size === 0 ){
return []
}
if( _.isUndefined( size ) ){
size = 1
}
if( ! _.isNumber( size ) ){
throw new Error( `Expected size to be undefined or a number - received a ${getTypeof( size )}` )
}
if( _.isUndefined( arr ) ){
return undefined
}
if( ! _.isArray( arr ) ){
throw new Error( `Expected to sample an array - received a ${getTypeof( arr )}` )
}
if( _.isEmpty( arr ) ){
return []
}
if( size > _.size( arr ) ){
size = _.size( arr )
}
const result = []
_.times( size, () => {
let sample = _.sample( arr )
_.pull( arr, sample )
result.push( sample )
} )
return _.flatten( result )
}
/**
* Validation for legitimate regular expression pattern validation
*
* @name module:_.validPattern
* @function module:_.validPattern
* @memberof module:_
* @param {Mixed} pattern Pattern to validate (String, number, regexp, etc)
* @param {string} flags Regular expression flags (Not required)
* @param {boolean} reason If pattern is invalid, instead of returning false, return the error (string),
* which would change the return to `true` = valid, and any string = invalid
* @returns {boolean|string} If the pattern will work in a regexp check, then true
* @note This is best used when validating strings, as invalid regexp elements will throw an error before this
* function even gets a chance to validate. Meaning something like `_.validPattern(/a/asdf)` will throw an
* exception on the line the invalid pattern was passed
* @todo Somehow parse a string for a regex pattern and flags; EG: /foo/g -> ['foo','g']; %bar%i -> ['bar','i']
*/
function validPattern( pattern, flags, reason ) {
const permitted = [
// Valid value types that can actually be valid regex patterns
'regexp', 'string', 'number',
// Empty/null can accidentally be sent, so don't reject them, as all it should do
// is save an empty regex value to the config
'undefined', 'null'
]
const ptrnType = getTypeof( pattern )
if( ! _.includes( permitted, ptrnType ) ){
if( reason === true ){
return `Illegal pattern value type, expecting a 'string', 'number' or RegExp object - received a '${ptrnType}'`
}
return false
}
// If flags are provided, they must be in string format
if( ! _.isUndefined( flags ) && ! _.isNull( flags ) && ! _.isString( flags ) ){
if( reason === true ){
return `Illegal flag value type, expecting type 'string' or nothing - received a '${getTypeof( flags )}'`
}
return false
}
// null/undefined
if( _.isEmpty( pattern ) ){
return true
}
// Use this instead of just `arguments`, because we dont want the `reason` to be passed down
const regexpParams = [ pattern ]
if( _.isString( flags ) ){
regexpParams.push( flags )
}
let isValid = true
let err
try {
RegExp.apply( RegExp, regexpParams )
} catch( e ) {
err = e
isValid = false
}
// If its valid, then return true and go no further!
if( isValid === true ){
return true
}
// If there needs to be a reason, then return one if there is one
if( reason === true ){
return err
? err.toString()
: 'Invalid RegExp pattern - Unknown reason'
}
return false
}
/**
* Return the type of a specific variable, much like the standard 'typeof', only
* with a little more functionality. This is primarily used for input from
* libraries/packages/modules that may convert the variable to a different type
* when interacting with it. For example, pretty much anything passed through the
* URI parameters will be a string, as well as anything passed through GetOpts,
* but you may want integers, for example, to actually be identified as numbers, or
* true/false/null/undefined strings to be identified as boolean/null/undefined.
* That's what the scrutinize parameter does here, it will process the variable
* to attempt to identify the type it originally was.
*
* NOTE: If no type is matched, then the toString() value will be returned
*
* @name module:_.typeof
* @function module:_.typeof
* @memberof module:_
* @param {*} value Value to process
* @param {boolean} inspect Determine if the true value type should be determined through logical
* processing
* @param {object} returnTypes Object of return type strings to overwrite
* @param {object} flaggedVals Values used to determine the real value types of flagged values (Only used
* if scrutinize is enabled)
* @returns {string} The variable type; The default type names are:
* undefined, null, string, boolean, array, element, date, regexp, object, number, function, unknown
* However, these can be overridden by providing an object as the 3rd parameter
* @example _.typeof( [1,2] ) // array
* _.typeof( 'foo' ) // string
* _.typeof( true ) // boolean
* _.typeof( 'true' ) // string
* _.typeof( 'true',true ) // boolean
* _.typeof( null ) // null
* _.typeof( 'null' ) // string
* _.typeof( 'null',true ) // null
*/
function getTypeof ( value, inspect, returnTypes, flaggedVals ) {
// String representations of the value types (Overridden by returnTypes if defined)
const types = _.extend( {
undefined: 'undefined',
null: 'null',
string: 'string',
boolean: 'boolean',
array: 'array',
element: 'element',
date: 'date',
regexp: 'regexp',
object: 'object',
number: 'number',
funct: 'function',
unknown: 'unknown'
}, returnTypes || {} )
// Flagged values for string variables; EG: if string is 'true', then the it's Boolean (Overridden by
// flaggedVals if defined)
const flagged = _.extend( {
boolean: [ 'true', 'false' ],
null: [ 'null', 'NULL' ],
undefined: [ 'undefined' ]
}, flaggedVals || {} )
// Retrieve the actual object type from the prototype
//const objType = Object.prototype.toString.call( value )
// Attempt to regex match the type (value should be [object TYPE]
//const objTypeRegex = objType.match( /^\[object\s(.*)\]$/ )
/* $lab:coverage:off$ */
// Match the type, or use the types.undefined (This shouldn't ever not match)
//const objTypeString = objTypeRegex[1] ? objTypeRegex[1].toLowerCase() : types.unknown
/* $lab:coverage:on$ */
if( _.isUndefined( value ) ){
return types.undefined
}
if( _.isNull( value ) ){
return types.null
}
// String values are what get opened to scrutiny, if enabled
if( _.isString( value ) ){
// If inspect isnt enabled, then just return string
if( !! inspect === false ){
return types.string
}
// Numbers should be the same value if leniently compared against it's float-parsed self
if( Number( value ) == value ){
return types.number
}
// Check if this string is inside the boolean flags
if( _.indexOf( flagged.boolean, value ) !== -1 ){
return types.boolean
}
// Check if its inside any null flags
if( _.indexOf( flagged.null, value ) !== -1 ){
return types.null
}
// Check if its inside any undefined flags
if( _.indexOf( flagged.undefined, value ) !== -1 ){
return types.undefined
}
// If no parser caught it, then it must be a string
return types.string
}
// Certain check types can't be misconstrued as other types, unlike other types (such as objects), get those out
// of the way
if( _.isBoolean( value ) ){
return types.boolean
}
if( _.isNumber( value ) ){
return types.number
}
if( _.isDate( value ) ){
return types.date
}
if( _.isRegExp( value ) ){
return types.regexp
}
/* $lab:coverage:off$ */
// Disabling coverage for this, since unit testing is done via node
if( _.isElement( value ) ){
return types.element
}
/* $lab:coverage:on$ */
// Since isObject returns true for functions, check this before that
if( _.isFunction( value ) ){
return types.funct
}
// Since isObject also returns true for arrays, check that before as well
if( _.isArray( value ) ){
return types.array
}
// isObject should be last for any possible object 'types'
if( _.isObject( value ) ){
return types.object
}
/* $lab:coverage:off$ */
// If nothing else was caught, then return the type found via the prototypes toString() call
// Note: Disabling coverage, since I can't find a value to reach this, and it's just in case I missed something.
// It helps me sleep at night
return getType( value )
/* $lab:coverage:on$ */
}
/**
* Process either a single path (in string format), or an array of path "segments", returning a simplified and concatenated path.
*
* @name module:_.getFullPath
* @function module:_.getFullPath
* @memberof module:_
* @param {string|array|object} segments Either a single path, an array of path 'segments', or an
* object with all settings.
* @param {string|boolean} pathType Define as 'absolute' (or true) to return an absolute path;
* 'relative' (or false) to return a relative path (from the
* current working dir), or a directory (string) to use as the
* cwd to return relative paths from.
* @param {boolean} verify Determines if the resulting path should be verified before
* returning; Note: This will also validate the directory
* defined in the pathType attr (if set as a path).
* @param {string} segments[*] Path segments to concatenate.
* @param {string|array} segments.segments Path segments to concatenate.
* @param {boolean|string} segments.pathType Define as 'absolute' (or true) to return an absolute path;
* 'relative' (or false) to return a relative path (from the
* current working dir), or a directory (string) to use as the
* cwd to return relative paths from.
* @param {boolean} segments.verify Determines if the resulting path should be verified before
* returning; Note: This will also validate the directory
* defined in the pathType attr (if set as a path).
* @returns {string} Returns the correct and simplified path
* However, these can be overridden by providing an object as the 3rd parameter
* @example _.typeof( [1,2] ) // array
* _.typeof( 'foo' ) // string
* _.typeof( true ) // boolean
* _.typeof( 'true' ) // string
* _.typeof( 'true',true ) // boolean
* _.typeof( null ) // null
* _.typeof( 'null' ) // string
* _.typeof( 'null',true ) // null
*/
function getFullPath( segments, pathType, verify ){
const _validateDir = function( dir ){
var fs = require( 'fs' )
try {
var stats = fs.lstatSync( dir )
return stats.isDirectory()
}
catch ( e ) {
return false
}
}
if ( _.isPlainObject( arguments[0] ) ){
const args = arguments[0]
//console.log('args:',args)
if ( args.segments ){
segments = args.segments
}
if ( args.pathType ){
pathType = args.pathType
}
if ( args.relativeDir ){
relativeDir = args.relativeDir
}
if ( _.isBoolean( args.verify ) ){
verify = args.verify
}
}
verify = !!verify
if ( _.isString( pathType ) ){
//console.log('Checkpoint A')
// If pathType is 'absolute' or 'relative', then re-define it as a boolean
if ( pathType.toLowerCase() === 'absolute' ){
//console.log('Checkpoint B')
pathType = true
}
else if ( pathType.toLowerCase() === 'relative' ){
//console.log('Checkpoint C')
pathType = false
}
// Anything else should be interpreted as a path to use for the cwd (returning a result relative to that value)
else {
//console.log('Checkpoint D')
if ( verify === true ){
//console.log('Checkpoint E')
var v = _validateDir( pathType )
//console.log( 'Verification result for %s:', pathType, v )
if ( v === false ){
throw new Error( 'The path defined does not exist (' + pathType + ')' )
}
}
}
// pathType should be a directory to assume as the cwd, returning a path relative to the absolute value
}
else if ( _.isBoolean( pathType ) ){
// true = return a path absolute from /
// false = return a path relative from .
}
if ( _.isString( segments ) ){
if ( _.isEmpty( segments ) ){
throw new Error( 'Empty string provided for segments' )
}
segments = [ segments ]
}
else if ( _.isArray( segments ) ){
if ( _.isEmpty( segments ) ){
throw new Error( 'Empty array provided for segments' )
}
if ( ! _.every( segments, _.isString ) ){
throw new Error( 'One or more of the values within the segments array is not a string' )
}
}
else {
throw new Error( 'Unable to process segments, expected a string or array - received typeof: ' + typeof segments )
}
var result = ''
var resultSegs = []
_.forEach( segments, ( segGrp, segGrpIdx ) => {
// If this is the last paths element, then its a FILE.. (or could be)
// If its not, then remove any trailing slash from the end
var subSegs = segGrp.split( '/' )
_.forEach( subSegs, ( subSeg, subSegIdx ) => {
if ( resultSegs.length === 0 ){
// If were on the first segment, and the segment is '/', or './' or '../', then add that to the result segments
// Note: These values only get copied to the result array if its the starting dir, otherwise, it modifies the
// result segment array
if ( _.indexOf( [ '', '.', '..' ], subSeg ) !== -1 ){
resultSegs.push( subSeg )
return
}
}
// If the segment is just a ./, then it can be ignored
if ( resultSegs.length === 1 && resultSegs[0] === '' && subSeg === '.' ){
resultSegs[0] = '.'
return
}
if ( subSeg === '' || subSeg === '.' ){
return
}
// If the segment is a ../, then remove the last element from resultSegs
if ( subSeg === '..' ){
// If the only segment added so far is a '.', then overwrite it with ..
if ( resultSegs.length === 1 && resultSegs[0] === '.' ){
resultSegs[0] = subSeg
return
}
// If the last element in the results is '..', then tack on another '..' (as opposed to removing it)
// Note: This is to accommodate for the results that end up being something like: ../../../../blah.js
else if ( resultSegs[ resultSegs.length-1 ] === '..' || resultSegs.length == 0 ){
resultSegs.push( '..' )
return
}
// Should only get here if resultSegs has at least one folder, which will get removed
else {
var rem = resultSegs.pop()
return
}
}
resultSegs.push( subSeg )
})
})
var res = resultSegs.join('/')
return _.isEmpty( res ) ? '.' : res
}
const defaultMixins = {
md5: md5,
swap: swap,
bool: bool,
sha1: sha1,
hash: makeHash,
type: getType,
nl2br: nl2br,
br2nl: br2nl,
maxOf: maxOf,
minOf: minOf,
isCase: isCase,
typeof: getTypeof,
censor: censor,
plural: plural,
isUniq: isUniq,
sortObj: sortObj,
isSnake: isSnake,
randStr: randStr,
isCamel: isCamel,
isKebab: isKebab,
isStart: isStart,
isLower: isLower,
isUpper: isUpper,
getCase: getCase,
strDist: strDist,
isEmail: isEmail,
endWith: endWith,
sumPaths: sumPaths,
uniqObjs: uniqObjs,
valTypes: valTypes,
replaceAt: replaceAt,
mergeObjs: mergeObjs,
isNumeric: isNumeric,
startWith: startWith,
sortMatch: sortMatch,
removeObj: removeObj,
utf8Encode: utf8Encode,
utf8Decode: utf8Decode,
valueTypes: valTypes,
pullSample: pullSample,
generateKey: generateKey,
mysqlEscape: mysqlEscape,
isCountable: isCountable,
dontEndWith: dontEndWith,
getFullPath: getFullPath,
levenshtein: levenshtein,
includesAll: includesAll,
validPattern: validPattern,
passwordHash: passwordHash,
setException: setException,
multiReplace: multiReplace,
dontStartWith: dontStartWith,
passwordVerify: passwordVerify,
pullSampleSize: pullSampleSize,
summarizePaths: sumPaths,
stripCommonRoot: stripCommonRoot
}
// Mixin the above functions into the fresh version of Lodash....
__.mixin( defaultMixins )
// module.exports = exports
module.exports = __
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment