0

Update:

Since this has been closed without an answer I just want to make clear what that answer was thanks to a user below, and also provide the correct code in case anyone wants to use this.

Solution:

Per user @Bergi, return cannot be used within Array.forEach(). Changing this to for...of solved the issue, though it was not immediately apparent to me, because I may have introduced another error into the code in the process of troubleshooting this.

Fixed Code:

The fixed code for this helper function, in case anyone finds this and wants to use it, is:

const makeConditional = (val, def = (val)=> val, ...args) => {
  const conds = args.map(arg => {
        const c = new Function('i','return i' + arg.c);
        return {
            c: c,
            f: arg.f
        }
    });

    if (conds.length) {
        for (let cond of conds) {
            if (cond.c(val)) { return cond.f(val); }
        }
        return def(val);
    } else {
        return def(val);
    }
}

makeConditional(2, val=>val+' is not a number', {c:'%2===0', f:val=>val+' is even'},{c:'%2===1', f:val=>val+' is odd'});
// returns '2 is even'

makeConditional(3, val=>val+' is not a number', {c:'%2===0', f:val=>val+' is even'},{c:'%2===1', f:val=>val+' is odd'})
// returns '3 is odd'

makeConditional('test', val=>val+' is not a number', {c:'%2===0', f:val=>val+' is even'},{c:'%2===1', f:val=>val+' is odd'})
// returns 'test is not a number'

makeConditional(49, val=>val+' doesn\'t match any criteria', {c:'===0', f:val=>val+' is zero'},{c:'===1', f:val=>val+' is one'},{c:'%2===0', f:val=>val+' is a multiple of 2'},{c:'%3===0', f:val=>val+' is a multiple of 3'},{c:'%5===0', f:val=>val+' is a multiple of 5'},{c:'%7===0', f:val=>val+' is a multiple of 7'})
// returns '49 is a multiple of 7'

Epilogue:

I ended up deciding against using the new Function() constructor, even though I think it's slick to be able to pass conditional strings into the function, because passing a function allows for more flexibility. And it simplifies the code a lot. See below, for a more complex condition and function passed:

const makeConditional = (val, def = (val)=> val, ...args) => {
    if (args.length) {
        for (let arg of args) {
            if (arg.c(val)) { return arg.f(val); }
        }
        return def(val);
    } else {
        return def(val);
    }
}

const iso8601Format = /^([0-9]{4})-?([01][0-9])-?([0-3][0-9])T([0-2][0-9]):?([0-5][0-9]):?([0-6][0-9])\.?(0{1,6})?(?:Z|(([+-])([01][0-9]):?([0-5][0-9])?))$/;

makeConditional(
  '2020-12-09T15:20:30.000Z',
  (val) => `${val} is not a valid ISO-8601 date.`,
  {
    c: (i) => iso8601Format.test(i),
    f: (val) => {
      let [, year, month, day, hours, min, sec, fracSec, offset, offsetSign, offsetHours, offsetMin] = iso8601Format.exec(val) || [];
      return `${+day}/${+month}/${year}, at ${+hours}:${min}`;
    }
  }
);
// returns '9/12/2020, at 15:20'

Original Post:

I am trying to write a function that creates custom conditional tests based on the args I put in, a helper function ultimately.

I started with this test, which works great:

const makeConditional = (val, case1, case2, def) => {
    const c1 = new Function('i','return i'+case1.c);
    const c2 = new Function('i','return i'+case2.c);
    
    if (c1(val)) {
        return case1.f(val);
    } else if (c2(val)) {
        return case2.f(val)
    } else { 
        return def(val);
    }
}

makeConditional(2, {c:'%2===0', f:val=>val+' is even'},{c:'%2===1', f:val=>val+' is odd'}, val=>val+' is not a number');
// returns '2 is even'

As you can see, I pass test value, and then two objects, which consist of a conditional string that the test value will be tested against, and the functions to perform if the tests are successful, and finally a default function. I use the "new Function()" constructor here, because the only other way to evaluate a string condition is with, eval(), which is is bad practice, and I wanted the 'purity' of passing in a pure string condition, as opposed to something like (i)=>i%2===0, even though that'd work very well. (this is my fallback option). This is something only I will be using, so I could have gone with eval(), but I just think this is somehow nicer...

OK, so having seen I could get this to work quite easily, I then tried to extrapolate this into letting the user (aka, me) pass as many conditions as I wanted, with the rest operator.

const makeConditional = (val, def = (val) => val, ...args) => {
  const conds = args.map(arg => {
    const c = new Function('i','return i' + arg.c);
    return {
      c: c,
      f: arg.f
    }
  });

  if (conds.length) {
    conds.forEach(cond => {
      if (cond.c(val)) { return cond.f(val); }
    });
    return def(val);
  } else {
    return def(val);
  }
}

makeConditional(2, {c:'%2===0', f:val=>val+' is even'},{c:'%2===1', f:val=>val+' is odd'}, val=>val+' is not a number');
// returns '2 is not a number, because conditional functions are never properly constructed, causing the function to return the default function passed'

So you can see here I put the 'default' function first, so I can use the rest operator with ...args to the map each argument to a new array that constructs a conditional function from the string 'c' in each given condition/case object, and then I loop through this new array, if it exists, returning if the given value meets the condition, or continuing the loop, and if no conditions match, or no arguments are given, the default/else function executes. The default/else arg is also specified with a default function in case none is passed, which will just return the passed value.

However, this doesn't work. Investigating what was going on I found that each 'c' conditional function (created by the new Function constructor) was not being created properly, as in the first test example. Instead, Javascript seems to only accept the first argument, 'i', and not the second, in the constructor, and I believe this must have something to do with scope, but I don't understand why. I tried refactoring the second function above to use a for...of loop instead of Array.map(), and got the same result. It really seems to be the case that you cannot use the new Function() constructor in any kind of loop or array iteration function.

I would really like to do get this working using the new Function() constructor, instead of passing in a fully-formed conditional function, so please let me know if this is going to be possible. If not I will just do it the other way.

Thanks for your time.

  • The problem is not with the `new Function` constructor at all. It's just that you cannot use `return` within a `forEach` callback. Use a proper `for … of` loop instead. – Bergi Dec 10 '20 at 18:41
  • So that's a lot to read. And I'll admit I skimmed it. However, your first snippet is trying to access `case1.cond`, but the objects you pass in have a `c` property, not a `cond` property – Taplar Dec 10 '20 at 18:41
  • @Bergi - I know it is the problem, because when I console log it even before the forEach, I can see that new Function is not creating the conditional function correctly as in the first example. In fact, it never even gets to the return in the forEach, though thanks for pointing that out... I will fix that but it doesn't solve the problem. Also as I mentioned in the post, I also tried this with "for..of" and it didn't work. – Avana Vana Dec 10 '20 at 18:46
  • @Taplar the first works fine, I just edited the var names in this post and forgot to change them everywhere equally. – Avana Vana Dec 10 '20 at 18:47
  • It's important that you show us exactly the code that you are having an error with. It doesn't do anyone any good to track down issues in logic that isn't being used. – Taplar Dec 10 '20 at 18:48
  • @Taplar if you had not just skimmed, you'll see that I'm not asking about the first snippet. It's merely there to show that in fact the idea works, and there is a problem with using the new Function() constructor in either a for...of loop or Array.map(). I think I made that abundantly clear in the title. – Avana Vana Dec 10 '20 at 18:50
  • @Bergi here you can see what's going on, and I fixed the forEach() https://i.imgur.com/MDcxlu8.png And the first snippet works fine https://i.imgur.com/Fl4q1S5.png – Avana Vana Dec 10 '20 at 18:57
  • 1
    @Bergi, sorry, I linked the wrong image. https://i.imgur.com/l7sqeRu.png is the correct one. Notice that the anonymous function in conds.c is missing its entire body. Whereas in the other function, if i log it, you can see the function body. (i have made it part of an object before logging, so both are in the same situation for comparison) https://i.imgur.com/kMAM6A4.png EDIT - mea culpa, i must have inadvertently added another error into the code when I changed it from forEach() to for...of per your recommendation. It does work. Thanks very much for your tip. – Avana Vana Dec 10 '20 at 19:33

0 Answers0