implement index array to preserve order of json object members

Fri, 03 Jan 2025 19:18:00 +0100

author
Mike Becker <universe@uap-core.de>
date
Fri, 03 Jan 2025 19:18:00 +0100
changeset 1082
46cdc8689fc4
parent 1081
33c9d7e7d830
child 1083
cf54e413793c

implement index array to preserve order of json object members

relates to #526 and resolves #462

src/cx/json.h file | annotate | diff | comparison | revisions
src/json.c file | annotate | diff | comparison | revisions
tests/test_json.c file | annotate | diff | comparison | revisions
--- a/src/cx/json.h	Fri Jan 03 17:16:49 2025 +0100
+++ b/src/cx/json.h	Fri Jan 03 19:18:00 2025 +0100
@@ -231,6 +231,10 @@
      * The key/value entries.
      */
     CX_ARRAY_DECLARE(CxJsonObjValue, values);
+    /**
+     * The original indices to reconstruct the order in which the members were added.
+     */
+    size_t *indices;
 };
 
 /**
--- a/src/json.c	Fri Jan 03 17:16:49 2025 +0100
+++ b/src/json.c	Fri Jan 03 19:18:00 2025 +0100
@@ -67,13 +67,66 @@
     }
 }
 
-static int json_add_objvalue(CxJsonValue *obj, CxJsonObjValue member) {
+static int json_add_objvalue(CxJsonValue *objv, CxJsonObjValue member) {
     assert(obj->type == CX_JSON_OBJECT);
-    CxArrayReallocator value_realloc = cx_array_reallocator(obj->allocator, NULL);
-    return cx_array_simple_add_sorted_a(
-        &value_realloc, obj->value.object.values,
-        member, json_cmp_objvalue
+    const CxAllocator * const al = objv->allocator;
+    CxJsonObject *obj = &(objv->value.object);
+
+    // determine the index where we need to insert the new member
+    size_t index = cx_array_binary_search_sup(
+        obj->values,
+        obj->values_size,
+        sizeof(CxJsonObjValue),
+        &member, json_cmp_objvalue
     );
+
+    // is the name already present?
+    if (index < obj->values_size && 0 == json_cmp_objvalue(&member, &obj->values[index])) {
+        // free the original value
+        cx_strfree_a(al, &obj->values[index].name);
+        cxJsonValueFree(obj->values[index].value);
+        // replace the item
+        obj->values[index] = member;
+
+        // nothing more to do
+        return 0;
+    }
+
+    // determine the old capacity and reserve for one more element
+    CxArrayReallocator arealloc = cx_array_reallocator(al, NULL);
+    size_t oldcap = obj->values_capacity;
+    if (cx_array_simple_reserve_a(&arealloc, obj->values, 1)) return 1;
+
+    // check the new capacity, if we need to realloc the index array
+    size_t newcap = obj->values_capacity;
+    if (newcap > oldcap) {
+        if (cxReallocateArray(al, &obj->indices, newcap, sizeof(size_t))) {
+            return 1;
+        }
+    }
+
+    // check if append or insert
+    if (index < obj->values_size) {
+        // move the other elements
+        memmove(
+            &obj->values[index+1],
+            &obj->values[index],
+            (obj->values_size - index) * sizeof(CxJsonObjValue)
+        );
+        // increase indices for the moved elements
+        for (size_t i = 0; i < obj->values_size ; i++) {
+            if (obj->indices[i] >= index) {
+                obj->indices[i]++;
+            }
+        }
+    }
+
+    // insert the element and set the index
+    obj->values[index] = member;
+    obj->indices[obj->values_size] = index;
+    obj->values_size++;
+
+    return 0;
 }
 
 static void token_destroy(CxJsonToken *token) {
@@ -323,21 +376,22 @@
 }
 
 static CxJsonValue* create_json_value(CxJson *json, CxJsonValueType type) {
-    CxJsonValue *v = cxMalloc(json->allocator, sizeof(CxJsonValue));
+    CxJsonValue *v = cxCalloc(json->allocator, 1, sizeof(CxJsonValue));
     if (v == NULL) return NULL; // LCOV_EXCL_LINE
 
     // initialize the value
+    v->type = type;
+    v->allocator = json->allocator;
     if (type == CX_JSON_ARRAY) {
         cx_array_initialize_a(json->allocator, v->value.array.array, 16);
         if (v->value.array.array == NULL) goto create_json_value_exit_error; // LCOV_EXCL_LINE
     } else if (type == CX_JSON_OBJECT) {
         cx_array_initialize_a(json->allocator, v->value.object.values, 16);
-        if (v->value.object.values == NULL) goto create_json_value_exit_error; // LCOV_EXCL_LINE
-    } else {
-        memset(v, 0, sizeof(CxJsonValue));
+        v->value.object.indices = cxCalloc(json->allocator, 16, sizeof(size_t));
+        if (v->value.object.values == NULL ||
+            v->value.object.indices == NULL)
+            goto create_json_value_exit_error; // LCOV_EXCL_LINE
     }
-    v->type = type;
-    v->allocator = json->allocator;
 
     // add the new value to a possible parent
     if (json->vbuf_size > 0) {
@@ -377,7 +431,7 @@
     return v;
     // LCOV_EXCL_START
 create_json_value_exit_error:
-    cxFree(json->allocator, v);
+    cxJsonValueFree(v);
     return NULL;
     // LCOV_EXCL_STOP
 }
@@ -657,6 +711,7 @@
                 cx_strfree_a(value->allocator, &obj.values[i].name);
             }
             cxFree(value->allocator, obj.values);
+            cxFree(value->allocator, obj.indices);
             break;
         }
         case CX_JSON_ARRAY: {
@@ -684,7 +739,18 @@
     v->allocator = allocator;
     v->type = CX_JSON_OBJECT;
     cx_array_initialize_a(allocator, v->value.object.values, 16);
-    if (v->value.object.values == NULL) { cxFree(allocator, v); return NULL; }
+    if (v->value.object.values == NULL) { // LCOV_EXCL_START
+        cxFree(allocator, v);
+        return NULL;
+        // LCOV_EXCL_STOP
+    }
+    v->value.object.indices = cxCalloc(allocator, 16, sizeof(size_t));
+    if (v->value.object.indices == NULL) { // LCOV_EXCL_START
+        cxFree(allocator, v->value.object.values);
+        cxFree(allocator, v);
+        return NULL;
+        // LCOV_EXCL_STOP
+    }
     return v;
 }
 
@@ -822,20 +888,15 @@
 }
 
 int cxJsonObjPut(CxJsonValue* obj, cxstring name, CxJsonValue* child) {
-    // TODO: optimize - issue #462
-    for (size_t i = 0; i < obj->value.object.values_size; i++) {
-        if (0 == cx_strcmp(name, cx_strcast(obj->value.object.values[i].name))) {
-            // free the original value
-            cxJsonValueFree(obj->value.object.values[i].value);
-            obj->value.object.values[i].value = child;
-            return 0;
-        }
-    }
-
     cxmutstr k = cx_strdup_a(obj->allocator, name);
     if (k.ptr == NULL) return -1;
     CxJsonObjValue kv = {k, child};
-    return json_add_objvalue(obj, kv);
+    if (json_add_objvalue(obj, kv)) {
+        cx_strfree_a(obj->allocator, &k);
+        return 1;
+    } else {
+        return 0;
+    }
 }
 
 CxJsonValue* cxJsonObjPutObj(CxJsonValue* obj, cxstring name) {
@@ -997,12 +1058,22 @@
                 expected++;
             }
             depth++;
-            CxIterator iter = cxJsonObjIter(value);
-            // TODO: unsorted output - realize after implementing index array
-            cx_foreach(CxJsonObjValue*, member, iter) {
+            size_t elem_count = value->value.object.values_size;
+            for (size_t look_idx = 0; look_idx < elem_count; look_idx++) {
+                // get the member either via index array or directly
+                size_t elem_idx = settings->sort_members
+                                      ? look_idx
+                                      : value->value.object.indices[look_idx];
+                CxJsonObjValue *member = &value->value.object.values[elem_idx];
+                if (settings->sort_members) {
+                    depth++;depth--;
+                }
+
                 // possible indentation
                 if (settings->pretty) {
-                    if (cx_json_writer_indent(target, wfunc, settings, depth)) return 1;
+                    if (cx_json_writer_indent(target, wfunc, settings, depth)) {
+                        return 1; // LCOV_EXCL_LINE
+                    }
                 }
 
                 // the name
@@ -1024,7 +1095,7 @@
                 if (cx_json_write_rec(target, member->value, wfunc, settings, depth)) return 1;
 
                 // end of object-value
-                if (iter.index < iter.elem_count - 1) {
+                if (look_idx < elem_count - 1) {
                     const char *obj_value_sep = ",\n";
                     if (settings->pretty) {
                         actual += wfunc(obj_value_sep, 1, 2, target);
@@ -1049,7 +1120,6 @@
             break;
         }
         case CX_JSON_ARRAY: {
-            // TODO: implement array wrapping
             actual += wfunc("[", 1, 1, target);
             expected++;
             CxIterator iter = cxJsonArrIter(value);
--- a/tests/test_json.c	Fri Jan 03 17:16:49 2025 +0100
+++ b/tests/test_json.c	Fri Jan 03 19:18:00 2025 +0100
@@ -730,8 +730,8 @@
     CxJsonValue *obj_in_arr[2] = {cxJsonCreateObj(allocator), cxJsonCreateObj(allocator)};
     cxJsonObjPutInteger(obj_in_arr[0], CX_STR("name1"), 1);
     cxJsonObjPutInteger(obj_in_arr[0], CX_STR("name2"), 3);
+    cxJsonObjPutInteger(obj_in_arr[1], CX_STR("name2"), 7);
     cxJsonObjPutInteger(obj_in_arr[1], CX_STR("name1"), 3);
-    cxJsonObjPutInteger(obj_in_arr[1], CX_STR("name2"), 7);
     cxJsonArrAddValues(objects, obj_in_arr, 2);
     cxJsonArrAddNumbers(cxJsonObjPutArr(nested, CX_STR("floats")),
                         (double[]){3.1415, 47.11, 8.15}, 3);
@@ -787,7 +787,6 @@
         CxJsonWriter writer = cxJsonWriterCompact();
         CX_TEST_CALL_SUBROUTINE(test_json_write_sub, allocator, expected, &writer);
         CX_TEST_ASSERT(cx_testing_allocator_verify(&talloc));
-        CX_TEST_ASSERT(cx_testing_allocator_verify(&talloc));
     }
     cx_testing_allocator_destroy(&talloc);
 }
@@ -820,7 +819,6 @@
         CxJsonWriter writer = cxJsonWriterPretty(true);
         CX_TEST_CALL_SUBROUTINE(test_json_write_sub, allocator, expected, &writer);
         CX_TEST_ASSERT(cx_testing_allocator_verify(&talloc));
-        CX_TEST_ASSERT(cx_testing_allocator_verify(&talloc));
     }
     cx_testing_allocator_destroy(&talloc);
 }
@@ -856,6 +854,39 @@
     cx_testing_allocator_destroy(&talloc);
 }
 
+CX_TEST(test_json_write_pretty_preserve_order) {
+    CxTestingAllocator talloc;
+    cx_testing_allocator_init(&talloc);
+    CxAllocator *allocator = &talloc.base;
+    CX_TEST_DO {
+        cxstring expected = CX_STR(
+"{\n"
+"    \"bool\": false,\n"
+"    \"int\": 47,\n"
+"    \"strings\": [\"hello\", \"world\"],\n"
+"    \"nested\": {\n"
+"        \"objects\": [{\n"
+"            \"name1\": 1,\n"
+"            \"name2\": 3\n"
+"        }, {\n"
+"            \"name2\": 7,\n"
+"            \"name1\": 3\n"
+"        }],\n"
+"        \"floats\": [3.1415, 47.11, 8.15],\n"
+"        \"literals\": [true, null, false],\n"
+"        \"ints\": [4, 8, 15, [16, 23], 42]\n"
+"    }\n"
+"}"
+        );
+
+        CxJsonWriter writer = cxJsonWriterPretty(true);
+        writer.sort_members = false;
+        CX_TEST_CALL_SUBROUTINE(test_json_write_sub, allocator, expected, &writer);
+        CX_TEST_ASSERT(cx_testing_allocator_verify(&talloc));
+    }
+    cx_testing_allocator_destroy(&talloc);
+}
+
 CxTestSuite *cx_test_suite_json(void) {
     CxTestSuite *suite = cx_test_suite_new("json");
 
@@ -877,6 +908,7 @@
     cx_test_register(suite, test_json_write_default_format);
     cx_test_register(suite, test_json_write_pretty_default_spaces);
     cx_test_register(suite, test_json_write_pretty_default_tabs);
+    cx_test_register(suite, test_json_write_pretty_preserve_order);
     
     return suite;
 }

mercurial