summaryrefslogtreecommitdiff
path: root/core/src/test/java/de/danoeh/antennapod
diff options
context:
space:
mode:
authorByteHamster <ByteHamster@users.noreply.github.com>2024-02-25 14:01:03 +0100
committerGitHub <noreply@github.com>2024-02-25 14:01:03 +0100
commit82c93bf7ee3d91395533068fdfe640dfa53113fe (patch)
tree83beaea8826b94adc181b9f3643e3b6a2c2fe603 /core/src/test/java/de/danoeh/antennapod
parentef4af0d29d6742fe07e32971b55a1236f8ad08ab (diff)
downloadAntennaPod-82c93bf7ee3d91395533068fdfe640dfa53113fe.zip
Guess next episode release date (#6925)
Diffstat (limited to 'core/src/test/java/de/danoeh/antennapod')
-rw-r--r--core/src/test/java/de/danoeh/antennapod/core/util/ReleaseScheduleGuesserRealWorldTest.java125
-rw-r--r--core/src/test/java/de/danoeh/antennapod/core/util/ReleaseScheduleGuesserTest.java172
2 files changed, 297 insertions, 0 deletions
diff --git a/core/src/test/java/de/danoeh/antennapod/core/util/ReleaseScheduleGuesserRealWorldTest.java b/core/src/test/java/de/danoeh/antennapod/core/util/ReleaseScheduleGuesserRealWorldTest.java
new file mode 100644
index 000000000..dd3cd763b
--- /dev/null
+++ b/core/src/test/java/de/danoeh/antennapod/core/util/ReleaseScheduleGuesserRealWorldTest.java
@@ -0,0 +1,125 @@
+package de.danoeh.antennapod.core.util;
+
+import de.danoeh.antennapod.parser.feed.util.DateUtils;
+import org.apache.commons.io.IOUtils;
+import org.junit.Test;
+
+import java.io.InputStream;
+import java.text.SimpleDateFormat;
+import java.util.ArrayList;
+import java.util.Calendar;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.Date;
+import java.util.GregorianCalendar;
+import java.util.List;
+import java.util.Locale;
+
+import static org.junit.Assert.assertTrue;
+
+public class ReleaseScheduleGuesserRealWorldTest {
+ private static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd HH:mm", Locale.ROOT);
+
+ private void printHistogram(int[] histogram) {
+ int max = 0;
+ for (int x : histogram) {
+ max = Math.max(x, max);
+ }
+ for (int row = 8; row >= 0; row--) {
+ for (int x : histogram) {
+ System.out.print((x > (double) row * (max / 9.0)) ? "#" : " ");
+ }
+ System.out.println();
+ }
+ for (int col = 0; col < histogram.length; col++) {
+ System.out.print(((col % 5) == 0) ? "|" : " ");
+ }
+ System.out.println();
+ }
+
+ @Test
+ public void testRealWorld() throws Exception {
+ InputStream inputStream = getClass().getClassLoader().getResource("release_dates.csv").openStream();
+ int numCorrectDay = 0;
+ int numFoundSchedule = 0;
+ int numFoundScheduleAndCorrectDay = 0;
+ int num3hoursCorrect = 0;
+ int numOffByMoreThan2days = 0;
+ int[] histogram = new int[101];
+
+ String csv = IOUtils.toString(inputStream, "UTF-8");
+ String[] lines = csv.split("\n");
+ int totalPodcasts = 0;
+ int lineNr = 0;
+ for (String line : lines) {
+ lineNr++;
+ String[] dates = line.split(";");
+ List<Date> releaseDates = new ArrayList<>();
+ for (String date : dates) {
+ releaseDates.add(DateUtils.parse(date));
+ }
+ if (releaseDates.size() <= 3) {
+ continue;
+ }
+ totalPodcasts++;
+ Collections.sort(releaseDates, Comparator.comparingLong(Date::getTime));
+ Date dateActual = releaseDates.get(releaseDates.size() - 1);
+ // Remove most recent one and possible duplicates of episodes on the same day
+ do {
+ releaseDates = releaseDates.subList(0, releaseDates.size() - 1);
+ } while (releaseDates.get(releaseDates.size() - 1).getTime()
+ > dateActual.getTime() - 30 * ReleaseScheduleGuesser.ONE_MINUTE);
+ ReleaseScheduleGuesser.Guess guess = ReleaseScheduleGuesser.performGuess(releaseDates);
+
+ final boolean is3hoursClose = Math.abs(dateActual.getTime() - guess.nextExpectedDate.getTime())
+ < 3 * ReleaseScheduleGuesser.ONE_HOUR;
+ System.out.println(lineNr + " guessed: " + DATE_FORMAT.format(guess.nextExpectedDate)
+ + ", actual: " + DATE_FORMAT.format(dateActual)
+ + " " + guess.schedule.name() + (is3hoursClose ? " ✔" : ""));
+ long deltaTime = dateActual.getTime() - guess.nextExpectedDate.getTime();
+ int histogramClass = (int) Math.max(0, Math.min(100, deltaTime / ReleaseScheduleGuesser.ONE_HOUR + 50));
+ histogram[histogramClass]++;
+ boolean foundSchedule = guess.schedule != ReleaseScheduleGuesser.Schedule.UNKNOWN;
+ if (foundSchedule) {
+ numFoundSchedule++;
+ }
+ Calendar calendarExpected = new GregorianCalendar();
+ calendarExpected.setTime(dateActual);
+ Calendar calendarGuessed = new GregorianCalendar();
+ calendarGuessed.setTime(guess.nextExpectedDate);
+ if (calendarExpected.get(Calendar.DAY_OF_YEAR) == calendarGuessed.get(Calendar.DAY_OF_YEAR)) {
+ numCorrectDay++;
+ if (foundSchedule) {
+ numFoundScheduleAndCorrectDay++;
+ }
+ }
+ if (Math.abs(deltaTime) > 2 * ReleaseScheduleGuesser.ONE_DAY) {
+ numOffByMoreThan2days++;
+ }
+ if (is3hoursClose) {
+ num3hoursCorrect++;
+ }
+ }
+
+ System.out.println("Podcasts tested: " + totalPodcasts);
+
+ double schedulePercentage = 100.0 * numFoundSchedule / totalPodcasts;
+ System.out.println("Found schedule: " + schedulePercentage);
+ double offByLessThan3HoursPercentage = 100.0 * num3hoursCorrect / totalPodcasts;
+ System.out.println("Off by less than 3 hours: " + offByLessThan3HoursPercentage);
+ double scheduleAndCorrectDayPercentage = 100.0 * numFoundScheduleAndCorrectDay / numFoundSchedule;
+ System.out.println("Correct day when schedule found: " + scheduleAndCorrectDayPercentage);
+ double correctDayPercentage = 100.0 * numCorrectDay / totalPodcasts;
+ System.out.println("Correct day: " + correctDayPercentage);
+ double offByLessThan2daysPercentage = 100.0 * (totalPodcasts - numOffByMoreThan2days) / totalPodcasts;
+ System.out.println("Off by less than 2 days: " + offByLessThan2daysPercentage);
+
+ assertTrue(schedulePercentage > 80);
+ assertTrue(offByLessThan3HoursPercentage > 55);
+ assertTrue(scheduleAndCorrectDayPercentage > 75);
+ assertTrue(correctDayPercentage > 60);
+ assertTrue(offByLessThan2daysPercentage > 75);
+
+ printHistogram(histogram);
+ }
+} \ No newline at end of file
diff --git a/core/src/test/java/de/danoeh/antennapod/core/util/ReleaseScheduleGuesserTest.java b/core/src/test/java/de/danoeh/antennapod/core/util/ReleaseScheduleGuesserTest.java
new file mode 100644
index 000000000..aaad125c9
--- /dev/null
+++ b/core/src/test/java/de/danoeh/antennapod/core/util/ReleaseScheduleGuesserTest.java
@@ -0,0 +1,172 @@
+package de.danoeh.antennapod.core.util;
+
+import org.junit.Test;
+
+import java.text.ParseException;
+import java.text.SimpleDateFormat;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.Locale;
+
+import static de.danoeh.antennapod.core.util.ReleaseScheduleGuesser.ONE_DAY;
+import static de.danoeh.antennapod.core.util.ReleaseScheduleGuesser.ONE_HOUR;
+import static de.danoeh.antennapod.core.util.ReleaseScheduleGuesser.ONE_MINUTE;
+import static de.danoeh.antennapod.core.util.ReleaseScheduleGuesser.performGuess;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+public class ReleaseScheduleGuesserTest {
+ private static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd HH:mm", Locale.ROOT);
+
+ private Date makeDate(String dateStr) {
+ try {
+ return DATE_FORMAT.parse(dateStr);
+ } catch (ParseException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ private void assertClose(Date expected, Date actual, long tolerance) {
+ assertTrue("Date should differ at most " + tolerance / 60000 + " minutes from "
+ + DATE_FORMAT.format(expected) + ", but is " + DATE_FORMAT.format(actual),
+ Math.abs(expected.getTime() - actual.getTime()) < tolerance);
+ }
+
+ @Test
+ public void testEdgeCases() {
+ ArrayList<Date> releaseDates = new ArrayList<>();
+ assertEquals(ReleaseScheduleGuesser.Schedule.UNKNOWN, performGuess(releaseDates).schedule);
+ releaseDates.add(makeDate("2024-01-01 16:30"));
+ assertEquals(ReleaseScheduleGuesser.Schedule.UNKNOWN, performGuess(releaseDates).schedule);
+ }
+
+ @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);
+ }
+
+ @Test
+ public void testWeekdays() {
+ 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")); // Friday
+ releaseDates.add(makeDate("2024-01-08 16:20")); // Monday
+ releaseDates.add(makeDate("2024-01-09 16:30"));
+ releaseDates.add(makeDate("2024-01-10 16:40"));
+ releaseDates.add(makeDate("2024-01-11 16:45")); // Thursday
+
+ // Next day
+ ReleaseScheduleGuesser.Guess guess = performGuess(releaseDates);
+ assertEquals(ReleaseScheduleGuesser.Schedule.WEEKDAYS, guess.schedule);
+ assertClose(makeDate("2024-01-12 16:30"), guess.nextExpectedDate, ONE_HOUR);
+
+ // After weekend
+ releaseDates.add(makeDate("2024-01-12 16:30")); // Friday
+ guess = performGuess(releaseDates);
+ assertClose(makeDate("2024-01-15 16:30"), guess.nextExpectedDate, ONE_HOUR);
+ }
+
+ @Test
+ public void testWeekly() {
+ ArrayList<Date> releaseDates = new ArrayList<>();
+ releaseDates.add(makeDate("2024-01-07 16:30")); // Sunday
+ releaseDates.add(makeDate("2024-01-14 16:25"));
+ releaseDates.add(makeDate("2024-01-21 14:25"));
+ releaseDates.add(makeDate("2024-01-28 16:15"));
+
+ // Next week
+ ReleaseScheduleGuesser.Guess guess = performGuess(releaseDates);
+ assertEquals(ReleaseScheduleGuesser.Schedule.WEEKLY, guess.schedule);
+ assertClose(makeDate("2024-02-04 16:30"), guess.nextExpectedDate, 2 * ONE_HOUR);
+
+ // One-off early release
+ releaseDates.add(makeDate("2024-02-02 16:35"));
+ guess = performGuess(releaseDates);
+ assertEquals(ReleaseScheduleGuesser.Schedule.WEEKLY, guess.schedule);
+ assertClose(makeDate("2024-02-11 16:30"), guess.nextExpectedDate, 2 * ONE_HOUR);
+
+ // One-off late release
+ releaseDates.add(makeDate("2024-02-13 16:35"));
+ guess = performGuess(releaseDates);
+ assertEquals(ReleaseScheduleGuesser.Schedule.WEEKLY, guess.schedule);
+ assertClose(makeDate("2024-02-18 16:30"), guess.nextExpectedDate, 2 * ONE_HOUR);
+ }
+
+ @Test
+ public void testMonthly() {
+ ArrayList<Date> releaseDates = new ArrayList<>();
+ releaseDates.add(makeDate("2024-01-01 16:30"));
+ releaseDates.add(makeDate("2024-02-01 16:30"));
+ releaseDates.add(makeDate("2024-03-01 16:30"));
+ releaseDates.add(makeDate("2024-04-01 16:30"));
+
+ // Next month
+ ReleaseScheduleGuesser.Guess guess = performGuess(releaseDates);
+ assertEquals(ReleaseScheduleGuesser.Schedule.MONTHLY, guess.schedule);
+ assertClose(makeDate("2024-05-01 16:30"), guess.nextExpectedDate, 10 * ONE_HOUR);
+
+ // One-off early release
+ releaseDates.add(makeDate("2024-04-30 16:30"));
+ guess = performGuess(releaseDates);
+ assertEquals(ReleaseScheduleGuesser.Schedule.MONTHLY, guess.schedule);
+ assertClose(makeDate("2024-06-01 16:30"), guess.nextExpectedDate, 10 * ONE_HOUR);
+
+ // One-off late release
+ releaseDates.remove(releaseDates.size() - 1);
+ releaseDates.add(makeDate("2024-05-13 16:30"));
+ guess = performGuess(releaseDates);
+ assertEquals(ReleaseScheduleGuesser.Schedule.MONTHLY, guess.schedule);
+ assertClose(makeDate("2024-06-01 16:30"), guess.nextExpectedDate, 10 * ONE_HOUR);
+ }
+
+ @Test
+ public void testFourweekly() {
+ ArrayList<Date> releaseDates = new ArrayList<>();
+ releaseDates.add(makeDate("2024-01-01 16:30"));
+ releaseDates.add(makeDate("2024-01-29 16:30"));
+ releaseDates.add(makeDate("2024-02-26 16:30"));
+ releaseDates.add(makeDate("2024-03-25 16:30"));
+
+ // 4 weeks later
+ ReleaseScheduleGuesser.Guess guess = performGuess(releaseDates);
+ assertEquals(ReleaseScheduleGuesser.Schedule.FOURWEEKLY, guess.schedule);
+ assertClose(makeDate("2024-04-22 16:30"), guess.nextExpectedDate, 10 * ONE_HOUR);
+ }
+
+ @Test
+ public void testUnknown() {
+ ArrayList<Date> releaseDates = new ArrayList<>();
+ releaseDates.add(makeDate("2024-01-01 16:30"));
+ releaseDates.add(makeDate("2024-01-03 16:30"));
+ releaseDates.add(makeDate("2024-01-03 16:31"));
+ releaseDates.add(makeDate("2024-01-04 16:30"));
+ releaseDates.add(makeDate("2024-01-04 16:31"));
+ releaseDates.add(makeDate("2024-01-07 16:30"));
+ releaseDates.add(makeDate("2024-01-07 16:31"));
+ releaseDates.add(makeDate("2024-01-10 16:30"));
+ ReleaseScheduleGuesser.Guess guess = performGuess(releaseDates);
+ assertEquals(ReleaseScheduleGuesser.Schedule.UNKNOWN, guess.schedule);
+ assertClose(makeDate("2024-01-12 16:30"), guess.nextExpectedDate, 2 * ONE_DAY);
+ }
+} \ No newline at end of file