Skip to content

Commit

Permalink
Introduce Duration type to keep apart nominal vs exact durations. (i…
Browse files Browse the repository at this point in the history
…cal-org#680)

* Make Todo.Duration nullable

* Remove Todo.ExtrapolateTimes()

* Test: Minor fix related to CalendarEvent.Duration being nullable now

* Introduce type `Duration` and replace `TimeSpanSerializer` by `DurationSerializer` in preparation of using the type throughout the lib.

* Remove redundant `CalDateTime.Subtract(TimeSpan)`, which can also be written as `CalDateTime.Add(-d)`.

* Remove obsolete `DaetUtil.Start/EndOfDay`

* Change `CalendarEvent.Duration`, `Todo.Duration`, `Trigger.Duration` from `TimeSpan` to `Duration`.

* Rename `CalDateTime.Subtract()` -> `CalDateTime.SubtractExact()`

* Change `TimeSpan CalendarEvent.GetTimeSpanToAddToPeriodStartTime()` -> `Duration CalendarEvent.GetEffectiveDuration()`.

* Fix date/time arithmetics: Keep apart nominal and exact durations.

* CalDateTime doc comments

* Change `Period.Duration` from `TimeSpan` to `Duration`. Remove extrapolation and introduce `GetEffectiveDuration()`, `GetEffectiveEndTime()`.

* CalDateTimeTests: Indentation and whitespace

* Test: Extend RecurrenceTest test cases to test for occurrences durations.

* Improve `CalDateTime.ToTimeZone()` conversion of floating time that doesn't exist in the target tz.

* RecurrencePatternEvaluator: Adjust nonexistent recurrence instances according to RFC 5545 3.3.5

* Test: DurationOfRecurrencesOverDst

* Allow `IDateTime.ToTimeZone` to convert to floating time.

* CalendarEvent: Avoid null-reference warnings.
  • Loading branch information
minichma authored Dec 28, 2024
1 parent 3cd54c0 commit 3e35d53
Show file tree
Hide file tree
Showing 29 changed files with 1,392 additions and 1,077 deletions.
71 changes: 35 additions & 36 deletions Ical.Net.Tests/CalDateTimeTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,21 @@ private static CalendarEvent GetEventWithRecurrenceRules(string tzId)
return calendarEvent;
}

[Test]
public void ToTimeZoneFloating()
{
var dt = new CalDateTime(2024, 12, 28, 17, 45, 05, "Europe/Vienna");
var floating = dt.ToTimeZone(null);
var dt2 = floating.ToTimeZone("Europe/Vienna");

Assert.Multiple(() =>
{
Assert.That(dt, Is.EqualTo(dt2));
Assert.That(floating.TzId, Is.Null);
Assert.That(floating.Value, Is.EqualTo(dt.Value));
});
}

[Test, TestCaseSource(nameof(ToTimeZoneTestCases))]
public void ToTimeZoneTests(CalendarEvent calendarEvent, string targetTimeZone)
{
Expand Down Expand Up @@ -157,7 +172,7 @@ public static IEnumerable ToStringTestCases()

yield return new TestCaseData(new CalDateTime(2024, 8, 30), null,
CultureInfo.GetCultureInfo("IT")) // Date only cannot have timezone
.Returns("30/08/2024")
.Returns("30/08/2024")
.SetName("Date only with 'IT' CultureInfo and default format arg");
}

Expand All @@ -176,14 +191,10 @@ public static IEnumerable DateTimeArithmeticTestCases()
.Returns(dateTime.AddHours(1))
.SetName($"{nameof(IDateTime.AddHours)} 1 hour");

yield return new TestCaseData(new Func<IDateTime, IDateTime>(dt => dt.Add(TimeSpan.FromSeconds(30))))
yield return new TestCaseData(new Func<IDateTime, IDateTime>(dt => dt.Add(Duration.FromSeconds(30))))
.Returns(dateTime.Add(TimeSpan.FromSeconds(30)))
.SetName($"{nameof(IDateTime.Add)} 30 seconds");

yield return new TestCaseData(new Func<IDateTime, IDateTime>(dt => dt.Add(TimeSpan.FromMilliseconds(100))))
.Returns(dateTime.Add(TimeSpan.FromMilliseconds(0)))
.SetName($"{nameof(IDateTime.Add)} 100 milliseconds round down");

yield return new TestCaseData(new Func<IDateTime, IDateTime>(dt => dt.AddMinutes(70)))
.Returns(dateTime.AddMinutes(70))
.SetName($"{nameof(IDateTime.AddMinutes)} 70 minutes");
Expand All @@ -197,15 +208,15 @@ public bool EqualityTests(Func<IDateTime, bool> operation)

public static IEnumerable EqualityTestCases()
{
yield return new TestCaseData(new Func<IDateTime, bool>(dt => (CalDateTime) dt == new CalDateTime(2025, 1, 15, 10, 20, 30, tzId: CalDateTime.UtcTzId)))
yield return new TestCaseData(new Func<IDateTime, bool>(dt => (CalDateTime)dt == new CalDateTime(2025, 1, 15, 10, 20, 30, tzId: CalDateTime.UtcTzId)))
.Returns(true)
.SetName("== operator 2 UTC timezones");

yield return new TestCaseData(new Func<IDateTime, bool>(dt => (CalDateTime) dt != new CalDateTime(2025, 1, 15, 10, 20, 30, tzId: "Europe/Berlin")))
yield return new TestCaseData(new Func<IDateTime, bool>(dt => (CalDateTime)dt != new CalDateTime(2025, 1, 15, 10, 20, 30, tzId: "Europe/Berlin")))
.Returns(true)
.SetName("!= operator 2 timezones");

yield return new TestCaseData(new Func<IDateTime, bool>(dt => (CalDateTime) dt == new CalDateTime(2025, 1, 15, 10, 20, 30, tzId: null)))
yield return new TestCaseData(new Func<IDateTime, bool>(dt => (CalDateTime)dt == new CalDateTime(2025, 1, 15, 10, 20, 30, tzId: null)))
.Returns(false)
.SetName("== operator UTC vs. floating");
}
Expand Down Expand Up @@ -245,9 +256,9 @@ public static IEnumerable DateOnlyValidArithmeticTestCases()
{
var dateTime = new DateTime(2025, 1, 15);

yield return new TestCaseData(new Func<IDateTime, IDateTime>(dt => dt.Subtract(TimeSpan.FromDays(1))))
yield return new TestCaseData(new Func<IDateTime, IDateTime>(dt => dt.Add(-Duration.FromDays(1))))
.Returns((dateTime.AddDays(-1), false))
.SetName($"{nameof(IDateTime.Subtract)} 1 day TimeSpan");
.SetName($"{nameof(IDateTime.Add)} -1 day TimeSpan");

yield return new TestCaseData(new Func<IDateTime, IDateTime>(dt => dt.AddYears(1)))
.Returns((dateTime.AddYears(1), false))
Expand All @@ -261,23 +272,11 @@ public static IEnumerable DateOnlyValidArithmeticTestCases()
.Returns((dateTime.AddDays(7), false))
.SetName($"{nameof(IDateTime.AddDays)} 7 days");

yield return new TestCaseData(new Func<IDateTime, IDateTime>(dt => dt.AddHours(24)))
.Returns((dateTime.AddHours(24), false))
.SetName($"{nameof(IDateTime.AddHours)} 24 hours");

yield return new TestCaseData(new Func<IDateTime, IDateTime>(dt => dt.AddMinutes(24 * 60)))
.Returns((dateTime.AddMinutes(24 * 60), false))
.SetName($"{nameof(IDateTime.AddMinutes)} 1 day in minutes");

yield return new TestCaseData(new Func<IDateTime, IDateTime>(dt => dt.AddSeconds(TimeSpan.FromDays(1).Seconds)))
.Returns((dateTime.AddSeconds(TimeSpan.FromDays(1).Seconds), false))
.SetName($"{nameof(IDateTime.AddSeconds)} 1 day in seconds");

yield return new TestCaseData(new Func<IDateTime, IDateTime>(dt => dt.Add(TimeSpan.FromDays(1))))
yield return new TestCaseData(new Func<IDateTime, IDateTime>(dt => dt.Add(Duration.FromDays(1))))
.Returns((dateTime.Add(TimeSpan.FromDays(1)), false))
.SetName($"{nameof(IDateTime.Add)} 1 day TimeSpan");

yield return new TestCaseData(new Func<IDateTime, IDateTime>(dt => dt.Add(TimeSpan.Zero)))
yield return new TestCaseData(new Func<IDateTime, IDateTime>(dt => dt.Add(Duration.Zero)))
.Returns((dateTime.Add(TimeSpan.Zero), false))
.SetName($"{nameof(IDateTime.Add)} TimeSpan.Zero");
}
Expand All @@ -289,7 +288,7 @@ public void DateOnlyInvalidArithmeticTests()

Assert.Multiple(() =>
{
Assert.That(() => dt.Add(TimeSpan.FromHours(1)), Throws.TypeOf<InvalidOperationException>());
Assert.That(() => dt.Add(Duration.FromHours(1)), Throws.TypeOf<InvalidOperationException>());
Assert.That(() => dt.AddHours(2), Throws.TypeOf<InvalidOperationException>());
Assert.That(() => dt.AddMinutes(3), Throws.TypeOf<InvalidOperationException>());
Assert.That(() => dt.AddSeconds(4), Throws.TypeOf<InvalidOperationException>());
Expand All @@ -316,7 +315,7 @@ public void Simple_PropertyAndMethod_HasTime_Tests()
Assert.That(CalDateTime.Today.Value.Kind, Is.EqualTo(DateTimeKind.Unspecified));
Assert.That(c.DayOfYear, Is.EqualTo(dt.DayOfYear));
Assert.That(c.Time?.ToTimeSpan(), Is.EqualTo(dt.TimeOfDay));
Assert.That(c.Subtract(TimeSpan.FromSeconds(dt.Second)).Value.Second, Is.EqualTo(0));
Assert.That(c.Add(-Duration.FromSeconds(dt.Second)).Value.Second, Is.EqualTo(0));
Assert.That(c.ToString("dd.MM.yyyy"), Is.EqualTo("02.01.2025 Europe/Berlin"));
// Create a date-only CalDateTime from a CalDateTime
Assert.That(new CalDateTime(new CalDateTime(2025, 1, 1)), Is.EqualTo(new CalDateTime(2025, 1, 1)));
Expand All @@ -325,23 +324,23 @@ public void Simple_PropertyAndMethod_HasTime_Tests()

public static IEnumerable<TestCaseData> AddAndSubtractTestCases()
{
yield return new TestCaseData(new CalDateTime(2024, 10, 27, 0, 0, 0, tzId: null), TimeSpan.FromHours(4))
.SetName("Floating");
yield return new TestCaseData(new CalDateTime(2024, 10, 27, 0, 0, 0, tzId: null), Duration.FromHours(4))
.SetName("Floating");

yield return new TestCaseData(new CalDateTime(2024, 10, 27, 0, 0, 0, tzId: CalDateTime.UtcTzId), TimeSpan.FromHours(4))
.SetName("UTC");
yield return new TestCaseData(new CalDateTime(2024, 10, 27, 0, 0, 0, tzId: CalDateTime.UtcTzId), Duration.FromHours(4))
.SetName("UTC");

yield return new TestCaseData(new CalDateTime(2024, 10, 27, 0, 0, 0, tzId: "Europe/Paris"), TimeSpan.FromHours(4))
.SetName("Zoned Date/Time with DST change");
yield return new TestCaseData(new CalDateTime(2024, 10, 27, 0, 0, 0, tzId: "Europe/Paris"), Duration.FromHours(4))
.SetName("Zoned Date/Time with DST change");
}

[Test, TestCaseSource(nameof(AddAndSubtractTestCases))]
public void AddAndSubtract_ShouldBeReversible(CalDateTime t, TimeSpan d)
public void AddAndSubtract_ShouldBeReversible(CalDateTime t, Duration d)
{
Assert.Multiple(() =>
{
Assert.That(t.Add(d).Subtract(d), Is.EqualTo(t));
Assert.That(t.Add(d).Subtract(t), Is.EqualTo(d));
Assert.That(t.Add(d).Add(-d), Is.EqualTo(t));
Assert.That(t.Add(d).SubtractExact(t), Is.EqualTo(d.ToTimeSpan()));
});
}
}
25 changes: 13 additions & 12 deletions Ical.Net.Tests/CalendarEventTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -475,7 +475,7 @@ public void HourMinuteSecondOffsetParsingTest()


[Test, Category("CalendarEvent")]
public void GetNominalDurationTests()
public void GetEffectiveDurationTests()
{
var dt = new DateTime(2025, 3, 1, 14, 30, 0);
const string tzIdStart = "America/New_York";
Expand All @@ -487,11 +487,12 @@ public void GetNominalDurationTests()
DtEnd = new CalDateTime(DateOnly.FromDateTime(dt.AddHours(1)), TimeOnly.FromDateTime(dt.AddHours(1)), tzIdEnd)
};

var ed = evt.GetEffectiveDuration();
Assert.Multiple(() =>
{
Assert.That(evt.DtStart.Value, Is.EqualTo(dt));
Assert.That(evt.DtEnd.Value, Is.EqualTo(dt.AddHours(1)));
Assert.That(evt.GetTimeSpanToAddToPeriodStartTime(), Is.EqualTo(TimeSpan.FromHours(1)));
Assert.That(evt.GetEffectiveDuration(), Is.EqualTo(Duration.FromHours(-4)));
});

evt = new CalendarEvent
Expand All @@ -503,7 +504,7 @@ public void GetNominalDurationTests()
Assert.Multiple(() =>
{
Assert.That(evt.DtStart.Value, Is.EqualTo(dt.Date));
Assert.That(evt.GetTimeSpanToAddToPeriodStartTime(), Is.EqualTo(TimeSpan.Zero));
Assert.That(evt.GetEffectiveDuration().IsZero, Is.True);
});

evt = new CalendarEvent
Expand All @@ -515,41 +516,41 @@ public void GetNominalDurationTests()
{
Assert.That(evt.DtStart.Value, Is.EqualTo(dt.Date));
Assert.That(evt.Duration, Is.Null);
Assert.That(evt.GetTimeSpanToAddToPeriodStartTime(), Is.EqualTo(TimeSpan.FromDays(1)));
Assert.That(evt.GetEffectiveDuration(), Is.EqualTo(Duration.FromDays(1)));
});

evt = new CalendarEvent
{
DtStart = new CalDateTime(DateOnly.FromDateTime(dt), TimeOnly.FromDateTime(dt)),
Duration = TimeSpan.FromHours(2),
Duration = Duration.FromHours(2),
};

Assert.Multiple(() => {
Assert.That(evt.DtStart.Value, Is.EqualTo(dt));
Assert.That(evt.DtEnd, Is.Null);
Assert.That(evt.GetTimeSpanToAddToPeriodStartTime(), Is.EqualTo(TimeSpan.FromHours(2)));
Assert.That(evt.GetEffectiveDuration(), Is.EqualTo(Duration.FromHours(2)));
});

evt = new CalendarEvent()
{
DtStart = new CalDateTime(DateOnly.FromDateTime(dt.Date), TimeOnly.FromDateTime(dt.Date)),
Duration = TimeSpan.FromHours(2),
Duration = Duration.FromHours(2),
};

Assert.Multiple(() => {
Assert.That(evt.DtStart.Value, Is.EqualTo(dt.Date));
Assert.That(evt.GetTimeSpanToAddToPeriodStartTime(), Is.EqualTo(TimeSpan.FromHours(2)));
Assert.That(evt.GetEffectiveDuration(), Is.EqualTo(Duration.FromHours(2)));
});

evt = new CalendarEvent()
{
DtStart = new CalDateTime(DateOnly.FromDateTime(dt)),
Duration = TimeSpan.FromDays(1),
Duration = Duration.FromDays(1),
};

Assert.Multiple(() => {
Assert.That(evt.DtStart.Value, Is.EqualTo(dt.Date));
Assert.That(evt.GetTimeSpanToAddToPeriodStartTime(), Is.EqualTo(TimeSpan.FromDays(1)));
Assert.That(evt.GetEffectiveDuration(), Is.EqualTo(Duration.FromDays(1)));
});
}

Expand All @@ -564,9 +565,9 @@ public void EitherEndTime_OrDuraction_CanBeSet()
Assert.Multiple(() =>
{
Assert.That(() => evt.DtEnd = new CalDateTime(2025, 12, 11), Throws.Nothing);
Assert.That(() => evt.Duration = TimeSpan.FromDays(1), Throws.InvalidOperationException);
Assert.That(() => evt.Duration = Duration.FromDays(1), Throws.InvalidOperationException);
Assert.That(() => evt.DtEnd = null, Throws.Nothing);
Assert.That(() => evt.Duration = TimeSpan.FromDays(1), Throws.Nothing);
Assert.That(() => evt.Duration = Duration.FromDays(1), Throws.Nothing);
Assert.That(() => evt.DtEnd = new CalDateTime(2025, 12, 11), Throws.InvalidOperationException);
});
}
Expand Down
4 changes: 2 additions & 2 deletions Ical.Net.Tests/CopyComponentTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ public void CopyFreeBusyTest()
{
Start = new CalDateTime(_now),
End = new CalDateTime(_later),
Entries = { new FreeBusyEntry { Language = "English", StartTime = new CalDateTime(2024, 10, 1), Duration = TimeSpan.FromDays(1), Status = FreeBusyStatus.Busy } }
Entries = { new FreeBusyEntry { Language = "English", StartTime = new CalDateTime(2024, 10, 1), Duration = Duration.FromDays(1), Status = FreeBusyStatus.Busy } }
};

var copy = orig.Copy<FreeBusy>();
Expand All @@ -133,7 +133,7 @@ public void CopyAlarmTest()
var orig = new Alarm
{
Action = AlarmAction.Display,
Trigger = new Trigger(TimeSpan.FromMinutes(15)),
Trigger = new Trigger(Duration.FromMinutes(15)),
Description = "Test Alarm"
};

Expand Down
2 changes: 1 addition & 1 deletion Ical.Net.Tests/DeserializationTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -562,7 +562,7 @@ public void KeepApartDtEndAndDuration_Tests(bool useDtEnd)
Assert.Multiple(() =>
{
Assert.That(calendar.Events.Single().DtEnd != null, Is.EqualTo(useDtEnd));
Assert.That(calendar.Events.Single().Duration != default, Is.EqualTo(!useDtEnd));
Assert.That(calendar.Events.Single().Duration != null, Is.EqualTo(!useDtEnd));
});
}
}
4 changes: 2 additions & 2 deletions Ical.Net.Tests/EqualityAndHashingTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ public void Calendar_Tests()
var e = new CalendarEvent
{
DtStart = new CalDateTime(_nowTime),
Duration = TimeSpan.FromHours(1),
Duration = Duration.FromHours(1),
RecurrenceRules = new List<RecurrencePattern> { rruleA },
};

Expand All @@ -128,7 +128,7 @@ public void Calendar_Tests()
expectedCalendar.Events.Add(new CalendarEvent
{
DtStart = new CalDateTime(_nowTime),
Duration = TimeSpan.FromHours(1),
Duration = Duration.FromHours(1),
RecurrenceRules = new List<RecurrencePattern> { rruleB },
});

Expand Down
32 changes: 16 additions & 16 deletions Ical.Net.Tests/PeriodTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ public void CreatePeriodWithArguments()
{
var period = new Period(new CalDateTime(2025, 1, 1, 0, 0, 0, "America/New_York"));
var periodWithEndTime = new Period(new CalDateTime(2025, 1, 1, 0, 0, 0, "America/New_York"), new CalDateTime(2025, 1, 1, 1, 0, 0, "America/New_York"));
var periodWithDuration = new Period(new CalDateTime(2025, 1, 1, 0, 0, 0, "America/New_York"), new TimeSpan(1, 0, 0));
var periodWithDuration = new Period(new CalDateTime(2025, 1, 1, 0, 0, 0, "America/New_York"), Duration.FromHours(1));

Assert.Multiple(() =>
{
Expand All @@ -27,12 +27,12 @@ public void CreatePeriodWithArguments()
Assert.That(period.Duration, Is.Null);

Assert.That(periodWithEndTime.StartTime, Is.EqualTo(new CalDateTime(2025, 1, 1, 0, 0, 0, "America/New_York")));
Assert.That(periodWithEndTime.EndTime, Is.EqualTo(new CalDateTime(2025, 1, 1, 1, 0, 0, "America/New_York")));
Assert.That(periodWithEndTime.Duration, Is.EqualTo(new TimeSpan(1, 0, 0)));
Assert.That(periodWithEndTime.GetEffectiveEndTime(), Is.EqualTo(new CalDateTime(2025, 1, 1, 1, 0, 0, "America/New_York")));
Assert.That(periodWithEndTime.GetEffectiveDuration(), Is.EqualTo(Duration.FromHours(1)));

Assert.That(periodWithDuration.StartTime, Is.EqualTo(new CalDateTime(2025, 1, 1, 0, 0, 0, "America/New_York")));
Assert.That(periodWithDuration.EndTime, Is.EqualTo(new CalDateTime(2025, 1, 1, 1, 0, 0, "America/New_York")));
Assert.That(periodWithDuration.Duration, Is.EqualTo(new TimeSpan(1, 0, 0)));
Assert.That(periodWithDuration.GetEffectiveEndTime(), Is.EqualTo(new CalDateTime(2025, 1, 1, 1, 0, 0, "America/New_York")));
Assert.That(periodWithDuration.GetEffectiveDuration(), Is.EqualTo(Duration.FromHours(1)));
});
}

Expand All @@ -42,7 +42,7 @@ public void CreatePeriodWithInvalidArgumentsShouldThrow()
Assert.Throws<ArgumentException>(() => _ = new Period(new CalDateTime(2025, 1, 1, 0, 0, 0, "America/New_York"),
new CalDateTime(2025, 1, 1, 0, 0, 0, "America/New_York")));
Assert.Throws<ArgumentException>(() =>
_ = new Period(new CalDateTime(2025, 1, 1, 0, 0, 0, "America/New_York"), new TimeSpan(-1, 0, 0)));
_ = new Period(new CalDateTime(2025, 1, 1, 0, 0, 0, "America/New_York"), Duration.FromHours(-1)));
}
[Test]
public void SetAndGetStartTime()
Expand All @@ -61,31 +61,31 @@ public void SetEndTime_GetDuration()
var endTime = new CalDateTime(2025, 1, 31, 0, 0, 0);
period.EndTime = endTime;

Assert.That(period.GetEffectiveEndTime(), Is.EqualTo(endTime));
Assert.That(period.EndTime, Is.EqualTo(endTime));
Assert.That(period.GetOriginalValues().EndTime, Is.EqualTo(endTime));
Assert.That(period.GetOriginalValues().Duration, Is.Null);
Assert.That(period.Duration, Is.EqualTo(new TimeSpan(30, 0, 0, 0)));
Assert.That(period.Duration, Is.Null);
Assert.That(period.GetEffectiveDuration(), Is.EqualTo(Duration.FromDays(30)));
}

[Test]
public void SetDuration_GetEndTime()
{
var period = new Period(new CalDateTime(2025, 1, 1, 0, 0, 0));
var duration = new TimeSpan(1, 0, 0);
var duration = Duration.FromHours(1);
period.Duration = duration;

Assert.That(period.GetEffectiveDuration(), Is.EqualTo(duration));
Assert.That(period.Duration, Is.EqualTo(duration));
Assert.That(period.GetOriginalValues().Duration, Is.EqualTo(duration));
Assert.That(period.GetOriginalValues().EndTime, Is.Null);
Assert.That(period.EndTime, Is.EqualTo(new CalDateTime(2025, 1, 1, 1, 0, 0)));
Assert.That(period.EndTime, Is.Null);
Assert.That(period.GetEffectiveEndTime(), Is.EqualTo(new CalDateTime(2025, 1, 1, 1, 0, 0)));
}

[Test]
public void CollidesWithPeriod()
{
var period1 = new Period(new CalDateTime(2025, 1, 1, 0, 0, 0), new TimeSpan(1, 0, 0));
var period2 = new Period(new CalDateTime(2025, 1, 1, 0, 30, 0), new TimeSpan(1, 0, 0));
var period3 = new Period(new CalDateTime(2025, 1, 1, 1, 30, 0), new TimeSpan(1, 0, 0));
var period1 = new Period(new CalDateTime(2025, 1, 1, 0, 0, 0), Duration.FromHours(1));
var period2 = new Period(new CalDateTime(2025, 1, 1, 0, 30, 0), Duration.FromHours(1));
var period3 = new Period(new CalDateTime(2025, 1, 1, 1, 30, 0), Duration.FromHours(1));

Assert.Multiple(() =>
{
Expand Down
Loading

0 comments on commit 3e35d53

Please sign in to comment.