Skip to content

Commit

Permalink
add - doc - Made date/time and offset tools public
Browse files Browse the repository at this point in the history
---

We've added date/time parsing and saving tools to the public API so that users can parse ISO 8601 formatted dates. You can also use the UTC offset parsing tools.

Please note that we no longer support dates that don't conform to this format.

---

Type: add
Breaking: False
Doc Required: True
Backport Required: False
Part: 1/1
  • Loading branch information
AptiviCEO committed Sep 30, 2024
1 parent f03ffa4 commit 15041ef
Show file tree
Hide file tree
Showing 6 changed files with 143 additions and 123 deletions.
2 changes: 1 addition & 1 deletion VisualCard.Calendar/Parts/Implementations/RecDateInfo.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
6 changes: 3 additions & 3 deletions VisualCard.ShowCalendars/TestFiles/vevent.vcs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
6 changes: 3 additions & 3 deletions VisualCard.ShowCalendars/TestFiles/veventAttach.vcs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion VisualCard.ShowCalendars/TestFiles/veventTransp.vcs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions VisualCard.ShowCalendars/TestFiles/veventXnonstandard.ics
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
246 changes: 133 additions & 113 deletions VisualCard/Parsers/VcardCommonTools.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@
//

using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
Expand All @@ -27,8 +26,140 @@

namespace VisualCard.Parsers
{
internal static class VcardCommonTools
/// <summary>
/// Common tools for vCard parsing
/// </summary>
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",
];

/// <summary>
/// Parses the POSIX date formatted with the representation according to the vCard and vCalendar specifications
/// </summary>
/// <param name="posixDateRepresentation">Date representation in basic or extended format of ISO 8601</param>
/// <param name="dateOnly">Whether to accept only date</param>
/// <returns>An instance of <see cref="DateTimeOffset"/> that matches the representation</returns>
/// <exception cref="ArgumentException"></exception>
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}");
}
/// <summary>
/// Tries to parse the POSIX date formatted with the representation according to the vCard and vCalendar specifications
/// </summary>
/// <param name="posixDateRepresentation">Date representation in basic or extended format of ISO 8601</param>
/// <param name="dateOnly">Whether to accept only date</param>
/// <param name="date">[<see langword="out"/>] Date output parsed from the representation</param>
/// <returns>True if parsed successfully; false otherwise.</returns>
public static bool TryParsePosixDate(string posixDateRepresentation, out DateTimeOffset date, bool dateOnly = false)
{
try
{
date = ParsePosixDate(posixDateRepresentation, dateOnly);
return true;
}
catch
{
return false;
}
}

/// <summary>
/// Saves the date to a ISO 8601 formatted date
/// </summary>
/// <param name="posixDateRepresentation">Date to save</param>
/// <param name="dateOnly">Whether to save only date</param>
/// <returns>A string representation of a date formatted with the basic ISO 8601 format</returns>
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();
}

/// <summary>
/// Parses the POSIX UTC offset formatted with the representation according to the vCard and vCalendar specifications
/// </summary>
/// <param name="utcOffsetRepresentation">UTC offset representation in basic or extended format of ISO 8601, prefixed by either a plus or a minus sign</param>
/// <returns>An instance of <see cref="TimeSpan"/> that matches the representation</returns>
/// <exception cref="ArgumentException"></exception>
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}");
}

/// <summary>
/// Saves the UTC offset to a ISO 8601 formatted time
/// </summary>
/// <param name="utcOffsetRepresentation">UTC offset to save</param>
/// <returns>A string representation of a UTC offset formatted with the basic ISO 8601 format</returns>
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..."
Expand Down Expand Up @@ -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();
}
}
}

0 comments on commit 15041ef

Please sign in to comment.