UNPKG

flounder

Version:
352 lines (297 loc) 8.39 kB
/* * Copyright (c) 2016-2018 dunnhumby Germany GmbH. * All rights reserved. * * This source code is licensed under the MIT license found in the LICENSE file * in the root directory of this source tree. * */ /** * search defaults * * @type {Object} */ export const defaults = { /* * minimum value length to search * * _Number_ */ minimumValueLength : 1, /* * minimum score to display * * _Number_ */ minimumScore : 0, /* * params to test for score * * called as: * score += this.scoreThis( search[ param ], weights[ param ] ); */ scoreProperties : [ 'text', 'textFlat', 'textSplit', 'value', 'valueFlat', 'valueSplit', 'description', 'descriptionSplit' ], /* * params to test with startsWith * * called as: * startsWith( query, search[ param ], weights[ param + 'StartsWith' ] ); */ startsWithProperties : [ 'text', 'value' ], /* * scoring weight */ weights : { text : 30, textStartsWith : 50, textFlat : 10, textSplit : 10, value : 30, valueStartsWith : 50, valueFlat : 10, valueSplit : 10, description : 15, descriptionSplit : 30 } }; /** * ## Sole * * turns out there`s all kinds of flounders */ export class Sole { /** * ## compareScoreCards * * Sorts out results by the score * * @param {Object} a result * @param {Object} b result to compare with * * @return {Number} comparison result */ compareScoreCards( a, b ) { if ( a && a.score && b && b.score ) { a = a.score; b = b.score; if ( a > b ) { return 1; } else if ( a < b ) { return -1; } return 0; } return null; } /** * ## constructor * * initial setup of Sole object * * @param {Object} flounder option object * * @return {Object} this */ constructor( flounder ) { this.flounder = flounder; this.getResultWeights = this.getResultWeights.bind( this ); this.getResultWeights.bound = true; this.scoreThis = this.scoreThis.bind( this ); this.scoreThis.bound = true; return this; } /** * ## escapeRegExp * * escapes a string to be compatible with regex * * @param {String} string string to be escaped * * @return {String} escaped string */ escapeRegExp( string ) { return string.replace( /[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, '\\$&' ); } /** * ## getResultWeights * * after the data is prepared this is mapped through the data to get * weighted results * * @param {Object} d object * @param {Number} i index * * @return {Object} res weighted results */ getResultWeights( d, i ) { let score = 0; const res = { i : i, d : d }; const search = d.search = d.search || {}; const weights = defaults.weights; const dText = `${d.text}`; const dValue = `${d.value}`; search.text = dText; search.textFlat = dText.toLowerCase(); search.textSplit = search.textFlat.split( ' ' ); search.value = dValue; search.valueFlat = dValue.toLowerCase(); search.valueSplit = search.valueFlat.split( ' ' ); search.description = d.description ? d.description.toLowerCase() : null; search.descriptionSplit = d.description ? search.description.split( ' ' ) : null; defaults.scoreProperties.forEach( param => { score += this.scoreThis( search[ param ], weights[ param ] ); } ); defaults.startsWithProperties.forEach( param => { score += this.startsWith( this.query, search[ param ], weights[ `${param}StartsWith` ] ); } ); res.score = score; return res; } /** * ## isItemAboveMinimum * * removes the items that have recieved a score lower than the set minimum * * @param {Object} d score object * * @return {Boolean} the minimum or not */ isItemAboveMinimum( d ) { return d.score >= defaults.minimumScore; } /** * ## isThereAnythingRelatedTo * * Check our search content for related query words, * here it applies the various weightings to the portions of the search * content. Triggers show results * * @param {Array} query array of words to search the content for * * @return {Array} results returns array of relevant search results */ isThereAnythingRelatedTo( query = '' ) { let ratedRes; query = query.length ? query : `${query}`; if ( query.length >= defaults.minimumValueLength ) { this.query = query.toLowerCase().split( ' ' ); let data = this.flounder.data; data = this.flounder.sortData( data ); ratedRes = this.ratedRes = data.map( this.getResultWeights ); } else { return false; } ratedRes = ratedRes.filter( this.isItemAboveMinimum ); ratedRes.sort( this.compareScoreCards ); return this.ratedRes = ratedRes; } /** * ## startsWith * * checks the beginning of the given text to see if the query matches * exactly * * @param {String} query string to search for * @param {String} value string to search in * @param {Integer} weight amount of points to give an exact match * * @return {Integer} points to award */ startsWith( query, value, weight ) { const valLength = value.length; const queryLength = query.length; if ( queryLength <= valLength ) { const valStr = value.toLowerCase().slice( 0, queryLength ); if ( valStr === query ) { return weight; } } return 0; } /** * ## scoreThis * * Queries a string or array for a set of search options and assigns a * weighted score. * * @param {String} target string to be search * @param {Integer} weight weighting of importance for this target. * higher is more important * @param {Boolean} noPunishment when passed true, this does not give * negative points for non-matches * * @return {Integer} the final weight adjusted score */ scoreThis( target, weight, noPunishment ) { let score = 0; if ( target ) { this.query.forEach( queryWord => { queryWord = this.escapeRegExp( queryWord ); let count = 0; if ( typeof target === 'string' ) { queryWord = new RegExp( queryWord, 'g' ); count = ( target.match( queryWord ) || [] ).length; } else if ( target[ 0 ] ) { target.forEach( word => { count += word.indexOf( queryWord ) !== -1 ? 1 : 0; } ); } else { count = target[ queryWord ] || 0.000001; } if ( count > 0 ) { score = weight * count * 10; } else if ( noPunishment !== true ) { score = -weight; } } ); } return Math.floor( score ); } } export default Sole;