diff --git a/Packages/ohif-viewerbase/client/lib/classes/TypeSafeCollection.js b/Packages/ohif-viewerbase/client/lib/classes/TypeSafeCollection.js index e5c602054..637ad8df8 100644 --- a/Packages/ohif-viewerbase/client/lib/classes/TypeSafeCollection.js +++ b/Packages/ohif-viewerbase/client/lib/classes/TypeSafeCollection.js @@ -1,15 +1,20 @@ import { Random } from 'meteor/random'; import { ReactiveVar } from 'meteor/reactive-var'; -const FUNCTION = 'function'; -const OBJECT = 'object'; -const STRING = 'string'; +/** + * Constants + */ + const PROPERTY_SEPARATOR = '.'; const ORDER_ASC = 'asc'; const ORDER_DESC = 'desc'; const MIN_COUNT = 0x00000000; const MAX_COUNT = 0x7FFFFFFF; +/** + * Class Definition + */ + export class TypeSafeCollection { constructor() { @@ -39,115 +44,6 @@ export class TypeSafeCollection { return this._elements(silent).find(item => item.id === id); } - /** - * Static Methods - */ - - /** - * Retrieve an object's property value by name - * @param {Object} object The object we want read the property from... - * @param {String} propertyName The property to be read (e.g., 'address.street.name' or 'address.street.number' - * to read object.address.street.name or object.address.street.number); - * @returns {Any} Returns whatever the property holds or undefined if the property cannot be read or reached. - */ - static getPropertyValue(object, propertyName) { - let propertyValue; - if (typeof object === OBJECT && object !== null && typeof propertyName === STRING) { - let fragments = propertyName.split(PROPERTY_SEPARATOR), - fragmentCount = fragments.length; - if (fragmentCount > 0) { - let firstFragment = fragments[0], - remainingFragments = fragmentCount > 1 ? fragments.slice(1).join(PROPERTY_SEPARATOR) : null; - propertyValue = object[firstFragment]; - if (remainingFragments !== null) { - propertyValue = TypeSafeCollection.getPropertyValue(propertyValue, remainingFragments); - } - } - } - return propertyValue; - } - - // [WIP]... - static compareToPropertyMapStrict(propertyMap, object) { - const hasOwnProperty = Object.prototype.hasOwnProperty; - const getPropertyValue = TypeSafeCollection.getPropertyValue; - for (let propertyName in propertyMap) { - if (hasOwnProperty.call(propertyMap, propertyName)) { - const propertyValue = getPropertyValue(object, propertyName); - if (propertyMap[propertyName] !== propertyValue) { - return false; - } - } - } - return true; - } - - /** - * Checks if a sorting specifier is valid. - * A valid sorting specifier consists of an array of arrays being each subarray a pair - * in the format ["property name", "sorting order"]. - * The following exemple can be used to sort studies by "date"" and use "time" to break ties in descending order. - * [ [ 'study.date', 'desc' ], [ 'study.time', 'desc' ] ] - * @param {Array} specifiers The sorting specifier to be tested. - * @returns {boolean} Returns true if the specifiers are valid, false otherwise. - */ - static isValidSortingSpecifier(specifiers) { - let result = true; - if (specifiers instanceof Array && specifiers.length > 0) { - for (let i = specifiers.length - 1; i >= 0; i--) { - const item = specifiers[i]; - if (item instanceof Array) { - const property = item[0]; - const order = item[1]; - if (typeof property === STRING && (order === ORDER_ASC || order === ORDER_DESC)) { - continue; - } - } - result = false; - break; - } - } - return result; - } - - /** - * Sorts an array based on sorting specifier options. - * @param {Array} list The that needs to be sorted. - * @param {Array} specifiers An array of specifiers. Please read isValidSortingSpecifier method definition for further details. - * @returns {void} No value is returned. The array is sorted in place. - */ - static sortListBy(list, specifiers) { - const thisClass = TypeSafeCollection; - if (list instanceof Array && thisClass.isValidSortingSpecifier(specifiers)) { - const getPropertyValue = thisClass.getPropertyValue; - const specifierCount = specifiers.length; - list.sort((a, b) => { - let index = 0; - while (index < specifierCount) { - const specifier = specifiers[index]; - const property = specifier[0]; - const order = specifier[1] === ORDER_DESC ? -1 : 1; - const aValue = getPropertyValue(a, property); - const bValue = getPropertyValue(b, property); - // @TODO: should we check for the types being compared, like: - // ~~ if (typeof aValue !== typeof bValue) continue; - // Not sure because dates, for example, can be correctly compared to numbers... - if (aValue < bValue) { - return order * -1; - } - if (aValue > bValue) { - return order * 1; - } - if (++index >= specifierCount) { - return 0; - } - } - }); - } else { - throw new Error('TypeSafeCollection::sortListBy Invalid Arguments'); - } - } - /** * Public Methods */ @@ -247,39 +143,40 @@ export class TypeSafeCollection { return found && found.payload; } - // the reactive var will not be notified if no valid callback is supplied... + /** + * Find an element by a criteria defined by the given callback function. + * Attention!!! The reactive source will not be notified if no valid callback is supplied... + * @param {function} callback A callback function which will define the search criteria. The callback + * function will be passed the collection element, its ID and its index in this very order. The callback + * shall return true when its criterea has been fulfilled. + * @returns {Any} The matched element or undefined if not match was found. + */ find(callback) { let found; - if (typeof callback === FUNCTION) { + if (_isFunction(callback)) { found = this._elements().find((item, index) => { - return callback.call(this, item.payload, index); - }); - } - return found && found.payload; - } - - // the reactive var will not be notified if no valid property map is supplied... - findBy(propertyMap) { - let found; - if (typeof propertyMap === OBJECT && propertyMap !== null) { - const hasOwn = Object.prototype.hasOwnProperty; - found = this._elements().find(item => { - const payload = item.payload; - for (let propertyName in propertyMap) { - if (hasOwn.call(propertyMap, propertyName)) { - if (propertyMap[propertyName] !== TypeSafeCollection.getPropertyValue(payload, propertyName)) { - return false; - } - } - } - return true; + return callback.call(this, item.payload, item.id, index); }); } return found && found.payload; } /** - * Find all elements that match a specified property map. + * Find the first element that strictly matches the specified property map. + * Attention!!! The reactive source will not be notified if no valid property map is supplied... + * @param {Object} propertyMap A property map that will be macthed against all collection elements. + * @returns {Any} The matched element or undefined if not match was found. + */ + findBy(propertyMap) { + let found; + if (_isObject(propertyMap)) { + found = this._elements().find(item => _compareToPropertyMapStrict(propertyMap, item.payload)); + } + return found && found.payload; + } + + /** + * Find all elements that strictly match the specified property map. * Attention!!! The reactive source will not be notified if no valid property map is supplied... * @param {Object} propertyMap A property map that will be macthed against all collection elements. * @returns {Array} An array of entries of all elements that match the given criteria. Each set in @@ -287,19 +184,12 @@ export class TypeSafeCollection { */ findAllEntriesBy(propertyMap) { const found = []; - if (typeof propertyMap === OBJECT && propertyMap !== null) { - const hasOwn = Object.prototype.hasOwnProperty; + if (_isObject(propertyMap)) { this._elements().forEach((item, index) => { - const payload = item.payload; - for (let propertyName in propertyMap) { - if (hasOwn.call(propertyMap, propertyName)) { - if (propertyMap[propertyName] !== TypeSafeCollection.getPropertyValue(payload, propertyName)) { - return; // skip this element since it does not match the criteria... - } - } + if (_compareToPropertyMapStrict(propertyMap, item.payload)) { + // Match! Add it to the found list... + found.push([ item.payload, item.id, index ]); } - // Match! Add it to the found list... - found.push([ payload, item.id, index ]); }); } return found; @@ -316,15 +206,23 @@ export class TypeSafeCollection { */ findAllBy(propertyMap, options) { const found = this.findAllEntriesBy(propertyMap).map(item => item[0]); // Only payload is relevant... - if (options instanceof Object && 'sort' in options) { - TypeSafeCollection.sortListBy(found, options.sort); + if (_isObject(options)) { + if ('sort' in options) { + _sortListBy(found, options.sort); + } } return found; } - // the reactive var will not be notified if no valid callback is supplied... + /** + * Executes the supplied callback function for each element of the collection. + * Attention!!! The reactive source will not be notified if no valid property map is supplied... + * @param {function} callback The callback function to be executed. The callback is passed the element, + * its ID and its index in this very order. + * @returns {void} Nothing is returned. + */ forEach(callback) { - if (typeof callback === FUNCTION) { + if (_isFunction(callback)) { this._elements().forEach((item, index) => { callback.call(this, item.payload, item.id, index); }); @@ -348,10 +246,150 @@ export class TypeSafeCollection { */ all(options) { let list = this._elements().map(item => item.payload); - if (options instanceof Object && 'sort' in options) { - TypeSafeCollection.sortListBy(list, options.sort); + if (_isObject(options)) { + if ('sort' in options) { + _sortListBy(list, options.sort); + } } return list; } } + +/** + * Utility Functions + */ + +/** + * Test if supplied argument is a valid object for current class purposes. + * Atention! The underscore version of this function should not be used for performance reasons. + */ +function _isObject(subject) { + return subject instanceof Object || typeof subject === 'object' && subject !== null; +} + +/** + * Test if supplied argument is a valid string for current class purposes. + * Atention! The underscore version of this function should not be used for performance reasons. + */ +function _isString(subject) { + return typeof subject === 'string'; +} + +/** + * Test if supplied argument is a valid function for current class purposes. + * Atention! The underscore version of this function should not be used for performance reasons. + */ +function _isFunction(subject) { + return typeof subject === 'function'; +} + +/** + * Retrieve an object's property value by name. Composite property names (e.g., 'address.country.name') are accepted. + * @param {Object} targetObject The object we want read the property from... + * @param {String} propertyName The property to be read (e.g., 'address.street.name' or 'address.street.number' + * to read object.address.street.name or object.address.street.number, respectively); + * @returns {Any} Returns whatever the property holds or undefined if the property cannot be read or reached. + */ +function _getPropertyValue(targetObject, propertyName) { + let propertyValue; // undefined (the default return value) + if (_isObject(targetObject) && _isString(propertyName)) { + const fragments = propertyName.split(PROPERTY_SEPARATOR); + const fragmentCount = fragments.length; + if (fragmentCount > 0) { + const firstFragment = fragments[0]; + const remainingFragments = fragmentCount > 1 ? fragments.slice(1).join(PROPERTY_SEPARATOR) : null; + propertyValue = targetObject[firstFragment]; + if (remainingFragments !== null) { + propertyValue = _getPropertyValue(propertyValue, remainingFragments); + } + } + } + return propertyValue; +} + +/** + * Compare a property map with a target object using strict comparison. + * @param {Object} propertyMap The property map whose properties will be used for comparison. Composite + * property names (e.g., 'address.country.name') will be tested against the "resolved" properties from the target object. + * @param {Object} targetObject The target object whose properties will be tested. + * @returns {boolean} Returns true if the properties match, false otherwhise. + */ +function _compareToPropertyMapStrict(propertyMap, targetObject) { + let result = false; + // "for in" loops do not thown exceptions for invalid data types... + for (let propertyName in propertyMap) { + // Attention!!! The 'hasOwnProperty' test has been intentionally skipped... + if (propertyMap[propertyName] !== _getPropertyValue(targetObject, propertyName)) { + result = false; + break; + } else if (result !== true) { + result = true; + } + } + return result; +} + +/** + * Checks if a sorting specifier is valid. + * A valid sorting specifier consists of an array of arrays being each subarray a pair + * in the format ["property name", "sorting order"]. + * The following exemple can be used to sort studies by "date"" and use "time" to break ties in descending order. + * [ [ 'study.date', 'desc' ], [ 'study.time', 'desc' ] ] + * @param {Array} specifiers The sorting specifier to be tested. + * @returns {boolean} Returns true if the specifiers are valid, false otherwise. + */ +function _isValidSortingSpecifier(specifiers) { + let result = true; + if (specifiers instanceof Array && specifiers.length > 0) { + for (let i = specifiers.length - 1; i >= 0; i--) { + const item = specifiers[i]; + if (item instanceof Array) { + const property = item[0]; + const order = item[1]; + if (_isString(property) && (order === ORDER_ASC || order === ORDER_DESC)) { + continue; + } + } + result = false; + break; + } + } + return result; +} + +/** + * Sorts an array based on sorting specifier options. + * @param {Array} list The that needs to be sorted. + * @param {Array} specifiers An array of specifiers. Please read isValidSortingSpecifier method definition for further details. + * @returns {void} No value is returned. The array is sorted in place. + */ +function _sortListBy(list, specifiers) { + if (list instanceof Array && _isValidSortingSpecifier(specifiers)) { + const specifierCount = specifiers.length; + list.sort(function _sortListByCallback(a, b) { // callback name for stack traces... + let index = 0; + while (index < specifierCount) { + const specifier = specifiers[index]; + const property = specifier[0]; + const order = specifier[1] === ORDER_DESC ? -1 : 1; + const aValue = _getPropertyValue(a, property); + const bValue = _getPropertyValue(b, property); + // @TODO: should we check for the types being compared, like: + // ~~ if (typeof aValue !== typeof bValue) continue; + // Not sure because dates, for example, can be correctly compared to numbers... + if (aValue < bValue) { + return order * -1; + } + if (aValue > bValue) { + return order * 1; + } + if (++index >= specifierCount) { + return 0; + } + } + }); + } else { + throw new Error('Invalid Arguments'); + } +}