# HG changeset patch # User Mike Becker # Date 1762380286 -3600 # Node ID 84de0ba791af53f472c492ec42f91b5314fbbb09 # Parent 944f02992369d7046523f0662966ce9b54bd6a4a implement cxMapUnion() - resolves #756 diff -r 944f02992369 -r 84de0ba791af docs/Writerside/topics/map.h.md --- a/docs/Writerside/topics/map.h.md Wed Nov 05 22:39:39 2025 +0100 +++ b/docs/Writerside/topics/map.h.md Wed Nov 05 23:04:46 2025 +0100 @@ -324,6 +324,11 @@ cx_clone_func clone_func, const CxAllocator *clone_allocator, void *data); + +int cxMapUnion(CxMap *dst, const CxMap *src, + cx_clone_func clone_func, + const CxAllocator *clone_allocator, + void *data); ``` With `cxMapClone()` you can create deep copies of the values in one map and insert them into another map. @@ -337,6 +342,11 @@ This is equivalent to computing the set difference for the set of keys. Likewise, `cxMapIntersection()` and `cxMapListIntersection()` only clone an element from the source map, when the key is contained in _both_ collections. +The function `cxMapUnion()` only clones elements from `src` to `dst` when their key is not present in `dst`. + +> The function `cxMapUnion()` does not operate on three maps for the sake of simplicity. +> If you want to combine two maps without modifying either of them, you can first use `cxMapClone()` +> to clone the first map into a new, empty, map, and then use `cxMapUnion()`. Refer to the documentation of the [clone-function callback](allocator.h.md#clone-function) to learn how to implement it. diff -r 944f02992369 -r 84de0ba791af src/cx/map.h --- a/src/cx/map.h Wed Nov 05 22:39:39 2025 +0100 +++ b/src/cx/map.h Wed Nov 05 23:04:46 2025 +0100 @@ -583,6 +583,26 @@ CX_EXPORT int cxMapListIntersection(CxMap *dst, const CxMap *src, const CxList *keys, cx_clone_func clone_func, const CxAllocator *clone_allocator, void *data); +/** + * Clones entries into a map if their key does not exist yet. + * + * If you want to calculate the union of two maps into a fresh new map, + * you can proceed as follows: + * 1. Clone the first map into a fresh, empty map. + * 2. Use this function to clone the second map into the result from step 1. + * + * @param dst the destination map + * @param src the map to clone the entries from + * @param clone_func the clone function for the values + * @param clone_allocator the allocator that is passed to the clone function + * @param data optional additional data that is passed to the clone function + * @retval zero when the elements were successfully cloned + * @retval non-zero when an allocation error occurred + */ +cx_attr_nonnull_arg(1, 2, 3) +CX_EXPORT int cxMapUnion(CxMap *dst, const CxMap *src, + cx_clone_func clone_func, const CxAllocator *clone_allocator, void *data); + #ifdef __cplusplus } // extern "C" #endif diff -r 944f02992369 -r 84de0ba791af src/map.c --- a/src/map.c Wed Nov 05 22:39:39 2025 +0100 +++ b/src/map.c Wed Nov 05 23:04:46 2025 +0100 @@ -267,3 +267,29 @@ } return 0; } + +int cxMapUnion(CxMap *dst, const CxMap *src, + cx_clone_func clone_func, const CxAllocator *clone_allocator, void *data) { + if (clone_allocator == NULL) clone_allocator = cxDefaultAllocator; + + CxMapIterator src_iter = cxMapIterator(src); + cx_foreach(const CxMapEntry *, entry, src_iter) { + if (cxMapContains(dst, *entry->key)) { + continue; + } + void** dst_mem = cxMapEmplace(dst, *entry->key); + if (dst_mem == NULL) { + return 1; // LCOV_EXCL_LINE + } + void *target = cxCollectionStoresPointers(dst) ? NULL : dst_mem; + void* dst_ptr = clone_func(target, entry->value, clone_allocator, data); + if (dst_ptr == NULL) { + cx_map_remove_uninitialized_entry(dst, *(entry->key)); + return 1; + } + if (cxCollectionStoresPointers(dst)) { + *dst_mem = dst_ptr; + } + } + return 0; +} diff -r 944f02992369 -r 84de0ba791af tests/test_hash_map.c --- a/tests/test_hash_map.c Wed Nov 05 22:39:39 2025 +0100 +++ b/tests/test_hash_map.c Wed Nov 05 23:04:46 2025 +0100 @@ -1063,6 +1063,98 @@ cx_testing_allocator_destroy(&talloc); } +CX_TEST(test_hash_map_union) { + CxMap *s1 = cxHashMapCreateSimple(sizeof(int)); + CxMap *s2 = cxHashMapCreateSimple(sizeof(int)); + const char *s1_keys[] = {"k1", "k2", "k3", "k4"}; + int s1_values[] = {1, 3, 4, 6}; + const char *s2_keys[] = {"k4", "k5", "k2", "k6"}; + int s2_values[] = {5, 9, 15, 23}; + for (unsigned int i = 0 ; i < 4 ; i++) { + cxMapPut(s1, s1_keys[i], &s1_values[i]); + cxMapPut(s2, s2_keys[i], &s2_values[i]); + } + CX_TEST_DO { + int c = 4; + CX_TEST_ASSERT(0 == cxMapUnion(s1, s2, test_hash_map_clone_func, NULL, &c)); + CX_TEST_ASSERT(cxMapSize(s1) == 6); + CX_TEST_ASSERT(*((int*)cxMapGet(s1, "k1")) == 1); + CX_TEST_ASSERT(*((int*)cxMapGet(s1, "k2")) == 3); + CX_TEST_ASSERT(*((int*)cxMapGet(s1, "k3")) == 4); + CX_TEST_ASSERT(*((int*)cxMapGet(s1, "k4")) == 6); + CX_TEST_ASSERT(*((int*)cxMapGet(s1, "k5")) == 13); + CX_TEST_ASSERT(*((int*)cxMapGet(s1, "k6")) == 27); + } + cxMapFree(s1); + cxMapFree(s2); +} + +CX_TEST(test_hash_map_union_ptr) { + CxTestingAllocator talloc; + cx_testing_allocator_init(&talloc); + CxAllocator *allocator = &talloc.base; + CxMap *dst = cxHashMapCreateSimple(CX_STORE_POINTERS); + cxDefineAdvancedDestructor(dst, cxFree, allocator); + + CxMap *s1 = cxHashMapCreateSimple(sizeof(int)); + CxMap *s2 = cxHashMapCreateSimple(sizeof(int)); + const char *s1_keys[] = {"k1", "k2", "k3", "k4"}; + int s1_values[] = {1, 3, 4, 6}; + const char *s2_keys[] = {"k4", "k5", "k2", "k6"}; + int s2_values[] = {5, 9, 15, 23}; + for (unsigned int i = 0 ; i < 4 ; i++) { + cxMapPut(s1, s1_keys[i], &s1_values[i]); + cxMapPut(s2, s2_keys[i], &s2_values[i]); + } + CX_TEST_DO { + int c = 4; + CX_TEST_ASSERT(0 == cxMapClone(dst, s1, test_hash_map_clone_func, allocator, &c)); + CX_TEST_ASSERT(0 == cxMapUnion(dst, s2, test_hash_map_clone_func, allocator, &c)); + CX_TEST_ASSERT(cxMapSize(dst) == 6); + CX_TEST_ASSERT(*((int*)cxMapGet(dst, "k1")) == 5); + CX_TEST_ASSERT(*((int*)cxMapGet(dst, "k2")) == 7); + CX_TEST_ASSERT(*((int*)cxMapGet(dst, "k3")) == 8); + CX_TEST_ASSERT(*((int*)cxMapGet(dst, "k4")) == 10); + CX_TEST_ASSERT(*((int*)cxMapGet(dst, "k5")) == 13); + CX_TEST_ASSERT(*((int*)cxMapGet(dst, "k6")) == 27); + CX_TEST_ASSERT(cx_testing_allocator_used(&talloc)); + } + cxMapFree(dst); + cxMapFree(s1); + cxMapFree(s2); + cx_testing_allocator_destroy(&talloc); +} + +CX_TEST(test_hash_map_union_alloc_fail) { + CxMap *s1 = cxHashMapCreateSimple(sizeof(int)); + CxMap *s2 = cxHashMapCreateSimple(sizeof(int)); + const char *s1_keys[] = {"k1", "k2", "k3", "k4"}; + int s1_values[] = {1, 3, 4, 6}; + const char *s2_keys[] = {"k4", "k5", "k2", "k6"}; + int s2_values[] = {5, 9, 15, 23}; + for (unsigned int i = 0 ; i < 4 ; i++) { + cxMapPut(s1, s1_keys[i], &s1_values[i]); + cxMapPut(s2, s2_keys[i], &s2_values[i]); + } + CX_TEST_DO { + int c = 4; + test_hash_map_clone_func_max_enabled = true; + test_hash_map_clone_func_max_clones = 1; + CX_TEST_ASSERT(0 != cxMapUnion(s1, s2, test_hash_map_clone_func, NULL, &c)); + test_hash_map_clone_func_max_enabled = false; + CX_TEST_ASSERT(cxMapSize(s1) == 5); + CX_TEST_ASSERT(*((int*)cxMapGet(s1, "k1")) == 1); + CX_TEST_ASSERT(*((int*)cxMapGet(s1, "k2")) == 3); + CX_TEST_ASSERT(*((int*)cxMapGet(s1, "k3")) == 4); + CX_TEST_ASSERT(*((int*)cxMapGet(s1, "k4")) == 6); + // the concrete element which is affected might change when the hash function changes + CX_TEST_ASSERT(cxMapGet(s1, "k5") == NULL); + CX_TEST_ASSERT(*((int*)cxMapGet(s1, "k6")) == 27); + } + cxMapFree(s1); + cxMapFree(s2); +} + CX_TEST(test_empty_map_size) { CX_TEST_DO { CX_TEST_ASSERT(cxEmptyMap->collection.size == 0); @@ -1425,6 +1517,9 @@ cx_test_register(suite, test_hash_map_list_intersection_alloc_fail); cx_test_register(suite, test_hash_map_intersection_non_empty_target); cx_test_register(suite, test_hash_map_list_intersection_non_empty_target); + cx_test_register(suite, test_hash_map_union); + cx_test_register(suite, test_hash_map_union_ptr); + cx_test_register(suite, test_hash_map_union_alloc_fail); cx_test_register(suite, test_empty_map_no_ops); cx_test_register(suite, test_empty_map_size); cx_test_register(suite, test_empty_map_get);