Skip to content

Commit 4ff2b7a

Browse files
Merge pull request #12 from enricobattocchi/claude/handle-deprecated-dates-dVKI0
Use best claim selection for Wikidata date extraction
2 parents f58f7b6 + c6c0491 commit 4ff2b7a

3 files changed

Lines changed: 93 additions & 10 deletions

File tree

package-lock.json

Lines changed: 2 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/lib/wikidata.test.ts

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
import { describe, it, expect } from "vitest";
2+
import { bestClaim, type WikidataClaim } from "./wikidata";
3+
4+
function makeClaim(time: string, rank?: "preferred" | "normal" | "deprecated"): WikidataClaim {
5+
return {
6+
mainsnak: {
7+
datavalue: {
8+
value: { time },
9+
type: "time",
10+
},
11+
},
12+
rank,
13+
};
14+
}
15+
16+
describe("bestClaim", () => {
17+
it("returns the preferred claim over normal ones", () => {
18+
const claims = [
19+
makeClaim("+2026-02-15T00:00:00Z", "normal"),
20+
makeClaim("+2026-04-01T00:00:00Z", "preferred"),
21+
];
22+
const result = bestClaim(claims);
23+
expect((result!.mainsnak.datavalue!.value as { time: string }).time).toBe("+2026-04-01T00:00:00Z");
24+
});
25+
26+
it("skips deprecated claims", () => {
27+
const claims = [
28+
makeClaim("+2026-02-15T00:00:00Z", "deprecated"),
29+
makeClaim("+2026-04-01T00:00:00Z", "normal"),
30+
];
31+
const result = bestClaim(claims);
32+
expect((result!.mainsnak.datavalue!.value as { time: string }).time).toBe("+2026-04-01T00:00:00Z");
33+
});
34+
35+
it("returns null when all claims are deprecated", () => {
36+
const claims = [
37+
makeClaim("+2026-02-15T00:00:00Z", "deprecated"),
38+
makeClaim("+2025-11-01T00:00:00Z", "deprecated"),
39+
];
40+
expect(bestClaim(claims)).toBeNull();
41+
});
42+
43+
it("returns the first normal claim when no preferred exists", () => {
44+
const claims = [
45+
makeClaim("+2020-01-01T00:00:00Z", "normal"),
46+
makeClaim("+2021-06-15T00:00:00Z", "normal"),
47+
];
48+
const result = bestClaim(claims);
49+
expect((result!.mainsnak.datavalue!.value as { time: string }).time).toBe("+2020-01-01T00:00:00Z");
50+
});
51+
52+
it("handles claims without an explicit rank (treats as non-deprecated)", () => {
53+
const claims = [
54+
makeClaim("+1969-07-20T00:00:00Z"),
55+
];
56+
const result = bestClaim(claims);
57+
expect((result!.mainsnak.datavalue!.value as { time: string }).time).toBe("+1969-07-20T00:00:00Z");
58+
});
59+
60+
it("prefers preferred over claims with no rank", () => {
61+
const claims = [
62+
makeClaim("+2020-01-01T00:00:00Z"),
63+
makeClaim("+2026-04-01T00:00:00Z", "preferred"),
64+
];
65+
const result = bestClaim(claims);
66+
expect((result!.mainsnak.datavalue!.value as { time: string }).time).toBe("+2026-04-01T00:00:00Z");
67+
});
68+
});

src/lib/wikidata.ts

Lines changed: 23 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -271,13 +271,21 @@ function parseWikidataTime(timeValue: string): { year: number; month: number | n
271271
};
272272
}
273273

274-
interface WikidataClaim {
274+
export interface WikidataClaim {
275275
mainsnak: {
276276
datavalue?: {
277277
value: { time: string; precision?: number } | { id: string } | string;
278278
type: string;
279279
};
280280
};
281+
rank?: "preferred" | "normal" | "deprecated";
282+
}
283+
284+
/** Pick the best claim from a list, skipping deprecated and preferring preferred rank */
285+
export function bestClaim(claims: WikidataClaim[]): WikidataClaim | null {
286+
const nonDeprecated = claims.filter((c) => c.rank !== "deprecated");
287+
if (nonDeprecated.length === 0) return null;
288+
return nonDeprecated.find((c) => c.rank === "preferred") ?? nonDeprecated[0];
281289
}
282290

283291
interface WikidataEntity {
@@ -294,7 +302,10 @@ function extractDate(claims: Record<string, WikidataClaim[]>): { year: number; m
294302
const propClaims = claims[prop];
295303
if (!propClaims?.length) continue;
296304

297-
const dv = propClaims[0].mainsnak.datavalue;
305+
const claim = bestClaim(propClaims);
306+
if (!claim) continue;
307+
308+
const dv = claim.mainsnak.datavalue;
298309
if (!dv || dv.type !== "time") continue;
299310

300311
const timeVal = dv.value as { time: string };
@@ -406,13 +417,16 @@ async function entitiesToEvents(qids: string[], locale: string = "en"): Promise<
406417
let deathDay: number | null = null;
407418
const p570 = entity.claims["P570"];
408419
if (p570?.length) {
409-
const dv = p570[0].mainsnak.datavalue;
410-
if (dv?.type === "time") {
411-
const parsed = parseWikidataTime((dv.value as { time: string }).time);
412-
if (parsed) {
413-
deathYear = parsed.year;
414-
deathMonth = parsed.month;
415-
deathDay = parsed.day;
420+
const deathClaim = bestClaim(p570);
421+
if (deathClaim) {
422+
const dv = deathClaim.mainsnak.datavalue;
423+
if (dv?.type === "time") {
424+
const parsed = parseWikidataTime((dv.value as { time: string }).time);
425+
if (parsed) {
426+
deathYear = parsed.year;
427+
deathMonth = parsed.month;
428+
deathDay = parsed.day;
429+
}
416430
}
417431
}
418432
}

0 commit comments

Comments
 (0)