From 62d277161e73e354619e65cf58e64828b37ab2b3 Mon Sep 17 00:00:00 2001 From: Adam Szwaja <115580186+289Adam289@users.noreply.github.com> Date: Wed, 18 Dec 2024 12:11:29 +0100 Subject: [PATCH] Improve range split function and add unit tests (#553) --- android/build.gradle | 2 + .../livemarkdown/MarkdownParser.java | 38 +-------- .../expensify/livemarkdown/MarkdownRange.java | 28 +++++++ .../expensify/livemarkdown/RangeSplitter.java | 53 ++++++++++++ .../livemarkdown/RangeSplitterTest.java | 81 +++++++++++++++++++ 5 files changed, 168 insertions(+), 34 deletions(-) create mode 100644 android/src/main/java/com/expensify/livemarkdown/RangeSplitter.java create mode 100644 android/src/test/java/com/expensify/livemarkdown/RangeSplitterTest.java diff --git a/android/build.gradle b/android/build.gradle index 8dc5b7639..3ba26f751 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -175,6 +175,8 @@ dependencies { implementation "com.facebook.react:react-android" // version substituted by RNGP implementation "com.facebook.react:hermes-android" // version substituted by RNGP implementation project(":react-native-reanimated") + + testImplementation "junit:junit:4.13.2" } if (isNewArchitectureEnabled()) { diff --git a/android/src/main/java/com/expensify/livemarkdown/MarkdownParser.java b/android/src/main/java/com/expensify/livemarkdown/MarkdownParser.java index 2dc82badf..791c4f2bc 100644 --- a/android/src/main/java/com/expensify/livemarkdown/MarkdownParser.java +++ b/android/src/main/java/com/expensify/livemarkdown/MarkdownParser.java @@ -1,5 +1,7 @@ package com.expensify.livemarkdown; +import static com.expensify.livemarkdown.RangeSplitter.splitRangesOnEmojis; + import androidx.annotation.NonNull; import com.facebook.react.bridge.ReactContext; @@ -32,38 +34,6 @@ public MarkdownParser(@NonNull ReactContext reactContext) { private native String nativeParse(@NonNull String text, int parserId); - private void splitRangesOnEmojis(List markdownRanges, String type) { - List emojiRanges = new ArrayList<>(); - for (MarkdownRange range : markdownRanges) { - if (range.getType().equals("emoji")) { - emojiRanges.add(range); - } - } - - int i = 0; - int j = 0; - while (i < markdownRanges.size() && j < emojiRanges.size()) { - MarkdownRange currentRange = markdownRanges.get(i); - MarkdownRange emojiRange = emojiRanges.get(j); - - if (!currentRange.getType().equals(type) || currentRange.getEnd() < emojiRange.getStart()) { - i += 1; - continue; - } else if (emojiRange.getStart() >= currentRange.getStart() && emojiRange.getEnd() <= currentRange.getEnd()) { - // Split range - MarkdownRange startRange = new MarkdownRange(currentRange.getType(), currentRange.getStart(), emojiRange.getStart() - currentRange.getStart(), currentRange.getDepth()); - MarkdownRange endRange = new MarkdownRange(currentRange.getType(), emojiRange.getEnd(), currentRange.getEnd() - emojiRange.getEnd(), currentRange.getDepth()); - - markdownRanges.add(i + 1, startRange); - markdownRanges.add(i + 2, endRange); - markdownRanges.remove(i); - i = i + 1; - } - j += 1; - } - } - - private List parseRanges(String rangesJSON, String innerText) { List markdownRanges = new ArrayList<>(); try { @@ -84,8 +54,8 @@ private List parseRanges(String rangesJSON, String innerText) { } catch (JSONException e) { return Collections.emptyList(); } - splitRangesOnEmojis(markdownRanges, "italic"); - splitRangesOnEmojis(markdownRanges, "strikethrough"); + markdownRanges = splitRangesOnEmojis(markdownRanges, "italic"); + markdownRanges = splitRangesOnEmojis(markdownRanges, "strikethrough"); return markdownRanges; } public synchronized List parse(@NonNull String text, int parserId) { diff --git a/android/src/main/java/com/expensify/livemarkdown/MarkdownRange.java b/android/src/main/java/com/expensify/livemarkdown/MarkdownRange.java index 1fda115b9..479f5fa11 100644 --- a/android/src/main/java/com/expensify/livemarkdown/MarkdownRange.java +++ b/android/src/main/java/com/expensify/livemarkdown/MarkdownRange.java @@ -36,4 +36,32 @@ public int getLength() { public int getDepth() { return mDepth; } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + + if (o instanceof MarkdownRange other) { + return this.mType.equals(other.mType) + && this.mStart == other.mStart + && this.mEnd == other.mEnd + && this.mLength == other.mLength + && this.mDepth == other.mDepth; + } + return false; + } + + @NonNull + @Override + public String toString() { + return "MarkdownRange{" + + "type='" + mType + "'" + + ", start=" + mStart + + ", end=" + mEnd + + ", length=" + mLength + + ", depth=" + mDepth + + "}"; + } } diff --git a/android/src/main/java/com/expensify/livemarkdown/RangeSplitter.java b/android/src/main/java/com/expensify/livemarkdown/RangeSplitter.java new file mode 100644 index 000000000..9212bbfda --- /dev/null +++ b/android/src/main/java/com/expensify/livemarkdown/RangeSplitter.java @@ -0,0 +1,53 @@ +package com.expensify.livemarkdown; + +import androidx.annotation.NonNull; + +import java.util.ArrayList; +import java.util.List; + +public class RangeSplitter { + public static ArrayList splitRangesOnEmojis(@NonNull List markdownRanges, @NonNull String type) { + ArrayList emojiRanges = new ArrayList<>(); + ArrayList oldRanges = new ArrayList<>(markdownRanges); + ArrayList newRanges = new ArrayList<>(); + for (MarkdownRange range : oldRanges) { + if (range.getType().equals("emoji")) { + emojiRanges.add(range); + } + } + + int i = 0; + int j = 0; + while (i < oldRanges.size()) { + MarkdownRange currentRange = oldRanges.get(i); + if (!currentRange.getType().equals(type)) { + newRanges.add(currentRange); + i += 1; + continue; + } + + // Iterate through all emoji ranges before the end of the current range, splitting the current range at each intersection. + while (j < emojiRanges.size()) { + MarkdownRange emojiRange = emojiRanges.get(j); + if (emojiRange.getStart() > currentRange.getEnd()) { + break; + } + + int currentStart = currentRange.getStart(); + int currentEnd = currentRange.getEnd(); + int emojiStart = emojiRange.getStart(); + int emojiEnd = emojiRange.getEnd(); + if (emojiStart >= currentStart && emojiEnd <= currentEnd) { // Intersection + MarkdownRange newRange = new MarkdownRange(currentRange.getType(), currentStart, emojiStart - currentStart, currentRange.getDepth()); + currentRange = new MarkdownRange(currentRange.getType(), emojiEnd, currentEnd - emojiEnd, currentRange.getDepth()); + + newRanges.add(newRange); + } + j += 1; + } + newRanges.add(currentRange); + i += 1; + } + return newRanges; + } +} diff --git a/android/src/test/java/com/expensify/livemarkdown/RangeSplitterTest.java b/android/src/test/java/com/expensify/livemarkdown/RangeSplitterTest.java new file mode 100644 index 000000000..48f0a3185 --- /dev/null +++ b/android/src/test/java/com/expensify/livemarkdown/RangeSplitterTest.java @@ -0,0 +1,81 @@ +package com.expensify.livemarkdown; + +import static com.expensify.livemarkdown.RangeSplitter.splitRangesOnEmojis; + +import static org.junit.Assert.assertEquals; + +import org.junit.Test; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +public class RangeSplitterTest { + + @Test + public void testNoOverlap() { + List markdownRanges = new ArrayList<>(); + markdownRanges.add(new MarkdownRange("strikethrough", 0, 10, 1)); + markdownRanges.add(new MarkdownRange("emoji", 12, 2, 1)); + + markdownRanges = splitRangesOnEmojis(markdownRanges, "strikethrough"); + + assertEquals(2, markdownRanges.size()); + assertEquals(new MarkdownRange("strikethrough", 0, 10, 1), markdownRanges.get(0)); + assertEquals(new MarkdownRange("emoji", 12, 2, 1), markdownRanges.get(1)); + } + + @Test + public void testOverlapDifferentType() { + List markdownRanges = new ArrayList<>(); + markdownRanges.add(new MarkdownRange("strikethrough", 0, 10, 1)); + markdownRanges.add(new MarkdownRange("emoji", 3, 4, 1)); + + markdownRanges = splitRangesOnEmojis(markdownRanges, "italic"); + + assertEquals(2, markdownRanges.size()); + assertEquals(new MarkdownRange("strikethrough", 0, 10, 1), markdownRanges.get(0)); + assertEquals(new MarkdownRange("emoji", 3, 4, 1), markdownRanges.get(1)); + } + + @Test + public void testSingleOverlap() { + List markdownRanges = new ArrayList<>(); + markdownRanges.add(new MarkdownRange("strikethrough", 0, 10, 1)); + markdownRanges.add(new MarkdownRange("emoji", 3, 4, 1)); // This range should split the strikethrough range + + markdownRanges = splitRangesOnEmojis(markdownRanges, "strikethrough"); + + // Sort is needed because ranges may get mixed while splitting + Collections.sort(markdownRanges, (r1, r2) -> Integer.compare(r1.getStart(), r2.getStart())); + + assertEquals(3, markdownRanges.size()); + assertEquals(new MarkdownRange("strikethrough", 0, 3, 1), markdownRanges.get(0)); + assertEquals(new MarkdownRange("emoji", 3, 4, 1), markdownRanges.get(1)); + assertEquals(new MarkdownRange("strikethrough", 7, 3, 1), markdownRanges.get(2)); + } + + @Test + public void testMultipleOverlapsMultipleTypes() { + List markdownRanges = new ArrayList<>(); + markdownRanges.add(new MarkdownRange("italic", 0, 20, 1)); + markdownRanges.add(new MarkdownRange("strikethrough", 2, 12, 1)); + markdownRanges.add(new MarkdownRange("emoji", 3, 1, 1)); + markdownRanges.add(new MarkdownRange("emoji", 8, 2, 1)); + markdownRanges.add(new MarkdownRange("strikethrough", 22, 5, 1)); + + markdownRanges = splitRangesOnEmojis(markdownRanges, "strikethrough"); + + // Sort is needed because ranges may get mixed while splitting + Collections.sort(markdownRanges, (r1, r2) -> Integer.compare(r1.getStart(), r2.getStart())); + + assertEquals(7, markdownRanges.size()); + assertEquals(new MarkdownRange("italic", 0, 20, 1), markdownRanges.get(0)); + assertEquals(new MarkdownRange("strikethrough", 2, 1, 1), markdownRanges.get(1)); + assertEquals(new MarkdownRange("emoji", 3, 1, 1), markdownRanges.get(2)); + assertEquals(new MarkdownRange("strikethrough", 4, 4, 1), markdownRanges.get(3)); + assertEquals(new MarkdownRange("emoji", 8, 2, 1), markdownRanges.get(4)); + assertEquals(new MarkdownRange("strikethrough", 10, 4, 1), markdownRanges.get(5)); + assertEquals(new MarkdownRange("strikethrough", 22, 5, 1), markdownRanges.get(6)); + } +}