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(); - } } }