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

Keep apart VEVENT.DTEND and .DURATION #598

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
79 changes: 78 additions & 1 deletion Ical.Net.Tests/CalendarEventTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -469,4 +469,81 @@ public void HourMinuteSecondOffsetParsingTest()
var expectedNegative = TimeSpan.FromMinutes(-17.5);
Assert.That(negativeOffset?.Offset, Is.EqualTo(expectedNegative));
}
}


[Test, Category("CalendarEvent")]
public void TestGetEffectiveDuration()
{
var now = _now.Subtract(TimeSpan.FromTicks(_now.Ticks % TimeSpan.TicksPerSecond));

var evt = new CalendarEvent()
{
DtStart = new CalDateTime(now) { HasTime = true },
DtEnd = new CalDateTime(now.AddHours(1)) { HasTime = true },
};

Assert.Multiple(() =>
{
Assert.That(evt.DtStart.Value, Is.EqualTo(now));
Assert.That(evt.DtEnd.Value, Is.EqualTo(now.AddHours(1)));
Assert.That(evt.GetFirstDuration(), Is.EqualTo(TimeSpan.FromHours(1)));
});

evt = new CalendarEvent()
{
DtStart = new CalDateTime(now.Date) { HasTime = true },
DtEnd = new CalDateTime(now.Date.AddHours(1)) { HasTime = true },
};

Assert.Multiple(() =>
{
Assert.That(evt.DtStart.Value, Is.EqualTo(now.Date));
Assert.That(evt.GetFirstDuration(), Is.EqualTo(TimeSpan.FromHours(1)));
});

evt = new CalendarEvent()
{
DtStart = new CalDateTime(now.Date) { HasTime = false },
};

Assert.Multiple(() =>
{
Assert.That(evt.DtStart.Value, Is.EqualTo(now.Date));
Assert.That(evt.GetFirstDuration(), Is.EqualTo(TimeSpan.FromDays(1)));
});

evt = new CalendarEvent()
{
DtStart = new CalDateTime(now) { HasTime = true },
Duration = TimeSpan.FromHours(2),
};

Assert.Multiple(() => {
Assert.That(evt.DtStart.Value, Is.EqualTo(now));
Assert.That(evt.DtEnd, Is.Null);
Assert.That(evt.GetFirstDuration(), Is.EqualTo(TimeSpan.FromHours(2)));
});

evt = new CalendarEvent()
{
DtStart = new CalDateTime(now.Date) { HasTime = true },
Duration = TimeSpan.FromHours(2),
};

Assert.Multiple(() => {
Assert.That(evt.DtStart.Value, Is.EqualTo(now.Date));
Assert.That(evt.GetFirstDuration(), Is.EqualTo(TimeSpan.FromHours(2)));
});

evt = new CalendarEvent()
{
DtStart = new CalDateTime(now.Date) { HasTime = false },
Duration = TimeSpan.FromDays(1),
};

Assert.Multiple(() => {
Assert.That(evt.DtStart.Value, Is.EqualTo(now.Date));
Assert.That(evt.GetFirstDuration(), Is.EqualTo(TimeSpan.FromDays(1)));
});
}
}
24 changes: 23 additions & 1 deletion Ical.Net.Tests/DeserializationTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -552,4 +552,26 @@ public void Property1()
Assert.That(props[i].Value, Is.EqualTo("2." + i));
}
}
}

[Test]
[TestCase(true)]
[TestCase(false)]
public void KeepApartDtEndAndDuration_Tests(bool useDtEnd)
{
var calStr = $@"BEGIN:VCALENDAR
BEGIN:VEVENT
DTSTART:20070406T230000Z
{(useDtEnd ? "DTEND:20070407T010000Z" : "DURATION:PT1H")}
END:VEVENT
END:VCALENDAR
";

var calendar = Calendar.Load(calStr);

Assert.Multiple(() =>
{
Assert.That(calendar.Events.Single().DtEnd != null, Is.EqualTo(useDtEnd));
Assert.That(calendar.Events.Single().Duration != default, Is.EqualTo(!useDtEnd));
});
}
}
30 changes: 30 additions & 0 deletions Ical.Net.Tests/RecurrenceTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3728,4 +3728,34 @@ public void ExecuteRecurrenceTestCase(RecurrenceTestCase testCase)

Assert.That(startDates, Is.EqualTo(testCase.Instances));
}

[Test]
// Reproducer from https://github.com/ical-org/ical.net/issues/629
public void ShouldCreateARecurringYearlyEvent()
{
var springAdminEvent = new CalendarEvent
{
Start = new CalDateTime(DateTime.Parse("2024-04-15")),
End = new CalDateTime(DateTime.Parse("2024-04-15")),
RecurrenceRules = new List<RecurrencePattern> { new RecurrencePattern(FrequencyType.Yearly, 1) },
};

var calendar = new Calendar();
calendar.Events.Add(springAdminEvent);
var searchStart = DateTime.Parse("2024-04-15");
var searchEnd = DateTime.Parse("2050-5-31");
var occurrences = calendar.GetOccurrences(searchStart, searchEnd);
Assert.That(occurrences.Count, Is.EqualTo(27));

springAdminEvent.Start = new CalDateTime(DateTime.Parse("2024-04-16"));
springAdminEvent.End = new CalDateTime(DateTime.Parse("2024-04-16"));
springAdminEvent.RecurrenceRules = new List<RecurrencePattern> { new RecurrencePattern(FrequencyType.Yearly, 1) };

searchStart = DateTime.Parse("2024-04-16");
searchEnd = DateTime.Parse("2050-5-31");
occurrences = calendar.GetOccurrences(searchStart, searchEnd);

//occurences is 26 here, omitting 4/16/2024
Assert.That(occurrences.Count, Is.EqualTo(27));
}
}
32 changes: 25 additions & 7 deletions Ical.Net.Tests/SymmetricSerializationTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,17 @@ public class SymmetricSerializationTests
private static readonly DateTime _later = _nowTime.AddHours(1);
private static CalendarSerializer GetNewSerializer() => new CalendarSerializer();
private static string SerializeToString(Calendar c) => GetNewSerializer().SerializeToString(c);
private static CalendarEvent GetSimpleEvent() => new CalendarEvent { DtStart = new CalDateTime(_nowTime), DtEnd = new CalDateTime(_later), Duration = _later - _nowTime };
private static CalendarEvent GetSimpleEvent(bool useDtEnd = true)
{
var evt = new CalendarEvent { DtStart = new CalDateTime(_nowTime) };
if (useDtEnd)
evt.DtEnd = new CalDateTime(_later);
else
evt.Duration = _later - _nowTime;

return evt;
}

private static Calendar UnserializeCalendar(string s) => Calendar.Load(s);

[Test, TestCaseSource(nameof(Event_TestCases))]
Expand All @@ -47,25 +57,33 @@ public void Event_Tests(Calendar iCalendar)
}

public static IEnumerable<ITestCaseData> Event_TestCases()
{
return Event_TestCasesInt(true).Concat(Event_TestCasesInt(false));
}

private static IEnumerable<ITestCaseData> Event_TestCasesInt(bool useDtEnd)
{
var rrule = new RecurrencePattern(FrequencyType.Daily, 1) { Count = 5 };
var e = new CalendarEvent
{
DtStart = new CalDateTime(_nowTime),
DtEnd = new CalDateTime(_later),
Duration = TimeSpan.FromHours(1),
RecurrenceRules = new List<RecurrencePattern> { rrule },
};

if (useDtEnd)
e.DtEnd = new CalDateTime(_later);
else
e.Duration = _later - _nowTime;

var calendar = new Calendar();
calendar.Events.Add(e);
yield return new TestCaseData(calendar).SetName("readme.md example");
yield return new TestCaseData(calendar).SetName($"readme.md example with {(useDtEnd ? "DTEND" : "DURATION")}");

e = GetSimpleEvent();
e = GetSimpleEvent(useDtEnd);
e.Description = "This is an event description that is really rather long. Hopefully the line breaks work now, and it's serialized properly.";
calendar = new Calendar();
calendar.Events.Add(e);
yield return new TestCaseData(calendar).SetName("Description serialization isn't working properly. Issue #60");
yield return new TestCaseData(calendar).SetName($"Description serialization isn't working properly. Issue #60 {(useDtEnd ? "DTEND" : "DURATION")}");
}

[Test]
Expand Down Expand Up @@ -234,4 +252,4 @@ public void CategoriesTest()
var deserialized = UnserializeCalendar(serialized);
Assert.That(deserialized, Is.EqualTo(c));
}
}
}
98 changes: 50 additions & 48 deletions Ical.Net/CalendarComponents/CalendarEvent.cs
Original file line number Diff line number Diff line change
Expand Up @@ -31,27 +31,6 @@ public class CalendarEvent : RecurringComponent, IAlarmContainer, IComparable<Ca
{
internal const string ComponentName = "VEVENT";

/// <summary>
/// The start date/time of the event.
/// <note>
/// If the duration has not been set, but
/// the start/end time of the event is available,
/// the duration is automatically determined.
/// Likewise, if the end date/time has not been
/// set, but a start and duration are available,
/// the end date/time will be extrapolated.
/// </note>
/// </summary>
public override IDateTime DtStart
{
get => base.DtStart;
set
{
base.DtStart = value;
ExtrapolateTimes(2);
}
}

/// <summary>
/// The end date/time of the event.
/// <note>
Expand All @@ -71,7 +50,6 @@ public virtual IDateTime DtEnd
if (!Equals(DtEnd, value))
{
Properties.Set("DTEND", value);
ExtrapolateTimes(0);
}
}
}
Expand Down Expand Up @@ -105,11 +83,58 @@ public virtual TimeSpan Duration
if (!Equals(Duration, value))
{
Properties.Set("DURATION", value);
ExtrapolateTimes(1);
}
}
}

/// <summary>
/// Calculates and returns the duration of the first occurrence of this event.
/// </summary>
/// <remarks>
/// If the 'DURATION' property is set, this method will return its value.
/// Otherwise, if DTSTART and DTEND are set, it will return DTSTART minus DTEND.
/// Otherwise it will return `default(TimeSpan)`.
/// Note that for recurring events, the duration of individual occurrences may vary
/// if they span a DST change.
/// </remarks>
/// <returns>The effective duration of this event.</returns>
public virtual TimeSpan GetFirstDuration()
{
if (Properties.ContainsKey("DURATION"))
return Duration;

if (DtStart is not null)
{
if (DtEnd is not null)
{
// The "DTEND" property
// for a "VEVENT" calendar component specifies the non-inclusive end
// of the event.
return DtEnd.Subtract(DtStart);
}
else if (!DtStart.HasTime)
{
// For cases where a "VEVENT" calendar component
// specifies a "DTSTART" property with a DATE value type but no
// "DTEND" nor "DURATION" property, the event's duration is taken to
// be one day.
return TimeSpan.FromDays(1);
}
else
{
// For cases where a "VEVENT" calendar component
// specifies a "DTSTART" property with a DATE-TIME value type but no
// "DTEND" property, the event ends on the same calendar date and
// time of day specified by the "DTSTART" property.
return TimeSpan.Zero;
}
}

// This is an illegal state. We return zero for compatibility reasons.
return TimeSpan.Zero;
}


/// <summary>
/// An alias to the DtEnd field (i.e. end date/time).
/// </summary>
Expand Down Expand Up @@ -264,31 +289,6 @@ protected override void OnDeserializing(StreamingContext context)
protected override void OnDeserialized(StreamingContext context)
{
base.OnDeserialized(context);

ExtrapolateTimes(-1);
}

private void ExtrapolateTimes(int source)
{
/*
* Source values, a fix introduced to prevent stack overflow exceptions from occuring.
* -1 = Anybody, stack overflow could maybe still occur in this case?
* 0 = End
* 1 = Duration
* 2 = DtStart
*/
if (DtEnd == null && DtStart != null && Duration != default(TimeSpan) && source != 0)
{
DtEnd = DtStart.Add(Duration);
}
else if (Duration == default(TimeSpan) && DtStart != null && DtEnd != null && source != 1)
{
Duration = DtEnd.Subtract(DtStart);
}
else if (DtStart == null && Duration != default(TimeSpan) && DtEnd != null && source != 2)
{
DtStart = DtEnd.Subtract(Duration);
}
}

protected bool Equals(CalendarEvent other)
Expand All @@ -301,6 +301,7 @@ protected bool Equals(CalendarEvent other)
&& string.Equals(Summary, other.Summary, StringComparison.OrdinalIgnoreCase)
&& string.Equals(Description, other.Description, StringComparison.OrdinalIgnoreCase)
&& Equals(DtEnd, other.DtEnd)
&& Equals(Duration, other.Duration)
&& string.Equals(Location, other.Location, StringComparison.OrdinalIgnoreCase)
&& resourcesSet.SetEquals(other.Resources)
&& string.Equals(Status, other.Status, StringComparison.Ordinal)
Expand Down Expand Up @@ -361,6 +362,7 @@ public override int GetHashCode()
hashCode = (hashCode * 397) ^ (Summary?.GetHashCode() ?? 0);
hashCode = (hashCode * 397) ^ (Description?.GetHashCode() ?? 0);
hashCode = (hashCode * 397) ^ (DtEnd?.GetHashCode() ?? 0);
hashCode = (hashCode * 397) ^ Duration.GetHashCode();
hashCode = (hashCode * 397) ^ (Location?.GetHashCode() ?? 0);
hashCode = (hashCode * 397) ^ Status?.GetHashCode() ?? 0;
hashCode = (hashCode * 397) ^ IsActive.GetHashCode();
Expand Down
8 changes: 4 additions & 4 deletions Ical.Net/Evaluation/EventEvaluator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -45,19 +45,19 @@ public override HashSet<Period> Evaluate(IDateTime referenceTime, DateTime perio

foreach (var period in Periods)
{
period.Duration = CalendarEvent.Duration;
period.Duration = CalendarEvent.GetFirstDuration();
period.EndTime = period.Duration == default
? period.StartTime
: period.StartTime.Add(CalendarEvent.Duration);
: period.StartTime.Add(CalendarEvent.GetFirstDuration());
}

// Ensure each period has a duration
foreach (var period in Periods.Where(p => p.EndTime == null))
{
period.Duration = CalendarEvent.Duration;
period.Duration = CalendarEvent.GetFirstDuration();
period.EndTime = period.Duration == default
? period.StartTime
: period.StartTime.Add(CalendarEvent.Duration);
: period.StartTime.Add(CalendarEvent.GetFirstDuration());
}

return Periods;
Expand Down
Loading