Files
wtr/lib/forecast-utils.ts
zv 91acdb39b8
Some checks failed
CI / Lint, test, typecheck and build (push) Has been cancelled
feat: add weather unit preferences
2026-07-04 20:16:11 +02:00

150 lines
5.7 KiB
TypeScript

import type { Language, TranslationKey } from "@/lib/i18n";
import { translate } from "@/lib/i18n";
import {
DEFAULT_TEMPERATURE_UNIT,
DEFAULT_WIND_SPEED_UNIT,
DEFAULT_PRECIPITATION_UNIT,
formatPrecipitation,
formatTemperature,
formatWindSpeed,
} from "@/lib/weather-utils";
import type { DailyForecast, HourlyForecast } from "@/types/forecast";
import type { PrecipitationUnit, TemperatureUnit, WindSpeedUnit } from "@/types/units";
export function getForecastConditionKey(code: number | null): TranslationKey {
if (code === 0) return "forecast.condition.clear";
if (code === 1 || code === 2) return "forecast.condition.partlyCloudy";
if (code === 3) return "forecast.condition.cloudy";
if (code === 45 || code === 48) return "forecast.condition.fog";
if (code !== null && code >= 51 && code <= 57) return "forecast.condition.drizzle";
if (code !== null && ((code >= 61 && code <= 67) || (code >= 80 && code <= 82))) return "forecast.condition.rain";
if (code !== null && ((code >= 71 && code <= 77) || code === 85 || code === 86)) return "forecast.condition.snow";
if (code !== null && code >= 95) return "forecast.condition.thunderstorm";
return "forecast.condition.unknown";
}
export function getForecastCondition(code: number | null, language: Language) {
return translate(language, getForecastConditionKey(code));
}
function isDryWeatherCode(code: number | null) {
return code === null || (code >= 0 && code <= 3) || code === 45 || code === 48;
}
export function getEffectiveHourlyWeatherCode(
hour: Pick<HourlyForecast, "weatherCode" | "precipitation" | "precipitationProbability">,
) {
const hasMeasuredPrecipitation = (hour.precipitation ?? 0) > 0;
const hasLikelyPrecipitation = (hour.precipitationProbability ?? 0) >= 70;
if (isDryWeatherCode(hour.weatherCode) && (hasMeasuredPrecipitation || hasLikelyPrecipitation)) return 61;
return hour.weatherCode;
}
export function formatForecastTemperature(
value: number | null,
language: Language,
unit: TemperatureUnit = DEFAULT_TEMPERATURE_UNIT,
) {
if (value === null) return "—";
return formatTemperature(value, language, unit);
}
export function formatForecastRainfall(
value: number | null,
language: Language,
unit: PrecipitationUnit = DEFAULT_PRECIPITATION_UNIT,
) {
if (value === null) return "—";
return formatPrecipitation(value, language, unit);
}
export function formatForecastWind(
value: number | null,
language: Language,
unit: WindSpeedUnit = DEFAULT_WIND_SPEED_UNIT,
) {
if (value === null) return "—";
return formatWindSpeed(value, language, unit);
}
function getWarsawForecastHour(date = new Date()) {
const parts = new Intl.DateTimeFormat("en-CA", {
timeZone: "Europe/Warsaw",
year: "numeric",
month: "2-digit",
day: "2-digit",
hour: "2-digit",
hourCycle: "h23",
}).formatToParts(date);
const getPart = (type: Intl.DateTimeFormatPartTypes) => parts.find((part) => part.type === type)?.value ?? "";
return `${getPart("year")}-${getPart("month")}-${getPart("day")}T${getPart("hour")}:00`;
}
function parseForecastHour(time: string) {
const match = time.match(/^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2})/);
if (!match) return null;
const [, year, month, day, hour, minute] = match;
const timestamp = Date.UTC(Number(year), Number(month) - 1, Number(day), Number(hour), Number(minute));
return Number.isFinite(timestamp) ? timestamp : null;
}
function getForecastHourOfDay(time: string) {
const match = time.match(/T(\d{2}):/);
return match ? Number(match[1]) : null;
}
export function getUpcomingHourlyForecast(hours: HourlyForecast[], limit = 24) {
const currentHour = getWarsawForecastHour();
const currentTimestamp = parseForecastHour(currentHour);
const upcomingHours =
currentTimestamp === null
? hours.filter((hour) => hour.time >= currentHour)
: hours.filter((hour) => {
const hourTimestamp = parseForecastHour(hour.time);
return hourTimestamp === null ? hour.time >= currentHour : hourTimestamp >= currentTimestamp;
});
if (upcomingHours.length) return upcomingHours.slice(0, limit);
const currentHourOfDay = getForecastHourOfDay(currentHour);
const fallbackHours =
currentHourOfDay === null
? hours
: hours.filter((hour) => {
const forecastHourOfDay = getForecastHourOfDay(hour.time);
return forecastHourOfDay === null || forecastHourOfDay >= currentHourOfDay;
});
return (fallbackHours.length ? fallbackHours : hours).slice(0, limit);
}
export function getHourlyForecastForDay(hours: HourlyForecast[], date: string) {
return hours.filter((hour) => hour.time.startsWith(`${date}T`));
}
export function getDailyForecastForHour(days: DailyForecast[], time: string) {
return days.find((day) => time.startsWith(`${day.date}T`)) ?? null;
}
export function isForecastNight(time: string, day: Pick<DailyForecast, "sunrise" | "sunset"> | null | undefined) {
const hourTimestamp = parseForecastHour(time);
const sunriseTimestamp = day?.sunrise ? parseForecastHour(day.sunrise) : null;
const sunsetTimestamp = day?.sunset ? parseForecastHour(day.sunset) : null;
if (hourTimestamp !== null && sunriseTimestamp !== null && sunsetTimestamp !== null) {
return hourTimestamp < sunriseTimestamp || hourTimestamp >= sunsetTimestamp;
}
const hour = getForecastHourOfDay(time);
return hour !== null && (hour < 6 || hour >= 21);
}
export function isForecastHourPast(time: string) {
const currentHour = getWarsawForecastHour();
const hourTimestamp = parseForecastHour(time);
const currentTimestamp = parseForecastHour(currentHour);
if (hourTimestamp === null || currentTimestamp === null) return time < currentHour;
return hourTimestamp < currentTimestamp;
}