chore: add prettier formatting
All checks were successful
CI / Lint, typecheck and build (push) Successful in 9m56s

This commit is contained in:
zv
2026-06-14 20:26:56 +02:00
parent 8bbd9397a1
commit ee55521803
79 changed files with 2451 additions and 969 deletions

View File

@@ -5,7 +5,12 @@ import type { HourlyForecast, WeatherForecast } from "@/types/forecast";
import type { WeatherWarning } from "@/types/imgw";
import type { Province } from "@/types/province";
import { warningMatchesCounty } from "@/lib/warning-regions";
import { DEFAULT_TEMPERATURE_UNIT, DEFAULT_WIND_SPEED_UNIT, formatTemperature as formatDisplayTemperature, formatWindSpeed } from "@/lib/weather-utils";
import {
DEFAULT_TEMPERATURE_UNIT,
DEFAULT_WIND_SPEED_UNIT,
formatTemperature as formatDisplayTemperature,
formatWindSpeed,
} from "@/lib/weather-utils";
import type { TemperatureUnit, WindSpeedUnit } from "@/types/units";
export type WeatherBriefSeverity = "normal" | "attention" | "warning";
@@ -74,14 +79,17 @@ function getWarsawDateKey(date: Date, dayOffset = 0) {
const part = (type: Intl.DateTimeFormatPartTypes) => parts.find((entry) => entry.type === type)?.value ?? "";
if (dayOffset === 0) return `${part("year")}-${part("month")}-${part("day")}`;
const shiftedDate = new Date(Date.UTC(Number(part("year")), Number(part("month")) - 1, Number(part("day")) + dayOffset, 12));
const shiftedDate = new Date(
Date.UTC(Number(part("year")), Number(part("month")) - 1, Number(part("day")) + dayOffset, 12),
);
const shiftedParts = new Intl.DateTimeFormat("en-CA", {
timeZone: "Europe/Warsaw",
year: "numeric",
month: "2-digit",
day: "2-digit",
}).formatToParts(shiftedDate);
const shiftedPart = (type: Intl.DateTimeFormatPartTypes) => shiftedParts.find((entry) => entry.type === type)?.value ?? "";
const shiftedPart = (type: Intl.DateTimeFormatPartTypes) =>
shiftedParts.find((entry) => entry.type === type)?.value ?? "";
return `${shiftedPart("year")}-${shiftedPart("month")}-${shiftedPart("day")}`;
}
@@ -98,7 +106,9 @@ function getUpcomingHours(hours: HourlyForecast[], now: Date, limit = 24) {
const currentTimestamp = parseForecastHour(currentHour);
const upcoming = hours.filter((hour) => {
const hourTimestamp = parseForecastHour(hour.time);
return hourTimestamp === null || currentTimestamp === null ? hour.time >= currentHour : hourTimestamp >= currentTimestamp;
return hourTimestamp === null || currentTimestamp === null
? hour.time >= currentHour
: hourTimestamp >= currentTimestamp;
});
return upcoming.slice(0, limit);
}
@@ -135,11 +145,18 @@ function getPeakHour(hours: HourlyForecast[], selector: (hour: HourlyForecast) =
}, null);
}
function isRelevantMeteoWarning(warning: WeatherWarning, province: Province | null, countyTeryt: string | null | undefined, now: number) {
function isRelevantMeteoWarning(
warning: WeatherWarning,
province: Province | null,
countyTeryt: string | null | undefined,
now: number,
) {
const validTo = getTimestamp(warning.validTo);
return warning.kind === "meteo"
&& (countyTeryt ? warningMatchesCounty(warning, countyTeryt) : !province || warning.provinces.includes(province))
&& (validTo === null || validTo > now);
return (
warning.kind === "meteo" &&
(countyTeryt ? warningMatchesCounty(warning, countyTeryt) : !province || warning.provinces.includes(province)) &&
(validTo === null || validTo > now)
);
}
function warningOverlapsWarsawDate(warning: WeatherWarning, dateKey: string) {
@@ -148,10 +165,17 @@ function warningOverlapsWarsawDate(warning: WeatherWarning, dateKey: string) {
return (!validFromKey || validFromKey <= dateKey) && (!validToKey || validToKey >= dateKey);
}
function isRelevantMeteoWarningForDate(warning: WeatherWarning, province: Province | null, countyTeryt: string | null | undefined, dateKey: string) {
return warning.kind === "meteo"
&& (countyTeryt ? warningMatchesCounty(warning, countyTeryt) : !province || warning.provinces.includes(province))
&& warningOverlapsWarsawDate(warning, dateKey);
function isRelevantMeteoWarningForDate(
warning: WeatherWarning,
province: Province | null,
countyTeryt: string | null | undefined,
dateKey: string,
) {
return (
warning.kind === "meteo" &&
(countyTeryt ? warningMatchesCounty(warning, countyTeryt) : !province || warning.provinces.includes(province)) &&
warningOverlapsWarsawDate(warning, dateKey)
);
}
function compareWarnings(a: WeatherWarning, b: WeatherWarning) {
@@ -164,7 +188,12 @@ function hasWeatherCode(hours: HourlyForecast[], predicate: (code: number) => bo
return hours.some((hour) => hour.weatherCode !== null && predicate(hour.weatherCode));
}
function getTomorrowCondition(hours: HourlyForecast[], rainfallTotal: number | null, maximumProbability: number | null, language: Language) {
function getTomorrowCondition(
hours: HourlyForecast[],
rainfallTotal: number | null,
maximumProbability: number | null,
language: Language,
) {
const hasThunderstorm = hasWeatherCode(hours, (code) => code >= 95);
const hasSnow = hasWeatherCode(hours, (code) => (code >= 71 && code <= 77) || code === 85 || code === 86);
const hasRain = hasWeatherCode(hours, (code) => (code >= 61 && code <= 67) || (code >= 80 && code <= 82));
@@ -199,7 +228,17 @@ function getTomorrowCondition(hours: HourlyForecast[], rainfallTotal: number | n
return language === "pl" ? "bez istotnych opadów" : "no significant precipitation";
}
export function buildWeatherBrief({ forecast, warnings, province, countyTeryt, locationName, language, temperatureUnit = DEFAULT_TEMPERATURE_UNIT, windSpeedUnit = DEFAULT_WIND_SPEED_UNIT, now = new Date() }: BuildWeatherBriefInput): WeatherBrief | null {
export function buildWeatherBrief({
forecast,
warnings,
province,
countyTeryt,
locationName,
language,
temperatureUnit = DEFAULT_TEMPERATURE_UNIT,
windSpeedUnit = DEFAULT_WIND_SPEED_UNIT,
now = new Date(),
}: BuildWeatherBriefInput): WeatherBrief | null {
const hours = getUpcomingHours(forecast.hourly, now, 24);
if (!hours.length) return null;
@@ -214,12 +253,17 @@ export function buildWeatherBrief({ forecast, warnings, province, countyTeryt, l
const maximumProbability = getMaximum(hours.map((hour) => hour.precipitationProbability));
const rainPeakHour = getPeakHour(hours, (hour) => hour.precipitationProbability);
const conditionPeakHour = hours.find((hour) => hour.weatherCode !== null) ?? null;
const temperatureRange = minimumTemperature !== null && maximumTemperature !== null
? `${formatDisplayTemperature(minimumTemperature, language, temperatureUnit)}-${formatDisplayTemperature(maximumTemperature, language, temperatureUnit)}`
: null;
const temperatureRange =
minimumTemperature !== null && maximumTemperature !== null
? `${formatDisplayTemperature(minimumTemperature, language, temperatureUnit)}-${formatDisplayTemperature(maximumTemperature, language, temperatureUnit)}`
: null;
const hasRainSignal = (maximumProbability ?? 0) >= 35 || (rainfallTotal ?? 0) >= 0.5;
const hasWindSignal = (maximumWind ?? 0) >= 8;
const severity: WeatherBriefSeverity = topWarning ? "warning" : hasRainSignal || hasWindSignal ? "attention" : "normal";
const severity: WeatherBriefSeverity = topWarning
? "warning"
: hasRainSignal || hasWindSignal
? "attention"
: "normal";
const provinceLabel = province ? formatProvinceName(province, language) : null;
const headline = topWarning
@@ -241,45 +285,71 @@ export function buildWeatherBrief({ forecast, warnings, province, countyTeryt, l
const body: string[] = [];
if (topWarning) {
const warningRegion = provinceLabel ? `${provinceLabel}: ` : "";
const probability = topWarning.probability !== null
? language === "pl" ? ` Prawdopodobieństwo: ${topWarning.probability}%.` : ` Probability: ${topWarning.probability}%.`
: "";
body.push(language === "pl"
? `${warningRegion}${topWarning.title || "ostrzeżenie meteorologiczne"}${topWarning.level !== null ? `, stopień ${topWarning.level}` : ""}.${probability}`
: `${warningRegion}${topWarning.title || "weather warning"}${topWarning.level !== null ? `, level ${topWarning.level}` : ""}.${probability}`);
const probability =
topWarning.probability !== null
? language === "pl"
? ` Prawdopodobieństwo: ${topWarning.probability}%.`
: ` Probability: ${topWarning.probability}%.`
: "";
body.push(
language === "pl"
? `${warningRegion}${topWarning.title || "ostrzeżenie meteorologiczne"}${topWarning.level !== null ? `, stopień ${topWarning.level}` : ""}.${probability}`
: `${warningRegion}${topWarning.title || "weather warning"}${topWarning.level !== null ? `, level ${topWarning.level}` : ""}.${probability}`,
);
}
if (temperatureRange) {
body.push(language === "pl"
? `Temperatura w najbliższych 24 godzinach: ${temperatureRange}.`
: `Temperature over the next 24 hours: ${temperatureRange}.`);
body.push(
language === "pl"
? `Temperatura w najbliższych 24 godzinach: ${temperatureRange}.`
: `Temperature over the next 24 hours: ${temperatureRange}.`,
);
}
if (rainfallTotal !== null || maximumProbability !== null) {
const rainWindow = rainPeakHour && maximumProbability !== null && maximumProbability >= 20
? language === "pl" ? ` Największa szansa około ${formatHour(rainPeakHour.time)}.` : ` Highest chance around ${formatHour(rainPeakHour.time)}.`
: "";
body.push(language === "pl"
? `Opad: ${rainfallTotal === null ? "brak pełnych danych" : formatRainfall(rainfallTotal, language)}, maks. szansa ${maximumProbability === null ? "brak danych" : `${maximumProbability}%`}.${rainWindow}`
: `Rainfall: ${rainfallTotal === null ? "not fully available" : formatRainfall(rainfallTotal, language)}, max chance ${maximumProbability === null ? "unavailable" : `${maximumProbability}%`}.${rainWindow}`);
const rainWindow =
rainPeakHour && maximumProbability !== null && maximumProbability >= 20
? language === "pl"
? ` Największa szansa około ${formatHour(rainPeakHour.time)}.`
: ` Highest chance around ${formatHour(rainPeakHour.time)}.`
: "";
body.push(
language === "pl"
? `Opad: ${rainfallTotal === null ? "brak pełnych danych" : formatRainfall(rainfallTotal, language)}, maks. szansa ${maximumProbability === null ? "brak danych" : `${maximumProbability}%`}.${rainWindow}`
: `Rainfall: ${rainfallTotal === null ? "not fully available" : formatRainfall(rainfallTotal, language)}, max chance ${maximumProbability === null ? "unavailable" : `${maximumProbability}%`}.${rainWindow}`,
);
}
if (maximumWind !== null) {
body.push(language === "pl"
? `Wiatr maksymalnie do ${formatWindSpeed(maximumWind, language, windSpeedUnit)}.`
: `Wind up to ${formatWindSpeed(maximumWind, language, windSpeedUnit)}.`);
body.push(
language === "pl"
? `Wiatr maksymalnie do ${formatWindSpeed(maximumWind, language, windSpeedUnit)}.`
: `Wind up to ${formatWindSpeed(maximumWind, language, windSpeedUnit)}.`,
);
}
if (conditionPeakHour) {
body.push(language === "pl"
? `Dominujący sygnał modelu: ${getForecastCondition(conditionPeakHour.weatherCode, language).toLowerCase()}.`
: `Model signal: ${getForecastCondition(conditionPeakHour.weatherCode, language).toLowerCase()}.`);
body.push(
language === "pl"
? `Dominujący sygnał modelu: ${getForecastCondition(conditionPeakHour.weatherCode, language).toLowerCase()}.`
: `Model signal: ${getForecastCondition(conditionPeakHour.weatherCode, language).toLowerCase()}.`,
);
}
const pushParts = [
`${locationName}:`,
temperatureRange,
maximumProbability !== null ? (language === "pl" ? `opad do ${maximumProbability}%` : `rain up to ${maximumProbability}%`) : null,
maximumWind !== null ? (language === "pl" ? `wiatr ${formatWindSpeed(maximumWind, language, windSpeedUnit)}` : `wind ${formatWindSpeed(maximumWind, language, windSpeedUnit)}`) : null,
maximumProbability !== null
? language === "pl"
? `opad do ${maximumProbability}%`
: `rain up to ${maximumProbability}%`
: null,
maximumWind !== null
? language === "pl"
? `wiatr ${formatWindSpeed(maximumWind, language, windSpeedUnit)}`
: `wind ${formatWindSpeed(maximumWind, language, windSpeedUnit)}`
: null,
].filter(Boolean);
const warningPrefix = topWarning
? language === "pl" ? `IMGW: ${topWarning.title || "ostrzeżenie"}. ` : `IMGW: ${topWarning.title || "warning"}. `
? language === "pl"
? `IMGW: ${topWarning.title || "ostrzeżenie"}. `
: `IMGW: ${topWarning.title || "warning"}. `
: "";
return {
@@ -292,7 +362,17 @@ export function buildWeatherBrief({ forecast, warnings, province, countyTeryt, l
};
}
export function buildTomorrowWeatherBrief({ forecast, warnings, province, countyTeryt, locationName, language, temperatureUnit = DEFAULT_TEMPERATURE_UNIT, windSpeedUnit = DEFAULT_WIND_SPEED_UNIT, now = new Date() }: BuildWeatherBriefInput): WeatherBrief | null {
export function buildTomorrowWeatherBrief({
forecast,
warnings,
province,
countyTeryt,
locationName,
language,
temperatureUnit = DEFAULT_TEMPERATURE_UNIT,
windSpeedUnit = DEFAULT_WIND_SPEED_UNIT,
now = new Date(),
}: BuildWeatherBriefInput): WeatherBrief | null {
const targetDateKey = getWarsawDateKey(now, 1);
const hours = getForecastHoursForDate(forecast.hourly, targetDateKey);
if (!hours.length) return null;
@@ -308,16 +388,19 @@ export function buildTomorrowWeatherBrief({ forecast, warnings, province, county
const maximumProbability = getMaximum(hours.map((hour) => hour.precipitationProbability));
const rainPeakHour = getPeakHour(hours, (hour) => hour.precipitationProbability);
const hasThunderstorm = hasWeatherCode(hours, (code) => code >= 95);
const hasRainSignal = hasThunderstorm
|| (rainfallTotal ?? 0) >= 0.5
|| (maximumProbability ?? 0) >= 35
|| hasWeatherCode(hours, (code) => (code >= 51 && code <= 67) || (code >= 80 && code <= 82));
const hasRainSignal =
hasThunderstorm ||
(rainfallTotal ?? 0) >= 0.5 ||
(maximumProbability ?? 0) >= 35 ||
hasWeatherCode(hours, (code) => (code >= 51 && code <= 67) || (code >= 80 && code <= 82));
const hasWindSignal = (maximumWind ?? 0) >= 8;
const condition = getTomorrowCondition(hours, rainfallTotal, maximumProbability, language);
const temperatureRange = minimumTemperature !== null && maximumTemperature !== null
? `${formatDisplayTemperature(minimumTemperature, language, temperatureUnit)} / ${formatDisplayTemperature(maximumTemperature, language, temperatureUnit)}`
: null;
const severity: WeatherBriefSeverity = topWarning || hasThunderstorm ? "warning" : hasRainSignal || hasWindSignal ? "attention" : "normal";
const temperatureRange =
minimumTemperature !== null && maximumTemperature !== null
? `${formatDisplayTemperature(minimumTemperature, language, temperatureUnit)} / ${formatDisplayTemperature(maximumTemperature, language, temperatureUnit)}`
: null;
const severity: WeatherBriefSeverity =
topWarning || hasThunderstorm ? "warning" : hasRainSignal || hasWindSignal ? "attention" : "normal";
const provinceLabel = province ? formatProvinceName(province, language) : null;
const headline = topWarning
@@ -343,49 +426,66 @@ export function buildTomorrowWeatherBrief({ forecast, warnings, province, county
const body: string[] = [];
if (topWarning) {
const warningRegion = provinceLabel ? `${provinceLabel}: ` : "";
const probability = topWarning.probability !== null
? language === "pl" ? ` Prawdopodobieństwo: ${topWarning.probability}%.` : ` Probability: ${topWarning.probability}%.`
: "";
body.push(language === "pl"
? `${warningRegion}${topWarning.title || "ostrzeżenie meteorologiczne"}${topWarning.level !== null ? `, stopień ${topWarning.level}` : ""}.${probability}`
: `${warningRegion}${topWarning.title || "weather warning"}${topWarning.level !== null ? `, level ${topWarning.level}` : ""}.${probability}`);
const probability =
topWarning.probability !== null
? language === "pl"
? ` Prawdopodobieństwo: ${topWarning.probability}%.`
: ` Probability: ${topWarning.probability}%.`
: "";
body.push(
language === "pl"
? `${warningRegion}${topWarning.title || "ostrzeżenie meteorologiczne"}${topWarning.level !== null ? `, stopień ${topWarning.level}` : ""}.${probability}`
: `${warningRegion}${topWarning.title || "weather warning"}${topWarning.level !== null ? `, level ${topWarning.level}` : ""}.${probability}`,
);
}
if (temperatureRange) {
body.push(language === "pl"
? `Temperatura jutro: ${temperatureRange}.`
: `Temperature tomorrow: ${temperatureRange}.`);
body.push(
language === "pl" ? `Temperatura jutro: ${temperatureRange}.` : `Temperature tomorrow: ${temperatureRange}.`,
);
}
body.push(language === "pl"
? `Sygnał modelu: ${condition}.`
: `Model signal: ${condition}.`);
body.push(language === "pl" ? `Sygnał modelu: ${condition}.` : `Model signal: ${condition}.`);
if (hasRainSignal || (rainfallTotal ?? 0) > 0) {
const rainWindow = rainPeakHour && maximumProbability !== null && maximumProbability >= 20
? language === "pl" ? ` Największa szansa około ${formatHour(rainPeakHour.time)}.` : ` Highest chance around ${formatHour(rainPeakHour.time)}.`
: "";
body.push(language === "pl"
? `Opad: ${rainfallTotal === null ? "brak pełnych danych" : formatRainfall(rainfallTotal, language)}, maks. szansa ${maximumProbability === null ? "brak danych" : `${maximumProbability}%`}.${rainWindow}`
: `Precipitation: ${rainfallTotal === null ? "not fully available" : formatRainfall(rainfallTotal, language)}, max chance ${maximumProbability === null ? "unavailable" : `${maximumProbability}%`}.${rainWindow}`);
const rainWindow =
rainPeakHour && maximumProbability !== null && maximumProbability >= 20
? language === "pl"
? ` Największa szansa około ${formatHour(rainPeakHour.time)}.`
: ` Highest chance around ${formatHour(rainPeakHour.time)}.`
: "";
body.push(
language === "pl"
? `Opad: ${rainfallTotal === null ? "brak pełnych danych" : formatRainfall(rainfallTotal, language)}, maks. szansa ${maximumProbability === null ? "brak danych" : `${maximumProbability}%`}.${rainWindow}`
: `Precipitation: ${rainfallTotal === null ? "not fully available" : formatRainfall(rainfallTotal, language)}, max chance ${maximumProbability === null ? "unavailable" : `${maximumProbability}%`}.${rainWindow}`,
);
} else {
body.push(language === "pl" ? "Bez istotnego opadu w prognozie." : "No significant precipitation in the forecast.");
}
if (maximumWind !== null) {
body.push(language === "pl"
? `Wiatr maksymalnie do ${formatWindSpeed(maximumWind, language, windSpeedUnit)}.`
: `Wind up to ${formatWindSpeed(maximumWind, language, windSpeedUnit)}.`);
body.push(
language === "pl"
? `Wiatr maksymalnie do ${formatWindSpeed(maximumWind, language, windSpeedUnit)}.`
: `Wind up to ${formatWindSpeed(maximumWind, language, windSpeedUnit)}.`,
);
}
const rainPushPart = (hasRainSignal || (rainfallTotal ?? 0) > 0.1) && rainfallTotal !== null
? formatRainfall(rainfallTotal, language)
: null;
const rainPushPart =
(hasRainSignal || (rainfallTotal ?? 0) > 0.1) && rainfallTotal !== null
? formatRainfall(rainfallTotal, language)
: null;
const pushParts = [
`${locationName}:`,
temperatureRange,
condition,
rainPushPart,
maximumWind !== null ? (language === "pl" ? `wiatr ${formatWindSpeed(maximumWind, language, windSpeedUnit)}` : `wind ${formatWindSpeed(maximumWind, language, windSpeedUnit)}`) : null,
maximumWind !== null
? language === "pl"
? `wiatr ${formatWindSpeed(maximumWind, language, windSpeedUnit)}`
: `wind ${formatWindSpeed(maximumWind, language, windSpeedUnit)}`
: null,
].filter(Boolean);
const warningPrefix = topWarning
? language === "pl" ? `IMGW: ${topWarning.title || "ostrzeżenie"}. ` : `IMGW: ${topWarning.title || "warning"}. `
? language === "pl"
? `IMGW: ${topWarning.title || "ostrzeżenie"}. `
: `IMGW: ${topWarning.title || "warning"}. `
: "";
return {