QuantityFormatter.java

package org.djunits.formatter;

import java.util.Locale;

import org.djunits.quantity.def.AbsBasic;
import org.djunits.quantity.def.Quantity;
import org.djunits.unit.Unit;
import org.djunits.unit.Units;
import org.djunits.unit.si.PrefixType;
import org.djunits.unit.si.SIPrefix;
import org.djunits.unit.si.SIPrefixes;

/**
 * QuantityFormatter formats a quantity as a String, using the settings of the {@link QuantityFormatContext}. The
 * {@link QuantityFormatContext} is filled by using the setters in the {@link QuantityFormat} class. Note that there is no
 * guarantee that the format settings can always be satisfied. As an example, when the required width is too small to fit the
 * answer, the output will show the correct result, but violate the width format setting.
 * <p>
 * Copyright (c) 2026-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
 */
public class QuantityFormatter extends Formatter<QuantityFormatContext>
{
    /**
     * @param quantity the quantity to format
     * @param ctx the quantity format context
     */
    QuantityFormatter(final Quantity<?> quantity, final QuantityFormatContext ctx)
    {
        super(quantity, ctx);
    }

    /**
     * Format a quantity according to a given {@link QuantityFormat}. Note that this method might not be thread-safe for setting
     * the Locale. If another thread changes the Locale while formatting, outcomes could vary.
     * @param quantity the quantity to format
     * @param quantityFormat the format to apply to the quantity
     * @return a String with a formatted quantity, matching the given format as closely as possible
     */
    public static String format(final Quantity<?> quantity, final QuantityFormat quantityFormat)
    {
        QuantityFormatContext ctx = quantityFormat.ctx;
        Locale savedLocale = Locale.getDefault();
        try
        {
            savedLocale = saveLocale(ctx.locale);
            return new QuantityFormatter(quantity, ctx).format();
        }
        finally
        {
            restoreLocale(savedLocale);
        }
    }

    /**
     * Format an absolute quantity according to a given {@link QuantityFormat}. Note that this method might not be thread-safe
     * for setting the Locale. If another thread changes the Locale while formatting, outcomes could vary.
     * @param absQuantity the absolute quantity to format
     * @param quantityFormat the format to apply to the absolute quantity
     * @return a String with a formatted absolute quantity, matching the given format as closely as possible
     */
    public static String format(final AbsBasic<?, ?, ?> absQuantity, final QuantityFormat quantityFormat)
    {
        QuantityFormatContext ctx = quantityFormat.ctx;
        Locale savedLocale = Locale.getDefault();
        try
        {
            savedLocale = saveLocale(ctx.locale);
            return new QuantityFormatter(absQuantity.getQuantity(), ctx).format()
                    + formatReference(ctx, absQuantity.getReference());
        }
        finally
        {
            restoreLocale(savedLocale);
        }
    }

    /**
     * Return the value as a quantity.
     * @return the value as a quantity
     */
    Quantity<?> quantity()
    {
        return (Quantity<?>) this.value;
    }

    /**
     * Return the quantity, formatted according to the context settings.
     * @return the formatted quantity
     */
    @Override
    String format()
    {
        formatUnit();
        double value = this.useSi ? quantity().si : this.unit.getScale().fromIdentityScale(quantity().si());
        return formatValue(value) + this.ctx.unitPrefix + this.unitStr + this.ctx.unitPostfix;
    }

    /**
     * Format the unit according to the context settings.
     */
    @SuppressWarnings("checkstyle:needbraces")
    @Override
    void formatUnit()
    {
        boolean formatted = checkSiUnits();
        if (!formatted)
            formatted = checkUnitString();
        if (!formatted)
            checkDisplayUnit();
        checkAutoSiPrefix();
        if (this.unitStr == null)
            this.unitStr = this.ctx.textual ? this.unit.getTextualAbbreviation() : this.unit.getDisplayAbbreviation();
    }

    /**
     * Prepare for automatic SI scaling if it has been turned on, and if it is possible to apply.
     * @return whether automatic SI prefix scaling was applied or not
     * @param <Q> the quantity type
     */
    @SuppressWarnings({"unchecked", "checkstyle:needbraces"})
    private <Q extends Quantity<Q>> boolean checkAutoSiPrefix()
    {
        if (!this.ctx.autoSiPrefix)
            return false;

        Q q = (Q) quantity();

        // Reset to base unit if needed
        if (this.unit.getSiPrefix() == null)
        {
            q.setDisplayUnit(q.getDisplayUnit().getBaseUnit());
            this.unit = q.getDisplayUnit();
        }

        // If, e.g., SIQuantity, do not format as SI unit
        if (this.unit.getSiPrefix() == null)
            return false;

        PrefixType type = this.unit.getSiPrefix().getType();

        double si = q.si();
        if (si == 0.0)
            return false;

        double log10 = Math.log10(Math.abs(si));
        boolean invert = false;
        String baseId = this.unit.getId();
        
        // normalize per type
        switch (type)
        {
            case UNIT:
                break;

            case KILO:
                log10 += 3.0;
                baseId = baseId.substring(1);
                break;

            case PER_UNIT:
                invert = true;
                baseId = "/" + baseId.substring(1);
                break;

            case PER_KILO:
                log10 -= 3.0;
                invert = true;
                baseId = "/" + baseId.substring(2);
                break;

            default:
                return false;
        }

        int exponent;
        if (this.ctx.allowExponents12 && Math.abs(Math.floor(log10)) < 3.0)
            exponent = (int) Math.floor(log10);
        else
            exponent = (int) (3 * Math.floor(log10 / 3.0));

        int lookupExponent = invert ? -exponent : exponent;
        if (lookupExponent < this.ctx.autoSiMinExponent || lookupExponent > this.ctx.autoSiMaxExponent)
            return false;
        SIPrefix prefix = SIPrefixes.FACTORS.getOrDefault(lookupExponent, SIPrefixes.getSiPrefix(""));
        String prefixText = prefix.getDefaultTextualPrefix();
        String key = invert ? "/" + prefixText + baseId.substring(1) : prefixText + baseId;
        this.unit = (Unit<?, Q>) Units.resolve(q.getDisplayUnit().getClass(), key);
        return true;
    }
        
}