最新消息:雨落星辰是一个专注网站SEO优化、网站SEO诊断、搜索引擎研究、网络营销推广、网站策划运营及站长类的自媒体原创博客

javascript - Position in JSON string to path in the object - Stack Overflow

programmeradmin0浏览0评论

I have a JSON string similar to this one:

{
    "Version": "XXX",
    "Statements": [
        {...},
        {...},
        {...}
    ]
}

How can I find out which object inside Statements property is defined at character XX of the JSON string? (considering that those objects can have arbitrarily deep nesting).

For example, if I have a string

{"Version":"XXX","Statements":[{"a":1},{"b":2},{"b":3}]}
--------------------------------------------------------
123456789 123456789 123456789 123456789 123456789 123456

then character at position 36 would correspond to the first statement object, while character at position 52 would correspond to the third statement object.

I have a JSON string similar to this one:

{
    "Version": "XXX",
    "Statements": [
        {...},
        {...},
        {...}
    ]
}

How can I find out which object inside Statements property is defined at character XX of the JSON string? (considering that those objects can have arbitrarily deep nesting).

For example, if I have a string

{"Version":"XXX","Statements":[{"a":1},{"b":2},{"b":3}]}
--------------------------------------------------------
123456789 123456789 123456789 123456789 123456789 123456

then character at position 36 would correspond to the first statement object, while character at position 52 would correspond to the third statement object.

Share Improve this question edited Sep 7, 2018 at 19:11 Bharata 14.2k6 gold badges43 silver badges53 bronze badges asked Sep 1, 2018 at 2:20 MaximMaxim 1,0222 gold badges10 silver badges21 bronze badges 11
  • 5 Please provide an example. I don’t know what string you’re talking about and what X could be, or how a character relates to positions of objects. – Sebastian Simon Commented Sep 1, 2018 at 2:28
  • indexOf? Or am I oversimplifying the problem.. – mckuok Commented Sep 1, 2018 at 2:39
  • I've added an example. Thank you! – Maxim Commented Sep 1, 2018 at 2:53
  • You're looking for a custom algorithm that will (1) make a lot of assumptions (2) take a lot of time to construct. Not saying someone won't take on this challenge, but I find it too much of an endeavor for me for volunteer work. – vol7ron Commented Sep 1, 2018 at 4:57
  • @vol7ron I'm hoping that this has been solved before. I imagine any decent editor with autocomplete functionality will have to do this in a more generic way. – Maxim Commented Sep 1, 2018 at 5:59
 |  Show 6 more comments

3 Answers 3

Reset to default 6 +50

Here is some dirty solution that doesn't require external libs:

const data = '{"Version":"XXX","Statements":[{"a":1},{"b":2},{"b":3}],"some":0}';

const getValuesPositionInArray = arrayKey => data => {
  const arrayNameSeparator = `"${arrayKey}":`;
  const targetArrayIndexOf = data.indexOf(arrayNameSeparator) + arrayNameSeparator.length;
  const arrayStringWithRest = data.slice(targetArrayIndexOf, data.length);
  
  const { result } = arrayStringWithRest.split('').reduce(
    (acc, char, idx, array) => {
      if (acc.finished) return acc;
      if (!acc.processingKey && char === '[') acc.nesting += 1;
      if (!acc.processingKey && char === ']') acc.nesting -= 1;
      
      const shouldFinish = acc.nesting === 0;
      const charIsDblQuote = char === '"';
      const charBefore = array[idx - 1];
      const charAfter = array[idx + 1];
      
      acc.position += 1;
      acc.finished = shouldFinish;

      if (acc.processingKey && !charIsDblQuote) acc.processedKey += char;
      if (charIsDblQuote) acc.processingKey = !acc.processingKey;
      if (charIsDblQuote && !acc.processingKey && charAfter === ':') {
      	acc.result[acc.processedKey] = acc.position;
        acc.processedKey = '';
      }
      
      return acc;
    }, 
    { 
      finished: false, 
      processingKey: false,
      processedKey: '',
      nesting: 0,
      position: targetArrayIndexOf + 1,
      result: {}
    }
  )
  
  return result;
  
}

const result = getValuesPositionInArray('Statements')(data);

console.log(result)

But this snippet will break if target objects would contain string values.

EDIT

And below is updated snippet with string values fix and with parsed values as well:

const data = '{"Version":"XXX","Statements":[{"aa":"some"},{"b":"ano:,{ther}"},{"bb":3}],"some":0}';

const getValuesPositionInArray = arrayKey => data => {
  const arrayNameSeparator = `"${arrayKey}":`;
  const targetArrayIndexOf = data.indexOf(arrayNameSeparator) + arrayNameSeparator.length;
  const arrayStringWithRest = data.slice(targetArrayIndexOf, data.length);
  const charsAfterValue = ['}', ','];
  const charsBeforeKey = ['{', ','];

  const { result } = arrayStringWithRest.split('').reduce(
    (acc, char, idx, array) => {
      if (acc.finished) return acc;
      if (!acc.processingKey && !acc.processingValue && char === '[') acc.nesting += 1;
      if (!acc.processingKey && !acc.processingValue && char === ']') acc.nesting -= 1;

      const shouldFinish = acc.nesting === 0;
      const charIsDblQuote = char === '"';
      const charBefore = array[idx - 1];
      const charAfter = array[idx + 1];
     
      const keyProcessingStarted = (
        charIsDblQuote &&
        !acc.processingKey &&
        !acc.processingValue &&
        charsBeforeKey.includes(charBefore)
      );

      const keyProcessingFinished = (
        charAfter === ':' &&
        charIsDblQuote && 
        acc.processingKey 
      );

      const valueProcessingStarted = (
        char === ':' &&
        !acc.processingKey &&
        !acc.processingValue
      );

      const valueProcessingFinished = (
        (acc.lastProcessedValueType === String
          ? charIsDblQuote
          : true
        ) &&
        acc.processingValue &&
        charsAfterValue.includes(charAfter)
      );

      acc.position += 1;
      acc.finished = shouldFinish;

      if (acc.processingKey && !charIsDblQuote) acc.processedKey += char;
      if (acc.processingValue && !charIsDblQuote) acc.processedValue += char;
      
      if (keyProcessingStarted) {
        acc.processingKey = true;
      } else if (keyProcessingFinished) {
        acc.processingKey = false;
        acc.result[acc.processedKey] = { position: acc.position };
        acc.lastProcessedKey = acc.processedKey;
        acc.processedKey = '';
      }

      if (valueProcessingStarted) {
        acc.processingValue = true;
        acc.lastProcessedValueType = charAfter === '"' ? String : Number;
      } else if (valueProcessingFinished) {
        acc.processingValue = false;
      	acc.result[acc.lastProcessedKey].value = (
          acc.lastProcessedValueType(acc.processedValue)
        );
        acc.processedValue = '';
        acc.lastProcessedKey = '';
        acc.lastProcessedValueType = (v) => v;
      }

      return acc;
    },
    {
      finished: false,
      processingKey: false,
      processingValue: false,
      processedKey: '',
      processedValue: '',
      lastProcessedKey: '',
      lastProcessedValueType: (v) => v,
      nesting: 0,
      position: targetArrayIndexOf + 1,
      result: {}
    }
  )

  return result;

}

const result = getValuesPositionInArray('Statements')(data);

console.log(result)

After doing a bunch of research, I think I have a way forward without writing my own parser by using esprima package. Since esprima it's not JSON specific (but rather JavaScript), I have to wrap my JSON string into brackets.

Each element in tree contains loc property with a range matching it to position in original JSON string.

var esprima = require("esprima");
var JSONPath = require('JSONPath');

function getStatementIndex(str, line, column) {
	var tree = esprima.parseScript(str, {loc:true});
	var query = "$.body[0].expression.properties[?(@.key.value=='Statement')].value.elements[*].loc";
	var locations = JSONPath({json: tree, path: query});
	
	console.log(locations);
	
	for(var i = 0; i < locations.length; i++) {
		var loc = locations[i];
		
		var contains = false;
		
		if (loc.start.line < line && loc.end.line > line) {
			continue;
		}
		
		// If a single line and in between
		if (loc.start.line == loc.end.line && loc.start.line == line) {
			if (loc.start.column <= column && loc.end.column >= column) {
				contains = true;
			}
			
		// If on the beginning line
		} else if (loc.start.line == line && loc.start.column <= column) {
			contains = true;
		
		// If on the end line
		} else if (loc.end.line == line && loc.end.column >= column) {
			contains = true;
		
		// If in between
		} else if (loc.start.line < line  && loc.end.line > line) {
			contains = true;
		}
					
		if (contains)
			return i;
	}
	
	return -1;
}

var result = getStatementIndex(str, 81, 7);

To find the position of something in the json string, if you want to build your own algorithm, there are several things to take into account, one issue is that several strings could lead to the same object literal, also the order of properties in the objects is not guaranteed, then same string could lead to different order in the properties. We know that every . means { in the string, but [ could mean [ or { . So to find the position of 1 for example, we should remove the spaces in the original string and perform recursive loops and build a json again and find the match. Here just an example to find the position of 1:

var json = '{"Version":"XXX","Statements":[{"a":1},{"b":2},{"b":3}]}';

var obj = JSON.parse(json)

var str2 = ""

for(p in obj){
    str2 += "{";
    str2 += p+":";
    if(p == "Statements"){
        str2 += ":["
        obj[p].forEach(o=>{            
            for(p2 in o){
                if(p2 == "a"){
                    str2 += '{"a":'
                }
            }
        })
    }else{
        str2 +='"'+obj[p]+'",'
    }    
}
console.log(str2)
console.log(str2.length+1)

This example is not accurate, it's just to show you one possible approach. In a real and general solution you should take into account hundreds of things.

发布评论

评论列表(0)

  1. 暂无评论