Skip to content

Commit

Permalink
CalendarEvent: Don't set DURATION resp. DTEND from each other. On…
Browse files Browse the repository at this point in the history
…ly calculate the duration on the fly when needed. This way we don't overwrite the information, which of both was originally set and can adjust calculations accordingly (in the future). See ical-org#574.
  • Loading branch information
minichma committed Oct 15, 2024
1 parent 587e8bb commit 5951a3c
Show file tree
Hide file tree
Showing 5 changed files with 128 additions and 54 deletions.
63 changes: 62 additions & 1 deletion Ical.Net.Tests/CalendarEventTest.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using Ical.Net.CalendarComponents;
using Ical.Net.CalendarComponents;
using Ical.Net.DataTypes;
using Ical.Net.Serialization;
using NUnit.Framework;
Expand Down Expand Up @@ -438,5 +438,66 @@ public void HourMinuteSecondOffsetParsingTest()
var expectedNegative = TimeSpan.FromMinutes(-17.5);
Assert.AreEqual(expectedNegative, negativeOffset?.Offset);
}

[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.AreEqual(now, evt.DtStart.Value);
Assert.AreEqual(now.AddHours(1), evt.DtEnd.Value);
Assert.AreEqual(TimeSpan.FromHours(1), evt.GetEffectiveDuration());

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

Assert.AreEqual(now.Date, evt.DtStart.Value);
Assert.AreEqual(TimeSpan.FromHours(1), evt.GetEffectiveDuration());

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

Assert.AreEqual(now.Date, evt.DtStart.Value);
Assert.AreEqual(TimeSpan.FromDays(1), evt.GetEffectiveDuration());

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

Assert.AreEqual(now, evt.DtStart.Value);
Assert.IsNull(evt.DtEnd);
Assert.AreEqual(TimeSpan.FromHours(2), evt.GetEffectiveDuration());

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

Assert.AreEqual(now.Date, evt.DtStart.Value);
Assert.AreEqual(TimeSpan.FromHours(2), evt.GetEffectiveDuration());

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

Assert.AreEqual(now.Date, evt.DtStart.Value);
Assert.AreEqual(TimeSpan.FromDays(1), evt.GetEffectiveDuration());
}
}
}
22 changes: 21 additions & 1 deletion Ical.Net.Tests/DeserializationTests.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using Ical.Net.CalendarComponents;
using Ical.Net.CalendarComponents;
using Ical.Net.DataTypes;
using Ical.Net.Serialization;
using Ical.Net.Serialization.DataTypes;
Expand Down Expand Up @@ -505,5 +505,25 @@ public void Property1()
Assert.AreEqual("2." + i, props[i].Value);
}
}


[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.AreEqual(useDtEnd, calendar.Events.Single().DtEnd != null);
Assert.AreEqual(!useDtEnd, calendar.Events.Single().Duration != default);
}
}
}
29 changes: 24 additions & 5 deletions Ical.Net.Tests/SymmetricSerializationTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,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)};
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 @@ -38,24 +48,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),
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
60 changes: 17 additions & 43 deletions Ical.Net/CalendarComponents/CalendarEvent.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,27 +26,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 @@ -66,7 +45,6 @@ public virtual IDateTime DtEnd
if (!Equals(DtEnd, value))
{
Properties.Set("DTEND", value);
ExtrapolateTimes(0);
}
}
}
Expand Down Expand Up @@ -100,11 +78,25 @@ public virtual TimeSpan Duration
if (!Equals(Duration, value))
{
Properties.Set("DURATION", value);
ExtrapolateTimes(1);
}
}
}

/// <summary>
/// Calculates and returns the effective duration of this event.
/// </summary>
/// <remarks>
/// If the 'DURATION' property iis set, this method will return it.
/// Otherwise, if DTSTART and DTEND are set, it will calculate the duration from those.
/// Otherwise it will return `default(TimeSpan)`.
/// </remarks>
/// <returns>The effective duration of this event.</returns>
public virtual TimeSpan GetEffectiveDuration()
=> (Properties.ContainsKey("DURATION")) ? Duration
: ((DtEnd != null) && (DtStart != null)) ? DtEnd.Subtract(DtStart)
: ((DtStart != null) && !DtStart.HasTime) ? TimeSpan.FromDays(1)
: default(TimeSpan);

/// <summary>
/// An alias to the DtEnd field (i.e. end date/time).
/// </summary>
Expand Down Expand Up @@ -257,26 +249,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
*/
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);
}
}

protected bool Equals(CalendarEvent other)
Expand All @@ -289,6 +261,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 @@ -348,6 +321,7 @@ public override int GetHashCode()
var hashCode = DtStart?.GetHashCode() ?? 0;
hashCode = (hashCode * 397) ^ (Summary?.GetHashCode() ?? 0);
hashCode = (hashCode * 397) ^ (Description?.GetHashCode() ?? 0);
hashCode = (hashCode * 397) ^ Duration.GetHashCode();
hashCode = (hashCode * 397) ^ (DtEnd?.GetHashCode() ?? 0);
hashCode = (hashCode * 397) ^ (Location?.GetHashCode() ?? 0);
hashCode = (hashCode * 397) ^ Status?.GetHashCode() ?? 0;
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 @@ -40,19 +40,19 @@ public override HashSet<Period> Evaluate(IDateTime referenceTime, DateTime perio

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

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

return Periods;
Expand Down

0 comments on commit 5951a3c

Please sign in to comment.