001/*
002 * MIT License
003 *
004 * Copyright (c) 2023 Michael Cowan
005 *
006 * Permission is hereby granted, free of charge, to any person obtaining a copy
007 * of this software and associated documentation files (the "Software"), to deal
008 * in the Software without restriction, including without limitation the rights
009 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
010 * copies of the Software, and to permit persons to whom the Software is
011 * furnished to do so, subject to the following conditions:
012 *
013 * The above copyright notice and this permission notice shall be included in all
014 * copies or substantial portions of the Software.
015 *
016 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
017 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
018 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
019 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
020 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
021 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
022 * SOFTWARE.
023 */
024
025package io.blt.util;
026
027import io.blt.util.functional.ThrowingFunction;
028import io.blt.util.functional.ThrowingSupplier;
029import java.util.HashMap;
030import java.util.Map;
031
032import static io.blt.util.Obj.newInstanceOf;
033import static java.util.Objects.nonNull;
034
035/**
036 * Static utility methods for operating on implementations of {@code Collection} and {@code Map} i.e., Containers.
037 * <p>
038 * For methods that return a modification of a passed container, the result will be of the same type if possible.
039 * This is accomplished using {@link Obj#newInstanceOf(Object)} and its limitations apply.
040 * </p>
041 */
042public final class Ctr {
043
044    private Ctr() {
045        throw new IllegalAccessError("Utility class should be accessed statically and never constructed");
046    }
047
048    /**
049     * Returns a new {@link Map} containing the entries of {@code source} with {@code transform} applied to the values.
050     * <p>
051     * If possible, the result is of the same type as the passed {@code source} map.
052     * </p>
053     * e.g.,
054     * <pre>{@code
055     * var scores = Map.of("Louis", 95, "Greg", 92, "Mike", 71, "Phil", 86);
056     * var grades = Ctr.transformValues(scores, score -> {
057     *     if (score >= 90) {
058     *         return "A";
059     *     } else if (score >= 80) {
060     *         return "B";
061     *     } else if (score >= 70) {
062     *         return "C";
063     *     } else if (score >= 60) {
064     *         return "D";
065     *     } else {
066     *         return "F";
067     *     }
068     * });
069     * // grades = Map.of("Louis", "A", "Greg", "A", "Mike", "C", "Phil", "B")
070     * }</pre>
071     *
072     * @param source    {@link Map} whose values should be transformed
073     * @param transform value transformation function
074     * @param <K>       {@code map} key type
075     * @param <V>       {@code map} value type
076     * @param <R>       returned map value type
077     * @param <E>       type of {@code transform} throwable
078     * @return a new {@link Map} containing the entries of {@code source} with {@code transform} applied to the values
079     * @see Obj#newInstanceOf(Object)
080     */
081    @SuppressWarnings("unchecked")
082    public static <K, V, R, E extends Throwable> Map<K, R> transformValues(
083            Map<K, V> source, ThrowingFunction<? super V, R, E> transform) throws E {
084        var result = newInstanceOf((Map<K, R>) source).orElse(new DefaultMap<>());
085
086        for (var entry : source.entrySet()) {
087            result.put(entry.getKey(), transform.apply(entry.getValue()));
088        }
089
090        return result instanceof DefaultMap ? Map.copyOf(result) : result;
091    }
092
093    /**
094     * For the specified {@code map}, if there is no value for the specified {@code key} then {@code compute} will be
095     * called and the result entered into the map. If a value is present, then it is returned.
096     * e.g.,
097     * <pre>{@code
098     * private final Map<URL, String> cache = new HashMap<>();
099     *
100     * public String fetch(URL url) throws IOException {
101     *     return Ctr.computeIfAbsent(cache, url, this::get);
102     * }
103     *
104     * private String get(URL url) throws IOException {
105     *     try (var stream = url.openStream()) {
106     *         return new String(stream.readAllBytes(), StandardCharsets.UTF_8);
107     *     }
108     * }
109     * }</pre>
110     *
111     * <p>If {@code compute} returns {@code null}, then the map is not modified and {@code null} is returned.
112     *
113     * <p>If {@code compute} throws, then map is not modified and the exception will bubble up.
114     *
115     * @param map     {@link Map} whose value is returned and may be computed
116     * @param key     key with which the specified value is to be associated
117     * @param compute the computation function to use if the value is absent
118     * @param <K>     {@code map} key type
119     * @param <V>     {@code map} value type
120     * @param <E>     type of {@code compute} throwable
121     * @return the existing or computed value associated with the key, or null if the computed value is null
122     * @throws E if an exception is thrown by the computation function
123     * @implNote The implementation is equivalent to the following:
124     * <pre> {@code
125     * if (map.get(key) == null) {
126     *     V value = compute.apply(key);
127     *     if (value != null) {
128     *         map.put(key, value);
129     *     }
130     * }
131     * return map.get(key);
132     * }</pre>
133     * @see Ctr#computeIfAbsent(Map, Object, ThrowingSupplier)
134     */
135    public static <K, V, E extends Throwable> V computeIfAbsent(
136            Map<K, V> map, K key, ThrowingFunction<? super K, ? extends V, E> compute) throws E {
137        return Obj.orElseGet(map.get(key), () -> {
138            var value = compute.apply(key);
139            if (nonNull(value)) {
140                map.put(key, value);
141            }
142            return value;
143        });
144    }
145
146    /**
147     * For the specified {@code map}, if there is no value for the specified {@code key} then {@code compute} will be
148     * called and the result entered into the map. If a value is present, then it is returned.
149     * e.g.,
150     * <pre>{@code
151     * private final Map<URL, String> cache = new HashMap<>();
152     *
153     * public String fetch(URL url) throws IOException {
154     *     return Ctr.computeIfAbsent(cache, url, () -> {
155     *         try (var stream = url.openStream()) {
156     *             return new String(stream.readAllBytes(), StandardCharsets.UTF_8);
157     *         }
158     *     });
159     * }
160     * }</pre>
161     *
162     * <p>If {@code compute} returns {@code null}, then the map is not modified and {@code null} is returned.
163     *
164     * <p>If {@code compute} throws, then map is not modified and the exception will bubble up.
165     *
166     * @param map     {@link Map} whose value is returned and may be computed
167     * @param key     key with which the specified value is to be associated
168     * @param compute the computation supplier to use if the value is absent
169     * @param <K>     {@code map} key type
170     * @param <V>     {@code map} value type
171     * @param <E>     type of {@code compute} throwable
172     * @return the existing or computed value associated with the key, or null if the computed value is null
173     * @throws E if an exception is thrown by the computation supplier
174     * @implNote The implementation is equivalent to the following:
175     * <pre> {@code
176     * if (map.get(key) == null) {
177     *     V value = compute.apply(key);
178     *     if (value != null) {
179     *         map.put(key, value);
180     *     }
181     * }
182     * return map.get(key);
183     * }</pre>
184     * @see Ctr#computeIfAbsent(Map, Object, ThrowingFunction)
185     */
186    public static <K, V, E extends Throwable> V computeIfAbsent(
187            Map<K, V> map, K key, ThrowingSupplier<? extends V, E> compute) throws E {
188        return computeIfAbsent(map, key, unused -> compute.get());
189    }
190
191    private static final class DefaultMap<K, V> extends HashMap<K, V> {}
192
193}