diff --git a/packages/playground/data-liberation/src/import/WP_Topological_Sorter.php b/packages/playground/data-liberation/src/import/WP_Topological_Sorter.php index 9aa42363cf..a430306d20 100644 --- a/packages/playground/data-liberation/src/import/WP_Topological_Sorter.php +++ b/packages/playground/data-liberation/src/import/WP_Topological_Sorter.php @@ -72,11 +72,11 @@ public function map_post( $byte_offset, $data ) { --$this->orphan_post_counter; } - // This is an array saved as: [ parent, byte_offset, moved ], to save space and not using an associative one. + // This is an array saved as: [ parent, byte_offset ], to save + // space and not using an associative one. $this->posts[ $data['post_id'] ] = array( $data['post_parent'], $byte_offset, - false, ); } @@ -120,23 +120,21 @@ public function is_sorted() { * * Sorted posts will be stored as attachments and posts/pages separately. */ - public function sort_topologically( $empty_memory = true ) { + public function sort_topologically( $free_space = true ) { foreach ( $this->categories as $slug => $category ) { $this->topological_category_sort( $slug, $category ); } - $this->sort_parent_child( $this->posts ); + $this->sort_elements( $this->posts ); - // Empty some memory. - if ( $empty_memory ) { + // Free some space. + if ( $free_space ) { + /** + * @TODO: all the elements that have not been moved can be flushed away. + */ foreach ( $this->posts as $id => $element ) { - if ( ! $element[2] ) { - // The element have not been moved, unset it. - unset( $this->posts[ $id ] ); - } else { - // Save only the byte offset. - $this->posts[ $id ] = $element[1]; - } + // Save only the byte offset. + $this->posts[ $id ] = $element[1]; } } @@ -144,86 +142,36 @@ public function sort_topologically( $empty_memory = true ) { } /** - * Recursive topological sorting. - * @todo Check for circular dependencies. - * - * @param array $elements The elements to sort. + * Recursive sort elements. Posts with parents will be moved to the correct position. * - * @return void + * @return true */ - private function sort_parent_child( &$elements ) { - // Sort the array in-place. - // reset( $elements ); - $position = 0; // key( $elements ); - $length = count( $elements ); - - if ( $length < 2 ) { - // No need to sort. - return; - } - - if ( 2 === $length ) { - $keys = array_keys( $elements ); - - // First element has a parent and is the second. - if ( $elements[ $keys[0] ][0] && $keys[1] === $elements[ $keys[0] ][0] ) { - // Swap. - $elements = array_reverse( $elements, true ); - - // Set the second as 'moved'. - $elements[ $keys[1] ][2] = true; + private function sort_elements( &$elements ) { + $sort_callback = function ( $a, $b ) use ( &$elements ) { + $parent_a = $elements[ $a ][0]; + $parent_b = $elements[ $b ][0]; + + if ( ! $parent_a && ! $parent_b ) { + // No parents. + return 0; + } elseif ( $a === $parent_b ) { + // A is the parent of B. + return -1; + } elseif ( $b === $parent_a ) { + // B is the parent of A. + return 1; } - return; - } - - foreach ( $elements as $id => $element ) { - if ( empty( $element[0] ) ) { - $this->move_element( $elements, $id, $position ); - } - } - } - - /** - * Move an element to a new position. - * - * @param array $elements The elements to sort. - * @param int $id The ID of the element to move. - * @param int $position The new position of the element. - * - * @return void - */ - private function move_element( &$elements, $id, &$position ) { - if ( ! isset( $elements[ $id ] ) ) { - return; - } - - $element = $elements[ $id ]; + return 0; + }; - if ( $id < $position ) { - // Already in the correct position. - return; - } - - // Move the element to the current position. - unset( $elements[ $id ] ); - - // Set as 'moved'. - $element[2] = true; - - // Generate the new array. - $elements = array_slice( $elements, 0, $position, true ) + - array( $id => $element ) + - array_slice( $elements, $position, null, true ); - - ++$position; - - // Move children. - foreach ( $elements as $child_id => $child_element ) { - if ( $id === $child_element[0] ) { - $this->move_element( $elements, $child_id, $position ); - } - } + /** + * @TODO: PHP uses quicksort: https://github.com/php/php-src/blob/master/Zend/zend_sort.c + * WordPress export posts by ID and so are likely to be already in order. + * Quicksort performs badly on already sorted arrays, O(n^2) is the worst case. + * Let's consider using a different sorting algorithm. + */ + uksort( $elements, $sort_callback ); } /** diff --git a/packages/playground/data-liberation/tests/WPTopologicalSorterTests.php b/packages/playground/data-liberation/tests/WPTopologicalSorterTests.php index d7b8d3e091..9e176d5be2 100644 --- a/packages/playground/data-liberation/tests/WPTopologicalSorterTests.php +++ b/packages/playground/data-liberation/tests/WPTopologicalSorterTests.php @@ -22,9 +22,10 @@ public function test_parent_after_child() { $sorter->map_post( 20, $this->generate_post( 2, 0 ) ); $sorter->sort_topologically(); - $this->assertEquals( array( 2 => 20 ), $sorter->posts ); - $this->assertFalse( $sorter->get_byte_offset( 1 ) ); + $this->assertEquals( array( 2 => 20, 1 => 10 ), $sorter->posts ); + $this->assertEquals( 10, $sorter->get_byte_offset( 1 ) ); $this->assertEquals( 20, $sorter->get_byte_offset( 2 ) ); + $this->assertFalse( $sorter->is_sorted() ); } public function test_child_after_parent() { @@ -35,8 +36,8 @@ public function test_child_after_parent() { $sorter->map_post( 30, $this->generate_post( 3, 2 ) ); $sorter->sort_topologically(); - $this->assertEquals( array(), $sorter->posts ); - $this->assertFalse( $sorter->get_byte_offset( 1 ) ); + $this->assertEquals( array( 1 => 10, 2 => 20, 3 => 30 ), $sorter->posts ); + $this->assertEquals( 10, $sorter->get_byte_offset( 1 ) ); } public function test_orphaned_post() { @@ -46,7 +47,8 @@ public function test_orphaned_post() { $sorter->map_post( 20, $this->generate_post( 2, 0 ) ); $sorter->sort_topologically(); - $this->assertEquals( array( 2 => 20 ), $sorter->posts ); + $this->assertEquals( array( 1 => 10, 2 => 20 ), $sorter->posts ); + $this->assertEquals( 10, $sorter->get_byte_offset( 1 ) ); $this->assertEquals( 20, $sorter->get_byte_offset( 2 ) ); } @@ -58,7 +60,7 @@ public function test_chain_parent_child_after() { $sorter->map_post( 30, $this->generate_post( 3, 0 ) ); $sorter->sort_topologically(); - $this->assertEquals( array( 3 => 30, 2 => 20 ), $sorter->posts ); + $this->assertEquals( array( 3 => 30, 2 => 20, 1 => 10 ), $sorter->posts ); } public function test_reverse_order() { @@ -67,23 +69,21 @@ public function test_reverse_order() { $this->multiple_map_posts( $sorter, array( 3, 2, 1 ) ); $sorter->sort_topologically(); - $this->assertEquals( array(), $sorter->posts ); + $this->assertEquals( array( 1 => 10, 2 => 20, 3 => 30 ), $sorter->posts ); } public function test_get_byte_offsets_consume_array() { $sorter = new WP_Topological_Sorter(); - $this->multiple_map_posts( $sorter, array( 3, 1, 2 ) ); + $this->multiple_map_posts( $sorter, array( 2, 3, 0 ) ); $sorter->sort_topologically(); - $this->assertEquals( array( 3 => 10 ), $sorter->posts ); - - // $this->assertEquals( 10, $sorter->get_byte_offset( 1 ) ); - // $this->assertEquals( 20, $sorter->get_byte_offset( 2 ) ); - // $this->assertEquals( 30, $sorter->get_byte_offset( 3 ) ); + $this->assertEquals( array( 3 => 30, 2 => 20, 1 => 10 ), $sorter->posts ); - $this->assertFalse( $sorter->get_byte_offset( 1 ) ); - $this->assertFalse( $sorter->is_sorted() ); + $this->assertEquals( 10, $sorter->get_byte_offset( 1 ) ); + $this->assertEquals( 20, $sorter->get_byte_offset( 2 ) ); + $this->assertEquals( 30, $sorter->get_byte_offset( 3 ) ); + $this->assertCount( 0, $sorter->posts ); } /**