/* * 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.Position; 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(); public static final String EMPTY = ""; 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); } public static String formatCoordinate(Context context, Position position) { return context.getString(R.string.location_latitude_longitude,position.latitude(), position.longitude()); } 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)); } /** * 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 localDateTime) { Log.w(TAG, "Date does not contain timezone information: using UTC."); t = localDateTime.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() .setDecimalPlaces(0) .setThreshold(Double.MAX_VALUE) .setUnit(unitSystem) .build(context); Distance distance = altitude_m != null ? Distance.of(altitude_m) : Distance.of(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 + ")"; } }