Formatter.java
package org.djunits.formatter;
import java.math.BigDecimal;
import java.text.DecimalFormat;
import java.text.DecimalFormatSymbols;
import java.util.Locale;
import org.djunits.quantity.def.Reference;
import org.djunits.unit.Unit;
import org.djunits.unit.Units;
import org.djunits.value.Value;
/**
* Formatter of quantities, vectors, matrices and tables according to the format options that are stored in the
* {@link FormatContext} or one of its extensions.
* <p>
* Copyright (c) 2025-2026 Delft University of Technology, Jaffalaan 5, 2628 BX Delft, the Netherlands. All rights reserved. See
* for project information <a href="https://djunits.org" target="_blank">https://djunits.org</a>. The DJUNITS project is
* distributed under a <a href="https://djunits.org/docs/license.html" target="_blank">three-clause BSD-style license</a>.
* @author Alexander Verbraeck
* @author Peter Knoppers
* @param <C> the specific {@link FormatContext} that is used
*/
@SuppressWarnings({"checkstyle:needbraces", "checkstyle:visibilitymodifier"})
public abstract class Formatter<C extends FormatContext>
{
/** the format context. */
final C ctx;
/** the value (quantity, vector, matrix) with a display unit. */
final Value<?, ?> value;
/** the unit to express the value in. */
Unit<?, ?> unit;
/** the formatted unit. */
String unitStr = null;
/** using SI value or valueInUnit for the calculated unit. */
boolean useSi = false;
/**
* @param value the value to format
* @param ctx the format context
*/
Formatter(final Value<?, ?> value, final C ctx)
{
this.ctx = ctx;
this.value = value;
this.unit = value.getDisplayUnit();
}
/**
* Format the value(s) and unit and return the String representation.
* @return the formatted value and unit
*/
abstract String format();
/**
* Check if SI formatting needs to be applied.
* @return whether SI formatting was applied
*/
boolean checkSiUnits()
{
if (!this.ctx.siUnits)
return false;
this.unit = this.unit.siUnit();
this.useSi = true;
this.unitStr = this.unit.siUnit().format(this.ctx.siDivisionSymbol, this.ctx.siDotSeparator, this.ctx.siPowerPrefix,
this.ctx.siPowerPostfix);
return true;
}
/**
* Apply the unit string if present.
* @return whether unit string formatting was applied
*/
boolean checkUnitString()
{
if (this.ctx.unitString != null)
{
try
{
this.unit = Units.resolve(this.unit.getClass(), this.ctx.unitString);
this.useSi = false;
return true;
}
catch (Exception e)
{
// silently ignore
}
}
return false;
}
/**
* Apply the display unit if present.
* @return whether display unit formatting was applied
*/
boolean checkDisplayUnit()
{
if (this.ctx.displayUnit != null)
{
try
{
this.unit = Units.resolve(this.unit.getClass(), this.ctx.displayUnit.getId());
this.useSi = false;
return true;
}
catch (Exception e)
{
// silently ignore
}
}
return false;
}
/**
* Format the unit according to the context settings.
*/
@SuppressWarnings("checkstyle:needbraces")
void formatUnit()
{
boolean formatted = checkSiUnits();
if (!formatted)
formatted = checkUnitString();
if (!formatted)
formatted = checkDisplayUnit();
if (this.unitStr == null)
this.unitStr = this.ctx.textual ? this.unit.getTextualAbbreviation() : this.unit.getDisplayAbbreviation();
}
/**
* Format the reference of an absolute value according to the context settings.
* @param ctx the format context with the settings for formatting a reference
* @param reference the reference to format
* @return the formatted reference, or an empty string when it is not displayed
*/
static String formatReference(final FormatContext ctx, final Reference<?, ?, ?> reference)
{
if (!ctx.printReference)
{
return "";
}
return ctx.referencePrefix + reference.getId() + ctx.referencePostfix;
}
/**
* Format a value according to the context settings.
* @param val the value to format
* @return the formatted value according to the context settings
*/
String formatValue(final double val)
{
return switch (this.ctx.formatMode)
{
case VARIABLE_LENGTH -> formatVariableLength(val);
case FIXED_FLOAT -> formatFixedFloat(val);
case SCIENTIFIC_ALWAYS -> formatScientific(val);
case ENGINEERING_ALWAYS -> formatEngineering(val);
case FIXED_WITH_SCI_FALLBACK -> formatFixedSciFallback(val);
case FIXED_WITH_ENG_FALLBACK -> formatFixedEngFallback(val);
case FORMAT_STRING -> String.format(this.ctx.formatString, val);
};
}
/**
* Format a value with variable length.
* @param val the value to format
* @return a formatted value with variable length
*/
String formatVariableLength(final double val)
{
BigDecimal bd = BigDecimal.valueOf(val).stripTrailingZeros();
int left = bd.precision() - bd.scale(); // # digits left of decimal point
DecimalFormatSymbols sym = DecimalFormatSymbols.getInstance(Locale.getDefault());
DecimalFormat plain = this.ctx.groupingSeparator ? new DecimalFormat("#,##0.############################", sym)
: new DecimalFormat("###0.############################", sym);
DecimalFormat sci = new DecimalFormat("0.################E0", sym);
// Heuristic similar to %g
if (left > 10 || left < -6)
{
return sci.format(bd);
}
else
{
return plain.format(bd);
}
}
/**
* Format a value with a fixed floating point length and a given number of decimals.
* @param val the value to format
* @return a formatted value with a fixed floating point length and a given number of decimals
*/
String formatFixedFloat(final double val)
{
String gs = this.ctx.groupingSeparator ? "%," : "%";
String fmt = gs + this.ctx.width + "." + this.ctx.decimals + "f";
return String.format(fmt, val);
}
/**
* Format a value using scientific notation with a fixed length and a given number of decimals.
* @param val the value to format
* @return a formatted value using scientific notation with a fixed length and a given number of decimals
*/
String formatScientific(final double val)
{
String fmt = "%" + this.ctx.width + "." + this.ctx.decimals + (this.ctx.upperE ? "E" : "e");
return String.format(fmt, val);
}
/**
* Format a value using engineering notation with a fixed length and a given number of decimals.
* @param val the value to format
* @return a formatted value using engineering notation with a fixed length and a given number of decimals
*/
String formatEngineering(final double val)
{
if (val == 0.0)
{
return formatFixedFloat(0.0);
}
double abs = Math.abs(val);
int exp = (int) Math.floor(Math.log10(abs));
int engExp = exp - (exp % 3);
double mantissa = val / Math.pow(10, engExp);
// Mantissa formatted as fixed
String gs = this.ctx.groupingSeparator ? "%,." : "%.";
String mantFmt = gs + this.ctx.decimals + "f";
String mant = String.format(mantFmt, mantissa);
String result = mant + (this.ctx.upperE ? "E" : "e") + String.format("%+d", engExp);
return pad(result, this.ctx.width);
}
/**
* Format a value using fixed length, but when it does not fit, fall back to scientific notation.
* @param val the value to format
* @return a formatted value using fixed length, but when it does not fit, fall back to scientific notation.
*/
String formatFixedSciFallback(final double val)
{
String s = formatFixedFloat(val);
if (s.length() <= this.ctx.width)
return s;
return formatScientific(val);
}
/**
* Format a value using fixed length, but when it does not fit, fall back to engineering notation.
* @param val the value to format
* @return a formatted value using fixed length, but when it does not fit, fall back to engineering notation.
*/
String formatFixedEngFallback(final double val)
{
String s = formatFixedFloat(val);
if (s.length() <= this.ctx.width)
return s;
return formatEngineering(val);
}
/**
* Pad a string with spaces.
* @param s the string to pad
* @param width the width
* @return a padded string
*/
static String pad(final String s, final int width)
{
if (s.length() >= width)
return s;
return String.format("%" + width + "s", s);
}
/**
* Save the current locale, and change the locale.
* @param newLocale the new locale (can be null if the locale does not change)
* @return the old locale, or null when the locale was not changed
*/
static Locale saveLocale(final Locale newLocale)
{
if (newLocale != null)
{
Locale oldLocale = Locale.getDefault();
Locale.setDefault(newLocale);
return oldLocale;
}
return null;
}
/**
* Restore the locale to the old locale.
* @param oldLocale the old locale (can be null if the locale was not changed earlier)
*/
static void restoreLocale(final Locale oldLocale)
{
if (oldLocale != null)
{
Locale.setDefault(oldLocale);
}
}
}