feat: added density conversion
This commit is contained in:
parent
bac7a14f95
commit
df26f0243b
5 changed files with 415 additions and 12 deletions
|
|
@ -272,9 +272,12 @@ describe('hybrid weight/volume measurements', () => {
|
|||
expect(html).toContain('data-volume-scalable=');
|
||||
});
|
||||
|
||||
it('does NOT add hybrid attributes for weight-to-weight alt', () => {
|
||||
const md = '- butter 227g (8 oz)';
|
||||
it('does NOT add manual hybrid attributes for weight-to-weight alt', () => {
|
||||
// weight-to-weight alt doesn't produce manual hybrid volume data,
|
||||
// but density-based auto hybrid will still kick in for known ingredients
|
||||
const md = '- cheddar 227g (8 oz)';
|
||||
const html = render(md);
|
||||
// "cheddar" is not in the density table, so no hybrid at all
|
||||
expect(html).not.toContain('data-hybrid');
|
||||
});
|
||||
|
||||
|
|
@ -323,4 +326,196 @@ describe('hybrid weight/volume measurements', () => {
|
|||
expect(html).toContain('data-default="80g"');
|
||||
expect(html).toContain('data-measurement-type="weight"');
|
||||
});
|
||||
});
|
||||
|
||||
// ─── Density Table ──────────────────────────────────────────
|
||||
|
||||
const { matchDensity } = require('../densities');
|
||||
|
||||
describe('matchDensity', () => {
|
||||
it('returns density for known ingredient', () => {
|
||||
expect(matchDensity('butter')).toBe(0.96);
|
||||
});
|
||||
|
||||
it('is case-insensitive', () => {
|
||||
expect(matchDensity('Butter')).toBe(0.96);
|
||||
expect(matchDensity('FLOUR')).toBe(0.53);
|
||||
});
|
||||
|
||||
it('returns null for unknown ingredient', () => {
|
||||
expect(matchDensity('dragon fruit')).toBeNull();
|
||||
});
|
||||
|
||||
it('matches longest keyword first (brown sugar vs sugar)', () => {
|
||||
expect(matchDensity('brown sugar')).toBe(0.93);
|
||||
expect(matchDensity('sugar')).toBe(0.85);
|
||||
});
|
||||
|
||||
it('matches longest keyword first (olive oil vs oil)', () => {
|
||||
expect(matchDensity('olive oil')).toBe(0.92);
|
||||
expect(matchDensity('vegetable oil')).toBe(0.92);
|
||||
});
|
||||
|
||||
it('matches ingredient within longer text', () => {
|
||||
expect(matchDensity('unsalted butter')).toBe(0.96);
|
||||
expect(matchDensity('all-purpose flour')).toBe(0.53);
|
||||
});
|
||||
|
||||
it('returns null for empty/null input', () => {
|
||||
expect(matchDensity('')).toBeNull();
|
||||
expect(matchDensity(null)).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
// ─── Density-Based Auto Hybrid Generation ───────────────────
|
||||
|
||||
function getWeightAttrs(html) {
|
||||
const matches = [];
|
||||
const re = /data-hybrid="true"[^>]*data-weight-default="([^"]*)"[^>]*data-weight-alt="([^"]*)"(?:[^>]*data-weight-scalable="([^"]*)")?/g;
|
||||
let m;
|
||||
while ((m = re.exec(html)) !== null) {
|
||||
matches.push({
|
||||
weightDefault: decodeHtmlEntities(m[1]),
|
||||
weightAlt: decodeHtmlEntities(m[2]),
|
||||
weightScalable: m[3] ? JSON.parse(decodeHtmlEntities(m[3])) : null,
|
||||
});
|
||||
}
|
||||
return matches;
|
||||
}
|
||||
|
||||
describe('density-based auto hybrid: weight -> volume', () => {
|
||||
it('auto-generates volume data for weight measurement with known ingredient', () => {
|
||||
const md = '- butter 227g';
|
||||
const html = render(md);
|
||||
expect(html).toContain('data-hybrid="true"');
|
||||
expect(html).toContain('data-volume-default=');
|
||||
expect(html).toContain('data-volume-alt=');
|
||||
expect(html).toContain('data-volume-scalable=');
|
||||
});
|
||||
|
||||
it('generates correct volume from density (butter 227g)', () => {
|
||||
const md = '- butter 227g';
|
||||
const html = render(md);
|
||||
const hybrids = getHybridAttrs(html);
|
||||
expect(hybrids.length).toBe(1);
|
||||
// 227g / 0.96 density = 236.46ml -> "236.5ml" metric
|
||||
expect(hybrids[0].volumeDefault).toBe('236.5ml');
|
||||
expect(hybrids[0].volumeScalable).toBeDefined();
|
||||
expect(hybrids[0].volumeScalable.type).toBe('volume');
|
||||
expect(hybrids[0].volumeScalable.base).toBeCloseTo(236.46, 0);
|
||||
});
|
||||
|
||||
it('generates correct volume from density (flour 500g)', () => {
|
||||
const md = '- all-purpose flour 500g';
|
||||
const html = render(md);
|
||||
const hybrids = getHybridAttrs(html);
|
||||
expect(hybrids.length).toBe(1);
|
||||
// 500g / 0.53 density = 943.4ml -> "943.4ml"
|
||||
expect(hybrids[0].volumeDefault).toBe('943.4ml');
|
||||
});
|
||||
|
||||
it('does not generate hybrid for unknown ingredient weight', () => {
|
||||
const md = '- mystery powder 100g';
|
||||
const html = render(md);
|
||||
expect(html).not.toContain('data-hybrid');
|
||||
});
|
||||
|
||||
it('handles range weight measurements with density', () => {
|
||||
const md = '- butter 200-250g';
|
||||
const html = render(md);
|
||||
const hybrids = getHybridAttrs(html);
|
||||
expect(hybrids.length).toBe(1);
|
||||
// 200/0.96 = 208.3ml, 250/0.96 = 260.4ml
|
||||
expect(hybrids[0].volumeDefault).toMatch(/208.*-.*260/);
|
||||
expect(hybrids[0].volumeScalable.base).toHaveLength(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe('density-based auto hybrid: volume -> weight', () => {
|
||||
it('auto-generates weight data for volume measurement with known ingredient', () => {
|
||||
const md = '- flour 2 cups';
|
||||
const html = render(md);
|
||||
expect(html).toContain('data-hybrid="true"');
|
||||
expect(html).toContain('data-weight-default=');
|
||||
expect(html).toContain('data-weight-alt=');
|
||||
expect(html).toContain('data-weight-scalable=');
|
||||
});
|
||||
|
||||
it('generates correct weight from density (flour 2 cups)', () => {
|
||||
const md = '- flour 2 cups';
|
||||
const html = render(md);
|
||||
const weightAttrs = getWeightAttrs(html);
|
||||
expect(weightAttrs.length).toBe(1);
|
||||
// 2 cups = 2 * 236.588ml = 473.176ml, * 0.53 density = 250.8g
|
||||
expect(weightAttrs[0].weightDefault).toBe('250.8g');
|
||||
expect(weightAttrs[0].weightScalable).toBeDefined();
|
||||
expect(weightAttrs[0].weightScalable.type).toBe('weight');
|
||||
expect(weightAttrs[0].weightScalable.base).toBeCloseTo(250.78, 0);
|
||||
});
|
||||
|
||||
it('generates correct weight from density (milk 1 cup)', () => {
|
||||
const md = '- milk 1 cup';
|
||||
const html = render(md);
|
||||
const weightAttrs = getWeightAttrs(html);
|
||||
expect(weightAttrs.length).toBe(1);
|
||||
// 1 cup = 236.588ml, * 1.03 density = 243.69g
|
||||
expect(weightAttrs[0].weightDefault).toBe('243.7g');
|
||||
});
|
||||
|
||||
it('does not generate hybrid for unknown ingredient volume', () => {
|
||||
const md = '- unicorn tears 1 cup';
|
||||
const html = render(md);
|
||||
expect(html).not.toContain('data-hybrid');
|
||||
});
|
||||
|
||||
it('handles range volume measurements with density', () => {
|
||||
const md = '- sugar 1-2 cups';
|
||||
const html = render(md);
|
||||
const weightAttrs = getWeightAttrs(html);
|
||||
expect(weightAttrs.length).toBe(1);
|
||||
// 1 cup = 236.588ml * 0.85 = 201.1g, 2 cups = 473.176ml * 0.85 = 402.2g
|
||||
expect(weightAttrs[0].weightDefault).toMatch(/201.*-.*402/);
|
||||
expect(weightAttrs[0].weightScalable.base).toHaveLength(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe('density vs manual hybrid precedence', () => {
|
||||
it('manual hybrid annotation takes precedence over density table', () => {
|
||||
const md = '- unsalted butter 80g (6 tablespoons)';
|
||||
const html = render(md);
|
||||
const hybrids = getHybridAttrs(html);
|
||||
expect(hybrids.length).toBe(1);
|
||||
// Should use the manual 6 tablespoons, not density-computed value
|
||||
expect(hybrids[0].volumeDefault).toBe('88.7ml');
|
||||
expect(hybrids[0].volumeAlt).toBe('6 tbsp');
|
||||
});
|
||||
|
||||
it('weight-to-weight alt does not trigger density hybrid (butter 227g (8 oz))', () => {
|
||||
// "227g (8 oz)" is weight-to-weight alt, no manual hybrid volume data,
|
||||
// but density should still auto-generate volume data
|
||||
const md = '- butter 227g (8 oz)';
|
||||
const html = render(md);
|
||||
// getHybridVolumeData returns null for weight-to-weight, so density kicks in
|
||||
expect(html).toContain('data-hybrid="true"');
|
||||
const hybrids = getHybridAttrs(html);
|
||||
expect(hybrids.length).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('density with approximate values', () => {
|
||||
it('preserves approximate prefix in density-generated volume', () => {
|
||||
const md = '- butter ~200g';
|
||||
const html = render(md);
|
||||
const hybrids = getHybridAttrs(html);
|
||||
expect(hybrids.length).toBe(1);
|
||||
expect(hybrids[0].volumeDefault).toMatch(/^~/);
|
||||
});
|
||||
|
||||
it('preserves approximate prefix in density-generated weight', () => {
|
||||
const md = '- flour ~2 cups';
|
||||
const html = render(md);
|
||||
const weightAttrs = getWeightAttrs(html);
|
||||
expect(weightAttrs.length).toBe(1);
|
||||
expect(weightAttrs[0].weightDefault).toMatch(/^~/);
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue