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