JavaScript: Finding successive matches with .exec() - javascript

I have an object with a myriad of properties, such as color and brand, that describes a product. I'm looking for a way to dynamically generate product descriptions in paragraph form (because API doesn't provide one), and I came up with a way to do so by writing "templates" that have "props" surrounded in brackets {{}}. I wrote a function to "parse" the template by injecting the object properties in the string by replacing the "props" with the value of the key.
For example:
Object: {color: 'white'}
Template: "The bowl is {{color}}."
Result: "The bowl is white."
For some reason, my parse function isn't working. {{general_description}} isn't parsed.
var obj = {
brand: "Oneida",
general_description: "Plate",
material: "China",
color: "Bone White",
product_width: "5\""
};
const templatePropRe = /{{(\w*)}}/g;
const parse = (template) => {
while ((result = templatePropRe.exec(template)) !== null) {
let match = result[0],
key = result[1];
template = template.replace(match, obj[key]);
}
return template;
}
console.log(parse('This {{color}}, {{material}} {{general_description}} supplied by {{brand}} has a width of {{product_width}}.'));
I followed the example provided in the MDN docs under Examples > Finding successive matches. It says that I need to first store the regular expression in a variable (e.g., templatePropRe), for the expression cannot be in the while loop condition or it will loop indefinitely. However, if I do that, my problem is resolved. See here...nothing broke.
I rewrote the function using String.prototype.match, and it works as expected, but I don't have access to the capture so I need to first strip off the brackets using stripBrackets. See the working example using match here.
What I want to know is why doesn't my parse() function that utilizes RegExp.prototype.exec work properly?

Remove the /g flag from your regex. According to the documentation, when this flag is present, it updates the regex object's lastIndex property which indicates from where next call to exec() will start to search a match.
var obj = {
brand: "Oneida",
general_description: "Plate",
material: "China",
color: "Bone White",
product_width: "5\""
};
const templatePropRe = /{{(\w*)}}/;
const parse = (template) => {
while ((result = templatePropRe.exec(template)) !== null) {
let match = result[0],
key = result[1];
template = template.replace(match, obj[key]);
}
return template;
}
console.log(parse('This {{color}}, {{material}} {{general_description}} supplied by {{brand}} has a width of {{product_width}}.'));

This happened because you modify and check the same string in your code.
Whereas regExp saves index of matched substring after each execution you change length of the string and regEx with next execution starts from other point than you expect.
var obj = {
brand: "Oneida",
general_description: "Plate",
material: "China",
color: "Bone White",
product_width: "5\""
};
const templatePropRe = /{{(\w*)}}/g;
const parse = (template) => {
var resultStr = template;
while ((result = templatePropRe.exec(template)) !== null) {
let match = result[0],
key = result[1];
resultStr = resultStr.replace(match, obj[key]);
}
return resultStr;
}
console.log(parse('This {{color}}, {{material}} {{general_description}} supplied by {{brand}} has a width of {{product_width}}.'));

Instead of performing the 2-step replacement (finding a match and then replacing the first occurrence with the required value) (that is prone with issues like the one you encountered when a new string is passed to the same RegExp with old, already invalid, index), you may use a callback method as a replacement argument inside a String#replace method. That way, the resulting string will be constructed on the fly upon each match making the code execute faster.
See an example fix below:
var obj = {
brand: "Oneida",
general_description: "Plate",
material: "China",
color: "Bone White",
product_width: "5\""
};
const parse = (template) => {
return template.replace(/{{(\w*)}}/g, ($0, $1) => obj[$1] ? obj[$1] : $0 );
// ES5 way:
// return template.replace(/{{(\w*)}}/g, function($0, $1) {
// return obj[$1] ? obj[$1] : $0;
// });
}
console.log(parse('{{keep}} This {{color}}, {{material}} {{general_description}} supplied by {{brand}} has a width of {{product_width}}.'));
Note that here, after finding a match, the ($0, $1) => obj[$1] ? obj[$1] : $0 code does the following: the whole match is assigned to $0 variable and the Group 1 value is assigned to $1; then, if there is a key with the name $1 in obj, the value will be put instead of the match into the right place in the resulting string. Else, the whole match is put back (replace with '' if you want to remove a {{...}} with a non-existent key name).

Related

ReplaceAll not replacing all in recursive function

I'm using a recursive function that takes a template string like this:
<div class='${classes}'>
and replaces the template literal placeholder ${classes} with valued from an object that looks like this:
{ classes: 'bg-${color} text-${color}', color: 'blue' }
Something to note here is that within the object, there is more template string nesting, hence the reason for the recursive function.
The thing I cannot work out is why the replace all function is only replacing the 1st instance of ${color}, and leaving the second undefined.
Working snippet below:
function buildTemplate(string) {
var matched = false;
string = string.replaceAll(/\$\{(.*?)\}/g, function (match, key) {
if (match) {
matched = true;
}
const selection = selectors[key];
delete selectors[key];
return selection;
});
if (matched) {
string = buildTemplate(string);
}
console.log(string);
}
let templateString = "<div class='${classes}'>";
const selectors = { classes: 'bg-${color} text-${color}', color: 'blue' }
buildTemplate(templateString);
You are deleting selectors, when you need it twice.
Remove line;
// delete selectors[key]
or selection get assigned nothing
No need to delete anything when it's an object that is accessed through a key.

How to get value of URL query parameter with dynamic name?

I need to extract the value of a query parameter in a URL, but the parameter changes on each page.
For example, I want to get the color variable, but it always changes based on the productID. In this case it is 'dwvar_2000440926_color' but for another product it will be 'dwvar_545240926_color'. _color stays consistent, so I'd like to key off that:
https://www.example.com/us/2000440926.html?dwvar_2000440926_color=02
Thanks!
Basic regular expression would work
const myURL = new URL("https://www.example.com/us/2000440926.html?dwvar_2000440926_color=02")
console.log(myURL.search.match(/_color=([^&]+)/)[1]);
// more specfic
console.log(myURL.search.match(/dwvar_\d+_color=([^&]+)/)[1]);
You should use regex. Based on the description of the URL behavior you described you could do something like this:
const url = new URL("https://www.example.com/us/2000440926.html?dwvar_2000440926_color=02");
// Now url.search contains your query parameters.
// We gonna apply the regex on it to capturing the color id
let matches = url.search.match(/dwvar_\d+_color=(\d+)/)
// `matches` now contains the captured groups
console.log(matches[1])
// log : 02
Assuming that 1) you want to do this on the client side 2) the color param always begins with dwvar as shown in your example and 3) that there is never more than one dwvar param, you can use the following javascript:
let searchParams = new URLSearchParams(document.location.search);
searchParams.forEach((param_value, param_name) => {
if (param_name.indexOf('dwvar') == 0) {
console.log(param_value)
}
})
window.location.search.slice(1).split('&').reduce((acc, it) => {
const [key, val] = it.split('=');
return {
...acc,
[key]: val,
};
}, {});

Generating an array out of a string seperated by commas and surrounded by parenthesis, in Javascript

I'm trying to parse a string into an array in Javascript under certain conditions:
Each comma in the string seperates different array elements
Elements surrounded by parenthesis are a sub array of the element
preceding them
Example:
total, limit, items(added_at, added_by)
will turn into
[total, limit, items[added_at, added_by]]
More information on why I'm doing this:
I'm trying to replicate spotify API's limit fields logic using Mongoose and MongoDB
to get just the total number of tracks and the request limit:
fields=total,limit
A dot separator can be used to specify non-reoccurring fields, while parentheses can be used to specify reoccurring fields within objects. For example, to get just the added date and user ID of the adder:
fields=items(added_at,added_by.id)
Use multiple parentheses to drill down into nested objects, for example:
fields=items(track(name,href,album(name,href)))
Link: https://developer.spotify.com/documentation/web-api/reference-beta/#endpoint-get-playlists-tracks
Here's one way of doing it by chunking the string into tokens and stack operators:
function parseNestedList(str) {
let pattern = /([^(),]*)([(),]?)/gm;
let part, ret = [], stack = [], context = ret;
while (((part = pattern.exec(str)) !== null) && (part[0].length > 0)) {
if (part[1].length) { context.push(part[1]); } // Push token
switch (part[2]) {
case "(": // Descend
stack.push(context);
context.push(context = []);
break;
case ",": // Next
continue;
default: // Ascend
context = stack.pop();
}
}
return ret;
}

How do I get a constructor value to other methods to be modified?

The goal of this project is to refactor a previous solution to work with actual objects. Currently when I run the Jasmine tests I get these two errors:
TypeError: Cannot read property 'split' of undefined
TypeError: Cannot set property 'title' of undefined
Why is the class not recognizing the title value when I attempt to pass it into other methods? Prior to me trying to send the value into other methods it seemed to work but now that I am trying to send the string value to the titleCreator method it keeps on returning undefined.
class bookTitle {
constructor(title) {
this.title = this.titleCreator(title); // this sets a title value to the bookTitle object/class
}
titleCreator(string) {
// Note that this isn't meant to be a fully fledged title creator, just designed to pass these specific tests
var littleWords = ["and", "over", "the"]; // These are the words that we don't want to capitalize
var modifiedString = this.string
.split(' ') // Splits string into array of words, basically breaks up the sentence
.map(function(word,index) {
if (index == 0) {
return capitalize(word); // capitalize the first word of the string
} else if (littleWords.indexOf(word) == -1) {
return capitalize(word); // capitalize any words that are not little, the -1 is returned by indexOf if it can't find the word in the array
} else if (littleWords.indexOf(word) >= 0) {
return word; // do not capitalize as this word is in the list of littleWords
}
})
.join(' '); // Joins every element of an array into a string with a space inbetween each value. Basically you created a sentence from an array of words
return modifiedString;
}
capitalize(word) {
return word.charAt(0).toUpperCase() + word.slice(1);
// This function just capitalizes the word given to it
}
}
module.exports = {
bookTitle
}
Edit: Here are my Jasmine test cases for context. Idea of the program is just to pass these cases
var bookTitles = require ('./bookTitles.js');
describe('bookTitle', function() {
var book; // this is the object that will be passed into the test cases, returns undefined here without beforeEach
beforeEach(function() {
book = new bookTitles.bookTitle(); // creates a new book instance before each test is run
});
describe('title', function() {
it('should capitalize the first letter', function() {
book.title = 'inferno';
expect(book.title).toEqual('Inferno'); // works without capitalizing
});
it('should capitalize every word', function() {
book.title = 'stuart little';
expect(book.title).toEqual('Stuart Little');
});
describe('should capitalize every word except...', function() {
describe('articles', function() {
it('does not capitalize "the"', function() {
book.title = 'alexander the great';
expect(book.title).toEqual('Alexander the Great');
});
it('does not capitalize "a"', function() {
book.title = 'to kill a mockingbird';
expect(book.title).toEqual('To Kill a Mockingbird');
});
it('does not capitalize "an"', function() {
book.title = 'to eat an apple a day';
expect(book.title).toEqual('To Eat an Apple a Day');
});
});
it('conjunctions', function() {
book.title = 'war and peace';
expect(book.title).toEqual('War and Peace');
});
it('prepositions', function() {
book.title = 'love in the time of cholera';
expect(book.title).toEqual('Love in the Time of Cholera');
});
});
describe('should always capitalize...', function() {
it('I', function() {
book.title = 'what i wish i knew when i was 20';
expect(book.title).toEqual('What I Wish I Knew When I Was 20');
});
it('the first word', function() {
book.title = 'the man in the iron mask';
expect(book.title).toEqual('The Man in the Iron Mask');
});
});
});
});
You are trying to access this.string in this line of code:
var modifiedString = this.string
before you have set this.string to have any value. Perhaps you meant to just use string, the argument passed to titleCreator. this.string is not the same as string. Since this.string has never been assigned, it is undefined and therefore any attempt to access a method on it will fail.
It's a little hard to know exactly what your intent was, but perhaps you meant to use use string instead of this.string:
titleCreator(string) {
// Note that this isn't meant to be a fully fledged title creator, just designed to pass these specific tests
var littleWords = ["and", "over", "the"]; // These are the words that we don't want to capitalize
var modifiedString = string
.split(' ') // Splits string into array of words, basically breaks up the sentence
.map(function(word,index) {
if (index == 0) {
return capitalize(word); // capitalize the first word of the string
} else if (littleWords.indexOf(word) == -1) {
return capitalize(word); // capitalize any words that are not little, the -1 is returned by indexOf if it can't find the word in the array
} else if (littleWords.indexOf(word) >= 0) {
return word; // do not capitalize as this word is in the list of littleWords
}
})
.join(' '); // Joins every element of an array into a string with a space inbetween each value. Basically you created a sentence from an array of words
return modifiedString;
}
From your error description, it also sounds like you may have an issue with how you call bookTitle (it should be called as a constructor as in
let bk = new (yourModule.bookTitle)("some string")
If you want help with that, please show the calling code that calls that constructor so we can advise on that too.
Here's a working piece of code that I had to fix several other things in:
class bookTitle {
constructor(title) {
this.title = this.titleCreator(title); // this sets a title value to the bookTitle object/class
}
titleCreator(string) {
// Note that this isn't meant to be a fully fledged title creator, just designed to pass these specific tests
var littleWords = ["and", "over", "the"]; // These are the words that we don't want to capitalize
var self = this;
var modifiedString = string
.split(' ') // Splits string into array of words, basically breaks up the sentence
.map(function(word,index) {
if (index == 0) {
return self.capitalize(word); // capitalize the first word of the string
} else if (littleWords.indexOf(word) == -1) {
return self.capitalize(word); // capitalize any words that are not little, the -1 is returned by indexOf if it can't find the word in the array
} else if (littleWords.indexOf(word) >= 0) {
return word; // do not capitalize as this word is in the list of littleWords
}
})
.join(' '); // Joins every element of an array into a string with a space inbetween each value. Basically you created a sentence from an array of words
return modifiedString;
}
capitalize(word) {
return word.charAt(0).toUpperCase() + word.slice(1);
// This function just capitalizes the word given to it
}
}
let bookTitles = {
bookTitle: bookTitle
};
let book = new bookTitles.bookTitle("some title of the book");
console.log(book)
Things I had to fix:
Change this.string.split(...) to string.split(...).
Define self to be this.
Use self.capitalize() instead of capitalize() to call method properly (in two places)
Pass in a string when calling the constructor (your code was calling the constructor with no parameters which makes an error in your constructor). Your code requires a string be passed to the constructor.
In addition, it appears that your code thinks that just assigning to the .title property will somehow run the titleCreator() method and make the proper capitalization. It will not. Assigning to the .title property just sets that property. It doesn't run any of your methods. You could define a setter method to make it run code when you assign to the property, but it would probably make more sense to just create a setTitle() method that does what you want (calls .titleCreator() and assigns the result to .title).

How to check if word in array exists in a given string

I'm trying to create suggested tags, first i take input string while the user is typing, then check for words from a long list of words in an array, if word exists in the first array, check other arrays categories that this word falls into, then append some tag in an input.
First Problem: So far i can only check if a string contains a word, i don't know how to search an array so i can find words in a given string that match words in an array.
Second Probelm
After first word if found on keyup, any other keyup runs the script whereas i want it to wait for another second match.
CODE HERE
$(document).on('keyup','.Post_TextArea',function(){
post_val = $(this).val();
if ($(this).val().length > 5){
var string_g = 'tempo';
var web_array = ['html', 'css', 'JavaScript'];
var music_array = ['tempo', 'blues', 'rhythm'];
if (post_val.toLowerCase().indexOf(string_g) >= 0){
if ($.inArray(string_g, web_array) !== -1){
$('.tags_holder').append('<span class="Tag_Style">Web</span>');
} else if ($.inArray(string_g, music_array) !== -1){
$('.tags_holder').append('<span class="Tag_Style">Music</span>');
}
}
}
})
There are several things that you can do to solve your problem:
Create re-usable components
Ensure each function does a single task
Here's my take on the problem.
Define your configuration
We can create a taxonomy for categorizing your words. In this case, we can group the words in an array and classify them by the key.
var taxonomy = {
'web': ['html', 'css', 'javascript'],
'music': ['tempo', 'blues', 'rhythm']
};
Defined your function
Here we create two functions. What we want to achieve are:
A predicate that checks for the word contained in the taxonomy
Function that returns the classification of the words
Below is a predicate to check if a word is contained within a wordList. We can use this with our check on whether or not a word is a 'tagged' word.
function contains(wordList) {
return function(word) {
return (wordList.indexOf(word) > -1);
};
};
Below is a function that returns the list of tags of words from the input. The key to this is using the some() function, which should be more light weight than the filter() function.
function getTags(input, taxonomy) {
var words = input.match(/\w+/g);
return Object.keys(taxonomy)
.reduce(function(tags, classification) {
var keywords = taxonomy[classification];
if (words.some(contains(keywords)))
tags.push(classification);
return tags;
}, []);
};
Connect to jQuery
Now that we have the core code, we can connect this to jQuery.
$(function() {
$('#input').on('keyup', function() {
$('#tags').empty();
var tags = getTags($(this).val(), taxonomy);
tags.forEach(function(tag) {
$('#tags').append($('<li>').text(tag));
});
});
});
Working Demo
See this jsFiddle for a live demo. Note: this should work only on IE9+, as the some(), keys(), and forEach() functions are not support in IE < 9.
I suggest you don't use a set of arrays, use a "map" instead - it's slightly less memory efficient but it's trivial to implement and very computationally efficient:
var tags = {
'html': 'web', 'css': 'web', 'javascript': 'web',
'tempo': 'music', 'blues': 'music', 'rhythm': 'music'
};
function suggestTags(input) {
var result = { };
var words = input.toLowerCase().split(/\s+/);
words.forEach(function(word) {
if (word in tags) {
result[tags[word]]++;
}
}
return Object.keys(result);
};
IMHO, an important point here is that the above is independent of any DOM manipulation. You'll get cleaner code by separating your word searching from your DOM logic.
NB: some of the above uses ES5 functions - use a "shim" to support them in older browsers if required.

Categories