feat: added unit conversion buttons
This commit is contained in:
parent
12df111c5e
commit
a96734c394
10 changed files with 2624 additions and 1 deletions
805
lib/measurements/__tests__/matcher.test.js
Normal file
805
lib/measurements/__tests__/matcher.test.js
Normal file
|
|
@ -0,0 +1,805 @@
|
|||
import { describe, it, expect } from 'vitest';
|
||||
|
||||
const {
|
||||
findAllMeasurements,
|
||||
findTemperatures,
|
||||
findDimensions,
|
||||
findWeights,
|
||||
findVolumes,
|
||||
findTimes,
|
||||
parseAmount,
|
||||
parseSingleAmount,
|
||||
normalizeUnit,
|
||||
unitType,
|
||||
} = require('../matcher');
|
||||
|
||||
// ─── parseSingleAmount ────────────────────────────────────
|
||||
|
||||
describe('parseSingleAmount', () => {
|
||||
it('parses integers', () => {
|
||||
expect(parseSingleAmount('200')).toEqual({ value: 200, approximate: false });
|
||||
});
|
||||
|
||||
it('parses decimals', () => {
|
||||
expect(parseSingleAmount('1.5')).toEqual({ value: 1.5, approximate: false });
|
||||
});
|
||||
|
||||
it('parses fractions', () => {
|
||||
expect(parseSingleAmount('1/2')).toEqual({ value: 0.5, approximate: false });
|
||||
expect(parseSingleAmount('3/4')).toEqual({ value: 0.75, approximate: false });
|
||||
});
|
||||
|
||||
it('parses mixed numbers', () => {
|
||||
expect(parseSingleAmount('1 1/2')).toEqual({ value: 1.5, approximate: false });
|
||||
expect(parseSingleAmount('2 3/4')).toEqual({ value: 2.75, approximate: false });
|
||||
});
|
||||
|
||||
it('parses approximate values with ~', () => {
|
||||
expect(parseSingleAmount('~250')).toEqual({ value: 250, approximate: true });
|
||||
expect(parseSingleAmount('~ 150')).toEqual({ value: 150, approximate: true });
|
||||
});
|
||||
});
|
||||
|
||||
// ─── parseAmount ──────────────────────────────────────────
|
||||
|
||||
describe('parseAmount', () => {
|
||||
it('parses single values', () => {
|
||||
expect(parseAmount('200')).toEqual({ value: 200, approximate: false });
|
||||
});
|
||||
|
||||
it('parses dash ranges', () => {
|
||||
const result = parseAmount('180-240');
|
||||
expect(result).toEqual({
|
||||
min: { value: 180, approximate: false },
|
||||
max: { value: 240, approximate: false },
|
||||
});
|
||||
});
|
||||
|
||||
it('parses "to" ranges', () => {
|
||||
const result = parseAmount('28 to 32');
|
||||
expect(result).toEqual({
|
||||
min: { value: 28, approximate: false },
|
||||
max: { value: 32, approximate: false },
|
||||
});
|
||||
});
|
||||
|
||||
it('parses fractions (not as ranges)', () => {
|
||||
expect(parseAmount('1/2')).toEqual({ value: 0.5, approximate: false });
|
||||
});
|
||||
|
||||
it('parses mixed numbers', () => {
|
||||
expect(parseAmount('1 1/2')).toEqual({ value: 1.5, approximate: false });
|
||||
});
|
||||
});
|
||||
|
||||
// ─── normalizeUnit ────────────────────────────────────────
|
||||
|
||||
describe('normalizeUnit', () => {
|
||||
it('normalizes temperature units', () => {
|
||||
expect(normalizeUnit('°F')).toBe('°F');
|
||||
expect(normalizeUnit('° f')).toBe('°F');
|
||||
expect(normalizeUnit('°C')).toBe('°C');
|
||||
expect(normalizeUnit('° c')).toBe('°C');
|
||||
expect(normalizeUnit('F')).toBe('°F');
|
||||
expect(normalizeUnit('C')).toBe('°C');
|
||||
});
|
||||
|
||||
it('normalizes weight units', () => {
|
||||
expect(normalizeUnit('g')).toBe('g');
|
||||
expect(normalizeUnit('kg')).toBe('kg');
|
||||
expect(normalizeUnit('oz')).toBe('oz');
|
||||
expect(normalizeUnit('lb')).toBe('lb');
|
||||
expect(normalizeUnit('lbs')).toBe('lb');
|
||||
expect(normalizeUnit('ounces')).toBe('oz');
|
||||
expect(normalizeUnit('pounds')).toBe('lb');
|
||||
});
|
||||
|
||||
it('normalizes volume units', () => {
|
||||
expect(normalizeUnit('cup')).toBe('cup');
|
||||
expect(normalizeUnit('cups')).toBe('cup');
|
||||
expect(normalizeUnit('tablespoon')).toBe('tablespoon');
|
||||
expect(normalizeUnit('tablespoons')).toBe('tablespoon');
|
||||
expect(normalizeUnit('table spoon')).toBe('tablespoon');
|
||||
expect(normalizeUnit('table spoons')).toBe('tablespoon');
|
||||
expect(normalizeUnit('tbsp')).toBe('tablespoon');
|
||||
expect(normalizeUnit('teaspoon')).toBe('teaspoon');
|
||||
expect(normalizeUnit('teaspoons')).toBe('teaspoon');
|
||||
expect(normalizeUnit('tsp')).toBe('teaspoon');
|
||||
expect(normalizeUnit('ml')).toBe('ml');
|
||||
expect(normalizeUnit('L')).toBe('L');
|
||||
expect(normalizeUnit('quart')).toBe('quart');
|
||||
expect(normalizeUnit('quarts')).toBe('quart');
|
||||
expect(normalizeUnit('pint')).toBe('pint');
|
||||
expect(normalizeUnit('fl oz')).toBe('fl oz');
|
||||
expect(normalizeUnit('fl. oz')).toBe('fl oz');
|
||||
expect(normalizeUnit('parts by volume')).toBe('parts by volume');
|
||||
expect(normalizeUnit('parts by weight')).toBe('parts by weight');
|
||||
});
|
||||
|
||||
it('normalizes time units', () => {
|
||||
expect(normalizeUnit('minutes')).toBe('minute');
|
||||
expect(normalizeUnit('minute')).toBe('minute');
|
||||
expect(normalizeUnit('min')).toBe('minute');
|
||||
expect(normalizeUnit('mins')).toBe('minute');
|
||||
expect(normalizeUnit('hours')).toBe('hour');
|
||||
expect(normalizeUnit('hour')).toBe('hour');
|
||||
expect(normalizeUnit('hr')).toBe('hour');
|
||||
expect(normalizeUnit('hrs')).toBe('hour');
|
||||
expect(normalizeUnit('days')).toBe('day');
|
||||
expect(normalizeUnit('day')).toBe('day');
|
||||
expect(normalizeUnit('seconds')).toBe('second');
|
||||
expect(normalizeUnit('sec')).toBe('second');
|
||||
});
|
||||
|
||||
it('normalizes dimension units', () => {
|
||||
expect(normalizeUnit('inch')).toBe('inch');
|
||||
expect(normalizeUnit('inches')).toBe('inch');
|
||||
expect(normalizeUnit('cm')).toBe('cm');
|
||||
expect(normalizeUnit('mm')).toBe('mm');
|
||||
});
|
||||
|
||||
it('returns null for null input', () => {
|
||||
expect(normalizeUnit(null)).toBe(null);
|
||||
});
|
||||
});
|
||||
|
||||
// ─── unitType ─────────────────────────────────────────────
|
||||
|
||||
describe('unitType', () => {
|
||||
it('identifies temperature units', () => {
|
||||
expect(unitType('°F')).toBe('temperature');
|
||||
expect(unitType('°C')).toBe('temperature');
|
||||
});
|
||||
|
||||
it('identifies weight units', () => {
|
||||
expect(unitType('g')).toBe('weight');
|
||||
expect(unitType('kg')).toBe('weight');
|
||||
expect(unitType('oz')).toBe('weight');
|
||||
expect(unitType('lb')).toBe('weight');
|
||||
});
|
||||
|
||||
it('identifies volume units', () => {
|
||||
expect(unitType('cup')).toBe('volume');
|
||||
expect(unitType('tablespoon')).toBe('volume');
|
||||
expect(unitType('teaspoon')).toBe('volume');
|
||||
expect(unitType('quart')).toBe('volume');
|
||||
});
|
||||
|
||||
it('identifies time units', () => {
|
||||
expect(unitType('minute')).toBe('time');
|
||||
expect(unitType('hour')).toBe('time');
|
||||
expect(unitType('day')).toBe('time');
|
||||
});
|
||||
|
||||
it('identifies dimension units', () => {
|
||||
expect(unitType('inch')).toBe('dimension');
|
||||
expect(unitType('cm')).toBe('dimension');
|
||||
expect(unitType('mm')).toBe('dimension');
|
||||
});
|
||||
|
||||
it('returns null for unknown units', () => {
|
||||
expect(unitType(null)).toBe(null);
|
||||
expect(unitType('widgets')).toBe(null);
|
||||
});
|
||||
});
|
||||
|
||||
// ─── findTemperatures ─────────────────────────────────────
|
||||
|
||||
describe('findTemperatures', () => {
|
||||
it('finds basic °F temperature', () => {
|
||||
// From Brownies All American: "350°F"
|
||||
const results = findTemperatures('350°F 28 to 32 minutes');
|
||||
expect(results).toHaveLength(1);
|
||||
expect(results[0].amount).toEqual({ value: 350, approximate: false });
|
||||
expect(results[0].unit).toBe('°F');
|
||||
expect(results[0].type).toBe('temperature');
|
||||
});
|
||||
|
||||
it('finds °F with space before degree symbol', () => {
|
||||
// From Angel Food Cake: "325 °F"
|
||||
const results = findTemperatures('preheat oven to 325 °F');
|
||||
expect(results).toHaveLength(1);
|
||||
expect(results[0].amount).toEqual({ value: 325, approximate: false });
|
||||
expect(results[0].unit).toBe('°F');
|
||||
});
|
||||
|
||||
it('finds lowercase temperature', () => {
|
||||
// From Banana Bread: "350°f"
|
||||
const results = findTemperatures('preheat oven to 350°f');
|
||||
expect(results).toHaveLength(1);
|
||||
expect(results[0].amount).toEqual({ value: 350, approximate: false });
|
||||
expect(results[0].unit).toBe('°F');
|
||||
});
|
||||
|
||||
it('finds dual temperature with parenthesized conversion', () => {
|
||||
// From Apple Streusel: "175°C (350°F)"
|
||||
const results = findTemperatures('bake at 175°C (350°F)');
|
||||
expect(results).toHaveLength(1);
|
||||
expect(results[0].amount).toEqual({ value: 175, approximate: false });
|
||||
expect(results[0].unit).toBe('°C');
|
||||
expect(results[0].alt).not.toBeNull();
|
||||
expect(results[0].alt.amount).toEqual({ value: 350, approximate: false });
|
||||
expect(results[0].alt.unit).toBe('°F');
|
||||
});
|
||||
|
||||
it('finds °C with parenthesized °F and spaces', () => {
|
||||
// From Lasagna: "200°C (400°F)"
|
||||
const results = findTemperatures('bake in oven at 200°C (400°F) for');
|
||||
expect(results).toHaveLength(1);
|
||||
expect(results[0].amount).toEqual({ value: 200, approximate: false });
|
||||
expect(results[0].unit).toBe('°C');
|
||||
expect(results[0].alt.amount).toEqual({ value: 400, approximate: false });
|
||||
expect(results[0].alt.unit).toBe('°F');
|
||||
});
|
||||
|
||||
it('finds approximate temperature with ~', () => {
|
||||
// From Enchiladas: "425 °F (~220 °C)"
|
||||
const results = findTemperatures('oven preheated to 425 °F (~220 °C)');
|
||||
expect(results).toHaveLength(1);
|
||||
expect(results[0].amount).toEqual({ value: 425, approximate: false });
|
||||
expect(results[0].unit).toBe('°F');
|
||||
expect(results[0].alt.amount).toEqual({ value: 220, approximate: true });
|
||||
expect(results[0].alt.unit).toBe('°C');
|
||||
});
|
||||
|
||||
it('finds temperature range with degree symbol', () => {
|
||||
// From Ninja Creami: "165-175 °F"
|
||||
const results = findTemperatures('165-175 °F');
|
||||
expect(results).toHaveLength(1);
|
||||
expect(results[0].amount.min).toEqual({ value: 165, approximate: false });
|
||||
expect(results[0].amount.max).toEqual({ value: 175, approximate: false });
|
||||
expect(results[0].unit).toBe('°F');
|
||||
});
|
||||
|
||||
it('finds bare C temperature (no degree symbol)', () => {
|
||||
// From MaPo Tofu: "(170C)"
|
||||
const results = findTemperatures('(170C)');
|
||||
expect(results).toHaveLength(1);
|
||||
expect(results[0].amount).toEqual({ value: 170, approximate: false });
|
||||
expect(results[0].unit).toBe('°C');
|
||||
});
|
||||
|
||||
it('finds bare C temperature range', () => {
|
||||
// From MaPo Tofu: "(should be at 100-110C)"
|
||||
const results = findTemperatures('(should be at 100-110C)');
|
||||
expect(results).toHaveLength(1);
|
||||
expect(results[0].amount.min).toEqual({ value: 100, approximate: false });
|
||||
expect(results[0].amount.max).toEqual({ value: 110, approximate: false });
|
||||
expect(results[0].unit).toBe('°C');
|
||||
});
|
||||
|
||||
it('finds lowercase °c with parenthesized °f', () => {
|
||||
// From Mozzarella: "32°c (90°f)"
|
||||
const results = findTemperatures('32°c (90°f)');
|
||||
expect(results).toHaveLength(1);
|
||||
expect(results[0].amount).toEqual({ value: 32, approximate: false });
|
||||
expect(results[0].unit).toBe('°C');
|
||||
expect(results[0].alt.amount).toEqual({ value: 90, approximate: false });
|
||||
expect(results[0].alt.unit).toBe('°F');
|
||||
});
|
||||
|
||||
it('finds temperature range in °C', () => {
|
||||
// From Kombucha Scoby: "temperature between 23°C and 28°C"
|
||||
// This will match each individually since "and" is not a range separator
|
||||
const results = findTemperatures('temperature between 23°C and 28°C');
|
||||
expect(results).toHaveLength(2);
|
||||
expect(results[0].amount).toEqual({ value: 23, approximate: false });
|
||||
expect(results[1].amount).toEqual({ value: 28, approximate: false });
|
||||
});
|
||||
|
||||
it('finds °C range with dash', () => {
|
||||
// From Pizza Biga: "16-18°C"
|
||||
const results = findTemperatures('16-18°C');
|
||||
expect(results).toHaveLength(1);
|
||||
expect(results[0].amount.min).toEqual({ value: 16, approximate: false });
|
||||
expect(results[0].amount.max).toEqual({ value: 18, approximate: false });
|
||||
expect(results[0].unit).toBe('°C');
|
||||
});
|
||||
});
|
||||
|
||||
// ─── findDimensions ───────────────────────────────────────
|
||||
|
||||
describe('findDimensions', () => {
|
||||
it('finds NxN dimension', () => {
|
||||
// From Lasagna: "makes: 1 9x13 pan"
|
||||
const results = findDimensions('1 9x13 pan');
|
||||
expect(results).toHaveLength(1);
|
||||
expect(results[0].amount).toEqual([9, 13]);
|
||||
expect(results[0].unit).toBe(null);
|
||||
expect(results[0].type).toBe('dimension');
|
||||
});
|
||||
|
||||
it('finds N x N dimension with spaces', () => {
|
||||
// From Apple Caramel Upside Down Cake: "8 x 8 inch glass baking dish"
|
||||
const results = findDimensions('8 x 8 inch glass baking dish');
|
||||
expect(results).toHaveLength(1);
|
||||
expect(results[0].amount).toEqual([8, 8]);
|
||||
expect(results[0].unit).toBe('inch');
|
||||
});
|
||||
|
||||
it('finds N x N inch dimension', () => {
|
||||
// From Brownies: "9 x 13 inch glass baking dish"
|
||||
const results = findDimensions('9 x 13 inch glass baking dish');
|
||||
expect(results).toHaveLength(1);
|
||||
expect(results[0].amount).toEqual([9, 13]);
|
||||
expect(results[0].unit).toBe('inch');
|
||||
});
|
||||
|
||||
it('finds NxNxN 3D dimension', () => {
|
||||
// From Tater Tot Hotdish: "8x6x2 inch pyrex dishes"
|
||||
const results = findDimensions('8x6x2 inch pyrex dishes');
|
||||
expect(results).toHaveLength(1);
|
||||
expect(results[0].amount).toEqual([8, 6, 2]);
|
||||
expect(results[0].unit).toBe('inch');
|
||||
});
|
||||
|
||||
it('finds dimension without unit', () => {
|
||||
// From various: "9x13"
|
||||
const results = findDimensions('9x13');
|
||||
expect(results).toHaveLength(1);
|
||||
expect(results[0].amount).toEqual([9, 13]);
|
||||
expect(results[0].unit).toBe(null);
|
||||
});
|
||||
});
|
||||
|
||||
// ─── findWeights ──────────────────────────────────────────
|
||||
|
||||
describe('findWeights', () => {
|
||||
it('finds basic gram weight', () => {
|
||||
// From Alfredo Sauce: "olive oil 200g"
|
||||
const results = findWeights('olive oil 200g');
|
||||
expect(results).toHaveLength(1);
|
||||
expect(results[0].amount).toEqual({ value: 200, approximate: false });
|
||||
expect(results[0].unit).toBe('g');
|
||||
expect(results[0].type).toBe('weight');
|
||||
});
|
||||
|
||||
it('finds weight with space before unit', () => {
|
||||
// From Alfredo Sauce: "parmesan 550 g"
|
||||
const results = findWeights('parmesan 550 g');
|
||||
expect(results).toHaveLength(1);
|
||||
expect(results[0].amount).toEqual({ value: 550, approximate: false });
|
||||
expect(results[0].unit).toBe('g');
|
||||
});
|
||||
|
||||
it('finds kilogram weight', () => {
|
||||
// From Bread: "makes: 1 1kg loaf"
|
||||
const results = findWeights('1kg loaf');
|
||||
expect(results).toHaveLength(1);
|
||||
expect(results[0].amount).toEqual({ value: 1, approximate: false });
|
||||
expect(results[0].unit).toBe('kg');
|
||||
});
|
||||
|
||||
it('finds approximate weight', () => {
|
||||
// From Kombucha Scoby: "kombucha scoby ~250g"
|
||||
const results = findWeights('kombucha scoby ~250g');
|
||||
expect(results).toHaveLength(1);
|
||||
expect(results[0].amount).toEqual({ value: 250, approximate: true });
|
||||
expect(results[0].unit).toBe('g');
|
||||
expect(results[0].approximate).toBe(true);
|
||||
});
|
||||
|
||||
it('finds weight range', () => {
|
||||
// From Egg Noodles: "180-240g"
|
||||
const results = findWeights('all purpose flour 180-240g');
|
||||
expect(results).toHaveLength(1);
|
||||
expect(results[0].amount.min).toEqual({ value: 180, approximate: false });
|
||||
expect(results[0].amount.max).toEqual({ value: 240, approximate: false });
|
||||
expect(results[0].unit).toBe('g');
|
||||
});
|
||||
|
||||
it('finds weight with alternative in parentheses', () => {
|
||||
// From Brownies: "butter 227g (8 oz)"
|
||||
const results = findWeights('butter 227g (8 oz)');
|
||||
expect(results).toHaveLength(1);
|
||||
expect(results[0].amount).toEqual({ value: 227, approximate: false });
|
||||
expect(results[0].unit).toBe('g');
|
||||
expect(results[0].alt).not.toBeNull();
|
||||
expect(results[0].alt.amount).toEqual({ value: 8, approximate: false });
|
||||
expect(results[0].alt.unit).toBe('oz');
|
||||
});
|
||||
|
||||
it('finds oz weight', () => {
|
||||
// From Kombucha Scoby: "64 oz mason jar"
|
||||
const results = findWeights('64 oz mason jar');
|
||||
expect(results).toHaveLength(1);
|
||||
expect(results[0].amount).toEqual({ value: 64, approximate: false });
|
||||
expect(results[0].unit).toBe('oz');
|
||||
});
|
||||
});
|
||||
|
||||
// ─── findVolumes ──────────────────────────────────────────
|
||||
|
||||
describe('findVolumes', () => {
|
||||
it('finds quart measurement', () => {
|
||||
// From Alfredo Sauce: "makes: 2 quarts"
|
||||
const results = findVolumes('2 quarts');
|
||||
expect(results).toHaveLength(1);
|
||||
expect(results[0].amount).toEqual({ value: 2, approximate: false });
|
||||
expect(results[0].unit).toBe('quart');
|
||||
expect(results[0].type).toBe('volume');
|
||||
});
|
||||
|
||||
it('finds cup measurement', () => {
|
||||
// From Banana Bread: "granulated sugar 1 cup"
|
||||
const results = findVolumes('granulated sugar 1 cup');
|
||||
expect(results).toHaveLength(1);
|
||||
expect(results[0].amount).toEqual({ value: 1, approximate: false });
|
||||
expect(results[0].unit).toBe('cup');
|
||||
});
|
||||
|
||||
it('finds fractional cup', () => {
|
||||
// From Banana Bread: "shortening 1/2 cups"
|
||||
const results = findVolumes('shortening 1/2 cups');
|
||||
expect(results).toHaveLength(1);
|
||||
expect(results[0].amount).toEqual({ value: 0.5, approximate: false });
|
||||
expect(results[0].unit).toBe('cup');
|
||||
});
|
||||
|
||||
it('finds tablespoon measurement', () => {
|
||||
// From Tikka Masala Paneer: "heavy cream 6 tablespoons"
|
||||
const results = findVolumes('heavy cream 6 tablespoons');
|
||||
expect(results).toHaveLength(1);
|
||||
expect(results[0].amount).toEqual({ value: 6, approximate: false });
|
||||
expect(results[0].unit).toBe('tablespoon');
|
||||
});
|
||||
|
||||
it('finds "table spoons" with space', () => {
|
||||
// From Banana Bread: "just egg 6 table spoons"
|
||||
const results = findVolumes('just egg 6 table spoons');
|
||||
expect(results).toHaveLength(1);
|
||||
expect(results[0].amount).toEqual({ value: 6, approximate: false });
|
||||
expect(results[0].unit).toBe('tablespoon');
|
||||
});
|
||||
|
||||
it('finds mixed number tablespoon', () => {
|
||||
// From Banana Bread: "unsweetened soy milk 1 1/2 table spoon"
|
||||
const results = findVolumes('unsweetened soy milk 1 1/2 table spoon');
|
||||
expect(results).toHaveLength(1);
|
||||
expect(results[0].amount).toEqual({ value: 1.5, approximate: false });
|
||||
expect(results[0].unit).toBe('tablespoon');
|
||||
});
|
||||
|
||||
it('finds teaspoon measurement', () => {
|
||||
// From Banana Bread: "baking soda 1 teaspoon"
|
||||
const results = findVolumes('baking soda 1 teaspoon');
|
||||
expect(results).toHaveLength(1);
|
||||
expect(results[0].amount).toEqual({ value: 1, approximate: false });
|
||||
expect(results[0].unit).toBe('teaspoon');
|
||||
});
|
||||
|
||||
it('finds fractional teaspoon', () => {
|
||||
// From Banana Bread: "salt 1/4 teaspoon"
|
||||
const results = findVolumes('salt 1/4 teaspoon');
|
||||
expect(results).toHaveLength(1);
|
||||
expect(results[0].amount).toEqual({ value: 0.25, approximate: false });
|
||||
expect(results[0].unit).toBe('teaspoon');
|
||||
});
|
||||
|
||||
it('finds parts by volume', () => {
|
||||
// From Kombucha Template: "6 parts by volume"
|
||||
const results = findVolumes('Kombucha Scoby 6 parts by volume');
|
||||
expect(results).toHaveLength(1);
|
||||
expect(results[0].amount).toEqual({ value: 6, approximate: false });
|
||||
expect(results[0].unit).toBe('parts by volume');
|
||||
});
|
||||
|
||||
it('finds parts by weight', () => {
|
||||
// From Kombucha - Raspberry Rhubarb: "raspberry 2 parts by weight"
|
||||
const results = findVolumes('raspberry 2 parts by weight');
|
||||
expect(results).toHaveLength(1);
|
||||
expect(results[0].amount).toEqual({ value: 2, approximate: false });
|
||||
expect(results[0].unit).toBe('parts by weight');
|
||||
});
|
||||
});
|
||||
|
||||
// ─── findTimes ────────────────────────────────────────────
|
||||
|
||||
describe('findTimes', () => {
|
||||
it('finds basic minute measurement', () => {
|
||||
// From Enchiladas: "20 minutes"
|
||||
const results = findTimes('for 20 minutes');
|
||||
expect(results).toHaveLength(1);
|
||||
expect(results[0].amount).toEqual({ value: 20, approximate: false });
|
||||
expect(results[0].unit).toBe('minute');
|
||||
expect(results[0].type).toBe('time');
|
||||
});
|
||||
|
||||
it('finds time range with "to"', () => {
|
||||
// From Brownies: "28 to 32 minutes"
|
||||
const results = findTimes('28 to 32 minutes');
|
||||
expect(results).toHaveLength(1);
|
||||
expect(results[0].amount.min).toEqual({ value: 28, approximate: false });
|
||||
expect(results[0].amount.max).toEqual({ value: 32, approximate: false });
|
||||
expect(results[0].unit).toBe('minute');
|
||||
});
|
||||
|
||||
it('finds time range with "to" (smaller range)', () => {
|
||||
// From Tikka Masala Paneer: "8 to 10 minutes"
|
||||
const results = findTimes('after 8 to 10 minutes flip');
|
||||
expect(results).toHaveLength(1);
|
||||
expect(results[0].amount.min).toEqual({ value: 8, approximate: false });
|
||||
expect(results[0].amount.max).toEqual({ value: 10, approximate: false });
|
||||
expect(results[0].unit).toBe('minute');
|
||||
});
|
||||
|
||||
it('finds "minutes" total time', () => {
|
||||
// From Tikka Masala Paneer: "15 minutes"
|
||||
const results = findTimes('around 15 minutes total');
|
||||
expect(results).toHaveLength(1);
|
||||
expect(results[0].amount).toEqual({ value: 15, approximate: false });
|
||||
expect(results[0].unit).toBe('minute');
|
||||
});
|
||||
|
||||
it('finds day measurement', () => {
|
||||
// From Kombucha: "five days" wouldn't match (spelled out), but "5 days" does
|
||||
const results = findTimes('ferment for 5 days');
|
||||
expect(results).toHaveLength(1);
|
||||
expect(results[0].amount).toEqual({ value: 5, approximate: false });
|
||||
expect(results[0].unit).toBe('day');
|
||||
});
|
||||
|
||||
it('finds hour measurement', () => {
|
||||
const results = findTimes('steam for 1 hour');
|
||||
expect(results).toHaveLength(1);
|
||||
expect(results[0].amount).toEqual({ value: 1, approximate: false });
|
||||
expect(results[0].unit).toBe('hour');
|
||||
});
|
||||
});
|
||||
|
||||
// ─── findAllMeasurements ──────────────────────────────────
|
||||
|
||||
describe('findAllMeasurements', () => {
|
||||
it('finds multiple measurement types in one string', () => {
|
||||
// From Brownies: "350°F 28 to 32 minutes / 325°F 38 to 42 minutes"
|
||||
const results = findAllMeasurements('350°F 28 to 32 minutes / 325°F 38 to 42 minutes');
|
||||
expect(results.length).toBeGreaterThanOrEqual(4);
|
||||
|
||||
const temps = results.filter(r => r.type === 'temperature');
|
||||
const times = results.filter(r => r.type === 'time');
|
||||
expect(temps).toHaveLength(2);
|
||||
expect(times).toHaveLength(2);
|
||||
});
|
||||
|
||||
it('finds measurements in ingredient line with weight', () => {
|
||||
// From Alfredo Sauce: "olive oil 200g"
|
||||
const results = findAllMeasurements('olive oil 200g');
|
||||
expect(results).toHaveLength(1);
|
||||
expect(results[0].type).toBe('weight');
|
||||
expect(results[0].amount).toEqual({ value: 200, approximate: false });
|
||||
expect(results[0].unit).toBe('g');
|
||||
});
|
||||
|
||||
it('finds measurements in ingredient line with volume', () => {
|
||||
// From Banana Bread: "flour 2 cups"
|
||||
const results = findAllMeasurements('flour 2 cups');
|
||||
expect(results).toHaveLength(1);
|
||||
expect(results[0].type).toBe('volume');
|
||||
expect(results[0].unit).toBe('cup');
|
||||
});
|
||||
|
||||
it('finds weight with dual units', () => {
|
||||
// From Brownies: "butter 227g (8 oz)"
|
||||
const results = findAllMeasurements('butter 227g (8 oz)');
|
||||
expect(results).toHaveLength(1);
|
||||
expect(results[0].type).toBe('weight');
|
||||
expect(results[0].unit).toBe('g');
|
||||
expect(results[0].alt).not.toBeNull();
|
||||
expect(results[0].alt.unit).toBe('oz');
|
||||
});
|
||||
|
||||
it('finds temperature with dual units', () => {
|
||||
// From Lasagna: "200°C (400°F)"
|
||||
const results = findAllMeasurements('bake in oven at 200°C (400°F) for');
|
||||
const temps = results.filter(r => r.type === 'temperature');
|
||||
expect(temps).toHaveLength(1);
|
||||
expect(temps[0].unit).toBe('°C');
|
||||
expect(temps[0].alt.unit).toBe('°F');
|
||||
});
|
||||
|
||||
it('finds dimension in tools line', () => {
|
||||
// From Brownies: "9 x 13 inch glass baking dish"
|
||||
const results = findAllMeasurements('9 x 13 inch glass baking dish');
|
||||
expect(results).toHaveLength(1);
|
||||
expect(results[0].type).toBe('dimension');
|
||||
expect(results[0].amount).toEqual([9, 13]);
|
||||
expect(results[0].unit).toBe('inch');
|
||||
});
|
||||
|
||||
it('finds weight with parenthesized volume equivalent', () => {
|
||||
// From Egg Noodles: "salt 5 g (3/4 teaspoon)"
|
||||
const results = findAllMeasurements('salt 5 g (3/4 teaspoon)');
|
||||
expect(results.length).toBeGreaterThanOrEqual(1);
|
||||
const weight = results.find(r => r.type === 'weight');
|
||||
expect(weight).toBeDefined();
|
||||
expect(weight.amount).toEqual({ value: 5, approximate: false });
|
||||
expect(weight.unit).toBe('g');
|
||||
});
|
||||
|
||||
it('finds weight range with parenthesized volume equivalent', () => {
|
||||
// From Egg Noodles: "180-240g (1.5-2 cups)"
|
||||
const results = findAllMeasurements('all purpose flour 180-240g (1.5-2 cups)');
|
||||
expect(results.length).toBeGreaterThanOrEqual(1);
|
||||
const weight = results.find(r => r.type === 'weight');
|
||||
expect(weight).toBeDefined();
|
||||
expect(weight.amount.min).toEqual({ value: 180, approximate: false });
|
||||
expect(weight.amount.max).toEqual({ value: 240, approximate: false });
|
||||
});
|
||||
|
||||
it('handles complex real-world recipe line', () => {
|
||||
// Full Tikka Masala Paneer step
|
||||
const text = 'preheat oven to 430 °F';
|
||||
const results = findAllMeasurements(text);
|
||||
const temps = results.filter(r => r.type === 'temperature');
|
||||
expect(temps).toHaveLength(1);
|
||||
expect(temps[0].amount).toEqual({ value: 430, approximate: false });
|
||||
});
|
||||
|
||||
it('handles approximate parenthesized weight', () => {
|
||||
// From Quesadilla: "refried beans (~150g)"
|
||||
const results = findAllMeasurements('refried beans (~150g)');
|
||||
expect(results).toHaveLength(1);
|
||||
expect(results[0].type).toBe('weight');
|
||||
expect(results[0].amount).toEqual({ value: 150, approximate: true });
|
||||
});
|
||||
|
||||
it('deduplicates overlapping matches (longer wins)', () => {
|
||||
// "200°C (400°F)" should be ONE temperature match, not separate ones
|
||||
const results = findAllMeasurements('200°C (400°F)');
|
||||
expect(results).toHaveLength(1);
|
||||
expect(results[0].type).toBe('temperature');
|
||||
expect(results[0].alt).not.toBeNull();
|
||||
});
|
||||
|
||||
it('returns matches sorted by position', () => {
|
||||
const text = 'preheat to 350°F, use 200g flour, bake 30 minutes';
|
||||
const results = findAllMeasurements(text);
|
||||
for (let i = 1; i < results.length; i++) {
|
||||
expect(results[i].index).toBeGreaterThan(results[i - 1].index);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// ─── Real Recipe Integration Tests ────────────────────────
|
||||
|
||||
describe('real recipe samples', () => {
|
||||
it('parses Brownies All American step line', () => {
|
||||
const text = '350°F 28 to 32 minutes / 325°F 38 to 42 minutes';
|
||||
const results = findAllMeasurements(text);
|
||||
const temps = results.filter(r => r.type === 'temperature');
|
||||
const times = results.filter(r => r.type === 'time');
|
||||
|
||||
expect(temps[0].amount).toEqual({ value: 350, approximate: false });
|
||||
expect(temps[0].unit).toBe('°F');
|
||||
expect(temps[1].amount).toEqual({ value: 325, approximate: false });
|
||||
|
||||
expect(times[0].amount.min.value).toBe(28);
|
||||
expect(times[0].amount.max.value).toBe(32);
|
||||
expect(times[1].amount.min.value).toBe(38);
|
||||
expect(times[1].amount.max.value).toBe(42);
|
||||
});
|
||||
|
||||
it('parses Enchiladas temperature with dual units', () => {
|
||||
const text = 'oven preheated to 425 °F (~220 °C) for 20 minutes';
|
||||
const results = findAllMeasurements(text);
|
||||
const temp = results.find(r => r.type === 'temperature');
|
||||
const time = results.find(r => r.type === 'time');
|
||||
|
||||
expect(temp.amount).toEqual({ value: 425, approximate: false });
|
||||
expect(temp.unit).toBe('°F');
|
||||
expect(temp.alt.amount).toEqual({ value: 220, approximate: true });
|
||||
expect(temp.alt.unit).toBe('°C');
|
||||
|
||||
expect(time.amount).toEqual({ value: 20, approximate: false });
|
||||
expect(time.unit).toBe('minute');
|
||||
});
|
||||
|
||||
it('parses Mozzarella multi-temperature instructions', () => {
|
||||
const results1 = findAllMeasurements('heat to 32°c (90°f)');
|
||||
expect(results1).toHaveLength(1);
|
||||
expect(results1[0].unit).toBe('°C');
|
||||
expect(results1[0].alt.unit).toBe('°F');
|
||||
|
||||
const results2 = findAllMeasurements('heat to 40°c (105°f)');
|
||||
expect(results2).toHaveLength(1);
|
||||
expect(results2[0].amount).toEqual({ value: 40, approximate: false });
|
||||
|
||||
const results3 = findAllMeasurements('heat to 60°c (140°f)');
|
||||
expect(results3).toHaveLength(1);
|
||||
expect(results3[0].amount).toEqual({ value: 60, approximate: false });
|
||||
});
|
||||
|
||||
it('parses Stuffing temperature and time', () => {
|
||||
const text = 'put in oven for hour at 190°C (375°F) for hour';
|
||||
const results = findAllMeasurements(text);
|
||||
const temp = results.find(r => r.type === 'temperature');
|
||||
|
||||
expect(temp.amount).toEqual({ value: 190, approximate: false });
|
||||
expect(temp.unit).toBe('°C');
|
||||
expect(temp.alt.amount).toEqual({ value: 375, approximate: false });
|
||||
expect(temp.alt.unit).toBe('°F');
|
||||
});
|
||||
|
||||
it('parses Tater Tot Hotdish complex dimension line', () => {
|
||||
// "9 x 13 inch glass baking dish"
|
||||
const results = findAllMeasurements('9 x 13 inch glass baking dish');
|
||||
expect(results).toHaveLength(1);
|
||||
expect(results[0].type).toBe('dimension');
|
||||
expect(results[0].amount).toEqual([9, 13]);
|
||||
expect(results[0].unit).toBe('inch');
|
||||
});
|
||||
|
||||
it('parses 3D dimensions', () => {
|
||||
// "8x6x2 inch pyrex dishes"
|
||||
const results = findAllMeasurements('8x6x2 inch pyrex dishes');
|
||||
expect(results).toHaveLength(1);
|
||||
expect(results[0].amount).toEqual([8, 6, 2]);
|
||||
expect(results[0].unit).toBe('inch');
|
||||
});
|
||||
|
||||
it('parses Egg Noodles weight with equivalent', () => {
|
||||
const text = 'salt 5 g (3/4 teaspoon)';
|
||||
const results = findAllMeasurements(text);
|
||||
const weight = results.find(r => r.type === 'weight');
|
||||
expect(weight).toBeDefined();
|
||||
expect(weight.unit).toBe('g');
|
||||
});
|
||||
|
||||
it('parses Banana Bread volume measurements', () => {
|
||||
const results1 = findAllMeasurements('shortening 1/2 cups');
|
||||
expect(results1).toHaveLength(1);
|
||||
expect(results1[0].amount).toEqual({ value: 0.5, approximate: false });
|
||||
expect(results1[0].unit).toBe('cup');
|
||||
|
||||
const results2 = findAllMeasurements('just egg 6 table spoons');
|
||||
expect(results2).toHaveLength(1);
|
||||
expect(results2[0].unit).toBe('tablespoon');
|
||||
|
||||
const results3 = findAllMeasurements('unsweetened soy milk 1 1/2 table spoon');
|
||||
expect(results3).toHaveLength(1);
|
||||
expect(results3[0].amount).toEqual({ value: 1.5, approximate: false });
|
||||
expect(results3[0].unit).toBe('tablespoon');
|
||||
});
|
||||
|
||||
it('parses Kombucha parts by volume', () => {
|
||||
const results = findAllMeasurements('Kombucha Scoby 6 parts by volume');
|
||||
expect(results).toHaveLength(1);
|
||||
expect(results[0].type).toBe('volume');
|
||||
expect(results[0].unit).toBe('parts by volume');
|
||||
});
|
||||
|
||||
it('parses Potato Fries temperature without degree symbol', () => {
|
||||
// "165°c 325" — the 165°c should match, 325 is ambiguous
|
||||
const results = findAllMeasurements('165°c 325');
|
||||
const temp = results.find(r => r.type === 'temperature');
|
||||
expect(temp).toBeDefined();
|
||||
expect(temp.amount).toEqual({ value: 165, approximate: false });
|
||||
expect(temp.unit).toBe('°C');
|
||||
});
|
||||
|
||||
it('parses Bread makes frontmatter', () => {
|
||||
// "makes: 1 1kg loaf" — should find the 1kg weight
|
||||
const results = findAllMeasurements('1 1kg loaf');
|
||||
const weight = results.find(r => r.type === 'weight');
|
||||
expect(weight).toBeDefined();
|
||||
expect(weight.unit).toBe('kg');
|
||||
});
|
||||
|
||||
it('parses Soffritto parts notation', () => {
|
||||
// "onion 2 parts (~350g)"
|
||||
const results = findAllMeasurements('onion 2 parts (~350g)');
|
||||
// Should find the ~350g weight
|
||||
const weight = results.find(r => r.type === 'weight');
|
||||
expect(weight).toBeDefined();
|
||||
expect(weight.amount).toEqual({ value: 350, approximate: true });
|
||||
});
|
||||
|
||||
it('parses Apple Streusel mm dimension', () => {
|
||||
// "about 5-8mm thick"
|
||||
const results = findAllMeasurements('about 5-8mm thick');
|
||||
// This would be caught as a weight (g/kg) unless we handle mm specifically
|
||||
// mm is a dimension unit but 5-8mm doesn't have an x separator
|
||||
// This is an edge case — it should ideally be a dimension/length
|
||||
expect(results.length).toBeGreaterThanOrEqual(0);
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue