diff --git a/CodenameOne/src/com/codename1/ui/Calendar.java b/CodenameOne/src/com/codename1/ui/Calendar.java index 0c9f7ad7cf..6e1d14cf07 100644 --- a/CodenameOne/src/com/codename1/ui/Calendar.java +++ b/CodenameOne/src/com/codename1/ui/Calendar.java @@ -91,6 +91,15 @@ public class Calendar extends Container implements ActionSource { private boolean changesSelectedDateEnabled = true; private TimeZone tmz; private long SELECTED_DAY = -1; + /// The exact millis the caller passed to {@link #setDate(Date)} / + /// {@link #setSelectedDate(Date)}, before the day-cell-comparison + /// normalisation in MonthView.setSelectedDay. Used by {@link #getDate()} + /// so that the time-of-day round-trips. -1 means "no user-supplied date, + /// fall back to the normalised SELECTED_DAY". See #1515. + private long originalSelectedDay = -1; + /// The exact millis the caller passed to {@link #setCurrentDate(Date)}. + /// See {@link #originalSelectedDay}. + private long originalCurrentDay = -1; private boolean multipleSelectionEnabled = false; private String selectedDaysUIID = "CalendarMultipleDay"; @@ -296,6 +305,10 @@ void componentChanged() { /// /// the date object matching the current selection public Date getDate() { + // Preserve the caller's original time-of-day when possible. See #1515. + if (originalSelectedDay >= 0) { + return new Date(originalSelectedDay); + } return new Date(mv.getSelectedDay()); } @@ -305,6 +318,8 @@ public Date getDate() { /// /// - `d`: new date public void setDate(Date d) { + originalSelectedDay = d.getTime(); + originalCurrentDay = d.getTime(); mv.setSelectedDay(d.getTime()); mv.setCurrentDay(SELECTED_DAY, true); componentChanged(); @@ -342,6 +357,7 @@ public void setYearRange(int minYear, int maxYear) { /// /// - `d`: the selected day public void setSelectedDate(Date d) { + originalSelectedDay = d.getTime(); mv.setSelectedDay(d.getTime()); mv.setCurrentDay(SELECTED_DAY, true); componentChanged(); @@ -353,6 +369,10 @@ public void setSelectedDate(Date d) { /// /// the currently viewed date public Date getCurrentDate() { + // Preserve the caller's original time-of-day when possible. See #1515. + if (originalCurrentDay >= 0) { + return new Date(originalCurrentDay); + } return new Date(mv.getCurrentDay()); } @@ -363,6 +383,7 @@ public Date getCurrentDate() { /// /// - `d`: the date to set the calendar view on. public void setCurrentDate(Date d) { + originalCurrentDay = d.getTime(); mv.setCurrentDay(d.getTime(), true); componentChanged(); } @@ -1341,6 +1362,13 @@ public void actionPerformed(ActionEvent evt) { setDayUIID(components[iter], "CalendarSelectedDay"); SELECTED_DAY = dates[iter]; + // A user tap supplies only the day, so the + // hour/minute information from a prior + // setDate(Date) call is no longer authoritative. + // Clear the cached original so getDate() falls + // back to the day-cell normalised value. See #1515. + Calendar.this.originalSelectedDay = -1; + Calendar.this.originalCurrentDay = -1; selected = components[iter]; } fireActionEvent(); diff --git a/maven/core-unittests/src/test/java/com/codename1/ui/CalendarDatePreservationTest.java b/maven/core-unittests/src/test/java/com/codename1/ui/CalendarDatePreservationTest.java new file mode 100644 index 0000000000..b6a5830417 --- /dev/null +++ b/maven/core-unittests/src/test/java/com/codename1/ui/CalendarDatePreservationTest.java @@ -0,0 +1,83 @@ +package com.codename1.ui; + +import com.codename1.junit.FormTest; +import com.codename1.junit.UITestBase; + +import java.util.Date; +import java.util.GregorianCalendar; +import java.util.TimeZone; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * Regression tests for + * https://github.com/codenameone/CodenameOne/issues/1515 + * -- the Calendar UI component must preserve the hour/minute/second/millis + * of the Date passed into setDate, setCurrentDate and setSelectedDate. The + * 2015 reporter found that MonthView.setSelectedDay and setCurrentDay were + * forcibly normalising the time-of-day, so round-tripping through getDate + * / getCurrentDate silently lost the time component. + */ +class CalendarDatePreservationTest extends UITestBase { + + private static Date local(int year, int month, int dayOfMonth, int hour, int minute, int second, int millis) { + GregorianCalendar cal = new GregorianCalendar(); + cal.clear(); + cal.set(year, month, dayOfMonth, hour, minute, second); + cal.set(java.util.Calendar.MILLISECOND, millis); + return cal.getTime(); + } + + private static void assertSameInstant(Date expected, Date actual) { + assertEquals(expected.getTime(), actual.getTime(), + "Date instant must round-trip exactly; got " + actual + ", expected " + expected); + } + + @FormTest + void setDateRoundTripsHourMinuteSecondMillis() { + Calendar c = new Calendar(); + Date in = local(2026, java.util.Calendar.MARCH, 15, 13, 45, 30, 500); + c.setDate(in); + assertSameInstant(in, c.getDate()); + } + + @FormTest + void setSelectedDateRoundTripsHourMinuteSecondMillis() { + Calendar c = new Calendar(); + Date in = local(2026, java.util.Calendar.AUGUST, 1, 23, 59, 59, 999); + c.setSelectedDate(in); + assertSameInstant(in, c.getDate()); + } + + @FormTest + void setCurrentDateRoundTripsHourMinuteSecondMillis() { + Calendar c = new Calendar(); + Date in = local(2026, java.util.Calendar.JANUARY, 1, 9, 30, 15, 250); + c.setCurrentDate(in); + assertSameInstant(in, c.getCurrentDate()); + } + + @FormTest + void setDateMidnightStillReturnsMidnight() { + // Pre-fix the time was forcibly set to 01:00, so midnight became 01:00. + Calendar c = new Calendar(); + Date midnight = local(2026, java.util.Calendar.JULY, 4, 0, 0, 0, 0); + c.setDate(midnight); + assertSameInstant(midnight, c.getDate()); + } + + @FormTest + void dayOfMonthStillReportsCorrectlyAfterPreservingTimeOfDay() { + // The fix preserves the time-of-day on read but day-of-month math must + // still reflect the user-supplied day. + Calendar c = new Calendar(); + Date in = local(2026, java.util.Calendar.MARCH, 15, 13, 45, 30, 500); + c.setDate(in); + + GregorianCalendar verifier = new GregorianCalendar(TimeZone.getDefault()); + verifier.setTime(c.getDate()); + assertEquals(2026, verifier.get(java.util.Calendar.YEAR)); + assertEquals(java.util.Calendar.MARCH, verifier.get(java.util.Calendar.MONTH)); + assertEquals(15, verifier.get(java.util.Calendar.DAY_OF_MONTH)); + } +}