import { describe, expect, it } from 'vitest'; import { parsePrice, formatMoney } from './money.js'; import { parseSourceDate } from './dates.js'; import { median, trimmedMean, ewMean, maxDrawdown } from './stats.js'; import { normalizeForMatch, jaccard, slugify } from './text.js'; describe('parsePrice', () => { it('parses US formats', () => { expect(parsePrice('$1,234.56')).toMatchObject({ amount: 1234.56, currency: 'USD' }); expect(parsePrice('US $2,500.00')).toMatchObject({ amount: 2500, currency: 'USD' }); expect(parsePrice('Sold for $420,000')).toMatchObject({ amount: 420000, currency: 'USD' }); }); it('parses EU formats', () => { expect(parsePrice('1.234,56 €')).toMatchObject({ amount: 1234.56, currency: 'EUR' }); expect(parsePrice('EUR 12,50')).toMatchObject({ amount: 12.5, currency: 'EUR' }); expect(parsePrice('2.500 €')).toMatchObject({ amount: 2500, currency: 'EUR' }); }); it('parses GBP/JPY/CAD', () => { expect(parsePrice('£12.99')).toMatchObject({ amount: 12.99, currency: 'GBP' }); expect(parsePrice('¥15,000')).toMatchObject({ amount: 15000, currency: 'JPY' }); expect(parsePrice('CA$1 200,00')).toMatchObject({ amount: 1200, currency: 'CAD' }); expect(parsePrice('C$99')).toMatchObject({ amount: 99, currency: 'CAD' }); }); it('does not invent a currency', () => { expect(parsePrice('1500')).toMatchObject({ amount: 1500, currency: null }); expect(parsePrice('1500', 'GBP')).toMatchObject({ amount: 1500, currency: 'GBP' }); expect(parsePrice('no price')).toBeNull(); }); it('formats', () => { expect(formatMoney(8450)).toBe('$8,450'); expect(formatMoney(12.5)).toBe('$12.50'); expect(formatMoney(null)).toBe('—'); }); }); describe('parseSourceDate', () => { it('handles common formats', () => { expect(parseSourceDate('2024-03-05')?.toISOString()).toBe('2024-03-05T00:00:00.000Z'); expect(parseSourceDate('Mar 5, 2024')?.toISOString()).toBe('2024-03-05T00:00:00.000Z'); expect(parseSourceDate('5 March 2024')?.toISOString()).toBe('2024-03-05T00:00:00.000Z'); expect(parseSourceDate('03/05/2024')?.toISOString()).toBe('2024-03-05T00:00:00.000Z'); expect(parseSourceDate('05.03.2024', { dayFirst: true })?.toISOString()).toBe('2024-03-05T00:00:00.000Z'); expect(parseSourceDate('garbage')).toBeNull(); }); }); describe('stats', () => { it('median/trimmed/ew/drawdown', () => { expect(median([3, 1, 2])).toBe(2); expect(median([4, 1, 2, 3])).toBe(2.5); expect(trimmedMean([1, 2, 3, 4, 100], 0.2)).toBe(3); expect(ewMean([{ value: 100, ageDays: 0 }, { value: 50, ageDays: 1000 }])).toBeGreaterThan(90); expect(maxDrawdown([100, 120, 60, 90])).toBeCloseTo(0.5); }); }); describe('text', () => { it('normalises and compares', () => { const a = '1999 Pokemon Base Charizard 1st PSA 10'; const b = 'Pokémon 1999 First Edition Charizard Holo PSA GEM MT 10'; expect(normalizeForMatch(a)).toBe('1999 pokemon base charizard 1st psa 10'); expect(jaccard(a, b)).toBeGreaterThan(0.3); expect(slugify('1999 Base Set Charizard #4/102 — 1st Edition')).toBe('1999-base-set-charizard-4-102-1st-edition'); }); });