fix QuantityType dimensionless one and time formatting (#4169)

Signed-off-by: Mark Herwege <mark.herwege@telenet.be>
This commit is contained in:
Mark Herwege 2024-05-18 14:54:18 +02:00 committed by GitHub
parent 3b9a97101b
commit 6b5eed782c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 104 additions and 15 deletions

View File

@ -18,16 +18,22 @@ import java.math.BigDecimal;
import java.text.DecimalFormat;
import java.text.NumberFormat;
import java.text.ParsePosition;
import java.time.Instant;
import java.time.ZoneOffset;
import java.time.ZonedDateTime;
import java.time.Duration;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashMap;
import java.util.IllegalFormatConversionException;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.MissingFormatArgumentException;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Stream;
import javax.measure.Dimension;
import javax.measure.IncommensurableException;
import javax.measure.MetricPrefix;
import javax.measure.Quantity;
import javax.measure.Quantity.Scale;
import javax.measure.UnconvertibleException;
@ -40,7 +46,6 @@ import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.core.internal.library.unit.UnitInitializer;
import org.openhab.core.items.events.ItemStateEvent;
import org.openhab.core.library.unit.MetricPrefix;
import org.openhab.core.library.unit.Units;
import org.openhab.core.types.Command;
import org.openhab.core.types.PrimitiveType;
@ -69,6 +74,13 @@ public class QuantityType<T extends Quantity<T>> extends Number
private static final long serialVersionUID = 8828949721938234629L;
private static final BigDecimal BIG_DECIMAL_HUNDRED = BigDecimal.valueOf(100);
// Patterns to identify formatting strings to format Time, derived from Java String formatting for DateTime
private static final Pattern DAYS_PATTERN = Pattern.compile("%(?:1\\$)?[tT][de]");
private static final Pattern HOURS_PATTERN = Pattern.compile("%(?:1\\$)?[tT][HkIl]");
private static final Pattern MINUTES_PATTERN = Pattern.compile("%(?:1\\$)?[tT]M");
private static final Pattern SECONDS_PATTERN = Pattern.compile("%(?:1\\$)?[tT][Ss]");
private static final Pattern MILLIS_PATTERN = Pattern.compile("%(?:1\\$)?[tT][LQ]");
public static final QuantityType<Dimensionless> ZERO = new QuantityType<>(0, AbstractUnit.ONE);
public static final QuantityType<Dimensionless> ONE = new QuantityType<>(1, AbstractUnit.ONE);
@ -376,6 +388,14 @@ public class QuantityType<T extends Quantity<T>> extends Number
@Override
public String format(String pattern) {
if (pattern.contains("%s") || pattern.contains("%S")) {
try {
return String.format(pattern, quantity);
} catch (IllegalFormatConversionException ifce) {
// The conversion is not valid. Fall through trying other formatting options.
}
}
boolean unitPlaceholder = pattern.contains(UnitUtils.UNIT_PLACEHOLDER);
final String formatPattern;
@ -386,16 +406,83 @@ public class QuantityType<T extends Quantity<T>> extends Number
formatPattern = pattern;
}
// The dimension could be a time value thus we want to support patterns to format datetime
// The dimension could be a time value thus we want to support patterns to format.
// Wile time is representing a duration (Scale.RELATIVE), formatting patterns mimic String format patterns for
// DateTime to not break backward compatibility and to avoid introducing specific duration formatting.
if (quantity.getUnit().isCompatible(Units.SECOND) && !unitPlaceholder) {
QuantityType<T> millis = toUnit(MetricPrefix.MILLI(Units.SECOND));
if (millis != null) {
Duration duration = Duration.ofMillis(millis.longValue());
String timeFormatPattern = formatPattern;
timeFormatPattern = timeFormatPattern.replaceAll("%(?:1\\$)?[tT]R", "%tH:%tM");
timeFormatPattern = timeFormatPattern.replaceAll("%(?:1\\$)?[tT]T", "%tH:%tM:%tS");
enum Type {
DAYS,
HOURS,
MINUTES,
SECONDS,
MILLIS
}
Map<Integer, Type> patternIndex = new HashMap<>();
Matcher matcher = DAYS_PATTERN.matcher(timeFormatPattern);
while (matcher.find()) {
patternIndex.put(matcher.start(), Type.DAYS);
}
matcher = HOURS_PATTERN.matcher(timeFormatPattern);
while (matcher.find()) {
patternIndex.put(matcher.start(), Type.HOURS);
}
matcher = MINUTES_PATTERN.matcher(timeFormatPattern);
while (matcher.find()) {
patternIndex.put(matcher.start(), Type.MINUTES);
}
matcher = SECONDS_PATTERN.matcher(timeFormatPattern);
while (matcher.find()) {
patternIndex.put(matcher.start(), Type.SECONDS);
}
matcher = MILLIS_PATTERN.matcher(timeFormatPattern);
while (matcher.find()) {
patternIndex.put(matcher.start(), Type.MILLIS);
}
long dd = duration.toDays();
long hh = (patternIndex.values().contains(Type.DAYS) ? 0 : dd * 24) + duration.toHoursPart();
long mm = (patternIndex.values().contains(Type.HOURS) ? 0 : hh * 60) + duration.toMinutesPart();
long ss = (patternIndex.values().contains(Type.MINUTES) ? 0 : mm * 60) + duration.toSecondsPart();
long mmm = (patternIndex.values().contains(Type.SECONDS) ? 0 : ss * 1000) + duration.toMillisPart();
List<Long> formatArgs = new ArrayList<>();
patternIndex.entrySet().stream().sorted(Comparator.comparingInt(e -> e.getKey())).forEach(p -> {
switch (p.getValue()) {
case DAYS:
formatArgs.add(dd);
break;
case HOURS:
formatArgs.add(hh);
break;
case MINUTES:
formatArgs.add(mm);
break;
case SECONDS:
formatArgs.add(ss);
break;
case MILLIS:
formatArgs.add(mmm);
break;
}
});
timeFormatPattern = timeFormatPattern.replaceAll("%(?:1\\$)?[tT][eklsQ]", "%d");
timeFormatPattern = timeFormatPattern.replaceAll("%(?:1\\$)?[tT][dHIMS]", "%02d");
timeFormatPattern = timeFormatPattern.replaceAll("%(?:1\\$)?[tT]L", "%03d");
try {
return String.format(formatPattern,
ZonedDateTime.ofInstant(Instant.ofEpochMilli(millis.longValue()), ZoneOffset.UTC));
} catch (IllegalFormatConversionException ifce) {
// The conversion is not valid for the type ZonedDateTime. This happens, if the format is like
// "%.1f". Fall through to default behavior.
return String.format(timeFormatPattern, formatArgs.toArray());
} catch (IllegalFormatConversionException | MissingFormatArgumentException ifce) {
// The conversion is not valid. Fall through to default behavior.
}
}
}
@ -439,11 +526,7 @@ public class QuantityType<T extends Quantity<T>> extends Number
@Override
public String toFullString() {
if (AbstractUnit.ONE.equals(quantity.getUnit())) {
return quantity.getValue().toString();
} else {
return quantity.toString();
}
return quantity.toString();
}
@Override

View File

@ -250,11 +250,17 @@ public class QuantityTypeTest {
assertThat(millis.format("%.1f " + UnitUtils.UNIT_PLACEHOLDER), is("80000" + ds + "0 ms"));
assertThat(minutes.format("%.1f " + UnitUtils.UNIT_PLACEHOLDER), is("1" + ds + "3 min"));
assertThat(seconds.format("%s"), is("80 s"));
assertThat(millis.format("%s"), is("80000 ms"));
assertThat(seconds.format("%.1f"), is("80" + ds + "0"));
assertThat(minutes.format("%.1f"), is("1" + ds + "3"));
assertThat(seconds.format("%1$tH:%1$tM:%1$tS"), is("00:01:20"));
assertThat(millis.format("%1$tHh %1$tMm %1$tSs"), is("00h 01m 20s"));
assertThat(millis.format("%1$tT.%1$tL"), is("00:01:20.000"));
assertThat(seconds.format("%1$tss and %1$tSs"), is("80s and 80s"));
assertThat(seconds.format("%1$tSs and %1$tMm"), is("20s and 01m"));
}
@Test