/*
 * EDUnits Copyright 2009, NOAA.
 * See the LICENSE.txt file in this file's directory.
 */
package ucar.nc2.ogc.erddap.util;

import ucar.nc2.util.Misc;

import java.io.IOException;
import java.io.InputStream;
import java.util.HashMap;

/**
 * This class has static methods to convert units from one standard to another.
 */
public class ErddapEDUnits {

    /**
     * UDUNITS and UCUM support metric prefixes.
     */
    public static String metricName[] = {
            "yotta", "zetta", "exa", "peta", "tera",
            "giga", "mega", "kilo", "hecto", "deka",
            "deci", "centi", "milli", "micro", "nano",
            "pico", "femto", "atto", "zepto", "yocto",
            "µ",};
    public static String metricAcronym[] = {
            "Y", "Z", "E", "P", "T",
            "G", "M", "k", "h", "da",
            "d", "c", "m", "u", "n",
            "p", "f", "a", "z", "y",
            "u"};
    public static int nMetric = metricName.length;

    /**
     * UCUM supports power-of-two prefixes, but UDUNITS doesn't.
     */
    public static String twoAcronym[] = {
            "Ki", "Mi", "Gi", "Ti"};
    public static String twoValue[] = {
            "1024", "1048576", "1073741824", "1.099511627776e12"};
    public static int nTwo = twoAcronym.length;

    private static final HashMap<String, String> udHashMap;
    private static final HashMap<String, String> ucHashMap;

    static {
        try (   InputStream udunitsToUcumStream = ErddapEDUnits.class.getResourceAsStream("UdunitsToUcum.properties");
                InputStream ucumToUdunitsStream = ErddapEDUnits.class.getResourceAsStream("UcumToUdunits.properties")) {
            udHashMap = getHashMapStringString(udunitsToUcumStream, "ISO-8859-1");
            ucHashMap = getHashMapStringString(ucumToUdunitsStream, "ISO-8859-1");
        } catch (IOException e) {
            throw new ExceptionInInitializerError(e);
        }
    }

    /**
     * This converts UDUnits to UCUM.
     *
UDUnits: http://www.unidata.ucar.edu/software/udunits/udunits-1/etc/udunits.dat
     * http://www.unidata.ucar.edu/software/udunits/udunits-2/udunits2.html
     * I worked with v 2.1.9
     *
UCUM: http://unitsofmeasure.org/ucum.html
     * I worked with Version: 1.8, $Revision: 28894 $
     *

     *
UDUnits supports lots of aliases (short and long)
     * and plurals (usually by adding 's').
     * These all get reduced to UCUM's short and canonical-only units.
     *

     *
Notes:
     *

     *
This method is a strictly case sensitive.
     * The only UDUnits that should be capitalized (other than acronyms) are
     * Btu, Gregorian..., Julian..., PI.
     *
The only UDUnits that may be capitalized are
     * Celsius, Fahrenheit, Kelvin, Rankine.
     *
For "10 to the", UCUM allows 10* or 10^. This method uses 10^.
     *
NTU becomes {ntu}.
     *
PSU or psu becomes {psu}.
     *

     *

     * return the UDUnits converted to UCUM.
     * null returns null. "" returns "".
     * throws Exception if trouble.
     */
    public static String udunitsToUcum(String udunits) {
        if (udunits == null) {
            return null;
        }

        //is it a point in time? e.g., seconds since 1970-01-01T00:00:00T
        int sincePo = udunits.indexOf(" since ");
        if (sincePo > 0) {
            try {
                //test if really appropriate
                double baf[] = ErddapCalendar2.getTimeBaseAndFactor(udunits); //throws exception if trouble

                //use 'factor', since it is more forgiving than udunitsToUcum converter
                String u;
                if (Misc.nearlyEquals(baf[1], 0.001, 1e-6)) {  // Can't simply do "baf[1] == 0.001".
                    u = "ms";
                } else if (baf[1] == 1) {
                    u = "s";
                } else if (baf[1] == ErddapCalendar2.SECONDS_PER_MINUTE) {
                    u = "min";
                } else if (baf[1] == ErddapCalendar2.SECONDS_PER_HOUR) {
                    u = "h";
                } else if (baf[1] == ErddapCalendar2.SECONDS_PER_DAY) {
                    u = "d";
                } else if (baf[1] == 30 * ErddapCalendar2.SECONDS_PER_DAY) {  // mo_j ?
                    u = "mo";
                } else if (baf[1] == 360 * ErddapCalendar2.SECONDS_PER_DAY) {  // a_j ?
                    u = "a";
                } else {
                    u = udunitsToUcum(udunits.substring(0, sincePo)); //shouldn't happen, but weeks? microsec?
                }

                //make "s{since 1970-01-01T00:00:00T}
                return u + "{" + udunits.substring(sincePo + 1) + "}";
            } catch (Exception e) {
            }
        }

        //parse udunits and build ucum, till done
        StringBuilder ucum = new StringBuilder();
        int udLength = udunits.length();
        int po = 0;  //po is next position to be read
        while (po < udLength) {
            char ch = udunits.charAt(po);

            //letter
            if (isUdunitsLetter(ch)) {     //includes 'µ' and '°'
                //find contiguous letters|_|digit (no '-')
                int po2 = po + 1;
                while (po2 < udLength &&
                        (isUdunitsLetter(udunits.charAt(po2)) || udunits.charAt(po2) == '_' ||
                                ErddapString2.isDigit(udunits.charAt(po2)))) {
                    po2++;
                }
                String tUdunits = udunits.substring(po, po2);
                po = po2;

                //some udunits have internal digits, but none end in digits
                //if it ends in digits, treat as exponent
                //find contiguous digits at end
                int firstDigit = tUdunits.length();
                while (firstDigit >= 1 && ErddapString2.isDigit(tUdunits.charAt(firstDigit - 1))) {
                    firstDigit--;
                }
                String exponent = tUdunits.substring(firstDigit);
                tUdunits = tUdunits.substring(0, firstDigit);
                String tUcum = oneUdunitsToUcum(tUdunits);

                //deal with PER -> /
                if (tUcum.equals("/")) {
                    char lastUcum = ucum.length() == 0 ? '\u0000' : ucum.charAt(ucum.length() - 1);
                    if (lastUcum == '/') {
                        ucum.setCharAt(ucum.length() - 1, '.'); //2 '/' cancel out
                    } else if (lastUcum == '.') {
                        ucum.setCharAt(ucum.length() - 1, '/'); //  '/' replaces '.'
                    } else {
                        ucum.append('/');
                    }

                } else {
                    ucum.append(tUcum);
                }

                //add the exponent
                ucum.append(exponent);
                //catch -exponent as a number below

                continue;
            }

            //number
            if (ch == '-' || ErddapString2.isDigit(ch)) {
                //find contiguous digits
                int po2 = po + 1;
                while (po2 < udLength && ErddapString2.isDigit(udunits.charAt(po2))) {
                    po2++;
                }

                //decimal place + digit (not just .=multiplication)
                boolean hasDot = false;
                if (po2 < udLength - 1 && udunits.charAt(po2) == '.' && ErddapString2.isDigit(udunits.charAt(po2 + 1))) {
                    hasDot = true;
                    po2 += 2;
                    while (po2 < udLength && ErddapString2.isDigit(udunits.charAt(po2))) {
                        po2++;
                    }
                }

                //exponent?     e-  or e{digit}
                boolean hasE = false;
                if (po2 < udLength - 1 && Character.toLowerCase(udunits.charAt(po2)) == 'e' &&
                        (udunits.charAt(po2 + 1) == '-' || ErddapString2.isDigit(udunits.charAt(po2 + 1)))) {
                    hasE = true;
                    po2 += 2;
                    while (po2 < udLength && ErddapString2.isDigit(udunits.charAt(po2))) {
                        po2++;
                    }
                }
                String num = udunits.substring(po, po2);
                po = po2;

                //convert floating point to rational number
                if (hasDot || hasE) {
                    int rational[] = ErddapString2.toRational(ErddapString2.parseDouble(num));
                    if (rational[1] == Integer.MAX_VALUE) {
                        ucum.append(num); //ignore the trouble !!! ???
                    } else if (rational[1] == 0) //includes {0, 0}
                    {
                        ucum.append(rational[0]);
                    } else {
                        ucum.append(rational[0]).append(".10^").append(rational[1]);
                    }
                } else {
                    //just copy num
                    ucum.append(num);
                }

                continue;
            }

            //space or . or · (183) (multiplication)
            if (ch == ' ' || ch == '.' || ch == 183) {
                char lastUcum = ucum.length() == 0 ? '\u0000' : ucum.charAt(ucum.length() - 1);
                if (lastUcum == '/' || lastUcum == '.') {
                    //if last token was / or .,  do nothing
                } else {
                    ucum.append('.');
                }
                po++;
                continue;
            }

            // *  (multiplication * or exponent **)
            if (ch == '*') {
                po++;
                if (po < udLength && udunits.charAt(po) == '*') {
                    ucum.append('^');  // exponent: ** -> ^
                    po++;
                } else {
                    char lastUcum = ucum.length() == 0 ? '\u0000' : ucum.charAt(ucum.length() - 1);
                    if (lastUcum == '/' || lastUcum == '.') {
                        //if last token was / or .,  do nothing
                    } else {
                        ucum.append('.');
                    }
                }
                continue;
            }

            // /
            if (ch == '/') {
                po++;
                char lastUcum = ucum.length() == 0 ? '\u0000' : ucum.charAt(ucum.length() - 1);
                if (lastUcum == '/') {
                    ucum.setCharAt(ucum.length() - 1, '.'); //  2 '/' cancel out
                } else if (lastUcum == '.') {
                    ucum.setCharAt(ucum.length() - 1, '/'); //  '/' replaces '.'
                } else {
                    ucum.append('/');
                }

                continue;
            }

            // "
            if (ch == '\"') {
                po++;
                ucum.append("''");
                continue;
            }

            //otherwise, punctuation.   copy it
            ucum.append(ch);
            po++;
        }

        return ucum.toString();
    }

    private static boolean isUdunitsLetter(char ch) {
        return ErddapString2.isLetter(ch) || ch == 'µ' || ch == '°';
    }

    /**
     * This converts one udunits term (perhaps with metric prefix(es)) to the corresponding ucum string.
     * If udunits is just metric prefix(es), this returns the prefix acronym(s) with "{count}" as suffix
     * (e.g., dkilo returns dk{count}).
     * If this can't completely convert udunits, it returns the original udunits
     * (e.g., kiloBobs remains kiloBobs  (to avoid 'exact' becoming 'ect' ).
     */
    private static String oneUdunitsToUcum(String udunits) {
        //repeatedly pull off start of udunits and build ucum, till done
        String oldUdunits = udunits;
        StringBuilder ucum = new StringBuilder();
        MAIN:
        while (true) {
            //try to find udunits in hashMap
            String tUcum = udHashMap.get(udunits);
            if (tUcum != null) {
                //success! done!
                ucum.append(tUcum);
                return ucum.toString();
            }

            //try to separate out a metricName prefix (e.g., "kilo")
            for (int p = 0; p < nMetric; p++) {
                if (udunits.startsWith(metricName[p])) {
                    udunits = udunits.substring(metricName[p].length());
                    ucum.append(metricAcronym[p]);
                    if (udunits.length() == 0) {
                        ucum.append("{count}");
                        return ucum.toString();
                    }
                    continue MAIN;
                }
            }

            //try to separate out a metricAcronym prefix (e.g., "k")
            for (int p = 0; p < nMetric; p++) {
                if (udunits.startsWith(metricAcronym[p])) {
                    udunits = udunits.substring(metricAcronym[p].length());
                    ucum.append(metricAcronym[p]);
                    if (udunits.length() == 0) {
                        ucum.append("{count}");
                        return ucum.toString();
                    }
                    continue MAIN;
                }
            }

            return oldUdunits;
        }
    }

    /**
     * This converts UCUM to UDUnits.
     *
UDUnits: http://www.unidata.ucar.edu/software/udunits/udunits-1/etc/udunits.dat
     * http://www.unidata.ucar.edu/software/udunits/udunits-2/udunits2.html
     *
UCUM: http://unitsofmeasure.org/ucum.html
     *

     *
UCUM tends to be short, canonical-only, and strict.
     * Many UCUM units are the same in UDUnits.
     *

     *
UDUnits supports lots of aliases (short and long)
     * and plurals (usually by adding 's').
     * This tries to convert UCUM to a short, common UDUNIT units.
     *

     *
Problems:
     *

     *
 UCUM has only "deg", no concept of degree_east|north|true|true.
     *

     *

     *
Notes:
     *

     *
This method is a strictly case sensitive.
     *
For "10 to the", UCUM allows 10* or 10^. This method uses 10^.
     *
{ntu} becomes NTU.
     *
{psu} becomes PSU.
     *

     *

     * return the UCUM converted to UDUNITS.
     * null returns null. "" returns "".
     */
    public static String ucumToUdunits(String ucum) {
        if (ucum == null) {
            return null;
        }

        StringBuilder udunits = new StringBuilder();
        int ucLength = ucum.length();
        if (ucLength == 0) {
            return "";
        }

        //is it a time point?  e.g., s{since 1970-01-01T00:00:00T}
        if (ucum.charAt(ucLength - 1) == '}' &&  //quick reject
                ucum.indexOf('}') == ucLength - 1) { //reasonably quick reject
            int sincePo = ucum.indexOf("{since ");
            if (sincePo > 0) {
                //is first part an atomic ucum unit?
                String tUdunits = ucHashMap.get(ucum.substring(0, sincePo));
                if (tUdunits != null) {
                    return tUdunits + " " + ucum.substring(sincePo + 1, ucLength - 1);
                }
            }
        }

        //parse ucum and build udunits, till done
        int po = 0;  //po is next position to be read
        while (po < ucLength) {
            char ch = ucum.charAt(po);

            //letter
            if (isUcumLetter(ch)) {     //includes [, ], {, }, 'µ' and "'"
                //find contiguous letters|_|digit (no '-')
                int po2 = po + 1;
                while (po2 < ucLength &&
                        (isUcumLetter(ucum.charAt(po2)) || ucum.charAt(po2) == '_' ||
                                ErddapString2.isDigit(ucum.charAt(po2)))) {
                    po2++;
                }
                String tUcum = ucum.substring(po, po2);
                po = po2;

                //some ucum have internal digits, but none end in digits
                //if it ends in digits, treat as exponent
                //find contiguous digits at end
                int firstDigit = tUcum.length();
                while (firstDigit >= 1 && ErddapString2.isDigit(tUcum.charAt(firstDigit - 1))) {
                    firstDigit--;
                }
                String exponent = tUcum.substring(firstDigit);
                tUcum = tUcum.substring(0, firstDigit);
                String tUdunits = oneUcumToUdunits(tUcum);

                //deal with PER -> /
                if (tUdunits.equals("/")) {
                    char lastUdunits = udunits.length() == 0 ? '\u0000' : udunits.charAt(udunits.length() - 1);
                    if (lastUdunits == '/') {
                        udunits.setCharAt(udunits.length() - 1, '.'); //2 '/' cancel out
                    } else if (lastUdunits == '.') {
                        udunits.setCharAt(udunits.length() - 1, '/'); //  '/' replaces '.'
                    } else {
                        udunits.append('/');
                    }

                } else {
                    udunits.append(tUdunits);
                }

                //add the exponent
                udunits.append(exponent);
                //catch -exponent as a number below

                continue;
            }

            //number
            if (ch == '-' || ErddapString2.isDigit(ch)) {
                //find contiguous digits
                int po2 = po + 1;
                while (po2 < ucLength && ErddapString2.isDigit(ucum.charAt(po2))) {
                    po2++;
                }

                // ^-  or ^{digit}
                if (po2 < ucLength - 1 && Character.toLowerCase(ucum.charAt(po2)) == '^' &&
                        (ucum.charAt(po2 + 1) == '-' || ErddapString2.isDigit(ucum.charAt(po2 + 1)))) {
                    po2 += 2;
                    while (po2 < ucLength && ErddapString2.isDigit(ucum.charAt(po2))) {
                        po2++;
                    }
                }
                String num = ucum.substring(po, po2);
                po = po2;
                udunits.append(num);
                continue;
            }

            // .
            if (ch == '.') {
                po++;
                udunits.append(' '); // ' ' is more common than '.' in udunits
                continue;
            }

            // *
            if (ch == '*') {
                po++;
                udunits.append('^');
                continue;
            }

            // '  ''
            if (ch == '\'') {
                po++;
                if (po < ucLength && ucum.charAt(po) == '\'') {
                    udunits.append("arc_second");
                    po++;
                } else {
                    udunits.append("arc_minute");
                }
                continue;
            }
            //otherwise, punctuation.   copy it
            //  / (division), " doesn't occur,
            udunits.append(ch);
            po++;
        }

        return udunits.toString();
    }

    private static boolean isUcumLetter(char ch) {
        return ErddapString2.isLetter(ch) ||
                ch == '[' || ch == ']' ||
                ch == '{' || ch == '}' ||
                ch == 'µ' || ch == '\'';
    }

    /**
     * This converts one ucum term (perhaps with metric prefix(es))
     * (   to the corresponding udunits string.
     * If ucum is just metric prefix(es), this returns the metric prefix
     * acronym(s) with "{count}" as suffix (e.g., dkilo returns dk{count}).
     * If this can't completely convert ucum, it returns the original ucum
     * (e.g., kiloBobs remains kiloBobs  (to avoid 'exact' becoming 'ect' ).
     */
    private static String oneUcumToUdunits(String ucum) {
        //repeatedly pull off start of ucum and build udunits, till done
        String oldUcum = ucum;
        StringBuilder udunits = new StringBuilder();
        MAIN:
        while (true) {
            //try to find ucum in hashMap
            String tUdunits = ucHashMap.get(ucum);
            if (tUdunits != null) {
                //success! done!
                udunits.append(tUdunits);
                return udunits.toString();
            }

            //try to separate out a metricAcronym prefix (e.g., "k")
            for (int p = 0; p < nMetric; p++) {
                if (ucum.startsWith(metricAcronym[p])) {
                    ucum = ucum.substring(metricAcronym[p].length());
                    udunits.append(metricAcronym[p]);
                    if (ucum.length() == 0) {
                        udunits.append("{count}");
                        return udunits.toString();
                    }
                    continue MAIN;
                }
            }

            //try to separate out a twoAcronym prefix (e.g., "Ki")
            for (int p = 0; p < nTwo; p++) {
                if (ucum.startsWith(twoAcronym[p])) {
                    ucum = ucum.substring(twoAcronym[p].length());
                    char udch = udunits.length() > 0 ? udunits.charAt(udunits.length() - 1) : '\u0000';
                    if (udch != '\u0000' && udch != '.' && udch != '/') {
                        udunits.append('.');
                    }
                    if (ucum.length() == 0) {
                        udunits.append("{count}");
                        return udunits.toString();
                    }
                    udunits.append(twoValue[p]).append(".");
                    continue MAIN;
                }
            }

            //ends in comment?  try to just convert the beginning
            int po1 = oldUcum.lastIndexOf('{');
            if (po1 > 0 && oldUcum.endsWith("}")) {
                return oneUcumToUdunits(oldUcum.substring(0, po1)) + oldUcum.substring(po1);
            }

            return oldUcum;
        }
    }

    /**
     * Reads the contents of {@code inputStream} into a HashMap. Each key-value pair in the input will result in an
     * entry in the map.
     *

     * The specified stream remains open after this method returns.
     *
     * @param inputStream a stream with line-based, key-value pairs.
     * @param charset     the name of a supported {@link java.nio.charset.Charset charset}.
     * @return a HashMap initialized from the stream.
     * @throws java.io.IOException if an I/O error occurs
     */
    public static HashMap<String, String> getHashMapStringString(InputStream inputStream, String charset)
            throws IOException {
        HashMap<String, String> ht = new HashMap<>();
        ErddapStringArray sa = ErddapStringArray.fromInputStream(inputStream, charset);
        int n = sa.size();
        int i = 0;
        while (i < n) {
            String s = sa.get(i++);
            if (s.startsWith("#")) {
                continue;
            }
            while (i < n && s.endsWith("\\")) {
                s = s.substring(0, s.length() - 1) + sa.get(i++);
            }
            int po = s.indexOf('=');
            if (po < 0) {
                continue;
            }
            //new String: so not linked to big source file's text
            ht.put(s.substring(0, po).trim(), s.substring(po + 1).trim());
        }
        return ht;
    }
}


/*
 * Copyright (c) 1998-2018 John Caron and University Corporation for Atmospheric Research/Unidata
 * See LICENSE for license information.
 */
package ucar.nc2.wmo;

import java.util.ArrayList;
import java.util.Formatter;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.annotation.Nullable;
import org.jdom2.Element;
import org.jdom2.JDOMException;
import org.jdom2.input.SAXBuilder;
import ucar.unidata.util.StringUtil2;

import java.io.IOException;
import java.io.InputStream;

/**
 * Read and process WMO common code and flag tables.
 *
 * <pre>
 *   see manual on codes I.2 —Co Tab — 1
  COMMON CODE TABLE C–1:   Identification of originating/generating centre GRIB-1, BUFR-3
  COMMON CODE TABLE C–2:   Radiosonde/sounding system used
  COMMON CODE TABLE C–3:   Instrument make and type for water temperature profile measurement with fall rate equation coefficients
  COMMON CODE TABLE C–4:   Water temperature profile recorder types
  COMMON CODE TABLE C–5:   Satellite identifier
  COMMON CODE TABLE C–6:   List of units for TDCFs
  COMMON CODE TABLE C–7:   Tracking techniques/status of system used
  COMMON CODE TABLE C–8:   Satellite instruments
  COMMON CODE TABLE C–11:  Originating/generating centres GRIB-2, BUFR-4
  COMMON CODE TABLE C–12:  Sub-centres of originating centres defined by entries in Common code tables C–1 or C–11
  COMMON CODE TABLE C–13:  Data sub-categories of categories defined by entries in BUFR Table A
  COMMON CODE TABLE C–14:  Atmospheric chemical or physical constituent type

 * </pre>
 * @author caron
 * @since 3/29/11
 */
public class CommonCodeTable implements Comparable<CommonCodeTable> {
  private static final org.slf4j.Logger logger = org.slf4j.LoggerFactory.getLogger(CommonCodeTable.class);
  private static final Map<Integer, CommonCodeTable> tableMap = new HashMap<>();
  private static final String version = "_20181107_en";

  //////////////////////////////////////////////////////////////////////////
/*
<Common_C13_20130508_en>
<No>35</No>
<CodeFigure_DataCategories>2</CodeFigure_DataCategories>
<Name_DataCategories_en>Vertical soundings (other than satellite)</Name_DataCategories_en>
<CodeFigure_InternationalDataSubcategories>21</CodeFigure_InternationalDataSubcategories>
<Name_InternationalDataSubcategories_en>Profiles of atmospheric constituents concentrations</Name_InternationalDataSubcategories_en>
<Status>Operational</Status>
</Common_C13_20130508_en>

<Common_C01_20141105_en>
<No>6</No>
<CodeFigureForF1F2>04</CodeFigureForF1F2>
<CodeFigureForF3F3F3>004</CodeFigureForF3F3F3>
<Octet5GRIB1_Octet6BUFR3>4</Octet5GRIB1_Octet6BUFR3>
<OriginatingGeneratingCentres_en>Moscow</OriginatingGeneratingCentres_en>
<Status>Operational</Status>
</Common_C01_20141105_en>

   */
  public enum Table {                              // code                code2                  value
    C1("Centers-GRIB1,BUFR3",    1, 1, new String[]{"CodeFigureForF1F2", "CodeFigureForF3F3F3", "OriginatingGeneratingCentres_en"}),
    // C2("Radiosondes",            2, 1, new String[]{"CodeFigureForBUFR", null, "RadiosondeSoundingSystemUsed_en"}),
    C3("Water temperature profile instrument", 3, 1, new String[]{"CodeFigureForBUFR", null, "InstrumentMakeAndType_en"}),
    C4("Water temperature profile recorder",   4, 1, new String[]{"CodeFigureForBUFR", null, "Meaning_en"}),
    C5("Satellite identifier",   5, 1, new String[]{"CodeFigureForBUFR", null, "SatelliteName_en"}),
    C7("Satellite tracking",     7, 1, new String[]{"CodeFigureForBUFR", null, "TrackingTechniquesStatusOfSystemUsed_en"}),
    C8("Satellite instruments",  8, 1, new String[]{"Code", null, "InstrumentLongName_en", "InstrumentShortName_en"}),
    C11("Centers-GRIB2,BUFR4",    11,1, new String[]{"GRIB2_BUFR4", null, "OriginatingGeneratingCentre_en"}),
                                                    // code                            value                       code2                  value2
    C12("Subcenters",             12,2, new String[]{"CodeFigure_OriginatingCentres", "Name_OriginatingCentres_en", "CodeFigure_SubCentres", "Name_SubCentres_en"}),
    C13("Data sub-categories",    13,2, new String[]{"CodeFigure_DataCategories", "Name_DataCategories_en", "CodeFigure_InternationalDataSubcategories", "Name_InternationalDataSubcategories_en"}),
    C14("Atmospheric chemical or physical constituent type",14,2, new String[]{"CodeFigure", "ChemicalFormula", null, "Meaning_en"}),
    ;

    String name;
    String[] elems; // type 1: 0 = code, 1 = code2 (may be null), 2 = value
                    // type 2: 0 = code1, 1 = comment , 2 = code3 (may be null), 3 = value
    int num, type; // type 1 = 1 code, type 2 = 2 codes
    String num0;
    Table(String name, int num, int type, String[] elems) {
      this.name = name;
      this.num = num;
      this.type = type;
      this.elems = elems;

      Formatter f = new Formatter();
      f.format("%02d", num);
      num0 = f.toString();
    }

    public String getResourceName() {
      return "/resources/wmo/Common_C" + this.num0 + version +".xml";
    }

    public String getRootElemName() {
      return "Common_C" + this.num0 + version;
    }

    public int getTableNo() {
      return num;
    }

    public int getTableType() {
      return type;
    }

    String[] getElemNames() {
      return elems;
    }

    public String getName() {
      return name;
    }
  }

  /**
   * Center name, from table C-1 or C-11
   *
   * @param center_id center id
   * @param edition grib edition
   * @return center name, or "unknown"
   */
  public static String getCenterName(int center_id, int edition) {
    String result = (edition == 1) ? getTableValue(1, center_id) : getTableValue(11, center_id);
    if (result != null) return result;
    if (center_id == 0) return "WMO standard table";
    return "Unknown center=" + center_id;
  }

  /**
   * Center name, from table C-1 or C-11
   *
   * @param center_id center id
   * @param edition bufr edition
   * @return center name, or "unknown"
   */
  public static String getCenterNameBufr(int center_id, int edition) {
    String result = (edition < 4) ? getTableValue(1, center_id) : getTableValue(11, center_id);
    if (result != null) return result;
    if (center_id == 0) return "WMO standard table";
    return "Unknown center=" + center_id;
  }

  /**
   * Subcenter name, from table C-12
   *
   * @param center_id    center id
   * @param subcenter_id subcenter id
   * @return subcenter name, or null if not found
   */
  @Nullable
  static public String getSubCenterName(int center_id, int subcenter_id) {
    return getTableValue(12, center_id, subcenter_id);
  }

  /**
   * data subcategory name, from table C-13
   *
   * @param cat    data category
   * @param subcat data subcategory
   * @return subcategory name, or null if not found
   */
  @Nullable
  static public String getDataSubcategoy(int cat, int subcat) {
    return getTableValue(13, cat, subcat);
  }

  public static CommonCodeTable getTable(int tableNo) {
    CommonCodeTable cct = tableMap.get(tableNo);

     if (cct == null) {
       Table want = null;
       for (Table t : Table.values())
         if (t.num == tableNo) {
           want = t;
           break;
         }
       if (want == null)
         throw new IllegalStateException("Unknown wmo common code table number= "+tableNo);

       try {
         cct = readCommonCodes(want);
         tableMap.put(tableNo, cct);
       } catch (IOException e) {
         throw new IllegalStateException("Cant open wmo common code table "+want);
       }
     }

    return cct;
  }

  @Nullable
  public static String getTableValue(int tableNo, int code) {
    CommonCodeTable cct = getTable(tableNo);
    if (cct == null) {
      logger.error("WMO common table {} is not implemented", tableNo);
      return null;
    }
    TableEntry te =  cct.get(code);
    if (te == null) return null;
    return te.value;
  }

  @Nullable
  public static String getTableValue(int tableNo, int code, int code2) {
    CommonCodeTable cct = getTable(tableNo);
    TableEntry te =  cct.get(code, code2);
    if (te == null) return null;
    return te.value;
  }

  static private CommonCodeTable readCommonCodes(Table version) throws IOException {
    InputStream ios = null;
    try {
      Class c = CommonCodeTable.class;
      ios = c.getResourceAsStream(version.getResourceName());
      if (ios == null) {
        throw new IllegalStateException("CommonCodeTable cannot open " + version.getResourceName());
      }

      org.jdom2.Document doc;
      try {
        SAXBuilder builder = new SAXBuilder();
        doc = builder.build(ios);
      } catch (JDOMException e) {
        throw new IOException(e.getMessage());
      }
      Element root = doc.getRootElement();
      String previousValue = null;
      CommonCodeTable ct = new CommonCodeTable(version.name(), version.getTableType());
      String[] elems = version.getElemNames();

      List<Element> featList = root.getChildren(version.getRootElemName());
      for (Element elem : featList) {
        if (version.type == 1) {
          String line = elem.getChildTextNormalize("No");
          String code = elem.getChildTextNormalize(elems[0]);
          String code2 = (elems[1] != null) ? elem.getChildTextNormalize(elems[1]) : null;
          String value = elem.getChildTextNormalize(elems[2]);
          //if ((value == null || value.length() == 0) && elems[3] != null) value = elem.getChildTextNormalize(elems[3]);  WTF ?
          Element statusElem = elem.getChild("Status");
          String status = (statusElem == null) ? null : statusElem.getTextNormalize();
          if (value != null && value.equals(")")) value = previousValue;
          ct.add(line, code, code2, value, status);
          previousValue = value;

        } else {
          String line = elem.getChildTextNormalize("No");
          String code = elem.getChildTextNormalize(elems[0]);
          String value = elem.getChildTextNormalize(elems[1]);
          String code2 = (elems[2] == null) ? null : elem.getChildTextNormalize(elems[2]);
          String value2 = elem.getChildTextNormalize(elems[3]);
          Element statusElem = elem.getChild("Status");
          String status = (statusElem == null) ? null : statusElem.getTextNormalize();
          ct.add(line, code, value, code2, value2, status);
        }
      }

      ios.close();
      return ct;

    } finally {
      if (ios != null)
        ios.close();
    }
  }

  ////////////////////////////////////////////////////////////////////////////////////

  public final String tableName;
  public final int type;
  public final List<TableEntry> entries = new ArrayList<>();
  private Map<Integer, String> map = null;

  CommonCodeTable(String name, int type) {
    this.tableName = name;
    this.type = type;
  }

  void add(String line, String code, String code2, String value, String status) {
    entries.add(new TableEntry(line, code, code2, value, status));
  }

  void add(String line, String code, String value, String code2, String value2, String status) {
    entries.add(new TableEntry(line, code, value, code2, value2, status));
  }

  // look replace with hash or array
  @Nullable
  TableEntry get(int code) {
    for (TableEntry p : entries) {
      if (p.code == code) return p;
    }
    return null;
  }

  @Nullable
  TableEntry get(int code, int code2) {
    for (TableEntry p : entries) {
      if ((p.code == code) && (p.code2 == code2))return p;
    }
    return null;
  }

  public String getTableName() {
    return tableName;
  }

  public Map<Integer, String> getMap() {
    if (map == null) {
      map = new HashMap<>(entries.size() * 2);
      for (TableEntry p : entries) {
        map.put(p.code, p.value);
      }
    }
    return map;
  }

  @Override
  public int compareTo(CommonCodeTable o) {
    return tableName.compareTo(o.tableName);
  }

  String state() {
    Set<Integer> set = new HashSet<>();
    int max = 0;
    int dups = 0;
    for (TableEntry entry : entries) {
      if (entry.comment != null) continue;
      if (entry.code > max) max = entry.code;
      if (set.contains(entry.code)) dups++;
      else set.add(entry.code);
    }
    return "density= "+entries.size() + "/" + max+"; dups= "+dups;
  }

  public class TableEntry implements Comparable<TableEntry> {
    public int line, code, code2;
    public String value, status, comment;

    int parse(String s) {
      if (s == null) return -1;
      try {
        return Integer.parseInt(s);
      } catch (NumberFormatException e2) {
        return -2;
      }
    }

    TableEntry(String line, String code1, String code2, String value, String status) {
      this.line = Integer.parseInt(line);
      this.code = parse(code1);
      if (this.code < 0)
        this.code = parse(code2);
      if (this.code < 1)
        comment = value;
      else {
        this.value = filter(value);
        this.status = status;
      }
    }

    TableEntry(String line, String code1, String value1, String code2, String value2, String status) {
      this.line = Integer.parseInt(line);
      this.code = parse(code1);
      this.code2 = parse(code2);
      this.value = filter(value2);
      this.comment = value1;
      this.status = status;
    }

    @Override
    public int compareTo(TableEntry o) {
      if (type == 1)
        return Integer.compare(code, o.code);
      else {
        int diff = Integer.compare(code, o.code);
        return (diff == 0) ?  Integer.compare(code2, o.code2) : diff;
      }
    }

    @Override
    public String toString() {
      if (comment != null)
        return "TableEntry{" +
              ", line=" + line +
              ", comment=" + comment +
                '}';

      else return "TableEntry{" +
              ", line=" + line +
              ", code=" + code +
              ", value='" + value + '\'' +
              ", status='" + status + '\'' +
              '}';
    }
  }

  private static final char badDash = 173;
  private String filter(String s) {
    if (s == null) return "";
    return StringUtil2.replace(s, badDash, "-");
  }

  public static void main(String arg[]) throws IOException {
    CommonCodeTable ct = readCommonCodes(Table.C1);
    for (TableEntry entry : ct.entries)
      System.out.printf("%s%n", entry);
    System.out.printf("%n%s%n", ct.state());
  }
}


/*
 * Copyright (c) 1998-2018 University Corporation for Atmospheric Research/Unidata
 * See LICENSE for license information.
 */
// $Id: SimpleCatalogBuilder.java 63 2006-07-12 21:50:51Z edavis $
package thredds.cataloggen;

import thredds.crawlabledataset.CrawlableDataset;
import thredds.catalog.*;

import java.io.IOException;

import org.jdom2.Document;

/**
 * Provide a simple interface for building single-level catalogs at any level
 * of the specified collection.
 *
 * @author edavis
 * @since Nov 30, 2005 3:17:23 PM
 */
public class SimpleCatalogBuilder implements CatalogBuilder
{

  private String collectionPath;

  private CrawlableDataset collectionCrDs;
  private InvService service;

  /**
   * Construct a SimpleCatalogBuilder for a given collection.
   *
   * @param collectionPath an path for the collection to be cataloged (used as the base of the generated URL, can be an empty string or null). If null, the collectionLevel final path segment is used.
   * @param collectionCrDs the root level of the collection to be cataloged.
   * @param serviceName specifies the name of the service to be used in the resulting catalogs.
   * @param serviceTypeName specifies the THREDDS service type, e.g., "OPENDAP"
   * @param serviceURL the base URL to the service
   */
  public SimpleCatalogBuilder( String collectionPath, CrawlableDataset collectionCrDs, String serviceName, String serviceTypeName, String serviceURL )
  {

    this.collectionPath = ( collectionPath == null
                            ? collectionCrDs.getName()
                            : collectionPath );
    this.collectionCrDs = collectionCrDs;

    service = new InvService( serviceName, serviceTypeName, serviceURL, null, null );
  }

  public CrawlableDataset requestCrawlableDataset( String path )
  {
    return CatalogBuilderHelper.verifyDescendantDataset( collectionCrDs, path, null );
  }

  public InvCatalogImpl generateCatalog( CrawlableDataset catalogCrDs ) throws IOException
  {
    CollectionLevelScanner scanner = new CollectionLevelScanner( collectionPath, collectionCrDs, catalogCrDs, null, null, service );

    scanner.scan();
    return scanner.generateCatalog();
  }

  /** Not supported by SimpleCatalogBuilder. */
  public InvCatalogImpl generateProxyDsResolverCatalog( CrawlableDataset catalogCrDs, ProxyDatasetHandler pdh ) throws IOException
  {
    throw new java.lang.UnsupportedOperationException( "This method not supported by SimpleCatalogBuilder.");
  }

  public Document generateCatalogAsDocument( CrawlableDataset catalogCrDs ) throws IOException
  {
    return CatalogBuilderHelper.convertCatalogToDocument( generateCatalog( catalogCrDs ) );
  }

  public String generateCatalogAsString( CrawlableDataset catalogCrDs ) throws IOException
  {
    return CatalogBuilderHelper.convertCatalogToString( generateCatalog( catalogCrDs ) );
  }
}


/*
 * Copyright (c) 1998-2018 University Corporation for Atmospheric Research/Unidata
 * See LICENSE for license information.
 */

package ucar.nc2.thredds;

import ucar.nc2.Attribute;
import ucar.nc2.constants.FeatureType;
import thredds.catalog.ThreddsMetadata;
import thredds.catalog.InvDatasetImpl;
import thredds.catalog.DataFormatType;
import ucar.nc2.ft.FeatureDatasetPoint;
import ucar.nc2.time.CalendarDateRange;
import ucar.nc2.units.DateRange;

import java.io.IOException;
import java.util.Date;
import java.util.List;

import ucar.nc2.dataset.CoordinateAxis1D;
import ucar.nc2.dataset.CoordinateAxis;
import ucar.nc2.dataset.CoordinateAxis1DTime;
import ucar.nc2.dt.*;
import ucar.nc2.VariableSimpleIF;
import ucar.nc2.units.DateUnit;
import ucar.unidata.geoloc.LatLonRect;

/**
 * Extract THREDDS metadata from the underlying CDM dataset.
 *
 * @author caron
 */
public class MetadataExtractor {
  static private org.slf4j.Logger logger = org.slf4j.LoggerFactory.getLogger(MetadataExtractor.class);

  /**
   * Extract the lat/lon/alt bounding boxes from the dataset.
   * @param threddsDataset open this dataset
   * @return ThreddsMetadata.GeospatialCoverage, or null if unable.
   * @throws IOException on read error
   */
  static public ThreddsMetadata.GeospatialCoverage extractGeospatial(InvDatasetImpl threddsDataset) throws IOException {
    ThreddsDataFactory.Result result = null;

    try {
      result = new ThreddsDataFactory().openFeatureDataset(threddsDataset, null);
      if (result.fatalError) {
        System.out.println(" openDatatype errs=" + result.errLog);
        return null;
      }

      if (result.featureType == FeatureType.GRID) {
        System.out.println(" GRID=" + result.location);
        GridDataset gridDataset = (GridDataset) result.featureDataset;
        return extractGeospatial( gridDataset);

      } else if (result.featureType == FeatureType.POINT) {
        PointObsDataset pobsDataset = (PointObsDataset) result.featureDataset;
        LatLonRect llbb = pobsDataset.getBoundingBox();
        if (null != llbb) {
          ThreddsMetadata.GeospatialCoverage gc = new ThreddsMetadata.GeospatialCoverage();
          gc.setBoundingBox(llbb);
          return gc;
        }
      } else if (result.featureType == FeatureType.STATION) {
        StationObsDataset sobsDataset = (StationObsDataset) result.featureDataset;
        LatLonRect llbb = sobsDataset.getBoundingBox();
        if (null != llbb) {
          ThreddsMetadata.GeospatialCoverage gc = new ThreddsMetadata.GeospatialCoverage();
          gc.setBoundingBox(llbb);
          return gc;
        }
      }

    } finally {
      try {
        if ((result != null) && (result.featureDataset != null))
          result.featureDataset.close();
      } catch (IOException ioe) {
        logger.error("Closing dataset "+result.featureDataset, ioe);
      }
    }

    return null;
  }

  static public ThreddsMetadata.GeospatialCoverage extractGeospatial(GridDataset gridDataset) {
    ThreddsMetadata.GeospatialCoverage gc = new ThreddsMetadata.GeospatialCoverage();
    LatLonRect llbb = null;
    CoordinateAxis1D vaxis = null;

    for(GridDataset.Gridset gridset : gridDataset.getGridsets()) {
      GridCoordSystem gsys = gridset.getGeoCoordSystem();
      if (llbb == null)
        llbb = gsys.getLatLonBoundingBox();

      CoordinateAxis1D vaxis2 = gsys.getVerticalAxis();
      if (vaxis == null)
        vaxis = vaxis2;
      else if ((vaxis2 != null) && (vaxis2.getSize() > vaxis.getSize()))
        vaxis = vaxis2;
    }

    if (llbb != null)
      gc.setBoundingBox(llbb);
    if (vaxis != null)
      gc.setVertical(vaxis);
    return gc;
  }

  /**
   * Extract a list of data variables (and their canonical names if possible) from the dataset.
   * @param threddsDataset open this dataset
   * @return ThreddsMetadata.Variables, or null if unable.
   * @throws IOException on read error
   */
  static public ThreddsMetadata.Variables extractVariables(InvDatasetImpl threddsDataset) throws IOException {
    ThreddsDataFactory.Result result = null;

    try {
      result = new ThreddsDataFactory().openFeatureDataset(threddsDataset, null);
      if (result.fatalError) {
        System.out.println(" openDatatype errs=" + result.errLog);
        return null;
      }

      if (result.featureType == FeatureType.GRID) {
        // System.out.println(" extractVariables GRID=" + result.location);
        GridDataset gridDataset = (GridDataset) result.featureDataset;
        return extractVariables(threddsDataset, gridDataset);

      } else if ((result.featureType == FeatureType.STATION) || (result.featureType == FeatureType.POINT)) {
        PointObsDataset pobsDataset = (PointObsDataset) result.featureDataset;
        ThreddsMetadata.Variables vars = new ThreddsMetadata.Variables("CF-1.0");
        for (VariableSimpleIF vs  : pobsDataset.getDataVariables()) {
          ThreddsMetadata.Variable v = new ThreddsMetadata.Variable();
          vars.addVariable( v);

          v.setName( vs.getShortName());
          v.setDescription( vs.getDescription());
          v.setUnits( vs.getUnitsString());

          ucar.nc2.Attribute att = vs.findAttributeIgnoreCase("standard_name");
          if (att != null)
            v.setVocabularyName(att.getStringValue());
        }
        vars.sort();
        return vars;
      }

    } finally {
      try {
        if ((result != null) && (result.featureDataset != null))
          result.featureDataset.close();
      } catch (IOException ioe) {
        logger.error("Closing dataset "+result.featureDataset, ioe);
      }
    }

    return null;
  }

  static public ThreddsMetadata.Variables extractVariables(InvDatasetImpl threddsDataset, GridDataset gridDataset) {
    return extractVariables( threddsDataset.getDataFormatType(), gridDataset);
  }

  static public ThreddsMetadata.Variables extractVariables(thredds.catalog.DataFormatType fileFormat, GridDataset gridDataset) {
    if ((fileFormat != null) && (fileFormat.equals(DataFormatType.GRIB1) || fileFormat.equals(DataFormatType.GRIB2))) {
      ThreddsMetadata.Variables vars = new ThreddsMetadata.Variables(fileFormat.toString());
      for (GridDatatype grid : gridDataset.getGrids()) {
        ThreddsMetadata.Variable v = new ThreddsMetadata.Variable();
        v.setName(grid.getFullName());
        v.setDescription(grid.getDescription());
        v.setUnits(grid.getUnitsString());

        Attribute att = grid.findAttributeIgnoreCase("Grib_Variable_Id");
        if (att != null) {
          v.setVocabularyId(att.getStringValue());
          v.setVocabularyName(att.getStringValue());
        } else {
          att = grid.findAttributeIgnoreCase("Grib_Parameter");
          v.setVocabularyId(att);
        }
        vars.addVariable(v);
      }
      vars.sort();
      return vars;

    } else { // GRID but not GRIB
      ThreddsMetadata.Variables vars = new ThreddsMetadata.Variables("CF-1.0");
      for (GridDatatype grid : gridDataset.getGrids()) {
        ThreddsMetadata.Variable v = new ThreddsMetadata.Variable();
        vars.addVariable(v);

        v.setName(grid.getFullName());
        v.setDescription(grid.getDescription());
        v.setUnits(grid.getUnitsString());

        ucar.nc2.Attribute att = grid.findAttributeIgnoreCase("standard_name");
        if (att != null)
          v.setVocabularyName(att.getStringValue());
      }
      vars.sort();
      return vars;
    }

  }

  static public CalendarDateRange extractCalendarDateRange(GridDataset gridDataset) {
    CalendarDateRange maxDateRange = null;

    for (GridDataset.Gridset gridset : gridDataset.getGridsets()) {
      GridCoordSystem gsys = gridset.getGeoCoordSystem();
      CalendarDateRange dateRange;

      CoordinateAxis1DTime time1D = gsys.getTimeAxis1D();
      if (time1D != null) {
        dateRange = time1D.getCalendarDateRange();
      } else {
        CoordinateAxis time = gsys.getTimeAxis();
        if (time == null)
          continue;

        try {
          DateUnit du = new DateUnit( time.getUnitsString());
          Date minDate = du.makeDate(time.getMinValue());
          Date maxDate = du.makeDate(time.getMaxValue());
          dateRange = CalendarDateRange.of( minDate, maxDate);
        } catch (Exception e) {
          logger.warn("Illegal Date Unit "+time.getUnitsString());
          continue;
        }
      }

      if (maxDateRange == null)
        maxDateRange = dateRange;
      else
        maxDateRange = maxDateRange.extend( dateRange);
    }

    return maxDateRange;
  }

  ///////////////////////////////////////////////////////////////////////////////
  static public ThreddsMetadata.Variables extractVariables(FeatureDatasetPoint fd) {
    ThreddsMetadata.Variables vars = new ThreddsMetadata.Variables("CF-1.5");
    List<VariableSimpleIF> dataVars =  fd.getDataVariables();
    if (dataVars == null)
      return vars;

    for (VariableSimpleIF v : dataVars) {
      ThreddsMetadata.Variable tv = new ThreddsMetadata.Variable();
      vars.addVariable(tv);

      tv.setName(v.getShortName());
      tv.setDescription(v.getDescription());
      tv.setUnits(v.getUnitsString());

      ucar.nc2.Attribute att = v.findAttributeIgnoreCase("standard_name");
      if (att != null)
         tv.setVocabularyName(att.getStringValue());
    }
    vars.sort();
    return vars;
  }

  static public ThreddsMetadata.GeospatialCoverage extractGeospatial(FeatureDatasetPoint fd) {
    LatLonRect llbb = fd.getBoundingBox();
    if (llbb != null) {
      ThreddsMetadata.GeospatialCoverage gc = new ThreddsMetadata.GeospatialCoverage();
      gc.setBoundingBox(llbb);
      return gc;
    }
    return null;
  }

  static public CalendarDateRange extractCalendarDateRange(FeatureDatasetPoint fd) {
    return fd.getCalendarDateRange();
  }

}


/*
 * Copyright (c) 1998-2018 John Caron and University Corporation for Atmospheric Research/Unidata
 * See LICENSE for license information.
 */
package ucar.nc2.time;

import javax.annotation.concurrent.Immutable;

import java.util.Date;
import java.util.Formatter;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * A Calendar Date Unit: "unit since date"
 *
 * <pre>
 * UNIT since [-]Y[Y[Y[Y]]]-MM-DD[(T| )hh[:mm[:ss[.sss*]]][ [+|-]hh[[:]mm]]]
 *
 * UNIT = "msecs" | "msec" |
          "seconds" | "second" | "secs" | "sec" | "s" |
          "minutes" | "minute" | "mins" | "min" |
          "hours" | "hour" | "hrs" | "hr" | "h" |
          "days" | "day" | "d"
 * </pre>
  *
 *  "months since base_date by calendar field"
 *  "years since base_date by calendar field"
 *
 *  bob simon's proposal:
 *
 *  "calendar_month since base_date"
 *  "calendar_year since base_date"
 *  * only integer values are allowed (?)
 *
 *  jon blowers comment:
In your view would the solution to add "by calendar field" to the existing udunits string be acceptable?  It’s
 backward-compatible with the current interpretation and adds clarification for the cases in which we *do* want to
 do calendar-field arithmetic (instead of adding fixed intervals).
There’s an alternative proposition, in which the new units of calendar_month and calendar_year are added, with the same semantic effect.
 (However, personally I like the "by calendar field" solution since it allows other fields to vary between calendars, e.g.
 because of leap-seconds.)
 *
 * @author caron
 * @since 3/18/11
 */
@Immutable
public class CalendarDateUnit {
  public static final CalendarDateUnit unixDateUnit = CalendarDateUnit.of(null, CalendarPeriod.Field.Second, CalendarDate.parseISOformat(null, "1970-01-01T00:00:00"));

  private static final String byCalendarString = "calendar ";
  //                                                  1                     2             3    4             5
  public static final String udunitPatternString = "(\\w*)\\s*since\\s*"+CalendarDateFormatter.isodatePatternString;
  //                                                                     "([\\+\\-\\d]+)([ Tt]([\\.\\:\\d]*)([ \\+\\-]\\S*)?z?)?$"; // public for testing
  private static final Pattern udunitPattern = Pattern.compile(udunitPatternString);

  /**
   * Create a CalendarDateUnit from a calendar name and a udunit string = "unit since calendarDate"
   * @param calendarName must match a calendar enum, or one of its aliases, see ucar.nc2.time.Calendar.get()
   * @param udunitString "unit since calendarDate"
   * @return CalendarDateUnit
   * @throws IllegalArgumentException if udunitString is not paresable
   */
  static public CalendarDateUnit of(String calendarName, String udunitString) {
    Calendar calt = Calendar.get(calendarName);
    if (calt == null) calt = Calendar.getDefault();
    return new CalendarDateUnit(calt, udunitString);
  }

  /**
   * Create a CalendarDateUnit from a calendar and a udunit string = "unit since calendarDate"
   * @param calt use this Calendar, or null for default calendar
   * @param udunitString "unit since calendarDate"
   * @return CalendarDateUnit
   * @throws IllegalArgumentException if udunitString is not paresable
   */
  static public CalendarDateUnit withCalendar(Calendar calt, String udunitString) {
    if (calt == null) calt = Calendar.getDefault();
    return new CalendarDateUnit(calt, udunitString);
  }

  /**
   * Create a CalendarDateUnit from a calendar, a CalendarPeriod.Field, and a base date
   * @param calt use this Calendar, or null for default calendar
   * @param periodField a CalendarPeriod.Field like Hour or second
   * @param baseDate "since baseDate"
   * @return CalendarDateUnit
   */
  static public CalendarDateUnit of(Calendar calt, CalendarPeriod.Field periodField, CalendarDate baseDate) {
    if (calt == null) calt = Calendar.getDefault();
    return new CalendarDateUnit(calt, periodField, baseDate);
  }

  ////////////////////////////////////////////////////////////////////////////////////////
  private final Calendar cal;
  // private final String unitString;
  private final CalendarPeriod period;
  private final CalendarPeriod.Field periodField;
  private final CalendarDate baseDate;
  private final boolean isCalendarField;

  private CalendarDateUnit(Calendar calt, String dateUnitString) {

    dateUnitString = dateUnitString.trim();
    // dateUnitString = dateUnitString.replaceAll("\\s+", " ");  LOOK think about should we allow this ??
    dateUnitString = dateUnitString.toLowerCase();

    isCalendarField =  dateUnitString.startsWith(byCalendarString);
    if (isCalendarField) {
      dateUnitString = dateUnitString.substring(byCalendarString.length()).trim();
    }

    Matcher m = udunitPattern.matcher(dateUnitString);
    if (!m.matches()) {
      //System.out.printf("'%s' does not match regexp '%s'%n", dateUnitString, udunitPatternString);
      throw new IllegalArgumentException(dateUnitString + " does not match " + udunitPatternString);
    }

    String unitString = m.group(1);
    period = CalendarPeriod.of(unitString);
    periodField = CalendarPeriod.fromUnitString(unitString);

    int pos = dateUnitString.indexOf("since");
    String iso = dateUnitString.substring(pos + 5);
    baseDate = CalendarDateFormatter.isoStringToCalendarDate(calt, iso);

    // calendar might change !!
    calt = baseDate.getCalendar();
    this.cal = calt;
  }

  private CalendarDateUnit(Calendar calt, CalendarPeriod.Field periodField, CalendarDate baseDate) {
    this.cal = calt;
    this.periodField = periodField;
    this.period = CalendarPeriod.of(1, periodField);
    this.baseDate = baseDate;

    if (periodField == CalendarPeriod.Field.Month || periodField == CalendarPeriod.Field.Year) {
      isCalendarField = true;
    } else {
      isCalendarField = false;
    }
  }

  // given a CalendarDate, find the values in this unit (secs, days, etc) from the baseDate
  // inverse of makeCalendarDate
  public double makeOffsetFromRefDate( CalendarDate date) {
    if (isCalendarField) {
      if (date.equals(baseDate)) return 0.0;
      return date.getDifference(baseDate, periodField);
    } else {
      long msecs = date.getDifferenceInMsecs(baseDate);
      return msecs / period.getValueInMillisecs();
    }
  }

  // given a value in this unit (secs, days, etc), create the CalendarDate from the RefDate
  // inverse of makeOffsetFromRefDate
  public CalendarDate makeCalendarDate(double value) {
    if (isCalendarField)
      return baseDate.add(CalendarPeriod.of( (int) value, periodField));  // LOOK int vs double
    else
      return baseDate.add( value, periodField);
  }

  public CalendarDate makeCalendarDate(int value) {
    if (isCalendarField)
      return baseDate.add(CalendarPeriod.of( value, periodField));
    else
      return baseDate.add( value, periodField);
  }

  public String getUdUnit() {
      return toString();
  }

  @Override
  public String toString() {
    Formatter f = new Formatter();
    if (isCalendarField) f.format("%s", byCalendarString);
    f.format("%s since %s", periodField, CalendarDateFormatter.toDateTimeStringISO(baseDate));
    return f.toString();
  }

  public CalendarDate getBaseCalendarDate() {
    return baseDate;
  }

  public CalendarPeriod getCalendarPeriod() {
    return period;
  }

  public CalendarPeriod.Field getCalendarField() {
    return periodField;
  }

  public Calendar getCalendar() {
    return cal;
  }

  public boolean isCalendarField() {
    return isCalendarField;
  }

  // testing
  Date getBaseDate() {
    return baseDate.toDate();
  }

  ////////////////////////////////////////////////////////////////////////////////////////////
  public static void main(String[] args) {
    CalendarDateUnit cdu;

    String s = "calendar Month since 2012-01-19T18:00:00.000Z";
    cdu = CalendarDateUnit.of(null, s);
    System.out.printf("%s == %s%n", s, cdu);
  }

}

/*

The acceptable units for time are listed in the udunits.dat file. The most commonly used of these strings (and their abbreviations)
includes day (d), hour (hr, h), minute (min) and second (sec, s). Plural forms are also acceptable. The reference time string
(appearing after the identifier since) may include date alone; date and time; or date, time, and time zone.
The reference time is required. A reference time in year 0 has a special meaning (see Section 7.4, Climatological Statistics).

Note: if the time zone is omitted the default is UTC, and if both time and time zone are omitted the default is 00:00:00 UTC.

We recommend that the unit year be used with caution. The Udunits package defines a year to be exactly 365.242198781 days
(the interval between 2 successive passages of the sun through vernal equinox). It is not a calendar year.
Udunits includes the following definitions for years: a common_year is 365 days, a leap_year is 366 days, a Julian_year is 365.25 days,
and a Gregorian_year is 365.2425 days.

For similar reasons the unit month, which is defined in udunits.dat to be exactly year/12, should also be used with caution.

Example 4.4. Time axis

double time(time) ;
  time:long_name = "time" ;
  time:units = "days since 1990-1-1 0:0:0" ;

A time coordinate is identifiable from its units string alone. The Udunits routines utScan() and utIsTime() can be used to
make this determination.

Optionally, the time coordinate may be indicated additionally by providing the standard_name attribute with an appropriate
value, and/or the axis attribute with the value
*/


/*
 * Copyright (c) 1998-2018 University Corporation for Atmospheric Research/Unidata
 * See LICENSE for license information.
 */
package ucar.nc2.ui.widget;

import ucar.util.prefs.PreferencesExt;

import javax.swing.*;
import javax.swing.filechooser.FileFilter;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;

/**
 * Cover for JFileChooser.
 *

 *

 *

   javax.swing.filechooser.FileFilter[] filters = new javax.swing.filechooser.FileFilter[2];
   filters[0] = new FileManager.HDF5ExtFilter();
   filters[1] = new FileManager.NetcdfExtFilter();
   fileChooser = new FileManager(parentFrame, null, filters, (PreferencesExt) prefs.node("FileManager"));

   AbstractAction fileAction =  new AbstractAction() {
     public void actionPerformed(ActionEvent e) {
       String filename = fileChooser.chooseFilename();
       if (filename == null) return;
       process(filename);
     }
   };
   BAMutil.setActionProperties( fileAction, "FileChooser", "open Local dataset...", false, 'L', -1);

 *
 * @author John Caron
 */

public class FileManager {
  private static final String BOUNDS = "Bounds";
  private static final String DEFAULT_DIR = "DefaultDir";
  private static final String DEFAULT_FILTER = "DefaultFilter";

  // regular
  private PreferencesExt prefs;
  private IndependentDialog w;
  private ucar.util.prefs.ui.ComboBox dirComboBox;
  private javax.swing.JFileChooser chooser = null;

  // for override
  protected JPanel main;
  //protected boolean selectedURL = false;
  //protected ComboBox urlComboBox;

  private boolean readOk = true, selectedFile = false;
  private static boolean debug = false;

  public FileManager(JFrame parent) {
    this(parent, null, null, null);
  }

  public FileManager(JFrame parent, String defDir) {
    this(parent, defDir, null, null);
  }

  public FileManager(JFrame parent, String defDir, String file_extension, String desc, PreferencesExt prefs) {
    this(parent, defDir, new FileFilter[]{new ExtFilter(file_extension, desc)}, prefs);
  }

  public FileManager(JFrame parent, String defDir, FileFilter[] filters, PreferencesExt prefs) {
    this.prefs = prefs;

    // where to start ?
    java.util.List<String> defaultDirs = new ArrayList<String>();
    if (defDir != null)
      defaultDirs.add(defDir);
    else {
      String dirName = (prefs != null) ? prefs.get(DEFAULT_DIR, ".") : ".";
      defaultDirs.add(dirName);
    }

    /* funky windows workaround
    String osName = System.getProperty("os.name");
    //System.out.println("OS ==  "+ osName+" def ="+defDir);
    boolean isWindose = (0 <= osName.indexOf("Windows"));
    if (isWindose)
      defaultDirs.add("C:/"); */

    File defaultDirectory = findDefaultDirectory(defaultDirs);
    try {
      chooser = new ImprovedFileChooser(defaultDirectory);

    } catch (SecurityException se) {
      System.out.println("FileManager SecurityException " + se);
      readOk = false;
      JOptionPane.showMessageDialog(null, "Sorry, this Applet does not have disk read permission.");
    }

    chooser.addActionListener(e -> {
        if (debug) System.out.println("**** chooser event=" + e.getActionCommand() + "\n  " + e);
        //if (debug) System.out.println("  curr directory="+chooser.getCurrentDirectory());
        //if (debug) System.out.println("  selected file="+chooser.getSelectedFile());

        if (e.getActionCommand().equals("ApproveSelection"))
          selectedFile = true;
        w.setVisible(false);
    });

    // set filters
    if (filters != null) {
      for (FileFilter filter : filters) {
        chooser.addChoosableFileFilter(filter);
      }
    }

    // saved file filter
    if (prefs != null) {
      String wantFilter = prefs.get(DEFAULT_FILTER, null);
      if (wantFilter != null) {
        for (FileFilter fileFilter : chooser.getChoosableFileFilters()) {
          if (fileFilter.getDescription().equals(wantFilter))
            chooser.setFileFilter(fileFilter);
        }
      }
    }

    // buttcons
    AbstractAction usedirAction = new AbstractAction() {
      public void actionPerformed(ActionEvent e) {
        String item = (String) dirComboBox.getSelectedItem();
        // System.out.println(" cb =  "+item);
        if (item != null)
          chooser.setCurrentDirectory(new File(item));
      }
    };
    BAMutil.setActionProperties(usedirAction, "FingerDown", "use this directory", false, 'U', -1);

    AbstractAction savedirAction = new AbstractAction() {
      public void actionPerformed(ActionEvent e) {
        File currDir = chooser.getCurrentDirectory();
        //System.out.println("  curr directory="+currDir);
        if (currDir != null)
          dirComboBox.addItem(currDir.getPath());
      }
    };
    BAMutil.setActionProperties(savedirAction, "FingerUp", "save current directory", false, 'S', -1);

    AbstractAction rescanAction = new AbstractAction() {
      public void actionPerformed(ActionEvent e) {
        chooser.rescanCurrentDirectory();
      }
    };
    BAMutil.setActionProperties(rescanAction, "Undo", "refresh", false, 'R', -1);

    // put together the UI
    JPanel buttPanel = new JPanel(new FlowLayout(FlowLayout.LEFT, 5, 0));
    BAMutil.addActionToContainer(buttPanel, usedirAction);
    BAMutil.addActionToContainer(buttPanel, savedirAction);
    BAMutil.addActionToContainer(buttPanel, rescanAction);

    JPanel dirPanel = new JPanel(new BorderLayout());
    dirComboBox = new ucar.util.prefs.ui.ComboBox(prefs);
    dirComboBox.setEditable(true);
    dirPanel.add(new JLabel(" Directories: "), BorderLayout.WEST);
    dirPanel.add(dirComboBox, BorderLayout.CENTER);
    dirPanel.add(buttPanel, BorderLayout.EAST);

    main = new JPanel(new BorderLayout());
    main.add(dirPanel, BorderLayout.NORTH);
    main.add(chooser, BorderLayout.CENTER);

    /* urlComboBox = new ComboBox(prefs);
    urlComboBox.addActionListener(e -> {
          selectedURL = true;
    });

    JPanel p = new JPanel(new FlowLayout(FlowLayout.LEFT, 5, 0));
    p.add( new JLabel("or a URL:"));
    p.add( urlComboBox);
    main.add(urlComboBox, BorderLayout.SOUTH); */

    //w = new IndependentWindow("FileChooser", BAMutil.getImage("FileChooser"), main);
    w = new IndependentDialog(parent, true, "FileChooser", main);
    if (null != prefs) {
      Rectangle b = (Rectangle) prefs.getObject(BOUNDS);
      if (b != null)
        w.setBounds(b);
    }
  }

  public void save() {
    if (prefs == null) return;

    File currDir = chooser.getCurrentDirectory();
    if (currDir != null)
      prefs.put(DEFAULT_DIR, currDir.getPath());

    FileFilter currFilter = chooser.getFileFilter();
    if (currFilter != null)
      prefs.put(DEFAULT_FILTER, currFilter.getDescription());

    if (dirComboBox != null)
      dirComboBox.save();

    prefs.putObject(BOUNDS, w.getBounds());
  }

  public JFileChooser getFileChooser() {
    return chooser;
  }

  /* public java.io.File chooseFile() {
   if (!readOk) return null;
   w.show();

   if (chooser.showOpenDialog( parent) == JFileChooser.APPROVE_OPTION) {
     File file = chooser.getSelectedFile();
     if (debug) System.out.println("FileManager result "+file.getPath());
     if (file != null)
       return file;
   }
   return null;
 } */

  public String chooseFilenameToSave(String defaultFilename) {
    chooser.setDialogType(JFileChooser.SAVE_DIALOG);
    String result = (defaultFilename == null) ? chooseFilename() : chooseFilename(defaultFilename);
    chooser.setDialogType(JFileChooser.OPEN_DIALOG);

    return result;
  }

  public String chooseDirectory(String defaultDirectory) {
    chooser.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY);
    String result = (defaultDirectory == null) ? chooseFilename() : chooseFilename(defaultDirectory);
    chooser.setFileSelectionMode(JFileChooser.FILES_AND_DIRECTORIES);
    return result;
  }

  /**
   * Allow user to select file, then return the filename, in canonical form,
   * always using '/', never '\'
   *
   * @return chosen filename in canonical form, or null if nothing chosen.
   */
  public String chooseFilename() {
    if (!readOk) return null;
    selectedFile = false;
    //selectedURL = false;
    w.setVisible(true); // modal, so blocks; listener calls hide(), which unblocks.

    if (selectedFile) {
      File file = chooser.getSelectedFile();
      if (file == null) return null;
      try {
        return file.getCanonicalPath().replace('\\', '/');
      } catch (IOException ioe) {
      } // return null
    }

    /* if (selectedURL) {
      return (String) urlComboBox.getSelectedItem();
    }  */

    return null;
  }

  public String chooseFilename(File def) {
    File parent = def.getParentFile();
    chooser.setCurrentDirectory(parent);
    chooser.setSelectedFile(def);
    return chooseFilename();
  }

  public String chooseFilename(String defaultFilename) {
    chooser.setSelectedFile(new File(defaultFilename));
    return chooseFilename();
  }

  public File[] chooseFiles() {
    chooser.setMultiSelectionEnabled(true);
    selectedFile = false;
    w.setVisible(true);

    if (selectedFile)
      return chooser.getSelectedFiles();

    return null;
  }

  public String getCurrentDirectory() {
    return chooser.getCurrentDirectory().getPath();
  }

  public void setCurrentDirectory(String dirName) {
    File dir = new File(dirName);
    chooser.setCurrentDirectory(dir);
  }

  private File findDefaultDirectory(java.util.List<String> tryDefaultDirectories) {
    boolean readOK = true;
    for (String tryDefaultDirectory : tryDefaultDirectories) {
      try {
        if (debug) System.out.print("FileManager try " + tryDefaultDirectory);
        File dir = new File(tryDefaultDirectory);
        if (dir.exists()) {
          if (debug) System.out.println(" = ok ");
          return dir;
        } else {
          if (debug) System.out.println(" = no ");
        }
      } catch (SecurityException se) {
        if (debug) System.out.println("SecurityException in FileManager: " + se);
        readOK = false;
      }
    }

    if (!readOK)
      JOptionPane.showMessageDialog(null, "Sorry, this Applet does not have disk read permission.");
    return null;
  }

  public static class ExtFilter extends FileFilter {
    String file_extension;
    String desc;

    public ExtFilter(String file_extension, String desc) {
      this.file_extension = file_extension;
      this.desc = desc;
    }

    public boolean accept(File file) {
      if (null == file_extension)
        return true;
      String name = file.getName();
      return file.isDirectory() || name.endsWith(file_extension);
    }

    public String getDescription() {
      return desc;
    }
  }

  public static class NetcdfExtFilter extends FileFilter {

    public boolean accept(File file) {
      String name = file.getName().toLowerCase();
      return file.isDirectory() || name.endsWith(".nc") || name.endsWith(".cdf");
    }

    public String getDescription() {
      return "netcdf";
    }
  }

  public static class HDF5ExtFilter extends FileFilter {

    public boolean accept(File file) {
      String name = file.getName().toLowerCase();
      return file.isDirectory() || name.endsWith(".h5") || name.endsWith(".hdf");
    }

    public String getDescription() {
      return "hdf5";
    }
  }

  public static class XMLExtFilter extends FileFilter {

    public boolean accept(File file) {
      String name = file.getName().toLowerCase();
      return file.isDirectory() || name.endsWith(".xml");
    }

    public String getDescription() {
      return "xml";
    }
  }

  public static void main(String args[]) throws IOException {

    JFrame frame = new JFrame("Test");
    frame.addWindowListener(new WindowAdapter() {
      public void windowClosing(WindowEvent e) {
        System.exit(0);
      }
    });

    final FileManager fm = new FileManager(frame);
    final JFileChooser fc = fm.chooser;
    fc.addActionListener(e -> {
        System.out.println("**** fm event=" + e.getActionCommand());
        System.out.println("  curr directory=" + fc.getCurrentDirectory());
        System.out.println("  selected file=" + fc.getSelectedFile());
    });

    JButton butt = new JButton("accept");
    butt.addActionListener(e -> {
        System.out.println("butt accept");
        //cb.accept();
    });

    fm.chooseFilename();
    /* JPanel main = new JPanel();
   main.add(fm);
   //main.add(butt);

   frame.getContentPane().add(main);
   // cb.setPreferredSize(new java.awt.Dimension(500, 200));

   frame.pack();
   frame.setLocation(300, 300);
   frame.setVisible(true); */
  }
}

/* claimed workaround for WebStart

 http://forum.java.sun.com/read/56761/q_D4Xl3nS380AAZX5#LR

 Hi Surya,

Unfortunately it looks like you have run into an annoying bug in the Java 2 SDK v1.2.2 or v1.3 .
This bug will be fixed in a future Java 2 SDK release. In the meantime, there are a couple workarounds:

1) Use the JNLP API FileOpenService and FileSaveService to do file operations; the reference
implementation uses the workaround I list under 2) below.

2) Keep your current code, but use a customized FileSystemView that you supply to JFileChooser
when instantiating it on Windows. e.g. :

new JFileChooser(currentDirectory, new WindowsAltFileSystemView ())

Code for WindowsAltFileSystemView follows, with apologies for the formatting :

// This class is necessary due to an annoying bug on Windows NT where
// instantiating a JFileChooser with the default FileSystemView will
// cause a "drive A: not ready" error every time. I grabbed the
// Windows FileSystemView impl from the 1.3 SDK and modified it so
// as to not use java.io.File.listRoots() to get fileSystem roots.
// java.io.File.listRoots() does a SecurityManager.checkRead() which
// causes the OS to try to access drive A: even when there is no disk,
// causing an annoying "abort, retry, ignore" popup message every time
// we instantiate a JFileChooser!
//
// Instead of calling listRoots() we use a straightforward alternate
// method of getting file system roots.

 private class WindowsAltFileSystemView extends FileSystemView {

   /**
   * Returns true if the given file is a root.
   *
   public boolean isRoot(File f) {
     if(!f.isAbsolute())
       return false;

     String parentPath = f.getParent();
     if(parentPath == null) {
       return true;
     } else {
       File parent = new File(parentPath);
       return parent.equals(f);
     }
   }

   /**
   * creates a new folder with a default folder name.
   *
   public File createNewFolder(File containingDir) throws IOException {
     if (containingDir == null)
       throw new IOException("Containing directory is null:");

     File newFolder = null;
     // Using NT's default folder name
     newFolder = createFileObject(containingDir, "New Folder");
     int i = 2;
     while (newFolder.exists() && (i < 100)) {
       newFolder = createFileObject(containingDir, "New Folder (" + i + ")");
       i++;
     }

     if(newFolder.exists()) {
       throw new IOException("Directory already exists:" + newFolder.getAbsolutePath());
     } else {
       newFolder.mkdirs();
     }

     return newFolder;
   }

   /**
   * Returns whether a file is hidden or not. On Windows
   * there is currently no way to get this information from
   * io.File, therefore always return false.
   *
   public boolean isHiddenFile(File f) {
     return false;
   }

   /**
   * Returns all root partitians on this system. On Windows, this
   * will be the A: through Z: drives.
   *
   public File[] getRoots() {
     Vector rootsVector = new Vector();

     System.out.println(" getRoots ");

     // Create the A: drive whether it is mounted or not
     FileSystemRoot floppy = new FileSystemRoot("A" + ":"+ "\\");
     rootsVector.addElement(floppy);

     // Run through all possible mount points and check
     // for their existance.
     for (char c = 'C'; c <= 'Z'; c++) {
       char device[] = {c, ':', '\\'};
       String deviceName = new String(device);
       System.out.println(" try ");
       System.out.println(" "+deviceName);
       File deviceFile = new FileSystemRoot(deviceName);
       boolean ok = deviceFile.exists();
       System.out.println(" "+ok);
       if (deviceFile != null && deviceFile.exists()) {
         rootsVector.addElement(deviceFile);
         System.out.println(" use "+deviceName);
       }
     }

     File[] roots = new File[rootsVector.size()];
     rootsVector.copyInto(roots);
     return roots;
   }
 } // class WindowsAltFileSystemView

 private class FileSystemRoot extends File {
   public FileSystemRoot(File f) {
     super(f, "");
   }

   public FileSystemRoot(String s) {
     super(s);
   }

   public boolean isDirectory() {
     return true;
   }
 } // class FileSystemRoot

} */


/*
 * Copyright (c) 1998-2018 University Corporation for Atmospheric Research/Unidata
 * See LICENSE for license information.
 */
package ucar.units;

import javax.annotation.concurrent.Immutable;

/*
 * Provides support for the concept of the dimension of a unit.  The
 * dimension of a unit is like the dimension of a quantity except that
 * the individual quantity dimensions have been replaced with the
 * corresponding base unit.  For example, the dimension of the quantity
 * "force" is "M.L.t-2" and the dimension of the corresponding SI unit
 * (the "newton") is "kg.m.s-2".
 *
 * Instances of this class are immutable.
 *
 * @author Steven R. Emmerson
 */
@Immutable
public final class
UnitDimension
    extends	Dimension
{
    /**
     * Constructs a dimensionless unit dimension.
     */
    public
    UnitDimension()
    {
	super();
    }

    /**
     * Constructs the unit dimension corresponding to a base unit.
     * @param baseUnit		A base unit.
     */
    public
    UnitDimension(BaseUnit baseUnit)
    {
	super(new Factor(baseUnit));
    }

    /**
     * Constructs a unit dimension comprised of the given factors.
     * @param factors		The factors that constitute the unit dimension.
     */
    private
    UnitDimension(Factor[] factors)
    {
	super(factors);
    }

    /**
     * Multiplies this unit dimension by another.
     * @param that		The other unit dimension.
     * @return			The product of this unit dimension multiplied
     *				by the other.
     */
    public UnitDimension
    multiplyBy(UnitDimension that)
    {
	return new UnitDimension(mult(that));
    }

    /**
     * Divides this unit dimension by another.
     * @param that		The other unit dimension.
     * @return			The quotient of this unit dimension divided
     *				by the other.
     */
    public UnitDimension
    divideBy(UnitDimension that)
    {
	return multiplyBy(that.raiseTo(-1));
    }

    /**
     * Raises this unit dimension to a power.
     * @param power		The power.
     * @return			The result of raising this unit dimension
     *				to the power.
     */
    public UnitDimension
    raiseTo(int power)
    {
	return new UnitDimension(pow(power));
    }

    /**
     * Returns the corresponding quantity dimension.
     * @return			The quantity dimension corresponding to this
     *				unit dimension.
     */
    public QuantityDimension
    getQuantityDimension()
    {
	Factor[]	factors = getFactors();
	for (int i = factors.length; --i >= 0; )
	{
	    Factor	factor = factors[i];
	    factors[i] =
		new Factor(
		    ((BaseUnit)factor.getBase()).getBaseQuantity(),
		    factor.getExponent());
	}
	return new QuantityDimension(factors);
    }

    /**
     * Tests this class.
     */
    public static void
    main(String[] args)
	throws	Exception
    {
	System.out.println("new UnitDimension() = \"" +
	    new UnitDimension() + '"');
	UnitDimension	timeDimension =
	    new UnitDimension(
		BaseUnit.getOrCreate(
		    UnitName.newUnitName("second", null, "s"),
		    BaseQuantity.TIME));
	System.out.println("timeDimension = \"" + timeDimension + '"');
	UnitDimension	lengthDimension =
	    new UnitDimension(
		BaseUnit.getOrCreate(
		    UnitName.newUnitName("meter", null, "m"),
		    BaseQuantity.LENGTH));
	System.out.println("lengthDimension = \"" + lengthDimension + '"');
	System.out.println(
	    "lengthDimension.isReciprocalOf(timeDimension) = \"" +
	    lengthDimension.isReciprocalOf(timeDimension) + '"');
	UnitDimension	hertzDimension = timeDimension.raiseTo(-1);
	System.out.println("hertzDimension = \"" + hertzDimension + '"');
	System.out.println(
	    "hertzDimension.isReciprocalOf(timeDimension) = \"" +
	    hertzDimension.isReciprocalOf(timeDimension) + '"');
	System.out.println("lengthDimension.divideBy(timeDimension) = \"" +
	    lengthDimension.divideBy(timeDimension) + '"');
	System.out.println(
	    "lengthDimension.divideBy(timeDimension).raiseTo(2) = \"" +
	    lengthDimension.divideBy(timeDimension).raiseTo(2) + '"');
    }
}