text: # Copyright (c) 2004,2018 Python-Metar Developers.
  # Distributed under the terms of the BSD 2-Clause License.
  # SPDX-License-Identifier: BSD-2-Clause
  """This module defines the Metar class.
  
  A Metar object represents the weather report encoded by a single METAR code.
  """
  import re
  import datetime
  import warnings
  import logging
  
  from metar import __version__, __author__, __email__, __LICENSE__
  from metar.Datatypes import (
      temperature, pressure, speed, distance, direction, precipitation
  )
  
  # logger
  _logger = logging.getLogger(__name__)
  
  # Exceptions
  
  class ParserError(Exception):
      """Exception raised when an unparseable group is found in body of the report."""
      pass
  
  # regular expressions to decode various groups of the METAR code
  
  MISSING_RE = re.compile(r"^[M/]+$")
  
  TYPE_RE =    re.compile(r"^(?P<type>METAR|SPECI)\s+")
  COR_RE =     re.compile(r"^(?P<cor>COR)\s+")
  STATION_RE = re.compile(r"^(?P<station>[A-Z][A-Z0-9]{3})\s+")
  TIME_RE = re.compile(r"""^(?P<day>\d\d)
                            (?P<hour>\d\d)
                            (?P<min>\d\d)Z?\s+""",
                            re.VERBOSE)
  MODIFIER_RE = re.compile(r"^(?P<mod>AUTO|FINO|NIL|TEST|CORR?|RTD|CC[A-G])\s+")
  WIND_RE = re.compile(r"""^(?P<dir>[\dO]{3}|[0O]|///|MMM|VRB)
                            (?P<speed>P?[\dO]{2,3}|[/M]{2,3})
                          (G(?P<gust>P?(\d{1,3}|[/M]{1,3})))?
                            (?P<units>KTS?|LT|K|T|KMH|MPS)?
                        (\s+(?P<varfrom>\d\d\d)V
                            (?P<varto>\d\d\d))?\s+""",
                            re.VERBOSE)
  VISIBILITY_RE = re.compile(r"""^(?P<vis>(?P<dist>(M|P)?\d\d\d\d|////)
                                          (?P<dir>[NSEW][EW]? | NDV)? |
                                          (?P<distu>(M|P)?(\d+|\d\d?/\d\d?|\d+\s+\d/\d))
                                          (?P<units>SM|KM|M|U) |
                                          CAVOK )\s+""",
                                   re.VERBOSE)
  RUNWAY_RE = re.compile(r"""^(RVRNO |
                               R(?P<name>\d\d(RR?|LL?|C)?)/
                                (?P<low>(M|P)?\d\d\d\d)
                              (V(?P<high>(M|P)?\d\d\d\d))?
                                (?P<unit>FT)?[/NDU]*)\s+""",
                                re.VERBOSE)
  WEATHER_RE = re.compile(r"""^(?P<int>(-|\+|VC)*)
                               (?P<desc>(MI|PR|BC|DR|BL|SH|TS|FZ)+)?
                               (?P<prec>(DZ|RA|SN|SG|IC|PL|GR|GS|UP|/)*)
                               (?P<obsc>BR|FG|FU|VA|DU|SA|HZ|PY)?
                               (?P<other>PO|SQ|FC|SS|DS|NSW|/+)?
                               (?P<int2>[-+])?\s+""",
                               re.VERBOSE)
  SKY_RE= re.compile(r"""^(?P<cover>VV|CLR|SKC|SCK|NSC|NCD|BKN|SCT|FEW|[O0]VC|///)
                          (?P<height>[\dO]{2,4}|///)?
                          (?P<cloud>([A-Z][A-Z]+|///))?\s+""",
                          re.VERBOSE)
  TEMP_RE = re.compile(r"""^(?P<temp>(M|-)?\d+|//|XX|MM)/
                            (?P<dewpt>(M|-)?\d+|//|XX|MM)?\s+""",
                            re.VERBOSE)
  PRESS_RE = re.compile(r"""^(?P<unit>A|Q|QNH)?
                             (?P<press>[\dO]{3,4}|////)
                             (?P<unit2>INS)?\s+""",
                             re.VERBOSE)
  RECENT_RE = re.compile(r"""^RE(?P<desc>MI|PR|BC|DR|BL|SH|TS|FZ)?
                                (?P<prec>(DZ|RA|SN|SG|IC|PL|GR|GS|UP)*)?
                                (?P<obsc>BR|FG|FU|VA|DU|SA|HZ|PY)?
                                (?P<other>PO|SQ|FC|SS|DS)?\s+""",
                                re.VERBOSE)
  WINDSHEAR_RE = re.compile(r"^(WS\s+)?(ALL\s+RWY|RWY(?P<name>\d\d(RR?|L?|C)?))\s+")
  COLOR_RE = re.compile(r"""^(BLACK)?(BLU|GRN|WHT|RED)\+?
                          (/?(BLACK)?(BLU|GRN|WHT|RED)\+?)*\s*""",
                          re.VERBOSE)
  RUNWAYSTATE_RE = re.compile(r"""((?P<name>\d\d) | R(?P<namenew>\d\d)(RR?|LL?|C)?/?)
                                  ((?P<special> SNOCLO|CLRD(\d\d|//)) |
                                   (?P<deposit>(\d|/))
                                   (?P<extent>(\d|/))
                                   (?P<depth>(\d\d|//))
                                   (?P<friction>(\d\d|//)))\s+""",
                               re.VERBOSE)
  TREND_RE = re.compile(r"^(?P<trend>TEMPO|BECMG|FCST|NOSIG)\s+")
  
  TRENDTIME_RE = re.compile(r"(?P<when>(FM|TL|AT))(?P<hour>\d\d)(?P<min>\d\d)\s+")
  
  REMARK_RE = re.compile(r"^(RMKS?|NOSPECI|NOSIG)\s+")
  
  # regular expressions for remark groups
  
  AUTO_RE = re.compile(r"^AO(?P<type>\d)\s+")
  SEALVL_PRESS_RE = re.compile(r"^SLP(?P<press>\d\d\d)\s+")
  PEAK_WIND_RE = re.compile(r"""^P[A-Z]\s+WND\s+
                                 (?P<dir>\d\d\d)
                                 (?P<speed>P?\d\d\d?)/
                                 (?P<hour>\d\d)?
                                 (?P<min>\d\d)\s+""",
                                 re.VERBOSE)
  WIND_SHIFT_RE = re.compile(r"""^WSHFT\s+
                                  (?P<hour>\d\d)?
                                  (?P<min>\d\d)
                                  (\s+(?P<front>FROPA))?\s+""",
                                  re.VERBOSE)
  PRECIP_1HR_RE = re.compile(r"^P(?P<precip>\d\d\d\d)\s+")
  PRECIP_24HR_RE = re.compile(r"""^(?P<type>6|7)
                                   (?P<precip>\d\d\d\d)\s+""",
                                   re.VERBOSE)
  PRESS_3HR_RE = re.compile(r"""^5(?P<tend>[0-8])
                                  (?P<press>\d\d\d)\s+""",
                                  re.VERBOSE)
  TEMP_1HR_RE = re.compile(r"""^T(?P<tsign>0|1)
                                 (?P<temp>\d\d\d)
                                 ((?P<dsign>0|1)
                                 (?P<dewpt>\d\d\d))?\s+""",
                                 re.VERBOSE)
  TEMP_6HR_RE = re.compile(r"""^(?P<type>1|2)
                                (?P<sign>0|1)
                                (?P<temp>\d\d\d)\s+""",
                                re.VERBOSE)
  TEMP_24HR_RE = re.compile(r"""^4(?P<smaxt>0|1)
                                  (?P<maxt>\d\d\d)
                                  (?P<smint>0|1)
                                  (?P<mint>\d\d\d)\s+""",
                                  re.VERBOSE)
  UNPARSED_RE = re.compile(r"(?P<group>\S+)\s+")
  
  LIGHTNING_RE = re.compile(r"""^((?P<freq>OCNL|FRQ|CONS)\s+)?
                               LTG(?P<type>(IC|CC|CG|CA)*)
                                  ( \s+(?P<loc>( OHD | VC | DSNT\s+ | \s+AND\s+ |
                                   [NSEW][EW]? (-[NSEW][EW]?)* )+) )?\s+""",
                                  re.VERBOSE)
  
  TS_LOC_RE = re.compile(r"""TS(\s+(?P<loc>( OHD | VC | DSNT\s+ | \s+AND\s+ |
                                             [NSEW][EW]? (-[NSEW][EW]?)* )+))?
                                            ( \s+MOV\s+(?P<dir>[NSEW][EW]?) )?\s+""",
                             re.VERBOSE)
  SNOWDEPTH_RE = re.compile(r"""^4/(?P<snowdepth>\d\d\d)\s+""")
  ICE_ACCRETION_RE = re.compile(
      r"^I(?P<ice_accretion_hours>[136])(?P<ice_accretion_depth>\d\d\d)\s+"
  )
  
  
  # translation of weather location codes
  loc_terms = [
      ("OHD", "overhead"),
      ("DSNT", "distant"),
      ("AND", "и"),
      ("VC", "nearby")
  ]
  
  
  def xlate_loc(loc):
      """Substitute English terms for the location codes in the given string."""
      for code, english in loc_terms:
          loc = loc.replace(code, english)
      return loc
  
  
  # translation of the sky-condition codes into english
  SKY_COVER = { "SKC":"ясно ",
                "CLR":"ясно ",
                "NSC":"ясно ",
                "NCD":"ясно ",
                "FEW":"несколько ",
                "SCT":"отдельные ",
                "BKN":"разорванные ",
                "OVC":"пасмурно",
                "///":"",
                "VV":"нихуя не видно" }
  
  
  CLOUD_TYPE = {
      "AC": "высококучевые облака",
      "ACC": "высококучевые башенковидные облака",
      "ACSL": "стоячие линзовидные высококучевые облака",
      "AS": "высокослоистые облака",
      "CB": "кучево-дождевые облака",
      "CBMAM": "кучево-дождевые вымеобразные облака",
      "CCSL": "стоячие линзовидные перисто-кучевые облака",
      "CC": "перисто-кучевые облака",
      "CI": "перистые облака",
      "CS": "перисто-слоистые облака",
      "CU": "кучевые облака",
      "NS": "слоисто-дождевые облака",
      "SC": "слоисто-кучевые облака",
      "ST": "слоистые",
      "SCSL": "стоячие линзовидные слоисто-кучевые облака",
      "TCU": "возвышающиеся кучевые облака",
  }
  
  # translation of the present-weather codes into english
  
  WEATHER_INT = { "-":"легкий",
                  "+":"тяжелый",
                  "-VC":"слегка легкий",
                  "+VC":"слегка тяжелый",
                  "VC":"слегка" }
  WEATHER_DESC = { "MI":"мелкий",
                   "PR":"частичный",
                   "BC":"пятна",
                   "DR":"низкодрейфующий",
                   "BL":"ветренный",
                   "SH":"",
                   "TS":"гроза",
                   "FZ":"заморозки" }
  WEATHER_PREC = { "DZ":"морось",
                   "RA":"дождь",
                   "SN":"снегопад",
                   "SG":"снежные зерна",
                   "IC":"ледяные кристаллы",
                   "PL":"ледяные гранулы",
                   "GR":"град",
                   "GS":"снежные гранулы",
                   "UP":"неизвестное говно с неба",
                   "//":"" }
  WEATHER_OBSC = { "BR":"туман",
                   "FG":"густой туман",
                   "FU":"дым",
                   "VA":"вулканический пепел",
                   "DU":"пыль",
                   "SA":"песок",
                   "HZ":"мгла",
                   "PY":"брызги" }
  WEATHER_OTHER = { "PO":"песчаные вихри",
                    "SQ":"шквалы",
                    "FC":"воронкообразное облако",
                    "SS":"песчаная буря",
                    "DS":"пылевая буря" }
  
  WEATHER_SPECIAL = { "+FC":"торнадо" }
  
  COLOR = { "BLU":"синий",
            "GRN":"зеленый",
            "WHT":"белый" }
  
  # translation of various remark codes into English
  
  PRESSURE_TENDENCY = { "0":"увеличивается, затем уменьшается",
                        "1":"увеличивается медленнее",
                        "2":"увеличение",
                        "3":"увеличивается быстрее",
                        "4":"стабильное",
                        "5":"уменьшается, затем увеличивается",
                        "6":"уменьшается медленнее",
                        "7":"уменьшение",
                        "8":"уменьшается быстрее" }
  
  LIGHTNING_FREQUENCY = { "OCNL":"нечастая",
                          "FRQ":"частая",
                          "CONS":"постоянная" }
  LIGHTNING_TYPE = { "IC":"intracloud",
                     "CC":"облако-в-облако",
                     "CG":"облако-в-землю",
                     "CA":"облако-в-воздух" }
  
  REPORT_TYPE = { "METAR":"routine report",
                  "SPECI":"special report",
                  "AUTO":"automatic report",
                  "COR":"manually corrected report" }
  
  # Helper functions
  
  def _sanitize(code):
      """Some string prep to improve parsing fidelity."""
      # Remove extraneous whitespace, any trailing =, then add trailing
      # whitespace as regex matches need that.
      return "%s " % (code.strip().rstrip("="), )
  
  
  def _report_match(handler, match):
      """Report success or failure of the given handler function. (DEBUG)"""
      if match:
          _logger.debug("%s matched '%s'", handler.__name__, match)
      else:
          _logger.debug("%s didn't match...", handler.__name__)
  
  
  def _unparsedGroup( self, d ):
      """
      Handle otherwise unparseable main-body groups.
      """
      self._unparsed_groups.append(d['group'])
  
  # METAR report objects
  
  debug = False
  
  class Metar(object):
      """METAR (aviation meteorology report)"""
  
      def __init__(self, metarcode, month=None, year=None, utcdelta=None,
                   strict=True):
          """
          Parse raw METAR code.
  
          Parameters
          ----------
          metarcode : str
          month, year : int, optional
            Date values to be used when parsing a non-current METAR code. If not
            provided, then the month and year are guessed from the current date.
          utcdelta : int or datetime.timedelta, optional
            An int of hours or a timedelta object used to specify the timezone.
          strict : bool (default is True)
            This option determines if a ``ParserError`` is raised when
            unparsable groups are found or an unexpected exception is encountered.
            Setting this to `False` will prevent exceptions from being raised and
            only generate warning messages.
          """
  
          self.code = metarcode              # original METAR code
          self.type = 'METAR'                # METAR (routine) or SPECI (special)
          self.correction = None             # COR (corrected - WMO spec)
          self.mod = "AUTO"                  # AUTO (automatic) or COR (corrected - US spec)
          self.station_id = None             # 4-character ICAO station code
          self.time = None                   # observation time [datetime]
          self.cycle = None                  # observation cycle (0-23) [int]
          self.wind_dir = None               # wind direction [direction]
          self.wind_speed = None             # wind speed [speed]
          self.wind_gust = None              # wind gust speed [speed]
          self.wind_dir_from = None          # beginning of range for win dir [direction]
          self.wind_dir_to = None            # end of range for wind dir [direction]
          self.vis = None                    # visibility [distance]
          self.vis_dir = None                # visibility direction [direction]
          self.max_vis = None                # visibility [distance]
          self.max_vis_dir = None            # visibility direction [direction]
          self.temp = None                   # temperature (C) [temperature]
          self.dewpt = None                  # dew point (C) [temperature]
          self.press = None                  # barometric pressure [pressure]
          self.runway = []                   # runway visibility (list of tuples)
          self.weather = []                  # present weather (list of tuples)
          self.recent = []                   # recent weather (list of tuples)
          self.sky = []                      # sky conditions (list of tuples)
          self.windshear = []                # runways w/ wind shear (list of strings)
          self.wind_speed_peak = None        # peak wind speed in last hour
          self.wind_dir_peak = None          # direction of peak wind speed in last hour
          self.peak_wind_time = None         # time of peak wind observation [datetime]
          self.wind_shift_time = None        # time of wind shift [datetime]
          self.max_temp_6hr = None           # max temp in last 6 hours
          self.min_temp_6hr = None           # min temp in last 6 hours
          self.max_temp_24hr = None          # max temp in last 24 hours
          self.min_temp_24hr = None          # min temp in last 24 hours
          self.press_sea_level = None        # sea-level pressure
          self.precip_1hr = None             # precipitation over the last hour
          self.precip_3hr = None             # precipitation over the last 3 hours
          self.precip_6hr = None             # precipitation over the last 6 hours
          self.precip_24hr = None            # precipitation over the last 24 hours
          self.snowdepth = None              # snow depth (distance)
          self.ice_accretion_1hr = None      # ice accretion over the past hour
          self.ice_accretion_3hr = None      # ice accretion over the past 3 hours
          self.ice_accretion_6hr = None      # ice accretion over the past 6 hours
          self._trend = False                # trend groups present (bool)
          self._trend_groups = []            # trend forecast groups
          self._remarks = []                 # remarks (list of strings)
          self._unparsed_groups = []
          self._unparsed_remarks = []
  
          self._now = datetime.datetime.utcnow()
          if utcdelta:
              self._utcdelta = utcdelta
          else:
              self._utcdelta = datetime.datetime.now() - self._now
  
          self._month = month
          self._year = year
  
          # Do some string prep before parsing
          code = _sanitize(self.code)
          try:
              ngroup = len(self.handlers)
              igroup = 0
              ifailed = -1
              while igroup < ngroup and code:
                  pattern, handler, repeatable = self.handlers[igroup]
                  if debug:
                      _logger.debug("%s: %s", handler.__name__, code)
                  m = pattern.match(code)
                  while m:
                      ifailed = -1
                      if debug:
                          _report_match(handler, m.group())
                      handler(self,m.groupdict())
                      code = code[m.end():]
                      if self._trend:
                          code = self._do_trend_handlers(code)
                      if not repeatable:
                          break
  
                      if debug:
                          _logger.debug("%s: %s", handler.__name__, code)
                      m = pattern.match(code)
                  if not m and ifailed < 0:
                      ifailed = igroup
                  igroup += 1
                  if igroup == ngroup and not m:
                      pattern, handler = (UNPARSED_RE, _unparsedGroup)
                      if debug:
                          _logger.debug("%s: %s", handler.__name__, code)
                      m = pattern.match(code)
                      if debug:
                          _report_match(handler, m.group())
                      handler(self,m.groupdict())
                      code = code[m.end():]
                      igroup = ifailed
                      ifailed = -2  # if it's still -2 when we run out of main-body
                                    #  groups, we'll try parsing this group as a remark
              if pattern == REMARK_RE or self.press:
                  while code:
                      for pattern, handler in self.remark_handlers:
                          if debug:
                              _logger.debug("%s: %s", handler.__name__, code)
                          m = pattern.match(code)
                          if m:
                              if debug:
                                  _report_match(handler, m.group())
                              handler(self,m.groupdict())
                              code = pattern.sub("",code,1)
                              break
  
          except Exception as err:
              message = (
                  ("%s failed while processing '%s'\n\t%s"
                   ) % (handler.__name__, code, "\n\t".join(err.args))
              )
              if strict:
                  raise ParserError(message)
              else:
                  warnings.warn(message, RuntimeWarning)
  
          if self._unparsed_groups:
              code = ' '.join(self._unparsed_groups)
              message = "Unparsed groups in body '%s' while processing '%s'" % (code, metarcode)
              if strict:
                  raise ParserError(message)
              else:
                  warnings.warn(message, RuntimeWarning)
  
      @property
      def decode_completed(self):
          """
          Indicate whether the decoding was complete for non-remark elements.
          """
          return not self._unparsed_groups
  
      def _do_trend_handlers(self, code):
          for pattern, handler, repeatable in self.trend_handlers:
              if debug: print(handler.__name__,":",code)
              m = pattern.match(code)
              while m:
                  if debug: _report_match(handler, m.group())
                  self._trend_groups.append(m.group().strip())
                  handler(self,m.groupdict())
                  code = code[m.end():]
                  if not repeatable: break
                  m = pattern.match(code)
          return code
  
      def __str__(self):
          return self.string()
  
      def _handleType( self, d ):
          """
          Parse the report-type group.
  
          The following attributes are set:
              type   [string]
          """
          self.type = d['type']
  
      def _handleCorrection( self, d ):
          """
          Parse the correction group.
  
          The following attributes are set:
              correction   [string]
          """
          self.correction = d['cor']
  
      def _handleStation( self, d ):
          """
          Parse the station id group.
  
          The following attributes are set:
              station_id   [string]
          """
          self.station_id = d['station']
  
      def _handleModifier( self, d ):
          """
          Parse the report-modifier group.
  
          The following attributes are set:
              mod   [string]
          """
          mod = d['mod']
          if mod == 'CORR': mod = 'COR'
          if mod == 'NIL' or mod == 'FINO': mod = 'NO DATA'
          self.mod = mod
  
      def _handleTime( self, d ):
          """
          Parse the observation-time group.
  
          The following attributes are set:
              time   [datetime]
              cycle  [int]
              _day   [int]
              _hour  [int]
              _min   [int]
          """
          self._day = int(d['day'])
          if not self._month:
              self._month = self._now.month
              if self._day > self._now.day:
                  if self._month == 1:
                      self._month = 12
                  else:
                      self._month = self._month - 1
          if not self._year:
              self._year = self._now.year
              if self._month > self._now.month:
                  self._year = self._year - 1
              elif self._month == self._now.month and self._day > self._now.day:
                  self._year = self._year - 1
          self._hour = int(d['hour'])
          self._min = int(d['min'])
          self.time = datetime.datetime(self._year, self._month, self._day,
                                                                      self._hour, self._min)
          if self._min < 45:
              self.cycle = self._hour
          else:
              self.cycle = self._hour+1
  
      def _handleWind( self, d ):
          """
          Parse the wind and variable-wind groups.
  
          The following attributes are set:
              wind_dir           [direction]
              wind_speed         [speed]
              wind_gust          [speed]
              wind_dir_from      [int]
              wind_dir_to        [int]
          """
          wind_dir = d['dir'].replace('O','0')
          if wind_dir != "VRB" and wind_dir != "///" and wind_dir != "MMM":
              self.wind_dir = direction(wind_dir)
          wind_speed = d['speed'].replace('O','0')
          units = d['units']
          if units == 'KTS' or units == 'K' or units == 'T' or units == 'LT':
              units = 'KT'
          if wind_speed.startswith("P"):
              self.wind_speed = speed(wind_speed[1:], units, ">")
          elif not MISSING_RE.match(wind_speed):
              self.wind_speed = speed(wind_speed, units)
          if d['gust']:
              wind_gust = d['gust']
              if wind_gust.startswith("P"):
                  self.wind_gust = speed(wind_gust[1:], units, ">")
              elif not MISSING_RE.match(wind_gust):
                  self.wind_gust = speed(wind_gust, units)
          if d['varfrom']:
              self.wind_dir_from = direction(d['varfrom'])
              self.wind_dir_to = direction(d['varto'])
  
      def _handleVisibility( self, d ):
          """
          Parse the minimum and maximum visibility groups.
  
          The following attributes are set:
              vis          [distance]
              vis_dir      [direction]
              max_vis      [distance]
              max_vis_dir  [direction]
          """
          vis = d['vis']
          vis_less = None
          vis_dir = None
          vis_units = "M"
          vis_dist = "10000"
          if d['dist'] and d['dist'] != '////':
              vis_dist = d['dist']
              if d['dir'] and d['dir'] != 'NDV':
                  vis_dir = d['dir']
          elif d['distu']:
              vis_dist = d['distu']
              if d['units'] and d['units'] != "U":
                  vis_units = d['units']
          if vis_dist == "9999":
              vis_dist = "10000"
              vis_less = ">"
          if self.vis:
              if vis_dir:
                  self.max_vis_dir = direction(vis_dir)
              self.max_vis = distance(vis_dist, vis_units, vis_less)
          else:
              if vis_dir:
                  self.vis_dir = direction(vis_dir)
              self.vis = distance(vis_dist, vis_units, vis_less)
  
      def _handleRunway(self, d):
          """
          Parse a runway visual range group.
  
          The following attributes are set:
              range   [list of tuples]
              . name  [string]
              . low   [distance]
              . high  [distance]
              . unit  [string]
          """
          if d['name'] is None:
              return
          unit = d['unit'] if d['unit'] is not None else 'FT'
          low = distance(d['low'], unit)
          if d['high'] is None:
              high = low
          else:
              high = distance(d['high'], unit)
          self.runway.append([d['name'], low, high, unit])
  
      def _handleWeather(self, d):
          """
          Parse a present-weather group.
  
          The following attributes are set:
              weather    [list of tuples]
              .  intensity     [string]
              .  description   [string]
              .  precipitation [string]
              .  obscuration   [string]
              .  other         [string]
          """
          inteni = d['int']
          if not inteni and d['int2']:
              inteni = d['int2']
          desci = d['desc']
          preci = d['prec']
          obsci = d['obsc']
          otheri = d['other']
          self.weather.append((inteni, desci, preci, obsci, otheri))
  
      def _handleSky( self, d ):
          """
          Parse a sky-conditions group.
  
          The following attributes are set:
              sky        [list of tuples]
              .  cover   [string]
              .  height  [distance]
              .  cloud   [string]
          """
          height = d['height']
          if not height or height == "///":
              height = None
          else:
              height = height.replace('O','0')
              height = distance(int(height)*100,"FT")
          cover = d['cover']
          if cover == 'SCK' or cover == 'SKC' or cover == 'CL': cover = 'CLR'
          if cover == '0VC': cover = 'OVC'
          cloud = d['cloud']
          if cloud == '///': cloud = ""
          self.sky.append((cover,height,cloud))
  
      def _handleTemp( self, d ):
          """
          Parse a temperature-dewpoint group.
  
          The following attributes are set:
              temp    temperature (Celsius) [float]
              dewpt   dew point (Celsius) [float]
          """
          temp = d['temp']
          dewpt = d['dewpt']
          if  temp and temp != "//" and temp != "XX" and temp != "MM" :
              self.temp = temperature(temp)
          if dewpt and dewpt != "//" and dewpt != "XX" and dewpt != "MM" :
              self.dewpt = temperature(dewpt)
  
      def _handlePressure( self, d ):
          """
          Parse an altimeter-pressure group.
  
          The following attributes are set:
              press    [int]
          """
          press = d['press']
          if press != '////':
              press = float(press.replace('O','0'))
              if d['unit']:
                  if d['unit'] == 'A' or (d['unit2'] and d['unit2'] == 'INS'):
                      self.press = pressure(press/100,'IN')
                  elif d['unit'] == 'SLP':
                      if press < 500:
                          press = press/10 + 1000
                      else:
                          press = press/10 + 900
                      self.press = pressure(press,'MB')
                      self._remarks.append("sea-level pressure %.1fhPa" % press)
                  else:
                      self.press = pressure(press,'MB')
              elif press > 2500:
                  self.press = pressure(press/100,'IN')
              else:
                  self.press = pressure(press,'MB')
  
      def _handleRecent( self, d ):
          """
          Parse a recent-weather group.
  
          The following attributes are set:
              weather    [list of tuples]
              .  intensity     [string]
              .  description   [string]
              .  precipitation [string]
              .  obscuration   [string]
              .  other         [string]
          """
          desci = d['desc']
          preci = d['prec']
          obsci = d['obsc']
          otheri = d['other']
          self.recent.append(("",desci,preci,obsci,otheri))
  
      def _handleWindShear( self, d ):
          """
          Parse wind-shear groups.
  
          The following attributes are set:
              windshear    [list of strings]
          """
          if d['name']:
              self.windshear.append(d['name'])
          else:
              self.windshear.append("ALL")
  
      def _handleColor( self, d ):
          """
          Parse (and ignore) the color groups.
  
          The following attributes are set:
              trend    [list of strings]
          """
          pass
  
      def _handleRunwayState( self, d ):
          """
          Parse (and ignore) the runway state.
  
          The following attributes are set:
          """
          pass
  
      def _handleTrend( self, d ):
          """
          Parse (and ignore) the trend groups.
          """
          if 'trend' in d:
              self._trend_groups.append(d['trend'])
          self._trend = True
  
      def _startRemarks( self, d ):
          """
          Found the start of the remarks section.
          """
          self._remarks = []
  
      def _handleSealvlPressRemark( self, d ):
          """
          Parse the sea-level pressure remark group.
          """
          value = float(d['press'])/10.0
          if value < 50:
              value += 1000
          else:
              value += 900
          self.press_sea_level = pressure(value,"MB")
  
      def _handlePrecip24hrRemark( self, d ):
          """
          Parse a 3-, 6- or 24-hour cumulative preciptation remark group.
          """
          value = float(d['precip'])/100.0
          if d['type'] == "6":
              if self.cycle == 3 or self.cycle == 9 or self.cycle == 15 or self.cycle == 21:
                  self.precip_3hr = precipitation(value,"IN")
              else:
                  self.precip_6hr = precipitation(value,"IN")
          else:
              self.precip_24hr = precipitation(value,"IN")
  
      def _handlePrecip1hrRemark( self, d ):
          """Parse an hourly precipitation remark group."""
          value = float(d['precip'])/100.0
          self.precip_1hr = precipitation(value,"IN")
  
      def _handleTemp1hrRemark( self, d ):
          """
          Parse a temperature & dewpoint remark group.
  
          These values replace the temp and dewpt from the body of the report.
          """
          value = float(d['temp'])/10.0
          if d['tsign'] == "1": value = -value
          self.temp = temperature(value)
          if d['dewpt']:
              value2 = float(d['dewpt'])/10.0
              if d['dsign'] == "1": value2 = -value2
              self.dewpt = temperature(value2)
  
      def _handleTemp6hrRemark( self, d ):
          """
          Parse a 6-hour maximum or minimum temperature remark group.
          """
          value = float(d['temp'])/10.0
          if d['sign'] == "1": value = -value
          if d['type'] == "1":
              self.max_temp_6hr = temperature(value,"C")
          else:
              self.min_temp_6hr = temperature(value,"C")
  
      def _handleTemp24hrRemark( self, d ):
          """
          Parse a 24-hour maximum/minimum temperature remark group.
          """
          value = float(d['maxt'])/10.0
          if d['smaxt'] == "1": value = -value
          value2 = float(d['mint'])/10.0
          if d['smint'] == "1": value2 = -value2
          self.max_temp_24hr = temperature(value,"C")
          self.min_temp_24hr = temperature(value2,"C")
  
      def _handlePress3hrRemark( self, d ):
          """
          Parse a pressure-tendency remark group.
          """
          value = float(d['press'])/10.0
          descrip = PRESSURE_TENDENCY[d['tend']]
          self._remarks.append("3-hr pressure change %.1fhPa, %s" % (value,descrip))
  
      def _handlePeakWindRemark( self, d ):
          """
          Parse a peak wind remark group.
          """
          peak_dir = int(d['dir'])
          peak_speed = int(d['speed'])
          self.wind_speed_peak = speed(peak_speed, "KT")
          self.wind_dir_peak = direction(peak_dir)
          peak_min = int(d['min'])
          if d['hour']:
              peak_hour = int(d['hour'])
          else:
              peak_hour = self._hour
          self.peak_wind_time = datetime.datetime(self._year, self._month, self._day,
                                                                      peak_hour, peak_min)
          if self.peak_wind_time > self.time:
              if peak_hour > self._hour:
                  self.peak_wind_time -= datetime.timedelta(hours=24)
              else:
                  self.peak_wind_time -= datetime.timedelta(hours=1)
          self._remarks.append("peak wind %dkt from %d degrees at %d:%02d" % \
                                                  (peak_speed, peak_dir, peak_hour, peak_min))
  
      def _handleWindShiftRemark( self, d ):
          """
          Parse a wind shift remark group.
          """
          if d['hour']:
              wshft_hour = int(d['hour'])
          else:
              wshft_hour = self._hour
          wshft_min = int(d['min'])
          self.wind_shift_time = datetime.datetime(self._year, self._month, self._day,
                                                                      wshft_hour, wshft_min)
          if self.wind_shift_time > self.time:
              if wshft_hour > self._hour:
                  self.wind_shift_time -= datetime.timedelta(hours=24)
              else:
                  self.wind_shift_time -= datetime.timedelta(hours=1)
          text = "смещение ветра в %d:%02d" %  (wshft_hour, wshft_min)
          if d['front']:
              text += " (front)"
          self._remarks.append(text)
  
      def _handleLightningRemark( self, d ):
          """
          Parse a lightning observation remark group.
          """
          parts = []
          if d['freq']:
              parts.append(LIGHTNING_FREQUENCY[d['freq']])
          parts.append("lightning")
          if d['type']:
              ltg_types = []
              group = d['type']
              while group:
                  ltg_types.append(LIGHTNING_TYPE[group[:2]])
                  group = group[2:]
              parts.append("("+",".join(ltg_types)+")")
          if d['loc']:
              parts.append(xlate_loc(d['loc']))
          self._remarks.append(" ".join(parts))
  
      def _handleTSLocRemark( self, d ):
          """
          Parse a thunderstorm location remark group.
          """
          text = "thunderstorm"
          if d['loc']:
              text += " "+xlate_loc(d['loc'])
          if d['dir']:
              text += " moving %s" % d['dir']
          self._remarks.append(text)
  
      def _handleAutoRemark( self, d ):
          """
          Parse an automatic station remark group.
          """
          if d['type'] == "1":
              self._remarks.append("Automated station")
          elif d['type'] == "2":
              self._remarks.append("Automated station (type 2)")
  
      def _handleSnowDepthRemark(self, d):
          """
          Parse the 4/ group snowdepth report
          """
          self.snowdepth = distance(float(d['snowdepth']), 'IN')
          self._remarks.append(" snowdepth %s" % (self.snowdepth, ))
  
      def _handleIceAccretionRemark(self, d):
          """
          Parse the I/ group ice accretion report.
          """
          myattr = "ice_accretion_%shr" % (d['ice_accretion_hours'], )
          value = precipitation(float(d['ice_accretion_depth']) / 100., 'IN')
          setattr(self, myattr, value)
  
      def _unparsedRemark( self, d ):
          """
          Handle otherwise unparseable remark groups.
          """
          self._unparsed_remarks.append(d['group'])
  
      # the list of handler functions to use (in order) to process a METAR report
  
      handlers = [ (TYPE_RE, _handleType, False),
                   (COR_RE, _handleCorrection, False),
                   (STATION_RE, _handleStation, False),
                   (TIME_RE, _handleTime, False),
                   (MODIFIER_RE, _handleModifier, False),
                   (WIND_RE, _handleWind, False),
                   (VISIBILITY_RE, _handleVisibility, True),
                   (RUNWAY_RE, _handleRunway, True),
                   (WEATHER_RE, _handleWeather, True),
                   (SKY_RE, _handleSky, True),
                   (TEMP_RE, _handleTemp, False),
                   (PRESS_RE, _handlePressure, True),
                   (RECENT_RE,_handleRecent, True),
                   (WINDSHEAR_RE, _handleWindShear, True),
                   (COLOR_RE, _handleColor, True),
                   (RUNWAYSTATE_RE, _handleRunwayState, True),
                   (TREND_RE, _handleTrend, False),
                   (REMARK_RE, _startRemarks, False) ]
  
      trend_handlers = [ (TRENDTIME_RE, _handleTrend, True),
                         (WIND_RE, _handleTrend, True),
                         (VISIBILITY_RE, _handleTrend, True),
                         (WEATHER_RE, _handleTrend, True),
                         (SKY_RE, _handleTrend, True),
                         (COLOR_RE, _handleTrend, True)]
  
      # the list of patterns for the various remark groups,
      # paired with the handler functions to use to record the decoded remark.
  
      remark_handlers = [ (AUTO_RE,         _handleAutoRemark),
                          (SEALVL_PRESS_RE, _handleSealvlPressRemark),
                          (PEAK_WIND_RE,    _handlePeakWindRemark),
                          (WIND_SHIFT_RE,   _handleWindShiftRemark),
                          (LIGHTNING_RE,    _handleLightningRemark),
                          (TS_LOC_RE,       _handleTSLocRemark),
                          (TEMP_1HR_RE,     _handleTemp1hrRemark),
                          (PRECIP_1HR_RE,   _handlePrecip1hrRemark),
                          (PRECIP_24HR_RE,  _handlePrecip24hrRemark),
                          (PRESS_3HR_RE,    _handlePress3hrRemark),
                          (TEMP_6HR_RE,     _handleTemp6hrRemark),
                          (TEMP_24HR_RE,    _handleTemp24hrRemark),
                          (SNOWDEPTH_RE,    _handleSnowDepthRemark),
                          (ICE_ACCRETION_RE, _handleIceAccretionRemark),
                          (UNPARSED_RE,     _unparsedRemark) ]
  
      # functions that return text representations of conditions for output
  
      def string( self ):
          """
          Return a human-readable version of the decoded report.
          """
          lines = []
          lines.append("station: %s" % self.station_id)
          if self.type:
              lines.append("type: %s" % self.report_type())
          if self.time:
              lines.append("time: %s" % self.time.ctime())
          if self.temp:
              lines.append("temperature: %s" % self.temp.string("C"))
          if self.dewpt:
              lines.append("dew point: %s" % self.dewpt.string("C"))
          if self.wind_speed:
              lines.append("wind: %s" % self.wind())
          if self.wind_speed_peak:
              lines.append("peak wind: %s" % self.peak_wind())
          if self.wind_shift_time:
              lines.append("wind shift: %s" % self.wind_shift())
          if self.vis:
              lines.append("visibility: %s" % self.visibility())
          if self.runway:
              lines.append("visual range: %s" % self.runway_visual_range())
          if self.press:
              lines.append("pressure: %s" % self.press.string("mb"))
          if self.weather:
              lines.append("weather: %s" % self.present_weather())
          if self.sky:
              lines.append("sky: %s" % self.sky_conditions("\n     "))
          if self.press_sea_level:
              lines.append("sea-level pressure: %s" % self.press_sea_level.string("mb"))
          if self.max_temp_6hr:
              lines.append("6-hour max temp: %s" % str(self.max_temp_6hr))
          if self.max_temp_6hr:
              lines.append("6-hour min temp: %s" % str(self.min_temp_6hr))
          if self.max_temp_24hr:
              lines.append("24-hour max temp: %s" % str(self.max_temp_24hr))
          if self.max_temp_24hr:
              lines.append("24-hour min temp: %s" % str(self.min_temp_24hr))
          if self.precip_1hr:
              lines.append("1-hour precipitation: %s" % str(self.precip_1hr))
          if self.precip_3hr:
              lines.append("3-hour precipitation: %s" % str(self.precip_3hr))
          if self.precip_6hr:
              lines.append("6-hour precipitation: %s" % str(self.precip_6hr))
          if self.precip_24hr:
              lines.append("24-hour precipitation: %s" % str(self.precip_24hr))
          if self.ice_accretion_1hr:
              lines.append("1-hour Ice Accretion: %s" % str(self.ice_accretion_1hr, ))
          if self.ice_accretion_3hr:
              lines.append("3-hour Ice Accretion: %s" % str(self.ice_accretion_3hr, ))
          if self.ice_accretion_6hr:
              lines.append("6-hour Ice Accretion: %s" % str(self.ice_accretion_6hr, ))
          if self._remarks:
              lines.append("remarks:")
              lines.append("- "+self.remarks("\n- "))
          if self._unparsed_remarks:
              lines.append("- "+' '.join(self._unparsed_remarks))
          lines.append("METAR: "+self.code)
          return "\n".join(lines)
  
      def report_type( self ):
          """
          Return a textual description of the report type.
          """
          if self.type is None:
              text = "unknown report type"
          elif self.type in REPORT_TYPE:
              text  = REPORT_TYPE[self.type]
          else:
              text = self.type+" report"
          if self.cycle:
              text += ", cycle %d" % self.cycle
          if self.mod:
              if self.mod in REPORT_TYPE:
                  text += " (%s)" % REPORT_TYPE[self.mod]
              else:
                  text += " (%s)" % self.mod
          if self.correction:
              text += ' (%s)' % REPORT_TYPE[self.correction]
          return text
  
      def wind( self, units="KT" ):
          """
          Return a textual description of the wind conditions.
  
          Units may be specified as "MPS", "KT", "KMH", or "MPH".
          """
          if self.wind_speed is None:
              return "missing"
          elif self.wind_speed.value() == 0.0:
              text = "спокойно"
          else:
              wind_speed = self.wind_speed.string(units)
              if not self.wind_dir:
                  text = "переменный со скоростью %s" % wind_speed
              elif self.wind_dir_from:
                  text = "%s на %s со скоростью %s" % \
                                 (self.wind_dir_from.compass(), self.wind_dir_to.compass(), wind_speed)
              else:
                  text = "%s со скоростью %s" % (self.wind_dir.compass(), wind_speed)
              if self.wind_gust:
                  text += ", с порывами ветра %s" % self.wind_gust.string(units)
          return text
  
      def peak_wind( self, units="KT" ):
          """
          Return a textual description of the peak wind conditions.
  
          Units may be specified as "MPS", "KT", "KMH", or "MPH".
          """
          if self.wind_speed_peak is None:
              return "missing"
          elif self.wind_speed_peak.value() == 0.0:
              text = "спокойно"
          else:
              wind_speed = self.wind_speed_peak.string(units)
              if not self.wind_dir_peak:
                  text = wind_speed
              else:
                  text = "%s со скоростью %s" % (self.wind_dir_peak.compass(), wind_speed)
                  if not self.peak_wind_time is None:
                      text += " at %s" % self.peak_wind_time.strftime('%H:%M')
          return text
  
      def wind_shift( self, units="KT" ):
          """
          Return a textual description of the wind shift time
  
          Units may be specified as "MPS", "KT", "KMH", or "MPH".
          """
          if self.wind_shift_time is None:
              return "missing"
          else:
              return self.wind_shift_time.strftime('%H:%M')
  
      def visibility( self, units=None ):
          """
          Return a textual description of the visibility.
  
          Units may be statute miles ("SM") or meters ("M").
          """
          if self.vis is None:
              return "missing"
          if self.vis_dir:
              text = "%s to %s" % (self.vis.string(units), self.vis_dir.compass())
          else:
              text = self.vis.string(units)
          if self.max_vis:
              if self.max_vis_dir:
                  text += "; %s to %s" % (self.max_vis.string(units), self.max_vis_dir.compass())
              else:
                  text += "; %s" % self.max_vis.string(units)
          return text
  
      def runway_visual_range(self, units=None):
          """
          Return a textual description of the runway visual range.
          """
          lines = []
          for name, low, high, unit in self.runway:
              reportunits = unit if units is None else units
              if low != high:
                  lines.append(
                      ("on runway %s, from %d to %s"
                       ) % (name, low.value(reportunits),
                            high.string(reportunits))
                  )
              else:
                  lines.append(
                      "on runway %s, %s" % (name, low.string(reportunits))
                  )
          return "; ".join(lines)
  
      def present_weather( self ):
          """
          Return a textual description of the present weather.
          """
          return self._weather( self.weather )
  
      def recent_weather( self ):
          """
          Return a textual description of the recent weather.
          """
          return self._weather( self.recent )
  
      def _weather( self, weather ):
          """
          Return a textual description of weather.
          """
          text_list = []
          for weatheri in weather:
              (inteni, desci, preci, obsci, otheri) = weatheri
              text_parts = []
              code_parts = []
              if inteni:
                  code_parts.append(inteni)
                  text_parts.append(WEATHER_INT[inteni])
              if desci:
                  code_parts.append(desci)
                  if desci != "SH" or not preci:
                      text_parts.append(WEATHER_DESC[desci[0:2]])
                      if len(desci) == 4:
                          text_parts.append(WEATHER_DESC[desci[2:]])
              if preci:
                  code_parts.append(preci)
                  if len(preci) == 2:
                      precip_text = WEATHER_PREC[preci]
                  elif len(preci) == 4:
                      precip_text = WEATHER_PREC[preci[:2]]+" and "
                      precip_text += WEATHER_PREC[preci[2:]]
                  elif len(preci) == 6:
                      precip_text = WEATHER_PREC[preci[:2]]+", "
                      precip_text += WEATHER_PREC[preci[2:4]]+" and "
                      precip_text += WEATHER_PREC[preci[4:]]
                  else:
                      precip_text = preci
                  if desci == "TS":
                      text_parts.append("with")
                  text_parts.append(precip_text)
                  if desci == "SH":
                      text_parts.append(WEATHER_DESC[desci])
              if obsci:
                  code_parts.append(obsci)
                  text_parts.append(WEATHER_OBSC[obsci])
  
              if otheri:
                  code_parts.append(otheri)
                  text_parts.append(WEATHER_OTHER[otheri])
              code = " ".join(code_parts)
              if code in WEATHER_SPECIAL:
                  text_list.append(WEATHER_SPECIAL[code])
              else:
                  text_list.append(" ".join(text_parts))
          return "; ".join(text_list)
  
      def sky_conditions( self, sep="; " ):
          """
          Return a textual description of the sky conditions.
          """
          text_list = []
          for skyi in self.sky:
              (cover,height,cloud) = skyi
              if cover in ["SKC", "CLR", "NSC"]:
                  text_list.append(SKY_COVER[cover])
              else:
                  if cloud:
                      what = CLOUD_TYPE.get(
                          cloud, "unknown CLOUD_TYPE of %s" % (cloud, )
                      )
                  elif SKY_COVER[cover].endswith(" "):
                      what = "облака"
                  else:
                      what = ""
                  label = "%s %s" % (SKY_COVER[cover], what)
                  # HACK here to account for 'empty' entries with above format
                  label = " ".join(label.strip().split())
                  if cover == "VV":
                      label += ", вертикальная видимость на %s" % (str(height), )
                  else:
                      label += " на %s" % (str(height), )
                  text_list.append(label)
          return sep.join(text_list)
  
      def trend( self ):
          """
          Return the trend forecast groups
          """
          return " ".join(self._trend_groups)
  
      def remarks( self, sep="; "):
          """
          Return the decoded remarks.
          """
          return sep.join(self._remarks)
  