QuantityFormatter.java

package org.djunits.formatter;

import java.util.Locale;

import org.djunits.quantity.def.AbsQuantity;
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 AbsQuantity<?, ?, ?> 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)
            formatted = checkDisplayUnit();
        checkScaleSiPrefixes();
        if (this.unitStr == null)
            this.unitStr = this.ctx.textual ? this.unit.getTextualAbbreviation() : this.unit.getDisplayAbbreviation();
    }

    /**
     * Apply the display unit if present.
     * @return whether display unit formatting was applied
     * @param <Q> the quantity type
     */
    @SuppressWarnings("unchecked")
    private <Q extends Quantity<Q>> boolean checkScaleSiPrefixes()
    {
        Q q = (Q) quantity();
        if (this.ctx.scaleSiPrefixes && this.unit.getSiPrefix() != null)
        {
            PrefixType type = this.unit.getSiPrefix().getType();
            double si = q.si();
            double log10si = Math.log10(si);
            int power = log10si > 0 ? 3 * (int) (Math.log10(si) / 3.0) : 3 * (int) (Math.log10(si) / 3.0 - 1);
            if (power >= this.ctx.minimumPrefixPower && power <= this.ctx.maximumPrefixPower)
            {
                switch (type)
                {
                    case UNIT:
                    {
                        SIPrefix prefix = SIPrefixes.FACTORS.getOrDefault(power, SIPrefixes.getSiPrefix(""));
                        String key = prefix.getDefaultTextualPrefix() + q.getDisplayUnit().getBaseUnit().getId();
                        this.unit = (Unit<?, Q>) Units.resolve(q.getDisplayUnit().getClass(), key);
                        return true;
                    }
                    case KILO:
                    {
                        power += 3;
                        SIPrefix prefix = SIPrefixes.FACTORS.getOrDefault(power, SIPrefixes.getSiPrefix(""));
                        String key = prefix.getDefaultTextualPrefix() + q.getDisplayUnit().getBaseUnit().getId().substring(1);
                        this.unit = (Unit<?, Q>) Units.resolve(q.getDisplayUnit().getClass(), key);
                        return true;
                    }
                    case PER_UNIT:
                    {
                        SIPrefix prefix = SIPrefixes.FACTORS.getOrDefault(-power, SIPrefixes.getSiPrefix(""));
                        String key =
                                "/" + prefix.getDefaultTextualPrefix() + q.getDisplayUnit().getBaseUnit().getId().substring(1);
                        this.unit = (Unit<?, Q>) Units.resolve(q.getDisplayUnit().getClass(), key);
                        return true;
                    }
                    case PER_KILO:
                    {
                        power -= 3;
                        SIPrefix prefix = SIPrefixes.FACTORS.getOrDefault(-power, SIPrefixes.getSiPrefix(""));
                        String key =
                                "/" + prefix.getDefaultTextualPrefix() + q.getDisplayUnit().getBaseUnit().getId().substring(2);
                        this.unit = (Unit<?, Q>) Units.resolve(q.getDisplayUnit().getClass(), key);
                        return true;
                    }
                    default:
                        // silently ignore
                }
            }
        }
        return false;
    }

}