diff --git a/VisualCard.Calendar/Parsers/VCalendarConstants.cs b/VisualCard.Calendar/Parsers/VCalendarConstants.cs index 1cd0ab2..c24cf89 100644 --- a/VisualCard.Calendar/Parsers/VCalendarConstants.cs +++ b/VisualCard.Calendar/Parsers/VCalendarConstants.cs @@ -83,6 +83,9 @@ internal static class VCalendarConstants internal const string _valueArgumentSpecifier = "VALUE="; internal const string _encodingArgumentSpecifier = "ENCODING="; + // Available in vCalendar 1.0 + internal const string _daylightSpecifier = "DAYLIGHT"; + // Available in vCalendar 2.0 internal const string _dateStampSpecifier = "DTSTAMP"; internal const string _calScaleSpecifier = "CALSCALE"; diff --git a/VisualCard.Calendar/Parsers/VCalendarParserTools.cs b/VisualCard.Calendar/Parsers/VCalendarParserTools.cs index e788979..1bc1739 100644 --- a/VisualCard.Calendar/Parsers/VCalendarParserTools.cs +++ b/VisualCard.Calendar/Parsers/VCalendarParserTools.cs @@ -26,6 +26,7 @@ using System.Linq; using VisualCard.Calendar.Parts.Implementations.TimeZone; using VisualCard.Calendar.Parts.Implementations.Todo; +using VisualCard.Calendar.Parts.Implementations.Legacy; namespace VisualCard.Calendar.Parsers { @@ -83,7 +84,8 @@ internal static bool EnumArrayTypeSupported(CalendarPartsArrayEnum partsArrayEnu CalendarPartsArrayEnum.TimeZoneName => calendarVersion.Major == 2 && TypeMatch(componentType, typeof(CalendarTimeZone)), CalendarPartsArrayEnum.TimeZoneOffsetFrom => calendarVersion.Major == 2 && TypeMatch(componentType, typeof(CalendarStandard), typeof(CalendarDaylight)), CalendarPartsArrayEnum.TimeZoneOffsetTo => calendarVersion.Major == 2 && TypeMatch(componentType, typeof(CalendarStandard), typeof(CalendarDaylight)), - CalendarPartsArrayEnum.RecDate => calendarVersion.Major == 2 && TypeMatch(componentType, typeof(CalendarEvent), typeof(CalendarTodo), typeof(CalendarJournal), typeof(CalendarStandard), typeof(CalendarDaylight)), + CalendarPartsArrayEnum.RecDate => calendarVersion.Major == 2 && TypeMatch(componentType, typeof(Parts.Calendar)), + CalendarPartsArrayEnum.Daylight => calendarVersion.Major == 1 && TypeMatch(componentType, typeof(CalendarEvent), typeof(CalendarTodo), typeof(CalendarJournal), typeof(CalendarStandard), typeof(CalendarDaylight)), CalendarPartsArrayEnum.NonstandardNames => true, _ => throw new InvalidOperationException("Invalid parts array enumeration type to get supported value"), @@ -142,6 +144,7 @@ internal static string GetPrefixFromPartsArrayEnum(CalendarPartsArrayEnum partsA CalendarPartsArrayEnum.TimeZoneOffsetFrom => VCalendarConstants._tzOffsetFromSpecifier, CalendarPartsArrayEnum.TimeZoneOffsetTo => VCalendarConstants._tzOffsetToSpecifier, CalendarPartsArrayEnum.RecDate => VCalendarConstants._recDateSpecifier, + CalendarPartsArrayEnum.Daylight => VCalendarConstants._daylightSpecifier, CalendarPartsArrayEnum.NonstandardNames => VCalendarConstants._xSpecifier, _ => throw new NotImplementedException($"Array enumeration {partsArrayEnum} is not implemented.") @@ -187,6 +190,8 @@ internal static (CalendarPartsArrayEnum, PartCardinality) GetPartsArrayEnumFromT return (CalendarPartsArrayEnum.TimeZoneOffsetTo, PartCardinality.MayBeOne); else if (partsArrayType == typeof(RecDateInfo)) return (CalendarPartsArrayEnum.RecDate, PartCardinality.Any); + else if (partsArrayType == typeof(DaylightInfo)) + return (CalendarPartsArrayEnum.Daylight, PartCardinality.MayBeOne); else if (partsArrayType == typeof(XNameInfo)) return (CalendarPartsArrayEnum.NonstandardNames, PartCardinality.Any); throw new NotImplementedException($"Type {partsArrayType.Name} doesn't represent any part array."); @@ -213,6 +218,7 @@ internal static (PartType type, object enumeration, Type? enumType, Func (PartType.PartsArray, CalendarPartsArrayEnum.TimeZoneOffsetFrom, typeof(TimeZoneOffsetFromInfo), TimeZoneOffsetFromInfo.FromStringVcalendarStatic, "", "", []), VCalendarConstants._tzOffsetToSpecifier => (PartType.PartsArray, CalendarPartsArrayEnum.TimeZoneOffsetTo, typeof(TimeZoneOffsetToInfo), TimeZoneOffsetToInfo.FromStringVcalendarStatic, "", "", []), VCalendarConstants._recDateSpecifier => (PartType.PartsArray, CalendarPartsArrayEnum.RecDate, typeof(RecDateInfo), RecDateInfo.FromStringVcalendarStatic, "", "", []), + VCalendarConstants._daylightSpecifier => (PartType.PartsArray, CalendarPartsArrayEnum.Daylight, typeof(DaylightInfo), DaylightInfo.FromStringVcalendarStatic, "", "", []), VCalendarConstants._xSpecifier => (PartType.PartsArray, CalendarPartsArrayEnum.NonstandardNames, typeof(XNameInfo), XNameInfo.FromStringVcalendarStatic, "", "", []), VCalendarConstants._productIdSpecifier => (PartType.Strings, CalendarStringsEnum.ProductId, null, null, "", "", []), VCalendarConstants._calScaleSpecifier => (PartType.Strings, CalendarStringsEnum.CalScale, null, null, "", "", []), diff --git a/VisualCard.Calendar/Parts/Enums/CalendarPartsArrayEnum.cs b/VisualCard.Calendar/Parts/Enums/CalendarPartsArrayEnum.cs index 21bab06..105bcfc 100644 --- a/VisualCard.Calendar/Parts/Enums/CalendarPartsArrayEnum.cs +++ b/VisualCard.Calendar/Parts/Enums/CalendarPartsArrayEnum.cs @@ -100,5 +100,9 @@ public enum CalendarPartsArrayEnum /// Time zone offset to /// TimeZoneOffsetTo, + /// + /// Daylight saving time info (vCalendar 1.0) + /// + Daylight, } } diff --git a/VisualCard.Calendar/Parts/Implementations/Legacy/DaylightInfo.cs b/VisualCard.Calendar/Parts/Implementations/Legacy/DaylightInfo.cs new file mode 100644 index 0000000..3b80b10 --- /dev/null +++ b/VisualCard.Calendar/Parts/Implementations/Legacy/DaylightInfo.cs @@ -0,0 +1,173 @@ +// +// 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.Collections.Generic; +using System.Diagnostics; +using VisualCard.Parsers; + +namespace VisualCard.Calendar.Parts.Implementations.Legacy +{ + /// + /// Daylight information (entire calendar) + /// + [DebuggerDisplay("Daylight = {Flag}")] + public class DaylightInfo : BaseCalendarPartInfo, IEquatable + { + /// + /// Whether it's enabled or not + /// + public bool Flag { get; } + + /// + /// UTC offset of the daylight saving time + /// + public TimeSpan UtcOffset { get; } + + /// + /// Start date and time of the daylight saving time + /// + public DateTime DaylightStart { get; } + + /// + /// End date and time of the daylight saving time + /// + public DateTime DaylightEnd { get; } + + /// + /// Standard time designation + /// + public string? StandardDesignation { get; } + + /// + /// Daylight saving time designation + /// + public string? DaylightDesignation { get; } + + internal static BaseCalendarPartInfo FromStringVcalendarStatic(string value, string[] finalArgs, string[] elementTypes, string valueType, Version calendarVersion) => + new DaylightInfo().FromStringVcalendarInternal(value, finalArgs, elementTypes, valueType, calendarVersion); + + internal override string ToStringVcalendarInternal(Version calendarVersion) + { + if (!Flag) + return "FALSE"; + string posixUtc = VcardParserTools.SaveUtcOffset(UtcOffset); + string posixStart = VcardParserTools.SavePosixDate(DaylightStart); + string posixEnd = VcardParserTools.SavePosixDate(DaylightEnd); + return $"TRUE;{posixUtc};{posixStart};{posixEnd};{StandardDesignation};{DaylightDesignation}"; + } + + internal override BaseCalendarPartInfo FromStringVcalendarInternal(string value, string[] finalArgs, string[] elementTypes, string valueType, Version calendarVersion) + { + // Get the values + string[] split = value.Split(';'); + if (split.Length == 1 && split[0] == "FALSE") + return new DaylightInfo(finalArgs, elementTypes, valueType, false, new(), new(), new(), "", ""); + if (split.Length != 6) + throw new ArgumentException($"When splitting daylight information, the split value is {split.Length} instead of 6."); + string unprocessedUtc = split[1]; + string unprocessedStart = split[2]; + string unprocessedEnd = split[3]; + string standard = split[4]; + string daylight = split[5]; + + // Process the UTC offset and start/end dates + TimeSpan utcOffset = VcardParserTools.ParseUtcOffset(unprocessedUtc); + DateTime startDate = VcardParserTools.ParsePosixDate(unprocessedStart); + DateTime endDate = VcardParserTools.ParsePosixDate(unprocessedEnd); + + // Populate the fields + DaylightInfo _geo = new(finalArgs, elementTypes, valueType, true, utcOffset, startDate, endDate, standard, daylight); + return _geo; + } + + /// + public override bool Equals(object obj) => + Equals((DaylightInfo)obj); + + /// + /// Checks to see if both the parts are equal + /// + /// The target instance to check to see if they equal + /// True if all the part elements are equal. Otherwise, false. + public bool Equals(DaylightInfo other) => + Equals(this, other); + + /// + /// Checks to see if both the parts are equal + /// + /// The source instance to check to see if they equal + /// The target instance to check to see if they equal + /// True if all the part elements are equal. Otherwise, false. + public bool Equals(DaylightInfo source, DaylightInfo target) + { + // We can't perform this operation on null. + if (source is null || target is null) + return false; + + // Check all the properties + return + source.Flag == target.Flag && + source.UtcOffset == target.UtcOffset && + source.DaylightStart == target.DaylightStart && + source.DaylightEnd == target.DaylightEnd && + source.StandardDesignation == target.StandardDesignation && + source.DaylightDesignation == target.DaylightDesignation + ; + } + + /// + public override int GetHashCode() + { + int hashCode = 269991333; + hashCode = hashCode * -1521134295 + base.GetHashCode(); + hashCode = hashCode * -1521134295 + Flag.GetHashCode(); + hashCode = hashCode * -1521134295 + UtcOffset.GetHashCode(); + hashCode = hashCode * -1521134295 + DaylightStart.GetHashCode(); + hashCode = hashCode * -1521134295 + DaylightEnd.GetHashCode(); + hashCode = hashCode * -1521134295 + EqualityComparer.Default.GetHashCode(StandardDesignation); + hashCode = hashCode * -1521134295 + EqualityComparer.Default.GetHashCode(DaylightDesignation); + return hashCode; + } + + /// + public static bool operator ==(DaylightInfo left, DaylightInfo right) => + left.Equals(right); + + /// + public static bool operator !=(DaylightInfo left, DaylightInfo right) => + !(left == right); + + internal override bool EqualsInternal(BaseCalendarPartInfo source, BaseCalendarPartInfo target) => + (DaylightInfo)source == (DaylightInfo)target; + + internal DaylightInfo() { } + + internal DaylightInfo(string[] arguments, string[] elementTypes, string valueType, bool flag, TimeSpan utcOffset, DateTime daylightStart, DateTime daylightEnd, string standardDesignation, string daylightDesignation) : + base(arguments, elementTypes, valueType) + { + Flag = flag; + UtcOffset = utcOffset; + DaylightStart = daylightStart; + DaylightEnd = daylightEnd; + StandardDesignation = standardDesignation; + DaylightDesignation = daylightDesignation; + } + } +} diff --git a/VisualCard.ShowCalendars/Properties/launchSettings.json b/VisualCard.ShowCalendars/Properties/launchSettings.json index 6648331..7e8a54e 100644 --- a/VisualCard.ShowCalendars/Properties/launchSettings.json +++ b/VisualCard.ShowCalendars/Properties/launchSettings.json @@ -23,6 +23,10 @@ "vCalendar 2.0 test - event with time zone": { "commandName": "Project", "commandLineArgs": "TestFiles/vcalendarEventTz.ics" + }, + "vCalendar 1.0 test": { + "commandName": "Project", + "commandLineArgs": "TestFiles/vevent1.vcs" } } } diff --git a/VisualCard.ShowCalendars/TestFiles/vevent1.vcs b/VisualCard.ShowCalendars/TestFiles/vevent1.vcs new file mode 100644 index 0000000..a8580ce --- /dev/null +++ b/VisualCard.ShowCalendars/TestFiles/vevent1.vcs @@ -0,0 +1,18 @@ +BEGIN:VCALENDAR +VERSION:1.0 +DAYLIGHT:TRUE;-09;19960407T115959;19961027T100000;PST;PDT +BEGIN:VEVENT +CATEGORIES:MEETING +STATUS:NEEDS ACTION +DTSTART:19960401T073000Z +DTEND:19960401T083000Z +SUMMARY:Steve's Proposal Review +DESCRIPTION:Steve and John to review newest proposal material +CLASS:PRIVATE +END:VEVENT +BEGIN:VTODO +SUMMARY:John to pay for lunch +DUE:19960401T083000Z +STATUS:NEEDS ACTION +END:VTODO +END:VCALENDAR diff --git a/VisualCard/Parsers/VcardParserTools.cs b/VisualCard/Parsers/VcardParserTools.cs index 1c1e3b5..90a6cd8 100644 --- a/VisualCard/Parsers/VcardParserTools.cs +++ b/VisualCard/Parsers/VcardParserTools.cs @@ -443,25 +443,26 @@ internal static string SavePosixDate(DateTime posixDateRepresentation, bool date internal static TimeSpan ParseUtcOffset(string utcOffsetRepresentation) { // Check for sanity - if (utcOffsetRepresentation.Length != 5 && utcOffsetRepresentation.Length != 7) + 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 or <+/->HHmm. + // because it might be either <+/->HHmmss, <+/->HHmm, or <+/->HH. string designatorStr = utcOffsetRepresentation.Substring(0, 1); string hourStr = utcOffsetRepresentation.Substring(1, 2); - string minuteStr = utcOffsetRepresentation.Substring(3, 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" && minuteStr == "00" && (!hasSeconds || (hasSeconds && secondStr == "00"))) + 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}:{minuteStr}:{(hasSeconds ? secondStr : "00")}", "hh\\:mm\\:ss", CultureInfo.InvariantCulture, out TimeSpan offset)) + 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}"); }