Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Serialized EXDATE is unpermitted PERIOD value type when originated as System.DateTime #591

Open
jvraines opened this issue Sep 1, 2024 · 9 comments · May be fixed by #684
Open

Serialized EXDATE is unpermitted PERIOD value type when originated as System.DateTime #591

jvraines opened this issue Sep 1, 2024 · 9 comments · May be fixed by #684
Labels

Comments

@jvraines
Copy link

jvraines commented Sep 1, 2024

This code

Calendar myCal = new Calendar();
CalendarEvent myEvent = new CalendarEvent {
    Start = new CalDateTime(2024, 12, 1, 12, 0, 0),
    End = new CalDateTime(2025, 1, 1, 18, 0, 0),
    RecurrenceRules = new List<RecurrencePattern> {
                            new RecurrencePattern {
                                Frequency = FrequencyType.Daily
                            }
                      }
};
myEvent.ExceptionDates.Add(new PeriodList { 
    new CalDateTime(2024, 12, 25)
    new CalDateTime(new DateTime(2024, 12, 26))
});
myCal.Events.Add(myEvent);
Console.WriteLine(new CalendarSerializer().SerializeToString(myCal));

produces

BEGIN:VCALENDAR
PRODID:-//github.com/rianjs/ical.net//NONSGML ical.net 4.0//EN
VERSION:2.0
BEGIN:VEVENT
DTEND:20250101T180000
DTSTAMP:20240901T232258Z
DTSTART:20241201T120000
EXDATE:20241225T000000,20241226/P1D
RRULE:FREQ=DAILY
SEQUENCE:0
UID:d65129f0-d00c-46d1-a7d2-81a73a41990d
END:VEVENT
END:VCALENDAR

The first excluded date is an acceptable DATE-TIME value type. The second, however, is a PERIOD value type which is not permitted by the EXDATE spec.

@RemcoBlok
Copy link

Another (presumably related) issue is that if you create the CalDateTime with a TZID (when it is an exception date for an event that is not an all-day event, but for a specific time on the day), iCal.NET does not serialize the TZID with the EXDATE. When deserializing this may result in GetOccurrences() producing an occurrence on the EXDATE, or what was supposed to be the EXDATE if it did not loose the TZID.

What can I do as a temporary workaround here? Fix the EXDATE after deserializing? Or remove the occurrence from the result of GetOccurrences()?

@axunonb
Copy link
Collaborator

axunonb commented Oct 17, 2024

Hi Remco, please kindly follow the Filing a bug report and submit a new issue. This makes it more simple and faster to understand the whole context of what your doing. If you think, ,your issue is related to this one, add a reference.

@jvraines
Copy link
Author

@RemcoBlok Which CalDateTime constructor are you using? One with string tzId? If so, you could try passing appropriate time values to the constructor without that parameter and see what happens.

@RemcoBlok
Copy link

Thanks for your reply. I created a separate issue #614

@axunonb
Copy link
Collaborator

axunonb commented Oct 20, 2024

For EXDATE with a timezone it looks like the parameter for the timezone gets recognized and set, but not processed when rendering to a string. Als EXDATE with different timezones is not implemented correctly.
We'll look closer into that, also in context of the referenced like #614, #588

else if (!string.IsNullOrWhiteSpace(dt.TzId))
{
dt.Parameters.Set("TZID", dt.TzId);
}
DateTime.SpecifyKind(dt.Value, kind);
// FIXME: what if DATE is the default value type for this?
// Also, what if the DATE-TIME value type is specified on something
// where DATE-TIME is the default value type? It should be removed
// during serialization, as it's redundant...
if (!dt.HasTime)
{
dt.SetValueType("DATE");
}
var value = new StringBuilder();
value.Append($"{dt.Year:0000}{dt.Month:00}{dt.Day:00}");
if (dt.HasTime)
{
value.Append($"T{dt.Hour:00}{dt.Minute:00}{dt.Second:00}");
if (dt.IsUtc)
{
value.Append("Z");
}
}

@axunonb
Copy link
Collaborator

axunonb commented Nov 1, 2024

@minichma, after examining the serialization and deserialization of the EXDATE property, my current conclusion is that it will cause a breaking change. The IList<PeriodList> CalendarEvent.ExceptionDates is being serialized by the PeriodListSerializer, which does not handle EXDATE correctly. What are your thoughts on the following points?

  1. Is using IList<PeriodList> for ExceptionDates the best choice, or would IList<IDateTime> be more appropriate?
  2. Should we create a dedicated ExDateSerializer that inherits from PropertySerializer?
  3. Would it be sensible to create a v5 branch for all upcoming changes and eventually publish alpha versions on NuGet? I believe updating future v4 versions without breaking changes would be challenging.

@jvraines
Copy link
Author

jvraines commented Nov 1, 2024

  1. Is using IList<PeriodList> for ExceptionDates the best choice, or would IList<IDateTime> be more appropriate?
    The latter. ExceptionDate is defined to be a Date or a DateTime, not a Period, by the iCal specification. I don't know why PeriodList was chosen, except perhaps it facilitates code flow elsewhere.
  2. Should we create a dedicated ExDateSerializer that inherits from PropertySerializer?
    I have no opinion since I have not comprehensively reviewed the code.
  3. Would it be sensible to create a v5 branch for all upcoming changes and eventually publish alpha versions on NuGet? I believe updating future v4 versions without breaking changes would be challenging.
    You are probably a better judge of this than I, although I would support the position that a breaking change should be handled explicity and with due caution.

@minichma
Copy link
Collaborator

minichma commented Nov 2, 2024

Is using IList<PeriodList> for ExceptionDates the best choice, or would IList<IDateTime> be more appropriate?

Yes, IList<IDateTime> would be more appropriate. I assume ExceptionDates has been copied from RecurrenceDates, where PeriodList is correct, but EXDATEs don't support durations, as this issue correctly points out. The question would be, whether it is perfectly correct to serialize RDATEs into List<Period>, as it could be any of date-time / date / period, but not mixed though.

Should we create a dedicated ExDateSerializer that inherits from PropertySerializer?
I have no opinion since I have not comprehensively reviewed the code.

Not sure, whether a separate ExDateSerializer is required. Basically we can have multiple list of ExDates and each list can be different in terms of type (DATE vs DATE-TIME) and time zone. In the end we'd end up with a something like List<List>.

Would it be sensible to create a v5 branch for all upcoming changes and eventually publish alpha versions on NuGet? I believe updating future v4 versions without breaking changes would be challenging.

Fully agree. #598 is waiting for a v5 branch too. Porting everything back to v4 would certainly be challenging in many cases. But v5 would probably be a work in progress for quite a while.

@axunonb
Copy link
Collaborator

axunonb commented Dec 19, 2024

@minichma I'll grab this issue as the next task. What about using a HashSet<CalDateTime> for EXDATE?

Finally found, that in ical.net v4.1.7, IList<PeriodList> RecurringComponent.ExceptionDates de/serialized properly. Let's first see, which code changes in a later version broke it. DATE-ONLY was a problem all the way

@axunonb axunonb added the bug label Dec 23, 2024
axunonb added a commit to axunonb/ical.net that referenced this issue Dec 29, 2024
Improve reliability and usability for `Period` and `PeriodList` to become less error-prone
by enforcing timezones being used consistently

Period
- Make parameterless CTOR `internal` to ensure proper initialization by users
- A period can be defined
  1. by a start time and an end time,
  2. by a start time and a duration,
  3. by a start time only, with the duration unspecified. This is for EXDATE and RDATE date-only and date/time.
- For cosistency, either the `EndTime` or the `Duration` can be set at a time. The last one set with a value not `null` will prevail, while the other will become `null`.
- Timezones of `StartTime`and (optional) `EndTime` must be the same
- `CompareTo` uses `AsUtc` for comparing the `StartTime`
- Remove returning a "magic" duration of 1 day, if `EndTime` is null and `StartTime` is date-only. This broke `EXDATE` and `RDATE` of an event, when the only a start time exists.
- Added `EffectiveEndTime` and `EffectiveDuration` properties to provide calculated values based on the set values.
- Update the `EndTime` and `Duration` properties to directly return the set values.
- Change the access modifiers of `GetEffectiveDuration()` and `GetEffectiveEndTime` methods from internal to private.

PeriodList
- `TzId`: `public` setter changed to `private`
- `EnsureConsistentTimezones`: The first period determines the timezone of the `PeriodList` and all other `Period`s added must have the same timezone
- Add `SetService(new PeriodListEvaluator(this))` for `StringReader` CTOR overload
- Add `static PeriodList FromStringReader(StringReader)`
- Add `static PeriodList FromDateTime(IDateTime)`
- Add `PeriodList AddPeriod(Period)` for chaining
- Add `PeriodList Add(IDateTime)` for chaining
- nullable enable

EventEvaluator:
- `EventEvaluator.WithEndTime(Period)` only sets the `EndTime`, as `Period.EffectiveDuration` returns the duration.

TodoEvaluator:
- Remove method `PeriodWithDuration(Period)` as it became redudant with the refactored `Period` class.

DataTypeSerializer and other serializers, CalendarObjectBase:
- `Activator.CreateInstance(TargetType, true)` allows for not `public` CTORs, so that parameterless CTORs can be excluded from the public API, if proper initialization can't be assured (like with `Period`).

PropertySerializer:
- TBD: Never set the UTC timezone ID (use appending 'Z')

- Resolves ical-org#590
- Resolves ical-org#591
- Resolves ical-org#614
- Resolves ical-org#676
axunonb added a commit to axunonb/ical.net that referenced this issue Dec 29, 2024
Improve reliability and usability for `Period` and `PeriodList` to become less error-prone
by enforcing timezones being used consistently

Period
- Make parameterless CTOR `internal` to ensure proper initialization by users
- A period can be defined
  1. by a start time and an end time,
  2. by a start time and a duration,
  3. by a start time only, with the duration unspecified. This is for EXDATE and RDATE date-only and date/time.
- For cosistency, either the `EndTime` or the `Duration` can be set at a time. The last one set with a value not `null` will prevail, while the other will become `null`.
- Timezones of `StartTime`and (optional) `EndTime` must be the same
- `CompareTo` uses `AsUtc` for comparing the `StartTime`
- Remove returning a "magic" duration of 1 day, if `EndTime` is null and `StartTime` is date-only. This broke `EXDATE` and `RDATE` of an event, when the only a start time exists.
- Added `EffectiveEndTime` and `EffectiveDuration` properties to provide calculated values based on the set values.
- Update the `EndTime` and `Duration` properties to directly return the set values.
- Change the access modifiers of `GetEffectiveDuration()` and `GetEffectiveEndTime` methods from internal to private.

PeriodList
- `TzId`: `public` setter changed to `private`
- `EnsureConsistentTimezones`: The first period determines the timezone of the `PeriodList` and all other `Period`s added must have the same timezone
- Add `SetService(new PeriodListEvaluator(this))` for `StringReader` CTOR overload
- Add `static PeriodList FromStringReader(StringReader)`
- Add `static PeriodList FromDateTime(IDateTime)`
- Add `PeriodList AddPeriod(Period)` for chaining
- Add `PeriodList Add(IDateTime)` for chaining
- nullable enable

EventEvaluator:
- `EventEvaluator.WithEndTime(Period)` only sets the `EndTime`, as `Period.EffectiveDuration` returns the duration.

TodoEvaluator:
- Remove method `PeriodWithDuration(Period)` as it became redudant with the refactored `Period` class.

DataTypeSerializer and other serializers, CalendarObjectBase:
- `Activator.CreateInstance(TargetType, true)` allows for not `public` CTORs, so that parameterless CTORs can be excluded from the public API, if proper initialization can't be assured (like with `Period`).

PropertySerializer:
- TBD: Never set the UTC timezone ID (use appending 'Z')

- Resolves ical-org#590
- Resolves ical-org#591
- Resolves ical-org#614
- Resolves ical-org#676
axunonb added a commit to axunonb/ical.net that referenced this issue Dec 29, 2024
Improve reliability and usability for `Period` and `PeriodList` to become less error-prone
by enforcing timezones being used consistently

Period
- Make parameterless CTOR `internal` to ensure proper initialization by users
- A period can be defined
  1. by a start time and an end time,
  2. by a start time and a duration,
  3. by a start time only, with the duration unspecified. This is for EXDATE and RDATE date-only and date/time.
- For cosistency, either the `EndTime` or the `Duration` can be set at a time. The last one set with a value not `null` will prevail, while the other will become `null`.
- Timezones of `StartTime`and (optional) `EndTime` must be the same
- `CompareTo` uses `AsUtc` for comparing the `StartTime`
- Remove returning a "magic" duration of 1 day, if `EndTime` is null and `StartTime` is date-only. This broke `EXDATE` and `RDATE` of an event, when the only a start time exists.
- Added `EffectiveEndTime` and `EffectiveDuration` properties to provide calculated values based on the set values.
- Update the `EndTime` and `Duration` properties to directly return the set values.
- Change the access modifiers of `GetEffectiveDuration()` and `GetEffectiveEndTime` methods from internal to private.

PeriodList
- `TzId`: `public` setter changed to `private`
- `EnsureConsistentTimezones`: The first period determines the timezone of the `PeriodList` and all other `Period`s added must have the same timezone
- Add `SetService(new PeriodListEvaluator(this))` for `StringReader` CTOR overload
- Add `static PeriodList FromStringReader(StringReader)`
- Add `static PeriodList FromDateTime(IDateTime)`
- Add `PeriodList AddPeriod(Period)` for chaining
- Add `PeriodList Add(IDateTime)` for chaining
- nullable enable

EventEvaluator:
- `EventEvaluator.WithEndTime(Period)` only sets the `EndTime`, as `Period.EffectiveDuration` returns the duration.

TodoEvaluator:
- Remove method `PeriodWithDuration(Period)` as it became redudant with the refactored `Period` class.

DataTypeSerializer and other serializers, CalendarObjectBase:
- `Activator.CreateInstance(TargetType, true)` allows for not `public` CTORs, so that parameterless CTORs can be excluded from the public API, if proper initialization can't be assured (like with `Period`).

PropertySerializer:
- TBD: Never set the UTC timezone ID (use appending 'Z')

- Resolves ical-org#590
- Resolves ical-org#591
- Resolves ical-org#614
- Resolves ical-org#676
axunonb added a commit to axunonb/ical.net that referenced this issue Dec 29, 2024
Improve reliability and usability for `Period` and `PeriodList` to become less error-prone
by enforcing timezones being used consistently

Period
- Make parameterless CTOR `internal` to ensure proper initialization by users
- A period can be defined
  1. by a start time and an end time,
  2. by a start time and a duration,
  3. by a start time only, with the duration unspecified. This is for EXDATE and RDATE date-only and date/time.
- For cosistency, either the `EndTime` or the `Duration` can be set at a time. The last one set with a value not `null` will prevail, while the other will become `null`.
- Timezones of `StartTime`and (optional) `EndTime` must be the same
- `CompareTo` uses `AsUtc` for comparing the `StartTime`
- Remove returning a "magic" duration of 1 day, if `EndTime` is null and `StartTime` is date-only. This broke `EXDATE` and `RDATE` of an event, when the only a start time exists.
- Added `EffectiveEndTime` and `EffectiveDuration` properties to provide calculated values based on the set values.
- Update the `EndTime` and `Duration` properties to directly return the set values.
- Change the access modifiers of `GetEffectiveDuration()` and `GetEffectiveEndTime` methods from internal to private.

PeriodList
- `TzId`: `public` setter changed to `private`
- `EnsureConsistentTimezones`: The first period determines the timezone of the `PeriodList` and all other `Period`s added must have the same timezone
- Add `SetService(new PeriodListEvaluator(this))` for `StringReader` CTOR overload
- Add `static PeriodList FromStringReader(StringReader)`
- Add `static PeriodList FromDateTime(IDateTime)`
- Add `PeriodList AddPeriod(Period)` for chaining
- Add `PeriodList Add(IDateTime)` for chaining
- nullable enable

EventEvaluator:
- `EventEvaluator.WithEndTime(Period)` only sets the `EndTime`, as `Period.EffectiveDuration` returns the duration.

TodoEvaluator:
- Remove method `PeriodWithDuration(Period)` as it became redudant with the refactored `Period` class.

DataTypeSerializer and other serializers, CalendarObjectBase:
- `Activator.CreateInstance(TargetType, true)` allows for not `public` CTORs, so that parameterless CTORs can be excluded from the public API, if proper initialization can't be assured (like with `Period`).

PropertySerializer:
- TBD: Never set the UTC timezone ID (use appending 'Z')

- Resolves ical-org#590
- Resolves ical-org#591
- Resolves ical-org#614
- Resolves ical-org#676
axunonb added a commit to axunonb/ical.net that referenced this issue Jan 5, 2025
Improve reliability and usability for `Period` and `PeriodList` to become less error-prone
by enforcing timezones being used consistently

Period
- Make parameterless CTOR `internal` to ensure proper initialization by users
- A period can be defined
  1. by a start time and an end time,
  2. by a start time and a duration,
  3. by a start time only, with the duration unspecified. This is for EXDATE and RDATE date-only and date/time.
- For cosistency, either the `EndTime` or the `Duration` can be set at a time. The last one set with a value not `null` will prevail, while the other will become `null`.
- Timezones of `StartTime`and (optional) `EndTime` must be the same
- `CompareTo` uses `AsUtc` for comparing the `StartTime`
- Remove returning a "magic" duration of 1 day, if `EndTime` is null and `StartTime` is date-only. This broke `EXDATE` and `RDATE` of an event, when the only a start time exists.
- Added `EffectiveEndTime` and `EffectiveDuration` properties to provide calculated values based on the set values.
- Update the `EndTime` and `Duration` properties to directly return the set values.
- Change the access modifiers of `GetEffectiveDuration()` and `GetEffectiveEndTime` methods from internal to private.

PeriodList
- `TzId`: `public` setter changed to `private`
- `EnsureConsistentTimezones`: The first period determines the timezone of the `PeriodList` and all other `Period`s added must have the same timezone
- Add `SetService(new PeriodListEvaluator(this))` for `StringReader` CTOR overload
- Add `static PeriodList FromStringReader(StringReader)`
- Add `static PeriodList FromDateTime(IDateTime)`
- Add `PeriodList AddPeriod(Period)` for chaining
- Add `PeriodList Add(IDateTime)` for chaining
- nullable enable

EventEvaluator:
- `EventEvaluator.WithEndTime(Period)` only sets the `EndTime`, as `Period.EffectiveDuration` returns the duration.

TodoEvaluator:
- Remove method `PeriodWithDuration(Period)` as it became redudant with the refactored `Period` class.

DataTypeSerializer and other serializers, CalendarObjectBase:
- `Activator.CreateInstance(TargetType, true)` allows for not `public` CTORs, so that parameterless CTORs can be excluded from the public API, if proper initialization can't be assured (like with `Period`).

PropertySerializer:
- TBD: Never set the UTC timezone ID (use appending 'Z')

- Resolves ical-org#590
- Resolves ical-org#591
- Resolves ical-org#614
- Resolves ical-org#676
axunonb added a commit to axunonb/ical.net that referenced this issue Jan 8, 2025
Improve reliability and usability for `Period` and `PeriodList` to become less error-prone
by enforcing timezones being used consistently

Period
- Make parameterless CTOR `internal` to ensure proper initialization by users
- A period can be defined
  1. by a start time and an end time,
  2. by a start time and a duration,
  3. by a start time only, with the duration unspecified. This is for EXDATE and RDATE date-only and date/time.
- For cosistency, either the `EndTime` or the `Duration` can be set at a time. The last one set with a value not `null` will prevail, while the other will become `null`.
- Timezones of `StartTime`and (optional) `EndTime` must be the same
- `CompareTo` uses `AsUtc` for comparing the `StartTime`
- Remove returning a "magic" duration of 1 day, if `EndTime` is null and `StartTime` is date-only. This broke `EXDATE` and `RDATE` of an event, when the only a start time exists.
- Added `EffectiveEndTime` and `EffectiveDuration` properties to provide calculated values based on the set values.
- Update the `EndTime` and `Duration` properties to directly return the set values.
- Change the access modifiers of `GetEffectiveDuration()` and `GetEffectiveEndTime` methods from internal to private.

PeriodList
- `TzId`: `public` setter changed to `private`
- `EnsureConsistentTimezones`: The first period determines the timezone of the `PeriodList` and all other `Period`s added must have the same timezone
- Add `SetService(new PeriodListEvaluator(this))` for `StringReader` CTOR overload
- Add `static PeriodList FromStringReader(StringReader)`
- Add `static PeriodList FromDateTime(IDateTime)`
- Add `PeriodList AddPeriod(Period)` for chaining
- Add `PeriodList Add(IDateTime)` for chaining
- nullable enable

EventEvaluator:
- `EventEvaluator.WithEndTime(Period)` only sets the `EndTime`, as `Period.EffectiveDuration` returns the duration.

TodoEvaluator:
- Remove method `PeriodWithDuration(Period)` as it became redudant with the refactored `Period` class.

DataTypeSerializer and other serializers, CalendarObjectBase:
- `Activator.CreateInstance(TargetType, true)` allows for not `public` CTORs, so that parameterless CTORs can be excluded from the public API, if proper initialization can't be assured (like with `Period`).

PropertySerializer:
- TBD: Never set the UTC timezone ID (use appending 'Z')

- Resolves ical-org#590
- Resolves ical-org#591
- Resolves ical-org#614
- Resolves ical-org#676
axunonb added a commit to axunonb/ical.net that referenced this issue Jan 8, 2025
Improve reliability and usability for `Period` and `PeriodList` to become less error-prone
by enforcing timezones being used consistently

Period
- Make parameterless CTOR `internal` to ensure proper initialization by users
- A period can be defined
  1. by a start time and an end time,
  2. by a start time and a duration,
  3. by a start time only, with the duration unspecified. This is for EXDATE and RDATE date-only and date/time.
- For cosistency, either the `EndTime` or the `Duration` can be set at a time. The last one set with a value not `null` will prevail, while the other will become `null`.
- Timezones of `StartTime`and (optional) `EndTime` must be the same
- `CompareTo` uses `AsUtc` for comparing the `StartTime`
- Remove returning a "magic" duration of 1 day, if `EndTime` is null and `StartTime` is date-only. This broke `EXDATE` and `RDATE` of an event, when the only a start time exists.
- Added `EffectiveEndTime` and `EffectiveDuration` properties to provide calculated values based on the set values.
- Update the `EndTime` and `Duration` properties to directly return the set values.
- Change the access modifiers of `GetEffectiveDuration()` and `GetEffectiveEndTime` methods from internal to private.

PeriodList
- `TzId`: `public` setter changed to `private`
- `EnsureConsistentTimezones`: The first period determines the timezone of the `PeriodList` and all other `Period`s added must have the same timezone
- Add `SetService(new PeriodListEvaluator(this))` for `StringReader` CTOR overload
- Add `static PeriodList FromStringReader(StringReader)`
- Add `static PeriodList FromDateTime(IDateTime)`
- Add `PeriodList AddPeriod(Period)` for chaining
- Add `PeriodList Add(IDateTime)` for chaining
- nullable enable

EventEvaluator:
- `EventEvaluator.WithEndTime(Period)` only sets the `EndTime`, as `Period.EffectiveDuration` returns the duration.

TodoEvaluator:
- Remove method `PeriodWithDuration(Period)` as it became redudant with the refactored `Period` class.

DataTypeSerializer and other serializers, CalendarObjectBase:
- `Activator.CreateInstance(TargetType, true)` allows for not `public` CTORs, so that parameterless CTORs can be excluded from the public API, if proper initialization can't be assured (like with `Period`).

PropertySerializer:
- TBD: Never set the UTC timezone ID (use appending 'Z')

- Resolves ical-org#590
- Resolves ical-org#591
- Resolves ical-org#614
- Resolves ical-org#676
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging a pull request may close this issue.

4 participants