Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
80 changes: 80 additions & 0 deletions DateTimeNano.Tests/DateTimeNanoTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -448,6 +448,57 @@
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<DateTimeNano> ────────────────────────────────────────────────

[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]
Expand Down Expand Up @@ -622,6 +673,35 @@
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<T>(string s) where T : IParsable<T> => T.Parse(s, null);
var result = ParseVia<Seerstone.DateTimeNano>("2025-02-10 20:27:12.123456789");
Assert.That(result.ToString(), Is.EqualTo("2025-02-10 20:27:12.123456789"));
}

// ── ISpanParsable<DateTimeNano> ───────────────────────────────────────────

[Test]
public void ISpanParsable_TryParse_WorksViaGenericConstraint()
{
static bool SpanTryParse<T>(ReadOnlySpan<char> s, out T result) where T : ISpanParsable<T>
=> T.TryParse(s, null, out result);

Check warning on line 698 in DateTimeNano.Tests/DateTimeNanoTests.cs

View workflow job for this annotation

GitHub Actions / build-and-test (net10.0, 10.0.x)

Possible null reference assignment.

Check warning on line 698 in DateTimeNano.Tests/DateTimeNanoTests.cs

View workflow job for this annotation

GitHub Actions / build-and-test (net10.0, 10.0.x)

Possible null reference assignment.

Check warning on line 698 in DateTimeNano.Tests/DateTimeNanoTests.cs

View workflow job for this annotation

GitHub Actions / build-and-test (net8.0, 8.0.x)

Possible null reference assignment.

Check warning on line 698 in DateTimeNano.Tests/DateTimeNanoTests.cs

View workflow job for this annotation

GitHub Actions / build-and-test (net8.0, 8.0.x)

Possible null reference assignment.

Check warning on line 698 in DateTimeNano.Tests/DateTimeNanoTests.cs

View workflow job for this annotation

GitHub Actions / build-and-test (net9.0, 9.0.x)

Possible null reference assignment.

Check warning on line 698 in DateTimeNano.Tests/DateTimeNanoTests.cs

View workflow job for this annotation

GitHub Actions / build-and-test (net9.0, 9.0.x)

Possible null reference assignment.

var success = SpanTryParse<Seerstone.DateTimeNano>("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()
{
Expand Down
47 changes: 46 additions & 1 deletion DateTimeNano/DateTimeNano.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

using ProtoBuf;
using System;
using System.Diagnostics.CodeAnalysis;
using System.Text.RegularExpressions;

namespace Seerstone
Expand All @@ -19,7 +20,8 @@ namespace Seerstone
/// DataBento timestamps are mainly in nanoseconds since epoch.
/// </summary>
[ProtoContract]
public partial struct DateTimeNano : IEquatable<DateTimeNano>, IComparable<DateTimeNano>
public partial struct DateTimeNano : IEquatable<DateTimeNano>, IComparable<DateTimeNano>,
IFormattable, IParsable<DateTimeNano>, ISpanParsable<DateTimeNano>
{
/// <summary>
/// Unix Epoch Date/Time (1970-01-01 00:00:00 UTC).
Expand Down Expand Up @@ -443,6 +445,49 @@ public override string ToString()
/// <summary>Implicitly converts a <see cref="System.DateTime"/> to a <see cref="DateTimeNano"/>.</summary>
public static implicit operator DateTimeNano(DateTime d) => new DateTimeNano(d);

/// <summary>
/// Returns a string representation using the specified format.
/// </summary>
/// <param name="format">
/// Format string. Use <see langword="null"/>, <c>"G"</c>, <c>"g"</c>, <c>"O"</c>, or <c>"o"</c>
/// for full nanosecond-precision output (<c>"yyyy-MM-dd HH:mm:ss.fffffffff"</c>).
/// Any other standard or custom <see cref="DateTime"/> format string produces output at
/// <see cref="DateTime"/> precision (sub-microsecond nanoseconds are not included).
/// </param>
/// <param name="formatProvider">Culture or format provider; ignored for full-precision formats.</param>
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);
}

/// <summary>
/// Parses a <see cref="DateTimeNano"/> from a string, ignoring <paramref name="provider"/>.
/// </summary>
public static DateTimeNano Parse(string s, IFormatProvider? provider) => Parse(s);

/// <summary>
/// Attempts to parse a <see cref="DateTimeNano"/> from a string, ignoring <paramref name="provider"/>.
/// </summary>
public static bool TryParse(
[NotNullWhen(true)] string? s,
IFormatProvider? provider,
[MaybeNullWhen(false)] out DateTimeNano result)
=> TryParse(s, out result);

/// <inheritdoc/>
static DateTimeNano ISpanParsable<DateTimeNano>.Parse(ReadOnlySpan<char> s, IFormatProvider? provider)
=> Parse(s.ToString());
Comment on lines +481 to +482

/// <inheritdoc/>
static bool ISpanParsable<DateTimeNano>.TryParse(
ReadOnlySpan<char> s,
IFormatProvider? provider,
out DateTimeNano result)
=> TryParse(s.IsEmpty ? null : s.ToString(), out result);

/// <summary>
/// Create a <see cref="DateTimeNano"/> from a <see cref="System.DateTimeOffset"/>.
/// The offset is converted to UTC before the nanosecond value is computed,
Expand Down
Loading