diff --git a/doc/apiref.rst b/doc/apiref.rst index 4bfb6879..8eb1e866 100644 --- a/doc/apiref.rst +++ b/doc/apiref.rst @@ -1872,6 +1872,21 @@ only if they are exactly the same value, but also if they have equal *NULL*. +Comparison +========== + +.. function:: int json_compare(json_t *value1, json_t *value2) + + Returns -1 if *value1* is less than *value2*, 0 if they are equal and 1 if + *value1* is greater than *value2*. This is akin to `strcmp()` and can be + used for comparing whole JSON data structures for sorting. + + *NULL* is a valid parameter for either argument and will compare equal if + both are *NULL* or greater/less than if only one is NULL. + + Nested arrays or objects will be recursively compared and any differences in + values, number of elements, object keys etc will compare consistently. + Copying ======= diff --git a/src/jansson.def b/src/jansson.def index 5c76c2f6..38655bcd 100644 --- a/src/jansson.def +++ b/src/jansson.def @@ -68,6 +68,7 @@ EXPORTS json_load_file json_load_callback json_equal + json_compare json_copy json_deep_copy json_pack diff --git a/src/jansson.h b/src/jansson.h index 391c85e9..97ca0764 100644 --- a/src/jansson.h +++ b/src/jansson.h @@ -348,6 +348,10 @@ json_t *json_vsprintf(const char *fmt, va_list ap) int json_equal(const json_t *value1, const json_t *value2); +/* comparison */ + +int json_compare(const json_t *value1, const json_t *value2); + /* copying */ json_t *json_copy(json_t *value) JANSSON_ATTRS((warn_unused_result)); diff --git a/src/value.c b/src/value.c index d5a11fe5..0e0951e7 100644 --- a/src/value.c +++ b/src/value.c @@ -370,6 +370,66 @@ static int json_object_equal(const json_t *object1, const json_t *object2) { return 1; } +static int json_strnncmp(const char *s1, size_t s1_len, const char *s2, size_t s2_len) +{ + int result = strncmp(s1, s2, (s1_len > s2_len ? s2_len : s1_len)); + + if (result > 1) + result = 1; + else if (result < -1) + result = -1; + + if (result == 0) { + if (s1_len < s2_len) + result = -1; + else if (s1_len > s2_len) + result = 1; + } + + return result; +} + +static void json_object_next_key(const json_t *object, const char **next_key, size_t *next_key_len, const char *prev_key, size_t prev_key_len) { + const char *key; + size_t key_len; + const json_t *value; + + *next_key = NULL; + + /* Linear search through the object to find the smallest key which is >prev_key */ + json_object_keylen_foreach((json_t *)object, key, key_len, value) { + if ((*next_key == NULL || json_strnncmp(*next_key, *next_key_len, key, key_len) < 0) + && (prev_key == NULL || json_strnncmp(prev_key, prev_key_len, key, key_len) > 0)) { + *next_key = key; + *next_key_len = key_len; + } + } +} + +static int json_object_compare(const json_t *object1, const json_t *object2) { + const char *key1 = NULL, *key2 = NULL; + size_t key1_len, key2_len; + + json_object_next_key(object1, &key1, &key1_len, key1, key1_len); + json_object_next_key(object2, &key2, &key2_len, key2, key2_len); + + while (key1 != NULL && key2 != NULL) { + int cmp = json_strnncmp(key1, key1_len, key2, key2_len); + if (cmp != 0) + return cmp; + + json_object_next_key(object1, &key1, &key1_len, key1, key1_len); + json_object_next_key(object2, &key2, &key2_len, key2, key2_len); + } + + if (key1 != NULL) + return 1; + else if(key2 != NULL) + return -1; + else + return 0; +} + static json_t *json_object_copy(json_t *object) { json_t *result; @@ -670,6 +730,33 @@ static int json_array_equal(const json_t *array1, const json_t *array2) { return 1; } +static int json_array_compare(const json_t *array1, const json_t *array2) { + size_t a1_size, a2_size, min_size, i; + + a1_size = json_array_size(array1); + a2_size = json_array_size(array2); + min_size = a1_size > a2_size ? a2_size : a1_size; + + for (i = 0; i < min_size; i++) { + json_t *value1, *value2; + int result; + + value1 = json_array_get(array1, i); + value2 = json_array_get(array2, i); + + result = json_compare(value1, value2); + if (result != 0) + return result; + } + + if (a1_size < a2_size) + return 1; + else if (a1_size > a2_size) + return -1; + + return 0; +} + static json_t *json_array_copy(json_t *array) { json_t *result; size_t i; @@ -838,6 +925,15 @@ static int json_string_equal(const json_t *string1, const json_t *string2) { return s1->length == s2->length && !memcmp(s1->value, s2->value, s1->length); } +static int json_string_compare(const json_t *string1, const json_t *string2) { + json_string_t *s1, *s2; + + s1 = json_to_string(string1); + s2 = json_to_string(string2); + + return json_strnncmp(s1->value, s1->length, s2->value, s2->length); +} + static json_t *json_string_copy(const json_t *string) { json_string_t *s; @@ -922,6 +1018,15 @@ static int json_integer_equal(const json_t *integer1, const json_t *integer2) { return json_integer_value(integer1) == json_integer_value(integer2); } +static int json_integer_compare(const json_t *integer1, const json_t *integer2) { + if (json_integer_value(integer1) < json_integer_value(integer2)) + return -1; + else if (json_integer_value(integer1) > json_integer_value(integer2)) + return 1; + else + return 0; +} + static json_t *json_integer_copy(const json_t *integer) { return json_integer(json_integer_value(integer)); } @@ -965,6 +1070,15 @@ static int json_real_equal(const json_t *real1, const json_t *real2) { return json_real_value(real1) == json_real_value(real2); } +static int json_real_compare(const json_t *real1, const json_t *real2) { + if (json_real_value(real1) < json_real_value(real2)) + return -1; + else if (json_real_value(real1) > json_real_value(real2)) + return 1; + else + return 0; +} + static json_t *json_real_copy(const json_t *real) { return json_real(json_real_value(real)); } @@ -1055,6 +1169,43 @@ int json_equal(const json_t *json1, const json_t *json2) { } } +/*** comparison ***/ + +int json_compare(const json_t *json1, const json_t *json2) { + if (!json1 && !json2) + return 0; + + if (!json1) + return -1; + + if (!json2) + return 1; + + if (json_typeof(json1) < json_typeof(json2)) + return -1; + else if (json_typeof(json1) > json_typeof(json2)) + return 1; + + /* this covers true, false and null as they are singletons */ + if (json1 == json2) + return 0; + + switch (json_typeof(json1)) { + case JSON_OBJECT: + return json_object_compare(json1, json2); + case JSON_ARRAY: + return json_array_compare(json1, json2); + case JSON_STRING: + return json_string_compare(json1, json2); + case JSON_INTEGER: + return json_integer_compare(json1, json2); + case JSON_REAL: + return json_real_compare(json1, json2); + default: + return 0; + } +} + /*** copying ***/ json_t *json_copy(json_t *json) { diff --git a/test/.gitignore b/test/.gitignore index 93cc8da4..92bfea09 100644 --- a/test/.gitignore +++ b/test/.gitignore @@ -7,6 +7,7 @@ suites/api/test_cpp suites/api/test_dump suites/api/test_dump_callback suites/api/test_equal +suites/api/test_compare suites/api/test_fixed_size suites/api/test_load suites/api/test_load_callback diff --git a/test/suites/api/Makefile.am b/test/suites/api/Makefile.am index 2bc638b8..a2ba10c5 100644 --- a/test/suites/api/Makefile.am +++ b/test/suites/api/Makefile.am @@ -7,6 +7,7 @@ check_PROGRAMS = \ test_dump \ test_dump_callback \ test_equal \ + test_compare \ test_fixed_size \ test_load \ test_load_callback \ diff --git a/test/suites/api/test_compare.c b/test/suites/api/test_compare.c new file mode 100644 index 00000000..11f50ca6 --- /dev/null +++ b/test/suites/api/test_compare.c @@ -0,0 +1,184 @@ +/* + * Copyright (c) 2009-2016 Petri Lehtinen + * + * Jansson is free software; you can redistribute it and/or modify + * it under the terms of the MIT license. See LICENSE for details. + */ + +#include "util.h" +#include + +static void test_compare_simple() { + json_t *value1, *value2; + + if (json_compare(NULL, NULL) != 0) + fail("json_compare returns wrong result for two NULLs"); + + value1 = json_true(); + if (json_compare(value1, NULL) != 1 || json_compare(NULL, value1) != -1) + fail("json_compare returns wrong result for true/NULL"); + + /* this covers true, false and null as they are singletons */ + if (json_compare(value1, value1) != 0) + fail("identical objects are not equal"); + json_decref(value1); + + /* integer */ + value1 = json_integer(1); + value2 = json_integer(1); + if (!value1 || !value2) + fail("unable to create integers"); + if (json_compare(value1, value2) != 0) + fail("json_compare returns wrong result for two equal integers"); + json_decref(value2); + + value2 = json_integer(2); + if (!value2) + fail("unable to create an integer"); + if (json_compare(value1, value2) != -1) + fail("json_compare returns wrong result for two inequal integers"); + if (json_compare(value2, value1) != 1) + fail("json_compare returns wrong result for two inequal integers"); + + json_decref(value1); + json_decref(value2); + + /* real */ + value1 = json_real(1.2); + value2 = json_real(1.2); + if (!value1 || !value2) + fail("unable to create reals"); + if (json_compare(value1, value2) != 0) + fail("json_compare returns wrong result for two equal reals"); + json_decref(value2); + + value2 = json_real(3.141592); + if (!value2) + fail("unable to create an real"); + if (json_compare(value1, value2) != -1) + fail("json_compare returns wrong result for two inequal reals"); + if (json_compare(value2, value1) != 1) + fail("json_compare returns wrong result for two inequal reals"); + + json_decref(value1); + json_decref(value2); + + /* string */ + value1 = json_string("foo"); + value2 = json_string("foo"); + if (!value1 || !value2) + fail("unable to create strings"); + if (json_compare(value1, value2) != 0) + fail("json_compare returns wrong result for two equal strings"); + json_decref(value2); + + value2 = json_string("bar"); + if (!value2) + fail("unable to create an string"); + if (json_compare(value1, value2) != 1) + fail("json_compare returns wrong result for two inequal strings"); + if (json_compare(value2, value1) != -1) + fail("json_compare returns wrong result for two inequal strings"); + json_decref(value2); + + value2 = json_string("foo2"); + if (!value2) + fail("unable to create an string"); + if (json_compare(value1, value2) != -1) + fail("json_compare returns wrong result for for two inequal length strings"); + if (json_compare(value2, value1) != 1) + fail("json_compare returns wrong result for for two inequal length strings"); + + json_decref(value1); + json_decref(value2); +} + +static void test_compare_array() { + json_t *array1, *array2; + + array1 = json_array(); + array2 = json_array(); + if (!array1 || !array2) + fail("unable to create arrays"); + + if (json_compare(array1, array2) != 0) + fail("json_compare returns wrong result for two empty arrays"); + + json_array_append_new(array1, json_integer(1)); + json_array_append_new(array2, json_integer(1)); + json_array_append_new(array1, json_string("foo")); + json_array_append_new(array2, json_string("foo")); + json_array_append_new(array1, json_integer(2)); + json_array_append_new(array2, json_integer(2)); + if (json_compare(array1, array2) != 0) + fail("json_compare returns wrong result for two equal arrays"); + + json_array_remove(array2, 2); + if (json_compare(array1, array2) != -1) + fail("json_compare returns wrong result for two inequal arrays"); + if (json_compare(array2, array1) != 1) + fail("json_compare returns wrong result for two inequal arrays"); + + json_array_append_new(array2, json_integer(0)); + if (json_compare(array1, array2) != 1) + fail("json_compare returns wrong result for two inequal arrays"); + if (json_compare(array2, array1) != -1) + fail("json_compare returns wrong result for two inequal arrays"); + + json_decref(array1); + json_decref(array2); +} + +static void test_compare_object() { + json_t *object1, *object2; + + object1 = json_object(); + object2 = json_object(); + if (!object1 || !object2) + fail("unable to create objects"); + + if (json_compare(object1, object2) != 0) + fail("json_compare returns wrong result for two empty objects"); + + json_object_set_new(object1, "a", json_integer(1)); + json_object_set_new(object1, "b", json_string("foo")); + json_object_set_new(object1, "c", json_integer(2)); + + /* Populate object2 in reverse to ensure comparison is not affected by + * insertion order. + */ + json_object_set_new(object2, "c", json_integer(2)); + json_object_set_new(object2, "b", json_string("foo")); + json_object_set_new(object2, "a", json_integer(1)); + + if (json_compare(object1, object2) != 0) + fail("json_compare returns the wrong result for two equal objects"); + + json_object_del(object2, "c"); + if (json_compare(object1, object2) != 1) + fail("json_compare returns the wrong result for two inequal objects (missing key)"); + if (json_compare(object2, object1) != -1) + fail("json_compare returns the wrong result for two inequal objects (missing key)"); + + json_object_set_new(object2, "d", json_integer(2)); + if (json_compare(object1, object2) != -1) + fail("json_compare returns the wrong result for two inequal objects (different key)"); + if (json_compare(object2, object1) != 1) + fail("json_compare returns the wrong result for two inequal objects (different key)"); + + json_object_del(object2, "c"); + json_object_set_new(object2, "c", json_integer(1)); + if (json_compare(object1, object2) != -1) + fail("json_compare returns the wrong result for two inequal objects (different value)"); + if (json_compare(object2, object1) != 1) + fail("json_compare returns the wrong result for two inequal objects (different value)"); + + json_decref(object1); + json_decref(object2); +} + +static void run_tests() { + test_compare_simple(); + test_compare_array(); + test_compare_object(); +}