diff --git a/DateTimeNano.Tests/DateTimeNanoTests.cs b/DateTimeNano.Tests/DateTimeNanoTests.cs index 2f4f73d..9511d42 100644 --- a/DateTimeNano.Tests/DateTimeNanoTests.cs +++ b/DateTimeNano.Tests/DateTimeNanoTests.cs @@ -448,6 +448,57 @@ public void Now_ShouldReturnRecentUtcTime() Assert.That(now.ToDateTimeUtc(), Is.LessThanOrEqualTo(after)); } + // ── IFormattable ────────────────────────────────────────────────────────── + + [Test] + public void IFormattable_NullFormat_ShouldMatchToString() + { + var nano = new Seerstone.DateTimeNano(1_739_219_232_123_456_789UL); + Assert.That(nano.ToString(null, null), Is.EqualTo(nano.ToString())); + } + + [TestCase("G")] + [TestCase("g")] + [TestCase("O")] + [TestCase("o")] + public void IFormattable_RoundTripFormats_ShouldMatchToString(string format) + { + var nano = new Seerstone.DateTimeNano(1_739_219_232_123_456_789UL); + Assert.That(nano.ToString(format, null), Is.EqualTo(nano.ToString())); + } + + [Test] + public void IFormattable_DateOnlyFormat_ShouldReturnDatePart() + { + var nano = new Seerstone.DateTimeNano(1_739_219_232_123_456_789UL); // 2025-02-10 + Assert.That(nano.ToString("yyyy-MM-dd", null), Is.EqualTo("2025-02-10")); + } + + [Test] + public void IFormattable_WorksViaStringFormat() + { + var nano = new Seerstone.DateTimeNano(1_739_219_232_123_456_789UL); + var result = string.Format("{0:yyyy-MM-dd}", nano); + Assert.That(result, Is.EqualTo("2025-02-10")); + } + + // ── IParsable ──────────────────────────────────────────────── + + [Test] + public void IParsable_Parse_ShouldReturnCorrectValue() + { + var result = Seerstone.DateTimeNano.Parse("2025-02-10 20:27:12.123456789", null); + Assert.That(result.ToString(), Is.EqualTo("2025-02-10 20:27:12.123456789")); + } + + [Test] + public void IParsable_TryParse_ValidString_ShouldReturnTrue() + { + var success = Seerstone.DateTimeNano.TryParse("2025-02-10 20:27:12.123456789", null, out var result); + Assert.That(success, Is.True); + Assert.That(result.ToString(), Is.EqualTo("2025-02-10 20:27:12.123456789")); + } + // ── DateTimeOffset interop ──────────────────────────────────────────────── [Test] @@ -622,6 +673,35 @@ public void TryParse_WithTSeparator_ShouldSucceed() Assert.That(result.ToString(), Is.EqualTo("2025-02-10 20:27:12.123456789")); } + [Test] + public void IParsable_TryParse_InvalidString_ShouldReturnFalse() + { + var success = Seerstone.DateTimeNano.TryParse("not-a-date", null, out var result); + Assert.That(success, Is.False); + Assert.That(result, Is.EqualTo(default(Seerstone.DateTimeNano))); + } + + [Test] + public void IParsable_WorksViaGenericConstraint() + { + static T ParseVia(string s) where T : IParsable => T.Parse(s, null); + var result = ParseVia("2025-02-10 20:27:12.123456789"); + Assert.That(result.ToString(), Is.EqualTo("2025-02-10 20:27:12.123456789")); + } + + // ── ISpanParsable ─────────────────────────────────────────── + + [Test] + public void ISpanParsable_TryParse_WorksViaGenericConstraint() + { + static bool SpanTryParse(ReadOnlySpan s, out T result) where T : ISpanParsable + => T.TryParse(s, null, out result); + + var success = SpanTryParse("2025-02-10 20:27:12.123456789".AsSpan(), out var result); + Assert.That(success, Is.True); + Assert.That(result.ToString(), Is.EqualTo("2025-02-10 20:27:12.123456789")); + } + [Test] public void Parse_WithTSeparator_ShouldSucceed() { diff --git a/DateTimeNano/DateTimeNano.cs b/DateTimeNano/DateTimeNano.cs index 14411f1..ee3d492 100644 --- a/DateTimeNano/DateTimeNano.cs +++ b/DateTimeNano/DateTimeNano.cs @@ -6,6 +6,7 @@ using ProtoBuf; using System; +using System.Diagnostics.CodeAnalysis; using System.Text.RegularExpressions; namespace Seerstone @@ -19,7 +20,8 @@ namespace Seerstone /// DataBento timestamps are mainly in nanoseconds since epoch. /// [ProtoContract] - public partial struct DateTimeNano : IEquatable, IComparable + public partial struct DateTimeNano : IEquatable, IComparable, + IFormattable, IParsable, ISpanParsable { /// /// Unix Epoch Date/Time (1970-01-01 00:00:00 UTC). @@ -443,6 +445,49 @@ public override string ToString() /// Implicitly converts a to a . public static implicit operator DateTimeNano(DateTime d) => new DateTimeNano(d); + /// + /// Returns a string representation using the specified format. + /// + /// + /// Format string. Use , "G", "g", "O", or "o" + /// for full nanosecond-precision output ("yyyy-MM-dd HH:mm:ss.fffffffff"). + /// Any other standard or custom format string produces output at + /// precision (sub-microsecond nanoseconds are not included). + /// + /// Culture or format provider; ignored for full-precision formats. + public string ToString(string? format, IFormatProvider? formatProvider) + { + if (string.IsNullOrEmpty(format) || format == "G" || format == "g" || + format == "O" || format == "o") + return ToString(); + return DateTime.ToString(format, formatProvider); + } + + /// + /// Parses a from a string, ignoring . + /// + public static DateTimeNano Parse(string s, IFormatProvider? provider) => Parse(s); + + /// + /// Attempts to parse a from a string, ignoring . + /// + public static bool TryParse( + [NotNullWhen(true)] string? s, + IFormatProvider? provider, + [MaybeNullWhen(false)] out DateTimeNano result) + => TryParse(s, out result); + + /// + static DateTimeNano ISpanParsable.Parse(ReadOnlySpan s, IFormatProvider? provider) + => Parse(s.ToString()); + + /// + static bool ISpanParsable.TryParse( + ReadOnlySpan s, + IFormatProvider? provider, + out DateTimeNano result) + => TryParse(s.IsEmpty ? null : s.ToString(), out result); + /// /// Create a from a . /// The offset is converted to UTC before the nanosecond value is computed,