implement cxMapUnion() - resolves #756 default tip

Wed, 05 Nov 2025 23:04:46 +0100

author
Mike Becker <universe@uap-core.de>
date
Wed, 05 Nov 2025 23:04:46 +0100
changeset 1474
84de0ba791af
parent 1473
944f02992369

implement cxMapUnion() - resolves #756

docs/Writerside/topics/map.h.md file | annotate | diff | comparison | revisions
src/cx/map.h file | annotate | diff | comparison | revisions
src/map.c file | annotate | diff | comparison | revisions
tests/test_hash_map.c file | annotate | diff | comparison | revisions
--- 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.
 
--- 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
--- 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;
+}
--- 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);

mercurial