SIUnit.java
package org.djunits.unit.si;
import java.util.Arrays;
import org.djunits.quantity.SIQuantity;
import org.djunits.unit.UnitInterface;
import org.djunits.unit.UnitRuntimeException;
import org.djunits.unit.scale.IdentityScale;
import org.djunits.unit.scale.Scale;
import org.djunits.unit.system.UnitSystem;
import org.djutils.exceptions.Throw;
/**
* SIUnit stores the dimensionality of a unit using the SI standards. Angle (rad) and solid angle (sr) have been added to be
* able to specify often used units regarding rotation.
* <p>
* Copyright (c) 2019-2026 Delft University of Technology, PO Box 5, 2600 AA, Delft, the Netherlands. All rights reserved. <br>
* BSD-style license. See <a href="https://djunits.org/docs/license.html">DJUNITS License</a>
* @author Alexander Verbraeck
*/
public class SIUnit implements UnitInterface<SIUnit, SIQuantity>
{
/** The (currently) 9 dimensions we take into account: rad, sr, kg, m, s, A, K, mol, cd. */
public static final int NUMBER_DIMENSIONS = 9;
/** The abbreviations of the SI units we use in SIUnit. */
private static final String[] SI_ABBREVIATIONS = new String[] {"rad", "sr", "kg", "m", "s", "A", "K", "mol", "cd"};
/** For parsing, the mol has to be parsed before the m, otherwise the "m" from "mol" is eaten; same for "s" and "sr". */
private static final int[] PARSE_ORDER = new int[] {0, 1, 2, 7, 3, 4, 5, 6, 8};
/** the dimensionless SIUnit. */
public static final SIUnit DIMLESS = new SIUnit(0, 0, 0, 0, 0, 0, 0, 0, 0);
/**
* The (currently) 9 dimensions of the SI unit we distinguish: 0: angle (rad), 1: solid angle (sr), 2: mass (kg), 3: length
* (m), 4: time (s), 5: current (A), 6: temperature (K), 7: amount of substance (mol), 8: luminous intensity (cd). As an
* example, speed is indicated as length = 1; time = -1.
*/
private final int[] dimensions;
/**
* Create an immutable SIUnit instance based on a safe copy of a given dimensions specification. As an example, speed is
* indicated as length = 1; time = -1 with the other dimensions equal to zero.
* @param dimensions The (currently) 9 dimensions of the SI unit we distinguish: 0: angle (rad), 1: solid angle (sr), 2:
* mass (kg), 3: length (m), 4: time (s), 5: current (A), 6: temperature (K), 7: amount of substance (mol), 8:
* luminous intensity (cd).
*/
public SIUnit(final int[] dimensions)
{
Throw.whenNull(dimensions, "dimensions cannot be null");
Throw.when(dimensions.length != NUMBER_DIMENSIONS, IllegalArgumentException.class,
"SIUnit has the wrong dimensionality: %s instead of %s", dimensions.length, NUMBER_DIMENSIONS);
this.dimensions = dimensions.clone(); // safe copy
}
/**
* Create an immutable SIUnit instance based on a safe copy of a given dimensions specification.
* @param angle dimension of the angle (rad)
* @param solidAngle dimension of the solidAngle (sr)
* @param mass dimension of the mass (kg)
* @param length dimension of the length (m)
* @param time dimension of the time (s)
* @param current dimension of the current (A)
* @param temperature dimension of the temperature (K)
* @param amountOfSubstance dimension of the amount of substance (mol)
* @param luminousIntensity dimension of the luminous intensity (cd)
*/
@SuppressWarnings("checkstyle:parameternumber")
public SIUnit(final int angle, final int solidAngle, final int mass, final int length, final int time, final int current,
final int temperature, final int amountOfSubstance, final int luminousIntensity)
{
this.dimensions = new int[NUMBER_DIMENSIONS];
this.dimensions[0] = angle;
this.dimensions[1] = solidAngle;
this.dimensions[2] = mass;
this.dimensions[3] = length;
this.dimensions[4] = time;
this.dimensions[5] = current;
this.dimensions[6] = temperature;
this.dimensions[7] = amountOfSubstance;
this.dimensions[8] = luminousIntensity;
}
/**
* Parse a string representing SI dimensions to an SIUnit object. Example: SIUnit.of("kgm/s2") and SIUnit.of("kgms-2") will
* both be translated to a dimensions object with vector {0,0,1,1,-2,0,0,0,0}. It is allowed to use 0 or 1 for the
* dimensions. Having the same unit in the numerator and the denominator is not seen as a problem: the values are subtracted
* from each other, so m/m will have a length dimensionality of 0. Dimensions between -9 and 9 are allowed. Spaces, periods
* and ^ are taken out, but other characters are not allowed and will lead to a UnitException. The order of allowed units is
* arbitrary, so "kg/ms2" is accepted as well as "kg/s^2.m".
* @param siString the string to parse
* @return the corresponding SI dimensions
* @throws UnitRuntimeException when the string could not be parsed into dimensions
*/
public static SIUnit of(final String siString) throws UnitRuntimeException
{
Throw.whenNull(siString, "siString cannot be null");
String dimString = siString.replaceAll("[ .^]", "");
if (dimString.contains("/"))
{
String[] parts = dimString.split("\\/");
if (parts.length != 2)
{
throw new UnitRuntimeException("SI String " + dimString + " contains more than one division sign");
}
int[] numerator = parse(parts[0]);
int[] denominator = parse(parts[1]);
for (int i = 0; i < NUMBER_DIMENSIONS; i++)
{
numerator[i] -= denominator[i];
}
return new SIUnit(numerator);
}
return new SIUnit(parse(dimString));
}
/**
* Translate a string representing SI dimensions to an SIUnit object. Example: SIUnit.of("kgm2") is translated to a vector
* {0,0,1,2,0,0,0,0,0}. It is allowed to use 0 or 1 for the dimensions. Dimensions between -9 and 9 are allowed. The parsing
* is quite lenient: periods and carets (^) are taken out, and the order can be arbitrary, so "kgms-2" is accepted as well
* as "m.s^-2.kg". Note that the empty string parses to the dimensionless unit.
* @param siString concatenation of SI units with positive or negative dimensions. No divisions sign is allowed.
* @return a vector of length <code>NUMBER_DIMENSIONS</code> with the dimensions for the SI units
* @throws UnitRuntimeException when the String cannot be parsed, e.g. due to units not being recognized
*/
private static int[] parse(final String siString) throws UnitRuntimeException
{
Throw.whenNull(siString, "siString cannot be null");
int[] result = new int[NUMBER_DIMENSIONS];
if (siString.equals("1") || siString.length() == 0)
{
return result;
}
String copy = siString;
int copyLength = copy.length();
while (copyLength > 0)
{
// find the next unit
for (int j = 0; j < SI_ABBREVIATIONS.length; j++)
{
int i = PARSE_ORDER[j];
String si = SI_ABBREVIATIONS[i];
if (copy.startsWith(si))
{
if (result[i] != 0)
{
throw new UnitRuntimeException("SI string " + siString + " has a double entry for unit " + si);
}
copy = copy.substring(si.length());
if (copy.length() == 0)
{
result[i] = 1;
break;
}
else if (copy.startsWith("-"))
{
if (copy.length() == 1)
{
throw new UnitRuntimeException("SI string " + siString + " ends with a minus sign");
}
if (Character.isDigit(copy.charAt(1)))
{
result[i] = (-1 * (copy.charAt(1) - '0'));
copy = copy.substring(2);
break;
}
throw new UnitRuntimeException(
"SI string " + siString + " has a minus sign for unit " + si + " but no dimension");
}
else if (Character.isDigit(copy.charAt(0)))
{
result[i] = (copy.charAt(0) - '0');
copy = copy.substring(1);
break;
}
else
{
result[i] = 1;
break;
}
}
}
if (copy.length() == copyLength)
{
// we did not parse anything... wrong character
break;
}
copyLength = copy.length();
}
if (copy.length() != 0)
{
throw new UnitRuntimeException("Trailing information in SI string " + siString);
}
return result;
}
/**
* Returns a safe copy of the SI abbreviations (a public static final String[] is mutable).
* @return a safe copy of the SI abbreviations
*/
public String[] siAbbreviations()
{
return SI_ABBREVIATIONS.clone();
}
/**
* Return a safe copy of the exponents of the SI dimensions in the order rad, sr, kg, m, s, A, K, mol, cd. Since it is a
* safe copy, calculations can be carried out on the int[] return value.
* @return a safe copy of the exponents of the SI dimensions in the order rad, sr, kg, m, s, A, K, mol, cd
*/
public int[] siDimensions()
{
return this.dimensions.clone();
}
/**
* Add a set of SI dimensions to this SIUnit. Note: as dimensions are considered to be immutable, a new dimension is
* returned. The original dimension (<code>this</code>) remains unaltered.
* @param other the dimensions to add (usually as a result of multiplication of scalars)
* @return the new dimensions with the dimensions of this object plus the dimensions in the parameter
*/
public SIUnit plus(final SIUnit other)
{
int[] result = new int[NUMBER_DIMENSIONS];
for (int i = 0; i < NUMBER_DIMENSIONS; i++)
{
result[i] = this.dimensions[i] + other.dimensions[i];
}
return new SIUnit(result);
}
/**
* Subtract a set of SI dimensions from this SIUnit. Note: as dimensions are considered to be immutable, a new dimension is
* returned. The original dimension (<code>this</code>) remains unaltered.
* @param other the dimensions to subtract (usually as a result of division of scalars)
* @return the new dimensions with the dimensions of this object minus the dimensions in the parameter
*/
public SIUnit minus(final SIUnit other)
{
int[] result = new int[NUMBER_DIMENSIONS];
for (int i = 0; i < NUMBER_DIMENSIONS; i++)
{
result[i] = this.dimensions[i] - other.dimensions[i];
}
return new SIUnit(result);
}
/**
* Invert a set of SI dimensions; instead of m/s we get s/m. Note: as dimensions are considered to be immutable, a new
* dimension is returned. The original dimension (<code>this</code>) remains unaltered.
* @return the new dimensions that are the inverse of the dimensions in this object
*/
public SIUnit invert()
{
int[] result = new int[NUMBER_DIMENSIONS];
for (int i = 0; i < NUMBER_DIMENSIONS; i++)
{
result[i] = -this.dimensions[i];
}
return new SIUnit(result);
}
/**
* Raise a set of SI dimensions to the n-th power. Note: as dimensions are considered to be immutable, a new dimension is
* returned. The original dimension (<code>this</code>) remains unaltered.
* @param n the power to which to raise this set of dimensions
* @return the new dimensions with the dimensions of this object raised to the n-th power
*/
public SIUnit pow(final int n)
{
int[] result = new int[NUMBER_DIMENSIONS];
for (int i = 0; i < NUMBER_DIMENSIONS; i++)
{
result[i] = this.dimensions[i] * n;
}
return new SIUnit(result);
}
/**
* Add two SIUnit and return the new SIUnit. Usually, dimensions are added as a result of multiplication of scalars.
* @param dim1 the first set of dimensions
* @param dim2 the second set of dimensions
* @return the new dimensions with the sum of the dimensions in the parameters
*/
public static SIUnit add(final SIUnit dim1, final SIUnit dim2)
{
int[] dim = new int[NUMBER_DIMENSIONS];
for (int i = 0; i < NUMBER_DIMENSIONS; i++)
{
dim[i] = dim1.dimensions[i] + dim2.dimensions[i];
}
return new SIUnit(dim);
}
/**
* Subtract an SIUnit (dim2) from another SIUnit (dim1) and return the new SIUnit. Usually, dimensions are added as a result
* of division of scalars.
* @param dim1 the first set of dimensions
* @param dim2 the second set of dimensions that will be subtracted from dim1
* @return the new dimensions with the difference of the dimensions in the parameters
*/
public static SIUnit subtract(final SIUnit dim1, final SIUnit dim2)
{
int[] dim = new int[NUMBER_DIMENSIONS];
for (int i = 0; i < NUMBER_DIMENSIONS; i++)
{
dim[i] = dim1.dimensions[i] - dim2.dimensions[i];
}
return new SIUnit(dim);
}
@Override
public SIQuantity ofSi(final double si)
{
return new SIQuantity(si, this);
}
/**
* Return a string such as "kgm/s2" or "kg.m/s^2" or "kg.m.s^-2" from this SIUnit.
* @param divided if true, return m/s2 for acceleration; if false return ms-2
* @param separator add this string between successive units, e.g. kg.m.s-2 instead of kgms-2
* @param powerPrefix the prefix for the power, e.g., "^" or "<sup>"
* @param powerPostfix the postfix for the power, e.g., "</sup>"
* @return a formatted string for this SIUnit
*/
public String toString(final boolean divided, final String separator, final String powerPrefix, final String powerPostfix)
{
StringBuffer s = new StringBuffer();
boolean first = true;
boolean negative = false;
for (int i = 0; i < NUMBER_DIMENSIONS; i++)
{
if (this.dimensions[i] < 0)
{
negative = true;
}
if ((!divided && this.dimensions[i] != 0) || (divided && this.dimensions[i] > 0))
{
if (!first)
{
s.append(separator);
}
else
{
first = false;
}
s.append(SI_ABBREVIATIONS[i]);
if (this.dimensions[i] != 1)
{
s.append(powerPrefix);
s.append(this.dimensions[i]);
s.append(powerPostfix);
}
}
}
if (s.length() == 0)
{
s.append("1");
}
if (divided && negative)
{
s.append("/");
}
if (divided)
{
first = true;
for (int i = 0; i < NUMBER_DIMENSIONS; i++)
{
if (this.dimensions[i] < 0)
{
if (!first)
{
s.append(separator);
}
else
{
first = false;
}
s.append(SI_ABBREVIATIONS[i]);
if (this.dimensions[i] < -1)
{
s.append(powerPrefix);
s.append(-this.dimensions[i]);
s.append(powerPostfix);
}
}
}
}
if (s.toString().equals("1"))
{
return "";
}
return s.toString();
}
/**
* Return a string such as "kgm/s2" or "kg.m/s2" or "kg.m.s-2" from this SIUnit.
* @param divided if true, return m/s2 for acceleration; if false return ms-2
* @param separator if true, add a period between successive units, e.g. kg.m.s-2 instead of kgms-2
* @return a formatted string describing this SIUnit
*/
public String toString(final boolean divided, final boolean separator)
{
return toString(divided, separator ? "." : "", "", "");
}
/**
* Return a string such as "kgm/s2" or "kg.m/s^2" or "kg.m.s^-2" from this SIUnit.
* @param divided if true, return m/s2 for acceleration; if false return ms-2
* @param separator if true, add a period between successive units, e.g. kg.m.s-2 instead of kgms-2
* @param power if true, add a ^ sign before the power, e.g., "kg.m^2/s^3" instead of "kg.m2/s3"
* @return a formatted string describing this SIUnit
*/
public String toString(final boolean divided, final boolean separator, final boolean power)
{
return toString(divided, separator ? "." : "", power ? "^" : "", "");
}
/**
* Return a string such as "kgm/s<sup>2</sup>" or or "kg.m.s<sup>-2</sup>" from this SIUnit.
* @param divided if true, return "m/s<sup>2</sup>" for acceleration; if false return "ms<sup>-2</sup>"
* @param separator if true, add a period between successive units, e.g. kg.m.s<sup>-2</sup>
* @return a formatted string describing this SIUnit
*/
public String toHTMLString(final boolean divided, final boolean separator)
{
return toString(divided, separator ? "." : "", "<sup>", "</sup>");
}
@Override
public String toString()
{
return Arrays.toString(this.dimensions);
}
@Override
public String getId()
{
return toString(true, false);
}
@Override
public Scale getScale()
{
return IdentityScale.SCALE;
}
@Override
public UnitSystem getUnitSystem()
{
return UnitSystem.SI_BASE;
}
@Override
public SIUnit siUnit()
{
return this;
}
@Override
public SIUnit getBaseUnit()
{
return this;
}
@Override
public String getTextualAbbreviation()
{
return toString(true, false);
}
@Override
public String getDisplayAbbreviation()
{
return toString(true, false);
}
@Override
public String getName()
{
return toString(true, false);
}
@Override
public String getStoredTextualAbbreviation()
{
return toString(true, false);
}
@Override
public String getStoredDisplayAbbreviation()
{
return toString(true, false);
}
@Override
public String getStoredName()
{
return toString(true, false);
}
@Override
public SIUnit setSiPrefix(final SIPrefix siPrefix)
{
return this;
}
@Override
public SIUnit setSiPrefix(final String prefix)
{
return this;
}
@Override
public SIUnit setSiPrefixKilo(final String prefix)
{
return this;
}
@Override
public SIUnit setSiPrefixPer(final String prefix)
{
return this;
}
@Override
public SIPrefix getSiPrefix()
{
return null;
}
@Override
public int hashCode()
{
final int prime = 31;
int result = 1;
result = prime * result + Arrays.hashCode(this.dimensions);
return result;
}
@Override
@SuppressWarnings("checkstyle:needbraces")
public boolean equals(final Object obj)
{
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
SIUnit other = (SIUnit) obj;
if (!Arrays.equals(this.dimensions, other.dimensions))
return false;
return true;
}
}