You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@groovy.apache.org by pa...@apache.org on 2022/02/26 11:25:02 UTC

[groovy] branch master updated (9bbbc78 -> 77039ed)

This is an automated email from the ASF dual-hosted git repository.

paulk pushed a change to branch master
in repository https://gitbox.apache.org/repos/asf/groovy.git.


    from 9bbbc78  GROOVY-10511: Bump json-unit (test dependency) version to 2.32.0
     new 00729e7  GROOVY-7802: MapWithDefault should have ability to not store the default value (test)
     new 77039ed  GROOVY-7802: MapWithDefault should be able to be configured to not store its default value

The 2 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.


Summary of changes:
 src/main/java/groovy/lang/MapWithDefault.java      | 83 +++++++++++++++++++---
 .../groovy/runtime/DefaultGroovyMethods.java       | 55 +++++++++++++-
 src/test/groovy/MapTest.groovy                     | 18 +++++
 3 files changed, 144 insertions(+), 12 deletions(-)

[groovy] 01/02: GROOVY-7802: MapWithDefault should have ability to not store the default value (test)

Posted by pa...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

paulk pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/groovy.git

commit 00729e7eb423b5c415bb09889c56203cfc034c07
Author: Pap LÅ‘rinc <pa...@yahoo.com>
AuthorDate: Sun Feb 21 19:16:40 2016 +0200

    GROOVY-7802: MapWithDefault should have ability to not store the default value (test)
    
    Please see https://github.com/careercup/CtCI-6th-Edition-Groovy/blob/d116d65469bdf17d1e215e89f3e76ac3a97660a9/src/main/groovy/Ch04_TreesAndGraphs/_04_12_PathsWithSum.groovy#L21 for valid usecases
---
 src/test/groovy/MapTest.groovy | 18 ++++++++++++++++++
 1 file changed, 18 insertions(+)

diff --git a/src/test/groovy/MapTest.groovy b/src/test/groovy/MapTest.groovy
index 4738099..9810730 100644
--- a/src/test/groovy/MapTest.groovy
+++ b/src/test/groovy/MapTest.groovy
@@ -355,4 +355,22 @@ final class MapTest extends GroovyTestCase {
             default: assert true
         }
     }
+
+    void testMapWithDefaultCanBeConfiguredToNotStoreDefaultValue() {
+        def defaultValue = 0
+        def m = [:].withDefault(false, true) { defaultValue }
+        assert m.isEmpty()
+
+        m[1] = defaultValue
+        assert m.isEmpty()
+
+        m[1] = defaultValue + 1
+        assert !m.isEmpty()
+
+        m[1]--
+        assert m.isEmpty()
+
+        m.putAll([a:0, b:1, c:0])
+        assert m.size() == 1
+    }
 }

[groovy] 02/02: GROOVY-7802: MapWithDefault should be able to be configured to not store its default value

Posted by pa...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

paulk pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/groovy.git

commit 77039ed8288c0b3612a0126d4d49bb71e951ff8b
Author: Paul King <pa...@asert.com.au>
AuthorDate: Fri Feb 25 17:50:41 2022 +1000

    GROOVY-7802: MapWithDefault should be able to be configured to not store its default value
---
 src/main/java/groovy/lang/MapWithDefault.java      | 83 +++++++++++++++++++---
 .../groovy/runtime/DefaultGroovyMethods.java       | 55 +++++++++++++-
 2 files changed, 126 insertions(+), 12 deletions(-)

diff --git a/src/main/java/groovy/lang/MapWithDefault.java b/src/main/java/groovy/lang/MapWithDefault.java
index ecd135d..405f8ce 100644
--- a/src/main/java/groovy/lang/MapWithDefault.java
+++ b/src/main/java/groovy/lang/MapWithDefault.java
@@ -23,22 +23,51 @@ import java.util.Map;
 import java.util.Set;
 
 /**
- * A wrapper for Map which allows a default value to be specified.
+ * A wrapper for Map which allows a default value to be specified using a closure.
+ * Normally not instantiated directly but used via the DGM <code>withDefault</code> method.
  *
  * @since 1.7.1
  */
 public final class MapWithDefault<K, V> implements Map<K, V> {
 
     private final Map<K, V> delegate;
-    private final Closure initClosure;
+    private final Closure<V> initClosure;
+    private final boolean autoGrow;
+    private final boolean autoShrink;
 
-    private MapWithDefault(Map<K, V> m, Closure initClosure) {
+    private MapWithDefault(Map<K, V> m, Closure<V> initClosure, boolean autoGrow, boolean autoShrink) {
         delegate = m;
         this.initClosure = initClosure;
-    }
-
-    public static <K, V> Map<K, V> newInstance(Map<K, V> m, Closure initClosure) {
-        return new MapWithDefault<K, V>(m, initClosure);
+        this.autoGrow = autoGrow;
+        this.autoShrink = autoShrink;
+    }
+
+    /**
+     * Decorates the given Map allowing a default value to be specified.
+     *
+     * @param m           a Map to wrap
+     * @param initClosure the closure which when passed the <code>key</code> returns the default value
+     * @return the wrapped Map
+     */
+    public static <K, V> Map<K, V> newInstance(Map<K, V> m, Closure<V> initClosure) {
+        return new MapWithDefault<>(m, initClosure, true, false);
+    }
+
+    /**
+     * Decorates the given Map allowing a default value to be specified.
+     * Allows the behavior to be configured using {@code autoGrow} and {@code autoShrink} parameters.
+     * The value of {@code autoShrink} doesn't alter any values in the initial wrapped map, but you
+     * can start with an empty map and use {@code putAll} if you really need the minimal backing map value.
+     *
+     * @param m           a Map to wrap
+     * @param autoGrow    when true, also mutate the map adding in this value; otherwise, don't mutate the map, just return to calculated value
+     * @param autoShrink  when true, ensure the key will be removed if attempting to store the default value using put or putAll
+     * @param initClosure the closure which when passed the <code>key</code> returns the default value
+     * @return the wrapped Map
+     * @since 4.0.1
+     */
+    public static <K, V> Map<K, V> newInstance(Map<K, V> m, boolean autoGrow, boolean autoShrink, Closure<V> initClosure) {
+        return new MapWithDefault<>(m, initClosure, autoGrow, autoShrink);
     }
 
     @Override
@@ -61,16 +90,48 @@ public final class MapWithDefault<K, V> implements Map<K, V> {
         return delegate.containsValue(value);
     }
 
+    /**
+     * Returns the value to which the specified key is mapped,
+     * or the default value as specified by the initializing closure
+     * if this map contains no mapping for the key.
+     *
+     * If <code>autoGrow</code> is true and the initializing closure is called,
+     * the map is modified to contain the new key and value so that the calculated
+     * value is effectively cached if needed again.
+     * Otherwise, the map will be unchanged.
+     */
     @Override
     public V get(Object key) {
-        if (!delegate.containsKey(key)) {
-            delegate.put((K)key, (V)initClosure.call(new Object[]{key}));
+        if (delegate.containsKey(key)) {
+            return delegate.get(key);
+        }
+        V value = getDefaultValue(key);
+        if (autoGrow) {
+            delegate.put((K)key, value);
         }
-        return delegate.get(key);
+        return value;
     }
 
+    private V getDefaultValue(Object key) {
+        return initClosure.call(new Object[]{key});
+    }
+
+    /**
+     * Associates the specified value with the specified key in this map.
+     *
+     * If <code>autoShrink</code> is true, the initializing closure is called
+     * and if it evaluates to the value being stored, the value will not be stored
+     * and indeed any existing value will be removed. This can be useful when trying
+     * to keep the memory requirements small for large key sets where only a spare
+     * number of entries differ from the default.
+     *
+     * @return the previous value associated with {@code key} if any, otherwise {@code null}.
+     */
     @Override
     public V put(K key, V value) {
+        if (autoShrink && value.equals(getDefaultValue(key))) {
+            return remove(key);
+        }
         return delegate.put(key, value);
     }
 
@@ -81,7 +142,7 @@ public final class MapWithDefault<K, V> implements Map<K, V> {
 
     @Override
     public void putAll(Map<? extends K, ? extends V> m) {
-        delegate.putAll(m);
+        m.entrySet().forEach(e -> put(e.getKey(), e.getValue()));
     }
 
     @Override
diff --git a/src/main/java/org/codehaus/groovy/runtime/DefaultGroovyMethods.java b/src/main/java/org/codehaus/groovy/runtime/DefaultGroovyMethods.java
index 38eb499..e56062d 100644
--- a/src/main/java/org/codehaus/groovy/runtime/DefaultGroovyMethods.java
+++ b/src/main/java/org/codehaus/groovy/runtime/DefaultGroovyMethods.java
@@ -8701,6 +8701,7 @@ public class DefaultGroovyMethods extends DefaultGroovyMethodsSupport {
      * to <code>get(key)</code>. If an unknown key is found, a default value will be
      * stored into the Map before being returned. The default value stored will be the
      * result of calling the supplied Closure with the key as the parameter to the Closure.
+     *
      * Example usage:
      * <pre class="groovyTestCase">
      * def map = [a:1, b:2].withDefault{ k {@code ->} k.toCharacter().isLowerCase() ? 10 : -10 }
@@ -8716,9 +8717,61 @@ public class DefaultGroovyMethods extends DefaultGroovyMethodsSupport {
      * @param init a Closure which is passed the unknown key
      * @return the wrapped Map
      * @since 1.7.1
+     * @see #withDefault(Map, boolean, boolean, Closure)
      */
     public static <K, V> Map<K, V> withDefault(Map<K, V> self, @ClosureParams(FirstParam.FirstGenericType.class) Closure<V> init) {
-        return MapWithDefault.newInstance(self, init);
+        return MapWithDefault.newInstance(self, true, false, init);
+    }
+
+    /**
+     * Wraps a map using the decorator pattern with a wrapper that intercepts all calls
+     * to <code>get(key)</code> and <code>put(key, value)</code>.
+     * If an unknown key is found for <code>get</code>, a default value will be returned.
+     * The default value will be the result of calling the supplied Closure with the key
+     * as the parameter to the Closure.
+     * If <code>autoGrow</code> is set, the value will be stored into the map.
+     *
+     * If <code>autoShrink</code> is set, then an attempt to <code>put</code> the default value
+     * into the map is ignored and indeed any existing value would be removed.
+     *
+     * If you wish the backing map to be as small as possible, consider setting <code>autoGrow</code>
+     * to <code>false</code> and <code>autoShrink</code> to <code>true</code>.
+     * This keeps the backing map as small as possible, i.e. sparse, but also means that
+     * <code>containsKey</code>, <code>keySet</code>, <code>size</code>, and other methods
+     * will only reflect the sparse values.
+     *
+     * If you are wrapping an immutable map, you should set <code>autoGrow</code>
+     * and <code>autoShrink</code> to <code>false</code>.
+     * In this scenario, the <code>get</code> method is essentially a shorthand
+     * for calling <code>getOrDefault</code> with the default value supplied once as a Closure.
+     *
+     * Example usage:
+     * <pre class="groovyTestCase">
+     * // sparse map example
+     * def answers = [life: 100].withDefault(false, true){ 42 }
+     * assert answers.size() == 1
+     * assert answers.foo == 42
+     * assert answers.size() == 1
+     * answers.life = 42
+     * answers.putAll(universe: 42, everything: 42)
+     * assert answers.size() == 0
+     *
+     * // immutable map example
+     * def certainties = [death: true, taxes: true].asImmutable().withDefault(false, false){ false }
+     * assert certainties.size() == 2
+     * assert certainties.wealth == false
+     * assert certainties.size() == 2
+     * </pre>
+     *
+     * @param self a Map
+     * @param autoGrow whether calling get could potentially grow the map if the key isn't found
+     * @param autoShrink whether calling put with the default value could potentially shrink the map
+     * @param init a Closure which is passed the unknown key
+     * @return the wrapped Map
+     * @since 4.0.1
+     */
+    public static <K, V> Map<K, V> withDefault(Map<K, V> self, boolean autoGrow, boolean autoShrink, @ClosureParams(FirstParam.FirstGenericType.class) Closure<V> init) {
+        return MapWithDefault.newInstance(self, autoGrow, autoShrink, init);
     }
 
     /**