Skip to content

Commit

Permalink
Floating date/time can convert to any timezone ID, keeping Value unch…
Browse files Browse the repository at this point in the history
…anged

Remove fallback to system's local timezone for floating date/time
  • Loading branch information
axunonb committed Dec 2, 2024
1 parent 67cda7c commit b8d5b8e
Show file tree
Hide file tree
Showing 4 changed files with 50 additions and 24 deletions.
10 changes: 5 additions & 5 deletions Ical.Net.Tests/CalDateTimeTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -73,8 +73,8 @@ public static IEnumerable ToTimeZoneTestCases()
.SetName($"IANA to BCL: {ianaNy} to {bclCst}");
}

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

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

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

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

yield return new TestCaseData(DateTime.SpecifyKind(localDt, DateTimeKind.Unspecified), null)
yield return new TestCaseData(DateTime.SpecifyKind(localDt, DateTimeKind.Local), null)
.Returns(DateTimeKind.Unspecified)
.SetName("DateTime with Kind = Unspecified and null tzid returns DateTimeKind.Unspecified");
.SetName("DateTime with Kind = Local and null tzid returns DateTimeKind.Unspecified");
}

[Test, TestCaseSource(nameof(ToStringTestCases))]
Expand Down
25 changes: 24 additions & 1 deletion Ical.Net.Tests/RecurrenceTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3786,5 +3786,28 @@ private static DateTime SimpleDateTimeToMatch(IDateTime dt, IDateTime toMatch)
}
return dt.Value;
}
}

[Test]
public void GetOccurrenceShouldExcludeDtEnd()
{
var ical = """
BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//github.com/ical-org/ical.net//NONSGML ical.net 5.0//EN
BEGIN:VEVENT
UID:123456
DTSTAMP:20240630T000000Z
DTSTART;VALUE=DATE:20241001
DTEND;VALUE=DATE:20241202
SUMMARY:Don't include the end date of this event
END:VEVENT
END:VCALENDAR
""";

var calendar = Calendar.Load(ical);
// Set start date for occurrences to search to the end date of the event
var occurrences = calendar.GetOccurrences(new CalDateTime(2024, 12, 2));

Assert.That(occurrences, Is.Empty);
}
}
22 changes: 8 additions & 14 deletions Ical.Net/DataTypes/CalDateTime.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,10 @@ namespace Ical.Net.DataTypes;
/// The iCalendar equivalent of the .NET <see cref="DateTime"/> class.
/// <remarks>
/// In addition to the features of the <see cref="DateTime"/> class, the <see cref="CalDateTime"/>
/// class handles timezones, and integrates seamlessly into the iCalendar framework.
/// class handles timezones, floating date/times and integrates seamlessly into the iCalendar framework.
/// <para/>
/// Any <see cref="Time"/> values are always rounded to the nearest second.
/// This is because RFC 5545, Section 3.3.5 does not allow for fractional seconds.
/// This is because RFC 5545, Section 3.3.5, does not allow for fractional seconds.
/// </remarks>
/// </summary>
public sealed class CalDateTime : EncodableDataType, IDateTime
Expand Down Expand Up @@ -190,6 +190,7 @@ public CalDateTime(string value, string? tzId = null)
var serializer = new DateTimeSerializer();
CopyFrom(serializer.Deserialize(new StringReader(value)) as ICopyable
?? throw new InvalidOperationException("Failure deserializing value"));
// The string may contain a date only, meaning that the tzId should be ignored.
_tzId = HasTime ? tzId : null;
}

Expand Down Expand Up @@ -357,18 +358,10 @@ public override int GetHashCode()
/// </summary>
public static implicit operator CalDateTime(DateTime left) => new CalDateTime(left);

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

/// <summary>
/// Gets the date and time value in the ISO calendar as a <see cref="DateTime"/> type with <see cref="DateTimeKind.Unspecified"/>.
/// The value has no associated timezone.
/// The precision of the time part is up to seconds.
/// <para/>
/// The value is equivalent to <seealso cref="NodaTime.LocalDateTime"/>.
/// </summary>
/// <inheritdoc/>
public DateTime Value
{
get
Expand Down Expand Up @@ -477,9 +470,10 @@ public DateTime Value
}

/// <inheritdoc/>
/// <remarks>If <see paramref="otherTzId"/> is not a well-known timezone ID, the system's local timezone will be used.</remarks>
public IDateTime ToTimeZone(string otherTzId)
{
if (IsFloating) return new CalDateTime(_dateOnly, _timeOnly, otherTzId);

var zonedOriginal = DateUtil.ToZonedDateTimeLeniently(Value, TzId);
var converted = zonedOriginal.WithZone(DateUtil.GetZone(otherTzId));

Expand Down
17 changes: 13 additions & 4 deletions Ical.Net/DataTypes/IDateTime.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@ public interface IDateTime : IEncodableDataType, IComparable<IDateTime>, IFormat
{
/// <summary>
/// Converts the date/time to UTC (Coordinated Universal Time)
/// If <see cref="IsFloating"/>==<see langword="true"/>
/// it means that the <see cref="Value"/> is considered as local time for every timezone:
/// The returned <see cref="Value"/> is unchanged, but with <see cref="DateTimeKind.Utc"/>.
/// </summary>
DateTime AsUtc { get; }

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

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

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

0 comments on commit b8d5b8e

Please sign in to comment.