Development About 2 min read

Timestamp normalisation in C#

Keep event occurrence and receipt times separate while converting both to UTC.

Sources and scopeRead before use

Synthetic fixtures and the stated local tests. No product conformance or equipment compatibility claim.

Runtime execution has not been recorded for this example.

Verification and testing

Run the demo#

Run from the repository root. The demo uses synthetic data and does not contact equipment.

sh
dotnet run --project examples/csharp/Timestamps -- --demo
dotnet run --project examples/csharp/Timestamps -- --self-test

The demo represents the same event in UTC and reports an observed delay of two seconds.

Use and limits#

Use the .NET 10 SDK. The included project file declares its target framework and requires no NuGet packages. Supply --occurred TIMESTAMP --received TIMESTAMP for your own inputs.

Inputs must include seconds and a known UTC offset or Z. The parser rejects local timestamps without offsets, invalid dates and the unknown offset form -00:00. .NET supports offsets within 14 hours of UTC. Fractional seconds up to seven digits are accepted.

The delay is the difference between the supplied clocks. Negative delay can reveal clock disagreement and is not silently clamped. It is not a latency measurement unless those clocks are suitably synchronised.

The project includes an executable test mode. Its runtime status is not executed. Run the test mode with the .NET 10 SDK before relying on the result. The publication workflow also requires that SDK and runs the test.

Source#

Download the source · Project file

csharp
using System.Globalization;
using System.Text.Json;
using System.Text.RegularExpressions;

// Keep occurrence and receipt timestamps separate. Do not infer a time zone.
static DateTimeOffset ParseTimestamp(string value)
{
    const string pattern = @"^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{1,7})?(?:Z|[+-]\d{2}:\d{2})$";
    if (value.Length > 40 || value.EndsWith("-00:00", StringComparison.Ordinal) ||
        !Regex.IsMatch(value, pattern, RegexOptions.CultureInvariant | RegexOptions.NonBacktracking,
            TimeSpan.FromMilliseconds(100)))
        throw new FormatException("Use a timestamp with a known UTC offset or Z");
    string normalised = value.EndsWith('Z') ? value[..^1] + "+00:00" : value;
    string[] formats = ["yyyy-MM-dd'T'HH:mm:sszzz", "yyyy-MM-dd'T'HH:mm:ss.FFFFFFFzzz"];
    if (!DateTimeOffset.TryParseExact(normalised, formats, CultureInfo.InvariantCulture,
        DateTimeStyles.None, out DateTimeOffset result))
        throw new FormatException("Invalid timestamp");
    return result.ToUniversalTime();
}

try
{
    if (args.Length == 1 && args[0] == "--self-test")
    {
        if (ParseTimestamp("2026-09-10T09:00:00+10:00") != ParseTimestamp("2026-09-09T23:00:00Z"))
            throw new InvalidOperationException("UTC conversion failed");
        foreach (string invalid in new[] { "2026-09-10T09:00:00", "2026-09-10T09:00:00-00:00", "2026-02-30T00:00:00Z" })
        {
            bool rejected = false;
            try { ParseTimestamp(invalid); } catch (FormatException) { rejected = true; }
            if (!rejected) throw new InvalidOperationException("Invalid timestamp was accepted");
        }
        Console.WriteLine("Timestamp tests passed");
        return 0;
    }
    bool demo = args.Length == 1 && args[0] == "--demo";
    if (!demo && (args.Length != 4 || args[0] != "--occurred" || args[2] != "--received"))
    {
        Console.Error.WriteLine("Use --demo, --self-test or --occurred TIMESTAMP --received TIMESTAMP");
        return 2;
    }
    DateTimeOffset occurred = ParseTimestamp(demo ? "2026-09-10T09:00:00+10:00" : args[1]);
    DateTimeOffset received = ParseTimestamp(demo ? "2026-09-09T23:00:02Z" : args[3]);
    Console.WriteLine(JsonSerializer.Serialize(new {
        occurred_at = occurred.ToString("O", CultureInfo.InvariantCulture),
        received_at = received.ToString("O", CultureInfo.InvariantCulture),
        observed_delay_seconds = (received - occurred).TotalSeconds
    }));
    return 0;
}
catch (Exception error) when (error is FormatException or InvalidOperationException or RegexMatchTimeoutException)
{
    Console.Error.WriteLine($"Error: {error.Message}");
    return 1;
}

References#

DateTimeOffset parsing

.NET support

All code examples · Code index