0

I would like to convert json to a string "filter" format and visa versa.

Json Format

{
  "condition": "and",
  "rules": [
    {
      "field": "Name",
      "operator": "=",
      "value": "Jack"
    },
    {
      "field": "UserId",
      "operator": "=",
      "value": 2
    },
    {
      "field": "Surname",
      "operator": "=",
      "value": "Rose"
    },
    {
      "condition": "or",
      "rules": [
        {
          "field": "Code",
          "operator": "=",
          "value": "X"
        },
        {
          "field": "Type",
          "operator": "=",
          "value": "Z"
        }
      ]
    }
  ]
};

String Filter Format

[
  ["Name","=", "Jack"],
  ["and"],
  ["UserId", "=", 2],
  ["and"],
  ["Surname","=", "Rose"]
,
["and"],
(
["Code","=","X"],
["or"],
["Type","=","Z"]
)
]

At the moment I have the following js code which outputs something that is halfway there, but I'm missing the "and" and "or" conditions which need to come between the respective fields as well as the "(" ")" brackets around to group the fields.

I need to be able to convert from json to the "filter" format and back again.

This is the code I have so far: query in this case is the json I posted above.

const query={condition:"and",rules:[{field:"Name",operator:"=",value:"Jack"},{field:"UserId",operator:"=",value:2},{field:"Surname",operator:"=",value:"Rose"},{condition:"or",rules:[{field:"Code",operator:"=",value:"X"},{field:"Type",operator:"=",value:"Z"}]}]};

const parseRule = (rule) => {
    
  if (rule.rules) {
    return rule.rules.map(r=> parseRule(r));
  }
    
    return [rule.field, rule.operator, rule.value];
};

let filterFormat = parseRule(query);

console.log(filterFormat);

I'm stuck here. I almost need something like a join for arrays where the condition can be added as an array between the other arrays, without losing the arrays like you would with a normal join.

6
  • 3
    I've created a Stack Snippet for you. This makes it easier for other user to debug and see the results. Please update the snippet if anything is wrong. Commented Jul 15, 2021 at 6:57
  • Creating your string format probably won't be too difficult. Parsing it back into a JS object will be very hard Commented Jul 15, 2021 at 7:45
  • 2
    The string format you're trying to get is not native to javascript. That means you need your own parser/renderer. Which might be some work. Which implies the question, do you want it that way for some arbitrary reason, or because it's a fixed requirement? If it's just aesthetics, sticking to json might be the better option. Commented Jul 15, 2021 at 7:46
  • @Terry they're not at the same level. Note the parentheses in the string format. Seems odd though, I would have wrapped the sub "or" condition in a group Commented Jul 15, 2021 at 7:47
  • @Phil, thanks for that spot. I had the parentheses wrong. It should be around the "or" Commented Jul 15, 2021 at 8:35

1 Answer 1

2

The part I think you already succeeded at is going from the rule object tree to the nested array format. I wrote it using 3 main functions:

  • childToArray only checks whether a node is a condition or a rule and forwards to the right parser function
  • ruleToArray takes the object and transforms it in to a tuple-like array
  • conditionToArray maps all nested children and interweaves them with the operator

For interweaving, I use a utility method that basically does this:

  (a, [1, 2, 3]) -> [1, a, 2, a, 3]
const childToArray = child => "condition" in child 
  ? conditionToArray(child)
  : ruleToArray(child);

const ruleToArray = ({ field, operator, value }) => [field, operator, value];
const conditionToArray = ({ condition, rules }) =>
  interweave(
    [ condition ],
    rules.map(childToArray)
  )

This array format might be slightly different from your requirement, but it is "lossless", which means we can translate it back in to the tree-like object!

A similar approach:

  • Check if an array represents a rule (values will be strings), or a condition (values will be arrays)
  • If it's a rule, transform the tuple to an object with named keys
  • If it's a condition, extract the condition and parse the nested arrays

To untangle our [1, a, 2, a, 3] type arrays, I wrote a utility that does:

([1, a, 2, a, 3]) -> [ a, [ 1, 2, 3] ]
const buildTree = arr => {
  const isCondition = Array.isArray(arr[0]);
  
  return isCondition
    ? conditionToObject(untangle(arr))
    : ruleToObject(arr);
}

const conditionToObject = ([ [ condition ], children ]) => ({
  condition,
  rules: children.map(buildTree)
});
const ruleToObject = ([ field, operator, value ]) => ({
  field, operator, value
});

Putting it all together with your test data:

// Transforming object trees to nested arrays
const childToArray = child => "condition" in child 
  ? conditionToArray(child)
  : ruleToArray(child);
  
const ruleToArray = ({ field, operator, value }) => [field, operator, value]
const conditionToArray = ({ condition, rules }) =>
  interweave(
    [ condition ],
    rules.map(childToArray)
  )
  
// Transforming nested arrays to object trees
const buildTree = arr => {
  const isCondition = Array.isArray(arr[0]);
  
  return isCondition
    ? conditionToObject(untangle(arr))
    : ruleToObject(arr);
}

const conditionToObject = ([ [ condition ], children ]) => ({
  condition,
  rules: children.map(buildTree)
});
const ruleToObject = ([ field, operator, value ]) => ({
  field, operator, value
});


// App:
// 1) To array
const arrayOutput = childToArray(getInput());

// 2) Back to object
const objectOutput = buildTree(arrayOutput);

// 3) Log output
console.log("Array output");
console.log(JSON.stringify(arrayOutput));
console.log("Object output");
console.log(objectOutput);

// 4) Verify equality
console.log(
  "inp obj -> arr -> out obj == inp obj:",
  JSON.stringify(objectOutput) === JSON.stringify(getInput())
);

// 5) arrayOutput with custom toString
const arrayOutputToString = arr => {
  const isCond = Array.isArray(arr[0]);
  
  return isCond
    ? `(${arr.map(arrayOutputToString)})`
    : JSON.stringify(arr)
}

console.log(
  arrayOutputToString(arrayOutput)
)


function getInput() {
  return {
    "condition": "and",
    "rules": [{
        "field": "Name",
        "operator": "=",
        "value": "Jack"
      },
      {
        "field": "UserId",
        "operator": "=",
        "value": 2
      },
      {
        "field": "Surname",
        "operator": "=",
        "value": "Rose"
      },
      {
        "condition": "or",
        "rules": [{
            "field": "Code",
            "operator": "=",
            "value": "X"
          },
          {
            "field": "Type",
            "operator": "=",
            "value": "Z"
          }
        ]
      }
    ]
  }
};
<script>
// Utils
const interweave = (y, xs) => xs.flatMap(
  (x, i) => i === xs.length - 1 ? [x] : [x, y]
);

const untangle = xs => {
  const wovenEl = xs[1];
  const els = [];
  
  for (let i = 0; i < xs.length; i += 2) {
    els.push(xs[i]);
  }
  
  return [ wovenEl, els ];
}
</script>

Sign up to request clarification or add additional context in comments.

4 Comments

The only thing missing for this solution is the parenthesis around the "or". The above solution outputs it as [["Code","=","X"],["or"],["Type","=","Z"]] where the final result should be (["Code","=","X"],["or"],["Type","=","Z"]). Other than that this solution solves all the other issues.
I added a quick custom toString function to use () around conditions. To be consistent however, I think you also need the () around the outermost part of the filter. In your example, that part has [].
what does that toString look like?
Woops, I actually did not press "Save edits" 🤦‍♂️ It's included now (in the code snippet)

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.