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 Nov 17, 2024
1 parent 2c6c637 commit 6779452
Show file tree
Hide file tree
Showing 5 changed files with 179 additions and 54 deletions.
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));
});
}
}
29 changes: 24 additions & 5 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) };
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,24 +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),
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
93 changes: 50 additions & 43 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,26 +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
*/
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 @@ -296,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 @@ -356,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

0 comments on commit 6779452

Please sign in to comment.