001/*
002 * Units of Measurement Reference Implementation
003 * Copyright (c) 2005-2021, Jean-Marie Dautelle, Werner Keil, Otavio Santana.
004 *
005 * All rights reserved.
006 *
007 * Redistribution and use in source and binary forms, with or without modification,
008 * are permitted provided that the following conditions are met:
009 *
010 * 1. Redistributions of source code must retain the above copyright notice,
011 *    this list of conditions and the following disclaimer.
012 *
013 * 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions
014 *    and the following disclaimer in the documentation and/or other materials provided with the distribution.
015 *
016 * 3. Neither the name of JSR-385, Indriya nor the names of their contributors may be used to endorse or promote products
017 *    derived from this software without specific prior written permission.
018 *
019 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
020 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
021 * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
022 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
023 * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
024 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
025 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
026 * AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
027 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
028 * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
029 */
030package tech.units.indriya.format;
031
032import javax.measure.Prefix;
033import javax.measure.Unit;
034import javax.measure.UnitConverter;
035
036import tech.units.indriya.AbstractUnit;
037import tech.units.indriya.function.AbstractConverter;
038import tech.units.indriya.function.MultiplyConverter;
039
040import java.lang.reflect.Field;
041import java.util.Collections;
042import java.util.Comparator;
043import java.util.Enumeration;
044import java.util.HashMap;
045import java.util.List;
046import java.util.Map;
047import java.util.ResourceBundle;
048import java.util.TreeMap;
049import java.util.logging.Level;
050import java.util.logging.Logger;
051import java.util.stream.Collectors;
052
053/**
054 * <p>
055 * This class provides a set of mappings between {@link AbstractUnit units} and symbols (both ways), between {@link javax.measure.Prefix prefixes} and symbols
056 * (both ways), and from {@link AbstractConverter unit converters} to {@link javax.measure.MetricPrefix MetricPrefix} (one way). No attempt is made to verify the
057 * uniqueness of the mappings.
058 * </p>
059 *
060 * <p>
061 * Mappings are read from a <code>ResourceBundle</code>, the keys of which should consist of a fully-qualified class name, followed by a dot ('.'),
062 * and then the name of a static field belonging to that class, followed optionally by another dot and a number. If the trailing dot and number are
063 * not present, the value associated with the key is treated as a {@link SymbolMap#label(AbstractUnit, String) label}, otherwise if the trailing dot
064 * and number are present, the value is treated as an {@link SymbolMap#alias(AbstractUnit,String) alias}. Aliases map from String to Unit only,
065 * whereas labels map in both directions. A given unit may have any number of aliases, but may have only one label.
066 * </p>
067 *
068 * @author <a href="mailto:eric-r@northwestern.edu">Eric Russell</a>
069 * @author <a href="mailto:werner@units.tech">Werner Keil</a>
070 * @version 2.1, November 21, 2020
071 */
072@SuppressWarnings("rawtypes")
073public final class SymbolMap {
074  private static final Logger logger = Logger.getLogger(SymbolMap.class.getName());
075
076  private final Map<String, Unit<?>> symbolToUnit;
077  private final Map<Unit<?>, String> unitToSymbol;
078  private final Map<String, Object> symbolToPrefix;
079  private final Map<Object, String> prefixToSymbol;
080  private final Map<UnitConverter, Prefix> converterToPrefix;
081
082  /**
083   * Creates an empty mapping.
084   */
085  private SymbolMap() {
086    symbolToUnit = new TreeMap<>();
087    unitToSymbol = new HashMap<>();
088    symbolToPrefix = new TreeMap<>();
089    prefixToSymbol = new HashMap<>();
090    converterToPrefix = new HashMap<>();
091  }
092
093  /**
094   * Creates a symbol map from the specified resource bundle,
095   *
096   * @param rb
097   *          the resource bundle.
098   */
099  private SymbolMap(ResourceBundle rb) {
100    this();
101    for (Enumeration<String> i = rb.getKeys(); i.hasMoreElements();) {
102      String fqn = i.nextElement();
103      String symbol = rb.getString(fqn);
104      boolean isAlias = false;
105      int lastDot = fqn.lastIndexOf('.');
106      String className = fqn.substring(0, lastDot);
107      String fieldName = fqn.substring(lastDot + 1, fqn.length());
108      if (Character.isDigit(fieldName.charAt(0))) {
109        isAlias = true;
110        fqn = className;
111        lastDot = fqn.lastIndexOf('.');
112        className = fqn.substring(0, lastDot);
113        fieldName = fqn.substring(lastDot + 1, fqn.length());
114      }
115      try {
116        Class<?> c = Class.forName(className);
117        Field field = c.getField(fieldName);
118        Object value = field.get(null);
119        if (value instanceof Unit<?>) {
120          if (isAlias) {
121            alias((Unit) value, symbol);
122          } else {
123            label((AbstractUnit<?>) value, symbol);
124          }
125        } else if (value instanceof Prefix) {
126          label((Prefix) value, symbol);
127        } else {
128          throw new ClassCastException("unable to cast " + value + " to Unit or Prefix");
129        }
130      } catch (Exception error) {
131        logger.log(Level.SEVERE, "Error", error);
132      }
133    }
134  }
135
136  /**
137   * Creates a symbol map from the specified resource bundle,
138   *
139   * @param rb
140   *          the resource bundle.
141   */
142  public static SymbolMap of(ResourceBundle rb) {
143    return new SymbolMap(rb);
144  }
145
146  /**
147   * Attaches a label to the specified unit. For example:<br>
148   * <code> symbolMap.label(DAY.multiply(365), "year"); symbolMap.label(Units.OHM, "Ω");
149   * </code>
150   *
151   * @param unit
152   *          the unit to label.
153   * @param symbol
154   *          the new symbol for the unit.
155   */
156  public void label(Unit<?> unit, String symbol) {
157    symbolToUnit.put(symbol, unit);
158    unitToSymbol.put(unit, symbol);
159  }
160
161  /**
162   * Attaches an alias to the specified unit. Multiple aliases may be attached to the same unit. Aliases are used during parsing to recognize
163   * different variants of the same unit.<code> symbolMap.alias(Units.MONTH, "month"); symbolMap.alias(Units.MONTH, "months");
164   * symbolMap.alias(Units.METER, "meter"); symbolMap.alias(Units.METER, "metre"); </code>
165   *
166   * @param unit
167   *          the unit to label.
168   * @param symbol
169   *          the new symbol for the unit.
170   */
171  public void alias(Unit<?> unit, String symbol) {
172    symbolToUnit.put(symbol, unit);
173  }
174
175  /**
176   * Attaches a label to the specified prefix. For example:<br>
177   * <code> symbolMap.label(MetricPrefix.GIGA, "G"); symbolMap.label(MetricPrefix.MICRO, "µ");
178   * </code>
179   * 
180   */
181  public void label(Prefix prefix, String symbol) {
182    symbolToPrefix.put(symbol, prefix);
183    prefixToSymbol.put(prefix, symbol);
184    converterToPrefix.put(MultiplyConverter.ofPrefix(prefix), prefix);
185  }
186
187  /**
188   * Returns the unit for the specified symbol.
189   *
190   * @param symbol
191   *          the symbol.
192   * @return the corresponding unit or <code>null</code> if none.
193   */
194  public Unit<?> getUnit(String symbol) {
195    return symbolToUnit.get(symbol);
196  }
197
198  /**
199   * Returns the symbol (label) for the specified unit.
200   *
201   * @param unit
202   *          the corresponding symbol.
203   * @return the corresponding symbol or <code>null</code> if none.
204   */
205  public String getSymbol(Unit<?> unit) {
206    return unitToSymbol.get(unit);
207  }
208
209  /**
210   * Returns the prefix (if any) for the specified symbol.
211   *
212   * @param symbol
213   *          the unit symbol.
214   * @return the corresponding prefix or <code>null</code> if none.
215   */
216  public Prefix getPrefix(String symbol) {
217        final List<String> list = symbolToPrefix.keySet().stream().collect(Collectors.toList());
218        final Comparator<String> comparator = Comparator.comparing(String::length);
219        Collections.sort(list, comparator.reversed());
220
221        for (String key : list) {
222            if (symbol.startsWith(key)) {
223                return (Prefix) symbolToPrefix.get(key);
224            }
225        }
226        return null;
227    }
228
229  /**
230   * Returns the prefix for the specified converter.
231   *
232   * @param converter
233   *          the unit converter.
234   * @return the corresponding prefix or <code>null</code> if none.
235   */
236  public Prefix getPrefix(UnitConverter converter) {
237    return converterToPrefix.get(converter);
238  }
239
240  /**
241   * Returns the symbol for the specified prefix.
242   *
243   * @param prefix
244   *          the prefix.
245   * @return the corresponding symbol or <code>null</code> if none.
246   */
247  public String getSymbol(Prefix prefix) {
248    return prefixToSymbol.get(prefix);
249  }
250
251@Override
252  public String toString() {
253    StringBuilder sb = new StringBuilder();
254    sb.append("tech.units.indriya.format.SymbolMap: [");
255    sb.append("symbolToUnit: ").append(symbolToUnit).append(',');
256    sb.append("unitToSymbol: ").append(unitToSymbol).append(',');
257    sb.append("symbolToPrefix: ").append(symbolToPrefix).append(',');
258    sb.append("prefixToSymbol: ").append(prefixToSymbol).append(',');
259    sb.append("converterToPrefix: ").append(converterToPrefix).append(',');
260    sb.append("converterToPrefix: ").append(converterToPrefix);
261    sb.append(" ]");
262    return sb.toString();
263  }
264}