public static Guess performGuess(List<Date> releaseDates) {
        if (releaseDates.size() <= 1) {
            return new Guess(Schedule.UNKNOWN, null, null);
        } else if (releaseDates.size() > MAX_DATA_POINTS) {
            releaseDates = releaseDates.subList(releaseDates.size() - MAX_DATA_POINTS, releaseDates.size());
        }
        Stats stats = getStats(releaseDates);
        final int maxTotalWrongDays = Math.max(1, releaseDates.size() / 5);
        final int maxSingleDayOff = releaseDates.size() / 10;

        GregorianCalendar last = new GregorianCalendar();
        last.setTime(releaseDates.get(releaseDates.size() - 1));
        last.set(Calendar.HOUR_OF_DAY, (int) stats.medianHour);
        last.set(Calendar.MINUTE, (int) ((stats.medianHour - Math.floor(stats.medianHour)) * 60));
        last.set(Calendar.SECOND, 0);
        last.set(Calendar.MILLISECOND, 0);

        if (Math.abs(stats.medianDistance - ONE_DAY) < 2 * ONE_HOUR
                && stats.avgDeltaToMedianDistance < 2 * ONE_HOUR) {
            addTime(last, ONE_DAY);
            return new Guess(Schedule.DAILY, Arrays.asList(Calendar.MONDAY, Calendar.TUESDAY, Calendar.WEDNESDAY,
                    Calendar.THURSDAY, Calendar.FRIDAY, Calendar.SATURDAY, Calendar.SUNDAY), last.getTime());
        } else if (Math.abs(stats.medianDistance - ONE_WEEK) < ONE_DAY
                && stats.avgDeltaToMedianDistance < 2 * ONE_DAY) {
            // Just using last.set(Calendar.DAY_OF_WEEK) could skip a week
            // when the last release is delayed over week boundaries
            addTime(last, 3 * ONE_DAY);
            do {
                addTime(last, ONE_DAY);
            } while (last.get(Calendar.DAY_OF_WEEK) != stats.mostOftenDayOfWeek);
            return new Guess(Schedule.WEEKLY, List.of(stats.mostOftenDayOfWeek), last.getTime());
        } else if (Math.abs(stats.medianDistance - 2 * ONE_WEEK) < ONE_DAY
                && stats.avgDeltaToMedianDistance < 2 * ONE_DAY) {
            // Just using last.set(Calendar.DAY_OF_WEEK) could skip a week
            // when the last release is delayed over week boundaries
            addTime(last, 10 * ONE_DAY);
            do {
                addTime(last, ONE_DAY);
            } while (last.get(Calendar.DAY_OF_WEEK) != stats.mostOftenDayOfWeek);
            return new Guess(Schedule.BIWEEKLY, List.of(stats.mostOftenDayOfWeek), last.getTime());
        } else if (Math.abs(stats.medianDistance - ONE_MONTH) < 5 * ONE_DAY
                && stats.avgDeltaToMedianDistance < 5 * ONE_DAY) {
            if (stats.daysOfMonth[stats.mostOftenDayOfMonth] >= releaseDates.size() - maxTotalWrongDays) {
                // Just using last.set(Calendar.DAY_OF_MONTH) could skip a week
                // when the last release is delayed over week boundaries
                addTime(last, 2 * ONE_WEEK);
                do {
                    addTime(last, ONE_DAY);
                } while (last.get(Calendar.DAY_OF_MONTH) != stats.mostOftenDayOfMonth);
                return new Guess(Schedule.MONTHLY, null, last.getTime());
            }

            addTime(last, 3 * ONE_WEEK + 3 * ONE_DAY);
            do {
                addTime(last, ONE_DAY);
            } while (last.get(Calendar.DAY_OF_WEEK) != stats.mostOftenDayOfWeek);
            return new Guess(Schedule.FOURWEEKLY, List.of(stats.mostOftenDayOfWeek), last.getTime());
        }

        // Find release days
        List<Integer> largeDays = new ArrayList<>();
        for (int i = Calendar.SUNDAY; i <= Calendar.SATURDAY; i++) {
            if (stats.daysOfWeek[i] > maxSingleDayOff) {
                largeDays.add(i);
            }
        }
        // Ensure that all release days are used similarly often
        int averageDays = releaseDates.size() / largeDays.size();
        boolean matchesAverageDays = true;
        for (int day : largeDays) {
            if (stats.daysOfWeek[day] < averageDays - maxSingleDayOff) {
                matchesAverageDays = false;
                break;
            }
        }

        if (matchesAverageDays && stats.medianDistance < ONE_WEEK) {
            // Fixed daily release schedule (eg Mo, Thu, Fri)
            addUntil(last, largeDays);

            if (largeDays.size() == 5 && largeDays.containsAll(Arrays.asList(
                    Calendar.MONDAY, Calendar.TUESDAY, Calendar.WEDNESDAY, Calendar.THURSDAY, Calendar.FRIDAY))) {
                return new Guess(Schedule.WEEKDAYS, largeDays, last.getTime());
            }
            return new Guess(Schedule.SPECIFIC_DAYS, largeDays, last.getTime());
        } else if (largeDays.size() == 1) {
            // Probably still weekly with more exceptions than others
            addUntil(last, largeDays);
            return new Guess(Schedule.WEEKLY, largeDays, last.getTime());
        }

        addTime(last, (long) (0.6f * stats.medianDistance));
        return new Guess(Schedule.UNKNOWN, null, last.getTime());
    }
@Test
    public void testDaily() {
        ArrayList<Date> releaseDates = new ArrayList<>();
        releaseDates.add(makeDate("2024-01-01 16:30")); // Monday
        releaseDates.add(makeDate("2024-01-02 16:25"));
        releaseDates.add(makeDate("2024-01-03 16:35"));
        releaseDates.add(makeDate("2024-01-04 16:40"));
        releaseDates.add(makeDate("2024-01-05 16:20"));
        releaseDates.add(makeDate("2024-01-06 16:10"));
        releaseDates.add(makeDate("2024-01-07 16:32")); // Sunday

        // Next day
        ReleaseScheduleGuesser.Guess guess = performGuess(releaseDates);
        assertEquals(ReleaseScheduleGuesser.Schedule.DAILY, guess.schedule);
        assertClose(makeDate("2024-01-08 16:30"), guess.nextExpectedDate, 10 * ONE_MINUTE);

        // One-off early release
        releaseDates.add(makeDate("2024-01-08 10:00"));
        guess = performGuess(releaseDates);
        assertEquals(ReleaseScheduleGuesser.Schedule.DAILY, guess.schedule);
        assertClose(makeDate("2024-01-09 16:30"), guess.nextExpectedDate, 10 * ONE_MINUTE);
    }