First of all, there is no need to use a Map object for the regexMap
. Reason is that maps only make sense when you are going to look up keys to get values. The structure here represents a regex rule which has a regex to test with and a replacement when the regex is matched. If this is represented as a map, it would never be used to lookup by the regex as the entire data structure needs to be traversed to get the matching rules. Also, crucially, more than one rule can match.
A more sensible approach is to directly have a list of regex rules:
const regexRules = [
{ regex: "valOne\\d{6}", replacement: 'replacementValue1' },
{ regex: "valTwo\\d{4}", replacement: 'replacementValue2' },
];
or directly use regex literals:
const regexRules = [
{ regex: /valOne\d{6}/, replacement: 'replacementValue1' },
{ regex: /valTwo\d{4}/, replacement: 'replacementValue2' },
];
Note that strings need extra escaping of backslashes. See Why do regex constructors need to be double escaped?
With this the a simple replacement engine can be defined as
const runRules = value => {
let result = value;
//replace anything that matches the `regex` part of the rule
//with the `replacement` part of the rule
for(const { regex, replacement } of regexRules)
result = result.replaceAll(new RegExp(regex, "g"), replacement);
return result;
};
Note that this explicitly creates a new regex object for each rule. It works with both a string as regex (when backslashes are properly escaped) as well as regex literals. The constructor will clone the regex pattern and force the global flag for the regular expression in order to replace all occurences. The cloning is required even if the regex pattern is defined with the g
flag like { regex: /valOne\d{6}/g, /* .... */ }
because global regular expression objects are stateful and re-using them between matches leads to incorrect output. See Why does a RegExp with global flag give wrong results?
The rest of the job can be done by defining a function that applies all rules against a subset of keys:
const exchangeValues = (keysToCheck, obj) => {
//only run over the supplied keys
for(const key of keysToCheck) {
//make sure the value can be replaced
if (key in obj && typeof obj[key] === "string")
obj[key] = runRules(obj[key]);
}
}
With this setup, the code to process the data can look like this:
const regexRules = [
{ regex: /valOne\d{6}/, replacement: 'replacementValue1' },
{ regex: /valTwo\d{4}/, replacement: 'replacementValue2' },
];
const runRules = value => {
let result = value;
for(const { regex, replacement } of regexRules)
result = result.replaceAll(new RegExp(regex, "g"), replacement);
return result;
};
const exchangeValues = (keysToCheck, obj) => {
for(const key of keysToCheck) {
if (key in obj && typeof obj[key] === "string")
obj[key] = runRules(obj[key]);
}
}
output.forEach(obj => exchangeValues(["key3"], obj));
console.log(output);
.as-console-wrapper { max-height: 100% !important; }
<script>
//setup the data
class CustomObj {
constructor({key1, key2, key3}) {
this.key1 = key1;
this.key2 = key2;
this.key3 = key3;
}
}
const output = [
new CustomObj({ 'key1': 'value1', 'key2': 'value2',
'key3': 'A sentence with valOne123456 and valTwo2345' }),
new CustomObj({ 'key1': 'value1', 'key2': 'value2',
'key3': 'A sentence with valTwo1234' }),
new CustomObj({ 'key1': 'value1', 'key2': 'value2',
'key3': 'A sentence with value3' })
];
</script>
To tidy up the code and make it more reusable, it can be defined into a library which parametrises the rules, thus can be called with different ones. For example, regexRules
can be loaded up from configuration in that case without needing to change the library code that uses them.
Further the usage can be simplified by partially applying the parameters using Function#bind
:
/* library code */
const ruleApplier = (rules, value) => {
let result = value;
for(const { regex, replacement } of regexRules)
result = result.replaceAll(new RegExp(regex, "g"), replacement);
return result;
};
const exchangeValues = (runRules, keysToCheck, obj) => {
for(const key of keysToCheck) {
if (key in obj && typeof obj[key] === "string")
obj[key] = runRules(obj[key]);
}
}
/* / library code */
const regexRules = [
{ regex: /valOne\d{6}/, replacement: 'replacementValue1' },
{ regex: /valTwo\d{4}/, replacement: 'replacementValue2' },
];
const process = exchangeValues.bind(
null,
ruleApplier.bind(null, regexRules),
["key3"],
)
output.forEach(process);
console.log(output);
.as-console-wrapper { max-height: 100% !important; }
<script>
//setup the data
class CustomObj {
constructor({key1, key2, key3}) {
this.key1 = key1;
this.key2 = key2;
this.key3 = key3;
}
}
const output = [
new CustomObj({ 'key1': 'value1', 'key2': 'value2',
'key3': 'A sentence with valOne123456 and valTwo2345' }),
new CustomObj({ 'key1': 'value1', 'key2': 'value2',
'key3': 'A sentence with valTwo1234' }),
new CustomObj({ 'key1': 'value1', 'key2': 'value2',
'key3': 'A sentence with value3' })
];
</script>
However, for .bind()
the first argument should always be passed (and null
is OK here) yet it is slightly annoying. A more fluent usage can be achieved by currying the parameters.
/* library code */
const ruleApplier = rules => value => {
let result = value;
for(const { regex, replacement } of regexRules)
result = result.replaceAll(new RegExp(regex, "g"), replacement);
return result;
};
//accept varargs of keys to check for simplicity
const exchangeValues = runRules => (...keysToCheck) => obj => {
for(const key of keysToCheck) {
if (key in obj && typeof obj[key] === "string")
obj[key] = runRules(obj[key]);
}
}
/* / library code */
const regexRules = [
{ regex: /valOne\d{6}/, replacement: 'replacementValue1' },
{ regex: /valTwo\d{4}/, replacement: 'replacementValue2' },
];
const process = exchangeValues (ruleApplier(regexRules)) ("key3");
output.forEach(process);
console.log(output);
.as-console-wrapper { max-height: 100% !important; }
<script>
//setup the data
class CustomObj {
constructor({key1, key2, key3}) {
this.key1 = key1;
this.key2 = key2;
this.key3 = key3;
}
}
const output = [
new CustomObj({ 'key1': 'value1', 'key2': 'value2',
'key3': 'A sentence with valOne123456 and valTwo2345' }),
new CustomObj({ 'key1': 'value1', 'key2': 'value2',
'key3': 'A sentence with valTwo1234' }),
new CustomObj({ 'key1': 'value1', 'key2': 'value2',
'key3': 'A sentence with value3' })
];
</script>
See: