avwx.parsing.remarks
Contains functions for handling and translating remarks.
1"""Contains functions for handling and translating remarks.""" 2 3# stdlib 4from __future__ import annotations 5 6from contextlib import suppress 7 8# module 9from avwx.parsing import core 10from avwx.static.core import REMARKS_ELEMENTS, REMARKS_GROUPS, WX_TRANSLATIONS 11from avwx.static.taf import PRESSURE_TENDENCIES 12from avwx.structs import Code, FiveDigitCodes, Number, PressureTendency, RemarksData 13 14Codes = list[str] 15 16 17def decimal_code(code: str, repr: str | None = None) -> Number | None: # noqa: A002 18 """Parse a 4-digit decimal temperature representation. 19 20 Ex: 1045 -> -4.5 0237 -> 23.7 21 """ 22 if not code: 23 return None 24 number = f"{'-' if code[0] == '1' else ''}{int(code[1:3])}.{code[3]}" 25 return core.make_number(number, repr or code) 26 27 28def temp_dew_decimal(codes: Codes) -> tuple[Codes, Number | None, Number | None]: 29 """Return the decimal temperature and dewpoint values.""" 30 temp, dew = None, None 31 for i, code in reversed(list(enumerate(codes))): 32 if len(code) in {5, 9} and code[0] == "T" and code[1:].isdigit(): 33 codes.pop(i) 34 temp, dew = decimal_code(code[1:5]), decimal_code(code[5:]) 35 break 36 return codes, temp, dew 37 38 39def temp_minmax(codes: Codes) -> tuple[Codes, Number | None, Number | None]: 40 """Return the 24-hour minimum and maximum temperatures.""" 41 maximum, minimum = None, None 42 for i, code in enumerate(codes): 43 if len(code) == 9 and code[0] == "4" and code.isdigit(): 44 maximum, minimum = decimal_code(code[1:5]), decimal_code(code[5:]) 45 codes.pop(i) 46 break 47 return codes, maximum, minimum 48 49 50def precip_snow(codes: Codes) -> tuple[Codes, Number | None, Number | None]: 51 """Return the hourly precipitation and snow depth.""" 52 precip, snow = None, None 53 for i, code in reversed(list(enumerate(codes))): 54 if len(code) != 5: 55 continue 56 # P0213 57 if code[0] == "P" and code[1:].isdigit(): 58 precip = core.make_number(f"{code[1:3]}.{code[3:]}", code) 59 codes.pop(i) 60 # 4/012 61 elif code[:2] == "4/" and code[2:].isdigit(): 62 snow = core.make_number(code[2:], code) 63 codes.pop(i) 64 return codes, precip, snow 65 66 67def sea_level_pressure(codes: Codes) -> tuple[Codes, Number | None]: 68 """Return the sea level pressure always in hPa.""" 69 sea = None 70 for i, code in enumerate(codes): 71 if len(code) == 6 and code.startswith("SLP") and code[-3:].isdigit(): 72 value = f"{'9' if int(code[-3]) > 4 else '10'}{code[-3:-1]}.{code[-1]}" 73 sea = core.make_number(value, code) 74 codes.pop(i) 75 break 76 return codes, sea 77 78 79def parse_pressure(code: str) -> PressureTendency: 80 """Parse a 5-digit pressure tendency.""" 81 return PressureTendency( 82 repr=code, 83 tendency=PRESSURE_TENDENCIES[code[1]], 84 change=float(f"{code[2:4]}.{code[4]}"), 85 ) 86 87 88def parse_precipitation(code: str) -> Number | None: 89 """Parse a 5-digit precipitation amount.""" 90 return core.make_number(f"{code[1:3]}.{code[3:]}", code) 91 92 93def five_digit_codes(codes: Codes) -> tuple[Codes, FiveDigitCodes]: 94 """Return a 5-digit min/max temperature code.""" 95 values = FiveDigitCodes() 96 for i, code in reversed(list(enumerate(codes))): 97 if len(code) == 5 and code.isdigit(): 98 key = int(code[0]) 99 if key == 1: 100 values.maximum_temperature_6 = decimal_code(code[1:], code) 101 elif key == 2: 102 values.minimum_temperature_6 = decimal_code(code[1:], code) 103 elif key == 5: 104 values.pressure_tendency = parse_pressure(code) 105 elif key == 6: 106 values.precip_36_hours = parse_precipitation(code) 107 elif key == 7: 108 values.precip_24_hours = parse_precipitation(code) 109 elif key == 9: 110 values.sunshine_minutes = core.make_number(code[2:], code) 111 else: 112 continue 113 codes.pop(i) 114 return codes, values 115 116 117def find_codes(rmk: str) -> tuple[Codes, list[Code]]: 118 """Find a remove known static codes from the starting remarks list.""" 119 ret = [] 120 for key, value in REMARKS_GROUPS.items(): 121 if key in rmk: 122 ret.append(Code(key, value)) 123 rmk.replace(key, "") 124 codes = [i for i in rmk.split() if i] 125 for i, code in reversed(list(enumerate(codes))): 126 with suppress(KeyError): 127 ret.append(Code(code, REMARKS_ELEMENTS[code])) 128 codes.pop(i) 129 # Weather began/ended 130 if len(code) == 5 and code[2] in ("B", "E") and code[3:].isdigit() and code[:2] in WX_TRANSLATIONS: 131 state = "began" if code[2] == "B" else "ended" 132 value = f"{WX_TRANSLATIONS[code[:2]]} {state} at :{code[3:]}" 133 ret.append(Code(code, value)) 134 codes.pop(i) 135 ret.sort(key=lambda x: x.repr) 136 return codes, ret 137 138 139def parse(rmk: str) -> RemarksData | None: 140 """Find temperature and dewpoint decimal values from the remarks.""" 141 if not rmk: 142 return None 143 codes, parsed_codes = find_codes(rmk) 144 codes, temperature, dewpoint = temp_dew_decimal(codes) 145 codes, max_temp, min_temp = temp_minmax(codes) 146 codes, precip, snow = precip_snow(codes) 147 codes, sea = sea_level_pressure(codes) 148 codes, fivedigits = five_digit_codes(codes) 149 return RemarksData( 150 codes=parsed_codes, 151 dewpoint_decimal=dewpoint, 152 temperature_decimal=temperature, 153 minimum_temperature_6=fivedigits.minimum_temperature_6, 154 minimum_temperature_24=min_temp, 155 maximum_temperature_6=fivedigits.maximum_temperature_6, 156 maximum_temperature_24=max_temp, 157 pressure_tendency=fivedigits.pressure_tendency, 158 precip_36_hours=fivedigits.precip_36_hours, 159 precip_24_hours=fivedigits.precip_24_hours, 160 sunshine_minutes=fivedigits.sunshine_minutes, 161 precip_hourly=precip, 162 snow_depth=snow, 163 sea_level_pressure=sea, 164 )
Codes =
list[str]
18def decimal_code(code: str, repr: str | None = None) -> Number | None: # noqa: A002 19 """Parse a 4-digit decimal temperature representation. 20 21 Ex: 1045 -> -4.5 0237 -> 23.7 22 """ 23 if not code: 24 return None 25 number = f"{'-' if code[0] == '1' else ''}{int(code[1:3])}.{code[3]}" 26 return core.make_number(number, repr or code)
Parse a 4-digit decimal temperature representation.
Ex: 1045 -> -4.5 0237 -> 23.7
def
temp_dew_decimal( codes: list[str]) -> tuple[list[str], avwx.structs.Number | None, avwx.structs.Number | None]:
29def temp_dew_decimal(codes: Codes) -> tuple[Codes, Number | None, Number | None]: 30 """Return the decimal temperature and dewpoint values.""" 31 temp, dew = None, None 32 for i, code in reversed(list(enumerate(codes))): 33 if len(code) in {5, 9} and code[0] == "T" and code[1:].isdigit(): 34 codes.pop(i) 35 temp, dew = decimal_code(code[1:5]), decimal_code(code[5:]) 36 break 37 return codes, temp, dew
Return the decimal temperature and dewpoint values.
def
temp_minmax( codes: list[str]) -> tuple[list[str], avwx.structs.Number | None, avwx.structs.Number | None]:
40def temp_minmax(codes: Codes) -> tuple[Codes, Number | None, Number | None]: 41 """Return the 24-hour minimum and maximum temperatures.""" 42 maximum, minimum = None, None 43 for i, code in enumerate(codes): 44 if len(code) == 9 and code[0] == "4" and code.isdigit(): 45 maximum, minimum = decimal_code(code[1:5]), decimal_code(code[5:]) 46 codes.pop(i) 47 break 48 return codes, maximum, minimum
Return the 24-hour minimum and maximum temperatures.
def
precip_snow( codes: list[str]) -> tuple[list[str], avwx.structs.Number | None, avwx.structs.Number | None]:
51def precip_snow(codes: Codes) -> tuple[Codes, Number | None, Number | None]: 52 """Return the hourly precipitation and snow depth.""" 53 precip, snow = None, None 54 for i, code in reversed(list(enumerate(codes))): 55 if len(code) != 5: 56 continue 57 # P0213 58 if code[0] == "P" and code[1:].isdigit(): 59 precip = core.make_number(f"{code[1:3]}.{code[3:]}", code) 60 codes.pop(i) 61 # 4/012 62 elif code[:2] == "4/" and code[2:].isdigit(): 63 snow = core.make_number(code[2:], code) 64 codes.pop(i) 65 return codes, precip, snow
Return the hourly precipitation and snow depth.
68def sea_level_pressure(codes: Codes) -> tuple[Codes, Number | None]: 69 """Return the sea level pressure always in hPa.""" 70 sea = None 71 for i, code in enumerate(codes): 72 if len(code) == 6 and code.startswith("SLP") and code[-3:].isdigit(): 73 value = f"{'9' if int(code[-3]) > 4 else '10'}{code[-3:-1]}.{code[-1]}" 74 sea = core.make_number(value, code) 75 codes.pop(i) 76 break 77 return codes, sea
Return the sea level pressure always in hPa.
80def parse_pressure(code: str) -> PressureTendency: 81 """Parse a 5-digit pressure tendency.""" 82 return PressureTendency( 83 repr=code, 84 tendency=PRESSURE_TENDENCIES[code[1]], 85 change=float(f"{code[2:4]}.{code[4]}"), 86 )
Parse a 5-digit pressure tendency.
89def parse_precipitation(code: str) -> Number | None: 90 """Parse a 5-digit precipitation amount.""" 91 return core.make_number(f"{code[1:3]}.{code[3:]}", code)
Parse a 5-digit precipitation amount.
94def five_digit_codes(codes: Codes) -> tuple[Codes, FiveDigitCodes]: 95 """Return a 5-digit min/max temperature code.""" 96 values = FiveDigitCodes() 97 for i, code in reversed(list(enumerate(codes))): 98 if len(code) == 5 and code.isdigit(): 99 key = int(code[0]) 100 if key == 1: 101 values.maximum_temperature_6 = decimal_code(code[1:], code) 102 elif key == 2: 103 values.minimum_temperature_6 = decimal_code(code[1:], code) 104 elif key == 5: 105 values.pressure_tendency = parse_pressure(code) 106 elif key == 6: 107 values.precip_36_hours = parse_precipitation(code) 108 elif key == 7: 109 values.precip_24_hours = parse_precipitation(code) 110 elif key == 9: 111 values.sunshine_minutes = core.make_number(code[2:], code) 112 else: 113 continue 114 codes.pop(i) 115 return codes, values
Return a 5-digit min/max temperature code.
118def find_codes(rmk: str) -> tuple[Codes, list[Code]]: 119 """Find a remove known static codes from the starting remarks list.""" 120 ret = [] 121 for key, value in REMARKS_GROUPS.items(): 122 if key in rmk: 123 ret.append(Code(key, value)) 124 rmk.replace(key, "") 125 codes = [i for i in rmk.split() if i] 126 for i, code in reversed(list(enumerate(codes))): 127 with suppress(KeyError): 128 ret.append(Code(code, REMARKS_ELEMENTS[code])) 129 codes.pop(i) 130 # Weather began/ended 131 if len(code) == 5 and code[2] in ("B", "E") and code[3:].isdigit() and code[:2] in WX_TRANSLATIONS: 132 state = "began" if code[2] == "B" else "ended" 133 value = f"{WX_TRANSLATIONS[code[:2]]} {state} at :{code[3:]}" 134 ret.append(Code(code, value)) 135 codes.pop(i) 136 ret.sort(key=lambda x: x.repr) 137 return codes, ret
Find a remove known static codes from the starting remarks list.
140def parse(rmk: str) -> RemarksData | None: 141 """Find temperature and dewpoint decimal values from the remarks.""" 142 if not rmk: 143 return None 144 codes, parsed_codes = find_codes(rmk) 145 codes, temperature, dewpoint = temp_dew_decimal(codes) 146 codes, max_temp, min_temp = temp_minmax(codes) 147 codes, precip, snow = precip_snow(codes) 148 codes, sea = sea_level_pressure(codes) 149 codes, fivedigits = five_digit_codes(codes) 150 return RemarksData( 151 codes=parsed_codes, 152 dewpoint_decimal=dewpoint, 153 temperature_decimal=temperature, 154 minimum_temperature_6=fivedigits.minimum_temperature_6, 155 minimum_temperature_24=min_temp, 156 maximum_temperature_6=fivedigits.maximum_temperature_6, 157 maximum_temperature_24=max_temp, 158 pressure_tendency=fivedigits.pressure_tendency, 159 precip_36_hours=fivedigits.precip_36_hours, 160 precip_24_hours=fivedigits.precip_24_hours, 161 sunshine_minutes=fivedigits.sunshine_minutes, 162 precip_hourly=precip, 163 snow_depth=snow, 164 sea_level_pressure=sea, 165 )
Find temperature and dewpoint decimal values from the remarks.