text: /**
   * \file DMS.cpp
   * \brief Implementation for GeographicLib::DMS class
   *
   * Copyright (c) Charles Karney (2008-2020) <charles@karney.com> and licensed
   * under the MIT/X11 License.  For more information, see
   * https://geographiclib.sourceforge.io/
   **********************************************************************/
  
  #include <GeographicLib/DMS.hpp>
  #include <GeographicLib/Utility.hpp>
  
  #if defined(_MSC_VER)
  // Squelch warnings about constant conditional expressions
  #  pragma warning (disable: 4127)
  #endif
  
  namespace GeographicLib {
  
    using namespace std;
  
    const char* const DMS::hemispheres_ = "SNWE";
    const char* const DMS::signs_ = "-+";
    const char* const DMS::digits_ = "0123456789";
    const char* const DMS::dmsindicators_ = "D'\":";
    const char* const DMS::components_[] = {"degrees", "minutes", "seconds"};
  
    Math::real DMS::Decode(const std::string& dms, flag& ind) {
      // Here's a table of the allowed characters
  
      // S unicode   dec  UTF-8      descripton
  
      // DEGREE
      // d U+0064    100  64         d
      // D U+0044     68  44         D
      // ° U+00b0    176  c2 b0      degree symbol
      // º U+00ba    186  c2 ba      alt symbol
      // ⁰ U+2070   8304  e2 81 b0   sup zero
      // ˚ U+02da    730  cb 9a      ring above
      // ∘ U+2218   8728  e2 88 98   compose function
      // * U+002a     42  2a         GRiD symbol for degrees
  
      // MINUTES
      // ' U+0027     39  27         apostrophe
      // ` U+0060     96  60         grave accent
      // ′ U+2032   8242  e2 80 b2   prime
      // ‵ U+2035   8245  e2 80 b5   back prime
      // ´ U+00b4    180  c2 b4      acute accent
      // ‘ U+2018   8216  e2 80 98   left single quote (also ext ASCII 0x91)
      // ’ U+2019   8217  e2 80 99   right single quote (also ext ASCII 0x92)
      // ‛ U+201b   8219  e2 80 9b   reversed-9 single quote
      // ʹ U+02b9    697  ca b9      modifier letter prime
      // ˊ U+02ca    714  cb 8a      modifier letter acute accent
      // ˋ U+02cb    715  cb 8b      modifier letter grave accent
  
      // SECONDS
      // " U+0022     34  22         quotation mark
      // ″ U+2033   8243  e2 80 b3   double prime
      // ‶ U+2036   8246  e2 80 b6   reversed double prime
      // ˝ U+02dd    733  cb 9d      double acute accent
      // “ U+201c   8220  e2 80 9c   left double quote (also ext ASCII 0x93)
      // ” U+201d   8221  e2 80 9d   right double quote (also ext ASCII 0x94)
      // ‟ U+201f   8223  e2 80 9f   reversed-9 double quote
      // ʺ U+02ba    698  ca ba      modifier letter double prime
  
      // PLUS
      // + U+002b     43  2b         plus sign
      // ➕ U+2795  10133  e2 9e 95   heavy plus
      //   U+2064   8292  e2 81 a4   invisible plus |⁤|
  
      // MINUS
      // - U+002d     45  2d         hyphen
      // ‐ U+2010   8208  e2 80 90   dash
      // ‑ U+2011   8209  e2 80 91   non-breaking hyphen
      // – U+2013   8211  e2 80 93   en dash (also ext ASCII 0x96)
      // — U+2014   8212  e2 80 94   em dash (also ext ASCII 0x97)
      // − U+2212   8722  e2 88 92   minus sign
      // ➖ U+2796  10134  e2 9e 96   heavy minus
  
      // IGNORED
      //   U+00a0    160  c2 a0      non-breaking space
      //   U+2007   8199  e2 80 87   figure space | |
      //   U+2009   8201  e2 80 89   thin space   | |
      //   U+200a   8202  e2 80 8a   hair space   | |
      //   U+200b   8203  e2 80 8b   invisible space |​|
      //   U+202f   8239  e2 80 af   narrow space | |
      //   U+2063   8291  e2 81 a3   invisible separator |⁣|
      // « U+00ab    171  c2 ab      left guillemot (for cgi-bin)
      // » U+00bb    187  c2 bb      right guillemot (for cgi-bin)
  
      string dmsa = dms;
      replace(dmsa, "\xc2\xb0",     'd' ); // U+00b0 degree symbol
      replace(dmsa, "\xc2\xba",     'd' ); // U+00ba alt symbol
      replace(dmsa, "\xe2\x81\xb0", 'd' ); // U+2070 sup zero
      replace(dmsa, "\xcb\x9a",     'd' ); // U+02da ring above
      replace(dmsa, "\xe2\x88\x98", 'd' ); // U+2218 compose function
  
      replace(dmsa, "\xe2\x80\xb2", '\''); // U+2032 prime
      replace(dmsa, "\xe2\x80\xb5", '\''); // U+2035 back prime
      replace(dmsa, "\xc2\xb4",     '\''); // U+00b4 acute accent
      replace(dmsa, "\xe2\x80\x98", '\''); // U+2018 left single quote
      replace(dmsa, "\xe2\x80\x99", '\''); // U+2019 right single quote
      replace(dmsa, "\xe2\x80\x9b", '\''); // U+201b reversed-9 single quote
      replace(dmsa, "\xca\xb9",     '\''); // U+02b9 modifier letter prime
      replace(dmsa, "\xcb\x8a",     '\''); // U+02ca modifier letter acute accent
      replace(dmsa, "\xcb\x8b",     '\''); // U+02cb modifier letter grave accent
  
      replace(dmsa, "\xe2\x80\xb3", '"' ); // U+2033 double prime
      replace(dmsa, "\xe2\x80\xb6", '"' ); // U+2036 reversed double prime
      replace(dmsa, "\xcb\x9d",     '"' ); // U+02dd double acute accent
      replace(dmsa, "\xe2\x80\x9c", '"' ); // U+201c left double quote
      replace(dmsa, "\xe2\x80\x9d", '"' ); // U+201d right double quote
      replace(dmsa, "\xe2\x80\x9f", '"' ); // U+201f reversed-9 double quote
      replace(dmsa, "\xca\xba",     '"' ); // U+02ba modifier letter double prime
  
      replace(dmsa, "\xe2\x9e\x95", '+' ); // U+2795 heavy plus
      replace(dmsa, "\xe2\x81\xa4", '+' ); // U+2064 invisible plus
  
      replace(dmsa, "\xe2\x80\x90", '-' ); // U+2010 dash
      replace(dmsa, "\xe2\x80\x91", '-' ); // U+2011 non-breaking hyphen
      replace(dmsa, "\xe2\x80\x93", '-' ); // U+2013 en dash
      replace(dmsa, "\xe2\x80\x94", '-' ); // U+2014 em dash
      replace(dmsa, "\xe2\x88\x92", '-' ); // U+2212 minus sign
      replace(dmsa, "\xe2\x9e\x96", '-' ); // U+2796 heavy minus
  
      replace(dmsa, "\xc2\xa0",     '\0'); // U+00a0 non-breaking space
      replace(dmsa, "\xe2\x80\x87", '\0'); // U+2007 figure space
      replace(dmsa, "\xe2\x80\x89", '\0'); // U+2007 thin space
      replace(dmsa, "\xe2\x80\x8a", '\0'); // U+200a hair space
      replace(dmsa, "\xe2\x80\x8b", '\0'); // U+200b invisible space
      replace(dmsa, "\xe2\x80\xaf", '\0'); // U+202f narrow space
      replace(dmsa, "\xe2\x81\xa3", '\0'); // U+2063 invisible separator
  
      replace(dmsa, "\xb0",         'd' ); // 0xb0 bare degree symbol
      replace(dmsa, "\xba",         'd' ); // 0xba bare alt symbol
      replace(dmsa, "*",            'd' ); // GRiD symbol for degree
      replace(dmsa, "`",            '\''); // grave accent
      replace(dmsa, "\xb4",         '\''); // 0xb4 bare acute accent
      // Don't implement these alternatives; they are only relevant for cgi-bin
      // replace(dmsa, "\x91",      '\''); // 0x91 ext ASCII left single quote
      // replace(dmsa, "\x92",      '\''); // 0x92 ext ASCII right single quote
      // replace(dmsa, "\x93",      '"' ); // 0x93 ext ASCII left double quote
      // replace(dmsa, "\x94",      '"' ); // 0x94 ext ASCII right double quote
      // replace(dmsa, "\x96",      '-' ); // 0x96 ext ASCII en dash
      // replace(dmsa, "\x97",      '-' ); // 0x97 ext ASCII em dash
      replace(dmsa, "\xa0",         '\0'); // 0xa0 bare non-breaking space
      replace(dmsa, "''",           '"' ); // '' -> "
      string::size_type
        beg = 0,
        end = unsigned(dmsa.size());
      while (beg < end && isspace(dmsa[beg]))
        ++beg;
      while (beg < end && isspace(dmsa[end - 1]))
        --end;
      // The trimmed string in [beg, end)
      real v = 0;
      int i = 0;
      flag ind1 = NONE;
      // p is pointer to the next piece that needs decoding
      for (string::size_type p = beg, pb; p < end; p = pb, ++i) {
        string::size_type pa = p;
        // Skip over initial hemisphere letter (for i == 0)
        if (i == 0 && Utility::lookup(hemispheres_, dmsa[pa]) >= 0)
          ++pa;
        // Skip over initial sign (checking for it if i == 0)
        if (i > 0 || (pa < end && Utility::lookup(signs_, dmsa[pa]) >= 0))
          ++pa;
        // Find next sign
        pb = min(dmsa.find_first_of(signs_, pa), end);
        flag ind2 = NONE;
        v += InternalDecode(dmsa.substr(p, pb - p), ind2);
        if (ind1 == NONE)
          ind1 = ind2;
        else if (!(ind2 == NONE || ind1 == ind2))
          throw GeographicErr("Incompatible hemisphere specifier in " +
                              dmsa.substr(beg, pb - beg));
      }
      if (i == 0)
        throw GeographicErr("Empty or incomplete DMS string " +
                            dmsa.substr(beg, end - beg));
      ind = ind1;
      return v;
    }
  
    Math::real DMS::InternalDecode(const string& dmsa, flag& ind) {
      string errormsg;
      do {                       // Executed once (provides the ability to break)
        int sign = 1;
        unsigned
          beg = 0,
          end = unsigned(dmsa.size());
        flag ind1 = NONE;
        int k = -1;
        if (end > beg && (k = Utility::lookup(hemispheres_, dmsa[beg])) >= 0) {
          ind1 = (k / 2) ? LONGITUDE : LATITUDE;
          sign = k % 2 ? 1 : -1;
          ++beg;
        }
        if (end > beg && (k = Utility::lookup(hemispheres_, dmsa[end-1])) >= 0) {
          if (k >= 0) {
            if (ind1 != NONE) {
              if (toupper(dmsa[beg - 1]) == toupper(dmsa[end - 1]))
                errormsg = "Repeated hemisphere indicators "
                  + Utility::str(dmsa[beg - 1])
                  + " in " + dmsa.substr(beg - 1, end - beg + 1);
              else
                errormsg = "Contradictory hemisphere indicators "
                  + Utility::str(dmsa[beg - 1]) + " and "
                  + Utility::str(dmsa[end - 1]) + " in "
                  + dmsa.substr(beg - 1, end - beg + 1);
              break;
            }
            ind1 = (k / 2) ? LONGITUDE : LATITUDE;
            sign = k % 2 ? 1 : -1;
            --end;
          }
        }
        if (end > beg && (k = Utility::lookup(signs_, dmsa[beg])) >= 0) {
          if (k >= 0) {
            sign *= k ? 1 : -1;
            ++beg;
          }
        }
        if (end == beg) {
          errormsg = "Empty or incomplete DMS string " + dmsa;
          break;
        }
        real ipieces[] = {0, 0, 0};
        real fpieces[] = {0, 0, 0};
        unsigned npiece = 0;
        real icurrent = 0;
        real fcurrent = 0;
        unsigned ncurrent = 0, p = beg;
        bool pointseen = false;
        unsigned digcount = 0, intcount = 0;
        while (p < end) {
          char x = dmsa[p++];
          if ((k = Utility::lookup(digits_, x)) >= 0) {
            ++ncurrent;
            if (digcount > 0)
              ++digcount;         // Count of decimal digits
            else {
              icurrent = 10 * icurrent + k;
              ++intcount;
            }
          } else if (x == '.') {
            if (pointseen) {
              errormsg = "Multiple decimal points in "
                + dmsa.substr(beg, end - beg);
              break;
            }
            pointseen = true;
            digcount = 1;
          } else if ((k = Utility::lookup(dmsindicators_, x)) >= 0) {
            if (k >= 3) {
              if (p == end) {
                errormsg = "Illegal for : to appear at the end of " +
                  dmsa.substr(beg, end - beg);
                break;
              }
              k = npiece;
            }
            if (unsigned(k) == npiece - 1) {
              errormsg = "Repeated " + string(components_[k]) +
                " component in " + dmsa.substr(beg, end - beg);
              break;
            } else if (unsigned(k) < npiece) {
              errormsg = string(components_[k]) + " component follows "
                + string(components_[npiece - 1]) + " component in "
                + dmsa.substr(beg, end - beg);
              break;
            }
            if (ncurrent == 0) {
              errormsg = "Missing numbers in " + string(components_[k]) +
                " component of " + dmsa.substr(beg, end - beg);
              break;
            }
            if (digcount > 0) {
              istringstream s(dmsa.substr(p - intcount - digcount - 1,
                                          intcount + digcount));
              s >> fcurrent;
              icurrent = 0;
            }
            ipieces[k] = icurrent;
            fpieces[k] = icurrent + fcurrent;
            if (p < end) {
              npiece = k + 1;
              icurrent = fcurrent = 0;
              ncurrent = digcount = intcount = 0;
            }
          } else if (Utility::lookup(signs_, x) >= 0) {
            errormsg = "Internal sign in DMS string "
              + dmsa.substr(beg, end - beg);
            break;
          } else {
            errormsg = "Illegal character " + Utility::str(x) + " in DMS string "
              + dmsa.substr(beg, end - beg);
            break;
          }
        }
        if (!errormsg.empty())
          break;
        if (Utility::lookup(dmsindicators_, dmsa[p - 1]) < 0) {
          if (npiece >= 3) {
            errormsg = "Extra text following seconds in DMS string "
              + dmsa.substr(beg, end - beg);
            break;
          }
          if (ncurrent == 0) {
            errormsg = "Missing numbers in trailing component of "
              + dmsa.substr(beg, end - beg);
            break;
          }
          if (digcount > 0) {
            istringstream s(dmsa.substr(p - intcount - digcount,
                                        intcount + digcount));
            s >> fcurrent;
            icurrent = 0;
          }
          ipieces[npiece] = icurrent;
          fpieces[npiece] = icurrent + fcurrent;
        }
        if (pointseen && digcount == 0) {
          errormsg = "Decimal point in non-terminal component of "
            + dmsa.substr(beg, end - beg);
          break;
        }
        // Note that we accept 59.999999... even though it rounds to 60.
        if (ipieces[1] >= 60 || fpieces[1] > 60 ) {
          errormsg = "Minutes " + Utility::str(fpieces[1])
            + " not in range [0, 60)";
          break;
        }
        if (ipieces[2] >= 60 || fpieces[2] > 60) {
          errormsg = "Seconds " + Utility::str(fpieces[2])
            + " not in range [0, 60)";
          break;
        }
        ind = ind1;
        // Assume check on range of result is made by calling routine (which
        // might be able to offer a better diagnostic).
        return real(sign) *
          ( fpieces[2] != 0 ?
            (60*(60*fpieces[0] + fpieces[1]) + fpieces[2]) / 3600 :
            ( fpieces[1] != 0 ?
              (60*fpieces[0] + fpieces[1]) / 60 : fpieces[0] ) );
      } while (false);
      real val = Utility::nummatch<real>(dmsa);
      if (val == 0)
        throw GeographicErr(errormsg);
      else
        ind = NONE;
      return val;
    }
  
    void DMS::DecodeLatLon(const string& stra, const string& strb,
                           real& lat, real& lon,
                           bool longfirst) {
      real a, b;
      flag ia, ib;
      a = Decode(stra, ia);
      b = Decode(strb, ib);
      if (ia == NONE && ib == NONE) {
        // Default to lat, long unless longfirst
        ia = longfirst ? LONGITUDE : LATITUDE;
        ib = longfirst ? LATITUDE : LONGITUDE;
      } else if (ia == NONE)
        ia = flag(LATITUDE + LONGITUDE - ib);
      else if (ib == NONE)
        ib = flag(LATITUDE + LONGITUDE - ia);
      if (ia == ib)
        throw GeographicErr("Both " + stra + " and "
                            + strb + " interpreted as "
                            + (ia == LATITUDE ? "latitudes" : "longitudes"));
      real
        lat1 = ia == LATITUDE ? a : b,
        lon1 = ia == LATITUDE ? b : a;
      if (abs(lat1) > 90)
        throw GeographicErr("Latitude " + Utility::str(lat1)
                            + "d not in [-90d, 90d]");
      lat = lat1;
      lon = lon1;
    }
  
    Math::real DMS::DecodeAngle(const string& angstr) {
      flag ind;
      real ang = Decode(angstr, ind);
      if (ind != NONE)
        throw GeographicErr("Arc angle " + angstr
                            + " includes a hemisphere, N/E/W/S");
      return ang;
    }
  
    Math::real DMS::DecodeAzimuth(const string& azistr) {
      flag ind;
      real azi = Decode(azistr, ind);
      if (ind == LATITUDE)
        throw GeographicErr("Azimuth " + azistr
                            + " has a latitude hemisphere, N/S");
      return Math::AngNormalize(azi);
    }
  
    string DMS::Encode(real angle, component trailing, unsigned prec, flag ind,
                       char dmssep) {
      // Assume check on range of input angle has been made by calling
      // routine (which might be able to offer a better diagnostic).
      if (!isfinite(angle))
        return angle < 0 ? string("-inf") :
          (angle > 0 ? string("inf") : string("nan"));
  
      // 15 - 2 * trailing = ceiling(log10(2^53/90/60^trailing)).
      // This suffices to give full real precision for numbers in [-90,90]
      prec = min(15 + Math::extra_digits() - 2 * unsigned(trailing), prec);
      real scale = 1;
      for (unsigned i = 0; i < unsigned(trailing); ++i)
        scale *= 60;
      for (unsigned i = 0; i < prec; ++i)
        scale *= 10;
      if (ind == AZIMUTH)
        angle -= floor(angle/360) * 360;
      int sign = angle < 0 ? -1 : 1;
      angle *= sign;
  
      // Break off integer part to preserve precision in manipulation of
      // fractional part.
      real
        idegree = floor(angle),
        fdegree = (angle - idegree) * scale + real(0.5);
      {
        // Implement the "round ties to even" rule
        real f = floor(fdegree);
        fdegree = (f == fdegree && fmod(f, real(2)) == 1) ? f - 1 : f;
      }
      fdegree /= scale;
      if (fdegree >= 1) {
        idegree += 1;
        fdegree -= 1;
      }
      real pieces[3] = {fdegree, 0, 0};
      for (unsigned i = 1; i <= unsigned(trailing); ++i) {
        real
          ip = floor(pieces[i - 1]),
          fp = pieces[i - 1] - ip;
        pieces[i] = fp * 60;
        pieces[i - 1] = ip;
      }
      pieces[0] += idegree;
      ostringstream s;
      s << fixed << setfill('0');
      if (ind == NONE && sign < 0)
        s << '-';
      switch (trailing) {
      case DEGREE:
        if (ind != NONE)
          s << setw(1 + min(int(ind), 2) + prec + (prec ? 1 : 0));
        s << Utility::str(pieces[0], prec);
        // Don't include degree designator (d) if it is the trailing component.
        break;
      default:
        if (ind != NONE)
          s << setw(1 + min(int(ind), 2));
        s << int(pieces[0])
          << (dmssep ? dmssep : char(tolower(dmsindicators_[0])));
        switch (trailing) {
        case MINUTE:
          s << setw(2 + prec + (prec ? 1 : 0)) << Utility::str(pieces[1], prec);
          if (!dmssep)
            s << char(tolower(dmsindicators_[1]));
          break;
        case SECOND:
          s << setw(2)
            << int(pieces[1])
            << (dmssep ? dmssep : char(tolower(dmsindicators_[1])))
            << setw(2 + prec + (prec ? 1 : 0)) << Utility::str(pieces[2], prec);
          if (!dmssep)
            s << char(tolower(dmsindicators_[2]));
          break;
        default:
          break;
        }
      }
      if (ind != NONE && ind != AZIMUTH)
        s << hemispheres_[(ind == LATITUDE ? 0 : 2) + (sign < 0 ? 0 : 1)];
      return s.str();
    }
  
  } // namespace GeographicLib
  