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) {