Reputation: 1022
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.
Upvotes: 14
Views: 4939
Reputation: 6086
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)
Upvotes: 6
Reputation: 5250
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.
Upvotes: 5
Reputation: 1022
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);
Upvotes: 5