From 722be8c01effc38f33d0715c771c493bc0a8de09 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Filip=20R=C5=AF=C5=BEi=C4=8Dka?= Date: Thu, 12 Mar 2026 12:47:46 +0100 Subject: [PATCH] Enable adding GP prediction after Sprint --- fixtures/raceResults.ts | 111 ------------------ fixtures/sprintWeekend.ts | 149 +++++++++++++++++++++++++ src/common/getEventDate.test.ts | 118 ++++++++++++++++++++ src/common/getEventDate.ts | 13 +++ src/components/StandingsController.tsx | 8 +- 5 files changed, 286 insertions(+), 113 deletions(-) delete mode 100644 fixtures/raceResults.ts create mode 100644 fixtures/sprintWeekend.ts create mode 100644 src/common/getEventDate.test.ts create mode 100644 src/common/getEventDate.ts diff --git a/fixtures/raceResults.ts b/fixtures/raceResults.ts deleted file mode 100644 index a48011c..0000000 --- a/fixtures/raceResults.ts +++ /dev/null @@ -1,111 +0,0 @@ -import { IRaceResult } from "../src/types/api"; - -export const raceResults: IRaceResult[][] = [ - [ - { - Driver: { - driverId: "leclerc", - permanentNumber: 16, - code: "LEC", - url: "http://en.wikipedia.org/wiki/Charles_Leclerc", - givenName: "Charles", - familyName: "Leclerc", - dateOfBirth: "1997-10-16", - nationality: "Monegasque", - }, - Constructors: [ - { - constructorId: "ferrari", - url: "http://en.wikipedia.org/wiki/Scuderia_Ferrari", - name: "Ferrari", - nationality: "Italian", - }, - ], - fastestLap: false, - }, - { - Driver: { - driverId: "sainz", - permanentNumber: 55, - code: "SAI", - url: "http://en.wikipedia.org/wiki/Carlos_Sainz_Jr.", - givenName: "Carlos", - familyName: "Sainz", - dateOfBirth: "1994-09-01", - nationality: "Spanish", - }, - Constructors: [ - { - constructorId: "ferrari", - url: "http://en.wikipedia.org/wiki/Scuderia_Ferrari", - name: "Ferrari", - nationality: "Italian", - }, - ], - fastestLap: true, - }, - { - Driver: { - driverId: "perez", - permanentNumber: 11, - code: "PER", - url: "http://en.wikipedia.org/wiki/Sergio_P%C3%A9rez", - givenName: "Sergio", - familyName: "Pérez", - dateOfBirth: "1990-01-26", - nationality: "Mexican", - }, - Constructors: [ - { - constructorId: "red_bull", - url: "http://en.wikipedia.org/wiki/Red_Bull_Racing", - name: "Red Bull", - nationality: "Austrian", - }, - ], - fastestLap: false, - }, - { - Driver: { - driverId: "russell", - permanentNumber: 63, - code: "RUS", - url: "http://en.wikipedia.org/wiki/George_Russell_%28racing_driver%29", - givenName: "George", - familyName: "Russell", - dateOfBirth: "1998-02-15", - nationality: "British", - }, - Constructors: [ - { - constructorId: "mercedes", - url: "http://en.wikipedia.org/wiki/Mercedes-Benz_in_Formula_One", - name: "Mercedes", - nationality: "German", - }, - ], - fastestLap: false, - }, - { - Driver: { - driverId: "max_verstappen", - permanentNumber: 33, - code: "VER", - url: "http://en.wikipedia.org/wiki/Max_Verstappen", - givenName: "Max", - familyName: "Verstappen", - dateOfBirth: "1997-09-30", - nationality: "Dutch", - }, - Constructors: [ - { - constructorId: "red_bull", - url: "http://en.wikipedia.org/wiki/Red_Bull_Racing", - name: "Red Bull", - nationality: "Austrian", - }, - ], - fastestLap: false, - }, - ], -]; diff --git a/fixtures/sprintWeekend.ts b/fixtures/sprintWeekend.ts new file mode 100644 index 0000000..2f4652a --- /dev/null +++ b/fixtures/sprintWeekend.ts @@ -0,0 +1,149 @@ +import { RaceEvent, RaceType } from "../src/types/entities"; + +const imolaRace = { + season: "2022", + round: 4, + raceName: "Emilia Romagna Grand Prix", + Circuit: { + circuitId: "imola", + url: "http://en.wikipedia.org/wiki/Autodromo_Enzo_e_Dino_Ferrari", + circuitName: "Autodromo Enzo e Dino Ferrari", + Location: { + lat: 44.3439, + long: 11.7167, + locality: "Imola", + country: "Italy", + }, + }, + date: "2022-04-24", + time: "13:00:00Z", + FirstPractice: new Date("2022-04-22T11:30:00Z"), + Qualifying: new Date("2022-04-22T15:00:00Z"), + SecondPractice: new Date("2022-04-23T10:30:00Z"), + Sprint: new Date("2022-04-23T14:30:00Z"), +}; + +export const sprintRaceEvent: RaceEvent = { + Race: imolaRace, + eventType: RaceType.SPRINT_RACE, + id: "4-s", +}; + +export const grandPrixEvent: RaceEvent = { + Race: imolaRace, + eventType: RaceType.GRAND_PRIX, + id: "4", +}; + +/** + * A schedule with rounds 1-5, where round 4 (Imola) is a sprint weekend. + * Events are in chronological order (sprint before GP for round 4). + */ +export const sprintWeekendSchedule: RaceEvent[] = [ + { + Race: { + season: "2022", + round: 1, + raceName: "Bahrain Grand Prix", + Circuit: { + circuitId: "bahrain", + url: "http://en.wikipedia.org/wiki/Bahrain_International_Circuit", + circuitName: "Bahrain International Circuit", + Location: { + lat: 26.0325, + long: 50.5106, + locality: "Sakhir", + country: "Bahrain", + }, + }, + date: "2022-03-20", + time: "15:00:00Z", + FirstPractice: new Date("2022-03-18T12:00:00Z"), + SecondPractice: new Date("2022-03-18T15:00:00Z"), + ThirdPractice: new Date("2022-03-19T12:00:00Z"), + Qualifying: new Date("2022-03-19T15:00:00Z"), + }, + eventType: RaceType.GRAND_PRIX, + id: "1", + }, + { + Race: { + season: "2022", + round: 2, + raceName: "Saudi Arabian Grand Prix", + Circuit: { + circuitId: "jeddah", + url: "http://en.wikipedia.org/wiki/Jeddah_Street_Circuit", + circuitName: "Jeddah Corniche Circuit", + Location: { + lat: 21.6319, + long: 39.1044, + locality: "Jeddah", + country: "Saudi Arabia", + }, + }, + date: "2022-03-27", + time: "17:00:00Z", + FirstPractice: new Date("2022-03-25T14:00:00Z"), + SecondPractice: new Date("2022-03-25T17:00:00Z"), + ThirdPractice: new Date("2022-03-26T14:00:00Z"), + Qualifying: new Date("2022-03-26T17:00:00Z"), + }, + eventType: RaceType.GRAND_PRIX, + id: "2", + }, + { + Race: { + season: "2022", + round: 3, + raceName: "Australian Grand Prix", + Circuit: { + circuitId: "albert_park", + url: "http://en.wikipedia.org/wiki/Melbourne_Grand_Prix_Circuit", + circuitName: "Albert Park Grand Prix Circuit", + Location: { + lat: -37.8497, + long: 144.968, + locality: "Melbourne", + country: "Australia", + }, + }, + date: "2022-04-10", + time: "05:00:00Z", + FirstPractice: new Date("2022-04-08T03:00:00Z"), + SecondPractice: new Date("2022-04-08T06:00:00Z"), + ThirdPractice: new Date("2022-04-09T03:00:00Z"), + Qualifying: new Date("2022-04-09T06:00:00Z"), + }, + eventType: RaceType.GRAND_PRIX, + id: "3", + }, + sprintRaceEvent, + grandPrixEvent, + { + Race: { + season: "2022", + round: 5, + raceName: "Miami Grand Prix", + Circuit: { + circuitId: "miami", + url: "http://en.wikipedia.org/wiki/Miami_International_Autodrome", + circuitName: "Miami International Autodrome", + Location: { + lat: 25.9581, + long: -80.2389, + locality: "Miami", + country: "USA", + }, + }, + date: "2022-05-08", + time: "19:30:00Z", + FirstPractice: new Date("2022-05-06T18:30:00Z"), + SecondPractice: new Date("2022-05-06T21:30:00Z"), + ThirdPractice: new Date("2022-05-07T17:00:00Z"), + Qualifying: new Date("2022-05-07T20:00:00Z"), + }, + eventType: RaceType.GRAND_PRIX, + id: "5", + }, +]; diff --git a/src/common/getEventDate.test.ts b/src/common/getEventDate.test.ts new file mode 100644 index 0000000..c9c12d4 --- /dev/null +++ b/src/common/getEventDate.test.ts @@ -0,0 +1,118 @@ +import { afterEach, describe, expect, test, vi } from "vitest"; + +import { + grandPrixEvent, + sprintRaceEvent, + sprintWeekendSchedule, +} from "../../fixtures/sprintWeekend"; +import { RaceEvent, RaceType } from "../types/entities"; +import { getEventDate } from "./getEventDate"; + +describe("getEventDate", () => { + test("returns Sprint date for a sprint race event", () => { + const result = getEventDate(sprintRaceEvent); + expect(result).toEqual(new Date("2022-04-23T14:30:00Z")); + }); + + test("returns race date+time for a grand prix event", () => { + const result = getEventDate(grandPrixEvent); + expect(result).toEqual(new Date("2022-04-24T13:00:00Z")); + }); + + test("returns undefined for a sprint race event without Sprint date", () => { + const event: RaceEvent = { + ...sprintRaceEvent, + Race: { ...sprintRaceEvent.Race, Sprint: undefined }, + }; + const result = getEventDate(event); + expect(result).toBeUndefined(); + }); + + test("returns undefined for a GP event without date", () => { + const event: RaceEvent = { + ...grandPrixEvent, + Race: { ...grandPrixEvent.Race, date: "" }, + }; + const result = getEventDate(event); + expect(result).toBeUndefined(); + }); + + test("returns undefined for a GP event without time", () => { + const event: RaceEvent = { + ...grandPrixEvent, + Race: { ...grandPrixEvent.Race, time: "" }, + }; + const result = getEventDate(event); + expect(result).toBeUndefined(); + }); +}); + +describe("lastRoundIndex logic during sprint weekend", () => { + afterEach(() => { + vi.useRealTimers(); + }); + + function findLastRound( + eventSchedule: RaceEvent[], + standingsRound: number, + now: Date, + ) { + const reversedSchedule = eventSchedule.toReversed(); + const lastRoundIndex = reversedSchedule.findIndex((event) => { + const eventDate = getEventDate(event); + return ( + event.Race.round <= standingsRound && + eventDate !== undefined && + eventDate <= now + ); + }); + return reversedSchedule[lastRoundIndex]; + } + + test("after sprint only: lastRound is the sprint, not the GP", () => { + // Between sprint (Apr 23 14:30Z) and GP (Apr 24 13:00Z) + const now = new Date("2022-04-23T18:00:00Z"); + const lastRound = findLastRound(sprintWeekendSchedule, 4, now); + + expect(lastRound).toBeDefined(); + expect(lastRound.eventType).toBe(RaceType.SPRINT_RACE); + expect(lastRound.id).toBe("4-s"); + }); + + test("after both sprint and GP: lastRound is the GP", () => { + // After GP (Apr 24 13:00Z) + const now = new Date("2022-04-24T16:00:00Z"); + const lastRound = findLastRound(sprintWeekendSchedule, 4, now); + + expect(lastRound).toBeDefined(); + expect(lastRound.eventType).toBe(RaceType.GRAND_PRIX); + expect(lastRound.id).toBe("4"); + }); + + test("before any events in the round: no match", () => { + // Before sprint (Apr 23 14:30Z), standings still at round 3 + const now = new Date("2022-04-22T10:00:00Z"); + const lastRound = findLastRound(sprintWeekendSchedule, 3, now); + + // Round 3 (Australian GP on Apr 10) is in the past, so it should match + expect(lastRound).toBeDefined(); + expect(lastRound.Race.round).toBe(3); + }); + + test("normal weekend after GP: lastRound is the GP", () => { + // After Australian GP (Apr 10 05:00Z) + const now = new Date("2022-04-10T08:00:00Z"); + const lastRound = findLastRound(sprintWeekendSchedule, 3, now); + + expect(lastRound).toBeDefined(); + expect(lastRound.eventType).toBe(RaceType.GRAND_PRIX); + expect(lastRound.Race.round).toBe(3); + }); + + test("no races yet: returns undefined", () => { + const now = new Date("2022-01-01T00:00:00Z"); + const lastRound = findLastRound(sprintWeekendSchedule, 0, now); + + expect(lastRound).toBeUndefined(); + }); +}); diff --git a/src/common/getEventDate.ts b/src/common/getEventDate.ts new file mode 100644 index 0000000..1f1a2ed --- /dev/null +++ b/src/common/getEventDate.ts @@ -0,0 +1,13 @@ +import { RaceEvent, RaceType } from "../types/entities"; + +export function getEventDate(event: RaceEvent): Date | undefined { + if (event.eventType === RaceType.SPRINT_RACE) { + return event.Race.Sprint ? new Date(event.Race.Sprint) : undefined; + } + + if (event.Race.date && event.Race.time) { + return new Date(`${event.Race.date}T${event.Race.time}`); + } + + return undefined; +} diff --git a/src/components/StandingsController.tsx b/src/components/StandingsController.tsx index c7f247c..ec9dc68 100644 --- a/src/components/StandingsController.tsx +++ b/src/components/StandingsController.tsx @@ -1,8 +1,9 @@ import { useState } from "react"; +import { getEventDate } from "../common/getEventDate"; import { getStandingsAfterRounds } from "../services/standings"; import { useStore } from "../store"; -import { RaceType, UpcomingRaceResult } from "../types/entities"; +import { UpcomingRaceResult } from "../types/entities"; import { LoadingLayout } from "./LoadingLayout"; import { NoSeasonData } from "./NoSeasonData"; import { Standings } from "./Standings/Standings"; @@ -37,10 +38,13 @@ export function StandingsController() { const reversedSchedule = eventSchedule.toReversed(); + const now = new Date(); const lastRoundIndex = reversedSchedule.findIndex((event) => { + const eventDate = getEventDate(event); return ( event.Race.round <= (driverStandings?.round || 0) && - event.eventType === RaceType.GRAND_PRIX + eventDate !== undefined && + eventDate <= now ); });