From 9e8dba125b396673c39a3a9903909b4db31226f5 Mon Sep 17 00:00:00 2001 From: Shai Almog <67850168+shai-almog@users.noreply.github.com> Date: Sat, 30 May 2026 16:58:30 +0300 Subject: [PATCH] fix(Calendar): preserve hour/minute/second/millis on setDate (#1515) The 2015 reporter showed that the UI Calendar component was silently normalising the time-of-day on every setDate / setCurrentDate / setSelectedDate call. MonthView.setSelectedDay and setCurrentDay forcibly reset HOUR_OF_DAY=1, MINUTE/SECOND/MILLISECOND=0 (lines 1184-1188 and 1055-1070 pre-fix) so any caller round-tripping a Date through Calendar lost the time component. The normalisation is load-bearing for the day-cell renderer: the dates[] array stores normalised millis and the highlight code does exact long equality against SELECTED_DAY/currentDay (lines 1122 and 1302), so simply storing the raw millis breaks day highlighting. Add side fields originalSelectedDay / originalCurrentDay that remember the millis the public setter received, separate from the normalised SELECTED_DAY / currentDay used for cell comparison. The public getDate() / getCurrentDate() return the original when one was supplied; setDate/setSelectedDate/setCurrentDate populate it; a user-initiated day-cell tap clears it (because a tap only conveys day, not time-of-day) so we cleanly fall back to the cell millis. Closes #1515. Adds maven/core-unittests/.../CalendarDatePreservationTest.java with five regression cases covering setDate, setSelectedDate, setCurrentDate, midnight (which pre-fix became 01:00 and could roll the day backwards), and day-of-month integrity after the round-trip. Existing CalendarTest stays green; 38 Calendar+date tests overall pass. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../src/com/codename1/ui/Calendar.java | 28 +++++++ .../ui/CalendarDatePreservationTest.java | 83 +++++++++++++++++++ 2 files changed, 111 insertions(+) create mode 100644 maven/core-unittests/src/test/java/com/codename1/ui/CalendarDatePreservationTest.java 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)); + } +}