From 26f7316d60b63178f1882327ba5cce874a86009b Mon Sep 17 00:00:00 2001 From: Henning Hall Date: Mon, 28 Dec 2020 21:17:29 +0100 Subject: [PATCH] Bugfix: Scroll away from invalid dates (#280) * initial working version * fix native variant issue * cleanup * cleanup --- .../ui/WheelChangeListenerImpl.java | 98 +++++++++++++++---- .../henninghall/date_picker/ui/Wheels.java | 41 ++++++-- .../date_picker/wheels/DateWheel.java | 2 +- .../henninghall/date_picker/wheels/Wheel.java | 8 ++ examples/detox/e2e/tests/invalidDates.spec.js | 52 ++++++++++ 5 files changed, 172 insertions(+), 29 deletions(-) create mode 100644 examples/detox/e2e/tests/invalidDates.spec.js diff --git a/android/src/main/java/com/henninghall/date_picker/ui/WheelChangeListenerImpl.java b/android/src/main/java/com/henninghall/date_picker/ui/WheelChangeListenerImpl.java index 4064847..c5cf7a0 100644 --- a/android/src/main/java/com/henninghall/date_picker/ui/WheelChangeListenerImpl.java +++ b/android/src/main/java/com/henninghall/date_picker/ui/WheelChangeListenerImpl.java @@ -13,7 +13,6 @@ import com.henninghall.date_picker.wheels.Wheel; import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.Calendar; -import java.util.Date; import java.util.TimeZone; public class WheelChangeListenerImpl implements WheelChangeListener { @@ -30,33 +29,96 @@ public class WheelChangeListenerImpl implements WheelChangeListener { this.rootView = rootView; } + private SimpleDateFormat getDateFormat(){ + TimeZone timeZone = state.getTimeZone(); + SimpleDateFormat dateFormat = uiManager.getDateFormat(); + dateFormat.setTimeZone(timeZone); + return dateFormat; + } + @Override public void onChange(Wheel picker) { if(wheels.hasSpinningWheel()) return; - WritableMap event = Arguments.createMap(); - TimeZone timeZone = state.getTimeZone(); - SimpleDateFormat dateFormat = uiManager.getDateFormat(); + if(!exists()){ + Calendar closestExistingDate = getClosestExistingDateInPast(); + if(closestExistingDate != null) { + uiManager.animateToDate(closestExistingDate); + } + return; + } + + Calendar selectedDate = getSelectedDate(); + if(selectedDate == null) return; + Calendar minDate = state.getMinimumDate(); + if (minDate != null && selectedDate.before(minDate)) { + uiManager.animateToDate(minDate); + return; + } + Calendar maxDate = state.getMaximumDate(); + if (maxDate != null && selectedDate.after(maxDate)) { + uiManager.animateToDate(maxDate); + return; + } + + emitDateChangeEvent(selectedDate); + } + + // Example: Jan 1 returns true, April 31 returns false. + private boolean exists(){ + SimpleDateFormat dateFormat = getDateFormat(); + String toParse = wheels.getDateTimeString(); try { - dateFormat.setTimeZone(timeZone); - Calendar date = Calendar.getInstance(timeZone); - String toParse = wheels.getDateString(); - Date newDate = dateFormat.parse(toParse); - date.setTime(newDate); - String dateString = Utils.dateToIso(date); - if (minDate != null && date.before(minDate)) uiManager.animateToDate(minDate); - else if (maxDate != null && date.after(maxDate)) uiManager.animateToDate(maxDate); - else { - event.putString("date", dateString); - event.putString("dateString", uiManager.getDisplayValueString()); - DatePickerManager.context.getJSModule(RCTEventEmitter.class) - .receiveEvent(rootView.getId(), "dateChange", event); - } + dateFormat.setLenient(false); // disallow parsing invalid dates + dateFormat.parse(toParse); + return true; + } catch (ParseException e) { + return false; + } + } + + private Calendar getSelectedDate(){ + SimpleDateFormat dateFormat = getDateFormat(); + String toParse = wheels.getDateTimeString(); + TimeZone timeZone = state.getTimeZone(); + Calendar date = Calendar.getInstance(timeZone); + try { + dateFormat.setLenient(true); // allow parsing invalid dates + date.setTime(dateFormat.parse(toParse)); + return date; } catch (ParseException e) { e.printStackTrace(); } + return null; + } + + private Calendar getClosestExistingDateInPast(){ + SimpleDateFormat dateFormat = getDateFormat(); + dateFormat.setLenient(false); // disallow parsing invalid dates + + int maxDaysInPastToCheck = 10; + for (int i = 0; i < maxDaysInPastToCheck; i++){ + try { + String toParse = wheels.getDateTimeString(i); + Calendar calendar = Calendar.getInstance(state.getTimeZone()); + calendar.setTime(dateFormat.parse(toParse)); + return calendar; + } catch (ParseException ignored) { + // continue checking if exception (which means invalid date) + } + } + return null; + } + + private void emitDateChangeEvent(Calendar date) { + WritableMap event = Arguments.createMap(); + String dateString = Utils.dateToIso(date); + event.putString("date", dateString); + event.putString("dateString", uiManager.getDisplayValueString()); + DatePickerManager.context.getJSModule(RCTEventEmitter.class) + .receiveEvent(rootView.getId(), "dateChange", event); } } diff --git a/android/src/main/java/com/henninghall/date_picker/ui/Wheels.java b/android/src/main/java/com/henninghall/date_picker/ui/Wheels.java index 7c35927..db1c832 100644 --- a/android/src/main/java/com/henninghall/date_picker/ui/Wheels.java +++ b/android/src/main/java/com/henninghall/date_picker/ui/Wheels.java @@ -6,7 +6,6 @@ import com.henninghall.date_picker.models.Variant; import com.henninghall.date_picker.pickers.Picker; import com.henninghall.date_picker.R; import com.henninghall.date_picker.State; -import com.henninghall.date_picker.Utils; import com.henninghall.date_picker.models.WheelType; import com.henninghall.date_picker.models.Mode; import com.henninghall.date_picker.wheelFunctions.SetDividerHeight; @@ -112,19 +111,41 @@ public class Wheels { return wheelPerWheelType.get(type); } - String getDateString() { + String getDateTimeString(int daysToSubtract) { + return getDateString(daysToSubtract) + " " + getTimeString(); + } + + private String getDateModeString(int daysToSubtract) { ArrayList wheels = getOrderedVisibleWheels(); - String dateString = (state.getMode() == Mode.date) - ? wheels.get(0).getValue() + " " - + wheels.get(1).getValue() + " " - + wheels.get(2).getValue() - : dayWheel.getValue(); - return dateString - + " " + hourWheel.getValue() + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < 3; i++) { + if (i != 0) sb.append(" "); + Wheel w = wheels.get(i); + if (w instanceof DateWheel) { + sb.append(w.getPastValue(daysToSubtract)); + } + else sb.append(w.getValue()); + } + return sb.toString(); + } + + private String getDateString(int daysToSubtract){ + if(state.getMode() == Mode.date ){ + return getDateModeString(daysToSubtract); + } + return dayWheel.getValue(); + } + + private String getTimeString(){ + return hourWheel.getValue() + " " + minutesWheel.getValue() + ampmWheel.getValue(); } + String getDateTimeString() { + return getDateTimeString(0); + } + String getDisplayValue() { StringBuilder sb = new StringBuilder(); for (Wheel wheel: getOrderedVisibleWheels()) { @@ -137,7 +158,7 @@ public class Wheels { ArrayList wheels = state.derived.getOrderedVisibleWheels(); for (WheelType wheelType : wheels) { Wheel wheel = getWheel(wheelType); - pickerWrapper.addPicker(wheel.picker.getView()); + pickerWrapper.addPicker(wheel.picker.getView()); } } diff --git a/android/src/main/java/com/henninghall/date_picker/wheels/DateWheel.java b/android/src/main/java/com/henninghall/date_picker/wheels/DateWheel.java index a12ef96..6486fd2 100644 --- a/android/src/main/java/com/henninghall/date_picker/wheels/DateWheel.java +++ b/android/src/main/java/com/henninghall/date_picker/wheels/DateWheel.java @@ -19,7 +19,7 @@ public class DateWheel extends Wheel Calendar cal = Calendar.getInstance(); ArrayList values = new ArrayList<>(); cal.set(Calendar.MONTH, 0); - cal.set(Calendar.DATE, 0); + cal.set(Calendar.DATE, 1); final int maxDate = 31; final int minDate = 1; for (int i = minDate; i <= maxDate; ++i) { diff --git a/android/src/main/java/com/henninghall/date_picker/wheels/Wheel.java b/android/src/main/java/com/henninghall/date_picker/wheels/Wheel.java index 9fbbb4e..ee690d8 100644 --- a/android/src/main/java/com/henninghall/date_picker/wheels/Wheel.java +++ b/android/src/main/java/com/henninghall/date_picker/wheels/Wheel.java @@ -53,6 +53,14 @@ public abstract class Wheel { return getValueAtIndex(getIndex()); } + public String getPastValue(int subtractIndex) { + if(!visible()) return format.format(userSetValue.getTime()); + int size = values.size(); + int pastValueIndex = (getIndex() + size - subtractIndex) % size; + return getValueAtIndex(pastValueIndex); + } + + private int getIndex() { return picker.getValue(); } diff --git a/examples/detox/e2e/tests/invalidDates.spec.js b/examples/detox/e2e/tests/invalidDates.spec.js new file mode 100644 index 0000000..affa4c3 --- /dev/null +++ b/examples/detox/e2e/tests/invalidDates.spec.js @@ -0,0 +1,52 @@ +const { scrollWheel, expectDate, setDate, setMode, setMaximumDate, setMinimumDate } = require('../utils') + +const scrollDays = (days) => scrollWheel(1, days) + +describe('Invalid dates', () => { + before(async () => { + await device.reloadReactNative() + await element(by.text('Advanced')).tap() + await setMinimumDate(undefined) + await setMaximumDate(undefined) + await setMode('date') + }) + + + it('scrolls back to last valid date', async () => { + await setDate(new Date("2001-02-28 00:00")) + await scrollDays(1) + await expectDate('2001-02-28 00:00:00') + }) + + it('scrolls back after scrolling multiple dates', async () => { + await setDate(new Date("2001-02-27 00:00")) + await scrollDays(2) + await expectDate('2001-02-28 00:00:00') + }) + + it('not scrolling back on unusual valid dates', async () => { + await setDate(new Date("2000-02-28 00:00")) + await scrollDays(1) + await expectDate('2000-02-29 00:00:00') + }) + + it('not scrolling back after scrolling past invalid dates', async () => { + await setDate(new Date("2001-02-28 00:00")) + await scrollDays(4) + await expectDate('2001-02-01 00:00:00') + }) + + it('works on months with 30 days', async () => { + await setDate(new Date("2001-04-30 00:00")) + await scrollDays(1) + await expectDate('2001-04-30 00:00:00') + }) + + it('works on months with 31 days', async () => { + await setDate(new Date("2001-05-30 00:00")) + await scrollDays(1) + await expectDate('2001-05-31 00:00:00') + }) + + +})