diff --git a/builtin/builtin.mbti b/builtin/builtin.mbti index 0167e70c0..792473ecc 100644 --- a/builtin/builtin.mbti +++ b/builtin/builtin.mbti @@ -326,6 +326,7 @@ impl Map { set[K : Hash + Eq, V](Self[K, V], K, V) -> Unit size[K, V](Self[K, V]) -> Int to_array[K, V](Self[K, V]) -> Array[(K, V)] + update[K : Hash + Eq, V](Self[K, V], K, (V?) -> V) -> V values[K, V](Self[K, V]) -> Iter[V] } impl[K, V] Default for Map[K, V] diff --git a/builtin/iter.mbt b/builtin/iter.mbt index ee211f938..df1d426ab 100644 --- a/builtin/iter.mbt +++ b/builtin/iter.mbt @@ -1029,10 +1029,13 @@ pub fn Iter::group_by[T, K : Eq + Hash]( let result = Map::new() for element in self { let key = f(element) - match result.get(key) { - Some(arr) => result.set(key, arr + [element]) - None => result.set(key, [element]) - } + let _ = result.update(key, fn(arr) { + match arr { + Some(existing) => existing + [element] + None => [element] + } + }) + } result } diff --git a/builtin/linked_hash_map.mbt b/builtin/linked_hash_map.mbt index 185224ee2..264eb1fb4 100644 --- a/builtin/linked_hash_map.mbt +++ b/builtin/linked_hash_map.mbt @@ -262,6 +262,59 @@ pub fn Map::get_or_init[K : Hash + Eq, V]( } } +///| +/// Updates the value associated with a key in the map using a function. +/// +/// This method applies a function to the current value associated with the key +/// (or `None` if the key does not exist) and sets the key to the result. +/// +/// # Parameters +/// +/// * `self` - The map to update. +/// * `key` - The key whose value will be updated. +/// * `f` - A function that takes the current value (or `None`) and returns the new value. +/// +/// # Returns +/// +/// The new value associated with the key after the update. +/// +/// # Example +/// +/// ```moonbit +/// test "Map::update" { +/// let map = { "a": 1, "b": 2 } +/// +/// // Update an existing key +/// let result1 = map.update("a", fn(v) { +/// match v { +/// Some(val) => val * 2 +/// None => 0 +/// } +/// }) +/// assert_eq!(result1, 2) +/// assert_eq!(map.get("a"), Some(2)) +/// +/// // Update a non-existing key +/// let result2 = map.update("c", fn(v) { +/// match v { +/// Some(val) => val +/// None => 42 +/// } +/// }) +/// assert_eq!(result2, 42) +/// assert_eq!(map.get("c"), Some(42)) +/// } +/// ``` +pub fn Map::update[K : Hash + Eq, V]( + self : Map[K, V], + key : K, + f : (V?) -> V +) -> V { + let value = f(self.get(key)) + self.set(key, value) + value +} + ///| /// Check if the hash map contains a key. pub fn Map::contains[K : Hash + Eq, V](self : Map[K, V], key : K) -> Bool { diff --git a/builtin/linked_hash_map_wbtest.mbt b/builtin/linked_hash_map_wbtest.mbt index 5b0b36c0e..19058909a 100644 --- a/builtin/linked_hash_map_wbtest.mbt +++ b/builtin/linked_hash_map_wbtest.mbt @@ -887,3 +887,130 @@ test "op_equal_key_value_mismatch" { // This should return false as the values are different assert_eq!(map1 == map2, false) } + +///| +test "Map::update basic functionality" { + let map = { "a": 1, "b": 2 } + let _ = map.update("a", fn(v) { + match v { + Some(val) => val * 2 + None => 0 + } + }) + assert_eq!(map.get("a"), Some(2)) + let _ = map.update("c", fn(v) { + match v { + Some(val) => val + None => 42 + } + }) + assert_eq!(map.get("c"), Some(42)) +} + +///| +test "Map::update with complex operations" { + let map = { "counter": 0 } + let _ = map.update("counter", fn(v) { + match v { + Some(val) => val + 1 + None => 1 + } + }) + assert_eq!(map.get("counter"), Some(1)) + + // Increment again + let _ = map.update("counter", fn(v) { + match v { + Some(val) => val + 1 + None => 1 + } + }) + assert_eq!(map.get("counter"), Some(2)) + + // Reset counter + let _ = map.update("counter", fn(_) { 0 }) + assert_eq!(map.get("counter"), Some(0)) +} + +///| +test "Map::update with different value types" { + let map : Map[String, Array[Int]] = {} + let res1 = map.update("numbers", fn(v) { + match v { + Some(arr) => { + let new_arr = [] + for i in arr { + new_arr.push(i) + } + new_arr.push(1) + new_arr + } + None => [1] + } + }) + assert_eq!(res1[0], 1) + assert_eq!(map.get("numbers").unwrap().length(), 1) + + // Add more elements + let res2 = map.update("numbers", fn(v) { + match v { + Some(arr) => { + let new_arr = [] + for i in arr { + new_arr.push(i) + } + new_arr.push(2) + new_arr.push(3) + new_arr + } + None => [2, 3] + } + }) + assert_eq!(res2, [1, 2, 3]) + assert_eq!(map.get("numbers").unwrap().length(), 3) +} + +///| +test "Map::update with multiple operations" { + let map = {} + + // Initialize multiple keys + let _ = map.update("a", fn(_) { 1 }) + let _ = map.update("b", fn(_) { 2 }) + let _ = map.update("c", fn(_) { 3 }) + assert_eq!(map.size(), 3) + assert_eq!(map.get("a"), Some(1)) + assert_eq!(map.get("b"), Some(2)) + assert_eq!(map.get("c"), Some(3)) + + // Update all values at once + for k in ["a", "b", "c"] { + let _ = map.update(k, fn(v) { + match v { + Some(val) => val * 10 + None => 0 + } + }) + + } + assert_eq!(map.get("a"), Some(10)) + assert_eq!(map.get("b"), Some(20)) + assert_eq!(map.get("c"), Some(30)) +} + +///| +test "update preserves order" { + let map = {} + + // Add keys in specific order + let _ = map.update("first", fn(_) { 1 }) + let _ = map.update("second", fn(_) { 2 }) + let _ = map.update("third", fn(_) { 3 }) + + // Update middle key + let _ = map.update("second", fn(_) { 22 }) + + // Check that order is preserved + let keys = map.keys().collect() + assert_eq!(keys, ["first", "second", "third"]) +}