From 40e74b27069f64f68121d0541e577ff7e3c040ce Mon Sep 17 00:00:00 2001 From: Henning Hall Date: Tue, 18 Apr 2023 08:09:51 +0200 Subject: [PATCH] fix(android): min/max dates not always correct when `timezoneOffsetInMinutes` is set (#635) --- .maestro/timezone-offset-in-minutes.yml | 27 +++++++++++++++++-- .maestro/utils/select-region.yml | 12 ++++++++- .maestro/utils/set-timezone-us.yml | 8 ------ .maestro/utils/set-timezone.yml | 3 ++- .../date_picker/DatePickerManager.java | 12 ++------- .../henninghall/date_picker/PickerView.java | 4 +-- .../com/henninghall/date_picker/State.java | 25 ++++++++++------- .../com/henninghall/date_picker/Utils.java | 6 +++++ .../props/TimezoneOffsetInMinutesProp.java | 13 +++++++++ .../date_picker/props/UtcProp.java | 12 --------- .../date_picker/wheels/DayWheel.java | 4 --- src/DatePickerAndroid.js | 16 +++++------ 12 files changed, 85 insertions(+), 57 deletions(-) delete mode 100644 .maestro/utils/set-timezone-us.yml create mode 100644 android/src/main/java/com/henninghall/date_picker/props/TimezoneOffsetInMinutesProp.java delete mode 100644 android/src/main/java/com/henninghall/date_picker/props/UtcProp.java diff --git a/.maestro/timezone-offset-in-minutes.yml b/.maestro/timezone-offset-in-minutes.yml index c52a1070..ccae728f 100644 --- a/.maestro/timezone-offset-in-minutes.yml +++ b/.maestro/timezone-offset-in-minutes.yml @@ -3,8 +3,9 @@ appId: com.rn069 - runFlow: file: utils/set-timezone.yml env: - TIMEZONE: Sweden + REGION: Sweden GMT: GMT+01:00 + STATE: '' - runFlow: utils/launch.yml @@ -53,8 +54,9 @@ appId: com.rn069 - runFlow: file: utils/set-timezone.yml env: - TIMEZONE: Sweden + REGION: Sweden GMT: GMT+02:00 + STATE: '' - runFlow: utils/launch.yml @@ -118,3 +120,24 @@ appId: com.rn069 - assertVisible: 'Thu Jun 1101 AM ' - runFlow: utils/reset.yml + +# test: timezoneOffsetInMinutes combined with maximumDate/minimumDate in another timezone than current device. +# Bug was reported here: https://github.com/henninghall/react-native-date-picker/issues/613 +- runFlow: + file: utils/set-timezone.yml + env: + REGION: 'United states' + STATE: Phoenix + GMT: GMT-07:00 +- runFlow: utils/launch.yml +- runFlow: + file: utils/change-prop.yml + env: + PROP: timeZoneOffsetInMinutes + VALUE: 180 +- repeat: + times: 5 + commands: + - runFlow: utils/swipe-wheel-1.yml +- runFlow: utils/swipe-wheel-4.yml +- assertVisible: 'Thu Jan 61000 AM ' diff --git a/.maestro/utils/select-region.yml b/.maestro/utils/select-region.yml index 388778f2..30c2c044 100644 --- a/.maestro/utils/select-region.yml +++ b/.maestro/utils/select-region.yml @@ -1,4 +1,14 @@ appId: com.android.settings --- - tapOn: Region -- inputText: ${TIMEZONE} +- inputText: ${REGION} +- tapOn: + text: ${REGION} + index: 1 + +- runFlow: + when: + true: ${REGION == 'United states'} + file: tap.yml + env: + TEXT: ${STATE} diff --git a/.maestro/utils/set-timezone-us.yml b/.maestro/utils/set-timezone-us.yml deleted file mode 100644 index d5fefe9d..00000000 --- a/.maestro/utils/set-timezone-us.yml +++ /dev/null @@ -1,8 +0,0 @@ -appId: com.android.settings ---- -- runFlow: - file: set-timezone.yml - env: - TIMEZONE: 'United states' - GMT: ${GMT} -- tapOn: ${STATE} diff --git a/.maestro/utils/set-timezone.yml b/.maestro/utils/set-timezone.yml index c84193ec..e9a2b4f5 100644 --- a/.maestro/utils/set-timezone.yml +++ b/.maestro/utils/set-timezone.yml @@ -29,4 +29,5 @@ appId: com.android.settings visible: Region file: select-region.yml env: - TIMEZONE: ${TIMEZONE} + REGION: ${REGION} + STATE: ${STATE} diff --git a/android/src/main/java/com/henninghall/date_picker/DatePickerManager.java b/android/src/main/java/com/henninghall/date_picker/DatePickerManager.java index 97ca4eb3..01ab1ccf 100644 --- a/android/src/main/java/com/henninghall/date_picker/DatePickerManager.java +++ b/android/src/main/java/com/henninghall/date_picker/DatePickerManager.java @@ -1,18 +1,10 @@ package com.henninghall.date_picker; -import android.app.AlertDialog; -import android.app.Dialog; -import android.app.DialogFragment; -import android.content.DialogInterface; -import android.os.Bundle; -import android.view.View; import android.widget.LinearLayout; import android.widget.RelativeLayout; import com.facebook.react.bridge.Dynamic; -import com.facebook.react.bridge.ReactApplicationContext; -import com.facebook.react.bridge.ReactMethod; import com.facebook.react.bridge.ReadableArray; import com.facebook.react.common.MapBuilder; import com.facebook.react.uimanager.SimpleViewManager; @@ -29,7 +21,7 @@ import com.henninghall.date_picker.props.MinuteIntervalProp; import com.henninghall.date_picker.props.ModeProp; import com.henninghall.date_picker.props.TextColorProp; -import com.henninghall.date_picker.props.UtcProp; +import com.henninghall.date_picker.props.TimezoneOffsetInMinutesProp; import java.lang.reflect.Method; @@ -54,7 +46,7 @@ public PickerView createViewInstance(ThemedReactContext context) { } @ReactPropGroup(names = { DateProp.name, ModeProp.name, LocaleProp.name, MaximumDateProp.name, - MinimumDateProp.name, FadeToColorProp.name, TextColorProp.name, UtcProp.name, MinuteIntervalProp.name, + MinimumDateProp.name, FadeToColorProp.name, TextColorProp.name, TimezoneOffsetInMinutesProp.name, MinuteIntervalProp.name, VariantProp.name, DividerHeightProp.name, Is24hourSourceProp.name }) public void setProps(PickerView view, int index, Dynamic value) { diff --git a/android/src/main/java/com/henninghall/date_picker/PickerView.java b/android/src/main/java/com/henninghall/date_picker/PickerView.java index b028cbf2..7bfbeded 100644 --- a/android/src/main/java/com/henninghall/date_picker/PickerView.java +++ b/android/src/main/java/com/henninghall/date_picker/PickerView.java @@ -11,7 +11,7 @@ import com.henninghall.date_picker.props.MaximumDateProp; import com.henninghall.date_picker.props.MinimumDateProp; import com.henninghall.date_picker.props.MinuteIntervalProp; -import com.henninghall.date_picker.props.UtcProp; +import com.henninghall.date_picker.props.TimezoneOffsetInMinutesProp; import com.henninghall.date_picker.props.VariantProp; import com.henninghall.date_picker.props.DateProp; import com.henninghall.date_picker.props.FadeToColorProp; @@ -77,7 +77,7 @@ public void update() { if (didUpdate(DateProp.name, HeightProp.name, LocaleProp.name, MaximumDateProp.name, MinimumDateProp.name, MinuteIntervalProp.name, ModeProp.name, - UtcProp.name, VariantProp.name + TimezoneOffsetInMinutesProp.name, VariantProp.name )) { uiManager.updateDisplayValues(); } diff --git a/android/src/main/java/com/henninghall/date_picker/State.java b/android/src/main/java/com/henninghall/date_picker/State.java index 606fca9c..b46f0856 100644 --- a/android/src/main/java/com/henninghall/date_picker/State.java +++ b/android/src/main/java/com/henninghall/date_picker/State.java @@ -17,7 +17,9 @@ import com.henninghall.date_picker.props.ModeProp; import com.henninghall.date_picker.props.Prop; import com.henninghall.date_picker.props.TextColorProp; -import com.henninghall.date_picker.props.UtcProp; +import com.henninghall.date_picker.props.TimezoneOffsetInMinutesProp; + +import net.time4j.tz.Timezone; import java.util.Calendar; import java.util.HashMap; @@ -35,7 +37,7 @@ public class State { private final MinuteIntervalProp minuteIntervalProp = new MinuteIntervalProp(); private final MinimumDateProp minimumDateProp = new MinimumDateProp(); private final MaximumDateProp maximumDateProp = new MaximumDateProp(); - private final UtcProp utcProp = new UtcProp(); + private final TimezoneOffsetInMinutesProp timezoneOffsetInMinutesProp = new TimezoneOffsetInMinutesProp(); private final HeightProp heightProp = new HeightProp(); private final VariantProp variantProp = new VariantProp(); private final DividerHeightProp dividerHeightProp = new DividerHeightProp(); @@ -50,7 +52,7 @@ public class State { put(MinuteIntervalProp.name, minuteIntervalProp); put(MinimumDateProp.name, minimumDateProp); put(MaximumDateProp.name, maximumDateProp); - put(UtcProp.name, utcProp); + put(TimezoneOffsetInMinutesProp.name, timezoneOffsetInMinutesProp); put(HeightProp.name, heightProp); put(VariantProp.name, variantProp); put(DividerHeightProp.name, dividerHeightProp); @@ -91,18 +93,23 @@ public Locale getLocale() { } public Calendar getMinimumDate() { - DateBoundary db = new DateBoundary(getTimeZone(), (String) minimumDateProp.getValue()); - return db.get(); + return Utils.isoToCalendar(minimumDateProp.getValue(), getTimeZone()); } public Calendar getMaximumDate() { - DateBoundary db = new DateBoundary(getTimeZone(), (String) maximumDateProp.getValue()); - return db.get(); + return Utils.isoToCalendar(maximumDateProp.getValue(), getTimeZone()); } public TimeZone getTimeZone() { - boolean utc = (boolean) utcProp.getValue(); - return utc ? TimeZone.getTimeZone("UTC") : TimeZone.getDefault(); + Integer offset = timezoneOffsetInMinutesProp.getValue(); + if(offset == null) return TimeZone.getDefault(); + int totalOffsetMinutes = Math.abs(offset); + char offsetDirection = offset < 0 ? '-' : '+'; + int offsetHours = (int) Math.floor(totalOffsetMinutes / 60f); + int offsetMinutes = totalOffsetMinutes - offsetHours * 60; + String timeZoneId = "GMT" + offsetDirection + offsetHours + ":" + Utils.toPaddedMinutes(offsetMinutes); + TimeZone zone = TimeZone.getTimeZone(timeZoneId); + return zone; } public String getIsoDate() { diff --git a/android/src/main/java/com/henninghall/date_picker/Utils.java b/android/src/main/java/com/henninghall/date_picker/Utils.java index 831bab1c..5cf06e3f 100644 --- a/android/src/main/java/com/henninghall/date_picker/Utils.java +++ b/android/src/main/java/com/henninghall/date_picker/Utils.java @@ -9,6 +9,7 @@ import net.time4j.PrettyTime; +import java.text.DecimalFormat; import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.ArrayList; @@ -110,4 +111,9 @@ public static String getLocalisedStringFromResources(Locale locale, String tagNa public static int toDp(int pixels){ return (int) (pixels * DatePickerPackage.context.getResources().getDisplayMetrics().density); } + + public static String toPaddedMinutes(int minutes){ + DecimalFormat df = new DecimalFormat("00"); + return df.format(minutes); + } } diff --git a/android/src/main/java/com/henninghall/date_picker/props/TimezoneOffsetInMinutesProp.java b/android/src/main/java/com/henninghall/date_picker/props/TimezoneOffsetInMinutesProp.java new file mode 100644 index 00000000..4896d493 --- /dev/null +++ b/android/src/main/java/com/henninghall/date_picker/props/TimezoneOffsetInMinutesProp.java @@ -0,0 +1,13 @@ +package com.henninghall.date_picker.props; + +import com.facebook.react.bridge.Dynamic; + +public class TimezoneOffsetInMinutesProp extends Prop { + public static final String name = "timezoneOffsetInMinutes"; + + @Override + Integer toValue(Dynamic value) { + if(value.isNull()) return null; + return value.asInt(); + } +} diff --git a/android/src/main/java/com/henninghall/date_picker/props/UtcProp.java b/android/src/main/java/com/henninghall/date_picker/props/UtcProp.java deleted file mode 100644 index 60e14c2d..00000000 --- a/android/src/main/java/com/henninghall/date_picker/props/UtcProp.java +++ /dev/null @@ -1,12 +0,0 @@ -package com.henninghall.date_picker.props; - -import com.facebook.react.bridge.Dynamic; - -public class UtcProp extends Prop { - public static final String name = "utc"; - - @Override - Boolean toValue(Dynamic value) { - return value.asBoolean(); - } -} diff --git a/android/src/main/java/com/henninghall/date_picker/wheels/DayWheel.java b/android/src/main/java/com/henninghall/date_picker/wheels/DayWheel.java index 308eea59..9753f9fa 100644 --- a/android/src/main/java/com/henninghall/date_picker/wheels/DayWheel.java +++ b/android/src/main/java/com/henninghall/date_picker/wheels/DayWheel.java @@ -50,10 +50,8 @@ private Calendar getStartCal(){ Calendar min = state.getMinimumDate(); if (min != null) { cal = (Calendar) min.clone(); - resetToMidnight(cal); } else if (max != null) { cal = (Calendar) max.clone(); - resetToMidnight(cal); cal.add(Calendar.DATE, -cal.getActualMaximum(Calendar.DAY_OF_YEAR) / 2); } else { cal = (Calendar) getInitialDate().clone(); @@ -68,10 +66,8 @@ private Calendar getEndCal(){ Calendar min = state.getMinimumDate(); if (max != null) { cal = (Calendar) max.clone(); - resetToMidnight(cal); } else if (min != null) { cal = (Calendar) min.clone(); - resetToMidnight(cal); cal.add(Calendar.DATE, cal.getActualMaximum(Calendar.DAY_OF_YEAR) / 2); } else { cal = (Calendar) getInitialDate().clone(); diff --git a/src/DatePickerAndroid.js b/src/DatePickerAndroid.js index 8fffcf63..dc79e69d 100644 --- a/src/DatePickerAndroid.js +++ b/src/DatePickerAndroid.js @@ -46,10 +46,15 @@ class DatePickerAndroid extends React.PureComponent { date: this._date(), minimumDate: this._minimumDate(), maximumDate: this._maximumDate(), - utc: this.props.timeZoneOffsetInMinutes !== undefined, + timezoneOffsetInMinutes: this._getTimezoneOffsetInMinutes(), style: this._getStyle(), }) + _getTimezoneOffsetInMinutes = () => { + if (this.props.timeZoneOffsetInMinutes == undefined) return undefined + return this.props.timeZoneOffsetInMinutes + } + _getStyle = () => { const width = this.props.mode === 'time' ? timeModeWidth : defaultWidth return [{ width, height }, this.props.style] @@ -74,16 +79,11 @@ class DatePickerAndroid extends React.PureComponent { _date = () => this._toIsoWithTimeZoneOffset(this.props.date) _fromIsoWithTimeZoneOffset = (timestamp) => { - const date = new Date(timestamp) - if (this.props.timeZoneOffsetInMinutes === undefined) return date - return addMinutes(date, -this.props.timeZoneOffsetInMinutes) + return new Date(timestamp) } _toIsoWithTimeZoneOffset = (date) => { - if (this.props.timeZoneOffsetInMinutes === undefined) - return date.toISOString() - - return addMinutes(date, this.props.timeZoneOffsetInMinutes).toISOString() + return date.toISOString() } _onConfirm = (isoDate) => {