diff --git a/VisualCard.Calendar/Parts/Implementations/RecDateInfo.cs b/VisualCard.Calendar/Parts/Implementations/RecDateInfo.cs
index e27f3202..a61aeecc 100644
--- a/VisualCard.Calendar/Parts/Implementations/RecDateInfo.cs
+++ b/VisualCard.Calendar/Parts/Implementations/RecDateInfo.cs
@@ -61,7 +61,7 @@ internal override BaseCalendarPartInfo FromStringVcalendarInternal(string value,
cardVersion.Major == 1 ?
Regex.Unescape(value).Split(';') :
[Regex.Unescape(value)];
- var recDates = recDateStrings.Select(VcardCommonTools.ParsePosixDate).ToArray();
+ var recDates = recDateStrings.Select((date) => VcardCommonTools.ParsePosixDate(date)).ToArray();
// Add the fetched information
RecDateInfo _time = new([], elementTypes, valueType, recDates);
diff --git a/VisualCard.ShowCalendars/TestFiles/vevent.vcs b/VisualCard.ShowCalendars/TestFiles/vevent.vcs
index c3b1d0c5..853ec74e 100644
--- a/VisualCard.ShowCalendars/TestFiles/vevent.vcs
+++ b/VisualCard.ShowCalendars/TestFiles/vevent.vcs
@@ -3,11 +3,11 @@ PRODID:-//xyz Corp//NONSGML PDA Calendar Version 1.0//EN
VERSION:2.0
CALSCALE:GREGORIAN
BEGIN:VEVENT
-DTSTAMP:1996-07-04 12:00:00
+DTSTAMP:1996-07-04T12:00:00
UID:uid1@example.com
ORGANIZER:mailto:jsmith@example.com
-DTSTART:1996-09-18 14:30:00
-DTEND:1996-09-20 22:00:00
+DTSTART:1996-09-18T14:30:00
+DTEND:1996-09-20T22:00:00
STATUS:CONFIRMED
CATEGORIES:CONFERENCE
SUMMARY:Networld+Interop Conference
diff --git a/VisualCard.ShowCalendars/TestFiles/veventAttach.vcs b/VisualCard.ShowCalendars/TestFiles/veventAttach.vcs
index 71ced48f..fce06326 100644
--- a/VisualCard.ShowCalendars/TestFiles/veventAttach.vcs
+++ b/VisualCard.ShowCalendars/TestFiles/veventAttach.vcs
@@ -3,13 +3,13 @@ METHOD:xyz
VERSION:2.0
PRODID:-//ABC Corporation//NONSGML My Product//EN
BEGIN:VEVENT
-DTSTAMP:1997-03-24 12:00:00
+DTSTAMP:1997-03-24T12:00:00
SEQUENCE:0
UID:uid3@example.com
ORGANIZER:mailto:jdoe@example.com
ATTENDEE;RSVP=TRUE:mailto:jsmith@example.com
-DTSTART:1997-03-24 12:30:00
-DTEND:1997-03-24 21:00:00
+DTSTART:1997-03-24T12:30:00
+DTEND:1997-03-24T21:00:00
CATEGORIES:MEETING,PROJECT
CLASS:PUBLIC
SUMMARY:Calendaring Interoperability Planning Meeting
diff --git a/VisualCard.ShowCalendars/TestFiles/veventTransp.vcs b/VisualCard.ShowCalendars/TestFiles/veventTransp.vcs
index e769796e..1fb391c5 100644
--- a/VisualCard.ShowCalendars/TestFiles/veventTransp.vcs
+++ b/VisualCard.ShowCalendars/TestFiles/veventTransp.vcs
@@ -3,7 +3,7 @@ VERSION:2.0
PRODID:-//ABC Corporation//NONSGML My Product//EN
BEGIN:VEVENT
UID:20070423T123432Z-541111@example.com
-DTSTAMP:2007-04-23 12:34:32
+DTSTAMP:2007-04-23T12:34:32
DTSTART;VALUE=DATE:2007-06-28
DTEND;VALUE=DATE:2007-07-09
SUMMARY:Festival International de Jazz de Montreal
diff --git a/VisualCard.ShowCalendars/TestFiles/veventXnonstandard.ics b/VisualCard.ShowCalendars/TestFiles/veventXnonstandard.ics
index d940282d..f0a9c852 100644
--- a/VisualCard.ShowCalendars/TestFiles/veventXnonstandard.ics
+++ b/VisualCard.ShowCalendars/TestFiles/veventXnonstandard.ics
@@ -3,10 +3,10 @@ PRODID:-//GALAXY CALENDAR//Calendar//EN
VERSION:2.0
CALSCALE:GREGORIAN
BEGIN:VEVENT
-DTSTAMP:2024-09-02 11:32:21
+DTSTAMP:2024-09-02T11:32:21Z
UID:20240902T113221Z-1@GALAXY-CALENDAR-EVENT-4ff38e00-2373-4580-87b2-0d88
26f60702
-CREATED:2024-09-02 11:32:21
+CREATED:2024-09-02T11:32:21Z
SUMMARY;ENCODING=QUOTED-PRINTABLE:Frameworks 6.6 tar
STATUS:CONFIRMED
TRANSP:TRANSPARENT
diff --git a/VisualCard/Parsers/VcardCommonTools.cs b/VisualCard/Parsers/VcardCommonTools.cs
index 8fb6180e..9189f811 100644
--- a/VisualCard/Parsers/VcardCommonTools.cs
+++ b/VisualCard/Parsers/VcardCommonTools.cs
@@ -18,7 +18,6 @@
//
using System;
-using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
@@ -27,8 +26,140 @@
namespace VisualCard.Parsers
{
- internal static class VcardCommonTools
+ ///
+ /// Common tools for vCard parsing
+ ///
+ public static class VcardCommonTools
{
+ private static readonly string[] supportedDateTimeFormats =
+ [
+ @"yyyyMMdd",
+ @"yyyy-MM-dd",
+ @"yyyyMMdd\THHmmss\Z",
+ @"yyyyMMdd\THHmmss",
+ @"yyyy-MM-dd\THH\:mm\:ss\Z",
+ @"yyyy-MM-dd\THH\:mm\:ss",
+ ];
+
+ private static readonly string[] supportedDateFormats =
+ [
+ @"yyyyMMdd",
+ @"yyyy-MM-dd",
+ ];
+
+ private static readonly string[] supportedTimeFormats =
+ [
+ @"hh",
+ @"hhmm",
+ @"hh\:mm",
+ @"hhmmss",
+ @"hh\:mm\:ss",
+ ];
+
+ ///
+ /// Parses the POSIX date formatted with the representation according to the vCard and vCalendar specifications
+ ///
+ /// Date representation in basic or extended format of ISO 8601
+ /// Whether to accept only date
+ /// An instance of that matches the representation
+ ///
+ public static DateTimeOffset ParsePosixDate(string posixDateRepresentation, bool dateOnly = false)
+ {
+ // Check for sanity
+ if (string.IsNullOrEmpty(posixDateRepresentation))
+ throw new ArgumentException($"Date representation is not provided.");
+
+ // Now, check the representation
+ if (DateTimeOffset.TryParseExact(posixDateRepresentation, dateOnly ? supportedDateFormats : supportedDateTimeFormats, CultureInfo.InvariantCulture, DateTimeStyles.None, out var date))
+ return date;
+ throw new ArgumentException($"Can't parse date {posixDateRepresentation}");
+ }
+ ///
+ /// Tries to parse the POSIX date formatted with the representation according to the vCard and vCalendar specifications
+ ///
+ /// Date representation in basic or extended format of ISO 8601
+ /// Whether to accept only date
+ /// [] Date output parsed from the representation
+ /// True if parsed successfully; false otherwise.
+ public static bool TryParsePosixDate(string posixDateRepresentation, out DateTimeOffset date, bool dateOnly = false)
+ {
+ try
+ {
+ date = ParsePosixDate(posixDateRepresentation, dateOnly);
+ return true;
+ }
+ catch
+ {
+ return false;
+ }
+ }
+
+ ///
+ /// Saves the date to a ISO 8601 formatted date
+ ///
+ /// Date to save
+ /// Whether to save only date
+ /// A string representation of a date formatted with the basic ISO 8601 format
+ public static string SavePosixDate(DateTimeOffset posixDateRepresentation, bool dateOnly = false)
+ {
+ StringBuilder posixDateBuilder = new(
+ $"{posixDateRepresentation.Year:0000}" +
+ $"{posixDateRepresentation.Month:00}" +
+ $"{posixDateRepresentation.Day:00}"
+ );
+ if (!dateOnly)
+ posixDateBuilder.Append(
+ $"T" +
+ $"{posixDateRepresentation.Hour:00}" +
+ $"{posixDateRepresentation.Minute:00}" +
+ $"{posixDateRepresentation.Second:00}" +
+ $"{(posixDateRepresentation.Offset == new TimeSpan() ? "Z" : "")}"
+ );
+ return posixDateBuilder.ToString();
+ }
+
+ ///
+ /// Parses the POSIX UTC offset formatted with the representation according to the vCard and vCalendar specifications
+ ///
+ /// UTC offset representation in basic or extended format of ISO 8601, prefixed by either a plus or a minus sign
+ /// An instance of that matches the representation
+ ///
+ public static TimeSpan ParseUtcOffset(string utcOffsetRepresentation)
+ {
+ // Check for sanity
+ if (string.IsNullOrEmpty(utcOffsetRepresentation))
+ throw new ArgumentException($"UTC offset representation is not provided.");
+
+ // Now, this representation might be a POSIX offset that follows the vCard specification, but check the sign,
+ // because it might be either <+/->HHmmss, <+/->HHmm, or <+/->HH.
+ string designatorStr = utcOffsetRepresentation.Substring(0, 1);
+ string offsetNoSign = utcOffsetRepresentation.Substring(1);
+ if (designatorStr != "+" && designatorStr != "-")
+ throw new ArgumentException($"Designator {designatorStr} is invalid.");
+ if (TimeSpan.TryParseExact(offsetNoSign, supportedTimeFormats, CultureInfo.InvariantCulture, out TimeSpan offset))
+ return designatorStr == "-" && offset != new TimeSpan() ? -offset : offset;
+ throw new ArgumentException($"Can't parse offset {utcOffsetRepresentation}");
+ }
+
+ ///
+ /// Saves the UTC offset to a ISO 8601 formatted time
+ ///
+ /// UTC offset to save
+ /// A string representation of a UTC offset formatted with the basic ISO 8601 format
+ public static string SaveUtcOffset(TimeSpan utcOffsetRepresentation)
+ {
+ StringBuilder utcOffsetBuilder = new(
+ $"{(utcOffsetRepresentation < new TimeSpan() ? "-" : "+")}" +
+ $"{Math.Abs(utcOffsetRepresentation.Hours):00}" +
+ $"{Math.Abs(utcOffsetRepresentation.Minutes):00}"
+ );
+ if (utcOffsetRepresentation.Seconds != 0)
+ utcOffsetBuilder.Append(
+ $"{Math.Abs(utcOffsetRepresentation.Seconds):00}"
+ );
+ return utcOffsetBuilder.ToString();
+ }
+
internal static string GetTypesString(string[] args, string @default, bool isSpecifierRequired = true)
{
// We're given an array of split arguments of an element delimited by the colon, such as: "...TYPE=home..."
@@ -160,116 +291,5 @@ internal static Stream GetBlobData(string[]? args, string? keyEncoded)
else
throw new InvalidOperationException("Not a blob. You should somehow handle it.");
}
-
- internal static DateTimeOffset ParsePosixDate(string posixDateRepresentation)
- {
- // Check to see if this date and time representation is supported by .NET
- if (DateTimeOffset.TryParse(posixDateRepresentation, out DateTimeOffset date))
- return date;
-
- // Now, this date might be a POSIX date that follows the vCard specification, but check it
- if (posixDateRepresentation.Length == 8)
- {
- // It might be yyyyMMdd, but check again
- string yearStr = posixDateRepresentation.Substring(0, 4);
- string monthStr = posixDateRepresentation.Substring(4, 2);
- string dayStr = posixDateRepresentation.Substring(6, 2);
- if (DateTimeOffset.TryParseExact($"{yearStr}/{monthStr}/{dayStr}", "yyyy/MM/dd", CultureInfo.InvariantCulture, DateTimeStyles.None, out date))
- return date;
- }
- else if (posixDateRepresentation.Length == 15 || posixDateRepresentation.Length == 16)
- {
- // It might be yyyyMMdd + "T" + HHmmss + ["Z"], but check again
- string yearStr = posixDateRepresentation.Substring(0, 4);
- string monthStr = posixDateRepresentation.Substring(4, 2);
- string dayStr = posixDateRepresentation.Substring(6, 2);
- char timeIndicator = posixDateRepresentation[8];
- string hourStr = posixDateRepresentation.Substring(9, 2);
- string minuteStr = posixDateRepresentation.Substring(11, 2);
- string secondStr = posixDateRepresentation.Substring(13, 2);
- if (timeIndicator != 'T')
- throw new ArgumentException($"Time indicator is invalid.");
- if (posixDateRepresentation.Length == 16 && posixDateRepresentation[15] != 'Z')
- throw new ArgumentException($"UTC indicator is invalid.");
- bool assumeUtc = posixDateRepresentation.Length == 16 && posixDateRepresentation[15] == 'Z';
- var utcOffset = assumeUtc ? DateTimeOffset.UtcNow.Offset : DateTimeOffset.Now.Offset;
- string renderedOffset = SaveUtcOffset(utcOffset);
- if (DateTimeOffset.TryParseExact($"{yearStr}/{monthStr}/{dayStr} {hourStr}:{minuteStr}:{secondStr} {renderedOffset}", "yyyy/MM/dd HH:mm:ss zzz", CultureInfo.InvariantCulture, assumeUtc ? DateTimeStyles.AssumeUniversal : DateTimeStyles.AssumeLocal, out date))
- return date;
- }
- throw new ArgumentException($"Can't parse date {posixDateRepresentation}");
- }
-
- internal static bool TryParsePosixDate(string posixDateRepresentation, out DateTimeOffset date)
- {
- try
- {
- date = ParsePosixDate(posixDateRepresentation);
- return true;
- }
- catch
- {
- return false;
- }
- }
-
- internal static string SavePosixDate(DateTimeOffset posixDateRepresentation, bool dateOnly = false)
- {
- StringBuilder posixDateBuilder = new(
- $"{posixDateRepresentation.Year:0000}" +
- $"{posixDateRepresentation.Month:00}" +
- $"{posixDateRepresentation.Day:00}"
- );
- if (!dateOnly)
- posixDateBuilder.Append(
- $"T" +
- $"{posixDateRepresentation.Hour:00}" +
- $"{posixDateRepresentation.Minute:00}" +
- $"{posixDateRepresentation.Second:00}" +
- $"{(posixDateRepresentation.Offset == new TimeSpan() ? "Z" : "")}"
- );
- return posixDateBuilder.ToString();
- }
-
- internal static TimeSpan ParseUtcOffset(string utcOffsetRepresentation)
- {
- // Check for sanity
- if (utcOffsetRepresentation.Length != 3 && utcOffsetRepresentation.Length != 5 && utcOffsetRepresentation.Length != 7)
- throw new ArgumentException($"UTC offset representation [{utcOffsetRepresentation}] is invalid.");
- bool hasMinutes = utcOffsetRepresentation.Length >= 5;
- bool hasSeconds = utcOffsetRepresentation.Length == 7;
-
- // Now, this representation might be a POSIX offset that follows the vCard specification, but check it,
- // because it might be either <+/->HHmmss, <+/->HHmm, or <+/->HH.
- string designatorStr = utcOffsetRepresentation.Substring(0, 1);
- string hourStr = utcOffsetRepresentation.Substring(1, 2);
- string minuteStr = hasMinutes ? utcOffsetRepresentation.Substring(3, 2) : "";
- string secondStr = hasSeconds ? utcOffsetRepresentation.Substring(5, 2) : "";
- if (designatorStr != "+" && designatorStr != "-")
- throw new ArgumentException($"Designator {designatorStr} is invalid.");
- if (hourStr == "00" && (!hasMinutes || (hasMinutes && minuteStr == "00")) && (!hasSeconds || (hasSeconds && secondStr == "00")))
- {
- if (designatorStr == "-")
- throw new ArgumentException($"Can't specify negative zero offset.");
- return new();
- }
- if (TimeSpan.TryParseExact($"{hourStr}:{(hasMinutes ? minuteStr : "00")}:{(hasSeconds ? secondStr : "00")}", "hh\\:mm\\:ss", CultureInfo.InvariantCulture, out TimeSpan offset))
- return designatorStr == "-" ? -offset : offset;
- throw new ArgumentException($"Can't parse offset {utcOffsetRepresentation}");
- }
-
- internal static string SaveUtcOffset(TimeSpan utcOffsetRepresentation)
- {
- StringBuilder utcOffsetBuilder = new(
- $"{(utcOffsetRepresentation < new TimeSpan() ? "-" : "+")}" +
- $"{Math.Abs(utcOffsetRepresentation.Hours):00}" +
- $"{Math.Abs(utcOffsetRepresentation.Minutes):00}"
- );
- if (utcOffsetRepresentation.Seconds != 0)
- utcOffsetBuilder.Append(
- $"{Math.Abs(utcOffsetRepresentation.Seconds):00}"
- );
- return utcOffsetBuilder.ToString();
- }
}
}