Inefficient/ugly replacement function - javascript

I wrote a function that is supposed to replace code in between of two delimiters with the value, it returns (The string I'm applying this to is the .outerHTML of a HTML-Object).
This will be used similar to how it is used in e.g. Vue.js or Angular.
It looks like this:
static elemSyntaxParse(elem) {
let elem = _elem.outerHTML;
if (elem.includes("{{") || elem.includes("}}")) {
let out = "";
if ((elem.match(/{{/g) || []).length === (elem.match(/}}/g) || []).length) {
let occurs = elem.split("{{"),
key,
temp;
for (let i = 1; i < occurs.length; i++) {
if (occurs[i].includes("}}")) {
key = occurs[i].substring(0, occurs[i].indexOf("}}"));
temp = eval(key) + occurs[i].substring(occurs[i].indexOf("}}") + 2);
out += temp;
} else {
ModularCore.err("Insert-Delimiters \"{{\" and \"}}\" do not match.");
break;
return elem;
}
}
return occurs[0] + out;
} else {
ModularCore.err("Insert-Delimiters \"{{\" and \"}}\" do not match.");
return elem;
}
}
return elem;
}
(The function is inside of a class and refers to some external functions.)
Example use:
<body>
<p id="test">{{ Test }}</p>
<script>
let Test = 27;
document.getElementById("test").outerHTML = elemSyntaxParse(document.getElementById("test"));
</script>
</body>
Returns this string:
<p id="test">27</p>
It works but it is rather ugly and kinda slow.
How would I go about cleaning this up a bit? I am open to ES6.
PS: I now "eval() is evil" but this is the only occurrence in my code and it is (as far as i know) not replaceable in this situation.
Thanks!

I think you can omit a few checks and end up at:
const text = elem.outerHTML.split("{{");
let result = text.shift();
for(const part of text) {
const [key, rest, overflow] = part.split("}}");
if(!key || rest == undefined || overflow) {
ModularCore.err("Insert-Delimiters \"{{\" and \"}}\" do not match.");
return elem.outerHTML;
}
result += eval(key) + rest;
}
return result;

Invert the testing logic to get rid of nesting and else clauses
if (! elem.includes("{{") || !elem.includes("}}")) {
ModularCore.err("Insert-Delimiters \"{{\" and \"}}\" do not match.");
return elem;
}
// original loop code here
Don't double check - as #Bergi comment says.
test for return values indicating "not found, etc"
// if is removed. next line...
let occurs = elem.split("{{"), key, temp;
// if the separator is not in the string,
// it returns a one-element array with the original string in it.
if(occurs[0] === elem) return "no substring found";
The above should eliminate 2 nesting levels. You can then do a similar thing in that inner for loop.
Simplify compound logic.
!a || !b is equivalent to !(a && b). This is De Morgan's law

Related

Extract Variables From String Regex

This might be a repeat question but I'm not sure how to look for the answer :P
I'm trying to extract and remove variables from a string.
The string might look like this: !text (<123456789>=<#$111111111>) (<7654312> = <#$222222222>) (🛠 =<#$3333333333>) Some text that I will need!
I need the two items in each block?
e.g. [["123456789", 111111111],['7654312','222222222'],["🛠","3333333333"]]
Then I need the string exactly but with the variables removed?
e.g. Some more text that I will need!
I'm not sure of the best way to do this, any help is appreciated.
You don't always have to use regexes, for instance why not write a parser? This gives you much more flexibility. Note that I added <> around the 🛠 for simplicity, but you could make brackets optional in the parser.
The parser assumes anything that isin't within () is free text and captures it as string nodes.
For instance if you wanted only the last text node you could do...
const endingText = parse(text).filter(t => typeof t === 'string').pop();
const text = '!text (<123456789>=<#$111111111>) (<7654312> = <#$222222222>) (<🛠> =<#$3333333333>) Some text that I will need!';
console.log(parse(text));
function parse(input) {
let i = 0, char = input[i], text = [];
const output = [];
while (char) {
if (char === '(') {
if (text.length) output.push(text.join(''));
output.push(entry());
text = [];
} else {
text.push(char);
consume();
}
}
if (text.length) output.push(text.join(''));
return output;
function entry() {
match('(');
const key = value();
whitespace();
match('=');
whitespace();
const val = value();
match(')');
return [key, val];
}
function value() {
const val = [];
match('<');
while (char && char !== '>') val.push(char), consume();
match('>');
return val.join('');
}
function whitespace() {
while (/\s/.test(char)) consume();
}
function consume() {
return char = input[++i];
}
function match(expected) {
if (char !== expected) throw new Error(`Expected '${expected}' at column ${i}.`);
consume();
}
}

A "translate" function in javascript like that in php?

Yes, I want a character "translate" function in javascript like that in php.
I made the following, but it is ghastly. Surely there must be a better way -- using regular expressions?
<html>
<head>
<script>
window.onload = function() {
"use strict";
console.log(translate("abcdefg", "bdf", "XYZ")); // gives aXcYeZg -=-=-
}
function translate(v1, xlatfrom, xlatto) {
var ch, ipos, retstr = "";
if (xlatfrom.length != xlatto.length) return ""; // lengths must be =
for (var i1=0; i1<v1.length; i1+=1) { // go through string
ch = v1.substring(i1, i1+1); // character by character
ipos = xlatfrom.indexOf(ch); // ck if in xlatfrom
if (ipos >= 0) ch = xlatto.substring(ipos, ipos+1); // if yes, replace
retstr += ch; } // build up return string
return retstr;
}
</script>
</head>
<body>
</body>
</html>
EDIT: I've accepted the #dani-sc answer. I'm not going to pursue performance. But it's so DIDACTIC! And thanks for the "spread operator" info. Here's how I might use his answer:
function translate(v1, xlatfrom, xlatto) { // like the PHP translate
var mapobj = strsToObject(xlatfrom, xlatto); // make str1.ch's:str2ch's object
return [...v1].map(ch => mapobj[ch] || ch).join(''); // ... is js "spread operator"
}
function strsToObject(str1, str2) { // make object from strings
if (str1.length != str2.length) return {}; // lengths must be =
var retobj = {};
for (var i1=0; i1<str1.length; i1+=1) { // just str[i1]: str2[i1]
retobj[str1.substring(i1, i1+1)] = str2.substring(i1, i1+1); }
return retobj;
}
or (this is GREAT! THANKS!)
function translate(v1, xlatfrom, xlatto) { // like the PHP translate
if (xlatfrom.length != xlatto.length) return ""; // lengths must be =
var mapobj = {}; // make object for mapping
for (var i1=0; i1<xlatfrom.length; i1+=1) { // just str[i1]: str2[i1]
mapobj[xlatfrom.substring(i1, i1+1)] = xlatto.substring(i1, i1+1); }
return [...v1].map(ch => mapobj[ch] || ch).join(''); // ... is js "spread operator"
}
Well, if you want, you could use regular expressions like this:
function translate(input, oldCharacters, newCharacters) {
let output = input;
const oldChArr = [...oldCharacters];
const newChArr = [...newCharacters];
for (let i = 0; i < oldChArr.length; i += 1) {
output = output.replace(new RegExp(oldChArr[i], 'g'), newChArr[i]);
}
return output;
}
function translateFixed(input, replacements) {
return input.replace(/./g, ch => replacements[ch] || ch);
}
function translateFixedNoRegEx(input, replacements) {
return [...input].map(ch => replacements[ch] || ch).join('');
}
console.log(translate("abcdefgbdb", "bdf", "XYZ"));
console.log(translate("abcdefg", "cde", "dec"));
console.log(translateFixed("abcdefg", {c: 'd', d: 'e', e: 'c'}));
console.log(translateFixedNoRegEx("abcdefg", {c: 'd', d: 'e', e: 'c'}));
If you would be okay with changing the method's signature, it could be made a bit more concise of course.
Edit: I've added two more methods which actually achieve what you're looking for. Just for reference, I left the original method translate in there as well.
translateFixed uses regular expressions to match every single character and replace it if it was specified in the replacements parameter.
translateFixedNoRegex just creates an array of characters out of the input string and iterates over them. If the character ch matches one in the replacements parameter, it's replaced, otherwise it's left unchanged. Afterwards, we'll convert it back to a string by concatenating the characters.
You asked about [...array]: It's the spread operator, introduced with ES6. When used on a string, it just takes every character and puts it as a single entry into an array. That means, these both lines are equivalent:
console.log([..."mystring"]);
console.log("mystring".split(''));
function translate(val, xlatfrom, xlatto) { //
if (xlatfrom.length !== xlatto.length) return "";
Array.from(xlatfrom).forEach((key, index) => {
val = val.replace(key, xlatto[index]);
})
return val;
}
console.log(translate("abcdefg", "bdf", "XYZ"));

JavaScript Throws Undefined Error

What it is supposed to do -
Example
url1(pages,"ALT") returns "www.xyz.ac.uk"
url1(pages,"xyz") returns ""
The error - TypeError: Cannot call method 'toUpperCase' of undefined
This is just for some coursework, Im stuck with these errors. Any help would be much appreciated
function index(string,pattern,caseSensitive) {
if(caseSensitive == false) {
var v = string.toUpperCase();
} else {
var v = string;
}
return indexNumber = v.indexOf(pattern);
}
var pages = [ "|www.lboro.ac.uk|Loughborough University offers degree programmes and world class research.", "!www.xyz.ac.uk!An alternative University" , "%www%Yet another University"];
alert(url1(pages, "ALT"));
function url1(pages,pattern) {
var siteContent = [];
for(i=0;i<pages.length;i++) {
var seperator = pages[i].charAt(0);
if(pages[i].indexOf(seperator)>0){
siteContent = pages[i].split(pages[i].indexOf(seperator));
}
if( index(siteContent[2],pattern,false)>=0){
return siteContent[1];
}else{
return "";
}
}
}
if(pages[i].indexOf(seperator)>0){
siteContent = pages[i].split(pages[i].indexOf(seperator));
}
if( index(siteContent[2],pattern,false)>=0){
return siteContent[1];
}else{
return "";
}
If pages[i].indexOf(seperator)<=0, siteContent is still whatever it was from the last iteration. If that happens on the first iteration, siteContent is still [], and siteContent[2] is undefined.
Another problem: the expression pages[i].indexOf(seperator) returns a number, and pages[i].split expects a delimiting string as an argument. Since the number doesn't appear in your input, you'll always get a single-element array, and siteContent[2] will always be undefined. Get rid of .indexOf(seperator), change it to siteContent = pages[i].split(seperator).
One more: get rid of the else { return ""; }. Add a return ""; after the for loop.
Finally, in the first if statement condition, change .indexOf(seperator) > 0 to .indexOf(seperator, 1) !== -1. Since you're getting seperator from the first character of the string, it will be found at 0. You want the second occurrence, so start the search at 1. In addition, .indexOf returns -1 if it doesn't find the substring. You'll need to account for this in both if conditions.
Side note, as this is not causing your problem: never use == false. JS will coerce stuff like 0 and "" to == false. If that's what you want, just use the ! operator, because the expression has nothing to do with the value false.
My final answer is http://jsfiddle.net/QF237/
Right here:
alert(url1(pages, ALT)); // ALT ISN'T DEFINED
I believe you forgot to quote it:
alert(url1(pages, "ALT"));
You should split the string passing the separator character itself. Your function then will look like:
function url1(pages,pattern) {
var siteContent = [];
for(i=0;i<pages.length;i++) {
var seperator = pages[i].charAt(0);
console.log(seperator);
if(pages[i].indexOf(seperator)>=0){
siteContent = pages[i].split(seperator); //fixed here
}
console.log(siteContent);
if( index(siteContent[2],pattern,false)>=0){
return siteContent[1];
}else{
return "";
}
}
}
Tell us if it worked, please.
EDIT: It seeems your index() also has a little problem. Please try the function below.
function index(string,pattern,caseSensitive) {
var v;
if(caseSensitive == false) {
v = string.toUpperCase();
pattern = pattern.toUpperCase(); //to clarify: pattern should be uppercased also if caseSensitiveness is false
} else {
v = string;
}
return v.indexOf(pattern);
}
EDIT 2:
And url1() is finally like this:
function url1(pages,pattern) {
var siteContent = [];
for(i=0;i<pages.length;i++) {
var seperator = pages[i].charAt(0);
if(pages[i].indexOf(seperator)>=0){
siteContent = pages[i].split(seperator);
}
if( index(siteContent[2],pattern,false)>=0){
return siteContent[1];
}
}
return "";
}
In this case, the first occurrence of pattern in all pages will be returned.

Javascript if value is in array else in next array

I have found a few posts on here with similar questions but not entirely the same as what I am trying. I am currently using a simple if statement that checks the data the user enters then checks to see if it starts with a number of different values. I am doing this with the following:
var value = string;
var value = value.toLowerCase();
country = "NONE";
county = "NONE";
if (value.indexOf('ba1 ') == 0 || value.indexOf('ba2 ') == 0 || value.indexOf('ba3 ') == 0) { //CHECK AVON (MAINLAND UK) UK.AVON
country = "UK";
county = "UK.AVON";
} else if(value.indexOf('lu') == 0){//CHECK BEDFORDSHIRE (MAINLAND UK) UK.BEDS
country = "UK";
county = "UK.BEDS";
}
I have about 20-30 different if, else statements that are basically checking the post code entered and finding the county associated. However some of these if statements are incredibly long so I would like to store the values inside an array and then in the if statement simply check value.indexOf() for each of the array values.
So in the above example I would have an array as follows for the statement:
var avon = new Array('ba1 ','ba 2','ba3 ');
then inside the indexOf() use each value
Would this be possible with minimal script or am I going to need to make a function for this to work? I am ideally wanting to keep the array inside the if statement instead of querying for each array value.
You can use the some Array method (though you might need to shim it for legacy environments):
var value = string.toLowerCase(),
country = "NONE",
county = "NONE";
if (['ba1 ','ba 2','ba3 '].some(function(str) {
return value.slice(0, str.length) === str;
})) {
country = "UK";
county = "UK.AVON";
}
(using a more performant How to check if a string "StartsWith" another string? implementation also)
For an even shorter condition, you might also resort to regex (anchor and alternation):
if (/^ba(1 | 2|3 )/i.test(string)) { … }
No, it doesn’t exist, but you can make a function to do just that:
function containsAny(string, substrings) {
for(var i = 0; i < substrings.length; i++) {
if(string.indexOf(substrings[i]) !== -1) {
return true;
}
}
return false;
}
Alternatively, there’s a regular expression:
/ba[123] /.test(value)
My recomendation is to rethink your approach and use regular expressions instead of indexOf.
But if you really need it, you can use the following method:
function checkStart(value, acceptableStarts){
for (var i=0; i<acceptableStarts.length; i++) {
if (value.indexOf(acceptableStarts[i]) == 0) {
return true;
}
}
return false;
}
Your previous usage turns into:
if (checkStart(value, ['ba1', ba2 ', 'ba3'])) {
country = 'UK';
}
Even better you can generalize stuff, like this:
var countryPrefixes = {
'UK' : ['ba1','ba2 ', 'ba3'],
'FR' : ['fa2','fa2']
}
for (var key in countryPrefixes) {
if (checkStart(value, countryPrefixes[key]) {
country = key;
}
}
I'd forget using hard-coded logic for this, and just use data:
var countyMapping = {
'BA1': 'UK.AVON',
'BA2': 'UK.AVON',
'BA3': 'UK.AVON',
'LU': 'UK.BEDS',
...
};
Take successive characters off the right hand side of the postcode and do a trivial lookup in the table until you get a match. Four or so lines of code ought to do it:
function getCounty(str) {
while (str.length) {
var res = countyMapping[str];
if (res !== undefined) return res;
str = str.slice(0, -1);
}
}
I'd suggest normalising your strings first to ensure that the space between the two halves of the postcode is present and in the right place.
For extra bonus points, get the table out of a database so you don't have to modify your code when Scotland gets thrown out of leaves the UK ;-)

Error in javascript recursive function

I have the following piece of code (for the present case it may be considered as a function to remove the attributes from a valid html string fed as input):
function parse(htmlStr)
{
console.log(htmlStr);
result+="<"+htmlStr.tagName.toLowerCase()+">";
var nodes=htmlStr.childNodes;
for(i=0;i<nodes.length;i++) {
var node=nodes[i];
if(node.nodeType==3) {
var text=$.trim(node.nodeValue);
if(text!=="") {
result+=text;
}
}
else if(node.nodeType==1) {
result+=parse(node);
}
}
result+="</"+htmlStr.tagName.toLowerCase()+">";
return result;
}
But it is not working as expected. For example, in the following case when I feed it the following html as input:
<div id="t2">
Hi I am
<b>
Test
</b>
</div>
it returns <div>Hi I am<div>Hi I am<b>Test</b></div>.
Also the page crashes if some large input is given to the function.
NOTE: I know there are better implementations of removing attributes from a string using jQuery, but I need to work with the above function here & also the complete code is not for removing attributes, the above is just a shortened part of the code
There is something wrong with your result variable. It is undefined and global. In each recursion you would append the same string to itself, which also makes it crashing for huge inputs. (I can't reproduce anything, it crashes right away with a Undefined variable Error)
BTW: Your argument is no htmlStr, it is a domNode. And you're not parsing anything. Please don't use wrong self-documenting variable names.
Corrected version:
function serialize(domElement) {
var tagname = domElement.tagName.toLowerCase();
var result = "<"+tagname+">";
// ^^^ ^ not a +=
var children = domElement.childNodes;
for (var i=0; i<children.length ;i++) {
// ^^^ was also missing
if (children[i].nodeType == 3) {
result += children[i].data;
} else if (children[i].nodeType == 1) {
result += serialize(children[i]);
// ^^ add a child's result here
}
}
result += "</"+tagname+">";
return result;
}
I would not use trim(), that would produce <div>Hi<b>I</b>am</div> from <div>Hi <b>I</b> am</div>. You might do something like .replace(/\s+/g, " ").
This result+=parse(node); -> In you case you shouldn't merge the result inside recursion like that..
What happens is the return result from <b> recursion call appends the existing result with returned result. Where the existing result is <div>Hi I am and the returned result is <div>Hi I am<b>Test and so at the end of recursion you have <div>Hi I am<div>Hi I am<b>Test.
var result = '';
function parse(htmlStr) {
result += "<" + htmlStr.tagName.toLowerCase() + ">";
var nodes = htmlStr.childNodes;
for (i = 0; i < nodes.length; i++) {
var node = nodes[i];
if (node.nodeType == 3) {
var text = $.trim(node.nodeValue);
if (text !== "") {
result += text;
}
} else if (node.nodeType == 1) {
parse(node);
}
}
console.log(result);
result += "</" + htmlStr.tagName.toLowerCase() + ">";
return result;
}
Fixed fiddle: http://jsfiddle.net/FBnYT/
Change
result+="<"+htmlStr.tagName.toLowerCase()+">";
to:
var result="<"+htmlStr.tagName.toLowerCase()+">";
WOrks fine in demo: http://jsfiddle.net/qtuUA/
The crash occurs because the loop control variable is not locally scoped. So in addition to the other recommended changes:
for (var i = 0; i < nodes.length; i++)
...

Categories