-2

I have below two variables regexMap and outputMap as below

var regexMap = new Map([
            ['valOne\d{6}', 'regexvalue1'],
            ['valTwo\d{4}', 'regexvalue2']
        ])

var outputMap = [
            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'
            })
        ]

I want to check for the value of specific key in outputMap (ex: value of key3) and if the value matches any of the keys in regexMap, replace it with the value in regexMap (ex: if value3 matches regex1 and/or regex2, replace them with regexvalue1 and/or regexvalue2 respectively )

For above example inputs, the output I want is below object

         [
            new CustomObj({
             'key1': 'value1',
             'key2': 'value2',
             'key3': 'A sentence with regexvalue1 and regexvalue2'
            }),
            new CustomObj({
             'key1': 'value1',
             'key2': 'value2',
             'key3': 'A sentence with regexvalue2'
            }),
            new CustomObj({
             'key1': 'value1',
             'key2': 'value2',
             'key3': 'A sentence with value3'
            })
        ]

How do I achieve this?

code-geek
  • 441
  • 1
  • 8
  • 22
  • 4
    thats not how map is initialized – cmgchess Apr 04 '23 at 13:36
  • 1
    In addition to what @cmgchess said - is `'regex1'` and `'regex2'` supposed to be regular expressions which you test against the values? Or just strings? Similarly, what is `'regexvalue1`' and `'regexvalue2'` supposed to be? – VLAZ Apr 04 '23 at 13:39
  • @cmgchess If the syntax is incorrect, please assume to have a list of Map object like I gave example and provide the solution as per the correct way of Map initialisation – code-geek Apr 04 '23 at 13:41
  • @VLAZ Yes they are supposed to be regular expressions – code-geek Apr 04 '23 at 13:42
  • @code-geek for example `var regexMap = new Map([['regex1', 'regexvalue1'],['regex2', 'regexvalue2']])` will be something like this .outputmap i have no idea – cmgchess Apr 04 '23 at 13:44
  • @code-geek it's still unclear what you have for `outputMap`. Is this a map with three key-value pairs `'key1'`, `'key2'`, and `'key3'`? Or three keys which are objects - in which case - what are the values? Or are the values supposed to be objects? It's not clear *what the value contains*. – VLAZ Apr 04 '23 at 13:44
  • @VLAZ outputMap is supposed to be a list of three key-value pairs. And both the key and value are going to be a String object – code-geek Apr 04 '23 at 13:47
  • @cmgchess outputMap is actually a list of key value pairs coming from the backend API call. As I cannot add the actual value, I quoted an example outputMap – code-geek Apr 04 '23 at 13:51
  • So outputMap might not actually be a "Map"? – James Apr 04 '23 at 13:57
  • @VLAZ and James I have added a picture of how outputMap will look like – code-geek Apr 04 '23 at 14:16
  • @James FYI added a picture of outputMap – code-geek Apr 04 '23 at 14:17
  • It looks like outputMap is an Array of Objects. Is it the same for regexMap? What are the key names of the elements of outputMap? – James Apr 04 '23 at 14:18
  • @James No regexMap is something I initialise in the code and i want it to be key value pairs – code-geek Apr 04 '23 at 14:23
  • @code-geek and is the `regexMap` also just a simple array of objects or is it another thing? – Luís Mestre Apr 04 '23 at 14:31
  • @LuísMestre I want to have `regexMap` as key value pairs with key being a regular expression and value being a string – code-geek Apr 04 '23 at 14:33
  • 1
    Please provide the input you have in valid JS -- we cannot guess what you intend to have with that Map. Also provide the expected output in a way that it corresponds to your sample input, and not as image, but as text. To really make input and output correspond, I guess you need to use actual regexes, not `'regexvalue1'`. Unless you mean that is actually a **string** and not a RegExp object. All this has to be clarified and a clear example where input and intended output correspond would help. – trincot Apr 04 '23 at 14:52
  • @trincot zI have edited my question and added relevant details – code-geek Apr 04 '23 at 15:08
  • Do you realise that `'valOne\d{6}'` is same as `'valOned{6}'`? -- the backslash doesn't do anything useful there. You probably want to escape it with another backslash to be meaningful. – trincot Apr 04 '23 at 15:16
  • @trincot Sorry I missed to escape the slash – code-geek Apr 04 '23 at 15:21
  • 1
    Why would `regexMap` be a map? If it's mapping regular expressions to replacement values, then being a map doesn't seem to be of any use. You cannot look up values by a key, since you need the original regex to use. And you need to traverse the whole map to find it anyway. It seems like a crutch just to define a list of two items that go together. – VLAZ Apr 04 '23 at 15:25
  • @VLAZ Please suggest an effective way to achieve this – code-geek Apr 04 '23 at 15:27

1 Answers1

2

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:

VLAZ
  • 26,331
  • 9
  • 49
  • 67