///|
fn taf_error_at(
token : Token,
code : DiagnosticCode,
message : String,
) -> Diagnostic {
Diagnostic::new(Error, code, message, token.span, token=token.lexeme)
}
///|
fn taf_warning_at(
token : Token,
code : DiagnosticCode,
message : String,
) -> Diagnostic {
Diagnostic::new(Warning, code, message, token.span, token=token.lexeme)
}
///|
fn default_taf_time() -> TafTime {
{ day: 1, hour: 0, minute: 0 }
}
///|
fn empty_conditions() -> TafConditions {
{
wind: None,
variable_wind: None,
visibility: None,
rvr: [],
cavok: false,
weather: [],
clouds: [],
raw_tokens: [],
}
}
///|
fn parse_taf_time4(text : StringView) -> TafTime? {
if text.length() != 4 || !is_digits(text) {
None
} else {
let day = parse_unsigned(text.sub(end=2)).unwrap()
let hour = parse_unsigned(text.sub(start=2)).unwrap()
if day >= 1 && day <= 31 && hour >= 0 && hour <= 24 {
Some({ day, hour, minute: 0 })
} else {
None
}
}
}
///|
fn parse_taf_time6(text : StringView) -> TafTime? {
if text.length() != 6 || !is_digits(text) {
None
} else {
let day = parse_unsigned(text.sub(end=2)).unwrap()
let hour = parse_unsigned(text.sub(start=2, end=4)).unwrap()
let minute = parse_unsigned(text.sub(start=4)).unwrap()
if day >= 1 &&
day <= 31 &&
hour >= 0 &&
hour <= 23 &&
minute >= 0 &&
minute <= 59 {
Some({ day, hour, minute })
} else {
None
}
}
}
///|
fn parse_taf_period_token(token : Token) -> Result[TafPeriod, Diagnostic] {
match token.lexeme.split_once("/") {
Some((start_text, end_text)) =>
match (parse_taf_time4(start_text), parse_taf_time4(end_text)) {
(Some(start), Some(end)) => Ok({ start, end })
_ =>
Err(
taf_error_at(
token,
InvalidTafPeriod,
"TAF period must use DDHH/DDHH with valid day and hour",
),
)
}
None =>
Err(
taf_error_at(token, InvalidTafPeriod, "TAF period must use DDHH/DDHH"),
)
}
}
///|
fn parse_fm_token(token : Token) -> Result[TafTime, Diagnostic] {
if token.lexeme.has_prefix("FM") {
match parse_taf_time6(token.lexeme.sub(start=2)) {
Some(value) => Ok(value)
None =>
Err(
taf_error_at(
token,
InvalidTafChange,
"FM change group must use FMDDHHMM",
),
)
}
} else {
Err(taf_error_at(token, InvalidTafChange, "expected FMDDHHMM"))
}
}
///|
fn parse_probability_token(token : Token) -> Int? {
if token.lexeme.has_prefix("PROB") && token.lexeme.length() == 6 {
match parse_unsigned(token.lexeme.sub(start=4)) {
Some(value) => if value >= 0 && value <= 99 { Some(value) } else { None }
None => None
}
} else {
None
}
}
///|
fn is_taf_change_marker(token : Token) -> Bool {
token.lexeme.has_prefix("FM") ||
token.lexeme == "BECMG" ||
token.lexeme == "TEMPO" ||
parse_probability_token(token) is Some(_)
}
///|
fn find_next_taf_change(tokens : Array[Token], start : Int) -> Int {
let mut index = start
while index < tokens.length() && !is_taf_change_marker(tokens[index]) {
index += 1
}
index
}
///|
fn parse_taf_condition_tokens(
tokens : Array[Token],
start : Int,
end : Int,
diagnostics : Array[Diagnostic],
) -> TafConditions {
let mut conditions = empty_conditions()
let mut index = start
while index < end {
let token = tokens[index]
let raw = conditions.raw_tokens.copy()
raw.push(token.lexeme)
conditions = { ..conditions, raw_tokens: raw }
if looks_like_wind(token.lexeme) {
match parse_wind_token(token) {
Ok(wind) => conditions = { ..conditions, wind: Some(wind) }
Err(diagnostic) => diagnostics.push(diagnostic)
}
} else {
match parse_variable_wind_token(token) {
Some(variable_wind) =>
conditions = { ..conditions, variable_wind: Some(variable_wind) }
None =>
match parse_visibility_token(token) {
Some(visibility) =>
conditions = {
..conditions,
visibility: Some(visibility),
cavok: token.lexeme == "CAVOK",
}
None =>
if looks_like_rvr(token.lexeme) {
match parse_rvr_token(token) {
Ok(rvr) => {
let rvr_items = conditions.rvr.copy()
rvr_items.push(rvr)
conditions = { ..conditions, rvr: rvr_items }
}
Err(diagnostic) => diagnostics.push(diagnostic)
}
} else {
match parse_weather_token(token) {
Some(weather) => {
let weather_items = conditions.weather.copy()
weather_items.push(weather)
conditions = { ..conditions, weather: weather_items }
}
None =>
match parse_cloud_token(token) {
Some(cloud) => {
let cloud_items = conditions.clouds.copy()
cloud_items.push(cloud)
conditions = { ..conditions, clouds: cloud_items }
}
None =>
diagnostics.push(
taf_warning_at(
token,
UnknownToken,
"unrecognized TAF condition token",
),
)
}
}
}
}
}
}
index += 1
}
conditions
}
///|
fn label_for_taf_change(kind : TafChangeKind) -> String {
match kind {
From => "FM"
Becoming => "BECMG"
Temporary => "TEMPO"
Probability => "PROB"
ProbabilityTemporary => "PROB TEMPO"
}
}
///|
fn next_from_time_after(
changes : Array[TafChangeGroup],
start_index : Int,
) -> TafTime? {
let mut index = start_index + 1
let mut found : TafTime? = None
while index < changes.length() && found is None {
match changes[index].from_time {
Some(time) => found = Some(time)
None => ()
}
index += 1
}
found
}
///|
fn push_taf_change(
changes : Array[TafChangeGroup],
kind : TafChangeKind,
period : TafPeriod?,
from_time : TafTime?,
probability_percent : Int?,
conditions : TafConditions,
) -> Unit {
changes.push({ kind, period, from_time, probability_percent, conditions })
}
///|
/// Parse a common TAF report with initial conditions and first-class FM, BECMG,
/// TEMPO, PROB and PROB TEMPO change groups.
pub fn parse_taf(source : String) -> ParseResult[TafReport] {
let diagnostics : Array[Diagnostic] = []
let tokens = tokenize(source)
if tokens.length() == 0 {
diagnostics.push(
Diagnostic::new(
Error,
EmptyInput,
"TAF report must not be empty",
Span::new(0, 0),
),
)
return { value: None, diagnostics }
}
let mut index = 0
if tokens[index].lexeme == "TAF" {
index += 1
}
if index < tokens.length() &&
(tokens[index].lexeme == "AMD" || tokens[index].lexeme == "COR") {
index += 1
}
if index >= tokens.length() || !is_station_code(tokens[index].lexeme) {
let anchor = if index < tokens.length() { tokens[index] } else { tokens[0] }
diagnostics.push(
taf_error_at(
anchor,
InvalidStation,
"TAF station code must be four uppercase letters/digits",
),
)
return { value: None, diagnostics }
}
let station = tokens[index].lexeme
index += 1
let mut issue_time : ObservationTime? = None
if index < tokens.length() && tokens[index].lexeme.has_suffix("Z") {
match parse_observation_time_token(tokens[index]) {
Ok(value) => issue_time = Some(value)
Err(diagnostic) => diagnostics.push(diagnostic)
}
index += 1
}
if index >= tokens.length() {
diagnostics.push(
Diagnostic::new(
Error,
InvalidTafPeriod,
"TAF valid period is missing",
Span::new(0, 0),
),
)
return { value: None, diagnostics }
}
let valid_period = match parse_taf_period_token(tokens[index]) {
Ok(value) => value
Err(diagnostic) => {
diagnostics.push(diagnostic)
{ start: default_taf_time(), end: default_taf_time() }
}
}
index += 1
let initial_end = find_next_taf_change(tokens, index)
let initial = parse_taf_condition_tokens(
tokens, index, initial_end, diagnostics,
)
index = initial_end
let changes : Array[TafChangeGroup] = []
while index < tokens.length() {
let marker = tokens[index]
if marker.lexeme.has_prefix("FM") {
let from_time = match parse_fm_token(marker) {
Ok(value) => Some(value)
Err(diagnostic) => {
diagnostics.push(diagnostic)
None
}
}
index += 1
let group_end = find_next_taf_change(tokens, index)
let conditions = parse_taf_condition_tokens(
tokens, index, group_end, diagnostics,
)
push_taf_change(changes, From, None, from_time, None, conditions)
index = group_end
} else if marker.lexeme == "BECMG" || marker.lexeme == "TEMPO" {
let kind = if marker.lexeme == "BECMG" { Becoming } else { Temporary }
index += 1
if index >= tokens.length() {
diagnostics.push(
taf_error_at(marker, InvalidTafPeriod, "TAF change period is missing"),
)
push_taf_change(changes, kind, None, None, None, empty_conditions())
} else {
let period = match parse_taf_period_token(tokens[index]) {
Ok(value) => Some(value)
Err(diagnostic) => {
diagnostics.push(diagnostic)
None
}
}
index += 1
let group_end = find_next_taf_change(tokens, index)
let conditions = parse_taf_condition_tokens(
tokens, index, group_end, diagnostics,
)
push_taf_change(changes, kind, period, None, None, conditions)
index = group_end
}
} else {
match parse_probability_token(marker) {
Some(probability) => {
index += 1
let mut kind = Probability
if index < tokens.length() && tokens[index].lexeme == "TEMPO" {
kind = ProbabilityTemporary
index += 1
}
if index >= tokens.length() {
diagnostics.push(
taf_error_at(marker, InvalidTafPeriod, "PROB period is missing"),
)
push_taf_change(
changes,
kind,
None,
None,
Some(probability),
empty_conditions(),
)
} else {
let period = match parse_taf_period_token(tokens[index]) {
Ok(value) => Some(value)
Err(diagnostic) => {
diagnostics.push(diagnostic)
None
}
}
index += 1
let group_end = find_next_taf_change(tokens, index)
let conditions = parse_taf_condition_tokens(
tokens, index, group_end, diagnostics,
)
push_taf_change(
changes,
kind,
period,
None,
Some(probability),
conditions,
)
index = group_end
}
}
None => {
diagnostics.push(
taf_warning_at(
marker,
UnknownToken,
"unrecognized TAF change marker",
),
)
index += 1
}
}
}
}
{
value: Some({
station,
issue_time,
valid_period,
initial,
changes,
raw: source,
}),
diagnostics,
}
}
///|
/// Expand a parsed TAF into visualization-friendly timeline entries. FM groups
/// become replacement intervals ending at the next FM or the forecast end;
/// BECMG/TEMPO/PROB groups keep their explicit periods as overlays.
pub fn taf_timeline(report : TafReport) -> Array[TafTimelineEntry] {
let entries : Array[TafTimelineEntry] = []
entries.push({
label: "BASE",
period: Some(report.valid_period),
conditions: report.initial,
probability_percent: None,
})
let mut index = 0
while index < report.changes.length() {
let change = report.changes[index]
let period = match change.kind {
From =>
match change.from_time {
Some(start) => {
let end = match next_from_time_after(report.changes, index) {
Some(value) => value
None => report.valid_period.end
}
Some({ start, end })
}
None => None
}
_ => change.period
}
entries.push({
label: label_for_taf_change(change.kind),
period,
conditions: change.conditions,
probability_percent: change.probability_percent,
})
index += 1
}
entries
}