blob: b8a15744e2042f1633670ae93d9d372c361b8a0a [file] [log] [blame]
package org.unicode.cldr.test;
import com.google.common.collect.Multiset;
import com.google.common.collect.TreeMultiset;
import com.ibm.icu.util.Output;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.TreeSet;
import java.util.stream.Collectors;
import org.unicode.cldr.util.*;
public class LogicalGroupChecker {
private static final int LIMIT_DISTANCE = 5;
private final CheckLogicalGroupings checkLogicalGroupings;
private final String path;
private final String value;
private final Output<LogicalGrouping.PathType> pathType;
private final CLDRFile cldrFile;
private final List<CheckCLDR.CheckStatus> result;
private final Set<String> paths;
private final boolean phaseCausesError;
private final CoverageLevel2 coverageLevel;
private boolean havePath = false;
private String firstPath = null;
private Set<String> missingPaths = null;
private CLDRFile.DraftStatus myStatus;
public LogicalGroupChecker(
CheckLogicalGroupings checkLogicalGroupings,
String path,
String value,
List<CheckCLDR.CheckStatus> result
) {
this.checkLogicalGroupings = checkLogicalGroupings;
this.path = path;
this.value = value;
this.result = result;
pathType = new Output<>();
cldrFile = checkLogicalGroupings.getCldrFileToCheck();
paths = LogicalGrouping.getPaths(cldrFile, path, pathType);
coverageLevel = CoverageLevel2.getInstance(SupplementalDataInfo.getInstance(), cldrFile.getLocaleID());
phaseCausesError = CheckLogicalGroupings.PHASES_CAUSE_ERROR.contains(checkLogicalGroupings.getPhase());
}
public void run() {
if (isTrivial()) {
return; // skip if not part of a logical grouping
}
checkEditDistances();
removeOptionalPaths();
if (isTrivial()) {
return; // skip if not part of a logical grouping
}
if (findMissingPaths()) {
return; // skip other errors once we find missing paths
}
if (avoidWork()) {
return; // bail if we are ok
}
loop();
}
private boolean isTrivial() {
// skip if not part of a logical grouping
return (paths == null || paths.size() < 2);
}
private void checkEditDistances() {
switch (pathType.value) {
case COUNT_CASE:
case COUNT:
case COUNT_CASE_GENDER:
// only check the first path
TreeSet<String> sorted = new TreeSet<>(paths);
if (path.equals(sorted.iterator().next())) {
reallyCheckEditDistances(sorted);
}
break;
default:
break;
}
}
private void reallyCheckEditDistances(TreeSet<String> sorted) {
Multiset<String> values = TreeMultiset.create();
int maxDistance = getMaxDistance(sorted, values);
if (maxDistance >= LIMIT_DISTANCE) {
maxDistance = getMaxDistance(paths, values);
result.add(
new CheckCLDR.CheckStatus()
.setCause(checkLogicalGroupings)
.setMainType(CheckCLDR.CheckStatus.warningType)
.setSubtype(CheckCLDR.CheckStatus.Subtype.largerDifferences) // typically warningType or errorType
.setMessage(
"{0} different characters within {1}; {2}",
maxDistance,
CheckLogicalGroupings.showInvisibles(values),
value
)
);
}
}
private int getMaxDistance(Set<String> paths, Multiset<String> values) {
values.clear();
Set<CheckLogicalGroupings.Fingerprint> fingerprints = new HashSet<>();
for (String path1 : paths) {
final String pathValue = CheckLogicalGroupings.cleanSpaces(
path.contentEquals(path1) ? value : cldrFile.getWinningValue(path1)
);
values.add(pathValue);
fingerprints.add(CheckLogicalGroupings.Fingerprint.make(pathValue));
}
return CheckLogicalGroupings.Fingerprint.maxDistanceBetween(fingerprints);
}
private void removeOptionalPaths() {
LogicalGrouping.removeOptionalPaths(paths, cldrFile);
}
private boolean findMissingPaths() {
missingPaths = getMissingPaths();
if (havePath && !missingPaths.isEmpty()) {
if (path.equals(firstPath)) {
handleMissingPaths();
return true;
}
}
return false;
}
private void handleMissingPaths() {
Set<String> missingCodes = missingPaths
.stream()
.map(x -> checkLogicalGroupings.getPathReferenceForMessage(x, true))
.collect(Collectors.toSet());
Level cLevel = coverageLevel.getLevel(path);
CheckCLDR.CheckStatus.Type showError = phaseCausesError
? CheckCLDR.CheckStatus.errorType
: CheckCLDR.CheckStatus.warningType;
result.add(
new CheckCLDR.CheckStatus()
.setCause(checkLogicalGroupings)
.setMainType(showError)
.setSubtype(CheckCLDR.CheckStatus.Subtype.incompleteLogicalGroup)
.setMessage(
"Incomplete logical group - missing values for: {0}; level={1}",
missingCodes.toString(),
cLevel
)
);
}
private Set<String> getMissingPaths() {
Set<String> missingPaths = new HashSet<>();
for (String apath : paths) {
if (isHereOrNonRoot(apath)) { // ok
havePath = true;
} else {
if (missingPaths.isEmpty()) {
firstPath = apath; // pick the first one in sorted order (LogicalGrouping.getPaths is sorted)
}
missingPaths.add(apath);
}
}
return missingPaths;
}
/**
* We are not as strict with sublocales (where the parent is neither root nor code_fallback).
*
* @param path the path
* @return true or false
*/
private boolean isHereOrNonRoot(String path) {
if (cldrFile.isHere(path)) {
return true;
}
if (!cldrFile.getLocaleID().contains("_")) { // quick check for top level
return false;
}
final CLDRFile resolvedCldrFile = checkLogicalGroupings.getResolvedCldrFileToCheck();
if (resolvedCldrFile.getStringValue(path) == null) {
return false;
}
// the above items are just for fast checking.
// check the origin of the value, and make sure it is not ≥ root
String source = resolvedCldrFile.getSourceLocaleID(path, null);
return !source.equals(XMLSource.ROOT_ID) && !source.equals(XMLSource.CODE_FALLBACK_ID);
}
/**
* Return true if we're finished
*
* @return true or false
*/
private boolean avoidWork() {
// only check for draft status if this path fails;
// avoids work for ones we are not going to alert on anyway.
// If the draft status of something in the set is lower, then an implementation that filters out that draft status
// will get the wrong value.
final String fPath = cldrFile.getFullXPath(path);
final XPathParts parts = XPathParts.getFrozenInstance(fPath);
myStatus = CLDRFile.DraftStatus.forString(parts.findFirstAttributeValue("draft"));
return myStatus.compareTo(CheckLogicalGroupings.MIMIMUM_DRAFT_STATUS) >= 0;
}
private void loop() {
// If some other path in the LG has a higher draft status, then cause error on this path.
// NOTE: changed to show in Vetting, not just Resolution
for (String apath : paths) {
if (apath.equals(path) || missingPaths.contains(apath)) { // skip this path, skip others unless present
continue;
}
final String fPath = cldrFile.getFullXPath(apath);
if (fPath == null) {
continue;
}
final XPathParts parts = XPathParts.getFrozenInstance(fPath);
final CLDRFile.DraftStatus draftStatus = CLDRFile.DraftStatus.forString(
parts.findFirstAttributeValue("draft")
);
// Cause error for anything above myStatus
if (draftStatus.compareTo(myStatus) > 0) {
addOneHigherStatus(apath, myStatus, draftStatus);
break; // no need to continue
}
}
}
private void addOneHigherStatus(String apath, CLDRFile.DraftStatus myStatus, CLDRFile.DraftStatus draftStatus) {
CheckCLDR.CheckStatus.Type showError = phaseCausesError
? CheckCLDR.CheckStatus.errorType
: CheckCLDR.CheckStatus.warningType;
result.add(
new CheckCLDR.CheckStatus()
.setCause(checkLogicalGroupings)
.setMainType(showError)
.setSubtype(CheckCLDR.CheckStatus.Subtype.inconsistentDraftStatus) // typically warningType or errorType
.setMessage(
"This item has draft status={0}, which is lower than the status={1} (for {2}).",
myStatus.name(),
draftStatus.name(),
checkLogicalGroupings.getPathReferenceForMessage(apath, true)
)
);
}
}