Skip to content

Commit

Permalink
add - doc - Added duration tools
Browse files Browse the repository at this point in the history
---

We've added duration tools to assist in parsing the durations from a string representation that is in the format of ISO-8601:2004

---

Type: add
Breaking: False
Doc Required: True
Backport Required: False
Part: 1/1
  • Loading branch information
AptiviCEO committed Sep 30, 2024
1 parent 8e7005e commit 22ec11a
Show file tree
Hide file tree
Showing 2 changed files with 233 additions and 0 deletions.
119 changes: 119 additions & 0 deletions VisualCard.Calendar/Parsers/Durations/DurationTools.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
//
// 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 <https://www.gnu.org/licenses/>.
//

using System;

namespace VisualCard.Calendar.Parsers.Durations
{
/// <summary>
/// Duration management tools
/// </summary>
public static class DurationTools
{
/// <summary>
/// Gets the date/time offset from the duration specifier that is compliant with the ISO-8601:2004 specification
/// </summary>
/// <param name="duration">Duration specifier in the ISO-8601:2004 format</param>
/// <param name="modern">Whether to disable parsing years and months or not</param>
/// <param name="utc">Whether to use UTC</param>
/// <returns>A date/time offset instance and a time span instance from the duration specifier</returns>
/// <exception cref="ArgumentException"></exception>
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);
}
}
}
114 changes: 114 additions & 0 deletions VisualCard.Tests/Durations/DurationParseTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
//
// 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 <https://www.gnu.org/licenses/>.
//

using Microsoft.VisualStudio.TestTools.UnitTesting;
using Shouldly;
using VisualCard.Calendar.Parsers.Durations;
using VisualCard.Calendar.Parsers.Recurrence;

namespace VisualCard.Tests.Durations
{
[TestClass]
public class DurationParseTests
{
[TestMethod]
[DataRow("P6W")]
[DataRow("PT15M")]
[DataRow("PT1H30M")]
[DataRow("P2Y10M15DT10H30M20S")]
[DataRow("P15DT5H0M20S")]
[DataRow("P7W")]
public void ParseDurations(string rule)
{
var span = DurationTools.GetDurationSpan(rule);
span.result.ShouldNotBe(new());
span.span.ShouldNotBe(new());
}

[TestMethod]
[DataRow("P6W")]
[DataRow("PT15M")]
[DataRow("PT1H30M")]
[DataRow("P2Y10M15DT10H30M20S")]
[DataRow("P15DT5H0M20S")]
[DataRow("P7W")]
public void ParseDurationsNoUtc(string rule)
{
var span = DurationTools.GetDurationSpan(rule, utc: false);
span.result.ShouldNotBe(new());
span.span.ShouldNotBe(new());
}

[TestMethod]
[DataRow("-P6W")]
[DataRow("-PT15M")]
[DataRow("-PT1H30M")]
[DataRow("-P2Y10M15DT10H30M20S")]
[DataRow("-P15DT5H0M20S")]
[DataRow("-P7W")]
public void ParseNegativeDurations(string rule)
{
var span = DurationTools.GetDurationSpan(rule);
span.result.ShouldNotBe(new());
span.span.ShouldNotBe(new());
}

[TestMethod]
[DataRow("-P6W")]
[DataRow("-PT15M")]
[DataRow("-PT1H30M")]
[DataRow("-P2Y10M15DT10H30M20S")]
[DataRow("-P15DT5H0M20S")]
[DataRow("-P7W")]
public void ParseNegativeDurationsNoUtc(string rule)
{
var span = DurationTools.GetDurationSpan(rule, utc: false);
span.result.ShouldNotBe(new());
span.span.ShouldNotBe(new());
}

[TestMethod]
public void ParseDuration()
{
var span = DurationTools.GetDurationSpan("P2Y10M15DT10H30M20S");

// We can't test against result because it's uninferrable due to CPU timings.
span.result.ShouldNotBe(new());
span.span.ShouldNotBe(new());
span.span.Days.ShouldBe(1048);
span.span.Hours.ShouldBe(10);
span.span.Minutes.ShouldBe(30);
span.span.Seconds.ShouldBe(20);
}

[TestMethod]
public void ParseNegativeDuration()
{
var span = DurationTools.GetDurationSpan("-P2Y10M15DT10H30M20S");

// We can't test against result because it's uninferrable due to CPU timings.
span.result.ShouldNotBe(new());
span.span.ShouldNotBe(new());
span.span.Days.ShouldBe(-1050);
span.span.Hours.ShouldBe(-10);
span.span.Minutes.ShouldBe(-30);
span.span.Seconds.ShouldBe(-20);
}
}
}

0 comments on commit 22ec11a

Please sign in to comment.