@@ -22,9 +22,13 @@ import net.fortuna.ical4j.model.component.VEvent
2222import net.fortuna.ical4j.model.component.VTimeZone
2323import net.fortuna.ical4j.model.parameter.Email
2424import net.fortuna.ical4j.model.property.Attendee
25+ import net.fortuna.ical4j.model.property.DtEnd
26+ import net.fortuna.ical4j.model.property.DtStart
2527import net.fortuna.ical4j.model.property.ProdId
28+ import net.fortuna.ical4j.transform.rfc5545.DatePropertyRule
2629import org.junit.Assert.assertEquals
2730import org.junit.Assert.assertNotEquals
31+ import org.junit.Assert.assertNotNull
2832import org.junit.Test
2933import java.io.StringReader
3034import java.io.StringWriter
@@ -178,6 +182,55 @@ class Ical4jTest {
178182 assertEquals(1609945200000 , dt2.time)
179183 }
180184
185+ @Test
186+ fun `TZID with parentheses and space + DatePropertyRule` () {
187+ /* DTSTART;TZID="...":... is formally invalid because RFC 5545 only allows tzidparam to be a
188+ paramtext and not a quoted-string for an unknown reason (see also https://www.rfc-editor.org/errata/eid5505).
189+ Some generators don't know that and still use DQUOTE. Doing so caused a problem with DAVx5.
190+ This test verifies that ical4j is capable to parse such TZIDs. */
191+ val tzRegistry = TimeZoneRegistryFactory .getInstance().createRegistry()
192+ val cal = CalendarBuilder (tzRegistry).build(
193+ StringReader (" BEGIN:VCALENDAR\n " +
194+ " VERSION:2.0\n " +
195+ " BEGIN:VTIMEZONE\n " +
196+ " TZID:(GMT -05:00)\n " +
197+ " BEGIN:STANDARD\n " +
198+ " DTSTART:19700101T020000\n " +
199+ " TZOFFSETFROM:-0400\n " +
200+ " TZOFFSETTO:-0500\n " +
201+ " RRULE:FREQ=YEARLY;BYDAY=1SU;BYMONTH=11;WKST=SU\n " +
202+ " END:STANDARD\n " +
203+ " BEGIN:DAYLIGHT\n " +
204+ " DTSTART:19700101T020000\n " +
205+ " TZOFFSETFROM:-0500\n " +
206+ " TZOFFSETTO:-0400\n " +
207+ " RRULE:FREQ=YEARLY;BYDAY=2SU;BYMONTH=3;WKST=SU\n " +
208+ " END:DAYLIGHT\n " +
209+ " END:VTIMEZONE\n " +
210+ " BEGIN:VEVENT\n " +
211+ " DTSTART;TZID=\" (GMT -05:00)\" :20250124T190000\n " + // technically invalid TZID parameter
212+ " DTEND;TZID=\" (GMT -05:00)\" :20250124T203000\n " + // technically invalid TZID parameter
213+ " SUMMARY:Special timezone definition\n " +
214+ " END:VEVENT\n " +
215+ " END:VCALENDAR\n "
216+ )
217+ )
218+ val event = cal.getComponent<VEvent >(Component .VEVENT )
219+ val tzGMT5 = tzRegistry.getTimeZone(" (GMT -05:00)" )
220+ assertNotNull(tzGMT5)
221+ assertEquals(DtStart (" 20250124T190000" , tzGMT5), event.startDate)
222+ assertEquals(DtEnd (" 20250124T203000" , tzGMT5), event.endDate)
223+
224+ // now apply DatePropertyRule
225+ DatePropertyRule ().applyTo(event.startDate)
226+ DatePropertyRule ().applyTo(event.endDate)
227+
228+ /* "(GMT -05:00)" is neither in msTimezones, nor in IANA timezones, so
229+ DatePropertyRule completely removes it, but keeps the offset. */
230+ assertEquals(DtStart (DateTime (" 20250125T000000Z" )), event.startDate)
231+ assertEquals(DtEnd (DateTime (" 20250125T013000Z" )), event.endDate)
232+ }
233+
181234 @Test(expected = ParserException ::class )
182235 fun `Unparseable event with timezone with RDATE with PERIOD` () {
183236 CalendarBuilder ().build(
0 commit comments