NumericPrecisionPrefs.java
/**
* VStar: a statistical analysis tool for variable star data.
* Copyright (C) 2010 AAVSO (http://www.aavso.org/)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.aavso.tools.vstar.util.prefs;
import java.text.DecimalFormat;
import java.text.DecimalFormatSymbols;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
import java.util.prefs.Preferences;
/**
* Numeric input/output format preferences.
*/
public class NumericPrecisionPrefs {
private enum Type {
MAG, TIME, OTHER;
public String toString() {
String s = null;
switch (this) {
case MAG:
s = "Magnitude";
break;
case TIME:
s = "Time";
break;
case OTHER:
s = "Other";
break;
}
return s;
}
};
// Input decimal place maps.
private static Map<Integer, String> timeInputFormats = new HashMap<Integer, String>();
private static Map<Integer, String> magInputFormats = new HashMap<Integer, String>();
private static Map<Integer, String> otherInputFormats = new HashMap<Integer, String>();
// Output decimal place maps.
private static Map<Integer, DecimalFormat> timeOutputFormats = new HashMap<Integer, DecimalFormat>();
private static Map<Integer, DecimalFormat> magOutputFormats = new HashMap<Integer, DecimalFormat>();
private static Map<Integer, DecimalFormat> otherOutputFormats = new HashMap<Integer, DecimalFormat>();
// Output decimal place maps: locale-independent formats.
private static Map<Integer, DecimalFormat> timeOutputFormatsLocaleIndependent = new HashMap<Integer, DecimalFormat>();
private static Map<Integer, DecimalFormat> magOutputFormatsLocaleIndependent = new HashMap<Integer, DecimalFormat>();
private static Map<Integer, DecimalFormat> otherOutputFormatsLocaleIndependent = new HashMap<Integer, DecimalFormat>();
// Default decimal place values.
private static int DEFAULT_TIME_DECIMAL_PLACES = 5;
private static int DEFAULT_MAG_DECIMAL_PLACES = 6;
private static int DEFAULT_OTHER_DECIMAL_PLACES = 12;
// Current decimal place values.
private static int timeDecimalPlaces = DEFAULT_TIME_DECIMAL_PLACES;
private static int magDecimalPlaces = DEFAULT_MAG_DECIMAL_PLACES;
private static int otherDecimalPlaces = DEFAULT_OTHER_DECIMAL_PLACES;
/**
* Clears stored locale-dependent formats (i.e. after locale change)
*/
public static void clearHashMaps() {
timeOutputFormats.clear();
magOutputFormats.clear();
otherOutputFormats.clear();
}
/**
* @return the timeDecimalPlaces
*/
public static int getTimeDecimalPlaces() {
return timeDecimalPlaces;
}
/**
* @param timeDecimalPlaces
* the timeDecimalPlaces to set
*/
public static void setTimeDecimalPlaces(int timeDecimalPlaces) {
NumericPrecisionPrefs.timeDecimalPlaces = timeDecimalPlaces;
}
/**
* @return the magDecimalPlaces
*/
public static int getMagDecimalPlaces() {
return magDecimalPlaces;
}
/**
* @param magDecimalPlaces
* the magDecimalPlaces to set
*/
public static void setMagDecimalPlaces(int magDecimalPlaces) {
NumericPrecisionPrefs.magDecimalPlaces = magDecimalPlaces;
}
/**
* @return the otherDecimalPlaces
*/
public static int getOtherDecimalPlaces() {
return otherDecimalPlaces;
}
/**
* @param otherDecimalPlaces
* the otherDecimalPlaces to set
*/
public static void setOtherDecimalPlaces(int otherDecimalPlaces) {
NumericPrecisionPrefs.otherDecimalPlaces = otherDecimalPlaces;
}
// Time (JD, phase)
public static String formatTime(double num) {
return getTimeOutputFormat().format(num);
}
public static DecimalFormat getTimeOutputFormat() {
return getOutputFormat(timeDecimalPlaces, Type.TIME);
}
public static String formatTimeLocaleIndependent(double num) {
return getTimeOutputFormatLocaleIndependent().format(num);
}
public static DecimalFormat getTimeOutputFormatLocaleIndependent() {
return getOutputFormatLocaleIndependent(timeDecimalPlaces, Type.TIME);
}
public static String getTimeInputFormat() {
return getInputFormatString(timeDecimalPlaces, Type.TIME);
}
// Magnitude
public static String formatMag(double num) {
return getMagOutputFormat().format(num);
}
public static DecimalFormat getMagOutputFormat() {
return getOutputFormat(magDecimalPlaces, Type.MAG);
}
public static String formatMagLocaleIndependent(double num) {
return getMagOutputFormatLocaleIndependent().format(num);
}
public static DecimalFormat getMagOutputFormatLocaleIndependent() {
return getOutputFormatLocaleIndependent(magDecimalPlaces, Type.MAG);
}
public static String getMagInputFormat() {
return getInputFormatString(magDecimalPlaces, Type.MAG);
}
// Other
public static String formatOther(double num) {
return getOtherOutputFormat().format(num);
}
public static DecimalFormat getOtherOutputFormat() {
return getOutputFormat(otherDecimalPlaces, Type.OTHER);
}
public static String formatOtherLocaleIndependent(double num) {
return getOtherOutputFormatLocaleIndependent().format(num);
}
public static DecimalFormat getOtherOutputFormatLocaleIndependent() {
return getOutputFormatLocaleIndependent(otherDecimalPlaces, Type.OTHER);
}
public static String getOtherInputFormat() {
return getInputFormatString(otherDecimalPlaces, Type.OTHER);
}
// PMAK (2021-03-25):
// Special case: format for polynomial coefficients.
// These coefficients have a huge value range and cannot be satisfactorily
// represented by DecimalFormat.
// What format would be better: general (%.xxG) or pure scientific (%.xxE)?
// Both have issues (for the current VeLa numeric parser):
// 1) signed positive exponent cannot be correctly parsed by VeLa now (i.e. 1.234E+15)
// 2) long number strings without decimal separator cannot be parsed too (i.e. -608516245008941)
// General format has both issues; scientific one has the (1) only.
// However, the general format looks more natural.
//
// DJB (2025-01-09):
// This generalises to other coefficients, e.g. those required for a
// piecewise linear model.
// CoefFormat
public static String formatCoef(double num) {
return formatScientific(num);
}
public static String formatCoefLocaleIndependent(double num) {
return formatScientificLocaleIndependent(num);
}
// Scientific format
private static String formatScientific(double num) {
// .replace("E+", "E"): VeLa issue workaround
return String.format(Locale.getDefault(), "%." + otherDecimalPlaces + "E", num).replace("E+", "E");
}
private static String formatScientificLocaleIndependent(double num) {
// .replace("E+", "E"): VeLa issue workaround
return String.format(Locale.ENGLISH, "%." + otherDecimalPlaces + "E", num).replace("E+", "E");
}
// Helpers
// Construct a formatter for numeric output.
private static DecimalFormat getOutputFormat(int decimalPlaces, Type type) {
Map<Integer, DecimalFormat> formats = null;
switch (type) {
case MAG:
formats = magOutputFormats;
break;
case TIME:
formats = timeOutputFormats;
break;
case OTHER:
formats = otherOutputFormats;
break;
}
if (!formats.containsKey(decimalPlaces)) {
formats.put(decimalPlaces, getOutputFormat(decimalPlaces));
}
return formats.get(decimalPlaces);
}
// Construct a formatter for locale-independent numeric output.
private static DecimalFormat getOutputFormatLocaleIndependent(int decimalPlaces, Type type) {
Map<Integer, DecimalFormat> formats = null;
switch (type) {
case MAG:
formats = magOutputFormatsLocaleIndependent;
break;
case TIME:
formats = timeOutputFormatsLocaleIndependent;
break;
case OTHER:
formats = otherOutputFormatsLocaleIndependent;
break;
}
if (!formats.containsKey(decimalPlaces)) {
formats.put(decimalPlaces, getOutputFormatLocaleIndependent(decimalPlaces));
}
return formats.get(decimalPlaces);
}
// Construct a format string of the form #.##... where the ellipsis
// denotes a variable number of hashes. This can be used with input text
// boxes where a numeric input is required.
private static String getInputFormatString(int decimalPlaces, Type type) {
Map<Integer, String> formats = null;
switch (type) {
case MAG:
formats = magInputFormats;
break;
case TIME:
formats = timeInputFormats;
break;
case OTHER:
formats = otherInputFormats;
break;
}
if (!formats.containsKey(decimalPlaces)) {
formats.put(decimalPlaces, getFormatString(decimalPlaces));
}
return formats.get(decimalPlaces);
}
private static DecimalFormat getOutputFormat(int decimalPlaces) {
DecimalFormatSymbols dfs = new DecimalFormatSymbols(Locale.getDefault());
dfs.setExponentSeparator("E"); // may differ for some locales
dfs.setMinusSign('-'); // "nn" locale fails without this
DecimalFormat decFormatter = new DecimalFormat(getFormatString(decimalPlaces), dfs);
return decFormatter;
}
private static DecimalFormat getOutputFormatLocaleIndependent(int decimalPlaces) {
DecimalFormat decFormatter = new DecimalFormat(
getFormatString(decimalPlaces), new DecimalFormatSymbols(Locale
.ENGLISH));
return decFormatter;
}
private static String getFormatString(int decimalPlaces) {
String s = "";
for (int i = 1; i <= decimalPlaces; i++) {
s += "#";
}
return "#." + s;
}
// Preferences members.
private final static String PREFS_PREFIX = "NUMERIC_DECIMAL_PLACES_";
private static Preferences prefs;
static {
// Create preferences node for numeric precision.
try {
prefs = Preferences.userNodeForPackage(NumericPrecisionPrefs.class);
retrieveDecimalPlacesPrefs();
} catch (Throwable t) {
// We need VStar to function in the absence of prefs.
}
}
private static void retrieveDecimalPlacesPrefs() {
timeDecimalPlaces = prefs.getInt(PREFS_PREFIX + "time_decimal_places",
DEFAULT_TIME_DECIMAL_PLACES);
magDecimalPlaces = prefs.getInt(PREFS_PREFIX + "mag_decimal_places",
DEFAULT_MAG_DECIMAL_PLACES);
otherDecimalPlaces = prefs.getInt(
PREFS_PREFIX + "other_decimal_places",
DEFAULT_OTHER_DECIMAL_PLACES);
}
public static void storeDecimalPlacesPrefs() {
try {
prefs.putInt(PREFS_PREFIX + "time_decimal_places",
timeDecimalPlaces);
prefs.putInt(PREFS_PREFIX + "mag_decimal_places", magDecimalPlaces);
prefs.putInt(PREFS_PREFIX + "other_decimal_places",
otherDecimalPlaces);
prefs.flush();
} catch (Throwable t) {
// We need VStar to function in the absence of prefs.
}
}
public static void setDefaultDecimalPlacePrefs() {
timeDecimalPlaces = DEFAULT_TIME_DECIMAL_PLACES;
magDecimalPlaces = DEFAULT_MAG_DECIMAL_PLACES;
otherDecimalPlaces = DEFAULT_OTHER_DECIMAL_PLACES;
storeDecimalPlacesPrefs();
}
}