feat: added support for more units

This commit is contained in:
Leyla Becker 2026-02-22 19:24:02 -06:00
parent df26f0243b
commit 750bfb912f
3 changed files with 102 additions and 6 deletions

View file

@ -29,8 +29,10 @@ const TEMP_UNIT_DEG = '°\\s*[FfCc]';
const TEMP_UNIT_BARE = '[FfCc]'; const TEMP_UNIT_BARE = '[FfCc]';
const WEIGHT_UNIT = '(?:kg|g|oz|lbs?|ounces?|pounds?)'; const WEIGHT_UNIT = '(?:kg|g|oz|lbs?|ounces?|pounds?)';
const VOLUME_UNIT = '(?:cups?|tablespoons?|table\\s+spoons?|tbsp|teaspoons?|tsp|ml|mL|L|liters?|litres?|quarts?|gallons?|pints?|fl\\.?\\s*oz|fluid\\s+ounces?|parts\\s+by\\s+(?:volume|weight))'; const VOLUME_UNIT = '(?:cups?|tablespoons?|table\\s+spoons?|tbsp|teaspoons?|tsp|ml|mL|L|liters?|litres?|quarts?|gallons?|pints?|fl\\.?\\s*oz|fluid\\s+ounces?|parts\\s+by\\s+(?:volume|weight))';
const TIME_UNIT = '(?:minutes?|mins?|hours?|hrs?|days?|weeks?|months?|seconds?|secs?)'; const TIME_UNIT = '(?:minutes?|mins?|hours?|hrs?|days?|weeks?|months?|years?|seconds?|secs?)';
const DIM_UNIT = '(?:inch(?:es)?|in\\.?|cm|mm)'; const DIM_UNIT = '(?:inch(?:es)?|in\\.?|cm|mm)';
const PRESSURE_UNIT = '(?:PSI|psi|kPa|bar)';
const PH_UNIT = '(?:pH)';
// ─── Amount Parsing ─────────────────────────────────────── // ─── Amount Parsing ───────────────────────────────────────
@ -192,6 +194,7 @@ function normalizeUnit(unit) {
if (/^weeks?$/.test(u)) return 'week'; if (/^weeks?$/.test(u)) return 'week';
if (/^months?$/.test(u)) return 'month'; if (/^months?$/.test(u)) return 'month';
if (/^seconds?$/.test(u) || /^secs?$/.test(u)) return 'second'; if (/^seconds?$/.test(u) || /^secs?$/.test(u)) return 'second';
if (/^years?$/.test(u)) return 'year';
// Dimension units // Dimension units
if (/^inch(es)?$/.test(u) || u === 'in') return 'inch'; if (/^inch(es)?$/.test(u) || u === 'in') return 'inch';
@ -199,6 +202,14 @@ function normalizeUnit(unit) {
if (u === 'mm') return 'mm'; if (u === 'mm') return 'mm';
if (/^f(oo|ee)t$/.test(u) || u === 'ft') return 'ft'; if (/^f(oo|ee)t$/.test(u) || u === 'ft') return 'ft';
// Pressure
if (u === 'psi') return 'psi';
if (u === 'kpa') return 'kPa';
if (u === 'bar') return 'bar';
// pH (normalizeUnit lowercases input, so 'pH' becomes 'ph')
if (u === 'ph') return 'pH';
return u; return u;
} }
@ -208,8 +219,10 @@ function unitType(normalizedUnit) {
if (['°F', '°C'].includes(normalizedUnit)) return 'temperature'; if (['°F', '°C'].includes(normalizedUnit)) return 'temperature';
if (['g', 'kg', 'oz', 'lb'].includes(normalizedUnit)) return 'weight'; if (['g', 'kg', 'oz', 'lb'].includes(normalizedUnit)) return 'weight';
if (['cup', 'tablespoon', 'teaspoon', 'ml', 'L', 'quart', 'gallon', 'pint', 'fl oz', 'parts by volume', 'parts by weight'].includes(normalizedUnit)) return 'volume'; if (['cup', 'tablespoon', 'teaspoon', 'ml', 'L', 'quart', 'gallon', 'pint', 'fl oz', 'parts by volume', 'parts by weight'].includes(normalizedUnit)) return 'volume';
if (['minute', 'hour', 'day', 'week', 'month', 'second'].includes(normalizedUnit)) return 'time'; if (['minute', 'hour', 'day', 'week', 'month', 'year', 'second'].includes(normalizedUnit)) return 'time';
if (['inch', 'cm', 'mm', 'ft'].includes(normalizedUnit)) return 'dimension'; if (['inch', 'cm', 'mm', 'ft'].includes(normalizedUnit)) return 'dimension';
if (['psi', 'kPa', 'bar'].includes(normalizedUnit)) return 'pressure';
if (normalizedUnit === 'pH') return 'pH';
return null; return null;
} }
@ -535,6 +548,72 @@ function findCounts(text) {
return results; return results;
} }
// ─── Pressure Matcher ─────────────────────────────────────
/**
* Find pressure measurements in text.
*
* Handles: 11 PSI, 15 psi, 100 kPa
*/
function findPressures(text) {
const results = [];
const pressureRe = new RegExp(
`(${AMOUNT})\\s*(${PRESSURE_UNIT})\\b`,
'gi'
);
let m;
while ((m = pressureRe.exec(text)) !== null) {
const amount = parseAmount(m[1]);
const unit = normalizeUnit(m[2]);
results.push({
match: m[0],
index: m.index,
type: 'pressure',
amount,
unit,
approximate: typeof amount.approximate === 'boolean' ? amount.approximate : false,
alt: null,
});
}
return results;
}
// ─── pH Matcher ───────────────────────────────────────────
/**
* Find pH measurements in text.
*
* Handles: 4.0 pH, 4.0 ph, 3.5 pH
*/
function findPH(text) {
const results = [];
const phRe = new RegExp(
`(${AMOUNT})\\s*(${PH_UNIT})\\b`,
'g'
);
let m;
while ((m = phRe.exec(text)) !== null) {
const amount = parseAmount(m[1]);
const unit = normalizeUnit(m[2]);
results.push({
match: m[0],
index: m.index,
type: 'pH',
amount,
unit,
approximate: typeof amount.approximate === 'boolean' ? amount.approximate : false,
alt: null,
});
}
return results;
}
// ─── Main Matcher ───────────────────────────────────────── // ─── Main Matcher ─────────────────────────────────────────
/** /**
@ -552,6 +631,8 @@ function findAllMeasurements(text) {
...findWeights(text), ...findWeights(text),
...findVolumes(text), ...findVolumes(text),
...findTimes(text), ...findTimes(text),
...findPressures(text),
...findPH(text),
...findCounts(text), ...findCounts(text),
]; ];
@ -595,6 +676,8 @@ module.exports = {
findWeights, findWeights,
findVolumes, findVolumes,
findTimes, findTimes,
findPressures,
findPH,
findCounts, findCounts,
// Parsing utilities (exported for testing) // Parsing utilities (exported for testing)

View file

@ -43,7 +43,10 @@ function unitLabel(unit, plural) {
'day': plural ? 'days' : 'day', 'day': plural ? 'days' : 'day',
'week': plural ? 'weeks' : 'week', 'week': plural ? 'weeks' : 'week',
'month': plural ? 'months' : 'month', 'month': plural ? 'months' : 'month',
'year': plural ? 'years' : 'year',
'second': plural ? 'seconds' : 'sec', 'second': plural ? 'seconds' : 'sec',
'psi': 'PSI', 'kPa': 'kPa', 'bar': 'bar',
'pH': 'pH',
'parts by volume': 'parts by volume', 'parts by volume': 'parts by volume',
'parts by weight': 'parts by weight', 'parts by weight': 'parts by weight',
}; };
@ -108,6 +111,10 @@ function toMetricValue(value, unit) {
case 'inch': return { value: value * 2.54, unit: 'cm' }; case 'inch': return { value: value * 2.54, unit: 'cm' };
case 'cm': return { value, unit: 'cm' }; case 'cm': return { value, unit: 'cm' };
case 'mm': return { value, unit: 'mm' }; case 'mm': return { value, unit: 'mm' };
// Pressure
case 'psi': return { value: value * 6.89476, unit: 'kPa' };
case 'kPa': return { value, unit: 'kPa' };
case 'bar': return { value: value * 100, unit: 'kPa' };
default: return { value, unit }; default: return { value, unit };
} }
} }
@ -137,6 +144,10 @@ function toImperialValue(value, unit) {
case 'cm': return { value: value / 2.54, unit: 'inch' }; case 'cm': return { value: value / 2.54, unit: 'inch' };
case 'mm': return { value: value / 25.4, unit: 'inch' }; case 'mm': return { value: value / 25.4, unit: 'inch' };
case 'inch': return { value, unit: 'inch' }; case 'inch': return { value, unit: 'inch' };
// Pressure
case 'kPa': return { value: value / 6.89476, unit: 'psi' };
case 'bar': return { value: value * 14.5038, unit: 'psi' };
case 'psi': return { value, unit: 'psi' };
default: return { value, unit }; default: return { value, unit };
} }
} }
@ -317,6 +328,7 @@ function collapsedTime(value, unit) {
if (unit === 'day') return formatValueUnit(value, 'day'); if (unit === 'day') return formatValueUnit(value, 'day');
if (unit === 'week') return formatValueUnit(value, 'week'); if (unit === 'week') return formatValueUnit(value, 'week');
if (unit === 'month') return formatValueUnit(value, 'month'); if (unit === 'month') return formatValueUnit(value, 'month');
if (unit === 'year') return formatValueUnit(value, 'year');
if (unit === 'second') return formatValueUnit(value, 'second'); if (unit === 'second') return formatValueUnit(value, 'second');
const mins = toMinutes(value, unit); const mins = toMinutes(value, unit);
if (mins === null) return formatValueUnit(value, unit); if (mins === null) return formatValueUnit(value, unit);
@ -327,6 +339,7 @@ function expandedTime(value, unit) {
if (unit === 'day') return formatValueUnit(value, 'day'); if (unit === 'day') return formatValueUnit(value, 'day');
if (unit === 'week') return formatValueUnit(value, 'week'); if (unit === 'week') return formatValueUnit(value, 'week');
if (unit === 'month') return formatValueUnit(value, 'month'); if (unit === 'month') return formatValueUnit(value, 'month');
if (unit === 'year') return formatValueUnit(value, 'year');
if (unit === 'second') return formatValueUnit(value, 'second'); if (unit === 'second') return formatValueUnit(value, 'second');
const mins = toMinutes(value, unit); const mins = toMinutes(value, unit);
if (mins === null) return formatValueUnit(value, unit); if (mins === null) return formatValueUnit(value, unit);
@ -350,7 +363,7 @@ function generateTexts(measurement) {
const { type, amount, unit, approximate } = measurement; const { type, amount, unit, approximate } = measurement;
// Non-convertible units // Non-convertible units
if (unit === 'parts by volume' || unit === 'parts by weight') { if (unit === 'parts by volume' || unit === 'parts by weight' || unit === 'pH') {
const text = formatMeasurementText(amount, unit, approximate); const text = formatMeasurementText(amount, unit, approximate);
return { defaultText: text, altText: text }; return { defaultText: text, altText: text };
} }

View file

@ -25,6 +25,6 @@ draft: true
#recipe #diet/plant_based #diet/vegetarian #recipe #diet/plant_based #diet/vegetarian
[^1]: can be substituted with store bought kombucha for first scoby [^1]: can be substituted with store bought kombucha for first scoby
[^2]: tap water works but is slower and has a higher chance of failure due to chlorine/fluoride in water [^2]: tap water works but is slower and has a higher chance of failure due to chlorine/fluoride in water
[^3]: sweet to sour balance matches [Kombucha Calibrator](/recipe/kombucha-calibrator/) or ph is below then 4.0 [^3]: sweet to sour balance matches [Kombucha Calibrator](/recipe/kombucha-calibrator/) is under a 4.0 pH