/* * Copyright 2008 Google Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of * the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the * License for the specific language governing permissions and limitations under * the License. */ package de.dennisguse.opentracks.util; import static java.time.temporal.ChronoUnit.DAYS; import android.content.Context; import android.location.Location; import android.text.TextUtils; import android.text.format.DateUtils; import android.util.Log; import android.util.Pair; import androidx.annotation.NonNull; import java.math.RoundingMode; import java.text.DecimalFormat; import java.time.Duration; import java.time.Instant; import java.time.LocalDate; import java.time.LocalDateTime; import java.time.OffsetDateTime; import java.time.ZoneId; import java.time.ZoneOffset; import java.time.ZonedDateTime; import java.time.format.DateTimeFormatter; import java.time.format.FormatStyle; import java.time.temporal.TemporalAccessor; import de.dennisguse.opentracks.R; import de.dennisguse.opentracks.data.models.Cadence; import de.dennisguse.opentracks.data.models.Distance; import de.dennisguse.opentracks.data.models.DistanceFormatter; import de.dennisguse.opentracks.data.models.HeartRate; import de.dennisguse.opentracks.data.models.Power; import de.dennisguse.opentracks.settings.UnitSystem; /** * Various string manipulation methods. * * @author Sandor Dornbush * @author Rodrigo Damazio */ public class StringUtils { private static final String TAG = StringUtils.class.getSimpleName(); private StringUtils() { } /** * Formats the date and time with the offset (using default Locale format). */ public static String formatDateTimeWithOffset(OffsetDateTime odt) { return odt.toZonedDateTime().format(DateTimeFormatter.ofLocalizedDateTime(FormatStyle.FULL)); } public static String formatDateTimeWithOffsetIfDifferent(OffsetDateTime odt) { if (!odt.getOffset().equals(OffsetDateTime.now().getOffset())) { return odt.toZonedDateTime().format(DateTimeFormatter.ofLocalizedDateTime(FormatStyle.FULL)); } return odt.toZonedDateTime().format(DateTimeFormatter.ofLocalizedDateTime(FormatStyle.MEDIUM)); } public static String formatLocalDateTime(LocalDateTime localDateTime) { return localDateTime.format(DateTimeFormatter.ofLocalizedDate(FormatStyle.FULL)); } /** * Formats the date relative to today date. */ public static String formatDateTodayRelative(Context context, OffsetDateTime odt) { LocalDate today = LocalDateTime.ofInstant(Instant.now(), ZoneId.systemDefault()).toLocalDate(); LocalDate ld = odt.toLocalDate(); long daysBetween = DAYS.between(ld, today); if (daysBetween == 0) { // Today return context.getString(R.string.generic_today); } else if (daysBetween == 1) { // Yesterday return context.getString(R.string.generic_yesterday); } else if (daysBetween < 7) { // Name of the week day return ld.format(DateTimeFormatter.ofPattern("EEEE")); } else if (today.getYear() == ld.getYear()) { // Short date without year return ld.format(DateTimeFormatter.ofPattern("d MMM")); } else { // Short date with year return ld.format(DateTimeFormatter.ofPattern("d MMM y")); } } /** * Formats the time using the ISO 8601 date time format with fractional seconds. */ public static String formatDateTimeIso8601(@NonNull Instant time, ZoneOffset zoneOffset) { return time .atOffset(zoneOffset) .format(DateTimeFormatter.ISO_OFFSET_DATE_TIME); } /** * Formats the elapsed timed in the form "MM:SS" or "H:MM:SS". */ public static String formatElapsedTime(@NonNull Duration time) { return DateUtils.formatElapsedTime(time.getSeconds()); } /** * Formats the elapsed time in the form "H:MM:SS". */ public static String formatElapsedTimeWithHour(@NonNull Duration time) { String value = formatElapsedTime(time); return TextUtils.split(value, ":").length == 2 ? "0:" + value : value; } /** * Format a decimal number while removing trailing zeros of the decimal part (if present). */ public static String formatDecimal(double value, int decimalPlaces) { DecimalFormat df = new DecimalFormat(); df.setMinimumFractionDigits(decimalPlaces); df.setMaximumFractionDigits(decimalPlaces); df.setRoundingMode(RoundingMode.HALF_EVEN); return df.format(value); } /** * Formats a coordinate * * @param context the context's object. * @param coordinate the coordinate */ public static String formatCoordinate(Context context, double coordinate) { return context.getString(R.string.location_coordinate, Location.convert(coordinate, Location.FORMAT_DEGREES)); } /** * Formats a complete coordinate (latitude, longitude) * * @param context the context's object. * @param latitude the coordinate's latitude. * @param longitude the coordinate's longitude. */ public static String formatCoordinate(Context context, double latitude, double longitude) { return context.getString(R.string.location_latitude_longitude, Location.convert(latitude, Location.FORMAT_DEGREES), Location.convert(longitude, Location.FORMAT_DEGREES)); } public static Pair getHeartRateParts(Context context, HeartRate heartrate) { String value = context.getString(R.string.value_unknown); if (heartrate != null) { value = StringUtils.formatDecimal(heartrate.getBPM(), 0); } return new Pair<>(value, context.getString(R.string.sensor_unit_beats_per_minute)); } public static Pair getCadenceParts(Context context, Cadence cadence) { String value = context.getString(R.string.value_unknown); if (cadence != null) { value = StringUtils.formatDecimal(cadence.getRPM(), 0); } return new Pair<>(value, context.getString(R.string.sensor_unit_rounds_per_minute)); } public static Pair getPowerParts(Context context, Power power) { String value = context.getString(R.string.value_unknown); if (power != null) { value = StringUtils.formatDecimal(power.getW(), 0); } return new Pair<>(value, context.getString(R.string.sensor_unit_power)); } /** * Gets a string for category. * * @param category the category */ public static String getCategory(String category) { if (category == null || category.length() == 0) { return null; } return "[" + category + "]"; } /** * Gets a string for category and description. * * @param category the category * @param description the description */ public static String getCategoryDescription(String category, String description) { if (category == null || category.length() == 0) { return description; } StringBuilder builder = new StringBuilder(); builder.append(getCategory(category)); if (description != null && description.length() != 0) { builder.append(" ").append(description); } return builder.toString(); } /** * Formats the given text as a XML CDATA element. * This includes adding the starting and ending CDATA tags. * NOTE: This may result in multiple consecutive CDATA tags. * * @param text the given text */ public static String formatCData(String text) { return "", "]]]]>") + "]]>"; } /** * Gets the time, in milliseconds, from an XML date time string (ISO8601) as defined at ... * Let's be lenient: if timezone information is not provided, UTC will be used. * * @param xmlDateTime the XML date time string */ public static OffsetDateTime parseTime(String xmlDateTime) { try { TemporalAccessor t = DateTimeFormatter.ISO_DATE_TIME.parseBest(xmlDateTime, ZonedDateTime::from, LocalDateTime::from); if (t instanceof LocalDateTime) { Log.w(TAG, "Date does not contain timezone information: using UTC."); t = ((LocalDateTime) t).atZone(ZoneOffset.UTC); } return OffsetDateTime.from(t); } catch (Exception e) { Log.e(TAG, "Invalid XML dateTime value"); throw e; } } /** * @return the formatted altitude_m (or null) and it's unit as {@link Pair} */ //TODO altitude_m should be double or a value object public static Pair getAltitudeParts(Context context, Float altitude_m, UnitSystem unitSystem) { DistanceFormatter formatter = DistanceFormatter.Builder() .setDecimalCount(0) .setThreshold(Double.MAX_VALUE) .setUnit(unitSystem) .build(context); Distance distance = altitude_m != null ? Distance.of(altitude_m) : Distance.of((Double) null); return formatter.getDistanceParts(distance); } public static String formatAltitude(Context context, Float altitude_m, UnitSystem unitSystem) { Pair altitudeParts = getAltitudeParts(context, altitude_m, unitSystem); return context.getString(R.string.altitude_with_unit, altitudeParts.first, altitudeParts.second); } public static String valueInParentheses(String text) { return "(" + text + ")"; } }