JSON Objects can contain an arbitrary number of keys and values (for which the value may itself be another JSON object). For example, a nested JSON object called "Monsters" might look like this:
{ "Troll": { "name":"Troll", "HD":4, "HP":75 },"Orc":
{ "name":"Orc", "HD":3, "HP":22 }}
Note that each value in the key-value pairs in the above object is actually a complete JSON object in its own right.
It is occasionally useful to be able to sort a JSON object that contains other JSON objects based on a value in one of the "sub-objects." For instance, if a JSON object exists that contains token names and distances to those tokens from a given point, one may want to sort the JSON so that the nearest objects are first, and the farthest are last.
Or, using the above "Monsters" example, one may wish to sort it by name, or by HP, or by HD.
The following macro routine is a generic method to sort JSON objects based on an arbitrary value within a nested object.
Contents |
Please see the full macro code for the complete macro.
This sequence simply creates a sample object to practice sorting. In actual use, you may wish to pass an object as an argument, or pull an object from a token's properties, as necessary.
[h:troll = json.set("{}", "name", "Troll", "HD", 4, "HP", 75)][h:orc = json.set("{}", "name", "Orc", "HD", 3, "HP", 13)][h:goblin = json.set("{}", "name", "Goblin", "HD", 2, "HP", 6)][h:gnoll = json.set("{}", "name", "Gnoll", "HD", 3, "HP", 19)][h:kobold=json.set("{}", "name", "Kobold", "HD", 1, "HP", 4)][h:monsters = json.set("{}", "Troll", troll, "Orc", orc, "Goblin", goblin, "Gnoll", gnoll, "Kobold", kobold)]This section is also optional (and not useful if this macro will be used as a function/called macro), but for the example code it makes it easier to experiment with. This section uses input() to gather user input, and abort() to halt processing if the user hits "Cancel." Finally, it uses an IF(): roll option to set a variable with a "friendly" indicator of sort direction, which will be used at the end in the final output.
[h:status = input("whichKey|name,HD,HP|Pick Sorting Key|LIST|SELECT=0 VALUE=STRING","whichDirection|A+,A-,N+,N-|Direction (A+/- for strings, N+/- for numbers!)|LIST|SELECT=0 VALUE=STRING")][h:abort(status)][h,if(substring(whichDirection,1)=="+"): dirString = "ascending"; dirString = "descending"]This segment initializes some variables that will be used later:
[h:sortObj=monsters][h:sortKey = whichKey][h:sortDirection = whichDirection][h:sortObjContentList = json.fields(sortObj)][h:keyList = ""] [h:sortedJSON = "{}"]Here, we use FOREACH() to loop through each element in sortObjContentList (in other words, go one-by-one through the list of monster names). The FOREACH() option lets us say that item holds the value of each of those (so for the first pass, item holds the first monster name in the list, and on the second pass, it moves to the next, and so on). We need to do this so that we can extract the detailed information about each monsters from the Monsters object (in this case, we assign the detailed information to a new variable called itemDetail).
With the nested objects extracted, we can then retrieve the value of the thing we're sorting on by using json.get() on the variable itemDetail. We stick that value in the previously empty list keyList.
Finally, once we've gone through each nested object held within Monsters and each nested object's value for our chosen sort (remember, we put that information in the variable sortKey) has been added to keyList, we're finished with the loop.
Now we actually can determine what the right order will ultimately be - we sort keyList using listSort() based on the direction specified by the user. This is a critical step! We've gone through each object, and figured out what the value of the thing we're sorting on is - so if we're sorting on "name", we've gone and actually retrieved each object's name, and put it in a list with the others. We then sort that list, which tells us the final order to use when we reassemble the main object!
[h,foreach(item, sortObjContentList),CODE:{ [h:itemDetail = json.get(sortObj,item)] [h: keyList = listAppend(keyList, json.get(itemDetail, sortKey))]}]
[h:keyList = listSort(keyList, sortDirection)]This is the most complex part of the routine. FOREACH(): through each element in the variable keyList (which, you will recall, contains the values corresponding to sortKey for each nested object). For each element in keyList, we then loop through all of the nested objects in Monsters to see which one(s) match up to the current element of keyList.
So, for example, if the current value - key - in the outer loop is 4, and we are sorting by "HD", the inner loop will iterate through each nested object and check to see if the value of "HD" for that nested object is equal to 4.
If a match is found, the matching nested object is added to sortedJSON using json.set(). In this fashion, we're using keyList to tell us what order the final nested objects should be in, and we then just need to go through our nested objects, setting them in that order via the following code.
[h,foreach(key,keyList),CODE:{ [foreach(object,sortObj),CODE: { [objectDetail = json.get(sortObj,object)] [h:sortOnValue = json.get(objectDetail, sortKey)] [if(sortOnValue == key): sortedJSON=json.set(sortedJSON, object, objectDetail);""]}]
}]
The final step is to output results. The use of json.indent() here simply makes the sorted JSON object easy to read.
JSON Object sorted by [r:whichKey], [r:dirString]:<br>
<pre>[r:json.indent(sortedJSON, 3)]</pre>