From ca862044142da39335c4eafa8458d1dc3cbe29dc Mon Sep 17 00:00:00 2001 From: fickludd Date: Sun, 26 Mar 2017 23:57:49 +0200 Subject: [PATCH] Added utility methods for sorted long array sets - intersect - symmetric difference - improved union to return sorted set --- .../neo4j/helpers/SortedLongArrayUtil.java | 217 +++++++++++++++--- .../helpers/SortedLongArrayUtilTest.java | 216 +++++++++++------ 2 files changed, 327 insertions(+), 106 deletions(-) diff --git a/community/kernel/src/main/java/org/neo4j/helpers/SortedLongArrayUtil.java b/community/kernel/src/main/java/org/neo4j/helpers/SortedLongArrayUtil.java index cd13dbc422933..0a9ca79906186 100644 --- a/community/kernel/src/main/java/org/neo4j/helpers/SortedLongArrayUtil.java +++ b/community/kernel/src/main/java/org/neo4j/helpers/SortedLongArrayUtil.java @@ -19,17 +19,17 @@ */ package org.neo4j.helpers; -import static java.util.Arrays.copyOf; - /** * Specialized methods for operations of sorted long arrays. */ public class SortedLongArrayUtil { + private static final long[] EMPTY = new long[]{}; + /** * Compute union of two sorted long arrays. - * @param left one sorted array - * @param right the other sorted array + * @param left a sorted array set + * @param right another sorted array set * @return the union, which is NOT sorted */ public static long[] union( long[] left, long[] right ) @@ -39,63 +39,210 @@ public static long[] union( long[] left, long[] right ) return left == null ? right : left; } - int missing = missing( left, right ); - if ( missing == 0 ) + long uniqueCounts = countUnique( left, right ); + if ( uniqueCounts == 0 || right( uniqueCounts ) == 0 ) { return left; } + if ( left( uniqueCounts ) == 0 ) + { + return right; + } - // An attempt to add the labels as efficiently as possible - long[] union = copyOf( left, left.length + missing ); + long[] union = new long[left.length + right( uniqueCounts )]; - int i = 0; - int cursor = left.length; - for ( long candidate : right ) + int cursor = 0; + int l = 0; + int r = 0; + while ( l < left.length && r < right.length ) { - while ( i < left.length && left[i] < candidate ) + if ( left[l] == right[r] ) + { + union[cursor++] = left[l]; + l++; + r++; + } + else if ( left[l] < right[r] ) { - i++; + union[cursor++] = left[l]; + l++; } - if ( i == left.length || candidate != left[i] ) + else { - union[cursor++] = candidate; - missing--; + union[cursor++] = right[r]; + r++; } } - assert missing == 0; + while ( l < left.length ) + { + union[cursor++] = left[l]; + l++; + } + while ( r < right.length ) + { + union[cursor++] = right[r]; + r++; + } + + assert cursor == union.length; return union; } /** - * Compute how many values in maybeMissing that are not in reference. - * @param reference sorted set - * @param maybeMissing sorted set - * @return number of missing values + * Compute intersect of two sorted long array sets. + * @param left a sorted array set + * @param right another sorted array set + * @return the union, which is NOT sorted + */ + public static long[] intersect( long[] left, long[] right ) + { + if ( left == null || right == null ) + { + return EMPTY; + } + + long uniqueCounts = countUnique( left, right ); + if ( uniqueCounts == 0 ) // complete intersection + { + return right; + } + if ( right( uniqueCounts ) == right.length || left( uniqueCounts ) == left.length ) // non-intersecting + { + return EMPTY; + } + + long[] intersect = new long[left.length - left( uniqueCounts )]; + + int cursor = 0; + int l = 0; + int r = 0; + while ( l < left.length && r < right.length ) + { + if ( left[l] == right[r] ) + { + intersect[cursor++] = left[l]; + l++; + r++; + } + else if ( left[l] < right[r] ) + { + l++; + } + else + { + r++; + } + } + + assert cursor == intersect.length; + return intersect; + } + + /** + * Compute the symmetric difference (set XOR basically) of two sorted long array sets. + * @param left a sorted array set + * @param right another sorted array set + * @return the union, which is NOT sorted + */ + public static long[] symmetricDifference( long[] left, long[] right ) + { + if ( left == null || right == null ) + { + return left == null ? right : left; + } + + long uniqueCounts = countUnique( left, right ); + if ( uniqueCounts == 0 ) // complete intersection + { + return EMPTY; + } + + long[] difference = new long[left( uniqueCounts ) + right( uniqueCounts )]; + + int cursor = 0; + int l = 0; + int r = 0; + while ( l < left.length && r < right.length ) + { + if ( left[l] == right[r] ) + { + l++; + r++; + } + else if ( left[l] < right[r] ) + { + difference[cursor++] = left[l]; + l++; + } + else + { + difference[cursor++] = right[r]; + r++; + } + } + while ( l < left.length ) + { + difference[cursor++] = left[l]; + l++; + } + while ( r < right.length ) + { + difference[cursor++] = right[r]; + r++; + } + + assert cursor == difference.length; + return difference; + } + + /** + * Compute the number of unique values in two sorted long array sets + * @param left a sorted array set + * @param right another sorted array set + * @return int pair packed into long */ - public static int missing( long[] reference, long[] maybeMissing ) + public static long countUnique( long[] left, long[] right ) { - int i = 0; - int j = 0; - int count = 0; - while ( i < reference.length && j < maybeMissing.length ) + int l = 0; + int r = 0; + int uniqueInLeft = 0; + int uniqueInRight = 0; + while ( l < left.length && r < right.length ) { - if ( reference[i] == maybeMissing[j] ) + if ( left[l] == right[r] ) { - i++; - j++; + l++; + r++; } - else if ( reference[i] < maybeMissing[j] ) + else if ( left[l] < right[r] ) { - i++; + uniqueInLeft++; + l++; } else { - count++; - j++; + uniqueInRight++; + r++; } } - count += maybeMissing.length - j; - return count; + uniqueInLeft += left.length - l; + uniqueInRight += right.length - r; + return intPair( uniqueInLeft, uniqueInRight ); + } + + public static long intPair( int left, int right ) + { + return (long)left << (Integer.BYTES * 8) | right; + } + + public static int left( long pair ) + { + return (int)(pair >> (Integer.BYTES * 8)); + } + + public static int right( long pair ) + { + return (int)pair; } private SortedLongArrayUtil() diff --git a/community/kernel/src/test/java/org/neo4j/helpers/SortedLongArrayUtilTest.java b/community/kernel/src/test/java/org/neo4j/helpers/SortedLongArrayUtilTest.java index c68ad3bbab8ea..882eca5609d72 100644 --- a/community/kernel/src/test/java/org/neo4j/helpers/SortedLongArrayUtilTest.java +++ b/community/kernel/src/test/java/org/neo4j/helpers/SortedLongArrayUtilTest.java @@ -24,9 +24,6 @@ import org.hamcrest.Matcher; import org.junit.Test; -import java.util.HashSet; -import java.util.Set; - import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.nullValue; @@ -37,126 +34,203 @@ public class SortedLongArrayUtilTest private static final long[] EMPTY = new long[0]; private static final long[] ONE_VALUE = new long[]{1}; - // missing() + // union() @Test - public void missingShouldNotHandleNulls() throws Exception + public void union_shouldHandleNullInput() { - assertException( () -> SortedLongArrayUtil.missing( null, null ), NullPointerException.class ); - assertException( () -> SortedLongArrayUtil.missing( EMPTY, null ), NullPointerException.class ); - assertException( () -> SortedLongArrayUtil.missing( null, EMPTY ), NullPointerException.class ); + assertThat( SortedLongArrayUtil.union( null, null ), nullValue() ); + assertThat( SortedLongArrayUtil.union( null, EMPTY ), equalTo( EMPTY ) ); + assertThat( SortedLongArrayUtil.union( EMPTY, null ), equalTo( EMPTY ) ); + assertThat( SortedLongArrayUtil.union( null, ONE_VALUE ), equalTo( ONE_VALUE ) ); + assertThat( SortedLongArrayUtil.union( ONE_VALUE, null ), equalTo( ONE_VALUE ) ); } @Test - public void missingShouldWorkWithNonIntersectingArrays() + public void union_shouldHandleNonIntersectingArrays() { - assertThat( SortedLongArrayUtil.missing( new long[]{1, 2, 3}, new long[]{4, 5, 6} ), equalTo( 3 ) ); - assertThat( SortedLongArrayUtil.missing( new long[]{1, 2, 3}, new long[]{14, 15, 16} ), equalTo( 3 ) ); - assertThat( SortedLongArrayUtil.missing( new long[]{4, 5, 6}, new long[]{1, 2, 3} ), equalTo( 3 ) ); + assertThat( SortedLongArrayUtil.union( new long[]{1, 2, 3}, new long[]{4, 5, 6} ), + isArray( 1, 2, 3, 4, 5, 6 ) ); + + assertThat( SortedLongArrayUtil.union( new long[]{1, 2, 3}, new long[]{14, 15, 16} ), + isArray( 1, 2, 3, 14, 15, 16 ) ); + + assertThat( SortedLongArrayUtil.union( new long[]{14, 15, 16}, new long[]{1, 2, 3} ), + isArray( 1, 2, 3, 14, 15, 16 ) ); } @Test - public void missingShouldWorkWithIntersectingArrays() + public void union_shouldHandleIntersectingArrays() { - assertThat( SortedLongArrayUtil.missing( new long[]{1, 2, 3}, new long[]{3, 4, 5} ), equalTo( 2 ) ); - assertThat( SortedLongArrayUtil.missing( new long[]{3, 4, 5}, new long[]{1, 2, 3} ), equalTo( 2 ) ); + assertThat( SortedLongArrayUtil.union( new long[]{1, 2, 3}, new long[]{3, 4, 5} ), + isArray( 1, 2, 3, 4, 5 ) ); + + assertThat( SortedLongArrayUtil.union( new long[]{3, 4, 5}, new long[]{1, 2, 3} ), + isArray( 1, 2, 3, 4, 5 ) ); } @Test - public void missingShouldWorkWithComplexIntersectingArraysWithGaps() + public void union_shouldHandleComplexIntersectingArraysWithGaps() { assertThat( - SortedLongArrayUtil.missing( new long[]{4, 6, 9, 11, 12, 15}, new long[]{2, 3, 4, 7, 8, 9, 12, 16, 19} ), - equalTo( 6 ) ); + SortedLongArrayUtil.union( new long[]{4, 6, 9, 11, 12, 15}, new long[]{2, 3, 4, 7, 8, 9, 12, 16, 19} ), + isArray( 2, 3, 4, 6, 7, 8, 9, 11, 12, 15, 16, 19 ) ); assertThat( - SortedLongArrayUtil.missing( new long[]{2, 3, 4, 7, 8, 9, 12, 16, 19}, new long[]{4, 6, 9, 11, 12, 15} ), - equalTo( 3 ) ); + SortedLongArrayUtil.union( new long[]{2, 3, 4, 7, 8, 9, 12, 16, 19}, new long[]{4, 6, 9, 11, 12, 15} ), + isArray( 2, 3, 4, 6, 7, 8, 9, 11, 12, 15, 16, 19 ) ); } - // union() + // intersect() @Test - public void shouldHandleNullInput() + public void intersect_shouldHandleNullInput() { - assertThat( SortedLongArrayUtil.union( null, null ), nullValue() ); - assertThat( SortedLongArrayUtil.union( null, EMPTY ), equalTo( EMPTY ) ); - assertThat( SortedLongArrayUtil.union( EMPTY, null ), equalTo( EMPTY ) ); - assertThat( SortedLongArrayUtil.union( null, ONE_VALUE ), equalTo( ONE_VALUE ) ); - assertThat( SortedLongArrayUtil.union( ONE_VALUE, null ), equalTo( ONE_VALUE ) ); + assertThat( SortedLongArrayUtil.intersect( null, null ), equalTo( EMPTY ) ); + assertThat( SortedLongArrayUtil.intersect( null, EMPTY ), equalTo( EMPTY ) ); + assertThat( SortedLongArrayUtil.intersect( EMPTY, null ), equalTo( EMPTY ) ); + assertThat( SortedLongArrayUtil.intersect( null, ONE_VALUE ), equalTo( EMPTY ) ); + assertThat( SortedLongArrayUtil.intersect( ONE_VALUE, null ), equalTo( EMPTY ) ); } @Test - public void shouldCreateUnionOfNonIntersectingArrays() + public void intersect_shouldHandleNonIntersectingArrays() { - assertThat( SortedLongArrayUtil.union( new long[]{1, 2, 3}, new long[]{4, 5, 6} ), - equalToIgnoringOrder( 1, 2, 3, 4, 5, 6 ) ); + assertThat( SortedLongArrayUtil.intersect( new long[]{1, 2, 3}, new long[]{4, 5, 6} ), + equalTo( EMPTY ) ); - assertThat( SortedLongArrayUtil.union( new long[]{1, 2, 3}, new long[]{14, 15, 16} ), - equalToIgnoringOrder( 1, 2, 3, 14, 15, 16 ) ); - - assertThat( SortedLongArrayUtil.union( new long[]{14, 15, 16}, new long[]{1, 2, 3} ), - equalToIgnoringOrder( 1, 2, 3, 14, 15, 16 ) ); + assertThat( SortedLongArrayUtil.intersect( new long[]{14, 15, 16}, new long[]{1, 2, 3} ), + equalTo( EMPTY ) ); } @Test - public void shouldCreateUnionOfIntersectingArrays() + public void intersect_shouldHandleIntersectingArrays() { - assertThat( SortedLongArrayUtil.union( new long[]{1, 2, 3}, new long[]{3, 4, 5} ), - equalToIgnoringOrder( 1, 2, 3, 4, 5 ) ); + assertThat( SortedLongArrayUtil.intersect( new long[]{1, 2, 3}, new long[]{3, 4, 5} ), + isArray( 3 ) ); - assertThat( SortedLongArrayUtil.union( new long[]{3, 4, 5}, new long[]{1, 2, 3} ), - equalToIgnoringOrder( 1, 2, 3, 4, 5 ) ); + assertThat( SortedLongArrayUtil.intersect( new long[]{3, 4, 5}, new long[]{1, 2, 3, 4} ), + isArray( 3, 4 ) ); } @Test - public void shouldCreateUnionOfComplexIntersectingArraysWithGaps() + public void intersect_shouldHandleComplexIntersectingArraysWithGaps() { assertThat( - SortedLongArrayUtil.union( new long[]{4, 6, 9, 11, 12, 15}, new long[]{2, 3, 4, 7, 8, 9, 12, 16, 19} ), - equalToIgnoringOrder( 2, 3, 4, 6, 7, 8, 9, 11, 12, 15, 16, 19 ) ); + SortedLongArrayUtil.intersect( new long[]{4, 6, 9, 11, 12, 15}, new long[]{2, 3, 4, 7, 8, 9, 12, 16, 19} ), + isArray( 4, 9, 12 ) ); assertThat( - SortedLongArrayUtil.union( new long[]{2, 3, 4, 7, 8, 9, 12, 16, 19}, new long[]{4, 6, 9, 11, 12, 15} ), - equalToIgnoringOrder( 2, 3, 4, 6, 7, 8, 9, 11, 12, 15, 16, 19 ) ); + SortedLongArrayUtil.intersect( new long[]{2, 3, 4, 7, 8, 9, 12, 16, 19}, new long[]{4, 6, 9, 11, 12, 15} ), + isArray( 4, 9, 12 ) ); } - // helpers + // symmetricDifference() + + @Test + public void symDiff_shouldHandleNullInput() + { + assertThat( SortedLongArrayUtil.symmetricDifference( null, null ), equalTo( null ) ); + assertThat( SortedLongArrayUtil.symmetricDifference( null, EMPTY ), equalTo( EMPTY ) ); + assertThat( SortedLongArrayUtil.symmetricDifference( EMPTY, null ), equalTo( EMPTY ) ); + assertThat( SortedLongArrayUtil.symmetricDifference( null, ONE_VALUE ), equalTo( ONE_VALUE ) ); + assertThat( SortedLongArrayUtil.symmetricDifference( ONE_VALUE, null ), equalTo( ONE_VALUE ) ); + } + + @Test + public void symDiff_shouldHandleNonIntersectingArrays() + { + assertThat( SortedLongArrayUtil.symmetricDifference( new long[]{1, 2, 3}, new long[]{4, 5, 6} ), + isArray( 1, 2, 3, 4, 5, 6 ) ); + + assertThat( SortedLongArrayUtil.symmetricDifference( new long[]{14, 15, 16}, new long[]{1, 2, 3} ), + isArray( 1, 2, 3, 14, 15, 16 ) ); + } - private static Matcher equalToIgnoringOrder( long... operand ) + @Test + public void symDiff_shouldHandleIntersectingArrays() { - return new IsEqualIgnoreOrder( operand ); + assertThat( SortedLongArrayUtil.symmetricDifference( new long[]{1, 2, 3}, new long[]{3, 4, 5} ), + isArray( 1, 2, 4, 5 ) ); + + assertThat( SortedLongArrayUtil.symmetricDifference( new long[]{3, 4, 5}, new long[]{1, 2, 3, 4} ), + isArray( 1, 2, 5 ) ); } - private static class IsEqualIgnoreOrder extends BaseMatcher + @Test + public void symDiff_shouldHandleComplexIntersectingArraysWithGaps() { - private final Set expectedValue = new HashSet<>(); + assertThat( + SortedLongArrayUtil.symmetricDifference( new long[]{4, 6, 9, 11, 12, 15}, new long[]{2, 3, 4, 7, 8, 9, 12, 16, 19} ), + isArray( 2, 3, 6, 7, 8, 11, 15, 16, 19 ) ); + assertThat( + SortedLongArrayUtil.symmetricDifference( new long[]{2, 3, 4, 7, 8, 9, 12, 16, 19}, new long[]{4, 6, 9, 11, 12, 15} ), + isArray( 2, 3, 6, 7, 8, 11, 15, 16, 19 ) ); + } + + // count unique + + @Test + public void shouldCountUnique() + { + assertThat( + SortedLongArrayUtil.countUnique( new long[]{1, 2, 3}, new long[]{4, 5, 6} ), + isIntPair( 3, 3 ) ); + + assertThat( + SortedLongArrayUtil.countUnique( new long[]{1, 2, 3}, new long[]{3, 6} ), + isIntPair( 2, 1 ) ); + + assertThat( + SortedLongArrayUtil.countUnique( new long[]{1, 2, 3}, new long[]{3} ), + isIntPair( 2, 0 ) ); + + assertThat( + SortedLongArrayUtil.countUnique( new long[]{3}, new long[]{1, 2, 3} ), + isIntPair( 0, 2 ) ); + + assertThat( + SortedLongArrayUtil.countUnique( new long[]{3}, new long[]{3} ), + isIntPair( 0, 0 ) ); + + assertThat( + SortedLongArrayUtil.countUnique( new long[]{3, 6, 8}, new long[]{} ), + isIntPair( 3, 0 ) ); - IsEqualIgnoreOrder( long[] equalArg ) + assertThat( + SortedLongArrayUtil.countUnique( new long[]{}, new long[]{3, 6, 8} ), + isIntPair( 0, 3 ) ); + + assertThat( + SortedLongArrayUtil.countUnique( new long[]{}, new long[]{} ), + isIntPair( 0, 0 ) ); + + assertThat( + SortedLongArrayUtil.countUnique( new long[]{4, 6, 9, 11, 12, 15}, new long[]{2, 3, 4, 7, 8, 9, 12, 16, 19} ), + isIntPair( 3, 6 ) ); + } + + // helpers + + private Matcher isIntPair( int left, int right ) + { + return new BaseMatcher() { - for ( long val : equalArg ) + @Override + public void describeTo( Description description ) { - this.expectedValue.add( val ); + description.appendValue( left ); + description.appendValue( right ); } - } - @Override - public boolean matches( Object o ) - { - if ( o instanceof long[] ) + @Override + public boolean matches( Object o ) { - Set values = new HashSet<>(); - for ( long val : (long[]) o ) - { - values.add( val ); - } - return values.equals( expectedValue ); + return o instanceof Long && ((Long) o) == ((long) left << 32 | right); } - return false; - } + }; + } - @Override - public void describeTo( Description description ) - { - description.appendValue( this.expectedValue ); - } + private static Matcher isArray( long... values ) + { + return equalTo( values ); } }