waterml/src/main/java/ucar/nc2/ogc/erddap/util/ErddapEDUnits.java
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();
    }
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();
    }
This converts UDUnits to UCUM.
<br>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
<br>UCUM: http://unitsofmeasure.org/ucum.html
I worked with Version: 1.8, $Revision: 28894 $
<p/>
<p>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.
<p/>
<p>Notes:
<ul>
<li>This method is a strictly case sensitive.
The only UDUnits that should be capitalized (other than acronyms) are
Btu, Gregorian..., Julian..., PI.
<br>The only UDUnits that may be capitalized are
Celsius, Fahrenheit, Kelvin, Rankine.
<li>For "10 to the", UCUM allows 10* or 10^. This method uses 10^.
<li>NTU becomes {ntu}.
<li>PSU or psu becomes {psu}.
</ul>
<p/>
return the UDUnits converted to UCUM.
null returns null. "" returns "".
throws Exception if trouble.