From 8ea915a9f5beb81629ae2228612b2832559b53fd Mon Sep 17 00:00:00 2001 From: Alex Baker Date: Thu, 12 Oct 2017 16:49:50 -0500 Subject: [PATCH] Fix weekly repeat bugs --- .../astrid/repeats/RepeatTaskHelperTest.java | 7 - .../tasks/repeats/RepeatRuleToStringTest.java | 57 ++++++++ .../recurrencepicker/WeekButton.java | 2 +- .../astrid/repeats/RepeatControlSet.java | 109 +-------------- .../main/java/org/tasks/locale/Locale.java | 2 +- .../tasks/repeats/CustomRecurrenceDialog.java | 91 +++++++++--- .../org/tasks/repeats/RepeatRuleToString.java | 131 ++++++++++++++++++ 7 files changed, 263 insertions(+), 136 deletions(-) create mode 100644 app/src/androidTest/java/org/tasks/repeats/RepeatRuleToStringTest.java create mode 100644 app/src/main/java/org/tasks/repeats/RepeatRuleToString.java diff --git a/app/src/androidTest/java/com/todoroo/astrid/repeats/RepeatTaskHelperTest.java b/app/src/androidTest/java/com/todoroo/astrid/repeats/RepeatTaskHelperTest.java index 8b8560e83..491e60393 100644 --- a/app/src/androidTest/java/com/todoroo/astrid/repeats/RepeatTaskHelperTest.java +++ b/app/src/androidTest/java/com/todoroo/astrid/repeats/RepeatTaskHelperTest.java @@ -39,13 +39,6 @@ import static org.tasks.makers.TaskMaker.newTask; @RunWith(AndroidJUnit4.class) public class RepeatTaskHelperTest extends DatabaseTestCase { - private final Task task = new Task(); - - { - task.setDueDate(new DateTime(2013, 12, 31, 17, 17, 32, 900).getMillis()); - task.setCompletionDate(new DateTime(2014, 1, 7, 17, 17, 32, 900).getMillis()); - } - @Inject TaskDao taskDao; private LocalBroadcastManager localBroadcastManager; diff --git a/app/src/androidTest/java/org/tasks/repeats/RepeatRuleToStringTest.java b/app/src/androidTest/java/org/tasks/repeats/RepeatRuleToStringTest.java new file mode 100644 index 000000000..ae330f143 --- /dev/null +++ b/app/src/androidTest/java/org/tasks/repeats/RepeatRuleToStringTest.java @@ -0,0 +1,57 @@ +package org.tasks.repeats; + +import android.support.test.runner.AndroidJUnit4; + +import com.google.ical.values.RRule; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.tasks.locale.Locale; + +import java.text.ParseException; + +import static android.support.test.InstrumentationRegistry.getTargetContext; +import static junit.framework.Assert.assertEquals; + +@RunWith(AndroidJUnit4.class) +public class RepeatRuleToStringTest { + + @Test + public void weekly() { + assertEquals("Repeats weekly", toString("RRULE:FREQ=WEEKLY;INTERVAL=1")); + } + + @Test + public void weeklyPlural() { + assertEquals("Repeats every 2 weeks", toString("RRULE:FREQ=WEEKLY;INTERVAL=2")); + } + + @Test + public void weeklyByDay() { + assertEquals("Repeats weekly on Mon, Tue, Wed, Thu, Fri", toString("RRULE:FREQ=WEEKLY;INTERVAL=1;BYDAY=MO,TU,WE,TH,FR")); + } + + @Test + public void printDaysInRepeatRuleOrder() { + assertEquals("Repeats weekly on Fri, Thu, Wed, Tue, Mon", toString("RRULE:FREQ=WEEKLY;INTERVAL=1;BYDAY=FR,TH,WE,TU,MO")); + } + + @Test + public void useLocaleForDays() { + assertEquals("Wiederhole wöchentlich am Sa., So.", toString("de", "RRULE:FREQ=WEEKLY;INTERVAL=1;BYDAY=SA,SU")); + } + + private String toString(String rrule) { + return toString(null, rrule); + } + + private String toString(String language, String rrule) { + try { + Locale locale = new Locale(java.util.Locale.getDefault(), language, -1); + return new RepeatRuleToString(locale.createConfigurationContext(getTargetContext()), locale) + .toString(new RRule(rrule)); + } catch (ParseException e) { + throw new RuntimeException(e); + } + } +} diff --git a/app/src/main/java/com/appeaser/sublimepickerlibrary/recurrencepicker/WeekButton.java b/app/src/main/java/com/appeaser/sublimepickerlibrary/recurrencepicker/WeekButton.java index e1de68398..f303a606c 100644 --- a/app/src/main/java/com/appeaser/sublimepickerlibrary/recurrencepicker/WeekButton.java +++ b/app/src/main/java/com/appeaser/sublimepickerlibrary/recurrencepicker/WeekButton.java @@ -82,7 +82,7 @@ public class WeekButton extends ToggleButton { // Reset text color for animation // The correct state color will be // set when animation is done or cancelled - setTextColor(isChecked() ? mCheckedTextColor : mDefaultTextColor); + setTextColor(mCheckedTextColor); mDrawable.setCheckedOnClick(isChecked(), mCallback); } } diff --git a/app/src/main/java/com/todoroo/astrid/repeats/RepeatControlSet.java b/app/src/main/java/com/todoroo/astrid/repeats/RepeatControlSet.java index 8c2bd3023..5f37ec899 100644 --- a/app/src/main/java/com/todoroo/astrid/repeats/RepeatControlSet.java +++ b/app/src/main/java/com/todoroo/astrid/repeats/RepeatControlSet.java @@ -22,13 +22,9 @@ import android.widget.LinearLayout; import android.widget.Spinner; import android.widget.TextView; -import com.google.common.base.Joiner; import com.google.common.base.Strings; import com.google.ical.values.Frequency; import com.google.ical.values.RRule; -import com.google.ical.values.Weekday; -import com.google.ical.values.WeekdayNum; -import com.todoroo.andlib.utility.DateUtilities; import com.todoroo.astrid.data.Task; import org.tasks.R; @@ -39,13 +35,13 @@ import org.tasks.injection.ForActivity; import org.tasks.injection.FragmentComponent; import org.tasks.locale.Locale; import org.tasks.repeats.CustomRecurrenceDialog; +import org.tasks.repeats.RepeatRuleToString; import org.tasks.themes.Theme; import org.tasks.time.DateTime; import org.tasks.ui.HiddenTopArrayAdapter; import org.tasks.ui.SingleCheckedArrayAdapter; import org.tasks.ui.TaskEditControlFragment; -import java.text.DateFormatSymbols; import java.text.ParseException; import java.util.ArrayList; import java.util.Arrays; @@ -77,7 +73,6 @@ public class RepeatControlSet extends TaskEditControlFragment implements CustomRecurrenceDialog.CustomRecurrenceCallback { public static final int TAG = R.string.TEA_ctrl_repeat_pref; - public static final List WEEKDAYS = Arrays.asList(Weekday.values()); private static final String FRAG_TAG_CUSTOM_RECURRENCE = "frag_tag_custom_recurrence"; @Override @@ -100,15 +95,15 @@ public class RepeatControlSet extends TaskEditControlFragment @Inject DialogBuilder dialogBuilder; @Inject @ForActivity Context context; @Inject Theme theme; - @Inject Locale locale; @Inject Tracker tracker; + @Inject RepeatRuleToString repeatRuleToString; @BindView(R.id.display_row_edit) TextView displayView; @BindView(R.id.repeatType) Spinner typeSpinner; @BindView(R.id.repeatTypeContainer) LinearLayout repeatTypeContainer; - private RRule rrule; private final List repeatTypes = new ArrayList<>(); + private RRule rrule; private HiddenTopArrayAdapter typeAdapter; private RepeatChangedListener callback; @@ -206,7 +201,7 @@ public class RepeatControlSet extends TaskEditControlFragment List repeatOptions = newArrayList(context.getResources().getStringArray(R.array.repeat_options)); SingleCheckedArrayAdapter adapter = new SingleCheckedArrayAdapter(context, repeatOptions, theme.getThemeAccent()); if (customPicked) { - adapter.insert(getRepeatString(), 0); + adapter.insert(repeatRuleToString.toString(rrule), 0); adapter.setChecked(0); } else if (rrule == null) { adapter.setChecked(0); @@ -339,103 +334,9 @@ public class RepeatControlSet extends TaskEditControlFragment displayView.setTextColor(getColor(context, R.color.text_tertiary)); repeatTypeContainer.setVisibility(View.GONE); } else { - displayView.setText(getRepeatString()); + displayView.setText(repeatRuleToString.toString(rrule)); displayView.setTextColor(getColor(context, R.color.text_primary)); repeatTypeContainer.setVisibility(View.VISIBLE); } } - - private String getRepeatString() { - int interval = rrule.getInterval(); - Frequency frequency = rrule.getFreq(); - DateTime repeatUntil = rrule.getUntil() == null ? null : DateTime.from(rrule.getUntil()); - int count = rrule.getCount(); - String countString = count > 0 ? getContext().getResources().getQuantityString(R.plurals.repeat_times, count) : ""; - if (interval == 1) { - String frequencyString = getString(getSingleFrequencyResource(frequency)); - if (frequency == WEEKLY && !rrule.getByDay().isEmpty()) { - String dayString = getDayString(); - if (count > 0) { - return getString(R.string.repeats_single_on_number_of_times, frequencyString, dayString, count, countString); - } else if (repeatUntil == null) { - return getString(R.string.repeats_single_on, frequencyString, dayString); - } else { - return getString(R.string.repeats_single_on_until, frequencyString, dayString, DateUtilities.getLongDateString(repeatUntil)); - } - } else if (count > 0) { - return getString(R.string.repeats_single_number_of_times, frequencyString, count, countString); - } else if (repeatUntil == null) { - return getString(R.string.repeats_single, frequencyString); - } else { - return getString(R.string.repeats_single_until, frequencyString, DateUtilities.getLongDateString(repeatUntil)); - } - } else { - int plural = getFrequencyPlural(frequency); - String frequencyPlural = getResources().getQuantityString(plural, interval, interval); - if (frequency == WEEKLY && !rrule.getByDay().isEmpty()) { - String dayString = getDayString(); - if (count > 0) { - return getString(R.string.repeats_plural_on_number_of_times, frequencyPlural, dayString, count, countString); - } else if (repeatUntil == null) { - return getString(R.string.repeats_plural_on, frequencyPlural, dayString); - } else { - return getString(R.string.repeats_plural_on_until, frequencyPlural, dayString, DateUtilities.getLongDateString(repeatUntil)); - } - } else if (count > 0) { - return getString(R.string.repeats_plural_number_of_times, frequencyPlural, count, countString); - } else if (repeatUntil == null) { - return getString(R.string.repeats_plural, frequencyPlural); - } else { - return getString(R.string.repeats_plural_until, frequencyPlural, DateUtilities.getLongDateString(repeatUntil)); - } - } - } - - private String getDayString() { - DateFormatSymbols dfs = new DateFormatSymbols(locale.getLocale()); - String[] shortWeekdays = dfs.getShortWeekdays(); - List days = new ArrayList<>(); - for (WeekdayNum weekday : rrule.getByDay()) { - days.add(shortWeekdays[WEEKDAYS.indexOf(weekday.wday) + 1]); - } - return Joiner.on(getString(R.string.list_separator_with_space)).join(days); - } - - private int getSingleFrequencyResource(Frequency frequency) { - switch (frequency) { - case MINUTELY: - return R.string.repeats_minutely; - case HOURLY: - return R.string.repeats_hourly; - case DAILY: - return R.string.repeats_daily; - case WEEKLY: - return R.string.repeats_weekly; - case MONTHLY: - return R.string.repeats_monthly; - case YEARLY: - return R.string.repeats_yearly; - default: - throw new RuntimeException("Invalid frequency: " + frequency); - } - } - - private int getFrequencyPlural(Frequency frequency) { - switch (frequency) { - case MINUTELY: - return R.plurals.repeat_n_minutes; - case HOURLY: - return R.plurals.repeat_n_hours; - case DAILY: - return R.plurals.repeat_n_days; - case WEEKLY: - return R.plurals.repeat_n_weeks; - case MONTHLY: - return R.plurals.repeat_n_months; - case YEARLY: - return R.plurals.repeat_n_years; - default: - throw new RuntimeException("Invalid frequency: " + frequency); - } - } } diff --git a/app/src/main/java/org/tasks/locale/Locale.java b/app/src/main/java/org/tasks/locale/Locale.java index bde0ca66d..41a76c8a9 100644 --- a/app/src/main/java/org/tasks/locale/Locale.java +++ b/app/src/main/java/org/tasks/locale/Locale.java @@ -76,7 +76,7 @@ public class Locale { private final int directionOverride; private final boolean hasUserOverrides; - private Locale(java.util.Locale deviceLocale, String languageOverride, int directionOverride) { + public Locale(java.util.Locale deviceLocale, String languageOverride, int directionOverride) { this.deviceLocale = deviceLocale; this.languageOverride = languageOverride; this.directionOverride = directionOverride; diff --git a/app/src/main/java/org/tasks/repeats/CustomRecurrenceDialog.java b/app/src/main/java/org/tasks/repeats/CustomRecurrenceDialog.java index 943bd5f23..02fb23303 100644 --- a/app/src/main/java/org/tasks/repeats/CustomRecurrenceDialog.java +++ b/app/src/main/java/org/tasks/repeats/CustomRecurrenceDialog.java @@ -24,6 +24,7 @@ import com.appeaser.sublimepickerlibrary.recurrencepicker.WeekButton; import com.google.common.base.Strings; import com.google.ical.values.Frequency; import com.google.ical.values.RRule; +import com.google.ical.values.Weekday; import com.google.ical.values.WeekdayNum; import com.todoroo.andlib.utility.DateUtilities; import com.todoroo.astrid.data.Task; @@ -52,6 +53,7 @@ import butterknife.BindView; import butterknife.ButterKnife; import butterknife.OnItemSelected; import butterknife.OnTextChanged; +import timber.log.Timber; import static android.support.v4.content.ContextCompat.getColor; import static com.google.ical.values.Frequency.DAILY; @@ -60,7 +62,6 @@ import static com.google.ical.values.Frequency.MINUTELY; import static com.google.ical.values.Frequency.MONTHLY; import static com.google.ical.values.Frequency.WEEKLY; import static com.google.ical.values.Frequency.YEARLY; -import static com.todoroo.astrid.repeats.RepeatControlSet.WEEKDAYS; import static java.util.Arrays.asList; import static org.tasks.date.DateTimeUtils.newDateTime; @@ -108,8 +109,9 @@ public class CustomRecurrenceDialog extends InjectingDialogFragment { @BindView(R.id.repeatTimesValue) EditText repeatTimes; @BindView(R.id.repeatTimesText) TextView repeatTimesText; - private ArrayAdapter repeatUntilAdapter; private final List repeatUntilOptions = new ArrayList<>(); + private ArrayAdapter repeatUntilAdapter; + private WeekButton[] weekButtons; private RRule rrule; @@ -119,13 +121,15 @@ public class CustomRecurrenceDialog extends InjectingDialogFragment { LayoutInflater inflater = LayoutInflater.from(getActivity()); View dialogView = inflater.inflate(R.layout.control_set_repeat, null); - Bundle arguments = getArguments(); - String rule = arguments.getString(EXTRA_RRULE); - if (!Strings.isNullOrEmpty(rule)) { - try { + String rule = savedInstanceState == null + ? getArguments().getString(EXTRA_RRULE) + : savedInstanceState.getString(EXTRA_RRULE); + try { + if (!Strings.isNullOrEmpty(rule)) { rrule = new RRule(rule); - } catch (Exception ignored) { } + } catch (Exception e) { + Timber.e(e, e.getMessage()); } if (rrule == null) { rrule = new RRule(); @@ -167,7 +171,7 @@ public class CustomRecurrenceDialog extends InjectingDialogFragment { repeatUntilSpinner.setAdapter(repeatUntilAdapter); updateRepeatUntilOptions(); - WeekButton[] weekButtons = new WeekButton[] { day1, day2, day3, day4, day5, day6, day7 }; + weekButtons = new WeekButton[] { day1, day2, day3, day4, day5, day6, day7 }; int expandedWidthHeight = getResources() .getDimensionPixelSize(R.dimen.week_button_state_on_circle_size); @@ -178,37 +182,31 @@ public class CustomRecurrenceDialog extends InjectingDialogFragment { // set up days of week ThemeAccent accent = theme.getThemeAccent(); DateFormatSymbols dfs = new DateFormatSymbols(locale.getLocale()); - Calendar calendar = Calendar.getInstance(); + Calendar calendar = Calendar.getInstance(locale.getLocale()); calendar.set(Calendar.DAY_OF_WEEK, calendar.getFirstDayOfWeek()); String[] shortWeekdays = dfs.getShortWeekdays(); for(int i = 0; i < 7; i++) { - String text = shortWeekdays[calendar.get(Calendar.DAY_OF_WEEK)]; - WeekdayNum weekdayNum = new WeekdayNum(0, WEEKDAYS.get(i)); + int dayOfWeek = calendar.get(Calendar.DAY_OF_WEEK); + String text = shortWeekdays[dayOfWeek]; + WeekdayNum weekdayNum = new WeekdayNum(0, calendarDayToWeekday(dayOfWeek)); WeekButton weekButton = weekButtons[i]; + weekButton.setTag(weekdayNum); weekButton.setBackgroundDrawable(new CheckableDrawable(accent.getAccentColor(), false, expandedWidthHeight)); weekButton.setTextColor(weekButtonUnselectedTextColor); weekButton.setTextOff(text); weekButton.setTextOn(text); weekButton.setText(text); - if (rrule.getByDay().contains(weekdayNum)) { - weekButton.setChecked(true); - } - weekButton.setOnCheckedChangeListener((compoundButton, b) -> { - List days = rrule.getByDay(); - if (b) { - days.add(weekdayNum); - } else { - days.remove(weekdayNum); - } - }); + weekButton.setCheckedNoAnimate(rrule.getByDay().contains(weekdayNum)); calendar.add(Calendar.DATE, 1); } return dialogBuilder.newDialog() .setView(dialogView) .setPositiveButton(android.R.string.ok, (dialog12, which) -> { - if (rrule.getFreq() != WEEKLY) { + if (rrule.getFreq() == WEEKLY) { + setByDays(); + } else { rrule.setByDay(Collections.emptyList()); } ((CustomRecurrenceCallback) getTargetFragment()).onSelected(rrule); @@ -218,6 +216,53 @@ public class CustomRecurrenceDialog extends InjectingDialogFragment { .show(); } + private void setByDays() { + List checked = new ArrayList<>(); + for (WeekButton button : weekButtons) { + if (button.isChecked()) { + checked.add((WeekdayNum) button.getTag()); + } + } + rrule.setByDay(checked); + } + + private Weekday calendarDayToWeekday(int calendarDay) { + switch (calendarDay) { + case Calendar.SUNDAY: + return Weekday.SU; + case Calendar.MONDAY: + return Weekday.MO; + case Calendar.TUESDAY: + return Weekday.TU; + case Calendar.WEDNESDAY: + return Weekday.WE; + case Calendar.THURSDAY: + return Weekday.TH; + case Calendar.FRIDAY: + return Weekday.FR; + case Calendar.SATURDAY: + return Weekday.SA; + } + throw new RuntimeException("Invalid calendar day: " + calendarDay); + } + + @Override + public void onResume() { + super.onResume(); + + for (WeekButton button : weekButtons) { + button.setCheckedNoAnimate(button.isChecked()); + } + } + + @Override + public void onSaveInstanceState(Bundle outState) { + super.onSaveInstanceState(outState); + + setByDays(); + outState.putString(EXTRA_RRULE, rrule.toIcal()); + } + private void setFrequency(Frequency frequency) { rrule.setFreq(frequency); int weekVisibility = frequency == WEEKLY ? View.VISIBLE : View.GONE; diff --git a/app/src/main/java/org/tasks/repeats/RepeatRuleToString.java b/app/src/main/java/org/tasks/repeats/RepeatRuleToString.java new file mode 100644 index 000000000..a3380dee4 --- /dev/null +++ b/app/src/main/java/org/tasks/repeats/RepeatRuleToString.java @@ -0,0 +1,131 @@ +package org.tasks.repeats; + +import android.content.Context; + +import com.google.common.base.Joiner; +import com.google.ical.values.Frequency; +import com.google.ical.values.RRule; +import com.google.ical.values.Weekday; +import com.google.ical.values.WeekdayNum; +import com.todoroo.andlib.utility.DateUtilities; + +import org.tasks.R; +import org.tasks.injection.ForApplication; +import org.tasks.locale.Locale; +import org.tasks.time.DateTime; + +import java.text.DateFormatSymbols; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import javax.inject.Inject; + +import static com.google.ical.values.Frequency.WEEKLY; + +public class RepeatRuleToString { + + private final Context context; + private final Locale locale; + private final List weekdays = Arrays.asList(Weekday.values()); + + @Inject + public RepeatRuleToString(@ForApplication Context context, Locale locale) { + this.context = context; + this.locale = locale; + } + + public String toString(RRule rrule) { + int interval = rrule.getInterval(); + Frequency frequency = rrule.getFreq(); + DateTime repeatUntil = rrule.getUntil() == null ? null : DateTime.from(rrule.getUntil()); + int count = rrule.getCount(); + String countString = count > 0 ? context.getResources().getQuantityString(R.plurals.repeat_times, count) : ""; + if (interval == 1) { + String frequencyString = context.getString(getSingleFrequencyResource(frequency)); + if (frequency == WEEKLY && !rrule.getByDay().isEmpty()) { + String dayString = getDayString(rrule); + if (count > 0) { + return context.getString(R.string.repeats_single_on_number_of_times, frequencyString, dayString, count, countString); + } else if (repeatUntil == null) { + return context.getString(R.string.repeats_single_on, frequencyString, dayString); + } else { + return context.getString(R.string.repeats_single_on_until, frequencyString, dayString, DateUtilities.getLongDateString(repeatUntil)); + } + } else if (count > 0) { + return context.getString(R.string.repeats_single_number_of_times, frequencyString, count, countString); + } else if (repeatUntil == null) { + return context.getString(R.string.repeats_single, frequencyString); + } else { + return context.getString(R.string.repeats_single_until, frequencyString, DateUtilities.getLongDateString(repeatUntil)); + } + } else { + int plural = getFrequencyPlural(frequency); + String frequencyPlural = context.getResources().getQuantityString(plural, interval, interval); + if (frequency == WEEKLY && !rrule.getByDay().isEmpty()) { + String dayString = getDayString(rrule); + if (count > 0) { + return context.getString(R.string.repeats_plural_on_number_of_times, frequencyPlural, dayString, count, countString); + } else if (repeatUntil == null) { + return context.getString(R.string.repeats_plural_on, frequencyPlural, dayString); + } else { + return context.getString(R.string.repeats_plural_on_until, frequencyPlural, dayString, DateUtilities.getLongDateString(repeatUntil)); + } + } else if (count > 0) { + return context.getString(R.string.repeats_plural_number_of_times, frequencyPlural, count, countString); + } else if (repeatUntil == null) { + return context.getString(R.string.repeats_plural, frequencyPlural); + } else { + return context.getString(R.string.repeats_plural_until, frequencyPlural, DateUtilities.getLongDateString(repeatUntil)); + } + } + } + + private String getDayString(RRule rrule) { + DateFormatSymbols dfs = new DateFormatSymbols(locale.getLocale()); + String[] shortWeekdays = dfs.getShortWeekdays(); + List days = new ArrayList<>(); + for (WeekdayNum weekday : rrule.getByDay()) { + days.add(shortWeekdays[weekdays.indexOf(weekday.wday) + 1]); + } + return Joiner.on(context.getString(R.string.list_separator_with_space)).join(days); + } + + private int getSingleFrequencyResource(Frequency frequency) { + switch (frequency) { + case MINUTELY: + return R.string.repeats_minutely; + case HOURLY: + return R.string.repeats_hourly; + case DAILY: + return R.string.repeats_daily; + case WEEKLY: + return R.string.repeats_weekly; + case MONTHLY: + return R.string.repeats_monthly; + case YEARLY: + return R.string.repeats_yearly; + default: + throw new RuntimeException("Invalid frequency: " + frequency); + } + } + + private int getFrequencyPlural(Frequency frequency) { + switch (frequency) { + case MINUTELY: + return R.plurals.repeat_n_minutes; + case HOURLY: + return R.plurals.repeat_n_hours; + case DAILY: + return R.plurals.repeat_n_days; + case WEEKLY: + return R.plurals.repeat_n_weeks; + case MONTHLY: + return R.plurals.repeat_n_months; + case YEARLY: + return R.plurals.repeat_n_years; + default: + throw new RuntimeException("Invalid frequency: " + frequency); + } + } +}