GreedyStringTiling.java 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235
  1. package de.jplag;
  2. import static de.jplag.TokenConstants.FILE_END;
  3. import static de.jplag.TokenConstants.SEPARATOR_TOKEN;
  4. import java.util.ArrayList;
  5. import java.util.List;
  6. import de.jplag.options.JPlagOptions;
  7. /**
  8. * This class implements the Greedy String Tiling algorithm as introduced by Michael Wise. However, it is very specific
  9. * to the classes {@link TokenList}, {@link Token}, and {@link Match}. While this class was reworked, it still contains
  10. * some quirks from the initial version.
  11. * @see <a href=
  12. * "https://www.researchgate.net/publication/262763983_String_Similarity_via_Greedy_String_Tiling_and_Running_Karp-Rabin_Matching">
  13. * String Similarity via Greedy String Tiling and Running Karp−Rabin Matching </a>
  14. */
  15. public class GreedyStringTiling {
  16. private final JPlagOptions options;
  17. public GreedyStringTiling(JPlagOptions options) {
  18. this.options = options;
  19. }
  20. /**
  21. * Creating hashes in linear time. The hash-code will be written in every Token for the next &lt;hashLength&gt; token
  22. * (includes the Token itself).
  23. * @param tokenList contains the tokens.
  24. * @param hashLength is the hash length (condition: 1 &lt; hashLength &lt; 26)
  25. * @param makeTable determines if a simple hash table is created in the structure.
  26. */
  27. public void createHashes(TokenList tokenList, int hashLength, boolean makeTable) {
  28. // Here the upper boundary of the hash length is set.
  29. // It is determined by the number of bits of the 'int' data type and the number of tokens.
  30. if (hashLength < 1) {
  31. hashLength = 1;
  32. }
  33. hashLength = (hashLength < 26 ? hashLength : 25);
  34. if (tokenList.size() < hashLength) {
  35. return;
  36. }
  37. int modulo = ((1 << 6) - 1); // Modulo 64!
  38. int loops = tokenList.size() - hashLength;
  39. tokenList.tokenHashes = (makeTable ? new TokenHashMap(3 * loops) : null);
  40. int hash = 0;
  41. int hashedLength = 0;
  42. for (int i = 0; i < hashLength; i++) {
  43. hash = (2 * hash) + (tokenList.getToken(i).type & modulo);
  44. hashedLength++;
  45. if (tokenList.getToken(i).isMarked()) {
  46. hashedLength = 0;
  47. }
  48. }
  49. int factor = (hashLength != 1 ? (2 << (hashLength - 2)) : 1);
  50. if (makeTable) {
  51. for (int i = 0; i < loops; i++) {
  52. if (hashedLength >= hashLength) {
  53. tokenList.getToken(i).setHash(hash);
  54. tokenList.tokenHashes.put(hash, i); // add into hashtable
  55. } else {
  56. tokenList.getToken(i).setHash(-1);
  57. }
  58. hash -= factor * (tokenList.getToken(i).type & modulo);
  59. hash = (2 * hash) + (tokenList.getToken(i + hashLength).type & modulo);
  60. if (tokenList.getToken(i + hashLength).isMarked()) {
  61. hashedLength = 0;
  62. } else {
  63. hashedLength++;
  64. }
  65. }
  66. } else {
  67. for (int i = 0; i < loops; i++) {
  68. tokenList.getToken(i).setHash((hashedLength >= hashLength) ? hash : -1);
  69. hash -= factor * (tokenList.getToken(i).type & modulo);
  70. hash = (2 * hash) + (tokenList.getToken(i + hashLength).type & modulo);
  71. if (tokenList.getToken(i + hashLength).isMarked()) {
  72. hashedLength = 0;
  73. } else {
  74. hashedLength++;
  75. }
  76. }
  77. }
  78. tokenList.hashLength = hashLength;
  79. }
  80. public final JPlagComparison compare(Submission firstSubmission, Submission secondSubmission) {
  81. return swapAndCompare(firstSubmission, secondSubmission, false);
  82. }
  83. public final JPlagComparison compareWithBaseCode(Submission firstSubmission, Submission secondSubmission) {
  84. return swapAndCompare(firstSubmission, secondSubmission, true);
  85. }
  86. private JPlagComparison swapAndCompare(Submission firstSubmission, Submission secondSubmission, boolean isBaseCodeComparison) {
  87. Submission smallerSubmission;
  88. Submission largerSubmission;
  89. if (firstSubmission.getTokenList().size() > secondSubmission.getTokenList().size()) {
  90. smallerSubmission = secondSubmission;
  91. largerSubmission = firstSubmission;
  92. } else {
  93. smallerSubmission = firstSubmission;
  94. largerSubmission = secondSubmission;
  95. }
  96. // if hashtable exists in first but not in second structure: flip around!
  97. if (largerSubmission.getTokenList().tokenHashes == null && smallerSubmission.getTokenList().tokenHashes != null) {
  98. Submission swap = smallerSubmission;
  99. smallerSubmission = largerSubmission;
  100. largerSubmission = swap;
  101. }
  102. return compare(smallerSubmission, largerSubmission, isBaseCodeComparison);
  103. }
  104. /**
  105. * Compares two submissions. FILE_END is used as pivot
  106. * @param firstSubmission is the submission with the smaller sequence.
  107. * @param secondSubmission is the submission with the larger sequence.
  108. * @param isBaseCodeComparison specifies whether one of the submissions is the base code.
  109. * @return the comparison results.
  110. */
  111. private JPlagComparison compare(Submission firstSubmission, Submission secondSubmission, boolean isBaseCodeComparison) {
  112. // first and second refer to the list of tokens of the first and second submission:
  113. TokenList first = firstSubmission.getTokenList();
  114. TokenList second = secondSubmission.getTokenList();
  115. // Initialize:
  116. JPlagComparison comparison = new JPlagComparison(firstSubmission, secondSubmission);
  117. int minimumTokenMatch = options.getMinimumTokenMatch(); // minimal required token match
  118. if (first.size() <= minimumTokenMatch || second.size() <= minimumTokenMatch) { // <= because of pivots!
  119. return comparison;
  120. }
  121. markTokens(first, isBaseCodeComparison);
  122. markTokens(second, isBaseCodeComparison);
  123. // create hashes:
  124. if (first.hashLength != minimumTokenMatch) {
  125. createHashes(first, minimumTokenMatch, isBaseCodeComparison); // don't make table if it is not a base code comparison
  126. }
  127. if (second.hashLength != minimumTokenMatch || second.tokenHashes == null) {
  128. createHashes(second, minimumTokenMatch, true);
  129. }
  130. List<Match> matches = new ArrayList<>();
  131. // start the black magic:
  132. int maxMatch;
  133. do {
  134. maxMatch = minimumTokenMatch;
  135. matches.clear();
  136. for (int x = 0; x < first.size() - maxMatch; x++) {
  137. List<Integer> hashedTokens = second.tokenHashes.get(first.getToken(x).getHash());
  138. if (first.getToken(x).isMarked() || first.getToken(x).getHash() == -1) {
  139. continue;
  140. }
  141. inner: for (Integer y : hashedTokens) {
  142. if (second.getToken(y).isMarked() || maxMatch >= second.size() - y) { // >= because of pivots!
  143. continue;
  144. }
  145. int j, hx, hy;
  146. for (j = maxMatch - 1; j >= 0; j--) { // begins comparison from behind
  147. if (first.getToken(hx = x + j).type != second.getToken(hy = y + j).type || first.getToken(hx).isMarked()
  148. || second.getToken(hy).isMarked()) {
  149. continue inner;
  150. }
  151. }
  152. // expand match
  153. j = maxMatch;
  154. while (first.getToken(hx = x + j).type == second.getToken(hy = y + j).type && !first.getToken(hx).isMarked()
  155. && !second.getToken(hy).isMarked()) {
  156. j++;
  157. }
  158. if (j > maxMatch && !isBaseCodeComparison || j != maxMatch && isBaseCodeComparison) { // new biggest match? -> delete current
  159. // smaller
  160. matches.clear();
  161. maxMatch = j;
  162. }
  163. addMatchIfNotOverlapping(matches, x, y, j);
  164. }
  165. }
  166. for (int i = matches.size() - 1; i >= 0; i--) {
  167. int x = matches.get(i).startOfFirst(); // Beginning of/in sequence A
  168. int y = matches.get(i).startOfSecond(); // Beginning of/in sequence B
  169. comparison.addMatch(x, y, matches.get(i).length());
  170. // in order that "Match" will be newly build (because reusing)
  171. for (int j = matches.get(i).length(); j > 0; j--) {
  172. first.getToken(x).setMarked(true); // mark all Tokens!
  173. second.getToken(y).setMarked(true);
  174. if (isBaseCodeComparison) {
  175. first.getToken(x).setBasecode(true);
  176. second.getToken(y).setBasecode(true);
  177. }
  178. x++;
  179. y++;
  180. }
  181. }
  182. } while (maxMatch != minimumTokenMatch);
  183. return comparison;
  184. }
  185. private void addMatchIfNotOverlapping(List<Match> matches, int startA, int startB, int length) {
  186. for (int i = matches.size() - 1; i >= 0; i--) { // starting at the end is better(?)
  187. if (matches.get(i).overlap(startA, startB, length)) {
  188. return; // no overlaps allowed!
  189. }
  190. }
  191. matches.add(new Match(startA, startB, length));
  192. }
  193. /**
  194. * Disable finding a match at separator tokens and basecode matches for non-basecode comparisons.
  195. * @param tokenList Tokens to mark.
  196. * @param isBaseCodeComparison Whether the {@link Token#isBasecode()} matches should be enabled for matching.
  197. */
  198. private void markTokens(TokenList tokenList, boolean isBaseCodeComparison) {
  199. for (Token token : tokenList.allTokens()) {
  200. if (isBaseCodeComparison) {
  201. token.setMarked(token.type == FILE_END || token.type == SEPARATOR_TOKEN);
  202. } else {
  203. token.setMarked(token.type == FILE_END || token.type == SEPARATOR_TOKEN || (token.isBasecode() && options.hasBaseCode()));
  204. }
  205. }
  206. }
  207. }