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