├── .idea
├── .name
├── copyright
│ └── profiles_settings.xml
├── vcs.xml
├── modules.xml
├── runConfigurations.xml
├── gradle.xml
├── compiler.xml
└── misc.xml
├── app
├── .gitignore
├── src
│ ├── main
│ │ ├── ic_launcher-web.png
│ │ ├── res
│ │ │ ├── mipmap-hdpi
│ │ │ │ └── ic_launcher.png
│ │ │ ├── mipmap-mdpi
│ │ │ │ └── ic_launcher.png
│ │ │ ├── mipmap-xhdpi
│ │ │ │ └── ic_launcher.png
│ │ │ ├── mipmap-xxhdpi
│ │ │ │ └── ic_launcher.png
│ │ │ ├── mipmap-xxxhdpi
│ │ │ │ └── ic_launcher.png
│ │ │ ├── values
│ │ │ │ ├── dimens.xml
│ │ │ │ ├── strings.xml
│ │ │ │ ├── colors.xml
│ │ │ │ └── styles.xml
│ │ │ ├── layout
│ │ │ │ ├── fragment_example.xml
│ │ │ │ ├── activity_main.xml
│ │ │ │ ├── activity_example.xml
│ │ │ │ └── editor_fragment.xml
│ │ │ ├── menu
│ │ │ │ └── menu_main.xml
│ │ │ └── values-w820dp
│ │ │ │ └── dimens.xml
│ │ ├── AndroidManifest.xml
│ │ └── java
│ │ │ └── com
│ │ │ └── werdpressed
│ │ │ └── partisan
│ │ │ └── undoredo
│ │ │ ├── fragmentexample
│ │ │ ├── FragmentExampleActivity.java
│ │ │ └── EditorFragment.java
│ │ │ ├── MainActivity.java
│ │ │ └── activityexample
│ │ │ └── ActivityExampleActivity.java
│ └── androidTest
│ │ └── java
│ │ └── com
│ │ └── werdpressed
│ │ └── partisan
│ │ └── undoredo
│ │ └── ApplicationTest.java
├── proguard-rules.pro
├── build.gradle
└── app.iml
├── rundo
├── .gitignore
├── src
│ ├── main
│ │ ├── AndroidManifest.xml
│ │ ├── java
│ │ │ └── com
│ │ │ │ └── werdpressed
│ │ │ │ └── partisan
│ │ │ │ └── rundo
│ │ │ │ ├── WriteToArrayDeque.java
│ │ │ │ ├── WriteToArrayDequeRunnable.java
│ │ │ │ ├── FixedSizeArrayDeque.java
│ │ │ │ ├── utils
│ │ │ │ └── SubtractStringUtils.java
│ │ │ │ ├── RunDo.java
│ │ │ │ ├── RunDoSupport.java
│ │ │ │ ├── RunDoNative.java
│ │ │ │ └── SubtractStrings.java
│ │ └── res
│ │ │ └── values
│ │ │ └── strings.xml
│ └── androidTest
│ │ └── java
│ │ └── com
│ │ └── werdpressed
│ │ └── partisan
│ │ └── rundo
│ │ └── ApplicationTest.java
├── proguard-rules.pro
├── build.gradle
└── rundo.iml
├── settings.gradle
├── gradle
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── .gitignore
├── gradle.properties
├── RunDo.iml
├── LICENSE.md
├── gradlew.bat
├── README.md
└── gradlew
/.idea/.name:
--------------------------------------------------------------------------------
1 | UndoRedo
--------------------------------------------------------------------------------
/app/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 |
--------------------------------------------------------------------------------
/rundo/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 |
--------------------------------------------------------------------------------
/settings.gradle:
--------------------------------------------------------------------------------
1 | include ':app', ':rundo'
2 |
--------------------------------------------------------------------------------
/app/src/main/ic_launcher-web.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/PPartisan/RunDo/HEAD/app/src/main/ic_launcher-web.png
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/PPartisan/RunDo/HEAD/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/.idea/copyright/profiles_settings.xml:
--------------------------------------------------------------------------------
1 |
ArrayDeque class that adds a fixed, maximum capacity.
10 | *
11 | * Thanks to contributors on StackOverflow at this post.
12 | *
13 | * @author Tom Calver
14 | */
15 | public final class FixedSizeArrayDequeArrayDeque.
19 | */
20 | private final int maxSize;
21 |
22 | /**
23 | * Creates ArrayDeque with fixed, maximum capacity of maxSize.
24 | *
25 | * @param maxSize Max capacity for ArrayDeque.
26 | */
27 | public FixedSizeArrayDeque(int maxSize) {
28 | super(maxSize);
29 | this.maxSize = maxSize;
30 | }
31 |
32 | protected FixedSizeArrayDeque(Parcel in) {
33 | maxSize = in.readInt();
34 | }
35 |
36 | public static final CreatorRunDo implementations monitor and manipulate {@link EditText} fields, by
11 | * periodically saving snippets of text to {@link java.util.Collection}s and reinstating them
12 | * through {@link #undo()} and {@link #redo()} calls.
13 | *
14 | * @author Tom Calver
15 | */
16 | public interface RunDo extends TextWatcher, WriteToArrayDeque {
17 |
18 | String TAG = "RunDo";
19 |
20 | String UNDO_TAG = "undo_queue";
21 | String REDO_TAG = "redo_queue";
22 | String OLD_TEXT_TAG = "old_text";
23 | String CONFIG_CHANGE_TAG = "return_from_config_change";
24 |
25 | int DEFAULT_QUEUE_SIZE = 10;
26 | int DEFAULT_TIMER_LENGTH = 2000;
27 |
28 | int TRACKING_STARTED = 12;
29 | int TRACKING_CURRENT = TRACKING_STARTED + 1;
30 | int TRACKING_ENDED = TRACKING_CURRENT + 1;
31 |
32 | /**
33 | * Sets size of Undo and Redo queues. Default size is {@value #DEFAULT_QUEUE_SIZE}.
34 | * Calling this clears any elements already in the queues.
35 | * @param size New queue size
36 | */
37 | void setQueueSize(int size);
38 |
39 | /**
40 | * Sets time in milliseconds before text is committed to the undo queue. This timer begins
41 | * immediately after text entry stops, and is reset if text changes before the timer can
42 | * complete. Default value is {@value #DEFAULT_TIMER_LENGTH}.
43 | * @param lengthInMillis Time in milliseconds before text is committed to undo queue
44 | */
45 | void setTimerLength(long lengthInMillis);
46 |
47 | /**
48 | * Updates attached {@link EditText} with text from the last entry in the undo queue, such that
49 | * it reverts to an earlier state.
50 | */
51 | void undo();
52 |
53 | /**
54 | * Reverts changes made by the last {@link #undo()} call.
55 | */
56 | void redo();
57 |
58 | /**
59 | * Removes all entries from both undo and redo queues.
60 | */
61 | void clearAllQueues();
62 |
63 | /**
64 | * Used by{@link RunDo} implementations to establish a link with an {@link EditText}
65 | */
66 | interface TextLink {
67 |
68 | /**
69 | *
70 | * @return The {@link EditText} to be monitored and updated by a {@link RunDo}
71 | * implementation.
72 | */
73 | EditText getEditTextForRunDo();
74 |
75 | }
76 |
77 | /**
78 | * Implement to receive callbacks whenever {@link #undo()} or {@link #redo()} methods are called
79 | */
80 | interface Callbacks {
81 |
82 | /**
83 | * {@link #undo()} called
84 | */
85 | void undoCalled();
86 |
87 | /**
88 | * {@link #redo()} called
89 | */
90 | void redoCalled();
91 |
92 | }
93 |
94 | /**
95 | * Returns a {@link RunDo} implementation which extends either
96 | * {@link android.support.v4.app.Fragment} or {@link android.app.Fragment}.
97 | */
98 | final class Factory {
99 |
100 | private Factory() { throw new AssertionError(); }
101 |
102 | @TargetApi(Build.VERSION_CODES.HONEYCOMB)
103 | public static RunDo getInstance(@NonNull android.app.FragmentManager fm) {
104 |
105 | RunDoNative frag = (RunDoNative) fm.findFragmentByTag(RunDo.TAG);
106 |
107 | if (frag == null) {
108 | frag = RunDoNative.newInstance();
109 | fm.beginTransaction().add(frag, RunDo.TAG).commit();
110 | }
111 |
112 | return frag;
113 |
114 | }
115 |
116 | public static RunDo getInstance(@NonNull android.support.v4.app.FragmentManager fm) {
117 |
118 | RunDoSupport frag = (RunDoSupport) fm.findFragmentByTag(RunDo.TAG);
119 |
120 | if (frag == null) {
121 | frag = RunDoSupport.newInstance();
122 | fm.beginTransaction().add(frag, RunDo.TAG).commit();
123 | }
124 |
125 | return frag;
126 |
127 | }
128 | }
129 |
130 | }
131 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # RunDo
2 | 
3 |
4 | __RunDo__ adds Undo/Redo functionality to `EditText` fields in Android.
5 |
6 | ## Releases
7 |
8 | #### [View Releases](https://github.com/PPartisan/RunDo/releases/ "Changelogs")
9 |
10 | [  ](https://bintray.com/ppartisan/maven/rundo/_latestVersion)
11 |
12 | Current version is `v1.0.5`
13 |
14 | JavaDoc available at [ppartisan.github.io](http://ppartisan.github.io/RunDo/JavaDoc/index.html "JavaDoc")
15 |
16 | ## Implementation ##
17 |
18 | #### Gradle Dependency
19 |
20 | ##### jcenter() & mavenCentral()
21 |
22 | Add the following to your module's `build.gradle` file:
23 |
24 | dependencies {
25 | compile 'com.werdpressed.partisan:rundo:1.0.5'
26 | }
27 |
28 | ##### maven
29 |
30 | In addition to the dependency above, add:
31 |
32 | repositories {
33 | maven {
34 | url 'https://dl.bintray.com/ppartisan/maven/'
35 | }
36 | }
37 |
38 | #### Usage
39 |
40 | Recommended usage is via the following method:
41 |
42 | RunDo.Factory.getInstance(FragmentManager fm);
43 |
44 | To be used in the following way (as an example):
45 |
46 | public class MyActivity extends Activity implements Rundo.TextLink, View.OnClickListener {
47 |
48 | private RunDo mRunDo;
49 | private EditText mEditText;
50 | private Button mButton;
51 |
52 | //...
53 |
54 | mRunDo = RunDo.Factory.getInstance(getFragmentManager());
55 | mButton.setOnClickListener(this);
56 |
57 | //...
58 |
59 | @Override
60 | public void onClick(View v) {
61 | int id = v.getId();
62 |
63 | switch (id) {
64 | case R.id.undo_button:
65 | mRunDo.undo();
66 | break;
67 | case R.id.redo_button:
68 | mRunDo.redo();
69 | break;
70 | }
71 |
72 | }
73 |
74 | @Override
75 | public EditText getEditText() {
76 | return mEditText;
77 | }
78 |
79 | }
80 |
81 | _See the [**sample app**](https://github.com/PPartisan/RunDo/blob/master/app/src/main/java/com/werdpressed/partisan/undoredo/) for a complete example_
82 |
83 | The `getInstance()` method requires a `FragmentManager` (whether [`android.app.FragmentManager`](http://developer.android.com/reference/android/app/FragmentManager.html) or [`android.suppoer.v4.app.FragmentManager`](http://developer.android.com/reference/android/support/v4/app/FragmentManager.html)) argument.
84 |
85 | `RunDo` implementations extend either [`android.app.Fragment`](http://developer.android.com/reference/android/app/Fragment.html) or [`android.support.v4.app.Fragment`](http://developer.android.com/reference/android/support/v4/app/Fragment.html).
86 |
87 | #### Calling Undo/Redo
88 |
89 | To call Undo or Redo, use the `undo()` and `redo()` methods:
90 |
91 | mRunDo.undo();
92 | mRunDo.redo();
93 |
94 | For example:
95 |
96 | @Override
97 | public void onClick(View v) {
98 | int id = v.getId();
99 | switch (id) {
100 | case R.id.undo_button:
101 | mRunDo.undo();
102 | break;
103 | case R.id.redo_button:
104 | mRunDo.redo();
105 | break;
106 | }
107 |
108 | }
109 |
110 | #### Tweaking Parameters
111 |
112 | There are two ways to customise `RunDo` objects; [`setQueueSize(int size)`](http://ppartisan.github.io/RunDo/JavaDoc/com/werdpressed/partisan/rundo/RunDo.html#setQueueSize(int)) and [`setTimerLength(long lengthInMillis)`](http://ppartisan.github.io/RunDo/JavaDoc/com/werdpressed/partisan/rundo/RunDo.html#setTimerLength(long)).
113 |
114 | `setQueueSize()` adjusts the size of the undo and redo queues to hold the specified number of entries, before entries from the opposite end of the queue begin to be removed. The default size is `10`. Calling this method will clear all current entries from both queues.
115 |
116 | `setTimerLength()` adjust the countdown between the user's last text entry and the period at which any altered text is saved to the undo queue. The timer is reset if further text is entered during this period. The default value is `2000` milliseconds (2 seconds).
117 |
118 | #### Clearing Queues
119 |
120 | Use [`clearAllQueues()`](http://ppartisan.github.io/RunDo/JavaDoc/com/werdpressed/partisan/rundo/RunDo.html#clearAllQueues()) to remove all elements from both undo and redo queues.
121 |
122 | #### Callbacks
123 |
124 | Implement [`RunDo.Callbacks`](http://ppartisan.github.io/RunDo/JavaDoc/com/werdpressed/partisan/rundo/RunDo.Callbacks.html) to be notified whenever [`undo()`](http://ppartisan.github.io/RunDo/JavaDoc/com/werdpressed/partisan/rundo/RunDo.html#undo()) or [`redo()`](http://ppartisan.github.io/RunDo/JavaDoc/com/werdpressed/partisan/rundo/RunDo.html#redo()) is called:
125 |
126 | @Override
127 | public void undoCalled() {
128 | }
129 |
130 | @Override
131 | public void redoCalled() {
132 | }
133 |
134 |
--------------------------------------------------------------------------------
/gradlew:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | ##############################################################################
4 | ##
5 | ## Gradle start up script for UN*X
6 | ##
7 | ##############################################################################
8 |
9 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
10 | DEFAULT_JVM_OPTS=""
11 |
12 | APP_NAME="Gradle"
13 | APP_BASE_NAME=`basename "$0"`
14 |
15 | # Use the maximum available, or set MAX_FD != -1 to use that value.
16 | MAX_FD="maximum"
17 |
18 | warn ( ) {
19 | echo "$*"
20 | }
21 |
22 | die ( ) {
23 | echo
24 | echo "$*"
25 | echo
26 | exit 1
27 | }
28 |
29 | # OS specific support (must be 'true' or 'false').
30 | cygwin=false
31 | msys=false
32 | darwin=false
33 | case "`uname`" in
34 | CYGWIN* )
35 | cygwin=true
36 | ;;
37 | Darwin* )
38 | darwin=true
39 | ;;
40 | MINGW* )
41 | msys=true
42 | ;;
43 | esac
44 |
45 | # For Cygwin, ensure paths are in UNIX format before anything is touched.
46 | if $cygwin ; then
47 | [ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"`
48 | fi
49 |
50 | # Attempt to set APP_HOME
51 | # Resolve links: $0 may be a link
52 | PRG="$0"
53 | # Need this for relative symlinks.
54 | while [ -h "$PRG" ] ; do
55 | ls=`ls -ld "$PRG"`
56 | link=`expr "$ls" : '.*-> \(.*\)$'`
57 | if expr "$link" : '/.*' > /dev/null; then
58 | PRG="$link"
59 | else
60 | PRG=`dirname "$PRG"`"/$link"
61 | fi
62 | done
63 | SAVED="`pwd`"
64 | cd "`dirname \"$PRG\"`/" >&-
65 | APP_HOME="`pwd -P`"
66 | cd "$SAVED" >&-
67 |
68 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
69 |
70 | # Determine the Java command to use to start the JVM.
71 | if [ -n "$JAVA_HOME" ] ; then
72 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
73 | # IBM's JDK on AIX uses strange locations for the executables
74 | JAVACMD="$JAVA_HOME/jre/sh/java"
75 | else
76 | JAVACMD="$JAVA_HOME/bin/java"
77 | fi
78 | if [ ! -x "$JAVACMD" ] ; then
79 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
80 |
81 | Please set the JAVA_HOME variable in your environment to match the
82 | location of your Java installation."
83 | fi
84 | else
85 | JAVACMD="java"
86 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
87 |
88 | Please set the JAVA_HOME variable in your environment to match the
89 | location of your Java installation."
90 | fi
91 |
92 | # Increase the maximum file descriptors if we can.
93 | if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then
94 | MAX_FD_LIMIT=`ulimit -H -n`
95 | if [ $? -eq 0 ] ; then
96 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
97 | MAX_FD="$MAX_FD_LIMIT"
98 | fi
99 | ulimit -n $MAX_FD
100 | if [ $? -ne 0 ] ; then
101 | warn "Could not set maximum file descriptor limit: $MAX_FD"
102 | fi
103 | else
104 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
105 | fi
106 | fi
107 |
108 | # For Darwin, add options to specify how the application appears in the dock
109 | if $darwin; then
110 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
111 | fi
112 |
113 | # For Cygwin, switch paths to Windows format before running java
114 | if $cygwin ; then
115 | APP_HOME=`cygpath --path --mixed "$APP_HOME"`
116 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
117 |
118 | # We build the pattern for arguments to be converted via cygpath
119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
120 | SEP=""
121 | for dir in $ROOTDIRSRAW ; do
122 | ROOTDIRS="$ROOTDIRS$SEP$dir"
123 | SEP="|"
124 | done
125 | OURCYGPATTERN="(^($ROOTDIRS))"
126 | # Add a user-defined pattern to the cygpath arguments
127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then
128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
129 | fi
130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
131 | i=0
132 | for arg in "$@" ; do
133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
135 |
136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
138 | else
139 | eval `echo args$i`="\"$arg\""
140 | fi
141 | i=$((i+1))
142 | done
143 | case $i in
144 | (0) set -- ;;
145 | (1) set -- "$args0" ;;
146 | (2) set -- "$args0" "$args1" ;;
147 | (3) set -- "$args0" "$args1" "$args2" ;;
148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
154 | esac
155 | fi
156 |
157 | # Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules
158 | function splitJvmOpts() {
159 | JVM_OPTS=("$@")
160 | }
161 | eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS
162 | JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME"
163 |
164 | exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@"
165 |
--------------------------------------------------------------------------------
/rundo/rundo.iml:
--------------------------------------------------------------------------------
1 |
2 |
68 | * {@code
69 | * int firstDeviation, lastDeviation;
70 | *
71 | * char[] mOldText = new String("one").toCharArray();
72 | * char[] mNewText = new String("one one").toCharArray();
73 | *
74 | * findFirstDeviation();
75 | * findLastDeviation();
76 | *
77 | * String output = new String(mNewText).subString(firstDeviation, lastDeviation);
78 | *
79 | * //firstDeviation will equal 3, last deviation 4, and "output" will be " ".
80 | * }
81 | *
82 | *
83 | * This is because the first deviation comes after the first "e", at index 3, yet when the
84 | * arrays are reversed, the "e" at the end of "one" in mOldText shifts to index 0. It is
85 | * effectively counted twice:
86 | *
87 | *
88 | * {@code
89 | * char[] mOldText = new char[]{ 'o', 'n', 'e' }
90 | * char[] mNewText = new char[]{ 'o', 'n', 'e', ' ', 'o', 'n', 'e' };
91 | *
92 | * mOldTextReversed = new char[]{ 'e', 'n', 'o' };
93 | * mNewTextReversed = new char[]{ 'e', 'n', 'o', ' ', 'e', 'n', 'o'};
94 | * }
95 | *
96 | *
97 | * lastDeviation values are adjusted in {@link #findLastDeviationOffsetSize(char[], char[], int)}
98 | * to account for such situations.
99 | *
100 | * @return Last point of deviation between old and new text, in relation to old text.
101 | *
102 | * @see #findLastDeviationNewText()
103 | * @see #getLastDeviationOldText()
104 | * @see Item#getLastDeviationOldText()
105 | */
106 | private int findLastDeviationOldText() {
107 |
108 | if (isOldTextEqualToNewText()) return 0;
109 |
110 | final int shortestLength = findShortestLength();
111 |
112 | final char[] oldTextReversed = SubtractStringUtils.reverseCharArray(mOldText);
113 | final char[] newTextReversed = SubtractStringUtils.reverseCharArray(mNewText);
114 |
115 | int tempLastDeviation = shortestLength;
116 | int difference = Math.abs((mNewText.length - mOldText.length));
117 |
118 | for (int i = 0; i < shortestLength; i++) {
119 | if (oldTextReversed[i] != newTextReversed[i]) {
120 | tempLastDeviation = i;
121 | break;
122 | }
123 | }
124 |
125 | int lastDeviation =
126 | findLastDeviationOffsetSize(oldTextReversed, newTextReversed, tempLastDeviation);
127 |
128 | return (isNewTextLonger() ? (lastDeviation - difference) : lastDeviation);
129 |
130 | }
131 |
132 | /**
133 | * Identical process to {@link #findLastDeviationOldText()}, except value returned is in
134 | * relation to new text.
135 | *
136 | * @return Last point of deviation between old and new text, in relation to new text.
137 | *
138 | * @see #findLastDeviationOldText()
139 | * @see #getLastDeviationNewText()
140 | * @see Item#getLastDeviationNewText()
141 | */
142 | private int findLastDeviationNewText() {
143 |
144 | if (isOldTextEqualToNewText()) return 0;
145 |
146 | final int shortestLength = findShortestLength();
147 |
148 | final char[] oldTextReversed = SubtractStringUtils.reverseCharArray(mOldText);
149 | final char[] newTextReversed = SubtractStringUtils.reverseCharArray(mNewText);
150 |
151 | int tempLastDeviation = shortestLength;
152 | int difference = Math.abs((mNewText.length - mOldText.length));
153 |
154 | for (int i = 0; i < shortestLength; i++) {
155 | if (oldTextReversed[i] != newTextReversed[i]) {
156 | tempLastDeviation = i;
157 | break;
158 | }
159 | }
160 |
161 | int lastDeviation =
162 | findLastDeviationOffsetSize(oldTextReversed, newTextReversed, tempLastDeviation);
163 |
164 | return (isNewTextLonger() ? lastDeviation : (lastDeviation - difference));
165 |
166 | }
167 |
168 | /**
169 | * Adjusts the last point at which the two {@code char[]} diverge, due to the reasons outlined in
170 | * {@link #findLastDeviationOldText()}. This is achieved by calculating the difference in length between
171 | * the old and new text, and comparing each {@code char} from this final end point to the char at the
172 | * same position less the offset difference. If the same value is found, then the current
173 | * index is used to determine the true last deviation value. For example:
174 | *
175 | *
176 | * {@code
177 | * mOldTextReversed = new char[]{ 'e', 'n', 'o' };
178 | * mNewTextReversed = new char[]{ 'e', 'n', 'o', ' ', 'e', 'n', 'o'};
179 | * }
180 | *
181 | *
182 | * In this case, the potential offset size (the length of the longest array subtracted from the
183 | * shortest) is {@code(7 - 3) = 4}. Thus, the char at index [4] of the longest array, which is
184 | * 'e', is compared to the char at index[0] (4 - (potentialOffsetSize of 4) = 0), which is also
185 | * 'e'. As the two values match, the final value returned is
186 | * (length of the longest array - (current index - potential offset size)), which translates
187 | * to (7 - (4 - 0)) = 3.
188 | *
189 | * @param oldText Reversed old text
190 | * @param newText Reversed new text
191 | * @param tempLastDeviation The current last deviation figure. This is the value that will be
192 | * checked and possibly altered before being returned.
193 | * @return The adjusted last deviation value.
194 | */
195 | private int findLastDeviationOffsetSize(char[] oldText, char[] newText, int tempLastDeviation) {
196 |
197 | final char[] longestArray = (isNewTextLonger()) ? newText : oldText;
198 | final int potentialOffsetSize = (newText.length - oldText.length);
199 |
200 | boolean isOffsetWithinArrayBounds =
201 | ((tempLastDeviation + potentialOffsetSize) < longestArray.length);
202 |
203 | final int maxValue = (isOffsetWithinArrayBounds)
204 | ? (tempLastDeviation + potentialOffsetSize)
205 | : longestArray.length;
206 |
207 | final int reverseDeviation = (tempLastDeviation < potentialOffsetSize)
208 | ? potentialOffsetSize
209 | : tempLastDeviation;
210 |
211 | for (int i = reverseDeviation; i < maxValue; i++) {
212 |
213 | if (longestArray[i] == longestArray[i - reverseDeviation]) {
214 | return (longestArray.length - (i - reverseDeviation));
215 | }
216 | }
217 |
218 | return findLongestLength();
219 |
220 | }
221 |
222 | private int findLastDeviationOldTextFromNew() {
223 | final int difference = findLengthDifference();
224 | return (isNewTextLonger())
225 | ? (lastDeviationNewText - difference)
226 | : (lastDeviationNewText + difference);
227 | }
228 |
229 | private int findLastDeviationNewTextFromOld() {
230 | final int difference = findLengthDifference();
231 | return (isNewTextLonger())
232 | ? (lastDeviationOldText + difference)
233 | : (lastDeviationOldText - difference);
234 | }
235 |
236 |
237 | /**
238 | * Populates the {@link #deviationType} field with one of three constant values,
239 | * representing an ADDITION of text, from old to new, with no text from old replaced. DELETION,
240 | * showing a removal of text from old to new, in which no text was replaced, and a REPLACEMENT,
241 | * in which text has been either added or removed from old to new, and overwritten the old text
242 | * in part or in its entirety. For example:
243 | *
244 | * ADDITION: The difference between "one" and "one two".
245 | *
246 | * DELETION: The difference between "one two" and "one".
247 | *
248 | * REPLACEMENT: The difference between "one" and "two".
249 | *
250 | * @see #getDeviationType()
251 | */
252 | private int findDeviationType() {
253 |
254 | if (firstDeviation == -1) getFirstDeviation();
255 |
256 | int deviationType;
257 |
258 | if (isNewTextLonger()) {
259 |
260 | if (lastDeviationNewText == -1) getLastDeviationNewText();
261 |
262 | deviationType = (SubtractStringUtils.isArrayEqualWithOmission(
263 | mNewText, mOldText, firstDeviation, lastDeviationNewText)
264 | )
265 | ? ADDITION
266 | : REPLACEMENT;
267 | } else if(isTextLengthEqual()) {
268 | deviationType = (isOldTextEqualToNewText())
269 | ? UNCHANGED
270 | : REPLACEMENT;
271 | } else {
272 |
273 | if (lastDeviationOldText == -1) getLastDeviationOldText();
274 |
275 | deviationType = (SubtractStringUtils.isArrayEqualWithOmission(
276 | mNewText, mOldText, firstDeviation, lastDeviationOldText)
277 | )
278 | ? DELETION
279 | : REPLACEMENT;
280 | }
281 |
282 | return deviationType;
283 |
284 | }
285 |
286 | private boolean isOldTextEqualToNewText() {
287 | return Arrays.equals(mOldText, mNewText);
288 | }
289 |
290 | private boolean isNewTextLonger() {
291 | return (mNewText.length > mOldText.length);
292 | }
293 |
294 | private boolean isTextLengthEqual() {
295 | return (mNewText.length == mOldText.length);
296 | }
297 |
298 | private int findLongestLength() {
299 | return Math.max(mNewText.length, mOldText.length);
300 | }
301 |
302 | private int findShortestLength() {
303 | return Math.min(mNewText.length, mOldText.length);
304 | }
305 |
306 | private int findLengthDifference() {
307 | return findLongestLength() - findShortestLength();
308 | }
309 |
310 | /**
311 | *
312 | * @return First deviation
313 | */
314 | int getFirstDeviation() {
315 |
316 | if (firstDeviation == -1) {
317 | firstDeviation = findFirstDeviation();
318 | }
319 |
320 | return firstDeviation;
321 | }
322 |
323 | int getLastDeviationOldText() {
324 |
325 | if (lastDeviationOldText == -1) {
326 | lastDeviationOldText = (lastDeviationNewText == -1 )
327 | ? findLastDeviationOldText()
328 | : findLastDeviationOldTextFromNew();
329 | }
330 |
331 | return lastDeviationOldText;
332 | }
333 |
334 | int getLastDeviationNewText() {
335 |
336 | if (lastDeviationNewText == -1) {
337 | lastDeviationNewText = (lastDeviationOldText == -1)
338 | ? findLastDeviationNewText()
339 | : findLastDeviationNewTextFromOld();
340 | }
341 |
342 | return lastDeviationNewText;
343 | }
344 |
345 | /**
346 | *
347 | * @return Deviation type, in the form of an int value. For a String representation, use
348 | * {@link #valueOfDeviation(int)} ()}
349 | */
350 | int getDeviationType() {
351 | if (deviationType == -1) {
352 | deviationType = findDeviationType();
353 | }
354 | return deviationType;
355 | }
356 |
357 | /**
358 | * Converts {@code int} value returned by {@link #getDeviationType()} to {@link String}
359 | * representation.
360 | * @param deviationType Value return from {@link #getDeviationType()}
361 | * @return String representation of argument if argument is valid. Otherwise, {@code null}
362 | */
363 | @SuppressWarnings("unused")
364 | public static String valueOfDeviation(int deviationType) {
365 |
366 | switch (deviationType) {
367 | case ADDITION:
368 | return "Addition";
369 | case DELETION:
370 | return "Deletion";
371 | case REPLACEMENT:
372 | return "Replacement";
373 | case UNCHANGED:
374 | return "Unchanged";
375 | default:
376 | return null;
377 | }
378 |
379 | }
380 |
381 | /**
382 | *
383 | * @return If text has been added or replaced, returns a substring of the new text that has
384 | * been altered in respect to old text.
385 | */
386 | public String getAlteredText() {
387 |
388 | if (firstDeviation == -1) getFirstDeviation();
389 | if (lastDeviationNewText == -1) getLastDeviationNewText();
390 |
391 | switch (deviationType) {
392 | case ADDITION:
393 | case REPLACEMENT:
394 | return new String(mNewText).substring(firstDeviation, lastDeviationNewText);
395 | }
396 |
397 | return "";
398 | }
399 |
400 | /**
401 | *
402 | * @return If text has been deleted or replaced, returns a substring of the old text that has
403 | * been removed or overwritten.
404 | */
405 | public String getReplacedText() {
406 |
407 | if (firstDeviation == -1) getFirstDeviation();
408 | if (lastDeviationOldText == -1) getLastDeviationOldText();
409 |
410 | switch (deviationType) {
411 | case DELETION:
412 | case REPLACEMENT:
413 | return new String(mOldText).substring(firstDeviation, lastDeviationOldText);
414 | }
415 |
416 | return "";
417 |
418 | }
419 |
420 | /**
421 | *
422 | * @return Encapsulates all important information relating to the differences between old and
423 | * new text.
424 | * @see com.werdpressed.partisan.rundo.SubtractStrings.Item
425 | */
426 | public Item getItem() {
427 |
428 | if (mItem == null) {
429 | mItem = new Item(
430 | getFirstDeviation(),
431 | getLastDeviationOldText(),
432 | getLastDeviationNewText(),
433 | getDeviationType(),
434 | getReplacedText(),
435 | getAlteredText()
436 | );
437 | }
438 |
439 | return mItem;
440 | }
441 |
442 | /**
443 | * Model class which encapsulates all important pieces of information relating to the differences
444 | * between two {@link String}s, calculated by {@link SubtractStrings}
445 | */
446 | static final class Item implements Parcelable {
447 |
448 | private final int firstDeviation, lastDeviationOldText, lastDeviationNewText, deviationType;
449 | private final String replacedText, alteredText;
450 |
451 | Item(
452 | int firstDeviation,
453 | int lastDeviationOldText,
454 | int lastDeviationNewText,
455 | int deviationType,
456 | String replacedText,
457 | String alteredText
458 | ) {
459 | this.firstDeviation = firstDeviation;
460 | this.lastDeviationOldText = lastDeviationOldText;
461 | this.lastDeviationNewText = lastDeviationNewText;
462 | this.deviationType = deviationType;
463 | this.replacedText = replacedText;
464 | this.alteredText = alteredText;
465 | }
466 |
467 | protected Item(Parcel in) {
468 | firstDeviation = in.readInt();
469 | lastDeviationOldText = in.readInt();
470 | lastDeviationNewText = in.readInt();
471 | deviationType = in.readInt();
472 | replacedText = in.readString();
473 | alteredText = in.readString();
474 | }
475 |
476 | /**
477 | *
478 | * @return First point of deviation between old and new text.
479 | */
480 | public int getFirstDeviation() {
481 | return firstDeviation;
482 | }
483 |
484 | /**
485 | *
486 | * @return Last point of deviation between old and new text, in relation to old text.
487 | */
488 | public int getLastDeviationOldText() {
489 | return lastDeviationOldText;
490 | }
491 |
492 | /**
493 | *
494 | * @return Last point of deviation between old and new text, in relation to new text.
495 | */
496 | public int getLastDeviationNewText() {
497 | return lastDeviationNewText;
498 | }
499 |
500 | /**
501 | *
502 | * @return Deviation type in the form of an {@code int}. Value will correlate to
503 | * {@link #ADDITION}. {@link #REPLACEMENT}, {@link #DELETION} or {@link #UNCHANGED}. For
504 | * a {@link String} representation, use {@link #valueOfDeviation(int)}
505 | *
506 | * @see #valueOfDeviation(int)
507 | */
508 | public int getDeviationType() {
509 | return deviationType;
510 | }
511 |
512 | /**
513 | *
514 | * @return Section of old text replaced by new text, if applicable.
515 | */
516 | public String getReplacedText() {
517 | return replacedText;
518 | }
519 |
520 | /**
521 | *
522 | * @return Section of new text replaced by old text, if applicable.
523 | */
524 | public String getAlteredText() {
525 | return alteredText;
526 | }
527 |
528 | public static final Creator