Algorithm Challenge: Fuzzy Search - javascript

I recently took part in an algorithm challenge to create a Fuzzy search with the following criteria:
Given a set array, create a function that receives one argument and returns a new array containing only the values that start with either:
A) The argument provided
B) The argument provided but with 1 difference (i.e. 1 incorrect letter)
The array was: fruits = [apple, apricot, banana, pear, mango, cherry, tomato]
so:
fuzzySearch('ap') = ['apple, apricot']
fuzzySearch('app') = ['apple', 'apricot']
fuzzySearch('appl') = ['apple']
fuzzySearch('pa') = ['banana', 'mango']
This is the solution I came up with:
const fruits = ['apple', 'apricot', 'banana', 'pear', 'mango', 'cherry', 'tomato']
function fuzzySearch(str) {
return fruits.filter(fruit =>
{
let letterCount = 0
const fruitLetArr = fruit.toLowerCase().split('')
const strArr = str.toLowerCase().split('')
for (var i = 0; i < strArr.length; i++) {
console.log(fruitLetArr[i], strArr[i], i, letterCount)
if (fruitLetArr[i] !== strArr[i]) letterCount++
if (letterCount === 2) break;
}
if (letterCount < 2) return true
});
}
fuzzySearch(str)
Can anyone think of a faster way that doesn't involve iterating over every value before a soltion can be found?

Here's something that should be slightly more efficient. Also easier to read. In this solution, I am assuming that by "difference" you mean a substitution of a letter for another letter, rather than the addition of another letter.
const fruits = ['apple', 'apricot', 'banana', 'pear', 'mango', 'cherry', 'tomato'];
const fuzzySearch = (str) => {
return fruits.filter((fruit) => {
// If our first case is met, immediately return
if (fruit.startsWith(str)) return true;
// Split the fruit based on the length of input string
const test = fruit.slice(0, str.length).split('');
let diffs = 0;
// Compare + keep track of differences between input + sliced fruit
test.forEach((letter, i) => letter !== str[i] && diffs++);
// If we have more than one difference, it doesn't meet case #2
if (diffs > 1) return false;
return true;
});
};
const testCases = ['ap', 'app', 'appl', 'pan', 'bp'];
for (const testCase of testCases) {
console.log(fuzzySearch(testCase));
}

Related

What is a Big O of this algorithm?

I wrote the following algorithm and it's pretty horrendous but at the moment it was the only way I was able to solve the problem. I was just wondering what the Big O of this equation is. My best guess is that it is O(n!) since I have the loops inside the filter function. Is this correct?
/*
Scrabble Validator, essentially.
const dictionary = ['apple', 'avocado', 'anteater', 'april', 'basket', 'ball', 'cat', 'cradle'] etc for about 100 or so words
const points = [{a:1}, {b:2}, {c:3}, {d:4}]; etc for all the letters in the alphabet. there are blanks - a blank is worth 0 but can be used in place of any letter
given a string of letters and a dictionary,
1. find all valid anagrams
2. find their point value using a Points object
3. Sort those valid options by highest scoring point
*/
const chars = 'aplpe';
const dictionary = ['apple', 'avocado', 'lap', 'app', 'anteater', 'april', 'basket', 'ball', 'cat', 'cradle'];
const points = [{p:1}, {a:2}, {e:3}, {l:4}];
let pointsMap = {};
points.forEach((point) => pointsMap = { ...pointsMap, ...point });
const charMap = {};
for (let char of chars) {
charMap[char] = charMap[char] + 1 || 1;
}
const matches = dictionary
.filter(word => {
if (word.length > chars.length) return false;
const wordMap = {};
for (let char of word) {
wordMap[char] = wordMap[char] + 1 || 1;
}
for (let char in wordMap) {
if (!charMap[char] || charMap[char] < wordMap[char]) {
return false;
}
}
return true;
})
.map((word) => {
let total = 0;
for (let char of word) {
total += pointsMap[char];
}
return { word, total }
})
.sort((a, b) => a.total > b.total ? -1 : 1)
.map(({ word }) => word);
return matches;

Moving Imperative to Declarative- character limitations

One module in my app pulls records from a mongodb and prints out the fields for the user to see. The fields are displayed in a particular order. The order is stored in an array. The mongodb document (pers) may not have all of the fields possible filled out. Anyway, here is an example of my original code. It works. I want to move onto something better. I will explain as I go.
const keys = ['banana', 'orange', 'apple', 'grape']
const pers = {
banana: 'word',
orange: 'e t',
apple: 'scrabble',
}
let entries = []
let entry = ''
keys.forEach(key => {
if (pers[key]) entries.push(pers[key])
})
for (let i=0; i<entries.length; i++) {
if ((entries.length + entries[i].length < 10) entry += '\n' + entries[i]
else {
console.log(entry) //message.send in my app
entry = entries[i]
}
}
console.log(entry)
So in my app, the message that is sent cannot be longer than 2000 characters. I am using 10 characters for the example here in the post. And I do not want them split up in the middle either. So for instance 'word' + 'e t' is the maximum I can concatenate together. If I try to add 'scrabble' to it, the total length will be over 10 characters in length. So I want to send 'word e t' to the console and then start the variable entry over. I also do not want to send 'word' and then 'e' and then 't' and then 'scrabble'. Like I said before, my existing code works. I just want to try and be clear on my intent.
Moving forward... Below is my second iteration of the code. I'm trying to move away from imperative statements to declarative. So I replaced the forEach block. This also works great.
const keys = ['banana', 'orange', 'apple', 'grape']
const pers = {
banana: 'word',
orange: 'e t',
apple: 'scrabble',
}
let entry = ''
const entries = keys.filter(key => pers.hasOwnProperty(key)).map(key => pers[key])
for (let i=0; i<entries.length; i++) {
if ((entries.length + entries[i].length < 10) entry += '\n' + entries[i]
else {
console.log(entry)
entry = entries[i]
}
}
console.log(entry)
The console output for both of these is:
word
e t
scrabble
That is exactly what I wanted. Now I really want to move away from that for block if possible. Surely there is a way to add another chain to my declarative statement to take care of that portion? I could possibly see a way of doing it if I didn't have the character limitation.
Anyway, if I have been unclear in any of my description or intent, please let me know and I will do my best to clarify.
Thanks!!
Try using .reduce like this. Your original code had a syntax error, so I'm assuming that was just a typo and not something important.
While that's functionally identical to your current code, it's still pretty convoluted. If I were you, I'd create an array of entryStrs while iterating, and then console.log them all later, like this:
const keys = ['banana', 'orange', 'apple', 'grape']
const pers = {
banana: 'word',
orange: 'e t',
apple: 'scrabble',
}
const { currentMessage, messages } = keys
.filter(key => pers.hasOwnProperty(key))
.map(key => pers[key])
.reduce(({ currentMessage = '', messages = [] }, entry, i, entries) => {
if (entries.length + entry.length < 10) {
currentMessage += currentMessage
? ('\n' + entry)
: entry;
} else {
messages.push(currentMessage)
currentMessage = entry;
}
return { currentMessage, messages };
}, {});
const allMessages = [...messages, currentMessage];
console.log(allMessages.join('\n-----\n-----\n'));
since in the for block you have to iterate and change an external variable and not changing anything in the array itself, you can use Conditional (tenary) Operator and replace for with forEach()
const keys = ['banana', 'orange', 'apple', 'grape']
const pers = {
banana: 'word',
orange: 'e t',
apple: 'scrabble',
}
let entry = ''
const entries = keys.filter(key => pers.hasOwnProperty(key)).map(key => pers[key])
entries.forEach(e => {
entries.length + e.length < 10 ? entry += '\n' + e : (console.log(entry), entry = e)
});
console.log(entry)

How to convert array to array of objects?

I have
let array = ['mango', 'mango_shake','banana', 'banana_shake', 'cherry', 'cherry_shake', 'Strawberry', 'Strawberry_shake', ...n];
What i want to do is:
let target = [{'fruit': 'mango', 'drink': 'mango_shake'},
{'fruit': 'banana', 'drink': 'banana_shake'}, ...n];
How can i do it?
You can simply loop through array and create an array of object like this
let array = ['mango', 'mango_shake', 'banana', 'banana_shake', 'cherry', 'cherry_shake', 'Strawberry', 'Strawberry_shake'];
var res = [];
for (var i = 0; i < array.length; i = i + 2) {
var ob = {};
ob.fruit = array[i];
ob.drink = array[i + 1];
res.push(ob);
}
console.log(res);
Note: This answer assumes the fruit and its corresponding drink are always right beside each other in the array. This will give wrong answers if items are out of order.
Just iterate over your original array until it is empty and take out pairs and map them to objects:
const result = [];
while(array.length)
result.push((([fruit, drink]) => ({fruit, drink}))(array.splice(0, 2));
(In case this is your homework: i think it will be harder to explain to your teacher how it works instead of just trying it on your own :))
You can iterate over the array to combine every other item
let target = {};
array.forEach( (curr, indx, arr) => {
if (indx %2 == 1) {
target[arr[indx-1]] = curr
}
});

Dedup array and sort array by most frequent occurrence (with LoDash)

Say I have an array that goes something like:
fruit_basket = ['apple', 'orange', 'banana', 'pear', 'banana']
I want to make an array fruits that consists of the fruits found in fruit basket, sorted in order of most frequently occurring fruit. (If there are ties I don't care about ordering.)
So one valid value for fruits is:
['banana', 'orange', 'apple', 'pear']
What's the most concise way to achieve this using LoDash? I don't care about run time performance.
First you'd count the occurences
var x = _.chain(fruit_basket).countBy(); // {apple: 1, orange: 1, banana: 2, pear: 1}
Then you'd pair them and sort by the number of occurences, using reverse to get the largest number first
var y = x.toPairs().sortBy(1).reverse(); //[["banana",2],["pear",1],["orange",1],["apple",1]]
Then you'd just map back the keys, and get the value as an array
var arr = y.map(0).value(); // ['banana', 'orange', 'apple', 'pear']
All chained together, it looks like
var arr = _.chain(fruit_basket).countBy().toPairs().sortBy(1).reverse().map(0).value();
Without loDash, something like this would do it
var fruit_basket = ['apple', 'orange', 'banana', 'pear', 'banana'];
var o = {};
fruit_basket.forEach(function(item) {
item in o ? o[item] += 1 : o[item] = 1;
});
var arr = Object.keys(o).sort(function(a, b) {
return o[a] < o[b];
});
Here's my take on this.
It doesn't use lodash as the question asks for. It's a vanilla alternative. I feel this could be valuable to people who land here from Google and don't want to use lodash (that's how I got here, at least).
const orderByCountAndDedupe = arr => {
const counts = new Map();
arr.forEach( item => {
if ( !counts.has(item) ) {
counts.set(item, 1);
} else {
counts.set(item, counts.get(item)+1);
}
});
return (
Array.from(counts)
.sort( (a, b) => b[1] - a[1])
.map( ([originalItem, count]) => originalItem)
);
};
An approach using for loop Array.prototype.splice()
var fruit_basket = ['apple', 'orange', 'banana', 'pear', 'banana'];
var res = [];
for (var i = 0; i < fruit_basket.length; i++) {
var index = res.indexOf(fruit_basket[i]);
// if current item does not exist in `res`
// push current item to `res`
if (index == -1) {
res.push(fruit_basket[i])
} else {
// else remove current item , set current item at index `0` of `res`
res.splice(index, 1);
res.splice(0, 0, fruit_basket[i])
}
}
console.log(res)
You can count occurences using _.countBy and then use it in _.sortBy:
var counter = _.countBy(fruit_basket)
var result = _(fruit_basket).uniq().sortBy(fruit => counter[fruit]).reverse().value()

Word Frequency Count, fix a bug with standard property

I'm trying to build a javascript function which would count the number of occurrences of each word in an input array.
Example :
Input
a=["a","booster","booster","constructor","adam","adam","adam","adam"]
Output:
"a":1
"booster":2
"constructor":1
"adam":4
Output should be dict-alike.
I'm new to javascript and I tried to use a dict. But objects have a property called "constructor", so cnt["constructor"] seems not to work.
Here is my code and the result:
var cnt={};
console.log("constructor");
for(var i=0;i<a.length;++i)
{
if(! (a[i] in cnt))
cnt[a[i]]=0;
else
cnt[a[i]]+=1;
}
for(var item in cnt)
console.log(item+":"+cnt[item]);
Result:
You can see that 1 is added to constructor of cnt as a string.
function count(arr){
return arr.reduce(function(m,e){
m[e] = (+m[e]||0)+1; return m
},{});
}
The idea behind are
the use of reduce for elegance
the conversion of m[e] to a number using +m[e] to avoid the constructor (or toString) problem
Demonstration
var arr = ['apple', 'orange', 'grape', 'apple'];
var initialValue = {};
var result = arr.reduce(function(accumulator, curr, idx, arr) {
if (Object.hasOwnProperty.call(accumulator, curr)) { // does current exist as key on initialValue object?
accumulator[curr]++;
} else { // no key for current on initialValue object
accumulator[curr] = 1;
}
return accumulator;
}, initialValue);
console.log(result);
You can also create an array just by initializing [] as the initial accumulator.
var fruits = ['apple', 'orange', 'grape', 'apple'].reduce((countFruits,currentFruit)=>{
if(typeof countFruits[currentFruit]!=="undefined"){
countFruits[currentFruit] = countFruits[currentFruit]+1
return countFruits
}
countFruits[currentFruit] = 1
return countFruits
},[])
console.log(fruits)

Categories