in languagetool-core/src/main/java/org/languagetool/rules/AbstractUnitConversionRule.java [422:557]
private void tryConversion(AnalyzedSentence sentence, List<RuleMatch> matches, Pattern unitPattern, Double customValue,
Unit customUnit, Matcher unitMatcher, List<Map.Entry<Integer, Integer>> ignoreRanges) {
Map.Entry<Integer, Integer> range = new AbstractMap.SimpleImmutableEntry<>(
unitMatcher.start(), unitMatcher.end());
ignoreRanges.add(range);
// search for an existing conversion, e.g. "5 miles (8km)"
String convertedInText = null;
int convertedOffset = unitMatcher.end();
Matcher convertedMatcher = null;
for (Pattern convertedPattern : convertedPatterns) {
convertedMatcher = convertedPattern.matcher(sentence.getText().substring(convertedOffset));
if (convertedMatcher.find() && convertedMatcher.start() == 0) {
convertedInText = convertedMatcher.group(0);
break;
}
}
// customValue/unit are used with patterns in specialPatterns, where unit and value are already extracted
Unit unit = unitPatterns.getOrDefault(unitPattern, customUnit);
double value;
if (customValue == null) {
try {
String valueAsString = unitMatcher.group(1);
// remove hyphen at start if it belongs to a range (e.g 1-5 miles)
// see https://github.com/languagetool-org/languagetool/issues/2170
// TODO convert whole range, not only end
if (detectNumberRange(sentence, unitMatcher)) {
valueAsString = valueAsString.substring(1);
}
value = getNumberFormat().parse(valueAsString).doubleValue();
} catch (ParseException e) {
return;
}
} else {
value = customValue;
}
List<String> converted = formatMeasurement(value, unit);
if (converted == null && convertedInText == null) {
// no conversion necessary, e.g. already metric
} else if (convertedInText == null) { // no conversion found -> suggest one
RuleMatch match = new RuleMatch(this, sentence, unitMatcher.start(), unitMatcher.end(),
getMessage(Message.SUGGESTION), getShortMessage(Message.SUGGESTION));
List<String> suggestions = converted.stream()
.map(formatted -> getSuggestion(unitMatcher.group(0), formatted))
.collect(Collectors.toList());
match.setSuggestedReplacements(suggestions);
match.setUrl(buildURLForExplanation(unitMatcher.group(0)));
matches.add(match);
} else { // check given conversion for accuracy
Map.Entry<Integer, Integer> convertedRange = new AbstractMap.SimpleImmutableEntry<>(
convertedMatcher.start(0) + convertedOffset, convertedMatcher.end(0) + convertedOffset);
ignoreRanges.add(convertedRange);
// already using one of our conversions?
String finalConvertedInText = convertedInText.trim();
String convertedTrimmed = finalConvertedInText.substring(1, finalConvertedInText.length()-1);
if (converted != null && converted.stream().anyMatch(s -> s.equals(convertedTrimmed))) {
return;
}
Optional<Pattern> convertedUnitPattern = unitPatterns.keySet().stream()
.filter(pattern -> pattern.matcher(finalConvertedInText).find())
.findFirst();
if (convertedUnitPattern.isPresent()) { // known unit used for conversion
Unit convertedUnit = unitPatterns.get(convertedUnitPattern.get());
// If the unit before and after conversion is the same, e.g. "22.3 cm (20.4 cm)", assume users either:
// 1. know what they're doing; or
// 2. there's a more complex expression at play here, which we can't parse
if (unit.equals(convertedUnit)) {
return;
}
Double convertedValueInText;
try {
convertedValueInText = getNumberFormat().parse(convertedMatcher.group(1)).doubleValue();
if (convertedMatcher.group().trim().matches("\\(\\d+ (feet|ft) \\d+ inch\\)")) {
// e.g. "(2 ft 6 inch)" would be interpreted as just "2 ft", given a wrong suggestion
return;
}
} catch (ParseException e) {
return;
}
if (converted == null) { // already metric, check conversion in convertedUnit / convertedValueInText (order may be reversed)
List<String> reverseConverted = null;
try {
double unitConverted = unit.getConverterTo(convertedUnit).convert(value);
double diff = Math.abs(unitConverted - convertedValueInText);
if (diff > DELTA) {
RuleMatch match = new RuleMatch(this, sentence,
convertedMatcher.start(1) + convertedOffset, convertedMatcher.end(1) + convertedOffset,
getMessage(Message.CHECK), getShortMessage(Message.CHECK));
match.setUrl(buildURLForExplanation(convertedTrimmed));
List<Map.Entry<Unit, Double>> numbers = new ArrayList<>();
numbers.add(new AbstractMap.SimpleImmutableEntry<>(convertedUnit, unitConverted));
reverseConverted = getFormattedConversions(numbers);
if (reverseConverted.stream().anyMatch(s -> s.equals(convertedTrimmed))) {
return;
}
match.setSuggestedReplacements(reverseConverted);
matches.add(match);
}
} catch (UnconvertibleException e) {
RuleMatch match = new RuleMatch(this, sentence, unitMatcher.start(), convertedMatcher.end() + convertedOffset,
getMessage(Message.UNIT_MISMATCH), getShortMessage(Message.UNIT_MISMATCH));
if (reverseConverted != null) {
match.setSuggestedReplacements(reverseConverted);
}
match.setUrl(buildURLForExplanation(convertedTrimmed));
matches.add(match);
}
} else { // found conversion to metric, check for accuracy
List<Map.Entry<Unit, Double>> metricEquivalents = getMetricEquivalent(value, unit);
if (metricEquivalents == null || metricEquivalents.isEmpty()) {
return;
}
Map.Entry<Unit, Double> metricEquivalent = metricEquivalents.get(0);
Unit metricUnit = metricEquivalent.getKey();
Double convertedValueComputed = metricEquivalent.getValue();
String original = unitMatcher.group(0);
List<String> corrected = converted.stream()
.map(suggestion -> getSuggestion(original, suggestion)).collect(Collectors.toList());
if (!(convertedUnit.equals(metricUnit) && Math.abs(convertedValueInText - convertedValueComputed) < DELTA)) {
RuleMatch match = new RuleMatch(this, sentence,
unitMatcher.start(), convertedMatcher.end(0) + convertedOffset,
getMessage(Message.CHECK), getShortMessage(Message.CHECK));
match.setSuggestedReplacements(corrected);
match.setUrl(buildURLForExplanation(unitMatcher.group(0)));
matches.add(match);
}
}
} else if (converted != null) { // unknown unit used for conversion
RuleMatch match = new RuleMatch(this, sentence,
convertedMatcher.start(1) + convertedOffset, convertedMatcher.end(2) + convertedOffset,
getMessage(Message.CHECK_UNKNOWN_UNIT), getShortMessage(Message.CHECK_UNKNOWN_UNIT));
match.setSuggestedReplacements(converted);
matches.add(match);
}
}
}