diff --git a/public/components/calculators.html b/public/components/calculators.html
index 95fc3be..ce01aa9 100644
--- a/public/components/calculators.html
+++ b/public/components/calculators.html
@@ -216,14 +216,8 @@
diff --git a/public/js/calculators.js b/public/js/calculators.js
index 19c9430..9910b6b 100644
--- a/public/js/calculators.js
+++ b/public/js/calculators.js
@@ -10,6 +10,52 @@
initCalculators();
});
+ // ── Flexible age parser ─────────────────────────────────────────────
+ // Accepts: "3y", "3 years", "29m", "29 months", "2y5m", "2 years 5 months",
+ // "15d", "15 days", "3y 2m 10d", "36" (months), "3.5 years", etc.
+ // Returns age in months (may be fractional), or null if unparseable.
+ function parseAgeMonths(input) {
+ if (input == null) return null;
+ var s = String(input).toLowerCase().trim();
+ if (!s) return null;
+ // Plain number with no unit: if >= 24 assume years, else months. Matches
+ // common shorthand ("36" for 3y, "6" for 6 months).
+ if (/^[\d.]+$/.test(s)) {
+ var n = parseFloat(s);
+ if (isNaN(n)) return null;
+ return n >= 24 ? n : n; // keep as months; user can add unit if they meant years
+ }
+ var total = 0;
+ var matched = false;
+ // Use (?![a-z]) instead of \b so "2y5m" terminates "y" correctly.
+ // Order matters: longest-first alternations (years before yrs before y, etc.).
+ var my = s.match(/([\d.]+)\s*(?:years|year|yrs|yr|y)(?![a-z])/);
+ if (my) { total += parseFloat(my[1]) * 12; matched = true; }
+ var mm = s.match(/([\d.]+)\s*(?:months|month|mos|mo|m)(?![a-z])/);
+ if (mm) { total += parseFloat(mm[1]); matched = true; }
+ var mw = s.match(/([\d.]+)\s*(?:weeks|week|wks|wk|w)(?![a-z])/);
+ if (mw) { total += parseFloat(mw[1]) * 7 / 30.4375; matched = true; }
+ var md = s.match(/([\d.]+)\s*(?:days|day|d)(?![a-z])/);
+ if (md) { total += parseFloat(md[1]) / 30.4375; matched = true; }
+ return matched ? total : null;
+ }
+
+ function formatParsedAge(months) {
+ if (months < 1) {
+ var d = Math.round(months * 30.4375);
+ return '= ' + d + ' day' + (d === 1 ? '' : 's') + ' (' + months.toFixed(2) + ' mo)';
+ }
+ if (months < 24) {
+ return '= ' + (Math.round(months * 10) / 10) + ' months';
+ }
+ var yr = Math.floor(months / 12);
+ var mo = Math.round(months - yr * 12);
+ if (mo === 12) { yr += 1; mo = 0; }
+ return '= ' + yr + ' yr' + (mo ? ' ' + mo + ' mo' : '') + ' (' + Math.round(months) + ' mo total)';
+ }
+ // Expose for debugging / other modules
+ window._parseAgeMonths = parseAgeMonths;
+
function initCalculators() {
// ── Top-level pill navigation (scoped to .calc-nav-pill only) ──
document.querySelectorAll('.calc-nav-pill').forEach(function(pill) {
@@ -1105,10 +1151,9 @@
return;
}
- var growthYr = parseFloat(document.getElementById('growth-age-yr').value) || 0;
- var growthMo = parseInt(document.getElementById('growth-age-mo').value) || 0;
- var age = growthYr * 12 + growthMo; // age in months
- if (isNaN(age)) { showToast('Enter age in months', 'error'); return; }
+ var rawAge = (document.getElementById('growth-age').value || '').trim();
+ var age = parseAgeMonths(rawAge); // age in months, may be fractional
+ if (age == null || isNaN(age) || age < 0) { showToast('Enter age (e.g. 2y 5m, 29 months, 15 days)', 'error'); return; }
if (currentGrowthType === 'wfa') {
var wt = parseFloat(document.getElementById('growth-weight').value);
@@ -1207,13 +1252,27 @@
});
document.getElementById('btn-clear-growth').addEventListener('click', function() {
- ['growth-sex', 'growth-age-yr', 'growth-ga', 'growth-weight', 'growth-length', 'growth-hc'].forEach(function(id) {
+ ['growth-sex', 'growth-age', 'growth-ga', 'growth-weight', 'growth-length', 'growth-hc'].forEach(function(id) {
var el = document.getElementById(id); if (el) el.value = '';
});
- var gaMo = document.getElementById('growth-age-mo'); if (gaMo) gaMo.value = '0';
+ var hint = document.getElementById('growth-age-parsed'); if (hint) hint.textContent = '';
document.getElementById('growth-result').classList.add('hidden');
});
+ // Live feedback as user types the age
+ var ageEl = document.getElementById('growth-age');
+ var ageHint = document.getElementById('growth-age-parsed');
+ if (ageEl && ageHint) {
+ ageEl.addEventListener('input', function() {
+ var v = ageEl.value.trim();
+ if (!v) { ageHint.textContent = ''; return; }
+ var m = parseAgeMonths(v);
+ if (m == null || isNaN(m) || m < 0) { ageHint.textContent = 'Unrecognized format'; ageHint.style.color = '#ef4444'; return; }
+ ageHint.style.color = 'var(--g500)';
+ ageHint.textContent = formatParsedAge(m);
+ });
+ }
+
// ═══════════════════════════════════════════════════════════
// BILIRUBIN — AAP 2022 Phototherapy + Bhutani Nomogram
// ═══════════════════════════════════════════════════════════