From 2589c222f9b6eb2941c720ec1ee7f22118005317 Mon Sep 17 00:00:00 2001 From: Naveed Khan Date: Fri, 2 Oct 2026 18:50:58 +0530 Subject: [PATCH] report the trailing replacement in ReplacementsFinder Pending insertions and deletions were only flushed from visitKeepCommand, so a script ending on a modification never reached the handler. EditScript.visit now signals the end of the script through a new CommandVisitor.visitEndOfScript default no-op. --- .../collections4/sequence/CommandVisitor.java | 13 ++++ .../collections4/sequence/EditScript.java | 1 + .../sequence/ReplacementsFinder.java | 25 ++++++- .../sequence/ReplacementsFinderTest.java | 75 +++++++++++++++++++ 4 files changed, 111 insertions(+), 3 deletions(-) create mode 100644 src/test/java/org/apache/commons/collections4/sequence/ReplacementsFinderTest.java diff --git a/src/main/java/org/apache/commons/collections4/sequence/CommandVisitor.java b/src/main/java/org/apache/commons/collections4/sequence/CommandVisitor.java index 3da1ad8944..2946bfeeae 100644 --- a/src/main/java/org/apache/commons/collections4/sequence/CommandVisitor.java +++ b/src/main/java/org/apache/commons/collections4/sequence/CommandVisitor.java @@ -130,6 +130,19 @@ public interface CommandVisitor { */ void visitDeleteCommand(T object); + /** + * Method called once all commands of the script have been visited. + *

+ * A visitor that groups consecutive commands must override this method to process the group that is still pending + * when the script ends. The default implementation does nothing. + *

+ * + * @since 4.6.1 + */ + default void visitEndOfScript() { + // empty + } + /** * Method called when an insert command is encountered. * diff --git a/src/main/java/org/apache/commons/collections4/sequence/EditScript.java b/src/main/java/org/apache/commons/collections4/sequence/EditScript.java index ab20ccd480..3ec83387cf 100644 --- a/src/main/java/org/apache/commons/collections4/sequence/EditScript.java +++ b/src/main/java/org/apache/commons/collections4/sequence/EditScript.java @@ -129,6 +129,7 @@ public void visit(final CommandVisitor visitor) { for (final EditCommand command : commands) { command.accept(visitor); } + visitor.visitEndOfScript(); } } diff --git a/src/main/java/org/apache/commons/collections4/sequence/ReplacementsFinder.java b/src/main/java/org/apache/commons/collections4/sequence/ReplacementsFinder.java index cfa787fa31..0c74012664 100644 --- a/src/main/java/org/apache/commons/collections4/sequence/ReplacementsFinder.java +++ b/src/main/java/org/apache/commons/collections4/sequence/ReplacementsFinder.java @@ -70,6 +70,15 @@ public ReplacementsFinder(final ReplacementsHandler handler) { this.handler = handler; } + /** + * Hands the pending insertions and deletions to the user handler as subsequences. + */ + private void handlePendingReplacement() { + handler.handleReplacement(skipped, pendingDeletions, pendingInsertions); + pendingDeletions.clear(); + pendingInsertions.clear(); + } + /** * Add an object to the pending deletions set. * @@ -80,6 +89,18 @@ public void visitDeleteCommand(final T object) { pendingDeletions.add(object); } + /** + * Handle the trailing replacement, if the script ended on insertions or deletions rather than on a + * synchronization object. + */ + @Override + public void visitEndOfScript() { + if (!pendingDeletions.isEmpty() || !pendingInsertions.isEmpty()) { + handlePendingReplacement(); + skipped = 0; + } + } + /** * Add an object to the pending insertions set. * @@ -103,9 +124,7 @@ public void visitKeepCommand(final T object) { if (pendingDeletions.isEmpty() && pendingInsertions.isEmpty()) { ++skipped; } else { - handler.handleReplacement(skipped, pendingDeletions, pendingInsertions); - pendingDeletions.clear(); - pendingInsertions.clear(); + handlePendingReplacement(); skipped = 1; } } diff --git a/src/test/java/org/apache/commons/collections4/sequence/ReplacementsFinderTest.java b/src/test/java/org/apache/commons/collections4/sequence/ReplacementsFinderTest.java new file mode 100644 index 0000000000..bf12f10d38 --- /dev/null +++ b/src/test/java/org/apache/commons/collections4/sequence/ReplacementsFinderTest.java @@ -0,0 +1,75 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.collections4.sequence; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Stream; + +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +class ReplacementsFinderTest { + + static Stream sequences() { + return Stream.of( + Arguments.of("abcd", "abcd", ""), + Arguments.of("xabc", "yabc", "0:[x]->[y]"), + Arguments.of("abxcd", "abycd", "2:[x]->[y]"), + Arguments.of("axbxc", "aybyc", "1:[x]->[y] 1:[x]->[y]"), + Arguments.of("abc", "abd", "2:[c]->[d]"), + Arguments.of("abcd", "abc", "3:[d]->[]"), + Arguments.of("abc", "abcd", "3:[]->[d]"), + Arguments.of("abc", "", "0:[a, b, c]->[]"), + Arguments.of("", "xyz", "0:[]->[x, y, z]"), + Arguments.of("a", "b", "0:[a]->[b]")); + } + + private static List sequence(final String string) { + final List list = new ArrayList<>(string.length()); + for (int i = 0; i < string.length(); i++) { + list.add(Character.valueOf(string.charAt(i))); + } + return list; + } + + /** + * The replacements reported for a whole script must transform the first sequence into the second one. + */ + @ParameterizedTest + @MethodSource("sequences") + void testReplacementsSpanWholeScript(final String left, final String right, final String expected) { + final List first = sequence(left); + final List reported = new ArrayList<>(); + final List rebuilt = new ArrayList<>(); + final int[] cursor = {0}; + new SequencesComparator<>(first, sequence(right)).getScript().visit(new ReplacementsFinder( + (skipped, from, to) -> { + reported.add(skipped + ":" + from + "->" + to); + rebuilt.addAll(first.subList(cursor[0], cursor[0] + skipped)); + cursor[0] += skipped + from.size(); + rebuilt.addAll(to); + })); + rebuilt.addAll(first.subList(cursor[0], first.size())); + assertEquals(expected, String.join(" ", reported)); + assertEquals(sequence(right), rebuilt); + } + +}