Convert JS interpolation string to function call - javascript

I'm writing some code that rips string literals out of Typescript/JavaScript source as the first stage of a localisation toolchain I have planned.
The fly in the ointment is string interpolation.
I was on the verge of writing a function to transform an interpolation string into a function call that rips the expressions and then replaces the interpolation string with a function call that takes the expressions as parameters.
const a = 5;
const b = 7;
const foo = `first value is ${a + b}, second value is ${a * b}`;
becomes
import { interpolate } from "./my-support-code";
...
const a = 5;
const b = 7;
const foo = interpolate("first value is ${0}, second value is ${1}", [a + b, a * b]);
with the interpolate function working through the array values and replacing strings generated from the ordinal position
function interpolate(template: string, expressions: Array<any>): string {
for (let i = 0; i < expressions.length; i++) {
template = template.replace("${" + i + "}", expressions[i].toString());
}
return template;
}
This will probably work (not yet tried) but it occurred to me that this is probably a thoroughly invented wheel. The question is basically is there a well-established package that does a comprehensive job of this?
I know the above doesn't localise anything. The point is to be rid of interpolation strings so the substitution mechanism can assume that all strings are simple literals. The base language string taken from the above would be "first value is ${0}, second value is ${1}" and translators would be expected to place the tokens appropriately in whatever string they produce.

If you're going to tackle this on a non-trivial sized code base, the best you can really do is:
Write a regular expression to identify common types of localization targets and identify them, probably by file + line number.
Add comments to your code in these locations using a keyword that's easy to git grep for, or even something that can be added to your editor's syntax highlighting rules. Personally I use things like // LOCALIZE.
If you're feeling ambitious, you could implement a rewriter that attempts to convert from template form to your localization's template requirements. Each conversion can be individually inspected, altered as required, and introduced. Hopefully you have test coverage to verify your code still works after this.

Related

Replacing Variables in String at Runtime

Currently I am using the following to evaluate variables that are placed in strings at runtime:
newVal = eval("`" + newVal + "`");
So if I have the string:
"Hello from channel: ${erpVars["CommandChannel"]["name"]}"
And erpVars["CommandChannel"]["name"] has value home, then the resulting string is:
Hello from channel: home
There are other objects than just erpVars that could be holding matching values for the string, but this is just one example. It's also important to note that each string could have more than one variable that needs replacing.
I am trying to achieve the same thing without using eval(), as some of the variable values come from user input.
Your case sounds super nasty (you should never ever use eval in JS! It poses a major security threat! also it looks weird that you want to replace this sort of a string) and perhaps if you told me more about where you get your inputs from and in what form, then maybe we could find together a much better solution for this. On that note, this is how I would solve your issue in its current form.
const newVal = 'Hello from channel: ${erpVars["CommandChannel"]["name"]}';
const strings = {
erpVars: {
CommandChannel: {
name: "home"
}
}
};
const vars = newVal.match(/\$\{.+?\}/g);
let result = newVal;
vars.forEach(v => {
let valuePath = '${erpVars["CommandChannel"]["name"]}'.match(/[\w\d]+/g).join('.');
result = result.replace(v, _.get(strings, valuePath));
});
console.log(result);
Note that I'm skipping here the edge scenarios, like getting a null result from the newVal.match when there are no variables in the newVal, but that's easy to handle.
Also note that over here i'm using the lodash library in _.get() (https://lodash.com/docs/4.17.15#get). It's super popular for this kind of small tasks. Of course there are really a lot of other tools that allow you to extract a value based on a property path like erpVars.CommandChannel.name that is stored in the valuePath variable, including a crazy amount of instructions that tell you how to do it yourself.

Lodash memoize key when the function being memoized accepts functions?

I have a large data collection that I frequently query by first applying a filter to it. I want to cache and reuse the results of the different filter functions I use because this part can be expensive. Here's a rough simulation of this:
const a = x => x + 1;
const b = x => x + 2;
const m = _.memoize(
f => (console.log('filtering data'), f(314159)),
f => String(f)
);
console.log(m(a));
console.log(m(b));
console.log(m(a));
console.log(m(b));
Here "a" and "b" would be the filter functions I want to use and "m" is acting on the same data each time.
How do I specify the key for the _.memoize function?
The above works but I'm using the string representation of the function which feels wrong. Is there a better way?
I'm worried this isn't safe when minification is applied. In my actual code, the "memoize" part is in one ES6 module, the "a" and "b" parts are in another module and the calls to "m" are in several different modules which import the "a" and "b" function. Will the string representation be stable across modules like this? Is converting to the string representation fast?
The only alternative I can think of is to create a string -> function dictionary so you would do calls like m("a") but JavaScript linters won't pick up if the name is wrong.
The above works but I'm using the string representation of the function which feels wrong.
Indeed.
I'm worried this isn't safe when minification is applied.
No, minification is not the problem. Different functions get minified to different code.
The problem is closures:
const addTo = x => y => x + y
const a = addTo(1),
b = addTo(2);
console.log(String(a) === String(b));
You can only reliably compare functions by their object identity. The best way would probably be to update Lodash to use an ES6 WeakMap that doesn't require any stringification.
As long as such is not available, you can use
const id = Symbol("function identity");
let count = 0;
function toMemoizeKey(fn) {
return fn[id] || (fn[id] = ++count);
}

Is it possible to create custom operators in JavaScript?

During the Math classes we learned how to define new operators. For example:
(ℝ, ∘), x ∘ y = x + 2y
This defines ∘ law. For any real numbers x and y, x ∘ y is x + 2y.
Example: 2 ∘ 2 = 2 + 4 = 6.
Is possible to define operators like this in JavaScript? I know that a function would do the job:
function foo (x, y) { return x + 2 * y; }
but I would like to have the following syntax:
var y = 2 ∘ 2; // returns 6
instead of this:
var y = foo(2, 2);
Which is the closest solution to this question?
The short answer is no. ECMAScript (the standard JS is based on) does not support operator overloading.
As an aside, in ECMAScript 7, you'll be able to overload a subset of the standard operators when designing custom value types. Here is a slide deck by language creator and Mozilla CTO Brendan Eich about the subject. This won't allow arbitary operators, however, and the overloaded meaning will only be applied to value types. <- haha that ended up not happening.
It is possible to use third party tools like sweet.js to add custom operators though that'd require an extra compilation step.
I've answered with a solution from outside JavaScript using esprima - this is changing JavaScript and extending it, it's not native.
Given the somewhat new tagged template literals feature that was added in ES6 one can create custom DSLs to handle embedded expressions such as these including different algebraic symbols.
ex. (run in stackblitz)
function math(strings, x, y) {
// NOTE: Naive approach as demonstration
const operator = strings[1].replace(/\s/gi, "");
if (operator == "∘") {
return x + 2 * y;
}
else if (operator == "^") {
return Math.pow(x, y);
}
else {
return `Unknown operator '${operator}'`;
}
}
console.log(math`${2} ∘ ${2}`)
Note that since tagged template literals don't necessarily return strings as results they can return more complex intermediate AST like structures to build up an expression that can then be further refined and then interpreted while keeping close to the domain specific language at hand. I haven't found any existing library that does this yet for Javascript but it should be an interesting and quite approachable endeavor from what it appears from what I know of tagged template literals and usage in such places as lit-html.
No. You can't do that in JS.
The closest you can have IMO is to implement your own object which has a chainable interface, aka "fluent" syntax. That way you can operate as if you were speaking out in sequence.
var eq = new YourEquationObject();
// Then operate like 1 - 2 * 3
eq.add(1).sub(2).mul(3);
Details are up to you though. Just giving out an idea.
No. JavaScript does not support operator overloading . but you can make a class method for doing this
var mathClass = function(value){
this.value = value;
}
mathClass.prototype.toLaw = function(){
return 2 * this.value;
}
var y = new mathClass(2)
2 + y.toLaw(); //2 + 2 * y
You can add pseudo-operators via methods on Number.prototype:
Object.defineProperty(Number.prototype, 'myOp', {
value: function(that) {
return this + 2 * that;
}
});
Then all of this syntax will work
alert( (2).myOp(2) )
alert( 2 .myOp(2) )
alert( 2..myOp(2) )
alert( 2.0.myOp(2) )
2.myOp(2) does not work because the period is treated as a decimal point
Read the comments below the answer.
Apparently you can't. Here is something close :
function exec(input) {
return Function(
'return ' + input.replace(/∘( *[\d.]+)/g, '+ 2 * $1') + ';'
)();
}
exec('2 ∘ 2'); // 6
Set of compiled to JS languages support custom operators.
I would highlight ReasonML (ocaml-syntax-readable-by-js-folks) and Bucklescript (ocaml-to-js-compiler) which makes custom operators look neat:
For example an operator to concatenate strings can look like:
let (>|<) = (list, seperator) => Belt.List.reduce(list, "", (a, b) => a ++ seperator ++ b);
which can then be used like:
[Styles.button, Media.Classes.atLeastTablet] >|< " "
The downside of all this is the fact it has to be written in such compiled-to-js language, and it comes with lots of pros and cons, but usually those languages have the appeal of tons of nice syntactic sugar you don't get in js/ts
The slightly longer then the short one is that Yes you can, but its a bit more involved then what you did in Math class
To extend the language with new constructs you can use a transformer like http://esprima.org/ or any of the others. You need to define your syntax, write the parser to parse your statement and finally add the actual code to do the math parts. When these parts is in place you have made a new language that works just as javascript but with the added support of the ∘ operator.
Its really not that hard to add new syntax, here is facebooks example how they add => arrow function syntax
https://github.com/facebook/jstransform/blob/master/visitors/es6-arrow-function-visitors.js
I was asking this question too, and here's my answer:
function myCustomOperation(string){
var res = String.fromCharCode(9762);
var thing = string.replace(/☢/g," + 10 + ");
thing=thing.replace(/☣/g," * 2 + ");
thing=thing.replace(/☠/g," / 3 * ");
return [eval(thing),string+" = "+eval(thing)];
};
var thing = myCustomOperation("
3 ☠ 4
");
document.write(thing[1]);
There are several tools, compilers, or language extensions providing such a possibility, and even using emojis and other stuff, that are uncommon in JavaScript itself. sweet.js is the closest one I can remember, and also another JavaScript compiled tool might be useful.

Is there any advantage of of using comma over + to concatenate primitive types in Javascript?

I have seen commas to concatenate primitive data types in Javascript and was wondering whether there was any difference in using a comma over say the + operator as well as the .concat() function?
So an example the following statement gives me abc
var value1 = a, value2 = b, value3 = c;
document.write(value1,value2,value3);
Apples and oranges. Nothing is being concatenated in your example; you are simply specifying 3 arguments to the write() function.
document.write(exp1, exp2, exp3, ...) accepts multiple parameters, and when given multiple parameters it will iterate through them all as if you called write() on each one individually.
However, the comma does have a use when evaluating expressions where it is used to process multiple expressions and returns the last one. To see that in action you need to wrap your parameters in a set of parenthesis so that it forms a single parameter:
document.write("a","b","c") // abc
document.write( ("a", "b", "c") ) // c
alert("a","b","c") // a
alert( ("a","b","c") ) // c
alert( (x=2, ++x) ) // 3
Since string concatenation is one of the haviest operations on computing, using document.write with various parameters would perform better.
See this test (it sometimes hangs in IE, so use other browser please) http://jsperf.com/document-write-vs-concatenation
Explaination:
document.write("val1", "val2", "val3");
is equivalent to
document.write("val1");
document.write("val2");
document.write("val3");
Thus, being much faster, since it doesn't concatenates the strings.
In most browsers (IE <= 8, Gecko) string concatenation using the + operator has horrible performance.
In my company, for example, we have to compose a large HTML fragment piece by piece. In this case we have something like this:
var id = 123;
var html = [];
html.push('<div id="', id, '">hello</div>');
var result = html.join('');
In the case where you have LOTS of concatenations, this is MUCH better than the following:
var id = 123;
result = '<div id="' + id + '">hello</div>';
So to answer your question - it really depends on the situation. If you are concatenating many strings together you should never use the + operator due to poor performance. But it should be fine 99% of the time where the performance gain will be so little it is just painful to try anything else. But in cases where you actually have the option to comma separate (like in the case of array.push, or document.write) then you should definately take advantage of it.

Convert string into storable variable names and values (as strings, and objects)

2015 Edit Don't do this. Be a good person and Just Use JSON.parse() :)
I am trying to take a string which contains variables and values in a javascript-like syntax, and store them in a global object (gv). My issue is just with the parsing of the string.
String (everything inside the <div>):
<div id="gv">
variableName = "variableValue,NoSpacesThough";
portal = "TheCakeIsALie";
</div>
Script (parses string above, places values into global object):
var s = (document.getElementById("gv").innerHTML).split(';');
for (var i = 0; i < s.length; i++) {
if (s[i] !== "\n" || "") {
s[i] = s[i].replace(/^\s*/gm, "");
var varName = s[i].substr(0, s[i].indexOf('=') - 1),
varValue = (s[i].substr((s[i].indexOf('"') + 1), s[i].length)).replace('"', "");
gv[varName] = varValue;
}
}
Result:
console.log(gv.variableName); //returns: variableValue,NoSpacesThough
console.log(gv.portal); //returns: TheCakeIsALie
Q: How can I modify this script to correctly store these variables:
exampleVariable = { name: "string with spaces", cake:lie };
variableName = "variableValue,NoSpacesThough";
portal = "The Cake Is A Lie";
The directly above has an object containing: A string with spaces (and "), a reference
Thanks.
Four options / thoughts / suggestions:
1. Use JSON
If you're in control of the source format, I'd recommend using JSON rather than rolling your own. Details on that page. JSON is now part of the ECMAScript (JavaScript) standard with standard methods for creating JSON strings from object graphs and vice-versa. With your example:
exampleVariable = { name: "string with spaces", cake:lie };
variableName = "variableValue,NoSpacesThough";
portal = "The Cake Is A Lie";
here's what the JSON equivalent would look like:
{
"exampleVariable": { name: "string with spaces", cake:lie },
"variableName": "variableValue,NoSpacesThough",
"portal": "The Cake Is A Lie"
}
As you can see, the only differences are:
You wrap the entire thing in curly braces ({}).
You put the "variable" names (property names) in double quotes.
You use a colon rather than an equal sign after the property name.
You use a comma rather than a semicolon to separate properties (just as in the object literal you have on your exampleVariable line).
You ensure that any string values use double, rather than single, quotes (JavaScript allows either; JSON is more restrictive). Your example uses double quotes, but I mention it just in case...
2. Pre-process it into JSON with regular expressions
If you're not in control of the source format, but it's exactly as you've shown, you could reformat it as JSON fairly easily via regular expressions, and then deserialize it with the JSON stuff. But if the format is more complicated than you've quoted, that starts getting hairy very quickly.
Here's an example (live copy) of transforming what you've quoted to JSON:
function transformToJSON(str) {
var rexSplit = /\r?\n/g,
rexTransform = /^\s*([a-zA-Z0-9_]+)\s*=\s*(.+);\s*$/g,
rexAllWhite = /\s+/g,
lines,
index,
line;
lines = str.split(rexSplit);
index = 0;
while (index < lines.length) {
line = lines[index];
if (line.replace(rexAllWhite, '').length === 0) {
// Blank line, remove it
lines.splice(index, 1);
}
else {
// Transform it
lines[index] = line.replace(rexTransform, '"$1": $2');
++index;
}
}
result = "{\n" + lines.join(",\n") + "\n}";
return result;
}
...but beware as, again, that relies on the format being exactly as you showed, and in particular it relies on each value being on a single line and any string values being in double quotes (a requirement of JSON). You'll probably need to handle complexities the above doesn't handle, but you can't do it with things like your first line var s = (document.getElementById("gv").innerHTML).split(';');, which will break lines on ; regardless of whether the ; is within quotes...
3. Actually parse it by modifying a JSON parser to support your format
If you can't change the format, and it's less precise than the examples you've quoted, you'll have to get into actual parsing; there are no shortcuts (well, no reliable ones). Actually parsing JavaScript literals (I'm assuming there are not expressions in your data, other than the assignment expression of course) isn't that bad. You could probably take a JSON parser and modify it to your needs, since it will already have nearly all the logic for literals. There are two on Crockford's github page (Crockford being the inventer of JSON), one using recursive descent and another using a state machine. Take your choice and start hacking.
4. The evil eval
I suppose I should mention eval here, although I don't recommend you use it. eval runs arbitrary JavaScript code from a string. But because it runs any code you give it, it's not a good choice for deserializing things like this, and any free variables (like the ones you've quoted) would end up being globals. Really very ugly, I mostly mention it in order to say: Don't use it. :-)

Categories

Resources