tz = new \DateTimeZone('Europe/Berlin'); } /** @return array */ private function hourly(): array { return json_decode((string) file_get_contents(__DIR__ . '/fixtures/forecast.json'), true)['hourly']; } public function testGroupsByDateAndSamplesEveryOtherHour(): void { $byDate = HourlyForecastMapper::mapByDate($this->hourly(), $this->tz); self::assertSame(['2026-09-11', '2026-09-12', '2026-09-13', '2026-09-14', '2026-09-15', '2026-09-16'], array_keys($byDate)); self::assertCount(12, $byDate['2026-09-11']); $hours = array_map( fn(array $p) => (int) (new \DateTimeImmutable($p['at']))->format('H'), $byDate['2026-09-11'], ); self::assertSame([0, 2, 4, 6, 8, 10, 12, 14, 16, 18, 20, 22], $hours); } public function testPointShapeAndValues(): void { $byDate = HourlyForecastMapper::mapByDate($this->hourly(), $this->tz); $noon = $byDate['2026-09-11'][6]; // index 6 = 12:00 (0,2,4,6,8,10,12,...) self::assertSame(['at', 'temp', 'precip_mm', 'wind_kmh', 'wind_dir'], array_keys($noon)); self::assertSame('2026-09-11T12:00:00+02:00', $noon['at']); self::assertIsFloat($noon['temp']); self::assertIsFloat($noon['precip_mm']); self::assertIsFloat($noon['wind_kmh']); self::assertIsInt($noon['wind_dir']); } public function testRainOnlyOnTheHoursThatHaveIt(): void { $byDate = HourlyForecastMapper::mapByDate($this->hourly(), $this->tz); // 2026-09-14 heeft 8,2 mm in de dagfixture, verdeeld over 14-16u; de bemonsterde uren zijn 0,2,4,..22. $rainy = array_filter($byDate['2026-09-14'], fn(array $p) => $p['precip_mm'] > 0); self::assertNotEmpty($rainy); foreach ($rainy as $p) { self::assertGreaterThanOrEqual(14, (int) (new \DateTimeImmutable($p['at']))->format('H')); } } public function testDaylightSavingDayWithTwentyFiveHoursSamplesWithoutError(): void { $hourly = json_decode((string) file_get_contents(__DIR__ . '/fixtures/hourly-dst.json'), true); $byDate = HourlyForecastMapper::mapByDate($hourly, $this->tz); self::assertCount(13, $byDate['2026-10-25'], '25 punten, om de 2: index 0,2,...,24 = 13 stuks'); } public function testNullValuesStayNull(): void { $hourly = $this->hourly(); $hourly['temperature_2m'][0] = null; $hourly['wind_direction_10m'][0] = 'kapot'; $byDate = HourlyForecastMapper::mapByDate($hourly, $this->tz); self::assertNull($byDate['2026-09-11'][0]['temp']); self::assertNull($byDate['2026-09-11'][0]['wind_dir']); } public function testMalformedTimestampsAreSkipped(): void { $byDate = HourlyForecastMapper::mapByDate([ 'time' => ['niet-een-datum', '2026-09-11T05:00'], 'temperature_2m' => [1.0, 2.0], 'precipitation' => [0.0, 0.0], 'wind_speed_10m' => [1.0, 1.0], 'wind_direction_10m' => [1, 1], ], $this->tz); self::assertSame(['2026-09-11'], array_keys($byDate)); self::assertCount(1, $byDate['2026-09-11']); } public function testMissingTimeArrayGivesEmptyResult(): void { self::assertSame([], HourlyForecastMapper::mapByDate([], $this->tz)); } }