avwx.parsing.translate.base
Functions for translating report data.
1"""Functions for translating report data.""" 2 3# stdlib 4from __future__ import annotations 5 6from contextlib import suppress 7 8# module 9from avwx.static.core import CLOUD_TRANSLATIONS 10from avwx.structs import Cloud, Code, Number, ReportTrans, SharedData, Units 11 12 13def get_cardinal_direction(direction: float) -> str: 14 """Return the cardinal direction (NSEW) for a degree direction. 15 16 Wind Direction - Cheat Sheet: 17 18 (360) -- 011/012 -- 033/034 -- (045) -- 056/057 -- 078/079 -- (090) 19 20 (090) -- 101/102 -- 123/124 -- (135) -- 146/147 -- 168/169 -- (180) 21 22 (180) -- 191/192 -- 213/214 -- (225) -- 236/237 -- 258/259 -- (270) 23 24 (270) -- 281/282 -- 303/304 -- (315) -- 326/327 -- 348/349 -- (360) 25 """ 26 ret = "" 27 if not isinstance(direction, int): 28 direction = int(direction) 29 # Convert to range [0 360] 30 while direction < 0: 31 direction += 360 32 direction = direction % 360 33 if 304 <= direction <= 360 or 0 <= direction <= 56: 34 ret += "N" 35 if 304 <= direction <= 348: 36 if 327 <= direction <= 348: 37 ret += "N" 38 ret += "W" 39 elif 12 <= direction <= 56: 40 if 12 <= direction <= 33: 41 ret += "N" 42 ret += "E" 43 elif 124 <= direction <= 236: 44 ret += "S" 45 if 124 <= direction <= 168: 46 if 147 <= direction <= 168: 47 ret += "S" 48 ret += "E" 49 elif 192 <= direction <= 236: 50 if 192 <= direction <= 213: 51 ret += "S" 52 ret += "W" 53 elif 57 <= direction <= 123: 54 ret += "E" 55 if 57 <= direction <= 78: 56 ret += "NE" 57 elif 102 <= direction <= 123: 58 ret += "SE" 59 elif 237 <= direction <= 303: 60 ret += "W" 61 if 237 <= direction <= 258: 62 ret += "SW" 63 elif 282 <= direction <= 303: 64 ret += "NW" 65 return ret 66 67 68WIND_DIR_REPR = {"000": "Calm", "VRB": "Variable"} 69 70 71def wind( 72 direction: Number | None, 73 speed: Number | None, 74 gust: Number | None, 75 vardir: list[Number] | None = None, 76 unit: str = "kt", 77 *, 78 cardinals: bool = True, 79 spoken: bool = False, 80) -> str: 81 """Format wind elements into a readable sentence. 82 83 Returns the translation string. 84 85 Ex: NNE-020 (variable 010 to 040) at 14kt gusting to 20kt 86 """ 87 ret = "" 88 target = "spoken" if spoken else "repr" 89 # Wind direction 90 if direction: 91 if direction.repr in WIND_DIR_REPR: 92 ret += WIND_DIR_REPR[direction.repr] 93 elif direction.value is None: 94 ret += direction.repr 95 else: 96 if cardinals: 97 ret += f"{get_cardinal_direction(direction.value)}-" 98 ret += getattr(direction, target) 99 # Variable direction 100 if vardir and isinstance(vardir, list): 101 vardir = [getattr(var, target) for var in vardir] 102 ret += f" (variable {vardir[0]} to {vardir[1]})" 103 # Speed 104 if speed and speed.value: 105 ret += f" at {speed.value}{unit}" 106 # Gust 107 if gust and gust.value: 108 ret += f" gusting to {gust.value}{unit}" 109 return ret 110 111 112VIS_REPR = { 113 "P6": "Greater than 6sm ( >10km )", 114 "M1/2": "Less than .5sm ( <0.8km )", 115 "M1/4": "Less than .25sm ( <0.4km )", 116 "M1/8": "Less than .125sm ( <0.2km )", 117} 118 119 120def visibility(vis: Number | None, unit: str = "m") -> str: 121 """Format a visibility element into a string with both km and sm values. 122 123 Ex: 8km ( 5sm ) 124 """ 125 if not (vis and unit in {"m", "sm"}): 126 return "" 127 with suppress(KeyError): 128 return VIS_REPR[vis.repr] 129 if vis.value is None: 130 return "" 131 if unit == "m": 132 meters = vis.value 133 miles = meters * 0.000621371 134 converted = str(round(miles, 1)).replace(".0", "") + "sm" 135 value = str(round(meters / 1000, 1)).replace(".0", "") 136 unit = "km" 137 elif unit == "sm": 138 miles = vis.value or 0 139 kilometers = miles / 0.621371 140 converted = str(round(kilometers, 1)).replace(".0", "") + "km" 141 value = str(miles).replace(".0", "") 142 else: 143 return "" 144 return f"{value}{unit} ({converted})" 145 146 147def temperature(temp: Number | None, unit: str = "C") -> str: 148 """Format a temperature element into a string with both C and F values. 149 150 Used for both Temp and Dew. 151 152 Ex: 34°C (93°F) 153 """ 154 unit = unit.upper() 155 if not (temp and temp.value is not None and unit in {"C", "F"}): 156 return "" 157 if unit == "C": 158 fahrenheit = temp.value * 1.8 + 32 159 converted = f"{int(round(fahrenheit))}°F" 160 elif unit == "F": 161 celsius = (temp.value - 32) / 1.8 162 converted = f"{int(round(celsius))}°C" 163 else: 164 return "" 165 return f"{temp.value}°{unit} ({converted})" 166 167 168def altimeter(alt: Number | None, unit: str = "hPa") -> str: 169 """Format the altimeter element into a string with hPa and inHg values. 170 171 Ex: 30.11 inHg (10.20 hPa) 172 """ 173 if not (alt and alt.value is not None and unit in {"hPa", "inHg"}): 174 return "" 175 if unit == "hPa": 176 value = str(alt.value) 177 inches = round(alt.value / 33.8638866667, 2) 178 converted = str(inches).ljust(5, "0") + " inHg" 179 elif unit == "inHg": 180 value = str(alt.value).ljust(5, "0") 181 pascals = alt.value * 33.8638866667 182 converted = f"{int(round(pascals))} hPa" 183 else: 184 return "" 185 return f"{value} {unit} ({converted})" 186 187 188def clouds(values: list[Cloud] | None, unit: str = "ft") -> str: 189 """Format cloud list into a readable sentence. 190 191 Returns the translation string. 192 193 Ex: Broken layer at 2200ft (Cumulonimbus), Overcast layer at 3600ft - Reported AGL 194 """ 195 if values is None: 196 return "" 197 ret = [] 198 for cloud in values: 199 if cloud.base is None: 200 continue 201 cloud_str = CLOUD_TRANSLATIONS[cloud.type] 202 if cloud.modifier and cloud.modifier in CLOUD_TRANSLATIONS: 203 cloud_str += f" ({CLOUD_TRANSLATIONS[cloud.modifier]})" 204 ret.append(cloud_str.format(cloud.base * 100, unit)) 205 return ", ".join(ret) + " - Reported AGL" if ret else "Sky clear" 206 207 208def wx_codes(codes: list[Code]) -> str: 209 """Join WX code values, 210 211 Returns the translation string, 212 """ 213 return ", ".join(code.value for code in codes) 214 215 216def current_shared(wxdata: SharedData, units: Units) -> ReportTrans: 217 """Translate Visibility, Altimeter, Clouds, and Other,""" 218 return ReportTrans( 219 visibility=visibility(wxdata.visibility, units.visibility), 220 altimeter=altimeter(wxdata.altimeter, units.altimeter), 221 clouds=clouds(wxdata.clouds, units.altitude), 222 wx_codes=wx_codes(wxdata.wx_codes), 223 )
14def get_cardinal_direction(direction: float) -> str: 15 """Return the cardinal direction (NSEW) for a degree direction. 16 17 Wind Direction - Cheat Sheet: 18 19 (360) -- 011/012 -- 033/034 -- (045) -- 056/057 -- 078/079 -- (090) 20 21 (090) -- 101/102 -- 123/124 -- (135) -- 146/147 -- 168/169 -- (180) 22 23 (180) -- 191/192 -- 213/214 -- (225) -- 236/237 -- 258/259 -- (270) 24 25 (270) -- 281/282 -- 303/304 -- (315) -- 326/327 -- 348/349 -- (360) 26 """ 27 ret = "" 28 if not isinstance(direction, int): 29 direction = int(direction) 30 # Convert to range [0 360] 31 while direction < 0: 32 direction += 360 33 direction = direction % 360 34 if 304 <= direction <= 360 or 0 <= direction <= 56: 35 ret += "N" 36 if 304 <= direction <= 348: 37 if 327 <= direction <= 348: 38 ret += "N" 39 ret += "W" 40 elif 12 <= direction <= 56: 41 if 12 <= direction <= 33: 42 ret += "N" 43 ret += "E" 44 elif 124 <= direction <= 236: 45 ret += "S" 46 if 124 <= direction <= 168: 47 if 147 <= direction <= 168: 48 ret += "S" 49 ret += "E" 50 elif 192 <= direction <= 236: 51 if 192 <= direction <= 213: 52 ret += "S" 53 ret += "W" 54 elif 57 <= direction <= 123: 55 ret += "E" 56 if 57 <= direction <= 78: 57 ret += "NE" 58 elif 102 <= direction <= 123: 59 ret += "SE" 60 elif 237 <= direction <= 303: 61 ret += "W" 62 if 237 <= direction <= 258: 63 ret += "SW" 64 elif 282 <= direction <= 303: 65 ret += "NW" 66 return ret
Return the cardinal direction (NSEW) for a degree direction.
Wind Direction - Cheat Sheet:
(360) -- 011/012 -- 033/034 -- (045) -- 056/057 -- 078/079 -- (090)
(090) -- 101/102 -- 123/124 -- (135) -- 146/147 -- 168/169 -- (180)
(180) -- 191/192 -- 213/214 -- (225) -- 236/237 -- 258/259 -- (270)
(270) -- 281/282 -- 303/304 -- (315) -- 326/327 -- 348/349 -- (360)
72def wind( 73 direction: Number | None, 74 speed: Number | None, 75 gust: Number | None, 76 vardir: list[Number] | None = None, 77 unit: str = "kt", 78 *, 79 cardinals: bool = True, 80 spoken: bool = False, 81) -> str: 82 """Format wind elements into a readable sentence. 83 84 Returns the translation string. 85 86 Ex: NNE-020 (variable 010 to 040) at 14kt gusting to 20kt 87 """ 88 ret = "" 89 target = "spoken" if spoken else "repr" 90 # Wind direction 91 if direction: 92 if direction.repr in WIND_DIR_REPR: 93 ret += WIND_DIR_REPR[direction.repr] 94 elif direction.value is None: 95 ret += direction.repr 96 else: 97 if cardinals: 98 ret += f"{get_cardinal_direction(direction.value)}-" 99 ret += getattr(direction, target) 100 # Variable direction 101 if vardir and isinstance(vardir, list): 102 vardir = [getattr(var, target) for var in vardir] 103 ret += f" (variable {vardir[0]} to {vardir[1]})" 104 # Speed 105 if speed and speed.value: 106 ret += f" at {speed.value}{unit}" 107 # Gust 108 if gust and gust.value: 109 ret += f" gusting to {gust.value}{unit}" 110 return ret
Format wind elements into a readable sentence.
Returns the translation string.
Ex: NNE-020 (variable 010 to 040) at 14kt gusting to 20kt
121def visibility(vis: Number | None, unit: str = "m") -> str: 122 """Format a visibility element into a string with both km and sm values. 123 124 Ex: 8km ( 5sm ) 125 """ 126 if not (vis and unit in {"m", "sm"}): 127 return "" 128 with suppress(KeyError): 129 return VIS_REPR[vis.repr] 130 if vis.value is None: 131 return "" 132 if unit == "m": 133 meters = vis.value 134 miles = meters * 0.000621371 135 converted = str(round(miles, 1)).replace(".0", "") + "sm" 136 value = str(round(meters / 1000, 1)).replace(".0", "") 137 unit = "km" 138 elif unit == "sm": 139 miles = vis.value or 0 140 kilometers = miles / 0.621371 141 converted = str(round(kilometers, 1)).replace(".0", "") + "km" 142 value = str(miles).replace(".0", "") 143 else: 144 return "" 145 return f"{value}{unit} ({converted})"
Format a visibility element into a string with both km and sm values.
Ex: 8km ( 5sm )
148def temperature(temp: Number | None, unit: str = "C") -> str: 149 """Format a temperature element into a string with both C and F values. 150 151 Used for both Temp and Dew. 152 153 Ex: 34°C (93°F) 154 """ 155 unit = unit.upper() 156 if not (temp and temp.value is not None and unit in {"C", "F"}): 157 return "" 158 if unit == "C": 159 fahrenheit = temp.value * 1.8 + 32 160 converted = f"{int(round(fahrenheit))}°F" 161 elif unit == "F": 162 celsius = (temp.value - 32) / 1.8 163 converted = f"{int(round(celsius))}°C" 164 else: 165 return "" 166 return f"{temp.value}°{unit} ({converted})"
Format a temperature element into a string with both C and F values.
Used for both Temp and Dew.
Ex: 34°C (93°F)
169def altimeter(alt: Number | None, unit: str = "hPa") -> str: 170 """Format the altimeter element into a string with hPa and inHg values. 171 172 Ex: 30.11 inHg (10.20 hPa) 173 """ 174 if not (alt and alt.value is not None and unit in {"hPa", "inHg"}): 175 return "" 176 if unit == "hPa": 177 value = str(alt.value) 178 inches = round(alt.value / 33.8638866667, 2) 179 converted = str(inches).ljust(5, "0") + " inHg" 180 elif unit == "inHg": 181 value = str(alt.value).ljust(5, "0") 182 pascals = alt.value * 33.8638866667 183 converted = f"{int(round(pascals))} hPa" 184 else: 185 return "" 186 return f"{value} {unit} ({converted})"
Format the altimeter element into a string with hPa and inHg values.
Ex: 30.11 inHg (10.20 hPa)
189def clouds(values: list[Cloud] | None, unit: str = "ft") -> str: 190 """Format cloud list into a readable sentence. 191 192 Returns the translation string. 193 194 Ex: Broken layer at 2200ft (Cumulonimbus), Overcast layer at 3600ft - Reported AGL 195 """ 196 if values is None: 197 return "" 198 ret = [] 199 for cloud in values: 200 if cloud.base is None: 201 continue 202 cloud_str = CLOUD_TRANSLATIONS[cloud.type] 203 if cloud.modifier and cloud.modifier in CLOUD_TRANSLATIONS: 204 cloud_str += f" ({CLOUD_TRANSLATIONS[cloud.modifier]})" 205 ret.append(cloud_str.format(cloud.base * 100, unit)) 206 return ", ".join(ret) + " - Reported AGL" if ret else "Sky clear"
Format cloud list into a readable sentence.
Returns the translation string.
Ex: Broken layer at 2200ft (Cumulonimbus), Overcast layer at 3600ft - Reported AGL
209def wx_codes(codes: list[Code]) -> str: 210 """Join WX code values, 211 212 Returns the translation string, 213 """ 214 return ", ".join(code.value for code in codes)
Join WX code values,
Returns the translation string,