text: # -*- coding: utf-8 -*-
  from __future__ import absolute_import, unicode_literals
  
  import re
  from datetime import datetime, timedelta
  
  from dateutil import tz
  
  from arrow import locales
  
  try:
      from functools import lru_cache
  except ImportError:  # pragma: no cover
      from backports.functools_lru_cache import lru_cache  # pragma: no cover
  
  
  class ParserError(RuntimeError):
      pass
  
  
  class DateTimeParser(object):
  
      _FORMAT_RE = re.compile(
          r"(YYY?Y?|MM?M?M?|Do|DD?D?D?|d?d?d?d|HH?|hh?|mm?|ss?|S+|ZZ?Z?|a|A|X)"
      )
      _ESCAPE_RE = re.compile(r"\[[^\[\]]*\]")
  
      _ONE_OR_MORE_DIGIT_RE = re.compile(r"\d+")
      _ONE_OR_TWO_DIGIT_RE = re.compile(r"\d{1,2}")
      _FOUR_DIGIT_RE = re.compile(r"\d{4}")
      _TWO_DIGIT_RE = re.compile(r"\d{2}")
      _TZ_RE = re.compile(r"[+\-]?\d{2}:?(\d{2})?")
      _TZ_NAME_RE = re.compile(r"\w[\w+\-/]+")
  
      _BASE_INPUT_RE_MAP = {
          "YYYY": _FOUR_DIGIT_RE,
          "YY": _TWO_DIGIT_RE,
          "MM": _TWO_DIGIT_RE,
          "M": _ONE_OR_TWO_DIGIT_RE,
          "DD": _TWO_DIGIT_RE,
          "D": _ONE_OR_TWO_DIGIT_RE,
          "HH": _TWO_DIGIT_RE,
          "H": _ONE_OR_TWO_DIGIT_RE,
          "hh": _TWO_DIGIT_RE,
          "h": _ONE_OR_TWO_DIGIT_RE,
          "mm": _TWO_DIGIT_RE,
          "m": _ONE_OR_TWO_DIGIT_RE,
          "ss": _TWO_DIGIT_RE,
          "s": _ONE_OR_TWO_DIGIT_RE,
          "X": re.compile(r"\d+"),
          "ZZZ": _TZ_NAME_RE,
          "ZZ": _TZ_RE,
          "Z": _TZ_RE,
          "S": _ONE_OR_MORE_DIGIT_RE,
      }
  
      MARKERS = ["YYYY", "MM", "DD"]
      SEPARATORS = ["-", "/", "."]
  
      def __init__(self, locale="en_us", cache_size=0):
  
          self.locale = locales.get_locale(locale)
          self._input_re_map = self._BASE_INPUT_RE_MAP.copy()
          self._input_re_map.update(
              {
                  "MMMM": self._generate_choice_re(
                      self.locale.month_names[1:], re.IGNORECASE
                  ),
                  "MMM": self._generate_choice_re(
                      self.locale.month_abbreviations[1:], re.IGNORECASE
                  ),
                  "Do": re.compile(self.locale.ordinal_day_re),
                  "dddd": self._generate_choice_re(
                      self.locale.day_names[1:], re.IGNORECASE
                  ),
                  "ddd": self._generate_choice_re(
                      self.locale.day_abbreviations[1:], re.IGNORECASE
                  ),
                  "d": re.compile(r"[1-7]"),
                  "a": self._generate_choice_re(
                      (self.locale.meridians["am"], self.locale.meridians["pm"])
                  ),
                  # note: 'A' token accepts both 'am/pm' and 'AM/PM' formats to
                  # ensure backwards compatibility of this token
                  "A": self._generate_choice_re(self.locale.meridians.values()),
              }
          )
          if cache_size > 0:
              self._generate_pattern_re = lru_cache(maxsize=cache_size)(
                  self._generate_pattern_re
              )
  
      def parse_iso(self, string):
  
          has_time = "T" in string or " " in string.strip()
          space_divider = " " in string.strip()
  
          if has_time:
              if space_divider:
                  date_string, time_string = string.split(" ", 1)
              else:
                  date_string, time_string = string.split("T", 1)
              time_parts = re.split("[+-]", time_string, 1)
              has_tz = len(time_parts) > 1
              has_seconds = time_parts[0].count(":") > 1
              has_subseconds = re.search("[.,]", time_parts[0])
  
              if has_subseconds:
                  formats = ["YYYY-MM-DDTHH:mm:ss%sS" % has_subseconds.group()]
              elif has_seconds:
                  formats = ["YYYY-MM-DDTHH:mm:ss"]
              else:
                  formats = ["YYYY-MM-DDTHH:mm"]
          else:
              has_tz = False
              # generate required formats: YYYY-MM-DD, YYYY-MM-DD, YYYY
              # using various separators: -, /, .
              len_markers = len(self.MARKERS)
              formats = [
                  separator.join(self.MARKERS[: len_markers - i])
                  for i in range(len_markers)
                  for separator in self.SEPARATORS
              ]
  
          if has_time and has_tz:
              formats = [f + "Z" for f in formats]
  
          if space_divider:
              formats = [item.replace("T", " ", 1) for item in formats]
  
          return self._parse_multiformat(string, formats)
  
      def _generate_pattern_re(self, fmt):
  
          # fmt is a string of tokens like 'YYYY-MM-DD'
          # we construct a new string by replacing each
          # token by its pattern:
          # 'YYYY-MM-DD' -> '(?P<YYYY>\d{4})-(?P<MM>\d{2})-(?P<DD>\d{2})'
          tokens = []
          offset = 0
  
          # Escape all special RegEx chars
          escaped_fmt = re.escape(fmt)
  
          # Extract the bracketed expressions to be reinserted later.
          escaped_fmt = re.sub(self._ESCAPE_RE, "#", escaped_fmt)
          # Any number of S is the same as one.
          escaped_fmt = re.sub("S+", "S", escaped_fmt)
          escaped_data = re.findall(self._ESCAPE_RE, fmt)
  
          fmt_pattern = escaped_fmt
  
          for m in self._FORMAT_RE.finditer(escaped_fmt):
              token = m.group(0)
              try:
                  input_re = self._input_re_map[token]
              except KeyError:
                  raise ParserError("Unrecognized token '{}'".format(token))
              input_pattern = "(?P<{}>{})".format(token, input_re.pattern)
              tokens.append(token)
              # a pattern doesn't have the same length as the token
              # it replaces! We keep the difference in the offset variable.
              # This works because the string is scanned left-to-right and matches
              # are returned in the order found by finditer.
              fmt_pattern = (
                  fmt_pattern[: m.start() + offset]
                  + input_pattern
                  + fmt_pattern[m.end() + offset :]
              )
              offset += len(input_pattern) - (m.end() - m.start())
  
          final_fmt_pattern = ""
          a = fmt_pattern.split(r"\#")
          b = escaped_data
  
          # Due to the way Python splits, 'a' will always be longer
          for i in range(len(a)):
              final_fmt_pattern += a[i]
              if i < len(b):
                  final_fmt_pattern += b[i][1:-1]
  
          return tokens, re.compile(final_fmt_pattern, flags=re.IGNORECASE)
  
      def parse(self, string, fmt):
  
          if isinstance(fmt, list):
              return self._parse_multiformat(string, fmt)
  
          fmt_tokens, fmt_pattern_re = self._generate_pattern_re(fmt)
  
          match = fmt_pattern_re.search(string)
          if match is None:
              raise ParserError(
                  "Failed to match '{}' when parsing '{}'".format(
                      fmt_pattern_re.pattern, string
                  )
              )
          parts = {}
          for token in fmt_tokens:
              if token == "Do":
                  value = match.group("value")
              else:
                  value = match.group(token)
              self._parse_token(token, value, parts)
          return self._build_datetime(parts)
  
      def _parse_token(self, token, value, parts):
  
          if token == "YYYY":
              parts["year"] = int(value)
          elif token == "YY":
              value = int(value)
              parts["year"] = 1900 + value if value > 68 else 2000 + value
  
          elif token in ["MMMM", "MMM"]:
              parts["month"] = self.locale.month_number(value.lower())
  
          elif token in ["MM", "M"]:
              parts["month"] = int(value)
  
          elif token in ["DD", "D"]:
              parts["day"] = int(value)
  
          elif token in ["Do"]:
              parts["day"] = int(value)
  
          elif token.upper() in ["HH", "H"]:
              parts["hour"] = int(value)
  
          elif token in ["mm", "m"]:
              parts["minute"] = int(value)
  
          elif token in ["ss", "s"]:
              parts["second"] = int(value)
  
          elif token == "S":
              # We have the *most significant* digits of an arbitrary-precision integer.
              # We want the six most significant digits as an integer, rounded.
              # FIXME: add nanosecond support somehow?
              value = value.ljust(7, str("0"))
  
              # floating-point (IEEE-754) defaults to half-to-even rounding
              seventh_digit = int(value[6])
              if seventh_digit == 5:
                  rounding = int(value[5]) % 2
              elif seventh_digit > 5:
                  rounding = 1
              else:
                  rounding = 0
  
              parts["microsecond"] = int(value[:6]) + rounding
  
          elif token == "X":
              parts["timestamp"] = int(value)
  
          elif token in ["ZZZ", "ZZ", "Z"]:
              parts["tzinfo"] = TzinfoParser.parse(value)
  
          elif token in ["a", "A"]:
              if value in (self.locale.meridians["am"], self.locale.meridians["AM"]):
                  parts["am_pm"] = "am"
              elif value in (self.locale.meridians["pm"], self.locale.meridians["PM"]):
                  parts["am_pm"] = "pm"
  
      @staticmethod
      def _build_datetime(parts):
  
          timestamp = parts.get("timestamp")
  
          if timestamp:
              tz_utc = tz.tzutc()
              return datetime.fromtimestamp(timestamp, tz=tz_utc)
  
          am_pm = parts.get("am_pm")
          hour = parts.get("hour", 0)
  
          if am_pm == "pm" and hour < 12:
              hour += 12
          elif am_pm == "am" and hour == 12:
              hour = 0
  
          # account for rounding up to 1000000
          microsecond = parts.get("microsecond", 0)
          if microsecond == 1000000:
              microsecond = 0
              second_increment = 1
          else:
              second_increment = 0
  
          increment = timedelta(seconds=second_increment)
  
          return (
              datetime(
                  year=parts.get("year", 1),
                  month=parts.get("month", 1),
                  day=parts.get("day", 1),
                  hour=hour,
                  minute=parts.get("minute", 0),
                  second=parts.get("second", 0),
                  microsecond=microsecond,
                  tzinfo=parts.get("tzinfo"),
              )
              + increment
          )
  
      def _parse_multiformat(self, string, formats):
  
          _datetime = None
  
          for fmt in formats:
              try:
                  _datetime = self.parse(string, fmt)
                  break
              except ParserError:
                  pass
  
          if _datetime is None:
              raise ParserError(
                  "Could not match input to any of {} on '{}'".format(formats, string)
              )
  
          return _datetime
  
      @staticmethod
      def _map_lookup(input_map, key):
  
          try:
              return input_map[key]
          except KeyError:
              raise ParserError('Could not match "{}" to {}'.format(key, input_map))
  
      @staticmethod
      def _try_timestamp(string):
  
          try:
              return float(string)
          except Exception:
              return None
  
      # generates a capture group of choices separated by an OR operator
      @staticmethod
      def _generate_choice_re(choices, flags=0):
          return re.compile(r"({})".format("|".join(choices)), flags=flags)
  
  
  class TzinfoParser(object):
  
      _TZINFO_RE = re.compile(r"([+\-])?(\d\d):?(\d\d)?")
  
      @classmethod
      def parse(cls, string):
  
          tzinfo = None
  
          if string == "local":
              tzinfo = tz.tzlocal()
  
          elif string in ["utc", "UTC"]:
              tzinfo = tz.tzutc()
  
          else:
  
              iso_match = cls._TZINFO_RE.match(string)
  
              if iso_match:
                  sign, hours, minutes = iso_match.groups()
                  if minutes is None:
                      minutes = 0
                  seconds = int(hours) * 3600 + int(minutes) * 60
  
                  if sign == "-":
                      seconds *= -1
  
                  tzinfo = tz.tzoffset(None, seconds)
  
              else:
                  tzinfo = tz.gettz(string)
  
          if tzinfo is None:
              raise ParserError('Could not parse timezone expression "{}"'.format(string))
  
          return tzinfo
  