# Querying by temporal extent

Both [Timeseries](/docs/datasets/types/timeseries) and [Spatio-temporal](/docs/datasets/types/spatiotemporal) datasets support efficient time-based queries.

## Time interval queries

To query data for a specific time interval, use a `tuple` in the form `(start, end)` as the `temporal_extent` parameter. Both `start` and `end` must be [TimeScalars](#time-scalar-queries), which can be `datetime` objects or strings in ISO 8601 format.

**Python**

```python title="Python"
from tilebox.datasets import Client

client = Client()
sentinel2_msi = client.dataset("open_data.copernicus.sentinel2_msi")
data = sentinel2_msi.query(
  collections=["S2A_S2MSI2A", "S2B_S2MSI2A", "S2C_S2MSI2A"],
  temporal_extent=("2025-05-01", "2025-06-01"),
  show_progress=True,
)

print(f"Queried {data.sizes['time']} data points.")
```

**Go**

```go title="Go"
startDate := time.Date(2025, time.May, 1, 0, 0, 0, 0, time.UTC)
endDate := time.Date(2025, time.June, 1, 0, 0, 0, 0, time.UTC)
interval := query.NewTimeInterval(startDate, endDate)

ctx := context.Background()
client := datasets.NewClient()

dataset, err := client.Datasets.Get(ctx, "open_data.copernicus.sentinel2_msi")
if err != nil {
  log.Fatalf("Failed to get dataset: %v", err)
}

collection, err := client.Collections.Get(ctx, dataset.ID, "S2A_S2MSI2A")
if err != nil {
  log.Fatalf("Failed to get collection: %v", err)
}

collectionB, err := client.Collections.Get(ctx, dataset.ID, "S2B_S2MSI2A")
if err != nil {
  log.Fatalf("Failed to get collection: %v", err)
}

collectionC, err := client.Collections.Get(ctx, dataset.ID, "S2C_S2MSI2A")
if err != nil {
  log.Fatalf("Failed to get collection: %v", err)
}

var datapoints []*v1.Sentinel2Msi
err = client.Datapoints.QueryInto(ctx,
  dataset.ID,
  &datapoints,
  datasets.WithCollectionIDs(collection.ID, collectionB.ID, collectionC.ID),
  datasets.WithTemporalExtent(interval),
)
if err != nil {
  log.Fatalf("Failed to query datapoints: %v", err)
}

log.Printf("Queried %d datapoints", len(datapoints))
```

```plaintext title="Output"
Queried 481079 data points.
```

The `show_progress` parameter is optional and can be used to display a [tqdm](https://tqdm.github.io/) progress bar while loading data.

### Endpoint inclusivity

A time interval specified as a tuple is interpreted as a half-closed interval. This means the start time is inclusive, and the end time is exclusive.
For instance, using an end time of `2025-06-01` includes data points up to `2025-05-31 23:59:59.999`, but excludes those from `2025-06-01 00:00:00.000`.
This behavior mimics the Python `range` function and is useful for chaining time intervals.

**Python**

```python title="Python"
import xarray as xr

data = []
for month in [4, 5, 6]:
    interval = (f"2025-{month}-01", f"2025-{month+1}-01")
    data.append(collection.query(temporal_extent=interval, show_progress=True))

# Concatenate the data into a single dataset, which is equivalent
# to the result of the single request in the code example above.
data = xr.concat(data, dim="time")
```

**Go**

```go title="Go"
var datapoints []*v1.Sentinel2Msi

for month := 4; month <= 6; month++ {
  startDate := time.Date(2025, month, 1, 0, 0, 0, 0, time.UTC)
  endDate := time.Date(2025, month + 1, 1, 0, 0, 0, 0, time.UTC)

  var partialDatapoints []*v1.Sentinel2Msi
  err = client.Datapoints.QueryInto(ctx,
    dataset.ID,
    &partialDatapoints,
    datasets.WithCollectionIDs(collection.ID),
    datasets.WithTemporalExtent(query.NewTimeInterval(startDate, endDate)),
  )
  if err != nil {
    log.Fatalf("Failed to query datapoints: %v", err)
  }

  // Concatenate the data into a single dataset, which is equivalent
  // to the result of the single request in the code example above.
  datapoints = append(datapoints, partialDatapoints...)
}
```

Above example demonstrates how to split a large time interval into smaller chunks while loading data in separate requests. Typically, this is not necessary as the datasets client auto-paginates large intervals.

### Manual endpoint inclusivity

For greater control over inclusivity of start and end times, you can explicitly specify a `TimeInterval`. This way you can specify both the `start` and `end` times, as well as their inclusivity. Here's an example of creating equivalent `TimeInterval` objects in two different ways.

**Python**

```python title="Python"
from datetime import datetime
from tilebox.datasets.query import TimeInterval

interval1 = TimeInterval(
    datetime(2021, 1, 1), datetime(2023, 1, 1),
    end_inclusive=False
)
interval2 = TimeInterval(
    # python datetime granularity is in milliseconds
    datetime(2021, 1, 1), datetime(2022, 12, 31, 23, 59, 59, 999999),
    end_inclusive=True
)

print("Inclusivity is indicated by interval notation: ( and [")
print(interval1)
print(interval2)
print(f"They are equivalent: {interval1 == interval2}")
print(interval2.to_half_open())

# Query data for a time interval
data = collection.query(temporal_extent=interval1, show_progress=True)
```

**Go**

```go title="Go"
interval1 := query.TimeInterval{
  Start:        time.Date(2021, time.January, 1, 0, 0, 0, 0, time.UTC),
  End:          time.Date(2023, time.January, 1, 0, 0, 0, 0, time.UTC),
  EndInclusive: false,
}

interval2 := query.TimeInterval{
  Start:        time.Date(2021, time.January, 1, 0, 0, 0, 0, time.UTC),
  // the granularity of time.Time in Go is nanoseconds
  End:          time.Date(2022, time.December, 31, 23, 59, 59, 999999999, time.UTC),
  EndInclusive: true,
}

log.Println("Inclusivity is indicated by interval notation: ( and [")
log.Println(interval1.String())
log.Println(interval2.String())
log.Println("They are equivalent:", interval1.Equal(&interval2))
log.Println(interval2.ToHalfOpen().String())

// Query data for a time interval
var datapoints []*v1.Sentinel2Msi
err = client.Datapoints.QueryInto(ctx,
  dataset.ID,
  &datapoints,
  datasets.WithCollectionIDs(collection.ID),
  datasets.WithTemporalExtent(interval1),
)
```

```plaintext title="Output"
Inclusivity is indicated by interval notation: ( and [
[2021-01-01T00:00:00.000 UTC, 2023-01-01T00:00:00.000 UTC)
[2021-01-01T00:00:00.000 UTC, 2022-12-31T23:59:59.999 UTC]
They are equivalent: True
[2021-01-01T00:00:00.000 UTC, 2023-01-01T00:00:00.000 UTC)
```

## Time scalar queries

You can query all datapoints linked to a specific timestamp by specifying a `TimeScalar` as the time query argument. A `TimeScalar` can be a `datetime` object or a string in ISO 8601 format.

Tilebox uses millisecond precision for time indexing datapoints. Thus, querying a specific time scalar, is equivalent to a time interval query of length 1 millisecond.

Here's how to query a data point at a specific millisecond from a [dataset](/docs/datasets/concepts/datasets) or [collection](/docs/datasets/concepts/collections).

**Python**

```python title="Python"
data = sentinel2_msi.query(temporal_extent="2025-06-15T02:31:41.024")
print(f"Queried {data.sizes['time']} data points.")
first_timestamp = data.time[0].dt.strftime("%Y-%m-%dT%H:%M:%S.%f").item()
print("First datapoint time:", first_timestamp)
```

**Go**

```go title="Go"
temporalExtent := query.NewPointInTime(time.Date(2025, time.June, 15, 2, 31, 41, 024000000, time.UTC))

var datapoints []*v1.Sentinel2Msi
err = client.Datapoints.QueryInto(ctx,
  dataset.ID,
  &datapoints,
  datasets.WithTemporalExtent(temporalExtent),
)

log.Printf("Queried %d datapoints", len(datapoints))
log.Printf("First datapoint time: %s", datapoints[0].GetTime().AsTime())
```

```plaintext title="Output"
Queried 714 datapoints
First datapoint time: 2025-06-15T02:31:41.024 +0000 UTC
```

A collection may contain multiple datapoints for the same millisecond, so multiple data points may be returned. If you want to fetch only a single data point, [query the collection by id](/docs/datasets/query/filter-by-id) instead.

## Timezone handling

All `TimeScalars` specified as a string are treated as UTC if they do not include a timezone suffix. If you want to query data for a specific time or time range
in another timezone, it's recommended to a type that includes timezone information. Tilebox will automatically convert such objects to `UTC` in order to send the right query requests.
All outputs will always contain UTC timestamps, which will need to be converted again to a different timezone if required.

**Python**

```python title="Python"
from datetime import datetime
import pytz

# Tokyo has a UTC+9 hours offset, so this is the same as
# 2017-01-01 02:45:25.679 UTC
tokyo_time = pytz.timezone('Asia/Tokyo').localize(
    datetime(2021, 1, 1, 11, 45, 25, 679000)
)
print(tokyo_time)
data = collection.query(temporal_extent=tokyo_time)
print(data)
```

**Go**

```go title="Go"
// Tokyo has a UTC+9 hours offset, so this is the same as
// 2017-01-01 02:45:25.679 UTC
location, _ := time.LoadLocation("Asia/Tokyo")
tokyoTime := query.NewPointInTime(time.Date(2021, 1, 1, 11, 45, 25, 679000000, location))
log.Println(tokyoTime)

var datapoints []*v1.Sentinel2Msi
err = client.Datapoints.QueryInto(ctx,
  dataset.ID,
  &datapoints,
  datasets.WithCollectionIDs(collection.ID),
  datasets.WithTemporalExtent(tokyoTime),
)
if err != nil {
  log.Fatalf("Failed to query datapoints: %v", err)
}

log.Printf("Queried %d datapoints", len(datapoints))
// time is in UTC since API always returns UTC timestamps
log.Printf("First datapoint time: %s", datapoints[0].GetTime().AsTime())
```

Output

**Python**

```plaintext title="Python"
2021-01-01 11:45:25.679000+09:00
<xarray.Dataset> Size: 725B
Dimensions:                (time: 1, latlon: 2)
Coordinates:
    ingestion_time         (time) datetime64[ns] 8B 2024-06-21T11:03:33.852435
    id                     (time) <U36 144B '015957ea-d82f-e454-34ab-a87603ee...
  * time                   (time) datetime64[ns] 8B 2017-01-01T02:45:25.679000
  * latlon                 (latlon) <U9 72B 'latitude' 'longitude'
Data variables:
    ...
```

**Go**

```plaintext title="Go"
[2021-01-01 11:45:25.679 +0900 JST, 2021-01-01 11:45:25.679 +0900 JST]
Queried 1 datapoints
First datapoint time: 2021-01-01 02:45:25.679 +0000 UTC
```
