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