/** Working paper identifiers: permanent, never renumbered, gaps allowed. `WP38` ⇄ slug `wp38`; `FIRC0002` ⇄ slug `firc2`. */ export type ParsedId = { seriesCode: string; number: number; identifier: string; slug: string }; export function makeIdentifier(seriesCode: string, number: number): ParsedId { const code = seriesCode.toUpperCase(); const identifier = code === "WP" ? `WP${String(number).padStart(2, "0")}` : `${code}${String(number).padStart(4, "0")}`; return { seriesCode: code, number, identifier, slug: `${code.toLowerCase()}${number}` }; } /** Accepts `wp38`, `WP38`, `WP038`, `wp-38`, `firc2`, `FIRC0002`, `38` (defaults to WP). */ export function parseIdentifier(input: string): ParsedId | null { const s = input.trim().toLowerCase().replace(/[\s_-]+/g, ""); const m = /^([a-z]*)0*(\d{1,5})$/.exec(s); if (!m) return null; const code = m[1] === "" ? "wp" : m[1]; const n = Number(m[2]); if (!Number.isFinite(n) || n <= 0) return null; return makeIdentifier(code, n); } /** Version labels: `v2`, `v1.1`, `2.0` → version number 2 / label "v2.0". Minor versions are kept in the label only. */ export function parseVersionLabel(v: string): { versionNo: number; label: string } | null { const m = /^v?(\d+)(?:\.(\d+))?$/i.exec(v.trim()); if (!m) return null; return { versionNo: Number(m[1]), label: `v${m[1]}${m[2] ? "." + m[2] : ""}` }; } /** Detect a working paper number in text extracted from a title page. */ export function detectWpNumber(text: string): { seriesCode: string; number: number } | null { const t = text.replace(/\s+/g, " "); const firc = /FIRC[^0-9]{0,40}?(?:No\.?|N[o°]|Paper)\s*0*(\d{1,4})/i.exec(t); if (firc) return { seriesCode: "FIRC", number: Number(firc[1]) }; const wp = /Working\s+Paper\s+(?:No\.?|N[o°]|Number|#)\s*0*(\d{1,4})/i.exec(t) ?? /\bWP\s*0*(\d{1,4})\b/.exec(t); if (wp) return { seriesCode: "WP", number: Number(wp[1]) }; return null; }