Sam
Sam

Reputation: 855

Replace numbers fro string to object

I have two variables

var sequence = '(1 AND 2) OR (2 AND 3)'
var filters =[{
        "name": "title",
        "opr": "=",
        "value": "My Post",
        "sid": "1"
    },
    {
        "name": "content",
        "opr": "like",
        "value": "Hello",
        "sid": "2"
    },
    {
        "name": "id",
        "opr": "gt",
        "value": "100",
        "sid": "3"
    }
]

now I am looking for to replace sequence and convert it into the format like

 var output = {
        "OR": [{
                "AND": [{
                        "title": "My Post"
                    },
                    {
                        "content": {
                            "like": "Hello"
                        }
                    }
                ]
            },
            {
                "AND": [{
                        "content": {
                            "like": "Hello"
                        }
                    },
                    {
                        "id": {
                            "gt": "1000"
                        }
                    }
                ]
            }
        ]
    }
}

For Sequence String each number represent sid from array of object And for opr from array of object if opr is equals to "=" it should create simple key value pair object like

{"title": "My Post"}

and for other operators it should create like

{"content": {"like": "Hello"}}

Upvotes: 0

Views: 75

Answers (1)

Sky
Sky

Reputation: 372

You want to create a parser.

Your use case may vary and change the trade-offs that you want to make, but here is a naive implementation that accomplishes what you've outlined above.

const SEQUENCE = "(1 AND 2) OR (2 AND 3)";

const FILTERS = [{
    name: "title",
    opr: "=",
    value: "My Post",
    sid: "1"
  },
  {
    name: "content",
    opr: "like",
    value: "Hello",
    sid: "2"
  },
  {
    name: "id",
    opr: "gt",
    value: "1000",
    sid: "3"
  }
];

const result = compiler(SEQUENCE, FILTERS);

console.log(result);

function compiler(sequence, filters) {
  let tokens = _tokenizer(sequence);
  let ast = _parser(tokens);
  let newAst = _transformer(ast);
  let output = _generator(newAst, filters, {});
  return output;
}

//////////////////////////
function _tokenizer(input) {
  let current = 0;
  let tokens = [];
  let expr = /^[(].*[)]$/;
  let needsParens;
  let parenCounter;

  [].forEach.call(input, function(char, index) {
    if (char === '(') parenCounter = (parenCounter == null) ? 1 : parenCounter + 1;
    if (char === ')') --parenCounter;
    if (parenCounter === 0 && index != input.length - 1) needsParens = true;
  });

  if (!expr.test(input) || needsParens) input = "(" + input + ")";

  while (current < input.length) {
    let char = input[current];

    if (char === '(') {
      tokens.push({
        type: 'paren',
        value: '(',
      });
      current++;
      continue;
    }

    if (char === ')') {
      tokens.push({
        type: 'paren',
        value: ')',
      });
      current++;
      continue;
    }

    let WHITESPACE = /\s/;
    if (WHITESPACE.test(char)) {
      current++;
      continue;
    }

    let NUMBERS = /[0-9]/;
    if (NUMBERS.test(char)) {
      let value = '';

      while (NUMBERS.test(char)) {
        value += char;
        char = input[++current];
      }

      tokens.push({
        type: 'number',
        value
      });

      continue;
    }

    let LETTERS = /[A-Z]/i;
    if (LETTERS.test(char)) {
      let value = '';

      while (LETTERS.test(char)) {
        value += char;
        char = input[++current];
      }

      tokens.push({
        type: 'name',
        value
      });

      continue;
    }

    throw new TypeError('Unknown token: ' + char);
  }

  return tokens;
}
//////////////////////////

//////////////////////////
function _parser(tokens) {
  let current = 0;

  function walk() {
    let token = tokens[current];

    if (token.type === 'number') {
      current++;

      return {
        type: 'NumberLiteral',
        value: token.value,
      };
    }

    if (token.type === 'name') {
      current++;

      return {
        type: 'NameLiteral',
        value: token.value,
      };
    }

    if (token.type === 'paren' && token.value === '(') {
      token = tokens[++current];

      let node = {
        type: 'Expression',
        params: [],
      };

      while ((token.type !== 'paren') || (token.type === 'paren' && token.value !== ')')) {
        node.params.push(walk());
        token = tokens[current];
      }

      current++;

      return node;
    }

    throw new TypeError(token.type);
  }

  let ast = {
    type: 'Program',
    body: [],
  };

  while (current < tokens.length) {
    ast.body.push(walk());
  }

  return ast;
}
//////////////////////////

//////////////////////////
function _traverser(ast, visitor) {

  function _traverseArray(array, parent) {
    array.forEach(child => {
      _traverseNode(child, parent);
    });
  }

  function _traverseNode(node, parent) {
    let methods = visitor[node.type];

    if (methods && methods.enter) methods.enter(node, parent);

    switch (node.type) {
      case 'Program':
        _traverseArray(node.body, node);
        break;

      case 'Expression':
        _traverseArray(node.params, node);
        break;

      case 'NumberLiteral':
      case 'NameLiteral':
        break;

      default:
        throw new TypeError(node.type);
    }

    if (methods && methods.exit) methods.exit(node, parent);
  }

  _traverseNode(ast, null);
}
//////////////////////////

//////////////////////////
function _transformer(ast) {

  let newAst = {
    type: 'Program',
    body: [],
  };

  ast._context = newAst.body;

  _traverser(ast, {
    NumberLiteral: {
      enter(node, parent) {
        parent._context.push({
          type: 'NumberLiteral',
          value: node.value,
        });
      },
    },

    NameLiteral: {
      enter(node, parent) {
        parent._expression.bool = node.value;
      },
    },

    Expression: {
      enter(node, parent) {
        let expression = {
          type: 'Expression',
          arguments: [],
        };

        node._expression = expression;
        node._context = expression.arguments;

        parent._context.push(expression);
      },
    }
  });

  return newAst;
}
//////////////////////////

//////////////////////////
function _generator(node, filters, map) {
  if (filters) filters.forEach(filter => map[filter.sid] = filter);
  switch (node.type) {

    case 'Program':
      return JSON.parse(_generator(node.body[0], null, map));

    case 'Expression':
      return '{"' + node.bool + '":[' + node.arguments.map(arg => _generator(arg, null, map)) + ']}';

    case 'NumberLiteral':
      let filter = map[node.value];
      if (filter.opr === '=') return '{"' + filter.name + '":"' + filter.value + '"}';
      return '{"' + filter.name + '":{"' + filter.opr + '":"' + filter.value + '"}}';

    default:
      throw new TypeError(node.type);
  }
}
//////////////////////////

This is based off of James Kyle's Super Tiny Compiler.

I've removed all of the comments for brevity and have made a decent amount of edits to fit your requirements. You should definitely take a look at that repo though if you want to understand how this works or to have any chance of debugging or extending it in the future.

From a high level, this is what is happening.

  1. The tokenizer separates special characters, whitespace, and values.
  2. The parser takes the tokens and generates an abstract syntax tree (AST).
  3. A transformer crawls the AST and modifies it for our use case.
  4. The generator compiles the desired input using the AST and filter data.

Once again, can't recommend James Kyle's Super Tiny Compiler enough.

Upvotes: 1

Related Questions