diff --git a/VisualCard.Calendar/Parsers/Durations/DurationTools.cs b/VisualCard.Calendar/Parsers/Durations/DurationTools.cs
deleted file mode 100644
index 0ed9948..0000000
--- a/VisualCard.Calendar/Parsers/Durations/DurationTools.cs
+++ /dev/null
@@ -1,119 +0,0 @@
-//
-// VisualCard Copyright (C) 2021-2024 Aptivi
-//
-// This file is part of VisualCard
-//
-// VisualCard is free software: you can redistribute it and/or modify
-// it under the terms of the GNU General Public License as published by
-// the Free Software Foundation, either version 3 of the License, or
-// (at your option) any later version.
-//
-// VisualCard is distributed in the hope that it will be useful,
-// but WITHOUT ANY WARRANTY, without even the implied warranty of
-// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-// GNU General Public License for more details.
-//
-// You should have received a copy of the GNU General Public License
-// along with this program. If not, see .
-//
-
-using System;
-
-namespace VisualCard.Calendar.Parsers.Durations
-{
- ///
- /// Duration management tools
- ///
- public static class DurationTools
- {
- ///
- /// Gets the date/time offset from the duration specifier that is compliant with the ISO-8601:2004 specification
- ///
- /// Duration specifier in the ISO-8601:2004 format
- /// Whether to disable parsing years and months or not
- /// Whether to use UTC
- /// A date/time offset instance and a time span instance from the duration specifier
- ///
- public static (DateTimeOffset result, TimeSpan span) GetDurationSpan(string duration, bool modern = false, bool utc = true)
- {
- // Sanity checks
- duration = duration.Trim();
- if (string.IsNullOrEmpty(duration))
- throw new ArgumentException($"Duration is not provided");
-
- // Check to see if we've been provided with a sign
- bool isNegative = duration[0] == '-';
- if (duration[0] == '+' || isNegative)
- duration = duration.Substring(1);
- if (duration[0] != 'P')
- throw new ArgumentException($"Duration is invalid: {duration}");
- duration = duration.Substring(1);
-
- // Populate the date time offset accordingly
- DateTimeOffset rightNow = utc ? DateTimeOffset.UtcNow : DateTimeOffset.Now;
- DateTimeOffset offset = rightNow;
- bool inDate = true;
- while (!string.IsNullOrEmpty(duration))
- {
- // Get the designator index
- int designatorIndex;
- for (designatorIndex = 0; designatorIndex < duration.Length - 1; designatorIndex++)
- if (!char.IsNumber(duration[designatorIndex]))
- break;
-
- // Split the duration according to the designator index
- string digits = duration.Substring(0, designatorIndex);
- string type = duration.Substring(designatorIndex, 1);
- int length = digits.Length + type.Length;
-
- // Add according to type, but check first for the time designator
- if (type == "T")
- {
- duration = duration.Substring(length);
- inDate = false;
- continue;
- }
- if (!int.TryParse(digits, out int value))
- throw new ArgumentException($"Digits are not numeric: {digits}, {duration}");
- value = isNegative ? -value : value;
- switch (type)
- {
- // Year and Month types are only supported in vCalendar 1.0
- case "Y":
- if (modern)
- throw new ArgumentException($"Year specifier is disabled in vCalendar 2.0, {duration}");
- offset = offset.AddYears(value);
- break;
- case "M":
- if (modern && inDate)
- throw new ArgumentException($"Month specifier is disabled in vCalendar 2.0, {duration}");
- if (inDate)
- offset = offset.AddMonths(value);
- else
- offset = offset.AddMinutes(value);
- break;
-
- // Supported in all vCalendars
- case "W":
- offset = offset.AddDays(value * 7);
- break;
- case "D":
- offset = offset.AddDays(value);
- break;
- case "H":
- offset = offset.AddHours(value);
- break;
- case "S":
- offset = offset.AddSeconds(value);
- break;
- default:
- throw new ArgumentException($"Type is invalid: {type}, {duration}");
- }
- duration = duration.Substring(length);
- }
-
- // Return the result
- return (offset, offset - rightNow);
- }
- }
-}
diff --git a/VisualCard.Tests/Durations/DurationParseTests.cs b/VisualCard.Tests/Durations/DurationParseTests.cs
index 535c09d..12523cf 100644
--- a/VisualCard.Tests/Durations/DurationParseTests.cs
+++ b/VisualCard.Tests/Durations/DurationParseTests.cs
@@ -19,8 +19,7 @@
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Shouldly;
-using VisualCard.Calendar.Parsers.Durations;
-using VisualCard.Calendar.Parsers.Recurrence;
+using VisualCard.Parsers;
namespace VisualCard.Tests.Durations
{
@@ -36,7 +35,7 @@ public class DurationParseTests
[DataRow("P7W")]
public void ParseDurations(string rule)
{
- var span = DurationTools.GetDurationSpan(rule);
+ var span = VcardCommonTools.GetDurationSpan(rule);
span.result.ShouldNotBe(new());
span.span.ShouldNotBe(new());
}
@@ -50,7 +49,7 @@ public void ParseDurations(string rule)
[DataRow("P7W")]
public void ParseDurationsNoUtc(string rule)
{
- var span = DurationTools.GetDurationSpan(rule, utc: false);
+ var span = VcardCommonTools.GetDurationSpan(rule, utc: false);
span.result.ShouldNotBe(new());
span.span.ShouldNotBe(new());
}
@@ -64,7 +63,7 @@ public void ParseDurationsNoUtc(string rule)
[DataRow("-P7W")]
public void ParseNegativeDurations(string rule)
{
- var span = DurationTools.GetDurationSpan(rule);
+ var span = VcardCommonTools.GetDurationSpan(rule);
span.result.ShouldNotBe(new());
span.span.ShouldNotBe(new());
}
@@ -78,7 +77,7 @@ public void ParseNegativeDurations(string rule)
[DataRow("-P7W")]
public void ParseNegativeDurationsNoUtc(string rule)
{
- var span = DurationTools.GetDurationSpan(rule, utc: false);
+ var span = VcardCommonTools.GetDurationSpan(rule, utc: false);
span.result.ShouldNotBe(new());
span.span.ShouldNotBe(new());
}
@@ -86,7 +85,7 @@ public void ParseNegativeDurationsNoUtc(string rule)
[TestMethod]
public void ParseDuration()
{
- var span = DurationTools.GetDurationSpan("P2Y10M15DT10H30M20S");
+ var span = VcardCommonTools.GetDurationSpan("P2Y10M15DT10H30M20S");
// We can't test against result because it's uninferrable due to CPU timings.
span.result.ShouldNotBe(new());
@@ -100,7 +99,7 @@ public void ParseDuration()
[TestMethod]
public void ParseNegativeDuration()
{
- var span = DurationTools.GetDurationSpan("-P2Y10M15DT10H30M20S");
+ var span = VcardCommonTools.GetDurationSpan("-P2Y10M15DT10H30M20S");
// We can't test against result because it's uninferrable due to CPU timings.
span.result.ShouldNotBe(new());
diff --git a/VisualCard.Tests/TimePeriod/PeriodParseTests.cs b/VisualCard.Tests/TimePeriod/PeriodParseTests.cs
new file mode 100644
index 0000000..3036167
--- /dev/null
+++ b/VisualCard.Tests/TimePeriod/PeriodParseTests.cs
@@ -0,0 +1,70 @@
+//
+// VisualCard Copyright (C) 2021-2024 Aptivi
+//
+// This file is part of VisualCard
+//
+// VisualCard is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// VisualCard is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY, without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program. If not, see .
+//
+
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+using Shouldly;
+using VisualCard.Parsers;
+
+namespace VisualCard.Tests.TimePeriod
+{
+ [TestClass]
+ public class PeriodParseTests
+ {
+ [TestMethod]
+ [DataRow("19970101T180000Z/19970102T070000Z")]
+ [DataRow("19970101T180000Z/PT5H30M")]
+ public void ParsePeriods(string rule)
+ {
+ var span = VcardCommonTools.GetTimePeriod(rule);
+ span.StartDate.ShouldNotBe(new());
+ span.EndDate.ShouldNotBe(new());
+ span.Duration.ShouldNotBe(new());
+ }
+
+ [TestMethod]
+ public void ParsePeriodWithDateDate()
+ {
+ var span = VcardCommonTools.GetTimePeriod("19970101T180000Z/19970102T070000Z");
+
+ // We can't test against result because it's uninferrable due to CPU timings.
+ span.StartDate.ShouldNotBe(new());
+ span.EndDate.ShouldNotBe(new());
+ span.Duration.ShouldNotBe(new());
+ span.Duration.Days.ShouldBe(0);
+ span.Duration.Hours.ShouldBe(13);
+ span.Duration.Minutes.ShouldBe(0);
+ span.Duration.Seconds.ShouldBe(0);
+ }
+
+ [TestMethod]
+ public void ParsePeriodWithDateDuration()
+ {
+ var span = VcardCommonTools.GetTimePeriod("19970101T180000Z/PT5H30M");
+
+ // We can't test against result because it's uninferrable due to CPU timings.
+ span.StartDate.ShouldNotBe(new());
+ span.EndDate.ShouldNotBe(new());
+ span.Duration.ShouldNotBe(new());
+ span.Duration.Days.ShouldBe(0);
+ span.Duration.Hours.ShouldBe(5);
+ span.Duration.Minutes.ShouldBe(30);
+ span.Duration.Seconds.ShouldBe(0);
+ }
+ }
+}
diff --git a/VisualCard/Parsers/TimePeriod.cs b/VisualCard/Parsers/TimePeriod.cs
new file mode 100644
index 0000000..4bc8f59
--- /dev/null
+++ b/VisualCard/Parsers/TimePeriod.cs
@@ -0,0 +1,65 @@
+//
+// VisualCard Copyright (C) 2021-2024 Aptivi
+//
+// This file is part of VisualCard
+//
+// VisualCard is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// VisualCard is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY, without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program. If not, see .
+//
+
+using System;
+using System.Diagnostics;
+
+namespace VisualCard.Parsers
+{
+ ///
+ /// Precise period of time
+ ///
+ [DebuggerDisplay("{StartDate} => {EndDate} in {Duration}")]
+ public class TimePeriod
+ {
+ private DateTimeOffset startDate;
+ private DateTimeOffset endDate;
+
+ ///
+ /// Start date
+ ///
+ public DateTimeOffset StartDate =>
+ startDate;
+
+ ///
+ /// End date or a resultant date from the duration
+ ///
+ public DateTimeOffset EndDate =>
+ endDate;
+
+ ///
+ /// Duration of the time period
+ ///
+ public TimeSpan Duration =>
+ endDate - startDate;
+
+ ///
+ /// Makes a new TimePeriod instance
+ ///
+ /// Start date
+ /// End date
+ public TimePeriod(DateTimeOffset startDate, DateTimeOffset endDate)
+ {
+ if (startDate > endDate)
+ throw new ArgumentException("Start date may not be later than the end date");
+ this.startDate = startDate;
+ this.endDate = endDate;
+ }
+ }
+}
diff --git a/VisualCard/Parsers/VcardCommonTools.cs b/VisualCard/Parsers/VcardCommonTools.cs
index 06d6ab8..136a9f4 100644
--- a/VisualCard/Parsers/VcardCommonTools.cs
+++ b/VisualCard/Parsers/VcardCommonTools.cs
@@ -160,6 +160,128 @@ public static string SaveUtcOffset(TimeSpan utcOffsetRepresentation)
);
return utcOffsetBuilder.ToString();
}
+ ///
+ /// Gets the date/time offset from the duration specifier that is compliant with the ISO-8601:2004 specification
+ ///
+ /// Duration specifier in the ISO-8601:2004 format
+ /// Whether to disable parsing years and months or not
+ /// Whether to use UTC
+ /// A date/time offset instance and a time span instance from the duration specifier
+ ///
+ public static (DateTimeOffset result, TimeSpan span) GetDurationSpan(string duration, bool modern = false, bool utc = true, DateTimeOffset? source = null)
+ {
+ // Sanity checks
+ duration = duration.Trim();
+ if (string.IsNullOrEmpty(duration))
+ throw new ArgumentException($"Duration is not provided");
+
+ // Check to see if we've been provided with a sign
+ bool isNegative = duration[0] == '-';
+ if (duration[0] == '+' || isNegative)
+ duration = duration.Substring(1);
+ if (duration[0] != 'P')
+ throw new ArgumentException($"Duration is invalid: {duration}");
+ duration = duration.Substring(1);
+
+ // Populate the date time offset accordingly
+ DateTimeOffset rightNow =
+ source is DateTimeOffset sourceDate ?
+ sourceDate :
+ (utc ? DateTimeOffset.UtcNow : DateTimeOffset.Now);
+ DateTimeOffset offset = rightNow;
+ bool inDate = true;
+ while (!string.IsNullOrEmpty(duration))
+ {
+ // Get the designator index
+ int designatorIndex;
+ for (designatorIndex = 0; designatorIndex < duration.Length - 1; designatorIndex++)
+ if (!char.IsNumber(duration[designatorIndex]))
+ break;
+
+ // Split the duration according to the designator index
+ string digits = duration.Substring(0, designatorIndex);
+ string type = duration.Substring(designatorIndex, 1);
+ int length = digits.Length + type.Length;
+
+ // Add according to type, but check first for the time designator
+ if (type == "T")
+ {
+ duration = duration.Substring(length);
+ inDate = false;
+ continue;
+ }
+ if (!int.TryParse(digits, out int value))
+ throw new ArgumentException($"Digits are not numeric: {digits}, {duration}");
+ value = isNegative ? -value : value;
+ switch (type)
+ {
+ // Year and Month types are only supported in vCalendar 1.0
+ case "Y":
+ if (modern)
+ throw new ArgumentException($"Year specifier is disabled in vCalendar 2.0, {duration}");
+ offset = offset.AddYears(value);
+ break;
+ case "M":
+ if (modern && inDate)
+ throw new ArgumentException($"Month specifier is disabled in vCalendar 2.0, {duration}");
+ if (inDate)
+ offset = offset.AddMonths(value);
+ else
+ offset = offset.AddMinutes(value);
+ break;
+
+ // Supported in all vCalendars
+ case "W":
+ offset = offset.AddDays(value * 7);
+ break;
+ case "D":
+ offset = offset.AddDays(value);
+ break;
+ case "H":
+ offset = offset.AddHours(value);
+ break;
+ case "S":
+ offset = offset.AddSeconds(value);
+ break;
+ default:
+ throw new ArgumentException($"Type is invalid: {type}, {duration}");
+ }
+ duration = duration.Substring(length);
+ }
+
+ // Return the result
+ return (offset, offset - rightNow);
+ }
+
+ ///
+ /// Gets the time period that contains dates or date/duration combination that satisfy the ISO-8601:2004 specification
+ ///
+ /// Either a date/date format or a date/duration format that conform with the ISO-8601:2004 specification
+ /// A instance that describes the time period
+ ///
+ public static TimePeriod GetTimePeriod(string period)
+ {
+ // Sanity checks
+ period = period.Trim();
+ if (string.IsNullOrEmpty(period))
+ throw new ArgumentException("Time period is not specified");
+
+ // Parse the time period by splitting with the slash character to two string variables
+ string[] splits = period.Split('/');
+ if (splits.Length != 2)
+ throw new ArgumentException($"After splitting, got {splits.Length} instead of 2: {period}");
+ string startStr = splits[0];
+ string endStr = splits[1];
+
+ // Now, parse them
+ if (!TryParsePosixDate(startStr, out DateTimeOffset start))
+ throw new ArgumentException($"Invalid start date {startStr}: {period}");
+ if (!TryParsePosixDate(endStr, out DateTimeOffset end))
+ end = GetDurationSpan(endStr, source: start).result;
+
+ // Return a new object
+ return new TimePeriod(start, end);
+ }
internal static string GetTypesString(string[] args, string @default, bool isSpecifierRequired = true)
{