1 /* 2 * JSUtils.js - Misc utilities to work around Javascript engine differences 3 * 4 * Copyright © 2013-2015, 2018, JEDLSoft 5 * 6 * Licensed under the Apache License, Version 2.0 (the "License"); 7 * you may not use this file except in compliance with the License. 8 * You may obtain a copy of the License at 9 * 10 * http://www.apache.org/licenses/LICENSE-2.0 11 * 12 * Unless required by applicable law or agreed to in writing, software 13 * distributed under the License is distributed on an "AS IS" BASIS, 14 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 * 16 * See the License for the specific language governing permissions and 17 * limitations under the License. 18 */ 19 20 var ilib = require("./ilib.js"); 21 22 var JSUtils = {}; 23 24 /** 25 * Perform a shallow copy of the source object to the target object. This only 26 * copies the assignments of the source properties to the target properties, 27 * but not recursively from there.<p> 28 * 29 * 30 * @static 31 * @param {Object} source the source object to copy properties from 32 * @param {Object} target the target object to copy properties into 33 */ 34 JSUtils.shallowCopy = function (source, target) { 35 var prop = undefined; 36 if (source && target) { 37 // using Object.assign is about 1/3 faster on nodejs 38 if (typeof(Object.assign) === "function") { 39 return Object.assign(target, source); 40 } 41 // polyfill 42 for (prop in source) { 43 if (prop !== undefined && typeof(source[prop]) !== 'undefined') { 44 target[prop] = source[prop]; 45 } 46 } 47 } 48 }; 49 50 /** 51 * Perform a recursive deep copy from the "from" object to the "deep" object. 52 * 53 * @static 54 * @param {Object} from the object to copy from 55 * @param {Object} to the object to copy to 56 * @return {Object} a reference to the the "to" object 57 */ 58 JSUtils.deepCopy = function(from, to) { 59 var prop; 60 61 for (prop in from) { 62 if (prop) { 63 if (typeof(from[prop]) === 'object') { 64 to[prop] = {}; 65 JSUtils.deepCopy(from[prop], to[prop]); 66 } else { 67 to[prop] = from[prop]; 68 } 69 } 70 } 71 return to; 72 }; 73 74 /** 75 * Map a string to the given set of alternate characters. If the target set 76 * does not contain a particular character in the input string, then that 77 * character will be copied to the output unmapped. 78 * 79 * @static 80 * @param {string} str a string to map to an alternate set of characters 81 * @param {Array.<string>|Object} map a mapping to alternate characters 82 * @return {string} the source string where each character is mapped to alternate characters 83 */ 84 JSUtils.mapString = function (str, map) { 85 var mapped = ""; 86 if (map && str) { 87 for (var i = 0; i < str.length; i++) { 88 var c = str.charAt(i); // TODO use a char iterator? 89 mapped += map[c] || c; 90 } 91 } else { 92 mapped = str; 93 } 94 return mapped; 95 }; 96 97 /** 98 * Check if an object is a member of the given array. If this javascript engine 99 * support indexOf, it is used directly. Otherwise, this function implements it 100 * itself. The idea is to make sure that you can use the quick indexOf if it is 101 * available, but use a slower implementation in older engines as well. 102 * 103 * @static 104 * @param {Array.<Object|string|number>} array array to search 105 * @param {Object|string|number} obj object being sought. This should be of the same type as the 106 * members of the array being searched. If not, this function will not return 107 * any results. 108 * @return {number} index of the object in the array, or -1 if it is not in the array. 109 */ 110 JSUtils.indexOf = function(array, obj) { 111 if (!array || !obj) { 112 return -1; 113 } 114 if (typeof(array.indexOf) === 'function') { 115 return array.indexOf(obj); 116 } else { 117 // polyfill 118 for (var i = 0; i < array.length; i++) { 119 if (array[i] === obj) { 120 return i; 121 } 122 } 123 return -1; 124 } 125 }; 126 127 /** 128 * Pad the str with zeros to the given length of digits. 129 * 130 * @static 131 * @param {string|number} str the string or number to pad 132 * @param {number} length the desired total length of the output string, padded 133 * @param {boolean=} right if true, pad on the right side of the number rather than the left. 134 * Default is false. 135 */ 136 JSUtils.pad = function (str, length, right) { 137 if (typeof(str) !== 'string') { 138 str = "" + str; 139 } 140 var start = 0; 141 // take care of negative numbers 142 if (str.charAt(0) === '-') { 143 start++; 144 } 145 return (str.length >= length+start) ? str : 146 (right ? str + JSUtils.pad.zeros.substring(0,length-str.length+start) : 147 str.substring(0, start) + JSUtils.pad.zeros.substring(0,length-str.length+start) + str.substring(start)); 148 }; 149 150 /** @private */ 151 JSUtils.pad.zeros = "00000000000000000000000000000000"; 152 153 /** 154 * Convert a string into the hexadecimal representation 155 * of the Unicode characters in that string. 156 * 157 * @static 158 * @param {string} string The string to convert 159 * @param {number=} limit the number of digits to use to represent the character (1 to 8) 160 * @return {string} a hexadecimal representation of the 161 * Unicode characters in the input string 162 */ 163 JSUtils.toHexString = function(string, limit) { 164 var i, 165 result = "", 166 lim = (limit && limit < 9) ? limit : 4; 167 168 if (!string) { 169 return ""; 170 } 171 for (i = 0; i < string.length; i++) { 172 var ch = string.charCodeAt(i).toString(16); 173 result += JSUtils.pad(ch, lim); 174 } 175 return result.toUpperCase(); 176 }; 177 178 /** 179 * Test whether an object in a Javascript Date. 180 * 181 * @static 182 * @param {Object|null|undefined} object The object to test 183 * @return {boolean} return true if the object is a Date 184 * and false otherwise 185 */ 186 JSUtils.isDate = function(object) { 187 if (typeof(object) === 'object') { 188 return Object.prototype.toString.call(object) === '[object Date]'; 189 } 190 return false; 191 }; 192 193 /** 194 * Merge the properties of object2 into object1 in a deep manner and return a merged 195 * object. If the property exists in both objects, the value in object2 will overwrite 196 * the value in object1. If a property exists in object1, but not in object2, its value 197 * will not be touched. If a property exists in object2, but not in object1, it will be 198 * added to the merged result.<p> 199 * 200 * Name1 and name2 are for creating debug output only. They are not necessary.<p> 201 * 202 * 203 * @static 204 * @param {*} object1 the object to merge into 205 * @param {*} object2 the object to merge 206 * @param {boolean=} replace if true, replace the array elements in object1 with those in object2. 207 * If false, concatenate array elements in object1 with items in object2. 208 * @param {string=} name1 name of the object being merged into 209 * @param {string=} name2 name of the object being merged in 210 * @return {Object} the merged object 211 */ 212 JSUtils.merge = function (object1, object2, replace, name1, name2) { 213 if (!object1 && object2) { 214 return object2; 215 } 216 if (object1 && !object2) { 217 return object1; 218 } 219 var prop = undefined, 220 newObj = {}; 221 for (prop in object1) { 222 if (prop && typeof(object1[prop]) !== 'undefined') { 223 newObj[prop] = object1[prop]; 224 } 225 } 226 for (prop in object2) { 227 if (prop && typeof(object2[prop]) !== 'undefined') { 228 if (ilib.isArray(object1[prop]) && ilib.isArray(object2[prop])) { 229 if (typeof(replace) !== 'boolean' || !replace) { 230 newObj[prop] = [].concat(object1[prop]); 231 newObj[prop] = newObj[prop].concat(object2[prop]); 232 } else { 233 newObj[prop] = object2[prop]; 234 } 235 } else if (typeof(object1[prop]) === 'object' && typeof(object2[prop]) === 'object') { 236 newObj[prop] = JSUtils.merge(object1[prop], object2[prop], replace); 237 } else { 238 // for debugging. Used to determine whether or not json files are overriding their parents unnecessarily 239 if (name1 && name2 && newObj[prop] === object2[prop]) { 240 console.log("Property " + prop + " in " + name1 + " is being overridden by the same value in " + name2); 241 } 242 newObj[prop] = object2[prop]; 243 } 244 } 245 } 246 return newObj; 247 }; 248 249 /** 250 * Return true if the given object has no properties.<p> 251 * 252 * 253 * @static 254 * @param {Object} obj the object to check 255 * @return {boolean} true if the given object has no properties, false otherwise 256 */ 257 JSUtils.isEmpty = function (obj) { 258 var prop = undefined; 259 260 if (!obj) { 261 return true; 262 } 263 264 for (prop in obj) { 265 if (prop && typeof(obj[prop]) !== 'undefined') { 266 return false; 267 } 268 } 269 return true; 270 }; 271 272 /** 273 * @static 274 */ 275 JSUtils.hashCode = function(obj) { 276 var hash = 0; 277 278 function addHash(hash, newValue) { 279 // co-prime numbers creates a nicely distributed hash 280 hash *= 65543; 281 hash += newValue; 282 hash %= 2147483647; 283 return hash; 284 } 285 286 function stringHash(str) { 287 var hash = 0; 288 for (var i = 0; i < str.length; i++) { 289 hash = addHash(hash, str.charCodeAt(i)); 290 } 291 return hash; 292 } 293 294 switch (typeof(obj)) { 295 case 'undefined': 296 hash = 0; 297 break; 298 case 'string': 299 hash = stringHash(obj); 300 break; 301 case 'function': 302 case 'number': 303 case 'xml': 304 hash = stringHash(String(obj)); 305 break; 306 case 'boolean': 307 hash = obj ? 1 : 0; 308 break; 309 case 'object': 310 var props = []; 311 for (var p in obj) { 312 if (obj.hasOwnProperty(p)) { 313 props.push(p); 314 } 315 } 316 // make sure the order of the properties doesn't matter 317 props.sort(); 318 for (var i = 0; i < props.length; i++) { 319 hash = addHash(hash, stringHash(props[i])); 320 hash = addHash(hash, JSUtils.hashCode(obj[props[i]])); 321 } 322 break; 323 } 324 325 return hash; 326 }; 327 328 /** 329 * Calls the given action function on each element in the given 330 * array arr in order and finally call the given callback when they are 331 * all done. The action function should take the array to 332 * process as its parameter, and a callback function. It should 333 * process the first element in the array and then call its callback 334 * function with the result of processing that element (if any). 335 * 336 * @param {Array.<Object>} arr the array to process 337 * @param {Function(Array.<Object>, Function(*))} action the action 338 * to perform on each element of the array 339 * @param {Function(*)} callback the callback function to call 340 * with the results of processing each element of the array. 341 */ 342 JSUtils.callAll = function(arr, action, callback, results) { 343 results = results || []; 344 if (arr && arr.length) { 345 action(arr, function(result) { 346 results.push(result); 347 JSUtils.callAll(arr.slice(1), action, callback, results); 348 }); 349 } else { 350 callback(results); 351 } 352 }; 353 354 module.exports = JSUtils; 355