package com.evancharlton.mileage.math; import android.content.Context; import android.text.TextUtils; import android.text.format.DateFormat; import com.evancharlton.mileage.R; import com.evancharlton.mileage.dao.Fillup; import com.evancharlton.mileage.dao.FillupSeries; import com.evancharlton.mileage.dao.Vehicle; import java.util.Currency; import java.util.Date; import java.util.Locale; // TODO(future) - Does the name of this still make sense? public class Calculator { // dates public static final long DAY_MILLIS = 1000L * 60L * 60L * 24L; public static final long MONTH_MS = DAY_MILLIS * 30L; public static final long YEAR_MS = DAY_MILLIS * 365L; public static final int DATE_DATE = 1; public static final int DATE_LONG = 2; public static final int DATE_MEDIUM = 3; public static final int DATE_TIME = 4; // distance public static final int KM = 1; public static final int MI = 2; // volume public static final int GALLONS = 3; public static final int LITRES = 4; public static final int IMPERIAL_GALLONS = 5; // economy public static final int MI_PER_GALLON = 6; public static final int KM_PER_GALLON = 7; public static final int MI_PER_IMP_GALLON = 8; public static final int KM_PER_IMP_GALLON = 9; public static final int MI_PER_LITRE = 10; public static final int KM_PER_LITRE = 11; public static final int GALLONS_PER_100KM = 12; public static final int LITRES_PER_100KM = 13; public static final int IMP_GAL_PER_100KM = 14; // cache private static String CURRENCY_SYMBOL = null; private static final java.text.DateFormat[] FORMATTERS = new java.text.DateFormat[4]; private Calculator() { // no initialization } /** * Returns a positive integer if first is a *better* economy than second, a * negative integer if second is *better* than first, and 0 if the two are * equal. * * @param first value of the first economy * @param firstUnit units on the first economy * @param second value of the second economy * @param secondUnit units on the second economy * @return positive if first is better than second, negative if second is * better, and 0 if equal */ public static int compareEconomies(double first, int firstUnit, double second, int secondUnit) { if (firstUnit == secondUnit) { switch (firstUnit) { case GALLONS_PER_100KM: case LITRES_PER_100KM: case IMP_GAL_PER_100KM: if (first < second) { return 1; } else if (first > second) { return -1; } return 0; case MI_PER_GALLON: case KM_PER_GALLON: case MI_PER_IMP_GALLON: case KM_PER_IMP_GALLON: case MI_PER_LITRE: case KM_PER_LITRE: default: if (first > second) { return 1; } else if (first < second) { return -1; } return 0; } } else { double converted = convert(second, secondUnit, firstUnit); return compareEconomies(first, firstUnit, converted, firstUnit); } } public static double averageEconomy(Vehicle vehicle, Fillup fillup) { if (!fillup.hasPrevious()) { throw new IllegalArgumentException("You can't calculate economy on one fillup"); } if (fillup.isPartial()) { return 0D; } Fillup clone = (Fillup) fillup.getPrevious().clone(); clone.setPrevious(null); return averageEconomy(vehicle, new FillupSeries(clone, fillup)); } /** * Calculate the economy of the most recent fillup of the series. * * @param vehicle * @param series * @return */ public static double fillupEconomy(Vehicle vehicle, FillupSeries series) { Fillup current = series.last(); if (current.isPartial()) { return 0D; } double nextValidOdometer = 0D; double topOdometer = current.getOdometer(); double volume = 0D; while (current.hasPrevious()) { volume += current.getVolume(); current = current.getPrevious(); nextValidOdometer = current.getOdometer(); if (!current.isPartial()) { break; } } double distance = topOdometer - nextValidOdometer; return getEconomy(vehicle, distance, volume); } /** * @param vehicle * @param first * @param second * @return true if first is BETTER than second */ public static boolean isBetterEconomy(Vehicle vehicle, double first, double second) { switch (vehicle.getEconomyUnits()) { case GALLONS_PER_100KM: case LITRES_PER_100KM: case IMP_GAL_PER_100KM: return first <= second; } return first >= second; } public static double averageEconomy(Vehicle vehicle, FillupSeries series) { return getEconomy(vehicle, series.getTotalDistance(), series.getEconomyVolume()); } private static double getEconomy(Vehicle vehicle, double distance, double volume) { // ALL CALCULATIONS ARE DONE IN MPG AND CONVERTED LATER double miles = convert(distance, vehicle.getDistanceUnits(), MI); double gallons = convert(volume, vehicle.getVolumeUnits(), GALLONS); switch (vehicle.getEconomyUnits()) { case KM_PER_GALLON: return convert(miles, KM) / gallons; case MI_PER_IMP_GALLON: return miles / convert(gallons, IMPERIAL_GALLONS); case KM_PER_IMP_GALLON: return convert(miles, KM) / convert(gallons, IMPERIAL_GALLONS); case MI_PER_LITRE: return miles / convert(gallons, LITRES); case KM_PER_LITRE: return convert(miles, KM) / convert(gallons, LITRES); case GALLONS_PER_100KM: return (100D * gallons) / convert(miles, KM); case LITRES_PER_100KM: return (100D * convert(gallons, LITRES)) / convert(miles, KM); case IMP_GAL_PER_100KM: return (100D * convert(gallons, IMPERIAL_GALLONS)) / convert(miles, KM); case MI_PER_GALLON: default: return miles / gallons; } } public static double averageDistanceBetweenFillups(FillupSeries series) { return series.getTotalDistance() / (series.size() - 1); } public static double averageFillupVolume(FillupSeries series) { return series.getTotalVolume() / series.size(); } public static double averageFillupCost(FillupSeries series) { return series.getTotalCost() / series.size(); } public static double averageCostPerDistance(FillupSeries series) { if (series.size() <= 1) { return 0D; } double totalCost = series.getTotalCost(); double totalDistance = series.getTotalDistance(); // We need to subtract out the cost for the very first fillup because // it does not have a distance associated with it. totalCost -= series.get(0).getTotalCost(); return (totalCost / totalDistance); } public static double averageFuelPerDay(FillupSeries series) { long timeRange = series.getTimeRange(); double numDays = Math.ceil((double) timeRange / (double) DAY_MILLIS); return series.getTotalVolume() / numDays; } public static double averageCostPerDay(FillupSeries series) { long timeRange = series.getTimeRange(); double numDays = Math.ceil((double) timeRange / (double) DAY_MILLIS); return series.getTotalCost() / numDays; } public static double averagePrice(FillupSeries series) { double total = 0D; final int SIZE = series.size(); for (int i = 0; i < SIZE; i++) { Fillup fillup = series.get(i); total += fillup.getUnitPrice(); } return total / SIZE; } // yes, this method makes it possible to convert from miles to litres. // if you do this, I'll hunt you down and beat you with a rubber hose. public static double convert(double value, int from, int to) { // going from whatever to miles or gallons (depending on context) switch (from) { case KM: value *= 0.621371192; break; case LITRES: value *= 0.264172052; break; case IMPERIAL_GALLONS: value *= 1.20095042; break; case KM_PER_GALLON: value *= 0.621371192; break; case MI_PER_IMP_GALLON: value *= 0.83267384; break; case KM_PER_IMP_GALLON: value *= 0.517399537; break; case MI_PER_LITRE: value *= 3.78541178; break; case KM_PER_LITRE: value *= 2.35214583; break; case GALLONS_PER_100KM: value *= 62.1371192; break; case LITRES_PER_100KM: value *= 235.214583; break; case IMP_GAL_PER_100KM: value *= 51.7399537; break; case MI: case GALLONS: default: break; } // at this point, "value" is either miles or gallons return convert(value, to); } // convert from (miles|gallons) to the other unit private static double convert(double value, int to) { // value is now converted to miles or gallons switch (to) { case MI: return value; case KM: return value /= 0.621371192; case GALLONS: return value; case LITRES: return value /= 0.264172052; case IMPERIAL_GALLONS: return value /= 1.20095042; case MI_PER_GALLON: return value; case MI_PER_LITRE: return value *= 0.264172052; case MI_PER_IMP_GALLON: return value *= 1.20095042; case KM_PER_GALLON: return value *= 1.609344; case KM_PER_LITRE: return value *= 0.425143707; case KM_PER_IMP_GALLON: return value *= 1.93274236; case GALLONS_PER_100KM: return value *= 62.1371192; case LITRES_PER_100KM: return value *= 235.214583; case IMP_GAL_PER_100KM: return value *= 51.7399537; } return value; } public static String getVolumeUnits(Context context, Vehicle vehicle) { switch (vehicle.getVolumeUnits()) { case LITRES: return context.getString(R.string.units_litres); case IMPERIAL_GALLONS: case GALLONS: default: return context.getString(R.string.units_gallons); } } public static String getVolumeUnitsAbbr(Context context, Vehicle vehicle) { switch (vehicle.getVolumeUnits()) { case LITRES: return context.getString(R.string.units_litres_abbr); case IMPERIAL_GALLONS: case GALLONS: default: return context.getString(R.string.units_gallons_abbr); } } public static String getDistanceUnits(Context context, Vehicle vehicle) { switch (vehicle.getDistanceUnits()) { case KM: return context.getString(R.string.units_kilometers); case MI: default: return context.getString(R.string.units_miles); } } public static String getDistanceUnitsAbbr(Context context, Vehicle vehicle) { switch (vehicle.getDistanceUnits()) { case KM: return context.getString(R.string.units_kilometers_abbr); case MI: default: return context.getString(R.string.units_miles_abbr); } } public static String getEconomyUnitsAbbr(Context context, Vehicle vehicle) { switch (vehicle.getEconomyUnits()) { case KM_PER_GALLON: case KM_PER_IMP_GALLON: return context.getString(R.string.units_kmpg); case MI_PER_LITRE: return context.getString(R.string.units_mpl); case KM_PER_LITRE: return context.getString(R.string.units_kmpl); case LITRES_PER_100KM: return context.getString(R.string.units_lpckm); case GALLONS_PER_100KM: case IMP_GAL_PER_100KM: return context.getString(R.string.units_gpckm); case MI_PER_GALLON: case MI_PER_IMP_GALLON: default: return context.getString(R.string.units_mpg); } } public static String getCurrencySymbol(Vehicle vehicle) { String savedCurrency = vehicle.getCurrency(); if (TextUtils.isEmpty(savedCurrency)) { savedCurrency = getCurrencySymbol(); } return savedCurrency; } public static String getCurrencySymbol() { return Currency.getInstance(Locale.getDefault()).getSymbol(); } public static String getDateString(Context context, int type, Date date) { if (FORMATTERS[type] == null) { switch (type) { case DATE_DATE: FORMATTERS[DATE_DATE] = DateFormat.getDateFormat(context); break; case DATE_LONG: FORMATTERS[DATE_LONG] = DateFormat.getTimeFormat(context); break; case DATE_MEDIUM: FORMATTERS[DATE_MEDIUM] = DateFormat.getMediumDateFormat(context); break; case DATE_TIME: FORMATTERS[DATE_TIME] = DateFormat.getMediumDateFormat(context); } } return FORMATTERS[type].format(date); } };