package com.onthegomap.planetiler.collection; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; import java.util.ArrayList; import java.util.Comparator; import java.util.List; import java.util.NoSuchElementException; import java.util.Random; import java.util.function.Supplier; import java.util.stream.LongStream; import java.util.stream.Stream; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.CsvSource; import org.junit.jupiter.params.provider.ValueSource; class LongMergerTest { record Item(long key, int secondary) implements HasLongSortKey, Comparable { @Override public int compareTo(Item o) { int cmp = Long.compare(key, o.key); if (cmp == 0) { cmp = Integer.compare(secondary, o.secondary); } return cmp; } long value() { return key + secondary; } } record ItemList(List items) {} private static ItemList list(boolean primaryKey, long... items) { return new ItemList( LongStream.of(items).mapToObj(i -> primaryKey ? new Item(i, 0) : new Item(0, (int) i)).toList()); } private static List merge(ItemList... lists) { List list = new ArrayList<>(); var iter = LongMerger.mergeIterators(Stream.of(lists) .map(d -> d.items.iterator()) .toList(), Comparator.naturalOrder()); iter.forEachRemaining(item -> list.add(item.value())); assertThrows(NoSuchElementException.class, iter::next); return list; } @Test void testMergeEmpty() { assertEquals(List.of(), merge()); } @ParameterizedTest @ValueSource(booleans = {true, false}) void testMergeSupplier(boolean primaryKey) { List list = new ArrayList<>(); var iter = LongMerger.mergeSuppliers(Stream.of(new ItemList[]{list(primaryKey, 1, 2)}) .map(d -> d.items.iterator()) .>map(d -> () -> { try { return d.next(); } catch (NoSuchElementException e) { return null; } }) .toList(), Comparator.naturalOrder()); iter.forEachRemaining(item -> list.add(item.value())); assertThrows(NoSuchElementException.class, iter::next); assertEquals(List.of(1L, 2L), list); } @ParameterizedTest @ValueSource(booleans = {true, false}) void testMerge1(boolean primaryKey) { assertEquals(List.of(), merge(list(primaryKey))); assertEquals(List.of(1L), merge(list(primaryKey, 1))); assertEquals(List.of(1L, 2L), merge(list(primaryKey, 1, 2))); } @ParameterizedTest @CsvSource(value = { ",,", "1,,1", "1,1,1 1", "1 2,,1 2", "1 2,2 3,1 2 2 3", "1,2,1 2", "1 2,3,1 2 3", "1 3,2,1 2 3", }, nullValues = {"null"}) void testMerge2(String a, String b, String output) { for (boolean primaryKey : List.of(false, true)) { var listA = list(primaryKey, parse(a)); var listB = list(primaryKey, parse(b)); assertEquals( LongStream.of(parse(output)).boxed().toList(), merge(listA, listB), "primary=" + primaryKey ); assertEquals( LongStream.of(parse(output)).boxed().toList(), merge(listB, listA), "primary=" + primaryKey ); } } @ParameterizedTest @CsvSource(value = { ",,,", "1,,,1", "1,1,1,1 1 1", "1 2,,,1 2", "1 2,2 3,,1 2 2 3", "1,2,,1 2", "1,2,3,1 2 3", "1 2,3,4,1 2 3 4", "1 3,2,4,1 2 3 4", }, nullValues = {""}) void testMerge3(String a, String b, String c, String output) { for (boolean primaryKey : List.of(false, true)) { var listA = list(primaryKey, parse(a)); var listB = list(primaryKey, parse(b)); var listC = list(primaryKey, parse(c)); assertEquals( LongStream.of(parse(output)).boxed().toList(), merge(listA, listB, listC), "ABC primary=" + primaryKey ); assertEquals( LongStream.of(parse(output)).boxed().toList(), merge(listA, listC, listB), "ACB primary=" + primaryKey ); assertEquals( LongStream.of(parse(output)).boxed().toList(), merge(listB, listA, listC), "BAC primary=" + primaryKey ); assertEquals( LongStream.of(parse(output)).boxed().toList(), merge(listB, listC, listA), "BCA primary=" + primaryKey ); assertEquals( LongStream.of(parse(output)).boxed().toList(), merge(listC, listA, listB), "CAB primary=" + primaryKey ); assertEquals( LongStream.of(parse(output)).boxed().toList(), merge(listC, listB, listA), "CBA primary=" + primaryKey ); } } @ParameterizedTest @CsvSource(value = { ",,,,", "1,,,,1", "1,1,1,1,1 1 1 1", "1 2,,,,1 2", "1 2,3,,,1 2 3", "1 3,2,,,1 2 3", "1 3,2 4,,,1 2 3 4", "1 5,2 4,,,1 2 4 5", "1 2,2 3,,,1 2 2 3", }, nullValues = {""}) void testMerge4(String a, String b, String c, String d, String output) { for (boolean primaryKey : List.of(false, true)) { var listA = list(primaryKey, parse(a)); var listB = list(primaryKey, parse(b)); var listC = list(primaryKey, parse(c)); var listD = list(primaryKey, parse(d)); assertEquals( LongStream.of(parse(output)).boxed().toList(), merge(listA, listB, listC, listD), "ABCD primary=" + primaryKey ); assertEquals( LongStream.of(parse(output)).boxed().toList(), merge(listB, listA, listC, listD), "BACD primary=" + primaryKey ); assertEquals( LongStream.of(parse(output)).boxed().toList(), merge(listB, listC, listA, listD), "BCAD primary=" + primaryKey ); assertEquals( LongStream.of(parse(output)).boxed().toList(), merge(listB, listC, listD, listA), "BCDA primary=" + primaryKey ); } } @ParameterizedTest @ValueSource(ints = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 20, 100}) void stressTest(int n) { int items = 10; int maxKey = 20; var random = new Random(0); List> featureLists = new ArrayList<>(); for (int i = 0; i < n; i++) { List list = new ArrayList<>(); featureLists.add(list); for (int j = 0; j < items; j++) { byte[] bytes = new byte[]{(byte) random.nextInt(256)}; random.nextBytes(bytes); list.add(new SortableFeature(random.nextLong(maxKey), bytes)); } list.sort(Comparator.naturalOrder()); } var iter = LongMerger.mergeIterators(featureLists.stream().map(List::iterator).toList(), SortableFeature.COMPARE_BYTES); var last = iter.next(); int i = 1; while (iter.hasNext()) { i++; var item = iter.next(); assertTrue(last.compareTo(item) <= 0, "items out of order i=" + i + " lists=" + n + " last=" + last + " item=" + item); last = item; } } private static long[] parse(String in) { return in == null ? new long[0] : Stream.of(in.split("\\s+")) .map(String::strip) .filter(d -> !d.isBlank()) .mapToLong(Long::parseLong) .toArray(); } }