# DateTimeOffset

> An instant *plus the offset it was recorded at*. Use it when the local time something happened is part of the fact — a warehouse `TIMESTAMP_TZ`, a receipt, an event log that spans zones. When you only need *when*, use `DateTime`: every one of those is already a UTC instant.

<!-- id: types-datetimeoffset · area: types · stability: stable · html: https://osysharp.com/reference/types/datetimeoffset/ -->

## Summary        {#summary}

A `DateTimeOffset` is an instant **and** the offset it was recorded at. That second half is the whole reason to
reach for it: a [`DateTime`](https://osysharp.com/reference/types/datetime/) here is always a UTC instant, so an invoice stamped `09:00+05:30`
and one stamped `03:30Z` are the same `DateTime` — and only the first tells you it was morning in India.

```osy syntax
DateTimeOffset recordedAt = order.PlacedAt;   // 2026-09-14T14:30:00+05:30

recordedAt.Hour        // 14 — the clock on the wall where it happened
recordedAt.Offset      // +05:30, as a TimeSpan
recordedAt.UtcDateTime // 09:00 — the same moment, as an ordinary DateTime
```

## Signature      {#signature}

```osy syntax
DateTimeOffset At;                 // on an entity or a class
DateTimeOffset Stamp(DateTimeOffset at) { … }

DateTimeOffset.UtcNow              // now, at offset zero
DateTimeOffset.FromUnixTimeSeconds(s)   // an instant sent as seconds since 1970, at offset zero
DateTimeOffset.Parse("2026-09-27T12:30:00+02:00")   // ISO text; text with no offset is read as UTC
new DateTimeOffset(someDateTime)        // the same instant, at offset zero
at.ToUniversalTime()                    // the same instant, at offset zero (a DateTime answers itself)
at.ToUnixTimeMilliseconds()             // …and back, as milliseconds
at.ToOffset(TimeSpan.FromHours(1))      // the same instant, read at another offset
```

## Description    {#description}

### The parts are the LOCAL reading   {#parts}

`.Year`, `.Month`, `.Day`, `.Hour`, `.Minute`, `.Second` and `.DayOfWeek` all answer about the wall clock the value
carries — the time where it was recorded. That is the information the offset exists to preserve, so reading them as
UTC would quietly be about a different moment of the day.

Two members have no counterpart on a `DateTime`:

| | |
|---|---|
| `.Offset` | the offset itself, as a `TimeSpan` ([TimeSpan (durations)](https://osysharp.com/reference/types/timespan/)) |
| `.UtcDateTime` | the same instant as a plain `DateTime`, offset discarded |
| `.DateTime` | the *local* reading as a plain `DateTime`, offset discarded |

### Comparison is by the instant   {#comparison}

`<`, `>` and `==` compare the **moment**, never the local reading. Two values recorded thirteen and a half hours
apart on the clock can be equal, because they name the same instant:

```osy syntax
// 14:30 in India and 01:00 in Los Angeles are both 09:00Z.
indiaStamp == losAngelesStamp   // true
```

Sorting a column of them orders by instant too, which is almost always what a timeline wants.

### Converting   {#converting}

A `DateTime` **widens to** a `DateTimeOffset` on its own, because nothing is lost — it lands at offset zero and
says so. There is no conversion back, deliberately: dropping an offset is a choice about *which* instant you meant,
so you name it.

```osy syntax
DateTimeOffset stamped = someDateTime;   // ✓ implicit — lands at +00:00
DateTime plain = stamped.UtcDateTime;    // ✓ the instant
DateTime local = stamped.DateTime;       // ✓ the wall clock it was recorded at
```

`new DateTimeOffset(dateTime)` says the widening out loud, and `ToUniversalTime()` moves a `DateTimeOffset` to offset
zero (on a `DateTime`, which is already a UTC instant here, it answers the value itself). `DateTimeOffset.Parse` reads
ISO-8601 text and keeps the offset the text carries; text with NO offset is read as UTC — never as the server's local
zone, which would make the answer depend on where it runs.

### Arithmetic, and mixing with a `DateTime`   {#mixing}

`DateTime` and `DateTimeOffset` are one family, as in C#. Wherever one of each meets an operator, the `DateTime`
widens (to offset zero) and the `DateTimeOffset` operator applies — so there is nothing to convert by hand:

| written | answers |
|---|---|
| `dto - dto`, `dateTime - dto`, `dto - dateTime` | a `TimeSpan` — the time between the two instants |
| `dto + span`, `dto - span` | a `DateTimeOffset`, **at the same offset** |
| `dateTime < dto`, `<=`, `>`, `>=` | ordered by the instant |
| `dateTime == dto`, `!=` | equal when they name the same instant, whatever the offsets |

```osy syntax
DateTime due = order.DueAt;                 // a UTC instant
DateTimeOffset seen = order.SeenAt;         // 14:30 +05:30
TimeSpan early = due - seen;                // ✓ widened, then subtracted
DateTimeOffset reminder = seen + TimeSpan.FromHours(1);   // 15:30 +05:30 — the offset is kept
bool late = seen > due;                     // ✓ by the instant
```

The same rules hold in a server function, in a browser action, and in a query that runs in the database —
`Reading.Where(r => r.At > cutoff)` with `cutoff` a `DateTime`, or `r.At - r.Logged > TimeSpan.FromHours(1)`, is
answered there exactly as it would be in memory.

**Choosing between them meets at `DateTimeOffset` too.** `flag ? dateTime : dto`, `maybeDateTime ?? dto`, and a
switch whose arms give one of each are all a `DateTimeOffset`, with the `DateTime` side converted (to offset zero, a
`DateTime?` that is null staying null) — as C#'s best common type is. The same conversion happens wherever a
`DateTime` is written into a `DateTimeOffset` slot: a local, an assignment, an argument, a `return`, a member. In a
query's select list the result carries the offset of whichever side the row took.

What C# does not have, Osy# does not either: there is no `TimeSpan + DateTimeOffset` (the instant goes on the left),
and two instants cannot be added.

### Unix time, and reading at another offset   {#unix-time}

An API that sends an instant as a count of seconds or milliseconds since 1970 is read the way C# reads it, with
`DateTimeOffset.FromUnixTimeSeconds` and `DateTimeOffset.FromUnixTimeMilliseconds`, and written back with
`.ToUnixTimeSeconds()` and `.ToUnixTimeMilliseconds()`. `ToOffset` reads the same instant at another offset, and
adding to a `DateTimeOffset` (`AddDays`, `AddHours`, `AddTicks`, …) keeps its offset:

```osy title="a wire timestamp in, another offset out, a wire timestamp back" syntax
var shipped = DateTimeOffset.FromUnixTimeSeconds(1616173619);   // 2021-03-19 17:06:59 +00:00
var inParis = shipped.ToOffset(TimeSpan.FromHours(1));          // 2021-03-19 18:06:59 +01:00
var dueBack = inParis.AddDays(14);                              // still +01:00
long sentAt  = dueBack.ToUnixTimeMilliseconds();                // milliseconds since 1970
long seconds = dueBack.ToUnixTimeSeconds();                     // …or seconds, the coarser wire form
```

⚠ **Both directions are `long`, and only a `DateTimeOffset` has them.** `.ToUnixTimeSeconds()` and
`.ToUnixTimeMilliseconds()` are declared on this type alone, because the conversion needs the offset to know which
instant it is naming — a bare `DateTime` does not carry one. Convert first if that is what you hold.

**The epoch itself** is a value you can name rather than a literal you retype: `DateTimeOffset.UnixEpoch` is
1970-01-01 at offset zero, and `DateTime.UnixEpoch` is the same moment as a plain instant, for arithmetic that
stays in `DateTime`:

```osy title="naming the epoch instead of retyping 1970" syntax
var age    = DateTimeOffset.UtcNow - DateTimeOffset.UnixEpoch;  // a TimeSpan since 1970
var asDate = DateTime.UnixEpoch.AddSeconds(1616173619);         // the same read, in DateTime terms
```

### It is the same value on both sides   {#parity}

A `DateTimeOffset` behaves identically in a server function and in a browser action — same instant, same offset,
same comparison rule. On the wire it travels as ISO-8601 text **with** its offset (`2026-09-14T14:30:00+05:30`),
which is the only textual form that keeps what the type is for.

## Examples       {#examples}

```osy title="reading the local wall clock a value was recorded at" test app=datetimeoffset-reading
// A reading whose local time matters — the same instant, recorded in three places.
entity Reading {
  [Required] string Sensor;
  DateTimeOffset At;
}

string WhenLocally(Reading r) {
  return Convert.ToString(r.At.Hour) + ":" + Convert.ToString(r.At.Minute);
}
```

A `DateTime` and a `DateTimeOffset` in one expression — subtracted, moved and compared, in memory and in a query:

```osy title="mixing the two instant types" test app=datetimeoffset-mixing
entity Delivery {
  [Required] string Parcel;
  DateTime Promised;          // a UTC instant
  DateTimeOffset Handed;      // when, and at what offset, it was handed over
}

TimeSpan Lateness(Delivery d) {
  return d.Handed - d.Promised;                  // DateTimeOffset − DateTime → TimeSpan
}

DateTimeOffset FollowUp(Delivery d) {
  return d.Handed + TimeSpan.FromDays(2);        // keeps the offset it was handed over at
}

List<Delivery> LateOnes() {
  return Delivery.Where(d => d.Handed > d.Promised).ToList();   // compared in the database, by the instant
}

DateTimeOffset Settled(Delivery d, bool handed) {
  return handed ? d.Handed : d.Promised;         // one of each → a DateTimeOffset; Promised lands at +00:00
}
```

## See also       {#see-also}
- [DateTime](https://osysharp.com/reference/types/datetime/) — a UTC instant, and the right choice when *when* is all you need
- [TimeSpan (durations)](https://osysharp.com/reference/types/timespan/) — what `.Offset` answers
- [DateOnly and TimeOnly](https://osysharp.com/reference/types/date-and-time/) — the wider date/time surface
