Skip to content
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
67 changes: 58 additions & 9 deletions Ical.Net.Tests/FreeBusyTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,25 +14,74 @@ namespace Ical.Net.Tests;
public class FreeBusyTest
{
/// <summary>
/// Ensures that GetFreeBusyStatus() return the correct status.
/// Ensures that GetFreeBusyStatus() return the correct status for date/time arguments.
/// </summary>
[Test, Category("FreeBusy")]
public void GetFreeBusyStatus1()
public void GetFreeBusyStatusByDateTime()
{
var cal = new Calendar();

var evt = cal.Create<CalendarEvent>();
evt.Summary = "Test event";
evt.Start = new CalDateTime(2010, 10, 1, 8, 0, 0);
evt.End = new CalDateTime(2010, 10, 1, 9, 0, 0);
evt.Start = new CalDateTime(2025, 10, 1, 8, 0, 0);
evt.End = new CalDateTime(2025, 10, 1, 9, 0, 0);

var freeBusy = cal.GetFreeBusy(
new CalDateTime(2025, 10, 1, 0, 0, 0),
new CalDateTime(2025, 10, 7, 11, 59, 59))!;

var freeBusy = cal.GetFreeBusy(new CalDateTime(2010, 10, 1, 0, 0, 0), new CalDateTime(2010, 10, 7, 11, 59, 59))!;
Assert.Multiple(() =>
{
Assert.That(freeBusy.GetFreeBusyStatus(new CalDateTime(2010, 10, 1, 7, 59, 59)), Is.EqualTo(FreeBusyStatus.Free));
Assert.That(freeBusy.GetFreeBusyStatus(new CalDateTime(2010, 10, 1, 8, 0, 0)), Is.EqualTo(FreeBusyStatus.Busy));
Assert.That(freeBusy.GetFreeBusyStatus(new CalDateTime(2010, 10, 1, 8, 59, 59)), Is.EqualTo(FreeBusyStatus.Busy));
Assert.That(freeBusy.GetFreeBusyStatus(new CalDateTime(2010, 10, 1, 9, 0, 0)), Is.EqualTo(FreeBusyStatus.Free));
Assert.That(freeBusy.GetFreeBusyStatus(new CalDateTime(2025, 10, 1, 7, 59, 59)),
Is.EqualTo(FreeBusyStatus.Free));
Assert.That(freeBusy.GetFreeBusyStatus(new CalDateTime(2025, 10, 1, 8, 0, 0)),
Is.EqualTo(FreeBusyStatus.Busy));
Assert.That(freeBusy.GetFreeBusyStatus(new CalDateTime(2025, 10, 1, 8, 59, 59)),
Is.EqualTo(FreeBusyStatus.Busy));
Assert.That(freeBusy.GetFreeBusyStatus(new CalDateTime(2025, 10, 1, 9, 0, 0)),
Is.EqualTo(FreeBusyStatus.Free));
});
}

[Test, Category("FreeBusy")]
public void GetFreeBusyStatusByPeriod()
{
var cal = new Calendar();

var evt = cal.Create<CalendarEvent>();
evt.Summary = "Test event";
evt.Start = new CalDateTime(2025, 6, 1, 8, 0, 0);
evt.End = new CalDateTime(2025, 6, 1, 10, 0, 0);

var freeBusy = cal.GetFreeBusy(
new CalDateTime(2025, 6, 1, 0, 0, 0),
new CalDateTime(2025, 6, 7, 0, 0, 0))!;

Assert.Multiple(() =>
{
// Period completely before the event (ends when event starts)
var periodBefore = new Period(new CalDateTime(2025, 6, 1, 7, 0, 0),
new CalDateTime(2025, 6, 1, 8, 0, 0));
Assert.That(freeBusy.GetFreeBusyStatus(periodBefore),
Is.EqualTo(FreeBusyStatus.Free));

// Period entirely within the event (should be busy)
var periodDuring = new Period(new CalDateTime(2025, 6, 1, 8, 0, 0),
new CalDateTime(2025, 6, 1, 8, 59, 59));
Assert.That(freeBusy.GetFreeBusyStatus(periodDuring),
Is.EqualTo(FreeBusyStatus.Busy));

// Period spanning before and into the event (should be busy)
var periodSpanningStart = new Period(new CalDateTime(2025, 6, 1, 7, 59, 59),
new CalDateTime(2025, 6, 1, 8, 30, 0));
Assert.That(freeBusy.GetFreeBusyStatus(periodSpanningStart),
Is.EqualTo(FreeBusyStatus.Busy));

// Period starting at the event's end (should be free)
var periodAfter = new Period(new CalDateTime(2025, 6, 1, 10, 0, 0),
new CalDateTime(2025, 6, 1, 12, 0, 0));
Assert.That(freeBusy.GetFreeBusyStatus(periodAfter),
Is.EqualTo(FreeBusyStatus.Free));
});
}
}
178 changes: 178 additions & 0 deletions Ical.Net.Tests/PeriodTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

#nullable enable
using System;
using System.Collections.Generic;
using Ical.Net.DataTypes;
using NUnit.Framework;

Expand Down Expand Up @@ -85,4 +86,181 @@
}
});
}

[Test]
public void CompareTo_ReturnsExpectedValues()
{
var dt = new CalDateTime(2025, 6, 1, 0, 0, 0, "Europe/Vienna");

Assert.Multiple(() =>
{
Assert.That(new Period(dt).CompareTo(null),
Is.EqualTo(1));
Assert.That(new Period(dt).CompareTo(new Period(dt)),
Is.EqualTo(0));
Assert.That(new Period(dt).CompareTo(new Period(dt.AddHours(-1))),
Is.EqualTo(1));
Assert.That(new Period(dt).CompareTo(new Period(dt.AddHours(1))),
Is.EqualTo(-1));
});
}

[Test, TestCaseSource(nameof(CollidesWithPeriodTestCases))]
public void CollidesWithPeriod(Period period1, Period? period2, bool expected)
{
Assert.Multiple(() =>
{
Assert.That(period1.CollidesWith(period2), Is.EqualTo(expected));
Assert.That(period2?.CollidesWith(period1) == true, Is.EqualTo(expected));
});
}

private static IEnumerable<TestCaseData> CollidesWithPeriodTestCases
{
get
{
// Overlapping periods
yield return new TestCaseData(
new Period(new CalDateTime(2025, 1, 1, 0, 0, 0), Duration.FromHours(2)),
new Period(new CalDateTime(2025, 1, 1, 1, 0, 0), Duration.FromHours(2)),
true
).SetName("Overlap: period1 and period2 overlap by 1 hour");

// Contiguous periods (end of one is start of another, exclusive end)
yield return new TestCaseData(
new Period(new CalDateTime(2025, 1, 1, 0, 0, 0), Duration.FromHours(1)),
new Period(new CalDateTime(2025, 1, 1, 1, 0, 0), Duration.FromHours(1)),
false
).SetName("Contiguous: period1 ends when period2 starts (no overlap)");

// One inside another
yield return new TestCaseData(
new Period(new CalDateTime(2025, 1, 1, 0, 0, 0), Duration.FromHours(4)),
new Period(new CalDateTime(2025, 1, 1, 1, 0, 0), Duration.FromHours(1)),
true
).SetName("Contained: period2 is inside period1");

// Identical periods
yield return new TestCaseData(
new Period(new CalDateTime(2025, 1, 1, 0, 0, 0), Duration.FromHours(2)),
new Period(new CalDateTime(2025, 1, 1, 0, 0, 0), Duration.FromHours(2)),
true
).SetName("Identical: periods are exactly the same");

// Non-overlapping periods
yield return new TestCaseData(
new Period(new CalDateTime(2025, 1, 1, 0, 0, 0), Duration.FromHours(1)),
new Period(new CalDateTime(2025, 1, 1, 2, 0, 0), Duration.FromHours(1)),
false
).SetName("NoOverlap: periods are completely separate");

// This Duration is zero (point in time)
yield return new TestCaseData(
new Period(new CalDateTime(2025, 1, 1, 0, 0, 0), Duration.Zero),
new Period(new CalDateTime(2025, 1, 1, 0, 0, 0), Duration.FromHours(1)),
false
).SetName("NoOverlap: this duration is zero");

// Other Duration is zero (point in time)
yield return new TestCaseData(
new Period(new CalDateTime(2025, 1, 1, 0, 0, 0), Duration.FromHours(1)),
new Period(new CalDateTime(2025, 1, 1, 0, 0, 0), Duration.Zero),
false
).SetName("NoOverlap: other duration is zero");

// other period is null
yield return new TestCaseData(
new Period(new CalDateTime(2025, 1, 1, 0, 0, 0), Duration.FromHours(1)),
null,
false
).SetName("NoOverlap: other period is null");

}
}

[Test]
public void PeriodCollidesWith_WhenNoDurationShouldThrow()
{
var period1 = new Period(new CalDateTime(2025, 1, 1, 0, 0, 0), Duration.FromHours(2));
var period2 = new Period(new CalDateTime(2025, 1, 1, 0, 0, 0));

Assert.Multiple(() =>
{
Assert.Throws<ArgumentException>(() => _ = period1.CollidesWith(period2));
Assert.Throws<ArgumentException>(() => _ = period2.CollidesWith(period1));
});
}

[Test]
public void EffectiveEndTime_WithoutDuration_ShouldBeNull()
{
var period = new Period(new CalDateTime(2025, 7, 1, 12, 0, 0));
Assert.That(period.EffectiveEndTime, Is.Null, "EffectiveEndTime should be null when no duration is set.");
}

[Test]
public void Contains_Tests()
{
var start = new CalDateTime(2025, 1, 1, 0, 0, 0, CalDateTime.UtcTzId);
var dtBefore = start.AddSeconds(-1);
var dtAtStart = start;
var dtMid = start.AddMinutes(30);
var dtAtEnd = start.AddHours(1);
var dtAfter = start.AddHours(1).AddSeconds(1);

// Period with duration: effective end time = start + 1 hour (exclusive)
var periodWithDuration = new Period(start, Duration.FromHours(1));
var periodWithoutDuration = new Period(start);
Assert.Multiple(() =>
{
Assert.That(periodWithDuration.Contains(null), Is.False, "Contains should return false for null dt.");
Assert.That(periodWithDuration.Contains(dtBefore), Is.False, "Contains should return false if dt is before start.");
Assert.That(periodWithDuration.Contains(dtAtStart), Is.True, "Contains should return true for dt equal to start.");
Assert.That(periodWithDuration.Contains(dtMid), Is.True, "Contains should return true for dt in the middle.");
Assert.That(periodWithDuration.Contains(dtAtEnd), Is.False, "Contains should return false for dt equal to effective end (exclusive).");
Assert.That(periodWithDuration.Contains(dtAfter), Is.False, "Contains should return false for dt after effective end.");
Assert.That(periodWithoutDuration.Contains(dtAtStart), Is.True, "Contains should return true for self without effective end");
});
}

[Test]
public void Equals_Tests()
{
var start = new CalDateTime(2025, 1, 1, 0, 0, 0, "America/New_York");
var duration1 = Duration.FromHours(1);
var duration2 = Duration.FromHours(2);

var period1 = new Period(start, duration1);
var period2 = new Period(new CalDateTime(2025, 1, 1, 0, 0, 0, "America/New_York"), duration1);
var period3 = new Period(start, duration2);

Assert.Multiple(() =>
{
// Test equality for identical periods.
Assert.That(period1.Equals(period2), Is.True,
"Periods with identical start and duration should be equal.");
Assert.That(period2.Equals(period1), Is.True,
"Symmetric equality failed.");
Assert.That(period1.GetHashCode(), Is.EqualTo(period2.GetHashCode()),
"Hash codes should match for equal periods.");

// Test non-equality with different duration.
Assert.That(period1.Equals(period3), Is.False,
"Periods with different durations should not be equal.");
Assert.That(period3.Equals(period1), Is.False,
"Symmetric non-equality failed.");

// Test equality with self.
Assert.That(period1.Equals(period1), Is.True,
"A period should equal itself.");

// Test equality with null.
Assert.That(period1.Equals(null), Is.False,
"A period should not equal null.");

// Test equality with object of different type.
Assert.That(period1.Equals("string"), Is.False,

Check warning on line 262 in Ical.Net.Tests/PeriodTests.cs

View workflow job for this annotation

GitHub Actions / coverage

Dereference of a possibly null reference.

Check warning on line 262 in Ical.Net.Tests/PeriodTests.cs

View workflow job for this annotation

GitHub Actions / coverage

Dereference of a possibly null reference.

Check warning on line 262 in Ical.Net.Tests/PeriodTests.cs

View workflow job for this annotation

GitHub Actions / coverage

Dereference of a possibly null reference.

Check warning on line 262 in Ical.Net.Tests/PeriodTests.cs

View workflow job for this annotation

GitHub Actions / tests

Dereference of a possibly null reference.

Check warning on line 262 in Ical.Net.Tests/PeriodTests.cs

View workflow job for this annotation

GitHub Actions / tests

Dereference of a possibly null reference.

Check warning on line 262 in Ical.Net.Tests/PeriodTests.cs

View workflow job for this annotation

GitHub Actions / tests

Dereference of a possibly null reference.
"A period should not be equal to an object of different type.");
});
}
}
34 changes: 27 additions & 7 deletions Ical.Net/DataTypes/Period.cs
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,8 @@ public virtual CalDateTime? EffectiveEndTime
{
null when _duration is null => null,
{ } endTime => endTime,

// If _duration is not null, EffectiveDuration is not null
_ => CalculateEndTime()
};
}
Expand Down Expand Up @@ -243,9 +245,8 @@ public virtual bool Contains(CalDateTime? dt)
return false;
}

var endTime = EffectiveEndTime;
// End time is exclusive
return endTime?.GreaterThan(dt) != false;
return EffectiveEndTime?.GreaterThan(dt) != false;
}

/// <summary>
Expand All @@ -258,11 +259,30 @@ public virtual bool Contains(CalDateTime? dt)
/// </remarks>
/// <param name="period"></param>
/// <returns></returns>
public virtual bool CollidesWith(Period period)
=> Contains(period.StartTime)
|| period.Contains(StartTime)
|| Contains(period.EffectiveEndTime)
|| period.Contains(EffectiveEndTime);
public virtual bool CollidesWith(Period? period)
{
if (period is null) return false;

if (EffectiveDuration is null || period.EffectiveDuration is null)
{
throw new ArgumentException("Both periods must have a defined (non-null) duration to check for collisions. For collisions with date/time use Contains().", nameof(period));
}

var thisStart = StartTime;
var thisEnd = EffectiveEndTime;
var otherStart = period.StartTime;
var otherEnd = period.EffectiveEndTime;

// Periods without a duration are not colliding
if (EffectiveDuration.Value.IsZero || period.EffectiveDuration.Value.IsZero)
{
return false;
}

// Periods overlap if their start and end times are overlapping.
// End time is exclusive.
return thisStart.LessThan(otherEnd) && otherStart.LessThan(thisEnd);
}

/// <inheritdoc/>
public int CompareTo(Period? other)
Expand Down
Loading