Files
flights_web/src/shared/utils/datetime/index.ts
T
gnezim 706b8f444b Clear the last 19 lint warnings — make check now passes clean
- BuyTicketButton / FlightsMiniListItem: narrow firstLeg/lastLeg with
  explicit null guards (throw / return '').
- FlightSchedule.tsx: `match?.[1] ?? iso` for the regex capture.
- OnlineBoardSearchPage + schedule/api: `split('T')[0] ?? iso` for the
  date-prefix extraction.
- ServicesPanel: icon lookup uses a third '' fallback instead of `!`.
- buildCountryCityRows: explicit `break` if cities[i] is undefined.
- useAppSettings: `match?.[1]` null-check before parseInt.
- datetime/index.ts: guard bare HH:MM capture groups together.
- ScheduleDetailsCatchAllRoute: drop unused `t` + useTranslation import.
- ScheduleDetailsPage.tsx: prefix unused `getLegs` with underscore.
- 4 seo/json-ld tests: drop now-redundant eslint-disable comments.
- calendarRange.test + api.test: prefix unused helper names with `_`.

Warning count: 19 → 0. make check (typecheck + lint + test) exits 0.
2026-04-20 09:30:34 +03:00

149 lines
5.1 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* Datetime utility functions.
*
* Pure functions ported from Angular pipes (DurationPipe, DatePipe).
* No Angular dependencies, no side effects.
*/
/** Match `ru`, `ru-RU`, `ru-ru`, `RU` — anything starting with `ru` */
function isRussianLocale(locale: string): boolean {
return locale.toLowerCase().startsWith("ru");
}
/**
* Format a duration given in total minutes into a human-readable string.
* Russian units mirror Angular's DurationPipe (SHORT-DAY='д.', SHORT-HOUR='ч.',
* SHORT-MIN='мин.') so values read as '1ч. 30мин.' not '1ч 30м'.
*
* Accepts either a short language code (`"ru"`) or a full BCP-47 locale
* (`"ru-ru"`).
*
* @example formatDuration(150) => "2h 30m"
* @example formatDuration(150, "ru") => "2ч. 30мин."
* @example formatDuration(150, "ru-ru") => "2ч. 30мин."
* @example formatDuration(0) => "0h 0m"
*/
export function formatDuration(
minutes: number,
locale: string = "en",
): string {
const ru = isRussianLocale(locale);
if (minutes < 0) return ru ? "Неизвестно" : "Unknown";
const days = Math.floor(minutes / (60 * 24));
const hours = Math.floor((minutes % (60 * 24)) / 60);
const mins = Math.floor(minutes % 60);
const units = ru
? { d: "д.", h: "ч.", m: "мин." }
: { d: "d", h: "h", m: "m" };
const daysPart = days > 0 ? `${days}${units.d} ` : "";
return `${daysPart}${hours}${units.h} ${mins}${units.m}`;
}
/**
* Format a date/ISO string into "HH:mm" time.
*
* @example formatTime("2025-01-15T10:30:00") => "10:30"
* @example formatTime(new Date(2025, 0, 15, 10, 30)) => "10:30"
*/
export function formatTime(date: string | Date): string {
const d = typeof date === "string" ? new Date(date) : date;
if (Number.isNaN(d.getTime())) return "";
const hours = String(d.getHours()).padStart(2, "0");
const minutes = String(d.getMinutes()).padStart(2, "0");
return `${hours}:${minutes}`;
}
/**
* Format a date into a localized date string.
*
* @example formatDate("2025-01-15", "ru") => "15 января 2025 г."
* @example formatDate("2025-01-15", "en") => "January 15, 2025"
*/
export function formatDate(
date: string | Date,
locale: string = "en",
): string {
const d = typeof date === "string" ? new Date(date) : date;
if (Number.isNaN(d.getTime())) return "";
return d.toLocaleDateString(isRussianLocale(locale) ? "ru-RU" : "en-US", {
year: "numeric",
month: "long",
day: "numeric",
});
}
const ISO_OFFSET_RE = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2})(?::\d{2})?(Z|[+-]\d{2}:?\d{2})?$/;
const BARE_HH_MM_RE = /^(\d{1,2}):(\d{2})(?::\d{2})?$/;
/**
* Return the HH:mm portion of an ISO string without reprojecting it to the
* browser's local timezone. The API sends strings like
* "2026-04-17T23:30:00+03:00"; `new Date(...)` would shift it to whatever
* locale the SSR/client runs in. We want the wall-clock value that was in
* the string. Plain "HH:mm" / "HH:mm:ss" inputs pass through unchanged.
*/
export function formatLocalTime(iso: string): string {
const full = ISO_OFFSET_RE.exec(iso);
if (full) return `${full[4]}:${full[5]}`;
const bare = BARE_HH_MM_RE.exec(iso);
if (bare?.[1] && bare[2]) return `${bare[1].padStart(2, "0")}:${bare[2]}`;
return formatTime(iso); // fall back to Date-based formatter
}
/**
* Extract the UTC offset (e.g. "+03:00", "-05:00", "UTC+00:00") from an
* ISO string. Returns "UTC±HH:mm" or "" if the string has no offset.
*/
export function formatUtcOffset(iso: string): string {
const m = ISO_OFFSET_RE.exec(iso);
const off = m?.[6];
if (!off) return "";
// Angular's captioned-time-group renders 'UTC&nbsp;{{ utc }}' — keep the
// non-breaking space so '15:30 UTC +03:00' reads the same across pages.
if (off === "Z") return "UTC\u00A0+00:00";
const normalized = off.includes(":") ? off : `${off.slice(0, 3)}:${off.slice(3)}`;
return `UTC\u00A0${normalized}`;
}
/**
* "DD.MM.YYYY" — matches the Angular board footer style.
*/
export function formatDayMonthYear(iso: string): string {
const m = ISO_OFFSET_RE.exec(iso);
if (m) return `${m[3]}.${m[2]}.${m[1]}`;
const d = new Date(iso);
if (Number.isNaN(d.getTime())) return "";
const day = String(d.getDate()).padStart(2, "0");
const month = String(d.getMonth() + 1).padStart(2, "0");
return `${day}.${month}.${d.getFullYear()}`;
}
/**
* Calculate the day offset between two dates (ignoring time).
* Returns 0 if same day, +1 if actual is the next day, etc.
*
* @example isDayChange("2025-01-15T23:00", "2025-01-16T01:00") => 1
*/
export function isDayChange(
scheduledDate: string | Date,
actualDate: string | Date,
): number {
const s = typeof scheduledDate === "string" ? new Date(scheduledDate) : scheduledDate;
const a = typeof actualDate === "string" ? new Date(actualDate) : actualDate;
if (Number.isNaN(s.getTime()) || Number.isNaN(a.getTime())) return 0;
// Compare calendar dates (ignoring time)
const sDay = new Date(s.getFullYear(), s.getMonth(), s.getDate());
const aDay = new Date(a.getFullYear(), a.getMonth(), a.getDate());
const diffMs = aDay.getTime() - sDay.getTime();
return Math.round(diffMs / (24 * 60 * 60 * 1000));
}