Skip to content

Commit f4a8682

Browse files
committed
Fixed streaming to allow values to be maps. Using simpler implementation, added more tests
1 parent eaaa082 commit f4a8682

File tree

3 files changed

+248
-82
lines changed

3 files changed

+248
-82
lines changed
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
package org.unicode.cldr.util;
2+
3+
import java.util.Iterator;
4+
import java.util.Map;
5+
import java.util.Map.Entry;
6+
import java.util.Spliterator;
7+
import java.util.function.Consumer;
8+
9+
/**
10+
* Optimized Spliterator for fixed-depth nested maps. Returns a Stream of Object[] where: [0...N-1]
11+
* are Keys [N] is the Value. Doesn't parallelize.
12+
*/
13+
public class FlatArraySpliterator implements Spliterator<Object[]> {
14+
private final int keyCount;
15+
private final Iterator<Entry<?, ?>>[] stack;
16+
private final Object[] buffer; // Keys + 1 slot for Value
17+
private int remainingKeys = 0;
18+
19+
@SuppressWarnings("unchecked")
20+
public FlatArraySpliterator(Map<?, ?> root, int keyCount) {
21+
this.keyCount = keyCount;
22+
this.stack = (Iterator<Entry<?, ?>>[]) new Iterator[keyCount];
23+
this.buffer = new Object[keyCount + 1];
24+
this.stack[0] = (Iterator<Entry<?, ?>>) (Iterator<?>) root.entrySet().iterator();
25+
// very ugly cast but works
26+
}
27+
28+
@Override
29+
@SuppressWarnings("unchecked")
30+
public boolean tryAdvance(Consumer<? super Object[]> action) {
31+
while (remainingKeys >= 0) {
32+
Iterator<Entry<?, ?>> it = stack[remainingKeys];
33+
34+
if (!it.hasNext()) {
35+
remainingKeys--;
36+
continue;
37+
}
38+
39+
Entry<?, ?> entry = it.next();
40+
buffer[remainingKeys] = entry.getKey(); // Place key in its specific slot
41+
42+
if (remainingKeys < keyCount - 1) {
43+
// Navigate deeper: Cast is safe per user guarantee
44+
stack[++remainingKeys] =
45+
(Iterator<Entry<?, ?>>)
46+
(Iterator<?>) ((Map<?, ?>) entry.getValue()).entrySet().iterator();
47+
// very ugly cast but works
48+
} else {
49+
// Leaf reached: Place value in the final slot
50+
buffer[keyCount] = entry.getValue();
51+
52+
// Clone creates a snapshot [K1, K2, ... KN, V]
53+
action.accept(buffer.clone());
54+
return true;
55+
}
56+
}
57+
return false;
58+
}
59+
60+
@Override
61+
public Spliterator<Object[]> trySplit() {
62+
return null;
63+
}
64+
65+
@Override
66+
public long estimateSize() {
67+
return Long.MAX_VALUE;
68+
}
69+
70+
@Override
71+
public int characteristics() {
72+
return IMMUTABLE | NONNULL | DISTINCT;
73+
}
74+
}

tools/cldr-code/src/main/java/org/unicode/cldr/util/NestedMap.java

Lines changed: 101 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -2,18 +2,13 @@
22

33
import com.google.common.collect.MapDifference;
44
import com.google.common.collect.Maps;
5-
import java.util.ArrayDeque;
65
import java.util.ArrayList;
76
import java.util.Arrays;
8-
import java.util.Deque;
9-
import java.util.Iterator;
7+
import java.util.HashMap;
108
import java.util.List;
119
import java.util.Map;
1210
import java.util.Objects;
1311
import java.util.Set;
14-
import java.util.Spliterator;
15-
import java.util.Spliterators;
16-
import java.util.function.Consumer;
1712
import java.util.function.Supplier;
1813
import java.util.stream.Stream;
1914
import java.util.stream.StreamSupport;
@@ -57,7 +52,9 @@
5752
* <li>ConcurrentHashMap::new — for thread-safety across all levels
5853
* </ul>
5954
*
60-
* Future possible extensions:
55+
* NOTE: neither keys nor values can be null.
56+
*
57+
* <p>Future possible extensions:
6158
*
6259
* <ul>
6360
* <li>The current implementation is limited to what CLDR needs. Addition methods like contains()
@@ -95,20 +92,23 @@ public class NestedMap {
9592
*/
9693
@SafeVarargs
9794
private NestedMap(int keyCount, Supplier<Map<Object, Object>>... mapFactories) {
98-
if (mapFactories.length == 0) {
99-
throw new IllegalArgumentException("Too few mapFactories");
100-
} else if (mapFactories.length > keyCount) {
95+
if (mapFactories.length > keyCount) {
10196
throw new IllegalArgumentException("Too many mapFactories");
10297
}
10398
this.keyCount = keyCount;
10499
if (mapFactories.length == keyCount) {
105100
this.mapFactories = List.of(mapFactories);
106101
} else { // if the list is too short, fill out with copies of the last
107102
List<Supplier<Map<Object, Object>>> temp = new ArrayList<>();
108-
temp.addAll(Arrays.asList(mapFactories));
109-
Supplier<Map<Object, Object>> last = temp.get(temp.size() - 1);
103+
Supplier<Map<Object, Object>> toDuplicate;
104+
if (mapFactories.length == 0) {
105+
toDuplicate = HashMap::new;
106+
} else {
107+
temp.addAll(Arrays.asList(mapFactories));
108+
toDuplicate = temp.get(temp.size() - 1);
109+
}
110110
while (temp.size() < keyCount) {
111-
temp.add(last);
111+
temp.add(toDuplicate);
112112
}
113113
this.mapFactories = List.copyOf(temp);
114114
}
@@ -147,7 +147,7 @@ private Object getInternal(Object... keys) {
147147
current = ((Map<Object, Object>) current).get(key);
148148
if (current == null) return null;
149149
}
150-
return current;
150+
return keys.length == keyCount ? current : ((Map<Object, Object>) current).keySet();
151151
}
152152

153153
@SuppressWarnings("unchecked")
@@ -161,7 +161,7 @@ private void putInternal(Object... keysAndValue) {
161161
}
162162
for (Object keyOrValue : keysAndValue) {
163163
if (keyOrValue == null) {
164-
throw new IllegalArgumentException("Cannot store null value");
164+
throw new IllegalArgumentException("Cannot store null key or value");
165165
}
166166
}
167167

@@ -181,7 +181,7 @@ private void putInternal(Object... keysAndValue) {
181181
* Deeply removes a value at the end of a key chain and prunes empty parent maps. If the
182182
* keysAndPossibleValue is not a long as the keyCount, then prunes the end.
183183
*/
184-
public void removeInternal(Object... keysAndPossibleValue) {
184+
private void removeInternal(Object... keysAndPossibleValue) {
185185
if (this.mapFactories == null) { // attempt to modify immutable
186186
throw new UnsupportedOperationException("Cannot modify immutable object");
187187
}
@@ -277,67 +277,14 @@ private NestedMap toImmutable() {
277277
}
278278

279279
/**
280-
* Returns a LAZY stream of all terminal entries in the nested map. The stream is built
281-
* on-the-fly and does not store all entries in memory. Each entry is a list of objects, where
282-
* the last element is the value.
280+
* Returns a lazy stream of all logical entries in the nested map. Each entry is a list of
281+
* objects, where the last element is the value.
283282
*/
284283
private Stream<List<Object>> stream() {
285-
return StreamSupport.stream(new NestedMapSpliterator(this.root), false);
286-
}
287-
288-
/**
289-
* A custom Spliterator that traverses the nested map structure lazily. It uses a stack of
290-
* iterators to perform a depth-first traversal.
291-
*/
292-
private static class NestedMapSpliterator
293-
extends Spliterators.AbstractSpliterator<List<Object>> {
294-
private final Deque<Iterator<Map.Entry<Object, Object>>> iteratorStack = new ArrayDeque<>();
295-
private final Deque<Object> keyPath = new ArrayDeque<>();
296-
297-
NestedMapSpliterator(Map<Object, Object> root) {
298-
super(
299-
Long.MAX_VALUE,
300-
Spliterator.ORDERED); // Assumes ORDERED for TreeMap, harmless for others.
301-
if (root != null && !root.isEmpty()) {
302-
iteratorStack.push(root.entrySet().iterator());
303-
}
304-
}
305-
306-
@SuppressWarnings("unchecked")
307-
@Override
308-
public boolean tryAdvance(Consumer<? super List<Object>> action) {
309-
while (!iteratorStack.isEmpty()) {
310-
Iterator<Map.Entry<Object, Object>> currentIterator = iteratorStack.peek();
311-
312-
if (currentIterator.hasNext()) {
313-
Map.Entry<Object, Object> entry = currentIterator.next();
314-
Object key = entry.getKey();
315-
Object value = entry.getValue();
316-
317-
keyPath.addLast(key);
318-
319-
if (value instanceof Map) {
320-
// Go deeper: push the new map's iterator onto the stack.
321-
iteratorStack.push(((Map<Object, Object>) value).entrySet().iterator());
322-
} else {
323-
// Terminal value found: build the result and consume it.
324-
List<Object> result = new ArrayList<>(keyPath);
325-
result.add(value);
326-
action.accept(result);
327-
keyPath.removeLast(); // Backtrack path after consuming.
328-
return true;
329-
}
330-
} else {
331-
// This level is exhausted: backtrack.
332-
iteratorStack.pop();
333-
if (!keyPath.isEmpty()) {
334-
keyPath.removeLast();
335-
}
336-
}
337-
}
338-
// The entire map has been traversed.
339-
return false;
340-
}
284+
// Note: the FlatArraySpliterator is much simpler to understand, but has some limitations.
285+
// Notably, it doesn't parallelize.
286+
return StreamSupport.stream(new FlatArraySpliterator(root, keyCount), false)
287+
.map(array -> Arrays.asList(array));
341288
}
342289

343290
@SuppressWarnings("unchecked")
@@ -446,7 +393,17 @@ private Map2(NestedMap engine) {
446393
this.engine = engine;
447394
}
448395

449-
/** Takes Treemap::new, HashMap::new, ConcurrentHashMap::new, and other suppliers */
396+
/**
397+
* Create a nested map with 2 keys, and 0..2 suppliers
398+
*
399+
* @param suppliers Common ones are Treemap::new, HashMap::new, ConcurrentHashMap::new, etc.
400+
* The default is a HashMap::new. If the number of suppliers is not insufficient, the
401+
* last supplier is used to fill it out.
402+
* <p>Examples So Map2.create(TreeMap::new) is equivalent to Map2.create(TreeMap::new,
403+
* TreeMap::new)<br>
404+
* So Map2.create() is equivalent to Map2.create(HashMap::new, HashMap::new)
405+
* @return
406+
*/
450407
@SafeVarargs
451408
public static <K1, K2, V> Map2<K1, K2, V> create(
452409
Supplier<Map<Object, Object>>... suppliers) {
@@ -476,10 +433,22 @@ public V get(K1 key1, K2 key2) {
476433
return (V) engine.getInternal(key1, key2);
477434
}
478435

436+
public Set<K2> keySet2(K1 key1) {
437+
return (Set<K2>) engine.getInternal(key1);
438+
}
439+
440+
public Set<K2> keySet() {
441+
return (Set<K2>) engine.root.keySet();
442+
}
443+
479444
public void put(K1 key1, K2 key2, V value) {
480445
engine.putInternal(key1, key2, value);
481446
}
482447

448+
public void put(Entry3<K1, K2, V> entry3) {
449+
engine.putInternal(entry3.getKey1(), entry3.getKey2(), entry3.getValue());
450+
}
451+
483452
public void putAll(Map2<K1, K2, V> other) {
484453
// Might optimize later
485454
other.stream().forEach(x -> put(x.getKey1(), x.getKey2(), x.getValue()));
@@ -527,7 +496,17 @@ private Map3(NestedMap engine) {
527496
this.engine = engine;
528497
}
529498

530-
/** Takes Treemap::new, HashMap::new, ConcurrentHashMap::new, and other suppliers */
499+
/**
500+
* Create a nested map with 3 keys, and 0..3 Suppliers
501+
*
502+
* @param suppliers Common ones are Treemap::new, HashMap::new, ConcurrentHashMap::new, etc.
503+
* The default is a HashMap::new. If the number of suppliers is not insufficient, the
504+
* last supplier is used to fill it out.
505+
* <p>Examples So Map3.create(TreeMap::new) is equivalent to Map2.create(TreeMap::new,
506+
* TreeMap::new, TreeMap::new)<br>
507+
* So Map3.create() is equivalent to Map2.create(HashMap::new, HashMap::new,
508+
* HashMap::new)
509+
*/
531510
@SafeVarargs
532511
public static <K1, K2, K3, V> Map3<K1, K2, K3, V> create(
533512
Supplier<Map<Object, Object>>... suppliers) {
@@ -538,10 +517,27 @@ public V get(K1 key1, K2 key2, K3 key3) {
538517
return (V) engine.getInternal(key1, key2, key3);
539518
}
540519

520+
public Set<K3> keySet3(K1 key1, K2 key2) {
521+
return (Set<K3>) engine.getInternal(key1, key2);
522+
}
523+
524+
public Set<K2> keySet2(K1 key1) {
525+
return (Set<K2>) engine.getInternal(key1);
526+
}
527+
528+
public Set<K2> keySet() {
529+
return (Set<K2>) engine.root.keySet();
530+
}
531+
541532
public void put(K1 key1, K2 key2, K3 key3, V value) {
542533
engine.putInternal(key1, key2, key3, value);
543534
}
544535

536+
public void put(Entry4<K1, K2, K3, V> entry3) {
537+
engine.putInternal(
538+
entry3.getKey1(), entry3.getKey2(), entry3.getKey3(), entry3.getValue());
539+
}
540+
545541
public void remove(K1 key1, K2 key2, K3 key3, V value) {
546542
engine.removeInternal(key1, key2, key3, value);
547543
}
@@ -615,6 +611,18 @@ private Multimap2(NestedMap engine) {
615611
* Takes Treemap::new, HashMap::new, ConcurrentHashMap::new, and other suppliers. Note: the
616612
* final supplier should be one suitable for producing Map<V, Boolean>.
617613
*/
614+
/**
615+
* Create a multimap map with 2 keys, and 0..3 suppliers
616+
*
617+
* @param suppliers Common ones are Treemap::new, HashMap::new, ConcurrentHashMap::new, etc.
618+
* The default is a HashMap::new. If the number of suppliers is not insufficient, the
619+
* last supplier is used to fill it out.
620+
* <p>Examples So Multimap2.create(TreeMap::new) is equivalent to
621+
* Multimap2.create(TreeMap::new, TreeMap::new, TreeMap::new)<br>
622+
* So Multimap2.create() is equivalent to Map2.create(HashMap::new, HashMap::new,
623+
* HashMap::new)<br>
624+
* Note: the final supplier should be one suitable for producing Map<V, Boolean>
625+
*/
618626
@SafeVarargs
619627
public static <K1, K2, V> Multimap2<K1, K2, V> create(
620628
Supplier<Map<Object, Object>>... suppliers) {
@@ -625,14 +633,27 @@ public void put(K1 key1, K2 key2, V value) {
625633
engine.putInternal(key1, key2, value, Boolean.TRUE);
626634
}
627635

636+
public void put(Entry3<K1, K2, V> entry3) {
637+
engine.putInternal(entry3.getKey1(), entry3.getKey2(), entry3.getValue(), Boolean.TRUE);
638+
}
639+
628640
@SuppressWarnings("unchecked")
629641
public Set<V> get(K1 key1, K2 key2) {
630-
Map<V, Boolean> map = (Map<V, Boolean>) engine.getInternal(key1, key2);
631-
return map == null ? null : map.keySet();
642+
return (Set<V>) engine.getInternal(key1, key2);
643+
}
644+
645+
@SuppressWarnings("unchecked")
646+
public Set<K2> keySet2(K1 key1) {
647+
return (Set<K2>) engine.getInternal(key1);
648+
}
649+
650+
@SuppressWarnings("unchecked")
651+
public Set<K1> keySet() {
652+
return (Set<K1>) engine.root.keySet();
632653
}
633654

634655
public boolean contains(K1 key1, K2 key2, V value) {
635-
return engine.getInternal(key1, key2) != null;
656+
return engine.getInternal(key1, key2, value) != null;
636657
}
637658

638659
public void remove(K1 key1, K2 key2, V value) {

0 commit comments

Comments
 (0)