Linked Art in C#

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

ParameterValuesMeaning
stext, up to 500 charactersThe date as written. Required.
intentauto (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.
profilea 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.
shapefull (default), timespan timespan returns only the TimeSpan JSON (or { timespan, begin, end } for a pair), ready to paste into a record.
emitInnerBoundstrue, 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.
labelPolicylabel (default), labelAndName, name Where the original text goes: the TimeSpan's _label, a Name under identified_by as well, or the Name only.
llmtrueAccepted and ignored unless the server has an interpreter configured; a note in the response says so.

The response

FieldMeaning
statusparsed, 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.
kindsingle, birthDeath, formationDissolution, floruit, or none.
confidence0 to 1. Readings at 0.75 and above are the ones the parser would act on unasked; below that, read the notes.
canonicalThe reading in the parser's own small language, which the service also accepts as input (see /v1/canonical). What this is.
edtfThe EDTF form, when EDTF can express the reading.
flagsWhat shaped the reading: Approximate, Uncertain, OpenStart, OpenEnd, Bce, Season, NamedPeriod, TwoDigitYear, Unconsumed and others.
timespan, begin, endLinked 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.
alternativesOther readings the parser considered, each with its own canonical form and confidence.
notes, unconsumedPlain-language remarks, and any part of the text the parser could not use.
profile, parserVersion, strategyWhat 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.

ProfileIntended for
default
archive-usUS 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-quartersMuseum 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.
libraryLibrary 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-manuscriptsManuscript 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.
multilingualEnglish 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

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)DefaultMeaning
Parser__ProfilesDirectory/app/profilesMount 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.jsonThe PeriodO period names; point it elsewhere or at a missing file to use only the built-in tables.
Parser__RequestsPerMinute600Per client address before 429. Raise it on a private network.
Parser__MaxInputsPerRequest, Parser__MaxInputLength500, 500Batch size and string length limits.
Capture__Enabled, Capture__PathfalseAppend 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_PORTS8080Listening 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.