Skip to content

Commit dae01d7

Browse files
committed
Floating date/time can convert to any timezone ID, keeping Value unchanged
Remove fallback to system's local timezone for floating date/time
1 parent 67cda7c commit dae01d7

File tree

4 files changed

+49
-24
lines changed

4 files changed

+49
-24
lines changed

Ical.Net.Tests/CalDateTimeTests.cs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -73,8 +73,8 @@ public static IEnumerable ToTimeZoneTestCases()
7373
.SetName($"IANA to BCL: {ianaNy} to {bclCst}");
7474
}
7575

76-
[Test(Description = "Calling AsUtc should always return the proper UTC time, even if the TzId has changed")]
77-
public void TestTzidChanges()
76+
[Test(Description = "A certain date/time value applied to different timezones should return the same UTC date/time")]
77+
public void SameDateTimeWithDifferentTzIdShouldReturnSameUtc()
7878
{
7979
var someTime = DateTimeOffset.Parse("2018-05-21T11:35:00-04:00");
8080

@@ -87,7 +87,7 @@ public void TestTzidChanges()
8787
Assert.That(berlinUtc, Is.Not.EqualTo(firstUtc));
8888
}
8989

90-
[Test, TestCaseSource(nameof(DateTimeKindOverrideTestCases))]
90+
[Test, TestCaseSource(nameof(DateTimeKindOverrideTestCases)), Description("DateTimeKind of values is always DateTimeKind.Unspecified")]
9191
public DateTimeKind DateTimeKindOverrideTests(DateTime dateTime, string tzId)
9292
=> new CalDateTime(dateTime, tzId).Value.Kind;
9393

@@ -120,9 +120,9 @@ public static IEnumerable DateTimeKindOverrideTestCases()
120120
.Returns(DateTimeKind.Unspecified)
121121
.SetName("DateTime with Kind = Local with null tzid returns DateTimeKind.Unspecified");
122122

123-
yield return new TestCaseData(DateTime.SpecifyKind(localDt, DateTimeKind.Unspecified), null)
123+
yield return new TestCaseData(DateTime.SpecifyKind(localDt, DateTimeKind.Local), null)
124124
.Returns(DateTimeKind.Unspecified)
125-
.SetName("DateTime with Kind = Unspecified and null tzid returns DateTimeKind.Unspecified");
125+
.SetName("DateTime with Kind = Local and null tzid returns DateTimeKind.Unspecified");
126126
}
127127

128128
[Test, TestCaseSource(nameof(ToStringTestCases))]

Ical.Net.Tests/RecurrenceTests.cs

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3786,5 +3786,28 @@ private static DateTime SimpleDateTimeToMatch(IDateTime dt, IDateTime toMatch)
37863786
}
37873787
return dt.Value;
37883788
}
3789-
}
37903789

3790+
[Test]
3791+
public void GetOccurrenceShouldExcludeDtEnd()
3792+
{
3793+
var ical = """
3794+
BEGIN:VCALENDAR
3795+
VERSION:2.0
3796+
PRODID:-//github.com/ical-org/ical.net//NONSGML ical.net 5.0//EN
3797+
BEGIN:VEVENT
3798+
UID:123456
3799+
DTSTAMP:20240630T000000Z
3800+
DTSTART;VALUE=DATE:20241001
3801+
DTEND;VALUE=DATE:20241202
3802+
SUMMARY:Don't include the end date of this event
3803+
END:VEVENT
3804+
END:VCALENDAR
3805+
""";
3806+
3807+
var calendar = Calendar.Load(ical);
3808+
// Set start date for occurrences to search to the end date of the event
3809+
var occurrences = calendar.GetOccurrences(new CalDateTime(2024, 12, 2));
3810+
3811+
Assert.That(occurrences, Is.Empty);
3812+
}
3813+
}

Ical.Net/DataTypes/CalDateTime.cs

Lines changed: 7 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,10 @@ namespace Ical.Net.DataTypes;
1717
/// The iCalendar equivalent of the .NET <see cref="DateTime"/> class.
1818
/// <remarks>
1919
/// In addition to the features of the <see cref="DateTime"/> class, the <see cref="CalDateTime"/>
20-
/// class handles timezones, and integrates seamlessly into the iCalendar framework.
20+
/// class handles timezones, floating date/times and integrates seamlessly into the iCalendar framework.
2121
/// <para/>
2222
/// Any <see cref="Time"/> values are always rounded to the nearest second.
23-
/// This is because RFC 5545, Section 3.3.5 does not allow for fractional seconds.
23+
/// This is because RFC 5545, Section 3.3.5, does not allow for fractional seconds.
2424
/// </remarks>
2525
/// </summary>
2626
public sealed class CalDateTime : EncodableDataType, IDateTime
@@ -357,18 +357,10 @@ public override int GetHashCode()
357357
/// </summary>
358358
public static implicit operator CalDateTime(DateTime left) => new CalDateTime(left);
359359

360-
/// <summary>
361-
/// Returns a representation of the <see cref="DateTime"/> in UTC.
362-
/// </summary>
363-
public DateTime AsUtc => ToTimeZone(UtcTzId).Value;
360+
/// <inheritdoc/>
361+
public DateTime AsUtc => DateTime.SpecifyKind(ToTimeZone(UtcTzId).Value, DateTimeKind.Utc);
364362

365-
/// <summary>
366-
/// Gets the date and time value in the ISO calendar as a <see cref="DateTime"/> type with <see cref="DateTimeKind.Unspecified"/>.
367-
/// The value has no associated timezone.
368-
/// The precision of the time part is up to seconds.
369-
/// <para/>
370-
/// The value is equivalent to <seealso cref="NodaTime.LocalDateTime"/>.
371-
/// </summary>
363+
/// <inheritdoc/>
372364
public DateTime Value
373365
{
374366
get
@@ -477,9 +469,10 @@ public DateTime Value
477469
}
478470

479471
/// <inheritdoc/>
480-
/// <remarks>If <see paramref="otherTzId"/> is not a well-known timezone ID, the system's local timezone will be used.</remarks>
481472
public IDateTime ToTimeZone(string otherTzId)
482473
{
474+
if (IsFloating) return new CalDateTime(_dateOnly, _timeOnly, otherTzId);
475+
483476
var zonedOriginal = DateUtil.ToZonedDateTimeLeniently(Value, TzId);
484477
var converted = zonedOriginal.WithZone(DateUtil.GetZone(otherTzId));
485478

Ical.Net/DataTypes/IDateTime.cs

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,9 @@ public interface IDateTime : IEncodableDataType, IComparable<IDateTime>, IFormat
1212
{
1313
/// <summary>
1414
/// Converts the date/time to UTC (Coordinated Universal Time)
15+
/// If <see cref="IsFloating"/>==<see langword="true"/>
16+
/// it means that the <see cref="Value"/> is considered as local time for every timezone:
17+
/// The returned <see cref="Value"/> is unchanged, but with <see cref="DateTimeKind.Utc"/>.
1518
/// </summary>
1619
DateTime AsUtc { get; }
1720

@@ -27,10 +30,12 @@ public interface IDateTime : IEncodableDataType, IComparable<IDateTime>, IFormat
2730
string? TimeZoneName { get; }
2831

2932
/// <summary>
30-
/// Gets the underlying DateTime value. This should always
31-
/// use DateTimeKind.Utc, regardless of its actual representation.
32-
/// Use IsUtc along with the TZID to control how this
33-
/// date/time is handled.
33+
/// Gets the date and time value in the ISO calendar as a <see cref="DateTime"/> type with <see cref="DateTimeKind.Unspecified"/>.
34+
/// The value has no associated timezone.<br/>
35+
/// The precision of the time part is up to seconds.
36+
/// <para/>
37+
/// Use <see cref="IsUtc"/> along with <see cref="TzId"/> and <see cref="IsFloating"/>
38+
/// to control how this date/time is handled.
3439
/// </summary>
3540
DateTime Value { get; }
3641

@@ -114,6 +119,10 @@ public interface IDateTime : IEncodableDataType, IComparable<IDateTime>, IFormat
114119
/// <summary>
115120
/// Converts the <see cref="Value"/> to a date/time
116121
/// within the specified <see paramref="otherTzId"/> timezone.
122+
/// <para/>
123+
/// If <see cref="IsFloating"/>==<see langword="true"/>
124+
/// it means that the <see cref="Value"/> is considered as local time for every timezone:
125+
/// The returned <see cref="Value"/> is unchanged and the <see paramref="otherTzId"/> is set as <see cref="TzId"/>.
117126
/// </summary>
118127
IDateTime ToTimeZone(string otherTzId);
119128
IDateTime Add(TimeSpan ts);

0 commit comments

Comments
 (0)