From a1200efba6bf0fc0a268cac877dd10a2280cbda8 Mon Sep 17 00:00:00 2001 From: Sameh Khamis Date: Tue, 10 Nov 2026 13:19:23 -0800 Subject: [PATCH] fix(shipwrecks): an end-of-service date (P576) keeps its precision, so a year-only one is not shown as 1 January The query read P576 through wdt:, which carries no precision, and a missing precision counted as a day. It now reads the best-rank statement's value with wikibase:timePrecision, and a value without a precision is trusted to the year only. Against the live query service the new query takes 23 s and dates 16 more wrecks while every date the old one gave is unchanged. --- src/layers/shipwrecks/records.js | 16 ++++----- src/layers/shipwrecks/records.test.mjs | 45 ++++++++++++++++++++++++++ 2 files changed, 53 insertions(+), 8 deletions(-) diff --git a/src/layers/shipwrecks/records.js b/src/layers/shipwrecks/records.js index 741adb320..76fcd53eb 100644 --- a/src/layers/shipwrecks/records.js +++ b/src/layers/shipwrecks/records.js @@ -16,16 +16,16 @@ export const SHIPWRECK_SOURCES = Object.freeze(['emodnet', 'wikidata']); * Wikidata (CC0): shipwreck items that have at least one Wikipedia article, * so the worldwide set stays to notable wrecks and skips bulk register * imports, plus sinking events ("sinking of the RMS Titanic"), which are where - * Wikidata keeps the death toll. Dates come with their precision so a - * month-precise date is not shown as the 1st. Only ship and event facts are - * read: no items about people. + * Wikidata keeps the death toll. Dates, the end of service (P576) included, + * come with their precision so a month-precise date is not shown as the 1st. + * Only ship and event facts are read: no items about people. */ -export const WIKIDATA_QUERY = `SELECT ?item ?itemLabel ?itemDescription ?coord ?sunk ?sunkPrecision ?end ?deaths ?survivors ?typeLabel ?enwiki ?ptwiki ?event WHERE { +export const WIKIDATA_QUERY = `SELECT ?item ?itemLabel ?itemDescription ?coord ?sunk ?sunkPrecision ?end ?endPrecision ?deaths ?survivors ?typeLabel ?enwiki ?ptwiki ?event WHERE { { ?item wdt:P31/wdt:P279* wd:Q852190 ; wdt:P625 ?coord ; wikibase:sitelinks ?links . FILTER(?links > 0) OPTIONAL { ?item p:P793 ?wreck . ?wreck ps:P793 wd:Q906512 ; pqv:P585 [ wikibase:timeValue ?sunk ; wikibase:timePrecision ?sunkPrecision ] } - OPTIONAL { ?item wdt:P576 ?end } + OPTIONAL { ?item p:P576 [ a wikibase:BestRank ; psv:P576 [ wikibase:timeValue ?end ; wikibase:timePrecision ?endPrecision ] ] } OPTIONAL { ?item wdt:P31 ?type . FILTER(?type != wd:Q852190) } } UNION { ?item wdt:P31/wdt:P279* wd:Q906512 ; wdt:P625 ?coord . @@ -203,8 +203,8 @@ function wikidataTime(value, precision) { const none = { date: null, year: null, dating: null }; const match = /^([+-]?)(\d{4,})-(\d{2})-(\d{2})T/.exec(value || ''); if (!match) return none; - // A missing precision (P576 is read without one) counts as a day. - const p = precision == null ? 11 : Number(precision); + // A value without a precision is only trusted to the year. + const p = precision == null ? 9 : Number(precision); // 11 = day; 10 = month; 9 = year; below 9 is a decade or coarser. if (!Number.isFinite(p) || p < 9) return none; const signed = Number(`${match[1] === '-' ? '-' : ''}${match[2]}`); @@ -253,7 +253,7 @@ export function normalizeWikidataResults(payload) { const qid = /\/entity\/(Q\d{1,12})$/.exec(value('item') || '')?.[1]; if (!qid) continue; const sunk = wikidataTime(value('sunk'), value('sunkPrecision')); - const ended = wikidataTime(value('end'), null); + const ended = wikidataTime(value('end'), value('endPrecision')); const known = byItem.get(qid); if (known) { // Extra rows only add what the first one lacked. diff --git a/src/layers/shipwrecks/records.test.mjs b/src/layers/shipwrecks/records.test.mjs index 61ec8fe64..2033e391f 100644 --- a/src/layers/shipwrecks/records.test.mjs +++ b/src/layers/shipwrecks/records.test.mjs @@ -6,6 +6,7 @@ import { normalizeShipwreckSnapshot, normalizeWikidataResults, parseYear, + WIKIDATA_QUERY, } from './records.js'; function feature(id, properties, coordinates = [-4.35, 54.13]) { @@ -86,6 +87,7 @@ test('Wikidata rows fold into one record per item', () => { itemDescription: { value: '1939 G and H-class destroyer' }, coord: { value: 'Point(2.26 51.13)' }, sunk: { value: '1940-06-01T00:00:00Z' }, + sunkPrecision: { value: '11' }, enwiki: { value: 'https://en.wikipedia.org/wiki/HMS_Havant_(H32)' }, }, { @@ -262,3 +264,46 @@ test('a date before AD 100 is kept as ancient text, never read as an AD year', ( assert.equal(roman.year, 300); assert.equal(roman.dating, null); }); + +test('an end-of-service date keeps its precision: a year-only one is no day', () => { + const entity = (q) => ({ value: `http://www.wikidata.org/entity/${q}` }); + const [yearOnly, dayPrecise, unknown] = normalizeWikidataResults({ + results: { + bindings: [ + { + item: entity('Q11'), + itemLabel: { value: 'Year only' }, + coord: { value: 'Point(1 50)' }, + end: { value: '1917-01-01T00:00:00Z' }, + endPrecision: { value: '9' }, + }, + { + item: entity('Q12'), + itemLabel: { value: 'Day precise' }, + coord: { value: 'Point(2 50)' }, + end: { value: '1917-03-04T00:00:00Z' }, + endPrecision: { value: '11' }, + }, + { + item: entity('Q13'), + itemLabel: { value: 'No precision' }, + coord: { value: 'Point(3 50)' }, + end: { value: '1917-01-01T00:00:00Z' }, + }, + ], + }, + }); + assert.equal(yearOnly.date, null, 'not shown as 1 Jan 1917'); + assert.equal(yearOnly.year, 1917); + assert.equal(dayPrecise.date, '1917-03-04'); + assert.equal(unknown.date, null, 'an unknown precision is a year only'); + assert.equal(unknown.year, 1917); +}); + +test('the query reads the end of service with its precision', () => { + assert.match( + WIKIDATA_QUERY, + /psv:P576 \[ wikibase:timeValue \?end ; wikibase:timePrecision \?endPrecision \]/, + ); + assert.doesNotMatch(WIKIDATA_QUERY, /wdt:P576/); +});