Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix QuantityType dimensionless one and time formatting #4169

Merged
merged 1 commit into from May 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
Expand Up @@ -18,16 +18,22 @@
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;
Expand All @@ -40,7 +46,6 @@
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;
Expand Down Expand Up @@ -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);

Expand Down Expand Up @@ -366,6 +378,14 @@ public int hashCode() {

@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;

Expand All @@ -376,16 +396,83 @@ public String format(String pattern) {
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.
}
}
}
Expand Down Expand Up @@ -429,11 +516,7 @@ public double doubleValue() {

@Override
public String toFullString() {
if (AbstractUnit.ONE.equals(quantity.getUnit())) {
return quantity.getValue().toString();
} else {
return quantity.toString();
}
return quantity.toString();
}

@Override
Expand Down
Expand Up @@ -250,11 +250,17 @@ public void testFormats() {
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
Expand Down