From 2ed6e27a1f13e04c6387a3c0c7b3376c33037dda Mon Sep 17 00:00:00 2001 From: Folker Kinzel Date: Fri, 24 Jan 2025 02:05:51 +0100 Subject: [PATCH] tests --- .../Converters/DateAndOrTimeConverterTests.cs | 28 +- .../Models/DateAndOrTimeTests.cs | 23 +- .../Properties/DateAndOrTimePropertyTests.cs | 2 +- src/FolkerKinzel.VCards.Tests/V4Tests.cs | 2 +- .../VCardBuilderTests.cs | 2 +- .../BuilderParts/DateAndOrTimeBuilder.cs | 3 +- .../FolkerKinzel.VCards.csproj | 2 +- .../Converters/DateAndOrTimeConverter.cs | 98 +++++-- .../Intls/Converters/TimeConverter.cs | 5 + .../Models/DateAndOrTime.cs | 243 +++++++++++++----- .../Properties/DateAndOrTimeProperty.cs | 12 +- 11 files changed, 315 insertions(+), 105 deletions(-) diff --git a/src/FolkerKinzel.VCards.Tests/Intls/Converters/DateAndOrTimeConverterTests.cs b/src/FolkerKinzel.VCards.Tests/Intls/Converters/DateAndOrTimeConverterTests.cs index 34fc74323..d848d83c4 100644 --- a/src/FolkerKinzel.VCards.Tests/Intls/Converters/DateAndOrTimeConverterTests.cs +++ b/src/FolkerKinzel.VCards.Tests/Intls/Converters/DateAndOrTimeConverterTests.cs @@ -59,6 +59,18 @@ public void DateTest4() [TestMethod] public void RoundtripsTest() { + Roundtrip("1984", true, VCdVersion.V3_0); + Roundtrip("1984", true, VCdVersion.V4_0); + + Roundtrip("1984-02", true, VCdVersion.V3_0); + Roundtrip("1984-02", true, VCdVersion.V4_0); + + Roundtrip("---17", true, VCdVersion.V3_0); + Roundtrip("---17", true, VCdVersion.V4_0); + + Roundtrip("--12", true, VCdVersion.V3_0); + Roundtrip("--12", true, VCdVersion.V4_0); + Roundtrip("1972-01-31", true, VCdVersion.V3_0); Roundtrip("19720131T15-07", false, VCdVersion.V3_0); Roundtrip("19720131T15+04", false, VCdVersion.V3_0); @@ -104,8 +116,8 @@ static string ToDateTimeString(DateAndOrTime dt, VCdVersion version) { var builder = new StringBuilder(); dt.Switch( - dateOnly => DateAndOrTimeConverter.AppendDateTo(builder, dateOnly, version), - dto => DateAndOrTimeConverter.AppendDateTimeOffsetTo(builder, dto, version) + dateOnly => DateAndOrTimeConverter.AppendDateTo(builder, dateOnly, version, dt.HasMonth, dt.HasDay), + dto => DateAndOrTimeConverter.AppendDateTimeOffsetTo(builder, dto, version, dt.HasMonth, dt.HasDay) ); return builder.ToString(); } @@ -141,7 +153,7 @@ static string ToTimeStamp(DateTimeOffset dt, VCdVersion version) public void AppendDateTimeStringToTest1() { var builder = new StringBuilder(); - DateAndOrTimeConverter.AppendDateTimeOffsetTo(builder, default, VCdVersion.V3_0); + DateAndOrTimeConverter.AppendDateTimeOffsetTo(builder, default, VCdVersion.V3_0, true, true); Assert.AreEqual(0, builder.Length); } @@ -149,7 +161,7 @@ public void AppendDateTimeStringToTest1() public void AppendDateTimeStringToTest2a() { var builder = new StringBuilder(); - DateAndOrTimeConverter.AppendDateTimeOffsetTo(builder, new DateTime(2, 1, 1, 0, 0, 0, DateTimeKind.Utc), VCdVersion.V4_0); + DateAndOrTimeConverter.AppendDateTimeOffsetTo(builder, new DateTime(2, 1, 1, 0, 0, 0, DateTimeKind.Utc), VCdVersion.V4_0, true, true); Assert.AreEqual(0, builder.Length); } @@ -157,7 +169,7 @@ public void AppendDateTimeStringToTest2a() public void AppendDateTimeStringToTest2b() { var builder = new StringBuilder(); - DateAndOrTimeConverter.AppendDateTimeOffsetTo(builder, new DateTime(2, 1, 2, 0, 0, 0, DateTimeKind.Utc), VCdVersion.V4_0); + DateAndOrTimeConverter.AppendDateTimeOffsetTo(builder, new DateTime(2, 1, 2, 0, 0, 0, DateTimeKind.Utc), VCdVersion.V4_0, true, true); Assert.AreEqual(0, builder.Length); } @@ -165,7 +177,7 @@ public void AppendDateTimeStringToTest2b() public void AppendDateTimeStringToTest2c() { var builder = new StringBuilder(); - DateAndOrTimeConverter.AppendDateTimeOffsetTo(builder, new DateTime(2, 1, 1, 0, 0, 0, DateTimeKind.Utc), VCdVersion.V3_0); + DateAndOrTimeConverter.AppendDateTimeOffsetTo(builder, new DateTime(2, 1, 1, 0, 0, 0, DateTimeKind.Utc), VCdVersion.V3_0, true, true); Assert.AreEqual(0, builder.Length); } @@ -173,7 +185,7 @@ public void AppendDateTimeStringToTest2c() public void AppendDateTimeStringToTest3() { var builder = new StringBuilder(); - DateAndOrTimeConverter.AppendDateTimeOffsetTo(builder, new DateTime(4, 1, 1), VCdVersion.V4_0); + DateAndOrTimeConverter.AppendDateTimeOffsetTo(builder, new DateTime(4, 1, 1), VCdVersion.V4_0, true, true); string s = builder.ToString(); Assert.IsTrue(s.StartsWith("--")); } @@ -182,7 +194,7 @@ public void AppendDateTimeStringToTest3() public void AppendDateToTest1() { var builder = new StringBuilder(); - DateAndOrTimeConverter.AppendDateTo(builder, new DateOnly(4, 5, 1), VCdVersion.V3_0); + DateAndOrTimeConverter.AppendDateTo(builder, new DateOnly(4, 5, 1), VCdVersion.V3_0, true, true); string s = builder.ToString(); Assert.IsTrue(s.StartsWith("--")); } diff --git a/src/FolkerKinzel.VCards.Tests/Models/DateAndOrTimeTests.cs b/src/FolkerKinzel.VCards.Tests/Models/DateAndOrTimeTests.cs index f5d43b136..cc09511e0 100644 --- a/src/FolkerKinzel.VCards.Tests/Models/DateAndOrTimeTests.cs +++ b/src/FolkerKinzel.VCards.Tests/Models/DateAndOrTimeTests.cs @@ -310,8 +310,27 @@ public void EqualsTest1() [TestMethod] public void AsStringTest1() { - var daot = DateAndOrTime.Create(2, 4); + var daot = DateAndOrTime.Create(new DateOnly(1984, 2, 4), ignoreYear: true); string str = daot.AsString(CultureInfo.InvariantCulture); - Assert.AreEqual("02/04", str); + Assert.AreEqual("--02-04", str); + } + + + [TestMethod] + public void CreateTest1() + { + var daot = DateAndOrTime.Create(new DateOnly(1984, 3, 17), ignoreMonth: true, ignoreDay: true); + + Assert.AreEqual("1984", daot.AsString(CultureInfo.InvariantCulture)); + Assert.IsTrue(daot.Equals(DateAndOrTime.Create(new DateOnly(1984, 8, 20), ignoreMonth: true, ignoreDay: true))); + } + + [TestMethod] + public void CreateTest2() + { + var daot = DateAndOrTime.Create(new DateTimeOffset(1984, 3, 17, 0, 0, 0, TimeSpan.Zero), ignoreMonth: true, ignoreDay: true); + + Assert.AreEqual("1984", daot.AsString(CultureInfo.InvariantCulture)); + Assert.IsTrue(daot.Equals(DateAndOrTime.Create(new DateTimeOffset(1984, 8, 20, 0, 0, 0, TimeSpan.Zero), ignoreMonth: true, ignoreDay: true))); } } diff --git a/src/FolkerKinzel.VCards.Tests/Models/Properties/DateAndOrTimePropertyTests.cs b/src/FolkerKinzel.VCards.Tests/Models/Properties/DateAndOrTimePropertyTests.cs index 9dfbd516e..ca8500fae 100644 --- a/src/FolkerKinzel.VCards.Tests/Models/Properties/DateAndOrTimePropertyTests.cs +++ b/src/FolkerKinzel.VCards.Tests/Models/Properties/DateAndOrTimePropertyTests.cs @@ -142,7 +142,7 @@ public void CloneTest1() [TestMethod] public void CloneTest2() { - var prop1 = new DateAndOrTimeProperty(DateAndOrTime.Create(3, 15)); + var prop1 = new DateAndOrTimeProperty(DateAndOrTime.Create(new DateOnly(4, 3, 15))); var prop2 = (DateAndOrTimeProperty)prop1.Clone(); diff --git a/src/FolkerKinzel.VCards.Tests/V4Tests.cs b/src/FolkerKinzel.VCards.Tests/V4Tests.cs index e55667877..d1575e80c 100644 --- a/src/FolkerKinzel.VCards.Tests/V4Tests.cs +++ b/src/FolkerKinzel.VCards.Tests/V4Tests.cs @@ -372,7 +372,7 @@ public void ImppTest1() [TestMethod] public void CalendarTest1() { - var prop = new DateAndOrTimeProperty(DateAndOrTime.Create(5, 5)); + var prop = new DateAndOrTimeProperty(DateAndOrTime.Create(new DateOnly(4, 5, 5), ignoreYear: true)); Assert.AreEqual("gregorian", prop.Parameters.Calendar); prop.Parameters.Calendar = "GREGORIAN"; Assert.AreEqual("GREGORIAN", prop.Parameters.Calendar); diff --git a/src/FolkerKinzel.VCards.Tests/VCardBuilderTests.cs b/src/FolkerKinzel.VCards.Tests/VCardBuilderTests.cs index 3915ac6c1..d64dee4fa 100644 --- a/src/FolkerKinzel.VCards.Tests/VCardBuilderTests.cs +++ b/src/FolkerKinzel.VCards.Tests/VCardBuilderTests.cs @@ -316,7 +316,7 @@ public void EditBirthDayViewsTest1() builder.BirthDayViews.Edit(p => prop = p); Assert.IsNotNull(prop); Assert.IsFalse(prop.Any()); - builder.VCard.BirthDayViews = new DateAndOrTimeProperty(DateAndOrTime.Create(12, 24)).Append(null); + builder.VCard.BirthDayViews = new DateAndOrTimeProperty(DateAndOrTime.Create(new DateOnly(1984, 12, 24), ignoreYear: true)).Append(null); builder.BirthDayViews.Edit(p => prop = p); Assert.IsTrue(prop.Any()); CollectionAssert.AllItemsAreNotNull(prop.ToArray()); diff --git a/src/FolkerKinzel.VCards/BuilderParts/DateAndOrTimeBuilder.cs b/src/FolkerKinzel.VCards/BuilderParts/DateAndOrTimeBuilder.cs index de67f57cf..63d7b9f62 100644 --- a/src/FolkerKinzel.VCards/BuilderParts/DateAndOrTimeBuilder.cs +++ b/src/FolkerKinzel.VCards/BuilderParts/DateAndOrTimeBuilder.cs @@ -2,6 +2,7 @@ using FolkerKinzel.VCards.Enums; using FolkerKinzel.VCards.Extensions; using FolkerKinzel.VCards.Intls; +using FolkerKinzel.VCards.Intls.Converters; using FolkerKinzel.VCards.Models; using FolkerKinzel.VCards.Models.Properties; using FolkerKinzel.VCards.Models.Properties.Parameters; @@ -209,7 +210,7 @@ public VCardBuilder Add(int month, try { - val = DateAndOrTime.Create(month, day); + val = DateAndOrTime.Create(new DateOnly(DateAndOrTimeConverter.FIRST_LEAP_YEAR, month, day), ignoreYear: true); } catch { diff --git a/src/FolkerKinzel.VCards/FolkerKinzel.VCards.csproj b/src/FolkerKinzel.VCards/FolkerKinzel.VCards.csproj index e2b1d1f67..746ef65ec 100644 --- a/src/FolkerKinzel.VCards/FolkerKinzel.VCards.csproj +++ b/src/FolkerKinzel.VCards/FolkerKinzel.VCards.csproj @@ -9,7 +9,7 @@ FolkerKinzel.VCards FolkerKinzel.VCards 8.0.0-alpha.2 - 8.0.0.35 + 8.0.0.37 8.0.0.0 https://github.com/FolkerKinzel/VCards/releases/tag/v8.0.0-alpha.2 .net vcard vcf c# diff --git a/src/FolkerKinzel.VCards/Intls/Converters/DateAndOrTimeConverter.cs b/src/FolkerKinzel.VCards/Intls/Converters/DateAndOrTimeConverter.cs index a0219c673..6885d9400 100644 --- a/src/FolkerKinzel.VCards/Intls/Converters/DateAndOrTimeConverter.cs +++ b/src/FolkerKinzel.VCards/Intls/Converters/DateAndOrTimeConverter.cs @@ -78,7 +78,7 @@ internal bool TryParse(ReadOnlySpan roSpan, [NotNullWhen(true)] out DateAn if (roSpan.StartsWith("--", StringComparison.Ordinal)) { - // "--MM" zu "0004-MM": + // "--MM" to "0004-MM": // Note the use of YYYY-MM in the second example above. YYYYMM is // disallowed to prevent confusion with YYMMDD. if (roSpan.Length == 4) @@ -92,7 +92,24 @@ internal bool TryParse(ReadOnlySpan roSpan, [NotNullWhen(true)] out DateAn return TryParseMonthDayWithoutYear(roSpan, ref dateAndOrTime); } - return TryParseInternal(roSpan, ref dateAndOrTime); + if(TryParseInternal(roSpan, ref dateAndOrTime)) + { + // yyyy + if(roSpan.Length == 4) + { + dateAndOrTime.HasDay = false; + dateAndOrTime.HasMonth = false; + } + else if (roSpan.Length == 7) + { + // yyyy-MM + dateAndOrTime.HasDay = false; + } + + return true; + } + + return false; ////////////////////////////////////////////////////////////////////////////////////// @@ -107,7 +124,13 @@ bool TryParseDateNoReduc(ReadOnlySpan roSpan, ref DateAndOrTime? dateAndOr Span slice = span.Slice(firstLeapYearJanuary.Length); roSpan.CopyTo(slice); - return TryParseInternal(span, ref dateAndOrTime); + if(TryParseInternal(span, ref dateAndOrTime)) + { + dateAndOrTime.HasMonth = false; + return true; + } + + return false; } bool TryParseMonthWithoutYear(ReadOnlySpan roSpan, ref DateAndOrTime? dateAndOrTime) @@ -131,6 +154,7 @@ bool TryParseMonthWithoutYear(ReadOnlySpan roSpan, ref DateAndOrTime? date out DateOnly dateOnly)) { dateAndOrTime = dateOnly; + dateAndOrTime.HasDay = false; return true; } @@ -202,7 +226,9 @@ private bool TryParseInternal(ReadOnlySpan span, [NotNullWhen(true)] ref D internal static void AppendDateTo(StringBuilder builder, DateOnly dt, - VCdVersion version) + VCdVersion version, + bool hasMonth, + bool hasDay) { switch (version) { @@ -210,35 +236,69 @@ internal static void AppendDateTo(StringBuilder builder, case VCdVersion.V3_0: { _ = dt.HasYear() - ? builder.AppendFormat(CultureInfo.InvariantCulture, - "{0:0000}-{1:00}-{2:00}", - dt.Year, dt.Month, dt.Day) - : builder.AppendFormat(CultureInfo.InvariantCulture, - "--{0:00}-{1:00}", - dt.Month, dt.Day); + ? hasDay + ? builder.AppendFormat(CultureInfo.InvariantCulture, + "{0:0000}-{1:00}-{2:00}", + dt.Year, dt.Month, dt.Day) + : hasMonth + ? builder.AppendFormat(CultureInfo.InvariantCulture, + "{0:0000}-{1:00}", + dt.Year, dt.Month) + : builder.AppendFormat(CultureInfo.InvariantCulture, + "{0:0000}", + dt.Year) + : hasMonth + ? hasDay + ? builder.AppendFormat(CultureInfo.InvariantCulture, + "--{0:00}-{1:00}", + dt.Month, dt.Day) + : builder.AppendFormat(CultureInfo.InvariantCulture, + "--{0:00}", + dt.Month) + : builder.AppendFormat(CultureInfo.InvariantCulture, + "---{0:00}", + dt.Day); break; } default: // vCard 4.0 { _ = dt.HasYear() - ? builder.AppendFormat(CultureInfo.InvariantCulture, - "{0:0000}{1:00}{2:00}", - dt.Year, dt.Month, dt.Day) - : builder.AppendFormat(CultureInfo.InvariantCulture, - "--{0:00}{1:00}", - dt.Month, dt.Day); + ? hasDay + ? builder.AppendFormat(CultureInfo.InvariantCulture, + "{0:0000}{1:00}{2:00}", + dt.Year, dt.Month, dt.Day) + : hasMonth + ? builder.AppendFormat(CultureInfo.InvariantCulture, + "{0:0000}-{1:00}", + dt.Year, dt.Month) + : builder.AppendFormat(CultureInfo.InvariantCulture, + "{0:0000}", + dt.Year) + : hasMonth + ? hasDay + ? builder.AppendFormat(CultureInfo.InvariantCulture, + "--{0:00}{1:00}", + dt.Month, dt.Day) + : builder.AppendFormat(CultureInfo.InvariantCulture, + "--{0:00}", + dt.Month) + : builder.AppendFormat(CultureInfo.InvariantCulture, + "---{0:00}", + dt.Day); break; } }//switch } internal static void AppendDateTimeOffsetTo(StringBuilder builder, - DateTimeOffset dt, - VCdVersion version) + DateTimeOffset dt, + VCdVersion version, + bool hasMonth, + bool hasDay) { if (HasDate(dt)) { - AppendDateTo(builder, DateOnly.FromDateTime(dt.Date), version); + AppendDateTo(builder, DateOnly.FromDateTime(dt.Date), version, hasMonth, hasDay); } if (HasTime(dt)) diff --git a/src/FolkerKinzel.VCards/Intls/Converters/TimeConverter.cs b/src/FolkerKinzel.VCards/Intls/Converters/TimeConverter.cs index 192bdbc73..0df8e3ec9 100644 --- a/src/FolkerKinzel.VCards/Intls/Converters/TimeConverter.cs +++ b/src/FolkerKinzel.VCards/Intls/Converters/TimeConverter.cs @@ -75,6 +75,8 @@ internal bool TryParse(ReadOnlySpan s, [NotNullWhen(true)] out DateAndOrTi { dtOffset = new DateTimeOffset(2, 1, 1, dtOffset.Hour, dtOffset.Minute, dtOffset.Second, dtOffset.Offset); dateAndOrTime = dtOffset; + dateAndOrTime.HasDay = false; + dateAndOrTime.HasMonth = false; return true; } } @@ -87,6 +89,8 @@ internal bool TryParse(ReadOnlySpan s, [NotNullWhen(true)] out DateAndOrTi out TimeOnly timeOnly)) { dateAndOrTime = timeOnly; + dateAndOrTime.HasDay = false; + dateAndOrTime.HasMonth = false; return true; } } @@ -132,6 +136,7 @@ internal static void AppendTimeTo(StringBuilder builder, DateTimeOffset dt, VCdV } else { + // The '-' sign is added by the TimeSpan.ToString() method. string sign = utcOffset < TimeSpan.Zero ? "" : "+"; switch (version) diff --git a/src/FolkerKinzel.VCards/Models/DateAndOrTime.cs b/src/FolkerKinzel.VCards/Models/DateAndOrTime.cs index f4a9e1c0e..dd89f6464 100644 --- a/src/FolkerKinzel.VCards/Models/DateAndOrTime.cs +++ b/src/FolkerKinzel.VCards/Models/DateAndOrTime.cs @@ -1,3 +1,4 @@ +using System; using System.ComponentModel; using System.Globalization; using FolkerKinzel.VCards.Enums; @@ -25,48 +26,134 @@ public sealed class DateAndOrTime : IEquatable public object Value => throw new NotImplementedException(); #pragma warning restore CS1591 // Missing XML comment for publicly visible type or member + [Obsolete("Use Create(DateOnly, bool, bool, bool) instead.", true)] + [EditorBrowsable(EditorBrowsableState.Never)] + [ExcludeFromCodeCoverage] +#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member + public static DateAndOrTime Create(int month, int day) +#pragma warning restore CS1591 // Missing XML comment for publicly visible type or member + => throw new NotImplementedException(); + #endregion - private DateAndOrTime(DateOnly value) => DateOnly = value; + private DateAndOrTime(DateOnly value, bool ignoreMonth, bool ignoreDay) + { + if (ignoreMonth) + { + HasMonth = false; + + if (value.Month != 1) + { + value = new DateOnly(value.Year, 1, value.Day); + } + } + + if (ignoreDay) + { + HasDay = false; + + if (value.Day != 1) + { + value = new DateOnly(value.Year, value.Month, 1); + } + } + + DateOnly = value; + } + + private DateAndOrTime(DateTimeOffset value, bool ignoreMonth, bool ignoreDay) + { + if(ignoreMonth) + { + HasMonth = false; - private DateAndOrTime(DateTimeOffset value) => DateTimeOffset = value; + if (value.Month != 1) + { + value = new DateTimeOffset(value.Year, 1, value.Day, value.Hour, value.Minute, value.Second, value.Offset); + } + } - private DateAndOrTime(TimeOnly value) => TimeOnly = value; + if (ignoreDay) + { + HasDay = false; - private DateAndOrTime(string value) => String = value; + if (value.Day != 1) + { + value = new DateTimeOffset(value.Year, value.Month, 1, value.Hour, value.Minute, value.Second, value.Offset); + } + } + + DateTimeOffset = value; + } + + private DateAndOrTime(TimeOnly value) + { + TimeOnly = value; + HasMonth = false; + HasDay = false; + } + + private DateAndOrTime(string value) + { + String = value; + HasMonth = false; + HasDay = false; + } /// /// Creates a new instance from a /// value. /// /// The value. - /// + /// Pass true to ignore the part, otherwise false. + /// Pass true to ignore the part, otherwise false. + /// Pass true to ignore the part, otherwise false. /// The newly created instance. - public static DateAndOrTime Create(DateOnly value) - => value.HasYear() ? new(value) - : new(new DateOnly(DateAndOrTimeConverter.FIRST_LEAP_YEAR, value.Month, value.Day)); + public static DateAndOrTime Create(DateOnly value, bool ignoreYear = false, bool ignoreMonth = false, bool ignoreDay = false) + { + return !ignoreYear && value.HasYear() + ? new(value, ignoreMonth, ignoreDay) + : ignoreMonth && ignoreDay + ? Empty + : value.Year == DateAndOrTimeConverter.FIRST_LEAP_YEAR + ? new(value, ignoreMonth, ignoreDay) + : new(new DateOnly(DateAndOrTimeConverter.FIRST_LEAP_YEAR, value.Month, value.Day), ignoreMonth, ignoreDay); + } /// /// Creates a new instance from a /// value. /// /// The value. - /// + /// Pass true to ignore the part, otherwise false. + /// Pass true to ignore the part, otherwise false. + /// Pass true to ignore the part, otherwise false. /// The newly created instance. /// /// If has neither a date nor a time, the method returns /// an empty instance. /// - public static DateAndOrTime Create(DateTimeOffset value) - => !DateAndOrTimeConverter.HasDate(value) - ? !DateAndOrTimeConverter.HasTime(value) ? Empty : new(new DateTimeOffset(2, 1, 1, value.Hour, value.Minute, value.Second, value.Offset)) - : DateAndOrTimeConverter.HasYear(value) ? new(value) : new(new DateTimeOffset(DateAndOrTimeConverter.FIRST_LEAP_YEAR, - value.Month, - value.Day, - value.Hour, - value.Minute, - value.Second, - value.Offset)); + public static DateAndOrTime Create(DateTimeOffset value, + bool ignoreYear = false, + bool ignoreMonth = false, + bool ignoreDay = false) + => (ignoreYear && ignoreMonth && ignoreDay) || !DateAndOrTimeConverter.HasDate(value) + ? DateAndOrTimeConverter.HasTime(value) + ? new(new DateTimeOffset(2, 1, 1, value.Hour, value.Minute, value.Second, value.Offset), true, true) + : Empty + : ignoreYear || !DateAndOrTimeConverter.HasYear(value) + ? (ignoreMonth && ignoreDay) + ? Empty + : value.Year == DateAndOrTimeConverter.FIRST_LEAP_YEAR + ? new(value, ignoreMonth, ignoreDay) + : new(new DateTimeOffset(DateAndOrTimeConverter.FIRST_LEAP_YEAR, + value.Month, + value.Day, + value.Hour, + value.Minute, + value.Second, + value.Offset), ignoreMonth, ignoreDay) + : new(value, ignoreMonth, ignoreDay); /// /// Creates a new instance from a @@ -89,30 +176,8 @@ public static DateAndOrTime Create(DateTimeOffset value) /// var result = DateAndOrTime.Create("after midnight"); /// /// - public static DateAndOrTime Create(string? value) => string.IsNullOrWhiteSpace(value) ? Empty : new(value); - - /// - /// Creates a new instance from a recurring date in the - /// Gregorian calendar. - /// - /// The month (1 bis 12). - /// The day (1 through the number of days in - - /// a leap year may be assumed.) - /// - /// The newly created instance. - /// - /// This overload is intended to be used for recurring dates, like, e.g., birthdays, or - /// if the year is unknown. - /// - /// - /// - /// is less than 1 or greater than 12. - /// -or- - /// is less than 1 or greater than the number of days - /// that has in a leap year. - /// - public static DateAndOrTime Create(int month, int day) - => new(new DateOnly(DateAndOrTimeConverter.FIRST_LEAP_YEAR, month, day)); + public static DateAndOrTime Create(string? value) + => string.IsNullOrWhiteSpace(value) ? Empty : new(value); /// /// Defines an implicit conversion of a value to a @@ -164,6 +229,40 @@ public static DateAndOrTime Create(int month, int day) /// public bool IsEmpty => ReferenceEquals(this, Empty); + /// + /// Gets a value indicating whether the encapsulated data contains information about the year. + /// + /// true if the instance contains information about the year, otherwise false. + /// + /// The vCard specification allows to omit the year in a date but the .NET data types doesn't. This property + /// gets the information whether the year component of an encapsulated or + /// value should be treated as irrelevant or not. + /// + public bool HasYear => (DateOnly.HasValue && DateOnly.Value.HasYear()) || + (DateTimeOffset.HasValue && DateAndOrTimeConverter.HasYear(DateTimeOffset.Value)); + + /// + /// Gets a value indicating whether the encapsulated data contains information about the month. + /// + /// true if the instance contains information about the month, otherwise false. + /// + /// The vCard specification allows to omit the month in a date but the .NET data types doesn't. This property + /// gets the information whether the month component of an encapsulated or + /// value should be treated as irrelevant or not. + /// + public bool HasMonth { get; internal set; } = true; + + /// + /// Gets a value indicating whether the encapsulated data contains information about the day. + /// + /// true if the instance contains information about the day, otherwise false. + /// + /// The vCard specification allows to omit the day in a date but the .NET data types doesn't. This property + /// gets the information whether the day component of an encapsulated or + /// value should be treated as irrelevant or not. + /// + public bool HasDay { get; internal set; } = true; + /// /// Gets the encapsulated value, /// or null, if the encapsulated value has a different . @@ -204,7 +303,7 @@ public static DateAndOrTime Create(int month, int day) /// or null, if the encapsulated value has a different . /// public string? String { get; } - + /// /// Tries to convert the encapsulated data to a value. /// @@ -237,7 +336,7 @@ public bool TryAsDateOnly(out DateOnly value) static str => System.DateOnly.TryParse(str, CultureInfo.CurrentCulture, DateTimeStyles.AllowWhiteSpaces, - out DateOnly dOnly) + out DateOnly dOnly) ? (dOnly, true) : (default, false) ); @@ -276,7 +375,7 @@ public bool TryAsDateTimeOffset(out DateTimeOffset value) static str => System.DateTimeOffset.TryParse(str, CultureInfo.CurrentCulture, DateTimeStyles.AllowWhiteSpaces, - out DateTimeOffset dto) + out DateTimeOffset dto) ? (dto, true) : (default, false) ); @@ -328,26 +427,33 @@ public bool TryAsTimeOnly(out TimeOnly value) /// public string AsString(IFormatProvider? formatProvider = null) { - return Convert + return Convert<(IFormatProvider FormatProvider, DateAndOrTime DateAndOrTime), string> ( - formatProvider ?? CultureInfo.CurrentCulture, - - static (date, fp) => date.HasYear() ? date.ToString(fp) - : date.ToString(fp) - .Replace(date.Year.ToString("0000"), "", StringComparison.Ordinal) - .Trim('/'), - static (dtOffset, fp) => DateTimeOffsetToString(dtOffset), - static (time, fp) => time.ToString(fp), - static (str, fp) => str + (formatProvider ?? CultureInfo.CurrentCulture, this), + + static (date, tuple) => DateOnlyToString(date, tuple.FormatProvider, tuple.DateAndOrTime), + static (dtOffset, tuple) => DateTimeOffsetToString(dtOffset, tuple.DateAndOrTime), + static (time, tuple) => time.ToString(tuple.FormatProvider), + static (str, tuple) => str ); - static string DateTimeOffsetToString(DateTimeOffset dtOffset) + static string DateOnlyToString(DateOnly date, IFormatProvider formatProvider, DateAndOrTime daot) { - if (dtOffset.HasDate()) { return dtOffset.ToString("O"); } + if (date.HasYear() && daot.HasMonth && daot.HasDay) + { + return date.ToString(formatProvider); + } + + var sb = new StringBuilder(); + DateAndOrTimeConverter.AppendDateTo(sb, date, VCdVersion.V3_0, daot.HasMonth, daot.HasDay); + return sb.ToString(); + } + static string DateTimeOffsetToString(DateTimeOffset dtOffset, DateAndOrTime daot) + { var sb = new StringBuilder(); - DateAndOrTimeConverter.AppendDateTimeOffsetTo(sb, dtOffset, VCdVersion.V3_0); + DateAndOrTimeConverter.AppendDateTimeOffsetTo(sb, dtOffset, VCdVersion.V3_0, daot.HasMonth, daot.HasDay); return sb.ToString(); } } @@ -424,7 +530,7 @@ public void Switch(TArg arg, } else { - stringAction?.Invoke(String!, arg); + stringAction?.Invoke(String!, arg); } } @@ -518,13 +624,16 @@ public override string ToString() public bool Equals(DateAndOrTime? other) => other is not null && (ReferenceEquals(this, other) - || (DateOnly.HasValue - ? DateOnly == other.DateOnly - : DateTimeOffset.HasValue - ? DateTimeOffset == other.DateTimeOffset - : TimeOnly.HasValue - ? TimeOnly == other.TimeOnly - : String!.Equals(other.String))); + || (HasYear == other.HasYear && HasMonth == other.HasMonth && HasDay == other.HasDay + && (DateOnly.HasValue + ? DateOnly == other.DateOnly + : DateTimeOffset.HasValue + ? DateTimeOffset == other.DateTimeOffset + : TimeOnly.HasValue + ? TimeOnly == other.TimeOnly + : String!.Equals(other.String, StringComparison.Ordinal)) + ) + ); /// /// Equality is given if is a diff --git a/src/FolkerKinzel.VCards/Models/Properties/DateAndOrTimeProperty.cs b/src/FolkerKinzel.VCards/Models/Properties/DateAndOrTimeProperty.cs index 4d748bdf2..3917b54e8 100644 --- a/src/FolkerKinzel.VCards/Models/Properties/DateAndOrTimeProperty.cs +++ b/src/FolkerKinzel.VCards/Models/Properties/DateAndOrTimeProperty.cs @@ -202,14 +202,18 @@ internal override void AppendValue(VcfSerializer serializer) if (Value.DateOnly.HasValue) { DateAndOrTimeConverter.AppendDateTo(serializer.Builder, - Value.DateOnly.Value, - serializer.Version); + Value.DateOnly.Value, + serializer.Version, + Value.HasMonth, + Value.HasDay); } else if (Value.DateTimeOffset.HasValue) { DateAndOrTimeConverter.AppendDateTimeOffsetTo(serializer.Builder, - Value.DateTimeOffset.Value, - serializer.Version); + Value.DateTimeOffset.Value, + serializer.Version, + Value.HasMonth, + Value.HasDay); } else if (Value.TimeOnly.HasValue) {