Replace string with multiple values, without interference - javascript

Say, I have a string to be replaced:
let searches = ['gone', 'go', 'run']
let s = 'go went gone-go'
const lookup = {
'go': '(go)',
'gone': '[gone]',
}
for (let key of searches) {
s = s.replaceAll(key, lookup[key])
}
console.log(s)
And I get (go) went [(go)ne]-(go).
Assume s can be any string with some words from lookup keys, and lookup values won't necessarily have consistent patterns. searches is the variable from outside inputs.
If I change orders in searches to, for example, ['go', 'gone', 'run'], result becomes (go) went (go)ne-(go).
The result I'm expecting is (go) went [gone]-(go), so that longer ones are replaced first, and won't be replaced by later matches.
I did come up with a solution that replacing values from lookup to uuid first, iterating from longer keys to shorter ones, then replace uuid back with corresponding values. Of course, this is rather stupid and inefficient:
let searches = ['go', 'gone', 'run']
let s = 'go went gone-go'
const lookup = {
'go': '(go)',
'gone': '[gone]',
}
const uuid = () => Date.now().toString(36) + Math.random().toString(36).substring(2) // pseudo uuid for quick demo. src: https://stackoverflow.com/a/44078785/17954892
let uuidKeys = {}
Object.keys(lookup).forEach(k => uuidKeys[k] = uuid()) // uuidKeys = {'go': 'random1', 'gone': 'random2'}
let uuids = Object.values(uuidKeys) // uuids = ['random1', 'random2']
let uuidValues = {}
Object.keys(lookup).forEach((k, i) => uuidValues[uuids[i]] = lookup[k]) // uuidValues = {'random1': '(go)', 'random2': '[gone]'}
searches.sort((a, b) => b.length -a.length) // searches = ['gone', 'run', 'go']
for (let key of searches) {
s = s.replaceAll(key, uuidKeys[key]) // s = 'random1 went random2-random1'
}
for (let key of searches.map(i => uuidKeys[i])) {
s = s.replaceAll(key, uuidValues[key]) // s = '(go) went [gone]-(go)'
}
console.log(s)
I then thought about loop-split the string by searches, then replace and record the index that's processed, and finally join the list back to string. However, I cannot find a nice way to implement it without expensive Array methods (flat, splice, etc.) in for-loops.
Is there an elegant/efficient way to achieve the result?

You can do this by using a regular expression with the g flag with replace, passing a callback function as the replacement; the function then picks the appropriate replacement based on what matched.
For instance:
let searches = ["gone", "go", "run"];
let s = "go went gone-go";
const lookup = {
"go": "(go)",
"gone": "[gone]",
};
let rex = new RegExp(searches.map(escapeRegex).join("|"), "g");
s = s.replace(rex, match => lookup[match]);
console.log(s);
...where escapeRegex escapes any charactesr in the search strings that have special meaning in regular expressions; see this question's answers for possible implementations.
Live Example:
function escapeRegex(string) {
return string.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&');
}
let searches = ["gone", "go", "run"];
let s = "go went gone-go";
const lookup = {
"go": "(go)",
"gone": "[gone]",
};
let rex = new RegExp(searches.map(escapeRegex).join("|"), "g");
s = s.replace(rex, match => lookup[match]);
console.log(s); // "(go) went [gone]-(go)"
Note: The order of the strings in the searches array matters. If you put "go" before "gone", it'll match first:
function escapeRegex(string) {
return string.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&');
}
let searches = ["go", "gone", "run"];
// Note −−−−−−−−^
let s = "go went gone-go";
const lookup = {
"go": "(go)",
"gone": "[gone]",
};
let rex = new RegExp(searches.map(escapeRegex).join("|"), "g");
s = s.replace(rex, match => lookup[match]);
console.log(s); // "(go) went (go)ne-(go)"
If you always want the longest one to have the highest precedence, and you can't control the contents of the input array, you could sort it prior to using it:
function escapeRegex(string) {
return string.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&');
}
let searches = ["go", "gone", "run"];
// Note −−−−−−−−^
let s = "go went gone-go";
const lookup = {
"go": "(go)",
"gone": "[gone]",
};
let rex = new RegExp(
searches.sort((a, b) => b.length - a.length)
.map(escapeRegex)
.join("|"),
"g"
);
s = s.replace(rex, match => lookup[match]);
console.log(s); // "(go) went [gone]-(go)"

Related

In "Unique Morse Code Words" Question, I am facing issue by using JavaScript

In this code for 1 testcase it is not working for 2nd testcases its working. Don't find where is the issue. Can someone help me to understand about its issue.
var arr1 = [".-","-...","-.-.","-..",".","..-.","--.","....","..",".---","-.-",".-..","--","-.","---",".--.","--.-",".-....","-","..-","...-",".--","-..-","-.--","--.."];
var arr2 = ['a','b','c','d','e','f','g','h','i','j','k','l','m','n','o','p','q','r','s','t','u','v','w','x','y','z'];
var words = ["gin","zen","gig","msg"]
var mapping={};
for(let i=0;i<arr1.length;i++) {
mapping[arr1[i]]=arr2[i];
}
const res = word => word.split('').map(c => mapping[c]).join('');
var result = new Set(words.map(res)).size;
console.log(result)
For this input its working but for above array its not working can some one help me why?
var arr1 = ['z','y','x'];
var arr2 = ['a','b','c'];
var words = ['zzz','xz','zzz','yzx']
International Morse Code defines a standard encoding where each letter is mapped to a series of dots and dashes, as follows:
'a' maps to ".-",
'b' maps to "-...",
'c' maps to "-.-.", and so on.
For convenience, the full table for the 26 letters of the English alphabet is given below:
Given an array of strings words where each word can be written as a concatenation of the Morse code of each letter.
For example, "cab" can be written as "-.-..--...", which is the concatenation of "-.-.", ".-", and "-...". We will call such a concatenation the transformation of a word.
Return the number of different transformations among all words we have.
If you take a close look at the two different examples (arrays) that you include, you'll find that the transformation/translation logic is reverted:
Example that works:
var arr1 = ['z','y','x']; // <- Alphabet A
var arr2 = ['a','b','c']; // <- Alphabet B
var words = ['zzz','xz','zzz','yzx'] // <- Words in Alphabet A
Second case:
var arr1 = [".-","-...","-.-." ... ,"--.."]; // <- Alphabet A
var arr2 = ['a','b','c','d','e' ... ,'z']; // <- Alphabet B
var words = ["gin","zen","gig","msg"] // <- Words in Alphabet B
Since the translation order is different, you are not expected to see the first set of array work exactly as the second set of arrays.
Your code is expected to work one way or the other, so make sure to transform the operations accordingly.
The way to detect the point of failure, is to break the transformation process apart and console.log what's exactly happening at each state:
Bring this code:
const res = word => word.split('').map(c => mapping[c]).join('');
To this form, and carefully inspect what's happening at each step. Looking closely at the output, you'll be able to detect the breaking point and refactor accordingly:
const res = word => {
const wordSplit = word.split('');
console.log({wordSplit}); // <- Check #1: OK
const mapped = wordSplit.map(c => {
console.log(c, mapping[c]); // <- Check #2 FAIL: Problem arises at this point
return mapping[c]
});
console.log({mapped}); // <- Check #3 FAIL
const result = mapped.join('');
console.log({result}); // <- Check #4 FAIL
return result;
}
Thanks #Kostas Got the solution after your suggestion
var arr1 = [".-","-...","-.-.","-..","-","..-.","--.","....","..",".---","-.-",".-..","--","--.","---",".--.","--.-",".-....","-","..-","...-",".--","-..-","-.--","--..","--"];
var arr2 = ['a','b','c','d','e','f','g','h','i','j','k','l','m','n','o','p','q','r','s','t','u','v','w','x','y','z'];
var words = ["gin","zen","gig","msg","mgr","mgr","gig","abp","apb",'bpr']
var mapping={};
for(let i=0;i<arr1.length;i++) {
mapping[arr2[i]]=arr1[i];
}
const res = word => word.split('').map(c => mapping[c]).join('');
var result = new Set(words.map(res)).size;
console.log(result)

How to replace letters from a list in `config.json` to noting using discord.js?

I'm adding an auto-mod for swearing, I want the bot to look for any word from the list in config.json named "badwords" and delete it, which works, but if the member adds " "(space) or "_" or anything like that, it bypasses the check, so I added .replace(//s/g,'') which works for space, but for dash and other stuff, I wanted to use a list in config.json, but I can't seem to get the bot to run thru the list, there are no errors, so how can I fix this problem?
here is my code:
const config = require('../../config');
module.exports = async (client, message) => {
if (!message.guild) return;
if(!message.author.bot) {
var badwords = config.badwords;
var thingstoremove = config.thingstoremove;
for (var i = 0; i < badwords.length; i++) {
if (message.content.toLowerCase().replace(thingstoremove[8],'').includes(badwords[i])) {
message.delete()
message.reply("Watch your language!").then(m => m.delete({timeout: 10000}))
break;
}
}
}
}
config.json:
{
"badwords": ["test1", "test2", "test3", "test4", "test5"],
"thingstoremove": ["-", "_", ".", ",", "`", "~", "#", "#"]
}
Thanks.
Use this simple one-liner to get the fully replaced string
let newStr = thingstoremove.reduce((a, c) => a.replaceAll(c, ""), message.content)
And then a simple check with this:
if (badwords.some(b => newStr.includes(b))) {
message.delete()
message.reply("Watch your language!").then(m => m.delete({ timeout: 10000 }))
}
The issue is:
The code is attempting to replace an undefined value from the message, as index no. 8 is not found on the array (since the array has 8 items; and indexes start from 0 (see), so the last index would be 7), i.e. thingstoremove[8]
And also, if you are trying to remove all the characters from your
array, you need to include them in a regex — if you are going to use
.replace — instead of a single element.
Therefore, you should create a set of characters from the array on the regex, to capture any of the characters, and replace them:
const regex = new RegExp(`[${thingstoremove.join('')}]`, 'g')
And then use the regex on .replace:
if (message.content.toLowerCase().replace(regex, '').includes(badwords[i]))
Resulting code:
const config = require('../../config');
module.exports = async (client, message) => {
if (!message.guild) return;
if (!message.author.bot) {
var badwords = config.badwords;
var thingstoremove = config.thingstoremove;
const regex = new RegExp(`[${thingstoremove.join('')}]`, 'g')
console.log(regex)
for (var i = 0; i < badwords.length; i++) {
if (message.content.toLowerCase().replace(regex, '').includes(badwords[i])) {
message.delete()
message.reply("Watch your language!").then(m => m.delete({timeout: 10000}))
break;
}
}
}
}

Naming; dynamically create a variable in javascript

To avoid a javascript heap problem, I use multiple arrays: family1, family2,family3 ..., dogs1, dogs2, dogs3 ... Use example: 'family1 and dogs1', or 'family132 and dogs132' to create a new array 'results'.
How do I pass the "id" correctly
let id = 'value here'
this.family + id
this.dogs + id
So far my str itself is pushed int the new array: t-h-i-s-.-f-a-m-i-l-y-1
for (let i = +0; i < +20; i++) {
const id = 1;
let str = 'this.family'+id; // ?
let str = 'this.dogs'+id; // ?
console.log(str);
const result = {
familyType: str[i], // behavior: t-h-i-s-.-f-a-m-i-l-y-1
protocol: this.dogs1[i], // expected original behavior
};
results.push(result);
}
}
You are looking for:
let str = this['family'+id];
But this is generally a bad design pattern. Don't name your variables with incremental numbers. Use 2D arrays (i.e. arrays having arrays as values), like this.dog[id][i]. If you have "a javascript heap problem", then it is caused by some other code.

Search for multiple elements in an array

I want to retrieve inside an array all the elements who match multiple strings (all of them & not necessary words): like a search engine returning all results matching term_searched#1 && term_searched#2.
It's not a question about duplicates in the array (there's none), but about searching for a conjunction of elements: traditionally, the search is for one element, by himself or in disjunction with others (a|b|c). Just want to search (a && b && c).
I tried:
indexOf() : I can work only with one element to locate in the array.
match() : there is no AND operator in a regex expression (only | - sadly, it would be so simple). So I tried to inject these regex expressions
/(?=element1).*(?=element2)/gim
/(?=element1)(?=element2)/gim see here
The first regex expression works, but not at every time: seems very fragile...
So I don't know if I'm in the good direction (match) or if I can't figure what is the right regex expression... Need your advices.
// filter grid by searching on 'input' event
'input #search': (e)=> {
var keypressed = e.currentTarget.value;
// create array on 'space' input
var keyarr = keypressed.toLowerCase().split(" ");
// format each array's element into regex expression
var keyarrReg = [];
for(i = 0; i < keyarr.length; i++) {
var reg = '(?=' + keyarr[i] + ')';
keyarrReg.push(reg);
}
// array to regex string into '/(?=element1).*(?=element2)/gim' format
var searching = new RegExp(keyarrReg.join(".*"), 'mgi');
// set grid
var grid = new Muuri('#gridre', {
layout: {
fillGaps: true,
}
});
if (keypressed) {
// filter all grid's items (grid of items is an array)
grid.filter(function (item) {
var searchoperator = item.getElement().textContent.toLowerCase().match(searching);
// get items + only their text + lower case their text + return true (not false) in the value ('keypressed') is found in them
//var searchoperator = item.getElement().textContent.toLowerCase().indexOf(keypressed.toLowerCase()) != -1;
return searchoperator;
}
[....]
}
}
Edit with Gawil's answer adapted to my initial code (to help if needed)
// filter grid by searching on 'input' event
'input #search': (e)=> {
var keypressed = e.currentTarget.value;
// create array on 'space' input
var keyarr = keypressed.toLowerCase().split(" ");
// convert the array to a regex string, in a '^(?=.*word1)(?=.*word2).*$' format
// here is Gawil's answer, formatted by Teemu
var searching = new RegExp('^(?=.*' + keyarr.join(')(?=.*') + ').*$', 'm');
// set grid
var grid = new Muuri('#gridre', {
layout: {
fillGaps: true,
}
});
if (keypressed) {
// filter all grid's items (grid of items is an array)
grid.filter(function (item) {
// get items + only their text + lower case their text + delete space between paragraphs
var searchraw = item.getElement().textContent.toLowerCase().replace(/\r\n|\n|\r/gm,' ');
var searchoperator = searchraw.match(searching);
return searchoperator;
}
[....]
}
}
The code bellow will log each element of the array containing words cats and dogs.
It uses the regex ^(?=.*word1)(?=.*word2).*$To handle new lines, use this one instead :
^(?=(?:.|\n)*word1)(?=(?:.|\n)*word2).*$
You can add as many words as you want following the same logic, and it does not take order of the words in count.
It is very similar to what you tried, except that you have to do all (?=) checks before matching the string. Indeed, your first regex works only when the words are in the right order (element1 and then element2). Your second regex almost works, but you wrote only lookaheads, so it checks the presence of each word, but won't match anything.
var words = ["cats", "dog"]
var array = [
"this is a string",
"a string with the word cats",
"a string with the word dogs",
"a string with both words cats and dogs",
"cats rule everything",
"dogs rule cats",
"this line is for dog\nbut cats prefer this one"
]
var regexString = "^";
words.forEach(function(word) { regexString += ("(?=(?:.|\n)*"+word+")"); });
var regex = new RegExp(regexString);
array.forEach(function(str) { // Loop through the array
if(str.match(regex)) {
console.log(str); // Display if words have been found
}
});
If I've correctly understood your question, you've an array of strings, and some keywords, which have to be found from every index in the array to be accepted in the search results.
You can use a "whitelist", i.e. a regExp where the keywords are separated with |. Then iterate through the array, and on every member create an array of matches against the whitelist. Remove the duplicates from the matches array, and check, that all the keywords are in the list simply by comparing the length of the matches array to the count of the keywords. Like so:
function searchAll (arr, keywords) {
var txt = keywords.split(' '),
len = txt.length,
regex = new RegExp(txt.join('|'), 'gi'), // A pipe separated whitelist
hits; // The final results to return, an array containing the contents of the matched members
// Create an array of the rows matching all the keywords
hits = arr.filter(function (row) {
var res = row.match(regex), // An array of matched keywords
final, temp;
if (!res) {return false;}
// Remove the dups from the matches array
temp = {}; // Temporary store for the found keywords
final = res.filter(function (match) {
if (!temp[match]) {
// Add the found keyword to store, and accept the keyword to the final array
return temp[match] = true;
}
return false;
});
// Return matches count compared to keywords count to make sure all the keywords were found
return final.length === len;
});
return hits;
}
var txt = "Some text including a couple of numbers like 8 and 9. More text to retrieve, also containing some numbers 7, 8, 8, 8 and 9",
arr = txt.split('.'),
searchBut = document.getElementById('search');
searchBut.addEventListener('change', function (e) {
var hits = searchAll(arr, e.target.value);
console.log(hits);
});
<input id="search">
The advantage of the whitelist is, that you don't have to know the exact order of the keywords in the text, and the text can contain any characters.

Loop over array and compare to other array

I'm trying to create a code that will take a sentence as a param, split that sentence into an array of words and then create a loop that checks if any of theses word matches a word in some other arrays.
In the example below, I have a sentence that contains the word "ski". This means that the return value should be categories.type3.
How can I have make the loop check this? Could I have a function switching between different categories ? (ie : if a word is not in action, look in adventure and so on).
var categories = {
type1: "action",
type2: "adventure",
type3: "sport"
}
var Sentence = "This sentence contains the word ski";
var sport = ["soccer", "tennis", "Ski"];
var action = ["weapon", "explosions"];
var adventure = ["puzzle", "exploring"];
var myFreeFunc = function (Sentence) {
for (var i = 0; i < arrayLength; i++) {
if (typeArr[i] == word) {
}
}
}
You appear to want to know which categories match the sentence.
To start with, get rid of the meaningless type1 etc identifiers and re-arrange your fixed data into objects that directly represent the required data, specifically a Map of key/value pairs, where each key is a "category" name, and each value is a Set of keywords associated with that category:
var categories = new Map([
['action', new Set(['weapon', 'explosions'])],
['adventure', new Set(['puzzle', 'exploring'])],
['sport', new Set(['soccer', 'tennis', 'ski'])]
]);
[NB: Set and Map are new ES6 features. Polyfills are available]
You now have the ability to iterate over the categories map to get the list of categories, and over the contents of each category to find the key words:
function getCategories(sentence) {
var result = new Set();
var words = new Set(sentence.toLowerCase().split(/\b/g)); /* "/b" for word boundary */
categories.forEach(function(wordset, category) {
wordset.forEach(function(word) {
if (words.has(word)) {
result.add(category);
}
});
});
return result.values(); // NB: Iterator interface
}
NB: I've avoided for .. of because it's not possible to polyfill that, whereas Set.prototype.forEach and Map.prototype.forEach can be.
I would rewrite the code (you should always combine var statements).
I've added a small fiddle snippet, how i would rewrite the function. Just as an example, how you could iterate your data. Of course you should check out the other posts to optimise this code snipped ( e.g. fix for multiple spaces! ).
// make sure, your dictionary contains lower case words
var categories = {
action: ["soccer", "tennis", "ski"],
adventure: ["weapon", "explosions"],
sport: ["puzzle", "exploring"]
}
var myFreeFunc = function myFreeFunc(Sentence) {
// iterates over all keys on the categories object
for (var key in categories) {
// convert the sentence to lower case and split it on spaces
var words = Sentence.toLowerCase().split(' ');
// iterates the positions of the words-array
for (var wordIdx in words)
{
// output debug infos
console.log('test:', words[wordIdx], categories[key], categories[key].indexOf(words[wordIdx]) != -1, '('+categories[key].indexOf(words[wordIdx])+')');
// lets the array function 'indexOf' check for the word on position wordIdx in the words-array
if (categories[key].indexOf(words[wordIdx]) != -1 ) {
// output the found key
console.log('found', key);
// return the found key and stop searching by leaving the function
return key;
}
}//-for words
}//-for categories
// nothing found while iterating categories with all words
return null;
}
stripped down the function part snippet (no comments, no extra spaces, no console.log):
var myFreeFunc = function myFreeFunc(Sentence) {
for (var key in categories) {
var words = Sentence.toLowerCase().split(' ');
for (var wordIdx in words)
{
if (categories[key].indexOf(words[wordIdx]) != -1 ) {
return key;
}
}
}
return null;
}
Accumulated the topics covered in the comments
check if the Object really owns the property: obj.hasOwnProperty(prop)
split string by word bounds, as mentioned by Alnitak (using RegExp): /\b/g
collecting categories for multiple matching
Snippet:
var myFreeFunc = function myFreeFunc(Sentence) {
var result = []; // collection of results.
for (var key in categories) {
if (categories.hasOwnProperty(key)) { // check if it really is an owned key
var words = Sentence.toLowerCase().split(/\b/g); // splitting on word bounds
for (var wordIdx in words)
{
if (categories[key].indexOf(words[wordIdx]) != -1 ) {
result.push(key);
}
}
}
}
return result;
}
One simple way would be to do like this :
function determineCategory(word){
var dictionnary = {
// I assume here you don't need category1 and such
action: ["weapon", "explosions"],
aventure: ["puzzle", "exploring"],
sport: ["soccer", "tennis", "ski"]
}
var categories = Object.keys(dictionnary);
for(var i = 0; i<categories.length; i++){
for(var j = 0; j<categories[i].length;j++){
var wordCompared = dictionnary[categories[i]][j];
if(wordCompared == word){
return categories[i];
}
}
}
return "not found";
}
var sentence = "This sentence contains the word ski";
var words = sentence.split(" "); // simple separation into words
var result = [];
for(var i=0; i<words.length; i++){
result[i] = determineCategory(words[i]);
}
A few notes on this approach :
it needs you to change your existing structure (I don't know if its possible)
it doesn't do much for your sentence splitting (just using the white space). For more clever approach, see Alnitak's answer, or look for tokenization/lemmatization methods.
it is up to you to determine what to do when a word doesn't belong to a category (right now, it just stores "not found".

Categories