mainframe-migration-toolkit 0.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- mainframe_migration_toolkit-0.2.0.dist-info/METADATA +16 -0
- mainframe_migration_toolkit-0.2.0.dist-info/RECORD +52 -0
- mainframe_migration_toolkit-0.2.0.dist-info/WHEEL +4 -0
- mainframe_migration_toolkit-0.2.0.dist-info/entry_points.txt +3 -0
- mainframe_toolkit/__init__.py +92 -0
- mainframe_toolkit/__main__.py +5 -0
- mainframe_toolkit/_workspace/.claude/skills/analyze-mainframe-similarity/SKILL.md +30 -0
- mainframe_toolkit/_workspace/.claude/skills/migrate-mainframe-job/SKILL.md +63 -0
- mainframe_toolkit/_workspace/.claude/skills/validate-golden-dataset/SKILL.md +12 -0
- mainframe_toolkit/_workspace/AGENTS.md +10 -0
- mainframe_toolkit/_workspace/CLAUDE.md +2 -0
- mainframe_toolkit/_workspace/validator-java/.mvn/wrapper/maven-wrapper.properties +3 -0
- mainframe_toolkit/_workspace/validator-java/mvnw +295 -0
- mainframe_toolkit/_workspace/validator-java/mvnw.cmd +189 -0
- mainframe_toolkit/_workspace/validator-java/pom.xml +116 -0
- mainframe_toolkit/_workspace/validator-java/src/main/java/io/mainframe/migration/validator/AvroValueFormatter.java +152 -0
- mainframe_toolkit/_workspace/validator-java/src/main/java/io/mainframe/migration/validator/CsvTabularReader.java +111 -0
- mainframe_toolkit/_workspace/validator-java/src/main/java/io/mainframe/migration/validator/DataFormat.java +62 -0
- mainframe_toolkit/_workspace/validator-java/src/main/java/io/mainframe/migration/validator/Difference.java +34 -0
- mainframe_toolkit/_workspace/validator-java/src/main/java/io/mainframe/migration/validator/InputFileSet.java +45 -0
- mainframe_toolkit/_workspace/validator-java/src/main/java/io/mainframe/migration/validator/InputOptions.java +23 -0
- mainframe_toolkit/_workspace/validator-java/src/main/java/io/mainframe/migration/validator/JsonReportWriter.java +40 -0
- mainframe_toolkit/_workspace/validator-java/src/main/java/io/mainframe/migration/validator/MultiFileTabularReader.java +80 -0
- mainframe_toolkit/_workspace/validator-java/src/main/java/io/mainframe/migration/validator/Normalization.java +16 -0
- mainframe_toolkit/_workspace/validator-java/src/main/java/io/mainframe/migration/validator/ParquetTabularReader.java +61 -0
- mainframe_toolkit/_workspace/validator-java/src/main/java/io/mainframe/migration/validator/TabularReader.java +13 -0
- mainframe_toolkit/_workspace/validator-java/src/main/java/io/mainframe/migration/validator/TabularReaderFactory.java +29 -0
- mainframe_toolkit/_workspace/validator-java/src/main/java/io/mainframe/migration/validator/ValidationOptions.java +40 -0
- mainframe_toolkit/_workspace/validator-java/src/main/java/io/mainframe/migration/validator/ValidationReport.java +44 -0
- mainframe_toolkit/_workspace/validator-java/src/main/java/io/mainframe/migration/validator/ValidationService.java +395 -0
- mainframe_toolkit/_workspace/validator-java/src/main/java/io/mainframe/migration/validator/ValidatorCli.java +190 -0
- mainframe_toolkit/_workspace/validator-java/src/main/java/io/mainframe/migration/validator/ValueNormalizer.java +27 -0
- mainframe_toolkit/_workspace/validator-java/src/test/java/io/mainframe/migration/validator/DirectoryValidationTest.java +70 -0
- mainframe_toolkit/_workspace/validator-java/src/test/java/io/mainframe/migration/validator/JsonReportWriterTest.java +43 -0
- mainframe_toolkit/_workspace/validator-java/src/test/java/io/mainframe/migration/validator/KeyedValidationTest.java +89 -0
- mainframe_toolkit/_workspace/validator-java/src/test/java/io/mainframe/migration/validator/ParquetValidationTest.java +146 -0
- mainframe_toolkit/_workspace/validator-java/src/test/java/io/mainframe/migration/validator/ValidationServiceTest.java +137 -0
- mainframe_toolkit/_workspace/validator-java/src/test/java/io/mainframe/migration/validator/ValidatorCliTest.java +102 -0
- mainframe_toolkit/cli.py +361 -0
- mainframe_toolkit/cobol.py +106 -0
- mainframe_toolkit/copybook.py +558 -0
- mainframe_toolkit/errors.py +15 -0
- mainframe_toolkit/external.py +48 -0
- mainframe_toolkit/io.py +202 -0
- mainframe_toolkit/jcl.py +126 -0
- mainframe_toolkit/pipeline.py +322 -0
- mainframe_toolkit/sequential.py +259 -0
- mainframe_toolkit/similarity.py +1171 -0
- mainframe_toolkit/sorting.py +60 -0
- mainframe_toolkit/specs.py +312 -0
- mainframe_toolkit/synthetic.py +108 -0
- mainframe_toolkit/workspace.py +133 -0
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
package io.mainframe.migration.validator;
|
|
2
|
+
|
|
3
|
+
import java.io.IOException;
|
|
4
|
+
import java.nio.file.Files;
|
|
5
|
+
import java.nio.file.Path;
|
|
6
|
+
import java.util.Comparator;
|
|
7
|
+
import java.util.List;
|
|
8
|
+
import java.util.Locale;
|
|
9
|
+
import java.util.stream.Stream;
|
|
10
|
+
|
|
11
|
+
final class InputFileSet {
|
|
12
|
+
private InputFileSet() {
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
static List<Path> files(InputOptions options) throws IOException {
|
|
16
|
+
if (!Files.isDirectory(options.path())) {
|
|
17
|
+
return List.of(options.path());
|
|
18
|
+
}
|
|
19
|
+
DataFormat format = options.resolvedFormat();
|
|
20
|
+
try (Stream<Path> entries = Files.list(options.path())) {
|
|
21
|
+
List<Path> parts = entries
|
|
22
|
+
.filter(Files::isRegularFile)
|
|
23
|
+
.filter(path -> isPart(path, format))
|
|
24
|
+
.sorted(Comparator.comparing(path -> path.getFileName().toString()))
|
|
25
|
+
.toList();
|
|
26
|
+
if (parts.isEmpty()) {
|
|
27
|
+
throw new IllegalArgumentException(
|
|
28
|
+
"Nenhuma parte " + format.name() + " encontrada em: " + options.path());
|
|
29
|
+
}
|
|
30
|
+
return parts;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
private static boolean isPart(Path path, DataFormat format) {
|
|
35
|
+
String name = path.getFileName().toString().toLowerCase(Locale.ROOT);
|
|
36
|
+
if (!name.startsWith("part-")) {
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
return switch (format) {
|
|
40
|
+
case CSV -> name.endsWith(".csv");
|
|
41
|
+
case PARQUET -> name.endsWith(".parquet") || name.endsWith(".parq");
|
|
42
|
+
case AUTO -> false;
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
package io.mainframe.migration.validator;
|
|
2
|
+
|
|
3
|
+
import java.nio.charset.Charset;
|
|
4
|
+
import java.nio.file.Path;
|
|
5
|
+
import java.util.Objects;
|
|
6
|
+
|
|
7
|
+
public record InputOptions(
|
|
8
|
+
Path path,
|
|
9
|
+
DataFormat format,
|
|
10
|
+
char delimiter,
|
|
11
|
+
boolean header,
|
|
12
|
+
Charset charset) {
|
|
13
|
+
|
|
14
|
+
public InputOptions {
|
|
15
|
+
Objects.requireNonNull(path, "path");
|
|
16
|
+
Objects.requireNonNull(format, "format");
|
|
17
|
+
Objects.requireNonNull(charset, "charset");
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
public DataFormat resolvedFormat() {
|
|
21
|
+
return format.resolve(path);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
package io.mainframe.migration.validator;
|
|
2
|
+
|
|
3
|
+
import com.fasterxml.jackson.core.util.DefaultIndenter;
|
|
4
|
+
import com.fasterxml.jackson.core.util.DefaultPrettyPrinter;
|
|
5
|
+
import com.fasterxml.jackson.databind.ObjectMapper;
|
|
6
|
+
import com.fasterxml.jackson.databind.ObjectWriter;
|
|
7
|
+
|
|
8
|
+
import java.io.IOException;
|
|
9
|
+
import java.io.Writer;
|
|
10
|
+
import java.nio.charset.StandardCharsets;
|
|
11
|
+
import java.nio.file.Files;
|
|
12
|
+
import java.nio.file.Path;
|
|
13
|
+
|
|
14
|
+
public final class JsonReportWriter {
|
|
15
|
+
private static final ObjectWriter JSON_WRITER;
|
|
16
|
+
|
|
17
|
+
static {
|
|
18
|
+
DefaultPrettyPrinter printer = new DefaultPrettyPrinter();
|
|
19
|
+
DefaultIndenter indenter = new DefaultIndenter(" ", "\n");
|
|
20
|
+
printer.indentObjectsWith(indenter);
|
|
21
|
+
printer.indentArraysWith(indenter);
|
|
22
|
+
JSON_WRITER = new ObjectMapper().writer(printer);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
public void write(ValidationReport report, Writer output) throws IOException {
|
|
26
|
+
output.write(JSON_WRITER.writeValueAsString(report));
|
|
27
|
+
output.write('\n');
|
|
28
|
+
output.flush();
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
public void write(ValidationReport report, Path output) throws IOException {
|
|
32
|
+
Path parent = output.toAbsolutePath().getParent();
|
|
33
|
+
if (parent != null) {
|
|
34
|
+
Files.createDirectories(parent);
|
|
35
|
+
}
|
|
36
|
+
try (Writer writer = Files.newBufferedWriter(output, StandardCharsets.UTF_8)) {
|
|
37
|
+
write(report, writer);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
package io.mainframe.migration.validator;
|
|
2
|
+
|
|
3
|
+
import java.io.IOException;
|
|
4
|
+
import java.nio.file.Path;
|
|
5
|
+
import java.util.List;
|
|
6
|
+
|
|
7
|
+
final class MultiFileTabularReader implements TabularReader {
|
|
8
|
+
private final InputOptions options;
|
|
9
|
+
private final List<Path> files;
|
|
10
|
+
private int nextFile;
|
|
11
|
+
private TabularReader current;
|
|
12
|
+
private List<String> columns = List.of();
|
|
13
|
+
private boolean namedColumns;
|
|
14
|
+
|
|
15
|
+
MultiFileTabularReader(InputOptions options, List<Path> files) throws IOException {
|
|
16
|
+
this.options = options;
|
|
17
|
+
this.files = files;
|
|
18
|
+
openNext();
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
@Override
|
|
22
|
+
public List<String> columns() {
|
|
23
|
+
return columns;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
@Override
|
|
27
|
+
public boolean hasNamedColumns() {
|
|
28
|
+
return namedColumns;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
@Override
|
|
32
|
+
public List<String> readRow() throws IOException {
|
|
33
|
+
while (current != null) {
|
|
34
|
+
List<String> row = current.readRow();
|
|
35
|
+
if (row != null) {
|
|
36
|
+
return row;
|
|
37
|
+
}
|
|
38
|
+
current.close();
|
|
39
|
+
current = null;
|
|
40
|
+
openNext();
|
|
41
|
+
}
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
private void openNext() throws IOException {
|
|
46
|
+
while (nextFile < files.size()) {
|
|
47
|
+
TabularReader candidate = TabularReaderFactory.openFile(options, files.get(nextFile++));
|
|
48
|
+
if (columns.isEmpty()) {
|
|
49
|
+
if (candidate.columns().isEmpty()) {
|
|
50
|
+
candidate.close();
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
columns = candidate.columns();
|
|
54
|
+
namedColumns = candidate.hasNamedColumns();
|
|
55
|
+
current = candidate;
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
if (namedColumns && candidate.hasNamedColumns()
|
|
59
|
+
&& !candidate.columns().isEmpty()
|
|
60
|
+
&& !columns.equals(candidate.columns())) {
|
|
61
|
+
candidate.close();
|
|
62
|
+
throw new IllegalArgumentException("Partes com colunas diferentes em: " + options.path());
|
|
63
|
+
}
|
|
64
|
+
if (candidate.columns().isEmpty()) {
|
|
65
|
+
candidate.close();
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
current = candidate;
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
@Override
|
|
74
|
+
public void close() throws IOException {
|
|
75
|
+
if (current != null) {
|
|
76
|
+
current.close();
|
|
77
|
+
current = null;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
package io.mainframe.migration.validator;
|
|
2
|
+
|
|
3
|
+
import java.util.Locale;
|
|
4
|
+
|
|
5
|
+
public enum Normalization {
|
|
6
|
+
TRIM,
|
|
7
|
+
DECIMAL;
|
|
8
|
+
|
|
9
|
+
public static Normalization parse(String value) {
|
|
10
|
+
try {
|
|
11
|
+
return valueOf(value.trim().toUpperCase(Locale.ROOT));
|
|
12
|
+
} catch (RuntimeException exception) {
|
|
13
|
+
throw new IllegalArgumentException("Normalização inválida: " + value + ". Use trim ou decimal.");
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
package io.mainframe.migration.validator;
|
|
2
|
+
|
|
3
|
+
import org.apache.avro.Schema;
|
|
4
|
+
import org.apache.avro.generic.GenericRecord;
|
|
5
|
+
import org.apache.parquet.avro.AvroParquetReader;
|
|
6
|
+
import org.apache.parquet.hadoop.ParquetFileReader;
|
|
7
|
+
import org.apache.parquet.hadoop.ParquetReader;
|
|
8
|
+
import org.apache.parquet.io.LocalInputFile;
|
|
9
|
+
|
|
10
|
+
import java.io.IOException;
|
|
11
|
+
import java.nio.file.Path;
|
|
12
|
+
import java.util.ArrayList;
|
|
13
|
+
import java.util.List;
|
|
14
|
+
|
|
15
|
+
final class ParquetTabularReader implements TabularReader {
|
|
16
|
+
private final ParquetReader<GenericRecord> reader;
|
|
17
|
+
private final List<String> columns;
|
|
18
|
+
|
|
19
|
+
ParquetTabularReader(Path path) throws IOException {
|
|
20
|
+
LocalInputFile input = new LocalInputFile(path);
|
|
21
|
+
try (ParquetFileReader metadataReader = ParquetFileReader.open(input)) {
|
|
22
|
+
columns = metadataReader.getFooter().getFileMetaData().getSchema().getFields().stream()
|
|
23
|
+
.map(type -> type.getName())
|
|
24
|
+
.toList();
|
|
25
|
+
}
|
|
26
|
+
reader = AvroParquetReader.genericRecordReader(new LocalInputFile(path));
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
@Override
|
|
30
|
+
public List<String> columns() {
|
|
31
|
+
return columns;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
@Override
|
|
35
|
+
public boolean hasNamedColumns() {
|
|
36
|
+
return true;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
@Override
|
|
40
|
+
public List<String> readRow() throws IOException {
|
|
41
|
+
GenericRecord record = reader.read();
|
|
42
|
+
if (record == null) {
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
List<String> values = new ArrayList<>(columns.size());
|
|
46
|
+
for (int index = 0; index < columns.size(); index++) {
|
|
47
|
+
Schema.Field field = record.getSchema().getField(columns.get(index));
|
|
48
|
+
if (field == null && index < record.getSchema().getFields().size()) {
|
|
49
|
+
field = record.getSchema().getFields().get(index);
|
|
50
|
+
}
|
|
51
|
+
Object value = field == null ? null : record.get(field.pos());
|
|
52
|
+
values.add(AvroValueFormatter.format(value, field == null ? null : field.schema()));
|
|
53
|
+
}
|
|
54
|
+
return values;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
@Override
|
|
58
|
+
public void close() throws IOException {
|
|
59
|
+
reader.close();
|
|
60
|
+
}
|
|
61
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
package io.mainframe.migration.validator;
|
|
2
|
+
|
|
3
|
+
import java.io.Closeable;
|
|
4
|
+
import java.io.IOException;
|
|
5
|
+
import java.util.List;
|
|
6
|
+
|
|
7
|
+
interface TabularReader extends Closeable {
|
|
8
|
+
List<String> columns();
|
|
9
|
+
|
|
10
|
+
boolean hasNamedColumns();
|
|
11
|
+
|
|
12
|
+
List<String> readRow() throws IOException;
|
|
13
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
package io.mainframe.migration.validator;
|
|
2
|
+
|
|
3
|
+
import java.io.IOException;
|
|
4
|
+
import java.nio.file.Files;
|
|
5
|
+
import java.nio.file.Path;
|
|
6
|
+
import java.util.List;
|
|
7
|
+
|
|
8
|
+
final class TabularReaderFactory {
|
|
9
|
+
private TabularReaderFactory() {
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
static TabularReader open(InputOptions options) throws IOException {
|
|
13
|
+
List<Path> files = InputFileSet.files(options);
|
|
14
|
+
if (Files.isDirectory(options.path())) {
|
|
15
|
+
return new MultiFileTabularReader(options, files);
|
|
16
|
+
}
|
|
17
|
+
return openFile(options, files.get(0));
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
static TabularReader openFile(InputOptions options, Path file) throws IOException {
|
|
21
|
+
InputOptions fileOptions = new InputOptions(
|
|
22
|
+
file, options.resolvedFormat(), options.delimiter(), options.header(), options.charset());
|
|
23
|
+
return switch (fileOptions.resolvedFormat()) {
|
|
24
|
+
case CSV -> new CsvTabularReader(fileOptions);
|
|
25
|
+
case PARQUET -> new ParquetTabularReader(file);
|
|
26
|
+
case AUTO -> throw new IllegalStateException("Formato AUTO não resolvido");
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
package io.mainframe.migration.validator;
|
|
2
|
+
|
|
3
|
+
import java.util.EnumSet;
|
|
4
|
+
import java.util.List;
|
|
5
|
+
import java.util.Objects;
|
|
6
|
+
import java.util.Set;
|
|
7
|
+
|
|
8
|
+
public record ValidationOptions(
|
|
9
|
+
InputOptions expected,
|
|
10
|
+
InputOptions actual,
|
|
11
|
+
Set<Normalization> normalizations,
|
|
12
|
+
int maxDifferences,
|
|
13
|
+
List<String> keys) {
|
|
14
|
+
|
|
15
|
+
public ValidationOptions {
|
|
16
|
+
Objects.requireNonNull(expected, "expected");
|
|
17
|
+
Objects.requireNonNull(actual, "actual");
|
|
18
|
+
normalizations = normalizations == null || normalizations.isEmpty()
|
|
19
|
+
? Set.of()
|
|
20
|
+
: Set.copyOf(EnumSet.copyOf(normalizations));
|
|
21
|
+
if (maxDifferences < 1) {
|
|
22
|
+
throw new IllegalArgumentException("maxDifferences deve ser maior que zero");
|
|
23
|
+
}
|
|
24
|
+
keys = keys == null ? List.of() : keys.stream().map(String::strip).toList();
|
|
25
|
+
if (keys.stream().anyMatch(String::isEmpty)) {
|
|
26
|
+
throw new IllegalArgumentException("Nome de chave vazio");
|
|
27
|
+
}
|
|
28
|
+
if (keys.stream().distinct().count() != keys.size()) {
|
|
29
|
+
throw new IllegalArgumentException("Colunas de chave duplicadas: " + keys);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
public ValidationOptions(
|
|
34
|
+
InputOptions expected,
|
|
35
|
+
InputOptions actual,
|
|
36
|
+
Set<Normalization> normalizations,
|
|
37
|
+
int maxDifferences) {
|
|
38
|
+
this(expected, actual, normalizations, maxDifferences, List.of());
|
|
39
|
+
}
|
|
40
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
package io.mainframe.migration.validator;
|
|
2
|
+
|
|
3
|
+
import com.fasterxml.jackson.annotation.JsonPropertyOrder;
|
|
4
|
+
|
|
5
|
+
import java.util.List;
|
|
6
|
+
|
|
7
|
+
@JsonPropertyOrder({
|
|
8
|
+
"status",
|
|
9
|
+
"expected",
|
|
10
|
+
"actual",
|
|
11
|
+
"expectedFormat",
|
|
12
|
+
"actualFormat",
|
|
13
|
+
"keys",
|
|
14
|
+
"normalizations",
|
|
15
|
+
"expectedRows",
|
|
16
|
+
"actualRows",
|
|
17
|
+
"expectedColumns",
|
|
18
|
+
"actualColumns",
|
|
19
|
+
"differenceCount",
|
|
20
|
+
"reportedDifferenceCount",
|
|
21
|
+
"truncated",
|
|
22
|
+
"differences"
|
|
23
|
+
})
|
|
24
|
+
public record ValidationReport(
|
|
25
|
+
String status,
|
|
26
|
+
String expected,
|
|
27
|
+
String actual,
|
|
28
|
+
String expectedFormat,
|
|
29
|
+
String actualFormat,
|
|
30
|
+
List<String> keys,
|
|
31
|
+
List<String> normalizations,
|
|
32
|
+
long expectedRows,
|
|
33
|
+
long actualRows,
|
|
34
|
+
int expectedColumns,
|
|
35
|
+
int actualColumns,
|
|
36
|
+
long differenceCount,
|
|
37
|
+
int reportedDifferenceCount,
|
|
38
|
+
boolean truncated,
|
|
39
|
+
List<Difference> differences) {
|
|
40
|
+
|
|
41
|
+
public boolean matches() {
|
|
42
|
+
return differenceCount == 0;
|
|
43
|
+
}
|
|
44
|
+
}
|