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)?(?