Using the dates API
The parser behind the playground runs as a small HTTP service at https://dates.linkedart.net. Send it a date as
written in a record and it returns a Linked Art TimeSpan, with the canonical reading, EDTF where one exists, a
confidence and the flags that explain the reading. No key, no account; any origin may call it from a browser.
One string
GET https://dates.linkedart.net/v1/timespan?s=c.%201850
Try it: https://dates.linkedart.net/v1/timespan?s=c.%201850&intent=auto&profile=default. The response below was produced for that request when this
page was served.
{"input":"c. 1850","ref":null,"status":"parsed","kind":"single","confidence":0.95,"canonical":"1850~","edtf":"1850~","flags":["Approximate"],"strategy":"grammar","timespan":{"type":"TimeSpan","_label":"c. 1850","begin_of_the_begin":"1850-01-01T00:00:00Z","end_of_the_end":"1850-12-31T23:59:59Z"},"begin":null,"end":null,"alternatives":[],"notes":[],"unconsumed":null,"profile":"default","parserVersion":"0.2.0"}
Query parameters
| Parameter | Values | Meaning |
|---|---|---|
s | text, up to 500 characters | The date as written. Required. |
intent | auto (default), single, pair |
Whether "1606-1669" is one span or a birth and a death. auto reads the text's own markers (b., d., fl., born, active); pair forces two spans; single forces one. |
profile | a name from /v1/profiles, default default |
The conventions to realise under: century counting, how wide "early" and "late" are, two-digit years, season dates, date order. See the profiles below. |
shape | full (default), timespan |
timespan returns only the TimeSpan JSON (or { timespan, begin, end } for a pair), ready to paste into a record. |
emitInnerBounds | true, false |
When true, a vague edge also sets end_of_the_begin and begin_of_the_end, the part of the span the date certainly covers. Off by default; the playground's timeline draws them. |
labelPolicy | label (default), labelAndName, name |
Where the original text goes: the TimeSpan's _label, a Name under identified_by as well, or the Name only. |
llm | true | Accepted and ignored unless the server has an interpreter configured; a note in the response says so. |
The response
| Field | Meaning |
|---|---|
status | parsed, partial (something was read, something was not), undated (the text says there is no date), or unparsed. Unreadable text is still HTTP 200; only a malformed request is an error. |
kind | single, birthDeath, formationDissolution, floruit, or none. |
confidence | 0 to 1. Readings at 0.75 and above are the ones the parser would act on unasked; below that, read the notes. |
canonical | The reading in the parser's own small language, which the service also accepts as input (see /v1/canonical). What this is. |
edtf | The EDTF form, when EDTF can express the reading. |
flags | What shaped the reading: Approximate, Uncertain, OpenStart, OpenEnd, Bce, Season, NamedPeriod, TwoDigitYear, Unconsumed and others. |
timespan, begin, end | Linked Art JSON exactly as the library writes it. A single reading fills timespan; a pair fills begin and end and gives their hull as timespan. Ends are inclusive: the last second of the period. |
alternatives | Other readings the parser considered, each with its own canonical form and confidence. |
notes, unconsumed | Plain-language remarks, and any part of the text the parser could not use. |
profile, parserVersion, strategy | What produced the result. Same input, profile and parser version always give the same output. |
Many strings
Post up to 500 inputs in one request, as plain strings or as objects that override the intent or profile and carry your own reference.
POST https://dates.linkedart.net/v1/timespan
Content-Type: application/json
{
"profile": "library",
"inputs": [
"1984",
"late 1950s",
{ "s": "1606-1669", "intent": "pair", "ref": "row-17" }
]
}
The reply is { "results": [ … ] } in input order, each result in the shape above with your ref echoed back. Optional intent and options (emitInnerBounds, labelPolicy) at the top level apply to every input that does not say otherwise.
Profiles
Conventions differ between collections, so a profile fixes them. The service ships these; GET https://dates.linkedart.net/v1/profiles/name returns a profile's effective settings as JSON.
| Profile | Intended for |
|---|---|
default | |
archive-us | US archival practice (DACS/EAD) as timetwister reads it: numeric dates are month-first, seasons run equinox to solstice (spring 1776 = 20 March to 21 June) with winter at the start of the year (January to 20 March), and decade bands overlap as halves (early 0-5, mid 3-8, late 5-9). |
catalogue-quarters | Museum catalogues that read 'early' and 'late' as the first and last quarter of a century (early 20th century = 1900-1924, late = 1975-1999), 'mid' as the middle fifth (1940-1959), 'beginning of' and 'end of' as the same quarters, and 'early 1790s' as 1790-1792, 'mid 1790s' as 1794-1796; a two-digit year only when written with a month ('69-10' is October 1969, but 'Bunka 11' is a regnal year); the conventions of jeresig/node-yearrange. |
library | Library catalogue records (MARC 260/264): bracketed inferred dates, '[17_]' patterns, copyright and 'cop.' noise, two-digit years read within 1900-1999 ('08 = 1908 unless it would be in the future), and 'early'/'late' as halves: early 15th century = 1400-1449, late = 1450-1499, mid = the middle half (1425-1474); early 1950s = 1950-1954, late = 1955-1959, mid = 1952-1957. |
medieval-manuscripts | Manuscript cataloguing: Latin alongside English ('saec. XIV', 's. xiv med.', 'c. 1400'), strict centuries (the 14th century is 1301-1400 as palaeographers count), bare two- and three-digit years are years (MinYearDigits 3 keeps 'c. 950' a year), hedged dates widened by ten years each way. |
multilingual | English with French, German, Italian, Spanish, Dutch and Latin lexicons active at once, for mixed European collections. Where two languages use one word differently the earlier language in the list wins. |
Canonical expressions without parsing
GET https://dates.linkedart.net/v1/canonical?e=late%20195X&profile=library
Realises a canonical expression under a profile, skipping the free-text parser. Useful once you have stored canonical forms and want the TimeSpans regenerated under different conventions, or to check what a form means.
Advice on a dataset
POST https://dates.linkedart.net/v1/advise
Content-Type: application/json
{ "inputs": ["early 19th century", "ca. 1850", "7/4/1776", "[18--?]"] }
Before parsing a collection, send a sample of its date strings (up to 20,000, or text/plain with one per line) and the
profile advisor reports the conventions in use: band words (early, mid, late, halves, quarters), hedges, era words,
century notations, numeric date order, two-digit years, brackets, seasons, open ends and words from other languages, each with a count
and example strings. The response ends with recommendations, a suggestedProfile stated as changes from
default, and a markdown rendering of the whole report. An optional languages array limits which lexicons
are checked. Nothing is stored. The advisor never changes how a string parses; it only recommends the profile to parse under.
Behaviour
- Caching. Responses carry
Cache-Control: public, max-age=86400and anETag; sendIf-None-Matchto get a 304. - Limits. 500 characters per input, 500 inputs per POST, a 256 KB body (20,000 strings and 2 MB for
/v1/advise), and 600 requests a minute per address, after which the service answers 429. - Errors. 400 for a missing or malformed parameter, 404 for an unknown profile, each with
{ "error": "…", "detail": "…" }. A date the parser cannot read is not an error: 200 withstatus: unparsed. - Versioning. The path is
/v1; a breaking change to the response would bump it. Every result names itsparserVersion, andGET https://dates.linkedart.net/versionreports the service, parser and model versions. - Reference. The interactive OpenAPI description is at https://dates.linkedart.net/docs, and the document itself at
https://dates.linkedart.net/openapi/v1.json.
Run it yourself
The service is a public container image, so a project in any language can have the same parser on its own machine or network with no account and no call to this site. It is a small, read-only process: no database, nothing phoned home, and the profiles and period tables ship inside the image.
docker run --rm -p 8080:8080 ghcr.io/tomcrane/linked-art-net/dates-service:latest
curl "http://localhost:8080/v1/timespan?s=late%201950s"
open http://localhost:8080/docs
The same API as above answers on port 8080. In a docker-compose.yml, pinned to a release:
services:
dates:
image: ghcr.io/tomcrane/linked-art-net/dates-service:v0.1.1
ports: ["8080:8080"]
environment:
Parser__RequestsPerMinute: "100000" # effectively no rate limit on a private network
restart: unless-stopped
| Setting (environment variable) | Default | Meaning |
|---|---|---|
Parser__ProfilesDirectory | /app/profiles | Mount your own folder of profile files here (-v ./profiles:/app/profiles:ro), for example one the advisor suggested for your collection. |
Parser__PeriodOPath | /app/periods/periodo.json | The PeriodO period names; point it elsewhere or at a missing file to use only the built-in tables. |
Parser__RequestsPerMinute | 600 | Per client address before 429. Raise it on a private network. |
Parser__MaxInputsPerRequest, Parser__MaxInputLength | 500, 500 | Batch size and string length limits. |
Capture__Enabled, Capture__Path | false | Append inputs the parser could not read to a JSONL file (mount a volume), to review later in a trainer of your own or to send us as issues. |
ASPNETCORE_HTTP_PORTS | 8080 | Listening port inside the container. |
From Python, for instance, with nothing but the standard library:
import json, urllib.parse, urllib.request
def timespan(text, profile="default"):
url = "http://localhost:8080/v1/timespan?" + urllib.parse.urlencode({"s": text, "profile": profile})
with urllib.request.urlopen(url) as response:
return json.load(response)
result = timespan("fl. 1480-1510")
print(result["canonical"], result["confidence"], result["timespan"])
Images are built by GitHub Actions from each release tag (v0.1.1, and latest); /version reports the
parser and dataset versions inside. The image is a chiseled Ubuntu base with the .NET runtime and runs as a non-root user; /healthz
is its health check. The site you are reading is the companion image, examples-site, and the compose file that runs both is
in the repository.
From code
JavaScript
const url = new URL("https://dates.linkedart.net/v1/timespan");
url.searchParams.set("s", "c. 1850");
const result = await fetch(url).then(r => r.json());
console.log(result.canonical, result.timespan);
C#, calling the service
using var http = new HttpClient { BaseAddress = new Uri("https://dates.linkedart.net/") };
var result = await http.GetFromJsonAsync<JsonElement>("v1/timespan?s=" + Uri.EscapeDataString("c. 1850"));
var timespan = result.GetProperty("timespan");
C#, no service at all
The parser is a library; the service is a thin host for it. Reference LinkedArtNet.Dates and the same results come back in-process, with the fluent bridge putting them straight onto the model.
var result = TimeSpanParser.Default.Parse("c. 1850");
result.Status; // ParseStatus.Parsed
result.Canonical; // "1850~"
result.TimeSpan; // a LinkedArtTimeSpan
// With a profile from the repository's profiles/ folder
var profiles = ProfileCatalog.Load("profiles");
var parser = new TimeSpanParser(profiles.TryGet("library", out var library) ? library : ParseProfile.Default);
// Straight onto the model
production.WithTimeSpan("c. 1850");
person.WithLifespan("1606-1669");
group.WithExistence("fl. 1480-1510");
Wrong readings are welcome as issues with the input and what it should mean; reviewers can correct them in the trainer, and the corpus records every expectation the parser is tested against.