A better approach instead of using String replace in javascript - javascript

I am trying to do some operations for each element of an array of strings, when it matches a regexp.
I have been able to resolve this by using the replace function:
const anyRegExp = new RegExp('string[(\d)]');
const sampleArr = ['string[1].name','string[2].name' /*,...*/]
const myOp1 = (...args) => /* operations */
const myOp2 = (...args) => /* different operations */
sampleArr.forEach(key => key.replace(anyRegExp, (par1, par2, par3, par4) => {
console.log(par1, par2, par3, par4);
// prints: 'string[1]', '1', 1, 'string[1].name'
// 'string[2]', '2', 2, 'string[2].name'
if (par3 > 1) {
myOp(par1,par2,par3,par4);
} else {
myOp2(par1,par2,par3,par4);
}
}));
As you see, I am not really needing the replace operation at all. I know it does not modify my key anyway, I'd just love to know if there is any other kind of function defined in String more appropiate for this job.
I tried match, but it does not receive a function as second parameter.
Output Sample (With google chrome)
const temp1 = /Tests.MyTests\[(\d+)\]/
const temp2 = "Tests.MyTests[0].name"
temp2.match(temp1, (...args) => console.log('args',args))
outputs--> ["Tests.MyTests[0]", "0"]
temp2.replace(temp1, (...args) => console.log('args',args))
logs--> args ["Tests.MyTests[0]", "0", 0, "Tests.MyTests[0].name"]
outputs--> "undefined.name"
As it can be seen, replace, receives a function, which gives me 4 parameters, which are the ones I want to use.
match does not log anything
const temp1 = /Tests.MyTests\[(\d+)\]/
const temp2 = "Tests.MyTests[0].name"
temp2.match(temp1, (...args) => console.log('args',args));
const temp1 = /Tests.MyTests\[(\d+)\]/
const temp2 = "Tests.MyTests[0].name"
temp2.replace(temp1, (...args) => console.log('args',args))

You might be looking for
for (let key of sampleArr)
myOp(...key.match(anyRegExp)) // or anyRegExp.exec(key)
assuming that the regex always matches the whole string, not multiple times on parts of it

you can use split then join for example you want to replace any symbol characters regex with "-" then
myString.split(/[^\w-]/).join('-')

Related

Map and filter in one method?

The following code processes a list of file paths and should return only the file names (without extension) of XML files. Currently I got to this:
const filteredFiles = files
.map(f => f.match(/.*\/(.*)\.xml/)) // map to regex match with capture
.filter(v => v) // non-matches returned null and will be filtered out here
.map(m => m[1]) // map out the regex capture
I find this code quite cumbersome. Is there no way to combine the matching and filtering in a more "efficient" way? And by "efficient" I mean code-readable-efficient and not time-efficient as the input array holds 100 values at most but most of the time between 10 and 20.
This doesn't solve your need of mapping and filtering out the non matching values in one shot... but it makes one step easier by using the optional chaining operator ?.
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Optional_chaining
I also slightly changed the regex to allow the filename with no path specifed.
const files = [
'./path/to/filename_01.xml',
'non_matching_value',
'./path/to/filename_02.xml',
'./path/to/filename_03.xml',
'./path/to/filename_04.xml',
'filename_05.xml',
];
//Approach using filter over map to ignore non matching array items
//-------------------------------------------------------------------------------
const filteredFiles = files
.map(filename => filename.match(/^(.*\/)?(.*)\.xml/)?.[2])
.filter(filename => filename);
console.log(filteredFiles);
//Approach using flatMap as suggested by another user answering the same question
//-------------------------------------------------------------------------------
const filteredFiles2 = files.flatMap((f)=>{
//here you are forced to put the match result in a dedicated variable..
const match = f.match(/^(.*\/)?(.*)\.xml/);
//because you need to use it both on the condition and the positive outcome
return ( match ) ? [match[2]] : [];
});
console.log(filteredFiles2);
//Approach using flatMap as suggested by another user answering the same question
//AND using the null coealeshing operator to return empty array in case of non matching string
//-------------------------------------------------------------------------------
const filteredFiles3 = files.flatMap(f => f.match(/^(.*\/)?(.*)\.xml/)?.[2] ?? []);
console.log(filteredFiles3);
You can (ab)use flat map:
const filteredFiles = files.flatMap((f)=>{
let match = f.match('...');
if (match) {
return [match[1]]
} else {
return []
}
})
Not sure if it's actually better than the original though.
Map and filter, otherwise known as reduce
const rx = /\/(.*)\.xml$/;
const filteredFiles = files.reduce((arr, f) => {
const match = f.match(rx);
return match ? [...arr, match[1]] : arr;
}, []);
As I added in comment, you could use reduce method of array to achieve this in single iteration
Example
const regex = /\/(.*)\.xml$/;
const filteredFiles = files.reduce((r, f) => {
const value = f.match(regex);
if (value?.[1]) {
return [...r, value[1]];//If matches found then return previous result + new value
}
return r; // If not matches found then return previous result
}, []);

how to match object-like pattern in a string

I have a string that looks like this:
var str='[{"id":"abcd","pin":5},{"id":"efgh","pin":6}]';
How can i match the object-like pattern in the string. I want to retrieve {"id":"abcd","pin":"5"}
I have tried this:
str.match( new RegExp('{"id":"abcd".*}'))
unfortunately, it matches till the end of the string which is not what i desire.
I just want to retrieve
{"id":"abcd","pin":"5"} by using regexp
If you do not wish to parse the JSON, you can use the following regular expression, but keep in mind that this only works for non-nested objects.
const str = '[{"id":"abcd","pin":5},{"id":"efgh","pin":6}]';
const [found] = str.match(/\{"id":"abcd"[^\}]*\}/);
if (found) {
console.log(found); // {"id":"abcd","pin":5}
}
If you want to make this a bit more dynamic, you can try the following:
const str = '[{"id":"abcd","pin":5},{"id":"efgh","pin":6}]';
const findById = (jsonArrStr, id) =>
str.match(new RegExp(`\\{"id":"${id}"[^\\}]*\\}`))?.pop();
console.log(findById(str, 'abcd')); // {"id":"abcd","pin":5}
console.log(findById(str, 'efgh')); // {"id":"efgh","pin":6}
If you need to retrieve the object, parse it, find it, and reserialize it.
const findBy = (jsonArrStr, predicate) =>
JSON.stringify(JSON.parse(jsonArrStr).find(predicate));
const str = '[{"id":"abcd","pin":5},{"id":"efgh","pin":6}]';
const found = findBy(str, ({ id }) => id === 'abcd');
console.log(found); // {"id":"abcd","pin":5}

How to handle a variable that might be an array or not in javascript inline?

I need to pass an argument, that is either an array of one type or the type on its own, to a function that requires the only argument be an array:
// myFunction throws an error if the first argument is not an array
const foo = typeOrArrayOfType => myFunction([...typeOrArrayOfType, oneDefault]);
However if I do this:
const anArray = ['oneItem', 'twoItem'];
const notAnArray = 'oneItem';
const nonIterator = 300;
const oneDefault = 20;
const foo = typeOrArrayOfType => console.log([...typeOrArrayOfType, oneDefault]);
foo(anArray) // prints array items
foo(notAnArray) // spreads the string
foo(nonIterator) // error
It either works, spreads the string into characters or breaks entirely.
How can I flexibly take arguments that may or may not be an array?
I know about Array.isArray but I don't want helper function or conditionals if possible. I don't care about nested arrays, but if they behave differently it would be worth a note about it.
Create an array only if necessary
Declare a simple helper that wraps non-array values in an array or leaves the original intact:
const toArray = data =>
Array.isArray(data)
? data
: [data];
Example:
const anArray = ['oneItem', 'twoItem'];
const notAnArray = 'oneItem';
const nonIterator = 300;
const oneDefault = 20;
const toArray = data =>
Array.isArray(data)
? data
: [data];
const foo = typeOrArrayOfType => console.log([...toArray(typeOrArrayOfType), oneDefault]);
foo(anArray) // works
foo(notAnArray) // works
foo(nonIterator) // works
Convert everything to new arrays
The above has a slight weakness - it returns the original array in some cases. Which means that mutations might affect it:
const toArray = data =>
Array.isArray(data)
? data
: [data];
function test(input) {
const arrOutput = toArray(input);
arrOutput.push("world");
return arrOutput;
}
const arrInput = ["hello"];
const output = test(arrInput);
console.log(output); // [ "hello", "world" ]
console.log(arrInput); // [ "hello", "world" ]
To handle this, you could copy every array uniformly using Array#concat() - if given an array, it will produce a new array with a copy of its contents (only one level), if given non-array it will create a new array with the argument(s) as item(s):
const toArray = data =>
[].concat(data);
Example:
const anArray = ['oneItem', 'twoItem'];
const notAnArray = 'oneItem';
const nonIterator = 300;
const oneDefault = 20;
const toArray = data =>
[].concat(data);
const foo = typeOrArrayOfType => console.log([...toArray(typeOrArrayOfType), oneDefault]);
foo(anArray) // works
foo(notAnArray) // works
foo(nonIterator) // works
Example which does not have a problem with shared arrays:
const toArray = data =>
[].concat(data);
function test(input) {
const arrOutput = toArray(input);
arrOutput.push("world");
return arrOutput;
}
const arrInput = ["hello"];
const output = test(arrInput);
console.log(output); // [ "hello", "world" ]
console.log(arrInput); // [ "hello" ]
This might be even simpler to use, since it removes the need to spread items. The .concat() method already creates a new array and accepts variable number of arguments, it can be used directly as a way to create a new array with extra items:
const anArray = ['oneItem', 'twoItem'];
const notAnArray = 'oneItem';
const nonIterator = 300;
const oneDefault = 20;
const foo = typeOrArrayOfType => console.log([].concat(typeOrArrayOfType, oneDefault));
foo(anArray) // works
foo(notAnArray) // works
foo(nonIterator) // works
Note: the ##isConcatSpreadable well-known symbol property can affect how Array#concat() works. When set to true then concatinating the object will be "flatten" similar to how arrays are. This will work on any array-like. Conversely setting the property to false will prevent .concat() from spreading the object:
//make an object string
const str = new String('bar');
//make it spreadable
str[Symbol.isConcatSpreadable] = true;
console.log([].concat(str));
//a spreadable array-like:
const arrayLike = {
0: "h", 1: "e", 2: "l", 3: "l", 4: "o",
length: 5,
[Symbol.isConcatSpreadable]: true
};
console.log([].concat(arrayLike));
//non-spredable array:
const arr = ["x", "y", "z"];
arr[Symbol.isConcatSpreadable] = false;
console.log([].concat(arr));
.as-console-wrapper { max-height: 100% !important };
You should handle the variable by testing if it's an array with Array.isArray():
e.g.
const anArray = ['oneItem', 'twoItem'];
const notAnArray = 'oneItem';
const nonIterator = 300;
const oneDefault = 20;
const foo = typeOrArrayOfType =>
console.log(Array.isArray(typeOrArrayOfType)
? [...typeOrArrayOfType, oneDefault]
: [typeOrArrayOfType, oneDefault]);
foo(anArray) // prints array items
foo(notAnArray) // spreads the string
foo(nonIterator) // error
It would take very little effort to factorise the check into a utility function, which other answers do.
You can wrap it in an array and then use .flat() before spreading*
I've not seen this anywhere else on Stack Overflow.
This means that you always end up spreading an array:
const anArray = ['oneItem', 'twoItem'];
const notAnArray = 'oneItem';
const nonIterator = 300;
const nestedArray = ['lets', ['go', 'nest', ['arrays']]];
const anIterator = new Set([1,3,5,3]);
const oneDefault = 20;
const foo = typeOrArrayOfType => console.log([...[typeOrArrayOfType].flat(), oneDefault]);
foo(anArray) // nested array items is flattened one level
foo(notAnArray) // wrapped string doesn't get flattened
foo(nonIterator) // wrapped number doesn't get flattened
foo(nestedArray) // only flattens one level, added to show how .flat(works)
foo(anIterator) // this does not work, and results in {}
N.B. Using .flat() only goes down one level by default (unlike, say lodash.flattenDeep or some other libraries). You could optionally supply a number, but that is out of the scope of the question.
const foo = (typeOrArrayOfType, depth=1) => console.log([...[typeOrArrayOfType].flat(depth), oneDefault]);
And not useful for the situation in the question.
* As an aside, some of the other answers use [].concat(variable) which to me is as clear or slightly less clear, but is somewhat more performant, as per this jsbenchmark without spreading or with spreading.

Using trim more than once in an array JS

I have a snippet of code where I am trying to parse a longer string with special characters into an array with no spaces or special characters.
input: name: this is some stuff, name2: this is more stuff
desired output: [name,this is some stuff,name2,this is more stuff]
current output: z.trim isn't a function
function parseOrder(custOrder) {
const custOrderArr = custOrder.split(',');
const trimedArr = custOrderArr.map((x) => x.trim());
const numberArr = trimedArr.map((y) => y.split(':'));
const processArr = numberArr.map((z) => z.trim());
console.log(processArr);
}
Why does trim work the first time and not the second?
You can not trim an array. But you could map the array and trim the values.
This result features Array#flatMap for preventing arrays with pairs.
function parseOrder(custOrder) {
return custOrder
.split(',')
.flatMap(y => y.split(':').map(x => x.trim()));
}
var input = 'name: this is some stuff, name2: this is more stuff ';
console.log(parseOrder(input));
Try to split by two signs, then trim your elements:
const result = str.split(/[\:,]+/).map(s => s.trim());
An example:
let str = 'test: It is me, test2: it is me 2 ';
console.log(str.split(/[\:,]+/).map(s => s.trim()));

Camelcase string to normal string

How to change NoOfSAP => No Of SAP? I have trying the replace method but it says undefined.
function unCamlelCase(result) {
return result.key.replace(/([^A-Z]*)([A-Z]*)([A-Z])([^A-Z]*)/g, '$1 $2 $3$4')
.replace(/ +/g, ' ');
};
How can I change the result key values camel case to normal string?
if (exactMatch) {
const { ...response } = json[0];
const result = Object.keys(response).reduce((acc, key) => {let newKey = key.charAt(0).toUpperCase() + key.slice(1);
return acc;
}, {});
You could use the following expression with a few helper methods to clean up the output:
"NoOfSAP".split(/([A-Z][a-z]+)/).filter(Boolean).join(' ');
This will match all upper case letters followed by one or more lower-case letters and split each chunk into an array. .filter(Boolean) is then used to remove any empty strings in the array and .join is then used to add spaces between the strings in the array.
See example below:
const getWords = wrd =>
wrd.split(/([A-Z][a-z]+)/).filter(Boolean).join(' ');
console.log(getWords("NoOfSAP")); // No Of SAP
console.log(getWords("ThisIsAWord")); // This Is A Word (notice how it will split individual letters such as A)
console.log(getWords("IAmAHuman")); // I Am A Human
console.log(getWords("JSIsGreat")); // JS Is Great (notice how it understands JS and Is are two seperate words and doesn't give JSI s Great)
As per your question about changing the keys in your object to the "uncamelcased" keys you can use .map with Object.keys to generate your result:
const getWords = wrd =>
wrd.split(/([A-Z][a-z]+)/).filter(Boolean).join(' ');
const obj = {
"NoOfSAP": 1,
"NoOfBUN": 2,
"NoOfBRE": 3,
"NoOfPEA": 4
}
const result = Object.keys(obj).map(getWords);
console.log(result);
You can go that way:
const camelToWords = (camelCaseWord) => camelCaseWord
.replace(/([A-Z]+)/g, " $1")
.replace(/([A-Z][a-z])/g, "$1");
There is also possibility to use existing libraries like lodash:
const _ = require('lodash');
console.log(_.startCase('abcDef'));
// result: Abc Def

Categories