package co.smartreceipts.android.model.utils; import android.content.Context; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.support.annotation.VisibleForTesting; import android.text.TextUtils; import java.math.BigDecimal; import java.sql.Date; import java.text.DecimalFormat; import java.util.Map; import java.util.TimeZone; import java.util.concurrent.ConcurrentHashMap; import co.smartreceipts.android.date.DateUtils; import co.smartreceipts.android.model.Price; import co.smartreceipts.android.model.PriceCurrency; /** * A utility class, which will be used to standard some common functions that are * shared across multiple model objects */ public class ModelUtils { private static final Map<Integer, DecimalFormat> sDecimalFormatCache = new ConcurrentHashMap<>(); private ModelUtils() { throw new RuntimeException("This class uses static calls only. It cannot be instantiated"); } public static String getFormattedDate(@NonNull java.util.Date date, @NonNull TimeZone timeZone, @NonNull Context context, @NonNull String separator) { return getFormattedDate(new Date(date.getTime()), timeZone, context, separator); } /** * Gets a formatted version of a date based on the timezone and locale for a given separator. In the US, * we might expect to see a result like "10/23/2014" returned if we set the separator as "/" * * @param date - the {@link Date} to format * @param timeZone - the {@link TimeZone} to use for this date * @param context - the current {@link Context} * @param separator - the date separator (e.g. "/", "-", ".") * @return the formatted date string for the start date */ public static String getFormattedDate(@NonNull Date date, @NonNull TimeZone timeZone, @NonNull Context context, @NonNull String separator) { final java.text.DateFormat format = android.text.format.DateFormat.getDateFormat(context); format.setTimeZone(timeZone); // Hack to shift the timezone appropriately final String formattedDate = format.format(date); return formattedDate.replace(DateUtils.getDateSeparator(context), separator); } /** * Generates "decimal-formatted" value, which would appear to the end user as "25.20" or "25,20" instead of * showing naively as "25.2" or "25.2001910" * * @param number - the {@link BigDecimal} to format * @return the decimal formatted price {@link String} */ public static String getDecimalFormattedValue(float number) { return getDecimalFormattedValue(new BigDecimal(number)); } /** * Generates "decimal-formatted" value, which would appear to the end user as "25.20" or "25,20" instead of * showing naively as "25.2" or "25.2001910" * * @param decimal - the {@link BigDecimal} to format * @return the decimal formatted price {@link String} */ @NonNull public static String getDecimalFormattedValue(@NonNull BigDecimal decimal) { return getDecimalFormattedValue(decimal, Price.DEFAULT_DECIMAL_PRECISION); } /** * Generates "decimal-formatted" value, which would appear to the end user as "25.20" or "25,20" instead of * showing naively as "25.2" or "25.2001910". The number of decimal digits is based on the set precision * * @param decimal - the {@link BigDecimal} to format * @param precision - the number of digits precision to use * @return the decimal formatted price {@link String} */ @NonNull public static String getDecimalFormattedValue(@NonNull BigDecimal decimal, int precision) { // Note: I'm not concerned if we have a few duplicate entries (ie this isn't fully thread safe) as the objects are all equal DecimalFormat decimalFormat = sDecimalFormatCache.get(precision); if (decimalFormat == null) { decimalFormat = new DecimalFormat(); decimalFormat.setMaximumFractionDigits(precision); decimalFormat.setMinimumFractionDigits(precision); decimalFormat.setGroupingUsed(false); sDecimalFormatCache.put(precision, decimalFormat); } return decimalFormat.format(decimal); } /** * The "currency-formatted" value, which would appear as "$25.20" or "$25,20" as determined by the user's locale. * By default, this assumes a decimal precision of {@link Price#DEFAULT_DECIMAL_PRECISION} * * @param decimal - the {@link BigDecimal} to format * @param currency - the {@link PriceCurrency} to use. If this is {@code null}, return {@link #getDecimalFormattedValue(BigDecimal)} * @return - the currency formatted price {@link String} */ public static String getCurrencyFormattedValue(@NonNull BigDecimal decimal, @Nullable PriceCurrency currency) { return getCurrencyFormattedValue(decimal, currency, Price.DEFAULT_DECIMAL_PRECISION); } /** * The "currency-formatted" value, which would appear as "$25.20" or "$25,20" as determined by the user's locale. * * @param decimal - the {@link BigDecimal} to format * @param currency - the {@link PriceCurrency} to use. If this is {@code null}, return {@link #getDecimalFormattedValue(BigDecimal)} * @param decimalPrecision - the desired decimal precision to use (eg 2 => "$25.20", 3 => "$25.200") * @return - the currency formatted price {@link String} */ public static String getCurrencyFormattedValue(@NonNull BigDecimal decimal, @Nullable PriceCurrency currency, int decimalPrecision) { if (currency != null) { return currency.format(decimal, decimalPrecision); } else { return getDecimalFormattedValue(decimal, decimalPrecision); } } /** * The "currency-code-formatted" value, which would appear as "USD25.20" or "USD25,20" as determined by the user's locale. * By default, this assumes a decimal precision of {@link Price#DEFAULT_DECIMAL_PRECISION} * * @param decimal - the {@link BigDecimal} to format * @param currency - the {@link PriceCurrency} to use. If this is {@code null}, return {@link #getDecimalFormattedValue(BigDecimal)} * @return - the currency formatted price {@link String} */ public static String getCurrencyCodeFormattedValue(@NonNull BigDecimal decimal, @Nullable PriceCurrency currency) { return getCurrencyCodeFormattedValue(decimal, currency, Price.DEFAULT_DECIMAL_PRECISION); } /** * The "currency-code-formatted" value, which would appear as "USD25.20" or "USD25,20" as determined by the user's locale. * * @param decimal - the {@link BigDecimal} to format * @param currency - the {@link PriceCurrency} to use. If this is {@code null}, return {@link #getDecimalFormattedValue(BigDecimal)} * @param decimalPrecision - the desired decimal precision to use (eg 2 => "USD25.20", 3 => "USD25.200") * @return - the currency formatted price {@link String} */ public static String getCurrencyCodeFormattedValue(@NonNull BigDecimal decimal, @Nullable PriceCurrency currency, int decimalPrecision) { final StringBuilder stringBuilder = new StringBuilder(); if (currency != null) { stringBuilder.append(currency.getCurrencyCode()); } stringBuilder.append(getDecimalFormattedValue(decimal, decimalPrecision)); return stringBuilder.toString(); } /** * Tries to parse a string to find the underlying numerical value * * @param number the string containing a number (hopefully) * @return the {@link BigDecimal} value or "0" if it cannot be found */ public static BigDecimal tryParse(@Nullable String number) { return tryParse(number, new BigDecimal(0)); } /** * Tries to parse a string to find the underlying numerical value * * @param number the string containing a number (hopefully) * @param defaultValue the default value to use if this string is not parseable * @return the {@link BigDecimal} value or "0" if it cannot be found */ public static BigDecimal tryParse(@Nullable String number, @Nullable BigDecimal defaultValue) { if (TextUtils.isEmpty(number)) { return defaultValue; } try { return new BigDecimal(number.replace(",", ".")); } catch (NumberFormatException e) { return defaultValue; } } @VisibleForTesting public static void clearStaticCachesForTesting() { sDecimalFormatCache.clear(); } }