/** * Parser for the "goauction" UK auction-house platform (Sworders, Chiswick Auctions, Dominic Winter …): * - results calendar: `.auction-calendar-item` blocks with title, "Tuesday 21 April 2026" (or a range), sale number * and a link carrying `au=`; * - lot grid: `.auction-grid-lot` / `.auction-lot` cards with "Lot 12 - Title", optional `.sub-title`, image, * "Sold for £1,800" (only sold lots carry a price; unsold lots show nothing), 48–96 per page, ``. * Prices are what the house publishes as "Sold for" — the pages do not say hammer vs premium, so the basis is * left to each connector (null = unknown unless the house's terms say otherwise). */ import type { CurrencyCode } from '@rareindex/shared'; import { parseEuMoney } from './index.js'; import { absolute, chunksBetween, pick, textOf, type ParsedLot, type ParsedSalePage, type SaleRef } from './sale-results.js'; export interface GoauctionHouse { base: string; currency: CurrencyCode; /** path of the lot-list page: 'details' → /auction/details//?au=ID ; 'search' → /Auction/Search?au=ID&sd=2 */ listStyle: 'details' | 'search'; } /** * "Tuesday 21 April 2026" | "Tuesday 1 September - Monday 7 September 2026" | "20th August 2026" | "4th Sep, 2026 12:00" * → first day ISO (UTC midnight). */ export function parseUkDate(text: string | null | undefined): string | null { if (!text) return null; const t = text.replace(/\s+/g, ' ').trim(); const range = t.match(/(\d{1,2})(?:st|nd|rd|th)?\s+([A-Za-z]{3,9})?,?\s*-\s*[A-Za-z]*,?\s*(\d{1,2})(?:st|nd|rd|th)?\s+([A-Za-z]{3,9}),?\s+(\d{4})/); const m = range ? { d: range[1]!, mon: range[2] ?? range[4]!, y: range[5]! } : (() => { const s = t.match(/(\d{1,2})(?:st|nd|rd|th)?\s+([A-Za-z]{3,9})\.?,?\s+(\d{4})/); return s ? { d: s[1]!, mon: s[2]!, y: s[3]! } : null; })(); if (!m) return null; const months = ['jan', 'feb', 'mar', 'apr', 'may', 'jun', 'jul', 'aug', 'sep', 'oct', 'nov', 'dec']; const mo = months.indexOf(m.mon.slice(0, 3).toLowerCase()); if (mo < 0) return null; const d = new Date(Date.UTC(Number(m.y), mo, Number(m.d))); return Number.isNaN(d.getTime()) ? null : d.toISOString(); } export function parseGoauctionCalendar(htmlText: string, house: GoauctionHouse): SaleRef[] { const out: SaleRef[] = []; const seen = new Set(); for (const chunk of chunksBetween(htmlText, /
([\s\S]*?)<\/H[3-6]>/i) ?? pick(chunk, /alt="([^"]*)"/) ?? `Auction ${au}`; // Sworders:
Tuesday 21 April 2026
; Chiswick (timed): "Starts: 24th Aug, 2026 17:00 … Ends: 4th Sep, 2026 12:00"; // Dominic Winter (sessions): "Lot: 1 to 410 - 2nd Sep, 2026 10:00" → first session day. const text = textOf(chunk.replace(/]*>|<\/a>/g, ' ')); const startsText = text.match(/Starts:\s*(\d{1,2}(?:st|nd|rd|th)?\s+[A-Za-z]{3,9},?\s+\d{4})/i)?.[1] ?? null; const endsText = text.match(/Ends:\s*(\d{1,2}(?:st|nd|rd|th)?\s+[A-Za-z]{3,9},?\s+\d{4})/i)?.[1] ?? null; const dateText = pick(chunk, /
([\s\S]*?)<\/h5>/) ?? pick(chunk, /

([\s\S]*?)<\/p>/) ?? startsText ?? text.match(/((?:\d{1,2}(?:st|nd|rd|th)?\s+[A-Za-z]+,?\s*-\s*)?\d{1,2}(?:st|nd|rd|th)?\s+[A-Za-z]{3,9},?\s+\d{4})/)?.[1] ?? null; const saleNo = pick(chunk, /Sale number:\s*([A-Z0-9]+)/i); const timed = /timed online|Starts:/i.test(chunk); const resultsPublished = /View Results|Download PDF results/i.test(chunk); out.push({ id: au, title, url: absolute(house.base, href.replace(/&/g, '&')) ?? `${house.base}/auction/details/?au=${au}`, date: parseUkDate(dateText), location: null, extra: { sale_number: saleNo, timed, date_text: dateText, end_date: parseUkDate(endsText), results_published: resultsPublished } }); } return out; } export function parseGoauctionLots(htmlText: string, sale: SaleRef, house: GoauctionHouse): ParsedSalePage | null { if (!/class="auction-lot"|class='auction-grid-lot|class="auction-lot-title"/.test(htmlText)) return null; const lots: ParsedLot[] = []; for (const chunk of chunksBetween(htmlText, /

/, /