planetiler/planetiler-core/src/main/java/com/onthegomap/planetiler/util/Format.java

194 wiersze
6.4 KiB
Java
Czysty Zwykły widok Historia

package com.onthegomap.planetiler.util;
2021-04-16 00:54:33 +00:00
import java.text.NumberFormat;
2021-06-04 11:22:40 +00:00
import java.time.Duration;
2022-01-28 01:23:24 +00:00
import java.util.Locale;
2021-04-16 00:54:33 +00:00
import java.util.Map;
import java.util.NavigableMap;
import java.util.TreeMap;
2022-01-28 01:23:24 +00:00
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
2023-03-18 18:38:04 +00:00
import java.util.stream.Collectors;
import java.util.stream.DoubleStream;
import org.apache.commons.text.StringEscapeUtils;
2021-10-20 01:57:47 +00:00
import org.locationtech.jts.geom.Coordinate;
2021-04-16 00:54:33 +00:00
2021-09-10 00:46:20 +00:00
/**
* Utilities for formatting values as strings.
*/
2021-04-16 00:54:33 +00:00
public class Format {
2022-01-28 01:23:24 +00:00
public static final Locale DEFAULT_LOCALE = Locale.getDefault(Locale.Category.FORMAT);
private static final ConcurrentMap<Locale, Format> instances = new ConcurrentHashMap<>();
2023-03-18 18:38:04 +00:00
@SuppressWarnings("java:S5164")
private static final NumberFormat latLonNF = NumberFormat.getNumberInstance(Locale.US);
private static final NavigableMap<Long, String> STORAGE_SUFFIXES = new TreeMap<>(Map.ofEntries(
Map.entry(1_000L, "k"),
Map.entry(1_000_000L, "M"),
Map.entry(1_000_000_000L, "G"),
Map.entry(1_000_000_000_000L, "T"),
Map.entry(1_000_000_000_000_000L, "P")
));
private static final NavigableMap<Long, String> NUMERIC_SUFFIXES = new TreeMap<>(Map.ofEntries(
Map.entry(1_000L, "k"),
Map.entry(1_000_000L, "M"),
Map.entry(1_000_000_000L, "B"),
Map.entry(1_000_000_000_000L, "T"),
Map.entry(1_000_000_000_000_000L, "Q")
));
static {
latLonNF.setMaximumFractionDigits(5);
}
// `NumberFormat` instances are not thread safe, so we need to wrap them inside a `ThreadLocal`.
//
// Ignore warnings about not removing thread local values since planetiler uses dedicated worker threads that release
// values when a task is finished and are not re-used.
@SuppressWarnings("java:S5164")
private final ThreadLocal<NumberFormat> pf;
@SuppressWarnings("java:S5164")
private final ThreadLocal<NumberFormat> nf;
@SuppressWarnings("java:S5164")
private final ThreadLocal<NumberFormat> intF;
2022-01-28 01:23:24 +00:00
private Format(Locale locale) {
pf = ThreadLocal.withInitial(() -> {
var f = NumberFormat.getPercentInstance(locale);
f.setMaximumFractionDigits(0);
return f;
});
nf = ThreadLocal.withInitial(() -> {
var f = NumberFormat.getNumberInstance(locale);
f.setMaximumFractionDigits(1);
return f;
});
intF = ThreadLocal.withInitial(() -> {
var f = NumberFormat.getNumberInstance(locale);
f.setMaximumFractionDigits(0);
return f;
});
2022-01-28 01:23:24 +00:00
}
2023-03-18 18:38:04 +00:00
/** Returns a string with {@code items} rounded to 5 decimals and joined with a comma. */
public static synchronized String joinCoordinates(double... items) {
return DoubleStream.of(items).mapToObj(latLonNF::format).collect(Collectors.joining(","));
}
2022-01-28 01:23:24 +00:00
public static Format forLocale(Locale locale) {
return instances.computeIfAbsent(locale, Format::new);
2022-01-28 01:23:24 +00:00
}
public static Format defaultInstance() {
return forLocale(DEFAULT_LOCALE);
2021-09-10 00:46:20 +00:00
}
2021-04-16 00:54:33 +00:00
public static String padRight(String str, int size) {
StringBuilder strBuilder = new StringBuilder(str);
while (strBuilder.length() < size) {
strBuilder.append(" ");
}
return strBuilder.toString();
}
public static String padLeft(String str, int size) {
StringBuilder strBuilder = new StringBuilder(str);
while (strBuilder.length() < size) {
strBuilder.insert(0, " ");
}
return strBuilder.toString();
}
2023-03-18 18:38:04 +00:00
/** Returns Java code that can re-create {@code string}: {@code null} if null, or {@code "contents"} if not empty. */
public static String quote(Object string) {
if (string == null) {
return "null";
}
return '"' + StringEscapeUtils.escapeJava(string.toString()) + '"';
}
/** Returns an openstreetmap.org map link for a lat/lon */
public static String osmDebugUrl(int zoom, Coordinate coord) {
return "https://www.openstreetmap.org/#map=%d/%.5f/%.5f".formatted(
zoom,
coord.y,
coord.x
);
}
2021-09-10 00:46:20 +00:00
/** Returns a number of bytes formatted like "123" "1.2k" "240M", etc. */
2022-01-28 01:23:24 +00:00
public String storage(Number num, boolean pad) {
2021-04-16 00:54:33 +00:00
return format(num, pad, STORAGE_SUFFIXES);
}
/** Alias for {@link #storage(Number, boolean)} where {@code pad=false}. */
public String storage(Number num) {
return storage(num, false);
}
/** Alias for {@link #numeric(Number, boolean)} where {@code pad=false}. */
public String numeric(Number num) {
return numeric(num, false);
}
2021-09-10 00:46:20 +00:00
/** Returns a number formatted like "123" "1.2k" "2.5B", etc. */
2022-01-28 01:23:24 +00:00
public String numeric(Number num, boolean pad) {
2021-04-16 00:54:33 +00:00
return format(num, pad, NUMERIC_SUFFIXES);
}
2022-01-28 01:23:24 +00:00
private String format(Number num, boolean pad, NavigableMap<Long, String> suffixes) {
2021-04-16 00:54:33 +00:00
long value = num.longValue();
2021-07-30 11:07:00 +00:00
double doubleValue = num.doubleValue();
2021-04-16 00:54:33 +00:00
if (value < 0) {
2021-08-05 01:22:20 +00:00
return padLeft("-", pad ? 4 : 0);
2021-07-30 11:07:00 +00:00
} else if (doubleValue > 0 && doubleValue < 1) {
2021-07-30 09:32:10 +00:00
return padLeft("<1", pad ? 4 : 0);
} else if (value < 1000) {
2021-09-10 00:46:20 +00:00
// 0-999
2021-04-16 00:54:33 +00:00
return padLeft(Long.toString(value), pad ? 4 : 0);
}
Map.Entry<Long, String> e = suffixes.floorEntry(value);
Long divideBy = e.getKey();
String suffix = e.getValue();
long truncated = value / (divideBy / 10);
boolean hasDecimal = truncated < 100 && (truncated % 10 != 0);
2022-01-28 01:23:24 +00:00
return padLeft(hasDecimal ? decimal(truncated / 10d) + suffix : (truncated / 10) + suffix, pad ? 4 : 0);
2021-04-16 00:54:33 +00:00
}
2021-09-10 00:46:20 +00:00
/** Returns 0.0-1.0 as a "0%" - "100%" with no decimal points. */
2022-01-28 01:23:24 +00:00
public String percent(double value) {
return pf.get().format(value);
2021-04-16 00:54:33 +00:00
}
2021-09-10 00:46:20 +00:00
/** Returns a number formatted with 1 decimal point. */
2022-01-28 01:23:24 +00:00
public String decimal(double value) {
return nf.get().format(value);
2021-04-16 00:54:33 +00:00
}
2021-06-04 11:22:40 +00:00
2021-09-10 00:46:20 +00:00
/** Returns a number formatted with 0 decimal points. */
2022-01-28 01:23:24 +00:00
public String integer(Number value) {
return intF.get().format(value);
2021-06-04 11:22:40 +00:00
}
2021-09-10 00:46:20 +00:00
/** Returns a duration formatted as fractional seconds with 1 decimal point. */
2022-01-28 01:23:24 +00:00
public String seconds(Duration duration) {
2021-06-04 11:22:40 +00:00
double seconds = duration.toNanos() * 1d / Duration.ofSeconds(1).toNanos();
2022-01-28 01:23:24 +00:00
return decimal(seconds < 1 ? seconds : Math.round(seconds)) + "s";
2021-06-04 11:22:40 +00:00
}
/** Returns a duration formatted like "1h2m" or "2m3s". */
public String duration(Duration duration) {
Duration simplified;
double seconds = duration.toNanos() * 1d / Duration.ofSeconds(1).toNanos();
if (seconds < 1) {
return decimal(seconds) + "s";
} else {
simplified = Duration.ofSeconds(Math.round(seconds));
}
return simplified.toString().replace("PT", "").toLowerCase(Locale.ROOT);
}
2021-04-16 00:54:33 +00:00
}