Temperature.java

package org.djunits.quantity;

import org.djunits.quantity.def.AbsoluteQuantity;
import org.djunits.quantity.def.AbstractReference;
import org.djunits.quantity.def.Quantity;
import org.djunits.unit.AbstractUnit;
import org.djunits.unit.UnitRuntimeException;
import org.djunits.unit.Units;
import org.djunits.unit.scale.LinearScale;
import org.djunits.unit.scale.Scale;
import org.djunits.unit.si.SIUnit;
import org.djunits.unit.system.UnitSystem;

/**
 * Temperature is the absolute equivalent of Temperature, and represents a true temperature rather than a temperature
 * difference.
 * <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
 */
public class Temperature extends AbsoluteQuantity<Temperature, TemperatureDifference, Temperature.Unit, Temperature.Reference>
{
    /** */
    private static final long serialVersionUID = 600L;

    /**
     * Instantiate a Temperature quantity with a unit and a reference point.
     * @param value the temperature value, expressed in a temperature unit
     * @param unit the temperature unit in which the value is expressed, relative to the reference point
     * @param reference the reference point of this absolute temperature
     */
    public Temperature(final double value, final Temperature.Unit unit, final Reference reference)
    {
        super(new TemperatureDifference(value, unit), reference);
    }

    /**
     * Instantiate a Temperature quantity with a unit and the KELVIN reference point.
     * @param value the temperature value, expressed in a temperature unit
     * @param unit the temperature unit in which the value is expressed, relative to the reference point
     */
    public Temperature(final double value, final Temperature.Unit unit)
    {
        this(value, unit, unit.getReference());
    }

    /**
     * Instantiate a Temperature quantity with a unit, expressed as a String, and a reference point.
     * @param value the temperature value, expressed in the unit, relative to the reference point
     * @param abbreviation the String abbreviation of the unit in which the value is expressed
     * @param reference the reference point of this absolute temperature
     */
    public Temperature(final double value, final String abbreviation, final Reference reference)
    {
        this(value, Units.resolve(Temperature.Unit.class, abbreviation), reference);
    }

    /**
     * Instantiate a Temperature quantity with a unit, expressed as a String, and the reference point belonging to the unit.
     * @param value the temperature value, expressed in the unit, relative to the reference point
     * @param abbreviation the String abbreviation of the unit in which the value is expressed
     */
    public Temperature(final double value, final String abbreviation)
    {
        this(value, Units.resolve(Temperature.Unit.class, abbreviation),
                Units.resolve(Temperature.Unit.class, abbreviation).getReference());
    }

    /**
     * Instantiate a Temperature instance based on an temperature and a reference point.
     * @param temperature the temperature, relative to the reference point
     * @param reference the reference point of this absolute temperature
     */
    public Temperature(final TemperatureDifference temperature, final Reference reference)
    {
        super(temperature, reference);
    }

    /**
     * Instantiate a Temperature instance based on an temperature and the KELVIN reference point.
     * @param temperature the temperature, relative to the reference point
     */
    public Temperature(final TemperatureDifference temperature)
    {
        this(temperature, Reference.KELVIN);
    }

    /**
     * Return a Temperature instance based on an SI value and a reference point.
     * @param si the temperature si value, relative to the reference point
     * @param reference the reference point of this absolute temperature
     * @return the Temperature instance based on an SI value
     */
    public static Temperature ofSi(final double si, final Reference reference)
    {
        return new Temperature(si, Temperature.Unit.SI, reference);
    }

    /**
     * Return a Temperature instance based on an SI value and the KELVIN reference point.
     * @param si the temperature si value, relative to the reference point
     * @return the Temperature instance based on an SI value
     */
    public static Temperature ofSi(final double si)
    {
        return new Temperature(si, Temperature.Unit.SI, Reference.KELVIN);
    }

    @Override
    public Temperature instantiate(final TemperatureDifference temperature, final Reference reference)
    {
        return new Temperature(temperature, reference);
    }

    @Override
    public SIUnit siUnit()
    {
        return Temperature.Unit.SI_UNIT;
    }

    /**
     * Returns a Temperature representation of a textual representation of a value with a unit. The String representation that
     * can be parsed is the double value in the unit, followed by a localized or English abbreviation of the unit. Spaces are
     * allowed, but not required, between the value and the unit.
     * @param text the textual representation to parse into a Temperature
     * @param reference the reference point of this absolute temperature
     * @return the Scalar representation of the value in its unit
     * @throws IllegalArgumentException when the text cannot be parsed
     * @throws NullPointerException when the text argument is null
     */
    public static Temperature valueOf(final String text, final Reference reference)
    {
        return new Temperature(Quantity.valueOf(text, TemperatureDifference.ZERO), reference);
    }

    /**
     * Returns a Temperature representation of a textual representation of a value with a unit, and the KELVIN reference. The
     * String representation that can be parsed is the double value in the unit, followed by a localized or English abbreviation
     * of the unit. Spaces are allowed, but not required, between the value and the unit.
     * @param text the textual representation to parse into a Temperature
     * @return the Scalar representation of the value in its unit
     * @throws IllegalArgumentException when the text cannot be parsed
     * @throws NullPointerException when the text argument is null
     */
    public static Temperature valueOf(final String text)
    {
        return new Temperature(Quantity.valueOf(text, TemperatureDifference.ZERO), Reference.KELVIN);
    }

    /**
     * Returns a Temperature based on a value and the textual representation of the unit, which can be localized.
     * @param value the value to use
     * @param unitString the textual representation of the unit
     * @param reference the reference point of this absolute temperature
     * @return the Scalar representation of the value in its unit
     * @throws IllegalArgumentException when the unit cannot be parsed or is incorrect
     * @throws NullPointerException when the unitString argument is null
     */
    public static Temperature of(final double value, final String unitString, final Reference reference)
    {
        return new Temperature(Quantity.of(value, unitString, TemperatureDifference.ZERO), reference);
    }

    /**
     * Returns a Temperature based on a value and the textual representation of the unit, which can be localized. Use the KELVIN
     * reference.
     * @param value the value to use
     * @param unitString the textual representation of the unit
     * @return the Scalar representation of the value in its unit
     * @throws IllegalArgumentException when the unit cannot be parsed or is incorrect
     * @throws NullPointerException when the unitString argument is null
     */
    public static Temperature of(final double value, final String unitString)
    {
        return new Temperature(Quantity.of(value, unitString, TemperatureDifference.ZERO), Reference.KELVIN);
    }

    @Override
    public TemperatureDifference subtract(final Temperature other)
    {
        var otherRef = other.relativeTo(getReference());
        return TemperatureDifference.ofSi(si() - otherRef.si()).setDisplayUnit(getDisplayUnit());
    }

    @Override
    public Temperature add(final TemperatureDifference other)
    {
        return new Temperature(TemperatureDifference.ofSi(si() + other.si()).setDisplayUnit(getDisplayUnit()), getReference());
    }

    @Override
    public Temperature subtract(final TemperatureDifference other)
    {
        return new Temperature(TemperatureDifference.ofSi(si() - other.si()).setDisplayUnit(getDisplayUnit()), getReference());
    }

    /**
     * The reference class to define a reference point for the absolute temperature.
     */
    public static final class Reference extends AbstractReference<Reference, TemperatureDifference>
    {
        /** Kelvin. */
        public static final Reference KELVIN =
                new Reference("KELVIN", "Kelvin scale temperature", TemperatureDifference.ZERO, null);

        /** Celsius. */
        public static final Reference CELSIUS =
                new Reference("CELSIUS", "Celsius scale temperature", TemperatureDifference.ofSi(273.15), KELVIN);

        /** Fahrenheit. */
        public static final Reference FAHRENHEIT = new Reference("FAHRENHEIT", "Fahrenheit scale temperature",
                TemperatureDifference.ofSi(273.15 - 32 / 1.8), KELVIN);

        /**
         * Define a new reference point for an absolute temperature.
         * @param id the id
         * @param name the name or explanation
         * @param offset the offset w.r.t. offsetReference
         * @param offsetReference the reference to which the offset is relative
         */
        public Reference(final String id, final String name, final TemperatureDifference offset,
                final Reference offsetReference)
        {
            super(id, name, offset, offsetReference);
        }

        /**
         * Define a new reference point for the absolute temperature, with an offset to 0 kelvin.
         * @param id the id
         * @param name the name or explanation
         * @param offset the offset w.r.t. offsetReference
         */
        public Reference(final String id, final String name, final TemperatureDifference offset)
        {
            super(id, name, offset, KELVIN);
        }

        /**
         * Define a new reference point for the absolute temperature, with an offset to 0 kelvin.
         * @param id the id
         * @param name the name or explanation
         * @param offset the offset of this scale relative to 0 kelvin
         */
        public static void add(final String id, final String name, final TemperatureDifference offset)
        {
            new Reference(id, name, offset);
        }

        /**
         * Get a reference point for the absolute temperature, based on its id. Return null when the id could not be found.
         * @param id the id
         * @return the Temperature.Reference object
         */
        public static Reference get(final String id)
        {
            return AbstractReference.get(Temperature.Reference.class, id);
        }
    }

    /******************************************************************************************************/
    /********************************************** UNIT CLASS ********************************************/
    /******************************************************************************************************/

    /**
     * Temperature.Unit encodes the units of relative and absolute temperature. Note that he reference is not initialized
     * immediately for the units, since Reference needs working units to initialize itself. Therefore, references will be
     * allocated when an reference is retrieved for the first time.
     * <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
     */
    @SuppressWarnings("checkstyle:constantname")
    public static class Unit extends AbstractUnit<Temperature.Unit, TemperatureDifference>
    {
        /** The dimensions of temperature: K. */
        public static final SIUnit SI_UNIT = SIUnit.of("K");

        /** Kelvin. */
        public static final Temperature.Unit K = new Temperature.Unit("K", "kelvin", 1.0, UnitSystem.SI_BASE);

        /** The SI or BASE unit. */
        public static final Temperature.Unit SI = K.generateSiPrefixes(false, false);

        /** Degree Celsius. */
        public static final Temperature.Unit degC =
                new Temperature.Unit("degC", "\u00B0C", "degree Celsius", new LinearScale(1.0), UnitSystem.SI_DERIVED);

        /** Degree Fahrenheit. */
        public static final Temperature.Unit degF =
                new Temperature.Unit("degF", "\u00B0F", "degree Fahrenheit", new LinearScale(5.0 / 9.0), UnitSystem.OTHER);

        /** Degree Rankine. */
        public static final Temperature.Unit degR =
                new Temperature.Unit("degR", "\u00B0R", "degree Rankine", new LinearScale(5.0 / 9.0), UnitSystem.OTHER);

        /** Degree Reaumur. */
        public static final Temperature.Unit degRe =
                new Temperature.Unit("degRe", "\u00B0R\u00E9", "degree Reaumur", new LinearScale(4.0 / 5.0), UnitSystem.OTHER);

        /** the default reference for this unit (used for absolute quantities). */
        private Reference reference;

        /** Guard to assign default references lazily, safely, exactly once. */
        private static volatile boolean defaultReferencesAssigned = false;

        /**
         * Create a new Temperature unit.
         * @param id the id or main abbreviation of the unit
         * @param name the full name of the unit
         * @param scaleFactorToBaseUnit the scale factor of the unit to convert it TO the base (SI) unit
         * @param unitSystem the unit system such as SI or IMPERIAL
         */
        public Unit(final String id, final String name, final double scaleFactorToBaseUnit, final UnitSystem unitSystem)
        {
            super(id, name, new LinearScale(scaleFactorToBaseUnit), unitSystem);
        }

        /**
         * Return a derived unit for this unit, with textual abbreviation(s) and a display abbreviation.
         * @param textualAbbreviation the textual abbreviation of the unit, which doubles as the id
         * @param displayAbbreviation the display abbreviation of the unit
         * @param name the full name of the unit
         * @param scale the scale to use to convert between this unit and the standard (e.g., SI, BASE) unit
         * @param unitSystem unit system, e.g. SI or Imperial
         */
        public Unit(final String textualAbbreviation, final String displayAbbreviation, final String name, final Scale scale,
                final UnitSystem unitSystem)
        {
            super(textualAbbreviation, displayAbbreviation, name, scale, unitSystem);
        }

        /**
         * Ensure default references for all Temperature units are assigned once and only when needed. This avoids circular
         * initialization between Temperature.Unit, Temperature.Reference, and TemperatureDifference. The method is deliberately
         * minimal: it only assigns references to existing unit singletons and does not create new objects or resolve units.
         */
        private static void ensureDefaultReferencesAssigned()
        {
            if (defaultReferencesAssigned)
            {
                return;
            }
            synchronized (Unit.class)
            {
                if (!defaultReferencesAssigned)
                {
                    // IMPORTANT:
                    // - Assign only to already-constructed constants.
                    // - Do NOT create new objects here.
                    // - Do NOT call Units.resolve(...) or valueOf(...) here.
                    // - Do NOT refer to TemperatureDifference.ZERO, etc., from here.

                    K.reference = Reference.KELVIN;
                    degC.reference = Reference.CELSIUS;
                    degF.reference = Reference.FAHRENHEIT;
                    degR.reference = Reference.KELVIN; // Rankine uses Kelvin as default reference
                    degRe.reference = Reference.CELSIUS; // Réaumur uses Celsius as default reference

                    defaultReferencesAssigned = true;
                }
            }
        }

        /**
         * Return the default reference for this unit. References are assigned lazily and safely (once) to avoid circular class
         * initialization issues.
         * @return the reference for an absolute temperature
         */
        protected Reference getReference()
        {
            if (this.reference == null)
            {
                // assign for all units exactly once, then return for this instance
                ensureDefaultReferencesAssigned();
            }
            return this.reference;
        }

        @Override
        public SIUnit siUnit()
        {
            return SI_UNIT;
        }

        @Override
        public Unit getBaseUnit()
        {
            return SI;
        }

        @Override
        public TemperatureDifference ofSi(final double si)
        {
            return TemperatureDifference.ofSi(si);
        }

        @Override
        public Unit deriveUnit(final String textualAbbreviation, final String displayAbbreviation, final String name,
                final double scaleFactor, final UnitSystem unitSystem)
        {
            if (getScale() instanceof LinearScale ls)
            {
                return new Temperature.Unit(textualAbbreviation, displayAbbreviation, name,
                        new LinearScale(ls.getScaleFactorToBaseUnit() * scaleFactor), unitSystem);
            }
            throw new UnitRuntimeException("Only possible to derive a unit from a unit with a linear scale");
        }

    }

}