Skip to content

Commit ae63991

Browse files
committed
Refactor and rename class for KMR algorithm.
1 parent 46c1a1d commit ae63991

3 files changed

Lines changed: 132 additions & 139 deletions

File tree

src/main/java/com/github/refhumbold/algolib/text/BaseWordsMap.java renamed to src/main/java/com/github/refhumbold/algolib/text/BasicFactorsMap.java

Lines changed: 48 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,21 @@
11
package com.github.refhumbold.algolib.text;
22

3-
import java.util.Comparator;
43
import java.util.HashMap;
54
import java.util.List;
65
import java.util.Map;
76
import java.util.Objects;
87
import java.util.function.BiFunction;
98
import java.util.stream.IntStream;
10-
import com.github.refhumbold.algolib.tuples.ComparablePair;
9+
import java.util.stream.Stream;
1110
import com.github.refhumbold.algolib.tuples.Pair;
1211

13-
/** Structure of base words map using Karp-Miller-Rosenberg algorithm. */
14-
public final class BaseWordsMap
12+
/** Structure of basic factors map using Karp-Miller-Rosenberg algorithm. */
13+
public final class BasicFactorsMap
1514
{
1615
private final String text;
1716
private final Map<Pair<Integer, Integer>, Integer> factors = new HashMap<>();
1817

19-
private BaseWordsMap(String text)
18+
private BasicFactorsMap(String text)
2019
{
2120
this.text = text;
2221
create();
@@ -27,21 +26,16 @@ public String getText()
2726
return text;
2827
}
2928

30-
public static BaseWordsMap build(String text)
29+
public static BasicFactorsMap build(String text)
3130
{
32-
return new BaseWordsMap(text);
31+
return new BasicFactorsMap(text);
3332
}
3433

3534
@Override
3635
public boolean equals(Object obj)
3736
{
38-
if(this == obj)
39-
return true;
40-
41-
if(!(obj instanceof BaseWordsMap other))
42-
return false;
43-
44-
return Objects.equals(text, other.text) && Objects.equals(factors, other.factors);
37+
return this == obj || obj instanceof BasicFactorsMap other && Objects.equals(text,
38+
other.text) && Objects.equals(factors, other.factors);
4539
}
4640

4741
@Override
@@ -68,57 +62,51 @@ public Pair<Integer, Integer> getCode(int startIndex)
6862
*/
6963
public Pair<Integer, Integer> getCode(int startIndex, int endIndex)
7064
{
71-
if(startIndex < 0 || startIndex >= text.length())
72-
throw new IndexOutOfBoundsException("Index out of range %d".formatted(startIndex));
73-
74-
if(endIndex < 0 || endIndex > text.length())
75-
throw new IndexOutOfBoundsException("Index out of range %d".formatted(endIndex));
65+
if(startIndex < 0 || startIndex >= text.length() || endIndex < 0
66+
|| endIndex > text.length())
67+
throw new IndexOutOfBoundsException(
68+
"Range [%d, %d) out of bounds for text".formatted(startIndex, endIndex));
7669

7770
if(endIndex <= startIndex)
78-
return Pair.of(0, 0);
71+
throw new IllegalArgumentException("Empty range of indices");
7972

8073
Integer code = factors.get(Pair.of(startIndex, endIndex));
8174

8275
if(code != null)
8376
return Pair.of(code, 0);
8477

8578
int n = getMaxLength(endIndex - startIndex);
79+
8680
return Pair.of(factors.get(Pair.of(startIndex, startIndex + n)),
8781
factors.get(Pair.of(endIndex - n, endIndex)));
8882
}
8983

90-
// Builds base words map using Karp-Miller-Rosenberg algorithm.
84+
// Builds basic factors map using Karp-Miller-Rosenberg algorithm.
9185
private void create()
9286
{
9387
int codeValue = extend(1, 0,
94-
(i, length) -> new int[]{ text.charAt(i), 1 + text.charAt(i), i, i + length });
88+
(i, length) -> new ExtensionCode(text.charAt(i), 1 + text.charAt(i), i));
9589

9690
for(int currentLength = 2; currentLength <= text.length(); currentLength *= 2)
9791
codeValue = extend(currentLength, codeValue,
98-
(i, length) -> new int[]{ factors.get(Pair.of(i, i + length / 2)),
99-
factors.get(Pair.of(i + length / 2, i + length)), i, i + length });
92+
(i, length) -> new ExtensionCode(factors.get(Pair.of(i, i + length / 2)),
93+
factors.get(Pair.of(i + length / 2, i + length)), i));
10094
}
10195

10296
// Encodes substring of given length using already counted factors.
103-
private int extend(int length, int codeValue, BiFunction<Integer, Integer, int[]> func)
97+
private int extend(int length, int codeValue, BiFunction<Integer, Integer, ExtensionCode> func)
10498
{
105-
ComparablePair<Integer, Integer> previousCode = ComparablePair.of(0, 0);
106-
List<int[]> codes = IntStream.range(0, text.length() - length + 1)
107-
.mapToObj(i -> func.apply(i, length))
108-
.sorted(new CodesComparator())
109-
.toList();
99+
List<ExtensionCode> codes = Stream.concat(Stream.of(new ExtensionCode(0, 0, -1)),
100+
IntStream.range(0, text.length() - length + 1)
101+
.mapToObj(i -> func.apply(i, length))
102+
.sorted()).toList();
110103

111-
for(int[] code : codes)
104+
for(int i = 1; i < codes.size(); ++i)
112105
{
113-
ComparablePair<Integer, Integer> codePair = ComparablePair.of(code[0], code[1]);
114-
115-
if(!Objects.equals(previousCode, codePair))
116-
{
106+
if(!codes.get(i).equals(codes.get(i - 1)))
117107
++codeValue;
118-
previousCode = codePair;
119-
}
120108

121-
factors.put(Pair.of(code[2], code[3]), codeValue);
109+
factors.put(Pair.of(codes.get(i).index, codes.get(i).index + length), codeValue);
122110
}
123111

124112
return codeValue;
@@ -138,17 +126,31 @@ private int getMaxLength(int n)
138126
return prev;
139127
}
140128

141-
private static class CodesComparator
142-
implements Comparator<int[]>
129+
private record ExtensionCode(int prefixCode, int suffixCode, int index)
130+
implements Comparable<ExtensionCode>
143131
{
144132
@Override
145-
public int compare(int[] a1, int[] a2)
133+
public boolean equals(Object obj)
134+
{
135+
return this == obj
136+
|| obj instanceof ExtensionCode other && prefixCode == other.prefixCode
137+
&& suffixCode == other.suffixCode;
138+
}
139+
140+
@Override
141+
public int hashCode()
146142
{
147-
return IntStream.range(0, Math.min(a1.length, a2.length))
148-
.map(i -> Integer.compare(a1[i], a2[i]))
149-
.filter(compareInts -> compareInts != 0)
150-
.findFirst()
151-
.orElse(0);
143+
return Objects.hash(prefixCode, suffixCode);
144+
}
145+
146+
@Override
147+
public int compareTo(ExtensionCode code)
148+
{
149+
int comparePrefixCodes = Integer.compare(prefixCode, code.prefixCode);
150+
151+
return comparePrefixCodes != 0
152+
? comparePrefixCodes
153+
: Integer.compare(suffixCode, code.suffixCode);
152154
}
153155
}
154156
}

src/test/java/com/github/refhumbold/algolib/text/BaseWordsMapTest.java

Lines changed: 0 additions & 93 deletions
This file was deleted.
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
package com.github.refhumbold.algolib.text;
2+
3+
import java.util.List;
4+
import java.util.stream.IntStream;
5+
import java.util.stream.Stream;
6+
import org.assertj.core.api.Assertions;
7+
import org.junit.jupiter.api.BeforeEach;
8+
import org.junit.jupiter.api.Test;
9+
import org.junit.jupiter.params.ParameterizedTest;
10+
import org.junit.jupiter.params.provider.Arguments;
11+
import org.junit.jupiter.params.provider.MethodSource;
12+
import com.github.refhumbold.algolib.tuples.Pair;
13+
14+
// Tests: Structure of basic factors map using Karp-Miller-Rosenberg algorithm.
15+
public class BasicFactorsMapTest
16+
{
17+
private BasicFactorsMap testObject;
18+
19+
@BeforeEach
20+
public void setUp()
21+
{
22+
testObject = BasicFactorsMap.build("mississippi");
23+
}
24+
25+
@ParameterizedTest
26+
@MethodSource("paramsFor_GetCode_WhenRange")
27+
public void getCode_WhenRange_ThenCode(
28+
int startIndex,
29+
Integer endIndex,
30+
Pair<Integer, Integer> expected)
31+
{
32+
// when
33+
Pair<Integer, Integer> result = endIndex == null
34+
? testObject.getCode(startIndex)
35+
: testObject.getCode(startIndex, endIndex);
36+
37+
// then
38+
Assertions.assertThat(result).isEqualTo(expected);
39+
}
40+
41+
@Test
42+
public void getCode_WhenStartIndexEqualToEndIndex_ThenIllegalArgumentException()
43+
{
44+
Assertions.assertThatThrownBy(() -> testObject.getCode(4, 4))
45+
.isInstanceOf(IllegalArgumentException.class);
46+
}
47+
48+
@Test
49+
public void getCode_WhenStartIndexGreaterThanEndIndex_ThenIllegalArgumentException()
50+
{
51+
Assertions.assertThatThrownBy(() -> testObject.getCode(6, 2))
52+
.isInstanceOf(IllegalArgumentException.class);
53+
}
54+
55+
@Test
56+
public void getCode_WhenInvalidStartIndex_ThenIndexOutOfRangeException()
57+
{
58+
Assertions.assertThatThrownBy(() -> testObject.getCode(-1))
59+
.isInstanceOf(IndexOutOfBoundsException.class);
60+
}
61+
62+
@Test
63+
public void getCode_WhenInvalidEndIndex_ThenIndexOutOfRangeException()
64+
{
65+
Assertions.assertThatThrownBy(() -> testObject.getCode(5, 15))
66+
.isInstanceOf(IndexOutOfBoundsException.class);
67+
}
68+
69+
private static Stream<Arguments> paramsFor_GetCode_WhenRange()
70+
{
71+
Integer[][] indices = {
72+
{ 0, 1 }, { 0, 3 }, { 0, null }, { 1, 2 }, { 3, 4 }, { 3, 7 }, { 4, 6 },
73+
{ 7, null }, { 8, 9 }, { 8, 10 }
74+
};
75+
List<Pair<Integer, Integer>> expectedResults =
76+
Stream.of(Pair.of(2, 0), Pair.of(7, 6), Pair.of(20, 21), Pair.of(1, 0),
77+
Pair.of(4, 0), Pair.of(16, 0), Pair.of(6, 0), Pair.of(12, 0), Pair.of(3, 0),
78+
Pair.of(9, 0)).toList();
79+
80+
return IntStream.range(0, indices.length)
81+
.mapToObj(i -> Arguments.of(indices[i][0], indices[i][1],
82+
expectedResults.get(i)));
83+
}
84+
}

0 commit comments

Comments
 (0)