diff --git a/Ical.Net.Tests/AlarmTest.cs b/Ical.Net.Tests/AlarmTest.cs index 122e8b8e2..ebd851838 100644 --- a/Ical.Net.Tests/AlarmTest.cs +++ b/Ical.Net.Tests/AlarmTest.cs @@ -3,11 +3,14 @@ // Licensed under the MIT license. // +#nullable enable using Ical.Net.CalendarComponents; using Ical.Net.DataTypes; using NUnit.Framework; using System.Collections.Generic; +using System.IO; using System.Linq; +using Ical.Net.Serialization.DataTypes; namespace Ical.Net.Tests; diff --git a/Ical.Net.Tests/DeserializationTests.cs b/Ical.Net.Tests/DeserializationTests.cs index 8a326f1f6..47f787c8b 100644 --- a/Ical.Net.Tests/DeserializationTests.cs +++ b/Ical.Net.Tests/DeserializationTests.cs @@ -612,4 +612,45 @@ public void CalendarWithMissingProdIdOrVersion_ShouldLeavePropertiesInvalid() Assert.That(deserialized, Does.Not.Contain("PRODID:").And.Not.Contains("VERSION:")); }); } + + [Test, Category("DurationSerializer")] + [TestCase("PT1H", 0, 0, 1, 0, 0)] + [TestCase("PT1H30M", 0, 0, 1, 30, 0)] + [TestCase("PT1H30M45S", 0, 0, 1, 30, 45)] + [TestCase("P1D", 0, 1, 0, 0, 0)] + [TestCase("-P1D", 0, -1, 0, 0, 0)] + [TestCase("P1W", 1, 0, 0, 0, 0)] + [TestCase("-P1W", -1, 0, 0, 0, 0)] + [TestCase("P0W", null, null, null, null, null)] + [TestCase("-P0W")] + [TestCase("-P1D", 0, -1, 0, 0, 0)] + [TestCase("-P1W", -1, 0, 0, 0, 0)] + [TestCase("PT0S", 0, 0, 0, 0, 0)] + [TestCase("-PT0S", 0, 0, 0, 0, 0)] + [TestCase("-PT1H", 0, 0, -1, 0, 0)] + [TestCase("-PT1H30M", 0, 0, -1, -30, 0)] + [TestCase("-P1D", 0, -1, 0, 0, 0)] + [TestCase("PT125H199M199S", 0, 0, 125, 199, 199)] + [TestCase("-PT125H199M199S", 0, 0, -125, -199, -199)] + [TestCase("-P0DT0H30M0S", 0, 0, 0, -30, 0)] + [TestCase("-P1DT1H", 0, -1, -1, 0, 0)] + [TestCase("PT1000H", 0, 0, 1000, 0, 0)] + [TestCase("-PT1000H", null, null, -1000, null, null)] + public void DurationSerializer_ShouldReturn_ExpectedDuration(string text, int? weeks = null, int? days = null, int? hours = null, int? minutes = null, int? seconds = null) + { + var s = new DurationSerializer(); + Assert.That((Duration?) s.Deserialize(new StringReader(text)), Is.EqualTo(new Duration(weeks, days, hours, minutes, seconds))); + } + + [Test, Category("DurationSerializer")] + [TestCase("")] + [TestCase("Invalid")] + public void Duration_InvalidArguments_ShouldThrow(string text) + { + Assert.Multiple(() => + { + Assert.That(new DurationSerializer().Deserialize(new StringReader(text)), Is.Null); + Assert.That(Duration.Parse(text), Is.Null); + }); + } } diff --git a/Ical.Net.Tests/SerializationTests.cs b/Ical.Net.Tests/SerializationTests.cs index df5ea7519..9a4f625bc 100644 --- a/Ical.Net.Tests/SerializationTests.cs +++ b/Ical.Net.Tests/SerializationTests.cs @@ -7,6 +7,7 @@ using System.Collections; using System.Collections.Generic; using System.Globalization; +using System.IO; using System.Linq; using System.Text; using System.Text.RegularExpressions; @@ -348,10 +349,6 @@ public void AttendeesSerialized() } } - //todo test event: - //-GeographicLocation - //-Alarm - [Test] public void ZeroDuration_Test() { @@ -359,6 +356,13 @@ public void ZeroDuration_Test() Assert.That("P0D".Equals(result, StringComparison.Ordinal), Is.True); } + [Test] + public void Duration_FromWeeks() + { + var weeks = Duration.FromWeeks(4).Weeks; + Assert.That(weeks, Is.EqualTo(4)); + } + [Test] public void DurationIsStable_Tests() { diff --git a/Ical.Net/DataTypes/Duration.cs b/Ical.Net/DataTypes/Duration.cs index 4c751ac64..d70bbda7a 100644 --- a/Ical.Net/DataTypes/Duration.cs +++ b/Ical.Net/DataTypes/Duration.cs @@ -22,6 +22,12 @@ public struct Duration /// Thrown if not all non-null arguments have the same sign. public Duration(int? weeks = null, int? days = null, int? hours = null, int? minutes = null, int? seconds = null) { + weeks = NullIfZero(weeks); + days = NullIfZero(days); + hours = NullIfZero(hours); + minutes = NullIfZero(minutes); + seconds = NullIfZero(seconds); + var sign = GetSign(weeks) ?? GetSign(days) ?? GetSign(hours) ?? GetSign(minutes) ?? GetSign(seconds) ?? 1; if ( ((GetSign(weeks) ?? sign) != sign) @@ -119,8 +125,8 @@ public static Duration FromSeconds(int seconds) => /// Parses the specified value according to RFC 5545. /// /// Thrown if the value is not a valid duration. - public static Duration Parse(string value) => - (Duration) new DurationSerializer().Deserialize(new StringReader(value))!; // throws if null + public static Duration? Parse(string value) => + (Duration?) new DurationSerializer().Deserialize(new StringReader(value))!; /// /// Creates an instance that represents the given time span as exact value, that is, time-only. @@ -200,7 +206,7 @@ internal bool IsEmpty /// /// Returns a negated copy of the given instance. /// - public static Duration operator-(Duration d) => + public static Duration operator -(Duration d) => new Duration(-d.Weeks, -d.Days, -d.Hours, -d.Minutes, -d.Seconds); /// @@ -215,5 +221,5 @@ internal bool IsEmpty < 0 => -1 }; - private static int? NullIfZero(int v) => (v == 0) ? null : v; + private static int? NullIfZero(int? v) => (v == 0) ? null : v; } diff --git a/Ical.Net/Serialization/DataTypes/DurationSerializer.cs b/Ical.Net/Serialization/DataTypes/DurationSerializer.cs index d17800b08..9abd16f59 100644 --- a/Ical.Net/Serialization/DataTypes/DurationSerializer.cs +++ b/Ical.Net/Serialization/DataTypes/DurationSerializer.cs @@ -58,6 +58,12 @@ private static string SerializeToString(Duration ts) new Regex(@"^(?\+|-)?P(((?\d+)W)|(?
((?\d+)D)?(?