package com.kickstarter.libs.utils; import android.content.Context; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.util.Pair; import com.kickstarter.R; import com.kickstarter.libs.KSString; import com.kickstarter.libs.NumberOptions; import com.kickstarter.libs.RelativeDateTimeOptions; import org.joda.time.DateTime; import org.joda.time.DateTimeZone; import org.joda.time.Seconds; import org.joda.time.format.DateTimeFormat; import org.joda.time.format.DateTimeFormatter; import java.util.Locale; public final class DateTimeUtils { private DateTimeUtils() {} /** * e.g.: December 2015. */ public static @NonNull String estimatedDeliveryOn(final @NonNull DateTime dateTime) { return estimatedDeliveryOn(dateTime, Locale.getDefault()); } /** * e.g.: December 2015. */ public static @NonNull String estimatedDeliveryOn(final @NonNull DateTime dateTime, final @NonNull Locale locale) { return dateTime.toString(DateTimeFormat.forPattern("MMMM yyyy").withLocale(locale).withZoneUTC()); } public static boolean isDateToday(final @NonNull DateTime dateTime) { return dateTime.withZone(DateTimeZone.UTC).withTimeAtStartOfDay() .equals(DateTime.now().withTimeAtStartOfDay().withZoneRetainFields(DateTimeZone.UTC)); } /** * Returns a boolean indicating whether or not a DateTime value is the Epoch. Returns `true` if the * DateTime equals 1970-01-01T00:00:00Z. */ public static boolean isEpoch(final @NonNull DateTime dateTime) { return dateTime.getMillis() == 0; } /** * e.g.: Dec 17, 2015. */ public static @NonNull String fullDate(final @NonNull DateTime dateTime) { return fullDate(dateTime, Locale.getDefault()); } /** * e.g.: Dec 17, 2015. */ public static @NonNull String fullDate(final @NonNull DateTime dateTime, final @NonNull Locale locale) { try { return dateTime.toString(DateTimeFormat.fullDate().withLocale(locale).withZoneUTC()); } catch (final IllegalArgumentException e) { // JodaTime doesn't support the 'cccc' pattern, triggered by fullDate and fullDateTime. See: https://github.com/dlew/joda-time-android/issues/30 // Instead just return a medium date. return mediumDate(dateTime, locale); } } /** * e.g.: Dec 17, 2015. */ public static @NonNull String mediumDate(final @NonNull DateTime dateTime) { return mediumDate(dateTime, Locale.getDefault()); } /** * e.g.: Dec 17, 2015. */ public static @NonNull String mediumDate(final @NonNull DateTime dateTime, final @NonNull Locale locale) { return dateTime.toString(DateTimeFormat.mediumDate().withLocale(locale).withZoneUTC()); } /** * e.g.: Jan 14, 2016 2:20 PM. */ public static @NonNull String mediumDateShortTime(final @NonNull DateTime dateTime) { return mediumDateShortTime(dateTime, DateTimeZone.getDefault(), Locale.getDefault()); } /** * e.g.: Jan 14, 2016 2:20 PM. */ public static @NonNull String mediumDateShortTime(final @NonNull DateTime dateTime, final @NonNull DateTimeZone dateTimeZone) { return mediumDateShortTime(dateTime, dateTimeZone, Locale.getDefault()); } /** * e.g.: Jan 14, 2016 2:20 PM. */ public static @NonNull String mediumDateShortTime(final @NonNull DateTime dateTime, final @NonNull DateTimeZone dateTimeZone, final @NonNull Locale locale) { final String mediumShortStyle = DateTimeFormat.patternForStyle("MS", locale); final DateTimeFormatter formatter = DateTimeFormat.forPattern(mediumShortStyle).withZone(dateTimeZone).withLocale(locale); return dateTime.toString(formatter); } /** * e.g.: Dec 17, 2015 6:35:05 PM. */ public static @NonNull String mediumDateTime(final @NonNull DateTime dateTime) { return mediumDateTime(dateTime, DateTimeZone.getDefault()); } /** * e.g.: Dec 17, 2015 6:35:05 PM. */ public static @NonNull String mediumDateTime(final @NonNull DateTime dateTime, final @NonNull DateTimeZone dateTimeZone) { return mediumDateTime(dateTime, dateTimeZone, Locale.getDefault()); } /** * e.g.: Dec 17, 2015 6:35:05 PM. */ public static @NonNull String mediumDateTime(final @NonNull DateTime dateTime, final @NonNull DateTimeZone dateTimeZone, final @NonNull Locale locale) { return dateTime.toString(DateTimeFormat.mediumDateTime().withLocale(locale).withZone(dateTimeZone)); } /** * Returns a string indicating the distance between {@link DateTime}s. Defaults to comparing the input {@link DateTime} to * the current time. */ public static @NonNull String relative(final @NonNull Context context, final @NonNull KSString ksString, final @NonNull DateTime dateTime) { return relative(context, ksString, dateTime, RelativeDateTimeOptions.builder().build()); } /** * Returns a string indicating the distance between {@link DateTime}s. Defaults to comparing the input {@link DateTime} to * the current time. */ public static @NonNull String relative(final @NonNull Context context, final @NonNull KSString ksString, final @NonNull DateTime dateTime, final @NonNull RelativeDateTimeOptions options) { final DateTime relativeToDateTime = ObjectUtils.coalesce(options.relativeToDateTime(), DateTime.now()); final Seconds seconds = Seconds.secondsBetween(dateTime, relativeToDateTime); final int secondsDifference = seconds.getSeconds(); if (secondsDifference >= 0.0 && secondsDifference <= 60.0) { return context.getString(R.string.dates_just_now); } else if (secondsDifference >= -60.0 && secondsDifference <= 0.0) { return context.getString(R.string.dates_right_now); } final Pair<String, Integer> unitAndDifference = unitAndDifference(secondsDifference, options.threshold()); if (unitAndDifference == null) { // Couldn't find a good match, just render the date. return mediumDate(dateTime); } final String unit = unitAndDifference.first; final int difference = unitAndDifference.second; boolean willHappenIn = false; boolean happenedAgo = false; if (!options.absolute()) { if (secondsDifference < 0) { willHappenIn = true; } else if (secondsDifference > 0) { happenedAgo = true; } } if (happenedAgo && "days".equals(unit) && difference == 1) { return context.getString(R.string.dates_yesterday); } final StringBuilder baseKeyPath = new StringBuilder(); if (willHappenIn) { baseKeyPath.append(String.format("dates_time_in_%s", unit)); } else if (happenedAgo) { baseKeyPath.append(String.format("dates_time_%s_ago", unit)); } else { baseKeyPath.append(String.format("dates_time_%s", unit)); } if (options.abbreviated()) { baseKeyPath.append("_abbreviated"); } return ksString.format(baseKeyPath.toString(), difference, "time_count", NumberUtils.format(difference, NumberOptions.builder().build())); } /** * Utility to pair a unit (e.g. "minutes", "hours", "days") with a measurement. Returns `null` if the difference * exceeds the threshold. */ private static @Nullable Pair<String, Integer> unitAndDifference(final int initialSecondsDifference, final int threshold) { final int secondsDifference = Math.abs(initialSecondsDifference); final int daysDifference = (int) Math.floor(secondsDifference / 86400); if (secondsDifference < 3600) { // 1 hour final int minutesDifference = (int) Math.floor(secondsDifference / 60.0); return new Pair<>("minutes", minutesDifference); } else if (secondsDifference < 86400) { // 24 hours final int hoursDifference = (int) Math.floor(secondsDifference / 60.0 / 60.0); return new Pair<>("hours", hoursDifference); } else if (secondsDifference < threshold) { return new Pair<>("days", daysDifference); } return null; } }